DIY USB 电流表(7):读取和显示 INA219 电流电压数据

在前一篇 《DIY USB 电流表(6):点个屏,使用 I2C 驱动 0.96 寸 OLED》 中,我们已经完成了屏幕显示驱动的开发,并且根据需求,列出了需要展示的数据项,确定了一下最终显示内容的布局。

在之前,显示的内容都是占位的测试数据,在这一节,就可以开始真正去读取 INA219 传感器的数据,将电路中测量的电压、电流等数据显示在屏幕上,这又是一节枯燥的编码工作 🙈。

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

准备工作

在开始读取 INA219 的数据之前,同样也要准备一些相关的库,例如 INA219 数据的读取、参数的配置,以及在 CH32V003-GameConsole 中所使用的 I2C 封装并未提供读取 I2C 数据方法,另外,为了计算 USB 电流表运行时间内消耗的电量,也需要有相关的计时方法。

I2C 读取方法

在 CH32V003-GameConsole 的代码中,已经有了 i2c_tx.h,提供了 I2C 写入相关的方法,对于一个游戏机来说,拥有写入方法就足够了,它只需要用 I2C 来刷新屏幕,但是在我们的 USB 电流表项目中,还需要使用 I2C 去读取 INA219 的数据,因此需要去封装一个 I2C 读取方法。

在开始编写 I2C 读取方法之前,先将 i2c_tx.c 文件中初始化 I2C 控制器的代码修改一下,启用自动发送 ACK 的标志位,防止后续读取失败。

diy-usb-meter-7-1

配置完后,在 i2c_tx.h 中添加 I2C 读取相关方法的定义:

// 开始读取 I2C 数据
void I2C_start_read(uint8_t addr);
// 从 I2C 总线读取 1 个字节
uint8_t I2C_read();
// 停止读取 I2C 数据
void I2C_read_stop(void);

接下来完成相关 I2C 读取方法的编写,在 i2c_tx.c 文件的末尾添加以下代码:

// 根据地址设置写入标志位
#define OADDR1_ADD0_Set          ((uint8_t)0x01)
// I2C 控制器读取状态
#define I2C_RECV_MODE_SELECTED  ((uint32_t)0x00020003)  /* BUSY, MSL and ADDR flags */

void I2C_start_read(uint8_t addr) {
    addr |= OADDR1_ADD0_Set;

    while(I2C1->STAR2 & I2C_STAR2_BUSY);            // wait until bus ready
    I2C1->CTLR1 |= I2C_CTLR1_START;                 // set START condition
    while(!(I2C1->STAR1 & I2C_STAR1_SB));           // wait for START generated
    I2C1->DATAR = addr;                             // send slave address + R/W bit
    while(!I2C_checkEvent(I2C_RECV_MODE_SELECTED));   // wait for address transmitted
}

uint8_t I2C_read() {
    while (!(I2C1->STAR1 & I2C_STAR1_RXNE));
    return I2C1->DATAR;
}

void I2C_read_stop(void) {
  while(!(I2C1->STAR1 & I2C_STAR1_RXNE));          // wait for last byte transmitted
  I2C1->CTLR1 |= I2C_CTLR1_STOP;                  // set STOP condition
}

完成这些之后,就可以开始适配一下 INA219 的驱动到 CH32V003 上了。

INA219 驱动适配

INA219 的驱动,我使用了一个之前在 ESP32 上使用的库 INA219_WE,将它稍微变化一下,适配到 CH32V003 上来使用。

项目地址:https://github.com/wollewald/INA219_WE

将项目代码下载下来之后,我们只需要使用 INA219_WE.hINA219_WE.cpp 两个文件,将它们复制到 USB 电流表固件项目的 src/drivers 目录中。

接下来的适配工作,主要变更的有以下几个地方:

  • 去除 Arduino 相关的头文件
  • 构造方法仅保留一个传入 I2C 地址的方法
  • 补全缺失的 Arduino 中相关方法
  • writeRegisterreadRegister 适配为 i2c_tx.c 中的相关方法

前面两点比较好修改,在 INA219_WE.h 中删除对应的代码就可以,就不多赘述,接下来的修改都在 INA219_WE.cpp 中完成。

添加头文件和缺失定义

因为默认情况下,项目中没有 byte 类型、abs、delayMicroseconds 这三个的定义,因此需要提供定义一下。

#include <debug.h>
#include "i2c_tx.h"

#define byte uint8_t
#define abs(v) (v > 0 ? v : -v)
#define delayMicroseconds(ms) Delay_Ms(ms)

适配 wrigeRegister 和 readRegister

将原代码中的这两个方法替换成以下内容,主要是将 Wire 相关的调用,替换成 i2c_tx.h 中相关方法的调用:

uint8_t INA219_WE::writeRegister(uint8_t reg, uint16_t val){
    I2C_start(i2cAddress);
    uint8_t lVal = val & 255;
    uint8_t hVal = val >> 8;
    I2C_write(reg);
    I2C_write(hVal);
    I2C_write(lVal);
    I2C_stop();
    return 0;
}

uint16_t INA219_WE::readRegister(uint8_t reg){
    uint8_t MSByte = 0, LSByte = 0;
    uint16_t regValue = 0;

    I2C_start(i2cAddress);
    I2C_write(reg);
    I2C_stop();

    I2C_start_read(i2cAddress);
    MSByte = I2C_read();
    LSByte = I2C_read();
    I2C_read_stop();

    regValue = (MSByte<<8) + LSByte;
    return regValue;
}

这样就完成了 INA219 驱动的适配,可以直接通过 INA219_WE 的类来读取 INA219 传感器数据了。

获取系统运行时间

为了计算 USB 电流表统计的电量,除了功率之后,还需要运行对应功率的时间,在默认情况下,CH32V003 SDK 并未提供类似 Arduino 中 millis() 这样的方法来获取系统运行时间,因此我们需要额外编写一个方法用来获取运行时间。

这个可以从 arduino_core_ch32 项目中获取实现,基于系统的 Tick 中断计数来实现运行时间的统计。

代码地址:https://github.com/Community-PIO-CH32V/arduino_core_ch32/blob/main/cores/arduino/ch32/clock.c

void systick_init(void)
{
    SysTick->SR  = 0;
    SysTick->CTLR= 0;
    SysTick->CNT = 0;
    SysTick->CMP = SystemCoreClock / 1000 - 1;
    SysTick->CTLR= 0xF;
    NVIC_SetPriority(SysTicK_IRQn,0xFF);
    NVIC_EnableIRQ(SysTicK_IRQn);
}

有了获取运行时间的方法之后,就可以计算累计消耗电量了。

读取 INA219 数据并显示

万事俱备,只欠东风,准备好所有驱动和相关依赖之后,就可以读取真实 INA219 的传感器数据了。

我们将 INA219 数据读取、计算相关的代码,都放一个独立的文件中来管理,就叫 data_manager.hpp 吧。

定义数据存储变量

在这里定义了采集电阻大小为 10mΩ,另外可以看到这里定义了 INA219 的 I2C 为 0x40 << ,这是因为在从 CH32V003-GameConsole 拿来使用的 I2C 驱动,是直接将读写位交给外部控制了,因此这里需要提前左移一位。

#include "drivers/INA219_WE.h"
#include <string.h>
#include "drivers/clock.h"

#define INA219_I2C_ADDRESS (0x40 << 1)
#define SHUNT_SIZE_IN_MR 10

INA219_WE ina219(INA219_I2C_ADDRESS);

int32_t measure_vbus_mv = 0;
int32_t measure_vshunt_uv = 0;
int32_t measure_current_ma = 0;
int32_t measure_power_mw = 0;
int32_t measure_cap_mwh = 0;
int32_t measure_cap_last_time = 0;

初始化 INA219 驱动

作为一个电流表,我们需要持续监测整个电路和功率,因此在这里将 INA219 初始化为持续采样模式。

void adc_setup() {
    ina219.init();
    ina219.setMeasureMode(CONTINUOUS);
    ina219.setPGain(PG_80);
    ina219.setShuntSizeInOhms(SHUNT_SIZE_IN_MR / 1000);
}

另外这里将 INA219 的增益模式设置为 PG_80,根据数据手册,表示测量压差范围为 80mV:

diy-usb-meter-7-2

这里使用的采样电阻大小为 10mΩ,在 PD 2.0 100W 的情况下,最大电流为 5A,通过计算 5A * 10mΩ = 50mV 可以得出最大压降为 50mV,因此设置测量范围为 80mV 完全够用。

读取数据并计算功率、电量

这里采用积分的方式来计算累计的电量,将当前计算出来的功率视为从上一次计算到当前时间点的持续功率,从而计算出这段时间的电量,再每次进行累计。

void adc_read_values() {
    measure_vbus_mv = ina219.getBusVoltage_V() * 1000;
    measure_vshunt_uv = ina219.getShuntVoltage_mV() * 1000;
    if (measure_vshunt_uv < 0) {
        measure_vshunt_uv = 0;
    }
    measure_current_ma = measure_vshunt_uv / SHUNT_SIZE_IN_MR;
    measure_power_mw = measure_vbus_mv * measure_current_ma / 1000;

    uint32_t now = millis();
    uint32_t period = now - measure_cap_last_time;
    measure_cap_last_time = now;
    int32_t period_cap = measure_power_mw * period / 1000;

    measure_cap_mwh += period_cap;
}

显示数据

在这里先写一个 show_power_metrics 方法将刷新屏幕,显示数据相关的操作封装起来。

这里显示数据就比较简单,将电压、电流、功率、电量四个数字格式化到缓冲区中,并通过显示驱动显示到每一行上。

void show_power_metrics() {
    char buf[17];
    display_clear();

    snprintf(buf, 17, (const char *)"Volt : %dmV", (int)measure_vbus_mv);
    display_write_line(0, buf);

    snprintf(buf, 17, (const char *)"Curr : %dmA", (int)measure_current_ma);
    display_write_line(1, buf);

    snprintf(buf, 17, (const char *)"Power: %dmW", (int)measure_power_mw);
    display_write_line(2, buf);

    snprintf(buf, 17, (const char *)"Cap  : %dmWh", (int)(measure_cap_mwh / 3600));
    display_write_line(3, buf);

    display_flush();
}

持续更新数据

最终,我们修改一下 main 方法,在死循环中持续读取 INA219 并更新到屏幕上,就完成了 USB 电流表的核心功能。

int main(void)
{
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    SystemCoreClockUpdate();
    Delay_Init();
    systick_init();

    // 初始化屏幕驱动
    display_init();
        // 初始化 INA219 驱动
    adc_setup();

    while (1) {
        // 读取 INA219
        adc_read_values();
        // 刷新屏幕显示
        show_power_metrics();
    }
}

实际运行效果

在这里,将固件烧录进 USB 电流表,并且在输出端接上负载,就可以在屏幕上看到当前负载消耗的实时功率了。

diy-usb-meter-7-3

这里使用的电源输入是 5V,可以看到 USB 电流表检测到的电压只有 4.8V 左右,这是因为通常 USB 电缆都有内阻,在有电流时,会有压降,最终到达负载时的电压就跟电源端的电压不一致了。

一般来说,更好的 USB 线材会拥有更低的内阻,在大功率充电的场景中,也会拥有发热更低的优势。

小结

至此,我们已经完成了 DIY 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>