https://pan.baidu.com/s/1rDsLAXGj8WbX82teSkhuIw?pwd1234这份FPGA 系统学习详细资料包是个人花大量时间精心整理的超多干货全覆盖从基础到实战一站式搞定不用再到处薅资料网盘链接随时可能失效提取码 1234先保存再学习别等失效拍大腿链接https://pan.baidu.com/s/1rDsLAXGj8WbX82teSkhuIw?pwd1234————————————————嵌入式高频接口FPGA I2C 总线从机/主机模式开发实战I2CInter-Integrated Circuit总线是嵌入式系统中使用最广泛的串行接口之一仅需两根信号线SDA和SCL就能挂载多个设备。与SPI相比I2C支持多主多从、具有总线仲裁机制非常适合板级设备间的配置和状态读取。在FPGA开发中我们经常需要实现自定义的I2C从机如模拟传感器、配置寄存器或主机如控制ADC/DAC、读取EEPROM。下面我将从实战角度详细讲解如何在FPGA上实现可靠、高效的I2C控制器。1. I2C协议核心要点——先理解物理层在动手写代码前必须理解I2C的物理层特性这是与SPI最大的不同点。1.1 开漏结构与上拉电阻I2C的两条线SDA数据线和SCL时钟线都是开漏Open-Drain结构通过外部上拉电阻连接到电源逻辑0设备主动将线路拉低逻辑1设备释放线路不驱动由上拉电阻拉高这个特性决定了多个设备可以“线与”连接——只要有一个设备拉低总线就是低电平FPGA的普通IO不能直接驱动I2C总线必须使用三态缓冲器实现开漏输出FPGA中实现开漏输出的Verilog代码// 使用三态缓冲器实现I2C开漏引脚 assign sda_io (sda_oe_n) ? 1bz : 1b0; // oe_n0时拉低1时释放 assign sda_i sda_io; // 输入采样 // 或者使用原语以Intel FPGA为例 gpio sda_iobuf ( .din(1b0), // 输出数据固定为0通过OE控制拉低 .oe(sda_oe), // 输出使能1-驱动低电平0-高阻 .dout(sda_in), // 输入到FPGA内部 .pad_io(fpga_i2c_sda) // 连接到FPGA引脚 );板级设计要点必须在SDA和SCL引脚外接上拉电阻通常1kΩ~10kΩ阻值根据总线电容和通信速率选择。1.2 时序基础起始、停止、数据I2C通信由以下基本信号组成信号类型条件SCL高电平时SDA的变化说明起始位STARTSDA由高→低主机发起通信占用总线停止位STOPSDA由低→高主机释放总线数据位SDA在SCL低电平时变化高电平时保持每字节8位MSB先发应答位ACK第9个SCL周期接收方拉低SDA表示成功接收关键点数据在SCL高电平期间必须稳定只有在SCL低电平时才能改变SDA。这是调试时最容易出错的地方。1.3 帧结构地址数据一次完整的I2C传输包含起始位START7位从机地址 读/写位R/W0写1读应答位ACK from 从机数据字节每字节后跟ACK停止位STOP重复起始Repeated Start在不发送停止位的情况下再次发送起始位用于改变传输方向如先写寄存器地址再读数据。2. I2C主机模式设计主机负责产生时钟、发起通信。设计核心是一个主控状态机和一个时钟分频器。2.1 模块架构一个完整的主机控制器包括时钟生成器将系统时钟分频产生SCL主状态机控制起始、停止、数据发送/接收移位寄存器并串/串并转换应答检测检查从机的ACK信号2.2 状态机设计核心下面是一个支持多字节读写的主机状态机typedef enum logic [3:0] { IDLE, // 空闲等待命令 START, // 发送起始条件 SEND_ADDR, // 发送地址读写位 SEND_DATA, // 发送数据字节 SEND_ACK, // 发送应答位读操作时 RECV_DATA, // 接收数据字节 RECV_ACK, // 接收应答位写操作时 STOP // 发送停止条件 } state_t; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; scl 1b1; // 空闲时SCL高 sda_oe 1b0; // 释放SDA end else begin case (state) IDLE: begin if (start_cmd) begin bit_cnt 0; state START; end end START: begin // 起始条件SCL高时SDA由高拉低 sda_oe 1b1; // 拉低SDA state SEND_ADDR; // 延时等待 end SEND_ADDR: begin // 发送地址读写位8位MSB first if (scl_low) begin // SCL低电平时改变数据 sda_oe addr[bit_cnt] ? 1b0 : 1b1; // 1释放0拉低 end if (scl_high) begin // SCL高电平时计数 if (bit_cnt 7) begin bit_cnt 0; state RECV_ACK; // 等待应答 end else begin bit_cnt bit_cnt 1; end end end RECV_ACK: begin // 第9个时钟周期主机释放SDA采样从机应答 sda_oe 1b0; // 释放SDA if (scl_high) begin if (sda_i 1b0) begin // ACK received if (rw_mode WRITE) begin state SEND_DATA; end else begin state RECV_DATA; end end else begin // NACK state STOP; // 无应答结束传输 end end end SEND_DATA: begin // 发送数据字节类似SEND_ADDR // ... end RECV_DATA: begin // 接收数据字节 if (scl_high) begin shift_reg {shift_reg[6:0], sda_i}; // 采样数据 end if (bit_cnt 7 scl_low) begin // 一字节接收完成发送ACK sda_oe (next_byte) ? 1b1 : 1b0; // 1拉低ACK0NACK state (next_byte) ? RECV_DATA : STOP; end end STOP: begin // 停止条件SCL高时SDA由低拉高 sda_oe 1b0; // 释放SDA state IDLE; end endcase end end2.3 时钟分频计算主机必须产生符合规范的SCL时钟。通常采用计数器分频localparam CLK_DIV (SYS_CLK_FREQ / (I2C_FREQ * 4)) - 1; // 乘4是因为每个SCL周期需要4个时钟节拍高半周期、低半周期常用I2C速率标准模式100kHz快速模式400kHz高速模式3.4MHzFPGA实现较少2.4 应答错误处理当主机发送地址或数据后如果从机未应答SDA保持高应视为错误并终止传输// 在RECV_ACK状态中检测到NACK时 if (sda_i 1b1) begin // NACK ack_error 1b1; // 置位错误标志 state STOP; // 发送停止位结束传输 end3. I2C从机模式设计从机不能产生时钟必须实时监测SCL和SDA的变化并适时响应。3.1 边沿检测与同步从机设计的关键是正确检测起始/停止条件和数据采样// 对SCL和SDA进行两级同步消除亚稳态 always (posedge clk) begin scl_sync1 scl_io; scl_sync2 scl_sync1; sda_sync1 sda_io; sda_sync2 sda_sync1; // 边沿检测 scl_negedge scl_sync2 !scl_sync1; // 下降沿 scl_posedge !scl_sync2 scl_sync1; // 上升沿 // 起始条件SCL高时SDA下降沿 start_detect scl_sync2 scl_sync1 sda_sync1 !sda_sync2; // 停止条件SCL高时SDA上升沿 stop_detect scl_sync2 scl_sync1 !sda_sync1 sda_sync2; end3.2 从机状态机从机状态机相对简单主要响应主机的读写请求typedef enum logic [2:0] { IDLE, // 空闲等待起始 GET_ADDR, // 接收地址读写位 ACK_ADDR, // 地址匹配发送ACK WRITE_DATA, // 接收写入数据 READ_DATA, // 发送读取数据 WAIT_ACK // 等待主机应答读完成后 } slave_state_t; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; sda_oe 1b0; // 释放SDA end else begin case (state) IDLE: begin if (start_detect) begin state GET_ADDR; bit_cnt 0; end end GET_ADDR: begin if (scl_posedge bit_cnt 8) begin // 在SCL上升沿采样地址位7位地址1位R/W addr_reg[7-bit_cnt] sda_sync2; bit_cnt bit_cnt 1; if (bit_cnt 7) begin // 第8位是R/W rw_bit sda_sync2; end end if (bit_cnt 8) begin // 8位接收完毕检查地址是否匹配 if (addr_reg[6:0] MY_ADDR) begin state ACK_ADDR; end else begin state IDLE; // 地址不匹配忽略 end end end ACK_ADDR: begin // 在第9个SCL周期发送ACK sda_oe 1b1; // 拉低SDA发送ACK if (scl_negedge) begin // 在SCL下降沿后释放SDA sda_oe 1b0; if (rw_bit 1b0) begin // 主机写 state WRITE_DATA; bit_cnt 0; end else begin // 主机读 state READ_DATA; bit_cnt 0; // 准备要发送的数据 shift_reg data_to_send; end end end WRITE_DATA: begin // 接收主机写入的数据 if (scl_posedge) begin data_reg[7-bit_cnt] sda_sync2; bit_cnt bit_cnt 1; end if (bit_cnt 8) begin state SEND_ACK; // 数据接收完发送ACK end end READ_DATA: begin // 向主机发送数据 sda_oe shift_reg[7] ? 1b0 : 1b1; // 1释放0拉低 if (scl_negedge) begin shift_reg {shift_reg[6:0], 1b0}; // 移位 bit_cnt bit_cnt 1; end if (bit_cnt 8) begin state WAIT_ACK; // 等待主机应答 end end WAIT_ACK: begin // 释放SDA检测主机的ACK/NACK sda_oe 1b0; if (scl_posedge) begin if (sda_sync2 1b0) begin // 主机ACK继续读 state READ_DATA; bit_cnt 0; // 准备下一字节 shift_reg next_data; end else begin // 主机NACK结束 state IDLE; end end end endcase end end3.3 用户接口设计从机需要与应用逻辑交换数据。常用接口定义写操作write_validwrite_addrwrite_data读操作read_requestread_addrread_data// 从机用户接口示例 i2c_slave #( .SLAVE_ADDR(7h50) // 从机地址 ) u_slave ( .clk(clk), .rst_n(rst_n), // I2C总线 .scl(scl_io), .sda(sda_io), // 用户接口 .write_en(write_en), .write_addr(write_addr), // 寄存器地址 .write_data(write_data), .read_req(read_req), .read_addr(read_addr), .read_data(read_data_from_user) );4. 关键特性时钟拉伸Clock Stretching时钟拉伸是I2C从机的一个重要功能当从机需要额外时间处理数据时可以拉低SCL线迫使主机等待。4.1 什么时候需要时钟拉伸从机是慢速器件如EEPROM写入需要时间从机需要准备数据如从内部存储器读取从机需要处理中断4.2 FPGA从机实现时钟拉伸在从机设计中可以在需要时拉低SCL// 在需要延时的时刻拉低SCL always (posedge clk) begin if (need_stretch) begin scl_oe 1b1; // 拉低SCL stretch_cnt stretch_cnt 1; if (stretch_cnt STRETCH_CYCLES) begin scl_oe 1b0; // 释放SCL need_stretch 1b0; end end end4.3 主机处理时钟拉伸作为主机必须能够处理从机的时钟拉伸——当检测到SCL被拉低时必须等待直到SCL被释放// 主机状态机中在改变SCL前检查是否被从机拉低 always (posedge clk) begin if (scl_release_needed) begin if (scl_i 1b1) begin // 从机已释放SCL scl_oe 1b0; // 主机开始驱动 scl_release_needed 1b0; end // 否则继续等待 end end5. 多从机系统设计要点5.1 地址冲突预防在同一I2C总线上每个从机必须有唯一的7位地址。如果使用相同型号的器件检查其地址引脚A0/A1/A2是否可配置。5.2 总线仲裁在多主机系统中当多个主机同时尝试控制总线时需要进行仲裁。仲裁原则谁先拉低SDA谁就赢得仲裁失去仲裁的主机必须立即释放总线FPGA实现仲裁逻辑较复杂通常建议采用单主模式避免多主冲突。5.3 总线电容限制I2C规范规定总线电容不能超过400pF。当挂载多个从机时如果走线过长需要降低通信速率如从400kHz降至100kHz使用更强的上拉电阻但不能太小否则功耗大增加I2C总线缓冲器/中继器6. 实战案例24C02 EEPROM读写6.1 24C02特点容量256字节2Kbit器件地址1010 A2 A1 A0 R/WA0~A2通常接地地址为7’h50页写一次最多写8字节随机读需要先写地址再读数据6.2 写操作流程单字节主机发送START主机发送器件地址 W0xA0等待从机ACK主机发送内存地址1字节等待从机ACK主机发送数据字节等待从机ACK主机发送STOP6.3 读操作流程随机读主机发送START主机发送器件地址 W0xA0等待从机ACK主机发送内存地址等待从机ACK主机发送重复起始Repeated START主机发送器件地址 R0xA1等待从机ACK主机接收数据字节主机发送NACK表示不再读主机发送STOP6.4 关键代码重复起始的实现// 在主机状态机中支持重复起始 always (posedge clk) begin if (need_repeated_start) begin // 在接收完ACK后不发送STOP直接再发送START state START; need_repeated_start 1b0; end end7. 调试技巧与常见问题7.1 示波器/逻辑分析仪必测点SCL波形频率是否正确是否有毛刺SDA与SCL的时序关系数据是否在SCL低电平时变化起始/停止条件是否满足时序要求7.2 常见问题排查现象可能原因解决方法无ACK响应从机地址错误/未上电检查从机地址和电源用示波器测SDA是否被拉低数据错位模式错误MSB/LSBI2C必须是MSB first检查移位方向偶尔通信失败时钟拉伸未处理主机需支持从机拉低SCL时的等待总线死锁SDA一直被拉低检查是否有从机故障复位总线高速下失败上拉电阻太小/太大调整上拉电阻1k~10k降低速率7.3 FPGA实现要点输入同步所有输入信号必须经过两级同步器三态控制正确实现开漏输出避免同时驱动状态机鲁棒性所有状态都有超时保护防止死锁8. 总结FPGA实现I2C主机和从机需要掌握的核心要点方面主机模式从机模式核心任务产生时钟、发起通信响应地址、被动传输关键模块时钟分频器、主状态机边沿检测、地址比较难点多字节传输、重复起始时钟拉伸、实时响应调试重点SCL频率准确、ACK检测起始/停止检测、数据采样工业级设计建议物理层正确使用开漏缓冲器外部上拉电阻协议层完整实现起始、停止、重复起始、ACK/NACK健壮性支持时钟拉伸处理从机无响应可配置性参数化地址、速率、数据宽度当你掌握了I2C主机和从机的完整设计你就具备了与绝大多数板级外设EEPROM、传感器、电源管理芯片、ADC/DAC进行可靠通信的能力这是嵌入式FPGA工程师的核心技能之一。