DIY USB 电流表(6):点个屏,使用 I2C 驱动 0.96 寸 OLED

在前一篇 《DIY USB 电流表(5):使用 VSCode + PlatformIO 搭建固件开发环境》 中,我们已经完成了开发 USB 电流表固件的开发环境搭建,整个开发环境基于 Visual Studio Code + PlatformIO 来完成,并且跑通了一个基本的 Demo,验证了开发环境。

固件开发的第一步,我们先将最终输出数据的显示设备,0.96 寸 OLED 屏幕给点亮,这样在后续开发过程中,也可以通过屏幕来显示一个调试数据信息。

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

准备工作

在开始开发之前,我们需要做一些准备工作,使用 PlatformIO 创建项目并不包含很多使用示例,例如如何使用 GPIO,如何使用 I2C 总线等,并且使用 platform-ch32v 时,它的函数定义与官方示例也不太相同,因此我们需要找一个类似的开源项目作为参考,从而可以更快速地完成 USB 电流表的固件开发。

参考项目 CH32V003-GameConsole

项目地址:https://github.com/wagiminator/CH32V003-GameConsole

diy-usb-meter-6-1

我就从 CH32V003-GameConsole 这个项目了解到 CH32V003 这款 MCU,并且开始尝试使用的,刚好这个项目本身包含了 0.96 寸 OLED 屏幕的驱动以及 GPIO、ADC 等的使用,与 USB 电流表的需求基本吻合,可以直接使用他其中的相关代码来加速开发。

在这个项目中,已经封装了 OLED 的驱动,并且包含了 I2C 总线的初始化等工作,在这里就可以稍加封装后拿来使用。

复制驱动代码

从 CH32V003-GameConsole 项目中,主要需要使用 OLED 和 I2C 相关的代码,将它们复制过来,放到 USB 电流表固件项目的 src/drivers 目录中。

diy-usb-meter-6-2

了解 I2C 协议

当前项目中使用的 0.96 寸 OLED,它的驱动芯片是 SSD1306,即支持使用 I2C 协议进行通信,也支持使用 SPI 协议进行通信,为了有参考项目可以使用,这里使用了现成的使用 I2C 通信的屏幕模块。

I2C 协议是嵌入式开发中经常会使用到的一种通信协议,具备占用引脚少,支持一对多通信等优点。

I2C(Inter-Integrated Circuit)是一种串行通信协议,用于在集成电路(IC)之间进行短距离数据传输。它由飞利浦(Philips)公司于1982年开发,旨在简化数字电路之间的通信。

I2C协议使用两根线来进行通信:

  1. SDA(Serial Data Line):用于传输数据。
  2. SCL(Serial Clock Line):用于同步数据传输的时钟信号。

I2C通信基于主从架构,其中主设备负责发起和控制通信,而从设备则响应主设备的请求。

对于 I2C 协议的更多其他知识,例如通信速度、时序等,可以参考这篇文章: https://zhuanlan.zhihu.com/p/362287272

计算屏幕刷新帧率

对于有显示屏幕的设备来说,相当重要的一点就是刷新时占用的时间,这将影响到以下几个方面:

  • 屏幕数据显示是否流畅,刷新越慢,会让人觉得数据更新慢,不及时
  • 单次刷新时间越长,越占用 CPU 时间,导致其他代码的运行时间减少
  • 屏幕刷新时间长也会导致有按钮操作时,让用户觉得响应不及时

CH32V003 的 I2C 可以工作在快速模式下,这个时候通信速率为 400kbit/s,我们可以根据这个计算一下 OLED 屏幕最高能达到的刷新速度。

项目中使用的屏幕是 0.96 寸 OLED 屏幕,分辨率为 128 * 64,因为是单色屏幕,每个像素只需要 1 个比特就可以表示亮还是灭,因此每次全屏刷新时的数据量为:

128 * 64 = 6912 bits

在不考虑通信中额外数据消耗时,理想情况下使用 400kbit/s 的速率可以达到的刷新率为:

400000 / 6912 = 57.87 FPS

再考虑其他代码的执行时间,应该至少可以达到 30 FPS 以上的屏幕刷新率,对于 USB 电流表这样一个项目而言,也足够让使用者感觉到数据刷新非常及时了。

字符显示驱动

在参考项目 CH32V003-GameConsole 中,它主要用来显示一些图片资源,但是我们需要显示字符,这就需要自己额外开发一下。

点阵字体

在这种低分辨率的点阵屏幕上,我们需要找一款合适一点的字体来显示内容。我们使用的屏幕分辨率为 128 * 64,为了能让显示的内容更容易看清楚,将屏幕分为 4 行,垂直方向每 16 个像素显示一行文字。

另外为了可以让显示的内容对齐,我们这里使用等宽字体,因为通常情况下,英文字符的宽度是高度的一半,因此这里可以得到每个字符占用的像素为 8 * 16 像素。

根据屏幕分辨率可以计算出来,使用这个尺寸的字体后,一行最多可以显示 16 个字符,一共 4 行,总计可以显示 64 个字符。

在网上找了一下之后,最终找到一款 ModernDos 字体,感觉比较适合用来显示 USB 电流表的数据。

字体项目地址:https://github.com/datacute/TinyOLED-Fonts

我们只需要文字项目中 src/ModernDos.h 这个文件,将它复制到 USB 电流表固件项目的 src/res 目录中:

diy-usb-meter-6-3

字体格式

在这个字体文件中,所以字符都以二进制数据的方式保存,前面已经说过,一个字符是 8 * 16 像素的宽和高,同样因为是点阵字体,不需要考虑颜色什么的,每个像素只需要 1 bit 来表示是否要点亮,因此一个字符需要 8 * 16 = 128 bits = 16 bytes,16 个字节来显示。

打开字体文件 ModernDos.h,可以看到字体文件中就是这样一大段十六进制数据:

diy-usb-meter-6-4

因为每个字符占有 16 个字节,同时这个字体文件包含了所以可见字符的数据,可见字符对应 ASCII 码的范围为 32~126,所以这些数据总共占用空间为 16 * 128 = 1520 bytes

对于数字 1 来说,它的 ASCII 为 65,经过计算找到对应的数据为:

0xf0, 0xf8, 0x8c, 0x84, 0x8c, 0xf8, 0xf0, 0x00, 0x0f, 0x0f, 0x00, 0x00, 0x00, 0x0f, 0x0f, 0x00,

将它们转换为二进制:

00000000
00010000
00011000
11111100
11111100
00000000
00000000
00000000

00000000
00001000
00001000
00001111
00001111
00001000
00001000
00000000

oled_min.c 的初始化代码中,是将 SSD1306 的显示模式设置为水平地址模式,在这种模式下,每个字节的数据对应到每列垂直方向的 8 个像素,因此我们需要将以上二进制数据转向,就可以得到对应显示到屏幕上像素是否开启。

这里需要注意,因为一个字符会占 16 个像素高,在字体文件中,是分两部分来存储的,每部分 8 个字节。

旋转后可以可以得到以下数据:

00000000
00000000
00011000
00111000
01111000
00011000
00011000
00011000
00011000
00011000
00011000
01111110
00000000
00000000
00000000
00000000

旋转之后,就可以隐约看出来数字 1 了,接下来,就是需要编写一个驱动,将字符串中的字符,在字体文件中查到数据,并通过 OLED 驱动显示到屏幕上。

显示驱动

这里新增一个 display.hpp 文件来编写显示驱动相关的代码。

为了方便处理数据,先定义一块内存区域,当作显示数据的缓存,根据前面的计算可以得到这里需要保留 1024 字节的内存来当作缓存。

// 引入 OLED 以及 I2C 驱动
#include "drivers/oled_min.h"
#include <stdio.h>
#include <string.h>

// 定义字体文件结构体
struct DCfont {
    uint8_t *data;
    uint8_t width;
    uint8_t height;
    uint8_t min, max;
};

// 引入字体文件
#include "res/ModernDos.h"

// 创建显示驱动初始化函数别名
#define display_init            OLED_init

// 创建字体文件数据别名
#define font_data               FONT8X16MDOS

// 显示缓存
constexpr int BUF_LEN = 128 * 64 / 8;
static uint8_t display_buffer[BUF_LEN] = { 0 };

接下来编写一个函数 display_write_line,将参数中的字符串,依次查找到字体数据,并写入到缓存中。

void display_write_line(int row, const char *str) {
    for (uint8_t row_offset = 0; row_offset < 2; ++row_offset) {
        int buf_offset = (row * 2 + row_offset) * 128;
        uint8_t chidx = 0;
        for (; str[chidx] != 0; ++chidx) {
            uint8_t ch = str[chidx];
            uint8_t *row_data = (uint8_t *)font_data->data + (ch - 32) * (font_data->width * font_data->height) + row_offset * font_data->width;
            for (uint8_t i = 0; i < font_data->width; ++i) {
                display_buffer[buf_offset] |= row_data[i];
                buf_offset++;
            }
        }
    }
}

在写入缓存完成后,还需要将显示缓存中的内容发送到 OLED 的驱动上。

void display_flush() {
    OLED_setpos(0, 0);
    for (int i = 0; i < BUF_LEN; ++i) {
        if (i % 16 == 0) {
            if (i > 0) {
                OLED_data_stop();
            }
            OLED_data_start();
        }
        I2C_write(display_buffer[i]);
    }
    OLED_data_stop();
}

这样就完成了一个简单的字符显示驱动代码。

测试显示

完成显示驱动代码的编写后,先来测试一下所有字符的显示效果吧,使用以下代码进行测试,将 main.cpp 中的主函数代码替换为以下内容:

// 引入显示驱动
#include "display.hpp"

int main(void)
{
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    SystemCoreClockUpdate();
    Delay_Init();
    USART_Printf_Init(115200);

    display_init();

    display_write_line(0, "ABCDEFGHIJKLMNOP");
    display_write_line(1, "QRSTUVWXYZabcdef");
    display_write_line(2, "ghijklmnopqrstuv");
    display_write_line(3, "wxyz1234567890.%");

    display_flush();

    while (1) {
        Delay_Ms(1000);
    }
}

显示效果

显示效果如下:

diy-usb-meter-6-5

确定显示数据布局

在确定字符显示驱动没有问题之后,就可以确定一下最终 USB 电流显示内容的布局了。

作为一个 USB 电流表,在进行功能设计时,已经大概确定了要在屏幕上显示输入电压、当前电流、当前功率、累计消耗电量这几个字段的数据,最终将数据视图设计为按顺序显示这 4 个字段。

目前还没有接入 ADC 数据,先使用以下测试代码来模拟一下:

    display_write_line(0, "Volt : 5000mV");
    display_write_line(1, "Curr : 500mA");
    display_write_line(2, "Power: 2500mW");
    display_write_line(3, "Cap  : 0mWh");

测试效果

diy-usb-meter-6-6

小结

到这里,我们总算离完成 USB 电流表项目在外观上又进了一步,已经可以将想要显示的内容正确地显示在屏幕上,并且在这过程中也大概了解了 I2C、OLED 驱动、点阵字体文件相关的知识,即使以后想更换为中文显示,也不是太困难的事。

接下来就可以开始尝试读取 INA219 的数据,将最核心的功能完成,显示电压、电流等数据。

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 电流表系列

参考资料

发表评论?

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>