简介:INA226.rar 是一套面向嵌入式开发者的完整驱动工程,聚焦于在 STM32 平台上驱动德州仪器 INA226 电流与功率监控芯片,适用于电源管理、电池管理、负载检测等需要实时采集电流、电压与功率数据的场景,适合具备一定 C 语言与单片机基础的开发者参考移植。压缩包共 151 个文件,约 2.97MB,以 36 个 .h 头文件与 35 个 .c 源文件为核心,涵盖 STM32 标准外设库与 INA226 寄存器配置、数据读取逻辑,另含 .uvprojx、.uvoptx 等 Keil MDK 工程文件及 .hex、.axf 编译产物,以及 .o、.crf、.d 等中间文件,可直接在 Keil MDK 中打开编译调试。目前已有 4709 人学习下载。工程结构完整,包含 I2C 通信、初始化配置、测量启动与中断处理等模块,读者可据此快速理解 INA226 数据手册关键寄存器的设置方法,掌握传感器与 STM32 的对接流程,并在此基础上二次开发电流功率监控功能。
1. 从一颗电流传感器说起:INA226 在 STM32 上到底能干什么
如果你手头有一块 STM32 的板子,想测一个负载的电压、电流和功率,又不想自己搭运放加采样电阻那套模拟前端,那 INA226 这颗芯片大概率会出现在你的选型清单里。它把分流电阻两端的微弱压差、总线电压、功率计算全部塞进一颗 MSOP-8 封装里,通过 I2C 把结果吐给 MCU,省掉了一堆外围器件和校准的麻烦。这个压缩包里的内容,就是围绕 INA226 在 STM32 平台上的 C 语言驱动实现展开的,配套 Keil MDK 工程,适合正在做电源管理、电池监测、电机电流采集这类项目的开发者直接拿去用。你不需要从数据手册第一页啃到最后一页,代码里已经把寄存器配置、I2C 读写时序、校准值计算这些容易翻车的地方都铺好了。接下来我会按「这项目怎么组织、怎么跑起来、怎么改」的顺序,把里面的关键实现拆开讲。
2. 工程结构与驱动分层:从 Keil MDK 目录看代码组织
2.1 目录树与文件职责划分
拿到一个 Keil MDK 工程,我第一件事不是急着编译,而是先把目录结构扫一遍,搞清楚哪些文件是芯片原厂或社区提供的,哪些是项目自己写的。这个 INA226 工程的组织方式比较典型,大致分成三层:底层 I2C 通信、中间 INA226 寄存器操作、上层应用调用。下面是一个常见的目录布局,不同版本可能略有差异,但核心文件跑不掉。
INA226_Project/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── ina226.h // INA226 驱动头文件 │ │ └── i2c_soft.h // 软件 I2C 或硬件 I2C 封装 │ └── Src/ │ ├── main.c // 应用入口,初始化与主循环 │ ├── ina226.c // INA226 核心驱动实现 │ └── i2c_soft.c // I2C 底层读写 ├── Drivers/ │ ├── STM32F1xx_HAL_Driver/ // HAL 库文件(若使用 HAL) │ └── CMSIS/ // 内核头文件 ├── MDK-ARM/ │ └── INA226.uvprojx // Keil 工程文件 └── README.md // 简要说明这个分层的好处是,如果你换一颗 MCU,比如从 STM32F1 换到 F4 或者 GD32,只需要动i2c_soft.c里的 GPIO 初始化和延时函数,ina226.c里的寄存器逻辑一行都不用改。我一般会在ina226.h里把芯片地址、寄存器地址、配置宏都定义好,这样上层调用的时候直接写INA226_ReadBusVoltage()这种函数名,可读性比裸写寄存器强太多。
2.2 INA226 寄存器映射与配置字
INA226 的寄存器不多,但每一个都跟你的采样需求强相关。下面这张表是我从驱动代码里整理出来的常用寄存器,地址和功能跟数据手册一致。
| 寄存器名称 | 地址 | 功能说明 |
|---|---|---|
| Configuration | 0x00 | 设置平均次数、转换时间、工作模式 |
| Shunt Voltage | 0x01 | 分流电阻两端压差,LSB=2.5μV |
| Bus Voltage | 0x02 | 总线电压,LSB=1.25mV |
| Power | 0x03 | 功率值,LSB=25×Current_LSB |
| Current | 0x04 | 计算出的电流值 |
| Calibration | 0x05 | 校准寄存器,决定 Current_LSB |
| Mask/Enable | 0x06 | 报警功能配置 |
| Alert Limit | 0x07 | 报警阈值 |
| Manufacturer ID | 0xFE | 厂商 ID,固定 0x5449 |
| Die ID | 0xFF | 芯片版本 |
配置寄存器 0x00 的默认值是 0x4127,对应平均 16 次、总线转换 1.1ms、分流转换 1.1ms、连续测量模式。如果你要测的是变化很快的电流,比如电机启动瞬间,那平均次数就得往下调,否则响应跟不上。代码里一般会提供一个INA226_Init()函数,把这些参数以宏的形式暴露出来,方便你改。
// ina226.h 中常见的配置宏定义 #define INA226_ADDR 0x80 // 7位地址左移一位,A1/A0接地 #define INA226_REG_CONFIG 0x00 #define INA226_REG_SHUNT 0x01 #define INA226_REG_BUS 0x02 #define INA226_REG_POWER 0x03 #define INA226_REG_CURRENT 0x04 #define INA226_REG_CALIB 0x05 // 配置字:平均16次,总线转换1.1ms,分流转换1.1ms,连续模式 #define INA226_CONFIG_VAL 0x4527这里INA226_ADDR为什么是 0x80 而不是 0x40?因为 I2C 的 7 位地址在传输时要左移一位,最低位是读写位。A1 和 A0 都接地时,7 位地址是 1000000,左移后就是 0x80 写、0x81 读。这个细节在调试的时候特别容易搞混,示波器抓波形发现地址不对,多半就是这里没对齐。
2.3 校准寄存器与 Current_LSB 的计算
INA226 不会直接告诉你电流是多少,它需要你先告诉它分流电阻的阻值和期望的电流分辨率,然后它用内部乘法器算出电流和功率。这个「告诉」的过程就是写校准寄存器 0x05。校准值的计算公式是:
CAL = 0.00512 / (Current_LSB × R_shunt)其中Current_LSB是你期望的电流最小分辨率,单位是 A/bit。比如你用 0.002Ω 的分流电阻,最大电流 10A,那Current_LSB一般取Max_Current / 32768,算下来约 0.000305A。代入公式得到 CAL ≈ 8390,十六进制就是 0x20C6。这个值写进校准寄存器后,读电流寄存器 0x04 得到的数值乘以Current_LSB就是实际安培数。
// 校准值计算与写入 void INA226_Calibrate(float r_shunt, float max_current) { float current_lsb = max_current / 32768.0f; uint16_t cal = (uint16_t)(0.00512f / (current_lsb * r_shunt)); INA226_WriteReg(INA226_REG_CALIB, cal); // 保存 current_lsb 供后续电流换算使用 g_current_lsb = current_lsb; }这段代码里0.00512是芯片内部固定的常数,不用改。r_shunt是你实际焊接的电阻值,注意要用高精度、低温漂的合金电阻,普通贴片电阻温漂能到 200ppm,测出来的电流随温度飘得你怀疑人生。max_current是你预期的最大电流,取大了分辨率变差,取小了会溢出,一般留 20% 余量。
3. 从零跑通:Keil MDK 编译、下载与 I2C 通信验证
3.1 打开工程与编译环境确认
拿到.uvprojx文件后,双击用 Keil MDK 打开。先别急着点编译,看一眼 Options for Target 里的器件型号是不是跟你手头的板子一致。常见的是 STM32F103C8T6 或者 STM32F103ZET6,如果不对,在 Device 标签页里换成你实际用的型号。然后检查 C/C++ 标签页里的 Include Paths,确保Core/Inc、Drivers/STM32F1xx_HAL_Driver/Inc、Drivers/CMSIS/Device/ST/STM32F1xx/Include这几个路径都在。缺了任何一个,编译的时候就会报cannot open source input file之类的错误。
还有一个容易忽略的地方是宏定义。如果你用的是 HAL 库,USE_HAL_DRIVER和STM32F103xB这两个宏必须在 C/C++ 标签页的 Define 框里写上。少了USE_HAL_DRIVER,HAL 的源文件会编译出一堆未定义引用;少了STM32F103xB,CMSIS 的头文件找不到对应的启动文件。这些坑我踩过不止一次,每次换新工程都要先确认一遍。
3.2 I2C 底层读写实现
INA226 支持标准模式 100kHz 和快速模式 400kHz 的 I2C 通信。工程里通常提供两种实现:硬件 I2C 和软件模拟 I2C。硬件 I2C 速度快、不占 CPU,但 STM32 的硬件 I2C 在某些系列上有已知的锁死问题,调试起来比较玄学。软件 I2C 虽然慢一点,但时序完全可控,换引脚也方便,很多项目最后都切到软件模拟。
// 软件 I2C 写一个字节到指定寄存器 void INA226_WriteReg(uint8_t reg, uint16_t value) { I2C_Start(); I2C_SendByte(INA226_ADDR); // 发送设备地址+写 I2C_WaitAck(); I2C_SendByte(reg); // 发送寄存器地址 I2C_WaitAck(); I2C_SendByte(value >> 8); // 高字节 I2C_WaitAck(); I2C_SendByte(value & 0xFF); // 低字节 I2C_WaitAck(); I2C_Stop(); } // 从指定寄存器读 16 位数据 uint16_t INA226_ReadReg(uint8_t reg) { uint16_t value; I2C_Start(); I2C_SendByte(INA226_ADDR); // 写地址 I2C_WaitAck(); I2C_SendByte(reg); // 寄存器地址 I2C_WaitAck(); I2C_Start(); // 重复起始条件 I2C_SendByte(INA226_ADDR | 0x01); // 读地址 I2C_WaitAck(); value = I2C_ReadByte() << 8; // 读高字节 I2C_SendAck(0); // 发送 ACK value |= I2C_ReadByte(); // 读低字节 I2C_SendAck(1); // 发送 NACK I2C_Stop(); return value; }INA226_WriteReg的流程是:起始条件 → 设备地址写 → 寄存器地址 → 数据高字节 → 数据低字节 → 停止条件。INA226_ReadReg稍微绕一点,先写寄存器地址,然后发重复起始条件,再发读地址,最后读两个字节。注意最后一个字节读完要发 NACK,否则从机会一直拉着 SDA 不放,总线就挂死了。这个 NACK 我早期经常忘,现象就是读一次之后再也读不出数据,拿逻辑分析仪一看,SDA 一直被拉低。
3.3 上电初始化与首次读数验证
初始化流程一般是:延时等待芯片上电稳定 → 写配置寄存器 → 写校准寄存器 → 读厂商 ID 确认通信正常。厂商 ID 寄存器 0xFE 的值固定是 0x5449,如果你读出来不是这个数,那说明 I2C 通信有问题,先别往下调,回头查地址、上拉电阻、供电。
void INA226_Init(void) { I2C_Init(); // 初始化 I2C 引脚 Delay_ms(10); // 等待芯片上电稳定 INA226_WriteReg(INA226_REG_CONFIG, INA226_CONFIG_VAL); INA226_Calibrate(0.002f, 10.0f); // 2mΩ 分流,最大 10A uint16_t id = INA226_ReadReg(0xFE); if (id != 0x5449) { // 通信异常,点亮错误指示灯或打印日志 printf("INA226 ID error: 0x%04X\r\n", id); } }Delay_ms(10)这个延时不能省。INA226 上电后内部需要一点时间稳定,尤其是你板子上还有大电容的时候,供电爬升慢,芯片可能还没准备好就被你写配置,结果写进去的值不对。我一般会延时 10 到 50ms,保险一点。读厂商 ID 是最直接的验证手段,比拿万用表量电压快得多。
4. 避坑与排查:I2C 读不出数、电流值跳变的常见原因
4.1 读出来全是 0xFFFF 或 0x0000
现象是调用INA226_ReadReg返回 0xFFFF 或者 0x0000,厂商 ID 也对不上。原因通常是 I2C 地址错了,或者 SDA/SCL 接反了,再或者上拉电阻没焊。INA226 的 A1、A0 引脚决定地址,悬空、接地、接 VCC 对应不同地址,很多人焊的时候没注意,代码里写的地址跟硬件对不上。解决方法是先拿万用表量 A1、A0 的电平,确认地址,然后检查上拉电阻,一般 4.7kΩ 到 10kΩ 之间,太小了功耗大,太大了上升沿变缓。
4.2 电流读数周期性跳变
现象是电流值每隔几秒跳一下,或者跟实际负载对不上。原因多半是校准寄存器的值算错了,或者Current_LSB跟分流电阻不匹配。还有一种可能是平均次数设得太低,噪声没滤干净。解决方法是重新核对R_shunt的实际阻值,用高精度万用表量一下,然后按公式重算 CAL 值。如果还跳,把配置寄存器里的平均次数从 16 调到 128 甚至 256,牺牲响应速度换稳定度。
4.3 总线电压正常但电流始终为零
现象是总线电压读出来是对的,但电流寄存器一直是 0。原因通常是分流电阻两端没有压差,要么是负载没接,要么是分流电阻焊在了错误的位置。INA226 测的是分流电阻两端的压差,如果你把分流电阻放在了负载和地之间,而负载电流没有完全流过它,那自然测不到。解决方法是确认分流电阻串联在供电回路里,负载电流必须全部经过它。另外检查一下分流电阻的阻值是不是太小了,比如 0.1mΩ 配 10A 最大电流,压差只有 1mV,LSB 是 2.5μV,理论上能测,但噪声一大就淹没了。
4.4 硬件 I2C 卡死在等待 ACK
现象是程序卡在while(I2C_WaitAck())里出不来,看门狗都复位不了。原因是 STM32 硬件 I2C 在从机拉低 SCL 做时钟延展的时候,如果时序不匹配就会死锁。解决方法是改用软件模拟 I2C,或者给硬件 I2C 加超时机制,超时后手动发 9 个时钟脉冲解锁总线。我现在的习惯是,只要项目里 I2C 从机超过两个,直接上软件模拟,省得跟硬件外设斗智斗勇。
4.5 电源纹波导致读数不稳
现象是读数在小范围内快速抖动,示波器看电源轨上有几十毫伏的纹波。原因是 INA226 的供电跟负载共用了一路电源,负载电流变化时电源波动直接耦合进芯片。解决方法是在 INA226 的 VCC 引脚旁边加一个 0.1μF 的陶瓷电容,再并一个 10μF 的钽电容,尽量靠近芯片引脚。如果还不行,就给 INA226 单独一路 LDO 供电,跟负载电源隔开。
5. 二次开发改造:从单芯片到多通道监测的扩展思路
5.1 多片 INA226 挂同一条 I2C 总线
INA226 的地址由 A1、A0 决定,最多可以在一条 I2C 总线上挂 16 片。如果你要做一个多路电源监测系统,比如同时测 4 路电池的充放电电流,只需要把每片 INA226 的 A1、A0 接成不同的组合,然后在代码里维护一个地址数组,循环读取即可。下面是一个多通道读取的示例结构。
// 多片 INA226 地址表,A1/A0 分别接不同电平 const uint8_t ina_addr_list[4] = {0x80, 0x82, 0x84, 0x86}; typedef struct { float bus_voltage; float current; float power; } INA226_Data; INA226_Data g_ina_data[4]; void INA226_ScanAll(void) { for (int i = 0; i < 4; i++) { uint16_t raw_bus = INA226_ReadRegAddr(ina_addr_list[i], INA226_REG_BUS); uint16_t raw_cur = INA226_ReadRegAddr(ina_addr_list[i], INA226_REG_CURRENT); g_ina_data[i].bus_voltage = raw_bus * 1.25f / 1000.0f; // mV 转 V g_ina_data[i].current = raw_cur * g_current_lsb; g_ina_data[i].power = g_ina_data[i].bus_voltage * g_ina_data[i].current; } }这里我把INA226_ReadReg改成了带地址参数的版本INA226_ReadRegAddr,这样同一个函数可以操作不同地址的芯片。g_current_lsb如果每片芯片的分流电阻和最大电流不一样,就需要为每片单独保存一个current_lsb,不能共用一个全局变量。这个细节在多通道项目里特别容易翻车,我见过有人四路电流读数一模一样,查了半天发现是current_lsb没分开。
5.2 报警功能与中断处理
INA226 的 Mask/Enable 寄存器 0x06 可以配置报警功能,比如电流超过阈值时拉低 ALERT 引脚。这个引脚可以接到 STM32 的外部中断,实现过流保护。配置步骤是:先写 Alert Limit 寄存器 0x07 设定阈值,再写 Mask/Enable 寄存器使能对应的报警位,最后把 ALERT 引脚配置成中断输入。注意 ALERT 引脚是开漏输出,需要上拉电阻。
// 配置过流报警:电流超过 8A 时触发 void INA226_EnableOverCurrentAlert(float threshold_current) { uint16_t limit = (uint16_t)(threshold_current / g_current_lsb); INA226_WriteReg(INA226_REG_ALERT_LIMIT, limit); // 使能电流超限报警,ALERT 引脚低有效 INA226_WriteReg(INA226_REG_MASK_ENABLE, 0x8000); }0x8000这个值对应的是 SOL(Shunt Voltage Over-Limit)位,具体位定义要查数据手册。写完之后,ALERT 引脚在电流超过阈值时会拉低,你在 STM32 的外部中断服务函数里做保护动作,比如切断继电器或者降功率。这个功能在电池管理系统里很实用,比软件轮询快得多。
5.3 从裸机到 RTOS 的移植注意点
如果你要把这个驱动移植到 FreeRTOS 或者 RT-Thread 上,最大的改动是 I2C 读写要加互斥锁。裸机的时候,INA226_ReadReg从头到尾执行完不会被中断打断,但 RTOS 里任务切换可能发生在任意时刻,两个任务同时操作 I2C 总线就会出问题。常见做法是给每个 I2C 总线配一个互斥信号量,读写之前先获取,读完释放。
// FreeRTOS 下的 I2C 互斥访问 SemaphoreHandle_t i2c_mutex; uint16_t INA226_ReadReg_RTOS(uint8_t reg) { uint16_t val; xSemaphoreTake(i2c_mutex, portMAX_DELAY); val = INA226_ReadReg(reg); xSemaphoreGive(i2c_mutex); return val; }另外,RTOS 下的延时要用vTaskDelay而不是裸机的Delay_ms,否则会阻塞整个任务调度。INA226_Init里的 10ms 延时如果放在任务里,用vTaskDelay(pdMS_TO_TICKS(10))更合适。这些改动不大,但漏了任何一个,系统跑起来就会偶发死锁或者读数错乱。
5.4 校准与验证的实用技巧
二次开发的时候,我建议保留一个「原始值读取」的调试接口,直接把分流电压寄存器和总线电压寄存器的原始值打印出来,不要经过任何换算。这样当你怀疑读数不对的时候,可以拿原始值跟万用表量到的实际值做对比,快速定位是芯片配置问题还是换算公式问题。我一般会在main.c里留一个条件编译的调试宏,定义INA226_DEBUG的时候走原始值打印,不定义的时候走正常业务逻辑。
#ifdef INA226_DEBUG uint16_t raw_shunt = INA226_ReadReg(INA226_REG_SHUNT); uint16_t raw_bus = INA226_ReadReg(INA226_REG_BUS); printf("Shunt raw: %d, Bus raw: %d\r\n", raw_shunt, raw_bus); #endif分流电压的 LSB 是 2.5μV,总线电压的 LSB 是 1.25mV,拿原始值乘以 LSB 就是实际物理量。如果你用 2mΩ 分流电阻,通过 1A 电流,分流电压应该是 2mV,对应原始值 800。拿这个数跟打印出来的 raw_shunt 对比,差太多就说明校准或者硬件有问题。这个习惯帮我省了很多来回换电阻、改代码的时间。
从那以后我每次拿到新的电流传感器驱动,都强制先跑一遍原始值打印,确认芯片配置和硬件连接没问题,再往上叠业务逻辑。希望帮到你。
本文还有配套的精品资源,点击获取