ARTICLE DETAIL

资讯详情

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

STM32G0 I2C通信异常排查:NACK与总线死锁实战解析

STM32G0 I2C通信异常排查:NACK与总线死锁实战解析 做嵌入式久了基本都跟 I2C 打过交道。这个协议看着简单——两根线、开漏、上拉、一个起始位加一个停止位但真到了 STM32G0 上通信异常排起来一点都不轻松。最近翻到一份应用笔记 LAT1490里面正好记录了 STM32G0 两个 I2C 通信异常的案例一个是在 400kHz 高速模式下偶发 NACK另一个是总线被异常拉死后直接“一睡不起”。这两个问题都不是一上来就能看出原因的排查过程很有代表性。这篇文章我会结合自己对 STM32G0 I2C 外设的理解把两个案例的现象、定位思路、根因和解决办法完整拆一遍也会顺便聊聊那些“常规文档里不会告诉你”的小坑希望对正在调试 G0 I2C 的朋友有点帮助。1. 先搞懂 STM32G0 的 I2C 外设“脾气”1.1 从 F1 到 G0I2C 外设变化很大如果你是从 STM32F1 时代过来的用 G0 的 I2C 第一反应可能是“怎么这么多寄存器”。F1 的 I2C 简单粗暴控制逻辑基本靠软件配合状态位来推动而 G0 上的 I2C 是一个完整的状态机外设支持超时检测、PEC、SMBus、时钟延展等一堆高级功能。好处是 CPU 干预少了坏处是初始化参数如果不匹配通信就会呈现出各种“玄学”问题。比如 G0 用 TIMINGR 这一个寄存器来定义 SCL 高低电平和建立保持时间不再像 F1 那样用 CCR 简单分频。很多工程师照抄例程里的 TIMINGR 数组却忽略了时钟源频率的差异最后 SCL 实际频率偏差很大高速模式下一错再错。另外一个容易被忽略的点是STM32G0 的 I2C 引脚默认不是开漏输出。I2C 协议本身要求开漏结构但如果 GPIO 初始化时没有配置成AF_OD而是配成了推挽输出那总线上的“线与”逻辑就失效了。两个设备一个想拉低、一个想释放推挽输出直接怼上轻则通信乱码重则烧引脚。所以排查 G0 I2C 问题第一步永远是确认 GPIO 模式。1.2 最容易出问题的三个点根据我自己的经验G0 I2C 通信异常几乎都集中在三个点上时序参数与实际电气环境不匹配导致建立/保持时间不足总线状态机卡死SDA 被拉低或者总线上残留上一次传输的状态中断/事件处理不合理比如从机中断里耗时太长或者在错误回调里没有正确清理标志。这三个点不是独立的经常互相纠缠。第一个案例偏重时序问题第二个案例偏重总线状态和处理机制问题。应用笔记 LAT1490 也正好把两个比较典型的场景放在了一起后面我会分别展开。2. 案例一400kHz 下偶发失败示波器一看是上升沿“慢半拍”2.1 现象100k 正常400k 随机 NACKLAT1490 里第一个案例是这样的系统里一颗 STM32G071 作为主机另一颗 STM32G031 作为从机I2C 速率配置成 400kHz。单次读写正常压力测试跑几十次就会冒出一个 NACK然后重试又能成功看起来毫无规律。把速率降到 100kHz连续跑一天也没问题。这种“高速就不稳定”的现象在嵌入式现场太典型了。一开始很多人都怀疑是代码逻辑问题结果逻辑分析仪一抓——问题在波形上。我拿到这个案例后在自己的板子上复现了同样的现象。示波器抓到的情况是SCL 低电平时间正常高电平时间也正常但 SCL 上升沿明显“软”从低到高的爬升不是陡峭的方波而是一条平缓的斜线。SDA 上的数据在时钟上升沿附近有建立时间不足的情况偶尔被从机采样错了导致返回 NACK。这种波形如果不刻意去看上升沿很容易以为时序完全正常因为频率计量的 SCL 频率确实也接近 400kHz。2.2 定位上升时间与 TIMINGR 的匹配关系为什么 SCL 上升沿会变软因为 I2C 是开漏结构输出低电平靠管子拉低释放后高电平靠外部上拉电阻把线路拉高。这个拉高过程本质上是一个 RC 充电过程时间常数由线上总电容和上拉电阻决定。如果上拉电阻太大或者总线的等效电容太大上升时间就会很长。I2C 规范里对上升时间有明确要求快速模式400kHz下上升时间一般不超过 300ns而如果实测上升时间已经超过 1us那无论怎么调 TIMINGR都无法满足协议要求。这里需要特别说明一下 TIMINGR 的作用。TIMINGR 里的 SCLH 和 SCLL 配置的是 SCL 高电平和低电平的持续时间它们不会改变物理上升沿。有些工程师以为把 SCLH 调大一点就能解决高速不稳定实际上只是变相降低了 SCL 频率并没有解决电气问题。那个案例的确认路径是示波器量 SCL 上升沿发现超过 1us同时检查原理图发现上拉电阻选用了 10kΩ而总线上除了两颗板子还有测试夹具和杜邦线等效电容不小。10kΩ 在这种负载下把上升时间拉到了非常危险的水平。2.3 处理换电阻 重算时序参数解决思路其实很朴素把上拉电阻从 10kΩ 换成 2.2kΩ然后再用 STM32CubeMX 重新生成 TIMINGR。CubeMX 里配置 I2C 时会要求填入 I2C 内核时钟频率和总线速率然后自动生成时序参数。但问题在于它生成的是理想参数不会考虑你的上拉电阻和线上电容。如果你手头没有准确的电气参数一个务实的办法是留出设计余量。比如目标 400kHz实际用 CubeMX 生成时按 380kHz 左右配置或者手动把 SCLL 稍微调大一点让低电平时间富余一些。改完电阻和时序后SDA、SCL 的波形明显“方正”了压力测试跑了十几万次也没再出现 NACK。具体计算时序寄存器时需要先知道 I2C 内核时钟。假设用 HSI16 作为 I2C 时钟源那么 I2C 内核时钟是 16MHz。要得到 100kHz 的总线速率SCL 周期是 10us对应 160 个时钟周期。TIMINGR 的低七位 SCLL 和高八位 SCLH 需要分配这些周期同时还要满足数据建立时间 SDADEL 和数据保持时间 SCLDEL 的要求。手工算起来比较繁琐所以我建议直接用 CubeMX 生成初值再用逻辑分析仪实测频率。如果实测频率偏差较大再微调 SCLL 和 SCLH。千万不要直接照抄另一块板子上的 TIMINGR 值——同样的值在 16MHz 和 32MHz 时钟下产生的 SCL 频率完全不一样。2.4 这类问题在量产中如何提前发现这类问题最大的坑在于样机阶段可能只有一两块板子上拉电阻用了 4.7k总线很短一切正常到量产时线路变长、多接几个负载同样的固件就出问题。所以对于 I2C 总线一定要用示波器实测上升时间并留足裕量。我见过有的项目在 PCB 上预留了上拉电阻的位置根据不同批次负载做微调这种做法在 I2C 上很实用。另外对于 STM32G0 的 I2CGPIO 内部可以开启上拉但我强烈建议外部加上拉。内部上拉一般为 40kΩ 左右对于 400kHz 的高速模式基本不够用只在 100kHz 且线很短时偶尔用一下还行。3. 案例二总线被拉死代码里没留“后门”导致通信永久中断3.1 现象异常后 SDA 长期为低第二个案例更典型。某设备上STM32G0 作为 I2C 主机与一个外部传感器通信。正常运行一段时间后I2C 总线突然卡住主机再也不发起后续传输。重新上电才恢复。用万用表量 SDA发现是低电平。逻辑分析仪抓到的是主机发完最后一个数据后SCL 正常但 SDA 被从机一直拉低。主机在发送下一个起始位时无法完成——因为 I2C 协议中 START 条件要求 SDA 在 SCL 为高时从高变低总线已经被拉死主机自然发不出任何东西。为什么 SDA 会被一直拉低常见原因有几种从机在传输中途收到异常信号导致内部状态机卡在某个输出数据为 0 的状态主机在传输未完成时被复位从机以为还在传输中或者总线上有干扰让某个设备错误地认为自己是接收方且需要持续拉低 SDA。在 G0 作为从机时如果它的 I2C 中断没有及时处理地址匹配后它会自动拉低 SCL 进行时钟延展但 SDA 不会。而如果从机正在发送数据并且它认为自己发送的每一位都是 0就会出现 SDA 持续为低的情况。3.2 根因缺少总线恢复机制回到 LAT1490 记录的案例。原应用笔记里的根因是主机的错误处理里只调用了HAL_I2C_DeInit和重新初始化但底层 I2C 外设并没有真正做“总线恢复”动作。总线恢复在 I2C 协议里指的是当 SDA 被拉低超过一定时间主机应该产生 9 个 SCL 脉冲让卡住的从机释放 SDA然后再发送一个 STOP 条件。很多开发者没有实现这一步直接重启 I2C 外设结果总线上的从机状态并没有复位重启后依然认为主机在传输中导致 SDA 继续被拉低。我在自己的项目里也遇到过类似的坑主机检测到 BUSY 后调用HAL_I2C_Reset去复位外设但总线上仍挂着 SDA 低电平外设复位并没有让远端从机恢复。只有把 SCL/SDA 引脚手动配成普通 GPIO模拟输出 9 个 SCL 时钟脉冲并在此期间观察 SDA 是否释放然后发送 STOP 条件最后再重新初始化 I2C通信才恢复。这种做法在嵌入式圈子里不算新鲜但很多使用 HAL 库的工程师会忽略它因为 HAL 库没有提供现成的“ResetBus”函数。3.3 修复增加总线恢复流程与错误处理正确的修复方式分两步。第一步在检测到总线卡死时由主机尝试恢复总线。核心代码示意如下void I2C_BusRecover(I2C_HandleTypeDef *hi2c) { GPIO_InitTypeDef gpio; // 先停掉 I2C 外设 HAL_I2C_DeInit(hi2c); // 将 SCL 和 SDA 引脚配置为开漏输出初始都为高 gpio.Mode GPIO_MODE_OUTPUT_OD; gpio.Pull GPIO_PULLUP; gpio.Speed GPIO_SPEED_FREQ_LOW; gpio.Pin SCL_PIN; HAL_GPIO_Init(SCL_PORT, gpio); gpio.Pin SDA_PIN; HAL_GPIO_Init(SDA_PORT, gpio); HAL_GPIO_WritePin(SCL_PORT, SCL_PIN, GPIO_PIN_SET); HAL_GPIO_WritePin(SDA_PORT, SDA_PIN, GPIO_PIN_SET); delay_us(10); // 产生 9 个 SCL 脉冲期间观察 SDA 是否释放 for (int i 0; i 9; i) { HAL_GPIO_WritePin(SCL_PORT, SCL_PIN, GPIO_PIN_RESET); delay_us(5); HAL_GPIO_WritePin(SCL_PORT, SCL_PIN, GPIO_PIN_SET); delay_us(5); if (HAL_GPIO_ReadPin(SDA_PORT, SDA_PIN) GPIO_PIN_SET) { // SDA 已释放可以提前结束 break; } } // 产生 STOP 条件SDA 在 SCL 高电平期间拉高 HAL_GPIO_WritePin(SCL_PORT, SCL_PIN, GPIO_PIN_SET); HAL_GPIO_WritePin(SDA_PORT, SDA_PIN, GPIO_PIN_RESET); delay_us(5); HAL_GPIO_WritePin(SCL_PORT, SCL_PIN, GPIO_PIN_SET); delay_us(5); HAL_GPIO_WritePin(SDA_PORT, SDA_PIN, GPIO_PIN_SET); delay_us(5); // 恢复引脚复用模式 MX_I2C1_Init(); }实际代码要注意引脚映射、延时精度和总线上其他设备的响应时间。上面只是恢复流程的骨架生产环境里还需要加超时保护避免程序卡在恢复流程中。第二步在应用层做超时重试当一次 I2C 读写返回错误且总线 BUSY 时调用恢复流程而不是直接返回失败。同时如果是多主环境还要注意总线仲裁问题不过在这个案例里是单主机恢复流程就够用了。3.4 补充中断优先级和防抖对总线卡死的影响案例二还有一个细节如果从机也会主动拉低 SDA比如从机是 G0 且配置了接收模式那么从机的中断响应时间会直接影响总线恢复的成功率。在从机中断被更高优先级任务长时间阻塞时从机 I2C 事件在很晚之后才处理可能导致一些异常状态。所以调试这类问题时不妨检查一下 I2C 中断优先级是否合理。STM32G0 上中断优先级寄存器是 4 位建议把 I2C 事件中断放在较高优先级避免和阻塞型延时操作冲突。也可以在 SDA/SCL 线上加几十 pF 的小电容滤波减少干扰引起的错误状态机切换但电容不能太大否则会拖慢上升沿跟案例一形成矛盾。这是 I2C 设计中常见的取舍。4. I2C 异常排查的“三板斧”和速查表4.1 波形看什么排查 I2C 异常最重要的工具是示波器或逻辑分析仪。别一上来就翻代码先看波形。重点关注几个位置起始条件是否满足 SDA 在 SCL 高电平期间由高到低地址字节的第 8 位R/W是否正确第 9 个 SCL 周期上 SDA 是否有 ACK 低电平数据字节是否完整停止条件是否出现。如果数据线上出现毛刺或者上升沿明显缓慢优先怀疑电气问题。如果时序看起来完全正常但从机就是不 ACK那问题在从机侧比如地址不匹配、从机未初始化完成、从机芯片本身损坏。4.2 寄存器读什么在 STM32G0 上I2C 的 ISR 寄存器包含了一堆状态位比如 TXE发送缓冲空、RXNE接收缓冲非空、ADDR地址匹配、NACKF收到 NACK、BUSY总线忙、ARLO仲裁丢失、BERR总线错误、OVR上溢/下溢。排查问题时在出错中断里读 ISR并记录下错误标志是最高效的做法。很多开发者在 HAL 错误回调里只返回错误码但忘了把 ISR 值读出来保存导致定位困难。正确的做法是在错误回调中先读 ISR 和 ICR清干净标志再根据标志分支处理。4.3 常见问题速查表我放一个临时整理的速查表基本覆盖了 STM32G0 I2C 的常见问题现象可能原因排查方向完全无 ACK从机地址错、从机未上电、总线电容过大检查地址、示波器看波形偶发 NACK时序裕量不足、上拉电阻过大测上升时间、调 TIMINGRSDA 一直为低从机状态机卡死、总线恢复缺失模拟 9 脉冲恢复发送后无 STOPDMA 配置错误、NACK 处理不当检查 DMA 传输完成回调偶尔 BUSY上次传输未正确结束、超时参数太小读 BUSY 标志、增加超时多字节丢失RXNE 没及时清、缓冲区溢出检查中断服务函数耗时这个表格在项目中可以直接作为排查手册。5. 几个 STM32G0 I2C 工程的“保命”习惯5.1 GPIO 配置开漏、复用、上拉一个都不能少I2C 引脚必须配置成复用开漏模式同时外部上拉必须加。有些工程师习惯把引脚配置成推挽输出然后去模拟 I2C这在低速下可能能跑但在 G0 上如果跟硬件 I2C 外设混用会出大问题。每次初始化 I2C 前建议用逻辑分析仪确认 SCL 和 SDA 的默认电平是否为高。如果上电后 SDA 是低先别查代码看看是不是有从机在乱拉总线。5.2 时钟与时序参数尽量由工具生成并实测验证TIMINGR 不要凭空写。使用 STM32CubeMX 或 STM32CubeG0 库里的工具生成时序参数然后通过实测 SCL 频率验证。如果你用内部 HSI16 作为内核时钟要注意 HSI16 的精度可能不够在对时序要求严格的场合建议使用外部晶振或校准 HSI。根据我的经验同一个固件在不同的板子上跑出不同的 SCL 频率多半是内核时钟源配置不一致导致的。这一点在从 G0 迁移到 G0B1 或 G0C1 时也要特别注意不同型号的时钟树有差异。5.3 错误处理把异常当常态I2C 是半双工、多设备共享总线出错是必然的。固件里一定要有超时和重试机制。我习惯把每次 I2C 操作包裹在带超时的函数里超时后尝试恢复总线并重试重试 2~3 次后再上报错误。这也是案例二给我们的核心教训不要指望一次成功更不要失败后愣在原地。另外HAL 库的默认超时参数有时会偏大建议根据实际总线长度和通信量调整不要让一次失败卡住整个控制循环。5.4 低功耗场景下的额外注意如果设备会进入 Stop 模式在进入低功耗前需要确保 I2C 总线处于空闲状态否则从机可能卡住。从 Stop 唤醒后I2C 外设可能需要重新初始化尤其是从机在睡眠期间如果收到了一个非法字节唤醒后很难自动恢复。G0 的 I2C 也有复杂的时钟门控和异步信号处理建议参考 ST 的参考手册和低功耗应用笔记。这里想强调一点低功耗模式下的 I2C 问题往往比正常运行更难排查因为它跟时钟开启时序、唤醒时间和外设复位条件都强相关最好在硬件设计阶段就把 I2C 的上拉电源和处理方式规划好比如确认总线设备是否能在主机睡眠期间保持静默。最后再分享一个我个人调试 I2C 的感受不要迷信“软件上包打天下”。I2C 通信是物理层、协议层和逻辑层一起协作的事情很多看似软件的问题最后都出在电阻、电容、引脚复用这些容易忽略的细节上。这两个案例归根到底一个是电气裕量不足一个是恢复机制缺失。把这两个方向想透STM32G0 上的 I2C 通信能稳一大半。希望这篇拆解对你有帮助。
返回列表