简介:ST7789V驱动例程STM32工程包,面向使用SPI接口驱动TFTLCD屏的嵌入式开发者,提供基于STM32F429平台的完整初始化与显示代码,适用于智能家居、仪器仪表、手持设备等需要小型彩色显示屏的项目。例程支持SPI三线、四线及GPIO模拟方式,涵盖点绘、画线、填充、文本显示等基本图形操作,可直接参考移植到类似MCU项目中。压缩包共250个文件,以C源码(.c)与头文件(.h)为主,附带Keil工程配置(.uvprojx)、编译产物(.o/.axf/.hex)及调试文件,打包后约10.09MB。已有6688人学习下载。代码内含ST7789V初始化序列,如COLMOD、CASET、RASET、MADCTL等命令配置,并给出横竖屏、像素格式与时序参数的设置方法,便于理解驱动芯片的工作原理。通过阅读代码可掌握SPI通信时序、命令发送流程、显存操作等关键知识点,也可作为二次开发的模板。工程经过实际测试,大部分基础显示功能可直接使用,适合需要快速完成LCD适配或学习SPI屏驱动、STM32 HAL库应用的嵌入式工程师。
1. 为什么十个人里有八个把这块240×240小屏调白:先从ST7789v是什么说起
拿到一块丝印着 ST7789v 的 1.3 寸 LCD 模组,接好线、烧进网上找的例程,屏幕纹丝不动或者整屏惨白——这是嵌入式交流群里出现频率最高的问题之一。ST7789v 是一颗内置显示 RAM、DCDC 和扫描时序的 LCD 驱动 IC,MCU 只需要通过 SPI 把命令和像素数据灌进去,它自己负责把 RGB 数据映射到面板上,理论上是完全不挑主控的。白屏翻车十有八九不是屏坏了,而是初始化序列、SPI 相位、DC 引脚时序这三个点没对齐。这篇文章要做的,就是把一块基于 ST7789v 的 240×240 SPI 屏,从接线到初始化再到 DMA 刷图,在 STM32 上完整跑通,并把常见白屏、花屏现象按可操作的排查顺序讲清楚。适合正在从 Arduino 库生态里走出来、想手动掌控底层、或者被某块屏折腾到怀疑人生的从业者。
2. 硬件接口与通信链路:先把 CS、DC、RESET 这三个坑填平
2.1 引脚定义与接线表:别被模块丝印误导
市面上所有基于 ST7789v 的小尺寸模组,无论来自哪家方案商,引脚功能几乎都收敛到同一组:电源、SPI 三线、以及 CS、DC、RESET、背光控制。模块上的丝印可能是 SDA/SCL/RS/RST,也可能直接写 DIN/CLK/DC,本质是同一个东西。接线前先确认模块是否板载电平转换和稳压芯片,这决定了电源该怎么给。
下面是一张通用于大多数 240×240 屏模组的接线对照表,MCU 列给的是我常用的 STM32F103 引脚的示例,具体以你的板子丝印为准。
| 模组引脚 | 常见丝印 | 连接方向 | 说明 |
|---|---|---|---|
| VCC / VDD | VCC | 3.3V 或 5V | 看板载稳压,建议 5V,见 2.3 |
| GND | GND | GND | 共地必须可靠 |
| DIN / SDA | DIN | PA7(SPI1_MOSI) | 主出从入 |
| CLK / SCK | CLK | PA5(SPI1_SCK) | 最高建议先按 9MHz 跑通 |
| CS | CS | 任意 GPIO | 低有效,软件拉低 |
| DC / RS / A0 | DC | 任意 GPIO | 命令/数据选择,见下 |
| RESET / RST | RESET | 任意 GPIO | 低有效复位 |
| BLK / BL / LED | BL | 任意 GPIO 或 3.3V | 背光控制,注意限流 |
DC 引脚不是 SPI 标准信号,它是 ST7789v 区分“当前发送的是命令还是数据”的依据。写命令时 DC 拉低,写像素数据时 DC 拉高。很多人把 DC 随手接到 GND 或者 VCC 上,结果初始化命令全部被当成像素数据,屏幕自然没有任何反应。RESET 引脚低有效,建议由 MCU 的 GPIO 独立控制,不要图省事接到系统复位电路上,否则上电瞬间的复位时序会和初始化代码互相打架。
2.2 硬件 SPI 还是软件模拟 SPI:不是所有场景都值得用硬件
ST7789v 的通信本质是 SPI,但工程上选硬件 SPI 还是软件模拟 SPI,取决于你手上的 MCU 资源。硬件 SPI 的吞吐量高,能配合 DMA 搬运整帧数据,刷新率能跑到每秒十几帧甚至更高;缺点是引脚必须绑定到 SPI 复用功能上,比如 STM32F103 的 SPI1 只能用 PA5、PA6、PA7。软件模拟 SPI 则完全不受引脚限制,任意 GPIO 都能驱动,特别适合 SPI1 已经被 W5500 或者其他外设占用的情况。
软件模拟发送一个字节的常见做法是这样,注意 MSB 先行和上升沿送数,和硬件 SPI 的 Mode 0 保持一致:
// 软件模拟 SPI,MSB 先行,上升沿输出数据 void st7789_write_byte(uint8_t dat) { for (int i = 0; i < 8; i++) { ST7789_SCK_LOW(); if (dat & 0x80) { ST7789_SDA_HIGH(); } else { ST7789_SDA_LOW(); } ST7789_SCK_HIGH(); // 上升沿,屏端完成采样 dat <<= 1; } }这段代码每组 GPIO 翻转之间的间隔决定了通信速率,通常在 1MHz 左右。刷一张 240×240 的 RGB565 图片要发 115200 字节,按 1MHz 算就是接近 1 秒,只能跑静态画面,动画基本没法看。软件模拟的优势在于排错方便,逻辑分析仪抓波形时一眼能看出时序问题,适合先把屏点亮再切换硬件 SPI。
硬件 SPI 的初始化配置我一般这样写:
void st7789_spi_init(void) { __HAL_RCC_SPI1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef gpio = {0}; gpio.Pin = GPIO_PIN_5 | GPIO_PIN_7; // SCK + MOSI gpio.Mode = GPIO_MODE_AF_PP; gpio.Pull = GPIO_NOPULL; gpio.Speed = GPIO_SPEED_FREQ_VERY_HIGH; // 高速翻转,减少边沿畸变 HAL_GPIO_Init(GPIOA, &gpio); hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; hspi1.Init.Direction = SPI_DIRECTION_2LINES; hspi1.Init.DataSize = SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // CPOL = 0 hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; // CPHA = 0 hspi1.Init.NSS = SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8; // 72MHz / 8 = 9MHz hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; HAL_SPI_Init(&hspi1); }关键参数有三个:CPOL=0、CPHA=0 组成的 SPI Mode 0,MSB 先行,以及分频系数。STM32F103 的 SPI1 挂在 APB2 总线上,主频 72MHz,分频 8 得到 9MHz,这个速率用杜邦线连接比较稳妥。等画面稳定后再尝试把分频降到 4,也就是 18MHz,大多数屏都能承受,但接线的质量决定了上限。
2.3 接线长度与电源:为什么一万根杜邦线翻车
ST7789v 这颗 IC 本身不矫情,但它的 IO 电平是 3.3V,如果模块上没有电平转换芯片,就不要把 MCU 的 5V 引脚直接怼上去。很多模块板上设有 AMS1117-3.3 这类稳压芯片,此时把 5V 接到模块的 VCC 脚才是正确选择,因为背光 LED 的压降和电流会让 3.3V 输入产生明显的压差波动,极端情况下表现为背光极暗、屏幕闪烁甚至 MCU 被拉复位。判断方法是看模块背面有没有稳压芯片丝印,有就 5V 供电,没有就老老实实 3.3V。
杜邦线的长度和走线方式也会直接影响刷新稳定。CLK 和 DIN 两根线尽量短,控制在 10cm 以内,中间最好隔一根地线。两根信号线绞在一起或贴着走,18MHz 以上时串扰会变成屏幕上的斜纹噪声,而且这种问题是软件排查不出来的。
提示:模块丝印不统一是常态,DC 可能标成 RS 或 A0,RESET 可能标成 RST,接线前用万用表对照原理图量一下,不要凭丝印猜。
3. ST7789v 初始化序列:寄存器配置里到底藏着哪些出厂差异
3.1 一条 init 函数背后的执行顺序:delay 是第一个坑
ST7789v 的初始化看起来就是一堆命令和数据,但真正坑人的是顺序和延时。上电后先软复位,然后 Sleep Out 唤醒,再设置扫描方向、像素格式、电压寄存器,最后开显示。每一步之间的延时如果被随意删掉,屏幕可能直接白屏,因为屏内部 DCDC 还没稳定,后续命令会丢。
下面是一条经过验证的通用初始化序列,命令名称和关键参数我写在注释里:
static void st7789_init(void) { uint8_t d; st7789_reset(); // 硬复位,见 3.3 st7789_write_command(0x01); // SWRESET 软复位 HAL_Delay(150); // 软复位后必须等内部电路稳定 st7789_write_command(0x11); // SLPOUT 退出睡眠 HAL_Delay(120); // 这里延时不够必白屏 st7789_write_command(0x36); // MADCTL 扫描方向 + RGB/BGR d = 0x00; // 颜色错乱时改 0x08 st7789_write_data(&d, 1); st7789_write_command(0x3A); // COLMOD d = 0x55; // RGB565,16bit 像素格式 st7789_write_data(&d, 1); st7789_write_command(0xB2); // PORCTRL porch 设置 { uint8_t p[3] = {0x05, 0x0C, 0x0C}; st7789_write_data(p, 3); } st7789_write_command(0xB7); // GCTRL gate control d = 0x35; st7789_write_data(&d, 1); st7789_write_command(0xBB); // VCOMS d = 0x19; st7789_write_data(&d, 1); st7789_write_command(0xC0); // LCMCTRL d = 0x2C; st7789_write_data(&d, 1); st7789_write_command(0xC2); // VDVVRHEN d = 0x01; st7789_write_data(&d, 1); st7789_write_command(0xC3); // VRH d = 0x12; st7789_write_data(&d, 1); st7789_write_command(0xC4); // VDV d = 0x20; st7789_write_data(&d, 1); st7789_write_command(0xC6); // FRCTRL2 帧率控制 d = 0x0F; st7789_write_data(&d, 1); st7789_write_command(0xD0); // PWCTRL1 { uint8_t p[2] = {0xA4, 0xA1}; st7789_write_data(p, 2); } st7789_write_command(0x21); // INVON 反色开启,部分面板必须开 HAL_Delay(10); st7789_write_command(0x13); // NORON 正常显示模式 HAL_Delay(10); st7789_write_command(0x29); // DISPON 开显示 HAL_Delay(20); }0x36 里的 bit3 控制 RGB 还是 BGR 排列,如果图片显示出来红蓝通道互换,改这一位就够了。0x3A 设为 0x55 就是 RGB565,每个像素两个字节,高位字节在前。0xB2、0xB7、0xBB、0xC0~0xC4 这组寄存器控制面板输出电压和 VCOM,不同模组厂会有不同的校准值,不是每块屏都适合完全相同的参数。
注意:初始化序列里的延时是流程的一部分,不是可有可无的等待。SWRESET 后至少等 120ms,SLPOUT 后至少等 120ms,删掉你会发现屏幕时好时坏,甚至彻底白屏。
3.2 不同厂家屏的初始化表为什么长得不一样:出厂校准决定参数
同一个 ST7789v 晶圆会被切割成不同规格的面板,模组厂在贴片时会根据自己面板的物理特性调整 VCOM、伽马、porch 等参数,并把调整后的寄存器值写进量产固件说明。所以你从网上下载的初始化序列能点亮 A 家的屏,未必能点亮 B 家的屏。这不是 ST7789v 玄学,是每一块屏在出厂前就有自己的“脾气”。
遇到换屏后白屏,不要急着怀疑 MCU 代码,先看两个最关键的差异点。第一是 0x21 反色命令,部分面板的偏光片方向决定了它必须开反色才能得到正常颜色,否则白色区域会变成发黄的负片效果。第二是 0xBB VCOM 电压,这个值直接决定黑位和灰阶亮度,设置偏低会让画面整体发白,设置偏高会让暗部发紫发蓝。
工程上处理这种差异,常见做法是优先向模组供应商索要初始化表,这是最可靠的路径。如果拿不到,就从开源驱动库里找一套 ST7789v 分支的通用配置跑通,然后只调整 0x36、0x3A、0x21 这三处,把画面方向和颜色先搞对,再回头微调 0xBB 和 0xC6 的舒适度。不要一上来就整套寄存器都改,你没那么多屏可以烧。
3.3 复位和供电时序:上电后别急着发命令
ST7789v 的 RESET 引脚低有效,复位脉冲宽度要足够,释放后还要给内部电路留出稳定时间。我一般会先硬复位再进初始化,保证上电后的状态是确定的:
static void st7789_reset(void) { ST7789_RESET_HIGH(); HAL_Delay(10); ST7789_RESET_LOW(); // 拉低,进入复位状态 HAL_Delay(50); // 低电平脉冲至少保持 50us,这里留余量 ST7789_RESET_HIGH(); // 释放复位 HAL_Delay(120); // 等待内部 DCDC 和振荡器稳定 }有的例程依赖上电瞬间的 RC 复位电路,从代码里不显式复位,也能点亮一部分屏。这种写法的问题在于:如果供电电源上升斜率比较缓,或者模块上的去耦电容比较大,RC 复位的释放时刻可能晚于 MCU 发送第一条命令的时机,屏幕就表现为这次上电正常、下次上电白屏。把复位引脚从 RC 电路上摘下来,由 GPIO 硬控,这个隐患就不存在了。
4. 从画点到 DMA 刷图:窗口指令是 ST7789v 的灵魂
4.1 画一个点也要走三个命令:CASET、RASET、RAMWR
ST7789v 内部有一块和面板分辨率对应的显示 RAM,MCU 不能直接写某个绝对地址,必须先通过 0x2A(列地址设置)和 0x2B(行地址设置)指定一个矩形窗口,再通过 0x2C 向窗口内连续写入像素数据。每次画点都重设窗口虽然啰嗦,但这是理解整个驱动流程的起点。窗口设置函数如下:
void st7789_set_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { uint8_t buf[4]; st7789_write_command(0x2A); // CASET 列地址 buf[0] = x0 >> 8; buf[1] = x0 & 0xFF; buf[2] = x1 >> 8; buf[3] = x1 & 0xFF; st7789_write_data(buf, 4); st7789_write_command(0x2B); // RASET 行地址 buf[0] = y0 >> 8; buf[1] = y0 & 0xFF; buf[2] = y1 >> 8; buf[3] = y1 & 0xFF; st7789_write_data(buf, 4); st7789_write_command(0x2C); // RAMWR 开始写像素 }画点函数就变成了窗口设置为单点,再发送一个 RGB565 颜色值:
void st7789_draw_pixel(uint16_t x, uint16_t y, uint16_t color) { uint8_t buf[2]; st7789_set_window(x, y, x, y); buf[0] = color >> 8; // RGB565 高字节先发 buf[1] = color & 0xFF; ST7789_DC_HIGH(); // 写数据,DC 拉高 HAL_SPI_Transmit(&hspi1, buf, 2, 100); }注意坐标范围是 0 到 239,不是 1 到 240。很多第一次写的人会把窗口终点写成 240,导致最右侧和最下侧各有一行像素永远点不亮,而且后续填充的坐标全部偏移一个像素。这种边界错误在纯色画面下看不出来,画彩色边框时才会暴露。
4.2 用 0x2C 连续灌像素:局部刷新和填充矩形
窗口的真正价值是局部刷新。窗口设置一旦完成,后续所有通过 0x2C 写入的数据都会自动填入窗口内的连续地址,不需要在每次像素之间重新发送命令。借助这个机制,填充矩形只需要设置一次窗口,然后连续发送颜色数据即可:
#define LCD_WIDTH 240 #define LCD_HEIGHT 240 static uint16_t line_buf[LCD_WIDTH]; // 只占 480 字节,放得下 void st7789_fill_rect(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) { uint16_t width = x1 - x0 + 1; uint16_t height = y1 - y0 + 1; uint32_t i; for (i = 0; i < width; i++) { line_buf[i] = color; } st7789_set_window(x0, y0, x1, y1); ST7789_DC_HIGH(); for (i = 0; i < height; i++) { HAL_SPI_Transmit(&hspi1, (uint8_t *)line_buf, width * 2, 100); } }这里没有采用满帧 malloc 的方式,因为在 STM32F103C8 这类只有 20KB RAM 的芯片上,240×240 的 RGB565 整帧缓冲区需要 115200 字节,直接放不下。用行缓冲把填充拆成 height 次发送,每次只占 240×2 字节,内存压力小得多。这种分块思路在局部刷新场景里特别实用,比如做进度条、按钮高亮、仪表盘指针更新,每次只需要重设一个很小的窗口,刷起来非常快。
4.3 DMA 搬运:把 115200 字节交给外设,而不是让 CPU 死等
刷整屏数据时,用 HAL_SPI_Transmit 一字节一字节发,会把 CPU 耗死在等待 SPI 发送寄存器空闲上。DMA 的作用是把数据从内存搬到 SPI 数据寄存器,全程不需要 CPU 干预,发送完成后再触发中断回调。配合硬件 SPI,刷屏帧率能提一个数量级。
static volatile uint8_t dma_transfer_done = 1; void st7789_flush(uint16_t *pixels, uint32_t pixel_count) { ST7789_CS_LOW(); st7789_set_window(0, 0, LCD_WIDTH - 1, LCD_HEIGHT - 1); dma_transfer_done = 0; ST7789_DC_HIGH(); HAL_SPI_Transmit_DMA(&hspi1, (uint8_t *)pixels, pixel_count * 2); // 等待 DMA 发送完成,注意这里不能直接死等,RTOS 下要改成信号量 while (dma_transfer_done == 0); ST7789_CS_HIGH(); } void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi->Instance == SPI1) { dma_transfer_done = 1; } }pixel_count 乘以 2 是因为每个像素是两字节。DMA 配置里有一个非常容易踩的坑:PeriphDataAlignment 和 MemoryDataAlignment 必须都是 BYTE,因为 STM32 的 SPI 数据寄存器是 8 位接口。如果配成了 WORD,SPI 会把内存里的两个字符合并成一个 32 位数据,产生非预期的字节序,画面会出现颜色错乱和像素错位。另外,传给 DMA 的像素缓冲区必须是全局变量或静态变量,不能是函数内局部数组,否则函数返回后栈空间被回收,DMA 还在背后偷偷读取已经无效的内存。
4.4 把一张图片转成 C 数组:工具 + 地址映射的注意事项
刷图片是验证整个链路最直观的方式。常见做法是在 PC 上把图片缩放成 240×240 的 RGB565 数组,导出成 C 头文件,再通过 st7789_flush 整帧发送。下面的 Python 脚本做了这个转换,用到的核心逻辑是 RGB888 到 RGB565 的位压缩:
from PIL import Image img = Image.open('demo.png').convert('RGB').resize((240, 240)) with open('image_data.h', 'w') as f: f.write('const uint16_t image_data[240 * 240] = {\n') for y in range(240): for x in range(240): r, g, b = img.getpixel((x, y)) rgb565 = ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3) f.write(f'0x{rgb565:04X},') f.write('\n') f.write('};\n')刷上去之后如果发现颜色反了或者上下颠倒,不需要重新生成数组,直接调 0x36 里的方向位就行。颜色通道错乱就切换 BGR 位,图片上下颠倒就切换 MY 位,左右镜像就切换 MX 位。我一般会先把一张标准彩条图刷上去,根据显示结果决定 0x36 到底设多少,而不是一开始就猜。
5. ST7789v 驱动常见问题排查:五个导致白屏和花屏的直接原因
5.1 上电全白屏,SPI 波形看不出明显问题
现象是屏幕背光亮,整屏纯白,逻辑分析仪抓 SPI 波形能看到 CLK 和 MOSI 都有数据在动,但屏幕就是没有内容。这种白屏最隐蔽,因为信号层面看起来一切正常。
原因通常是 SPI 模式不匹配。ST7789v 的时序要求是时钟空闲为低、上升沿采样数据,也就是 Mode 0。如果把 CPOL 或 CPHA 配成了 Mode 3,命令字节照样能发出去,但每一位的采样点都落在数据跳变附近,屏端解析出来的命令就变成了乱码。另一个常见原因是初始化序列里的延时被压缩,尤其是 0x11 Sleep Out 之后,内部 DCDC 还没稳定就继续灌命令。
解决方法是先确认硬件 SPI 的 CPOL=0、CPHA=0,然后在软件复位和 Sleep Out 后各加 120ms 以上延时,再跑一遍初始化。如果还是白屏,用 GPIO 软件模拟 SPI 初始化一次,排除硬件 SPI 配置问题。
5.2 背光亮但黑屏,十有八九是 DC 引脚时序
现象是屏幕背光正常亮,但内容区域全黑,看起来像完全没有初始化成功。
原因往往在 DC 引脚。DC 是命令和数据的判别信号,初始化阶段每条命令都要先拉低 DC 再发送命令字节,然后拉高 DC 发送参数。如果 DC 接到了固定电平,比如直接被跳线帽短接到高电平,那么 MCU 发出的命令全被屏端当成像素数据,初始化命令永远无法生效。这类问题在原理图上是看不出来的,因为接线文档通常不会把 DC 的时序画得很详细。
解决方法是先确认 DC 引脚驱动的 GPIO 有没有正确配置为推挽输出,然后在 st7789_write_command 和 st7789_write_data 两个函数内部检查 DC 拉低和拉高的位置。用万用表量 DC 引脚在发送命令时的电平,命令阶段应该是低电平,数据阶段应该是高电平。
5.3 显示颜色偏红或偏蓝,或者整个画面变成负片
现象是刷一张纯红图片,屏幕显示出来偏紫或偏橙,刷一张白色背景画面,显示出来的像是底片效果,黑的地方泛白,白的地方发暗。
原因集中在 0x36 寄存器的 RGB/BGR 位和 0x21 反色命令。ST7789v 的显示 RAM 内部排列支持 RGB 和 BGR 两种颜色顺序,如果面板物理走线和驱动 IC 的输出极性不匹配,就需要通过寄存器把通道顺序纠正过来。0x21 命令是进入反色显示,部分面板必须开反色才能显示正常颜色。
解决方法是把 0x36 的值从 0x00 改成 0x08,对比显示效果。如果颜色还是不对,交换 RGB565 数据中的高低字节再试。反色问题则是在初始化序列里增加或移除 0x21,以实际显示效果为准。这类排错没有捷径,只能改一次刷一张纯色图验证一次。
5.4 花屏或下半屏错乱,数据链路存在信号完整性问题
现象是画面能显示,但出现斜纹噪声、横向条纹闪烁,或者下半屏的内容错位成奇怪图案。
原因可能是 SPI 时钟频率过高,配合杜邦线的分布电容和串扰,数据信号的边沿已经畸变到屏端无法正确采样。另一个常见原因是整帧数据在发送过程中 CS 被过早拉高,导致后半段数据丢失,显示 RAM 里的内容只有上半部分是完整的。
解决方法是先把 SPI 分频降到 fPCLK/16 以下,确认画面稳定后再逐步提速。同时把 CS 的拉高时机放到 DMA 发送完成回调里,不要在主函数里估算延时后提前拉高。杜邦线超过 10cm 时,18MHz 以上基本是拿稳定性做赌注。
5.5 初始化偶尔成功、偶尔失败,重新上电又恢复正常
现象是同一份代码,第一次上电花屏,按复位键后正常,再断电重新上电又花屏。
原因是供电上电时序不稳定。模块上的 ST7789v 内部有 DCDC 转换器,上电瞬间如果外部电源电压爬升较慢,MCU 已经执行完初始化,屏幕内部的逻辑电压还没稳定,后续命令全部无效。这类问题在 3.3V 直接供电且背光电流较大时尤其明显。
解决方法是先确保模组由 5V 供电,利用板载稳压芯片生成 3.3V,同时把硬复位延时从 50us 加长到 100ms 以上。如果复位后依然偶发失败,在初始化结束后读一次屏幕状态再刷一帧纯色画面,根据返回值判断是否需要重新初始化。
6. 进阶技巧:把屏幕刷新率从 16fps 推到 30fps 的三步操作
先测基线数据,再谈优化。用 HAL_GetTick 包住一帧刷新,确定当前的实际帧率,而不是靠感觉猜:
uint32_t t0 = HAL_GetTick(); st7789_flush(frame_buffer, LCD_WIDTH * LCD_HEIGHT); uint32_t cost_ms = HAL_GetTick() - t0;在 STM32F103 72MHz 主频、SPI 9MHz 的条件下,一帧 115200 字节大约需要 102ms,也就是还不到 10fps。把 SPI 分频降到 4 即 18MHz,一帧理论传输时间降到约 51ms,加上窗口设置和 DMA 启动开销,实测能做到 60~70ms,大约 14~16fps。到这一步,瓶颈已经不是 SPI 速率,而是单缓冲架构里“发送一帧”和“准备下一帧”是串行的。
下一步是分块双缓冲。把屏幕分成上下两半,各配一个半帧缓冲区,每次只把半帧交给 DMA 发送。上半帧发送期间,CPU 去填充下半帧数据;下半帧发送期间,CPU 再回来更新上半帧。这样把刷屏和绘图重叠起来,帧率提升接近一倍。注意 ST7789v 的窗口地址在帧与帧之间不会自动清零,每次发送前都要重设窗口,否则数据会落在上一帧的末尾地址后面,画面直接错乱。
最后一步,如果 MCU 的 SPI 外设支持 16 位数据宽度,可以开启 DFF 位,让每次硬件发送直接携带一个完整 RGB565 像素,减少访问 SPI 数据寄存器的频率。开启后要注意字节序:STM32 是小端,写入 16 位数据寄存器时值和屏幕期盼的高字节在前并不一致,通常需要在软件层做一次高低字节交换。不要教条,先跑一张彩条图验证字节序,再决定交换方向。
我自己的习惯是先把屏幕用 9MHz 调通,确认初始化、DMA、窗口三条链路都没有问题,再一步一步提速。曾经为了追求参数好看,直接上 36MHz SPI 并用 15cm 杜邦线,结果屏幕下半部分出现周期性横纹,折腾了整整一下午。后来换排线、降分频,问题当场消失。屏幕驱动这种活,看着是代码问题,实际上很多是物理问题。希望这些踩过的坑能帮到你,少走几段弯路。
本文还有配套的精品资源,点击获取