1. IIC协议与FPGA应用场景
IIC(Inter-Integrated Circuit)作为Philips(现NXP)推出的两线制串行通信协议,在嵌入式领域已有30多年历史。你可能在树莓派上用过它读取温湿度传感器数据,或者在Arduino项目里配置过OLED屏幕。但用FPGA实现IIC主控制器完全是另一种体验——就像从开自动挡汽车突然变成手动挡赛车手,所有时序细节都需要你精确掌控。
在FPGA项目中,IIC主控制器最常见的应用场景包括:
- 多传感器管理系统:比如同时读取16路ADC芯片ADS1115的环境监测系统
- 非易失性存储阵列:使用24LC256系列EEPROM构建分布式配置存储器
- RTC时钟同步:为整个系统提供精确时间基准
- IO扩展器控制:通过PCA9538等芯片实现GPIO扩展
传统MCU方案受限于固定硬件IIC控制器,遇到多字节地址设备(如16位地址的EEPROM)或需要突发传输时,往往要依赖软件模拟,效率低下。而FPGA实现的参数化IIC控制器可以:
- 硬件级适配不同地址宽度(8/16/24位)
- 动态调整传输速率(标准/快速/高速模式)
- 支持连续读写时的自动地址递增
- 实现真正的并行多设备管理
2. 参数化设计核心思路
2.1 模块接口定义
先看这个支持参数化的模块声明:
module iic_master #( parameter SYS_CLK = 50_000_000, // 输入时钟频率 parameter IIC_CLK = 100_000, // IIC目标时钟频率 parameter ADDR_LEN = 2, // 地址字节数(1/2/3) parameter DATA_LEN = 1 // 单次传输数据字节数 )( input wire clk, input wire rst_n, // 用户接口 input wire start, input wire rw, // 0:写 1:读 input wire [6:0] dev_addr, input wire [23:0] reg_addr, // 自动根据ADDR_LEN截取 input wire [7:0] data_in, output reg [7:0] data_out, output reg done, // 物理接口 inout sda, output reg scl );关键参数说明:
- ADDR_LEN:动态配置地址长度,1字节时使用reg_addr[7:0],2字节时用reg_addr[15:0]
- DATA_LEN:突发传输长度,实现自动地址递增时,每次传输后内部地址寄存器+1
- IIC_CLK:通过系统时钟分频实现精确的SCL时序控制
2.2 状态机设计精髓
IIC协议本质上是典型的状态机应用场景。我们的设计采用三段式状态机结构:
localparam [3:0] IDLE = 4'd0, START = 4'd1, SEND_ADDR= 4'd2, ACK1 = 4'd3, SEND_REG = 4'd4, ACK2 = 4'd5, WR_DATA = 4'd6, RD_DATA = 4'd7, STOP = 4'd8; always @(posedge clk or negedge rst_n) begin if(!rst_n) begin state <= IDLE; // 其他寄存器复位 end else begin case(state) IDLE: if(start) state <= START; START: if(scl_edge) state <= SEND_ADDR; // 其他状态转移... endcase end end状态机设计技巧:
- 每个状态保持至少一个完整的SCL周期
- 在SCL低电平期间改变SDA信号(建立时间满足)
- 使用边沿检测信号scl_edge协调状态转移
- 对ACK/NACK处理增加超时保护
3. 多字节地址实现细节
3.1 地址自动装配机制
对于24LC512这类16位地址的EEPROM,需要发送两个地址字节。我们的参数化设计通过ADDR_LEN参数自动处理:
// 地址字节选择逻辑 wire [7:0] addr_byte; assign addr_byte = (addr_cnt == 0) ? reg_addr[15:8] : (addr_cnt == 1) ? reg_addr[7:0] : 8'h00; // 地址计数器控制 always @(posedge clk) begin if(state == SEND_ADDR && bit_cnt == 7 && scl_neg) addr_cnt <= addr_cnt + 1; else if(state == IDLE) addr_cnt <= 0; end3.2 突发读写实现
突发传输时,DATA_LEN>1会启用自动地址递增模式。以写操作为例:
// 数据写入流程 always @(posedge clk) begin if(state == WR_DATA && bit_cnt == 7 && scl_neg) begin if(data_cnt < DATA_LEN-1) begin data_cnt <= data_cnt + 1; reg_addr_int <= reg_addr_int + 1; // 地址自增 end end else if(state == IDLE) begin data_cnt <= 0; reg_addr_int <= reg_addr; // 初始地址加载 end end关键点:
- 内部维护reg_addr_int实现地址递增
- 每个数据字节传输完成后更新地址
- 最后字节传输后不发送地址递增
4. 时序精确控制技术
4.1 SCL时钟生成
精确的时钟控制是IIC协议的关键。我们采用计数器分频方案:
// SCL时钟分频计算 localparam DIVIDER = SYS_CLK / (IIC_CLK * 2); reg [15:0] scl_cnt; wire scl_pos = (scl_cnt == DIVIDER-1); wire scl_neg = (scl_cnt == DIVIDER*2-1); always @(posedge clk) begin if(state == IDLE) begin scl_cnt <= 0; scl <= 1'b1; end else begin if(scl_pos) scl <= 1'b0; else if(scl_neg) scl <= 1'b1; if(scl_cnt == DIVIDER*2-1) scl_cnt <= 0; else scl_cnt <= scl_cnt + 1; end end4.2 建立/保持时间保障
根据IIC规范:
- 数据建立时间(tSU:DAT) > 100ns(标准模式)
- 数据保持时间(tHD:DAT) > 0ns
我们的实现方案:
// SDA数据变化时刻控制 always @(posedge clk) begin if(scl_neg && !scl) begin // SCL低电平中点改变SDA sda_out <= next_bit; end end // 输入采样时刻 always @(posedge clk) begin if(scl_pos && scl) begin // SCL高电平中点采样SDA sda_in <= sda; end end5. 实测优化经验分享
在实际项目中使用这个IIC控制器时,有几个容易踩的坑:
上拉电阻选择:
- 标准模式(100kHz)建议4.7kΩ
- 快速模式(400kHz)建议2.2kΩ
- 线缆较长时需要减小阻值
跨时钟域处理:
// 用户接口到IIC时钟域的同步 reg [1:0] start_sync; always @(posedge clk) start_sync <= {start_sync[0], user_start}; wire start_rise = (start_sync == 2'b01);异常恢复机制:
- 增加看门狗计数器检测总线挂死
- 超时后主动发送STOP条件复位总线
- 状态机增加ERROR状态进行恢复
多主竞争处理:
// 总线仲裁检测 wire arbitration_lost = (sda_out == 1'b1) && (sda_in == 1'b0); always @(posedge clk) begin if(arbitration_lost) state <= IDLE; // 立即释放总线 end
这个设计在Xilinx Artix-7平台上实测,可以稳定驱动:
- 24系列EEPROM(16位地址)
- ADXL345加速度计(8位地址)
- MCP4725 DAC(支持连续写)
- 多设备混合连接场景
完整代码已通过仿真验证,包含完整的Testbench测试用例,支持随机化测试和时序检查。在实际部署时,建议根据具体设备特性调整以下参数:
- tHD:STA(起始条件保持时间)
- tSU:STO(停止条件建立时间)
- tBUF(总线空闲时间)