本文讲解 IIC 总线协议,并给出可直接参考的 Verilog 主从机代码样例。
文章时序解析内容基于该视频。
IIC总线介绍

IIC(Inter-Integrated Circuit,常写作 I2C)是一种同步串行总线协议,由 Philips 提出由一个主机和若干从机构成,一个主机和多个从机共享同一对 SDA/SCL 信号线。
IIC总线仅使用两根线工作:SCL(Serial Clock,时钟线)与 SDA(Serial Data,数据线),二者均为开漏结构、靠上拉电阻拉高,主从机控制拉低。本文采用7位地址表示法。同一条总线上,每个从机地址必须唯一,主机通过地址选中特定从机进行通信。
IIC协议

上图为 IIC 标准传输格式,图中区分了"主机发出"与"从机发出"的信号。一次完整传输由起始条件开始、停止条件结束,中间按"地址 → 寄存器地址 → 数据"的顺序交互:
- 读时序:起始位 → 地址+W → ACK(从机) → 寄存器地址 → ACK(从机) → 地址+R → ACK(从机) → 数据 → ACK → … → 停止位
- 写时序:起始位 → 地址+W → ACK(从机) → 寄存器地址 → ACK(从机) → 写入数据 → ACK → … → 停止位
时序解析
IIC 是同步时序:SDA 上的数据在 SCL 高电平期间必须保持稳定,接收方通常在 SCL 上升沿附近采样;而 SDA 的电平翻转只能发生在 SCL 低电平期间。
这一约束是为了使得 SCL 高电平期间的 SDA 跳变被专门保留给"起始/停止条件"。
起始位(START, S)
总线空闲时,外接的上拉电阻将 SDA 与 SCL 都维持在高电平(IDLE 状态)。当主机准备通信时,在 SCL 为高电平期间将 SDA 拉低,即产生起始条件,标志一次传输开始。

地址位 + W/R
起始条件之后的第一个字节用于寻址:主机在 SCL 上升沿逐位送出 8 位数据,其中前 7 位为从机地址,第 8 位为读写标志 R/W(0 = 写,1 = 读)。

对于"读"操作,主机之所以先发"地址+W",是为了先把要读取的寄存器地址写进从机;随后再用 repeated START 切换为"地址+R",从机才据此把对应寄存器的数据发回来。
应答位(ACK)

图中白色信号即应答位,ACK 由当前字节的接收方产生。每传输完 8 位数据后,接收方需在紧跟的第 9 个 SCL 周期给出应答:ACK = SDA 被拉低(低电平),NACK = SDA 释放(高电平)。
NACK 表示发送方未收到应答。在主机读数据时,主机通常会在最后一个字节返回 NACK,通知从机停止继续发送;在其他场景下,NACK 也可能表示地址不存在、设备忙或通信异常。只有收到有效 ACK,通信才会继续。
停止位(STOP, P)

当前一次传输结束后,主机在 SCL 为高电平期间将 SDA 由低释放为高,产生停止条件,总线回到 IDLE 状态。
Verilog代码
本文实现的是标准模式(Standard-mode,≤100 kHz)下的 IIC 主从通信,覆盖 7 位地址、读写双方向、寄存器地址 + 连续多字节数据等最常见的外设访问场景,可作为学习与原型验证的基础 IP。
本文实现代码范围
| 类别 |
支持内容 |
说明 |
| 通信模式 |
标准模式 Standard-mode(默认 100 kHz) |
主从双机,单主机结构 |
| 寻址 |
7 位从机地址 |
主机 addr 输入、从机 SLAVE_ADDR 参数 |
| 传输方向 |
写(主机→从机)/ 读(主机←从机) |
均支持连续多字节 |
| 寄存器访问 |
先写寄存器地址,再读写数据 |
指针自增,支持跨寄存器连续访问 |
| 起止条件 |
START / STOP |
读操作支持 repeated START(不发 STOP 直接重启) |
| 应答机制 |
每字节后 ACK |
从机回 ACK;读末字节主机回 NACK |
| 时钟延长 |
主机侧支持 |
检测到 SCL 被从机拉低即等待(clock stretching) |
| 总线建模 |
SDA/SCL 开漏 |
逻辑 1 = 释放、逻辑 0 = 拉低,板级外接上拉 |
| 错误处理 |
error 标志 |
地址或数据被 NACK 时置位 |
未覆盖的部分(如需可在此基础上扩展):10 位地址、Fast-mode / Fast-mode Plus / High-speed 等高速模式、从机主动时钟延长(本从机不拉低 SCL)、广播呼叫/设备复位等保留地址、多主机仲裁与同步。
主机
主机驱动模块用状态机完成一次完整 IIC 传输:IDLE → START → ADDR_DEVICE(设备地址+W) → ADDR_REG(寄存器地址) → [读时 repeated START + ADDR_RD(设备地址+R)] → DATA(收发数据) → STOP。
SDA/SCL 均为开漏建模:输出逻辑 0 表示主动拉低总线,输出逻辑 1 并不会驱动高电平来实现,而是直接释放引脚,由片外上拉电阻恢复为高电平。因此 SDA_OE 与 SCL_OE 仅控制"是否拉低"总线。
SCL 由检测SCL边沿的计数器驱动——每检测到一次 SCL 实际边沿就重触发半周期计时,数满 T_HALF 翻转 SCL_OE。每 8 位数据后跟 1 拍 ACK:从机拉低为 ACK,释放为高即为 NACK。
当从机把 SCL 拉低时主机检测不到上升沿,计数器不被重触发,于是停在原位等待,直到从机释放产生上升沿才继续,这便实现了时钟延长(clock stretching)。因此主机并不是"按固定时间翻转 SCL",而是"等待 SCL 实际发生边沿后再开始下一段计时"。
len 表示 DATA 阶段收发的字节数;写时主机连续发送、从机每字节回 ACK,末字节后进入 STOP;读时从机发送、主机对末字节回 NACK 后进入 STOP。与外部的握手复用单个 req 位——写时表示"本字节已发完、请准备下一字节",读时表示"本字节已收满、请锁存 rd_data";由于读写不可能同时发生,二者共用一位即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
|
module IIC_Master #(
parameter CLK_FREQ = 100_000_000, // 系统时钟 100MHz
parameter IIC_FREQ = 100_000 // IIC 速率 100kHz
)(
input wire clk, // 100MHz 系统时钟
input wire rst_n, // 异步复位(低有效)
// 用户接口
input wire start, // 启动一次传输
input wire rw, // 1=读, 0=写
input wire [6:0] addr, // 从机地址
input wire [7:0] reg_addr, // 寄存器地址(ADDR_REG 发送)
input wire [7:0] len, // 本次传输字节数 n(len>=1,0 视为 1)
input wire [7:0] wr_data, // 待写数据(写模式:被 req 请求时由外部更新)
output reg [7:0] rd_data, // 读到的数据(读模式:被 req 通知后由外部锁存)
output reg req, // 单 bit 握手:写=本字节已发完、请求下一字节;读=本字节已收满、通知可锁存
output reg busy, // 传输进行中
output reg done, // 传输完成
output reg error, // 错误标志:从机 NACK 地址或写数据时置 1,保持到下次 start 或成功完成才清零
// IIC 双线(片外总线,开漏:逻辑1=释放引脚、由板级外接上拉拉高,逻辑0=本模块拉低)
inout wire sda,
inout wire scl
);
// 时序参数
localparam DIV = CLK_FREQ / (2 * IIC_FREQ); // 半周期 clk 数
localparam T_HALF = DIV; // 每半周期固定计数
localparam T_BUF = CLK_FREQ / 200_000; // STOP 后总线空闲时间
localparam T_STOP = CLK_FREQ / 250_000;
localparam T_HDSTA = CLK_FREQ / 250_000; // (repeated) START 后 SDA 保持低的保持时间 t_HD:STA(标准模式 4.0us)
reg [$clog2(DIV):0] scl_cnt;
reg [$clog2(T_STOP):0] stop_cnt;
//SCL和SDA的OUTPUT ENABLE
//起的是下拉作用,为1就是把SCL或SDA拉到0
reg SCL_OE,SDA_OE;
assign sda = SDA_OE ? 1'd0 : 1'dz;
assign scl = SCL_OE ? 1'd0 : 1'dz;
// 状态机
localparam IDLE = 3'd0,
START = 3'd1,
ADDR_DEVICE = 3'd2,
ADDR_REG = 3'd3,
ADDR_RD = 3'd4,
DATA = 3'd5,
DATA_NEXT = 3'd7,
STOP = 3'd6;
reg [2:0] state;
// 收发/计时寄存器
reg [7:0] shift_reg; // 发送移位 / 接收收集
reg [3:0] bit_cnt; // 0~7 数据位, 8=第9拍(ACK/NACK)
reg [7:0] remain; // 剩余待传字节数(含当前)
reg is_last; // 当前读字节是否末字节(决定回 ACK 还是 NACK)
reg ack_phase; // 读:ACK 拍已在上升沿被从机采样,待低电平释放
reg scl_run; // 1=通信中生成 SCL;0=释放 SCL(高)
reg scl_run_d;
wire scl_run_pos = scl_run && !scl_run_d; // 刚进入通信,产生首拍下降沿
reg [$clog2(T_HDSTA):0] hd_cnt; // t_HD:STA 保持计时
reg first_bit; // 1=当前首字节首位需遵守 t_HD:STA(START 后保持 SDA 低)
reg start_fell; // 1=已检测到 START 的 SCL 下降沿
// 同步 SCL/SDA(异步于 clk),打拍检测边沿与读取实际电平
reg [2:0] scl_sync, sda_sync;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
scl_sync <= 3'b111;
sda_sync <= 3'b111;
end
else begin
scl_sync <= {scl_sync[1:0], scl};
sda_sync <= {sda_sync[1:0], sda};
end
end
wire scl_in = scl_sync[1]; // 同步后的 SCL 电平
wire sda_in = sda_sync[1]; // 同步后的 SDA 电平
wire scl_rise = (scl_sync[2:1] == 2'b01); // SCL 上升沿
wire scl_fall = (scl_sync[2:1] == 2'b10); // SCL 下降沿
// SCL 生成(边沿触发 + 支持从机时钟延长)
// 每检测到一次 SCL 实际边沿,重触发半周期计时;数到 T_HALF-1 翻转 SCL_OE。
// 从机把 SCL 拉低时不会检测到上升沿 → 计数器不被重触发 → 主机停在原位等待,
// 直到从机释放产生上升沿才继续,即时钟延长。
// scl_run_pos 时直接拉低,产生通信的第一个下降沿(START 之后的首个 SCL 跳变)。
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
scl_cnt <= 0;
SCL_OE <= 1'b0;
scl_run_d <= 1'b0;
end
else begin
scl_run_d <= scl_run;
if (!scl_run) begin
scl_cnt <= 0;
SCL_OE <= 1'b0; // 空闲/START/STOP:释放 SCL,保持高
end
else begin
if (scl_run_pos) begin
SCL_OE <= 1'b1; // 刚启动:直接拉低,产生首拍下降沿
scl_cnt <= 0;
end
else if (scl_rise || scl_fall) begin
scl_cnt <= 0; // 边沿重触发半周期计时
end
else if (scl_cnt < T_HALF) begin
scl_cnt <= scl_cnt + 1'b1;
end
if (scl_cnt == (T_HALF - 1))
SCL_OE <= ~SCL_OE; // 半周期到,翻转驱动(释放<->拉低)
end
end
end
// SDA/状态机:基于 SCL 边沿收发
// scl_fall(进入低电平):发送方设置下一位 SDA;第9拍释放 SDA 让从机回 ACK
// scl_rise(进入高电平):采样 SDA(ACK / 数据位)
// 每 8 位数据后跟 1 拍 ACK(从机拉低=ACK,释放=NACK)
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
SDA_OE <= 1'b0;
scl_run <= 1'b0;
busy <= 1'b0;
done <= 1'b0;
req <= 1'b0;
error <= 1'b0;
shift_reg<= 8'h00;
bit_cnt <= 4'd0;
remain <= 8'd0;
is_last <= 1'b0;
ack_phase<= 1'b0;
stop_cnt <= 0;
hd_cnt <= 0;
first_bit<= 1'b0;
start_fell<= 1'b0;
end
else begin
req <= 1'b0; // 默认不请求;字节边界处脉冲置 1
case (state)
// 空闲:SDA/SCL 均释放(高)。start 拉低 SDA 形成 START
IDLE: begin
SDA_OE <= 1'b0;
scl_run <= 1'b0;
if (start) begin
busy <= 1'b1;
done <= 1'b0;
error <= 1'b0;
SDA_OE <= 1'b1; // SCL 高时拉低 SDA = START
state <= START;
end
end
// START:拉低 SDA(SCL 仍高)形成 START 条件,装载地址+W。
// 首字节首位需遵守 t_HD:STA:ADDR_DEVICE 会等 SCL 落下后保持 SDA 低 t_HD:STA,再驱动地址 MSB。
START: begin
SDA_OE <= 1'b1; // 维持 START(SDA 低)
shift_reg <= {addr, 1'b0}; // 设备地址 + W
bit_cnt <= 4'd0;
scl_run <= 1'b1; // 触发首拍下降沿(scl_run_pos)
first_bit <= 1'b1; // 首位需 t_HD:STA 保持
hd_cnt <= 0;
start_fell<= 1'b0;
state <= ADDR_DEVICE;
end
// 设备地址 + W:主机发 8 位,第9拍采样从机 ACK
ADDR_DEVICE: begin
if (first_bit) begin
// t_HD:STA:START 的 SCL 落下后,SDA 继续维持低 t_HD:STA,再驱动首位(地址 MSB)
if (!start_fell) begin
if (scl_fall) start_fell <= 1'b1; // 等 START 的 SCL 下降沿
SDA_OE <= 1'b1; // 维持 START 低
end
else if (hd_cnt < T_HDSTA) begin
hd_cnt <= hd_cnt + 1'b1; // 计 t_HD:STA
SDA_OE <= 1'b1;
end
else begin
SDA_OE <= ~shift_reg[7]; // t_HD:STA 满足,驱动地址 MSB
first_bit<= 1'b0;
start_fell<= 1'b0;
end
end
else if (scl_fall) begin
if (bit_cnt < 4'd8) SDA_OE <= ~shift_reg[7];
else SDA_OE <= 1'b0; // 第9拍:释放等 ACK
end
else if (scl_rise) begin
if (bit_cnt < 4'd8) begin
shift_reg <= {shift_reg[6:0], 1'b0};
bit_cnt <= bit_cnt + 1'b1;
end
else begin
if (sda_in) begin // 从机 NACK 地址
error <= 1'b1; busy <= 1'b0; done <= 1'b1;
scl_run <= 1'b0; state <= IDLE;
end
else begin
shift_reg <= reg_addr; // 装载寄存器地址
bit_cnt <= 4'd0;
state <= ADDR_REG;
end
end
end
end
// 寄存器地址:主机发 8 位,第9拍采样 ACK;写则进 DATA,读则 repeated START
ADDR_REG: begin
if (scl_fall) begin
if (bit_cnt < 4'd8) SDA_OE <= ~shift_reg[7];
else SDA_OE <= 1'b0;
end
else if (scl_rise) begin
if (bit_cnt < 4'd8) begin
shift_reg <= {shift_reg[6:0], 1'b0};
bit_cnt <= bit_cnt + 1'b1;
end
else begin
if (sda_in) begin // 从机 NACK
error <= 1'b1; busy <= 1'b0; done <= 1'b1;
scl_run <= 1'b0; state <= IDLE;
end
else if (rw == 1'b0) begin
// 写:进入 DATA 发送 len 字节(首字节 wr_data 在 start 时已备好)
shift_reg <= wr_data;
remain <= (len == 8'd0) ? 8'd1 : len;
is_last <= ((len == 8'd0) ? 8'd1 : len) == 8'd1;
bit_cnt <= 4'd0;
state <= DATA;
end
else begin
// 读:SCL 高时拉低 SDA = repeated START,随后发 地址+R
// repeated START 与 START 一样需要保持 t_HD:STA,
// 因此进入 ADDR_RD 时置位 first_bit 等标志,由其完成保持后再驱动首位。
SDA_OE <= 1'b1;
shift_reg <= {addr, 1'b1}; // 设备地址 + R
bit_cnt <= 4'd0;
first_bit <= 1'b1; // 首位需 t_HD:STA 保持
start_fell<= 1'b0;
hd_cnt <= 0;
state <= ADDR_RD;
end
end
end
end
// 设备地址 + R(仅读):repeated START 后的首位同样需保持 t_HD:STA,
// 然后主机发 8 位,第9拍采样 ACK 后进入 DATA 接收
ADDR_RD: begin
if (first_bit) begin
// t_HD:STA:repeated START 的 SCL 落下后,SDA 继续维持低 t_HD:STA,再驱动首位(地址 MSB)
if (!start_fell) begin
if (scl_fall) start_fell <= 1'b1; // 等 repeated START 的 SCL 下降沿
SDA_OE <= 1'b1; // 维持 START 低
end
else if (hd_cnt < T_HDSTA) begin
hd_cnt <= hd_cnt + 1'b1; // 计 t_HD:STA
SDA_OE <= 1'b1;
end
else begin
SDA_OE <= ~shift_reg[7]; // t_HD:STA 满足,驱动地址 MSB
first_bit<= 1'b0;
start_fell<= 1'b0;
end
end
else if (scl_fall) begin
if (bit_cnt < 4'd8) SDA_OE <= ~shift_reg[7];
else SDA_OE <= 1'b0;
end
else if (scl_rise) begin
if (bit_cnt < 4'd8) begin
shift_reg <= {shift_reg[6:0], 1'b0};
bit_cnt <= bit_cnt + 1'b1;
end
else begin
if (sda_in) begin // 从机 NACK
error <= 1'b1; busy <= 1'b0; done <= 1'b1;
scl_run <= 1'b0; state <= IDLE;
end
else begin
shift_reg <= 8'h00;
remain <= (len == 8'd0) ? 8'd1 : len;
is_last <= ((len == 8'd0) ? 8'd1 : len) == 8'd1;
bit_cnt <= 4'd0;
state <= DATA;
end
end
end
end
// 数据阶段:写=主机发+从机ACK;读=从机发+主机ACK/NACK
DATA: begin
// —— 写 ——
if (rw == 1'b0) begin
if (scl_fall) begin
if (bit_cnt < 4'd8) SDA_OE <= ~shift_reg[7];
else SDA_OE <= 1'b0;
end
else if (scl_rise) begin
if (bit_cnt < 4'd8) begin
shift_reg <= {shift_reg[6:0], 1'b0};
bit_cnt <= bit_cnt + 1'b1;
end
else begin
if (sda_in) begin // 从机 NACK 数据
error <= 1'b1; busy <= 1'b0; done <= 1'b1;
scl_run <= 1'b0; state <= IDLE;
end
else if (remain > 8'd1) begin
req <= 1'b1; // 请求外部准备下一字节
remain <= remain - 1'b1;
is_last <= (remain - 1'b1) == 8'd1;
bit_cnt <= 4'd0;
state <= DATA_NEXT; // 等 wr_data 更新后再装载
end
else begin
state <= STOP; // 全部发完
end
end
end
end
// —— 读 ——
else begin
if (scl_fall) begin
if (bit_cnt < 4'd8) SDA_OE <= 1'b0; // 接收期间释放
else if (!ack_phase) SDA_OE <= is_last ? 1'b0 : 1'b1; // ACK/NACK
else begin
SDA_OE <= 1'b0;
if (!is_last) begin
remain <= remain - 1'b1;
shift_reg<= 8'h00;
bit_cnt <= 4'd0;
is_last <= (remain - 1'b1) == 8'd1;
end
ack_phase <= 1'b0;
end
end
else if (scl_rise) begin
if (bit_cnt < 4'd8) begin
shift_reg <= {shift_reg[6:0], sda_in};
if (bit_cnt == 4'd7) begin
rd_data <= {shift_reg[6:0], sda_in}; // 收满 8 位
req <= 1'b1; // 通知外部锁存
bit_cnt <= 4'd8; // 进入第9拍
end
else bit_cnt <= bit_cnt + 1'b1;
end
else begin
// 第9拍上升沿:从机已采样到本字节 ACK/NACK
if (is_last) state <= STOP;
else ack_phase <= 1'b1;
end
end
end
end
// 字节边界缓冲:本拍外部已在 req 后把 wr_data 更新为新字节,
// 此刻再装入 shift_reg,确保下一个字节发出的是"新值"而非刚发完的旧值。
DATA_NEXT: begin
shift_reg <= wr_data;
state <= DATA;
end
// 停止位:先停 SCL(释放→高),SCL 高时拉低 SDA,保持 T_STOP 后释放 SDA=STOP
STOP: begin
scl_run <= 1'b0; // 释放 SCL,等待变高
SDA_OE <= 1'b1; // 保持 SDA 低,等 SCL 高
if (scl_in) begin
if (stop_cnt < T_STOP) stop_cnt <= stop_cnt + 1'b1;
else begin
SDA_OE <= 1'b0; // 释放 SDA→高:SCL 高时 SDA 上升 = STOP
done <= 1'b1;
error <= 1'b0;
busy <= 1'b0;
state <= IDLE;
end
end
else stop_cnt <= 0;
end
default: state <= IDLE;
endcase
end
end
endmodule
|
从机
从机作为总线外设,通过一组片内接口与内部逻辑交互。本从机仅实现 IIC 协议,不包含具体设备功能(例如 EEPROM、RTC 等寄存器逻辑),因此可以作为通用 IIC Slave IP 复用。
端口分为三组:片内总线侧 clk/rst_n,IIC 侧 scl/sda,以及内部交互接口 reg_addr / data_out / data_in / req / busy / done / error / rw。
IIC 侧先把 scl/sda 打两拍同步以消除亚稳态,再据此检测边沿与起始/停止条件,用状态机完成与主机对齐的标准 IIC 从机协议:先匹配 7 位地址 → 收 1 字节寄存器地址(reg_addr)→ 进入数据收发。寄存器指针 reg_ptr 取自主机发来的寄存器地址字节(8 位),每收发 1 字节指针自增,因此天然支持跨多个寄存器的连续读写:
- 写:主机先写
reg_addr,随后连发 N 字节;从机每收到 1 字节,就把 data_out 连同当前 reg_addr 通过 req 脉冲交给内部锁存,并使指针自增。
- 读:主机先写
reg_addr,再发 repeated START + 地址+R;从机在每个读字节开始前提前一拍用 req 请求内部把 reg_addr 处数据放到 data_in,随后从 data_in 取数连续送出,每字节后指针自增。
SDA 采用开漏建模:仅在需要拉低(回 ACK、或发送的数据位为 0)时驱动 0,其余时刻释放为高阻,由片外引脚上的上拉电阻拉高;从机只在 SCL 低电平期间更新 SDA、在高电平期间保持稳定,因此自身的跳变不会被误判为起始/停止条件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
|
module iic_slave #(
parameter [6:0] SLAVE_ADDR = 7'h50 // 本从机地址(由上层指定)
)(
input wire clk, // 片内总线时钟
input wire rst_n, // 异步复位(低有效)
input wire scl, // IIC 时钟(主机驱动,异步于 clk)
inout wire sda, // IIC 数据(开漏,双向)
// ===== 内部交互接口(片内逻辑侧)=====
output wire [7:0] reg_addr, // 当前访问的寄存器地址(字节偏移),输出给内部
output reg [7:0] data_out, // 写方向:主机写来的数据,输出给内部锁存
input wire [7:0] data_in, // 读方向:内部给从机的数据,由内部按 reg_addr 提供
output reg req, // 单 bit 握手:
// 写=收到 1 字节、请求内部把 data_out 锁存到 reg_addr;
// 读=即将发送 1 字节、请求内部把 reg_addr 处数据放到 data_in
output reg busy, // 从机正处于一次传输中(START 到 STOP)
output reg done, // 一次传输完成(检测到 STOP)
output reg error, // 错误标志(保留/可扩展:如内部 NACK、越界等,正常为 0)
output reg rw // 当前传输方向:1=读(从机发,数据来自 data_in),0=写(从机收,给 data_out)
);
// ============================================================
// 一、SCL/SDA 打两拍同步 + 边沿 / 起止条件检测
// ============================================================
reg [2:0] scl_sync, sda_sync;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
scl_sync <= 3'b111;
sda_sync <= 3'b111;
end else begin
scl_sync <= {scl_sync[1:0], scl};
sda_sync <= {sda_sync[1:0], sda};
end
end
wire scl_rise = (scl_sync[2:1] == 2'b01);
wire scl_fall = (scl_sync[2:1] == 2'b10);
wire scl_high = scl_sync[1];
wire sda_in = sda_sync[1];
wire start_cond = scl_high && (sda_sync[2:1] == 2'b10);
wire stop_cond = scl_high && (sda_sync[2:1] == 2'b01);
// ============================================================
// 二、SDA 开漏输出(片外总线):仅在需要"拉低"时驱动 0,其余释放为高阻。
// ============================================================
reg sda_oe, sda_d;
assign sda = (sda_oe && (sda_d == 1'b0)) ? 1'b0 : 1'bz;
// ============================================================
// 三、IIC 从机状态机(与主机协议对齐:地址+W → 寄存器地址 → [repeated START+地址+R] → 数据)
// 寄存器地址 reg_ptr 取自主机发来的寄存器地址字节(8 位),收发后自增;
// 写:每收到 1 字节经 req 交给内部(data_out + reg_addr);
// 读:每发 1 字节前经 req 向内部要数(data_in),从 data_in 取出连续送出。
// ============================================================
localparam S_IDLE = 3'd0,
S_START = 3'd1,
S_ADDR = 3'd2,
S_ACK = 3'd3,
S_DATA = 3'd4,
S_STOP = 3'd5;
reg [2:0] state;
reg [3:0] bit_cnt;
reg [7:0] shift_rx;
reg [7:0] addr_byte;
reg [7:0] shift_tx;
reg [7:0] reg_ptr; // 寄存器地址指针(8 位,主机发来的寄存器地址字节),收发后自增
reg mack;
reg got_addr;
reg reg_done;
reg slave_tx;
assign reg_addr = reg_ptr; // 当前寄存器地址直接给内部
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= S_IDLE;
sda_oe <= 1'b0;
sda_d <= 1'b1;
bit_cnt <= 4'd0;
shift_rx <= 8'd0;
shift_tx <= 8'd0;
reg_ptr <= 8'd0;
rw <= 1'b0;
mack <= 1'b0;
got_addr <= 1'b0;
reg_done <= 1'b0;
slave_tx <= 1'b0;
data_out <= 8'd0;
req <= 1'b0;
busy <= 1'b0;
done <= 1'b0;
error <= 1'b0;
end
else begin
req <= 1'b0; // 默认不请求;需要时脉冲置 1
done <= 1'b0; // 默认非完成;STOP 时脉冲置 1
// 起始/停止条件最高优先级:随时重定位状态机
if (start_cond) begin
state <= S_START;
bit_cnt <= 4'd0;
sda_oe <= 1'b0;
got_addr <= 1'b0;
reg_done <= 1'b0;
slave_tx <= 1'b0;
busy <= 1'b1;
// reg_ptr 保留,repeated START 后读仍从已设 reg_addr 起
end
else if (stop_cond) begin
state <= S_STOP;
sda_oe <= 1'b0;
busy <= 1'b0;
done <= 1'b1; // 传输完成脉冲
end
else begin
case (state)
S_IDLE: begin
sda_oe <= 1'b0;
end
S_START: begin
bit_cnt <= 4'd0;
sda_oe <= 1'b0;
state <= S_ADDR;
end
// ADDR:收 8 位地址字节(MSB 先入)。收满后在 ACK 拍回 ACK 并判匹配/R-W。
S_ADDR: begin
if (scl_rise) begin
if (bit_cnt < 4'd8) begin
shift_rx <= {shift_rx[6:0], sda_in};
bit_cnt <= bit_cnt + 1'b1;
if (bit_cnt == 4'd7)
addr_byte <= {shift_rx[6:0], sda_in};
end
end
else if (scl_fall) begin
if (bit_cnt == 4'd8) begin
if (addr_byte[7:1] == SLAVE_ADDR) begin
rw <= addr_byte[0];
sda_oe <= 1'b1;
sda_d <= 1'b0;
// 读方向:提前一拍请求内部提供 reg_ptr 指向的数据(下一字节即读首字节)
if (addr_byte[0] == 1'b1) req <= 1'b1;
state <= S_ACK;
end else begin
sda_oe <= 1'b0;
state <= S_IDLE;
end
end
end
end
// ACK:每字节后的应答拍。
// 从机发送方向(slave_tx=1,读数据):释放 SDA,SCL 高采样主机 ACK/NACK,下降沿续传/结束;
// 从机接收方向(slave_tx=0):本低电平已驱动 ACK,下降沿释放并按子阶段决定去向。
S_ACK: begin
if (slave_tx == 1'b1) begin
if (scl_rise) begin
mack <= sda_in; // 0=ACK 续传,1=NACK 结束
end
else if (scl_fall) begin
if (mack == 1'b0) begin
// 主机 ACK:续传下一字节(数据已在上一字节末经 req 请求)
bit_cnt <= 4'd0;
state <= S_DATA;
end else begin
sda_oe <= 1'b0; // NACK:主机结束读
state <= S_IDLE;
end
end
end
else begin
if (scl_fall) begin
sda_oe <= 1'b0; // 释放 ACK
bit_cnt <= 4'd0;
if (got_addr == 1'b0) begin
// 刚 ACK 的是"地址字节"
got_addr <= 1'b1;
if (rw == 1'b1) begin
// 读:进入发送(reg_ptr 已由前面 reg_addr 字节设好,数据经 req 已请求)
slave_tx <= 1'b1;
reg_done <= 1'b1;
bit_cnt <= 4'd0;
state <= S_DATA;
end else begin
// 写:进入 DATA 收寄存器地址
slave_tx <= 1'b0;
reg_done <= 1'b0;
state <= S_DATA;
end
end
else if (reg_done == 1'b0) begin
// 刚 ACK 的是"寄存器地址字节":锁存寄存器地址
reg_ptr <= shift_rx;
reg_done <= 1'b1;
slave_tx <= 1'b0;
state <= S_DATA;
end
else begin
// 刚 ACK 的是"写数据字节":交给内部锁存
data_out <= shift_rx; // 写来的数据给内部
req <= 1'b1; // 请求内部锁存到当前 reg_addr
reg_ptr <= reg_ptr + 1'b1; // 指针自增
slave_tx <= 1'b0;
state <= S_DATA;
end
end
end
end
// DATA:收发一个字节(MSB 先)。
// 发(slave_tx=1,读数据):首拍从 data_in 装载,SCL 低电平驱动位,发满 8 位后释放供主机应答;
// 收(slave_tx=0,含 reg_addr 与写数据):SCL 上升沿移位,收满 8 位后本低电平驱动 ACK。
S_DATA: begin
if (slave_tx == 1'b1) begin
if (scl_fall) begin
if (bit_cnt == 4'd0) begin
// 首拍:装载内部提供的读数据(req 已提前一拍请求)
shift_tx <= {data_in[6:0], 1'b0};
sda_oe <= 1'b1;
sda_d <= data_in[7];
bit_cnt <= 4'd1;
end
else if (bit_cnt < 4'd8) begin
sda_oe <= 1'b1;
sda_d <= shift_tx[7];
shift_tx <= {shift_tx[6:0], 1'b0};
bit_cnt <= bit_cnt + 1'b1;
end
else begin
// 8 位发完,释放供主机 ACK/NACK;自增指针并提前请求下一字节
sda_oe <= 1'b0;
reg_ptr <= reg_ptr + 1'b1;
req <= 1'b1;
state <= S_ACK;
end
end
end
else begin
if (scl_rise) begin
if (bit_cnt < 4'd8) begin
shift_rx <= {shift_rx[6:0], sda_in};
bit_cnt <= bit_cnt + 1'b1;
end
end
else if (scl_fall) begin
if (bit_cnt == 4'd8) begin
sda_oe <= 1'b1; // 驱动 ACK
sda_d <= 1'b0;
state <= S_ACK;
end
end
end
end
S_STOP: begin
sda_oe <= 1'b0;
state <= S_IDLE;
end
default: state <= S_IDLE;
endcase
end
end
end
endmodule
|