1. GUI基础与刷新原理:像素点、帧缓冲区、屏幕刷新率、撕裂效应、全屏刷新 vs 局部刷新
各位同学,咱们今天正式开课。做嵌入式GUI优化,说白了就是跟屏幕上的每一个像素点较劲。你想想看,一个嵌入式设备,用户第一眼看到的就是屏幕——要是界面卡顿、撕裂、闪烁,那体验感直接归零。
我个人习惯,在讲任何优化技巧之前,先把最底层的刷新机制掰扯清楚。这就像盖房子,地基不牢,后面再花哨的装修也白搭。
1.1 像素点与帧缓冲区
先问个问题:屏幕上那个发光的点,到底是怎么被控制的?
每个像素点背后,对应着一组RGB数据。比如一个16位色的屏幕,每个像素用5位红、6位绿、5位蓝来表示。说白了,就是三个数字拼在一起,告诉屏幕这个点该亮什么颜色。
那这些数据存在哪?
存在帧缓冲区(Frame Buffer)里。帧缓冲区就是一块连续的内存区域,每个像素点按顺序排好。MCU往这块内存里写数据,屏幕控制器再从里面读数据,然后刷新到液晶面板上。
核心概念:帧缓冲区 = 显存映射到系统内存的一块区域。你往里面写什么,屏幕就显示什么。
我记得刚入行那会儿,接手一个温控器项目。屏幕总是一闪一闪的,查了半天发现是帧缓冲区的地址没对齐,DMA传输的时候数据错位了。嗯,从那以后我养成了一个习惯——分配帧缓冲区时,一定按32字节对齐。
// 帧缓冲区分配示例(STM32 + LTDC)
// 注意:缓冲区地址必须按 32 字节对齐
#define LCD_WIDTH 480
#define LCD_HEIGHT 272
#define LCD_PIXEL_SIZE 2 // RGB565 格式,每个像素2字节
// 静态分配,确保对齐
__attribute__((aligned(32)))
static uint8_t frame_buffer[LCD_WIDTH * LCD_HEIGHT * LCD_PIXEL_SIZE];
// 或者用动态分配
// uint8_t *frame_buffer = (uint8_t *)malloc(size);
// 记得检查对齐!
1.2 屏幕刷新率
屏幕刷新率,单位是Hz。60Hz就是每秒刷新60次,每次刷新间隔约16.67ms。
这个数字怎么来的?早期CRT显示器怕人眼感觉到闪烁,定了个60Hz的标准。到了LCD时代,虽然原理变了,但这个习惯保留了下来。
但嵌入式设备里,刷新率不一定固定60Hz。我见过一些低端屏只有30Hz,甚至20Hz。刷新率越低,留给MCU绘制的时间就越充裕,但画面流畅度也越差。
| 刷新率 | 每帧时间 | 适用场景 | 注意 |
|---|---|---|---|
| 30 Hz | 33.33 ms | 静态仪表、温控器 | 动画会感觉卡顿 |
| 60 Hz | 16.67 ms | 通用HMI、菜单 | 最常见的标准 |
| 120 Hz | 8.33 ms | 高端仪表、游戏 | MCU压力大 |
小技巧:如果你的MCU算力有限,可以降低刷新率来换取更复杂的UI效果。但别低于24Hz,否则人眼能明显感知到闪烁。
1.3 撕裂效应(Tearing)
撕裂效应,英文叫Tearing。你肯定见过——屏幕滚动时,画面中间出现一条错位的横线,上半部分和下半部分对不齐。
为什么会这样?
因为MCU往帧缓冲区写数据的同时,屏幕控制器也在从同一个缓冲区读数据。如果MCU写到一半,屏幕控制器开始读新的一帧,那读到的就是「半成品」——上半部分是旧数据,下半部分是新数据,中间那条线就是撕裂。
我曾经在一个医疗监护仪项目上吃过这个亏。波形滚动的时候,撕裂线刚好出现在关键数据区域,医生差点误判。从那以后,我对撕裂问题零容忍。
避坑指南:撕裂不是硬件故障,是软件同步没做好。别一上来就怀疑屏坏了,先检查你的刷新策略。
解决撕裂,常见三种方法:
- 等待垂直消隐期(VBlank)——在屏幕刷新完一帧、准备开始下一帧的间隙里更新缓冲区。这是最轻量的方案。
- 双缓冲——准备两个缓冲区,一个给屏幕读,一个给MCU写,写完再交换。这是最彻底的方案,也是咱们这门课的核心。
- 使用硬件同步信号——比如LTDC的TE(Tearing Effect)信号,告诉MCU「现在别动缓冲区」。
// 等待VBlank的伪代码示例
void wait_for_vblank() {
// 假设硬件提供了VBlank中断标志位
while (!(LTDC->ISR & LTDC_ISR_VBLANK_MASK));
// VBlank期间,安全更新缓冲区
update_frame_buffer(new_data);
// 清除标志位
LTDC->ICR = LTDC_ICR_VBLANK_MASK;
}
1.4 全屏刷新 vs 局部刷新
全屏刷新,就是每次更新都把整个帧缓冲区重绘一遍。简单粗暴,但效率低。
局部刷新,只更新画面中变化的那一小块区域。比如一个数字时钟,只有秒数在变,其他部分不变。
你想想看,一个480x272的屏幕,全屏刷新要写480x272x2 = 261,120字节。如果只更新一个50x50的按钮,只需要写5,000字节。差了50多倍!
| 刷新方式 | 传输数据量 | CPU占用 | 适用场景 |
|---|---|---|---|
| 全屏刷新 | 大(整帧) | 高 | 视频播放、全屏切换 |
| 局部刷新 | 小(脏区域) | 低 | 按钮、数字、进度条 |
但局部刷新有个坑——你得自己维护「脏区域」。说白了,就是记录哪些区域变了,然后只更新这些区域。如果脏区域管理不好,要么漏更新导致画面残留,要么更新太多等于全屏刷新。
我的经验:局部刷新配合双缓冲,效果最好。一个缓冲区做后台绘制,只画脏区域;另一个缓冲区给屏幕显示。画完后交换,既避免撕裂,又减少绘制量。
好了,基础概念就讲到这里。下面我用一张图把今天的内容串起来。
这张图把今天讲的核心流程串起来了。MCU往帧缓冲区写数据,屏幕控制器从缓冲区读数据刷新到面板。如果读写不同步,就会出现撕裂。解决思路要么是等VBlank,要么用双缓冲。刷新方式上,局部刷新比全屏刷新高效得多,但需要维护脏区域。
嗯,第一章的内容就到这。这些概念看着简单,但后面每一章都会跟它们打交道。尤其是双缓冲,咱们后面会花大篇幅来深入。