4、单缓冲模式剖析:直接写入帧缓冲区

好,咱们今天来聊聊单缓冲。这是最直接、最原始的显示方式。说白了,就是你的程序直接往显存里写数据,写完就显示。听起来很简单对吧?但简单背后,藏着不少坑。

我刚开始做嵌入式图形开发时,用的就是单缓冲。那时候觉得,这有什么难的?直接写不就完了。结果被现实狠狠教育了一顿。嗯,咱们今天就把它彻底讲透。

4.1 直接写入帧缓冲区:最朴素的显示方式

单缓冲模式下,整个系统只有一个帧缓冲区。你的绘图操作,比如画点、画线、填充矩形,都是直接对这个缓冲区进行读写。而显示控制器(Display Controller)呢,也在同时读取这个缓冲区,把数据送到屏幕上。

你可以想象成只有一个黑板。老师(CPU)在上面写板书,学生(显示控制器)同时在看。老师写一个字,学生就看一个字。这就是单缓冲的本质。

核心特征:

  • 只有一个帧缓冲区,CPU 和显示控制器共享访问
  • 绘图操作直接修改当前正在显示的内容
  • 硬件结构简单,内存占用少

我建议你在学习双缓冲之前,先把单缓冲搞明白。因为双缓冲的很多设计思路,正是为了解决单缓冲的痛点。你想想看,如果连痛点都不知道,怎么理解解决方案呢?

4.2 撕裂现象产生的根本原因

撕裂(Tearing),这是单缓冲最让人头疼的问题。我在一个工业 HMI 项目里遇到过,屏幕上的仪表指针在快速转动时,总会出现一条水平的分割线,上半部分和下半部分对不齐。客户看了直摇头。

为什么会这样?

咱们来拆解一下。显示控制器读取帧缓冲区是有固定节奏的,通常从左上角开始,一行一行往下扫,扫到右下角算一帧。这个节奏叫「刷新率」,比如 60Hz 就是一秒钟扫 60 遍。

假设现在屏幕正在扫到第 300 行,你的程序突然往帧缓冲区里写入了新的一帧数据。那么问题来了:

  • 第 1~299 行:显示的是旧数据
  • 第 300 行及以后:显示的是新数据

这一帧画面里,上半截是旧的,下半截是新的。中间那条分界线,就是撕裂。

避坑指南:我曾经在一个 320x240 的小屏上调试撕裂问题,折腾了两天才发现,是因为 DMA 传输和显示刷新周期没对齐。记住:撕裂的本质是「读写冲突」,不是「速度不够快」。

撕裂的严重程度取决于两个因素:

因素 影响
刷新率 刷新率越低,撕裂窗口期越长,越容易看到撕裂
绘图耗时 绘图时间越长,被刷到一半的概率越大
缓冲区大小 缓冲区越大,撕裂的视觉影响范围越广

4.3 单缓冲的性能瓶颈

除了撕裂,单缓冲还有几个明显的性能问题。我习惯把它们归纳为「三慢」:

  1. 绘图慢:每次绘图都要等待上一次操作完成。因为只有一个缓冲区,你不能一边画一边显示。
  2. 响应慢:如果绘图操作和显示刷新周期冲突,你可能需要等到下一个垂直消隐期(VBlank)才能开始绘图。这个等待时间是不确定的。
  3. 更新慢:对于复杂画面,比如地图缩放、3D 渲染,单帧绘图时间可能超过一个刷新周期。这时候画面会明显卡顿。

你想想看,如果一帧画面需要 20ms 才能画完,而刷新周期是 16.67ms(60Hz),那每一帧都会和下一帧的刷新重叠。结果就是:撕裂 + 卡顿,双重打击。

个人经验:在低分辨率、低刷新率的场景下,单缓冲其实够用。比如 128x64 的单色 OLED 屏,刷新率只有 30Hz,画面简单,单缓冲完全没问题。别盲目上双缓冲,资源开销不是白给的。

4.4 一个简单的单缓冲绘图示例(伪代码)

下面这个例子,展示了一个典型的单缓冲绘图流程。我用伪代码写,方便你理解核心逻辑。

// 单缓冲绘图示例(伪代码)
// 假设:帧缓冲区地址为 FRAME_BUFFER,分辨率 320x240,像素格式 RGB565

// 1. 初始化帧缓冲区
void init_framebuffer() {
    // 清空缓冲区为黑色
    memset(FRAME_BUFFER, 0, 320 * 240 * 2);
}

// 2. 绘制一个矩形
void draw_rect(int x, int y, int w, int h, uint16_t color) {
    for (int row = y; row < y + h; row++) {
        for (int col = x; col < x + w; col++) {
            // 直接写入帧缓冲区
            FRAME_BUFFER[row * 320 + col] = color;
        }
    }
}

// 3. 主循环
void main_loop() {
    while (1) {
        // 清除画面
        clear_screen(BLACK);
        
        // 绘制新内容
        draw_rect(50, 50, 100, 80, RED);
        draw_circle(160, 120, 40, BLUE);
        draw_text("Hello", 10, 10, WHITE);
        
        // 注意:这里没有等待 VBlank
        // 绘制完成后,显示控制器可能正在读取缓冲区
        // 撕裂随时可能发生
        
        // 处理其他任务
        handle_input();
    }
}

看到问题了吗?draw_rect 和 draw_circle 直接往 FRAME_BUFFER 里写数据。如果显示控制器刚好在读取同一块区域,撕裂就出现了。

而且,整个绘图过程没有和显示刷新周期同步。你画你的,它扫它的,互不商量。这就是单缓冲的典型特征——简单粗暴,但隐患重重。

关键点总结:

  • 单缓冲 = 一个缓冲区,CPU 和显示控制器共享
  • 撕裂 = 读写冲突导致画面上下不一致
  • 性能瓶颈 = 绘图慢 + 响应慢 + 更新慢
  • 适用场景 = 低分辨率、低刷新率、简单画面

下面这张图,帮你直观理解单缓冲的工作流程和撕裂产生的位置。

单缓冲模式工作流程与撕裂产生原理 CPU / 绘图程序 帧缓冲区 显示控制器 写入数据 读取数据 撕裂产生区域 读写冲突发生点 显示刷新周期(60Hz ≈ 16.67ms) 屏幕扫描过程(逐行扫描) 撕裂分界线 当 CPU 写入与显示控制器读取同时发生时,在扫描位置产生撕裂