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

重庆及时雨暖通设备有限公司 2026/09/09 20:24: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配置问题、内存不足崩溃、或是刷新不同步的情况,欢迎在评论区留言,我们一起排查解决。

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

建设网站教程容桂网站建设

旧Mac系统升级优化焕新指南:突破官方限制的完美方案【免费下载链接】OpenCore-Legacy-Patcher体验与之前一样的macOS项目地址: https://gitcode.c

2026/06/30 10:28:20

电器网站建设长沙网站建设

提供专属技术支持:帮助客户顺利对接GLM-4.6V-Flash-WEB API在今天这个AI应用快速落地的时代,越来越多企业希望将视觉理解能力嵌入到自己的产品中——无论是智

2026/06/30 11:19:55

北京网站建设公司南充网站建设

快速体验打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容:开发一个电商商品分页展示系统演示,包含前端界面和后端API。要

2026/06/30 13:40:36

淄博网站建设英文网站建设

在 Python 中获取列表嵌套字典(多层嵌套)的键值对,需要根据数据的嵌套层级、结构是否固定,选择直接访问、循环遍历、递归解析或专用库查询等方

2026/06/30 11:13:24

永康网站建设六安网站建设

段落对齐是Word文档格式排版的基础需求,合理的对齐样式能提升文档的可读性和美观度。Free Spire.Doc for .NET 是一款免费的 Word 文档处理组件,支

2026/06/30 12:01:59

唐山网站建设黄石网站建设

古诗词打卡学习小程序 - 产品介绍项目概述古诗词打卡学习小程序是一款专为诗词爱好者设计的学习工具,特别适合中老年用户群体。该小程序收录了15首经典古诗词,包括《静夜思》、《

2026/06/30 12:45:33

永州网站建设北海网站建设

第一章:模型稳定性频出问题,是Open-AutoGLM 2.0的锅吗?近期多个用户反馈在使用 Open-AutoGLM 2.0 进行自动化文本生成任务时&#x

2026/06/30 12:14:30

昆明网站建设网站建设团队

温馨提示:文末有资源获取方式企业急需一个强大的内部管理引擎来整合资源、提升效率。现在,一款旨在成为企业生产经营活动物质基础数字化核心的ERP进销存管理系统源码已准备就绪。它

2026/06/30 13:32:36

网站建设运营十堰网站建设

Qwen3-VL部署成本分析:租用云GPU vs 自建集群性价比比较在AI应用加速落地的今天,多模态大模型正从实验室走向真实业务场景。无论是智能客服中识别用户上传的截图&#

2026/06/30 10:54:22