)
本文是《FPGA入门到实战》专栏第15篇。前两篇完成了 SPI 与 UART 通信本篇继续实现常见的板级低速总线——I2C。我们将从起始、停止、应答和开漏结构讲起实现支持时钟拉伸的字节命令引擎并在其上完成 AT24C02 EEPROM 的单字节写入与随机读取控制。1. I2C 总线的核心特点I2CInter-Integrated Circuit使用两根信号线SCL串行时钟SDA双向串行数据。与 UART 点对点连接不同一条 I2C 总线可以挂接多个从设备。Master 通过 7 位地址选择目标设备同一时刻只允许一个器件驱动总线。I2C 的工程特点包括只占用两根信号线SDA 和 SCL 使用开漏结构需要外部上拉电阻每发送 8 位数据接收方必须返回 1 位 ACK/NACK支持重复起始不释放总线即可切换传输方向从设备可以拉低 SCL 实现时钟拉伸。2. 为什么 SDA 不能直接输出高电平2.1 开漏总线I2C 设备只能主动拉低信号不能主动驱动高电平。所有设备释放信号后由外部上拉电阻把线路拉高VCC | [R] 上拉电阻 | ---------- SDA/SCL ---------- | | Master 开漏输出 Slave 开漏输出这种“线与”结构允许多个器件共享总线只要有一个器件拉低线上就是低电平只有全部器件释放线路才变高。Verilog 中应使用三态输出描述释放状态assign i2c_sda sda_low ? 1b0 : 1bz; assign i2c_scl scl_low ? 1b0 : 1bz;1bz表示不驱动由外部上拉决定实际高电平。绝不能写成在不同阶段主动输出1b1否则当从设备拉低时会产生电气冲突。2.2 上拉电阻如何选择常见开发板会预装 2.2 kΩ10 kΩ 的上拉电阻。阻值选择与总线电容、供电电压和目标速率有关阻值过大上升沿太慢高速通信可能采样错误阻值过小低电平电流增大器件功耗和驱动压力上升线越长、器件越多总线电容越大需要更强的上拉。入门实验可先使用板载推荐值出现上升沿缓慢时再结合示波器调整。3. I2C 基本时序3.1 起始与停止条件普通数据位只允许在 SCL 低电平期间变化SCL 高电平期间必须保持稳定。只有两个例外STARTSCL 为高时SDA 从高变低STOPSCL 为高时SDA 从低变高。START STOP SCL ‾‾‾‾‾\____ ____/‾‾‾‾‾ SDA ‾‾‾\_______ ____/‾‾‾‾‾ ↑ ↑3.2 数据位与应答位每个字节高位先发与 UART 的低位先发正好相反。发送 8 位后发送方释放 SDA接收方在第 9 个时钟返回ACK 0接收成功NACK 1未应答或结束读取。Master 读取多个字节时应对前面的字节返回 ACK最后一个字节返回 NACK然后产生 STOP。3.3 标准速率常见 I2C 速率包括模式典型速率Standard-mode100 kHzFast-mode400 kHzFast-mode Plus1 MHzAT24C02 的具体上限取决于器件型号和供电电压设计前应查阅对应数据手册。本文示例默认使用 100 kHz。4. AT24C02 地址与读写流程AT24C02 是常见的 2 Kbit EEPROM内部容量为 256 × 8 bit因此存储地址为 8 位。器件控制字节通常写成1 0 1 0 A2 A1 A0 R/W当地址引脚A2:A0全部接地时写控制字节8hA0读控制字节8hA1。4.1 单字节写入START - A0 - ACK - 存储地址 - ACK - 写数据 - ACK - STOPSTOP 之后 EEPROM 会进行内部写周期此时器件可能暂时不应答。典型写周期为数毫秒不能立即发起下一次访问。4.2 随机读取START - A0 - ACK - 存储地址 - ACK - Repeated START - A1 - ACK - 读数据 - NACK - STOP前半段先设置 EEPROM 内部地址指针随后用重复起始切换为读方向。读取最后一个字节后Master 返回 NACK 表示结束。5. 设计分层把 I2C 设计拆成两层更容易验证AT24C02 事务控制层 ├─ 决定 START、设备地址、存储地址、数据和 STOP 的顺序 └─ 处理读写请求、完成标志与错误标志 | v I2C 字节命令引擎 ├─ START / WRITE / READ / STOP ├─ 产生 SCL 四分之一周期节拍 ├─ 发送数据、采样数据和检查 ACK └─ 开漏驱动 SDA/SCL并等待时钟拉伸协议波形由底层统一生成上层只排列命令。以后驱动温度传感器、RTC 或 ADC 时可以直接复用底层引擎。6. I2C 字节命令引擎6.1 命令定义底层支持四类操作命令功能CMD_START产生起始或重复起始CMD_WRITE发送 8 位并采样 ACKCMD_READ读取 8 位并发送 ACK/NACKCMD_STOP产生停止条件每条命令用cmd_valid/cmd_ready握手完成时输出单周期done。6.2 完整代码module i2c_master_byte #( parameter integer CLK_FREQ 50_000_000, parameter integer I2C_FREQ 100_000 ) ( input wire clk , // 系统时钟 input wire rstn , // 低有效复位 input wire cmd_valid , // 命令有效 input wire [1:0] cmd , // 命令类型 input wire [7:0] tx_data , // 写数据 input wire read_nack , // 读取后发送 NACK output wire cmd_ready , // 可接收命令 output reg done , // 命令完成脉冲 output reg [7:0] rx_data , // 读数据 output reg rx_valid , // 读数据有效脉冲 output reg ack_error , // 从设备未应答 output wire busy , // 引擎忙 inout wire i2c_scl , // I2C 时钟 inout wire i2c_sda // I2C 数据 ); localparam [1:0] CMD_START 2d0; localparam [1:0] CMD_WRITE 2d1; localparam [1:0] CMD_READ 2d2; localparam [1:0] CMD_STOP 2d3; localparam [4:0] STATE_IDLE 5d0; localparam [4:0] STATE_START_A 5d1; localparam [4:0] STATE_START_B 5d2; localparam [4:0] STATE_START_C 5d3; localparam [4:0] STATE_WRITE_LOW 5d4; localparam [4:0] STATE_WRITE_HIGH 5d5; localparam [4:0] STATE_ACK_LOW 5d6; localparam [4:0] STATE_ACK_HIGH 5d7; localparam [4:0] STATE_ACK_END 5d8; localparam [4:0] STATE_READ_LOW 5d9; localparam [4:0] STATE_READ_HIGH 5d10; localparam [4:0] STATE_READ_ACK_LOW 5d11; localparam [4:0] STATE_READ_ACK_HIGH 5d12; localparam [4:0] STATE_READ_END 5d13; localparam [4:0] STATE_STOP_A 5d14; localparam [4:0] STATE_STOP_B 5d15; localparam [4:0] STATE_STOP_C 5d16; localparam integer DIV_CNT_MAX CLK_FREQ / (I2C_FREQ * 4) - 1; reg [31:0] div_cnt; reg [4:0] state; reg [7:0] tx_shift; reg [7:0] rx_shift; reg [2:0] bit_cnt; reg read_nack_r; reg scl_low; reg sda_low; wire quarter_tick; assign quarter_tick (div_cnt DIV_CNT_MAX); assign cmd_ready (state STATE_IDLE); assign busy (state ! STATE_IDLE); assign i2c_scl (scl_low 1b1) ? 1b0 : 1bz; assign i2c_sda (sda_low 1b1) ? 1b0 : 1bz; always (*) begin scl_low 1b0; sda_low 1b0; case (state) STATE_START_B: begin sda_low 1b1; end STATE_START_C: begin scl_low 1b1; sda_low 1b1; end STATE_WRITE_LOW: begin scl_low 1b1; sda_low ~tx_shift[bit_cnt]; end STATE_WRITE_HIGH: begin sda_low ~tx_shift[bit_cnt]; end STATE_ACK_LOW, STATE_ACK_END, STATE_READ_LOW, STATE_READ_END: begin scl_low 1b1; end STATE_READ_ACK_LOW: begin scl_low 1b1; sda_low ~read_nack_r; end STATE_READ_ACK_HIGH: begin sda_low ~read_nack_r; end STATE_STOP_A: begin scl_low 1b1; sda_low 1b1; end STATE_STOP_B: begin sda_low 1b1; end default: begin scl_low 1b0; sda_low 1b0; end endcase end always (posedge clk or negedge rstn) begin if (rstn 1b0) begin div_cnt 32d0; end else if (state STATE_IDLE) begin div_cnt 32d0; end else if (quarter_tick 1b1) begin div_cnt 32d0; end else begin div_cnt div_cnt 1b1; end end always (posedge clk or negedge rstn) begin if (rstn 1b0) begin state STATE_IDLE; tx_shift 8d0; rx_shift 8d0; rx_data 8d0; bit_cnt 3d0; read_nack_r 1b1; done 1b0; rx_valid 1b0; ack_error 1b0; end else begin done 1b0; rx_valid 1b0; if (state STATE_IDLE) begin if (cmd_valid 1b1) begin ack_error 1b0; case (cmd) CMD_START: begin state STATE_START_A; end CMD_WRITE: begin tx_shift tx_data; bit_cnt 3d7; state STATE_WRITE_LOW; end CMD_READ: begin rx_shift 8d0; bit_cnt 3d7; read_nack_r read_nack; state STATE_READ_LOW; end CMD_STOP: begin state STATE_STOP_A; end default: begin state STATE_IDLE; end endcase end end else if (quarter_tick 1b1) begin case (state) STATE_START_A: begin if (i2c_scl 1b1) begin state STATE_START_B; end end STATE_START_B: begin if (i2c_scl 1b1) begin state STATE_START_C; end end STATE_START_C: begin state STATE_IDLE; done 1b1; end STATE_WRITE_LOW: begin state STATE_WRITE_HIGH; end STATE_WRITE_HIGH: begin if (i2c_scl 1b1) begin if (bit_cnt 3d0) begin state STATE_ACK_LOW; end else begin bit_cnt bit_cnt - 1b1; state STATE_WRITE_LOW; end end end STATE_ACK_LOW: begin state STATE_ACK_HIGH; end STATE_ACK_HIGH: begin if (i2c_scl 1b1) begin ack_error i2c_sda; state STATE_ACK_END; end end STATE_ACK_END: begin state STATE_IDLE; done 1b1; end STATE_READ_LOW: begin state STATE_READ_HIGH; end STATE_READ_HIGH: begin if (i2c_scl 1b1) begin rx_shift[bit_cnt] i2c_sda; if (bit_cnt 3d0) begin state STATE_READ_ACK_LOW; end else begin bit_cnt bit_cnt - 1b1; state STATE_READ_LOW; end end end STATE_READ_ACK_LOW: begin state STATE_READ_ACK_HIGH; end STATE_READ_ACK_HIGH: begin if (i2c_scl 1b1) begin state STATE_READ_END; end end STATE_READ_END: begin rx_data rx_shift; rx_valid 1b1; state STATE_IDLE; done 1b1; end STATE_STOP_A: begin state STATE_STOP_B; end STATE_STOP_B: begin if (i2c_scl 1b1) begin state STATE_STOP_C; end end STATE_STOP_C: begin if (i2c_scl 1b1) begin state STATE_IDLE; done 1b1; end end default: begin state STATE_IDLE; end endcase end end end endmodule6.3 时钟拉伸处理在需要 SCL 为高的状态中代码会检查i2c_scl 1b1后才继续。Master 释放 SCL 后如果 Slave 仍将其拉低状态机会停留并等待这就是时钟拉伸兼容。如果不读取管脚真实电平而是假设“释放就一定为高”遇到支持时钟拉伸的器件就会提前改变 SDA破坏时序。7. AT24C02 事务控制器上层把一次读写拆成多条字节命令。start是单周期请求write_enable1执行写入write_enable0执行随机读取。module at24c02_ctrl #( parameter integer CLK_FREQ 50_000_000, parameter integer I2C_FREQ 100_000 ) ( input wire clk , // 系统时钟 input wire rstn , // 低有效复位 input wire start , // 单周期操作请求 input wire write_enable, // 1 写入0 读取 input wire [7:0] mem_addr , // EEPROM 地址 input wire [7:0] write_data , // 写入数据 output reg [7:0] read_data , // 读取数据 output reg busy , // 控制器忙 output reg done , // 操作完成脉冲 output reg error , // 任一字节未应答 inout wire i2c_scl , // I2C 时钟 inout wire i2c_sda // I2C 数据 ); localparam [1:0] CMD_START 2d0; localparam [1:0] CMD_WRITE 2d1; localparam [1:0] CMD_READ 2d2; localparam [1:0] CMD_STOP 2d3; localparam [3:0] STATE_IDLE 4d0; localparam [3:0] STATE_START_1 4d1; localparam [3:0] STATE_DEV_W 4d2; localparam [3:0] STATE_MEM 4d3; localparam [3:0] STATE_DATA_W 4d4; localparam [3:0] STATE_START_2 4d5; localparam [3:0] STATE_DEV_R 4d6; localparam [3:0] STATE_DATA_R 4d7; localparam [3:0] STATE_STOP 4d8; reg [3:0] state; reg launched; reg operation_write; reg [7:0] mem_addr_r; reg [7:0] write_data_r; reg cmd_valid; reg [1:0] cmd; reg [7:0] cmd_tx_data; reg cmd_read_nack; wire cmd_ready; wire cmd_done; wire [7:0] cmd_rx_data; wire cmd_rx_valid; wire cmd_ack_error; wire cmd_busy; i2c_master_byte #( .CLK_FREQ(CLK_FREQ), .I2C_FREQ(I2C_FREQ) ) u_i2c_master_byte ( .clk (clk ), .rstn (rstn ), .cmd_valid (cmd_valid ), .cmd (cmd ), .tx_data (cmd_tx_data ), .read_nack (cmd_read_nack), .cmd_ready (cmd_ready ), .done (cmd_done ), .rx_data (cmd_rx_data ), .rx_valid (cmd_rx_valid ), .ack_error (cmd_ack_error), .busy (cmd_busy ), .i2c_scl (i2c_scl ), .i2c_sda (i2c_sda ) ); always (*) begin cmd_valid (state ! STATE_IDLE) (launched 1b0); cmd CMD_START; cmd_tx_data 8d0; cmd_read_nack 1b1; case (state) STATE_START_1, STATE_START_2: begin cmd CMD_START; end STATE_DEV_W: begin cmd CMD_WRITE; cmd_tx_data 8hA0; end STATE_MEM: begin cmd CMD_WRITE; cmd_tx_data mem_addr_r; end STATE_DATA_W: begin cmd CMD_WRITE; cmd_tx_data write_data_r; end STATE_DEV_R: begin cmd CMD_WRITE; cmd_tx_data 8hA1; end STATE_DATA_R: begin cmd CMD_READ; cmd_read_nack 1b1; end STATE_STOP: begin cmd CMD_STOP; end default: begin cmd CMD_START; end endcase end always (posedge clk or negedge rstn) begin if (rstn 1b0) begin state STATE_IDLE; launched 1b0; operation_write 1b0; mem_addr_r 8d0; write_data_r 8d0; read_data 8d0; busy 1b0; done 1b0; error 1b0; end else begin done 1b0; if (state STATE_IDLE) begin launched 1b0; if (start 1b1) begin operation_write write_enable; mem_addr_r mem_addr; write_data_r write_data; busy 1b1; error 1b0; state STATE_START_1; end end else if ((launched 1b0) (cmd_ready 1b1)) begin launched 1b1; end else if ((launched 1b1) (cmd_done 1b1)) begin launched 1b0; if ((cmd_ack_error 1b1) ((state STATE_DEV_W) || (state STATE_MEM) || (state STATE_DATA_W) || (state STATE_DEV_R))) begin error 1b1; state STATE_STOP; end else begin case (state) STATE_START_1: begin state STATE_DEV_W; end STATE_DEV_W: begin state STATE_MEM; end STATE_MEM: begin if (operation_write 1b1) begin state STATE_DATA_W; end else begin state STATE_START_2; end end STATE_DATA_W: begin state STATE_STOP; end STATE_START_2: begin state STATE_DEV_R; end STATE_DEV_R: begin state STATE_DATA_R; end STATE_DATA_R: begin read_data cmd_rx_data; state STATE_STOP; end STATE_STOP: begin busy 1b0; done 1b1; state STATE_IDLE; end default: begin busy 1b0; error 1b1; state STATE_IDLE; end endcase end end end end endmodulecmd_rx_valid和cmd_busy在这个最小控制器中没有参与决策但保留连接便于 ILA 调试和后续扩展。综合工具会对未使用逻辑进行优化。8. 写周期等待与 ACK PollingAT24C02 收到 STOP 后会把数据写入非易失存储阵列。内部写周期尚未结束时器件不会响应地址。最简单的处理方式是固定等待例如 5 ms。更稳妥的方法是 ACK PollingSTART - 发送 A0 - 检查 ACK ├─ NACKSTOP等待一小段时间后重试 └─ ACK 器件写入完成可以继续访问ACK Polling 不依赖最坏写周期器件一准备好就能继续工作。正式工程应在事务控制器上方增加超时计数避免器件断开时无限重试。9. Testbench 仿真要点仿真模型需要给 SDA 和 SCL 增加上拉否则释放后的信号会显示为Ztri1 i2c_scl; tri1 i2c_sda;或者显式实例化pullup(i2c_scl); pullup(i2c_sda);从设备模型至少要实现三件事检测 START 和 STOP在 SCL 上升沿接收 Master 发出的 8 位数据在第 9 个时钟拉低 SDA 返回 ACK并在读取阶段驱动数据。9.1 单字节写入预期波形向地址8h20写入8h5A时波形应依次出现START - A0(ACK) - 20(ACK) - 5A(ACK) - STOP检查点START 只在 SCL 高时拉低 SDA每个数据位只在 SCL 低时改变第 9 个时钟 Master 释放 SDASlave 拉低 SDA 后ack_error保持为 0STOP 只在 SCL 高时释放 SDA。9.2 随机读取预期波形从地址8h20读取时波形应为START - A0(ACK) - 20(ACK) - Repeated START - A1(ACK) - 5A - NACK - STOP读取最后一个字节后Master 必须发送 NACK。如果错误地发送 ACK部分从设备会认为还要继续输出下一个字节。10. Vivado 工程与 XDC 约束将i2c_master_byte.v和at24c02_ctrl.v加入 Design Sources。SDA、SCL 都声明为inout约束格式如下set_property PACKAGE_PIN I2C_SCL_PIN [get_ports i2c_scl] set_property IOSTANDARD LVCMOS33 [get_ports i2c_scl] set_property PACKAGE_PIN I2C_SDA_PIN [get_ports i2c_sda] set_property IOSTANDARD LVCMOS33 [get_ports i2c_sda]是否需要内部上拉取决于开发板硬件。板上已经有外部上拉时不要重复添加过强的上拉。若确认硬件没有上拉可以根据器件手册和 FPGA 系列支持情况设置set_property PULLUP true [get_ports i2c_scl] set_property PULLUP true [get_ports i2c_sda]外部电阻通常比 FPGA 内部弱上拉更适合稳定的 I2C 通信。11. 下板验证步骤建议按照“先写后读”的顺序验证复位系统向8h20写入8h5A等待写周期结束或执行 ACK Polling从8h20随机读取一个字节检查read_data是否为8h5A使用 ILA 观察state、cmd、ack_error、read_data使用示波器或逻辑分析仪检查真实 SCL/SDA 波形。ILA 适合观察 FPGA 内部状态但开漏线路的上升时间、毛刺和电平幅度必须用外部仪器确认。12. 常见问题排查12.1 所有地址都返回 NACK优先检查SDA/SCL 是否存在上拉设备地址是否包含正确的R/W位A2:A0硬件连接是否与地址匹配FPGA IO 电压是否与 EEPROM 兼容器件是否供电、是否共地START 和 STOP 是否满足数据手册时序。用示波器观察线路空闲状态。若 SDA 或 SCL 无法回到高电平先排查硬件不要继续修改状态机。12.2 写入后立即读取还是旧数据这是 EEPROM 内部写周期未完成。增加固定等待只能临时解决推荐使用 ACK Polling并设置毫秒级超时。12.3 SDA 一直为低系统死锁可能原因是 Master 在传输中途复位而 Slave 仍停留在发送状态。常见总线恢复方法释放 SDA手动产生最多 9 个 SCL 脉冲观察 SDA 是否释放产生一个合法 STOP重新开始事务。为什么是 9 个脉冲从设备最多可能还剩 8 个数据位和 1 个 ACK 位补足时钟后通常能让内部状态机回到等待状态。12.4 100 kHz 正常400 kHz 失败重点检查上拉电阻是否过大总线电容和线缆长度是否过大SCL/SDA 上升时间是否满足 Fast-modeFPGA 分频是否产生准确频率器件在当前供电电压下是否支持 400 kHz。不要只看数字波形“像高电平”。实际模拟电压可能尚未越过输入门限。12.5 偶发读错一个位可以依次排查输入是否直接使用管脚电平避免错误的内部回读SCL 高电平是否被时钟拉伸SDA 是否在 SCL 高电平期间发生变化上升沿是否过慢是否需要在高电平中点采样而不是刚释放 SCL 就采样。本文按四分之一周期分阶段在 SCL 高阶段结束时采样给信号留出了稳定时间。13. I2C、SPI、UART 对比对比项I2CSPIUART信号线SDA、SCLSCLK、MOSI、MISO、CSTX、RX时钟同步Master 提供同步Master 提供异步双方约定波特率多设备7/10 位地址选择独立 CS 选择通常点对点数据方向SDA 半双工全双工可全双工应答每字节 ACK/NACK协议本身无应答协议本身无应答典型速率100 kHz、400 kHz数 MHz数十 MHz9600数 Mbps典型外设EEPROM、RTC、传感器Flash、ADC、DAC调试串口、通信模块I2C 引脚最少且便于多设备扩展但协议状态更多、速度通常低于 SPI。UART 最适合点对点调试SPI 更适合高速板内通信。14. 工程扩展方向在当前字节引擎上可以继续实现连续多字节读写EEPROM 页写入和跨页边界处理ACK Polling 与超时退出总线死锁检测和 9 脉冲恢复多个从设备的地址表10 位地址模式仲裁丢失检测AXI-Lite 寄存器接口。扩展时建议保持分层底层只负责合法波形上层负责设备命令和业务流程。这样换用不同 I2C 外设时底层无需重写。15. 本篇总结本篇完成了 I2C 与 AT24C02 的完整设计思路理解开漏输出、外部上拉和线与关系掌握 START、STOP、数据位、ACK/NACK 和重复起始实现支持 START、WRITE、READ、STOP 的字节命令引擎在 SCL 高阶段检查真实管脚电平兼容时钟拉伸实现 AT24C02 单字节写入与随机读取流程理解 EEPROM 写周期与 ACK Polling掌握 NACK、总线死锁和高速失败的排查方法。至此UART、SPI、I2C 三类常用通信接口已经具备完整的协议基础。下一篇将进入有限状态机 FSM总结这些控制器背后的统一设计方法并对比一段式、二段式和三段式写法。