STM32 I2C 总是 NACK?先别怀疑传感器,把这 9 个地方查一遍
STM32 I2C 总是 NACK?先别怀疑传感器,把这 9 个地方查一遍

做 STM32 的人,迟早会遇到 I2C。
板子上接了温湿度传感器、EEPROM、触摸芯片、电源管理芯片,CubeMX 也配好了,代码一跑,不是 HAL_ERROR,就是 HAL_TIMEOUT,再打一下错误码,发现是 ACK failure。
这时候很多人第一反应是:是不是传感器坏了?是不是 HAL 有问题?
先别急。
I2C 的坑经常不在那几行读写代码里,而在地址、上拉、电平、复位、时序、寄存器地址和总线状态这些地方。今天这篇就按工程排查来,把 STM32 I2C NACK、BUSY、读不到设备这类问题捋一遍。
先把 I2C 的几个事实想清楚
I2C 看起来只有两根线:
SCL:时钟线
SDA:数据线
但这两根线不是普通推挽输出。
标准 I2C 是开漏/开集电极结构,设备只能主动把线拉低,不能主动输出高电平。高电平依靠外部或内部上拉电阻把总线拉起来。
所以第一件事要记住:I2C 通不通,不只是软件问题,它首先是电气问题。
一个典型连接大概是这样:
3.3V
| |
Rp Rp
| |
SCL SDA
| |
STM32 ----- Sensor / EEPROM / PMIC / Touch IC
如果没有合适的上拉,或者上拉太弱,SCL/SDA 上升沿会很慢;如果电平不匹配,设备可能根本识别不了高电平;如果某个从设备把 SDA 拉死,总线就会一直 BUSY。

1. 先查 7 位地址和 8 位地址有没有搞混
I2C 地址是最常见的坑。
很多传感器手册会写:
7-bit address: 0x38
write address: 0x70
read address : 0x71
这里的 0x38 是 7 位地址。
0x70 和 0x71 是把 7 位地址左移一位后,再加上 R/W 位形成的 8 位地址。
STM32 HAL 的很多 I2C API,传入的是左移后的地址。
比如设备 7 位地址是 0x38,HAL 调用时通常要写:
#define AHT20_ADDR_7BIT 0x38
#define AHT20_ADDR_HAL (AHT20_ADDR_7BIT << 1)
HAL_I2C_IsDeviceReady(&hi2c1, AHT20_ADDR_HAL,
3
,
100
);
如果你把 0x70 又左移了一次,就会变成 0xE0,设备当然不 ACK。
如果你把 0x38 原样传进去,有些 API 下也会扫不到。
所以排查第一步很简单:
-
数据手册写的是 7 位地址,还是 8 位读写地址;
-
HAL 函数要求的地址格式是什么;
-
逻辑分析仪上看到的地址是不是你以为的那个。
不要靠猜,抓一次波形最省时间。
2. 写一个扫描函数,先确认总线上有没有设备
调 I2C 时,我一般先不急着读寄存器。
先扫地址。
下面这个函数可以放在调试阶段使用:
void I2C_Scan(I2C_HandleTypeDef *hi2c)
{
printf
(
"I2C scan start\r\n"
);
for
(
uint8_t
addr =
0x03
; addr <=
0x77
; addr++) {
HAL_StatusTypeDef ret;
ret = HAL_I2C_IsDeviceReady(hi2c, (
uint16_t
)(addr <<
1
),
2
,
10
);
if
(ret == HAL_OK) {
printf
(
"I2C device found: 0x%02X\r\n"
, addr);
}
}
printf
(
"I2C scan done\r\n"
);
}
这里扫的是 7 位地址范围,传给 HAL 时再左移。
如果扫描能找到设备,说明:
-
SCL/SDA 大概率接通;
-
地址大概率正确;
-
上拉和电平至少能支撑当前速率;
-
从设备已经上电并响应地址。
如果扫描都找不到设备,不要马上写复杂驱动。先回到硬件和基础配置。
注意,某些设备在特殊状态下不会响应扫描,比如进入低功耗、需要先复位、需要特定启动时间,或者地址引脚还没稳定。扫描不是最终真理,但它是一个很好的第一刀。
3. 上拉电阻别随便填,线长和速率会改变结果
I2C 上拉电阻太大,上升沿慢;上拉电阻太小,低电平电流变大,设备拉低会吃力。
常见短线、板内通信,很多项目会用:
2.2kΩ
4.7kΩ
10kΩ
但这不是万能答案。
如果你把线拉长、挂很多设备、提高到 400kHz,10kΩ 可能就太弱。示波器上会看到 SCL/SDA 上升沿拖泥带水,看起来像慢慢爬上去。
排查时可以这样做:
-
示波器看 SCL/SDA 的高电平是否能到 3.3V 或目标电平;
-
看上升沿是否明显过慢;
-
看低电平是否足够低;
-
看 ACK 位时 SDA 有没有被从设备拉低;
-
临时降低 I2C 速率到 100kHz 试试。
如果 400kHz 不行,100kHz 能跑,通常就要怀疑上拉、线长、总线电容、布局或从设备时序。
这类问题靠改代码很难根治。
4. 电平不匹配,比地址错误还隐蔽
STM32 是 3.3V,传感器可能也是 3.3V,这种最省心。
但现场经常不是这么整齐:
-
有的模块上拉到 5V;
-
有的传感器只支持 1.8V;
-
有的开发板自带上拉,外接模块也自带上拉;
-
有的电平转换模块方向和 I2C 开漏特性不匹配;
-
有的从设备上电比主控慢,启动阶段把线拉住。
I2C 的 SCL/SDA 是共享总线,只要一个设备状态不对,就可能影响整条线。
所以遇到 NACK 或 BUSY,要问几个问题:
-
SCL/SDA 到底被上拉到几伏;
-
STM32 对应 IO 是否 5V tolerant;
-
从设备的 VDD 和 IO 电平是不是同一域;
-
总线上有没有多个上拉并联,等效阻值过小;
-
上电时序是否满足从设备手册要求。
很多“传感器没 ACK”,实际是传感器根本还没完成上电初始化。
5. BUSY 一直置位,可能是 SDA 被拉低了
STM32 I2C 有时会卡在 BUSY 状态。
典型现象是:
-
程序复位后 I2C 初始化失败;
-
第一次通信超时;
-
重新下载程序后仍然 BUSY;
-
给整板断电再上电又好了。
这种情况很可能是总线上某个设备把 SDA 拉低,主控认为总线一直忙。
常见原因有:
-
主控复位时,从设备还在一次传输中;
-
从设备状态机卡住,等主控继续给 SCL;
-
通信中途主控异常复位,没有发 STOP;
-
电源抖动导致从设备内部状态不完整;
-
SDA/SCL 接反或被外部短路。
排查方法很直接:
用万用表或示波器看空闲状态下:
SCL 是否为高
SDA 是否为高
空闲时两根线都应该被上拉为高。
如果 SDA 长期为低,先别看 HAL 代码,先找是谁在拉线。
6. 总线恢复:给 SCL 几个脉冲,再重新初始化 I2C
当从设备卡住 SDA 时,一个常见恢复办法是:先把 I2C 引脚临时当 GPIO 用,手动给 SCL 打几个脉冲,然后造一个 STOP 条件,再重新初始化 I2C 外设。
思路大概是:
static void I2C_BusRecovery(void)
{
/* * 这里是思路示例: * 1. 先关闭 I2C 外设; * 2. 把 SCL/SDA 配成开漏输出并保持上拉; * 3. 如果 SDA 被拉低,手动翻转 SCL 9 次; * 4. 产生 STOP:SDA 低 -> SCL 高 -> SDA 高; * 5. 再重新初始化 I2C。 * * 具体 GPIO 端口和引脚要按你的板子改。 */
}
一个简化版示例:
#define I2C_SCL_PORT GPIOB
#define I2C_SCL_PIN GPIO_PIN_6
#define I2C_SDA_PORT GPIOB
#define I2C_SDA_PIN GPIO_PIN_7
static void gpio_set(GPIO_TypeDef *port, uint16_t pin, GPIO_PinState s)
{
HAL_GPIO_WritePin(port, pin, s);
}
void I2C1_BusRecovery(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {
0
};
__HAL_RCC_GPIOB_CLK_ENABLE();
HAL_I2C_DeInit(&hi2c1);
GPIO_InitStruct.Pin = I2C_SCL_PIN | I2C_SDA_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
gpio_set(I2C_SCL_PORT, I2C_SCL_PIN, GPIO_PIN_SET);
gpio_set(I2C_SDA_PORT, I2C_SDA_PIN, GPIO_PIN_SET);
HAL_Delay(
1
);
for
(
int
i =
0
; i <
9
; i++) {
gpio_set(I2C_SCL_PORT, I2C_SCL_PIN, GPIO_PIN_RESET);
HAL_Delay(
1
);
gpio_set(I2C_SCL_PORT, I2C_SCL_PIN, GPIO_PIN_SET);
HAL_Delay(
1
);
if
(HAL_GPIO_ReadPin(I2C_SDA_PORT, I2C_SDA_PIN) == GPIO_PIN_SET) {
break
;
}
}
// 产生一个 STOP 条件
gpio_set(I2C_SDA_PORT, I2C_SDA_PIN, GPIO_PIN_RESET);
HAL_Delay(
1
);
gpio_set(I2C_SCL_PORT, I2C_SCL_PIN, GPIO_PIN_SET);
HAL_Delay(
1
);
gpio_set(I2C_SDA_PORT, I2C_SDA_PIN, GPIO_PIN_SET);
HAL_Delay(
1
);
MX_I2C1_Init();
}
这段代码不要无脑复制到量产项目里。
它的价值是说明恢复思路。真正项目里要根据芯片系列、GPIO 复用、外部上拉、RTOS 互斥锁、中断状态来整理。
但有一点很重要:**I2C 现场设备不能只靠重启 MCU 自救。**如果从设备没复位,MCU 重启后总线状态可能还是坏的。
7. 用逻辑分析仪看 ACK,比盯代码快得多
I2C 调试最怕纯看代码。
因为你看到的是:
HAL_I2C_Mem_Read() == HAL_ERROR
但你不知道错误发生在什么位置。
逻辑分析仪能告诉你:
-
地址发出去后有没有 ACK;
-
写寄存器地址有没有 ACK;
-
repeated start 有没有发出来;
-
读阶段从设备有没有返回数据;
-
主机最后有没有发 NACK 和 STOP;
-
波形是不是被拉低或变形。
同样是 NACK,位置不同,原因完全不同。
如果是地址阶段 NACK:
-
地址错;
-
设备没上电;
-
设备复位脚没释放;
-
电平不匹配;
-
上拉/线序有问题;
-
设备还没启动完成。
如果是寄存器地址阶段 NACK:
-
寄存器地址不支持;
-
寄存器地址宽度错;
-
设备状态不允许访问;
-
写保护或模式没切换。
如果是读数据阶段异常:
-
repeated start 时序不符合;
-
读长度错;
-
设备需要等待转换完成;
-
最后一字节 ACK/NACK 处理不对。

8. 读寄存器时,8 位寄存器地址和 16 位寄存器地址别写反
很多 I2C 设备不是直接读地址就给数据,而是要先写寄存器地址,再读数据。
比如常见的 8 位寄存器地址:
uint8_t
reg =
0x00
;
uint8_t
buf[
2
];
HAL_I2C_Mem_Read(&hi2c1,
DEVICE_ADDR_HAL,
reg,
I2C_MEMADD_SIZE_8BIT,
buf,
sizeof
(buf),
100
);
但 EEPROM 或某些传感器可能是 16 位寄存器地址:
uint16_t
reg =
0x0100
;
uint8_t
buf[
16
];
HAL_I2C_Mem_Read(&hi2c1,
EEPROM_ADDR_HAL,
reg,
I2C_MEMADD_SIZE_16BIT,
buf,
sizeof
(buf),
100
);
如果寄存器地址宽度写错,波形上看地址阶段可能 ACK,但后面的寄存器地址或读数据就不对。
另外有些设备不支持 HAL 的 Mem_Read 这种通用流程,需要先发命令,再等内部转换,再读结果。
比如某些温湿度传感器:
uint8_t
cmd[] = {
0xAC
,
0x33
,
0x00
};
uint8_t
data[
6
];
HAL_I2C_Master_Transmit(&hi2c1, AHT20_ADDR_HAL, cmd,
sizeof
(cmd),
100
);
HAL_Delay(
80
);
HAL_I2C_Master_Receive(&hi2c1, AHT20_ADDR_HAL, data,
sizeof
(data),
100
);
所以写驱动前一定要看清楚手册里的 transaction,不要把所有 I2C 设备都套成“写寄存器地址,再读数据”。
9. HAL 错误码要打印,不要只看返回值
很多代码只写:
if
(HAL_I2C_Mem_Read(...) != HAL_OK) {
Error_Handler();
}
这对调试帮助很小。
至少把错误码打出来:
static void I2C_PrintError(I2C_HandleTypeDef *hi2c)
{
uint32_t
err = HAL_I2C_GetError(hi2c);
printf
(
"I2C error: 0x%08lX\r\n"
, err);
if
(err & HAL_I2C_ERROR_AF) {
printf
(
" - ACK failure / NACK\r\n"
);
}
if
(err & HAL_I2C_ERROR_BERR) {
printf
(
" - Bus error\r\n"
);
}
if
(err & HAL_I2C_ERROR_ARLO) {
printf
(
" - Arbitration lost\r\n"
);
}
if
(err & HAL_I2C_ERROR_OVR) {
printf
(
" - Overrun / underrun\r\n"
);
}
if
(err & HAL_I2C_ERROR_TIMEOUT) {
printf
(
" - Timeout\r\n"
);
}
}
再配合一个包装函数:
HAL_StatusTypeDef I2C_ReadReg8(I2C_HandleTypeDef *hi2c, uint16_t dev_addr, uint8_t reg, uint8_t *data, uint16_t len)
{
HAL_StatusTypeDef ret;
ret = HAL_I2C_Mem_Read(hi2c,
dev_addr,
reg,
I2C_MEMADD_SIZE_8BIT,
data,
len,
100
);
if
(ret != HAL_OK) {
I2C_PrintError(hi2c);
}
return
ret;
}
有错误码、有波形、有扫描结果,排查速度会快很多。
最后给一份排查顺序

如果 STM32 I2C NACK、读不到设备、卡 BUSY,建议按这个顺序查:
-
查电源从设备 VDD 是否正常,复位脚是否释放,上电等待时间是否满足。
-
查线序SCL/SDA 有没有接反,GND 有没有共地,模块丝印是否和原理图一致。
-
查上拉和电平SCL/SDA 空闲是否为高,上拉到几伏,上升沿是否太慢。
-
查地址7 位地址和 8 位地址有没有搞混,HAL 是否需要左移。
-
查速率先降到 100kHz,确认能跑后再提高到 400kHz 或更高。
-
查 BUSY空闲时 SDA/SCL 是否被拉低,必要时做总线恢复。
-
查寄存器访问流程寄存器地址是 8 位还是 16 位,是否需要先写命令再延时读取。
-
查波形用逻辑分析仪看 NACK 出现在地址、寄存器地址,还是读数据阶段。
-
查错误码打印 HAL I2C error,不要只看
HAL_ERROR和HAL_TIMEOUT。
小站观点
I2C 的难点不在协议有多复杂,而在它太“像软件问题”了。
你看到的是一个函数返回失败,但背后可能是上拉电阻、线长、电平、上电时序、地址格式、从设备状态机、总线被拉低,或者寄存器读写流程不对。
所以调 I2C 时,最稳的顺序不是先改代码,而是先确认总线活着:电平对、地址对、ACK 位置看得到、错误码打得出来。
把这些基础动作做完,I2C 就没那么玄了。
你们现场遇到 I2C 最多的是 NACK,还是 BUSY 卡死?如果后面继续写一篇,我可以把“STM32 I2C + 传感器完整驱动模板”单独拆出来。
更多推荐
所有评论(0)