太阳能作为清洁能源的代表,正在越来越多地进入我们的日常生活------太阳能充电的效率受光照强度、温度、负载变化等多种因素影响,普通用户很难直观地了解"当前到底发了多少电"、"充电状态是否正常"。
目录
[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.3 中断驱动设计](#3.3 中断驱动设计)
[3.4 滑动窗口均值滤波](#3.4 滑动窗口均值滤波)
[3.5 图标旋转帧动画](#3.5 图标旋转帧动画)
[4.1 操作流程](#4.1 操作流程)
[4.2 视频演示](#4.2 视频演示)
[5.1 INA219电流功率监测](#5.1 INA219电流功率监测)
[5.2 TFT显示原理](#5.2 TFT显示原理)
[5.3 太阳能光伏供电](#5.3 太阳能光伏供电)
[Q1:上电后屏幕显示INA219 ERROR?](#Q1:上电后屏幕显示INA219 ERROR?)
项目概述
本文基于零知派标准板(主控STM32F103RBT6) ,结合INA219高精度电流/功率监测芯片和ST7789 240×240 TFT彩色显示屏 ,设计了一款太阳能充电功率实时监测系统 。系统能够实时采集太阳能板的输出电压、电流和功率,并以大字卡片+彩色进度条+动态太阳图标的直观方式呈现在屏幕
项目难点
问题描述:TFT 每次更新数值时若清除区域不精准,会留下上一帧的字符残影,特别是数字位数变化
解决方案: 所有数值使用 snprintf 固定宽度格式化,字符串比较缓存避免未变化时重发 SPI,fillRect 清除区域与文字区域严格对齐
一、系统接线部分
1.1 硬件清单
| 序号 | 硬件名称 | 型号/规格 | 数量 | 说明 |
|---|---|---|---|---|
| 1 | 零知派标准板 | STM32F103RBT6 | 1 | 主控核心 |
| 2 | TFT彩屏 | ST7789 240×240 | 1 | SPI接口彩色显示屏 |
| 3 | 电流功率传感器 | INA219 | 1 | I²C接口高侧监测 |
| 4 | 太阳能充电板 | 5V/2A(或类似) | 1 | 被测电源 |
| 5 | 直流电机/负载 | 5V直流电机 | 1 | 典型负载 |
| 6 | 按键模块 | 轻触开关×2 | 2 | 带内部上拉 |
| 7 | 杜邦线 | 公对公/公对母 | 若干 | 连接线材 |
1.2 接线方案表
以下接线必须严格按照 config.h中的引脚定义执行
①ST7789 接线
| ST7789引脚 | 零知派引脚 | 代码定义 | 注意 |
|---|---|---|---|
| GND | GND | --- | ST7789直插零知派标准板,无需单独接线 |
| VCC | 3.3V | --- | ST7789直插零知派标准板,无需单独接线 |
| SCL | D13 / PA5 | 硬件SPI1 SCK | ST7789直插零知派标准板,无需单独接线 |
| SDA | D11 / PA7 | 硬件SPI1 MOSI | ST7789直插零知派标准板,无需单独接线 |
| RES | D4 | TFT_RST 4 |
ST7789直插零知派标准板,无需单独接线 |
| DC | D2 | TFT_DC 2 |
ST7789直插零知派标准板,无需单独接线 |
| CS | D10 | TFT_CS 10 |
ST7789直插零知派标准板,无需单独接线 |
②INA219 接线
| INA219引脚 | 零知派引脚 | 代码定义 | 注意 |
|---|---|---|---|
| GND | GND | --- | --- |
| VCC | 3.3V | --- | --- |
| SDA | A4 | SDA A4 |
SoftWire软件I²C |
| SCL | A5 | SCL A5 |
SoftWire软件I²C |
| VIN+ | 太阳能板正极 | --- | 分流电阻正端 |
| VIN− | 负载正极 | --- | 分流电阻负端 |
③按键接线
| 按键 | 零知派引脚 | 功能 | 另一端 |
|---|---|---|---|
| BTN_NEXT | D0 | 短按切换数据页↔图片页 | GND |
| BTN_ACT | D1 | 长按芯片级睡眠/唤醒 | GND |
两个按键均使用INPUT_PULLUP模式,按下时引脚被拉低,无需外接上拉电阻电阻
1.3 具体接线图
**充电电路:**VIN+ 接太阳能板输出正极,VIN- 接负载正极,电流方向为太阳能板→分流电阻→负载,getCurrent_mA() 读值为正表示有光照在供电

D2在之前的项目中用作水墨屏显示功能,本项目改为ST7789的DC引脚,请注意区分
1.4 接线实物图

二、安装与使用部分
2.1 开源平台-输入"INA219太阳能充电板"并搜索-代码下载自动打开

2.2 连接-验证-上传

2.3 调试-串口监视器

三、代码讲解部分
项目代码结构
本项目代码分为四大模块:主程序(EPD_Solar_Monitor_v2.ino)、INA219驱动(BatteryMonitor.cpp/.h)、ST7789显示驱动与UI绘制(DisplayManager.cpp/.h)以及按键中断与消抖处理(ButtonManager)。以下仅讲解项目中最重要的五个部分
3.1 增量刷新架构
cpp
// 不画数据区,避免覆盖后续错误页的内容
void DisplayManager::drawTopBarOnly(bool online) {
_tft.fillScreen(cBg());
drawTopBar(online);
// 重置缓存,防止后续 drawDynamic 以为状态未变而跳过首次绘制
invalidateCache();
}
// ─── 缓存失效(强制下次 drawDynamic 全量重绘)
void DisplayManager::invalidateCache() {
_cacheV = _cacheI = _cacheP = _cacheCap = _cacheRt = "\x01";
_cachePBar = -1;
_cacheMode = 255;
_sunFrame = 0;
_connCacheValid = false;
}
void DisplayManager::drawDynamic(const SolarSnapshot &d) {
// 图片页时不刷数据
if (_currentPage != PAGE_DATA) return;
updateConnStatus(d.inaOnline);
String sv = fmtV(d.voltage);
String si = fmtI(d.current);
String sp = fmtP(d.power);
uint16_t colorI = (d.current > 0) ? cValue() : cLabel();
if (_cacheV != sv) { updateCard(0, sv, cValue()); _cacheV = sv; }
if (_cacheI != si) { updateCard(1, si, colorI); _cacheI = si; }
if (_cacheP != sp) { updateCard(2, sp, cAccent()); _cacheP = sp; }
int pct = (int)(d.power / POWER_PEAK_MW * 100.0f);
updatePowerBar(pct);
// updateMode 和 updateSunIcon 顺序:必须先Mode(Mode会更新_cacheMode),
// 再Sun(Sun读_cacheMode判断是否首次)
updateMode(d.mode);
updateSunIcon(d.mode);
updateCapacity(fmtCap(d.capacity));
updateRuntime(fmtRt(d.runtime));
}
PAGE_DATA/PAGE_IMAGE 页面枚举,图片页时 drawDynamic 直接 return,防止数据覆盖图片
3.2 单色位图渲染
imagedata文件时1bpp格式,2888 字节存于 PROGMEM,需要逐像素展开
cpp
void DisplayManager::drawMonoBitmap(int16_t x, int16_t y, int16_t w, int16_t h,
const unsigned char *img,
uint16_t fg, uint16_t bg) {
int bytesPerRow = (w + 7) / 8; // 每行字节数,向上取整
for (int16_t row = 0; row < h; row++) {
for (int16_t col = 0; col < w; col++) {
// 从PROGMEM读取对应字节,判断对应bit
uint8_t byte = pgm_read_byte(&img[row * bytesPerRow + col / 8]);
bool pixel = (byte >> (7 - (col % 8))) & 0x01; // 高位在左
_tft.drawPixel(x + col, y + row, pixel ? fg : bg);
}
}
}
// 图片152×152,居中显示在240×240中(内容区y=TOP_H~239,约205px高)
// 上下各留少量边距
void DisplayManager::drawImagePage() {
_tft.fillRect(0, TOP_H, 240, 240 - TOP_H, cBg());
// IMAGE_DATA 152×152 1bpp:fg=白,bg=黑,居中
int16_t ix = (240 - 152) / 2; // 水平居中: (240-152)/2=44
int16_t iy = TOP_H + (205 - 152) / 2; // 垂直居中
drawMonoBitmap(ix, iy, 152, 152, IMAGE_DATA, cValue(), cBg());
// 图片下方加一行小字标注
_tft.setTextSize(1); _tft.setTextColor(cLabel());
_tft.setCursor(80, 230); _tft.print("IMAGE_DATA");
_currentPage = PAGE_IMAGE;
}
- 逐像素渲染 152×152=23104 次 drawPixel
- 每次 drawPixel 需要发送多个 SPI 字节设置坐标+发数据
3.3 中断驱动设计
ISR 只写 volatile 变量,不调用 SPI/I2C 等耗时函数
cpp
// ButtonManager.h --- ISR只记录时间戳,主循环消费事件
void Button::onInterrupt() {
uint32_t now = millis();
bool pressed = (digitalRead(_pin) == LOW);
if (now - _lastEdge < BTN_DEBOUNCE_MS) { _lastEdge = now; return; } // 消抖
_lastEdge = now;
if (pressed) {
_pressStart = now;
_isPressed = true;
} else {
if (!_isPressed) return; // 无对应按下边沿,是噪声,丢弃
_isPressed = false;
uint32_t dur = now - _pressStart;
if (dur >= BTN_LONGPRESS_MS) _pendingLong = true;
else _pendingShort = true;
}
}
// loop 中原子消费(noInterrupts保护)------本项目直接用consumeShort/consumeLong
// ButtonManager.h 的 consumeShort/consumeLong 本身是原子操作(单字节volatile读写)
bool consumeShort() {
if (!_pendingShort) return false;
_pendingShort = false;
return true;
}
bool consumeLong() {
if (!_pendingLong) return false;
_pendingLong = false;
return true;
}
_isPressed 防噪声判断解决了开机时 _pressStart=0,噪声触发松开边沿会算出"开机到现在"的巨大时长被误判为超长按
3.4 滑动窗口均值滤波
cpp
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;
}
void BatteryMonitor::updateSensorData() {
// 读取分流电压(mV)
float shuntV_mV = ina219->getShuntVoltage_mV();
// 读取总线电压(VIN- 对 GND,单位 V)
float busV = ina219->getBusVoltage_V();
// [Fix3] 太阳能板真实输出电压 = 总线电压 + 分流电压
// V_bus 是电机正极对地,V_shunt 是分流电阻两端压降
float solarV = busV + (shuntV_mV / 1000.0f);
// 电流(原始值,含偏移校准)
float rawCurrent = ina219->getCurrent_mA() - 0.8f; // 0.8mA 零偏校准
filteredCurrent = filterCurrentValue(rawCurrent);
// 填充数据结构
currentData.shuntVoltage = shuntV_mV;
currentData.voltage = solarV; // [Fix3] 太阳能板电压
currentData.current = filteredCurrent; // 保留符号,正=供电
// [Fix3] 功率也用太阳能板电压计算(而非原来的 getPower_mW())
// getPower_mW() 内部用 V_bus × I,会低估真实功率
currentData.power = solarV * abs(filteredCurrent);
// 太阳能场景下不需要电量百分比(充电板无法用电压估算SOC)
// 保留字段,设为0
currentData.batteryPercent = 0.0f;
}
FILTER_WINDOW_SIZE采样5秒数据,适用太阳能缓慢变化信号
3.5 图标旋转帧动画
cpp
void DisplayManager::drawSunIcon(int cx, int cy, uint8_t frame, uint16_t color) {
// 清背景(用正方形清,比圆形快,且不会有圆形抗锯齿残留)
_tft.fillRect(cx - SUN_CLEAR, cy - SUN_CLEAR,
SUN_CLEAR * 2 + 1, SUN_CLEAR * 2 + 1, cBg());
// 圆心
_tft.fillCircle(cx, cy, SUN_INNER, color);
// 8根光线(均匀分布,每帧旋转5°)
for (int i = 0; i < 8; i++) {
float deg = (i * 45.0f + frame * 5.0f) * (float)PI / 180.0f;
float cs = cos(deg), sn = sin(deg);
_tft.drawLine(cx + (int)(SUN_R0 * cs), cy + (int)(SUN_R0 * sn),
cx + (int)(SUN_R1 * cs), cy + (int)(SUN_R1 * sn),
color);
}
}
void DisplayManager::updateSunIcon(BatteryMode mode) {
bool active = (mode == MODE_CHARGING);
uint16_t color = active ? cAccent() : cStandby();
// [F5] 修复:_cacheMode==255表示首次,任何模式都需要画;
// 非首次且不是CHARGING则只在模式刚切换时画一次(由updateMode的缓存判断保证)
if (!active && _cacheMode != 255 && _cacheMode == (uint8_t)mode) return;
if (active) _sunFrame = (_sunFrame + 1) % 72;
drawSunIcon(SUN_CX, SUN_CY, _sunFrame, color);
}
仅 MODE_CHARGING 时旋转,STANDBY时灰色静止,节省SPI带宽
主函数设计
主程序承担系统初始化、状态机管理和任务调度三大职责
cpp
/**************************************************************************************
* 文件: EPD_Solar_Monitor_v2.ino(ST7789版)
* 作者:零知实验室(深圳市在芯间科技有限公司)
* 时间: 2026-07-24 版本: v2.1
*
* 功能:太阳能板实时监测------INA219采集电压/电流/功率,ST7789 240×240 TFT显示
* BTN_NEXT 短按 → 数据页 ↔ 图片页切换
* BTN_ACT 长按 → 进入/退出 ST7789 芯片级睡眠(省电,非填黑)
* 任意按键 → 从睡眠唤醒
*
* 初始化流程:
* 1. 画顶栏(drawTopBarOnly)
* 2. INA219 ACK扫描
* 3. 失败 → 画错误页,停留3秒,进主界面等待重连(不卡死)
* 4. 成功 → 直接进主界面
*
* 接线:
* ST7789: GND→GND VCC→3.3V SCL→D13 SDA→D11 RES→D4 DC→D2 CS→D10
* INA219: SDA→A4 SCL→A5 VIN+→太阳能板正极 VIN-→负载正极
* 按键: BTN_NEXT=D0 BTN_ACT=D1(内部上拉,对地)
**************************************************************************************/
#include "Config.h"
#include "BatteryMonitor.h"
#include "DisplayManager.h"
#include "ButtonManager.h"
#include <SoftWire.h>
DisplayManager display;
Button btnNext;
Button btnAct;
AppState appState = APP_RUNNING;
AppState stateBeforeSleep = APP_RUNNING;
static uint32_t tSample = 0;
static uint32_t tDisplay = 0;
static uint32_t tRetry = 0;
void btnNextISR() { btnNext.onInterrupt(); }
void btnActISR() { btnAct.onInterrupt(); }
// INA219 主动 ACK 扫描(防止 SoftWire 不检测 ACK 导致误报成功)
static bool inaAckCheck() {
SoftWire probe(SCL, SDA, SOFT_STANDARD);
probe.begin();
probe.setClock(100000);
delay(20);
probe.beginTransmission(INA219_ADDR_MAIN);
return (probe.endTransmission() == 0);
}
void setup() {
Serial.begin(115200);
DEBUG_PRINTLN("[SYS] Solar Monitor v2.1 (ST7789)");
btnNext.begin(BTN_NEXT_PIN, btnNextISR);
btnAct.begin(BTN_ACT_PIN, btnActISR);
DEBUG_PRINTLN("[BTN] 已挂载中断");
display.begin();
DEBUG_PRINTLN("[TFT] ST7789 初始化完成");
// [F2] 先只画顶栏,INA219还未确认
display.drawTopBarOnly(false);
// INA219 初始化
bool inaOk = false;
if (!inaAckCheck()) {
DEBUG_PRINTLN("[INA] ACK扫描失败,0x40无响应");
display.drawInitError();
delay(3000); // 停留3秒让用户看清错误信息
// 不卡死,继续进主界面,loop里会定时重试
} else {
inaOk = Battery.begin();
if (!inaOk) {
DEBUG_PRINTLN("[INA] 寄存器初始化失败");
display.drawInitError();
delay(3000);
} else {
DEBUG_PRINTLN("[INA] 初始化成功");
}
}
// 画完整底图(数据页)
display.drawStatic(inaOk);
tSample = tDisplay = millis();
}
void loop() {
uint32_t now = millis();
// ── 休眠:任意键唤醒 ──────────────────────────────────────────────────────
if (appState == APP_SLEEP) {
bool wake = btnNext.consumeShort() || btnNext.consumeLong() ||
btnAct.consumeShort() || btnAct.consumeLong();
if (wake) {
btnNext.clearPending();
btnAct.clearPending();
DEBUG_PRINTLN("[SYS] 唤醒");
appState = stateBeforeSleep;
display.wakeScreen();
// 唤醒后重绘当前页(芯片sleep-out后GRAM内容保留,但重绘更保险)
bool online = (Battery.getSystemState() == STATE_RUNNING);
display.drawStatic(online);
tDisplay = now;
}
return;
}
// ── BTN_NEXT 短按:数据页 ↔ 图片页切换 ──────────────────────────────────
if (btnNext.consumeShort()) {
bool online = (Battery.getSystemState() == STATE_RUNNING);
display.togglePage(online);
DEBUG_PRINTLN("[BTN] 切换页面");
}
// ── BTN_ACT 长按:进入 ST7789 芯片级睡眠 ────────────────────────────────
if (btnAct.consumeLong()) {
DEBUG_PRINTLN("[SYS] 进入芯片睡眠(ST7789 sleep-in)");
stateBeforeSleep = appState;
appState = APP_SLEEP;
display.sleepScreen();
btnNext.clearPending();
return;
}
// ── BTN_ACT 短按:预留(当前无动作,可扩展)─────────────────────────────
if (btnAct.consumeShort()) {
DEBUG_PRINTLN("[BTN] ACT短按(预留)");
}
// ── INA219 离线重试 ───────────────────────────────────────────────────────
bool inaOnline = (Battery.getSystemState() == STATE_RUNNING);
if (!inaOnline && (now - tRetry >= INA_RETRY_MS)) {
tRetry = now;
DEBUG_PRINTLN("[INA] 重试初始化...");
if (inaAckCheck() && Battery.begin()) {
DEBUG_PRINTLN("[INA] 重新初始化成功");
display.drawStatic(true);
tDisplay = now;
inaOnline = true;
}
}
// ── 采样 ─────────────────────────────────────────────────────────────────
if (inaOnline && (now - tSample >= SAMPLE_INTERVAL)) {
tSample = now;
Battery.update();
}
// ── 显示刷新(仅数据页生效)──────────────────────────────────────────────
if (now - tDisplay >= DISPLAY_REFRESH_MS) {
tDisplay = now;
BatteryData raw = Battery.getData();
SolarSnapshot snap;
snap.voltage = raw.voltage;
snap.current = raw.current;
snap.power = raw.power;
snap.shuntV = raw.shuntVoltage;
snap.capacity = raw.capacity;
snap.runtime = raw.runtime;
snap.mode = raw.mode;
snap.inaOnline = inaOnline;
display.drawDynamic(snap);
static uint32_t tSerial = 0;
if (now - tSerial >= 3000UL) {
tSerial = now;
Serial.print(F("[SOLAR] "));
Serial.print(inaOnline ? "online" : "OFFLINE");
Serial.print(F(" V=")); Serial.print(snap.voltage, 3);
Serial.print(F("V I=")); Serial.print(snap.current, 1);
Serial.print(F("mA P=")); Serial.print(snap.power, 1);
Serial.print(F("mW mode="));
Serial.println(snap.mode == MODE_CHARGING ? "CHG" :
snap.mode == MODE_DISCHARGING ? "DSG" : "STBY");
}
}
}
/******************************************************************************
* 深圳市在芯间科技有限公司
* 淘宝店铺:在芯间科技零知板
* 店铺网址:https://shop533070398.taobao.com
* 版权说明:
* 1.本代码的版权归【深圳市在芯间科技有限公司】所有,仅限个人非商业性学习使用。
* 2.严禁将本代码或其衍生版本用于任何商业用途(包括但不限于产品开发、付费服务、企业内部使用等)。
* 3.任何商业用途均需事先获得【深圳市在芯间科技有限公司】的书面授权,未经授权的商业使用行为将被视为侵权。
******************************************************************************/
INA219初始化失败时显示错误页3秒后自动进入主界面,loop()中定时重试
芯片级低功耗睡眠
cpp
void DisplayManager::sleepScreen() {
_tft.enableSleep(true); // ST7789 sleep-in命令(0x10)【DisplayManager.cpp†L287】
_sleeping = true;
}
void DisplayManager::wakeScreen() {
if (!_sleeping) return;
_tft.enableSleep(false); // ST7789 sleep-out命令(0x11)【DisplayManager.cpp†L291】
delay(120); // 规格书要求:sleep-out后等待120ms[reference:10]
_sleeping = false;
}
- 发送0x10(SLEEP_IN)命令,ST7789进入深度睡眠模式,睡眠电流可低至10μA以下
- 唤醒时发送0x11(SLEEP_OUT)并延时120ms
系统流程图

四、项目结果演示
4.1 操作流程
①上电启动
屏幕先显示顶栏,"SOLAR MONITOR"标题 + 连接状态圆点

- INA219初始化成功 → 显示完整数据界面
- INA219初始化失败 → 显示红色错误页,3秒后进入主界面
②数据页展示
INA219 正常后进入主界面,电压/电流/功率大字卡片,彩色进度条,Mode 徽标

模拟太阳能充电,即将VIN+接到5V或者3.3V,观察 Mode 切换为绿色 CHARGING,太阳图标开始旋转
③页面切换

- 短按BTN_NEXT(D0按键)→ 数据页 ↔ 图片页切换
- 图片页展示152×152单色位图
④低功耗睡眠

- 长按BTN_ACT(D1按键,≥800ms)→ ST7789进入芯片级睡眠
- 任意按键 → 唤醒屏幕
4.2 视频演示
零知派STM32与INA219太阳能充电功率监测系统演示
本视频演示了基于零知派标准板(STM32F103RBT6)的太阳能充电功率实时监测系统。系统通过INA219采集太阳能板的电压/电流/功率数据,在ST7789 240×240彩色屏幕上以大字卡片、彩色进度条、动态太阳图标等方式直观展示。视频内容包括:上电启动流程、数据页实时刷新、功率进度条动态变化、数据页与图片页切换、长按进入低功耗睡眠模式等完整操作演示
五、技术原理讲解
5.1 INA219电流功率监测
INA219是高侧(High-Side)电流与功率监测芯片,集成了12位ADC、可编程增益放大器(PGA)和I2C数字接口

- 同时监测分流电压降和总线电压,具有可编程的转换时间和滤波功能
- 可编程的校准值与内部乘法器配合,可直接读出以安培为单位的电流值
- 附加的乘法寄存器计算瓦特数
寄存器结构
INA219 内部拥有六个 16 位寄存器:Configuration配置、Shunt Voltage分流电压、Bus Voltage总线电压、Power功率、Current电流和Calibration校准

其中配置寄存器和校准寄存器可读可写,其余四个为只读寄存器
高侧电流检测接法
INA219 采用高侧检测方式,分流电阻串联在电源正极回路中

INA219 通过测量分流电阻两端的电压来计算流过该电阻的电流,负载供电电压可以通过将分流电压和总线电压相加来得出
本项目的电压修正公式为:
V_solar(太阳能板真实输出)= V_bus + V_shunt = getBusVoltage_V() + getShuntVoltage_mV()/1000
对应代码:
cpp
float busV = ina219->getBusVoltage_V(); // VIN- 对 GND(电机端电压)
float shuntV_mV = ina219->getShuntVoltage_mV(); // VIN+ - VIN-(分流压降)
float solarV = busV + (shuntV_mV / 1000.0f); // 太阳能板真实输出电压
校准寄存器与电流分辨率
INA219 不能直接检测分流电阻的阻值,因此需要通过Calibration Register校准寄存器来告知芯片分流电阻的大小
校准寄存器的计算公式:

以本项目 setCalibration_32V_2A() 为例
- 目标量程:32V 总线 / 2A 最大电流
- 分流电阻:R_shunt = 0.1Ω
① 最大分流电压:V_shunt_max = 2A × 0.1Ω = 0.2V = 200mV
② 选择 Current_LSB = 100μA/bit(向上取整到整数)
③ 校准值 Cal = 0.04096 ÷ (0.0001 × 0.1) = 4096 → 写入 0x05 寄存器
④ 电流读数:I = Current_Register × Current_LSB
= Current_Register × 0.1mA
分流电压寄存器(Shunt Voltage Register)中每个计数对应 10μV,由于分流电阻为 0.1Ω,理论最小可检测电流为 10μV ÷ 0.1Ω = 100μA = 0.1mA

功率寄存器的 LSB 固定为电流 LSB 的 20 倍,这是芯片内部的固定设置
I2C通信时序
INA219 通过标准 I²C 协议(100kHz 标准模式)与 STM32 通信。本项目使用 SoftWire 软件 I2C(SDA=A4,SCL=A5)
读寄存器完整时序:

ACK 扫描验证的必要性:SoftWire 的 endTransmission() 在标准库实现中有时不检测 ACK 电平,导致设备断线也返回成功
cpp
//本项目在setup()中加入显式 ACK 检测:
static bool inaAckCheck() {
SoftWire probe(SCL, SDA, SOFT_STANDARD);
probe.begin();
probe.setClock(100000);
delay(20);
probe.beginTransmission(INA219_ADDR_MAIN); // 发起通信
return (probe.endTransmission() == 0); // 0=ACK收到=设备在线
}
// endTransmission 返回值含义:
// 0 = 成功(收到ACK)
// 2 = 地址阶段NACK(设备不在线或地址错误)
// 3 = 数据阶段NACK
// 4 = 其他错误(总线忙、超时)
零偏校准与滑动窗口滤波
①零偏问题: INA219 在无负载(空载)时由于运放输入失调电压,getCurrent_mA() 并非精确返回 0,而是有约 0.5~1.5mA 的偏差。本项目实测偏差约 0.8mA,在代码中硬编码校准:
float rawCurrent = ina219->getCurrent_mA() - 0.8f; // 实测零偏补偿
②滑动窗口均值滤波: 太阳能板在云层遮挡和光照变化时,INA219 读数会产生高频抖动,直接显示在屏幕上视觉效果很差。采用环形缓冲区实现 O(1) 写入的均值滤波:

5.2 TFT显示原理
ST7789 是一款用于 262K 色彩图形型 TFT-LCD 的单芯片控制器/驱动器。它由 720 个源线和 320 个门线驱动电路组成,能够直接连接到外部微处理器,并接受 8/9/16/18 位并行接口
4线SPI接口定义
| 引脚 | 功能 | 说明 |
|---|---|---|
| CSX | 片选 | 低电平有效,使能串行接口 |
| D/CX | 数据/命令 | 低=命令,高=数据 |
| SCL | 串行时钟 | SPI时钟线 |
| SDA | 串行数据 | 双向数据线 |
像素写入时序与 RGB565 格式
ST7789 使用RGB565格式,每个像素 2 字节(16bit)

本项目cWarn()红色 0xF800 = 1111 1000 0000 0000
ST7789 芯片级睡眠
ST7789 支持通过 SLPIN 命令(0x10)进入睡眠模式,通过 SLPOUT 命令(0x11)退出睡眠模式
5.3 太阳能光伏供电
太阳能电池的核心是 PN 结。当光子能量大于半导体禁带宽度时,激发价带电子跃迁至导带,形成电子-空穴对

PN 结内建电场(从 N 区指向 P 区)将电子和空穴向相反方向漂移,若连接外电路则形成持续电流
太阳能板 I-V 特性曲线

影响输出的主要因素:
| 因素 | 影响 | 本项目可观测现象 |
|---|---|---|
| 光照强度增加 | Isc↑,P↑ | 电流增大,进度条变长变红 |
| 温度升高 | Voc↓(约 -0.45%/℃) | 电压略降 |
| 负载电阻增大 | 工作点右移,趋近 Voc | 电压升高,电流降低 |
| 遮挡太阳能板 | I→0,Mode→STANDBY | 太阳图标停止旋转 |
六、常见问题解答FAQ
Q1:上电后屏幕显示INA219 ERROR?
A:检查 SDA→A4、SCL→A5 接线、确认 INA219 供电为 3.3V、I2C 地址 0x40确保A0/A1 不接或接 GND。接好后 3 秒后自动重试,无需重新上电
Q2:数值区偶尔出现残影?
A:格式化字符串未使用固定宽度,如果用了 String(val) 而不是 snprintf("%6.3f"),导致 5 位数变 4 位数时旧的第 5 位没被覆盖。检查 fmtV/fmtI/fmtP 的 snprintf 格式串是否带固定宽度
项目资源整合
INA219 库(支持 SoftWire): RobTillaart/INA219
INA219 数据手册: INA219 DataSheet