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,建议按这个顺序查:

  1. 查电源从设备 VDD 是否正常,复位脚是否释放,上电等待时间是否满足。

  2. 查线序SCL/SDA 有没有接反,GND 有没有共地,模块丝印是否和原理图一致。

  3. 查上拉和电平SCL/SDA 空闲是否为高,上拉到几伏,上升沿是否太慢。

  4. 查地址7 位地址和 8 位地址有没有搞混,HAL 是否需要左移。

  5. 查速率先降到 100kHz,确认能跑后再提高到 400kHz 或更高。

  6. 查 BUSY空闲时 SDA/SCL 是否被拉低,必要时做总线恢复。

  7. 查寄存器访问流程寄存器地址是 8 位还是 16 位,是否需要先写命令再延时读取。

  8. 查波形用逻辑分析仪看 NACK 出现在地址、寄存器地址,还是读数据阶段。

  9. 查错误码打印 HAL I2C error,不要只看 HAL_ERROR 和 HAL_TIMEOUT。

小站观点

I2C 的难点不在协议有多复杂,而在它太“像软件问题”了。

你看到的是一个函数返回失败,但背后可能是上拉电阻、线长、电平、上电时序、地址格式、从设备状态机、总线被拉低,或者寄存器读写流程不对。

所以调 I2C 时,最稳的顺序不是先改代码,而是先确认总线活着:电平对、地址对、ACK 位置看得到、错误码打得出来。

把这些基础动作做完,I2C 就没那么玄了。

你们现场遇到 I2C 最多的是 NACK,还是 BUSY 卡死?如果后面继续写一篇,我可以把“STM32 I2C + 传感器完整驱动模板”单独拆出来。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐