INA226 软件 I2C 初始化踩坑:读取返回 0xFFFF

INA226 电流检测芯片初始化时读取 Manufacturer ID 返回 0xFFFF?本文记录软件 I2C 与硬件 I2C GPIO 模式冲突的排查全过程

问题现象

项目里用了两颗 INA226(U3 和 U7)做电流检测,驱动是自己写的软件 I2C。测试时发现一个奇怪的现象:

不调用 INA226_Scan() 直接初始化时,INA226_Init_U3() / INA226_Init_U7() 读取 Manufacturer ID 返回 0xFFFF,芯片识别失败。

必须先执行一次 INA226_Scan(),后续的 Init 和读取才能正常工作。

问题来了:ScanInit 明明是两回事,为什么必须先 Scan 才能 Init?


根因分析

1. 两套 I2C 初始化逻辑

系统中存在两套 I2C 初始化代码,都操作 PB6/PB7,但配置的模式完全不同:

初始化来源 配置模式 目的
wk_i2c1_init()src/wk_i2c.c GPIO_MODE_MUX(复用模式) 硬件 I2C1 外设
swi2c_gpio_init()drivers/ina226/ina226.c GPIO_MODE_OUTPUT(通用输出) 软件 I2C bit-bang

wk_i2c1_init() 的代码:

1
2
3
4
5
6
7
8
9
/* configure the SCL pin */
gpio_init_struct.gpio_mode = GPIO_MODE_MUX;   // ← 复用模式
gpio_init_struct.gpio_pins = GPIO_PINS_6;
gpio_init(GPIOB, &gpio_init_struct);

/* configure the SDA pin */
gpio_init_struct.gpio_mode = GPIO_MODE_MUX;   // ← 复用模式
gpio_init_struct.gpio_pins = GPIO_PINS_7;
gpio_init(GPIOB, &gpio_init_struct);

swi2c_gpio_init() 的代码:

1
2
3
4
5
6
gpio_init_struct.gpio_mode = GPIO_MODE_OUTPUT;  // ← 通用输出模式
gpio_init_struct.gpio_pins = SWI2C_SCL_PIN;
gpio_init(SWI2C_SCL_PORT, &gpio_init_struct);

gpio_init_struct.gpio_pins = SWI2C_SDA_PIN;
gpio_init(SWI2C_SDA_PORT, &gpio_init_struct);

2. 软件 I2C 到底怎么操作引脚?

当前驱动使用软件 I2C(INA226_USE_HW_I2C = 0),通过直接操作 GPIO 寄存器实现时序:

1
2
3
4
5
#define SCL_H()   SWI2C_SCL_PORT->scr = SWI2C_SCL_PIN
#define SCL_L()   SWI2C_SCL_PORT->clr = SWI2C_SCL_PIN
#define SDA_H()   SWI2C_SDA_PORT->scr = SWI2C_SDA_PIN
#define SDA_L()   SWI2C_SDA_PORT->clr = SWI2C_SDA_PIN
#define SDA_READ()  ((SWI2C_SDA_PORT->idt & SWI2C_SDA_PIN) != 0)
  • scr(set output data register):写 1 输出高电平
  • clr(clear output data register):写 1 输出低电平
  • idt(input data register):读取引脚输入电平

关键发现scr/clr/idt 这些寄存器只有在引脚处于 通用 GPIO 模式 时才有效。当引脚配置为 复用模式(MUX) 时,GPIO 寄存器操作无法影响引脚电平,软件 I2C 的 START、STOP、ACK/NACK 全部失效。

3. 为什么先 Scan 就能工作?

把两条路径画出来就清楚了:

正常路径(有 Scan):

1
2
3
4
5
6
main()
  └─ wk_i2c1_init()          → PB6/PB7 = MUX 模式
  └─ INA226_Scan()
       └─ swi2c_gpio_init()  → PB6/PB7 = OUTPUT 模式 ← 切过来了!
       └─ ... 扫描成功
  └─ INA226_Init_U3/U7()     → 软件 I2C 正常工作 ✓

问题路径(无 Scan):

1
2
3
4
main()
  └─ wk_i2c1_init()          → PB6/PB7 = MUX 模式
  └─ INA226_Init_U3/U7()     → 软件 I2C 操作无效 ✗
       └─ 读 ManufID → 0xFFFF(全部 NACK)

真相INA226_Scan() 内部调用了 swi2c_gpio_init(),把 PB6/PB7 从 MUX 模式切回了 OUTPUT 模式。所以先 Scan 再 Init 是"歪打正着",真正起作用的是 swi2c_gpio_init()


修复方案

思路

swi2c_gpio_init() 的调用从 INA226_Scan() 中提取出来,封装为公共函数,确保 任何 需要进行软件 I2C 通信的地方都先初始化 GPIO。

代码改动

1. 新增 INA226_Swi2cInit()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/**
 * @brief  软件 I2C GPIO 初始化(确保 PB6/PB7 为通用开漏输出)
 * @note   wk_i2c1_init() 把 PB6/PB7 配成了 MUX 复用模式,软件 I2C 需要切回 OUTPUT
 */
void INA226_Swi2cInit(void)
{
#if !INA226_USE_HW_I2C
    swi2c_gpio_init();
#endif
}

2. 三个调用点全部加上

INA226_Scan()(替换原有的直接调用):

1
2
3
4
5
6
7
8
9
uint8_t INA226_Scan(uint8_t *found_addrs, uint8_t max_num)
{
    // ...
#if !INA226_USE_HW_I2C
    INA226_Swi2cInit();   // ← 替代 swi2c_gpio_init()
    if (swi2c_check_pullup() != 0) { ... }
#endif
    // ...
}

INA226_Init_U3()(新增):

1
2
3
4
5
6
7
8
9
uint8_t INA226_Init_U3(void)
{
    uint16_t manuf_id;

    INA226_Swi2cInit();   // ← 新增

    manuf_id = INA226_ReadReg_Addr(INA226_U3_ADDR, INA226_REG_MANUF_ID);
    // ...
}

INA226_Init_U7()(新增):

1
2
3
4
5
6
7
8
9
uint8_t INA226_Init_U7(void)
{
    uint16_t manuf_id;

    INA226_Swi2cInit();   // ← 新增

    manuf_id = INA226_ReadReg_Addr(INA226_U7_ADDR, INA226_REG_MANUF_ID);
    // ...
}

修复后的路径

1
2
3
4
main()
  └─ wk_i2c1_init()          → PB6/PB7 = MUX 模式
  └─ INA226_Init_U3/U7()     → INA226_Swi2cInit() → OUTPUT 模式
       └─ 软件 I2C 正常工作 ✓

现在不需要先 Scan 也能直接 Init 了。


延伸思考

硬件 I2C 会碰到这个问题吗?

不会。如果 INA226_USE_HW_I2C = 1,驱动使用 AT32 I2C1 外设,通过标准库函数(i2c_start_generate()i2c_data_send() 等)通信。此时 PB6/PB7 保持 MUX 模式即可,硬件外设自动控制引脚,不存在 GPIO 模式切换问题。

软件 I2C 和硬件 I2C 能混用吗?

不能同时工作。 两套机制共用 PB6/PB7,软件 I2C 要求 OUTPUT 模式,硬件 I2C 要求 MUX 模式。切换时必须先调用对应的 GPIO 初始化函数。

当前项目使用软件 I2C(INA226_USE_HW_I2C = 0),因此 wk_i2c1_init() 实际上只起到"总线解锁"的作用(发送 9 个 SCL 脉冲),后续 INA226 驱动完全走软件 I2C。

怎么避免以后再踩这个坑?

核心原则:软件 I2C 的读写函数自身应该保证 GPIO 模式正确,不能依赖外部先调用某个初始化函数。

更彻底的修复方式:在 i2c1_write() / i2c1_read() 的开头调用 swi2c_gpio_init(),这样任何 I2C 操作都自动确保 GPIO 模式正确。GPIO 初始化本身很快(几条寄存器写),实际性能影响可忽略。


总结

问题 原因 解决
读取返回 0xFFFF PB6/PB7 处于 MUX 模式,软件 I2C 寄存器操作无效 在 Init 前调用 swi2c_gpio_init() 切回 OUTPUT 模式
必须先 Scan 才能 Init INA226_Scan() 内部调了 swi2c_gpio_init() 提取为公共函数 INA226_Swi2cInit(),所有 I2C 操作前都调用

这个 bug 最坑的地方在于:它不是每次必现。如果你的代码恰好先调了 Scan,一切正常;一旦跳过 Scan,问题才暴露。这种"隐式依赖"是嵌入式开发中最容易踩的坑之一。


环境:AT32F403A + INA226 + 软件 I2C (bit-bang)

世界是你们
使用 Hugo 构建
主题 StackJimmy 设计