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渲染管线分这几步:
- 顶点处理:把四边形(两个三角形)的顶点坐标算好。
- 光栅化:把三角形变成像素点。
- 片段着色:对每个像素算颜色。这里就是Shader发挥的地方。
- 输出合并:把算好的像素写进帧缓冲。
你可能会问:“我画个按钮,用得着这么复杂吗?”
嗯,直接画确实用不着。但如果你要做圆角阴影、高斯模糊、动态渐变,用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 一张图看懂硬件加速体系
下面这张图,把上面讲的几个模块串起来。你对照着看,思路会更清晰。
从这张图你能看到:CPU在最上层,只负责下发命令。真正的像素级苦活——搬运、旋转、混合、格式转换——全由下面的硬件模块扛了。帧缓冲是最终汇合点,所有绘制结果都写到这里,再由显示控制器扫出去。
总结一句话:硬件加速的本质,就是把CPU从“逐像素计算”中解放出来,让它专心干它最擅长的事——跑逻辑、管事件。剩下的,交给专用硬件。