从时序到状态机:FPGA实现IIC主控制器核心逻辑详解
1. IIC协议的核心时序解析IIC协议作为一种经典的串行通信协议其精妙之处在于仅用两根线SDA和SCL就实现了完整的主从设备通信。理解IIC协议首先要掌握其四大关键时序起始条件、停止条件、数据有效性和应答信号。起始信号START的时序要求非常严格当SCL为高电平时SDA线从高电平跳变到低电平。这个下降沿就像是一个起跑枪声告诉所有从设备注意通信要开始了。我在实际项目中遇到过因为起始信号时序偏差导致通信失败的情况后来发现是FPGA时钟域切换时产生了毛刺。停止信号STOP则是相反的时序当SCL为高电平时SDA线从低电平跳变到高电平。这个上升沿就像是说好了我说完了。这里有个容易踩坑的地方有些工程师会在SCL为低电平时就提前拉高SDA这是不符合规范的。数据有效性规则简单但必须严格遵守只有在SCL为低电平时SDA线上的数据才能变化当SCL为高电平时SDA必须保持稳定。这就好比在课堂上老师SCL说现在可以发言低电平时学生SDA才能说话老师敲黑板高电平时所有人都要保持安静。应答信号ACK发生在每个字节传输后的第9个时钟周期。发送方可能是主设备或从设备会释放SDA线输出高阻态接收方需要将SDA拉低表示正确接收。如果没有这个应答发送方就应该认为传输失败。我在调试一个温度传感器时就曾因为忽略了ACK信号导致读取的数据总是错误。2. 状态机设计方法论将IIC协议转化为FPGA实现时三段式状态机是最佳选择。这种设计方法将状态机的描述分为三部分状态定义、状态转移条件和状态输出使得代码既清晰又易于维护。2.1 状态划分的艺术设计IIC主控制器时状态划分需要与协议时序严格对应。基本状态应包括IDLE空闲状态等待启动信号START产生起始条件ADDR发送从机地址和读写位DATA传输数据字节ACK等待或产生应答STOP产生停止条件对于更复杂的操作比如EEPROM读写还需要细分状态。例如写操作可能需要ADDR_WRITE发送写地址DATA_WRITE发送写数据ADDR_READ发送读地址虚写操作DATA_READ读取数据我在设计一个多从机系统时发现状态划分过细会导致状态机臃肿过粗又无法处理异常情况。最终采用了分层状态机的设计主状态机处理基本流程子状态机处理特定从机的特殊时序要求。2.2 状态转移条件设计状态转移条件需要精确对应IIC协议的时序要求。以发送一个数据位为例SCL为低电平时设置SDA为要发送的位等待半个时钟周期拉高SCL等待从设备采样通常是一个完整的时钟周期拉低SCL准备发送下一位在FPGA中我们常用计数器来控制这些时序。例如使用四倍频时钟时可以这样设计case(clk4_cnt) 0: begin sda_out data_bit; scl 0; end // 准备数据 1: scl 0; // 保持 2: scl 1; // 上升沿 3: scl 1; // 高电平保持 4: begin scl 0; clk4_cnt 0; end // 下降沿准备下一位 endcase2.3 三段式状态机实现三段式状态机的Verilog实现有其固定模式。第一段用同步时序电路描述状态寄存器always (posedge clk or negedge rst_n) begin if(!rst_n) current_state IDLE; else current_state next_state; end第二段用组合逻辑描述状态转移条件always (*) begin case(current_state) IDLE: next_state (start) ? START : IDLE; START: next_state (cnt_done) ? ADDR : START; // 其他状态转移... endcase end第三段用时序逻辑描述每个状态的输出always (posedge clk or negedge rst_n) begin if(!rst_n) begin sda_out 1b1; scl 1b1; // 其他信号初始化... end else begin case(current_state) START: begin if(cnt 0) sda_out 1b0; // 其他输出控制... end // 其他状态输出... endcase end end这种结构清晰地将状态存储、转移逻辑和输出逻辑分开大大提高了代码的可读性和可维护性。3. 四倍频时钟的工程实践3.1 为什么需要四倍频IIC协议要求在SCL高电平期间SDA保持稳定在SCL低电平时才能改变SDA。如果直接用IIC时钟频率来驱动FPGA设计很难精确控制SDA的变化时刻。四倍频时钟提供了足够的时间分辨率让我们能够在SCL的上升沿和下降沿前后精确控制SDA的变化。举个例子标准模式IIC时钟频率是100kHz周期10μs。如果用四倍频时钟400kHz周期2.5μs可以将每个IIC时钟周期分为四个阶段SCL低电平前半段可以安全改变SDASCL低电平后半段保持稳定SCL高电平前半段数据采样窗口SCL高电平后半段保持稳定3.2 时钟分频实现在FPGA中实现四倍频时钟通常采用计数器分频的方式。假设系统时钟是50MHz要实现400kHz的四倍频时钟对应100kHz的IIC时钟可以这样计算分频系数分频系数 系统时钟频率 / (IIC时钟频率 × 4) 50,000,000 / (100,000 × 4) 125Verilog实现代码parameter SYS_CLK 50_000_000; parameter I2C_CLK 100_000; localparam DIV_CNT_MAX SYS_CLK/(I2C_CLK*4); reg [15:0] clk_cnt; reg i2c_drive_clk; always (posedge sys_clk or negedge rst_n) begin if(!rst_n) begin clk_cnt 0; i2c_drive_clk 0; end else begin if(clk_cnt DIV_CNT_MAX-1) begin clk_cnt 0; i2c_drive_clk ~i2c_drive_clk; end else begin clk_cnt clk_cnt 1; end end end3.3 精确边沿控制有了四倍频时钟我们可以用计数器精确控制SCL和SDA的边沿。以起始信号为例case(clk4_cnt) 0: begin scl 1b1; sda_out 1b1; end // 空闲状态 1: sda_out 1b0; // SCL高电平时SDA下降沿 2: scl 1b0; // 拉低SCL // ...其他计数状态 endcase这种方法的优势在于边沿位置精确可控状态机可以同步在四倍频时钟的上升沿便于添加延时调整功能适应不同的从设备时序要求我在一个多厂商从设备的项目中就是通过调整四倍频时钟各阶段的占比实现了对不同从设备的兼容。有些传感器需要更长的数据保持时间有些则需要更快的响应速度。4. 完整IIC主控制器实现4.1 模块架构设计一个完整的IIC主控制器通常分为两个主要模块IIC接口模块实现IIC协议的状态机和物理层时序控制模块提供用户接口组织读写操作接口模块的主要信号包括module i2c_master ( input clk, input rst_n, input start, // 启动传输 input [6:0] addr, // 从机地址 input wr_rd, // 读写控制 input [7:0] data_in, // 写入数据 output [7:0] data_out,// 读取数据 output done, // 传输完成 output ack, // 应答信号 inout sda, // IIC数据线 output scl // IIC时钟线 );4.2 物理层实现细节SDA线的控制需要特别注意因为它是一个双向信号。在FPGA中通常这样实现reg sda_out; // SDA输出寄存器 reg sda_ctrl; // SDA输出使能 assign sda sda_ctrl ? sda_out : 1bz; // 输出或高阻 wire sda_in sda; // 输入采样在发送阶段主设备驱动SDAsda_ctrl 1b1; // 使能输出 sda_out data_bit;// 输出数据在接收阶段从设备驱动SDAsda_ctrl 1b0; // 禁用输出释放总线 data_bit sda_in; // 采样输入数据4.3 典型读写流程写一个字节到从设备的流程发送起始条件发送从机地址 写位(0)等待应答发送数据字节等待应答发送停止条件从从设备读取一个字节的流程发送起始条件发送从机地址 写位(0)虚写操作等待应答发送内存地址如果需要等待应答发送重复起始条件发送从机地址 读位(1)等待应答读取数据字节发送非应答(ACK1)发送停止条件4.4 错误处理机制健壮的IIC控制器需要包含错误处理机制常见的有超时检测如果从设备长时间不应答应放弃当前传输总线忙检测启动传输前检查总线是否空闲SCL和SDA都为高仲裁丢失处理在多主系统中如果检测到SDA与自身输出不一致应立即转为从模式实现超时检测的例子reg [15:0] timeout_cnt; always (posedge clk or negedge rst_n) begin if(!rst_n) begin timeout_cnt 0; end else begin if(current_state ! next_state) timeout_cnt 0; else if(timeout_cnt TIMEOUT_MAX) timeout_cnt timeout_cnt 1; end end wire timeout (timeout_cnt TIMEOUT_MAX);在实际项目中我发现添加这些错误处理机制虽然增加了设计复杂度但大大提高了系统的可靠性特别是在工业环境等干扰较大的场合。