修复 ESP-IDF I2C 读取崩溃:FIFO 溢出与信号量竞态的三步补丁
【免费下载链接】esp-idfEspressif IoT Development Framework. Official development framework for Espressif SoCs.项目地址: https://gitcode.com/GitHub_Trending/es/esp-idf
E (4812) i2c.master: I2C transaction timeout detected E (4812) i2c.master: clear bus failed. Guru Meds: core 0 panic'ed (Syscall): HardFault这是 ESP-IDF I2C 主模式读取时 FIFO 边界没守住。
30 秒分诊表:对号入座
四条日志路径,先判断你的崩溃落在哪一格:
| 日志特征 | 最可能根因 | 对应章节 | 置信度 |
|---|---|---|---|
I2C transaction timeout detected | 读长于 FIFO 时分批推进,填充量算错 | 第 4 段 Step 1 | 高 |
clear bus failed. | 状态机复位不彻底,FIFO 残留字节卡死总线 | 第 4 段 Step 2 | 高 |
HardFault,崩溃栈指向i2c_master.c | 溢出写穿接收缓冲区 | 第 4 段 Step 1 | 中 |
| 短读偶发 NACK,长读必超时 | ISR 放信号量后没触发调度,任务迟到 | 第 4 段 Step 3 | 中 |
提示:如果你的日志不在上表里,大概率是时钟配置问题,先看第 4 段 Step 1 里的
I2C_FIFO_LEN取值前提。
FIFO 为什么会在边界翻车:ESP32 I2C 超时与状态机复位的根源
FIFO 就是硬件里一排固定格子:主控制器往格子里塞数据,读长度超过一格容量,就得靠中断分批搬走。
问题出在分批推进时的剩余空间计算。i2c_master.c 的s_i2c_read_command里有这么一行:
// 改前:read_len_static 是上一批已读入 RX FIFO 的字节数 uint32_t fifo_len = I2C_FIFO_LEN(i2c_master->base->port_num); // FIFO 总容量 *fifo_fill = MIN(remaining_bytes, fifo_len - i2c_master->read_len_static); // ← 问题在这fifo_len - read_len_static没有下限保护。read_len_static一旦残留或没清零,这一项持续减小,填充量就会超过 RX FIFO 剩余空间,下一个读进来的字节落在错误位置,随后超时、总线卡死、HardFault 轮番登场。
三步补丁:i2c_master.c 修复的 FIFO 边界钳位、状态机复位与信号量竞态
说明:以下补丁展示修复思路,仓库为只读代码,落地时请在你本地检出的同版本 ESP-IDF 树内操作。
Step 1 | FIFO 填充量先做下限钳位,溢出挡在赋值前定位:components/esp_driver_i2c/i2c_master.c →s_i2c_read_command
// 改前 *fifo_fill = MIN(remaining_bytes, fifo_len - i2c_master->read_len_static); // 改后 size_t fifo_available = fifo_len - i2c_master->read_len_static; if (i2c_master->read_len_static >= fifo_len) { fifo_available = 0; } *fifo_fill = MIN(remaining_bytes, fifo_available);为什么:fifo_len - read_len_static可能为负,MIN挡不住负数,只剩显式钳零。 ⚠️ 坑:read_len_static是跨批次共享状态,别顺手把它挪进局部作用域。
Step 2 | 状态机复位时连 FIFO 和中断一起清,杜绝残留字节定位:components/esp_driver_i2c/i2c_master.c →s_i2c_hw_fsm_reset
// 改前:只复位状态机 i2c_ll_master_fsm_rst(hal->dev); // 改后 i2c_ll_master_fsm_rst(hal->dev); i2c_hal_disable(hal, I2C_HAL_IN_RXFIFO); // 清 RX FIFO i2c_hal_disable(hal, I2C_HAL_IN_TXFIFO); // 清 TX FIFO i2c_ll_clear_intr_mask(hal->dev, I2C_LL_INTR_MASK); // 清脏中断标志为什么:超时后复位,上一轮卡在 FIFO 的字节和脏中断标志会让下一次传输直接进错状态。 ⚠️ 坑:清 FIFO 前确认传输已停,否则清的是正在使用的数据。
Step 3 | ISR 放完信号量必须触发调度,别让任务迟到定位:components/esp_driver_i2c/i2c_master.c →s_i2c_read_command
// 改前 if (xPortInIsrContext()) { xSemaphoreGiveFromISR(i2c_master->cmd_semphr, do_yield); // 只置位,无人接管 } else { xSemaphoreGive(i2c_master->cmd_semphr); } // 改后 if (xPortInIsrContext()) { xSemaphoreGiveFromISR(i2c_master->cmd_semphr, do_yield); if (*do_yield == pdTRUE) { portYIELD_FROM_ISR(*do_yield); } } else { xSemaphoreGive(i2c_master->cmd_semphr); }为什么:do_yield置位后 ISR 返回若不执行portYIELD_FROM_ISR,等信号量的任务抢不到 CPU,RX FIFO 继续灌数据,溢出。 ⚠️ 坑:do_yield是调用方传入的指针,改前后类型保持一致,别在任务上下文误调portYIELD_FROM_ISR。
回归清单:验证 I2C 主模式 FIFO 溢出修复是否生效
按顺序跑,每一项都要留串口日志:
- 短读 32 字节(一次 FIFO 以内),重复 100 次(预期:无 timeout 日志)
- 长读 256 字节,超过 FIFO 容量,重复 100 次(预期:数据逐字节校验通过)
- 循环读写混合 1000 轮,含连续读命令链(预期:无 HardFault,计数完整)
- 总线挂死后注入超时,调用 reset 再来一轮传输(预期:
clear bus failed.不再出现,复位后传输正常) - 同总线挂两个从设备交替读(预期:无 NACK 风暴,两设备数据均正确)
⚠️ 备忘:
- 该组改动已随上游合入 v5.1.2,升级前 diff 一下
i2c_master.c,别重复打补丁。 - 补丁只适配当前 IDF 版本的
i2c_hal接口,跨版本照搬会编译不过。
跑完清单后,把五项结果和串口日志贴到对应 issue,附你的 IDF 版本号。
【免费下载链接】esp-idfEspressif IoT Development Framework. Official development framework for Espressif SoCs.项目地址: https://gitcode.com/GitHub_Trending/es/esp-idf
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考