零知派ESP32——基于INA238高精度功率监测芯片的18650锂电池充电状态可视化与数据导出

✔零知派(零知开源)是一个专为电子初学者/电子兴趣爱好者设计的开源软硬件平台,在硬件上提供超高性价比STM32系列开发板、物联网控制板。取消了Bootloader程序烧录,让开发重心从 "配置环境" 转移到 "创意实现",极大降低了技术门槛。零知开源编程软件,内置上千个覆盖多场景的示例代码,支持项目源码一键下载,项目文章在线浏览。零知派(零知开源)平台通过软硬件协同创新,让你的创意快速转化为实物,来动手试试吧!

✔访问零知实验室,获取更多实战项目和教程资源吧!

www.lingzhilab.com

目录

一、系统接线部分

[1.1 硬件清单](#1.1 硬件清单)

[1.2 接线方案表](#1.2 接线方案表)

[1.3 具体接线图](#1.3 具体接线图)

[1.4 接线实物图](#1.4 接线实物图)

二、安装与使用部分

三、代码讲解部分

[3.1 双层滤波与OCV补偿](#3.1 双层滤波与OCV补偿)

[3.2 融合SOC估算算法](#3.2 融合SOC估算算法)

[3.3 内嵌HTML网页与CSV导出](#3.3 内嵌HTML网页与CSV导出)

[3.4 LVGL监视器界面](#3.4 LVGL监视器界面)

[3.5 INA238 库函数](#3.5 INA238 库函数)

四、项目演示

[4.1 操作流程](#4.1 操作流程)

[4.2 视频演示](#4.2 视频演示)

五、INA238技术原理讲解

[5.1 寄存器操作](#5.1 寄存器操作)

[5.2 I2C通信协议](#5.2 I2C通信协议)

六、常见问题解答(FAQ)

Q1:波形图刷新太慢,卡顿明显?

Q2:网页数据不更新,一直显示0?


项目概述

本项目使用零知派ESP32 (主控芯片为ESP-WROOM-32 )和INA238 高精度数字功率监测芯片,实现一套完整的18650锂电池充放电实时监测系统。系统能够精确采集电池的电压、电流和功率数据,通过双层滤波算法 有效抑制ADC噪声,引入OCV内阻动态补偿 技术解决充电状态下电压虚高问题,并采用OCV电压法与库仑计融合算法实现高精度的SOC估算,误差控制在±5%以内。监测数据可通过ST7789彩色显示屏实时展示波形曲线,同时支持Web网页远程监控和CSV格式数据一键导出

项目难点及解决方案

问题描述:充电状态下电压虚高 → OCV内阻动态补偿

**解决方案:**采用欧姆定律建立OCV补偿模型 OCV = Vbus - I × R_int

一、系统接线部分

1.1 硬件清单

编号 组件名称 型号规格 数量 备注
1 主控板 零知派ESP32(ESP-WROOM-32) 1 核心控制板
2 电流传感器 INA238模块(R015分流电阻) 1 TI高精度功率监测芯片
3 显示屏 ST7789 240×320 TFT彩屏 1 SPI接口
4 散热风扇 L9110H 直流电机驱动 1 用于负载/放电演示
5 按键 轻触按键 1 物理控制风扇开关
6 锂电池 18650锂电池 1 待监测目标电池
7 充电模块 TP4056或其他锂电池充电板 1 演示充电状态

1.2 接线方案表

以下引脚定义严格依据 config.h 中的宏定义

ESP32引脚 连接目标 引脚功能 代码宏定义
GPIO21 INA238 SDA I2C数据线 I2C_SDA 21
GPIO22 INA238 SCL I2C时钟线 I2C_SCL 22
GPIO25 风扇PWM控制端 LEDC PWM输出 FAN_PIN 25
GPIO26 按键(一端,另一端接GND) 内部上拉输入低有效 KEY_PIN 26
GPIO18 TFT SCK SPI时钟 TFT_eSPI库文件定义
GPIO23 TFT MOSI SPI主机输出 TFT_eSPI库文件定义
GPIO19 TFT MISO SPI主机输入 TFT_eSPI库文件定义
GPIO15 TFT CS SPI片选 TFT_eSPI库文件定义
GPIO2 TFT DC 数据/命令选择 TFT_eSPI库文件定义
GPIO4 TFT RST 复位 TFT_eSPI库文件定义

注意:ST7789显示屏直插到ESP32扩展板上、无需单独接线

充放电的双向电流检测务必按照以下方式接线:

充电回路

INA238引脚 连接目标
VIN+ 电池负极
VIN- 充电板OUT-

充电时:电流从充电器OUT+流出 -> 电池正极/负极 -> 进入 VIN+ -> 采样电阻 -> 流向 VIN- 。此时电流方向为正 (+),系统识别为充电。

放电回路

| INA238引脚 | 连接目标 |
| VIN+ | 负载/直流电机 VCC正极 |

VIN- 负载 GND负极

放电时:电流从充电器OUT-流出 -> 进入 VIN- -> 流向 VIN+ -> 进入负载。此时电流方向为负 (-),系统识别为放电。

1.3 具体接线图

电池负极输出端接分流电阻(R015)的负载侧(VIN+),分流电阻另一端接VIN-并最终到GND

电流必须按照「从电池负极流出,经VIN+ → Rshunt → VIN- 形成回路」的完整路径流过差分感测通道

1.4 接线实物图

VBUS引脚不可悬空,必须正确连接到电源总线正极。INA238通过VBUS引脚直接采样总线电压以完成电压和功率测量,若悬空或接错位置,功率寄存器将无法参与计算,输出持续为零

二、安装与使用部分

2.1 开源平台-输入"INA238" 并搜索-下载代码自动打开

2.2 连接-验证-上传

2.3 调试-串口监视器

三、代码讲解部分

本项目代码采用模块化拆分:BatteryMonitor负责核心算法、DisplayHandler负责LVGL界面、FanController负责风扇PWM控制、WebHandler负责网络服务

以下重点讲解四个核心模块的实现:

3.1 双层滤波与OCV补偿

BatteryMonitor.cpp 是整个系统的核心算法文件,每1000ms执行一次完整的数据采集与处理流水线

cpp 复制代码
void BatteryMonitor::update() {
  unsigned long now = millis();
  if (now - _lastMs < SAMPLE_INTERVAL) return;
  float dtH   = (now - _lastMs) / 3600000.0f;
  _lastMs     = now;

  float rawV = _ina.getBusVoltage();
  float rawI = _ina.getCurrent();
  float rawP = _ina.getPower();

  float avgI = _slidingAvg(rawI);
  BattMode m = _detectMode(avgI);
  _updateEMA(avgI, rawV, m);

  // 库仑计(用原始电流)
  if (rawI >  CHARGE_THRESH) _d.chargeAh    += rawI    * dtH;
  if (rawI < -CHARGE_THRESH) _d.dischargeAh += (-rawI) * dtH;

  float ocv = rawV - _emaI * BATT_R_INT;
  ocv = constrain(ocv, 2.50f, 4.25f);

  uint8_t soc  = _estimateSOC(rawV, m);
  float   tMin = _estimateTime((float)soc, m);

  _d.voltage    = rawV;
  _d.ocv        = ocv;
  _d.current    = rawI;
  _d.emaCurrent = _emaI;
  _d.emaVoltage = _emaV;
  _d.power      = rawP;
  _d.soc        = soc;
  _d.timeEstMin = tMin;
  _d.mode       = m;
  _d.uptimeSec  = (now - _startMs) / 1000UL;

  if (m == MODE_DISCHARGING && rawV < BATT_MIN_V) {
    _sys = SYS_LOW_V;
    DEBUG_PRINTLN("[WARN] 低电压保护触发");
  }
  _lastMode = m;
}

OCV 补偿公式

ocv = rawV - _emaI * BATT_R_INT 本质是欧姆定律,参数 BATT_R_INT = 0.15Ω 为实测等效内阻,用户可根据实际接线校

充电时 _emaI > 0rawV 包含了线路压降,减去 I×R 得到真实开路电压; 放电时 _emaI < 0,减去负值相当于加上电压,补偿负载拉低的总线电压

3.2 融合SOC估算算法

cpp 复制代码
uint8_t BatteryMonitor::_estimateSOC(float busV, BattMode m) {
  float ocv  = busV - _emaI * BATT_R_INT;
  ocv = constrain(ocv, 2.50f, 4.25f);
  float socV = _ocvToSOC(ocv);

  if (_socCoulomb < 0.0f) {
    _socCoulomb  = socV;
    _socAnchorAh = _d.chargeAh - _d.dischargeAh;
  } else {
    float netAh = (_d.chargeAh - _d.dischargeAh) - _socAnchorAh;
    _socCoulomb  = socV + (netAh / BATT_CAPACITY_AH) * 100.0f;
    if (m == MODE_STANDBY) {
      _socCoulomb  = socV;
      _socAnchorAh = _d.chargeAh - _d.dischargeAh;
    }
  }
  _socCoulomb = constrain(_socCoulomb, 0.0f, 100.0f);

  float wV = (m == MODE_STANDBY) ? 1.0f : 0.3f;
  float wC = (m == MODE_STANDBY) ? 0.0f : 0.7f;
  float fused = wV*socV + wC*_socCoulomb;
  return (uint8_t)constrain(fused+0.5f, 0.0f, 100.0f);
}

融合算法核心逻辑

OCV电压法,将补偿后的OCV电压通过10段分段线性表映射为SOC百分比;库仑计数法,从初始SOC基准点加减变化量得到当前SOC

3.3 内嵌HTML网页与CSV导出

WebHandler.cpp

通过/data接口每1秒轮询JSON 获取全部监测数据,Canvas 实时绘制60点三通道波形。风扇控制通过/cmd接口实现开关/循环模式/PWM 占空比远程调节,/csv接口支持历史数据一键下载

cpp 复制代码
void WebHandlerBattery::_onData() {
  BatteryData d = Battery.getData();
  String j = "{";
  j += "\"voltage\":"    + String(d.voltage,    3);
  j += ",\"ocv\":"       + String(d.ocv,         3);
  j += ",\"current\":"   + String(d.current,     3);
  j += ",\"emaCurrent\":" + String(d.emaCurrent, 3);
  j += ",\"power\":"     + String(d.power,       2);
  j += ",\"soc\":"       + String((int)d.soc);
  j += ",\"chargeAh\":"  + String(d.chargeAh,   4);
  j += ",\"dischargeAh\":" + String(d.dischargeAh,4);
  j += ",\"mode\":"      + String((int)d.mode);
  j += ",\"timeEst\":"   + String(d.timeEstMin, 1);
  j += ",\"fan\":"       + String(Fan.isRunning()?"true":"false");
  j += ",\"pwm\":"       + String(Fan.getDutyPct());
  j += ",\"cycle\":"     + String(Fan.isCycleMode()?"true":"false");
  j += ",\"uptime\":"    + String((unsigned long)d.uptimeSec);
  j += ",\"heap\":"      + String(ESP.getFreeHeap()/1024);
  j += "}";
  _srv.sendHeader("Access-Control-Allow-Origin","*");
  _srv.send(200,"application/json",j);
}

void WebHandlerBattery::_onCmd() {
  if (_srv.hasArg("fan")) {
    String v = _srv.arg("fan");
    if      (v=="toggle") Fan.toggle();
    else if (v=="on")     Fan.setOn(true);
    else if (v=="off")    Fan.setOn(false);
    DEBUG_PRINTF("[Web] 风扇: %s → %s\n", v.c_str(), Fan.isRunning()?"ON":"OFF");
  }
  if (_srv.hasArg("duty")) {
    int pct = _srv.arg("duty").toInt();
    Fan.setDuty((uint8_t)(constrain(pct,0,100)*255/100));
    DEBUG_PRINTF("[Web] PWM: %d%%\n", pct);
  }
  if (_srv.hasArg("cycle")) {
    bool en = (_srv.arg("cycle")=="on");
    Fan.enableCycleMode(en);
    DEBUG_PRINTF("[Web] 循环模式: %s\n", en?"ON":"OFF");
  }
  _srv.send(200,"text/plain","OK");
}

void WebHandlerBattery::_onCsv() {
  String csv = "Timestamp_s,Voltage_V,Current_A,Power_W,SOC_pct\n";
  for (int i=0; i<CSV_SIZE; i++) {
    int idx=(_csvIdx+i)%CSV_SIZE;
    if (_csv[idx].ts==0) continue;
    csv += String(_csv[idx].ts)+"," +String(_csv[idx].v,3)+","
          +String(_csv[idx].i,4)+","+String(_csv[idx].p,2)+","
          +String((int)_csv[idx].soc)+"\n";
  }
  _srv.sendHeader("Content-Disposition","attachment; filename=batt.csv");
  _srv.send(200,"text/csv",csv);
}

void WebHandlerBattery::_pushCsv() {
  BatteryData d=Battery.getData();
  _csv[_csvIdx]={millis()/1000UL, d.voltage, d.current, d.power, d.soc};
  _csvIdx=(_csvIdx+1)%CSV_SIZE;
}

**JSON接口设计:**覆盖电压、电流、功率、OCV、EMA平滑值、SOC、充放电容量、时间估算、模式、风扇状态、PWM百分比、循环模式标志、运行时长和剩余堆内存

3.4 LVGL监视器界面

cpp 复制代码
void DisplayHandler::_buildChart() {
  lv_obj_t* card = makeCard(_scr, 0, 28, 202, 130, COL_CARD, COL_BORDER);

  _chart = lv_chart_create(card);
  lv_obj_set_size(_chart, 186, 116);
  lv_obj_align(_chart, LV_ALIGN_CENTER, 0, 0);
  lv_obj_set_style_bg_color(_chart, lv_color_make(8,13,24), 0);
  lv_obj_set_style_bg_opa(_chart, LV_OPA_COVER, 0);
  lv_obj_set_style_border_color(_chart, COL_BORDER, 0);
  lv_obj_set_style_border_width(_chart, 1, 0);
  lv_obj_set_style_radius(_chart, 3, 0);
  lv_obj_set_style_pad_all(_chart, 2, 0);

  lv_chart_set_type(_chart, LV_CHART_TYPE_LINE);
  lv_chart_set_point_count(_chart, CHART_POINTS);
  lv_chart_set_update_mode(_chart, LV_CHART_UPDATE_MODE_SHIFT);
  // Y轴范围:-60~500(单位 0.01,覆盖 -0.6A~5.0V/A/W)
  lv_chart_set_range(_chart, LV_CHART_AXIS_PRIMARY_Y, -60, 500);

  // 分割线(网格)
  lv_chart_set_div_line_count(_chart, 4, 8);
  lv_obj_set_style_line_color(_chart, COL_BORDER, LV_PART_MAIN);

  // 三路数据系列
  _serV = lv_chart_add_series(_chart, COL_VOLT, LV_CHART_AXIS_PRIMARY_Y);
  _serI = lv_chart_add_series(_chart, COL_CURR, LV_CHART_AXIS_PRIMARY_Y);
  _serP = lv_chart_add_series(_chart, COL_POW,  LV_CHART_AXIS_PRIMARY_Y);

  // 用初始值填充,避免显示乱线
  for (int i = 0; i < CHART_POINTS; i++) {
    lv_chart_set_next_value(_chart, _serV, 0);
    lv_chart_set_next_value(_chart, _serI, 0);
    lv_chart_set_next_value(_chart, _serP, 0);
  }

  // 线宽
  lv_obj_set_style_line_width(_chart, 2, LV_PART_ITEMS);
  lv_obj_set_style_size(_chart, 0, 0, LV_PART_INDICATOR); // 隐藏点

  // Y 轴刻度标签(放在 card 外侧,用 label 模拟)
  _lblYMax  = makeLabel(card, "5.0",  COL_DIM, 0, 2,  &lv_font_montserrat_10);
  _lblYZero = makeLabel(card, "0",    COL_DIM, 0, 56, &lv_font_montserrat_10);
  _lblYMin  = makeLabel(card, "-0.6", COL_DIM, 0, 110,&lv_font_montserrat_10);
}

LVGL内存控制策略

Draw buffer采用 SCREEN_W × 10 行 单缓冲、lv_chart固定60点三路数据系列、label使用LVGL内置Montserrat小字体、LV_MEM_SIZE 配置为32KB

系统流程图

3.5 INA238 库函数

getCurrent() 如何获得真实电流?

INA238库内部调用了 _readRegister(INA238_CURRENT, 2) 获得16位原始值 value,然后乘以 _current_LSB 得到实际电流

cpp 复制代码
float INA238::getCurrent()
{
  //  remove reserved bits.
  int16_t value = _readRegister(INA238_CURRENT, 2);
  //  int16_t handles negative values (16 bit)
  float current = value * _current_LSB;  //  8.1.2 formula (3)
  return current;
}

int INA238::setMaxCurrentShunt(float maxCurrent, float shunt)
{
  //  Shunt can be really small
  if (shunt < 0.0001)   return -2;
  if (maxCurrent < 0.0) return -3;
  _maxCurrent = maxCurrent;
  _shunt = shunt;
  _current_LSB = _maxCurrent / (float)(1UL << 15);  //  pow(2, -15);
  // _current_LSB = _maxCurrent * 3.0517578125e-5;  //  pow(2, -15);

  //  PAGE 28-29 (8.1.2)
  float shunt_cal = 819.2e6 * _current_LSB * _shunt;  //  8.1.2  formula (1,2)
  //  depends on ADCRANGE in INA238_CONFIG register.
  if (_ADCRange == true)
  {
    shunt_cal *= 4;
  }
  //  shunt_cal must be written to its REGISTER.
  _writeRegister(INA238_SHUNT_CAL, shunt_cal);

  return 0;
}

_current_LSB公式计算

该项目中:Max_Current = 5A,Rshunt = 0.015Ω,则 CURRENT_LSB ≈ 152.6 µA,SHUNT_CAL ≈ 1874。该值被写入芯片的 SHUNT_CAL 寄存器(0x02),芯片即按此比例输出电流值

INA238寄存器地址表

地址 寄存器名 位宽 本项目使用情况
0x00 CONFIG 16-bit 芯片复位、ADC量程选择(ADCRANGE位)
0x01 ADC_CONFIG 16-bit 采样均值数、转换时间配置
0x02 SHUNT_CAL 16-bit 关键:校准寄存器,决定电流精度

四、项目演示

4.1 操作流程

①硬件上电初始化

接通零知派ESP32的5V供电(USB或外部电源),系统自动完成INA238初始化、TFT屏幕初始化和WiFi热点启动。屏幕顶部标题栏显示"BATT MONITOR"即为初始化成功

②连接WiFi查看Web页面

手机或电脑搜索WiFi热点INA238_Monitor,密码12345678。浏览器访问http://192.168.4.1进入仪表盘页面,页面以1Hz频率自动刷新全部数据

③充放电演示

断开充电器并接入负载(如风扇),电流跃至负半轴、模式变为"DISCHG"

接入CSM4056充电模块对18650电池充电,观察屏幕波形区电流跃升至正半轴、模式变为"CHARGING"、SOC百分比缓慢上升

④数据导出

在Web仪表盘点击" CSV"按钮,浏览器自动下载历史数据CSV文件,可在Excel中直接打开进行趋势分析

4.2 视频演示

ESP32驱动INA238的18650锂电池充电监测系统

基于零知派ESP32和TI INA238高精度功率监测芯片,完整演示锂电池充放电实时监测系统的搭建与使用。涵盖硬件接线、TFT显示屏波形刷新、Web网页远程监控仪表盘、CSV数据一键导出整个操作流程。充放电状态自动识别,SOC融合算法精准跟踪,OCV内阻补偿消除充电虚高

五、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位有符号但数值始终为正的存储格式

功率计算在芯片内部硬件完成

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:波形图刷新太慢,卡顿明显?

*A:SAMPLE_INTERVAL_MS设置过小(默认200ms)时,TFT全屏刷新和WiFi处理会竞争CPU。*建议:① 采样间隔不要小于200ms;② WebServer.handleClient()在loop里每帧都要调用,不能少;③ TFT_eSPI的SPI频率设置不要超过屏幕规格

Q2:网页数据不更新,一直显示0?

A:① 检查手机是否连接到了INA238_Monitor热点; ② 浏览器输入http://192.168.4.1(注意是http不是https);③ 如果用了Safari或Chrome有时会缓存,尝试强制刷新;④ ESP32堆内存不足时服务器可能挂掉,串口查看HeapFree数值,建议保持>50KB

项目资源整合

INA238数据手册: INA238 datasheet

INA238库文件: RobTillaart/INA238

TFT_eSPI: Bodmer/TFT_eSPI

相关推荐
水利行业RTU手艺人3 小时前
STM32 野外 RTU 固件升级配置丢失错乱终极解法:告别结构体字节偏移与 FATFS 掉电损坏的 KV 键值对实战
stm32·单片机·嵌入式硬件
d111111111d3 小时前
MQTT+STM32+云平台+AT命令的编写
服务器·笔记·stm32·单片机·嵌入式硬件·算法
LCG元4 小时前
STM32实战:基于STM32F103的触摸屏(TSC2046)驱动与校准
stm32·单片机·嵌入式硬件
国科安芯4 小时前
抗辐射 MCU 赋能商业航天电源系统:基于 AS32S601 的高可靠能量管理控制器设计与辐照验证
stm32·单片机·嵌入式硬件·mcu·risc-v·空间计算
阿昊真人4 小时前
AIR780E MINI开发日志
esp32
大志出奇迹5 小时前
传输协议为大端,STM32为小端,数据传输的字节序问题
c语言·stm32·单片机·mcu·算法·rtos
踏着七彩祥云的小丑6 小时前
嵌入式测试学习第 8 天:万用表使用:测电压、电阻、通断、二极管档
单片机·嵌入式硬件
济6177 小时前
FreeRTOS日志任务设计----LogTask 日志任务
单片机·嵌入式·freertos
振南的单片机世界7 小时前
PWM模拟电压:数字信号“平均”一下,就能变成模拟量
stm32·单片机·嵌入式硬件