STM32驱动INA226:校准寄存器配置与I2C读取完整指南
简介STM32通过IIC协议控制INA226电流检测芯片的驱动源码面向嵌入式开发与电源管理方向学习人群解决微控制器与高精度电流/电压监测芯片之间的通信配置问题。压缩包内共2个文件包含1个C源文件和1个头文件整体仅4KB结构精简方便直接查看与移植。目前已有6641人学习下载适用于STM32 HAL库或LL库工程。代码实现覆盖IIC引脚初始化、起始/停止条件与应答处理以及INA226内部寄存器如配置寄存器、分流电压寄存器、总线电压寄存器的读写操作可帮助开发者快速完成采样电阻设定、实时电压电流读取等核心功能。对于需要将INA226集成到电池管理系统、电源监控或负载检测场景的工程师这份驱动源码提供了清晰可用的参考实现节省协议调试时间。1. 从“读回来全是 0”开始一次 INA226 驱动翻车后的完整复盘用 STM32 通过 I²C 总线读取 INA226 的电压、电流和功率数据听起来是入门级任务实际项目里翻车率不低。最常见的是数据手册翻完了I²C 波形也抓了寄存器读回来全是 0要么是电压正常了电流和功率却怎么都对不上。问题大概率不是芯片有问题而是 INA226 这类带内部校准功能的监测芯片有一个必须先遵守的初始化顺序——校准寄存器没写或者写错后面读再多次数据寄存器都是空转。这篇笔记以 STM32 HAL 库为主线把从初始化到读回物理量的流程完整走一遍重点讲清校准数学、配置字怎么定、数据怎么换算以及五个高频故障的排查顺序。新手可以照着抄代码熟手可以直接跳到最后一章的闭环校准方法。2. INA226 寄存器地图与校准数学为什么校准寄存器决定数据是否有效2.1 一张表理清四个核心寄存器INA226 对外暴露的寄存器不算多真正决定数据链路的是四个配置寄存器0x00、分流电压寄存器0x01、总线电压寄存器0x02和校准寄存器0x05。功率寄存器0x03和电流寄存器0x04是芯片内部根据分流电压和校准值算出来的结果不是 ADC 直接量化的产物。地址寄存器位宽数据含义读写属性0x00配置寄存器16 位模式、平均次数、转换时间读 / 写0x01分流电压寄存器16 位分流电阻两端压差有符号补码只读0x02总线电压寄存器16 位总线电压低 3 位恒为 0只读0x03功率寄存器16 位内部计算功耗结果只读0x04电流寄存器16 位内部计算电流结果有符号补码只读0x05校准寄存器16 位电流 LSB 对应的校准系数读 / 写这张表能解释很多初学时的困惑。比如为什么单独读 0x02 有电压但读 0x04 电流永远是 0因为电流寄存器的值不是直接量化结果而是内部用分流电压寄存器的数值结合校准寄存器算出来的。校准寄存器在上电复位后默认是 0x0000不做初始化就读数据电流和功率自然是 0。先弄懂这个数据流关系后面所有初始化步骤就有了逻辑基础。另一个容易忽略的是寄存器地址长度。INA226 的寄存器地址是 8 位不是某些 EEPROM 或数字电位器那种 16 位寄存器地址。HAL 库的HAL_I2C_Mem_Read里有个MemAddSize参数选I2C_MEMADD_SIZE_8BIT如果照抄别的传感器代码用了 16BIT读写地址会整体错位一个字节表现就是读回来的数据和寄存器表对不上。2.2 校准寄存器先定电流 LSB再反推校准值校准计算是 INA226 使用里最容易被忽略、也最容易错的环节。数据手册给的公式是校准值 0.00512 /电流 LSB × 分流电阻值其中 0.00512 是芯片内部固定的校准系数电流 LSB 是自己希望达到的电流分辨率单位是安培。这个公式本质上是把“每一位代表多少电流”翻译成芯片能用的整数系数。选电流 LSB 时有一个硬约束电流 LSB × 32767 必须大于最大测量电流因为电流寄存器是 16 位有符号数最大正数只有 32767。如果最大负载是 5A电流 LSB 最小可以取 5 / 32767 ≈ 0.0001526A再小就会溢出。但实际使用中不建议卡着上限选还要考虑电流方向和瞬时过冲常见做法是留 50% 以上的余量。举例分流电阻 10mΩ0.01Ω最大工作电流 5A我取电流 LSB 为 0.5mA0.0005A余量能覆盖到 16A。代入公式校准值 0.00512 /0.0005 × 0.01 0.00512 / 0.000005 1024 0x0400如果把电流 LSB 取成 1mA校准值变成 512代码改动只有一行但电流分辨率从 0.5mA 变成 1mA12V / 5A 场景下功率分辨率也跟着翻倍。所以这个值应该在设计评审阶段就定下来而不是写代码时随手填个 512。2.3 配置寄存器平均次数、转换时间与模式配置寄存器各位的用途很多文章一句话带过但实际影响很大。bit15 是软件复位位bit11-9 是平均次数设置bit8-6 是总线电压转换时间bit5-3 是分流电压转换时间bit2-0 是工作模式。如果完全用默认值INA226 处于连续转换模式平均次数为 1 次总线电压和分流电压的转换时间均为 1.1ms 左右。对于电源纹波大、负载快速变化的场景单次平均的读数会来回跳。我一般会打开平均功能把平均次数设为 4 次两个转换时间提到 2116us得到一个更稳定的读数。对应的配置字计算平均 4 次对应 bit11-9 写成 001总线电压转换时间 2116us 对应 bit8-6 写成 101分流电压转换时间相同bit5-3 也是 101连续分流 总线模式对应 bit2-0 写成 111。拼接成 16 位二进制为 0000 0011 0110 1111十六进制就是 0x036F。注意保留位和复位位都写 0避免配置过程中触发意外复位。这里提醒一句如果你用的不是原厂芯片而是替代料少数型号的位域编码可能不一样改配置前先对照手头芯片的数据手册把 2.2 节提到的表过一遍别直接套原厂配置字。3. STM32 HAL 库 I2C 驱动封装把 Mem_Write 和 Mem_Read 用对3.1 I2C 初始化速率、上拉电阻和地址左移STM32 的 HAL 库把 I²C 主机操作封装成了几个现成函数其中最适合 INA226 的是HAL_I2C_Mem_Write和HAL_I2C_Mem_Read。这两个函数带寄存器地址参数适合访问有内部寄存器映射的器件能少写很多手动组帧代码。初始化时最重要的参数是时钟频率。INA226 支持标准模式 100kHz 和快速模式 400kHz但 400kHz 对总线电容和上拉电阻更敏感。如果 PCB 上 I²C 走线超过 5cm或者总线上并联了多个设备标配的 4.7kΩ 上拉可能不够信号上升沿变缓最明显的就是偶发 NACK。实际项目中我一般先用 100kHz 排除通信问题稳定后再切 400kHz。下面是一段可直接编译的 I2C1 初始化函数I2C_HandleTypeDef hi2c1; void I2C1_Init(uint32_t speed_hz) { hi2c1.Instance I2C1; hi2c1.Init.ClockSpeed speed_hz; /* 100000 或 400000 */ hi2c1.Init.DutyCycle I2C_DUTYCYCLE_2; hi2c1.Init.OwnAddress1 0; /* 主机模式用不到 */ hi2c1.Init.AddressingMode I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode I2C_DUALADDRESS_DISABLE; hi2c1.Init.GeneralCallMode I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode I2C_NOSTRETCH_DISABLE; if (HAL_I2C_Init(hi2c1) ! HAL_OK) { Error_Handler(); } }这段代码里重点说明两点。第一NoStretchMode必须保持I2C_NOSTRETCH_DISABLE。如果误开了时钟拉伸禁止从设备在数据未准备好时拉低时钟会被主控忽略读回来的数据是旧的或空的。第二ClockSpeed在调试时建议先用 100k通信稳定后再改 400k不要追求速度掩盖掉潜在的电气问题。INA226 的 7 位器件地址由 A0、A1 引脚电平决定。两个引脚都接地时地址是 0x40A0 接 VCC、A1 接地是 0x41反过来是 0x44都接 VCC 是 0x45。HAL 库函数里填的地址需要左移一位因为 7 位地址在总线上高 7 位发送最低位是读写标志。所以0x40 1 0x80才是总线地址直接填 0x40 会变成访问 0x20 那台设备。3.2 单寄存器写入字节序和超时设置INA226 所有寄存器都是 16 位I²C 读写时高字节在前、低字节在后。写一个寄存器的封装如下#define INA226_ADDR (0x40 1) static uint8_t tx_buf[2]; HAL_StatusTypeDef INA226_WriteReg(uint8_t reg, uint16_t val) { tx_buf[0] (uint8_t)(val 8); /* 高字节在前 */ tx_buf[1] (uint8_t)(val 0xFF); /* 低字节在后 */ return HAL_I2C_Mem_Write(hi2c1, INA226_ADDR, reg, I2C_MEMADD_SIZE_8BIT, tx_buf, 2, 100); }这里val 8之后一定要显式截断成 uint8_t避免隐式类型转换问题。把val声明成int16_t的话负数右移会带符号位扩展高字节变成 0xFF寄存器值被写坏这个细节我见过不止一次。timeout 参数 100ms 在单独调用时没问题如果系统里有多任务共享同一总线或者用了 DMA 模式建议按任务调度周期适当放大。残余超时错误可以靠HAL_I2C_GetError捕获返回HAL_I2C_ERROR_TIMEOUT时做一次总线复位。3.3 多寄存器连续读取一次拿回四个物理量分三次读分流电压、总线电压、电流会浪费总线时间。INA226 的数据寄存器地址从 0x01 到 0x04 是连续的支持从 0x01 开始连续读 8 字节一次把四个量全拿回来static uint8_t rx_buf[8]; HAL_StatusTypeDef INA226_ReadAll(uint16_t *shunt_raw, uint16_t *bus_raw, uint16_t *power_raw, uint16_t *current_raw) { HAL_StatusTypeDef status HAL_I2C_Mem_Read(hi2c1, INA226_ADDR, 0x01, I2C_MEMADD_SIZE_8BIT, rx_buf, 8, 100); if (status ! HAL_OK) { return status; } *shunt_raw (uint16_t)((rx_buf[0] 8) | rx_buf[1]); *bus_raw (uint16_t)((rx_buf[2] 8) | rx_buf[3]); *power_raw (uint16_t)((rx_buf[4] 8) | rx_buf[5]); *current_raw (uint16_t)((rx_buf[6] 8) | rx_buf[7]); return HAL_OK; }连续读模式下这四个量来自同一时刻的快照因为 INA226 会在每次转换完成后把所有数据寄存器同步更新。这也解释了为什么推荐连读而不是分三次读——分三次读虽然也能拿到数但三次读之间可能有新的转换完成四个量在时间上不一致算出的功率会出现偏差。返回顺序容易出错的是rx_buf从 0x01 开始依次是分流电压、总线电压、功率、电流。如果只需要前两个量长度写 4 就读两个寄存器剩下的留在下一次。不要在主循环里高频连续调用这个函数数据转换本身需要时间读得太快只会拿到上一次的旧快照。3.4 错误处理HAL_I2C_GetError 怎么用轮询模式下 I²C 偶发错误不可避免比较实际的做法是在读取失败后查一下错误标志再做相应恢复void INA226_HandleError(void) { uint32_t err HAL_I2C_GetError(hi2c1); if (err HAL_I2C_ERROR_AF) { /* 从设备无应答检查地址和硬件连接 */ INA226_WriteReg(0x00, 0x8000); /* 复位从设备 */ } else if (err HAL_I2C_ERROR_TIMEOUT) { /* 总线超时先发停止位释放总线 */ HAL_I2C_Init(hi2c1); /* 重新初始化 I2C 外设 */ } }错误处理的原则是“能恢复就不打断主流程”。如果错误连续出现 3 次以上再考虑重新初始化 I²C 外设和 INA226因为大概率是硬件级问题软件恢复只是暂时掩盖。4. 寄存器值换算物理量初始化序列与数据解析的完整闭环4.1 初始化三步复位、配置、校准在读取数据之前必须按照固定顺序完成初始化。顺序错了不会报错但数据一定不对。第一步是软件复位。往配置寄存器写 0x8000触发一次内部复位把芯片恢复到已知状态。注意复位完成后配置寄存器会被重置回默认值所以复位操作要独立执行不能和后续配置写在一起否则复位会把你刚写的配置清掉。第二步是写配置寄存器。配置字按照第 2 章的位域计算得出。下面这段代码把配置设为平均 4 次、转换时间 2116us、连续分流 总线模式对应 0x036Fuint16_t cal_value; /* 第一步软件复位 */ INA226_WriteReg(0x00, 0x8000); HAL_Delay(1); /* 第二步写配置寄存器 */ /* 二进制 0000 0011 0110 1111 */ /* AVG001(4次), VBUSCT101, VSHCT101, MODE111 */ INA226_WriteReg(0x00, 0x036F); /* 第三步写校准寄存器 */ /* 分流电阻 0.01R电流 LSB 0.5mA - 校准值 1024 0x0400 */ cal_value (uint16_t)(0.00512f / (0.0005f * 0.01f)); INA226_WriteReg(0x05, cal_value); HAL_Delay(10); /* 等待第一次转换完成 */cal_value的计算用浮点完成再截断成整型是因为 0.00512 和 0.0005、0.01 都不是整数直接用整数除法会得到 0。计算完成后最好把 cal_value 回读一次确认写入成功回读值和写入值不一致时优先检查地址引脚和 I²C 错误标志。延时 10ms 是因为配置 0x036F 下单次转换时间是 2116us总线 2116us分流平均 4 次意味着最多需要 16.9ms 才能完成第一批数据。如果只延时 2ms 就开始读读到的可能还是复位后的默认值。实际项目中我把延时放在初始化函数末尾轮询读数据时靠调用间隔自然保证时序不额外加延时。4.2 补码解析与位宽对齐四个物理量的缩放系数数据读回来了还要乘对应的缩放系数才是物理值。四个寄存器的系数如下分流电压LSB 2.5µV数据为有符号补码总线电压LSB 1.25mV寄存器低 3 位恒为 0有效数据在 bit15-3需要右移 3 位电流LSB 电流 LSB自己设定的值数据为有符号补码功率LSB 25 × 电流 LSB无符号用代码实现换算float current; float shunt_v, bus_v, power; int16_t shunt_raw, current_raw; uint16_t bus_raw, power_raw; /* 假设已经通过 INA226_ReadAll 拿到原始值 */ shunt_raw (int16_t)shunt_raw; /* 有符号补码 */ current_raw (int16_t)current_raw; /* 有符号补码 */ shunt_v shunt_raw * 2.5e-6f; bus_v (bus_raw 3) * 1.25e-3f; power power_raw * (25.0f * 0.0005f); current current_raw * 0.0005f;bus_raw 右移 3 位再乘 1.25mV和直接乘 0.15625mV 等价但前者更容易看懂也方便对应数据手册的位定义。power 的系数写成25.0f * 0.0005f而不是硬编码 0.0125好处是以后改电流 LSB 时只要改一处不会因为忘改功率系数而翻车。有一个容易混淆的点总线电压只有 12 位有效数据但它是无符号的。如果你把bus_raw转成int16_t再右移在 bit15 为 1 的高电压场景下会得到负数解析错误。正确做法是先用无符号类型右移再转浮点乘比例系数。分流电压和电流则相反必须先转成int16_t再乘系数否则负方向的值会变成 4 万以上的大数。4.3 主循环读取示例与量级自检把上面的封装组合起来就是一个能直接跑的实际读取循环void System_ReadINA226(void) { uint16_t shunt_raw, bus_raw, power_raw, current_raw; float shunt_v, bus_v, power_w, current_a; if (INA226_ReadAll(shunt_raw, bus_raw, power_raw, current_raw) ! HAL_OK) { /* 错误计数连续 3 次失败再重新初始化 */ return; } shunt_v (int16_t)shunt_raw * 2.5e-6f; bus_v ((uint16_t)(bus_raw 3)) * 1.25e-3f; power_w power_raw * (25.0f * 0.0005f); current_a (int16_t)current_raw * 0.0005f; printf(Bus%.3f V, Shunt%.6f V, I%.4f A, P%.4f W\r\n, bus_v, shunt_v, current_a, power_w); }printf 在嵌入式平台上如果没有重定向串口输出会卡住这点调试时要注意。更好的做法是只保留一个浮点转字符串的轻量函数或者直接发十六进制原始值、在 PC 端解析。量级自检也是重要一环总线电压应该和万用表测的电源电压接近电流在空载时接近 0功率约等于电压乘电流。如果功率和电压乘电流对不上优先检查 25 倍系数有没有漏乘。5. INA226 高频问题排查五个典型踩坑记录与检查顺序5.1 现象电流和功率寄存器一直为 0电压却正常这是最经典的坑。表现为总线电压有读数但读 0x04 电流和 0x03 功率都是 0x0000I²C 通信波形正常。原因初始化时漏写或写错校准寄存器0x05。上电默认校准寄存器是 0芯片内部没有参考系数电流和功率计算不出来。解决先读 0x05 寄存器确认当前值和预期一致。如果是 0重新执行校准写入如果写 1024 之后回读仍然是 0检查 INA226 的 A0、A1 引脚是否虚焊。地址引脚悬空会造成实际地址不确定写操作可能落在总线上别的位置校验回读自然失败。还可以读 0x3E 制造 ID 寄存器验证 I²C 路径是否真的通到了 INA226 内部返回值和器件手册里的 ID 对上说明通信没问题问题在初始化逻辑。5.2 现象偶发读到 0x0000 或数据跳变表现为轮询读取时偶尔一次读回全 0下次又正常没有固定周期。原因在转换未完成时发起了读取。配置了 4 次平均和 2116us 转换时间后一次完整转换要十几毫秒如果主循环 5ms 读一次必然读到中间状态或者旧快照被覆盖后的空值。解决把两次读取之间最小间隔拉到 20ms 以上或者配置更短的转换时间。如果是负载快速变化的场景可以降平均次数到 1 次、转换时间改 588us把单次转换压到 1.2ms 以内牺牲一点精度换取实时性。实测最稳的方案是每次读之前检查 I²C 总线是否忙不要在中断回调里频繁调读函数。5.3 现象电流读数固定偏大约 1.5%电压和功率跟着偏表现为万用表测 1AINA226 读出来 1.015A偏差比例稳定不随电流变化。原因校准公式里的分流电阻用了标称值 10mΩ实际板子上电阻自身误差加上焊盘、铜皮走线电阻真实阻值可能是 10.15mΩ。校准值按 10mΩ 算出来是 1024分母变大后实际电流就偏小还是偏大这里要算清楚校准值 0.00512 /0.0005 × Rshunt如果 Rshunt 实际是 10.15mΩ而代码里用的是 0.01电流 LSB 对应的实际电流会偏小不对——校准值是 0.00512 / 0.0005 / 0.01 1024这个值偏大导致芯片算出来的电流读数偏小让我再想实际物理电流 1A 流过 10.15mΩ 电阻产生压降 10.15mV。芯片按照校准值 1024 反推的电流 LSB 0.00512 /校准值 × R标称 0.00512 / (1024 × 0.01) 0.5mA。分流电压寄存器实际读值是 10.15mV / 2.5µV 4060。电流寄存器 4060 × 0.00512 /1024 × 0.01 × 0.01这样算太绕了。换个简单的说法芯片把 10.15mV 压差按 0.01Ω 假设换算成 1.015A 的电流但真实电阻是 10.15mΩ实际电流是 1A所以读数偏大 1.5%。对是偏大。因为校准值按 0.01 算芯片内部认为电阻是 R0_IN/0.00512? 这样可以简化为芯片电流寄存器值的本质是 分流电压/分流电阻内部估计值。芯片不知道真实电阻它按你校准公式隐含的电阻值0.01Ω来换算。所以真实分流电阻大于你想当然的值时同一压差算出的电流偏大。结论读数偏大。解决用四线法测出分流电阻的实际值代入校准公式重新算 cal_value。焊接后 PCB 铜皮走线过长时走线电阻也会串入分流回路这时要把走线电阻也算进分流电阻里。改动后电流读数会回到 ±0.5% 以内。5.4 现象总线电压读数明显偏小但万用表测电源端正常表现为 INA226 报的总线电压比万用表低 30mV 以上并且随负载电流增大而偏差变大。原因测量点选择错误。INA226 的 VS 引脚如果接在负载端而不是电源输出端负载电流在长导线上产生的压降会被计进去。负载电流 5A、导线电阻 6mΩ 时压降就有 30mV。解决把 VS 引脚接在分流电阻靠近电源的那一端并在 PCB 布局上让 VS 走线单独从采样点引出不要共享功率回路的铜皮。电源监测芯片对采样点位置极其敏感这个靠改布局解决不是软件能修的。5.5 现象I²C 总线挂死或设备无应答表现为加电后 SCL/SDA 其中一个一直被拉低或者 read 一直返回 HAL_BUSY逻辑分析仪看波形只有地址帧没有 ACK。原因从设备地址配置错误、总线上有多个相同地址的设备或者 GPIO 引脚被复用成其他功能没切换干净。INA226 的 A0、A1 如果和别的 I²C 设备地址冲突两颗芯片会在同一地址上同时应答逻辑电平产生竞争总线就乱套了。解决先用逻辑分析仪看地址帧内容确认首字节是 0x810x40 左移 1 位再加读标志。如果地址帧正确但无应答逐个断开其他 I²C 设备排查地址冲突如果 SCL 被拉死重点查是否发生了总线死锁给 SCL 引脚连续发 9 个脉冲可以解锁从设备卡死状态。另外STM32 的 GPIO 如果配置成开漏输出必须在外部接上拉电阻内部上拉在 400kHz 下是不够的。6. 最后一道验证用精密电阻把读数偏差锁死在 0.5% 以内代码能跑通、数据能打印不代表测量系统就是准的。每一次把 INA226 驱动调完我都会做一轮闭环校准方法不复杂但对系统可信度提升很明显。准备一颗 1Ω / 0.1% 的精密功率电阻串接在 INA226 的采样回路里作为负载。用恒流源或电子负载从 0A 到 1A 步进每 0.1A 记录一次 INA226 读数和万用表读数画一条散点对比。理想情况下两条线重合误差带在 ±0.5% 以内。如果偏差固定比如万用表 1.000A、INA226 读 1.015A直接返回第 2 章的校准公式把实际分流电阻值代进去重算。具体操作是先读分流电压寄存器的原始值用万用表量出实际分流电压两者相除得到真实分流电阻再代入0.00512 /电流LSB × 真实电阻重写校准寄存器。测量时要用四线法把万用表表笔直接压在分流电阻两端不要压在焊盘边缘焊锡层和铜箔的接触电阻也会混进去。如果偏差随电流变大而变大比如 0.1A 时偏差 1%1A 时偏差 3%这已经不是校准能解决的了而是分流电阻在大电流下发热导致的阻值漂移需要换大封装电阻或改进散热铜皮。如果只是在空载时读数在 0 附近波动 ±10mA先用平均次数解决把 0x036F 里的 AVG 从 4 次提到 16 次即把 bit11-9 从 001 改成 010对应配置字 0x076F。平均次数提高后噪声会变小但响应变慢负载快速变化的场景要权衡。做完这轮校验整套测量链路才算真正有数。从那以后我每次调完 INA226 都会强制走一遍这个校准流程哪怕只花五分钟也比在产线上追数据漂移省事得多。希望这篇笔记能帮你少踩几个我踩过的坑。本文还有配套的精品资源点击获取