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

目录

一、系统接线部分

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 零知派标准板 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
SCL D13 / PA5 硬件SPI1 SCK
SDA D11 / PA7 硬件SPI1 MOSI
RES D4 TFT_RST 4
DC D2 TFT_DC 2
CS D10 TFT_CS 10

②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 增量刷新架构

// 不画数据区,避免覆盖后续错误页的内容
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[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 等耗时函数

// 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 滑动窗口均值滤波

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 图标旋转帧动画

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带宽

主函数设计

        主程序承担系统初始化、状态机管理和任务调度三大职责

/**************************************************************************************
 * 文件: 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()中定时重试

芯片级低功耗睡眠

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

对应代码:

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 电平,导致设备断线也返回成功

//本项目在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

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐