2. 嵌入式GUI基础

做嵌入式GUI优化,绕不开一个核心问题:CPU到底该不该管画图?

我早年做第一块彩色屏驱动时,所有像素点都是CPU一个个算出来的。刷个320×240的界面,CPU占用率直接飙到80%。后来换了带硬件加速的芯片,同样的界面,CPU占用率掉到15%。差距就这么大。

这一章,我把硬件加速的几个关键模块拆开讲。你理解透了,选型、调优心里就有底了。

2.1 2D加速引擎:把CPU从像素搬运中解放出来

2D加速引擎,说白了就是一块专门干“图形搬运工”的硬件。它不负责3D渲染,只做一件事:快速搬运和填充像素块。

核心能力清单:

  • 位块传输(BitBLT):把显存里的一块矩形区域,快速拷贝到另一块区域。支持透明叠加、Alpha混合。
  • 矩形填充(Fill):用指定颜色快速填充一块区域。画背景、画进度条底色,全靠它。
  • 颜色格式转换:比如从RGB565转ARGB8888,硬件一步到位,不用CPU逐像素算。
  • 旋转/镜像:硬件级90°/180°/270°旋转,横竖屏切换零开销。

我遇到过最典型的坑:用CPU做窗口拖动时的背景刷新。每拖动一帧,CPU要算一遍脏矩形、拷贝一遍背景图。后来换成2D引擎的BitBLT,代码量少了,帧率反而翻倍。

我的习惯:只要芯片手册里写了“2D Graphics Accelerator”,我就优先用它做图层合成和背景刷新。CPU只负责算逻辑,不碰像素。

2.2 DMA2D控制器:给显存搬运装上涡轮增压

DMA2D是2D加速引擎的“亲兄弟”。它比普通DMA多了什么?多了对2D数据的理解能力。

普通DMA只知道“从地址A搬N个字节到地址B”。DMA2D知道“从地址A搬一个宽W高H的矩形,每行间隔P个像素,搬到地址B的另一个矩形里,中间还要做颜色格式转换”。

特性 普通DMA DMA2D
传输单位 字节/字 矩形区域(宽×高)
行间距处理 不支持,需CPU干预 硬件自动处理行偏移
颜色转换 不支持 支持RGB565/888/ARGB互转
Alpha混合 不支持 硬件级半透明混合
典型延迟 低(纯搬运) 略高(带处理)但远低于CPU

我曾经在一个手表项目里,用DMA2D做秒针的旋转刷新。传统做法是CPU算出旋转后的新像素,再刷到显存。用DMA2D后,我只需要把原始秒针位图放在SRAM里,配置好旋转角度和输出位置,DMA2D自己算、自己搬。CPU全程睡觉。

注意:DMA2D不是万能的。它适合大块连续区域的搬运。如果你只改几个像素(比如光标闪烁),用DMA2D的配置开销反而比CPU直接写更大。小修改还是让CPU干。

2.3 GPU管线与Shader:不只是3D的专利

很多做嵌入式的朋友一听“GPU”就觉得是跑3D游戏的。其实嵌入式GPU的管线,对2D GUI同样有用。

典型的GPU渲染管线分这几步:

  1. 顶点处理:把四边形(两个三角形)的顶点坐标算好。
  2. 光栅化:把三角形变成像素点。
  3. 片段着色:对每个像素算颜色。这里就是Shader发挥的地方。
  4. 输出合并:把算好的像素写进帧缓冲。

你可能会问:“我画个按钮,用得着这么复杂吗?”

嗯,直接画确实用不着。但如果你要做圆角阴影、高斯模糊、动态渐变,用Shader写一个几十行的片段着色器,比CPU算几万次像素快得多。

一个真实的优化案例:

我做车载仪表盘时,需要实现一个半透明反射效果(玻璃盖板上的倒影)。用CPU逐像素算Alpha混合,每帧耗时12ms。改用GPU的片段着色器,每帧降到0.8ms。代码量从200行C变成30行GLSL。

当然,不是所有MCU都带GPU。但如果你选型的芯片有,别浪费。哪怕只用来做图层合成和Alpha混合,也值了。

2.4 帧缓冲(FrameBuffer)机制:显存怎么管才高效

帧缓冲,就是显存里那块存着当前屏幕画面的区域。听起来简单,但管理方式直接影响性能。

常见的帧缓冲策略有三种:

策略 原理 优点 缺点
单缓冲 只有一个显存区,CPU和LCD控制器共用 省内存 画面撕裂、闪烁
双缓冲 两个显存区,一个显示,一个绘制 无撕裂,流畅 内存翻倍
三缓冲 三个显存区,绘制、显示、等待各一个 最高帧率,抗抖动 内存占用大,延迟略增

我个人习惯:能双缓冲就双缓冲。单缓冲的撕裂问题在GUI上特别明显——你拖动一个窗口,上半截和下半截刷新不同步,看着就像屏幕被撕开了。

避坑指南:我曾经在一个低端MCU上强行用三缓冲,结果内存不够,频繁触发缺页中断,帧率反而比双缓冲还低。选缓冲策略前,先算清楚:分辨率×像素深度×缓冲数 ≤ 可用显存。别贪多。

另外,帧缓冲的对齐也很关键。很多LCD控制器要求帧缓冲起始地址按32字节对齐,每行像素数也要对齐到某个宽度。不对齐的话,DMA传输会出错,或者性能骤降。

// 帧缓冲对齐示例(以STM32 LTDC为例)
#define LCD_WIDTH  480
#define LCD_HEIGHT 272
#define PIXEL_SIZE  2  // RGB565

// 每行实际占用的字节数(需对齐到32字节)
#define LINE_BYTES  ((LCD_WIDTH * PIXEL_SIZE + 31) & ~31)

// 帧缓冲总大小
#define FB_SIZE     (LINE_BYTES * LCD_HEIGHT)

// 分配时确保地址对齐
uint8_t framebuffer[FB_SIZE] __attribute__((aligned(32)));

这段代码里,LINE_BYTES的计算就是对齐的关键。不加这个对齐,你可能会遇到“画面偏移”、“颜色错乱”等诡异问题。

2.5 一张图看懂硬件加速体系

下面这张图,把上面讲的几个模块串起来。你对照着看,思路会更清晰。

嵌入式GUI硬件加速体系结构图 嵌入式GUI硬件加速体系 CPU(应用逻辑) 2D加速引擎(BitBLT/填充/旋转) DMA2D控制器 GPU管线 + Shader 帧缓冲(FrameBuffer) LCD/OLED显示屏 说明:CPU只下发绘制命令,实际像素搬运和处理由2D引擎/DMA2D/GPU完成,最后写入帧缓冲刷新显示

从这张图你能看到:CPU在最上层,只负责下发命令。真正的像素级苦活——搬运、旋转、混合、格式转换——全由下面的硬件模块扛了。帧缓冲是最终汇合点,所有绘制结果都写到这里,再由显示控制器扫出去。

总结一句话:硬件加速的本质,就是把CPU从“逐像素计算”中解放出来,让它专心干它最擅长的事——跑逻辑、管事件。剩下的,交给专用硬件。