深圳营销型网站建设成都市网站建设

南通鑫旺饲料添加剂厂 2026/09/09 19:20:17

如何让ST7789显示不再“闪瞎眼”?双缓冲实战全解析

你有没有遇到过这种情况:在STM32或ESP32上驱动一块小小的240×320的TFT屏幕,画个渐变色条或者刷新波形图时,画面像老电视一样“撕裂”,甚至每动一下就白光一闪?别急——这并不是你的代码写得差,而是你还在用单缓冲绘图的老办法

我曾经也踩过这个坑。直到有一天,我把项目从“能看”升级到“好看”的过程中,终于下定决心搞清楚:为什么别人家的界面丝滑流畅,而我的却像幻灯片翻页?

答案就是——双缓冲(Double Buffering)

今天,我就带你一步步拆解ST7789上实现双缓冲的核心逻辑,不讲空话,只讲你能立刻上手的实战技巧。


一、先说痛点:为什么ST7789容易“闪”?

我们先来直面问题根源。

ST7789是一款非常流行的TFT驱动芯片,支持SPI通信、RGB565格式,最大分辨率240×320。它便宜、好买、资料多,是嵌入式开发者的首选之一。但它有个致命弱点:没有独立显存,所有像素数据都靠MCU提供

这意味着什么?

当你调用draw_pixel(x, y, color)的时候,CPU其实是通过SPI一条条把数据发给ST7789的GRAM(图形内存)。而与此同时,屏幕本身也在不停地扫描显示当前内容。

于是就出现了经典的“画面撕裂”场景:

你想画一个移动的小方块。
刚开始清屏 → 开始画 → 屏幕刚好扫到中间 → 显示的是“上半空白 + 下半有方块”。
用户看到的就是——撕裂!

更糟的是,如果你采用“先清屏再重绘”的策略,整个屏幕会先变白,再慢慢画出来,视觉上就是明显的“闪烁”。

这不是硬件缺陷,这是绘制与显示未分离导致的必然结果。


二、解决之道:双缓冲是怎么“骗过眼睛”的?

想象一下电影院是怎么放电影的:胶片是一帧一帧切换的,但我们看到的是连续动作。关键就在于——画面切换发生在观众看不见的时候

双缓冲正是用了同样的原理。

它的核心思想只有三句话:

  1. 我有两个画布:一个正在展示(前缓冲),另一个你在背后偷偷画(后缓冲)。
  2. 你画完了告诉我一声,我立刻换画布。
  3. 换的过程要快,最好在用户没察觉时完成。

这样一来,用户永远只看到完整的画面,不会看到“正在画”的中间状态。

那么具体怎么实现?

✅ 第一步:分配两块内存
#define LCD_WIDTH 240 #define LCD_HEIGHT 320 #define FRAME_SIZE (LCD_WIDTH * LCD_HEIGHT) // 分配两个帧缓冲区(建议放在DMA可访问区域) uint16_t frame_buffer[2][FRAME_SIZE] __attribute__((aligned(32)));

每个像素用RGB565格式存储,占2字节。一帧就是240 × 320 × 2 = 153,600 字节 ≈ 150KB。双缓冲就是300KB——对STM32F4这类小容量MCU确实吃紧,但像STM32H7、ESP32这种带PSRAM的平台完全扛得住。

✅ 第二步:定义前后缓冲指针
uint16_t *buf_front = frame_buffer[0]; // 当前显示用 uint16_t *buf_back = frame_buffer[1]; // 当前绘制用

注意:这里只是指针交换,不是 memcpy!效率极高。

✅ 第三步:绘制全部在后台进行

比如你要画个彩色渐变:

void draw_gradient(void) { for (int y = 0; y < LCD_HEIGHT; y++) { for (int x = 0; x < LCD_WIDTH; x++) { uint16_t r = (x >> 3) & 0x1F; uint16_t g = (y >> 2) & 0x3F; uint16_t b = (x ^ y) >> 3 & 0x1F; buf_back[y * LCD_WIDTH + x] = (r << 11) | (g << 5) | b; } } }

所有的操作都在buf_back上完成,屏幕仍然显示的是buf_front的内容,毫无干扰。

✅ 第四步:原子化切换

等你画完了,调用一个swap_buffers()函数:

void swap_buffers(void) { spi_wait_idle(); // 确保上一帧传输已完成 // 交换指针 uint16_t *temp = buf_front; buf_front = buf_back; buf_back = temp; // 启动DMA推送新帧到ST7789 st7789_update_screen(buf_front); }

就这么简单?没错。没有复制、没有延迟、没有复杂协议,只是一个指针切换 + 一次全屏更新。


三、关键优化:如何让刷新更快更稳?

虽然双缓冲解决了“撕裂”和“闪烁”,但如果处理不当,还是会卡顿、掉帧、发热严重。下面这几个实战技巧,都是我在调试中一点点试出来的。

🔧 技巧1:一定要用DMA传数据!

别再用裸SPI发送像素了!每一帧15万多个像素点,如果靠CPU轮询发送,不仅耗时长(可能几十毫秒),还会阻塞其他任务。

正确的做法是:

  • 初始化SPI为DMA模式;
  • 调用HAL_SPI_Transmit_DMA()或 ESP-IDF 中的spi_device_transmit()
  • 让硬件自动搬运数据,CPU腾出手干别的事。

示例(基于HAL库):

void st7789_update_screen(uint16_t *frame) { st7789_set_address_window(0, 0, LCD_WIDTH - 1, LCD_HEIGHT - 1); st7789_write_command(ST7789_RAMWR); // 写GRAM命令 HAL_GPIO_WritePin(LCD_DC_GPIO_Port, LCD_DC_Pin, GPIO_PIN_SET); HAL_SPI_Transmit_DMA(&hspi1, (uint8_t *)frame, FRAME_SIZE * 2); }

这样一次刷新可以压到5~10ms以内(取决于SPI速率),轻松做到30fps动画。


🔧 技巧2:局部刷新比全刷香多了

你真的需要每次刷新整块屏幕吗?

比如你只是改了个按钮颜色,或者弹了个提示框,没必要把整个背景重新传一遍。

引入“脏矩形”机制(Dirty Rectangle):

typedef struct { uint16_t x, y, w, h; } rect_t; rect_t dirty_rect = {0, 0, LCD_WIDTH, LCD_HEIGHT}; // 默认全刷 void mark_dirty(uint16_t x, uint16_t y, uint16_t w, uint16_t h) { // 合并区域(简化版) uint16_t x1 = MIN(dirty_rect.x, x); uint16_t y1 = MIN(dirty_rect.y, y); uint16_t x2 = MAX(dirty_rect.x + dirty_rect.w, x + w); uint16_t y2 = MAX(dirty_rect.y + dirty_rect.h, y + h); dirty_rect.x = x1; dirty_rect.y = y1; dirty_rect.w = x2 - x1; dirty_rect.h = y2 - y1; } void flush_dirty(void) { if (dirty_rect.w == 0 || dirty_rect.h == 0) return; st7789_set_address_window( dirty_rect.x, dirty_rect.y, dirty_rect.x + dirty_rect.w - 1, dirty_rect.y + dirty_rect.h - 1 ); uint32_t offset = (dirty_rect.y * LCD_WIDTH + dirty_rect.x); uint32_t size = dirty_rect.w * dirty_rect.h; st7789_write_command(ST7789_RAMWR); HAL_SPI_Transmit_DMA(&hspi1, (uint8_t*)&buf_front[offset], size * 2); // 清除标记 dirty_rect.w = 0; }

配合双缓冲使用,既能保证完整性,又能大幅降低带宽压力。


🔧 技巧3:模拟VSync,避免“错峰切换”

ST7789本身不输出垂直同步信号(VSync),但你可以通过定时器模拟。

假设你的屏幕刷新率大约是60Hz(每帧约16.6ms),可以用一个定时器中断来标记“新的一帧开始了”:

static volatile bool vsync_flag = true; void TIM6_IRQHandler(void) { if (TIM6->SR & TIM_SR_UIF) { TIM6->SR = ~TIM_SR_UIF; vsync_flag = true; } } void swap_buffers_vsync(void) { while (!vsync_flag); // 等待VSync开始 spi_wait_idle(); vsync_flag = false; // 切换并刷新 uint16_t *temp = buf_front; buf_front = buf_back; buf_back = temp; st7789_update_screen(buf_front); }

虽然仍是软件模拟,但在高帧率下能显著减少因“中途切换”带来的轻微抖动感。


四、真实场景中的取舍:内存 vs 流畅性

双缓冲最大的代价是什么?内存翻倍

对于某些资源紧张的项目(如STM32F1/F4系列),300KB SRAM直接没了,怎么办?

这里有几种折中方案:

方案说明适用场景
使用外部PSRAMESP32/QSPI PSRAM可达4MB推荐首选
降分辨率改为128×160或160×120对清晰度要求不高
单缓冲+局部绘制不撕裂但仍有闪烁风险极端资源限制
三缓冲+优先级调度更平滑,适合复杂UI高端应用

我个人建议:宁可牺牲一点功能,也要保证显示质量。因为用户体验是从第一眼开始建立的。


五、常见坑点与避坑指南

❌ 坑1:指针没对齐,DMA报错

尤其是Cortex-M7平台,DMA要求缓冲区地址按特定字节对齐(如32字节)。记得加:

__attribute__((aligned(32)))

否则可能出现传输异常或HardFault。


❌ 坑2:忘记等待SPI空闲就切换

// 错误示范: buf_front = buf_back; return; // 此时DMA还在传!下一帧可能覆盖旧数据!

正确做法是在swap_buffers()前确保上一帧DMA已完成:

while (spi_dma_busy()); // 或注册DMA完成回调

❌ 坑3:频繁刷新导致功耗飙升

如果你做的是电池供电设备(比如智能手表),一直跑60fps等于持续烧电。

解决方案:

  • 静态画面降到5fps;
  • 动画期间恢复高刷;
  • 利用ST7789的SLEEP_IN/SLEEP_OUT命令休眠屏幕。

六、结语:从“能用”到“好用”,只差一个双缓冲

回到开头的问题:怎么让ST7789不再闪?

答案已经很清晰了:

用双缓冲隔离绘制与显示,用DMA加速传输,用局部刷新节省资源。

这套组合拳下来,哪怕是最基础的MCU,也能做出媲美消费电子产品的视觉体验。

更重要的是,一旦你掌握了这套方法,后续接入LVGL、LittlevGL、TouchGFX Lite等GUI框架时就会发现——它们底层其实也是这么干的。

你现在做的,不只是修一个“闪屏”的Bug,而是在搭建一套现代嵌入式图形系统的地基。

所以,别再让你的用户盯着“撕裂的画面”皱眉了。
现在就去改代码,加上双缓冲,让他们眼前一亮吧!

如果你在实现过程中遇到了SPI DMA配置问题、内存不足崩溃、或是刷新不同步的情况,欢迎在评论区留言,我们一起排查解决。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

福州网站建设南昌网站建设

Markmap:将Markdown文档秒变思维导图的革命性工具【免费下载链接】markmap项目地址: https://gitcode.com/gh_mirrors/mar/markma

2026/06/30 13:20:35

义乌网站建设南昌网站建设公司

HoneySelect2优化补丁完整使用指南:从新手到专家的全面解决方案【免费下载链接】HS2-HF_PatchAutomatically translate, uncensor and

2026/06/30 10:44:52

移动网站建设怎么网站建设

高级计算器:从简单到复杂的编程之旅1. 高级计算器概述高级计算器是一个小型但较为实用的编译器扩展。它在原计算器的基础上,增加了命名变量和赋值、比较表达式(如大于、小于、等于等)、if/then/els

2026/06/30 14:08:39

兰州网站建设商丘网站建设

C# WPF界面设计:为IndexTTS2开发图形化本地运行工具在AI语音合成技术日益普及的今天,越来越多的内容创作者和开发者开始尝试将高质量TTS模型应用于有声读物、视频

2026/06/30 11:50:27

大庆网站建设网站建设哪家

第一章:Rust扩展PHP函数注册的核心概念在现代Web开发中,PHP作为一门动态脚本语言广泛用于服务器端逻辑处理。然而,其性能瓶颈在高并发或计算密集型场景中

2026/06/30 12:22:00

凯里网站建设马鞍山网站建设

YOLOv8-NAS网络架构搜索技术应用,找到最优GPU结构在工业质检、自动驾驶和智能监控等高实时性场景中,目标检测模型不仅要“看得准”,更要“跑得快”。然而

2026/06/30 12:23:00

青岛网站建设公司网站建设运营

联想拯救者BIOS高级设置实用技巧:深度解析隐藏功能优化方案【免费下载链接】LEGION_Y7000Series_Insyde_Advanced_Settings_Tools支持一键修改

2026/06/30 11:46:27

龙岗网站建设合川网站建设

如何快速掌握ER存档编辑器:从新手到专家的终极指南【免费下载链接】ER-Save-EditorElden Ring Save Editor. Compatible with PC and

2026/06/30 12:07:59

青岛网站建设南京网站建设

快速体验打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容:创建一个React应用,使用Zustand进行状态管理。需要实

2026/06/30 12:02:28

电子商务网站建设宁波市网站建设

为什么项目中要经常用到threadlocal?在后端项目开发中,我们常与多线程打交道——无论是处理并发请求的Tomcat线程池,还是异步任务的线程池ÿ

2026/06/30 12:03:59