文章目录
在之前做 CH32V003 系列项目时,总是会用到 0.96 寸 12864 OLED 屏幕,用来显示数据和进行用户交互操作。一开始是直接用了 CH32V003-GameConsole 项目中的 oled_min.c
,不过那个是只用来显示一个位图,并且还不太能支持任意位置显示,因此就需要改造一番。
另外之前在做电流表项目时,也找了好多点阵字体,发现都是用了一个 DCfont
的结构体,如果能兼容这个格式,后续项目中使用不同字体时就可以很方便的替换了。
至于为什么要自己写这个库,是因为这个是在 CH32V003 上用的,16KB Flash + 2KB SRAM 的资源相当紧张,没办法使用 Arduino 中成熟的库,只好自己写一个,顺便学习一下。
字体及位图数据结构
DCfont
这个数据结构如下所示。
struct DCfont {
uint8_t *data;
uint8_t width;
uint8_t height;
uint8_t min, max;
};
刚好对于单色 XBMP 图像来说,在渲染时也差不多是需要相同的参数,刚好用同一个函数来同时兼容绘制 DCfont
和 XBMP 了。
为什么使用 framebuffer
对于常用的 12864 OLED 来说,OLED 控制器通常把竖直方向 8 个像素打包成 1 个字节,存为“页(page)”,控制器并不能跨页显示零散的像素。
在原来的 TinyConsole 项目中,位图是直接发送到 OLED 驱动芯片的显存中,这样的话在显示位图时,Y 坐标就只能按 8 的倍数进行设置。
因此如果想要位图在任意位置能显示,或者想画点,就必须需要先在 MCU 中处理好,再发送到 OLED 控制器中。
对于 128x64 分辨率的 OLED 来说,如果需要在内存中保存全屏幕的像素数据,就需要 128*64/8=1,024
字节,刚好 1KB。
虽然对于 CH32V003 的 2K 内存来说,1KB 有点大,不过为了能方便显示位图,还是可以在其他地方省一省的 🙈。
另外 WCH 还推出了 CH32V003 的升级款 CH32V006,拥有 8KB 内存,这样就不用担心内存不够用了 😃。
核心函数设计
要实现这个驱动,主要就是定义一个 framebuffer,并且编写绘制位图函数就可以了。
framebuffer 定义
直接分配一块 1KB 的内存用作 OLED 的 framebuffer。
#define BUF_LEN (128 * 64 / 8)
static uint8_t display_buffer[BUF_LEN] = { 0 };
核心函数 display_draw_xbmp
void display_draw_xbmp(int x, int y, int w, int h, const uint8_t *data) {
int byte_offset = y % 8;
for (int tx = 0; tx < w; ++tx) {
int px = tx + x;
if (px >= 128) {
break;
}
if (px < 0) {
continue;
}
uint8_t prev_b = 0;
int ty_len = (h / 8) + (h % 8 > 0 ? 1 : 0);
int py = 0;
for (int ty = 0; ty < ty_len; ++ty) {
py = y / 8 + ty;
if (py >= 8) {
continue;
}
uint8_t b = *(data + (ty * w + tx));
uint8_t b2 = b;
b = (b << byte_offset) | prev_b;
display_buffer[py * 128 + px] |= b;
prev_b = b2 >> (7 - byte_offset);
}
py = y / 8 + ty_len;
if (prev_b > 0 && py < 8) {
display_buffer[py * 128 + px] |= prev_b;
}
}
}
设计拆解
- 非 8 倍数 Y 拆解
(b << byte_offset)
和b2 >> (7 - byte_offset)
- 使用 位移 操作,将分散在两个页中的像素拆分出来。
- 与已有像素混合
display_buffer[py * 128 + px] |= b;
- 使用 位与 操作,将要显示的像素与 framebuffer 中已有的像素混合。
- 裁剪逻辑
if (px >= 128) break;
—— 右边界溢出直接结束当前列循环,避免不必要计算;if (px < 0) continue;
—— 左边界溢出跳过写入,保留下一列机会。
- 尾页残片处理
- 最后一页外还可能残留高位像素,需要判断并写入 framebuffer,并且仅当仍在屏幕范围内才写入,防止越界。
if (prev_b > 0 && py < 8) { ... }
- 位移方向选择
- OLED 页模式是“最低位在上”。左移
b << byte_offset
恰好让位 0 贴近起点,符合硬件刷屏顺序,降低人脑与机器“逻辑反转”负担。
- OLED 页模式是“最低位在上”。左移
清屏函数
这个没什么好说的,直接将 framebuffer 全部置 0 就可以实现清屏效果。
void display_clear() {
memset(display_buffer, 0, BUF_LEN);
}
相比直接写 OLED 显存的优势
相对直接将显示数据写入到 OLED 显存,用 framebuffer 的方式,就可以实现一些额外的效果。
实现启动 Logo & 开机动画
因为可以将位图在任意 Y 坐标,因此可以通过变化绘图时 Y 坐标的参数,实现在 Y 轴方向的动画。
实时波形/柱状图叠加
相比直接写 OLED 控制器会覆盖已有的像素数据,使用 framebuffer 可以将新数据与原有数据进行叠加,这样在显示曲线图等类型的图表时,可以在绘制图表的同时而不会导致标尺等图像数据丢失。
更容易兼容中文字库
对于中文字库来说,只需要修改 DCfont
的定义,将位图尺寸定义为例如常见 16x16 像素,就可以直接显示中文了。
全屏刷新,减少闪烁
虽然每次绘制都需要发送全屏数据,在使用 I2C 400Kbps 模式时,理论需要 23ms,但是对于一般使用场景来说,也足够了,可以达到 30 FPS。而这带来的好处就是减少屏幕闪烁,对于感官来说效果更好。
另外如果使用 SPI + DMA,可以达到更高的刷新率,不再成为 MCU 应用运行的瓶颈。
总结
总的来说,写一个这样的驱动也不是复杂的事,主要需要考虑的就是位操作、边界检测,不过感觉也算是一个不错的练手过程 🙈。
0 条评论。