Featured image of post IIC总线

IIC总线

IIC 总线协议讲解与可复用的 Verilog 主从机代码样例。

本文讲解 IIC 总线协议,并给出可直接参考的 Verilog 主从机代码样例。

文章时序解析内容基于该视频

IIC总线介绍

IIC(Inter-Integrated Circuit,常写作 I2C)是一种同步串行总线协议,由 Philips 提出由一个主机和若干从机构成,一个主机和多个从机共享同一对 SDA/SCL 信号线。

IIC总线仅使用两根线工作:SCL(Serial Clock,时钟线)SDA(Serial Data,数据线),二者均为开漏结构、靠上拉电阻拉高,主从机控制拉低。本文采用7位地址表示法。同一条总线上,每个从机地址必须唯一,主机通过地址选中特定从机进行通信。

IIC协议

image

上图为 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 拉低,即产生起始条件,标志一次传输开始。

begin

地址位 + W/R

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

addr

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

应答位(ACK)

ack

图中白色信号即应答位,ACK 由当前字节的接收方产生。每传输完 8 位数据后,接收方需在紧跟的第 9 个 SCL 周期给出应答:ACK = SDA 被拉低(低电平)NACK = SDA 释放(高电平)

NACK 表示发送方未收到应答。在主机读数据时,主机通常会在最后一个字节返回 NACK,通知从机停止继续发送;在其他场景下,NACK 也可能表示地址不存在、设备忙或通信异常。只有收到有效 ACK,通信才会继续。

停止位(STOP, P)

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
使用 Hugo 构建
主题 StackJimmy 设计