零知IDE——零知标准板+INA219电流传感器的锂电池智能充放电监测系统

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

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

www.lingzhilab.com

目录

一、系统接线部分

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

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

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

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

二、安装与使用部分

三、核心代码解析

[3.1 数据采集与滤波](#3.1 数据采集与滤波)

[3.2 三通道波形显示](#3.2 三通道波形显示)

[3.4 实时数据显示](#3.4 实时数据显示)

[3.3 充放电模式判断](#3.3 充放电模式判断)

[3.5 主程序接口](#3.5 主程序接口)

四、项目结果演示

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

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

五、INA219电流传感器知识点讲解

[5.1 寄存器配置](#5.1 寄存器配置)

[5.2 软件I2C总线通信](#5.2 软件I2C总线通信)

六、常见问题解答(FAQ)

[Q1: INA219测量电流始终为0?](#Q1: INA219测量电流始终为0?)

[Q2: 低电压保护过早触发?](#Q2: 低电压保护过早触发?)

[Q3: 风扇PWM控制不响应?](#Q3: 风扇PWM控制不响应?)


项目概述

本项目基于零知标准板(STM32F103RBT6)INA219高精度电流传感器,实现了一个功能完善的锂电池智能监测系统。系统通过INA219实时采集电池的电压、电流数据,经过STM32处理后在ST7789彩色TFT屏上显示实时波形和数据,同时支持按键控制风扇转速

项目难点及解决方案

问题描述:电压、电流、功率量程差异大,难以在同一坐标系中清晰显示

**解决方案:**采用三分区独立映射,实现了多参数同屏对比显示

一、系统接线部分

1.1 硬件清单

模块 型号/参数 数量 备注
主控板 零知标准板(STM32F103RBT6) 1 核心控制器
电流传感器 INA219模块 1 双向电流检测
显示屏 ST7789 TFT LCD(240×320) 1 SPI接口
风扇 5V直流风扇 1 PWM调速
电池+电池座 18650锂电池(3.7V) 1 监测对象
按键 四位独立开关模块 1 功能控制
连接线 杜邦线 若干 硬件连接

1.2 接线方案表

根据config.h文件定义的引脚,接线如下:

模块 引脚功能 零知标准板引脚 代码定义 说明
显示屏 片选CS 10 TFT_CS SPI片选
显示屏 数据/命令DC 2 TFT_DC 数据命令选择
显示屏 复位RST 4 TFT_RST 硬件复位
显示屏 SDA 11 - SPI数据输出
显示屏 SCL 13 - SPI时钟
INA219 SCL时钟 A5 SCL 软件I2C时钟
INA219 SDA数据 A4 SDA 软件I2C数据
风扇控制 正转PWM 9 FAN_IA_PIN Timer4 CH2
风扇控制 反转PWM 5 FAN_IB_PIN Timer4 CH1
按键 风扇开关 33 BTN_FAN_TOGGLE_PIN 内部上拉
按键 加速按键 35 BTN_SPEED_UP_PIN 内部上拉
按键 减速按键 37 BTN_SPEED_DOWN_PIN 内部上拉

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

充电回路

INA219引脚 连接目标 说明
VIN+ 充电板输出 B+ 高侧电压检测点
VIN- 电池正极 低侧电压检测点

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

放电回路

| INA219引脚 | 连接目标 | 说明 |
| VIN+ | 负载(风扇)正极 | 高侧电压检测点 |

VIN- 电池正极 低侧电压检测点

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

1.3 具体接线图

**请注意:**INA219的VCC(逻辑供电)与VIN+(被测电压)是分开的,电池负极、负载负极、充电器负极全部共地 (GND)

1.4 接线实物图

建议:INA219模块和风扇建议单独供电,避免大电流干扰传感器测量

二、安装与使用部分

2.1 开源平台-输入"INA219电流传感器"并搜索-代码下载自动打开

2.2 连接-验证-上传

2.3 调试-串口监视器

三、核心代码解析

本项目采用模块化设计,将电池监测、风扇控制、按键处理封装成独立类,主程序只负责调度

3.1 数据采集与滤波

cpp 复制代码
void BatteryMonitor::updateSensorData() {
  // 读取原始数据并校准偏移
  float rawCurrent = ina219->getCurrent_mA() - 0.8;  // 校准偏移
  
  // 滑动平均滤波处理
  filteredCurrent = filterCurrentValue(rawCurrent);
  
  // 更新传感器数据
  currentData.shuntVoltage = ina219->getShuntVoltage_mV();
  currentData.voltage = ina219->getBusVoltage_V();
  currentData.power = ina219->getPower_mW();
  currentData.current = abs(filteredCurrent);
  currentData.batteryPercent = calculateBatteryPercent(currentData.voltage);
}

float BatteryMonitor::filterCurrentValue(float newValue) {
  currentBuffer[filterIndex] = newValue;
  filterIndex = (filterIndex + 1) % FILTER_WINDOW_SIZE;
  
  float sum = 0;
  for (int i = 0; i < FILTER_WINDOW_SIZE; i++) {
    sum += currentBuffer[i];
  }
  return sum / FILTER_WINDOW_SIZE;
}

使用filterCurrentValue()函数实现滑动平均滤波,有效抑制噪声

Adafruit_INA219库函数说明

函数 返回值 说明
getCurrent_mA() float 电流(mA),已经过内部校准
getBusVoltage_V() float 总线电压(V)
getShuntVoltage_mV() float 分流电阻压降(mV)
getPower_mW() float 功率(mW),=电压×电流

3.2 三通道波形显示

cpp 复制代码
void drawWaveforms() {
  // 分区显示:三个40像素区域对应不同参数
  const int zoneHeight = GRAPH_HEIGHT / 3;  // 40像素
  
  for (int i = 1; i < HISTORY_SIZE; i++) {
    // 电压映射:0-4.2V → 0-40像素
    float v1 = constrain(voltageHistory[prevIndex], 0, 4.2);
    int y1_v = GRAPH_Y + zoneHeight - (int)(v1 / 4.2 * zoneHeight);
    
    // 电流映射:0-500mA → 0-40像素  
    float i1 = constrain(currentHistory[prevIndex], 0, 500);
    int y1_i = GRAPH_Y + zoneHeight + zoneHeight - (int)(i1 / 500.0 * zoneHeight);
    
    // 功率映射:0-2000mW → 0-40像素
    float p1 = constrain(powerHistory[prevIndex], 0, 2000);
    int y1_p = GRAPH_Y + GRAPH_HEIGHT - (int)(p1 / 2000.0 * zoneHeight);
    
    // 绘制连线
    tft.drawLine(x1, y1_v, x2, y2_v, COLOR_VOLTAGE);
    tft.drawLine(x1, y1_i, x2, y2_i, COLOR_CURRENT);
    tft.drawLine(x1, y1_p, x2, y2_p, COLOR_POWER);
  }
}

分区显示电压、电流和功率曲线,不同曲线量程分隔

3.4 实时数据显示

cpp 复制代码
void updatePanelValues() {
  BatteryData data = Battery.getData(); 
  
  tft.setTextSize(1);
  
  // 电压
  tft.fillRect(PANEL_X + 15, PANEL_Y + 65, 43, 12, COLOR_PANEL_BG); 
  tft.setTextColor(COLOR_VOLTAGE);
  tft.setCursor(PANEL_X + 15, PANEL_Y + 70);
  tft.print(data.voltage, 2);
  
  // 电流(实时显示优化)
  tft.fillRect(PANEL_X + 15, PANEL_Y + 100, 43, 12, COLOR_PANEL_BG);
  tft.setTextColor(COLOR_CURRENT);
  tft.setCursor(PANEL_X + 15, PANEL_Y + 105);
  tft.print(data.current, 1);
  
  // 功率
  tft.fillRect(PANEL_X + 15, PANEL_Y + 135, 43, 12, COLOR_PANEL_BG);
  tft.setTextColor(COLOR_POWER);
  tft.setCursor(PANEL_X + 15, PANEL_Y + 140);
  tft.print(data.power, 1);
  
  // 电量百分比
  tft.fillRect(PANEL_X + 30, PANEL_Y + 160, 45, 12, COLOR_PANEL_BG);
  tft.setTextColor(COLOR_TEXT);
  tft.setCursor(PANEL_X + 30, PANEL_Y + 165);
  tft.print((int)data.batteryPercent);
  tft.print("%");
  
  // 时间显示(新增)- 显示充放电时间
  tft.fillRect(PANEL_X + 8, PANEL_Y + 190, 70, 15, COLOR_PANEL_BG);
  tft.setCursor(PANEL_X + 8, PANEL_Y + 195);
  char timeStr[10];
  formatTime(data.runtime, timeStr);
  
  if (data.mode == MODE_CHARGING) {
    tft.setTextColor(COLOR_CHARGE);
    tft.print("C:");
  } else if (data.mode == MODE_DISCHARGING) {
    tft.setTextColor(COLOR_DISCHARGE);
    tft.print("D:");
  } else {
    tft.setTextColor(COLOR_STANDBY);
    tft.print("S:");
  }
  tft.print(timeStr);
  
  // 风扇状态
  tft.fillRect(PANEL_X + 30, PANEL_Y + 215, 45, 15, COLOR_PANEL_BG);
  tft.setTextColor(COLOR_TEXT);
  tft.setCursor(PANEL_X + 30, PANEL_Y + 220);
  if (Fan.isRunning()) {
    tft.print(Fan.getSpeedPercent());
    tft.print("%");
  } else {
    tft.print("OFF");
  }
}

电压、电流、功率每250ms更新一次,充电(C)、放电(D)和待机(S)状态用不同颜色标识

3.3 充放电模式判断

cpp 复制代码
/**
 * 函数功能:根据电流方向自动识别充电/放电/待机模式
 * 判断逻辑:
 *   - 电流 > +15mA  → 充电
 *   - 电流 < -15mA  → 放电
 *   - -15mA ~ +15mA → 待机
 */
void BatteryMonitor::updateMode() {
  BatteryMode oldMode = currentMode;
  
  // 【核心判断】基于阈值的模式识别
  if (filteredCurrent > CURRENT_THRESHOLD) {
    currentMode = MODE_CHARGING;      // 正电流 = 充电
  } else if (filteredCurrent < -CURRENT_THRESHOLD) {
    currentMode = MODE_DISCHARGING;   // 负电流 = 放电
  } else {
    currentMode = MODE_STANDBY;       // 小电流 = 待机
  }
  
  currentData.mode = currentMode;
  
  // 【模式切换处理】
  if (currentMode != oldMode) {
    modeStartTime = millis();    // 重置计时起点
    totalCapacity_mAh = 0;       // 清零容量
    smoothedTimeEst = -1;        // 重置时间预估
    
    DEBUG_PRINT("模式切换: ");
    DEBUG_PRINTLN(currentMode == MODE_CHARGING ? "充电" : 
                  currentMode == MODE_DISCHARGING ? "放电" : "待机");
  }
  
  // 计算当前模式运行时长(秒)
  currentData.runtime = (millis() - modeStartTime) / 1000;
}

根据CURRENT_THRESHOLD阈值判断正电流>15mA为充电,负电流<-15mA为放电,否则为待机

3.5 主程序接口

cpp 复制代码
/**************************************************************************************
 * 文件: Battery_Monitor_STM32.ino
 * 作者:零知实验室(深圳市在芯间科技有限公司)
 * -^^- 零知实验室,让电子制作变得更简单! -^^-
 * 时间: 2026-1-15
 * 说明: 零知标准板(STM32F103RBT6主控) 监测锂电池充放电系统
 * 功能: INA219(SoftWire) + ST7789显示 + 按键控制风扇
 ***************************************************************************************/

#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>
#include "config.h"
#include "BatteryMonitor.h"
#include "FanController.h"
#include "ButtonController.h"

// ==================== 全局显示变量与对象 ====================
Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST);

float voltageHistory[HISTORY_SIZE];
float currentHistory[HISTORY_SIZE];
float powerHistory[HISTORY_SIZE];
int historyIndex = 0;
unsigned long lastDrawTime = 0;

// ==================== 函数声明 ====================
void initDisplay();
void updateUI();
void showSplashScreen();
void drawStaticUI();
void drawAxes();
void drawLegend();
void drawPanelStaticText();
void drawStatusIndicator(BatteryMode mode);
void drawWaveforms();
void updatePanelValues();
void updateHistoryData();
float getHistoryMax(float* data, float minRange);
void showLowVoltageWarning();
void formatTime(unsigned long seconds, char* buffer);

// ==================== 串口打印控制 ====================
unsigned long lastSerialPrint = 0;
const unsigned long SERIAL_PRINT_INTERVAL = 3000; // 降低到3秒打印一次

// ==================== 主设置函数 ====================
void setup() {
  Serial.begin(DEBUG_BAUD_RATE);
  delay(500);
  
  Serial.println("\n=== 系统启动中 ===");

  initDisplay();
  
  if (!Battery.begin()) {
    Serial.println("❌ INA219初始化失败!");
    while(1) { delay(100); }
  }
  
  Fan.begin();
  Buttons.begin();

  memset(voltageHistory, 0, sizeof(voltageHistory));
  memset(currentHistory, 0, sizeof(currentHistory));
  memset(powerHistory, 0, sizeof(powerHistory));

  Serial.println("✅ 系统初始化完成");
}

// ==================== 主循环函数 ====================
void loop() {
  Battery.update();
  Buttons.update();
  
  handleButtonEvents();
  
  // 提高UI刷新频率到250ms,使电流显示更实时
  updateUI();
  
  printBatteryDataToSerial();
  
  if (Battery.getSystemState() == STATE_LOW_VOLTAGE) {
    showLowVoltageWarning();
    Fan.stop();
    while(1);
  }
  
  delay(10);
}

// ==================== 显示功能实现 ====================

void initDisplay() {
  tft.init(240, 320);
  tft.invertDisplay(false);
  tft.setRotation(3);
  tft.fillScreen(COLOR_BACKGROUND);
  
  showSplashScreen();
  delay(2000);
  drawStaticUI();
}

void showSplashScreen() {
  tft.fillScreen(COLOR_BACKGROUND);
  tft.setTextColor(COLOR_VOLTAGE);
  tft.setTextSize(3);
  tft.setCursor(70, 80);
  tft.print("BATTERY");
  tft.setCursor(85, 120);
  tft.print("MONITOR");
  
  tft.setTextColor(COLOR_POWER);
  tft.setTextSize(1);
  tft.setCursor(100, 180);
  tft.print("STM32F103RBT6");
  tft.setCursor(90, 200);
  tft.print("Initializing...");
}

void drawStaticUI() {
  tft.fillScreen(COLOR_BACKGROUND);
  tft.setTextColor(COLOR_TEXT);
  tft.setTextSize(2);
  tft.setCursor(50, 5);
  tft.print("POWER MONITOR");
  
  // 波形图区域
  tft.drawRect(GRAPH_X, GRAPH_Y, GRAPH_WIDTH, GRAPH_HEIGHT, COLOR_AXIS);
  drawAxes();
  
  // 右侧面板
  tft.fillRect(PANEL_X, PANEL_Y, PANEL_WIDTH, PANEL_HEIGHT, COLOR_PANEL_BG);
  tft.drawRect(PANEL_X, PANEL_Y, PANEL_WIDTH, PANEL_HEIGHT, COLOR_TEXT);
  
  tft.setTextSize(1);
  tft.setCursor(PANEL_X + 10, PANEL_Y + 10);
  tft.print("REALTIME");
  tft.setCursor(PANEL_X + 15, PANEL_Y + 20);
  tft.print("VALUES");
  tft.drawFastHLine(PANEL_X, PANEL_Y + 35, PANEL_WIDTH, COLOR_TEXT);
  
  drawLegend();
  drawPanelStaticText();

  // 坐标轴标签
  tft.setTextSize(1);
  tft.setTextColor(COLOR_TEXT);
  tft.setCursor(GRAPH_X - 5, GRAPH_Y - 15);
  tft.print("Value");
  tft.setCursor(GRAPH_X + GRAPH_WIDTH - 30, GRAPH_Y + GRAPH_HEIGHT + 5);
  tft.print("Time");
  
  Serial.println("[显示] 静态UI绘制完成");
}

void drawAxes() {
  // Y轴箭头
  tft.drawFastVLine(GRAPH_X, GRAPH_Y, GRAPH_HEIGHT, COLOR_AXIS);
  tft.drawLine(GRAPH_X, GRAPH_Y, GRAPH_X - 3, GRAPH_Y + 5, COLOR_AXIS);
  tft.drawLine(GRAPH_X, GRAPH_Y, GRAPH_X + 3, GRAPH_Y + 5, COLOR_AXIS);
  
  // X轴箭头
  tft.drawFastHLine(GRAPH_X, GRAPH_Y + GRAPH_HEIGHT, GRAPH_WIDTH, COLOR_AXIS);
  tft.drawLine(GRAPH_X + GRAPH_WIDTH, GRAPH_Y + GRAPH_HEIGHT, 
                GRAPH_X + GRAPH_WIDTH - 5, GRAPH_Y + GRAPH_HEIGHT - 3, COLOR_AXIS);
  tft.drawLine(GRAPH_X + GRAPH_WIDTH, GRAPH_Y + GRAPH_HEIGHT, 
                GRAPH_X + GRAPH_WIDTH - 5, GRAPH_Y + GRAPH_HEIGHT + 3, COLOR_AXIS);
                
  // 网格线 - 分三个区域
  for (int i = 1; i <= 2; i++) {
    int yPos = GRAPH_Y + i * GRAPH_HEIGHT / 3;
    for (int x = GRAPH_X; x < GRAPH_X + GRAPH_WIDTH; x += 4) {
      tft.drawPixel(x, yPos, 0x4208); // 深灰色虚线
    }
  }
}

void drawPanelStaticText() {
  tft.setTextColor(COLOR_TEXT);
  tft.setTextSize(2);
  
  // 电压
  tft.setCursor(PANEL_X + 10, PANEL_Y + 50);
  tft.print("V:");
  tft.setTextSize(1);
  tft.setCursor(PANEL_X + 60, PANEL_Y + 70);
  tft.print("V");
  
  // 电流
  tft.setTextSize(2);
  tft.setCursor(PANEL_X + 10, PANEL_Y + 85);
  tft.print("I:");
  tft.setTextSize(1);
  tft.setCursor(PANEL_X + 60, PANEL_Y + 105);
  tft.print("mA");
  
  // 功率
  tft.setTextSize(2);
  tft.setCursor(PANEL_X + 10, PANEL_Y + 120);
  tft.print("P:");
  tft.setTextSize(1);
  tft.setCursor(PANEL_X + 60, PANEL_Y + 140);
  tft.print("mW");
  
  // 电量
  tft.setCursor(PANEL_X + 8, PANEL_Y + 155);
  tft.print("BAT:");
  
  // 时间显示(新增)
  tft.setCursor(PANEL_X + 8, PANEL_Y + 180);
  tft.print("TIME:");
  
  // 风扇
  tft.setCursor(PANEL_X + 8, PANEL_Y + 210);
  tft.print("FAN:");
}

void drawLegend() {
  int legendY = GRAPH_Y + GRAPH_HEIGHT + 25;
  
  tft.fillRect(20, legendY, 15, 3, COLOR_VOLTAGE);
  tft.setTextColor(COLOR_VOLTAGE);
  tft.setTextSize(1);
  tft.setCursor(40, legendY - 3);
  tft.print("V");
  
  tft.fillRect(70, legendY, 15, 3, COLOR_CURRENT);
  tft.setTextColor(COLOR_CURRENT);
  tft.setCursor(90, legendY - 3);
  tft.print("I");
  
  tft.fillRect(120, legendY, 15, 3, COLOR_POWER);
  tft.setTextColor(COLOR_POWER);
  tft.setCursor(140, legendY - 3);
  tft.print("P");
}

void updateUI() {
  unsigned long currentTime = millis();
  // 提高刷新频率到250ms
  if (currentTime - lastDrawTime >= 250) {
    updateHistoryData();
    drawWaveforms();
    updatePanelValues();
    drawStatusIndicator(Battery.getCurrentMode());
    lastDrawTime = currentTime;
  }
}

void updateHistoryData() {
  BatteryData data = Battery.getData();
  voltageHistory[historyIndex] = data.voltage;
  currentHistory[historyIndex] = data.current;
  powerHistory[historyIndex] = data.power;
  historyIndex = (historyIndex + 1) % HISTORY_SIZE;
}

void drawWaveforms() {
  // 清除波形区域
  tft.fillRect(GRAPH_X + 1, GRAPH_Y + 1, GRAPH_WIDTH - 1, GRAPH_HEIGHT - 1, COLOR_BACKGROUND);
  drawAxes();
  
  // 分区显示优化:将图表分为三个区域
  // 上区 (0-40像素): 电压 2.9V-4.2V (优化范围,变化更明显)
  // 中区 (40-80像素): 电流 0-500mA
  // 下区 (80-120像素): 功率 0-2000mW
  
  const int zoneHeight = GRAPH_HEIGHT / 3;
  
  // 【关键修改】电压映射范围
  const float V_MIN = 2.9;  // 最低电压(低压保护阈值)
  const float V_MAX = 4.2;  // 最高电压(满电)
  const float V_RANGE = V_MAX - V_MIN;  // 有效范围 = 1.3V
  
  for (int i = 1; i < HISTORY_SIZE; i++) {
    int prevIndex = (historyIndex + i - 1) % HISTORY_SIZE;
    int currIndex = (historyIndex + i) % HISTORY_SIZE;
    int x1 = GRAPH_X + (i - 1) * 2;
    int x2 = GRAPH_X + i * 2;
    if (x2 > GRAPH_X + GRAPH_WIDTH) break;

    // 【优化后】电压曲线 - 上区 (2.9V-4.2V映射到整个区域)
    // 映射公式:Y = 顶部 + 区域高度 - ((电压-最小值) / 范围 × 区域高度)
    float v1 = constrain(voltageHistory[prevIndex], V_MIN, V_MAX);
    float v2 = constrain(voltageHistory[currIndex], V_MIN, V_MAX);
    
    int y1_v = GRAPH_Y + zoneHeight - (int)((v1 - V_MIN) / V_RANGE * zoneHeight);
    int y2_v = GRAPH_Y + zoneHeight - (int)((v2 - V_MIN) / V_RANGE * zoneHeight);
    tft.drawLine(x1, y1_v, x2, y2_v, COLOR_VOLTAGE);
    
    // 电流曲线 - 中区 (0-500mA映射到40-80像素)
    float i1 = constrain(currentHistory[prevIndex], 0, 500);
    float i2 = constrain(currentHistory[currIndex], 0, 500);
    int y1_i = GRAPH_Y + zoneHeight + zoneHeight - (int)(i1 / 500.0 * zoneHeight);
    int y2_i = GRAPH_Y + zoneHeight + zoneHeight - (int)(i2 / 500.0 * zoneHeight);
    tft.drawLine(x1, y1_i, x2, y2_i, COLOR_CURRENT);
    
    // 功率曲线 - 下区 (0-2000mW映射到80-120像素)
    float p1 = constrain(powerHistory[prevIndex], 0, 2000);
    float p2 = constrain(powerHistory[currIndex], 0, 2000);
    int y1_p = GRAPH_Y + GRAPH_HEIGHT - (int)(p1 / 2000.0 * zoneHeight);
    int y2_p = GRAPH_Y + GRAPH_HEIGHT - (int)(p2 / 2000.0 * zoneHeight);
    tft.drawLine(x1, y1_p, x2, y2_p, COLOR_POWER);
  }

  // 显示刻度值(修改电压刻度)
  tft.setTextColor(0x7BEF);
  tft.setTextSize(1);
  
  // 电压刻度 - 显示最大值和最小值
  tft.setCursor(GRAPH_X + 2, GRAPH_Y + 2);
  tft.print("4.2V");  // 顶部刻度
  
  // 电流刻度
  tft.setCursor(GRAPH_X + 2, GRAPH_Y + zoneHeight + 2);
  tft.print("500mA");
  
  // 功率刻度
  tft.setCursor(GRAPH_X + 2, GRAPH_Y + 2*zoneHeight + 2);
  tft.print("2000mW");
}

void updatePanelValues() {
  BatteryData data = Battery.getData(); 
  
  tft.setTextSize(1);
  
  // 电压
  tft.fillRect(PANEL_X + 15, PANEL_Y + 65, 43, 12, COLOR_PANEL_BG); 
  tft.setTextColor(COLOR_VOLTAGE);
  tft.setCursor(PANEL_X + 15, PANEL_Y + 70);
  tft.print(data.voltage, 2);
  
  // 电流(实时显示优化)
  tft.fillRect(PANEL_X + 15, PANEL_Y + 100, 43, 12, COLOR_PANEL_BG);
  tft.setTextColor(COLOR_CURRENT);
  tft.setCursor(PANEL_X + 15, PANEL_Y + 105);
  tft.print(data.current, 1);
  
  // 功率
  tft.fillRect(PANEL_X + 15, PANEL_Y + 135, 43, 12, COLOR_PANEL_BG);
  tft.setTextColor(COLOR_POWER);
  tft.setCursor(PANEL_X + 15, PANEL_Y + 140);
  tft.print(data.power, 1);
  
  // 电量百分比
  tft.fillRect(PANEL_X + 30, PANEL_Y + 160, 45, 12, COLOR_PANEL_BG);
  tft.setTextColor(COLOR_TEXT);
  tft.setCursor(PANEL_X + 30, PANEL_Y + 165);
  tft.print((int)data.batteryPercent);
  tft.print("%");
  
  // 时间显示(新增)- 显示充放电时间
  tft.fillRect(PANEL_X + 8, PANEL_Y + 190, 70, 15, COLOR_PANEL_BG);
  tft.setCursor(PANEL_X + 8, PANEL_Y + 195);
  char timeStr[10];
  formatTime(data.runtime, timeStr);
  
  if (data.mode == MODE_CHARGING) {
    tft.setTextColor(COLOR_CHARGE);
    tft.print("C:");
  } else if (data.mode == MODE_DISCHARGING) {
    tft.setTextColor(COLOR_DISCHARGE);
    tft.print("D:");
  } else {
    tft.setTextColor(COLOR_STANDBY);
    tft.print("S:");
  }
  tft.print(timeStr);
  
  // 风扇状态
  tft.fillRect(PANEL_X + 30, PANEL_Y + 215, 45, 15, COLOR_PANEL_BG);
  tft.setTextColor(COLOR_TEXT);
  tft.setCursor(PANEL_X + 30, PANEL_Y + 220);
  if (Fan.isRunning()) {
    tft.print(Fan.getSpeedPercent());
    tft.print("%");
  } else {
    tft.print("OFF");
  }
}

void drawStatusIndicator(BatteryMode mode) {
  int x = 220, y = 10, r = 8;
  uint16_t color = (mode == MODE_CHARGING) ? COLOR_CHARGE : 
                   (mode == MODE_DISCHARGING ? COLOR_DISCHARGE : COLOR_STANDBY);
  tft.fillCircle(x, y, r, color);
  tft.drawCircle(x, y, r + 1, COLOR_TEXT);
}

float getHistoryMax(float* data, float minRange) {
  float maxVal = minRange;
  for (int i = 0; i < HISTORY_SIZE; i++) 
    if (data[i] > maxVal) maxVal = data[i];
  return maxVal * 1.15;
}

void showLowVoltageWarning() {
  tft.fillScreen(COLOR_BACKGROUND);
  tft.setTextColor(COLOR_WARNING);
  tft.setTextSize(3);
  tft.setCursor(40, 60); 
  tft.print("WARNING!");
  tft.setCursor(50, 110); 
  tft.setTextSize(2); 
  tft.print("LOW VOLTAGE");
}

// 时间格式化函数:将秒数转换为 HH:MM:SS 或 MM:SS
void formatTime(unsigned long seconds, char* buffer) {
  unsigned long hours = seconds / 3600;
  unsigned long minutes = (seconds % 3600) / 60;
  unsigned long secs = seconds % 60;
  
  if (hours > 0) {
    sprintf(buffer, "%02lu:%02lu", hours, minutes);
  } else {
    sprintf(buffer, "%02lu:%02lu", minutes, secs);
  }
}

// ==================== 其他逻辑函数 ====================

void handleButtonEvents() {
  if (Buttons.isFanTogglePressed()) Fan.toggle();
  if (Buttons.isSpeedUpPressed()) Fan.increaseSpeed();
  if (Buttons.isSpeedDownPressed()) Fan.decreaseSpeed();
}

void printBatteryDataToSerial() {
  unsigned long currentTime = millis();
  // 降低打印频率到3秒
  if (currentTime - lastSerialPrint >= SERIAL_PRINT_INTERVAL) {
    BatteryData data = Battery.getData();
    
    Serial.println("========================================");
    Serial.print("⚙️  风扇: ");
    if (Fan.isRunning()) {
      Serial.print("运行 | ");
      Serial.print(Fan.getCurrentDirection() == FAN_FORWARD ? "正转" : "反转");
      Serial.print(" | ");
      Serial.print(Fan.getSpeedPercent());
      Serial.println("%");
    } else {
      Serial.println("停止");
    }
    
    Serial.println("\n🔋 电池数据:");
    Serial.print("   电压: ");
    Serial.print(data.voltage, 2);
    Serial.print(" V  |  电流: ");
    Serial.print(data.current, 1);
    Serial.print(" mA  |  功率: ");
    Serial.print(data.power, 1);
    Serial.println(" mW");
    
    Serial.print("   电量: ");
    Serial.print(data.batteryPercent, 1);
    Serial.print(" %  |  容量: ");
    Serial.print(data.capacity, 1);
    Serial.println(" mAh");
    
    Serial.print("   模式: ");
    switch(data.mode) {
      case MODE_CHARGING:
        Serial.print("充电中⚡");
        break;
      case MODE_DISCHARGING:
        Serial.print("放电中🔋");
        break;
      default:
        Serial.print("待机⏸");
    }
    
    // 显示运行时间
    char timeStr[10];
    formatTime(data.runtime, timeStr);
    Serial.print("  |  时长: ");
    Serial.println(timeStr);
    
    Serial.println("========================================\n");
    
    lastSerialPrint = currentTime;
  }
}

/******************************************************************************
 * 深圳市在芯间科技有限公司
 * 淘宝店铺:在芯间科技零知板
 * 店铺网址:https://shop533070398.taobao.com
 * 版权说明:
 *  1.本代码的版权归【深圳市在芯间科技有限公司】所有,仅限个人非商业性学习使用。
 *  2.严禁将本代码或其衍生版本用于任何商业用途。
 *  3.任何商业用途均需事先获得【深圳市在芯间科技有限公司】的书面授权。
 ******************************************************************************/

系统流程图

容量积分计算

// 安时积分:电流 × 时间

totalCapacity_mAh += abs(filteredCurrent) * deltaTime_h;

计算公式

充放电分别累计,每次模式切换清零重新计算

四、项目结果演示

按接线表完成零知标准板、INA219、锂电池、TP4056充电板的接线,确认正负极无误

4.1 操作流程

①系统启动

上电后显示"BATTERY MONITOR"启动界面、自动连接INA219传感器、串口打印待机模式信息

②功能验证

三通道波形实时显示、按键控制风扇启停并调节风扇PWM占空比,观察转速变化和电池数据改变

③测试场景

充电测试:连接充电器观察充电状态

放电测试:连接负载观察放电过程、低压保护:放电模式下电压<2.9V,立即停止所有操作

模式切换后自动切换并计算当前模式下的运行时间

4.2 视频演示

零知标准板+INA219锂电池监测系统全功能演示

视频完整展示了基于STM32F103RBT6和INA219电流传感器的锂电池充放电监测系统。视频内容包括系统启动过程、实时波形显示、充放电状态切换、风扇PWM调速控制、低电压保护触发等核心功能。通过实际充放电测试,直观展示了系统的监测精度和响应速度

五、INA219电流传感器知识点讲解

INA219是一款基于I²C接口的零漂移、双向电流/功率监测传感器。其核心工作原理如下:

根据芯片手册,经过分流电阻N(采样电阻)后,能够采集到的最低有效电压LSB为10uV。

利用欧姆定律计算电流公式:

采用高侧电流检测技术,将微小阻值的分流电阻(通常0.1Ω)串联在电源正极和负载之间。当电流流过时,根据欧姆定律(V = I × R),分流电阻两端产生微小压降,这个压降与电流成正比

功率计算原理

INA219内部集成了硬件乘法器,实时计算功率:

5.1 寄存器配置

INA219内部有16个寄存器,本项目使用的主要寄存器:

地址 寄存器名称 功能 本项目配置
0x00 Configuration 配置寄存器 0x399F
0x01 Shunt Voltage 分流电压 只读
0x02 Bus Voltage 总线电压 只读
0x03 Power 功率 只读
0x04 Current 电流 只读
0x05 Calibration 校准寄存器 0x1000

校准寄存器(0x05)

校准值计算公式:

其中电流LSB = 最大预期电流 / 32768

Current_LSB=10010^-6=100uA=0.0001A

计算基准值:Cal=0.04096/(Current_LSB/R)=0.04096/(0.0001A0.1R)=4096=0x1000

校准寄存器与缩放

如果发现测量到的电流值有误,用电流表测到的实际值为0.290A,INA219测量结果为0.342A

采用Cal的校准公式(缩放校准后的)Cal=4096*0.290/0.3421 = 3472 = 0x0D90

5.2 软件I2C总线通信

INA219 采用标准 I2C 通信协议,SDA(数据)和 SCL(时钟)为双向引脚

1)串行总线地址

INA219 有两个地址引脚A0 和 A1都设置为GND,该从机地址为0x40

从机地址可通过模块上的 A0和A1 引脚短接修改(只短接A0 从机地址为0x41,只短接A1 从机地址为0x44,短接A0和A1 从机地址0x45)

2)软件I2C时序

总线上的所有从机在 SCL 的上升沿移入从机地址字节,其中最后一位指示要进行的是读操作还是写操作。在第九个时钟脉冲期间,被寻址的从机通过生成确认信号并将 SDA 拉至低电平来响应主机

通信时序分析

六、常见问题解答(FAQ)

Q1: INA219测量电流始终为0?

*A:采取解决方案:*验证setCalibration_32V_2A()调用成功,确保总线电压在0-26V范围内,使用I2C扫描工具确认设备地址

Q2: 低电压保护过早触发?

*A:校准方法:*测量电池实际电压与INA219读数差异、在代码中调整BATTERY_MIN_VOLTAGE阈值、考虑电池内阻导致的压降、添加电压补偿算法

Q3: 风扇PWM控制不响应?

*A:调试步骤:*检查Timer4初始化是否成功、验证PWM引脚映射、测量PWM输出波形、确认风扇工作电压匹配

项目资源整合

INA219数据手册: INA219 DataSheet ​

INA219 库: Adafruit_INA219-master(软件I2C驱动)

相关推荐
Q_219327645511 小时前
基于单片机的无线通信报警系统
单片机·嵌入式硬件
布丁oo11 小时前
ESP32智能天气时钟:温湿度气压全掌控
单片机·嵌入式硬件
蝎蟹居12 小时前
GBT 4706.1-2024逐句解读系列(26) 第7.6条款:正确使用符号标识
人工智能·单片机·嵌入式硬件·物联网·安全
F1331689295713 小时前
5G矿山车载监控终端山河矿卡定位监控终端
stm32·单片机·嵌入式硬件·5g·51单片机·硬件工程
vsropy14 小时前
keil5无法注释中文
stm32·单片机
csdn_te_download_00414 小时前
Keil5安装教程 基于C51 安装教程与配置完全指南
stm32·单片机·嵌入式硬件
ベadvance courageouslyミ14 小时前
51单片机相关
单片机·51单片机·定时器·pwm·蜂鸣器·中断·独立按键
送外卖的工程师15 小时前
STM32F103 驱动 BMP280 气压温湿度传感器 + OLED 显示教程
stm32·单片机·嵌入式硬件·mcu·物联网·proteus·rtdbs
jerwey15 小时前
VSCode + Copilot下:配置并使用 DeepSeek
ide·vscode·copilot