使用 C 语言位运算操作,编写一个简单高效的单片机 OLED framebuffer 驱动

在之前做 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 贴近起点,符合硬件刷屏顺序,降低人脑与机器“逻辑反转”负担。

清屏函数

这个没什么好说的,直接将 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 条评论。

发表评论


注意 - 你可以用以下 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>