ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FPGA读STM32L452竟收到0xFF?I2C时钟延展问题解析与解决

FPGA读STM32L452竟收到0xFF?I2C时钟延展问题解析与解决 翻车现场FPGA从STM32L452读到一堆0xFF前阵子调一块STM32L452和FPGA协作的板子FPGA做主控通过I2C每10毫秒从STM32L452这边读一组传感器状态。跑起来之后数据一会儿对一会儿错读十个包能烂两个严重的时候I2C直接卡死SCL被拉在地上一动不动。拿逻辑分析仪抓了波形才发现问题出在I2C的一个老朋友——时钟延展Clock Stretching上后面又连带炸出OVR溢出错误。这篇文章把我从现象到解决的完整过程捋一遍遇到同样问题的朋友可以直接照着查。适用场景很明确STM32做I2C从机、FPGA做I2C主机或者反过来也行只要其中一端是FPGA自己写的那套简易I2C控制器你都可能踩到这个坑。内容包含时钟延展的原理说明、完整的波形排查思路、关闭NOSTRETCH的具体配置、OVR错误的三种解法以及在STM32L452这块板上实测下来的一些细节。先说结论如果你的FPGA是自研的I2C主机状态机且代码里没有“检测SCL被从机拉低就等待”的逻辑那STM32这边默认开启的时钟延展就是通信不稳定的头号嫌疑。下面展开说。1. 项目背景与故障表现1.1 硬件连接与初始配置STM32L452用I2C1引脚是PB8SCL、PB9SDA都配成开漏复用功能外部上拉4.7k总线上挂着STM32L452作为从机和一块FPGA作为主机速率跑400kHz。STM32L452侧初始化参数大概是这样hi2c1.Instance I2C1; hi2c1.Init.ClockSpeed 400000; // 400kHz hi2c1.Init.DutyCycle I2C_DUTYCYCLE_2; hi2c1.Init.OwnAddress1 0x32; // 7位从机地址 hi2c1.Init.AddressingMode I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode I2C_DUALADDRESS_DISABLE; hi2c1.Init.GeneralCallMode I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode I2C_NOSTRETCH_DISABLE; // 默认使能时钟延展 hi2c1.Init.OvfrDisableMode I2C_OVERRUN_DISABLE; // 默认禁止覆盖这一版配置其实是CubeMX生成的默认值。注意NoStretchMode I2C_NOSTRETCH_DISABLE意思是“不禁止延展”也就是允许时钟延展。这个默认配置在STM32和单片机之间通信时通常没问题因为对方的I2C主机也会配合等待。但换成FPGA做主机后问题很快暴露了。1.2 故障现象的具体表现故障不是一开始就出现而是跑了大概几十次通信后开始零星冒出来典型表现有三个FPGA读回来的数据包中偶尔某个字节变成0xFF或者0x00和STM32内部实际值对不上连续读取时从机地址应答正常但数据阶段卡住SCL一直被拉低后续所有I2C通信全部挂起STM32侧查询状态寄存器发现I2C_ISR里OVR标志位置位而且一旦置位后没及时清除后面通信就一直处于错误状态。最迷惑的是卡死现象。因为从机地址能正常应答说明GPIO配置、地址匹配都没问题但数据阶段SCL被拉死这说明从机在数据传输过程中主动做了“某种动作”把SCL拖住了。当时的第一反应是怀疑STM32的I2C中断响应不及时导致内部的RXNE/TXIS标志没人处理于是把中断优先级拉高试了一下——结果只是让故障出现频率降低并没有根治。其实这里就指向了时钟延展。STM32L452的硬件I2C在从机模式下一旦接收数据寄存器满RXNE1或者发送数据寄存器空TXIS1而软件还没及时处理硬件就会自动把SCL拉低告诉主机“我还没准备好你等等”。主机如果不理会这个信号继续打时钟就必然乱套。1.3 为什么不怀疑电气问题排查时顺手验证过上拉电阻和信号完整性。4.7k上拉在400kHz下上升沿大概是70ns级别逻辑分析仪上波形没有明显畸变SDA电平也干净排除了硬件驱动能力不足的可能。另外在降速到100kHz后故障只是变少没有消失这更说明问题不在电气参数而在于协议交互逻辑。2. 时钟延展是什么FPGA主机为什么对它视而不见2.1 I2C协议里给从机的“拖延特权”I2C协议里有个很容易被忽略的设计虽然SCL时钟是由主机产生的但从机拥有一个“特权”——它可以在任意时刻主动将SCL拉低并保持。主机看到SCL为低就应该停下当前的时序一直等到SCL被释放后再继续。这个机制存在是因为从机的处理速度通常远慢于主机。拿STM32L452举例它内部主频虽然能跑到80MHz但I2C外设收到一个字节后CPU需要从这个外设的中断响应、读取数据寄存器、把数据搬到内存这一串动作下来至少需要几十到几百纳秒。如果主机以400kHz连续发字节两个字节之间的间隔只有2.5微秒CPU万一被其他中断耽误一下就来不及处理。这时候时钟延展就是给从机的“缓冲时间”——从机把SCL拉低主机就不继续发等CPU处理完了再放行。用大白话说I2C总线上的从机就像一个服务员主机是不断提问的顾客。顾客问得太快服务员腾不出手记录这时候服务员可以伸手示意“打住”等自己记完再说。时钟延展就是那个“打住”的手势。2.2 STM32L452从机会在哪些节点拉低SCL具体到STM32L452参考手册RM0351从机模式下拉低SCL的时机主要有四类地址匹配之后内部会置位ADDR标志硬件会一直把SCL拉低等待软件对ADDR标志做清除操作写入ADDRCF从机接收模式下收到一个字节后RXNE置位数据寄存器满此时如果软件没及时读取DRSCL会被拉低从机发送模式下发送数据寄存器空TXIS置位软件没及时写入下一个字节SCL也会被拉低某些需要软件介入的总线事件处理期间硬件都会暂时锁住SCL。也就是说只要软件处理速度跟不上主机节奏SCL就会被拉低。这在标准I2C协议里是完全合法且在预期之内的行为。2.3 FPGA I2C主机代码最常见的盲区问题恰恰出在FPGA这头。很多FPGA工程师写I2C主机状态机时会默认一个前提SCL是我产生的所以高低电平节奏完全由我掌握。典型的状态机类似这样IDLE产生START条件SEND_ADDR循环8次每次拉高SCL期间把地址bit放到SDA上WAIT_ACK释放SDA产生第9个时钟检测ACKSEND_DATA类似地址阶段逐bit发送STOP产生STOP条件。这个状态机的问题在于每个bit的时长是用计数器固定的比如高电平保持1.5微秒低电平保持1微秒。整个过程中完全没有人去采样SCL线的实际电平作为状态跳转条件。一旦从机拉低SCLFPGA并不知道计数器一到就继续执行下一步SCL上的真实电平和FPGA内部状态机的预期完全脱节。更要命的是从机拉低SCL是发生在某些特定数据字节之后的。于是FPGA下一个bit可能在SCL还没释放的时候就开始了SDA上的数据采样点错位收回来一堆乱码。这就是“FPGA读到的数据偶尔变0xFF或0x00”的直接原因。如果你之前用过单片机软件模拟I2C多半不会犯这个错因为软件模拟I2C时通常会在切换SCL后去读回引脚电平做确认。但Verilog状态机的写法更容易让人陷入“我发了时钟就是发完了”的思维定式。3. 从一段异常波形到锁定NOSTRETCH位的排查过程3.1 逻辑分析仪抓到的关键证据排查I2C问题逻辑分析仪是必不可少的工具。我用的是一台24MHz采样率的小型逻辑分析仪抓400kHz的I2C绰绰有余。故障触发后抓到的波形有非常明显的特征在数据阶段的某个SCL低电平之后SCL线没有按时拉高而是保持低电平很长一段时间持续了接近两个字节的时间然后才恢复。这段异常低电平是“异常”吗站在从机角度看完全正常——这是时钟延展。但站在FPGA主机角度看它已经按自己的状态机把后续的时钟全都“发”出去了SDA在SCL低电平期间也发生了多次变化。两边完全是各走各的。关键点在于如果SCL是被从机拉低的那波形上可以看到SCL的电平变化不是主机主动产生的因为主机发出的SCL本来已经进入高电平阶段实际却保持低。这就是时钟延展的典型特征——低电平被拉长而且拉长的时间不是主机控制的。3.2 对照RM0351手册确认行为抓到异常波形后我在STM32L4参考手册RM0351的I2C章节里仔细看了一遍。手册里对NOSTRETCH位I2C_CR1寄存器的Bit7的说明很清楚0使能时钟延展。当从机需要时间处理数据时硬件自动拉低SCL1禁止时钟延展。从机不会拉低SCL主机可以按自己的节奏发送。再回头比对波形从机拉低SCL的那段时间恰好是STM32L452收到数据后RXNE置位、CPU还没来得及读取的时刻。这就完全对上了——默认配置下STM32硬件确实在主动延展时钟而FPGA主机没有响应这个信号。3.3 检查FPGA代码确认主机盲区接着打开FPGA工程里自己写的I2C主机模块逐段检查状态机。这个模块是从一个Cortex-M单片机软件模拟I2C代码移植过来的逻辑上保留了“SCL拉高”“SCL拉低”这些输出信号但唯独漏掉了对SCL输入电平的检测。标准I2C要求主机在输出SCL高电平后必须先确认SCL线上的实际电平是否为高如果仍为低说明有从机在拉伸时钟必须等待。这个检查在状态机里被完全遗漏了。实际上很多FPGA开发者的自研I2C IP都有这个问题反倒是Xilinx和Altera官方提供的I2C控制器IP会正确处理时钟延展因为它们的状态机设计更贴近协议标准。3.4 两条路线的取舍改FPGA还是关STM32的时钟延展既然确认了问题根源修复的方向就有两条路线A修改FPGA I2C主机状态机在SCL高电平阶段加上“如果SCL输入为低则等待”的逻辑让它支持外部时钟延展。这样STM32侧保持默认配置符合I2C协议对从机侧的照顾后续通信最稳路线B修改STM32L452侧配置关闭时钟延展让从机不再拖SCL。这样FPGA侧代码完全不用动。理论上路线A才是根因修复协议角度也更正确但实际项目里FPGA那侧的状态机还挂了其他几个设备改动时序会影响很多逻辑的回归测试时间上不划算。于是选择了路线B先让通信跑通再说。后来证明了这条路确实会引出另一个坑——OVR溢出错误这部分在第5节详述。顺便说一句如果你手头两个端都能改强烈建议优先做路线A。时钟延展是I2C协议给从机的保命机制硬生生关掉等于让从机裸奔后续只能靠提高中断优先级、开DMA这些手段来补位。但现实项目交付在即的时候路线B就是最务实的快车道。4. 关闭时钟延展的配置方法与必须接受的副作用4.1 三种配置方式我用的HAL库版本里NoStretchMode字段对应的就是I2C_CR1寄存器的NOSTRETCH位。关闭时钟延展只需要把它从I2C_NOSTRETCH_DISABLE改成I2C_NOSTRETCH_ENABLE。hi2c1.Init.NoStretchMode I2C_NOSTRETCH_ENABLE; // 禁止时钟延展 HAL_I2C_Init(hi2c1);如果你习惯直接操作寄存器同样可行但有个细节必须注意NOSTRETCH位只能在PE0I2C外设禁用时修改。我见过有人直接在通信过程中用赋值语句去改结果写进去的值根本没生效排查了半天才发现是修改时序的问题。CLEAR_BIT(I2C1-CR1, I2C_CR1_PE); // 先失能I2C SET_BIT(I2C1-CR1, I2C_CR1_NOSTRETCH); // 禁止时钟延展 SET_BIT(I2C1-CR1, I2C_CR1_PE); // 重新使能I2C如果用的是CubeMX图形配置在I2C参数的“No Stretch”选项里勾选Enable即可。但说实话这种细节建议直接在代码里改CubeMX重新生成代码时容易不小心覆盖配置。4.2 配置之后必踩的三个坑第一关闭时钟延展后SMBus相关的功能就不能用了。RM0351里明确写了NOSTRETCH1时SMBus模式不支持。如果项目中走的是SMBus协议这条路直接否决老老实实改FPGA。第二从机少了“缓冲时间”后面所有数据处理的担子都压给了CPU和DMA。STM32L452的I2C中断响应不及时或者有其他高优先级中断抢占数据就会溢出——这就引出了OVR问题。第三从机发送模式下的TXIS标志处理同样变严格了。以前RXNE/TXIS没来得及处理时还能靠拉低SCL“续命”现在续命工具没了软件侧必须保证在主机打完一个字节的时间内把下一个字节准备好否则发送数据会出现重复或者丢字节。配置完后再抓波形SCL上的异常拉低消失FPGA连续读一万次数据不再出错表面问题算是解决了。5. OVR溢出错误时钟延展关闭后的连锁反应5.1 为什么关了时钟延展反而容易OVROVR全称Overrun Error溢出错误。在I2C从机接收模式下硬件收到一个字节后会把数据放到移位寄存器再转移到数据寄存器DR里同时置位RXNE。如果RXNE1说明DR里还有数据没被软件取走此时硬件又收到了新的字节新的数据就会把旧数据覆盖掉同时硬件置位OVR标志。在时钟延展开着的时候RXNE1会触发SCL拉低主机停下等待所以极少出现OVR。一旦关了时钟延展主机不再等待只要软件没有在主机下一个字节到达之前取走DR里的数据OVR几乎必然出现。我的实测场景中STM32L452主频80MHzI2C速率400kHz一个字节间隔2.5微秒。中断方式下从中断触发到CPU执行完读DR的操作通常需要几十个时钟周期理论上来得及但一旦有其他高优先级中断插入延迟就可能超时。所以故障表现为偶发性的OVR置位通信数据偶尔丢一个字节。还有一个细节OVR一旦置位后续的通信会一直处于异常状态必须手动清除。很多人在HAL库里没处理HAL_I2C_ErrorCallback导致OVR置位后通信悄悄崩溃这是更隐蔽的坑。5.2 三个解决方向方向一把取数速度提上去。最有效的做法是使用DMA把I2C从机接收配置成DMA模式数据不占用CPU时间硬件直接存入内存缓冲区。uint8_t rxBuf[64]; HAL_I2C_Slave_Receive_DMA(hi2c1, rxBuf, 64);DMA模式下连续接收多个字节时数据搬移由DMA控制器完成只要DMA的响应速度跟得上400kHz的速率就不会触发OVR。实测下来用DMA后OVR基本绝迹。另外如果没有DMA通道可用也可以提高I2C中断优先级到NVIC的0级最高并保证中断处理函数里只做最少的操作不调用任何耗时函数。方向二允许覆盖让OVR不炸。即便RXN1新数据来了直接覆盖旧数据而不是报错。这个模式对应I2C_CR1寄存器的OVRDIS位Bit12。置1后硬件不再产生OVR错误标志直接让新数据覆盖DR。CLEAR_BIT(I2C1-CR1, I2C_CR1_PE); SET_BIT(I2C1-CR1, I2C_CR1_OVRDIS); // 禁止OVR检测允许覆盖 SET_BIT(I2C1-CR1, I2C_CR1_PE);这个方案适用于业务上允许丢一两个字节的场景但如果你传输的数据包含帧头和CRC丢一个字节就可能导致整个帧作废那就不推荐。我的做法是数据量小、实时性要求高时用OVRDIS1配合CRC校验宁可丢掉错误帧也不能让OVR卡死整条总线。方向三降低I2C速率。把速率从400kHz降到100kHz相当于给CPU争取了4倍的缓冲时间。实测100kHz下中断方式也基本不会OVR。缺点显而易见吞吐量降下来了。如果你的FPGA侧对速率不敏感这是最简单粗暴的解法。下面这个表格是我在这三种方案之间做选择的参考依据方案改动量对吞吐影响适用场景DMA接收中等无数据帧较长、传输频繁OVRDIS1允许覆盖极小无数据量小可容忍偶发丢字节降低I2C速率到100kHz极小降低吞吐对速率要求不高5.3 HAL库错误回调与状态恢复不管选哪个方案OVR的错误处理都得做。HAL库的I2C错误回调里至少要主动清除OVR标志并把I2C外设重新设置为接收状态void HAL_I2C_ErrorCallback(I2C_HandleTypeDef *hi2c) { if (hi2c-ErrorCode HAL_I2C_ERROR_OVR) { __HAL_I2C_CLEAR_FLAG(hi2c, I2C_FLAG_OVR); hi2c-ErrorCode ~HAL_I2C_ERROR_OVR; HAL_I2C_Slave_Receive_DMA(hi2c1, rxBuf, 64); } }注意在DMA中断模式和普通中断模式下错误后的恢复流程略有差异。普通模式只需要重新启动一次接收DMA模式则要先停DMA再重新启动否则DMA通道状态可能残留。6. 通信稳定性加固上拉电阻、中断优先级与错误恢复6.1 上拉电阻和GPIO内部上拉的关系热搜词里有一句“I2C有外部上拉是否还需配置内部上拉”这个在STM32L452上用GPIO做I2C时特别容易弄错。如果在CubeMX里把I2C引脚模式配成了GPIO_MODE_AF_OD但没注意内部上拉的选项STM32默认是内部不带上拉的。此时如果外部上拉也没接SCL/SDA在高电平阶段上升沿就会很慢波形严重畸形FPGA采样点错乱的概率极大。反过来如果外部已经接了4.7k上拉又在CubeMX里同时使能了内部上拉得到的并联等效电阻会偏小内部上拉典型值约40kΩ并联后约3.6kΩ虽然一般不至于直接通信失败但在低功耗场景下会额外增加漏电流。正确做法是外部有上拉电阻时内部上拉关闭外部没有上拉时可以短时间用内部上拉调试但正式布线必须加上外部上拉。GPIO的复用推挽/开漏配置也值得检查。I2C引脚的GPIO模式必须配成开漏复用AF_OD如果误配成推挽那么主机和从机的输出可能造成电气冲突轻则数据出错重则拉低总线导致复位电流冲击。6.2 中断优先级、DMA与验证方法把所有I2C相关中断的NVIC优先级设成0级最高之后再把DMA接收打开通信稳定性明显提升。在STM32L452上I2C1的事件中断、错误中断和DMA1通道的中断优先级都要检查一遍DMA中断优先级尽量不低于I2C中断避免数据搬移完成信号被延迟处理。验证方法上我写了一个简单的压力测试FPGA作为主机循环发起1万次读取每次读取固定64字节STM32内部维护一个累加计数器把计数器值写入每个包的最后一个字节。FPGA读回来后校验最后一个字节是否等于本地记录的包序号就能精确统计丢包率和错误率。跑完1万次错误从最初的每百十几次降到0这个压力测试才算过关。6.3 备选方案软件模拟I2C从机最后再说一个备选方案。如果FPGA代码完全不能改STM32L452的硬件I2C又调不通还有一个兜底方案用GPIO软件模拟I2C从机。也就是把SCL和SDA都配成普通GPIOSCL引脚用外部中断的方式检测边沿SDA按主机的节奏读bit用定时器辅助判定位周期最终实现从机功能。STM32L452主频80MHzIO翻转和外部中断响应速度足够支撑400kHz的I2C从机模拟。但代价是CPU占用率高而且中断响应同样可能被其他高优先级中断打断导致时序误差。软件模拟方案我实际跑通过但这玩意非常耗费精力不到万不得已别碰。回到我自己的项目最终方案是STM32侧关闭时钟延展打开OVRDIS允许覆盖同时配合DMA接收和最高中断优先级。FPGA侧没做任何修改连续跑了48小时压力测试数据零错误。如果时间允许回头我还会把FPGA的状态机补上时钟延展检测毕竟协议上的正确性才是长期稳定的基础。
返回列表