简介:基于STM32的INA226电流与功率传感器驱动工程,面向嵌入式电源监控、电池管理等应用场景,适合需要高精度电压、电流采集的开发者。压缩包内共151个文件,以36个h头文件、35个c源文件为主体,涵盖stm32f10x系列外设驱动与INA226底层逻辑,另含Keil MDK工程配置(uvprojx)、编译中间文件(o/d/crf)以及hex固件等,整包约2.97MB。该资源已吸引4709人下载学习。读者可直接在Keil MDK中打开工程,查看寄存器配置、I2C读取时序与数据处理流程,各模块按功能拆分,h/c文件分层清晰,系统初始化、外设驱动与业务逻辑分离,便于按需移植复用。对于需要将INA226接入自己项目的开发者,现有源码提供了完整的驱动参考,结合内容预览中的stm32f10x外设源文件,可快速定位各模块调用关系并理解测量数据的计算与上报过程。
1. 一份能直接搬进工程的INA226驱动:STM32平台上把电流电压功率一次读明白
做电池充放电监测时,最怕的不是电流大,而是测不准。INA226这颗I2C接口的电流电压功率传感器,16位分辨率、能同时读分流电压和总线电压、还带一路报警输出脚,一直是STM32项目里做电源监测的热门选择。这份INA226.rar解压后就是一套基于Keil MDK的STM32工程,用C语言把初始化、I2C读取、电压电流功率换算和报警中断回调都封装好了。对正在调电源板、做BMS或者想验证电流采样方案的开发者,核心驱动代码可以直接参考复用。先剧透一个反直觉的点:这颗芯片直接读“电流寄存器”经常是0,真正可靠的读法是读分流电压自己换算,原因后面细说。
2. 工程骨架与I2C通信层:先搞清文件怎么摆、数据怎么出来
2.1 I2C底层引脚与地址确认
INA226走的是I2C总线,芯片上有A0和A1两个地址引脚,两个引脚的电平组合决定了设备的7位地址,范围从0x40到0x4F。很多工程里看到0x80、0x81这样的地址,那是把7位地址左移了一位后的8位读写地址。我习惯把宏定义写成这样,方便在STM32的标准外设库和HAL里通用:
#define INA226_ADDR_A0_GND (0x40 << 1) /* A0接GND,A1接GND,8位写地址0x80 */ #define INA226_ADDR_A0_VS (0x41 << 1) /* A0接VS,A1接GND */ #define INA226_ADDR_A0_SDA (0x42 << 1) /* A0接SDA */ #define INA226_ADDR_A0_SCL (0x43 << 1)参数说明:A0和A1除了接GND和VS,还能接SDA或SCL,目的是在一条I2C总线上挂最多16颗INA226。大部分模块默认把A0、A1都拉低,也就是0x40这个地址,读写地址是0x80。地址搞错是新手第一个翻车点,后面避坑章节会专门说。
工程里的I2C底层我用的是模拟I2C,两个GPIO手动翻转时序。好处是引脚随便选,不占用硬件外设,换平台时只需要改引脚宏。坏处是速度不如硬件I2C,但INA226的转换时间本身在毫秒级,模拟I2C完全够用。引脚配置用开漏输出,外部必须接上拉电阻,一般用4.7kΩ,SCL和SDA各一颗。没有上拉电阻时总线拉不上去,读回来的数据全是0xFF,这是第二个高频问题。
#define INA226_SCL_PIN GPIO_PIN_6 #define INA226_SDA_PIN GPIO_PIN_7 #define INA226_SCL_LOW() HAL_GPIO_WritePin(GPIOB, INA226_SCL_PIN, GPIO_PIN_RESET) #define INA226_SCL_HIGH() HAL_GPIO_WritePin(GPIOB, INA226_SCL_PIN, GPIO_PIN_SET) #define INA226_SDA_LOW() HAL_GPIO_WritePin(GPIOB, INA226_SDA_PIN, GPIO_PIN_RESET) #define INA226_SDA_HIGH() HAL_GPIO_WritePin(GPIOB, INA226_SDA_PIN, GPIO_PIN_SET) #define INA226_SDA_READ() HAL_GPIO_ReadPin(GPIOB, INA226_SDA_PIN)逻辑说明:SCL和SDA都配置成开漏输出,读SDA时先把引脚模式切到输入。模拟I2C的核心是四个动作:起始信号、停止信号、发送字节、等待应答。起始信号是SCL高电平时SDA拉低,停止信号是SCL高电平时SDA拉高。发送字节时高位先出,SCL拉低期间改变SDA电平,SCL拉高期间数据被对方采样。
2.2 单字节读写函数与工程文件职责
读写寄存器是一切的基础。INA226的寄存器都是16位,读的时候要先写寄存器指针,再用重复起始信号切换为读方向,最后连续读两个字节,先高后低:
uint16_t INA226_ReadReg(uint8_t reg) { uint8_t hi, lo; I2C_Start(); I2C_SendByte(INA226_ADDR_A0_GND & 0xFE); /* 写方向 */ I2C_SendByte(reg); I2C_Start(); /* 重复起始 */ I2C_SendByte(INA226_ADDR_A0_GND | 0x01); /* 读方向 */ hi = I2C_RecvByte(1); /* 主机回ACK */ lo = I2C_RecvByte(0); /* 最后一字节回NACK */ I2C_Stop(); return (hi << 8) | lo; }参数说明:接收最后一字节时主机回NACK,表示“不要了”,这是I2C协议的标准收尾动作。如果最后一字节也回ACK,从机还会继续发下一字节,总线时序就乱了。发送地址时,& 0xFE把最低位置0表示写,| 0x01置1表示读,这就是8位地址的由来。
解压后的Keil MDK工程里,文件职责分得比较清楚:
| 文件 | 职责 |
|---|---|
| ina226.c / ina226.h | 芯片驱动:初始化、寄存器读写、数据换算、报警配置 |
| bsp_i2c.c / bsp_i2c.h | 模拟I2C底层时序,含延时、起始、停止、收发字节 |
| main.c | 演示流程:初始化后周期打印电压电流功率 |
| stm32f1xx_it.c | 报警引脚对应外部中断的回调入口,转发给驱动层处理 |
有一种常见误解是驱动文件越全越好,实际上对这种传感器,驱动层应该只有三层:底层I2C收发、寄存器映射、物理量换算。main.c里不要写寄存器操作,后续换芯片、换采集策略时,驱动层才能做到只改一处。
3. 核心驱动拆解:从配置寄存器到电压电流功率换算
3.1 初始化流程:配置寄存器和校准寄存器怎么填
INA226上电后默认配置是可用的,但默认的测量模式和平均次数不一定适合你的场景。我建议初始化时显式写一次Configuration寄存器,把平均次数、转换时间、工作模式一次定死。下面这段是工程里初始化的核心:
void INA226_Init(void) { /* 第一步:软件复位,让芯片回到已知状态 */ INA226_WriteReg(0x00, 0x8000); HAL_Delay(10); /* * 第二步:写Configuration寄存器,地址0x00 * 0x1378的位含义: * AVG=010 -> 16次平均滤波 * VBUSCT=011 -> 总线电压转换时间588us * VSHCT=011 -> 分流电压转换时间588us * MODE=110 -> 连续测量分流+总线电压 */ INA226_WriteReg(0x00, 0x1378); HAL_Delay(1); /* * 第三步:写Calibration寄存器,地址0x05 * 电流LSB = 最大期望电流 / 32768 * 示例:采样电阻0.01R,最大电流10A * LSB = 10/32768 = 0.000305A * Calibration = 0.00512 / (LSB * Rshunt) * = 0.00512 / (0.000305 * 0.01) = 1678 = 0x068E */ INA226_WriteReg(0x05, 0x068E); /* 读回验证 */ uint16_t cfg = INA226_ReadReg(0x00); if (cfg != 0x1378) { /* 配置没写进去,检查I2C地址和接线 */ } }逻辑说明:复位位写1会触发一次上电复位,复位后所有寄存器回到默认值,然后再写入新配置。Configuration寄存器里AVG字段控制内部平均次数,VBUSCT和VSHCT分别控制总线和分流通道的ADC转换时间。MODE字段选择连续模式还是触发模式,工程里用的连续模式,读一次寄存器就能拿到最新结果。
参数说明:0x1378这个值不是随意拼的,AVG=16次平均能有效压掉电源纹波带来的采样跳动,转换时间588µs是比较均衡的档位。如果你的系统对实时性要求高,可以把AVG降到4次,配置值改成0x0B78,响应快但读数会抖一些。Calibration寄存器的值要和你选用的采样电阻强相关,换电阻必须重算,否则读出来的功率值没有物理意义。
3.2 数据读取与换算:原始值到物理量的三段公式
INA226的核心是三个寄存器:0x01分流电压、0x02总线电压、0x03功率。分流电压是有符号数,LSB是2.5µV;总线电压是无符号数,LSB是1.25mV,而且寄存器bit0恒为0;功率寄存器的LSB由校准值决定。工程里的换算函数如下:
float INA226_GetBusVoltage_V(void) { uint16_t raw = INA226_ReadReg(0x02); return (float)(raw >> 1) * 0.00125f; /* LSB = 1.25mV */ } float INA226_GetShuntVoltage_mV(void) { int16_t raw = (int16_t)INA226_ReadReg(0x01); return (float)raw * 0.0025f; /* LSB = 2.5uV,单位mV */ } float INA226_GetCurrent_A(void) { /* 最稳的电流算法:分流电压 / 采样电阻 */ float vshunt = INA226_GetShuntVoltage_mV() * 0.001f; return vshunt / 0.01f; /* Rshunt = 0.01R */ } float INA226_GetPower_W(void) { return INA226_GetBusVoltage_V() * INA226_GetCurrent_A(); }逻辑说明:总线电压寄存器虽然16位,但最低位永远是0,所以要先右移一位再乘1.25mV,得到实际的母线电压。分流电压寄存器必须用有符号类型接收,否则电流反灌时读出来的会是一个很大的正数,而不是负值。电流这里用的是“分流电压除以采样电阻”的方式,不依赖Calibration寄存器配置正确与否,容错性更高。
参数说明:如果你的负载电流方向固定,可以把shunt电阻的取值和Calibration寄存器写死,然后直接读0x04电流寄存器,那个值乘上电流LSB就是安培数。但工程里没这么做,原因是0x01分流电压是最原始的数据,它不会因为校准寄存器写错而失真,排障时先看分流电压再看电流,能快速定位问题。
这里补充一个我在调试时的经验:总线电压、分流电压、电流、功率四个值,先确保前两个对,再谈后两个。很多时候电流不对,查到最后是分流电压符号处理错了,或者采样电阻阻值写错了。把换算函数集中放在一起,用打印函数逐个输出原始寄存器值,是排查换算问题最快的手段。
4. 报警中断与数据平均:让采样值真正用在控制逻辑里
4.1 ALERT引脚:过压报警的中断回调
INA226的ALERT引脚是开漏输出,平时靠上拉电阻维持高电平,有报警事件时拉低。MCU这边把它接到一个外部中断引脚,就能实现过压、过流的硬件级保护。工程里报警配置和中断处理是分开的,配置代码集中在驱动里:
void INA226_AlertConfig(void) { /* * Mask/Enable寄存器,地址0x06 * 使能Bus Over-Voltage报警,总线电压超过阈值触发 * Alert Limit寄存器,地址0x07 * 阈值:12V,对应寄存器值 = 12 / 0.00125 = 9600 = 0x2580 */ INA226_WriteReg(0x07, 0x2580); INA226_WriteReg(0x06, 0xB000); /* 使能BOL和LATCH模式 */ /* 配置外部中断引脚 */ HAL_GPIO_WritePin(ALERT_GPIO_Port, ALERT_Pin, GPIO_PIN_SET); HAL_GPIO_EXTI_IRQHandler_Config(); }逻辑说明:Mask/Enable寄存器0x06的bit13是总线过压报警使能,bit15是分流过流报警使能,bit14是分流欠压。使能之后,内部比较器会把对应测量值跟Alert Limit寄存器里的阈值做比较。ALERT引脚一旦拉低,就进MCU的外部中断回调。开漏输出的意思是芯片只能把引脚拉低,不能主动拉高,所以外部必须接上拉电阻,这是报警功能能不能用的前提。
中断回调里要做两件事:第一是读一次Mask/Enable寄存器,这个动作会清除芯片内部的报警锁存标志;第二是把标志位赋值给一个volatile全局变量,供主循环查询:
volatile uint8_t g_ina226_alert_flag = 0; void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == ALERT_Pin) { uint16_t status = INA226_ReadReg(0x06); /* 读状态并清标志 */ if (status & 0x2000) { /* BOL位置位 */ g_ina226_alert_flag |= 0x01; } if (status & 0x8000) { /* SOL位置位 */ g_ina226_alert_flag |= 0x02; } } }参数说明:如果不清除芯片侧标志,ALERT引脚会一直保持低电平,中断反复触发,主循环根本没法做别的事。清标志的标准动作就是读一遍0x06寄存器,旧值丢弃即可。中断回调里只做标志置位,不要在回调里做打印或者长时间处理,这是嵌入式中断服务程序的基本素养。
4.2 采样均值与转换完成标志:实时性和精度的取舍
INA226内置了硬件平均功能,在Configuration寄存器的AVG字段里配置,可选1次到1024次平均。平均次数越多,噪声越小,但每次读数背后的转换时间也越长。以工程里0x1378的配置为例,总线通道转换时间588µs,分流通道588µs,AVG=16次,一次完整数据更新周期大约是16乘以两者之和,接近19ms。这意味着用轮询方式读寄存器时,19ms以内读到的可能是旧数据。
要解决这个问题有两种思路。第一种是把AVG调小,比如AVG=4,更新周期接近5ms,精度损失在电源纹波比较大的场合比较明显。第二种是保留硬件平均,但读之前先查看转换完成标志。INA226的转换完成标志可以通过读取状态寄存器判断,工程里的做法是直接在读取函数里加一个轮询等待:
uint16_t INA226_ReadVoltageStable(uint8_t reg) { uint16_t status = 0; for (uint16_t i = 0; i < 100; i++) { status = INA226_ReadReg(0x06); if (status & 0x0008) { /* CNVR位置位,表示数据更新完成 */ break; } } return INA226_ReadReg(reg); }逻辑说明:每次读取数据寄存器前先轮询转换完成标志,等于告诉程序“这份数据是新的,可以用了”。100次循环的轮询上限是为了防止标志位因为配置错误一直不置位导致死等。参数上,如果AVG=16,轮询间隔建议大于1ms,否则还没转换完就去查标志,白白空转。
工程里滤波还有一个补充手段,在应用层加了一个简单的滑动平均:
float g_voltage_buf[5] = {0}; uint8_t g_voltage_index = 0; float Voltage_Filter(float new_value) { g_voltage_buf[g_voltage_index] = new_value; g_voltage_index = (g_voltage_index + 1) % 5; float sum = 0; for (uint8_t i = 0; i < 5; i++) { sum += g_voltage_buf[i]; } return sum / 5; }这段代码放在应用层而不是驱动层,原因是滤波策略属于业务逻辑,换一个项目可能就想换成中值滤波或者一阶低通。硬件平均滤的是ADC采样噪声,软件平均滤的是负载突变引起的抖动,两层叠加后数据曲线会非常平滑,但也别把窗口开得太大,否则负载真正变化时读数的响应速度会慢半拍。
5. 避坑手记:I2C无应答、电流为0与报警不触发
5.1 读寄存器全是0xFF,芯片像不存在
现象:上电后扫描地址读不到设备,读任何寄存器都返回0xFF,示波器看SDA线上完全没有应答信号。
原因:最常见的是地址搞错。INA226用7位地址0x40,但很多代码模板里直接写0x80,半路出家的程序用了8位地址却没有左移,结果发送的地址跟芯片不匹配。其次是SDA、SCL引脚接反,或者总线没有上拉电阻。
解决:先用逻辑分析仪抓I2C波形,确认起始信号、地址字节、ACK位都存在。如果SCL有波形但SDA没有应答,用枚举法把所有可能的地址都试一遍,A0和A1接GND、VS、SDA、SCL的16种组合逐一探测。定位到地址问题后,把地址宏改成实际值,这条总线就能通了。上拉电阻缺失导致的0xFF也很好判断,测量SCL和SDA电平,正常空闲应该是高电平,如果一直低电平,就是上拉没接好。
5.2 电流寄存器读出来永远是0
现象:总线电压和分流电压读出来都正常,但0x04电流寄存器一直是0,换了采样电阻也是0。
原因:INA226的电流寄存器是Calibration寄存器配合工作的。Calibration寄存器默认值是0x0000,此时电流寄存器的输出没有意义,很多程序手册里的示例代码压根没写Calibration,直接读电流寄存器,读出来就是0。这不是芯片坏了,是驱动没把校准步骤走完。
解决:先算Current_LSB,公式是最大期望电流除以32768,然后代入校准公式算出Calibration寄存器值,写入0x05后电流寄存器才有真实数据。更稳的办法是干脆不读电流寄存器,用分流电压寄存器除以采样电阻算电流,这样即使Calibration没配,电流数据也不会丢。两种方法我都验证过,读电流寄存器省一条除法,但可维护性差,因为你得在配置文件里维护一份LSB值的注释。
5.3 电压跳变、分流电压显示成负数
现象:总线电压在某个范围跳动,分流电压偶尔显示成几百毫伏的大正数,负载关断后读数不为0。
原因:分流电压寄存器是有符号的,如果程序里用uint16_t接收,0xFFFF这样的值被当成65535而不是-1,再乘上2.5µV就成了正的163mV,看起来就像跳变。另一个原因是采样电阻的焊盘虚接,或者使用了两个不同阻值的采样电阻,分流电压实际测的是两端的差,接触不良会把噪声引进来。
解决:代码里强制用int16_t接收分流电压原始值,这一点已经写进驱动代码。硬件上检查采样电阻两端焊点,尤其大电流场景要用开尔文接法,把采样线单独引到MCU模拟输入端。软件层面配置AVG=16能明显改善焊接不良带来的噪声,但如果焊盘虚接严重,软件滤波治标不治本,还是要动硬件。
5.4 报警中断一次都不进,进了又出不来
现象:过压时ALERT引脚没拉低,或者拉低了之后一直保持低电平,程序卡在中断里出不来。
原因:ALERT是开漏引脚,外部没接上拉电阻时,芯片拉低后没有任何机制能把引脚恢复成高电平,这就是“进了出不来”。另一个原因是中断回调里只清了MCU侧的中断挂起位,没有读INA226的Mask/Enable寄存器,芯片侧的报警标志还锁存着。
解决:硬件上ALERT引脚必须接上拉电阻,和I2C上拉同理。软件上中断回调里顺序执行两步:先读0x06寄存器清芯片侧标志,再清MCU的EXTI挂起位。顺序反了可能导致标志清掉之前又进一次中断,虽然功能没问题,但会增加调试时的困惑。排查时可以临时把报警阈值调到当前电压附近,人为触发一次报警,验证中断链路是否畅通。
5.5 Keil工程里中文注释乱码
现象:工程文件拷到另一台电脑,打开后中文注释变成乱码,编译也没报错,但看代码像看天书。
原因:Keil MDK老版本默认用GB2312编码保存源文件,新版本或Windows区域设置不同时会按UTF-8解析,编码不一致导致显示乱码。INA226驱动里的注释如果用了中文字符,在UTF-8和GB2312之间来回切,很容易出现这种问题。
解决:工程建立时统一用UTF-8编码,并在Keil的Edit菜单里把Encoding改成UTF-8。更稳妥的做法是驱动文件和注释全部用英文,或者只保留关键中文注释。我一般把换算公式、寄存器地址意义这类容易记混的注释写成中文,其余的宏定义注释用英文,既保证可读性,也避免编码问题反复出现。如果你拿到的工程已经乱码,用文本编辑器转一次编码格式即可恢复。
6. 二次开发思路:从单点读数到多通道电源管理
6.1 一条I2C总线上挂多颗INA226
A0和A1引脚提供了16种地址组合,实测可以用数组管理多颗芯片。我在工程基础上扩展时,第一步就是把单实例的读写函数改成带地址参数的版本,然后建一个设备表:
typedef struct { uint8_t addr_8bit; /* 8位读写地址 */ float shunt_res; /* 采样电阻 */ float bus_voltage; float current; } ina226_dev_t; ina226_dev_t dev[4] = { {0x80, 0.01f}, {0x82, 0.01f}, {0x84, 0.01f}, {0x86, 0.01f}, };逻辑说明:每颗芯片采样电阻可能不同,所以采样电阻放进设备结构体里。扫描函数写一个for循环,依次调用INA226_ReadReg(addr, reg),就能在一条总线上同时监测4路电源。注意总线上挂多颗芯片时,SCL和SDA的上拉电阻要适当减小到2.2kΩ,否则总线电容过大,通信速率上不去。
6.2 结合状态机做周期采集,兼容实时控制
把轮询改成状态机是嵌入式项目里更常见的做法。主循环里维护一个采集状态,每隔固定时间触发一次,读取完成后把结果写入结构体,控制逻辑只消费结构体里的数据。这样INA226驱动和应用逻辑解耦,以后要换采集芯片,替换驱动层即可,应用层不用动。
我最常用的一套改造是按功能拆成三个文件:采集任务、数据处理、报警联动。INA226的驱动只负责“把寄存器值变成物理量”,数据处理负责滤波和SoC估算,报警联动负责在过压时切断负载。这个分层方式帮我解决了很多“改一处崩一片”的问题。从那以后,我每次拿到新的INA226模块都会先跑一遍地址扫描,再回读Configuration寄存器确认配置生效,最后才写业务逻辑。这套流程救过我好几次。希望帮到你。
本文还有配套的精品资源,点击获取