STM32 I²C 通信卡在错误 121?别急,我们一步步把它“抓”出来 🛠️
你有没有遇到过这样的场景:STM32 的代码写得一丝不苟,CubeMX 配置也导出无误,传感器明明接上了、电源正常、线也没接反——可一调 HAL_I2C_Master_Transmit() ,返回值就是 121 ?
没错,那个让人头皮发麻的数字。
不是超时,不是忙状态,也不是总线冲突。它悄无声息地告诉你:“我没收到 ACK。”
但问题是—— 谁没回 ACK?为什么没回?是硬件问题还是软件搞错了节奏?
今天我们就来把这个问题彻底扒开,从物理层到寄存器,从上拉电阻到 Timing 配置,不靠猜、不靠试,用工程思维一步步定位这个“幽灵故障”。
从一个真实项目说起 🔍
几个月前,我在调试一块基于 STM32F407VG 的环境监测板,主控要通过 I²C 读取两个设备:
- SHT30(温湿度传感器,地址 0x44)
- AT24C02(EEPROM,地址 0x50)
I²C 总线走线很短,不到 5cm,电源稳定 3.3V,所有 GND 都连在一起。看起来天时地利人和。
结果呢?SHT30 偶尔能通,AT24C02 根本不通, HAL_I2C_GetError(&hi2c1) 返回的永远是 121 。
打开逻辑分析仪一看:每次发送完设备地址后,SDA 在第9个时钟周期根本没被拉低 —— 没有 ACK。
也就是说,主机喊:“嘿!你是 0x50 吗?”
没人答应。
于是主控翻脸: AF=1 ,中断触发,HAL 层报错,函数返回 HAL_ERROR ,再一查错误码 → 121。
这事儿听起来简单,但如果你只盯着代码看,很容易陷入死循环:“我地址没错啊!”、“难道芯片坏了?”、“是不是 HAL 库有 bug?”
其实真相往往藏在细节里。
错误代码 121 到底是什么?🔢
先澄清一个常见的误解: 121 并不是一个标准定义的宏 ,比如你不会在 stm32f4xx_hal_i2c.h 里直接找到 #define HAL_I2C_ERROR_121 。
那它是怎么来的?
我们来看 HAL 库的设计逻辑:
#define HAL_I2C_ERROR_NONE (0x00U)
#define HAL_I2C_ERROR_BERR (0x01U)
#define HAL_I2C_ERROR_ARLO (0x02U)
#define HAL_I2C_ERROR_AF (0x04U) // <-- 注意这里!
#define HAL_I2C_ERROR_OVR (0x08U)
// ...
这些错误码是按位定义的。当发生多种错误时,会进行“或运算”合并。
但在某些情况下,尤其是使用了自定义日志系统、RTOS 错误封装、或者 CubeMX 自动生成的错误处理回调中,开发者可能会将整个 hi2c->ErrorCode 打印成整数。
而 HAL_I2C_ERROR_AF 单独出现时应该是 4,但如果和其他标志组合呢?
等等……你说 121?那可能是 多个错误叠加后的结果 !
让我拆解一下:
👉 121 = 0x79 = 0b01111001
如果我们假设每一位代表一种错误类型:
| Bit | 对应错误 |
|---|---|
| 0 | ? |
| 3 | OVR? |
| 4 | AF ← 关键! |
| 5 | TIMEOUT? |
| 6 | BUSY? |
| 7 | ERROR? |
但等等,HAL 库中的错误码通常只占低字节,高字节用于保留或其他模块。
所以更合理的解释是: 你在某个地方看到的“121”,其实是调试打印时误用了变量类型,或者是第三方库对错误进行了重新编码。
📌 真相大白:
绝大多数情况下,“错误 121” 实际对应的就是
HAL_I2C_ERROR_AF—— 应答失败(No Acknowledge Received) 。
你可以这样验证:
if (HAL_I2C_Master_Transmit(&hi2c1, DevAddr << 1, pData, Size, 100) != HAL_OK) {
uint32_t err = HAL_I2C_GetError(&hi2c1);
if (err & HAL_I2C_ERROR_AF) {
printf("ACK Failure detected!\n"); // 👉 就是你!
}
}
只要这一位被置起,说明从机压根就没回应。
接下来的问题变成了: 为什么没回应?
物理层才是第一战场 ⚡
很多工程师习惯性地先改代码、换速率、加延时,却忘了最基础的一点: I²C 是个模拟协议 。
它依赖的是电平变化、上升时间、负载匹配、噪声抑制……这些都是模拟世界的规则。
上拉电阻:不能省,也不能乱来
I²C 使用开漏输出(Open-Drain),意味着任何一个设备只能“拉低”信号,不能主动“推高”。
要把 SCL 和 SDA 拉回高电平,全靠外部上拉电阻。
如果没有上拉?
→ 总线永远处于低电平或浮动状态 → START 条件无法识别 → ACK 自然也不会有。
哪怕你启用了 STM32 的内部上拉( GPIO_PULLUP ),也不行。
因为内部上拉电阻阻值太大,一般在 30kΩ ~ 50kΩ 范围内,导致上升沿太慢,尤其是在 Fast Mode(400kHz)下,根本来不及升到高电平就被下一个时钟采样了。
🔍 实测数据对比(示波器截图脑补 😅):
| 上拉方式 | 上升时间 Tr | 是否满足 I²C 规范(≤1000ns) |
|---|---|---|
| 内部上拉(40kΩ) | ~3.2 μs | ❌ 严重超标 |
| 外部 10kΩ | ~800 ns | ✅ 边缘合格(适合标准模式) |
| 外部 4.7kΩ | ~350 ns | ✅ 完美支持 Fast Mode |
结论很明显: 必须外接上拉电阻!
推荐参数(3.3V 系统):
- 标准模式(100kHz) :4.7kΩ ~ 10kΩ
- 快速模式(400kHz) :4.7kΩ 最佳
- 长距离或多设备 :考虑 2.2kΩ ~ 3.3kΩ,但注意功耗
⚠️ 切记不要低于 2kΩ,否则电流过大,每个低电平周期都会消耗约 1.6mA(3.3V / 2k = 1.65mA),多设备同时通信时可能烧毁 IO 口。
总线电容:隐形杀手 💣
I²C 规范明确规定: 总线电容不得超过 400pF 。
这是为了保证上升时间可控。
每厘米 PCB 走线大约引入 1~2pF 电容,加上连接器、探针、设备输入电容(典型值 10pF/设备),很容易超标。
举个例子:
- 3 个传感器 × 10pF = 30pF
- 15cm 走线 × 1.5pF/cm = 22.5pF
- 示波器探头 × 10pF = 10pF(调试时特别容易忽略!)
- 总计 ≈ 62.5pF → 已接近极限!
此时即使用了 4.7kΩ 上拉,Tr = 0.845 × R × C = 0.845 × 4700 × 62.5e-12 ≈ 248ns ,看似没问题……
但别忘了,这是理想计算。实际中还有分布电感、串扰、电源波动等因素。
一旦超过 400pF,上升沿拖尾严重,可能导致:
- 主机误判为“假 START”
- 采样点落在不稳定区域 → 读错数据位或误判 ACK/NACK
✅ 解决方案:
- 缩短走线长度
- 减少设备数量(或使用 I²C 多路复用器如 PCA9548A)
- 加入缓冲器(如 NXP 的 PCA9515A,支持长达 20 米传输)
- 调试时拔掉示波器探头再测试
地址问题:你以为你知道,其实你不知道 🧩
另一个高频陷阱: 地址错位 。
I²C 地址有两种形式:
- 7-bit 地址 :如 SHT30 是 0x44
- 8-bit 地址 :分为写地址(0x88)和读地址(0x89)
HAL 库的 API 如 HAL_I2C_Master_Transmit() 第一个参数要求传入的是 8-bit 设备地址 。
也就是说,你要传的是 0x44 << 1 | 0 = 0x88 ,而不是 0x44 !
常见错误写法:
// ❌ 错误!传了 7-bit 地址
HAL_I2C_Master_Transmit(&hi2c1, 0x44, data, len, 100);
// ✅ 正确做法
#define SHT30_ADDR 0x44
HAL_I2C_Master_Transmit(&hi2c1, (SHT30_ADDR << 1), data, len, 100);
有些厂商的数据手册会在首页写清楚:“Device Address: 1000100x”,其中 x 是 R/W 位。
如果你忽略了最后一位,就会导致地址完全不对,自然收不到 ACK。
💡 小技巧:写一个 I²C 扫描函数,自动探测总线上有哪些设备响应:
void I2C_ScanBus(I2C_HandleTypeDef *hi2c) {
printf("Scanning I2C bus...\n");
for (uint8_t addr = 0; addr < 128; addr++) {
if (HAL_I2C_IsDeviceReady(hi2c, addr << 1, 1, 10) == HAL_OK) {
printf("Device found at 0x%02X\n", addr);
}
}
}
运行一下,如果什么都没扫到,那就不是代码问题,而是物理连接或供电问题。
初始化配置:Timing 字段的秘密 🔐
STM32 的硬件 I²C 控制器非常强大,但也极其敏感。其中一个关键参数就是 hi2c->Init.Timing 。
这个 32 位值包含了:
- SCL 上升时间与下降时间
- 数字滤波器阈值
- 高低电平持续时间
- 建立/保持时间
如果设置不当,哪怕只差几个纳秒,也可能导致采样时机偏移,从而把本来存在的 ACK 误判为 NACK。
如何获得正确的 Timing?
最佳实践: 用 STM32CubeMX 自动生成 。
你只需要选择目标频率(如 100kHz)、APB1 时钟源(比如 8MHz),CubeMX 就会算出合适的 Timing 值。
例如:
hi2c1.Init.Timing = 0x2010091A; // 100kHz, APB1=8MHz, Rise=250ns, Fall=10ns
但如果你手动移植代码、换了主频、或者复制别人的工程没改时钟树……这个值就可能失效。
📌 曾经有个项目,客户坚持要用 12MHz 外部晶振驱动 HSE,然后 PLL 倍频到 168MHz,APB1 分频为 5 → 得到 33.6MHz。
但他沿用了原来 8MHz 系统下的 Timing 值 → 结果 I²C 几乎从不成功。
解决方法:
- 打开 ST 官方 I²C Timing Calculator
- 输入你的实际时钟参数
- 导出新的 Timing 值替换进代码
或者干脆放弃手调,全程使用 CubeMX 配置并生成初始化代码。
GPIO 配置:别让“默认设置”坑了你 🛑
再来看看底层 GPIO 配置。
你有没有检查过自己的 MSP 初始化函数?
void HAL_I2C_MspInit(I2C_HandleTypeDef *hi2c) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_I2C1_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_OD; // 必须是开漏复用!
GPIO_InitStruct.Pull = GPIO_PULLUP; // 可选,辅助作用
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF4_I2C1;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}
重点来了:
-
Mode必须是GPIO_MODE_AF_OD,不能是GPIO_MODE_OUTPUT_PP(推挽输出会破坏总线仲裁) -
Pull设为GPIO_PULLUP可以增强抗干扰能力,但它只是“弱上拉”,无法替代外部电阻 -
Alternate必须正确,I2C1 通常是 AF4,但不同系列可能不同(F1 是 AF4,L4 是 AF4,H7 可能是 AF4 或 AF1,要看 datasheet!)
📌 特别提醒:某些开发板为了节省元件,把 SCL/SDA 直接连到板载传感器,并且只靠 MCU 内部上拉工作。这种设计在实验室环境下勉强可用,但在工业现场极易受干扰。
电源与时序:谁先醒?谁后说话?⏰
还有一个常被忽视的问题: 上电时序 。
假设你的 STM32 启动速度很快,Reset 释放后立即执行 MX_I2C1_Init() 并尝试通信,而此时 SHT30 还在上电复位过程中(典型延迟 15ms),会发生什么?
答案是: 主机发地址,从机还没准备好,自然不回应 ACK 。
解决方案很简单:
MX_I2C1_Init();
HAL_Delay(20); // 给所有 I²C 设备留足启动时间!
if (HAL_I2C_IsDeviceReady(&hi2c1, (0x44 << 1), 3, 5) != HAL_OK) {
Error_Handler(); // 可选:重试机制
}
此外,有些 EEPROM(如 AT24C02)在写操作完成后需要内部擦除时间(最大 10ms),在此期间不会响应任何新请求。
如果你连续写两页数据中间没有等待,就会遭遇“突然失联”的情况。
✅ 建议做法:
- 对于 EEPROM 类设备,在每次写入后加入
HAL_Delay(10)或使用轮询方式检测是否 ready:
while (HAL_I2C_Master_Transmit(&hi2c1, dev_addr, NULL, 0, 100) != HAL_OK) {
; // Wait until EEPROM finishes writing
}
如何高效诊断?工具比经验更重要 🧰
光靠脑子想不如动手测。
以下是几种高效的诊断手段:
1. 逻辑分析仪(必用!)
推荐 Saleae Logic Pro 8 或低成本替代品(如 Kingst VIS)。
抓一次完整的 I²C 波形,你能立刻看出:
- 是否有 START/STOP
- 地址是否正确
- 数据是否匹配
- 第9位是否有 ACK
一眼定生死。
2. 示波器双通道观察 SCL & SDA
看上升沿是否陡峭,有无振铃、过冲、毛刺。
如果有反射现象,说明阻抗不匹配,建议增加串联电阻(22Ω~47Ω)靠近 MCU 输出端。
3. 万用表测量上拉电压
空闲状态下,SDA/SCL 应该接近 VDD(3.3V)。
如果只有 2.x V,说明上拉不足或存在漏电。
4. 使用 I²C Scanner 工具
可以是自己写的代码,也可以是开源固件(如 Arduino 的 I2CScanner),快速确认设备是否存在。
设计阶段的最佳实践 ✅
与其出了问题再修,不如一开始就做对。
PCB 布局建议:
- SCL 与 SDA 尽量等长,避免差分延迟
- 远离 SPI、USB、DC-DC 开关节点等高频干扰源
- 星型拓扑优于菊花链(尤其多设备时)
- 所有 I²C 设备共地,且接地路径尽量短而宽
硬件选型建议:
| 项目 | 推荐做法 |
|---|---|
| 上拉电阻 | 使用 4.7kΩ ±1%,贴片电阻,靠近主控放置 |
| 多设备总线 | 总设备数 ≤ 4;否则加缓冲器或 mux |
| 长距离通信 | >30cm 时使用专用 I²C 隔离器(如 PCA82C250) |
| 电平转换 | 不同电压域间使用双向电平转换器(如 TXS0108E) |
软件设计建议:
- 初始化后加入全局延时(≥20ms)
- 所有 I²C 访问封装成带重试机制的函数(最多 3 次)
- 添加超时保护(避免死等)
- 出错时自动复位 I²C 外设(关闭时钟 → 重新初始化)
示例封装函数:
HAL_StatusTypeDef I2C_WriteWithRetry(I2C_HandleTypeDef *hi2c,
uint16_t devAddr,
uint8_t *pData,
uint16_t size,
uint32_t timeout,
uint8_t max_retries) {
uint8_t attempt = 0;
HAL_StatusTypeDef status;
do {
status = HAL_I2C_Master_Transmit(hi2c, devAddr, pData, size, timeout);
if (status == HAL_OK) break;
uint32_t err = HAL_I2C_GetError(hi2c);
if (!(err & HAL_I2C_ERROR_AF)) break; // 非 AF 错误不再重试
HAL_Delay(5);
attempt++;
} while (attempt < max_retries);
return status;
}
最后一点思考:为什么 HAL 库不能自动修复这些问题?🤔
这是一个好问题。
理论上,HAL 库可以做更多事情,比如:
- 自动检测总线是否空闲
- 发现 AF 后尝试软重启
- 动态调整 Timing 参数
但现实是: 嵌入式系统讲究确定性和可控性 。
如果库函数偷偷帮你“修复”了问题,反而会让开发者失去对系统的掌控感。比如某次通信失败是因为传感器掉电,而 HAL 自动重试成功了——表面上看一切正常,实际上隐患仍在。
所以,HAL 的设计哲学是:“暴露问题,而非掩盖问题。”
作为工程师,我们要做的不是抱怨“为什么这么难用”,而是学会解读它的语言:
→ HAL_ERROR 是警报,
→ AF=1 是线索,
→ 121 是代号,
而真正的答案,在电路板上,在示波器里,在每一个精心设计的细节之中。
🔧 总结一下实战口诀,方便记忆:
“一查上拉,二看地址,三测电平,四抓波形,五验Timing,六加延时,七做重试”
记住,I²C 不复杂,但它要求严谨。
下次当你看到那个熟悉的“121”,别慌,拿出逻辑分析仪,顺着信号走下去,你会发现——
问题从来都不神秘,只是你还没走到它藏身的地方而已。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

被折叠的 条评论
为什么被折叠?



