4、显存与帧缓冲:理解硬件层面的数据流动

做嵌入式GUI,很多人一上来就调算法、改刷新率,折腾半天卡顿依旧。我见过不少团队,代码写得漂亮,但跑在硬件上就是掉帧。问题出在哪?说白了,他们没搞懂数据是怎么在显存和帧缓冲之间流动的。

这一章,我们就来扒一扒硬件层面的数据通路。你只有看懂了数据怎么走,才知道瓶颈在哪。

4.1 显存:GUI的“临时画板”

显存,全称是显示存储器。你可以把它想象成一块专用的画板。CPU和GPU往上面画东西,显示控制器再从上面读数据,送给屏幕。

它和普通的内存不一样。普通内存存的是变量、堆栈,显存存的是像素。每个像素点,可能是RGB565的16位数据,也可能是ARGB8888的32位数据。显存的容量,直接决定了你能开多大的画布。

核心概念:显存是屏幕的“影子”。屏幕上的每一个点,在显存里都有一个对应的存储单元。你改显存,屏幕就跟着变。

我在一个项目里遇到过,工程师为了省成本,把显存砍到刚好够一帧。结果做滑动菜单时,画面撕裂得一塌糊涂。为什么?因为显示控制器读数据的速度,赶不上CPU写数据的速度,读到一半就被覆盖了。

4.2 帧缓冲:显存的“逻辑视图”

帧缓冲是显存在软件层面的抽象。你写代码时,不会直接操作物理地址,而是操作一个二维数组。这个数组,就是帧缓冲。

举个例子,一个320x240分辨率、RGB565格式的屏幕,帧缓冲就是一个320x240的二维数组,每个元素是16位的颜色值。

// 帧缓冲的定义(伪代码)
uint16_t framebuffer[240][320];  // 240行,320列

// 在坐标(x, y)处画一个红色像素
framebuffer[y][x] = 0xF800;  // RGB565 红色

你写framebuffer[y][x],硬件最终会映射到显存的某个物理地址。这个映射关系,由内存控制器和显示控制器共同维护。

我的习惯:在项目初期,我会先确认帧缓冲的起始地址和对齐方式。很多MCU要求帧缓冲地址按32字节对齐,否则DMA传输会出问题。这个坑,我踩过不止一次。

4.3 数据流动:从CPU到像素

数据从你的代码到屏幕,到底走了多远?我画了一张图,你看完就明白了。

显存与帧缓冲数据流动图 显存与帧缓冲数据流动图 CPU / GPU 写入像素数据 帧缓冲 二维数组 / 逻辑地址 显存 SDRAM / SRAM 写入 MMU映射 显示控制器 DMA读取 屏幕 LCD / OLED DMA读取 像素时钟 数据流动说明: 1. CPU/GPU 将像素数据写入帧缓冲(软件层) 2. 帧缓冲通过MMU映射到显存物理地址(硬件层) 3. 显示控制器通过DMA从显存读取数据 4. 显示控制器将数据按像素时钟发送到屏幕

你看,数据走了四步:

  1. CPU/GPU写入帧缓冲:你的绘图函数,最终会修改帧缓冲数组。
  2. 帧缓冲映射到显存:通过MMU(内存管理单元),逻辑地址变成物理地址。
  3. 显示控制器DMA读取:显示控制器不经过CPU,直接通过DMA从显存拿数据。
  4. 像素时钟发送到屏幕:数据按屏幕的刷新率,逐行发送。

注意:第三步和第一步是同时进行的!如果CPU写显存的速度,快于显示控制器读显存的速度,就会出现撕裂。这就是为什么我们需要双缓冲。

4.4 显存带宽:卡顿的根源

显存带宽,是衡量显存读写速度的指标。公式很简单:

显存带宽 = 显存时钟频率 × 数据位宽 × 通道数

举个例子,一个SDRAM时钟频率100MHz,数据位宽16位,单通道:

带宽 = 100MHz × 16bit × 1 = 1600Mbit/s = 200MB/s

听起来不小?我们来算一笔账。一个480x272分辨率、RGB565格式的屏幕,刷新率60Hz:

每帧数据量 = 480 × 272 × 2字节 = 261,120字节
每秒数据量 = 261,120 × 60 = 15.67MB/s

看起来200MB/s的带宽绰绰有余。但别忘了,你的CPU也在读写显存!如果你在刷屏的同时,还要做图像缩放、Alpha混合,带宽就会被吃光。

操作 带宽消耗(MB/s) 说明
屏幕刷新(480x272@60Hz) 15.67 显示控制器DMA读取
全屏填充 ~30 CPU逐像素写入
Alpha混合(半透明叠加) ~50 读-修改-写操作
JPEG解码 ~80 大量随机访问

你看,几个操作叠加,200MB/s的带宽瞬间就不够用了。卡顿就是这么来的。

我曾经在一个项目里,用STM32F429驱动800x480的屏幕。单层显示没问题,一开Alpha混合,帧率直接掉到15fps。后来查了数据手册,发现FMC(外部存储器控制器)的带宽只有不到100MB/s。换了个带32位SDRAM的芯片,问题才解决。

4.5 双缓冲:用空间换时间

理解了数据流动,双缓冲的原理就很简单了。说白了,就是在显存里开两块区域:

  • 前缓冲(Front Buffer):显示控制器正在读的区域,内容直接显示在屏幕上。
  • 后缓冲(Back Buffer):CPU正在写的区域,用户看不到。

当后缓冲绘制完成后,交换指针。显示控制器开始读新的前缓冲,CPU继续写新的后缓冲。

// 双缓冲交换逻辑(伪代码)
void swap_buffers() {
    // 等待垂直消隐(V-Sync)
    while (!is_vsync_active());
    
    // 交换帧缓冲指针
    uint16_t *temp = front_buffer;
    front_buffer = back_buffer;
    back_buffer = temp;
    
    // 更新显示控制器起始地址
    display_controller_set_address(front_buffer);
}

这样做的好处很明显:用户永远看不到“画到一半”的画面。代价是显存翻倍。

关键点:交换操作必须在垂直消隐期间完成。如果在屏幕正在刷新时交换,就会出现上半张是旧画面、下半张是新画面的撕裂现象。

4.6 显存类型:选型决定性能

不同的硬件平台,显存的类型和性能差异很大。我整理了一个对比表:

显存类型 典型带宽 延迟 典型应用
内部SRAM 200-400 MB/s 低(1-2周期) 小尺寸屏幕(320x240以下)
外部SDRAM 100-200 MB/s 中(5-10周期) 中尺寸屏幕(480x272)
外部DDR 500-2000 MB/s 中(5-10周期) 大尺寸屏幕(800x480以上)
显存专用(GRAM) 集成在LCD控制器内 极低 低功耗、小尺寸

选型时,我的建议是:先算带宽需求,再选显存类型。别一上来就上DDR,成本高、布线复杂。但也不要为了省钱用内部SRAM做800x480的屏幕,带宽肯定不够。

避坑指南:外部SDRAM的初始化时序非常敏感。我曾经因为PCB走线长度不匹配,导致SDRAM工作在50MHz都报错。后来加了等长布线,才稳定跑到100MHz。如果你用SDRAM,一定要让硬件工程师严格按数据手册走线。

4.7 本章小结

显存和帧缓冲,是嵌入式GUI的基石。你理解了数据怎么从CPU流到屏幕,就能明白:

  • 卡顿的本质,是带宽不足或访问冲突。
  • 双缓冲的核心,是用空间换时间,避免撕裂。
  • 显存选型,要算带宽、看延迟、考虑成本。