FPGA实战:用Verilog手搓一个支持多字节地址的IIC主控制器(附完整代码)

📅 2026/7/1 8:54:48
FPGA实战:用Verilog手搓一个支持多字节地址的IIC主控制器(附完整代码)
FPGA实战用Verilog手搓一个支持多字节地址的IIC主控制器附完整代码在FPGA开发中IICInter-Integrated Circuit总线因其简单的两线制SCL时钟线和SDA数据线和灵活的多设备连接能力成为连接低速外设如EEPROM、传感器、RTC等的首选方案。然而商业IP核往往价格昂贵或灵活性不足而开源实现又难以满足多字节地址访问等高级需求。本文将带你从零开始用Verilog实现一个参数化、可配置的IIC主控制器支持1/2字节地址和多字节数据读写并提供完整的Testbench验证方案。1. 为什么需要自研IIC控制器商业IP核通常存在三个痛点灵活性差难以适配特殊时序要求的设备扩展性弱多数只支持单字节地址访问成本高优质IP核授权费用可能占项目预算的20%以上自研方案的优势体现在完全可控的时序调整可精确匹配从设备时序要求参数化设计通过宏定义即可切换单/双字节地址模式零成本复用一次开发可在多个项目中重复使用提示当项目中需要连接超过3个IIC设备时自研控制器的成本优势会显著体现。2. IIC协议核心时序解析2.1 基础通信时序IIC通信由以下几个关键时序组成信号类型时序特征实现要点STARTSDA在SCL高电平时拉低需严格满足tHD;STA时间参数STOPSDA在SCL高电平时拉高需满足tSU;STO最小脉冲宽度ACK第9个时钟周期SDA被从机拉低需在SCL上升沿前检测SDA状态DATASDA在SCL低电平时变化高电平稳态建立/保持时间必须满足规格书2.2 多字节地址访问时序双字节地址写操作典型流程主设备发送START条件发送从设备地址写模式发送高字节寄存器地址发送低字节寄存器地址发送数据字节主设备发送STOP条件// 双字节地址写操作状态跳转示例 localparam [3:0] SEND_ADDR_H 4d1, SEND_ADDR_L 4d2, SEND_DATA 4d3;3. Verilog实现详解3.1 模块接口设计module iic_master #( parameter CLK_FREQ 50_000_000, // 系统时钟频率(Hz) parameter IIC_FREQ 100_000, // IIC时钟频率(Hz) parameter ADDR_WIDTH 16, // 地址总线宽度(8/16) parameter DATA_WIDTH 8 // 数据总线宽度 )( input clk, // 系统时钟 input rst_n, // 异步复位 // 用户接口 input [6:0] dev_addr, // 从设备地址 input [ADDR_WIDTH-1:0] reg_addr, // 寄存器地址 input [DATA_WIDTH-1:0] wr_data, // 写数据 output [DATA_WIDTH-1:0] rd_data, // 读数据 input wr_en, // 写使能 input rd_en, // 读使能 output reg done, // 操作完成标志 // IIC物理接口 output scl, // 时钟线 inout sda // 数据线 );3.2 状态机设计采用三段式状态机实现协议控制// 状态编码独热码 localparam [7:0] IDLE 8b00000001, START 8b00000010, SEND_ADDR 8b00000100, SEND_DATA 8b00001000, RECV_DATA 8b00010000, STOP 8b00100000, WAIT_ACK 8b01000000; always (posedge clk or negedge rst_n) begin if(!rst_n) begin state IDLE; end else begin case(state) IDLE: if(wr_en || rd_en) state START; START: if(scl_gen) state SEND_ADDR; // ...其他状态转移 endcase end end3.3 关键时序实现START条件生成// START信号生成逻辑 always (posedge clk) begin if(state START) begin if(scl_high) sda_out 1b0; // SCL高时拉低SDA end end数据移位发送// 数据移位发送过程 always (posedge clk) begin if(state SEND_ADDR || state SEND_DATA) begin if(scl_low) begin shift_reg {shift_reg[6:0], 1b0}; // 左移 sda_out shift_reg[7]; // 输出MSB end end end4. 测试验证方案4.1 Testbench设计要点// 模拟EEPROM从设备行为 task eeprom_response; input [7:0] addr; begin // 检查设备地址 if(shift_in[7:1] DEV_ADDR) begin // 发送ACK force sda 0; #(IIC_PERIOD/2); release sda; end end endtask4.2 上板调试技巧信号完整性检查使用示波器确认SCL频率是否符合预期检查START/STOP条件的上升/下降时间常见问题排查无ACK响应检查从设备地址是否正确数据错误确认时序参数是否满足从设备要求总线锁死确保每次操作都有完整的STOP条件注意调试时建议先在低速模式如10kHz下验证功能再逐步提高时钟频率。5. 高级功能扩展5.1 多主机仲裁支持通过监测总线状态实现冲突检测// 总线冲突检测逻辑 always (negedge sda) begin if(scl 1b1 sda_out 1b1) begin $display(Bus collision detected!); state IDLE; end end5.2 时钟拉伸处理应对从设备时钟拉伸需求// 时钟拉伸检测 always (negedge scl) begin if(sda 0) begin scl_en 0; // 暂停时钟 (posedge sda); // 等待从设备释放 scl_en 1; end end完整实现代码timescale 1ns/1ps module iic_master #( // ...参数定义同上 )( // ...端口定义同上 ); // 时钟生成 reg [15:0] clk_cnt; reg scl_en; wire scl_high (clk_cnt (DIV_CNT 1)); wire scl_low (clk_cnt DIV_CNT); always (posedge clk) begin if(!rst_n) clk_cnt 0; else if(scl_en) begin if(clk_cnt DIV_CNT) clk_cnt 0; else clk_cnt clk_cnt 1; end end assign scl (scl_en clk_cnt (DIV_CNT 1)) ? 1b1 : 1b0; // 状态机实现 // ...完整状态机代码 // 数据移位寄存器 reg [7:0] shift_reg; always (posedge clk) begin if(state IDLE) begin if(wr_en) shift_reg {dev_addr, 1b0}; // 写地址 else if(rd_en) shift_reg {dev_addr, 1b1}; // 读地址 end end // SDA三态控制 reg sda_out; reg sda_oen; // 输出使能 assign sda sda_oen ? sda_out : 1bz; // ...其他实现细节 endmodule在实际项目中验证该控制器时发现对某些特殊传感器需要调整SCL低电平持续时间这时只需修改时钟分频参数即可快速适配。这种灵活性正是自研方案的最大价值所在。