简介:INA226.rar 是一份面向嵌入式开发者的完整驱动工程,围绕 TI INA226 高精度电流/功率监控芯片在 STM32 平台上的接入与数据采集展开,适合正在做电源管理、电池管理、负载检测或传感器课程设计的中初级开发者参考。压缩包共 151 个文件,约 2.97MB,包含 36 个 h 头文件、35 个 c 源文件,以及 Keil MDK 工程文件、编译中间文件、hex 输出和说明文本等;源码主要承担寄存器配置、I2C 通信、测量启动与数据读取,工程文件便于直接编译调试。资源已有 4713 人学习,价值在于提供可运行的软硬件交互范例:读者可据此理解 INA226 的配置流程、电压电流功率换算、STM32 外设初始化与工程组织方式,也能参考其从初始化到读取的完整链路,并在此基础上改造成自己的监测方案或排错参照,对课程设计、毕业设计或项目原型验证都较有参考意义。
1. INA226 不是“读个电压”那么简单:从一块电流监测板说起
INA226 这颗电流/功率监测芯片,配上 STM32 和 C 语言工程,在 Keil MDK 里跑通并不难,难的是把读数做稳。它把分流电压、总线电压、电流和功率都放进寄存器,I2C 一读就能出数,适合做电源监测、电池充放电、电机电流采样这类传感器场景。
不少人第一次接上模块,串口能打印出 0.00A 就以为成了,换个负载才发现小数点后全是噪声。INA226 的坑不在“能不能读到”,而在“读到的数能不能信”,分流电阻、校准寄存器、平均次数和采样周期任何一处对不上,结果就会偏。
这份 INA226.rar 工程把驱动、校准和采样任务串在一起,适合已经会点灯、能建 Keil MDK 工程,但一碰传感器就卡在时序和校准上的人。先别急着抄代码,把分流电阻和寄存器之间的关系理清,后面少翻车。
2. 硬件连接与 I2C 时序:先让 STM32 认到 0x40
INA226 的硬件连接不复杂,VCC 接 3.3V,GND 共地,SCL/SDA 各挂一只上拉电阻,分流电阻串在待测回路里,A0/A1 决定 I2C 地址。真正容易出问题的是分流电阻选型和地址字节,前者决定量程和分辨率,后者决定 STM32 能不能在总线上找到它。
2.1 分流电阻与量程的选型账
INA226 测电流的方式是测分流电阻两端的压降,分流电压满量程是 ±81.92mV。选 Rshunt 时先看最大电流和允许功耗,再留 20%~30% 余量。比如最大 2A,用 10mΩ,满量程压降 20mV,电阻功耗 40mW,0603 或 0805 都能扛;如果最大 5A,同样 10mΩ 压降 50mV,功耗 250mW,最好用 2512 封装,且选低温漂的合金电阻。
| 最大电流 | 分流电阻 | 满量程分流电压 | 电阻功耗 | 常见处理 |
|---|---|---|---|---|
| 2A | 10mΩ | 20mV | 40mW | 0805 合金电阻,Kelvin 走线 |
| 5A | 10mΩ | 50mV | 250mW | 2512 封装,注意散热 |
| 10A | 5mΩ | 50mV | 500mW | 四线开尔文,远离热源 |
| 20A | 2mΩ | 40mV | 800mW | 专用分流器,铜排连接 |
分流电阻不是越小越好。阻值太小,小电流下压降只有几十微伏,INA226 的 2.5μV LSB 还能分辨,但板上的热电动势和运放失调会把它淹没;阻值太大,功耗和压降上去了,负载端电压被拉低,测出来的电流也会随温度漂。常见做法是让最大电流对应的分流电压落在 40mV~60mV,既留了余量,又不会让功耗失控。
PCB 上一定要做开尔文连接。分流电阻两个电流脚走大电流,两个电压采样脚单独引到 INA226 的 IN+ 和 IN-,采样线从电阻焊盘内侧引出,不要和电流路径共用一段铜箔。否则那段铜箔的电阻会直接叠进测量结果,几毫欧的走线电阻在 10A 下就是几十毫伏的误差,校准寄存器怎么调都救不回来。
注意:分流电阻的功率降额要按实际工作温度算。标称 1W 的 2512 电阻在 70℃ 环境里可能只能用到一半,长期跑 5A 以上时,摸上去烫手不是正常现象。
2.2 STM32 的 I2C 外设配置与扫描地址
STM32 这边用硬件 I2C 或软件模拟都可以,INA226 支持 100kHz 和 400kHz。板上总线不长时用 400kHz 能缩短读取时间,但上拉电阻要跟着调,常见 4.7kΩ 对应 100kHz,400kHz 可以降到 2.2kΩ 左右。上拉接到 3.3V,不要接到 5V,INA226 的 IO 耐压虽然不低,但和 STM32 共总线时没必要冒这个险。
INA226 的 7 位地址由 A0/A1 决定,默认都接地是 0x40。用 HAL 库时传入的是左移一位后的 8 位地址,也就是 0x80。很多人写HAL_I2C_Mem_Read(&hi2c1, 0x40, ...)读不到,就是因为把 7 位地址直接塞进了 8 位参数。先跑一遍总线扫描,确认 0x40 能应答,再往下写寄存器。
#include <stdio.h> #include "i2c.h" /* 扫描 7 位地址,打印能应答的器件地址 */ uint8_t i2c_scan(I2C_HandleTypeDef *hi2c) { uint8_t found = 0; for (uint8_t addr = 0x08; addr < 0x78; addr++) { /* HAL 接口要传 8 位地址,所以左移一位 */ if (HAL_I2C_IsDeviceReady(hi2c, (uint16_t)(addr << 1), 3, 10) == HAL_OK) { printf("I2C device found: 0x%02X\r\n", addr); found++; } } return found; }这段扫描代码的逻辑很直接:从 0x08 到 0x77 逐个发地址,HAL_I2C_IsDeviceReady返回HAL_OK说明从机应答。第三个参数是重试次数,给 3 次能过滤掉偶发干扰;第四个参数是超时,单位毫秒,10ms 对 100kHz 总线足够。扫描到 0x40 后,下一步不是马上读电流,而是先读配置寄存器 0x00,看返回值是不是复位默认值 0x4127,确认写保护、电源和总线都正常。
如果扫描不到,先量 SCL/SDA 的静态电平,正常应该被上拉到 3.3V;再量 INA226 的 VCC 和 GND,别只看模块上的丝印;最后确认 A0/A1 的电平,有些模块把地址脚焊死了,丝印写的 0x40 实际可能是 0x41。总线扫描是排错的第一道黑匣子,别跳过它直接怀疑芯片坏了。
3. 寄存器配置与校准:把原始码变成安培和瓦特
INA226 读出来的是 16 位原始码,不写校准寄存器,电流寄存器就是一堆没有单位的数。配置寄存器决定平均次数和转换时间,校准寄存器决定电流 LSB,两者配合才能把原始码换成安培。顺序上先写配置、再写校准,最后读电流,中间留够一次转换时间。
3.1 配置寄存器 0x00 的四个关键位
配置寄存器 0x00 的 bit15 是复位位,写 1 会软复位;bit11~9 是 AVG,决定平均次数;bit8~6 是 VBUSCT,总线电压转换时间;bit5~3 是 VSHCT,分流电压转换时间;bit2~0 是 MODE,选择连续或触发模式。常用值是 AVG=2(16 次平均)、VBUSCT=4(1.1ms)、VSHCT=4(1.1ms)、MODE=7(分流和总线连续转换),合起来是 0x0527。
| 位段 | 名称 | 本例取值 | 含义 |
|---|---|---|---|
| bit15 | RST | 0 | 不复位 |
| bit11~9 | AVG | 2 | 16 次平均 |
| bit8~6 | VBUSCT | 4 | 总线转换 1.1ms |
| bit5~3 | VSHCT | 4 | 分流转换 1.1ms |
| bit2~0 | MODE | 7 | 分流+总线连续转换 |
按这个配置,一次分流加总线转换约 2.2ms,16 次平均后约 35ms 才更新一轮数据。采样任务如果设成 5ms 读一次,读到的多半还是旧值,看起来像“数据不刷新”。我一般把采样周期设在 50ms 以上,或者把 AVG 降到 4 次,先用短周期看趋势,稳定后再加平均。
#define INA226_ADDR (0x40 << 1) #define INA226_REG_CONFIG 0x00 #define INA226_REG_CALIB 0x05 static HAL_StatusTypeDef ina226_write_u16(uint8_t reg, uint16_t val) { uint8_t buf[2]; buf[0] = (uint8_t)(val >> 8); buf[1] = (uint8_t)(val & 0xFF); return HAL_I2C_Mem_Write(&hi2c1, INA226_ADDR, reg, I2C_MEMADD_SIZE_8BIT, buf, 2, 100); } void ina226_config(void) { /* AVG=16, VBUSCT=1.1ms, VSHCT=1.1ms, 连续分流+总线 */ uint16_t config = (2u << 9) | (4u << 6) | (4u << 3) | 0x7u; ina226_write_u16(INA226_REG_CONFIG, config); }这段代码只写了配置寄存器,config的计算按位段拼出来,比直接写 0x0527 更容易改。HAL_I2C_Mem_Write的第四个参数是寄存器地址长度,INA226 是 8 位寄存器地址,所以用I2C_MEMADD_SIZE_8BIT;超时给 100ms,避免总线被拉死时卡在中断里。写完后建议回读一次 0x00,确认值确实是 0x0527,有些模块的 I2C 上拉太弱,写进去的值会丢。
3.2 校准寄存器 0x05 与电流 LSB 的推导
电流 LSB 是电流寄存器最低位代表的安培数,通常取最大预期电流除以 32768。比如最大 2A,Current_LSB = 2 / 32768 ≈ 61.035μA。校准寄存器 CAL 的公式是CAL = 0.00512 / (Current_LSB × Rshunt),其中 0.00512 是芯片内部固定值,Rshunt 单位是欧姆。2A、10mΩ 代入,CAL = 0.00512 / (61.035e-6 × 0.01) ≈ 8389。
这个值要写进 0x05,且必须在配置寄存器之后写。CAL 是 16 位无符号数,写 0 或写错单位都会让电流寄存器输出 0 或满量程。有人把 10mΩ 写成 10,算出来的 CAL 小了 1000 倍,读出来的电流也小 1000 倍,查半天以为是传感器坏了。
static float g_current_lsb = 0.0f; void ina226_calibrate(float r_shunt_ohm, float i_max_a) { /* 电流 LSB 单位:A/bit */ g_current_lsb = i_max_a / 32768.0f; /* CAL 公式中的 0.00512 是固定常数,Rshunt 单位欧姆 */ float cal_f = 0.00512f / (g_current_lsb * r_shunt_ohm); uint16_t cal = (uint16_t)(cal_f + 0.5f); ina226_write_u16(INA226_REG_CALIB, cal); }r_shunt_ohm传 0.01 而不是 10,i_max_a传 2.0 而不是 2000,单位统一成安培和欧姆后公式才不会错。cal_f + 0.5f是四舍五入,直接截断在临界值上会差 1 个 LSB。写完后把g_current_lsb存成全局变量,后面读电流直接乘,不要每次重算,否则改了分流电阻忘了同步,数据会悄悄偏掉。
3.3 连续读取与数据就绪标志
电流寄存器 0x04 是有符号 16 位,读出来先转int16_t再乘g_current_lsb;总线电压寄存器 0x02 是无符号,LSB 固定 1.25mV;功率寄存器 0x03 是无符号,LSB 是25 × Current_LSB。分流电压寄存器 0x01 也常用,LSB 2.5μV,适合看小电流下的原始噪声。
static uint16_t ina226_read_u16(uint8_t reg) { uint8_t buf[2] = {0}; if (HAL_I2C_Mem_Read(&hi2c1, INA226_ADDR, reg, I2C_MEMADD_SIZE_8BIT, buf, 2, 100) != HAL_OK) { return 0; } return ((uint16_t)buf[0] << 8) | buf[1]; } float ina226_read_current_a(void) { int16_t raw = (int16_t)ina226_read_u16(0x04); return raw * g_current_lsb; } float ina226_read_bus_voltage_v(void) { uint16_t raw = ina226_read_u16(0x02); return raw * 0.00125f; }读电流时先做强转int16_t,因为反向电流会表现为补码负数,直接按uint16_t乘会得到几万安培的离谱值。总线电压的 0.00125f 是 1.25mV 的浮点写法,不要写成 1.25,否则读出来是千伏级。读失败返回 0 只是兜底,实际工程里要把HAL_StatusTypeDef往外传,让上层决定是重试还是报警。
数据就绪标志在屏蔽/使能寄存器 0x06 的 bit10,连续模式下可以不查,靠固定周期读;触发模式下必须等这一位置 1 再读,否则读到的是上一次的旧数据。我一般在连续模式里也保留一次回读 0x06 的动作,初始化阶段确认转换真的在跑,比盲等 50ms 踏实。
4. Keil MDK 工程落地:C 语言驱动分层与采样任务
Keil MDK 里最怕把所有东西塞进 main.c,INA226 的驱动、校准参数和业务采样混在一起,改一个分流电阻要翻三处。比较稳的做法是分三层:ina226.c/h只负责寄存器和原始码转换,app_power.c/h做滤波和告警,main.c只调用初始化、启动定时器、跑主循环。C 语言工程不需要多复杂,但接口要清楚。
4.1 驱动文件划分与接口定义
ina226.h里暴露初始化、读电流、读总线电压、读功率四个接口,寄存器地址和 I2C 句柄放在.c里,上层不直接碰HAL_I2C_Mem_Read。这样换 I2C 外设或改地址时只动一个文件。Keil MDK 的 C/C++ 选项里勾选 C99,包含路径加上驱动目录,ina226.c加进工程组,编译时就不会报隐式声明。
#ifndef INA226_H #define INA226_H #include <stdint.h> void ina226_init(float r_shunt_ohm, float i_max_a); float ina226_read_current_a(void); float ina226_read_bus_voltage_v(void); float ina226_read_power_w(void); uint8_t ina226_is_ready(void); #endif头文件只放函数声明和stdint.h,不把i2c.h暴露出去,避免上层工程被 HAL 头文件缠住。ina226_init的两个参数就是分流电阻和最大电流,调用时写ina226_init(0.01f, 2.0f),和硬件选型表对上。ina226_is_ready返回 0/1,业务层可以用它做超时判断。
ina226.c里定义#define INA226_ADDR (0x40 << 1),把配置和校准写进ina226_init,读功率可以直接读 0x03 乘功率 LSB,也可以用电流 × 总线电压自己算。前者省事,后者方便做交叉验证。我一般两个都留,调试时打印出来对比,差太多说明校准或量程有问题。
4.2 定时采样与滑动平均滤波
采样周期要大于一次完整转换时间。按 3.1 的配置,16 次平均约 35ms,定时器可以设 50ms 一次。滑动平均窗口取 8 或 16,窗口太大响应慢,太小滤波效果不明显。用环形缓冲区实现,避免每次搬数据。
#define FILTER_N 16 static float s_current_buf[FILTER_N]; static uint8_t s_index = 0; static uint8_t s_count = 0; float current_filter(float new_sample) { s_current_buf[s_index] = new_sample; s_index = (s_index + 1) % FILTER_N; if (s_count < FILTER_N) { s_count++; } float sum = 0.0f; for (uint8_t i = 0; i < s_count; i++) { sum += s_current_buf[i]; } return sum / s_count; }环形缓冲区只覆盖已写入的s_count个点,上电初期不会把 0 算进平均,否则前几秒电流会明显偏低。FILTER_N取 16 时,50ms 采样对应 800ms 的平滑窗口,适合看电池充放电趋势;如果要抓电机启动电流,把窗口降到 4,或者直接去掉滤波看原始值。滤波只对电流做,总线电压本身比较稳,做不做影响不大。
定时器回调里不要直接调用HAL_I2C_Mem_Read,I2C 是阻塞操作,放在中断里会拖慢其他任务。常见做法是回调里置标志位,主循环查到标志再读。读回来的原始电流送current_filter,滤波结果存到全局结构体,串口打印和告警都用这一份,避免多处读取导致数据不一致。
4.3 功率与告警的二次计算
功率寄存器 0x03 的 LSB 是25 × Current_LSB,2A/10mΩ 配置下约 1.525mW。读出来乘这个系数就是瓦特。也可以直接用滤波后的电流乘总线电压,两种算法在稳态下应该很接近,差超过 2% 就要查校准寄存器是不是写对了。
#define OVER_CURRENT_A 1.8f #define UNDER_VOLTAGE_V 3.0f typedef struct { float current_a; float bus_v; float power_w; uint8_t over_current; uint8_t under_voltage; } power_status_t; void power_task(power_status_t *st) { float raw_i = ina226_read_current_a(); st->current_a = current_filter(raw_i); st->bus_v = ina226_read_bus_voltage_v(); st->power_w = st->current_a * st->bus_v; st->over_current = (st->current_a > OVER_CURRENT_A) ? 1 : 0; st->under_voltage = (st->bus_v < UNDER_VOLTAGE_V) ? 1 : 0; }阈值用宏定义,改的时候不用翻业务代码。过流判断用滤波后的电流,避免尖峰误触发;如果要做硬件保护,还是得靠比较器或 MCU 的模拟看门狗,软件告警有采样周期延迟。欠压判断放在功率计算之后,母线掉电时电流也会跟着掉,先看电压能区分“负载关了”和“电源塌了”。
5. 避坑与排查:读不到、跳变、温漂的六条血泪经验
INA226 的排查顺序和普通 I2C 传感器差不多:先看总线有没有应答,再看寄存器写得对不对,最后看模拟前端和校准。下面五条是实际工程里最容易翻车的地方,每条按现象、原因、解决写,照着顺序排能省不少时间。
5.1 现象:I2C 扫描不到 0x40
现象是HAL_I2C_IsDeviceReady一直返回HAL_ERROR,示波器上看 SCL 有波形,SDA 偶尔被拉低。原因通常是地址字节没左移、上拉电阻缺失、INA226 的 VCC 没到 3.3V,或者 A0/A1 被模块焊到了非默认电平。有人把 7 位地址 0x40 直接传给 HAL,HAL 再左移一次变成 0x80,实际发出去的是 0x80 地址,当然不应答。
解决方式是先跑 2.2 的扫描代码,确认总线上有没有 0x40 或其他地址。如果扫到 0x41,说明 A0 被拉高,改INA226_ADDR就行。扫不到就量电压:VCC 对 GND 是不是 3.3V,SCL/SDA 静态是不是 3.3V,上拉电阻是不是 4.7kΩ 左右。还不行就断开 INA226,只留 STM32 和上拉,用扫描代码看总线能不能释放,排除总线被拉死。
注意:有些模块把 I2C 上拉电阻和 LED 做在一起,LED 亮的时候 SDA 电平会被拉低,扫描时好时坏。先把无关负载断开再测。
5.2 现象:电流值固定为 0 或满量程
现象是电流寄存器读出来一直是 0,或者一直是 32767 附近的数,换负载也不变。原因多半是校准寄存器没写、CAL 写成了 0、Current_LSB 单位算错,或者分流电阻值传参时把 10mΩ 写成 10Ω。CAL 为 0 时电流寄存器不更新,读出来就是 0;CAL 过大时一点点分流电压就顶到满量程。
解决方式是打印 CAL 和g_current_lsb,2A/10mΩ 时 CAL 应该在 8389 左右,g_current_lsb约 0.000061。如果 CAL 是 8389000,说明 Rshunt 传成了 0.00001 或者电流 LSB 少除了 1000。还要确认写 CAL 之前配置寄存器已经写成功,有些模块写配置后需要 1ms 才接受校准写入,中间加个短延时更稳。
5.3 现象:小电流跳动大
现象是 100mA 以下电流读数在 0.08A~0.12A 之间跳,负载稳定但数字不稳。原因是分流电阻太小,100mA 在 10mΩ 上只有 1mV,除以 2.5μV 的 LSB 只有 400 个码,再被电源纹波和 ADC 噪声一搅,低位全是抖动。平均次数低、采样周期短也会放大这个问题。
解决方式是先加大分流电阻,比如 2A 量程用 20mΩ,100mA 对应 2mV,码值翻倍;再把 AVG 提到 64 或 128 次,采样周期放到 100ms 以上。电源端加 10μF 加 100nF 去耦,分流电阻采样线远离 DC-DC 电感。如果还要更快响应,就别硬压噪声,改用硬件比较器做过流保护,INA226 只负责监测。
5.4 现象:长时间运行后读数漂移
现象是刚上电校准完很准,跑半小时后电流偏了 3%~5%,环境温度越高越明显。原因是分流电阻温漂、电阻自热、PCB 铜箔热电动势,或者 INA226 靠近发热器件。普通厚膜电阻的温度系数可能到 200ppm/℃,10mΩ 在 50℃ 温升下阻值变化 0.1%,听起来不大,但满量程 2A 时就是 2mA 的固定偏差。
解决方式是换低温漂合金电阻,温度系数 50ppm/℃ 以内,功率降额至少 50%。分流电阻下面铺铜散热,但采样线不要从散热铜箔上取。软件上可以做温度补偿,用板上的温度传感器读环境温度,按电阻温度系数修正增益;没有温度传感器就做定期零点校准,在已知零电流时记下偏移。
5.5 现象:Keil 下载后第一次正常,复位后异常
现象是 Keil MDK 下载完程序,串口数据正常;按复位键或断电重上电后,电流读数为 0 或乱跳。原因是初始化顺序依赖了调试器的复位状态,或者 INA226 没有软复位,配置寄存器还留着上一次的值;也可能是校准参数存在 RAM 里,复位后没重新写。
解决方式是在ina226_init开头先写配置寄存器 bit15 做软复位,延时 1ms,再写配置和校准。不要在main里先读电流再初始化,读到的可能是上一次的残留值。复位后把 CAL 回读一遍,确认和写入值一致。如果用了看门狗,注意 I2C 阻塞超时不要设太长,否则复位循环里卡在HAL_I2C_Mem_Read出不来。
6. 进阶验证:用已知负载和示波器把误差压到 1% 以内
校准寄存器算得再准,也只是纸面值。真正上板后,分流电阻的的实际阻值、走线电阻和焊接应力都会带来偏差,想压到 1% 以内,得用已知负载做两点校准。先断掉负载读零电流偏移,再接一个稳定电子负载读满度附近的值,算增益和偏移,比只写公式靠谱。
/* 两点校准:zero_raw 为零电流原始码,full_raw 为已知负载原始码 */ float current_calibrated(int16_t raw, int16_t zero_raw, int16_t full_raw, float full_current) { float gain = full_current / (float)(full_raw - zero_raw); return (raw - zero_raw) * gain; }这段代码里zero_raw要在负载断开、母线有电但无电流时读,full_raw在电子负载设到 80% 量程时读,full_current是负载实际值。增益算出来后,小电流和大电流都按同一条直线修正。注意零点会随温度漂,冷机校准完最好跑 10 分钟再复校一次。
| 设定电流 | 读回电流 | 误差 | 处理 |
|---|---|---|---|
| 0.1A | 0.102A | +2.0% | 检查零点偏移 |
| 0.5A | 0.503A | +0.6% | 在允许范围 |
| 1.0A | 1.004A | +0.4% | 记录增益 |
| 1.5A | 1.507A | +0.5% | 观察温升 |
| 2.0A | 2.012A | +0.6% | 降额使用 |
阶梯验证时每个点稳定 30 秒再记数,同时用示波器看分流电阻两端的纹波,纹波超过 5mV 就先处理电源,别急着改校准。温度补偿只在宽温场景需要,普通室内设备把低温漂电阻和两点校准做好,1% 以内不难。从那以后我每次拿到新的分流电阻,都先跑一遍零点和满度两点校准再上板,省得后面把软件翻个底朝天。希望帮到你。
本文还有配套的精品资源,点击获取