ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM8 I2C BUSY位卡死排查与恢复:从寄存器状态到总线释放方案

2026/8/29 13:13:40 拓冰建站 浏览量
STM8 I2C BUSY位卡死排查与恢复:从寄存器状态到总线释放方案 1. Busy bit卡死的第一现场现象记录与初步判断如果你在STM8S105K6上调试I2C外设八成会遇到一个让人抓狂的问题I2C通信跑着跑着突然卡死读I2C_SR2寄存器BUSY位死活是1主设备既发不了起始条件也收不到应答整个总线像被焊死了一样。我这次遇到的情况是驱动板上的STM8S105K6作为主控外挂一颗24C02 EEPROM和一个温湿度传感器。板子上电后第一次读写EEPROM一切正常但只要通信中发生一次异常——比如从设备忙、上电时序不好、或者线缆被碰了一下——I2C就再也恢复不了。代码里反复调用I2C_GenerateSTART()EV5事件就是不出现打断点看I2C_SR2寄存器的值BUSY位稳如泰山一直是1。先说判断思路。BUSY位卡住严格来说不一定是总线物理上有数据传输而是I2C模块认为总线忙。STM8的I2C判断总线忙闲的方式很直接在I2C_CR1的STOP位清零、BUSY标志为0的情况下检测到SDA或SCL上的起始条件就置1检测到停止条件就清零。问题在于一旦通信过程中出现异常主从两侧没有完成一次完整的“起始—数据—停止”序列那个起始条件之后就一直没等到合法停止条件BUSY位就永远置1了。我最初怀疑是从设备把SDA拉低了。拿示波器量了一下SDA和SCL其实都是高电平总线物理上完全是空闲状态。这就有意思了——物理空闲寄存器却说忙。这说明问题不是硬件短路而是I2C模块内部状态机卡在了“等待停止条件”的状态。另一个典型特征是此时如果强制给SWRST位写1复位I2C外设BUSY位会清零但如果你在代码里只是清标志不做处理那么下一次通信只要遇到同样的异常又会卡死。这个问题的根源往往不在硬件而在主设备发起传输的时机和对外部异常的处理方式上。后续也会说到为什么STM8的I2C模块比STM32更容易出现这个问题以及它在设计上少了哪些保护机制。先把现象和寄存器状态定位清楚排查就有了方向。2. STM8S105K6硬件模块细节为什么BUSY位容易卡住2.1 STM8 I2C外设的硬件设计差异STM8S105K6的I2C模块其实是早期设计没有ES1和ES2那种完整的错误处理机制也没有硬件的超时保护。跟STM32的I2C比它在很多地方更像一颗“裸奔”的状态机。最典型的就是BUSY位它的置位和清理由硬件自动管理但硬件只认起始条件和停止条件——只要总线上的时序不满足合法停止条件BUSY就永远不会自己清零。从寄存器手册来看I2C_SR2的BUSY位定义是“总线忙”清0条件写的是“检测到停止条件”。这意味着如果通信过程中出现以下情况之一停止条件就不会出现主设备已经开始传输但在产生停止条件前总线异常断开。从设备拉低时钟进行时钟拉伸主设备没有等待足够长时间就超时放弃导致停止条件发送不完整。主设备发起新的起始条件时前一个传输的停止条件尚未完全送出也就是“背靠背”传输时序处理不当。SCL或SDA线上有毛刺被I2C模块误识别为起始条件而后又没有合法的停止条件。STM32的I2C在检测到总线错误时会触发BERR错误中断而且有AF、ARLO等明确的错误标志位配合超时机制可以相对从容地做恢复。STM8这个模块就简单很多错误标志少没有超时计数器一切靠用户代码自己兜底。所以很多人从STM32转过来都会在这上面栽跟头。2.2 寄存器层面的状态组合排查时要重点区分I2C_SR1和I2C_SR2的状态组合这会直接影响恢复策略。I2C_SR2里除了BUSY还有MSL主从模式、ADSL地址发送、BTF字节发送完成、TRA收发状态等位。正常主模式发送时BUSY1、MSL1、TRA1。卡死时的一个典型状态是BUSY1、MSL0或者MSL1但TRA0说明状态机已经不在正常的数据传输流程里。我踩坑的一个细节是很多人看到BUSY1就急着复位I2C外设但复位前没有记录I2C_SR1的值。有时候SR1里的AF(应答失败)或OVR(溢出)位已经置位而错误标志没被清掉的话单纯复位BUSY后下一次传输还会被这些残留标志干扰。所以我在排查时习惯先把SR1整个读出来打印或记录下来再看SR2。这样才能判断是“物理空闲但状态机忙”还是“确实有错误标志残留”。2.3 硬件上拉与总线上设备的依赖关系还有一个硬件层面的原因值得单独说STM8S105K6的I2C引脚是开漏输出外部必须有上拉电阻。上拉电阻的取值直接影响信号边沿的上升时间。如果上拉电阻太大比如用了10kΩ甚至更大或者总线电容太大连线过长、过孔太多、接了多个器件那么SCL/SDA的上升沿会变缓。I2C模块内部对边沿的检测存在施密特触发器边沿过缓时可能在一个时钟周期内无法被稳定识别极端情况下会误判起始或停止条件。我自己测试时用的板子是手工焊接的飞线比较长上拉电阻用了10kΩ结果就是I2C在低速时偶尔能跑但一旦把速率提到100kHz以上就出现莫名其妙的BUSY卡死。后来把上拉改成4.7kΩ飞线尽量缩短问题频率显著下降。这个属于硬件层面的诱发因素不一定每次都是它但值得在排查早期就排除。2.4 为什么“仲裁丢失”和“应答失败”也要一并关注BUSY位卡死很多时候不是孤立发生的。在单主机模式下仲裁丢失的情况比较少但如果是多主机共享总线或者其他设备在总线上产生了噪声主设备可能在发送起始条件时丢失仲裁。STM8的I2C在仲裁丢失时会置位ARLO标志此时BUSY位可能已经置1但主设备已经失去总线控制权。这种状态下如果代码只是简单判断BUSY位是否为0来决定是否发起传输那么一旦ARLO置位加上BUSY卡住你就永远发不出起始条件。关键是仲裁丢失之后要做的不是清掉BUSY而是先复位I2C的配置寄存器重新初始化为主模式再执行总线恢复。3. 完整定位链路从代码追踪到时序问题的排查过程3.1 第一步缩小问题范围——是代码问题还是外部因素排查这类问题我习惯分三步走先看代码时序再量物理波形最后做单步实验。当时我先在代码里加了详细的标志打印每次I2C操作前后都记录状态寄存器的值。跑了一会儿发现卡死前最后一次成功的操作是读温湿度传感器的第一个字节紧接着的下一次写操作就再也没有发生过起始条件。从这个现象判断问题大概率出在“读操作结束到写操作开始”的过渡期间。查看代码发现我用的I2C库函数在读取最后一个字节后发送的是NACK加STOP条件。但问题在于读取函数返回后主循环里紧接着调用了下一个写操作而读结束时的STOP条件是否已经在总线上完整发送完毕代码里并没有检查。3.2 第二步逻辑分析仪看时序——STOP条件没有完整发送用逻辑分析仪抓取卡死时刻的波形真相很清楚SCL上出现了一个残缺的脉冲SDA的电平在STOP条件应有的位置只拉高了一半然后就维持高电平不动了。也就是说软件发了STOP命令但硬件还没来得及完成STOP条件序列新的起始条件命令就已经到来。这里要解释一下为什么STM8的I2C会出现这种“命令覆盖”的情况。看数据手册I2C_CR2寄存器的START位和STOP位它们的置位是以软件写寄存器的方式触发的但硬件状态机执行需要时间。你在前一个操作还没结束时置位了START硬件可能无法正确排队处理导致当前状态机混乱。尤其在一些HAL库的实现里发送STOP后并没有等待BUSY位清零就返回了于是主代码立刻调用下一个传输START位被设置时序就乱了。3.3 第三步单步调试定位到“背靠背传输”问题为了验证这一点我在发送STOP条件后加了一个延时等BUSY位清零再继续。神奇的是问题真的消失了。但这只是表面修复因为延时时间靠不住不同温度、不同从设备响应速度下延时不够依然会出问题。进一步看代码发现背靠背传输不只发生在读后写写后读比如先写寄存器地址再读数据也容易触发。这是因为STM8的I2C外设在处理“重复起始条件”(repeated start)时如果上一笔传输的停止条件还没完成就被新的START命令打断行为是不确定的。有些新出的MCU在硬件上支持自动排队但STM8不行。3.4 第四步异常注入实验——人为制造卡死为了确认根因我特意在代码里做了一个异常注入实验。在正常发送停止条件之前插入一个跳转跳过STOP发送直接准备下一个START。实验结果是BUSY位果然卡住了现象和现场问题一致。这就彻底证明根因就是停止条件未完成状态下启动新传输导致I2C模块内部状态机无法退出繁忙态。实验还顺带发现一个规律在100kHz标准模式下从发出STOP到BUSY位清零大约需要10到20微秒。这看起来很短但对主频16MHz的STM8来说已经是不少指令周期了。如果在STOP发送后紧接着执行一个耗时很短的下一次I2C操作比如直接写一个字节这期间极有可能撞上硬件还未清零BUSY的窗口。3.5 排查过程总结这一段排查链路如果总结成一句话就是遇到I2C Busy bit卡死首先不要怀疑是MCU坏了先检查上一个传输是否完整结束再检查当前时间点是否处于停止条件的发送窗口内。其次是检查错误标志位最后才是考虑是硬件上拉和信号完整性的问题。按照这个顺序排查基本能在半小时内定位问题。4. 解决方案释放busy bit的三种有效手段既然根因已经很明确下面直接给出实测有效的三种恢复方案。根据你的应用场景选择就好我按推荐程度排序。4.1 方案A发送STOP后等待BUSY清零从流程上根治代码层面最简单的修复是在每次发送STOP条件后等待BUSY位清零然后再执行下一个传输。// I2C发送停止条件并等待总线释放 void I2C_SendStop_WaitBusyFree(void) { I2C_CR2 | I2C_CR2_STOP; // 发送停止条件 // 等待总线释放超时保护 uint16_t timeout 0xFFFF; while ((I2C_SR2 I2C_SR2_BUSY) timeout--) ; if (timeout 0) { // 超时仍忙走恢复流程见方案B I2C_BusRecovery(); } }这个方案在实践中最为可靠。关键在于timeout不能省因为某些异常情况下BUSY可能永远不清零如果没有超时保护程序就真的死在等待里了。超时时间的取值我一般设为100ms级别对应16MHz主频下足够大循环次数避免干扰正常业务。另外注意一点在某些STM8库函数里I2C_GenerateSTOP()函数内部只是简单置位STOP位并没有等待BUSY清零。所以在调用库函数后你仍然需要自己加等待逻辑不能依赖库函数替你完成。4.2 方案BI2C外设软复位加总线恢复序列如果BUSY位已经卡死或者等待超时了就需要更彻底的恢复手段。首选是使用I2C_CR1寄存器的SWRST位复位整个I2C外设。操作顺序如下置位SWRST让I2C模块回到复位状态。清除SWRST重新初始化I2C配置时钟、模式、地址等。通过GPIO软件模拟的方式对总线执行最多9个时钟脉冲的恢复序列确保任何处于异常状态的从设备都能复位。void I2C_BusRecovery(void) { // 1. 软复位I2C外设 I2C_CR1 | I2C_CR1_SWRST; I2C_CR1 ~I2C_CR1_SWRST; // 2. 重新初始化I2C配置 I2C_DeInit(); I2C_Init(100000, 0xA0, I2C_MODE_I2C, I2C_DUTYCYCLE_2, I2C_ACK_CURR, I2C_ADDMODE_7BIT, 16); // 3. GPIO模拟时钟恢复序列 // 先把SCL/SDA配成普通开漏输出 GPIO_Init(GPIOC, GPIO_PIN_4 | GPIO_PIN_5, GPIO_MODE_OUT_OD_HISLOW); for (int i 0; i 9; i) { GPIO_WriteLow(GPIOC, GPIO_PIN_4); // SCL拉低 delay_us(5); GPIO_WriteHigh(GPIOC, GPIO_PIN_4); // SCL拉高 delay_us(5); } // 4. 发送一个停止条件让从设备彻底释放总线 GPIO_WriteLow(GPIOC, GPIO_PIN_5); // SDA拉低 GPIO_WriteLow(GPIOC, GPIO_PIN_4); // SCL拉低 delay_us(5); GPIO_WriteHigh(GPIOC, GPIO_PIN_4); // SCL先高 delay_us(5); GPIO_WriteHigh(GPIOC, GPIO_PIN_5); // SDA再高形成停止条件 delay_us(5); // 5. 重新配置为I2C复用功能 GPIO_Init(GPIOC, GPIO_PIN_4 | GPIO_PIN_5, GPIO_MODE_OUT_OD_HISLOW); // 根据实际引脚复用配置 }这里的代码是基于标准外设库风格写的具体引脚和模式要结合你的板子调整。核心思路是先用软件时钟脉冲把所有可能处于异常状态的从设备时钟状态机复位再发一个停止条件让它们释放SDA。这个序列是I2C总线规范里推荐的总线恢复方法对大多数从设备都有效。我实测过在执行完9个时钟脉冲加停止条件后再用I2C外设重新发起通信成功率是100%。注意如果总线上有不支持时钟拉伸的从设备9个脉冲期间该设备可能主动释放总线但没关系我们只是想让它回到空闲状态。4.3 方案C彻底改用GPIO模拟I2C如果项目对成本不敏感或者你已经被硬件I2C折腾得没脾气了还有个终极大法——直接用GPIO模拟I2C时序。STM8S105K6上有充足的GPIO资源随便找两个引脚就能模拟出I2C时序完全绕开硬件模块的状态机问题。GPIO模拟的好处是时序完全由自己控制想怎么等就怎么等不会出现硬件状态机卡死的问题。坏处是CPU占用率稍高但对于绝大多数低速传感器、EEPROM的应用100kHz速率下CPU占用率完全可接受。模拟I2C的代码网上有很多成熟实现这里不贴完整代码只提醒几个关键点引脚要配置为开漏输出并启用内部上拉或者外部上拉。SCL和SDA的电平变化之间要保证足够的建立时间和保持时间标准模式下至少4.7微秒的建立时间。起始条件SCL高电平时SDA从高变低。停止条件SCL高电平时SDA从低变高。读取从设备应答时要把SDA切为输入模式读取完成后切回输出模式。我用GPIO模拟I2C驱动SSD1306 OLED显示模块连续跑了48小时没有出现一次卡死。所以在稳定性优先的场景这个方案可以作为备选。4.4 三种方案的选型建议方案适用场景优点缺点A(等BUSY清零)代码还未完全稳定处于开发调试期改动小最直接如果异常原因多样不一定全面覆盖B(软复位总线恢复)产品已量产需要现场自动恢复能处理各种异常稳定性高需要额外的GPIO操作代码C(GPIO模拟)极端追求稳定对速率要求不高完全避开硬件I2C占用CPU代码量大大多数情况下我会先用A方案做快速修复保证开发进度不受影响然后在发布生产版本前加上B方案的总线恢复逻辑。C方案除非遇到硬件I2C无法解决的棘手问题才启用。5. 驱动代码的健壮性改造预防busy bit卡死的实战建议5.1 每个I2C操作都要有超时机制经验不足的开发者在写I2C驱动时经常用while(!(I2C_SR1 I2C_SR1_SB))这种死等的方式。在正常通信时没问题但在总线上有任何异常时这个while就变成了死循环程序卡死而BUSY位恰好在一边高挂。正确做法是给每个等待条件都设置超时uint8_t I2C_WaitEvent(uint16_t event, uint16_t timeout_ms) { uint16_t timeout timeout_ms * 200; // 粗略换算具体数值需要实际标定 while (timeout--) { if (I2C_GetLastEvent() event) return 0; // 成功 if ((I2C_SR2 I2C_SR2_BUSY) 0 (I2C_SR1 I2C_SR1_SB) 0) break; // 总线已释放但事件仍未发生说明有问题 } return 1; // 超时 }超时时间的选择建议覆盖最大可能的从设备时钟拉伸时间。比如有些温湿度传感器在转换期间会把SCL拉低几十毫秒超时时间至少设到100毫秒才稳妥。如果设得太短正常通信也会被误判为超时。5.2 错误标志的及时清理STM8的I2C在发生错误时往往会置位I2C_SR1中的错误标志位比如AF、BERR、ARLO。如果不及时清除这些标志会干扰后续传输。清除方式通常是读I2C_SR1然后写I2C_CR2或者读I2C_DR。我自己的习惯是每次I2C传输开始前主动检查并清除所有错误标志确保外设处于干净的状态。void I2C_ClearErrorFlags(void) { uint16_t sr1 I2C_SR1; // 如果有错误标志读SR1后写CR2来清除 if (sr1 (I2C_SR1_AF | I2C_SR1_BERR | I2C_SR1_ARLO | I2C_SR1_OVR)) { I2C_CR2 | I2C_CR2_SWRST; // 软复位或者按库函数的具体操作 I2C_CR2 ~I2C_CR2_SWRST; } }5.3 中断模式下的一致性保护如果I2C操作放在中断里执行尤其是主循环里也在操作I2C就可能出现重入问题。两个上下文同时操作I2C_CR2一个置START一个置STOP硬件收到相互矛盾的指令BUSY位卡死的概率会成倍上升。解决方案很简单加互斥锁或者保证同一时刻只有一个上下文操作I2C。我用一个全局变量i2c_busy做互斥标志进中断前检查如果在忙就丢弃本次操作比单纯的关闭中断更灵活。volatile uint8_t i2c_busy 0; uint8_t I2C_StartTransfer(uint8_t devAddr, uint8_t rw) { if (i2c_busy) return 1; // 总线忙直接返回失败 i2c_busy 1; // 发起传输... }5.4 配合硬件设计减少卡死概率软件做了万全准备硬件上也需要配合。上面提到的上拉电阻建议标准模式下用4.7kΩ快速模式400kHz下用2.2kΩ。走线方面SDA和SCL要尽量靠近远离大电流走线和晶振等干扰源。如果板子上有多个I2C从设备建议每个设备都单独加上拉电阻而不是共用一个上拉。多个上拉并联会降低等效电阻这就需要计算一下保证不超出I2C规范的最大IOL容限。另外给I2C供电加一个RC滤波器或者磁珠也能有效抑制电源上的毛刺耦合到总线上。别小看这些硬件细节我遇到过一块板子怎么改代码都偶尔卡死最后发现是I2C跟旁边的DC-DC电感靠得太近信号被干扰了。重新布线后才彻底好。5.5 从实际项目中总结的驱动模板这里给出一份我个人在项目里使用的简易I2C驱动写操作模板它结合了超时、错误标志清零和停止条件等待。读操作类似只是把发送和接收顺序调整一下。// 向从设备写一个字节 uint8_t I2C_WriteByte(uint8_t devAddr, uint8_t regAddr, uint8_t data) { // 1. 等待总线空闲 if (I2C_WaitBusFree(100) ! 0) { I2C_BusRecovery(); return 1; } // 2. 清除残留错误标志 I2C_ClearErrorFlags(); // 3. 发送起始条件 I2C_GenerateSTART(ENABLE); if (I2C_WaitEvent(I2C_EVENT_MASTER_START_SENT, 100) ! 0) { I2C_BusRecovery(); return 2; } // 4. 发送从设备地址写 I2C_Send7bitAddress(devAddr, I2C_DIRECTION_TX); if (I2C_WaitEvent(I2C_EVENT_MASTER_ADDRESS_ACKED, 100) ! 0) { I2C_BusRecovery(); return 3; } // 5. 发送寄存器地址 I2C_SendData(regAddr); if (I2C_WaitEvent(I2C_EVENT_MASTER_BYTE_TRANSMITTED, 100) ! 0) { I2C_BusRecovery(); return 4; } // 6. 发送数据字节 I2C_SendData(data); if (I2C_WaitEvent(I2C_EVENT_MASTER_BYTE_TRANSMITTED, 100) ! 0) { I2C_BusRecovery(); return 5; } // 7. 发送停止条件并等总线释放 I2C_GenerateSTOP(ENABLE); if (I2C_WaitBusFree(100) ! 0) { I2C_BusRecovery(); return 6; } return 0; }这份模板在项目里经受住了长时间运行测试卡死的概率从最初每个小时一次降到了几乎为零。核心思想就一句话每次传输开始前确保总线真正空闲每次传输结束后确保总线上真的发出了停止条件。很多看似玄学的I2C问题根源都是这几个基础点没做好。6. 最后的几点体会STM8S105K6的I2C总线问题归根结底不是芯片有缺陷而是它的I2C模块缺少现代MCU上的那些自动保护和恢复机制。这就像一把没有保险栓的老式步枪好用但需要使用者自己养成严格的操枪习惯。我个人的习惯是在开发初期就把I2C驱动做成带完整错误处理和恢复机制的标准模块而不是先用一个能跑的版本等出了问题再打补丁。因为I2C的异常往往是稀发性的可能跑几个小时才出现一次靠现场调试去抓问题效率太低。把驱动做健壮之后这类问题基本不会在你眼前出现。如果你正在被STM8或者其他老架构MCU的I2C忙标志卡死困扰不妨按这篇文章的顺序走一遍先确认物理总线是否真的空闲再看上一个传输是否完整结束最后补上超时和总线恢复逻辑。大部分情况下这三板斧下去问题就解决了。