零知派——STM32驱动INA238功率监测芯片的18650锂电池充放电状态可视化
目录
项目概述
本项目基于零知派标准板(主控STM32F103RBT6)和TI INA238高精度数字功率监测芯片,实现了一套完整的18650锂电池充放电实时监测系统;高精度实时采集锂电池总线电压、充放电电流和功率数据,引入OCV内阻动态补偿技术解决充电状态下电压虚高问题;数据通过ST7789彩色240x240显示屏以波形+数值面板实时展示;同时支持硬件按键控制L9110H风扇模块负载
项目难点及解决方案
问题描述:短路时电池端电压虚高/虚低导致SOC误判
解决方案:通过三态分离的OCV补偿模型,充电态消除压升虚高,放电态补回压降损失
一、系统接线部分
1.1 硬件清单
| 组件名称 | 型号/规格 | 数量 | 备注 |
|---|---|---|---|
| 主控板 | 零知派标准板 | 1 | STM32F103RBT6 |
| 电流/功率传感器 | INA238模块 | 1 | R015分流电阻(0.015Ω),内置 |
| 显示屏 | ST7789 | 1 | 240×240 TFT彩屏,SPI接口 |
| 锂电池 | 18650型 | 1 | NMC三元锂电池,满充4.20V,安全截止2.80V |
| 充电模块 | CSM4056/充电板 | 1 | 用于演示充电状态 |
| 散热风扇 | L9110H驱动模块 + 风扇 | 1 | 用于模拟负载 |
| 按键 | 轻触按键 | 1 | 风扇物理开关 |
| 连接线 | 杜邦线 | 若干 | 公对公、母对母 |
1.2 接线方案表
以下引脚定义均取自
config.h中的宏定义,请按此表格进行硬件连接。
| 零知派标准板引脚 | 连接目标 | 说明 | 代码宏定义 |
|---|---|---|---|
| A5 (SCL) | INA238 —— SCL | 软件I2C时钟线 | SW_SCL_PIN |
| A4 (SDA) | INA238 —— SDA | 软件I2C数据线 | SW_SDA_PIN |
| 5V | INA238 —— VBUS | 传感器电源 | — |
| GND | INA238 —— GND | 公共地 | — |
| 10 (CS) | ST7789 —— CS | 屏幕片选 |
直插零知派标准板 |
| 2 (DC) | ST7789 —— DC | 数据/命令选择 | |
| 4 (RES) | ST7789 —— RST | 屏幕复位 | |
| 5 (PWM) | L9110H风扇模块——INB | 风扇PWM控制 | FAN_CTRL_PIN |
| 3 (KEY) | 轻触按键——一端接引脚,另一端接GND | 风扇开关(内部上拉) | KEY_PIN |
ST7789接线提示:ST7789直插零知派标准板的TFT扩展引脚,无需单独接线。此外,TFT_RST(引脚4)需接VCC(3.3V)微上拉稳定复位,同时在
DisplayHandler.begin()中进行了软件复位初始化
1.3 连接示意图
避免差分感测路径断路:IN+与IN-的接线逻辑按照低侧检测要求,使分流电阻两端形成有效的差分感测路径,芯片采集电流信号

充电时电流从充电板
OUT+流出 -> 锂电池负载 ->VIN+采样电阻 ->VIN-,最后返回充电板OUT-形成回路
IN+/IN-差分感测路径 —— 在低侧检测方案中,分流电阻位于负载与电源负极之间:
- VIN+ 接 分流电阻的负载侧(即靠近负载的那一端)
- VIN- 接 分流电阻接地侧(即靠近GND的那一端)
充电回路
| INA238引脚 | 连接目标 |
|---|---|
| VIN+ | 电池负极 |
| VIN- | 充电板OUT- |
充电时:电流从充电器OUT+流出 -> 电池正极/负极 -> 进入 VIN+ -> 采样电阻 -> 流向 VIN- 。此时电流方向为正 (+),系统识别为充电
放电回路
| INA238引脚 | 连接目标 |
|---|---|
| VIN+ | 负载/直流电机 VCC正极 |
| VIN- | 负载 GND负极 |
放电时:电流从充电器OUT-流出 -> 进入 VIN- -> 流向 VIN+ -> 进入负载。此时电流方向为负 (-),系统识别为放电
1.4 具体接线图

VBUS引脚不可悬空,必须正确连接到电源总线正极。INA238通过VBUS引脚直接采样总线电压以完成电压和功率测量,若悬空或接错位置,功率寄存器将无法参与计算,输出持续为零
二、安装与使用教程
2.1 开源平台-输入"INA238" 并搜索-下载代码自动打开

2.2 连接-验证-上传

2.3 调试-串口监视器

三、代码讲解部分
本项目代码采用模块化拆分:BatteryMonitor负责核心算法、DisplayHandler负责波形显示、通过INA238_STM32_Monitor文件统一调度
3.1 滑动平均+EMA双层滤波
滑动平均消除高频噪声、EMA 提供更快的跟随性
float BatteryMonitor::_slidingAvg(float newI) {
_iBuf[_iBufIdx] = newI;
_iBufIdx = (_iBufIdx + 1) % FILTER_SIZE;
float s = 0;
for (int i = 0; i < FILTER_SIZE; i++) s += _iBuf[i];
return s / FILTER_SIZE; // 窗口大小为 8 的 FIR 低通
}
void BatteryMonitor::_updateEMA(float avgI, float rawV, BattState st) {
// 状态切换时电流 EMA 立即重置,防止旧值污染新态
if (st != _lastState || !_emaIInit) {
_emaI = avgI;
_emaIInit = true;
} else {
_emaI = EMA_I_ALPHA * avgI + (1.0f - EMA_I_ALPHA) * _emaI; // α=0.08
}
// 电压 EMA 始终平滑,不随状态重置
if (!_emaVInit) { _emaV = rawV; _emaVInit = true; }
else _emaV = EMA_V_ALPHA * rawV + (1.0f - EMA_V_ALPHA) * _emaV; // α=0.05
}
充→放切换瞬间,旧的正电流平均值会使新放电电流被低估,重置后立即正确
3.2 库仑计融合与限速滤波
float BatteryMonitor::_fusedSOC(float busV, BattState st) {
float ocv = _calcOCV(busV, st);
float socV = _ocvToSOC(ocv); // 电压法 SOC
if (!_socAnchored) { // 首次锚定
_socCoulomb = socV;
_socAnchorAh = _d.chargeAh - _d.dischargeAh;
_socAnchored = true;
return socV;
}
float netAh = (_d.chargeAh - _d.dischargeAh) - _socAnchorAh;
_socCoulomb = constrain(socV + (netAh / BATT_CAPACITY_AH) * 100.0f, 0, 100);
float wV = (st == STATE_CHARGING) ? W_VOLTAGE_CHG :
(st == STATE_DISCHARGING) ? W_VOLTAGE_DSG : 1.0f;
return constrain(wV * socV + (1.0f - wV) * _socCoulomb, 0.0f, 100.0f);
}
void BatteryMonitor::_updateDisplaySOC(float targetSOC, BattState st) {
float diff = targetSOC - _socDisplay;
switch (st) {
case STATE_CHARGING:
if (diff > 0) _socDisplay += min(diff, SOC_RISE_RATE); // 只升不降
break;
case STATE_DISCHARGING:
if (diff < 0) _socDisplay += max(diff, -SOC_FALL_RATE); // 只降不升
break;
default:
float rate = min(SOC_RISE_RATE, SOC_FALL_RATE) * 0.5f;
if (diff > 0) _socDisplay += min(diff, rate);
else _socDisplay += max(diff, -rate);
break;
}
_socDisplay = constrain(_socDisplay, 0, 100);
}
净容量变化 = 当前净容量 – 锚点净容量,再除以总容量得到 SOC 变化量,加到锚点 SOC 上;待机持续超过 COULOMB_ANCHOR_MS(5 秒)时,在 update() 中重新执行锚定
3.3 波形区循环刷新
独立计算正半轴和负半轴量程,根据历史缓存 _vHist、_iHist、_pHist绘制折线
void DisplayHandler::_updateWave(const BatteryData& d) {
// 清除波形区(保留Y轴像素列)
_tft.fillRect(GRAPH_X+1, GRAPH_Y, GRAPH_W-1, GRAPH_H, C_BG);
// 重绘网格
for (int row=1; row<4; row++) {
int yh = GRAPH_Y + row*GRAPH_H/4;
_tft.drawFastHLine(GRAPH_X+1, yh, GRAPH_W-1, (row==2)?C_ZEROLINE:C_GRID_H);
}
for (int col=1; col<8; col++) {
int xv = GRAPH_X + col*(GRAPH_W/8);
for (int gy=GRAPH_Y+2; gy<GRAPH_Y+GRAPH_H; gy+=4)
_tft.drawPixel(xv, gy, C_GRID_V);
}
_tft.drawFastVLine(GRAPH_X, GRAPH_Y, GRAPH_H, C_AXIS);
_tft.drawFastHLine(GRAPH_X, GRAPH_Y+GRAPH_H, GRAPH_W, C_AXIS);
_tft.setTextColor(C_ZEROLINE); _tft.setTextSize(1);
_tft.setCursor(GRAPH_X+2, ZERO_Y-8); _tft.print("0");
_tft.setTextColor(C_DIM);
_tft.setCursor(GRAPH_X+2, GRAPH_Y+2); _tft.print("+");
_tft.setCursor(GRAPH_X+2, GRAPH_Y+GRAPH_H-10); _tft.print("-");
// 计算正负半轴独立量程
float posMax = 0.3f, negMax = 0.3f;
for (int k=0; k<HISTORY_SIZE; k++) {
float c = _iHist[k];
if (c > posMax) posMax = c;
if (-c > negMax) negMax = -c;
if (_vHist[k] > posMax) posMax = _vHist[k];
if (_pHist[k] > posMax) posMax = _pHist[k];
}
posMax *= 1.15f;
negMax *= 1.15f;
int zeroY = ZERO_Y;
int posH = zeroY - GRAPH_Y - 1; // 正半轴像素高度
int negH = GRAPH_Y + GRAPH_H - 1 - zeroY; // 负半轴像素高度
// Y轴刻度标注
_tft.setTextColor(C_LABEL); _tft.setTextSize(1);
_tft.fillRect(GRAPH_X+1,GRAPH_Y,28,7,C_BG);
_tft.setCursor(GRAPH_X+1,GRAPH_Y+1); _tft.print(posMax,1);
_tft.fillRect(GRAPH_X+1,zeroY-11,28,7,C_BG);
_tft.setCursor(GRAPH_X+1,zeroY-11); _tft.print(posMax/2,1);
_tft.fillRect(GRAPH_X+1,zeroY+5,28,7,C_BG);
_tft.setCursor(GRAPH_X+1,zeroY+5); _tft.print(-negMax/2,1);
_tft.fillRect(GRAPH_X+1,GRAPH_Y+GRAPH_H-10,28,7,C_BG);
_tft.setCursor(GRAPH_X+1,GRAPH_Y+GRAPH_H-10); _tft.print(-negMax,1);
// 映射宏(正值映射到零线上方,负电流映射到零线下方)
#define MAP_POS(val) \
constrain((int)(zeroY - max((val),0.0f)/posMax*posH), GRAPH_Y+1, zeroY-1)
#define MAP_CURR(val) \
((val)>=0 \
? constrain((int)(zeroY-(val)/posMax*posH), GRAPH_Y+1, zeroY-1) \
: constrain((int)(zeroY+(-(val))/negMax*negH), zeroY+1, GRAPH_Y+GRAPH_H-1))
// 绘制折线(环形缓存按时间顺序展开)
for (int k=1; k<HISTORY_SIZE; k++) {
int pi = (_hIdx+k-1)%HISTORY_SIZE;
int ci = (_hIdx+k) %HISTORY_SIZE;
int x1 = GRAPH_X + (k-1)*2 + 1;
int x2 = GRAPH_X + k*2 + 1;
if (x2 >= GRAPH_X+GRAPH_W) break;
_tft.drawLine(x1, MAP_POS(_vHist[pi]), x2, MAP_POS(_vHist[ci]), VOLTAGE_COLOR);
_tft.drawLine(x1, MAP_CURR(_iHist[pi]), x2, MAP_CURR(_iHist[ci]), CURRENT_COLOR);
_tft.drawLine(x1, MAP_POS(_pHist[pi]), x2, MAP_POS(_pHist[ci]), POWER_COLOR);
}
#undef MAP_POS
#undef MAP_CURR
// 写入游标线
int curX = GRAPH_X + (HISTORY_SIZE-1)*2 + 1;
if (curX < GRAPH_X+GRAPH_W-1)
_tft.drawFastVLine(curX, GRAPH_Y+1, GRAPH_H-2, C_DIVIDER);
}
环形缓存
hIdx 指向最新写入位置,绘制时从 _hIdx+1 开始顺序画出,确保波形从左向右滚动
3.4 信息栏动态刷新
更新电池图标(含SOC填充)、SOC百分比数字、累计充放电容量、以及状态圆点
void DisplayHandler::_updateRight(const BatteryData& d) {
int px = PANEL_X + 3;
uint16_t battColor = (d.soc>60)?C_OK:(d.soc>25)?C_YELLOW:C_WARN;
// 框1:电池图标 + SOC%(y=15~83)
_tft.fillRect(PANEL_X+1,16,PANEL_W-2,57,C_PANEL);
_drawBattIcon(px,18,50,26,d.soc,battColor);
_tft.fillRect(PANEL_X+1,56,PANEL_W-2,16,C_PANEL);
_tft.setTextColor(battColor); _tft.setTextSize(1);
_tft.setCursor(px+(d.soc<10?8:2),58);
_tft.print(d.soc); _tft.print('%');
// 框2:CHG/DSG累计容量(y=85~173)
_tft.fillRect(PANEL_X+1,85,PANEL_W-2,88,C_PANEL);
_tft.setTextColor(C_OK); _tft.setTextSize(1);
_tft.setCursor(px,98); _tft.print("CHG");
_tft.setCursor(px,110);
if (d.chargeAh<1.0f)
{ _tft.print(d.chargeAh*1000,1); _tft.print("mAh"); }
else
{ _tft.print(d.chargeAh,3); _tft.print("Ah"); }
_tft.setTextColor(CURRENT_COLOR);
_tft.setCursor(px,135); _tft.print("DSG");
_tft.setCursor(px,147);
if (d.dischargeAh<1.0f)
{ _tft.print(d.dischargeAh*1000,1); _tft.print("mAh"); }
else
{ _tft.print(d.dischargeAh,3); _tft.print("Ah"); }
// 框3:状态圆点(y=175~198)
_tft.fillRect(PANEL_X+1,175,PANEL_W-2,24,C_PANEL);
uint16_t dotColor;
if (d.state==STATE_CHARGING) {
dotColor = battColor;
_tft.fillCircle(PANEL_X+PANEL_W/2,187,6,dotColor);
} else if (d.state==STATE_DISCHARGING) {
// 放电电流越大越偏红(视觉警示)
float ratio = constrain((-d.current)/MAX_CURRENT,0.0f,1.0f);
dotColor = (ratio>0.5f) ? C_WARN : CURRENT_COLOR;
_tft.fillCircle(PANEL_X+PANEL_W/2,187,6,dotColor);
} else {
_tft.fillCircle(PANEL_X+PANEL_W/2,187,6,C_DIM);
}
}
为了减少闪烁,每次刷新前先用 fillRect 清除对应区域、放电时圆点颜色随电流大小渐变
3.5 主程序架构
主程序将初始化、主循环、延时调度和风扇按键响应完整串联起来;SoftWire 读取VBUS寄存器 → 返回电压
/******************************************************************************
* 文件: INA238_STM32_Monitor/INA238_STM32_Monitor.ino
* 作者: 零知派(深圳市在芯间科技有限公司)
* -^^- 零知派,让电子制作变得更简单! -^^-
* 日期: 2026-05-13
* 功能: 零知派标准板(STM32F103RBT6) + INA238 锂电池充放电监测系统
* 集成电池监测算法、ST7789 实时波形与信息显示、物理按键风扇控制、
* 串口调试输出(含容量、SOC、时间估算等)
* 电流从电池负极流出经充电板OUT+ →VIN+ →Rshunt →VIN- →充电板OUT-形成回路
* 充电时INA238读正电流,放电时读负电流,与低侧接线物理方向一致
******************************************************************************/
#include "config.h"
#include "BatteryMonitor.h"
#include "DisplayHandler.h"
// 全局变量
static unsigned long lastPrintTime = 0;
static unsigned long lastLowVoltageCheck = 0;
static unsigned long lastKeyCheck = 0;
static bool fanOn = false; // 风扇运行标志
static bool lastKeyState = HIGH; // 按键上次电平
// 辅助函数:将浮点数转换为指定小数位数的字符串并输出(避免 printf)
static void printFloat(float value, int precision) {
char buffer[32];
dtostrf(value, 0, precision, buffer);
DBG(buffer);
}
// 函数声明
void handleKey();
void checkLowVoltage();
void printDebugInfo();
// ═══════════════════════════════════════════════════════════
void setup() {
#if DEBUG_ENABLE
Serial.begin(DEBUG_BAUD);
delay(1000);
DBGLN("\n\n");
DBGLN("╔═════════════════╗");
DBGLN("║ STM32 INA238 锂电池充放电监测 ║");
DBGLN("╚═════════════════╝");
DBGLN();
#endif
DBGLN("=== 系统初始化开始 ===");
DBGLN("[1/3] 初始化 INA238 与电池算法...");
if (!Battery.begin()) {
DBGLN("❌ INA238 初始化失败,系统停止");
while (1) delay(100);
}
DBGLN("[2/3] 初始化 ST7789 显示屏...");
Display.begin();
DBGLN("[3/3] 初始化风扇 PWM 与物理按键...");
pinMode(FAN_CTRL_PIN, OUTPUT);
analogWrite(FAN_CTRL_PIN, 0);
pinMode(KEY_PIN, INPUT_PULLUP);
lastKeyState = digitalRead(KEY_PIN);
DBGLN("\n=== 所有模块初始化完成 ===");
DBGLN("=== 开始实时监测 ===\n");
delay(500);
}
// ═══════════════════════════════════════════════════════════
void loop() {
Battery.update();
Display.update();
if (millis() - lastKeyCheck >= KEY_DEBOUNCE_MS) {
lastKeyCheck = millis();
handleKey();
}
if (millis() - lastLowVoltageCheck >= 2000) {
lastLowVoltageCheck = millis();
checkLowVoltage();
}
printDebugInfo();
delay(5);
}
// ═══════════════════════════════════════════════════════════
void handleKey() {
bool reading = digitalRead(KEY_PIN);
if (lastKeyState == HIGH && reading == LOW) {
delay(KEY_DEBOUNCE_MS);
if (digitalRead(KEY_PIN) == LOW) {
fanOn = !fanOn;
analogWrite(FAN_CTRL_PIN, fanOn ? FAN_PWM_DUTY : 0);
DBG("[按键] 风扇已");
DBGLN(fanOn ? "开启" : "关闭");
}
}
lastKeyState = reading;
}
// ═══════════════════════════════════════════════════════════
void checkLowVoltage() {
if (Battery.isLowVoltage()) {
DBGLN("\n⚠️⚠️⚠️ 低电压保护触发!系统将停止 ⚠️⚠️⚠️");
analogWrite(FAN_CTRL_PIN, 0);
Display.showLowVoltageWarning();
}
}
// ═══════════════════════════════════════════════════════════
void printDebugInfo() {
#if DEBUG_ENABLE
unsigned long now = millis();
if (now - lastPrintTime >= DEBUG_INTERVAL_MS) {
BatteryData data = Battery.getData();
BattState state = data.state;
const char* stateStr[] = {"待机", "充电", "放电"};
DBGLN("──────────────────────────────────────────");
DBG("⚡ 电池状态: ");
DBGLN(stateStr[state]);
DBGLN("\n📊 实时数据:");
DBG(" 总线电压: "); printFloat(data.voltage, 3);
DBG(" V (EMA: "); printFloat(data.emaVoltageV, 3); DBGLN(" V");
DBG(" 原始电流: ");
if (data.current >= 0) DBG("+");
printFloat(data.current, 4);
DBG(" A (EMA: ");
if (data.emaCurrentA >= 0) DBG("+");
printFloat(data.emaCurrentA, 4);
DBGLN(" A");
DBG(" 功率: "); printFloat(data.power, 3); DBGLN(" W");
DBG(" OCV 估算: "); printFloat(data.ocv, 3); DBGLN(" V");
DBG(" 融合 SOC: "); printFloat(data.socFused, 1);
DBG(" % (显示 "); DBG(data.soc); DBGLN(" %)");
DBGLN("\n🔋 累计容量:");
DBG(" 充入: "); printFloat(data.chargeAh * 1000, 1);
DBG(" mAh ("); printFloat(data.chargeAh, 4); DBGLN(" Ah)");
DBG(" 放出: "); printFloat(data.dischargeAh * 1000, 1);
DBG(" mAh ("); printFloat(data.dischargeAh, 4); DBGLN(" Ah)");
if (state == STATE_CHARGING || state == STATE_DISCHARGING) {
float timeMin = (state == STATE_CHARGING) ? Battery.estimateTimeToFull() : Battery.estimateTimeToEmpty();
if (timeMin > 0 && timeMin < 600) {
int hours = (int)(timeMin / 60);
int mins = (int)timeMin % 60;
DBG("\n⏱ 预计");
DBG(state == STATE_CHARGING ? "充满" : "放完");
DBG("剩余时间: ");
DBG(hours); DBG("小时 ");
DBG(mins); DBGLN("分钟");
}
}
DBG("\n🌀 风扇: ");
DBG(fanOn ? "运行中" : "停止");
DBG(" (PWM 占空比 ");
DBG(fanOn ? FAN_PWM_DUTY : 0);
DBGLN("/255)");
DBGLN("──────────────────────────────────────────\n");
lastPrintTime = now;
}
#endif
}
/******************************************************************************
* 深圳市在芯间科技有限公司
* 淘宝店铺:在芯间科技零知板
* 店铺网址:https://shop533070398.taobao.com
* 版权说明:
* 1.本代码的版权归【深圳市在芯间科技有限公司】所有,仅限个人非商业性学习使用。
* 2.严禁将本代码或其衍生版本用于任何商业用途(包括但不限于产品开发、付费服务、企业内部使用等)。
* 3.任何商业用途均需事先获得【深圳市在芯间科技有限公司】的书面授权,未经授权的商业使用行为将被视为侵权。
******************************************************************************/
Battery.update() → INA238::getBusVoltage() / getCurrent() / getPower() → 寄存器读取(通过 SoftWire)→ 滤波 → OCV 补偿 → SOC 融合 → 数据更新
软件 I2C(SoftWire)时序模拟
采用 SoftWire 库通过 GPIO 位操作模拟 I2C 时序
void SoftWire::i2c_start() {
set_sda(LOW);
set_scl(LOW);
}
void SoftWire::i2c_shift_out(uint8 val) {
for (int i = 0; i < 8; i++) {
set_sda(!!(val & (1 << (7 - i))));
set_scl(HIGH);
set_scl(LOW);
}
}
系统流程图

四、项目结果演示
4.1 操作过程
系统上电

零知派标准板供电,屏幕显示启动画面,然后进入主界面(左侧波形区,右侧电池图标、SOC%、充放电容量)
充电测试

连接TP4056充电模块至电池,观察屏幕右下角状态变为“CHG”,右侧SOC百分比逐渐上升,电池图标填充,底部电压数值稳定在4.10~4.20V,电流显示正值
放电测试

拔掉充电器,按下按键启动风扇负载(或连接其他负载),屏幕状态变为“DSG”,SOC逐渐下降,放电容量累加。若放电电流较大,右侧圆点可变为红色
低电压保护

bool BatteryMonitor::isLowVoltage() const {
return (_d.state == STATE_DISCHARGING && _d.voltage < BATT_MIN_V);
}
当电压低于2.80V时,屏幕显示“LOW VOLT!”并死循环,风扇停止
4.2 视频演示
零知派标准板+INA238锂电池充放电状态可视化
本视频完整演示了零知派标准板驱动INA238监测18650锂电池充放电全过程。包括上电初始化、充电时SOC平滑上升、放电时SOC平稳下降、按键控制风扇散热、串口调试信息实时刷新以及低电压保护触发。核心亮点:OCV内阻补偿消除了电压虚高,限速滤波进行SOC缓慢变化
五、INA238技术原理讲解
核心架构
INA238是一款超精密数字功率监测器,内置16位delta-sigma(Δ-Σ)ADC,专为电流检测应用设计。可测量±163.84 mV或±40.96 mV的全量程差分输入,共模电压支持范围从-0.3 V到+85 V
Δ-Σ ADC的核心原理是过采样 + 噪声整形:

工作原理:分流电阻Rshunt串入被测回路,电流I流过时产生压降Vshunt = I × Rshunt;INA238的内部Δ-Σ ADC对Vshunt进行16位高精度差分采样;同时VBUS引脚独立采样总线电压Vbus;内置乘法器计算功率P = Vbus × I
5.1 寄存器操作
①VSHUNT寄存器
INA238支持两种分流电压测量量程:±163.84 mV 或 ±40.96 mV,由CONFIG寄存器中的ADCRANGE位控制
本项目使用默认量程±163.84 mV(ADCRANGE=0),对应分辨率:

分流电压可以是正值或负值,因为系统中的电流是双向的,VSHUNT寄存器中的数据可以为正也可以为负

②POWER寄存器
VBUS寄存器的转换系数为3.125 mV/LSB,总线电压始终为正值,采用16位有符号但数值始终为正的存储格式
功率计算在芯片内部硬件完成

③配置寄存器
| 位域 | 名称 | 本项目设置 | 说明 |
|---|---|---|---|
| 15 | RST | 0 | 不复位 |
| 13-6 | CONVDLY | 0 | 无转换延迟 |
| 4 | ADCRANGE | 0 | ±163.84mV量程(适合5A/0.015Ω) |
未显式写入,保持默认0。若需要更高精度可设ADCRANGE=1(±40.96mV),此项目需将SHUNT_CAL乘以4
5.2 I2C通信协议
INA238使用标准I²C接口(最高1MHz),7位器件地址默认0x40(A0=GND, A1=GND时可配置为0x41~0x4F)
I2C地址配置:INA238的I2C地址由A0、A1两个引脚的接法决定:

I2C读取时序:主设备先发送从设备地址+寄存器指针字节,再发送或读取对应数据字节
写字节时序
从设备地址字节的值由A0和A1引脚的设置决定
读字节时序
读取数据来自最后一个寄存器指针位置;若需使用新寄存器,必须更新寄存器指针;主设备也可发送ACK确认信号
六、常见问题解答(FAQ)
Q1:为什么电流读数总是0?
A:可能原因:(1) 未调用setMaxCurrentShunt()写入校准值;(2) 分流电阻两端接线错误,导致差分电压为零。用万用表测VIN+与VIN-之间的电压
Q2:充电时SOC一开始就跳变到90%以上?
A:R_CHG参数设置过小,导致OCV补偿不足。增大config.h中的R_CHG(例如0.25Ω)重新编译
项目资源整合
INA238数据手册: INA238 datasheet
INA238库文件: RobTillaart/INA238
TFT_eSPI: Bodmer/TFT_eSPI
更多推荐
所有评论(0)