简介:INA226.rar是一套面向STM32嵌入式学习者与电子设计人员的电流/功率传感器驱动完整工程,围绕电流、总线电压和功率监测场景,提供从底层寄存器配置到数据采集处理的参考实现,可用于课程设计、电池管理及电源监控类项目学习借鉴。压缩包共151个文件、约2.97MB,含35个c源文件与36个h头文件,并配有Keil MDK工程文件、编译中间文件及少量说明和输出文件;源码主要承担外设初始化、I2C通信与测量流程,uvprojx等工程文件便于直接用Keil打开编译和调试。已有4713人学习下载,说明其工程结构和实现方式具有一定参考价值。读者可从中了解INA226寄存器配置、启动测量、读取电流电压数据以及与STM32外设协同的完整组织方式,并借助现成工程快速搭建验证环境、对照排查驱动问题,积累传感器驱动开发经验。
1. INA226 这颗电流计芯片,为什么值得单独拆一个驱动包
手上有个板子要测功耗,很多人第一反应是串个采样电阻,用 STM32 的 ADC 采一下,再乘乘除除。真跑起来才发现问题一大堆:ADC 分辨率被参考电压吃掉一半,几毫安的小电流基本埋在噪声里,正负电流方向还得靠外部运放抬偏置。INA226 就是把这个活儿收进一颗小芯片里:内置 16 位 ADC、I2C 接口、可编程平均次数和转换时间,总线电压最大 36V,分流电压满量程 ±81.92mV,靠一颗分流电阻就能同时给出电压、电流、功率。
INA226.rar里放的是一套面向 STM32 的纯 C 语言驱动与例程,KEIL MDK 工程结构,覆盖寄存器读写、校准值计算、四路数据换算和告警配置。它解决的不是“能不能点亮”,而是“读出来的数敢不敢拿去写报告”。适合正在做电池管理、模块功耗测试、多路电源监控的嵌入式开发者,尤其是被 ADC 方案精度折磨过一轮的人。
2. 寄存器地图与 I2C 时序:先搞清楚这颗芯片在算什么
拿到一个压缩包,最忌讳上来就main.c里看一眼然后开编译。INA226 的驱动能写多稳,取决于你对它内部那套“配置—转换—换算”链条理解到哪一步。它不是一个“读一下就有数”的器件,而是一个带状态机的小系统:配置寄存器决定它多久转一次、平均几次;分流电压和总线电压是两个独立通道;电流和功率寄存器里的数,本质上是用校准值从分流电压推算出来的。链条上任何一环设错,读数都会看起来“正常但不对”。
2.1 六个寄存器撑起整套驱动
INA226 对外只有九个可访问的 16 位寄存器,其中真正参与业务逻辑的是六个。压缩包里的ina226.h把这几个地址做成了宏,值不值得抄先不说,至少这张表得先在脑子里建起来:
| 地址 | 名称 | 复位值 | 属性 | 用途 |
|---|---|---|---|---|
| 0x00 | Config | 0x4127 | R/W | 平均次数、两路转换时间、工作模式 |
| 0x01 | Shunt Voltage | 0x0000 | R | 分流电阻两端压降,LSB = 2.5 µV,有符号 |
| 0x02 | Bus Voltage | 0x0000 | R | 总线对地电压,LSB = 1.25 mV,无符号 |
| 0x03 | Power | 0x0000 | R | 功率,LSB = 25 × Current_LSB,无符号 |
| 0x04 | Current | 0x0000 | R | 电流,LSB = Current_LSB,有符号 |
| 0x05 | Calibration | 0x0000 | R/W | 校准值,决定电流与功率的换算系数 |
| 0x06 | Mask/Enable | 0x0000 | R/W | 告警阈值、转换就绪标志、告警使能 |
| 0xFE | Manufacturer ID | 0x5449 | R | 固定值,用来验证通信链路 |
| 0xFF | Die ID | 0x2260 | R | 固定值,用来验证型号与驱动匹配 |
复位值 0x4127 拆开看是0100 0001 0010 0111:AVG = 000 表示不平均,两路转换时间都是 100 即 1.1ms,MODE = 111 表示分流和总线连续转换。这个默认值能出数,但精度很差,一次转换的结果直接往外送,噪声全在里面。压缩包例程里把它改成 AVG = 16,代价是单次完整周期拉长到约 35ms,换来的是读数跳动明显收敛。这个取舍后面还会细说。
两个固定 ID 是排查通信问题的第一道关卡。0xFE 读出来不是 0x5449,说明 I2C 根本没通,别往下查换算;0xFF 读出来不是 0x2260,说明通是通了,但挂的不是这颗片子,可能是地址撞了别的器件。很多“读出来全是 0”的现场问题,卡在这一步就能定性。
2.2 硬件 I2C 还是软件 I2C:先看板子和时钟
STM32 的硬件 I2C 早年有过不少争议,现在 F0/F1/F4/G0/H7 各系列基本都能用,但前提是引脚复用、上拉电阻、时钟频率这几件事都配对了。总线速率 100kHz 或 400kHz 都行,INA226 支持到 400kHz 以上,实际工程里 100kHz 更稳,因为它的转换周期本来就是几十毫秒级别,总线快那么一点对整体刷新率没有意义。
压缩包里的做法是把 I2C 抽象成两个函数指针塞进设备结构体,硬件 I2C 和软件模拟 I2C 各写一份实现,上层完全不用改:
/* ina226.h —— 把总线操作抽象出去,硬件/软件 I2C 都能挂 */ typedef struct { uint8_t addr; /* 7 位地址,0x40 ~ 0x4F */ float i_lsb; /* 电流 LSB,单位 A */ float p_lsb; /* 功率 LSB,单位 W */ uint16_t cal; /* 写进校准寄存器的值 */ int (*i2c_write)(uint8_t addr, const uint8_t *buf, uint16_t len); int (*i2c_write_read)(uint8_t addr, const uint8_t *wbuf, uint16_t wlen, uint8_t *rbuf, uint16_t rlen); } ina226_dev_t;i2c_write_read这一路是关键:INA226 读寄存器要求先写一个字节的寄存器地址,然后不发 STOP,直接 repeated start 再读两个字节。用 STM32 HAL 的话对应HAL_I2C_Mem_Read,用标准库就得自己拼时序。软件模拟 I2C 的实现里必须保证 SCL 高电平期间 SDA 稳定,延时不能省,很多人图快把delay_us(1)改成空循环,结果在 72MHz 主频下时序直接被编译器优化掉,读出来全是 0xFF。
地址方面,A1、A0 两个引脚决定 7 位地址的低两位,都接地是 0x40,都接 VS 是 0x4F。板上如果挂了好几颗,靠这两个脚区分。压缩包默认按 0x40 配置,改地址只需要动结构体里的addr字段。
2.3 上电第一件事:读回两个固定 ID
不管后面要写多复杂的业务逻辑,驱动里一定要有个probe函数,不做别的,只读 ID。这是整个包子里最便宜也最值钱的一段代码:
/* ina226_reg.c —— 最底层两个函数,所有上层读数都建立在它们之上 */ #include "ina226.h" /* 写 16 位寄存器:先发寄存器地址,再发高字节、低字节 */ int ina226_write_reg(ina226_dev_t *dev, uint8_t reg, uint16_t val) { uint8_t buf[3]; buf[0] = reg; buf[1] = (uint8_t)(val >> 8); /* INA226 是大端,高字节先出 */ buf[2] = (uint8_t)(val & 0xFF); return dev->i2c_write(dev->addr, buf, 3); /* 返回 0 表示成功 */ } /* 读 16 位寄存器:先写寄存器地址,再 repeated start 读两字节 */ int ina226_read_reg(ina226_dev_t *dev, uint8_t reg, uint16_t *val) { uint8_t raw[2]; int ret = dev->i2c_write_read(dev->addr, ®, 1, raw, 2); if (ret != 0) return ret; *val = ((uint16_t)raw[0] << 8) | raw[1]; return 0; } /* probe:只验证通信和型号,不碰配置寄存器 */ int ina226_probe(ina226_dev_t *dev) { uint16_t mid = 0, did = 0; if (ina226_read_reg(dev, INA226_REG_MANU_ID, &mid) != 0) return -1; if (mid != 0x5449) return -2; /* 链路不通或地址错 */ if (ina226_read_reg(dev, INA226_REG_DIE_ID, &did) != 0) return -3; if (did != 0x2260) return -4; /* 型号不匹配 */ return 0; }参数上唯一需要注意的是字节序。INA226 的所有 16 位寄存器都是高字节在前,STM32 是小端,所以读写都必须手动拆字节,不能直接把uint16_t*强转成uint8_t*丢给 I2C 外设。这个坑踩一次记一辈子:现象是读出来的电压值等于实际值乘 256 再加点什么,看着像量程错了,其实是高低字节反了。
probe返回值的分层设计也有讲究,-1 是总线错误,-2 是 ID 不对,-3 是第二次读失败,-4 是型号不对。现场调试时看一眼返回值就知道该查硬件还是查代码,比打印一堆日志高效得多。
3. 校准寄存器怎么算:分流电阻、Current_LSB 与量程的三角关系
INA226 的电流寄存器不是直接测出来的,是芯片用校准寄存器里的系数,把分流电压换算成电流的。校准值写错,读出来的电流可以差几倍甚至几十倍,而且因为数值“看起来在动”,特别容易骗过初测。这一章把公式拆开,再给三组不同量程的现成参数。
3.1 CAL 是怎么推出来的
推导只有三步,但每一步都值得写下来贴在工位上。
第一步,分流电压的 LSB 是固定的 2.5 µV,所以分流电压寄存器的原始值shunt_raw对应实际压降shunt_raw × 2.5µV。
第二步,电流 = 压降 / 分流电阻,即I = shunt_raw × 2.5µV / R_shunt。同时芯片规定电流寄存器的 LSB 是一个由用户自己定义的Current_LSB,于是current_raw = I / Current_LSB。
第三步,芯片内部实际执行的是current_raw = shunt_raw × CAL / 2048。把前两步代进去:
shunt_raw × 2.5e-6 / (R × I_LSB) = shunt_raw × CAL / 2048 CAL = 2048 × 2.5e-6 / (R × I_LSB) = 0.00512 / (R × I_LSB)0.00512这个常数就是这么来的,不是什么玄学系数。而Current_LSB通常按I_max / 32768取,32768 是 15 位有符号的正半轴,因为校准寄存器最高位必须为 0,有效范围只有 0~32767。
功率那边是顺带的:Power_LSB = 25 × Current_LSB。芯片内部做的是power_raw = current_raw × bus_raw / 20000,这个关系可以用来做自检,后面第 6 章会用到。
还有个硬约束容易被忽略:分流电压满量程是 ±81.92mV,所以R_shunt × I_max必须小于 81.92mV,否则大电流时寄存器会饱和削顶。选电阻的时候先算这个乘积,再去算 CAL,顺序不能反。
3.2 三组典型量程的参数表与代码
不同场景对分流电阻的要求差别很大。测电机堵转电流要用毫欧级,测传感器待机电流要用百毫欧级。下面三组是我在不同板子上实际用过的配置,可以直接抄:
| 方案 | R_shunt | 预期 I_max | 满量程压降 | Current_LSB | CAL 写入值 | Power_LSB |
|---|---|---|---|---|---|---|
| A 大电流 | 2 mΩ | ±10 A | 20 mV | 312.5 µA | 8192 | 7.8125 mW |
| B 通用 | 10 mΩ | ±2 A | 20 mV | 62.5 µA | 8192 | 1.5625 mW |
| C 小电流 | 100 mΩ | ±0.4 A | 40 mV | 12.5 µA | 4096 | 0.3125 mW |
三组的压降都留了余量,没往 81.92mV 上顶。方案 C 已经到 40mV,再往上加电流就会削顶,用的时候心里要有数。
压缩包里的校准计算写成了自适应的,传入电阻和最大电流,自己算 LSB 和 CAL,CAL 溢出就自动把 LSB 翻倍重算:
/* ina226_cal.c —— 由分流电阻和预期最大电流反算 CAL */ #define INA226_CAL_K 0.00512f static uint16_t ina226_build_cal(float r_shunt, float i_max, float *i_lsb_out) { float i_lsb = i_max / 32768.0f; /* 先按 15 位正满量程取 LSB */ uint16_t cal; for (;;) { cal = (uint16_t)(INA226_CAL_K / (i_lsb * r_shunt) + 0.5f); if (cal > 0u && cal <= 32767u) break; i_lsb *= 2.0f; /* CAL 超 15 位就放大 LSB 重算 */ } *i_lsb_out = i_lsb; return cal; } int ina226_init(ina226_dev_t *dev, float r_shunt, float i_max) { uint16_t cfg; if (ina226_probe(dev) != 0) return -1; /* AVG=16(010), VBUSCT=1.1ms(100), VSHCT=1.1ms(100), MODE=连续(111) */ cfg = (2u << 11) | (4u << 9) | (4u << 6) | (7u << 3); if (ina226_write_reg(dev, INA226_REG_CONFIG, cfg) != 0) return -2; dev->cal = ina226_build_cal(r_shunt, i_max, &dev->i_lsb); dev->p_lsb = 25.0f * dev->i_lsb; return ina226_write_reg(dev, INA226_REG_CALIB, dev->cal); }r_shunt单位是欧姆,传 0.01 而不是 10,单位错了 CAL 会差三个数量级,读数直接飞。i_max传的是正负对称量程的单边值,比如 ±2A 就传 2.0。配置寄存器的四个字段:AVG 占 bit13~11,写 2 表示 16 次平均;两个转换时间字段各占 3 位,写 4 表示 1.1ms;MODE 占 bit5~3,写 7 表示分流和总线都连续转换。
AVG = 16 时一轮完整周期是16 × (1.1ms + 1.1ms) ≈ 35.2ms。后面调度采样的时候,间隔必须大于这个数,否则读到的还是上一轮的结果,看起来像“数据不更新”。
3.3 补码与符号:负电流怎么读
分流电压寄存器和电流寄存器都是有符号的 16 位补码,功率和总线电压是无符号的。这个区别在充电、放电双向场景里是致命的:电池放电时电流为正,充电时为负,如果按无符号读,充电电流会变成一个三万多的大数,直接把人看懵。
/* ina226_read.c —— 一次读齐四路数据,符号处理全在这一层做完 */ int ina226_read_all(ina226_dev_t *dev, ina226_sample_t *s) { uint16_t raw; if (ina226_read_reg(dev, INA226_REG_BUS_V, &raw) != 0) return -1; s->bus_v = (float)raw * 0.00125f; /* 1.25 mV/LSB,无符号 */ if (ina226_read_reg(dev, INA226_REG_SHUNT_V, &raw) != 0) return -2; s->shunt_v = (float)(int16_t)raw * 0.0000025f; /* 2.5 µV/LSB,先转 int16_t */ if (ina226_read_reg(dev, INA226_REG_CURRENT, &raw) != 0) return -3; s->current_a = (float)(int16_t)raw * dev->i_lsb; /* 有符号,负值代表反向 */ if (ina226_read_reg(dev, INA226_REG_POWER, &raw) != 0) return -4; s->power_w = (float)raw * dev->p_lsb; /* 无符号 */ return 0; }关键就在那两个(int16_t)强转。少了它,-1会被当成65535,乘完 LSB 变成 +20A 之类的天文数字。这一层把所有符号问题收口,上层业务拿到的current_a就是带正负的物理量,不用再关心寄存器格式。
shunt_v这项单独留着很有用:拿shunt_v / current_a反算出来的电阻值,应该和焊上去的分流电阻一致。不一致说明电阻焊错了或者型号买错了,比对着丝印用放大镜找快得多。
4. 把驱动挂进 STM32 工程:C 语言分层与 Keil MDK 配置
压缩包能在别人机器上编译通过,在你机器上报一堆undefined symbol,八成不是代码问题,是工程配置问题。这一章说清楚文件怎么分层、宏怎么开、Keil MDK 里哪几个勾必须打。
4.1 三层结构:bsp / drv / app
包里按三层组织,虽然只是几个文件,但边界划得很清楚:
Core/ bsp_i2c.c / bsp_i2c.h ← 只做 I2C 收发,不认 INA226 bsp_delay.c ← 微秒延时,软件 I2C 要用 Drivers/ina226/ ina226.h ← 寄存器宏、结构体、对外接口 ina226_reg.c ← 读写寄存器 + probe ina226_cal.c ← 校准计算 + 初始化 ina226_read.c ← 四路数据读取与换算 App/ app_power_monitor.c ← 采样调度、滤波、业务上报bsp_i2c.c里只暴露i2c_write和i2c_write_read两个函数,它们不包含任何ina226.h。ina226.h里也不包含stm32f1xx_hal.h,只依赖stdint.h。这样一来,同一份驱动换到别的 MCU 上,只要重写 bsp 那两个函数,drv 层一个字都不用改。这个分层不是为了好看,是为了三个月后换平台时不用重写。
移植时唯一要改的是ina226_init调用前给结构体两个函数指针赋值,以及addr字段:
/* app_power_monitor.c —— 在应用层完成绑定,drv 层保持平台无关 */ static ina226_dev_t g_ina = { .addr = 0x40, .i2c_write = bsp_i2c_write, .i2c_write_read = bsp_i2c_write_read, }; void power_monitor_start(void) { int ret = ina226_init(&g_ina, 0.010f, 2.0f); /* 10 mΩ,±2A */ if (ret != 0) { /* ret 的负值直接对应 probe 里的失败点,方便定位 */ app_log("ina226 init failed: %d", ret); return; } g_ready = 1; }0.010f这个f后缀别省。在 Keil 的 ARMCC 默认设置下,浮点字面量不带f会被当成 double,整个表达式升到双精度,代码体积和执行时间都上去,虽然 Cortex-M4 以上能扛,但没必要。
4.2 轮询、中断与 Alert 引脚怎么选
三种读取方式,压缩包里都给了,实际选哪个看采样率和实时性要求。
纯定时轮询最简单:主循环里按 50ms 间隔调一次ina226_read_all,因为 35.2ms 的转换周期摆在那,轮询再快也是读重复值。这种方式的好处是阻塞时间可控,坏处是如果主循环里有别的耗时任务,采样间隔会被拉长,数据时间戳不准。
转换就绪中断依赖 Mask/Enable 寄存器里的 CVRF 位和 ALERT 引脚。配置好之后每次转换完成 ALERT 拉低,STM32 用外部中断接。这种方式时间戳最准,但要注意 ALERT 是开漏输出,必须配上拉,而且中断服务函数里不能直接做 I2C 读,得置个标志回主循环处理。
还有一种是阈值告警,用 Mask/Enable 里的 SOL/SUL 位设置电流上下限,超限才触发中断。做电池保护、过流检测的时候用这个,正常情况下完全不占 CPU。压缩包的例程里给了ina226_set_alert_current的实现,写的是 0x06 和 0x07 两个寄存器组成的阈值。
我一般这么选:需要连续记录功耗曲线用轮询,需要精确时间间隔用 CVRF 中断,只关心超限用阈值告警。三者在同一个工程里可以共存,因为都基于同一套寄存器访问函数。
4.3 Keil MDK 里必须打对的几个勾
工程配置这块翻车的人最多,列几条必查项。
第一,Include Paths 里必须加上Drivers/ina226和Core两个目录。少加一个,报错信息是cannot open source input file "ina226.h",看着像文件丢了,其实是路径没配。
第二,C/C++ 选项卡里的 C 标准要选 C99 或更高。驱动里用了//注释和声明与语句混排,C89 模式下直接报错。ARMCC 5 的话在Misc Controls里填--c99,ARMCC 6 默认就是 C99,不用管。
第三,优化等级。软件 I2C 的延时函数如果写成空循环,-O2及以上会被整个优化掉,时序全乱。两个办法:延时函数里加__NOP()或者用volatile计数器;或者干脆把 bsp 文件单独设成-O0。我一般选后者,省事。
第四,如果你的板子用的是硬件 I2C 且开了中断或 DMA,注意HAL_I2C_Mem_Read的超时参数别设成HAL_MAX_DELAY。INA226 掉线的时候总线会被拉死,超时设成无穷大等于整个系统卡死在那里。给个 10ms 就够,错了就返回错误码让上层处理。
第五,工程里如果有多个.c都包含了ina226.h,注意结构体定义别放在头文件里直接定义变量,只放typedef和extern声明。这个错误的表现是multiply defined symbol,链接阶段才报,编译阶段一切正常,很有迷惑性。
5. 避坑与排查:五条血泪记录
下面五条都是我在不同板子上真实遇到过的问题,按“现象 → 原因 → 解决”写,遇到类似的直接对号入座。
5.1 通信层:读回来全是 0xFFFF 或 0x0000
现象:ina226_probe返回 -2,或者读出来的所有寄存器都是 0xFFFF,偶尔是 0x0000,两种情况随机出现。
原因:0xFFFF 通常是 SDA 被释放后上拉电阻拉高,主机读到的就是全 1,说明从机根本没应答;0x0000 则可能是 SDA 被一直拉低,总线被某个器件锁死。前者查地址和接线,后者查上拉和总线状态。还有一种情况是上拉电阻用了 10kΩ,在 400kHz 下上升沿太缓,读出来的高位会丢。
解决:先用万用表量 SDA/SCL 静态电平,正常应该都是 3.3V。再把上拉换成 2.2kΩ 或 4.7kΩ。如果总线被锁死,给 SCL 手动发 9 个时钟脉冲让从机释放 SDA,然后发 STOP。地址方面,A1/A0 悬空不等于接地,很多板子上是接到 VS 的,实际地址可能是 0x44 而不是 0x40,用扫描程序把 0x40~0x4F 全过一遍最省事。
5.2 换算层:电压正常,电流一直是 0
现象:总线电压读数完全正确,分流电压也有几毫伏的数值,但电流寄存器和功率寄存器始终是 0。
原因:校准寄存器没写进去,或者写进去的是 0。INA226 的电流和功率完全依赖校准值,CAL = 0 时芯片算出来就是 0,而分流电压是硬件直接转换的,不受影响。这是最容易误判成“芯片坏了”的一种情况。
解决:读回 0x05 寄存器确认值不为 0,且和预期一致。常见诱因是ina226_init里先写 Config 再写 CAL,中间某一步 I2C 出错提前 return 了,CAL 那步没执行到。加个返回值检查,或者初始化完成后统一回读一遍配置寄存器和校准寄存器做校验。
5.3 精度层:小电流段读数乱跳,大电流反而稳
现象:测几十毫安以下的时候,读数在 ±30mA 范围内随机跳,接上 1A 负载反而很稳。
原因:瓶颈在分流电压的量化误差。2.5µV 的 LSB,配上 10mΩ 电阻,单步就对应 0.25mA,再加上 ADC 噪声和电源纹波,小信号段信噪比很差。AVG = 1 的时候尤其明显。
解决:把 AVG 提到 64 或 128,代价是转换周期变成 140ms 以上,只适合慢速监测。另一个办法是换更大的分流电阻,比如测小电流专用板用 100mΩ,单步降到 0.025mA,但要注意压降不能超 81.92mV,最大电流就被限制在 0.8A 以内。两个办法可以叠加,先按量程选电阻,再用平均压噪声。
5.4 一致性:功率寄存器和“电压×电流”对不上
现象:自己拿bus_v × current_a算出来的功率,和直接读功率寄存器的值差 1%~5%,有时候差得更多。
原因:三个可能。一是两次读取不在同一个转换周期,电压和电流来自不同时刻,负载波动时自然对不上;二是功率寄存器的 LSB 是25 × Current_LSB,如果p_lsb算错(比如漏乘 25,或者用错了 i_lsb),换算出来就偏;三是功率寄存器本身是无符号的,负电流场景下芯片内部的乘法结果和你想的不一样。
解决:连续性要求高的场景,先把 Mask/Enable 里的 CVRF 位读出来确认转换完成,再一次性连读四个寄存器。p_lsb在初始化时算好存进结构体,别在读取函数里现算。负电流场景下不要用功率寄存器,直接用bus_v × current_a自己乘,符号才正确。
5.5 硬件层:读数随温度漂,或者一接负载就偏
现象:空载时读数正常,一加上负载,电流值比万用表串测的高出 5%~10%,板子发热后漂得更厉害。
原因:分流电阻的温漂和走线电阻。普通厚膜电阻的温漂能到 100ppm/℃ 以上,大电流下自身发热几十度,阻值就变了。另外,如果电压采样点没有严格从分流电阻的两个焊盘内侧引出(开尔文接法),PCB 走线的那段铜箔电阻会被算进去,等效阻值偏大,读数就偏高。
解决:换合金分流电阻,温漂能压到 50ppm/℃ 以内甚至更低。PCB 上做四线开尔文连接,采样走线从电阻焊盘内侧单独引出,不要和大电流路径共用。这两条改完,同一块板子的偏差能从 8% 降到 1% 以内。
6. 精度验证与两点校准:让读数敢写进测试报告
驱动跑通只是及格线,真正要交付的时候,别人会问一句“你这个数准吗”。我一般的做法是先做自检,再做外部比对。
自检最省事的一条是拿功率寄存器反推:power_raw × 25 × i_lsb应该约等于bus_v × current_a,误差在 1% 以内说明芯片内部的乘法和你的换算链路是一致的。如果不一致,问题一定在你的换算系数上,跟硬件无关。这个检查不用任何外部设备,写个断言就行。
外部比对需要一个能当基准的万用表和一个可调负载。步骤是:空载记录一次,加上一个已知电流(比如用万用表串测出来的 1.000A)再记录一次,两次数据就能算出增益和偏置:
/* 两点校准:ref 为万用表读数,raw 为 INA226 原始读数 */ typedef struct { float gain; float offset; } ina_cal_t; void ina226_two_point_fix(ina_cal_t *c, float ref_lo, float raw_lo, float ref_hi, float raw_hi) { c->gain = (ref_hi - ref_lo) / (raw_hi - raw_lo); c->offset = ref_lo - c->gain * raw_lo; } /* 使用:把驱动读到的值过一遍 */ static inline float ina226_apply_cal(const ina_cal_t *c, float raw) { return c->gain * raw + c->offset; }两点之间的距离要拉大,空载到 80% 量程效果最好,两个点挨太近的话,噪声会被放大成错误的增益。校准完再取中间点(比如 40% 量程)验证一次,残差应该在 0.5% 以内。如果中间点偏得多,说明不是线性误差,得回去查分流电阻的温漂或者 PCB 走线。
还有个细节:这套校准系数别写死在代码里,放到 Flash 的配置区或者外部 EEPROM,每块板子出厂标定一次。同一批板子用同一组系数,看着省事,实际上分流电阻的 1% 公差会直接变成读数的 1% 偏差,白瞎了 16 位 ADC 的分辨率。
从那以后我每次焊完新板子,第一件事不是烧主程序,而是烧一个只跑probe和单点比对的测试固件,把校准系数读出来记在板子背面。这个习惯救过我两次,一次是分流电阻贴片贴反了型号,一次是一批电阻的公差超标,都是在上整机之前发现的。希望帮到你。
本文还有配套的精品资源,点击获取