文章目录
在前一篇 《DIY USB 电流表(7):读取和显示 INA219 电流电压数据》 中,我们已经基本完成了这个 DIY USB 电流表的核心功能开发,已经可以在屏幕上显示当前电源输入电压、负载的电流、功率以及累计电量等数据,如果不需要更多功能,已经可以结项了 😃。
但是如果想拿这个 USB 电流表在一些分析场景使用,那么还需要再增加一些功能,例如,是否可以通过这个 USB 电流表来记录给手机充电时的功率曲线,这样可以知道手机的快充是什么样的节奏来完成充电的。
这次我们就来完成这个功能,记录负载的功率历史,并绘制成曲线来显示功率趋势。
PS. 我也还是一个初学者,如果文章中有一些错误或不足,还请多多指教。
准备工作
在开始绘制功率曲线之前,同样有一些准备工作需要进行,例如在多了一个功率曲线页面之后,原来的电压数据页面怎么办,怎么切换多个页面?
在最早的功能设计中,我们已经给这个 DIY USB 电流表添加了两个按键,现在就可以通过这两个按键来完成页面切换操作了。
按键检测
按键原理图
在原理图中可以看到,两个按键已经接到 CH32V003 的 PD2、PD3 引脚,在初始化代码中,加上初始化两个引脚为输入模式。
按键状态结构体
为了维护按键的状态,我们先定义一个结构体来维护状态。在 ButtonState
结构体中,定义了一个 ts 字段,这个用来记录按键状态变化时的时间戳,这样可以在状态变化时,例如从低电平变化为高电平时,计算出来按键按下去的持续时间,如果超过 3000 毫秒,就认为是长按事件。
#pragma once
#include "drivers/clock.h"
#include "drivers/gpio.h"
#define BTN_LEFT 0
#define BTN_RIGHT 1
#define LONG_PRESS_PERIOD 3000
struct ButtonState {
uint8_t pin;
uint8_t state;
uint32_t ts;
};
ButtonState buttons[] = {
{
.pin = PD2,
.state = 1,
.ts = 0,
},
{
.pin = PD3,
.state = 1,
.ts = 0,
},
};
这里的默认 state
为 1 是因为在原理图中添加了上拉电阻,默认 GPIO 状态为高电平。
初始化按键检测
初始化按键检测比较简单,只需要将 PD2、PD3 两个 GPIO 设置为输入模式即可。
void button_setup() {
PIN_input(buttons[BTN_LEFT].pin);
PIN_input(buttons[BTN_RIGHT].pin);
}
按键事件检测
int check_button(int index) {
int new_state = PIN_read(buttons[index].pin);
int old_state = buttons[index].state;
int action = BUTTON_NO_ACTION;
if (new_state != old_state) {
if (new_state == 1) {
if (millis() - buttons[index].ts > LONG_PRESS_PERIOD) {
action = BUTTON_LONG_CLICK;
} else {
action = BUTTON_CLICK;
}
}
buttons[index].state = new_state;
buttons[index].ts = millis();
}
return action;
}
实现显示页面切换
为了实现页面切换,先定义两个页面 ID,并且将当前页面 ID 保存在 current_page
变量中,默认为数据字段页面。
int current_page = 0;
#define PAGE_METRICS 0
#define PAGE_HISTORY 1
#define PAGE_COUNT 2
在 main
函数中先初始化一下按钮设置,再在 while 循环中的检测按键事件,根据 current_page
的值来确定显示哪个页面:
- current_page = 0 时,调用
show_power_metrics()
显示数据字段页面 - current_page = 1 时,调用
show_power_history()
显示功率历史曲线页面
屏幕画点驱动
另外,之前的显示驱动只实现了显示英文字符,这里为了绘制功率曲线,还需要实现一个画点函数,可以将每个时间点的功率根据比例绘制在屏幕指定位置上。
void display_draw_dot(int x, int y) {
int row = y / 8;
int offset = row * 128 + x;
int pixel_offset = y % 8;
u8 b = display_buffer[offset];
b = b | (1 << pixel_offset);
display_buffer[offset] = b;
}
完成以上准备功能,就可以开始实现真正的业务功能了。
记录功率曲线
为了记录功率的历史数据,先在 data_manager.hpp
中添加一块缓存区域,用于存放历史的功率数据。
因为我们使用的屏幕分辨率是 128 * 64,可以将历史数据点数量定义为 128 个,数据点再多,屏幕也显示不下,意义不大了。
#define POWER_HISTORY_PERIOD 60000
#define POWER_HISTORY_MAX_COUNT 128
u16 power_history[POWER_HISTORY_MAX_COUNT] = { 0 };
int power_history_index = 0;
int power_history_count = 0;
uint32_t power_history_last_ts = 0;
在 adc_read_values
方法中,读取完 INA219 的数据,就可以将它记录下来存放到 power_history
中了。
不过这里存在一个问题,adc_read_values
是在主循环中一直被调用的,为了避免数据被一直刷新,可以定义一个时间间隔,在超过确定的时间间隔后才将数据记录下来。
这里通过 POWER_HISTORY_PERIOD
定义了每 60000 毫秒记录一次数据,即每分钟会有一个数据点产生。
然后在 adc_read_values
的最后,加上数据保存逻辑。
void adc_read_values() {
// ..... 数据读取代码
measure_cap_mwh += period_cap;
// 记录功率数据
if (now - power_history_last_ts >= POWER_HISTORY_PERIOD) {
int save_index = (power_history_index + power_history_count) % POWER_HISTORY_MAX_COUNT;
power_history[save_index] = measure_power_mw;
power_history_count = power_history_count + 1;
if (power_history_count >= POWER_HISTORY_MAX_COUNT) {
power_history_count = POWER_HISTORY_MAX_COUNT;
power_history_index = (power_history_index + 1) % POWER_HISTORY_MAX_COUNT;
}
power_history_last_ts = now;
}
}
这里使用了一个简单的 Ring Buffer 实现数据的循环覆盖记录,总是保留最新数据。关于 Ring Buffer 的介绍可以参考这篇文章: https://zhuanlan.zhihu.com/p/534098236 。
绘制功率曲线
最后在 main.cpp
中加上 show_power_history
方法,在切换到功率历史页面的时候,绘制曲线出来。
void show_power_history() {
display_clear();
display_write_line(0, "Power History");
for (int i = 0; i < power_history_count; ++i) {
u16 p = power_history[(power_history_index + i) % POWER_HISTORY_MAX_COUNT];
int32_t y = 63 - (p * 63 / 100000);
display_draw_dot(i, y);
}
display_flush();
}
这段代码中 int32_t y = 63 - (p * 63 / 100000);
用来计算功率值对应到屏幕上点的位置,因为屏幕的 Y 轴坐标是向下增长的,而绘制曲线时,Y 轴是向上增长的,因此需要使用屏幕高度减去功率占最大功率的比例。
另外这里的 Y 轴上限使用了 100000mW 作为参考,即我们的 USB 电流最大可以测量 100W 的功率,当然这会导致 Y 轴的分辨率变低,每个像素点可以表示约 1.5W 的数据范围。如果实际使用过程中不会有这么大的功率测量需求,也可以降低上限。
实际运行效果
这里为了更好地展示曲线效果,使用了 30W 的功率上限,将并使用电子负载仪模拟了功率变化,可以看到功率的历史曲线已经正确的绘制出来了。
在实际使用时,根据 128 个数据点,以及 1 分钟的记录间隔来计算,一屏可以显示历史 2 小时的功率变化记录。
小结
在完成功率曲线绘制后,我们已经完成了 DIY USB 电流表固件的大部分功能开发工作,并且已经使用到了 PCB 上的所有硬件,包括屏幕、INA219、按键。
固件上还有什么功能可以完善一下呢?目前这个 USB 电流表的数据都只保存在内存中,一旦电源输入断电,或者是负载停止耗电,整个 USB 电流表中记录的数据都会丢失,如果我们想要通过这个 USB 电流表来统计一个充电宝实际输出的电量,那么就不能达到目标了。
下一篇我们来给 USB 电流表加上数据保存功能,使它可以成为一个电池容量测量工具。
USB 电流表开源地址
这个 USB 电流表所有资料已经开源,可以在以下仓库中获取,包含固件代码、PCB 生产 Gerber 文件、原理图和外壳 STL 文件。
https://github.com/ohdarling/CH32V003-USBMeter
硬件相关的源文件已经在立创开源平台开源,访问以下地址可以进行一键 PCB 下单和一键 BOM 配单操作:
https://oshwhub.com/wandaeda/ji-yu-ch32v003-de-usb-dian-liu-biao
DIY USB 电流表系列
- DIY USB 电流表(7):读取和显示 INA219 电流电压数据
- DIY USB 电流表(6):点个屏,使用 I2C 驱动 0.96 寸 OLED
- DIY USB 电流表(5):使用 VSCode + PlatformIO 搭建固件开发环境
- DIY USB 电流表(4):PCB 焊接与调试
- DIY USB 电流表(3):PCB 免费打样详解
- DIY USB 电流表(2):PCB 布局布线
- DIY USB 电流表(1):元件选型和原理图绘制
其他 DIY 项目
30 元 DIY 一个柔性灯丝氛围灯
教程地址: https://xujiwei.com/blog/2024/04/diy-ambient-light/
0 条评论。