eefocus_4087784 发表于 2026-8-20 15:24:10

零知派——STM32F103驱动ST7789与INA219太阳能充电功率实时监测系统

​         太阳能作为清洁能源的代表,正在越来越多地进入我们的日常生活——太阳能充电的效率受光照强度、温度、负载变化等多种因素影响,普通用户很难直观地了解“当前到底发了多少电”、“充电状态是否正常”。


目录
一、系统接线部分
1.1 硬件清单1.2 接线方案表1.3 具体接线图1.4 接线实物图二、安装与使用部分
三、代码讲解部分
3.1 增量刷新架构3.2 单色位图渲染3.3 中断驱动设计3.4 滑动窗口均值滤波3.5 图标旋转帧动画四、项目结果演示
4.1 操作流程4.2 视频演示五、技术原理讲解
5.1 INA219电流功率监测5.2 TFT显示原理5.3 太阳能光伏供电六、常见问题解答FAQ
Q1:上电后屏幕显示INA219 ERROR?Q2:数值区偶尔出现残影?
项目概述      
      本文基于零知派标准板(主控STM32F103RBT6) ,结合INA219高精度电流/功率监测芯片和ST7789 240×240 TFT彩色显示屏,设计了一款太阳能充电功率实时监测系统。系统能够实时采集太阳能板的输出电压、电流和功率,并以大字卡片+彩色进度条+动态太阳图标的直观方式呈现在屏幕


项目难点
      问题描述:TFT 每次更新数值时若清除区域不精准,会留下上一帧的字符残影,特别是数字位数变化


解决方案: 所有数值使用 snprintf 固定宽度格式化,字符串比较缓存避免未变化时重发 SPI,fillRect 清除区域与文字区域严格对齐

一、系统接线部分1.1 硬件清单
序号硬件名称型号/规格数量说明
1零知派标准板STM32F103RBT61主控核心
2TFT彩屏ST7789 240×2401SPI接口彩色显示屏
3电流功率传感器INA2191I²C接口高侧监测
4太阳能充电板5V/2A(或类似)1被测电源
5直流电机/负载5V直流电机1典型负载
6按键模块轻触开关×22带内部上拉
7杜邦线公对公/公对母若干连接线材

1.2 接线方案表      以下接线必须严格按照 config.h中的引脚定义执行


①ST7789 接线

ST7789引脚零知派引脚代码定义注意
GNDGND—ST7789直插零知派标准板,无需单独接线
VCC3.3V—
SCLD13 / PA5硬件SPI1 SCK
SDAD11 / PA7硬件SPI1 MOSI
RESD4TFT_RST 4
DCD2TFT_DC 2
CSD10TFT_CS 10
②INA219 接线

INA219引脚零知派引脚代码定义注意
GNDGND——
VCC3.3V——
SDAA4SDA A4SoftWire软件I²C
SCLA5SCL A5SoftWire软件I²C
VIN+太阳能板正极—分流电阻正端
VIN−负载正极—分流电阻负端
③按键接线

按键零知派引脚功能另一端
BTN_NEXTD0短按切换数据页↔图片页GND
BTN_ACTD1长按芯片级睡眠/唤醒GND
两个按键均使用INPUT_PULLUP模式,按下时引脚被拉低,无需外接上拉电阻电阻

1.3 具体接线图      充电电路: VIN+ 接太阳能板输出正极,VIN- 接负载正极,电流方向为太阳能板→分流电阻→负载,getCurrent_mA() 读值为正表示有光照在供电

https://file1.elecfans.com//web3/M00/72/9C/wKgZPGqGfHiAF4dZAA8heM-mLb0066.pngD2在之前的项目中用作水墨屏显示功能,本项目改为ST7789的DC引脚,请注意区分

1.4 接线实物图https://file1.elecfans.com//web3/M00/72/9C/wKgZO2qGfHmAfVA7AA_uyYJ-nOE07.jpeg
二、安装与使用部分2.1 开源平台-输入"INA219太阳能充电板"并搜索-代码下载自动打开
https://file1.elecfans.com//web3/M00/72/9C/wKgZPGqGfHmAXWC8AAduHGQQQn4516.png2.2 连接-验证-上传
https://file1.elecfans.com//web3/M00/72/9C/wKgZO2qGfHqAamOWAAxDlyfoa_M137.png2.3 调试-串口监视器
https://file1.elecfans.com//web3/M00/72/9C/wKgZPGqGfHqADc1PAAwpcnAKxCE803.png
三、代码讲解部分项目代码结构
      本项目代码分为四大模块:主程序(EPD_Solar_Monitor_v2.ino)、INA219驱动(BatteryMonitor.cpp/.h)、ST7789显示驱动与UI绘制(DisplayManager.cpp/.h)以及按键中断与消抖处理(ButtonManager)。以下仅讲解项目中最重要的五个部分

3.1 增量刷新架构
// 不画数据区,避免覆盖后续错误页的内容
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,需要逐像素展开


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);
      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 等耗时函数
// 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 滑动窗口均值滤波<pre><code class="language-cpp">float BatteryMonitor::filterCurrentValue(float newValue) {
currentBuffer = newValue;
filterIndex = (filterIndex + 1) % FILTER_WINDOW_SIZE;

float sum = 0;
for (int i = 0; i < FILTER_WINDOW_SIZE; i++) {
    sum += currentBuffer;
}
return sum / FILTER_WINDOW_SIZE;
}

void BatteryMonitor::updateSensorData() {
// 读取分流电压(mV)
float shuntV_mV = ina219- >getShuntVoltage_mV();

// 读取总线电压(VIN- 对 GND,单位 V)
float busV = ina219- >getBusVoltage_V();

// 太阳能板真实输出电压 = 总线电压 + 分流电压
// 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;          // 太阳能板电压
currentData.current      = filteredCurrent; // 保留符号,正=供电
// 功率也用太阳能板电压计算(而非原来的 getPower_mW())
// getPower_mW() 内部用 V_bus × I,会低估真实功率
currentData.power      = solarV * abs(filteredCurrent);

// 太阳能场景下不需要电量百分比(充电板无法用电压估算SOC)
// 保留字段,设为0
currentData.batteryPercent = 0.0f;
}</code></pre><p>FILTER_WINDOW_SIZE采样5秒数据,适用太阳能缓慢变化信号</p><p> </p><h2>3.5 图标旋转帧动画</h2><pre><code class="language-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();

// 修复:_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);
}</code></pre>仅 MODE_CHARGING 时旋转,STANDBY时灰色静止,节省SPI带宽
主函数设计
      主程序承担系统初始化、状态机管理和任务调度三大职责
/**************************************************************************************
* 文件: 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(" Solar Monitor v2.1 (ST7789)");

btnNext.begin(BTN_NEXT_PIN, btnNextISR);
btnAct.begin(BTN_ACT_PIN,btnActISR);
DEBUG_PRINTLN(" 已挂载中断");

display.begin();
DEBUG_PRINTLN(" ST7789 初始化完成");

// 先只画顶栏,INA219还未确认
display.drawTopBarOnly(false);

// INA219 初始化
bool inaOk = false;
if (!inaAckCheck()) {
    DEBUG_PRINTLN(" ACK扫描失败,0x40无响应");
    display.drawInitError();
    delay(3000);   // 停留3秒让用户看清错误信息
    // 不卡死,继续进主界面,loop里会定时重试
} else {
    inaOk = Battery.begin();
    if (!inaOk) {
      DEBUG_PRINTLN(" 寄存器初始化失败");
      display.drawInitError();
      delay(3000);
    } else {
      DEBUG_PRINTLN(" 初始化成功");
    }
}

// 画完整底图(数据页)
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(" 唤醒");
      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_ACT 长按:进入 ST7789 芯片级睡眠 ────────────────────────────────
if (btnAct.consumeLong()) {
    DEBUG_PRINTLN(" 进入芯片睡眠(ST7789 sleep-in)");
    stateBeforeSleep = appState;
    appState = APP_SLEEP;
    display.sleepScreen();
    btnNext.clearPending();
    return;
}

// ── BTN_ACT 短按:预留(当前无动作,可扩展)─────────────────────────────
if (btnAct.consumeShort()) {
    DEBUG_PRINTLN(" ACT短按(预留)");
}

// ── INA219 离线重试 ───────────────────────────────────────────────────────
bool inaOnline = (Battery.getSystemState() == STATE_RUNNING);
if (!inaOnline && (now - tRetry >= INA_RETRY_MS)) {
    tRetry = now;
    DEBUG_PRINTLN(" 重试初始化...");
    if (inaAckCheck() && Battery.begin()) {
      DEBUG_PRINTLN(" 重新初始化成功");
      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(" "));
      Serial.print(inaOnline ? "online" : "OFFLINE");
      Serial.print(F("V=")); Serial.print(snap.voltage, 3);
      Serial.print(F("VI=")); Serial.print(snap.current, 1);
      Serial.print(F("mAP=")); Serial.print(snap.power, 1);
      Serial.print(F("mWmode="));
      Serial.println(snap.mode == MODE_CHARGING ? "CHG" :
                     snap.mode == MODE_DISCHARGING ? "DSG" : "STBY");
    }
}
}

/******************************************************************************
* 深圳市在芯间科技有限公司
* 淘宝店铺:在芯间科技零知板
* 店铺网址:https://shop533070398.taobao.com
* 版权说明:
*1.本代码的版权归【深圳市在芯间科技有限公司】所有,仅限个人非商业性学习使用。
*2.严禁将本代码或其衍生版本用于任何商业用途(包括但不限于产品开发、付费服务、企业内部使用等)。
*3.任何商业用途均需事先获得【深圳市在芯间科技有限公司】的书面授权,未经授权的商业使用行为将被视为侵权。
******************************************************************************/
      INA219初始化失败时显示错误页3秒后自动进入主界面,loop()中定时重试


芯片级低功耗睡眠
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
_sleeping = false;
}
https://file1.elecfans.com/web2/M00/03/FB/poYBAGDYdXCAWkKMAAAAK8RNs4s030.png
[*]发送0x10(SLEEP_IN)命令,ST7789进入深度睡眠模式,睡眠电流可低至10μA以下
[*]唤醒时发送0x11(SLEEP_OUT)并延时120ms
系统流程图
https://file1.elecfans.com//web3/M00/72/9C/wKgZO2qGfHuAShXCABcYQIH3Hc8028.png
四、项目结果演示4.1 操作流程①上电启动
      屏幕先显示顶栏,“SOLAR MONITOR”标题 + 连接状态圆点
https://file1.elecfans.com//web3/M00/72/9C/wKgZPGqGfHyAewb5ABwnlFloXGY23.jpeg
[*]INA219初始化成功 → 显示完整数据界面
[*]INA219初始化失败 → 显示红色错误页,3秒后进入主界面
②数据页展示
INA219 正常后进入主界面,电压/电流/功率大字卡片,彩色进度条,Mode 徽标
https://file1.elecfans.com//web3/M00/72/9C/wKgZO2qGfH2AB4y3ABvJVBnCNVg39.jpeg      模拟太阳能充电,即将VIN+接到5V或者3.3V,观察 Mode 切换为绿色 CHARGING,太阳图标开始旋转
③页面切换
https://file1.elecfans.com//web3/M00/72/9C/wKgZPGqGfH6AFDNMAB2B4V3aB4w81.jpeg
[*]短按BTN_NEXT(D0按键)→ 数据页 ↔ 图片页切换
[*]图片页展示152×152单色位图
④低功耗睡眠
https://file1.elecfans.com//web3/M00/72/9C/wKgZO2qGfH6AWvesAAse7zeBAA0555.png
[*]长按BTN_ACT(D1按键,≥800ms)→ ST7789进入芯片级睡眠
[*]任意按键 → 唤醒屏幕

4.2 视频演示https://live.csdn.net/v/540348?spm=1001.2014.3001.5501
本视频演示了基于零知派标准板(STM32F103RBT6)的太阳能充电功率实时监测系统。系统通过INA219采集太阳能板的电压/电流/功率数据,在ST7789 240×240彩色屏幕上以大字卡片、彩色进度条、动态太阳图标等方式直观展示。视频内容包括:上电启动流程、数据页实时刷新、功率进度条动态变化、数据页与图片页切换、长按进入低功耗睡眠模式等完整操作演示

五、技术原理讲解5.1 INA219电流功率监测      INA219是高侧(High-Side)电流与功率监测芯片,集成了12位ADC、可编程增益放大器(PGA)和I2C数字接口

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

https://file1.elecfans.com//web3/M00/72/9C/wKgZPGqGfICAEI8XAADtzfxh2bE683.png​
其中配置寄存器和校准寄存器可读可写,其余四个为只读寄存器
高侧电流检测接法
      INA219 采用高侧检测方式,分流电阻串联在电源正极回路中

https://file1.elecfans.com//web3/M00/72/9C/wKgZO2qGfICAIRLPAAAy8EP-u-c405.png​
INA219 通过测量分流电阻两端的电压来计算流过该电阻的电流,负载供电电压可以通过将分流电压和总线电压相加来得出
本项目的电压修正公式为:
      V_solar(太阳能板真实输出)= V_bus + V_shunt = getBusVoltage_V() + getShuntVoltage_mV()/1000


对应代码:
float busV      = ina219- >getBusVoltage_V();    // VIN- 对 GND(电机端电压)float shuntV_mV = ina219- >getShuntVoltage_mV(); // VIN+ - VIN-(分流压降)float solarV    = busV + (shuntV_mV / 1000.0f); // 太阳能板真实输出电压
https://file1.elecfans.com/web2/M00/03/FB/poYBAGDYdXCAWkKMAAAAK8RNs4s030.png校准寄存器与电流分辨率
      INA219 不能直接检测分流电阻的阻值,因此需要通过Calibration Register校准寄存器来告知芯片分流电阻的大小


校准寄存器的计算公式:
https://file1.elecfans.com//web3/M00/72/9C/wKgZPGqGfIGAXIEJAAAvMoF0F6c951.png以本项目 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

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


读寄存器完整时序:
https://file1.elecfans.com//web3/M00/72/9C/wKgZPGqGfIGAJo_WAAETxbsfqp4514.pngACK 扫描验证的必要性:SoftWire 的 endTransmission() 在标准库实现中有时不检测 ACK 电平,导致设备断线也返回成功
//本项目在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) 写入的均值滤波:
https://file1.elecfans.com//web3/M00/72/9C/wKgZO2qGfIKAAtP-AAA5f4fxEDs433.png
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)
https://file1.elecfans.com//web3/M00/72/9C/wKgZO2qGfIOAAycqAAA5JzBBlbs485.png本项目cWarn()红色 0xF800 = 1111 1000 0000 0000
ST7789 芯片级睡眠
ST7789 支持通过 SLPIN 命令(0x10)进入睡眠模式,通过 SLPOUT 命令(0x11)退出睡眠模式

5.3 太阳能光伏供电      太阳能电池的核心是 PN 结。当光子能量大于半导体禁带宽度时,激发价带电子跃迁至导带,形成电子-空穴对

https://file1.elecfans.com//web3/M00/72/9C/wKgZPGqGfIOAEncBAAiKU40q1KI027.pngPN 结内建电场(从 N 区指向 P 区)将电子和空穴向相反方向漂移,若连接外电路则形成持续电流
太阳能板 I-V 特性曲线
https://file1.elecfans.com//web3/M00/72/9C/wKgZO2qGfISABQxmAATKVc9UDio191.png​
影响输出的主要因素:

因素影响本项目可观测现象
光照强度增加Isc↑,P↑电流增大,进度条变长变红
温度升高Voc↓(约 -0.45%/℃)电压略降
负载电阻增大工作点右移,趋近 Voc电压升高,电流降低
遮挡太阳能板I→0,Mode→STANDBY太阳图标停止旋转

六、常见问题解答FAQQ1:上电后屏幕显示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


页: [1]
查看完整版本: 零知派——STM32F103驱动ST7789与INA219太阳能充电功率实时监测系统