ST7789V + STM32:从零搞懂TFT屏驱动的底层逻辑
你有没有遇到过这样的场景?
接上一块小小的1.3寸TFT彩屏,代码烧进去,结果屏幕要么白屏、花屏,要么图像倒着显示……调试半天,发现不是SPI速率太高,就是初始化序列漏了一条延时。
这背后,往往不是硬件坏了,而是对ST7789V 这颗“小心脏”的脾气还不够了解。
今天我们就来一次彻底拆解:不讲套话,不堆术语,带你从工程实战角度,真正吃透 ST7789V 如何在 STM32 上稳定点亮、高效绘图。
为什么是 ST7789V?它到底强在哪?
市面上能用的TFT驱动不少,比如老牌的 ILI9341、SSD1351,那为啥现在越来越多开发板都转向了 ST7789V?
答案很现实——快、省、适配好。
我们先来看一组关键参数对比:
| 特性 | ST7789V | ILI9341 |
|---|---|---|
| 最大SPI时钟支持 | ✅ 可达60MHz | ⚠️ 通常建议≤36MHz |
| 初始化流程 | 简洁明了,寄存器少 | 复杂冗长,需多次延迟 |
| 圆形屏原生支持 | ✅ 内建240x240裁剪模式 | ❌ 需手动坐标映射 |
| 抗干扰能力 | 强(内部时序优化) | 易受电源波动影响 |
| RGB565字节顺序兼容性 | 更友好于STM32 MSB传输 | 常需额外字节翻转 |
换句话说,ST7789V 更适合现代MCU的高速通信习惯,尤其是在使用STM32这类带硬件SPI和DMA的芯片时,优势非常明显。
而且它的典型分辨率是240×320,正好匹配很多常见的圆形表盘屏(通过区域裁剪实现),非常适合做智能手表、手环类UI原型。
它是怎么工作的?一张图说清楚
想象一下,ST7789V 就像一个“画布管理员”。你不需要自己去控制每一个像素点怎么发光,只需要告诉它三件事:
- “我要下命令”还是“我发的是颜色数据”?
- “你想改哪块区域?”
- “新颜色是什么?”
它内部有一块叫GRAM(Graphic RAM)的显存,每个像素占16位(即RGB565格式),总共能存240×320×2 = 153,600 字节的图像数据。
当你往 GRAM 写入数据后,ST7789V 自动将内容刷新到屏幕上。整个过程无需CPU持续干预,非常轻量。
核心引脚作用一览
| 引脚名 | 功能说明 |
|---|---|
| SCK / SCL | SPI时钟线,由STM32主控提供 |
| SDA / MOSI | 数据输入,串行发送命令或颜色值 |
| CS(片选) | 低电平有效,拉低表示开始与ST7789V通信 |
| DC(D/CX) | 最关键!高电平=数据,低电平=命令 |
| RST | 复位信号,一般接GPIO,软件可控更稳妥 |
| BLK / LED | 背光控制,可接PWM调节亮度 |
🔥 注意:虽然有些模块把RST接到VCC让它一直高电平,但强烈建议你用GPIO控制复位脚——否则初始化失败时无法软重启。
初始化为何总失败?因为你忽略了这三个细节
别急着写“Hello World”,先搞定启动流程。很多开发者直接复制别人的初始化代码,却忽略了一个事实:ST7789V 对上电时序极其敏感。
下面是经过实测验证的标准初始化步骤,每一步都不能跳:
void ST7789V_Init(void) { // 【Step 1】上电后等待至少120ms HAL_Delay(120); // 【Step 2】软复位(0x01) WRITE_CMD(0x01); HAL_Delay(150); // 必须等够! // 【Step 3】退出睡眠模式(0x11) WRITE_CMD(0x11); HAL_Delay(120); // 关键延时!不能省 // 【Step 4】设置色彩格式为16bit/pixel(RGB565) WRITE_CMD(0x3A); WRITE_DATA(0x05); // 注意:必须发数据! // 【Step 5】设置显示方向(MADCTL = 0x36) WRITE_CMD(0x36); WRITE_DATA(0x08); // 示例:竖屏+RGB顺序 // 【Step 6】设置列地址范围(COLUMN ADDR, 0x2A) WRITE_CMD(0x2A); WRITE_DATA(0x00); WRITE_DATA(0x00); // 起始X=0 WRITE_DATA(0x00); WRITE_DATA(0xEF); // 结束X=239 // 【Step 7】设置页地址范围(PAGE ADDR, 0x2B) WRITE_CMD(0x2B); WRITE_DATA(0x00); WRITE_DATA(0x00); // 起始Y=0 WRITE_DATA(0x01); WRITE_DATA(0x3F); // 结束Y=319 // 【Step 8】开启显示(DISPLAY ON, 0x29) WRITE_CMD(0x29); }📌重点提醒三个坑点:
- 所有
WRITE_CMD后如果有参数,必须紧跟WRITE_DATA发送,且中间不要加延时! HAL_Delay()时间必须足够长,特别是复位和退出睡眠之后,否则芯片还没准备好。- DC引脚切换要准确,否则命令会被当成数据处理,导致配置错乱。
你可以把这套初始化看作“唤醒仪式”——少一步,它就不理你。
STM32如何高效通信?SPI配置要点全解析
接下来我们看看 STM32 怎么跟它对话。
推荐使用硬件SPI + DMA模式
如果你还在用IO模拟SPI(Bit-Banging),赶紧停手吧。不仅速度慢,还容易因中断打断造成数据错位。
正确的做法是:启用STM32的硬件SPI,并搭配DMA进行大批量图像传输。
SPI配置关键参数(以STM32F4为例)
hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; // 主机模式 hspi1.Init.Direction = SPI_DIRECTION_1LINE; // 半双工单向(只发不收) hspi1.Init.DataSize = SPI_DATASIZE_8BIT; // 每次传8位 hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // 空闲时SCK为低 hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; // 第一个边沿采样 hspi1.Init.NSS = SPI_NSS_SOFT; // 软件控制CS hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4; // 分频后约21MHz hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; // 先发高位📌为什么推荐 Mode 0(CPOL=0, CPHA=0)?
因为 ST7789V 默认工作在此模式下,无需额外配置时序寄存器,兼容性最好。
📌为什么要用SPI_DIRECTION_1LINE?
因为我们只向屏幕发数据,不需要读回任何状态。节省MISO引脚,还能提高稳定性。
📌SPI速率设多少合适?
- 初始调试阶段:建议设为10MHz(如分频为8)
- 正常运行后:可提升至20~40MHz,视PCB布线质量而定
- 极限情况可达60MHz,但要求走线短、电源稳
如何快速刷图?学会这个函数,效率翻倍
最耗CPU的操作是什么?当然是逐个像素写入。
但我们有更聪明的办法:设定地址窗口 + 批量写入数据。
void ST7789V_SetAddressWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { WRITE_CMD(0x2A); // Column Address Set WRITE_DATA(x0 >> 8); WRITE_DATA(x0 & 0xFF); WRITE_DATA(x1 >> 8); WRITE_DATA(x1 & 0xFF); WRITE_CMD(0x2B); // Page Address Set WRITE_DATA(y0 >> 8); WRITE_DATA(y0 & 0xFF); WRITE_DATA(y1 >> 8); WRITE_DATA(y1 & 0xFF); WRITE_CMD(0x2C); // Memory Write } void ST7789V_FillArea(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color) { uint32_t total_pixels = w * h; uint8_t *buffer = (uint8_t *)malloc(total_pixels * 2); // RGB565每像素2字节 for (int i = 0; i < total_pixels; i++) { buffer[2*i] = (color >> 8) & 0xFF; // 高字节先发 buffer[2*i+1] = color & 0xFF; } ST7789V_SetAddressWindow(x, y, x+w-1, y+h-1); HAL_GPIO_WritePin(DC_PORT, DC_PIN, GPIO_PIN_SET); // 数据模式 HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, buffer, total_pixels * 2, HAL_MAX_DELAY); HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_SET); free(buffer); }但这还不是最快的。真正的性能飞跃来自DMA传输:
void ST7789V_DrawImage_DMA(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t *image) { ST7789V_SetAddressWindow(x, y, x + w - 1, y + h - 1); HAL_GPIO_WritePin(DC_PORT, DC_PIN, GPIO_PIN_SET); HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_RESET); HAL_SPI_Transmit_DMA(&hspi1, (uint8_t *)image, w * h * 2); // 注意:DMA传输异步完成,若需同步可在回调中处理 }✅ 使用DMA后,CPU只需启动传输即可去做别的事,图像刷屏完全无阻塞,特别适合RTOS环境或多任务系统。
实际开发中常见问题及应对策略
🟡 问题1:屏幕白屏或闪屏
可能原因:
- 上电时序不对,未满足最小延迟
- SPI速率过高,数据没对齐
- 电源不稳定,未加滤波电容
解决方法:
- 加大初始化中的HAL_Delay()时间
- 初期降低SPI速率至10MHz测试
- 在VCC引脚就近并联一个0.1μF陶瓷电容 + 10μF钽电容
🟡 问题2:图像上下颠倒、左右反了
原因:MADCTL 寄存器设置错误!
MADCTL(0x36)控制显示方向,其8位含义如下:
| 位 | 名称 | 功能 |
|---|---|---|
| 7 | MY | 行扫描方向(0: top→bottom, 1: bottom→top) |
| 6 | MX | 列扫描方向(0: left→right, 1: right→left) |
| 5 | MV | XY轴是否交换 |
常用组合示例:
| 显示方向 | 参数值(十六进制) |
|---|---|
| 竖屏,正常方向 | 0x08 |
| 竖屏,上下翻转 | 0xC8 |
| 横屏,左转90度 | 0x68 |
| 横屏,右转90度 | 0xA8 |
👉 记住一句话:改方向,先查MADCTL;调不好,试试0x68和0xA8。
🟡 问题3:刷新卡顿、动画掉帧
根本原因:CPU轮询发送大量数据,占用过高资源。
优化方案:
1. 改用DMA传输
2. 引入双缓冲机制(front/back buffer)
3. 只更新变化区域(脏矩形刷新)
4. 降低非必要区域的刷新频率
例如,做一个时钟界面,秒针每秒动一次,但背景不变,那就只重绘秒针部分,而不是整屏刷新。
工程实践建议:写出可移植、易维护的驱动
别再把所有代码扔进main.c了!良好的模块化设计才能走得远。
推荐文件结构
/drivers/ └── st7789v/ ├── st7789v.h ├── st7789v.c └── st7789v_config.h ← 用户可配置项集中在此提供简洁API接口
// 统一绘图接口 void st7789v_draw_pixel(uint16_t x, uint16_t y, uint16_t color); void st7789v_fill_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color); void st7789v_draw_image(uint16_t x, uint16_t y, const uint16_t *image, uint16_t w, uint16_t h); void st7789v_set_rotation(uint8_t rotation); // 0~3对应四个方向屏幕尺寸宏定义适配
// st7789v_config.h #define ST7789V_WIDTH 240 #define ST7789V_HEIGHT 320 // 如果是圆屏变体(240x240) // #define ST7789V_WIDTH 240 // #define ST7789V_HEIGHT 240这样换不同型号屏幕时,只需修改头文件,无需动核心逻辑。
向前一步:为LVGL打基础
一旦你能稳定刷图,下一步就可以接入图形库了。
LVGL(Light and Versatile Graphics Library)是目前嵌入式领域最受欢迎的开源GUI框架之一。而它的底层依赖,正是我们刚刚实现的这些基本函数:
flush_cb→ 负责把LVGL缓存中的内容刷到屏幕上round_corner_cb→ 可选加速input device→ 可接触摸芯片(如XPT2046)
当你能在STM32上跑起LVGL的按钮、滑动条甚至动画特效时,你会发现:原来这一切,都是建立在对ST7789V的深刻理解之上。
写在最后:掌握底层,才能驾驭复杂
很多人觉得“驱动屏幕”是个黑盒子,只要调库就行。但现实是,一旦出问题,只会调库的人只能重启、换线、换屏,却找不到根因。
而真正厉害的工程师,知道:
- 为什么加那一段延时;
- 为什么DC引脚必须精准切换;
- 什么时候该降速保稳,什么时候可以提速冲性能。
这才是嵌入式开发的魅力所在——看得见底层,才撑得起上层繁华。
如果你正在做一个带屏项目,不妨停下来,亲手写一遍初始化流程,跑一次DMA刷图实验。也许下次遇到花屏,你会笑着说:“哦,不过是少了个120ms延时罢了。”
💬 如果你在集成过程中遇到了其他挑战,欢迎在评论区留言交流。一起踩过的坑,才是成长最快的路。