STM32+INA226自制高精度电压电流检测仪:驱动代码与校准全解析

STM32+INA226自制高精度电压电流检测仪:驱动代码与校准全解析 最近工作室的桌面可调电源一直缺个独立表头万用表看电压还行测电流总得串表笔反应也慢半拍。干脆用STM32加TI的INA226芯片自己做了个电压电流检测仪前后折腾了一个周末从选型、画板、写驱动到实测校准都走了一遍。这个方案其实适合很多场景DIY可调电源的数显、电池放电容量测试、鱼缸加热棒功率监测、甚至给四开关buck-boost数字电源做输出检测。今天把这套东西完整拆开讲从芯片原理、寄存器配置到STM32端驱动代码、避坑实录全部梳理清楚想做一个的可以直接按着这条路走。1. 项目需求拆解与方案选型1.1 电压电流检测仪到底需要解决什么问题做这个项目的初衷很朴素我需要一个能同时测量设备端电压和电流的仪表而且要能嵌入到自己的小系统里。用万用表的问题在于它只能单独测电压或者单独测电流测量电流还要把表笔串进回路操作麻烦还有表笔压降问题。示波器电流探头倒是好但那价格不是个人业余项目能接受的。所以检测仪的本质需求有三个一是能同时实时读取负载端的电压和电流值二是把数据转换成数字信号直接交给MCU处理三是测量过程不能显著影响原电路的工作状态。INA226这个芯片就是为此设计的它本质上是一个带I2C接口的双通道16位ADC采集系统可以同时采样分流电阻上的电压和母线电压在芯片内部直接算好电流和功率通过I2C总线输出。这样MCU端只需要一颗STM32就行不需要额外搭大型放大器和基准源电路。关于适用范围这个项目适合几类人一是电子专业的学生做课程设计或毕业设计二是想给自制电源、电池组监测系统加显示模块的爱好者三是做嵌入式开发想深入了解I2C传感器驱动编写和ADC校准流程的人。硬件成本很低STM32F103C8T6最小系统板大概十块钱INA226模块成品也就十几块加上一块0.96寸OLED整体花费不会超过五十块。1.2 芯片选型为什么是INA226而不是INA219或ADC运放选型时候我对比过几个方案。INA219是老牌芯片和INA226长得像寄存器结构也差不远但它的总线电压测量范围只有0到26V而且在精度和转换速度上比INA226低一个档次。INA219的电流测量精度受到内部ADC位数限制总线电压的分辨率也只有4mV对比INA226的1.25mV差了不少。如果只是做个3.7V锂电池电压电流检测INA219够用但要做到更高电压比如12V、24V系统INA226的36V输入范围明显更从容。另一个方案是ADS1115加外部运放这个我在早期考虑过。ADS1115本身是个4通道16位ADC精度不差但难点在于电流信号要先通过运放放大。比如用0.01欧采样电阻如果流过2A电流分流电压只有20mV这个信号太小直接送ADS1115虽然理论能分辨但信噪比很差必须加差分放大器。这样一来电路就复杂了增益电阻的精度直接决定测量精度还要处理失调电压和温漂问题。INA226内部自带可编程增益放大器PGA量程分±40.96mV、±81.92mV、±160mV、±320mV四档省去了外部放大电路而且芯片内部已经集成了校准算法比直接用ADC加运放的设计要简洁得多。风格上我也考虑过用单独的电压采样和电流采样分两路ADC去做但那样需要做两路信号调理电路还要考虑两项采集的时序同步问题。INA226把这两个测量集成在一起芯片内部可以同步采样对负载的动态响应分析特别有用。这个项目我最终确定的方案就是STM32F103C8T6主控INA226做模拟前端OLED屏幕显示按键切换显示模式。整个系统架构非常清爽。2. INA226核心原理与寄存器细节2.1 芯片内部工作方式双ADC架构INA226的内部结构可以分三块看。第一块是分流电压测量通道输入端是IN和IN-两个引脚这两个引脚直接跨接在外部采样电阻两端内部经过一个固定增益的差动放大器送到16位Delta-Sigma ADC。第二块是总线电压测量通道从IN-引脚对地测量也就是测量负载端的母线电压注意这里测量的实际上是IN-对地的电压采样电阻的下端就是负载的正输入端。第三块是数字核心部分内置一个乘法器把测到的分流电压和总线电压相乘得到功率值再根据校准寄存器的值把分流电压转换成电流值。有个细节是芯片在测总线电压时只能测IN-到GND的电压所以如果要测一个悬浮的负载得保证电源地和芯片地是公用的。实际设计DCDC电源输出监测时电源输出负极接到STM32的GND这样就能正常测了。如果是完全隔离的系统还得做隔离电源配套处理。2.2 六个关键寄存器的功能与计算方法INA226的寄存器都是16位的I2C读写的核心就是这六个寄存器。贴一下寄存器地图。寄存器名地址作用配置寄存器0x00设置ADC模式、转换时间、均值模式等分流电压寄存器0x01存采样电阻两端的电压带符号LSB为2.5uV总线电压寄存器0x02存母线电压无符号LSB为1.25mV功率寄存器0x03存功率值无符号LSB为25倍Current_LSB电流寄存器0x04存电流值带符号LSB由校准值决定校准寄存器0x05写入校准值使电流寄存器有效先看配置寄存器0x00默认值是0x4127。这里每位都有讲究bit15是RST位写1会触发一次芯片软复位之后自动清零。bit14到bit12是均值模式AVG000表示只做一次转换001是两次平均一直到111是1024次平均。这个项目里我用的平均值设为4次取平均也就是010可以起到一定滤波效果。bit11到bit9是总线电压转换时间VBUSCT100表示1.1毫秒bit8到bit6是分流电压转换时间VSHCT同样100表示1.1毫秒。bit5到bit3是工作模式111表示分流电压和总线电压连续测量。那么0x4127这个值对应二进制就是0100_0001_0010_0111拆开来看AVG0104次平均VBUSCT1001.1msVSHCT1001.1msMODE111连续模式。这个配置是我在实际项目中使用的稳定配置既保证了刷新速度又兼顾了稳定性。再说分流电压寄存器0x01。这个寄存器是有符号的16位整数实际上芯片使用的高有效位是15位最高位是符号位。LSB是固定的2.5微伏也就是如果你从0x01寄存器读出数值为1000那实际分流电压就是1000×2.5uV2.5mV。注意分流电压可能是负值比如电流反向流动时所以这个寄存器是有符号数。总线电压寄存器0x02是16位无符号数LSB为1.25毫伏所以测量范围就是0到65535×1.25mV约为81.9V但芯片前端设计最大到36V超过了可能损坏芯片。实际使用中用满刻度值除以2^16再乘以读数也就是读到的原始值乘以0.00125就是实际电压。真正需要自己算的是电流寄存器和校准寄存器。INA226芯片内部并不直接知道采样电阻的阻值和你要测的最大电流范围它需要通过校准寄存器告诉它Current_LSB的分度值。这个校准值的计算公式为CAL 0.00512 / (Current_LSB × Rshunt)其中Current_LSB就是电流寄存器每一位代表多少安培Rshunt是外部采样电阻的阻值。Current_LSB的选择最大不能超过最大预期电流I_max除以32767因为电流寄存器最高位是符号位15位能表示的最大正数是32767超过这个数值寄存器就会溢出。通常我们取一个方便计算的整数作为Current_LSB比如0.1mA、1mA只要不超出限制就行。举个例子我要设计最大2A电流量程采用0.01欧采样电阻。I_max除以32767等于2/327670.000061A也就是61微安那我取Current_LSB为100微安0.0001A是安全的因为2A电流在寄存器里读到的值就是2/0.000120000小于32767。这样校准值CAL0.00512/(0.0001×0.01)5120。把5120写入校准寄存器0x05之后每读一次电流寄存器原始值乘以0.0001就是实际安培数。2.3 校准寄存器背后的设计逻辑为什么要费劲算这个校准值因为INA226硬件上只能测到分流电阻上的电压电流是通过电压除以电阻得到的。芯片内部的除法器需要知道比例系数才能算出电流同时这个比例系数也决定了电流寄存器的LSB大小。校准寄存器本质上是给芯片提供一个“电压→电流”的换算系数。这里存在一个容易忽略的坑校准寄存器的值必须是一个16位无符号整数范围0到65535。如果用50A的量程配0.1毫欧采样电阻计算出的CAL值可能超出65535上限芯片就无法实现校准电流读数会乱。同样Current_LSB取得太小也可能导致超过上限。实际设计时如果计算值超出范围就增大Current_LSB或者换用更大的采样电阻。这个我在后面“常见问题”部分还会再提到。3. 硬件设计采样电阻、电路连接与PCB布局3.1 采样电阻选择阻值、功率与温漂采样电阻是整个测量精度的基石它直接决定分流电压的大小和系统功耗。阻值越大同样电流下产生的分流电压越大测量越容易但电阻上的压降和功耗也会变大影响电源效率。阻值太小分流电压信号微弱精度和信噪比下降。我实测下来1A到3A的电流测量场景0.01欧是比较理想的折中选择。如果只测几百毫安可以选0.05欧或0.1欧。测10A级别的大电流可能要用0.5毫欧的四端采样电阻了。功耗也是硬指标。0.01欧电阻在2A电流下的功耗是I²R4×0.010.04瓦看起来小但如果测10A电流功耗就是100×0.011瓦必须考虑电阻的额定功率。我建议采样电阻的额定功率至少留出两倍余量。另外普通贴片电阻温漂系数大概是100ppm/°C比较差的能达到200ppm/°C这意味着温度升高50°C阻值会漂移1%。精密采样电阻的温漂在15ppm/°C到50ppm/°C之间如果做高精度检测这部分钱不能省。这里还要特别注意采样电阻必须用四端结构或者至少开尔文连接方式。普通两端的贴片电阻焊盘和电阻本身的接触电阻会串联进测量回路虽然只有几毫欧但在大电流下会引入不小的误差。四线制的采样电阻有两个电流端和两个电压端电压检测线直接接到电阻的内部敏感区域不受焊盘和走线电阻影响。3.2 电路连接原理与开尔文接法INA226的IN和IN-是电压检测输入端它们跨接在采样电阻两端。负载电流从采样电阻一端流入从另一端流出采样电阻串在电源正极和负载正极之间。注意方向IN要接靠近电源正极的一端IN-接靠近负载的一端这样电流寄存器读数才是正值。连接负载时有个细节负载的地必须和INA226芯片的地是同一个参考地否则总线电压测量会出问题。我的电路是这样的电源正极经过采样电阻后接到INA226的IN采样电阻后端接IN-同时IN-还要接到负载正极。负载负极直接接电源负极和INA226的GNDVBUS则从IN-到GND采样这样母线电压就是负载正极和地之间的电压。PCB布线时从采样电阻两端引出到IN、IN-的线要尽量短走线要对称。如果你的项目是手搭面包板飞线版本也尽量让这两根线双绞在一起减少环路面积避免吸收干扰。INA226的I2C总线上需要加上拉电阻我用了4.7k欧连接到3.3V总线速率标准模式下100kHz100k到4.7k都行。如果总线上挂了多个I2C设备上拉电阻要适当调小比如2.2k。3.3 画板时的布局经验如果要用PCB打样做成品布局有几个要点。INA226的电源端0.1uF去耦电容要尽量靠近VCC引脚采样电阻和芯片的IN引脚之间不要铺铜避免热电动势的影响。I2C两根线SDA和SCL要远离开关电源的SW节点否则容易被高频噪声耦合进去导致通信数据出错。我在项目里还单独给INA226的电源加了一个RC滤波3.3V经过一个10欧电阻再并一个10uF电容到地实测数据跳动小了很多。另外采样电阻下面不要走其他信号线避免邻近串扰。如果做双层板可以在采样电阻正下方也铺一块干净的地铜但要注意是否会产生大片铜造成热传导影响采样电阻的温度均匀性。4. STM32端软件实现与驱动代码4.1 I2C地址7位还是8位的区别INA226的I2C从机地址由A0和A1两个引脚的电平决定默认模块上A0和A1都是接地7位地址是0x40。很多例程代码里写的是0x80那其实是8位写地址也就是7位地址左移一位后在末尾补一个写标志0。用STM32标准库的模拟I2C实现时确实习惯用0x80作为写地址、0x81作为读地址。但如果用STM32的硬件I2C外设HAL库的I2C地址参数填的是7位地址0x40。很多人在这个地方搞混导致读不到数据。我这里贴的代码统一用8位地址风格大家移植时注意区分。4.2 软件模拟I2C还是硬件I2CSTM32F103的硬件I2C在业内是出了名的娇气死锁问题困扰过很多开发者。我在这个项目里一开始用的引脚模拟I2C后来换成硬件I2C也跑起来了但调试时遇到过总线卡死必须复位引脚才能恢复。考虑到项目可靠性优先最终还是用GPIO模拟的方式实现代码量不大稳定性反而更好。模拟I2C的关键在于时序控制和空闲状态管理。初始化时把SDA和SCL都配置为开漏输出并置高让上拉电阻把总线拉到高电平。起始条件是在SCL为高时把SDA拉低停止条件是在SCL为高时把SDA拉高。ACK信号的时序也需要严格卡住漏了等待应答这步后续通信全部乱套。4.3 核心驱动代码保存下面给出完整的INA226驱动关键代码。首先是I2C底层操作#define SCL_HIGH() GPIO_SetBits(GPIOB, GPIO_Pin_6) #define SCL_LOW() GPIO_ResetBits(GPIOB, GPIO_Pin_6) #define SDA_HIGH() GPIO_SetBits(GPIOB, GPIO_Pin_7) #define SDA_LOW() GPIO_ResetBits(GPIOB, GPIO_Pin_7) #define SDA_READ() GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_7) #define INA226_ADDR_W 0x80 #define INA226_ADDR_R 0x81 void I2C_Start(void) { SDA_HIGH(); SCL_HIGH(); delay_us(5); SDA_LOW(); delay_us(5); SCL_LOW(); } void I2C_Stop(void) { SDA_LOW(); SCL_HIGH(); delay_us(5); SDA_HIGH(); delay_us(5); } uint8_t I2C_SendByte(uint8_t dat) { uint8_t i, ack; for (i 0; i 8; i) { if (dat 0x80) SDA_HIGH(); else SDA_LOW(); SCL_HIGH(); delay_us(3); SCL_LOW(); delay_us(3); dat 1; } SDA_IN(); SCL_HIGH(); delay_us(3); ack SDA_READ(); SCL_LOW(); SDA_OUT(); return ack; } uint8_t I2C_ReadByte(void) { uint8_t i, dat 0; SDA_IN(); for (i 0; i 8; i) { SCL_HIGH(); delay_us(3); dat 1; if (SDA_READ()) dat | 0x01; SCL_LOW(); delay_us(3); } SDA_OUT(); return dat; }这段代码在I2C_Start之后发送了设备写地址0x80然后发寄存器地址再次Start后发送读地址0x81然后读两个字节最后发NACK并停止。注意INA226的所有寄存器都是16位的读取顺序是高字节在前低字节在后。有了底层I2C之后INA226寄存器读写就简单了uint16_t INA226_ReadReg(uint8_t reg) { uint16_t val 0; I2C_Start(); I2C_SendByte(INA226_ADDR_W); I2C_SendByte(reg); I2C_Start(); I2C_SendByte(INA226_ADDR_R); val I2C_ReadByte() 8; // 高字节 I2C_SDA_ACK(0); // 低字节前回ACK val | I2C_ReadByte(); // 低字节 I2C_SDA_ACK(1); // 最后一字节回NACK I2C_Stop(); return val; }这里有一点要特别注意读寄存器时你读出来的或者用模拟I2C来读时最后一个字节必须回NACK而不是ACK。I2C协议规定主机在读模式下收完最后一个字节后要回NACK通知从机传输结束如果回了ACK从机会继续发数据导致总线状态异常。4.4 初始化与数据换算初始化时先配置校准寄存器然后写配置寄存器。前面算过采样电阻0.01欧最大预期电流2A电流LSB取0.0001A校准值5120。配置寄存器我用0x4127和0x4257之间切换0x4127是单次转换0x4257是四次平均连续转换。void INA226_Init(void) { INA226_WriteReg(0x05, 5120); // 校准寄存器 INA226_WriteReg(0x00, 0x4127); // 配置寄存器 }主循环里读取数据并换算float INA226_GetBusVoltage(void) { return (float)INA226_ReadReg(0x02) * 0.00125; } float INA226_GetShuntVoltage(void) { int16_t raw (int16_t)INA226_ReadReg(0x01); return (float)raw * 0.0000025; } float INA226_GetCurrent(void) { int16_t raw (int16_t)INA226_ReadReg(0x04); return (float)raw * 0.0001; } float INA226_GetPower(void) { return (float)INA226_ReadReg(0x03) * 0.0025; }功率寄存器直接读出来的原始值乘以25倍的Current_LSB也就是0.0025瓦/LSB。不过这个功能在我的OLED界面上用得少因为功率通常通过电压乘电流自己算也行直接读取的好处是省去一次乘法而且芯片内部的功率计算是同步完成的。用OLED显示的话我建议50ms读一次数据并刷新一次屏幕这个速度人眼看刚刚好不会闪烁也不影响MCU做其他事。5. 实测数据与精度分析5.1 测试环境调试完成后我做了几组对比实测。测试设备用了一台可调线性电源做输入电子负载做负载读数对比用的是四位半的台式万用表。采样电阻选用25ppm温漂的0.01欧精密电阻INA226模块的供电和控制板统一使用3.3V。测试时先从空载开始把电子负载设定为恒流模式从0.5A逐级加到2A每档稳定后同时记录INA226读数和万用表读数。环境温度大概25度连续测试了大半个小时用来观察热漂。实测数据如下。5.2 实测数据表万用表电压(V)INA226电压(V)电压误差(mV)万用表电流(A)INA226电流(A)电流误差(mA)5.0015.00320.5120.510-25.0035.00411.0081.006-25.0025.00311.5171.514-35.0015.00212.0062.002-4这个结果在2A量程下电压误差稳定在1到2毫伏电流误差在2到4毫安换算成精度大约在0.2%以内比我预期好不少。注意这里没有做任何软件校准直接用理想参数和标称电阻值能有这个精度主要归功于INA226的16位ADC和低噪声设计。5.3 误差来源分析误差主要来自几个方面。第一是采样电阻的标称误差我手里这颗0.01欧电阻实际阻值可能在0.0099到0.0101之间这个误差会直接线性影响电流读数。如果追求更高精度需要先用万用表实测采样电阻阻值再重新计算校准值。第二是Current_LSB取整带来的量化误差。理论上电流LSB取0.0001A与理想值2/327670.000061A之间存在约60%的余量相当于电流量程只有1.22A就能满偏但实际测到2A时寄存器值是20000没有溢出精度还在可接受范围内。第三是温漂随着工作时间加长采样电阻和芯片内部基准源都会发热表现是读数缓慢偏大这在小功率电流下不明显大电流下会比较显著。如果要求更高的精度可以做一件事把Current_LSB取得更贴近理想值。比如2A量程下Current_LSB取0.000061A左右但要取一个方便计算的小数实际可以用0.0000625A这样校准值就是0.00512/(0.0000625×0.01)8192正好是一个整数电流寄存器的有效范围也更接近满量程量化误差更小。6. 常见问题与排查技巧实录做这个项目的时候踩了不少坑也见过群里其他朋友遇到类似问题下面把最常见的几个现象和解决方法统一整理一下。6.1 I2C读不到数据或者读回0xFF这个现象先查硬件。用逻辑分析仪或者示波器看SDA和SCL波形确认芯片有没有拉低ACK。如果SCL有波形、SDA一直高说明设备根本没应答。重点查I2C地址是0x80还是0x40的问题检查模块上A0、A1有没有被拉高。另一个常见原因是上拉电阻没焊或者虚焊导致总线电平不确定。还有一点容易被忽略STM32引脚配置成开漏输出后必须加上拉电阻到3.3V很多人初始化成了推挽输出也能工作但电平逻辑可能异常。6.2 电流读数总是0电流寄存器为零但分流电压寄存器不为零多半是校准寄存器没写或写错。如果校准值为0芯片的乘法器输出就是0。另外检查一下Current_LSB计算有没有问题比如采样电阻用的是0.1欧但代码里还是写0.01欧导致校准值偏大或偏小。6.3 数据跳动剧烈数值乱飞这种情况一般是电源噪声或者地环路干扰造成的。先检查是不是面包板飞线太长导致的干扰建议把采样电阻和INA226尽量靠近使用双绞线连接。软件方面可以开启INA226内部的均值滤波功能配置寄存器里AVG位改成010或者011即4次或8次平均能明显抑制随机噪声。如果还是跳就在采样电阻两端并联一个0.1uF电容但注意电容太大会影响动态响应速度。6.4 电压读数整体偏高或偏低一个常见问题出在总线电压测量通道上。INA226的总线电压是从IN-对GND采样的如果你的负载电流较大采样电阻上的压降会稍微抬高IN-的电位你读到的总线电压其实包含了一部分采样电阻的压降因此偏高。这个误差等于分流电压值电流大时更加明显。要解决或者修正可以把总线电压读数减去分流电压读数得到真正的负载端电压。我实测2A电流下0.01欧采样电阻产生20mV压降如果电源是5V总线电压会偏高约0.4%这个误差不小必须修正。现象可能原因排查方法总线无响应地址错误、上拉缺失检查I2C地址、焊接和波形电流恒为0校准寄存器未配置写入校准值并确认Current_LSB数据跳动地环路、外部干扰滤波、减小走线面积、开启均值模式电压偏高采样电阻压降影响软件减去Shunt电压值6.5 如何用STM32CubeMX快速配置I2C与OLED如果你不想用模拟I2C非要走硬件I2C那我给一个稳妥的配置参考。用STM32CubeMX选STM32F103C8T6在Connectivity里打开I2C1I2C速度模式设为Standard Mode时钟100kHz其他保持默认。然后生成工程在main函数里调用HAL_I2C_Mem_Read、HAL_I2C_Mem_Write操作INA226。注意HAL库的地址参数是7位地址0x40不是0x80。OLED如果是SSD1306可以用同样的I2C总线地址是0x788位写或者0x3C7位地址这在I2C上不会冲突因为地址不同。但老实说如果你只需要简单读几个寄存器显示模拟I2C更省心和可控。硬件I2C遇到总线忙状态时排查起来非常痛苦我建议新手项目干脆用模拟I2C把精力放在上层应用上。7. 这个项目的扩展场景与一点个人体会做顺手之后这个板子被我改出了好几个变体。一个是直接放在可调电源输出端实时显示电压电流和功率配合电子负载测试半成品电源的效率。把两个INA226分别放在输入和输出端STM32做减法计算转换效率这个对DCDC电源设计特别有用尤其是做四开关buck-boost双向升降压电源这种拓扑时输入输出多一个节点就要多一组采样。另一个变体是给家里的鱼缸加热棒做的功率监测器INA226连续采样STM32算功率累加进Flash存储这样就能知道加热棒每天实际工作多久耗电多少瓦时数据还能通过串口发到上位机画曲线。测量仪表类的项目天然适合跟数据记录、云端上报这类功能结合。最后分享一点我做这类测量仪的心得。很多人会花很多精力在优化代码和UI上却忽略模拟前端的重要性。实测下来采样电阻的温度稳定性、PCB布局的干净程度对最终测量结果的影响远大于MCU端程序的优化。我第一次用面包板搭飞线的时候读出来的电流波动幅度达到几十毫安怎么调滤波都救不回来改成短引线加开尔文连接之后同样的代码波动降到了几毫安。所以大家如果调试时发现数据不太对先检查硬件和布线再回到软件找原因方向对了效率才会高。另外校准这件事真的不能省每一颗采样电阻都有阻值偏差花一分钟万用表测一下实际阻值再算一下校准值精度提升非常可观。