尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

STM32L452 I2C总线卡死排查:时钟延展NOSTRETCH配置与恢复实践

STM32L452 I2C总线卡死排查:时钟延展NOSTRETCH配置与恢复实践 1. 改版浮出问题I2C总线卡死且软件复位无效前阵子做一款采集器改版主控从STM32F103换成了STM32L452I2C总线上挂了一颗温湿度传感器和一颗EEPROM逻辑基本没动就是把库函数和外设初始化按L4重写了一遍。结果样机测试不到两天现场反馈设备偶尔“失联”——不是整机死机而是传感器数据读不出来EEPROM读写也失败重启整机才能恢复。一开始我以为是传感器批次问题换了三颗新传感器问题依旧。示波器挂上去抓了整整一天终于抓到一次卡死瞬间的波形SCL被死死拉在低电平SDA释放为高整个总线处于“假死”状态。更麻烦的是在代码里调用HAL_I2C_DeInit再重新HAL_I2C_Init把外设彻底复位一遍总线依然无法恢复只有MCU整机复位才能救回来。这个现象很有代表性——I2C总线一旦因为时钟延展卡死单纯复位主控的I2C外设没用因为SCL低电平是总线上的某个设备拉住的外设复位不会让那个设备自动释放SCL。当时我翻了不少资料最后把矛头指向了STM32L452的I2C时钟延展Clock Stretching功能。L4系列和F1系列的I2C外设在底层设计上有很大差异F1上没有这个控制位L4上专门留了一个NOSTRETCH位用来关闭时钟延展。实测下来正确关闭这个功能后总线卡死的问题基本绝迹。这篇笔记就是把这次排查过程、配置方法以及踩过的坑完整记录下来。2. 时钟延展在L452上的真实行为与坑点2.1 I2C协议里“拉低SCL”并不一定意味着错误I2C是开漏结构的总线SCL和SDA都可以被任意设备拉低靠上拉电阻恢复高电平。很多人对I2C的理解停留在线路层面的“起始条件、停止条件、ACK/NACK”容易忽略一个关键行为——时钟延展。从设备在处理数据或者没有准备好接收下一个字节时可以把SCL拉低此时主设备必须等待直到从设备释放SCL才能继续传输。这是I2C协议本身定义的流控机制不是为了刁难主设备而是让慢速的从设备有机会“叫停”总线。在STM32L452上I2C外设也实现了这个机制而且不只是从设备会延展时钟主设备模式同样会延展——如果发送数据寄存器在需要填充时没有及时写入硬件会自动把SCL拉低等软件把数据塞进去之后再恢复。这个细节很多文档写得比较隐晦实际调试的人才会真正感知到。2.2 L452的I2C_CR1寄存器与NOSTRETCH位的硬件行为STM32L452的I2C外设跟F1系列完全是两代设计。F1的I2C用I2C_CCR控制时序L4改成了I2C_TIMINGR寄存器语义和配置方式完全不同。在I2C_CR1寄存器的第7位有一个NOSTRETCH位就是专门用来关闭时钟延展的。复位默认值是0表示允许时钟延展。把这个位置1之后主设备模式下发送时就算TXIS标志位没有被及时清除硬件也不再拉低SCL等待而是继续往下走从设备模式下接收数据寄存器没来得及读走时硬件同样不会拉低SCL阻止主设备继续发送。这里有一个很容易忽略的细节这个位只能在PE外设使能为0时修改也就是说动态切换NOSTRETCH之前必须先关掉I2C外设改完寄存器再重新使能才算真正生效。2.3 关闭时钟延展的两个典型动机为什么要关闭一个协议自带的流控功能我这次是因为总线卡死但正经的应用场景主要有两个第一个是SMBus兼容。SMBus标准对时钟低电平时间有严格上限不允许从设备无限期拉低SCL。如果总线上挂了SMBus设备主设备又默认开启时钟延展一旦某个设备异常总线可能长时间锁死这跟SMBus的可靠性设计初衷直接冲突。第二个是增强主设备的容错能力。模拟传感器或者老式EEPROM芯片的时序不规范有些从设备在异常状态下会一直把SCL拉低不释放。如果主设备允许时钟延展就只能无限期等下去最终表现为总线卡死。这种情况下关闭时钟延展配合超时检测机制就能快速识别异常并主动恢复避免整个系统因为一颗故障从设备而瘫痪。3. NOSTRETCH位配置从CubeMX到寄存器级代码3.1 CubeMX配置路径与HAL库初始化代码先说最简单的做法用STM32CubeMX配置I2C时在I2C外设的Parameter Settings界面里往下翻能看到No Stretch Mode选项默认是Disabled即允许时钟延展。改成Enabled重新生成代码就完成了初步配置。不过日常项目里我更习惯直接在HAL初始化结构体里明确写上这个字段而不是依赖CubeMX重新生成因为代码评审时一眼就能看出I2C的配置状态。I2C_HandleTypeDef hi2c1; hi2c1.Instance I2C1; hi2c1.Init.Timing 0x2000090E; /* 由CubeMX根据I2CCLK80MHz、400kHz计算 */ hi2c1.Init.AddressingMode I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode I2C_DUALADDRESS_DISABLE; hi2c1.Init.GeneralCallMode I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode I2C_NOSTRETCH_ENABLE; hi2c1.Init.OwnAddress1 0; hi2c1.Init.OwnAddress2 0; hi2c1.Init.OwnAddress2Masks I2C_OA2_NOMASK; hi2c1.Init.OwnAddress2Periodic I2C_OA2_NOMASK; if (HAL_I2C_Init(hi2c1) ! HAL_OK) { Error_Handler(); }关键是NoStretchMode这一行。HAL库里对应两个宏I2C_NOSTRETCH_DISABLE和I2C_NOSTRETCH_ENABLE。语义上别搞混了DISABLE是“禁止关闭时钟延展”也就是默认允许延展ENABLE才是关闭延展功能。这块命名确实有点反直觉我看不少人在这里栽过跟头。3.2 动态切换NOSTRETCH的寄存器写法如果不想因为改一个位就把整个I2C外设重新初始化一遍可以直接操作寄存器。但要注意写入NOSTRETCH位时PE位必须已经被清零。/* 关闭I2C外设 */ hi2c1.Instance-CR1 ~I2C_CR1_PE; /* 关闭时钟延展 */ hi2c1.Instance-CR1 | I2C_CR1_NOSTRETCH; /* 重新使能I2C外设 */ hi2c1.Instance-CR1 | I2C_CR1_PE;同样如果想恢复默认行为把CR1的NOSTRETCH位清零就行。有一个实操细节动态切换NOSTRETCH前最好确认总线上没有正在进行的传输否则在传输中间突然关闭外设总线状态机会乱掉可能直接导致后面的通信失败。稳妥的做法是在空闲状态切换比如每次通信完成后、下一次通信开始前。3.3 时序参数计算的常见误区TIMINGR与CCR不是一回事很多人从F1项目迁移到L4时习惯把I2C的时序配置理解成CCR寄存器那套——设置一个分频值再设置占空比。L4的TIMINGR寄存器完全不一样它分成PRESC、SCLDEL、SDADEL、SCLH、SCLL几段分别控制同步周期、数据建立/保持时间、时钟高电平时间和低电平时间。在关闭时钟延展后SCLL和SCLH的配置变得更重要了因为主设备不会再因为从设备来不及处理而放慢速度一切以主设备写好的时序参数为准。如果时序参数设得过于激进从设备跟不上就会直接出现NACK或数据错乱而不是以前那种等一等的效果。我在这个项目里用的是400kHz的Fast ModeCubeMX根据80MHz的I2C外设时钟自动算出的Timing值是0x2000090E直接抄进代码实测波形的高电平、低电平、建立保持时间都在规范范围内。如果要手工计算务必去参考手册里查I2C_TIMINGR的说明别套F1那套公式。4. 关闭时钟延展后的副作用与配套保护机制4.1 主设备发送阶段的Underrun风险关闭时钟延展不是没有代价的。在主设备发送模式下原来硬件的逻辑是发送数据寄存器空TXIS1之后如果软件没有及时把下一个字节写进去硬件就拉低SCL等一等。关闭NOSTRETCH之后硬件不等了但如果软件还没准备好数据就会产生下溢Underrun错误I2C_ISR寄存器里的OVR标志位被置位通信过程直接报错。解决办法有两个方向一是发数据时把TXIS中断优先级提到足够高保证数据能及时填充二是用DMA传输让DMA自动填充发送数据寄存器彻底绕开软件响应延迟的问题。我实测下来用DMA发送最省心CPU占用低还不会因为中断嵌套导致数据断档。4.2 从设备模式下直接触发OVR数据被覆盖如果L452本身作为从设备关闭时钟延展的影响更直接。原来从设备收到一个字节后如果应用层没来得及读走数据硬件会把SCL拉低让主设备暂停发送。关闭之后从设备失去这个“刹车”能力下一个字节如果来了接收数据寄存器里上一字节还没被读走新数据就会把旧数据覆盖OVR标志位置位。所以如果项目里STM32L452是作为I2C从设备与外部主机通信关闭时钟延展前要仔细斟酌。除非你的应用层能在每个字节到达时都及时响应或者主设备侧的发送间隔足够长否则不要轻易关闭。4.3 和超时检测配合配置I2C_TIMEOUTR的实际参数关闭时钟延展只解决了“主设备自己不会拉低SCL”的问题但总线上如果有第三方异常设备把SCL拉低主设备会照样卡住。所以在做总线容错时我更建议把NOSTRETCH和I2C超时检测一起开。L452的I2C外设自带超时检测功能寄存器是I2C_TIMEOUTR。可以分别检测SDA和SCL的电平持续时间超过阈值就触发超时事件。配置代码如下/* 使能SCL低电平超时检测阈值为0xFFF个I2C时钟周期 */ hi2c1.Instance-TIMEOUTR (0xFFF I2C_TIMEOUTR_TIMEOUTB_Pos) | I2C_TIMEOUTR_TEXTEN; /* 使能SDA低电平超时检测阈值为0xFFF个I2C时钟周期 */ hi2c1.Instance-TIMEOUTR | (0xFFF I2C_TIMEOUTR_TIMEOUTA_Pos) | I2C_TIMEOUTR_TIMOUTEN;这里有一个要注意的点超时检测的阈值是按I2C外设时钟周期数的TIMEOUTx 1来算的不是直接填一个微秒数。以80MHz的I2C外设时钟为例0xFFF也就是4096个周期大约是51微秒左右。这个值对于低速传感器来说可能太短了正常通信时从设备处理数据也可能超过这个时间。实测下来需要根据总线上最慢的从设备实际响应时间来调整。如果51微秒不够可以用超时检测的扩展时钟位TIMEOUTG来延长。设置TIMEOUTG1后超时检测被分频超时时间变大。但具体的分频系数不同型号有差异建议以L452参考手册中关于I2C_TIMEOUTR的时序说明为准计算好实际超时时间再填值。超时事件触发后硬件并不是自动恢复总线而是通过中断通知软件。软件在超时中断里需要手动复位I2C外设必要时还要用GPIO翻转产生9个SCL时钟脉冲把可能卡在异常状态的外部从设备释放掉。这一步是总线恢复的关键只复位I2C外设而不恢复外部从设备的话故障重现是大概率事件。void HAL_I2C_ErrorCallback(I2C_HandleTypeDef *hi2c) { if (hi2c-Instance I2C1) { /* 超时或其他I2C错误处理 */ /* 步骤1关闭I2C外设 */ __HAL_I2C_DISABLE(hi2c); /* 步骤2手动配置PB6/PB7为GPIO输出产生9个SCL脉冲 */ recover_i2c_bus(); /* 步骤3重新初始化I2C外设 */ MX_I2C1_Init(); /* 步骤4记录日志便于现场定位 */ log_i2c_error(); } }recover_i2c_bus这个函数是模拟I2C时序的恢复逻辑核心就是切换SCL和SDA引脚为GPIO模式先把SDA释放为高然后CLK引脚连续翻转至少9次让卡住的从设备完成状态机复位最后再产生一个停止条件让总线回到空闲状态。这里不展开全部代码实际工程里网上有很多现成的实现可以参照。5. 一次定位问题的完整排查链路与最终方案5.1 示波器抓到的波形说明了什么卡死那一刻SCL固定在低电平SDA是高电平。总线在SCL低电平期间所有设备都处于等待状态谁也不会主动去改SDA。从波形上看问题非常明确某个设备在访问过程中把SCL拖住了而且一直没释放。问题是到底是哪个设备拖的我这里L452是主设备传感器和EEPROM都是从设备。按常理推断最有可能是从设备侧的故障因为传感器是外购件规格参差不齐。但用了排除法之后发现换传感器、换EEPROM都不解决问题这才怀疑到主设备自身的行为上。5.2 软件复位I2C外设为何不能恢复现场当时最让我困惑的是卡死后调用HAL_I2C_DeInit再HAL_I2C_Init总线依然没有恢复。现在回过头来看原因很清楚——主设备的I2C外设复位之后SCL引脚恢复到高阻输入模式此时总线上的上拉电阻会把SCL拉高。但外部从设备如果正处于一个“等待释放”的状态它可能依然在驱动SCL低电平哪怕主设备已经把外设关了从设备那边的驱动还在SCL自然就保持在低电平。也就是说卡死的根因如果是某个从设备一直拉着SCL不放手那光靠主设备侧复位是没用的。必须让所有从设备都退出异常状态总线才能回到空闲。这也是为什么后来我不仅关了NOSTRETCH还加了超时检测和GPIO恢复逻辑双管齐下才把问题压住。5.3 关闭NOSTRETCH后的对比测试数据关闭NOSTRETCH之后我跑了三轮对比测试测试项连续读写EEPROM 1000次间隔50ms开启时钟延展默认状态下第372次出现一次总线卡死恢复失败关闭时钟延展无超时检测没有出现总线挂死但偶尔出现通信错误需要业务层重试关闭时钟延展开启SCL超时检测没有出现总线挂死通信错误由重试机制消化测试全程无故障三组数据放在一起结论就很明显了单纯关闭时钟延展并不能让总线永远不坏但它把“无限期卡死”变成了“可检测、可恢复的瞬时错误”配合超时检测后系统有了自愈能力。5.4 最终方案的完整配置清单最终定型方案包括四个部分I2C_CR1的NOSTRETCH位置1关闭时钟延展I2C_TIMEOUTR配置SCL低电平超时约100us触发错误中断错误中断里先GPIO模拟恢复总线再重新初始化I2C外设业务层对I2C读写增加重试机制最多重试3次仍失败则报警这个方案在随后一个月的长期运行中I2C总线再也没有出现过需要整机复位才能恢复的情况。6. 实践沉淀什么时候该关什么时候不能关6.1 三条判断标准经历过这次踩坑我总结出三条判断标准供大家参考第一L452作为主设备、总线上挂多个从设备、系统不允许长时间阻塞的优先考虑关闭NOSTRETCH。比如数据采集器、工业控制板这类场景一个设备异常不能拖垮整个系统。第二L452作为从设备、外部主机的发送节奏不可控的不要轻易关闭NOSTRETCH。除非你的从设备中断响应时间有充分保障否则数据覆盖问题比时钟延展更让人头疼。第三总线上有SMBus设备或者对总线可靠性要求极高的场景不仅要关闭NOSTRETCH还要把超时检测做全并确保错误恢复路径经过了完整测试。另外有一个容易被忽略的细节关闭NOSTRETCH之后I2C进入低功耗模式再唤醒CR1寄存器的配置状态可能被改变唤醒后要记得检查NOSTRETCH位是否还是预想的值。我在低功耗唤醒后遇到过外设寄存器被重置的情况后来在唤醒流程里固定重新设置一遍I2C相关寄存器才把这个隐患排掉。最后分享一个调试经验排查I2C总线卡死时别急着改代码先把波形抓下来确认SCL和SDA的信号状态再用示波器看总线空闲时两个引脚是否都能被上拉到高电平。确定波形状态后再针对主设备侧的行为做分析排查效率会高很多。如果上来就怀疑时钟延展盲目关闭NOSTRETCH很可能解决了主设备延展的问题却忽略了外部从设备卡住总线的风险治标不治本。
返回列表