1. IIC总线协议基础与FPGA实现概述
IIC(Inter-Integrated Circuit)总线是嵌入式系统中最常用的低速串行通信协议之一。作为一名FPGA开发者,我经常需要在项目中实现IIC主从设备接口。与SPI等协议相比,IIC最大的优势在于仅需两根信号线(SCL时钟线和SDA数据线)即可实现多设备通信,这在PCB布局和引脚资源紧张的场合尤为珍贵。
在Xilinx FPGA平台上实现IIC控制器时,我们通常面临几种选择:使用官方AXI IIC IP核、利用Zynq PS端的硬核控制器,或者直接手写RTL代码。每种方案各有优劣:
- AXI IIC IP核功能完善但资源消耗较大
- PS端硬核性能稳定但引脚分配受限
- RTL实现灵活度最高但开发周期较长
本文将重点分享如何用Verilog手写IIC主从控制器,这是我在多个工业项目中验证过的可靠方案。特别是在需要FPGA作为从设备与外部MCU通信,或者需要实现特殊IIC时序的场景下,RTL实现几乎是唯一选择。
2. IIC主机控制器设计详解
2.1 状态机架构设计
我设计的IIC主机控制器采用经典的状态机架构,包含11个主要状态:
verilog复制localparam IDLE = 4'd0; // 空闲状态
localparam START = 4'd1; // 起始信号
localparam SEND_DEV_ADDR = 4'd2; // 发送设备地址
localparam SEND_REG_ADDR_H = 4'd3; // 发送寄存器地址高字节
localparam SEND_REG_ADDR_L = 4'd4; // 发送寄存器地址低字节
localparam WRITE_DATA = 4'd5; // 写数据模式
localparam START_READ = 4'd6; // 读操作起始信号
localparam SEND_DEV_ADDR_R = 4'd7; // 读模式设备地址
localparam READ_DATA = 4'd8; // 读数据模式
localparam SEND_ACK = 4'd9; // 发送应答
localparam STOP = 4'd10; // 停止信号
状态转移完全遵循IIC协议规范。例如在写操作时,流程为:IDLE → START → SEND_DEV_ADDR → SEND_REG_ADDR_H → SEND_REG_ADDR_L → WRITE_DATA → STOP。
2.2 时钟生成与同步机制
IIC总线支持多种速度模式(标准模式100kHz,快速模式400kHz等)。本设计通过参数化配置实现灵活的时钟频率调整:
verilog复制parameter SYS_CLK_FREQ = 100_000_000; // 系统时钟100MHz
parameter IIC_CLK_FREQ = 400_000; // IIC时钟400kHz
localparam SCL_CNT_MAX = SYS_CLK_FREQ / IIC_CLK_FREQ; // 时钟分频系数
// SCL时钟生成
always @(posedge i_clk) begin
if (i_rst) begin
scl_cnt <= 16'd0;
o_scl <= 1'b1;
end else if (iic_clk_en) begin
if (scl_cnt == SCL_CNT_MAX - 1) begin
scl_cnt <= 16'd0;
o_scl <= ~o_scl;
end else begin
scl_cnt <= scl_cnt + 1'b1;
end
end
end
关键时序点(如SCL上升沿/下降沿)通过计数器比较实现精确控制:
verilog复制wire scl_low_mid = (scl_cnt == (SCL_CNT_MAX/4)); // 时钟低电平中点
wire scl_high_mid = (scl_cnt == (SCL_CNT_MAX/4)*3); // 时钟高电平中点
wire scl_fall = (scl_cnt == SCL_CNT_MAX - 1); // 下降沿位置
2.3 数据收发实现
数据发送采用移位寄存器结构,确保bit级精确控制:
verilog复制always @(posedge i_clk) begin
if (scl_low_mid) begin
if (bit_cnt < 8) begin
sda_out_en <= 1'b1;
sda_out_reg <= tx_shift_reg[7-bit_cnt]; // 高位先出
end else begin
sda_out_en <= 1'b0; // 第9个时钟释放SDA线
end
end
end
数据接收时,在SCL高电平中点采样保证稳定性:
verilog复制always @(posedge i_clk) begin
if (scl_high_mid && bit_cnt < 8) begin
rx_shift_reg[7-bit_cnt] <= sda_in; // 高位先入
end
end
2.4 地址与寄存器配置
本设计支持7位设备地址和8/16位寄存器地址配置:
verilog复制input [6:0] i_device_addr; // 7位设备地址
input [15:0] i_reg_addr; // 16位寄存器地址
input [1:0] i_addr_num; // 地址字节数选择(1或2)
地址发送状态机根据配置自动处理单/双字节地址:
verilog复制if (r_addr_num == 2) begin
state <= SEND_REG_ADDR_H; // 发送高字节地址
tx_shift_reg <= r_reg_addr[15:8];
end else begin
state <= SEND_REG_ADDR_L; // 单字节地址模式
tx_shift_reg <= r_reg_addr[7:0];
end
3. IIC从机实现与仿真验证
3.1 从机行为模型设计
为验证主机控制器,我开发了M24LC04B EEPROM的行为模型。这个模型实现了完整的IIC从机协议:
verilog复制module M24LC04B (A0, A1, A2, WP, SDA, SCL, RESET);
// 引脚定义...
// 存储器阵列
reg [7:0] MemoryBlock0 [0:255];
reg [7:0] MemoryBlock1 [0:255];
// 协议状态机
always @(negedge SDA) begin
if (SCL == 1) begin // 检测START条件
START_Rcvd <= 1;
// 状态初始化...
end
end
always @(posedge SCL) begin
// 数据移位寄存器
ShiftRegister <= {ShiftRegister[6:0], SDA};
end
endmodule
3.2 测试平台搭建
测试平台采用分层验证方法:
verilog复制module tb_iic_master();
// 时钟生成
initial i_clk = 0;
always #(`clk_period/2) i_clk = ~i_clk;
// 实例化DUT
iic_master u_iic_master(/* 端口连接 */);
// I2C上拉电阻模拟
pullup(o_sda);
pullup(o_scl);
// EEPROM模型实例化
M24LC04B U_M24LC04B(/* 引脚连接 */);
// 自动化测试任务
task wr_single_byte(input [15:0] addr, input [7:0] data);
begin
i_wr = 1;
i_reg_addr = addr;
i_wrdata = data;
#(`clk_period);
i_wr = 0;
@(posedge o_done);
end
endtask
endmodule
3.3 典型测试案例
- 单字节写操作测试:
verilog复制// 地址: 16'h0102, 数据: 8'hAA
wr_single_byte(16'h0102, 8'hAA);
波形验证点:
- START信号后正确发送设备地址(0xA1)
- 后续发送16位地址(0x0102)
- 最后发送数据字节(0xAA)
- 从机在每个字节后返回ACK
- 单字节读操作测试:
verilog复制rd_task(16'h0102);
验证点:
- 重复START信号正确生成
- 设备地址R/W位切换为读模式(0xA0→0xA1)
- 主机在最后一个字节发送NAK
4. 工程实践中的经验总结
4.1 时序收敛关键点
在FPGA实现中,IIC接口的时序收敛需要特别注意:
- 跨时钟域处理:
verilog复制// 异步复位同步释放
always @(posedge i_clk or posedge i_async_rst) begin
if (i_async_rst) begin
rst_meta <= 1'b1;
i_rst <= 1'b1;
end else begin
rst_meta <= 1'b0;
i_rst <= rst_meta;
end
end
- 输出延迟约束:
tcl复制set_output_delay -clock [get_clocks iic_clk] -max 2.0 [get_ports o_sda]
set_output_delay -clock [get_clocks iic_clk] -min 1.0 [get_ports o_sda]
4.2 常见问题排查
- ACK无响应问题:
- 检查设备地址是否匹配(包括R/W位)
- 确认上拉电阻值合适(通常4.7kΩ)
- 用示波器检查信号完整性
- 时钟拉伸超时:
verilog复制// 添加超时计数器
always @(posedge i_clk) begin
if (scl_stretched) begin
stretch_timeout <= stretch_timeout + 1;
if (stretch_timeout > TIMEOUT_VALUE) begin
// 错误处理
end
end else begin
stretch_timeout <= 0;
end
end
- 多主机冲突处理:
verilog复制// 仲裁检测逻辑
wire arbitration_lost = (sda_out_reg && !sda_in);
if (arbitration_lost) begin
state <= IDLE;
o_arbitration_lost <= 1'b1;
end
5. 性能优化技巧
- 流水线化设计:
verilog复制// 预取下一个数据字节
always @(posedge i_clk) begin
if (o_wrdata_done && (byte_cnt < r_wrdata_num-1)) begin
tx_shift_reg <= wr_fifo[byte_cnt+1];
end
end
- 时钟拉伸优化:
verilog复制// 动态时钟调整
always @(posedge i_clk) begin
if (scl_stretched) begin
scl_cnt <= scl_cnt; // 暂停计数器
end
end
- 批量传输支持:
verilog复制// 增加FIFO接口
input [7:0] wr_fifo_data;
input wr_fifo_valid;
output wr_fifo_ready;
assign wr_fifo_ready = (state == WRITE_DATA) && (bit_cnt == 3);
在实际项目中,这个IIC控制器已经成功应用于多个工业场景,包括:
- 传感器数据采集(温度、湿度传感器)
- FPGA配置管理(通过IIC接口配置时钟芯片)
- 设备状态监控(读取电源管理芯片寄存器)
这种RTL实现方式最大的优势在于其极低的延迟(纳秒级响应)和完全的时序可控性,特别适合对实时性要求高的工业控制场景。