文章目录
在前一篇 《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
我就从 CH32V003-GameConsole 这个项目了解到 CH32V003 这款 MCU,并且开始尝试使用的,刚好这个项目本身包含了 0.96 寸 OLED 屏幕的驱动以及 GPIO、ADC 等的使用,与 USB 电流表的需求基本吻合,可以直接使用他其中的相关代码来加速开发。
在这个项目中,已经封装了 OLED 的驱动,并且包含了 I2C 总线的初始化等工作,在这里就可以稍加封装后拿来使用。
复制驱动代码
从 CH32V003-GameConsole 项目中,主要需要使用 OLED 和 I2C 相关的代码,将它们复制过来,放到 USB 电流表固件项目的 src/drivers
目录中。
了解 I2C 协议
当前项目中使用的 0.96 寸 OLED,它的驱动芯片是 SSD1306,即支持使用 I2C 协议进行通信,也支持使用 SPI 协议进行通信,为了有参考项目可以使用,这里使用了现成的使用 I2C 通信的屏幕模块。
I2C 协议是嵌入式开发中经常会使用到的一种通信协议,具备占用引脚少,支持一对多通信等优点。
I2C(Inter-Integrated Circuit)是一种串行通信协议,用于在集成电路(IC)之间进行短距离数据传输。它由飞利浦(Philips)公司于1982年开发,旨在简化数字电路之间的通信。
I2C协议使用两根线来进行通信:
- SDA(Serial Data Line):用于传输数据。
- 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
目录中:
字体格式
在这个字体文件中,所以字符都以二进制数据的方式保存,前面已经说过,一个字符是 8 * 16 像素的宽和高,同样因为是点阵字体,不需要考虑颜色什么的,每个像素只需要 1 bit 来表示是否要点亮,因此一个字符需要 8 * 16 = 128 bits = 16 bytes
,16 个字节来显示。
打开字体文件 ModernDos.h,可以看到字体文件中就是这样一大段十六进制数据:
因为每个字符占有 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);
}
}
显示效果
显示效果如下:
确定显示数据布局
在确定字符显示驱动没有问题之后,就可以确定一下最终 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");
测试效果
小结
到这里,我们总算离完成 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 电流表系列
- DIY USB 电流表(5):使用 VSCode + PlatformIO 搭建固件开发环境
- DIY USB 电流表(4):PCB 焊接与调试
- DIY USB 电流表(3):PCB 免费打样详解
- DIY USB 电流表(2):PCB 布局布线
- DIY USB 电流表(1):元件选型和原理图绘制
0 条评论。