✔零知开源(零知IDE)是一个专为电子初学者/电子兴趣爱好者设计的开源软硬件平台,在硬件上提供超高性价比STM32系列开发板、物联网控制板。取消了Bootloader程序烧录,让开发重心从 "配置环境" 转移到 "创意实现",极大降低了技术门槛。零知IDE编程软件,内置上千个覆盖多场景的示例代码,支持项目源码一键下载,项目文章在线浏览。零知开源(零知IDE)平台通过软硬件协同创新,让你的创意快速转化为实物,来动手试试吧!
✔访问零知实验室,获取更多实战项目和教程资源吧!
目录
[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 视频演示)
[5.1 寄存器配置](#5.1 寄存器配置)
[5.2 软件I2C总线通信](#5.2 软件I2C总线通信)
[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驱动)