目录

一、系统接线部分

1.1 硬件清单

1.2 接线方案表

1.3 连接示意图

1.4 具体接线图

二、安装与使用教程

三、代码讲解部分

3.1 滑动平均+EMA双层滤波

3.2 库仑计融合与限速滤波

3.3 波形区循环刷新

3.4 信息栏动态刷新

3.5 主程序架构

四、项目结果演示

4.1 操作过程

4.2 视频演示

五、INA238技术原理讲解

5.1 寄存器操作

5.2 I2C通信协议

六、常见问题解答(FAQ)

Q1:为什么电流读数总是0?

Q2:充电时SOC一开始就跳变到90%以上?


项目概述

        本项目基于零知派标准板(主控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 

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐