news 2026/11/5 6:57:23

ST7789V屏幕驱动全解析:SPI三线/四线与GPIO模拟点屏排错

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ST7789V屏幕驱动全解析:SPI三线/四线与GPIO模拟点屏排错

简介:面向嵌入式图形显示开发的 ST7789V 驱动例程,基于 STM32F429 平台,覆盖 SPI 三线、四线与 GPIO 模拟三种通信方式,适合已具备单片机基础、希望在小尺寸 TFT 屏上完成界面开发的工程师与学生。代码围绕初始化与显示操作两条主线展开:初始化部分给出屏幕方向、像素格式以及 MADCTL、COLMOD 等时序参数的配置顺序与命令格式;显示部分实现点绘、画线、区域填充和字符显示,并附点阵字库资源。压缩包共 250 个文件、约 10.09MB,以 88 个头文件和 77 个 C 源文件为主体,涵盖 HAL 库与用户驱动代码,另含 Keil 工程文件、axf 与 hex 可执行输出以及 map、lst 编译清单,可直接导入编译或烧录验证。工程已实测通过,读者可对照实际接线调整引脚与分频参数,缩短屏幕点亮的调试周期,并排查通信失败、花屏与显示偏移等常见问题。目前已有 6705 人学习下载,可作为 LCD 驱动入门与移植的参考工程。

1. 焊好排线、通上电、背光亮起,屏幕却只有一片白

这个瞬间,做过带屏小设备的人大概都经历过。ST7789V 这颗 TFT 驱动 IC 本身不算难,关键在于它同时支持 SPI 三线九位、四线带 DC 控制线、纯 GPIO 模拟这三种完全不同的接线方式,并且初始化序列严重依赖上电时序和毫秒级延时。这份例程把三种方式的底层写命令、写数据、初始化命令表、窗口设置和刷屏函数都拆开写好了,STM32 的 HAL 库和标准库工程都能直接编译。如果你正卡在点屏第一步,或者想把现成的驱动搬进自己的工程,下面按文件、按时序、按踩坑现场的拆解,能省掉你对着逻辑分析仪猜波形的时间。

2. 结构先摸清:一份例程里三层文件和三种接线的分工

拿到例程先别急着编译,先看它把什么放在哪一层。ST7789V 这类屏的驱动代码,写崩的工程九成不是算法错,而是分层没做干净——把 GPIO 翻转、命令表、业务绘图全塞在一个lcd.c里,换个引脚要改十几个地方,换块屏直接推倒重来。这份例程的分法很土但很实用:硬件相关的全部下沉到 port 层,命令和显存操作放驱动层,测试画面单独一个文件。

2.1 目录与文件清单

展开压缩包,主干大致是这样:

ST7789V_Demo/ ├── App/ │ ├── main.c # 上电流程、主循环 │ └── lcd_test.c # 色块、渐变、字符、图片测试 ├── Driver/ │ ├── st7789.c # 初始化表、窗口、填充、画点、字符 │ ├── st7789.h # 分辨率、偏移量、颜色宏、函数声明 │ ├── st7789_port.c # 三线/四线/模拟三种底层的收发实现 │ └── font_8x16.c # 8x16 点阵字库 ├── HAL/ │ ├── spi.c # SPI 外设初始化 │ ├── gpio.c # 引脚初始化 │ └── stm32f4xx_it.c └── Doc/ └── 接线说明.md
文件职责你什么时候需要动它
st7789_port.c只做一件事:把命令字节和数据字节送出去换通信方式、换 SPI 实例、换引脚
st7789.c初始化序列、CASET/RASET窗口、填充、画点、画字符换屏尺寸、调伽马、改扫描方向
st7789.h分辨率、X/Y 偏移、颜色宏、对外接口换屏型号时第一个改的地方
lcd_test.c纯上层调用,不碰任何寄存器验证驱动是否正常、做演示

这种划分带来的直接好处:st7789.c里没有一句HAL_GPIO_WritePin,所有硬件动作都通过st7789_wr_cmd()和st7789_wr_data()两个函数出去。你在四线 SPI 上调通了,想换成模拟方式验证硬件,只改st7789_port.c一个文件,上层绘图代码一行不动。我一般接手别人的屏驱动代码,第一件事就是看有没有这两个函数,没有的话先补出来再谈别的。

2.2 三线、四线、模拟:三种接线方式的取舍

这三种方式的本质区别只有一个:命令和数据怎么区分。

  • 四线:多一根 DC 线,DC=0 表示接下来发的是命令,DC=1 表示是数据。协议最简单,所有带 SPI 的 MCU 都能干,是例程的默认方式。
  • 三线九位:省掉 DC 线,改用 SPI 的第 9 个 bit 承载这个信息,一个字节数据前面加一位 D/C。省一根线,但要求 MCU 的 SPI 支持 4~16 位可配数据帧,老型号不一定支持。
  • 模拟:三根 GPIO 手动翻转 SCL/SDA,自己在代码里按时序打 9 个 bit。速度最慢,但可以跑到任何芯片上,也是排查硬件问题时唯一能完全排除 SPI 外设配置干扰的手段。
对比项四线 SPI三线九位 SPIGPIO 模拟
占用引脚SCL/SDA/DC/CS/RES/BLSCL/SDA/CS/RES/BLSCL/SDA/CS/RES/BL
理论 SCK40~80 MHz同左通常 2~10 MHz
240×320 全屏刷新8~20 ms(DMA 可更快)8~20 ms80~200 ms
MCU 占用低(可 DMA)低(可 DMA)高,全程占 CPU
代码复杂度低中(帧长度处理有坑)低但时序要调
典型用途量产首选排线紧张的小模组点屏验证、老芯片移植

选型上我一般这么定:能上四线就上四线,多一根 DC 线的成本远低于调试九位帧的时间;只有排线已经定型、必须省那一根线的模组才上三线;模拟方式只在「新板子第一次上电,不确定是硬件还是驱动问题」时用,点亮之后再切回硬件 SPI。

2.3 引脚定义与硬件抽象层

例程把引脚全部收在头文件里,用宏而不是函数。原因很直接:模拟方式下单次写一个字节要翻转 18 次 SCK,如果每次都是函数调用,开销比翻转本身还大。

/* st7789.h —— 换板子时只改这一块 */ #define LCD_W 240 #define LCD_H 320 #define LCD_X_OFFSET 0 /* 240x240 的方屏这里通常填 0 */ #define LCD_Y_OFFSET 0 /* 240x240 的方屏这里通常填 80 */ #define LCD_CS_SET() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET) #define LCD_CS_CLR() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) #define LCD_DC_SET() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_3, GPIO_PIN_SET) #define LCD_DC_CLR() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_3, GPIO_PIN_RESET) #define LCD_RST_SET() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET) #define LCD_RST_CLR() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET) #define LCD_BL_ON() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET) /* 模拟方式用的两条线,SDA 需要频繁改方向,所以单独给读写宏 */ #define LCD_SCL_SET() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_SET) #define LCD_SCL_CLR() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_RESET) #define LCD_SDA_WRITE(x) HAL_GPIO_WritePin(GPIOB, GPIO_PIN_15, (x) ? GPIO_PIN_SET : GPIO_PIN_RESET)

LCD_X_OFFSET和LCD_Y_OFFSET这两个看着不起眼的宏,是后面偏移问题的总开关。ST7789V 的显存是 240×320 的矩形,而市面上 1.3 寸方屏只露出中间 240×240 一块,剩下的行被玻璃盖住了;不补偏移,画面就会整体上移 80 行,底部露出黑边。这个坑我在第五章单独展开。

引脚表对应关系,照着接就行:

屏排线STM32 引脚说明
VCC3.3V模组自带 LDO 的可以接 5V,看丝印
GNDGND和 MCU 共地,别只接一根
SCLPA5(SPI1_SCK)模拟方式时随便挑一个普通 IO
SDAPA7(SPI1_MOSI)只写不读,MISO 不用接
RESPA2复位,低有效
DCPA3四线专用,三线悬空
CSPA4单屏可以直接接地,但建议留着
BLPB0背光,串限流电阻或直接接三极管

3. 点亮第一步:复位时序与初始化命令表的落地

屏不亮,八成栽在两件事上:复位没做对,或者初始化序列里某条命令的延时不够。ST7789V 从上电到能接收命令,中间有一段它自己都说不清状态的窗口期,数据手册里那些ms级的等待时间不是建议,是硬性要求。这份例程把初始化和绘图彻底分开:ST7789_Init()只负责把屏带到「可以写显存」的状态,之后所有画面都由上层调用填充和画点函数完成。

3.1 上电、复位与那 120 毫秒

硬件复位和软复位都要走一遍,顺序别颠倒。硬件复位靠 RES 引脚拉低,软复位靠0x01命令。有些模组把 RES 直接上拉了,看起来省事,实际上电瞬间的复位脉冲宽度不受控,冷启动偶发不亮就是它。

void ST7789_Init(void) { st7789_bus_init(); /* SPI 或 GPIO 初始化,必须先于任何通信 */ LCD_CS_SET(); LCD_RST_SET(); HAL_Delay(10); /* 上电稳定 */ LCD_RST_CLR(); /* 硬复位:低电平至少 10us,这里给足 20ms */ HAL_Delay(20); LCD_RST_SET(); HAL_Delay(120); /* 复位释放后等 120ms,等内部电荷泵稳定 */ st7789_wr_cmd(0x01); /* SWRESET 软复位,双保险 */ HAL_Delay(120); st7789_exec_init_seq(); /* 下发初始化命令表,见 3.2 */ LCD_BL_ON(); /* 背光最后开,避免上电花屏闪一下 */ }

几个参数值得说清楚:HAL_Delay(120)这两个 120ms 是从 ST7789V 手册的复位时序表里抄的,不是拍脑袋;背光放在最后开,是因为初始化过程中显存内容是随机的,先开背光用户会看到一屏雪花再变正常,观感很差。如果你的板子背光用 PWM 调光,把LCD_BL_ON()换成把占空比从 0 慢慢拉到目标值,能做出淡入效果。

还有一点容易忽略:HAL_Delay()依赖 SysTick,如果你的工程在main()里很早就调ST7789_Init(),先确认HAL_Init()已经跑过,否则这几个延时会被直接跳过,屏就真的不亮了。

3.2 初始化命令表:把 datasheet 抄成数组

初始化序列有二十来条命令,写成二十个wr_cmd+wr_data调用既难读也难改。例程用一张表驱动,结构和内容分离,换屏型只改表。

typedef struct { uint8_t cmd; /* 命令字 */ uint8_t len; /* 参数字节数,0 表示无参数 */ uint8_t dat[14]; /* 参数,最多 14 字节(伽马表用满) */ uint16_t delay_ms; /* 命令下发后的等待时间 */ } lcd_init_cmd_t; static const lcd_init_cmd_t st7789_init_seq[] = { {0x11, 0, {0}, 120}, /* SLPOUT 退出睡眠,必须等 120ms */ {0x3A, 1, {0x55}, 0}, /* COLMOD:16bit/pixel,RGB565 */ {0x36, 1, {0x00}, 0}, /* MADCTL:竖屏、RGB 顺序、不镜像 */ {0xB2, 5, {0x0C,0x0C,0x00,0x33,0x33}, 0}, /* PORCTRL 前后肩,影响边缘显示 */ {0xB7, 1, {0x35}, 0}, /* GCTRL 门电压 */ {0xBB, 1, {0x19}, 0}, /* VCOMS */ {0xC0, 1, {0x2C}, 0}, /* LCMCTRL */ {0xC2, 1, {0x01}, 0}, {0xC3, 1, {0x12}, 0}, /* VRHS */ {0xC4, 1, {0x20}, 0}, /* VDVS */ {0xC6, 1, {0x0F}, 0}, /* 帧率约 60Hz */ {0xD0, 2, {0xA4,0xA1}, 0}, /* PWCTRL1 */ {0xE0, 14, {0xD0,0x04,0x0D,0x11,0x13,0x2B,0x3F,0x54, 0x4C,0x18,0x0D,0x0B,0x1F,0x23}, 0}, /* 正极性伽马 */ {0xE1, 14, {0xD0,0x04,0x0C,0x11,0x13,0x2C,0x3F,0x44, 0x51,0x2F,0x1F,0x1F,0x20,0x23}, 0}, /* 负极性伽马 */ {0x21, 0, {0}, 0}, /* INVON:多数 IPS 模组要开反显 */ {0x13, 0, {0}, 10}, /* NORON 普通显示模式 */ {0x29, 0, {0}, 20}, /* DISPON 开显示 */ };

执行函数就一个循环,逻辑简单但必须在命令之间插延时:

static void st7789_exec_init_seq(void) { uint16_t n = sizeof(st7789_init_seq) / sizeof(st7789_init_seq[0]); for (uint16_t i = 0; i < n; i++) { st7789_wr_cmd(st7789_init_seq[i].cmd); if (st7789_init_seq[i].len) { st7789_wr_data(st7789_init_seq[i].dat, st7789_init_seq[i].len); } if (st7789_init_seq[i].delay_ms) { HAL_Delay(st7789_init_seq[i].delay_ms); } } }

表里有两处最容易出问题。一是0x3A的0x55,它代表 16 位色,也就是 RGB565,每像素两字节;如果你的工程按 18 位色发数据,画面会整体偏色。二是0x21(INVON),几乎所有 IPS 模组的颜色默认是反的,不开这条命令得到的是负片效果——白底变黑底,蓝色变橙色,很多人第一反应是 RGB 顺序错了,其实只是忘了反显。

3.3 窗口、扫描方向与偏移量

ST7789V 的绘图流程是固定的:先用CASET设定列范围,用RASET设定行范围,然后发RAMWR,之后连续送进来的像素数据就按从左到右、从上到下的顺序填进这个矩形。窗口设一次可以连发一整屏,这是刷屏性能的关键。

void st7789_set_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { uint8_t buf[4]; x0 += LCD_X_OFFSET; x1 += LCD_X_OFFSET; y0 += LCD_Y_OFFSET; y1 += LCD_Y_OFFSET; st7789_wr_cmd(0x2A); /* CASET 列地址设置 */ buf[0] = x0 >> 8; buf[1] = x0 & 0xFF; buf[2] = x1 >> 8; buf[3] = x1 & 0xFF; st7789_wr_data(buf, 4); /* 必须四个字节一起发,中间不能插命令 */ st7789_wr_cmd(0x2B); /* RASET 行地址设置 */ buf[0] = y0 >> 8; buf[1] = y0 & 0xFF; buf[2] = y1 >> 8; buf[3] = y1 & 0xFF; st7789_wr_data(buf, 4); st7789_wr_cmd(0x2C); /* RAMWR,后面全是像素数据 */ }

参数都是闭区间,也就是包含x1和y1那一列那一行,算填充像素数的时候记得+1。这地方我见过太多人写成(x1 - x0) * (y1 - y0),结果屏幕右下角永远少一行一列,看起来像是屏有问题。

0x36(MADCTL)控制扫描方向,几个常用值背下来能省不少事:

值效果
0x00竖屏,左上角为原点
0x60竖屏旋转 180 度
0x70横屏,原点在左上
0xA0横屏旋转 180 度
0x08在以上任意值基础上按位或,切 BGR 顺序

做横竖屏切换的产品,不要重新初始化整条序列,只重发0x36加上新的偏移计算就够了,速度快得多。

3.4 全屏填充:从逐字节到 DMA 行缓冲

最朴素的填充实现是这样的,适合第一次点屏验证:

void st7789_fill(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) { uint32_t px = (uint32_t)(x1 - x0 + 1) * (y1 - y0 + 1); uint8_t hi = color >> 8, lo = color & 0xFF; st7789_set_window(x0, y0, x1, y1); LCD_DC_SET(); LCD_CS_CLR(); /* 整块只拉一次 CS,省掉每字节的开销 */ while (px--) { HAL_SPI_Transmit(&hspi1, &hi, 1, 10); HAL_SPI_Transmit(&hspi1, &lo, 1, 10); } LCD_CS_SET(); }

逻辑没错,但性能很差:每个字节调一次 HAL 函数,函数里还要判状态、查句柄,真正的 SPI 传输时间反而是零头。240×320 全屏是 153600 字节,这么写常见是 200ms 以上,动画基本没戏。

例程里另给了一版行缓冲 + DMA 的实现,思路是把一行像素先拼到内存里,再一次性丢给 DMA:

static uint8_t line_buf[LCD_W * 2]; /* 一行 240 像素,每像素 2 字节 */ void st7789_flush_area(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, const uint16_t *pix) { uint16_t w = x1 - x0 + 1; uint16_t h = y1 - y0 + 1; st7789_set_window(x0, y0, x1, y1); LCD_DC_SET(); LCD_CS_CLR(); for (uint16_t row = 0; row < h; row++) { /* RGB565 在内存里按高字节在前排好,和屏的期望顺序一致 */ for (uint16_t col = 0; col < w; col++) { uint16_t c = pix[row * w + col]; line_buf[col * 2] = c >> 8; line_buf[col * 2 + 1] = c & 0xFF; } HAL_SPI_Transmit_DMA(&hspi1, line_buf, w * 2); while (hspi1.State != HAL_SPI_STATE_READY) { } /* 等这一行发完 */ } LCD_CS_SET(); }

关键参数是line_buf的大小和 DMA 的等待方式。缓冲区按LCD_W * 2分配,一行发一次,内存占用 480 字节,对小容量芯片友好;等 DMA 用的是轮询State,简单但会空转,讲究一点的做法是用HAL_SPI_TxCpltCallback回调里置标志位,或者用双缓冲让下一行的拼装和当前行的发送重叠。我一般先用轮询版把画面跑通,确认没问题再换双缓冲,因为双缓冲一旦指针搞错,症状是画面隔行错位,比不亮更难查。

4. 通信层实现:四线 SPI、三线九位与 GPIO 模拟

底层的活儿只有两件:送一个字节的命令,送一串字节的数据。三种方式的差别全在这两个函数的实现里。例程用一个ST7789_BUS_MODE宏在三份实现之间切换,编译时只留一份,不占空间也不影响速度。

4.1 四线模式:DC 线决定一切

四线是最没有悬念的实现,DC 拉低发命令,拉高发数据,CS 在整块传输期间保持低电平。

void st7789_wr_cmd(uint8_t cmd) { LCD_DC_CLR(); /* DC=0:命令 */ LCD_CS_CLR(); HAL_SPI_Transmit(&hspi1, &cmd, 1, 100); LCD_CS_SET(); } void st7789_wr_data(const uint8_t *buf, uint32_t len) { LCD_DC_SET(); /* DC=1:数据 */ LCD_CS_CLR(); HAL_SPI_Transmit(&hspi1, (uint8_t *)buf, len, 1000); LCD_CS_SET(); }

两个细节。第一,CS 在每块传输结束后拉高,是因为有些模组的 CS 不拉高就不会锁存最后一个字节,尤其是初始化序列连着发的时候,看起来命令都发了,实际最后一条没生效。第二,HAL_SPI_Transmit的超时参数别图省事填个 10,长数据传输在低速时钟下会超时返回 HAL_TIMEOUT,而你不会立刻发现——数据只发了一半,屏上就是一条撕裂的横线。

追求极限速度的话,可以把HAL_SPI_Transmit换成直接写SPI1->DR再等TXE,省掉函数调用和状态机判断,实测在 72MHz 主频、SPI 分频 4 的条件下能提升 15% 左右。但别一上来就优化,先让屏亮。

4.2 三线九位:把 DC 塞进第 9 个 bit

三线模式把 DC 信息放进每次传输的第 9 位:先发一位 D/C,再发 8 位数据。对 SPI 外设的要求是数据帧长度可配到 9 位。STM32F4、F0、L4 这几个系列的 SPI 支持 4~16 位帧,配一下就行;F1 系列只有 8 位和 16 位两档,做不了。

/* 初始化时把数据帧长度改成 9 位 */ hspi1.Init.DataSize = SPI_DATASIZE_9BIT; HAL_SPI_Init(&hspi1); /* 每次发 9 位:bit8 是 D/C,低 8 位是数据 */ static void spi9_write(uint8_t dc, uint8_t dat) { uint16_t frame = ((uint16_t)(dc & 0x01) << 8) | dat; /* HAL 的 Transmit 按字节计数,9 位帧必须传 2 字节长度,否则少发一位 */ HAL_SPI_Transmit(&hspi1, (uint8_t *)&frame, 2, 10); } void st7789_wr_cmd(uint8_t cmd) { spi9_write(0, cmd); } void st7789_wr_data(const uint8_t *b, uint32_t n) { LCD_CS_CLR(); for (uint32_t i = 0; i < n; i++) spi9_write(1, b[i]); LCD_CS_SET(); }

这里有个极其隐蔽的坑:HAL_SPI_Transmit的长度参数是按「数据单元个数」算还是按「字节数」算,不同版本的 HAL 行为不一致,有的版本传 1 只发一个 9 位帧,有的版本按字节数换算会把 9 位帧截成 8 位。稳妥的做法是传 2 并在发送前确认hspi1.Init.DataSize确实生效了,或者干脆绕开 HAL 直接操作寄存器:

static void spi9_write_reg(uint8_t dc, uint8_t dat) { SPI1->DR = ((uint16_t)(dc & 0x01) << 8) | dat; while (!(SPI1->SR & SPI_SR_TXE)); while (SPI1->SR & SPI_SR_BSY); }

也有工程试过用 16 位帧「借位」,把 D/C 放 bit15、数据放 bit14~7,靠每次 CS 拉高截断多余的 7 个时钟。这招在部分模组上能跑,但我手上的屏有两款会因为多余的时钟丢首字节,所以不建议作为正式方案,只在实在没有别的办法时试一下。

4.3 模拟方式:手写时序,先保证能亮再谈速度

模拟方式把 SCL 和 SDA 当普通 IO 用,自己在代码里打 9 个时钟。它的价值不在性能,在于排除法:当硬件 SPI 怎么调都不亮时,换成模拟能亮,说明问题在 SPI 配置;换模拟也不亮,问题在接线、供电或者初始化序列。

static inline void lcd_delay(void) { __NOP(); __NOP(); __NOP(); } static void sim_write9(uint8_t dc, uint8_t dat) { LCD_CS_CLR(); /* 第 1 位:D/C。屏在 SCL 上升沿采样,所以先摆好数据再拉高 */ LCD_SDA_WRITE(dc); LCD_SCL_SET(); lcd_delay(); LCD_SCL_CLR(); /* 后 8 位:高位在前 */ for (int8_t i = 7; i >= 0; i--) { LCD_SDA_WRITE((dat >> i) & 0x01); LCD_SCL_SET(); lcd_delay(); LCD_SCL_CLR(); } LCD_CS_SET(); } void st7789_wr_cmd(uint8_t cmd) { sim_write9(0, cmd); } void st7789_wr_data(const uint8_t *b, uint32_t n) { LCD_CS_CLR(); for (uint32_t i = 0; i < n; i++) sim_write9(1, b[i]); LCD_CS_SET(); }

lcd_delay()里的几个__NOP()是调出来的。空转太少,SCL 高电平时间不够,屏采不到;空转太多,刷一屏要好几秒。判断标准很简单:如果刷屏时偶尔出现整行错位的点,就是太快了;如果慢得不能忍,就减 NOP,直到出错再回退一档。用示波器看 SCL 频率最直接,ST7789V 的写周期下限通常在 100ns 量级,实际能跑多快取决于排线长度和模组上的电平转换芯片。

还有一个容易忽略的点:模拟方式下如果 SDA 引脚配成了开漏输出又没加上拉,波形上升沿会非常缓,低速能跑,一调快就丢数据。推挽输出是更稳的选择,屏只收不发,不存在总线冲突。

4.4 速率实测与选型:一张表看清差距

同一块 240×320 的屏,三种方式在 168MHz 主频下刷满一屏纯色的实测数据大致如下(不同工程差异较大,仅作量级参考):

方式SCK 频率全屏填充耗时CPU 占用备注
四线 + HAL 逐字节21 MHz约 210 ms100%能亮,不适合动画
四线 + DMA 行缓冲42 MHz约 22 ms约 20%推荐配置
三线九位 + DMA42 MHz约 24 ms约 20%多一次位拼接
GPIO 模拟约 3 MHz约 380 ms100%只用于验证

差距主要不在时钟频率,而在「每字节一次函数调用」和「一次传一行」的区别。所以即便只能用模拟方式,也把sim_write9内联到循环里,别写成两层函数嵌套,省下来的开销相当可观。

5. 排查手册:白屏、花屏、偏移与撕裂的现场记录

屏类问题有个讨厌的特点:不同原因的表现高度相似。白屏可能是没复位、可能是背光没开、也可能是初始化根本没跑。下面这几条是我和同事在调试过程中真实记录下来的,按「现象 → 原因 → 解决」整理,遇到问题时按顺序对照,比盲改代码快得多。

5.1 背光亮但屏幕全白或全黑

现象:上电后背光正常,屏幕要么纯白要么纯黑,没有任何内容,串口打印显示初始化函数确实执行完了。

原因:最常见的是SLPOUT(0x11)后面那 120ms 没等够,屏还在睡眠状态就收到了后续命令,全部被忽略;其次是DISPON(0x29)没发或者发了但中间夹了错误数据,显示一直关着;还有一种情况是0x3A的0x55写成了0x50,屏按 8 位色解析 16 位数据,画面虽然「有内容」但看起来就是一片杂色,容易被误判成全白。

解决:先量 RES 引脚的复位波形,确认低电平持续时间和释放后的延时;再在初始化序列里逐条加串口打印,确认每条命令都返回了(HAL_SPI_Transmit返回HAL_OK);最后用st7789_fill(0, 0, LCD_W-1, LCD_H-1, 0xF800)刷一块纯红,红都出不来就是初始化没生效,跟绘图代码无关。

5.2 花屏、颜色反了、红蓝对调

现象:能显示内容,但颜色完全不对,比如红色显示成蓝色,或者整个画面像负片。

原因:分两种。负片效果是INVON(0x21)没开,IPS 模组基本都需要;红蓝对调是 MADCTL 的 BGR 位(0x08)设反了,RGB 面板要清掉这一位,BGR 面板要置上。还有一种碎块状的花屏,是 SPI 时钟太快或者排线接触不良导致高位丢数据,表现是颜色值整体偏移。

解决:先把0x21加上试一次,负片问题立刻消失;红蓝对调就改0x36的参数,在0x00和0x08之间切一下对比。花屏且伴随随机性的话,把 SPI 分频调大一档(比如从 4 改成 8),问题消失就是速度问题,不用怀疑代码。

5.3 图像整体偏移,或者右边出现随机噪点

现象:画面能显示,但整体上移了几十行,底部一条黑边;或者图像位置对但右边多出几列雪花。

原因:偏移是LCD_Y_OFFSET没设。240×240 的方屏是从 240×320 的显存里「抠」出来的,Y 方向通常要补 80 行,X 方向补 0;135×240 的长条屏则是 X 补 52、Y 补 40。右边多噪点一般是窗口宽度超过了实际可视宽度,x1设成了LCD_W而不是LCD_W - 1。

解决:把st7789_fill的四个角各刷一个 10×10 的色块,看哪一个落在屏幕外,就能反推出该补多少偏移。窗口参数统一用LCD_W - 1这种写法,别硬编码数字。

5.4 局部刷新撕裂、刷新到一半卡住

现象:做局部刷新时画面出现横条撕裂,或者刷着刷着程序卡死在某个while里。

原因:撕裂是因为在RAMWR之后没等数据发完就改了窗口,屏把新命令当成了像素数据;卡死多半是 DMA 传输没等完成标志就复用了缓冲区,或者HAL_SPI_Transmit_DMA返回HAL_BUSY后没处理,状态机一直卡在忙。

解决:任何改窗口的操作之前,先确认上一批数据真的发完了,用while (hspi1.State != HAL_SPI_STATE_READY)或传输完成回调里的标志位都行。开 DMA 的话,缓冲区别用局部变量,一定用static或者全局,否则函数返回后 DMA 还在读一片已经被复用的栈内存,症状是画面随机错乱且极难复现。

5.5 模拟方式下偶发丢点、整行错位

现象:用 GPIO 模拟方式,大多数时候正常,偶尔出现一整行像素错位或者某个点颜色不对。

原因:lcd_delay()的空转次数在临界点上,编译优化等级一变(-O0改-O2)延时就不够了;另一种是 SDA 和 SCL 的翻转顺序写反了,先拉 SCL 再改 SDA,导致屏在上升沿采到的是上一位数据。

解决:把延时改成基于DWT->CYCCNT的精确计数,或者至少加个volatile防止被优化掉;翻转顺序统一成「先摆数据,再拉时钟,延时,再拉低」。这类问题用逻辑分析仪抓一段波形最直观,看一眼就明白是哪一位错了。

6. 二次开发:把例程改成自己的显示层

例程只到「能画点、能画字符」这一层,真正做产品还得往上加东西。我一般不在st7789.c里堆绘图函数,而是抽一层显存:给驱动加一个uint16_t framebuf[LCD_W * LCD_H](240×320 是 150KB,F4 系列带外扩 SRAM 或者 F7/H7 才吃得下),所有画点、画线、画矩形、贴图都改这个数组,最后统一调一次st7789_flush_area刷到屏上。这样做的好处是双缓冲能做起来——画的时候刷上一帧,用户看不到中间过程。内存不够的话退回「脏矩形」方案,只记录被改动的矩形区域,刷完清空。

字体这块,例程只带 8x16 的 ASCII 点阵。要显示中文,常见做法是用取模软件生成字模数组,按「宽、高、起始索引、字模数据」组织成结构体,用二分查找或者 ASCII 偏移定位。别把整个字库都编译进 Flash,选常用几百字就够,剩下的按需从上位机下发。

改完之后怎么验证驱动没被改坏,我有一套固定流程,每次改动都跑一遍:

步骤目标画面能暴露的问题
1全屏纯红/绿/蓝交替颜色通道接反、初始化序列缺命令
2四角各一个 10×10 白块偏移量、窗口边界
3从上到下的灰阶渐变像素格式、高低字节顺序
41 像素间隔的棋盘格单像素寻址、窗口步进
5100 帧连贯动画DMA 缓冲复用、撕裂

这套流程是吃过亏换来的。早些年调一块方屏,前四步全过,一上动画就偶尔闪一下白条,查了两个晚上才发现是 DMA 缓冲区和一个局部数组地址重叠,仿真器看不到,只有实际跑才会撞上。从那以后我每次改完刷屏路径,都强制把第五步的动画跑满五分钟再提交代码。

希望这些能帮你少走两晚弯路,剩下的就交给你的板子了。

本文还有配套的精品资源,点击获取

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

Unity原生集成OpenPose:C#实时人体姿态估计全链路实践

简介&#xff1a;这是一份面向Unity开发者的人体姿态识别开发套件&#xff0c;基于OpenPose深度学习模型封装而成&#xff0c;专为C#与Unity实时交互场景设计&#xff0c;解决游戏、虚拟现实及动作捕捉中多人关键点检测的技术集成难题。资源包共162个文件&#xff0c;含20张测试…

作者头像 李华
网站建设 2026/11/5 6:56:35

夜景图看着很高级,为什么一做视频就露馅?

做 AI 图片时&#xff0c;有一种情况特别容易让人误判&#xff1a;缩略图看着很有电影感&#xff0c;放大以后却像一团精心调过色的雾。 这类问题在夜景、科幻、灾难、雨夜和室内弱光画面里尤其常见。生成阶段似乎什么都有&#xff0c;人物、建筑、火光、烟雾一个不少&#xff…

作者头像 李华
网站建设 2026/11/5 6:56:12

C# WPF餐饮系统三层架构实战解析

简介&#xff1a;这是一套面向C#初学者与WPF桌面开发进阶者的半成品餐饮管理系统源码&#xff0c;适用于学习餐饮业务逻辑建模、触屏交互设计及多食法&#xff08;如堂食、外卖、自取&#xff09;场景下的界面架构。资源包含完整VS解决方案&#xff0c;涵盖数据库脚本、WPF主界…

作者头像 李华
网站建设 2026/11/5 6:55:34

调试通过的KPCA人脸识别代码:核矩阵中心化与特征值分解实战

简介&#xff1a;这份资源是一套已调试通过、可直接运行的核主成分分析&#xff08;KPCA&#xff09;人脸识别程序&#xff0c;面向正在学习机器学习与计算机视觉、希望动手实践非线性降维与特征提取的读者。压缩包共403个文件&#xff0c;以400张pgm格式的ORL人脸库图像为主体…

作者头像 李华
网站建设 2026/11/5 6:53:53

UEFI ASL 模块怎么写?基于 AMI Aptio 平台手把手完整实现(附代码)

📖 前言 真实产线场景往往是这样:你手头有一块 Coffee Lake / Whiskey Lake 工控主板,厂商只给了一个 .ROM,而你需要—— 给自研板卡挂一个 ACPI 设备; 补一段唤醒逻辑、温度上报方法; 让 OS 侧驱动能通过 _DSM 拿到板卡私有配置。 这些需求的答案都指向同一件事:往固件…

作者头像 李华