尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

AXI_IIC调试实战:从寄存器映射到FIFO设计的稳定性优化

AXI_IIC调试实战:从寄存器映射到FIFO设计的稳定性优化 1. 为什么AXI_IIC的调试总在最后一公里翻车做过FPGA嵌入式开发的人大概都有这种体验IIC协议本身并不复杂两根线、几个时序状态理论上半天就能把时序图吃透。可一旦把IIC挂到AXI总线上用软核或者硬核处理器去访问事情就变得微妙起来——寄存器读写偶尔成功、偶尔失败读EEPROM时数据错位FIFO深度设小了丢数据设大了又占资源。更让人头疼的是这类问题往往不是完全不通而是时好时坏调试起来像在抓幽灵。这篇内容围绕AXI_IIC调试实战展开核心是把我从反复失败到最终稳定读写的一整套经验拆开讲清楚。涉及的关键点包括AXI4-Lite总线与IIC控制器之间的寄存器映射关系、IIC时序中的起始/停止/应答位处理、EEPROM页写与随机读的地址机制、FIFO在收发路径上的深度设计以及调试过程中如何用寄存器回读和示波器交叉验证定位问题。适合正在做FPGA IIC控制器开发、用软核访问EEPROM、或者被AXI外设调试卡住的工程师参考无论你是刚接触AXI总线的新手还是已经能跑通Demo但稳定性不够的老手都能从中找到对应的排查思路。我最初的做法很教科书照着IIC时序图写状态机把SCL和SDA的拉高拉低按部就班实现然后挂到AXI总线上用处理器写控制寄存器启动传输。仿真波形看着完美上板之后却频繁出错。后来才意识到仿真通过不等于硬件可靠AXI总线的握手时序、IIC的开漏输出特性、EEPROM的写周期时间这三者之间的配合才是真正的难点。下面我按实际调试的顺序把每个环节的坑和解决方案逐一展开。2. AXI4-Lite寄存器映射别让地址偏移毁掉整个控制器2.1 寄存器规划决定了调试的难易程度AXI_IIC控制器的第一步不是写IIC状态机而是设计寄存器映射。很多人觉得寄存器随便分配几个地址就行实际上寄存器规划直接决定了后续调试的效率。我的建议是把寄存器分成四类控制类启动、停止、使能、状态类忙标志、应答错误、传输完成、数据类发送FIFO写入、接收FIFO读出、配置类时钟分频、从机地址、传输长度。每一类寄存器占一个32位字地址按4字节递增。比如基地址0x43C00000那么偏移地址寄存器名称功能说明读写属性0x00CTRL启动传输、使能IIC读写0x04STATUS忙标志、ACK错误、完成标志只读0x08TX_DATA写入待发送数据只写0x0CRX_DATA读出接收数据只读0x10CLK_DIVSCL时钟分频系数读写0x14SLAVE_ADDR从机地址7位读写0x18TX_LEN本次传输字节数读写这个规划看起来简单但我在实际调试中踩过一个坑地址偏移算错了一位。当时把TX_DATA放在0x08RX_DATA放在0x0A结果处理器按32位访问时地址对不齐读出来的数据全是乱的。AXI4-Lite要求所有寄存器按字对齐偏移必须是4的倍数这一点在写Verilog地址译码时就要严格保证。2.2 AXI握手信号里最容易忽略的细节AXI4-Lite的写通道有五个信号AWADDR、AWVALID、AWREADY、WDATA、WVALID、WREADY再加上响应通道BRESP和BVALID。读通道类似。很多人写从机接口时只关注数据对不对忽略了VALID和READY的握手必须同时有效才能完成一次传输。我遇到过一个典型问题写寄存器时数据偶尔写不进去。用逻辑分析仪抓AXI波形发现AWVALID拉高后我的从机在下一个周期才拉高AWREADY但此时主机已经把WDATA撤掉了。原因是我把AWREADY和WREADY分开处理没有做联合握手。正确的做法是当AWVALID和WVALID同时有效时才拉高对应的READY信号或者用一个小的状态机确保地址和数据都被锁存后再返回响应。// AXI4-Lite写通道握手简化逻辑 always (posedge aclk) begin if (!aresetn) begin awready 1b0; wready 1b0; bvalid 1b0; end else begin // 地址和数据都有效时才接收 if (awvalid wvalid !bvalid) begin awready 1b1; wready 1b1; // 锁存地址和数据 reg_addr awaddr; reg_data wdata; bvalid 1b1; end else begin awready 1b0; wready 1b0; if (bvalid) bvalid 1b0; end end end这段代码的关键点是地址和数据必须同时有效才完成一次写操作响应信号在下一个周期返回。如果你分开处理主机可能在地址握手完成后就认为写操作结束导致数据丢失。2.3 寄存器回读是调试的第一把钥匙在调试IIC之前先确保AXI寄存器本身读写正常。我的做法是写一个已知值到CTRL寄存器然后立刻读回来比对。如果读回值不对问题一定在AXI接口层跟IIC无关。这个步骤能帮你快速隔离问题——先证明总线通再证明IIC通。实测中我发现有些软核处理器的AXI互联模块会对未对齐访问做拆分导致一个32位写变成两个16位写如果你的从机没有处理这种拆分寄存器值就会错。解决办法是在地址译码时严格判断AWADDR的低两位是否为0非对齐访问直接返回SLVERR响应。3. IIC时序状态机从能跑到稳定的关键改造3.1 起始和停止条件的时序余量IIC的起始条件是SCL高电平时SDA从高变低停止条件是SCL高电平时SDA从低变高。听起来简单但实际实现时SDA的变化必须避开SCL的高电平窗口否则会被误判为起始或停止。我最初的状态机在SCL拉高后立刻改变SDA仿真时因为延迟模型理想看起来没问题。上板后因为SDA线上的上拉电阻和寄生电容SDA变化有延迟导致SCL高电平期间SDA还在跳变EEPROM直接不响应。后来我在状态机里加了半个SCL周期的保持时间SCL拉高后先等一段时间确保SDA稳定再改变SDA。具体做法是把SCL的时钟分频系数设大一些比如系统时钟100MHzSCL目标频率100kHz分频系数就是500。然后在状态机里用计数器控制每个SCL相位的时间确保SDA的变化发生在SCL低电平期间。3.2 应答位采样为什么你的ACK总是读错IIC每传输一个字节后接收方要拉低SDA表示应答。主机在发送完8位数据后需要释放SDA设为高阻态然后在第9个SCL周期采样SDA。如果SDA为低表示ACK为高表示NACK。我遇到的问题是ACK采样时机不对。最初我在SCL上升沿采样但此时SDA可能还没稳定。正确的做法是在SCL高电平的中间时刻采样或者用SCL的下降沿锁存SDA值。因为IIC协议规定SDA在SCL低电平期间变化在SCL高电平期间必须稳定。// ACK采样逻辑 always (posedge clk) begin if (scl_phase PHASE_ACK_SAMPLE) begin ack_bit sda_in; // 在SCL高电平中间采样 end end另外SDA必须做成三态输出。发送时驱动接收时释放为高阻。如果你一直驱动SDA从机拉低时会发生总线冲突轻则数据错误重则烧毁引脚。在Verilog里用assign sda sda_oe ? sda_out : 1bz;来实现。3.3 时钟拉伸被忽略的从机反制机制很多EEPROM支持时钟拉伸Clock Stretching即在从机还没准备好数据时主动拉低SCL强制主机等待。如果你的IIC控制器不支持时钟拉伸读EEPROM时就会读到错误数据。我调试AT24C02时遇到过这个问题连续读多个字节时第二个字节开始数据就错了。后来查手册发现AT24C02在内部写周期内会拉低SCL。我的控制器因为没有检测SCL实际电平继续按自己的节奏走导致采样时机错位。解决办法是在状态机里增加一个检测每次准备拉高SCL之前先检查SCL是否真的被拉高了。如果SCL被从机拉低就进入等待状态直到SCL释放。// 时钟拉伸检测 always (posedge clk) begin if (scl_oe !scl_in) begin // 从机正在拉伸时钟等待 state WAIT_SCL_RELEASE; end end这个改动让我的EEPROM连续读稳定性从偶尔出错变成了完全可靠。4. EEPROM读写实战页写、随机读与写周期等待4.1 字节写与页写的地址回卷机制EEPROM的写操作分字节写和页写。字节写一次写一个字节页写一次可以写一页通常8到64字节。页写的优势是效率高但有个坑地址会在页边界回卷。比如AT24C02的页大小是8字节如果你从地址0x07开始写8个字节写到0x08时地址不会进位到0x08而是回卷到0x00。这意味着你原本想写0x07到0x0E实际写的是0x07和0x00到0x06。这个机制如果不注意数据就会莫名其妙被覆盖。我的做法是在软件层做地址对齐每次页写前计算当前地址到页边界的剩余空间如果剩余空间小于要写的字节数就分两次写。这样虽然多了一次传输但保证了数据正确。4.2 随机读的伪写地址设置EEPROM的随机读操作需要先发送一个伪写来设置读地址然后重新发送起始条件和读命令。具体流程是发送起始条件发送设备地址 写标志发送要读的地址再次发送起始条件发送设备地址 读标志读取数据发送NACK和停止条件这个流程里最容易出错的是第二次起始条件。有些控制器在发送完地址后直接切换到读模式没有重新发起始条件导致EEPROM不响应。我在状态机里专门加了一个RESTART状态确保第二次起始条件的时序正确。4.3 写周期等待别在EEPROM消化时打扰它EEPROM写完一个字节或一页后需要内部写周期时间通常5ms左右来把数据固化。在这段时间内EEPROM不会响应任何命令。如果你立刻发下一个读或写会收到NACK。我最初的代码没有处理这个等待连续写EEPROM时第二个字节总是失败。后来加了应答轮询机制写完之后不断发送起始条件和设备地址直到收到ACK说明EEPROM内部写周期结束。// 应答轮询等待EEPROM写周期结束 void eeprom_wait_ready(uint8_t dev_addr) { while (1) { iic_start(); if (iic_send_byte(dev_addr 1) ACK) { iic_stop(); break; // EEPROM准备好了 } iic_stop(); delay_us(100); // 等待一段时间再试 } }这个轮询比固定延时更可靠因为不同批次的EEPROM写周期时间可能有差异固定延时要么浪费 time要么不够。5. FIFO深度设计收发路径上的缓冲策略5.1 发送FIFO解决AXI与IIC的速度失配AXI总线是高速并行接口IIC是低速串行接口两者速度差了几个数量级。如果没有FIFO缓冲处理器写一个字节就要等IIC发完效率极低。发送FIFO的作用是让处理器可以连续写入多个字节IIC控制器慢慢发送。发送FIFO的深度怎么定我的经验是至少能容纳一次完整传输的字节数。比如你要写EEPROM的一页64字节FIFO深度至少64。如果深度不够处理器写一半就要等待失去了缓冲的意义。但深度也不是越大越好FIFO每增加一级都会消耗FPGA的BRAM资源。对于大多数应用深度设成128或256就能覆盖绝大多数场景。5.2 接收FIFO防止数据溢出接收FIFO的问题更微妙。IIC控制器接收完一个字节后如果处理器没有及时读走下一个字节就会覆盖旧数据。接收FIFO的作用是给处理器留出读取窗口。我调试时遇到过一个现象连续读EEPROM时每隔几个字节就丢一个。用逻辑分析仪抓波形发现IIC控制器接收数据的速度是稳定的但处理器因为中断响应延迟偶尔来不及读。接收FIFO深度从4增加到16后问题消失。接收FIFO的深度建议至少是发送FIFO的一半因为读操作通常比写操作对实时性要求更低。但如果你用的是中断方式读取深度要适当加大给中断响应留余量。5.3 FIFO的空满标志与阈值中断FIFO的空满标志是调试的重要观测点。我习惯在STATUS寄存器里暴露三个标志TX_FIFO_FULL、RX_FIFO_EMPTY、RX_FIFO_LEVEL当前数据量。这样处理器在写之前先查满标志读之前先查空标志避免无效操作。更进一步可以设置阈值中断当接收FIFO数据量超过某个阈值时触发中断处理器一次性读走一批数据。这样比每接收一个字节中断一次效率高得多。阈值通常设为FIFO深度的1/2或3/4。FIFO类型建议深度中断阈值适用场景发送FIFO128空时触发批量写EEPROM接收FIFO64半满触发批量读EEPROM发送FIFO16空时触发单字节寄存器读写接收FIFO8半满触发低速传感器读取6. 调试链路复盘从波形异常到根因定位6.1 用寄存器回读缩小问题范围调试AXI_IIC时最怕的是不知道问题出在哪一层。我的排查顺序是先查AXI寄存器写CTRL读回来对不对不对就是AXI接口问题。再查IIC状态机启动传输后STATUS的忙标志有没有变化没有就是状态机没启动。最后查物理层用示波器看SCL和SDA有没有波形波形不对就是引脚约束或上拉电阻问题。这个顺序能帮你快速定位问题层级避免在错误的层面上浪费时间。6.2 示波器抓IIC波形的三个关键点用示波器调试IIC时我重点关注三个地方起始条件的建立时间SDA下降沿到SCL下降沿的时间要满足EEPROM手册要求的最小值通常4.7us for 100kHz。ACK位的电平第9个时钟周期SDA是否为低确认从机应答。停止条件的保持时间SDA上升沿到SCL上升沿的时间确保总线正确释放。我遇到过一次读数据全FF的问题示波器抓波形发现ACK位一直是高说明EEPROM根本没应答。后来发现是设备地址写错了——把7位地址当8位用了左移一位后多了个0。6.3 上拉电阻选型4.7k不是万能答案IIC的SDA和SCL需要上拉电阻常见值是4.7k。但这个值不是固定的它取决于总线电容和通信速率。总线电容越大上拉电阻要越小否则上升沿太慢速率越高上拉电阻也要越小。计算公式是R_max t_r / (0.8473 × C_bus)其中t_r是允许的最大上升时间100kHz时是1000nsC_bus是总线电容。如果C_bus是200pFR_max大约是5.9k。所以4.7k在大多数场景下够用但如果你的总线挂了很多设备电容大就要换成2.2k甚至1k。我调试时遇到过上升沿太慢导致数据错误的问题把4.7k换成2.2k后波形明显改善。但电阻也不能太小否则从机拉低时灌电流太大可能超过引脚的最大灌电流能力。7. 几个让我少走弯路的实操习惯调试AXI_IIC这类外设我慢慢养成了几个习惯分享出来可能对你有用。第一个习惯先写一个寄存器自检函数。上电后先往每个可写寄存器写0x55和0xAA读回来比对。这个函数能在几秒钟内告诉你AXI接口是否正常省去大量猜测时间。第二个习惯给IIC状态机加一个超时计数器。IIC总线可能因为从机故障一直拉低SCL导致状态机死等。加一个超时计数器超过一定时间就强制复位状态机并置错误标志。这个计数器救过我很多次尤其是在热插拔EEPROM的时候。第三个习惯把FIFO的当前深度实时暴露在STATUS寄存器里。调试时通过读寄存器就能知道FIFO里有多少数据不用每次都抓波形。这个信息对于判断是处理器读太慢还是IIC发太快非常有用。第四个习惯EEPROM的写周期等待用轮询而不是固定延时。固定延时在不同批次的芯片上可能不够轮询虽然多几次IIC传输但可靠性高得多。实测下来轮询方式在AT24C02和AT24C256上都能稳定工作。第五个习惯保留一个回环测试模式。在控制器里加一个模式位设置后IIC控制器不真正驱动总线而是把发送的数据直接回送到接收FIFO。这样可以在不接外部EEPROM的情况下验证AXI接口、FIFO和状态机的基本功能。这个模式在板子还没焊EEPROM时特别有用。最后说一个关于时钟分频的细节。SCL的频率不是越低越稳太低会导致某些EEPROM进入超时复位。标准模式100kHz、快速模式400kHz是经过验证的可靠值。如果你不确定先用100kHz跑通再尝试提高。分频系数要按系统时钟精确计算比如100MHz系统时钟100kHz的SCL需要500分频但别忘了状态机每个相位可能占多个时钟实际分频系数要除以相位数量。
返回列表