DIY USB 电流表(8):检测按键和绘制功率曲线

在前一篇 《DIY USB 电流表(7):读取和显示 INA219 电流电压数据》 中,我们已经基本完成了这个 DIY USB 电流表的核心功能开发,已经可以在屏幕上显示当前电源输入电压、负载的电流、功率以及累计电量等数据,如果不需要更多功能,已经可以结项了 😃。

但是如果想拿这个 USB 电流表在一些分析场景使用,那么还需要再增加一些功能,例如,是否可以通过这个 USB 电流表来记录给手机充电时的功率曲线,这样可以知道手机的快充是什么样的节奏来完成充电的。

这次我们就来完成这个功能,记录负载的功率历史,并绘制成曲线来显示功率趋势。

PS. 我也还是一个初学者,如果文章中有一些错误或不足,还请多多指教。

准备工作

在开始绘制功率曲线之前,同样有一些准备工作需要进行,例如在多了一个功率曲线页面之后,原来的电压数据页面怎么办,怎么切换多个页面?

在最早的功能设计中,我们已经给这个 DIY USB 电流表添加了两个按键,现在就可以通过这两个按键来完成页面切换操作了。

按键检测

按键原理图

在原理图中可以看到,两个按键已经接到 CH32V003 的 PD2、PD3 引脚,在初始化代码中,加上初始化两个引脚为输入模式。

diy-usb-meter-8-1
diy-usb-meter-8-2

按键状态结构体

为了维护按键的状态,我们先定义一个结构体来维护状态。在 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() 显示功率历史曲线页面

diy-usb-meter-8-3

屏幕画点驱动

另外,之前的显示驱动只实现了显示英文字符,这里为了绘制功率曲线,还需要实现一个画点函数,可以将每个时间点的功率根据比例绘制在屏幕指定位置上。

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 的功率上限,将并使用电子负载仪模拟了功率变化,可以看到功率的历史曲线已经正确的绘制出来了。

diy-usb-meter-8-4

在实际使用时,根据 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 项目

30 元 DIY 一个柔性灯丝氛围灯

diy-ambient-light-1

教程地址: https://xujiwei.com/blog/2024/04/diy-ambient-light/

参考资料

发表评论?

0 条评论。

发表评论


注意 - 你可以用以下 HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>