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

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

目录

一、系统接线部分

[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 太阳能光伏供电)

六、常见问题解答FAQ

[Q1:上电后屏幕显示INA219 ERROR?](#Q1:上电后屏幕显示INA219 ERROR?)

Q2:数值区偶尔出现残影?


项目概述

本文基于零知派标准板(主控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

相关推荐
wuyk55512 小时前
98.C语言易混难点:字符数组与字符串指针的底层差异
c语言·开发语言·c++·stm32·嵌入式硬件·算法
恒锐丰-罗生13 小时前
国产 H 桥驱动 SS6289|18V/4A 有刷电机驱动芯片性与应用分析 内置采样 + 多保护,门锁 / 机器人优选器件
驱动开发·嵌入式硬件·硬件工程
zcmodeltech17 小时前
工程机械与矿山机械沙盘模型多系统协同控制系统设计:基于STM32与Modbus RTU的露天开采-井下掘进-智慧矿山全场景联动方案
数据库·stm32·单片机·嵌入式硬件·制造·多分类
云泽80818 小时前
深入浅出 STM32(十五):多通道GPIO驱动的软件抽象与实现
stm32·单片机·嵌入式硬件
嵌入式阿蔡18 小时前
OTA 实战五(收官):量产落地 Checklist —— 版本号 / 防回滚 / 灰度 / 断点续传
网络·stm32·单片机·嵌入式硬件·嵌入式实时数据库
碧海银沙音频科技研究院19 小时前
基于杰理AC7016C的GTCRN门控卷积神经网络语音增强方法
人工智能·嵌入式硬件·语音识别
ACP广源盛1392462567320 小时前
Qwen3.8‑2.4T 开源落地@ACP#国产 MoE 大模型私有化部署中 DP 重定时器 GSV6155 的硬件链路价值与应用场景
大数据·数据库·人工智能·嵌入式硬件·矩阵·开源
遇雪长安21 小时前
STM32_BOOT未引出_串口升级系统Bootloader方案
stm32·单片机·嵌入式硬件·bootloader
dcdannycheung21 小时前
【无标题】
嵌入式硬件