零知派——STM32驱动INA219电流功率监测计实现高精度电源管理

一款基于零知派标准板的高精度电流/电压/功率监测解决方案

✔零知派(零知开源)是一个专为电子初学者/电子兴趣爱好者设计的开源软硬件平台,在硬件上提供超高性价比STM32系列开发板、物联网控制板。取消了Bootloader程序烧录,让开发重心从 "配置环境" 转移到 "创意实现",极大降低了技术门槛。零知开源编程软件,内置上千个覆盖多场景的示例代码,支持项目源码一键下载,项目文章在线浏览。零知派(零知开源)平台通过软硬件协同创新,让你的创意快速转化为实物,来动手试试吧!

✔访问零知实验室,获取更多实战项目和教程资源吧!

www.lingzhilab.com

目录

一、硬件系统设计

[1.1 硬件清单](#1.1 硬件清单)

[1.2 接线方案表](#1.2 接线方案表)

[1.3 具体接线图](#1.3 具体接线图)

[1.4 连接实物图](#1.4 连接实物图)

二、软件架构设计

[2.1 系统初始化](#2.1 系统初始化)

[2.2 数据采集与处理](#2.2 数据采集与处理)

[2.3 UI显示功能](#2.3 UI显示功能)

[2.4 INA219库文件解析](#2.4 INA219库文件解析)

三、项目演示

[3.1 监测过程与比较](#3.1 监测过程与比较)

[3.2 万用表对比测试](#3.2 万用表对比测试)

[3.3 视频演示](#3.3 视频演示)

[四、INA219 电流功率监测计技术讲解](#四、INA219 电流功率监测计技术讲解)

[4.1 INA219工作原理](#4.1 INA219工作原理)

[4.2 I2C通信协议](#4.2 I2C通信协议)

[4.3 寄存器配置](#4.3 寄存器配置)

五、常见问题解答(Q&A)

Q1:测量值不准确怎么办?

[Q2:软件 I2C 通信不稳定怎么办?](#Q2:软件 I2C 通信不稳定怎么办?)

Q3:软件I2C通信失败如何排查?


(1)项目概述

本项目基于STM32F103RBT6 微控制器和零知派INA219 电流功率监测计,实现了一个高精度的电源监控系统。该系统能够实时监测电路中的总线电压电流消耗功率输出,并通过ST7789 TFT显示屏进行可视化展示。通过软件I2C(SoftWire)驱动INA219传感器,实现了稳定的数据采集和实时波形显示

(2)项目难点及解决方案

问题描述:INA219 库默认使用硬件 I2C 通信,与零知标准板的 STM32F103RBT6 存在兼容性问题

**解决方案:**将INA219库中所有TwoWire类型替换为SoftWire类型;调整构造函数,使其接受SoftWire指针参数

一、硬件系统设计

1.1 硬件清单

序号 元件名称 型号规格 数量
1 主控板 零知派标准板(STM32F103RBT6) 1
2 电流功率监测计 零知派INA219 1
3 显示屏 ST7789(240×320) 1
5 杜邦线 公对公、公对母 若干
6 USB 数据线 Mini USB 1
7 LED 模块 5mm 发光二极管 1

1.2 接线方案表

元件 引脚 连接到 零知派标准板的引脚 备注
INA219 SCL A5 软件 I2C 时钟线
INA219 SDA A4 软件 I2C 数据线
INA219 VCC 3.3V 传感器电源
INA219 GND GND 接地
INA219 Vin+ 被测电源正极/3.3V
INA219 Vin- 负载正极 通过 0.1Ω 电阻连接到 Vin+
ST7789 直插 直插零知派标准板TFT扩展引脚 硬件 SPI通信
ST7789
ST7789
ST7789
ST7789
ST7789

INA219电流功率监测计的Vin+接3.3V供电电源、Vin-接负载(本项目连接到LED模块IN引脚)

1.3 具体接线图

INA219与零知派标准板通过软件I2C进行通信;各器件接线、LED模块按照代码接线所示:

1.4 连接实物图

二、软件架构设计

2.1 系统初始化

cpp 复制代码
void setup(void) {
  Serial.begin(115200);          // 初始化串口通信
  tft.init(240, 320);            // 初始化TFT显示屏
  tft.invertDisplay(false);      // 设置显示方向
  tft.setRotation(1);            // 旋转显示屏
  tft.fillScreen(BACKGROUND);    // 清屏
  showSplashScreen();            // 显示启动画面
  delay(1500);
  
  sw.begin();                    // 初始化软件I2C
  sw.setClock(100000);           // 设置I2C时钟频率为100kHz
  
  if (!INA.begin()) {            // 初始化INA219传感器
    Serial.println("Failed to find INA219 chip");
    while (1) { delay(10); }     // 初始化失败则死循环
  }
  
  // 校准传感器:最大电流0.5A,分流电阻0.1Ω
  if (!INA.setMaxCurrentShunt(0.5, 0.1)) {
    Serial.println("Calibration failed!");
    while(1);                    // 校准失败则死循环
  }
  
  // 初始化历史数据缓冲区
  for (int i = 0; i < HISTORY_SIZE; i++) {
    voltageHistory[i] = 0;
    currentHistory[i] = 0;
    powerHistory[i] = 0;
  }
  
  drawStaticUI();                // 绘制静态UI界面
  Serial.println("System initialized");
  Serial.print("INA219_LIB_VERSION: ");
  Serial.println(INA219_LIB_VERSION);
}

初始化软件 I2C 并设置通信速率,进行INA219传感器校准;初始化历史数据缓冲区,用于存储绘图数据

2.2 数据采集与处理

cpp 复制代码
void loop(void) {
  // 读取传感器数据
  float busVoltage = INA.getBusVoltage();      // 读取总线电压(V)
  float current = INA.getCurrent_mA() - 0.8;   // 读取电流(mA)并进行零点校准
  float power = INA.getPower_mW();             // 读取功率(mW)
  
  // 存储历史数据
  voltageHistory[historyIndex] = busVoltage;
  currentHistory[historyIndex] = current;
  powerHistory[historyIndex] = power;
  
  // 更新UI显示
  updateUI(busVoltage, current, power);
  
  // 更新历史数据索引(循环缓冲区)
  historyIndex = (historyIndex + 1) % HISTORY_SIZE;
  
  // 串口输出数据
  Serial.println("\n\tBUS\t\tSHUNT\t\tCURRENT\t\tPOWER\t\tOVF\t\tCNVR");
  Serial.print("\t");
  Serial.print(busVoltage, 2);
  Serial.print("\t\t");
  Serial.print(INA.getShuntVoltage_mV(), 2);
  Serial.print("\t\t");
  Serial.print(current, 2);
  Serial.print("\t\t");
  Serial.print(power, 2);
  Serial.print("\t\t");
  Serial.print(INA.getMathOverflowFlag());  // 数学溢出标志
  Serial.print("\t\t");
  Serial.print(INA.getConversionFlag());    // 转换完成标志
  Serial.println();
  
  delay(1000);  // 1秒刷新一次
}

每秒从 INA219 读取总线电压、电流和功率;使用循环缓冲区将新数据存入历史缓冲区,实现数据的滚动存储

2.3 UI显示功能

cpp 复制代码
void updateUI(float voltage, float current, float power) {
  updatePanelValues(voltage, current, power);  // 更新右侧面板的实时数据
  
  // 清除图表区域并重新绘制坐标轴
  tft.fillRect(GRAPH_X + 1, GRAPH_Y + 1, GRAPH_WIDTH - 1, GRAPH_HEIGHT - 1, BACKGROUND);
  drawAxes();
  
  // 计算最大值用于自动缩放
  float maxVal = 0.1;  // 确保至少有一个较小的初始值
  for (int i = 0; i < HISTORY_SIZE; i++) {
    if (voltageHistory[i] > maxVal) maxVal = voltageHistory[i];
    if (currentHistory[i] > maxVal) maxVal = currentHistory[i];
    if (powerHistory[i] > maxVal) maxVal = powerHistory[i];
  }
  maxVal *= 1.15;  // 增加15%的余量
  
  // 绘制历史曲线
  for (int i = 1; i < HISTORY_SIZE; i++) {
    // 计算历史索引(循环缓冲区)
    int prevIndex = (historyIndex + i - 1) % HISTORY_SIZE;
    int currIndex = (historyIndex + i) % HISTORY_SIZE;
    
    // 计算X坐标
    int x1 = GRAPH_X + (i-1) * 2;
    int x2 = GRAPH_X + i * 2;
    if (x2 > GRAPH_X + GRAPH_WIDTH) break;  // 超出图表区域则停止
    
    // 绘制电压曲线(黄色)
    int y1_voltage = GRAPH_Y + GRAPH_HEIGHT - constrain(voltageHistory[prevIndex] / maxVal * GRAPH_HEIGHT, 0, GRAPH_HEIGHT);
    int y2_voltage = GRAPH_Y + GRAPH_HEIGHT - constrain(voltageHistory[currIndex] / maxVal * GRAPH_HEIGHT, 0, GRAPH_HEIGHT);
    tft.drawLine(x1, y1_voltage, x2, y2_voltage, VOLTAGE_COLOR);
    
    // 绘制电流曲线(绿色)
    int y1_current = GRAPH_Y + GRAPH_HEIGHT - constrain(currentHistory[prevIndex] / maxVal * GRAPH_HEIGHT, 0, GRAPH_HEIGHT);
    int y2_current = GRAPH_Y + GRAPH_HEIGHT - constrain(currentHistory[currIndex] / maxVal * GRAPH_HEIGHT, 0, GRAPH_HEIGHT);
    tft.drawLine(x1, y1_current, x2, y2_current, CURRENT_COLOR);
    
    // 绘制功率曲线(青色)
    int y1_power = GRAPH_Y + GRAPH_HEIGHT - constrain(powerHistory[prevIndex] / maxVal * GRAPH_HEIGHT, 0, GRAPH_HEIGHT);
    int y2_power = GRAPH_Y + GRAPH_HEIGHT - constrain(powerHistory[currIndex] / maxVal * GRAPH_HEIGHT, 0, GRAPH_HEIGHT);
    tft.drawLine(x1, y1_power, x2, y2_power, POWER_COLOR);
  }
  
  // 绘制Y轴刻度值
  tft.setTextColor(0xAD75);
  tft.setTextSize(1);
  tft.setCursor(GRAPH_X + 2,GRAPH_Y + 5);
  tft.print(maxVal, 1);                // 最大值
  tft.setCursor(GRAPH_X + 2,GRAPH_Y + GRAPH_HEIGHT/2 + 5);
  tft.print(maxVal/2, 1);              // 中间值
  tft.setCursor(GRAPH_X - 5, GRAPH_Y + GRAPH_HEIGHT + 5);
  tft.print("0");                      // 最小值
}

采用自动缩放机制,根据历史数据的最大值动态调整 Y 轴范围;使用不同颜色绘制三条曲线,分别表示电压、电流和功率

项目代码

cpp 复制代码
/**************************************************************************************
 * 文件: /STM32F103_INA219_RealTime_PowerMonitor/STM32F103_INA219_RealTime_PowerMonitor.ino
 * 作者:零知派(深圳市在芯间科技有限公司)
 * -^^- 零知派,让电子制作变得更简单! -^^-
 * 时间: 2026-4-16 15:30:45
 * 说明: 基于零知派标准板(STM32F103RBT6主控)和零知派INA219电流功率监测计,通过软件I2C(SoftWire)实现传感器稳定通信。
 *       实时采集总线电压、负载电流及功率数据,在ST7789 TFT屏可视化展示实时数值与历史趋势曲线,同步串口输出数据供调试分析。
 ***************************************************************************************/

#include "INA219.h"
#include <Adafruit_GFX.h>      // 图形显示基础库(适配ST7789)
#include <Adafruit_ST7789.h>   // ST7789 TFT屏硬件驱动库
#include <SoftWire.h>          // 软件I2C库

// -------------------------- 硬件引脚定义模块 --------------------------
/**
 * ST7789 TFT屏硬件SPI引脚配置
 * 硬件SPI固定引脚:SCK=13(时钟)、SDA(MOSI)=11(数据),以下为可配置引脚
 */
#define TFT_CS    10  // TFT片选引脚(低电平有效)
#define TFT_DC    2   // TFT数据/命令选择引脚(高=数据,低=命令)
#define TFT_RST   4   // TFT复位引脚(低电平复位,可不接则设为-1)

// -------------------------- 全局参数配置模块 --------------------------
/**
 * INA219传感器配置
 * @param 0x44: INA219默认I2C地址0x40(可通过A0/A1引脚修改为0x41/0x44/0x45)
 * @param &sw: 绑定软件I2C实例
 */
SoftWire sw(SCL, SDA, SOFT_STANDARD);   // 软件I2C引脚
INA219 INA(0x44, &sw); 

/**
 * ST7789 TFT屏配置
 * 分辨率:240x320(宽x高),旋转后实际显示方向由setRotation(1)决定
 */
Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST);
#define TFT_WIDTH  240    // ST7789屏物理宽度(像素)
#define TFT_HEIGHT 320    // ST7789屏物理高度(像素)

/**
 * 波形绘制历史数据配置
 * HISTORY_SIZE:波形缓存长度(决定时间轴跨度),建议根据刷新频率调整
 */
const int HISTORY_SIZE = 100;    // 最大历史数据点数(100个采样点,对应100秒)
float voltageHistory[HISTORY_SIZE];  // 电压历史数据缓存
float currentHistory[HISTORY_SIZE];  // 电流历史数据缓存
float powerHistory[HISTORY_SIZE];    // 功率历史数据缓存
int historyIndex = 0;               // 历史数据当前写入索引(循环覆盖)

/**
 * 显示界面颜色配置(RGB565格式,5位红+6位绿+5位蓝)
 */
#define BACKGROUND    ST77XX_BLACK    // 屏幕背景色
#define VOLTAGE_COLOR ST77XX_YELLOW   // 电压波形/文本颜色
#define CURRENT_COLOR ST77XX_GREEN    // 电流波形/文本颜色
#define POWER_COLOR   ST77XX_CYAN     // 功率波形/文本颜色
#define TEXT_COLOR    ST77XX_WHITE    // 通用文本颜色
#define AXIS_COLOR    ST77XX_WHITE    // 坐标轴颜色
#define PANEL_COLOR   0x18A0          // 数值面板背景色(RGB565:深灰蓝)

/**
 * 显示区域坐标配置(像素单位)
 * 波形区:左上方主要区域;数值面板:右侧固定面板
 */
#define GRAPH_WIDTH   200       // 波形显示区域宽度
#define GRAPH_HEIGHT  120       // 波形显示区域高度
#define GRAPH_X       10        // 波形区左上角X坐标
#define GRAPH_Y       60        // 波形区左上角Y坐标
#define PANEL_WIDTH   80        // 实时数值面板宽度
#define PANEL_X       240       // 数值面板左上角X坐标(补充:原代码240超出屏宽,建议改为150)
#define PANEL_Y       0         // 数值面板左上角Y坐标

// -------------------------- 初始化函数 --------------------------
/**
 * @brief 系统初始化入口函数
 * @details 完成串口、TFT屏、INA219传感器、数据缓存、静态UI的初始化
 */
void setup(void) {
  // 1. 串口初始化(调试用,波特率115200)
  Serial.begin(115200);

  // 2. TFT屏初始化
  tft.init(TFT_WIDTH, TFT_HEIGHT);       // 初始化屏显参数
  tft.invertDisplay(false);              // 关闭显示反转(true为反色显示)
  tft.setRotation(1);                    // 旋转屏幕(0-3,1为90度顺时针)
  tft.fillScreen(BACKGROUND);            // 清屏(背景色)
  showSplashScreen();                    // 显示启动画面
  delay(1500);                           // 启动画面停留1.5秒

  // 3. INA219传感器初始化
  sw.begin();                            // 启动软件I2C
  sw.setClock(100000);                   // 设置I2C时钟频率(100kHz,INA219最大支持400kHz)
  if (!INA.begin()) {                    // 检测INA219是否连接
    Serial.println("Failed to find INA219 chip");
    while (1) { delay(10); }             // 硬件错误,死循环报错
  }
  // 校准INA219(最大预期电流0.5A,分流电阻0.1Ω)
  if (!INA.setMaxCurrentShunt(0.5, 0.1)) {
    Serial.println("Calibration failed!");
    while(1);                            // 校准失败,死循环报错
  }

  // 4. 历史数据缓存初始化(清零)
  for (int i = 0; i < HISTORY_SIZE; i++) {
    voltageHistory[i] = 0;
    currentHistory[i] = 0;
    powerHistory[i] = 0;
  }

  // 5. 绘制静态UI框架(仅初始化时绘制一次)
  drawStaticUI();

  // 初始化完成提示
  Serial.println("System initialized");
  Serial.print("INA219_LIB_VERSION: ");
  Serial.println(INA219_LIB_VERSION);
}

// -------------------------- 主循环函数 --------------------------
/**
 * @brief 系统主循环
 * @details 每秒采集一次数据,更新缓存、UI和串口输出,循环执行
 */
void loop(void) {
  // 1. 读取INA219数据(核心传感器数据采集)
  float busVoltage = INA.getBusVoltage();        // 读取总线电压(V)
  float current = INA.getCurrent_mA() - 0.8;     // 读取电流(mA),减去0.8mA校准零点偏移
  float power = INA.getPower_mW();               // 读取功率(mW)

  // 2. 存储数据到历史缓存(循环覆盖)
  voltageHistory[historyIndex] = busVoltage;
  currentHistory[historyIndex] = current;
  powerHistory[historyIndex] = power;

  // 3. 更新屏幕显示(实时数值+波形)
  updateUI(busVoltage, current, power);

  // 4. 更新缓存索引(循环复用缓存)
  historyIndex = (historyIndex + 1) % HISTORY_SIZE;

  // 5. 串口输出调试信息(格式化打印)
  Serial.println("\n\tBUS(V)\t\tSHUNT(mV)\tCURRENT(mA)\tPOWER(mW)\tOVF\t\tCNVR");
  Serial.print("\t");
  Serial.print(busVoltage, 2);                  // 总线电压(保留2位小数)
  Serial.print("\t\t");
  Serial.print(INA.getShuntVoltage_mV(), 2);    // 分流电阻电压(mV)
  Serial.print("\t\t");
  Serial.print(current, 2);                     // 电流(mA)
  Serial.print("\t\t");
  Serial.print(power, 2);                       // 功率(mW)
  Serial.print("\t\t");
  Serial.print(INA.getMathOverflowFlag());      // 数学溢出标志(1=溢出,需重新校准)
  Serial.print("\t\t");
  Serial.print(INA.getConversionFlag());        // 转换完成标志(1=数据有效)
  Serial.println();

  // 6. 采样间隔(1秒,可根据需求调整,最小受INA219转换时间限制)
  delay(1000);
}

// -------------------------- 屏幕显示功能模块 --------------------------
/**
 * @brief 启动画面显示
 * @details 系统初始化时显示品牌/功能提示,提升用户体验
 */
void showSplashScreen() {
  tft.fillScreen(BACKGROUND);          // 清屏
  tft.setTextColor(ST77XX_YELLOW);     // 设置文本颜色为黄色
  tft.setTextSize(3);                  // 文本大小(1=8x8像素,3=24x24像素)
  tft.setCursor(70, 80);               // 设置文本光标位置(X,Y)
  tft.print("POWER");                  // 打印"POWER"
  tft.setCursor(85, 120);              // 调整光标位置
  tft.print("MONITOR");                // 打印"MONITOR"
  
  tft.setTextColor(ST77XX_CYAN);       // 切换文本颜色为青色
  tft.setTextSize(1);                  // 缩小文本大小
  tft.setCursor(90, 180);              // 调整光标位置
  tft.print("Initializing...");        // 打印初始化提示
}

/**
 * @brief 绘制静态UI框架
 * @details 绘制仅需初始化一次的界面元素(标题、边框、坐标轴、面板、图例)
 */
void drawStaticUI() {
  tft.fillScreen(BACKGROUND);          // 清屏

  // 1. 绘制标题
  tft.setTextColor(TEXT_COLOR);
  tft.setTextSize(2);
  tft.setCursor(50, 5);
  tft.print("POWER MONITOR");

  // 2. 绘制波形区边框
  tft.drawRect(GRAPH_X, GRAPH_Y, GRAPH_WIDTH, GRAPH_HEIGHT, AXIS_COLOR);
  drawAxes();  // 绘制坐标轴

  // 3. 绘制实时数值面板(右侧)
  tft.fillRect(PANEL_X, PANEL_Y, PANEL_WIDTH, 240, PANEL_COLOR);  // 面板背景
  tft.drawRect(PANEL_X, PANEL_Y, PANEL_WIDTH, 240, TEXT_COLOR);   // 面板边框
  tft.setTextColor(TEXT_COLOR);
  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, TEXT_COLOR);  // 分隔线

  // 4. 绘制波形图例
  drawLegend();

  // 5. 绘制坐标轴标签
  tft.setTextSize(1);
  tft.setCursor(GRAPH_X - 5, GRAPH_Y - 15);
  tft.print("Value");          // Y轴标签
  tft.setCursor(GRAPH_X + GRAPH_WIDTH - 30, GRAPH_Y + GRAPH_HEIGHT + 5);
  tft.print("Time (s)");       // X轴标签

  // 6. 绘制数值面板静态文本(V/I/P标签)
  drawPanelStaticText();
}

/**
 * @brief 绘制波形区坐标轴
 * @details 绘制X/Y轴、轴尖标记和水平网格线,提升波形可读性
 */
void drawAxes() {
  // 绘制Y轴(垂直轴)
  tft.drawFastVLine(GRAPH_X, GRAPH_Y, GRAPH_HEIGHT, AXIS_COLOR);
  tft.drawLine(GRAPH_X, GRAPH_Y, GRAPH_X - 3, GRAPH_Y + 5, AXIS_COLOR);  // Y轴上尖
  tft.drawLine(GRAPH_X, GRAPH_Y, GRAPH_X + 3, GRAPH_Y + 5, AXIS_COLOR);

  // 绘制X轴(水平轴)
  tft.drawFastHLine(GRAPH_X, GRAPH_Y + GRAPH_HEIGHT, GRAPH_WIDTH, AXIS_COLOR);
  tft.drawLine(GRAPH_X + GRAPH_WIDTH, GRAPH_Y + GRAPH_HEIGHT, 
               GRAPH_X + GRAPH_WIDTH - 5, GRAPH_Y + GRAPH_HEIGHT - 3, AXIS_COLOR);  // X轴右尖
  tft.drawLine(GRAPH_X + GRAPH_WIDTH, GRAPH_Y + GRAPH_HEIGHT, 
               GRAPH_X + GRAPH_WIDTH - 5, GRAPH_Y + GRAPH_HEIGHT + 3, AXIS_COLOR);

  // 绘制水平网格线(4等分)
  for (int i = 1; i < 4; i++) {
    int yPos = GRAPH_Y + i * GRAPH_HEIGHT / 4;  // 网格线Y坐标
    for (int x = GRAPH_X; x < GRAPH_X + GRAPH_WIDTH; x += 4) {  // 虚线绘制
      tft.drawPixel(x, yPos, 0x5AEB);  // 浅灰色像素(RGB565)
    }
  }
}

/**
 * @brief 绘制数值面板静态文本
 * @details 绘制V/I/P标签和单位,仅初始化时绘制
 */
void drawPanelStaticText() {
  tft.setTextColor(TEXT_COLOR);
  tft.setTextSize(2);

  // 电压标签
  tft.setCursor(PANEL_X + 10, PANEL_Y + 50);
  tft.print("V:");
  // 电流标签
  tft.setCursor(PANEL_X + 10, PANEL_Y + 110);
  tft.print("I:");
  // 功率标签
  tft.setCursor(PANEL_X + 10, PANEL_Y + 170);
  tft.print("P:");

  // 单位标注
  tft.setTextSize(1);
  tft.setCursor(PANEL_X + 55, PANEL_Y + 70);
  tft.print("V");    // 电压单位
  tft.setCursor(PANEL_X + 55, PANEL_Y + 130);
  tft.print("mA");   // 电流单位
  tft.setCursor(PANEL_X + 55, PANEL_Y + 190);
  tft.print("mW");   // 功率单位
}

/**
 * @brief 绘制波形图例
 * @details 绘制电压/电流/功率的颜色标识,方便用户识别波形
 */
void drawLegend() {
  int legendY = GRAPH_Y + GRAPH_HEIGHT + 25;  // 图例Y坐标

  // 电压图例
  tft.fillRect(20, legendY, 15, 3, VOLTAGE_COLOR);  // 颜色块
  tft.setTextColor(VOLTAGE_COLOR);
  tft.setTextSize(1);
  tft.setCursor(40, legendY - 3);
  tft.print("VOLTAGE");

  // 电流图例
  tft.fillRect(100, legendY, 15, 3, CURRENT_COLOR);
  tft.setTextColor(CURRENT_COLOR);
  tft.setCursor(120, legendY - 3);
  tft.print("CURRENT");

  // 功率图例
  tft.fillRect(180, legendY, 15, 3, POWER_COLOR);
  tft.setTextColor(POWER_COLOR);
  tft.setCursor(200, legendY - 3);
  tft.print("POWER");
}

/**
 * @brief 更新整个UI界面
 * @details 包含实时数值更新和波形绘制,是界面动态更新的核心函数
 * @param voltage 总线电压(V)
 * @param current 电流(mA)
 * @param power 功率(mW)
 */
void updateUI(float voltage, float current, float power) {
  // 1. 更新右侧数值面板
  updatePanelValues(voltage, current, power);

  // 2. 清空波形区(保留边框和坐标轴)
  tft.fillRect(GRAPH_X + 1, GRAPH_Y + 1, GRAPH_WIDTH - 1, GRAPH_HEIGHT - 1, BACKGROUND);
  drawAxes();  // 重新绘制坐标轴(避免被清空)

  // 3. 计算波形最大值(用于归一化显示)
  float maxVal = 0.1;  // 初始值(避免除以0)
  for (int i = 0; i < HISTORY_SIZE; i++) {
    if (voltageHistory[i] > maxVal) maxVal = voltageHistory[i];
    if (currentHistory[i] > maxVal) maxVal = currentHistory[i];
    if (powerHistory[i] > maxVal) maxVal = powerHistory[i];
  }
  maxVal *= 1.15;  // 留15%余量,避免波形超出显示区

  // 4. 绘制波形(循环绘制历史数据点)
  for (int i = 1; i < HISTORY_SIZE; i++) {
    // 计算缓存索引(循环读取)
    int prevIndex = (historyIndex + i - 1) % HISTORY_SIZE;
    int currIndex = (historyIndex + i) % HISTORY_SIZE;

    // 计算X坐标(时间轴)
    int x1 = GRAPH_X + (i-1) * 2;
    int x2 = GRAPH_X + i * 2;
    if (x2 > GRAPH_X + GRAPH_WIDTH) break;  // 超出显示区则停止

    // 绘制电压波形(归一化到显示区高度)
    int y1_voltage = GRAPH_Y + GRAPH_HEIGHT - constrain(voltageHistory[prevIndex] / maxVal * GRAPH_HEIGHT, 0, GRAPH_HEIGHT);
    int y2_voltage = GRAPH_Y + GRAPH_HEIGHT - constrain(voltageHistory[currIndex] / maxVal * GRAPH_HEIGHT, 0, GRAPH_HEIGHT);
    tft.drawLine(x1, y1_voltage, x2, y2_voltage, VOLTAGE_COLOR);

    // 绘制电流波形
    int y1_current = GRAPH_Y + GRAPH_HEIGHT - constrain(currentHistory[prevIndex] / maxVal * GRAPH_HEIGHT, 0, GRAPH_HEIGHT);
    int y2_current = GRAPH_Y + GRAPH_HEIGHT - constrain(currentHistory[currIndex] / maxVal * GRAPH_HEIGHT, 0, GRAPH_HEIGHT);
    tft.drawLine(x1, y1_current, x2, y2_current, CURRENT_COLOR);

    // 绘制功率波形
    int y1_power = GRAPH_Y + GRAPH_HEIGHT - constrain(powerHistory[prevIndex] / maxVal * GRAPH_HEIGHT, 0, GRAPH_HEIGHT);
    int y2_power = GRAPH_Y + GRAPH_HEIGHT - constrain(powerHistory[currIndex] / maxVal * GRAPH_HEIGHT, 0, GRAPH_HEIGHT);
    tft.drawLine(x1, y1_power, x2, y2_power, POWER_COLOR);
  }

  // 5. 绘制Y轴数值标签(最大值、中间值、0)
  tft.setTextColor(0xAD75);  // 浅紫色
  tft.setTextSize(1);
  tft.setCursor(GRAPH_X + 2, GRAPH_Y + 5);
  tft.print(maxVal, 1);              // 最大值(保留1位小数)
  tft.setCursor(GRAPH_X + 2, GRAPH_Y + GRAPH_HEIGHT/2 + 5);
  tft.print(maxVal/2, 1);            // 中间值
  tft.setCursor(GRAPH_X - 5, GRAPH_Y + GRAPH_HEIGHT + 5);
  tft.print("0");                    // 最小值
}

/**
 * @brief 更新实时数值面板
 * @details 清空原有数值区域并打印新值,避免文本重叠
 * @param voltage 总线电压(V)
 * @param current 电流(mA)
 * @param power 功率(mW)
 */
void updatePanelValues(float voltage, float current, float power) {
  tft.setTextColor(TEXT_COLOR);
  tft.setTextSize(1);

  // 1. 更新电压数值
  tft.fillRect(PANEL_X + 25, PANEL_Y + 65, 30, 12, PANEL_COLOR);  // 清空原有数值
  tft.setCursor(PANEL_X + 25, PANEL_Y + 70);
  tft.print(voltage, 2);  // 保留2位小数

  // 2. 更新电流数值
  tft.fillRect(PANEL_X + 25, PANEL_Y + 125, 30, 12, PANEL_COLOR);
  tft.setCursor(PANEL_X + 25, PANEL_Y + 130);
  tft.print(current, 2);

  // 3. 更新功率数值
  tft.fillRect(PANEL_X + 25, PANEL_Y + 185, 30, 12, PANEL_COLOR);
  tft.setCursor(PANEL_X + 25, PANEL_Y + 190);
  tft.print(power, 2);
}

/******************************************************************************
 * 深圳市在芯间科技有限公司
 * 淘宝旗舰店:零知派
 * 网 址:https://shop533070398.taobao.com
 * 版权说明:
 *  1.本代码的版权归【深圳市在芯间科技有限公司】所有,仅限个人非商业性学习使用。
 *  2.严禁将本代码或其衍生版本用于任何商业用途(包括但不限于产品开发、付费服务、企业内部使用等)。
 *  3.任何商业用途均需事先获得【深圳市在芯间科技有限公司】的书面授权,未经授权的商业使用行为将被视为侵权。
******************************************************************************/

系统流程图

2.4 INA219库文件解析

1)传感器软件I2C 初始化

初始化传感器对象,指定 I2C 地址和软件 I2C 总线

cpp 复制代码
INA219::INA219(const uint8_t address, SoftWire *wire)  // SoftWire使用I2C
{
  _address     = address;
  _wire        = wire;
  //  no calibrated values by default.
  _current_LSB = 0;
  _maxCurrent  = 0;
  _shunt       = 0;
  _error       = 0;
}


bool INA219::begin()
{
  if (! isConnected()) return false;
  return true;
}


bool INA219::isConnected()
{
  if ((_address < 0x40) || (_address > 0x4F)) return false;
  _wire->beginTransmission(_address);
  return ( _wire->endTransmission() == 0);
}

2)寄存器读写函数

INA219 通过 I2C 读写寄存器实现数据交互

cpp 复制代码
uint16_t INA219::_readRegister(uint8_t reg)
{
  _wire->beginTransmission(_address);
  _wire->write(reg);
  int n = _wire->endTransmission();
  if (n != 0)
  {
    _error = -1;
    return 0;
  }

  uint16_t value = 0;
  if (2 == _wire->requestFrom(_address, (uint8_t)2))
  {
    value = _wire->read();
    value <<= 8;
    value |= _wire->read();
  }
  else
  {
    _error = -2;
    return 0;
  }
  return value;
}


uint16_t INA219::_writeRegister(uint8_t reg, uint16_t value)
{
  _wire->beginTransmission(_address);
  _wire->write(reg);
  _wire->write(value >> 8);
  _wire->write(value & 0xFF);
  int n = _wire->endTransmission();
  if (n != 0)
  {
    _error = -1;
  }
  return n;
}

三、项目演示

3.1 监测过程与比较

系统实时显示电压、电流和功率信息,并通过波形图表展示历史数据变化趋势

界面包含三个主要区域

左侧波形区:实时显示电压、电流、功率的三通道波形;右侧数据区:实时数值显示,包含单位标识;底部图例:颜色标识区分不同参数

Tips

零知派标准板上的A1和A0都与Vs+引脚连接引出排针接口,通过跳线帽短接直接切换0x41、0x44和0x45 I2C总线地址,本项目将A1通过跳线帽短接,I2C地址为0x44

3.2 万用表对比测试

将万用表分别打到量程为20V的电压档以及20mA的电流档

进行电压档测试

进行电流档测试

| 参数 | 万用表测量值 | 系统测量值 | 误差 |
| 电压 | 4.90V | 4.97V | +0.07V |
| 电流 | 1.917mA | 1.893mA | -0.024mA |

功率 9.393mW 9.408mW -0.015mW

注:电压值和电流值误差控制在整个温度范围内的0.5%精度

3)通过串口输出数据,便于进一步分析

3.3 视频演示

零知标准板+INA219锂电池监测系统全功能演示

系统启动过程、实时数据监测、波形显示效果,以及在不同负载条件下系统的响应特性。可以看到当电路负载变化时,电流和功率读数实时更新,波形图表平滑滚动,系统响应迅速且稳定。

四、INA219 电流功率监测计技术讲解

INA219是一款基于I2C接口的高端电流分流和功率监控器,能够监测分流器压降和电源电压。其内部包含16位ADC、可编程增益放大器(PGA) 和精密基准电压源,能够实现高精度测量

4.1 INA219工作原理

根据芯片手册,经过分流电阻N(采样电阻)后,能够采集到的最低有效电压LSB为10uV。

利用欧姆定律计算电流公式:

电流测量:根据Rshunt(0.1Ω)分流电阻两端的电压降计算;电压测量:直接测量总线电压(支持0-26V范围);功率计算:内部乘法器实时计算功率

4.2 I2C通信协议

本项目采用软件模拟 I2C(SoftWire)与 INA219 通信

1)串行总线地址

INA219 有两个地址引脚A0 和 A1都设置为GND,该从机地址为0x40

2)软件I2C时序

总线上的所有从机在 SCL 的上升沿移入从机地址字节,其中最后一位指示要进行的是读操作还是写操作。在第九个时钟脉冲期间,被寻址的从机通过生成确认信号并将 SDA 拉至低电平来响应主机

4.3 寄存器配置

1)配置寄存器(0x00)

BRNG位(13):总线电压范围选择(0=16V, 1=32V);PG位(11-12):PGA增益设置(00=±40mV, 01=±80mV, 10=±160mV, 11=±320mV);BADC位(7-10):总线ADC分辨率和平均模式;SADC位(3-6):分流ADC分辨率和平均模式;

eg:MODE位(0-2):操作模式选择

阴影部分的值为默认值,采取连续分流和总线测量模式

2)校准寄存器(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

五、常见问题解答(Q&A)

Q1:测量值不准确怎么办?

***A:可以尝试:***重新校准传感器,确保setMaxCurrentShunt()函数的参数与实际使用的分流电阻匹配;进行零点校准,在无负载情况下测量电流值,并在代码中进行补偿

Q2:软件 I2C 通信不稳定怎么办?

***A:可以:***调整i2c_delay参数,确保通信时序正确;降低通信速率(如从 400kHz 降至 100kHz)

Q3:软件I2C通信失败如何排查?

检查引脚配置是否正确(SCL接零知标准板A5、SDA接零知标准板A4);采用I2C扫描函数确认INA219地址设置(默认0x40)

项目资源整合

INA219 库(支持 SoftWire): RobTillaart/INA219

INA219 数据手册: INA219 DataSheet

相关推荐
QH139292318805 小时前
KEYSIGHT N9030B PXA信号/频谱分析仪
科技·嵌入式硬件·集成测试
Shang180989357265 小时前
T31ZX 君正/INGENIC智能视频处理器T31ZX可提供软硬件资料T31Z采用先进的低功耗设计
嵌入式硬件·fpga开发·音视频·t31zx智能视频处理器
ahccqw5 小时前
CAN总线通信入门及实例代码(stm32f4系列)
stm32·单片机·嵌入式硬件
云栖梦泽6 小时前
Linux内核与驱动:13.从设备树到Platform平台总线
linux·运维·c++·嵌入式硬件
振南的单片机世界7 小时前
电源、复位、时钟:单片机的“生存三要素”
单片机·嵌入式硬件
charlie1145141917 小时前
嵌入式Linux驱动开发指南02——内核空间基础与硬件访问
linux·运维·c语言·驱动开发·嵌入式硬件
踏着七彩祥云的小丑7 小时前
嵌入式——小白入门
嵌入式硬件
charlie1145141918 小时前
嵌入式现代C++工程实践——第14篇:第二次重构 —— 模板登场,编译时绑定端口和引脚
开发语言·c++·stm32·安全·重构
SUNNYSPY0019 小时前
16N65-ASEMI重塑功率电子新标杆16N65
单片机