简介面向嵌入式图形显示开发的 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 线DC0 表示接下来发的是命令DC1 表示是数据。协议最简单所有带 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 MHz240×320 全屏刷新8~20 msDMA 可更快8~20 ms80~200 msMCU 占用低可 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 共地别只接一根SCLPA5SPI1_SCK模拟方式时随便挑一个普通 IOSDAPA7SPI1_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_cmdwr_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}, /* COLMOD16bit/pixelRGB565 */ {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 位色发数据画面会整体偏色。二是0x21INVON几乎所有 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)结果屏幕右下角永远少一行一列看起来像是屏有问题。0x36MADCTL控制扫描方向几个常用值背下来能省不少事值效果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 的实现思路是把一行像素先拼到内存里再一次性丢给 DMAstatic 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(); /* DC0命令 */ 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(); /* DC1数据 */ 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 背光亮但屏幕全白或全黑现象上电后背光正常屏幕要么纯白要么纯黑没有任何内容串口打印显示初始化函数确实执行完了。原因最常见的是SLPOUT0x11后面那 120ms 没等够屏还在睡眠状态就收到了后续命令全部被忽略其次是DISPON0x29没发或者发了但中间夹了错误数据显示一直关着还有一种情况是0x3A的0x55写成了0x50屏按 8 位色解析 16 位数据画面虽然「有内容」但看起来就是一片杂色容易被误判成全白。解决先量 RES 引脚的复位波形确认低电平持续时间和释放后的延时再在初始化序列里逐条加串口打印确认每条命令都返回了HAL_SPI_Transmit返回HAL_OK最后用st7789_fill(0, 0, LCD_W-1, LCD_H-1, 0xF800)刷一块纯红红都出不来就是初始化没生效跟绘图代码无关。5.2 花屏、颜色反了、红蓝对调现象能显示内容但颜色完全不对比如红色显示成蓝色或者整个画面像负片。原因分两种。负片效果是INVON0x21没开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 方向补 0135×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 是 150KBF4 系列带外扩 SRAM 或者 F7/H7 才吃得下所有画点、画线、画矩形、贴图都改这个数组最后统一调一次st7789_flush_area刷到屏上。这样做的好处是双缓冲能做起来——画的时候刷上一帧用户看不到中间过程。内存不够的话退回「脏矩形」方案只记录被改动的矩形区域刷完清空。字体这块例程只带 8x16 的 ASCII 点阵。要显示中文常见做法是用取模软件生成字模数组按「宽、高、起始索引、字模数据」组织成结构体用二分查找或者 ASCII 偏移定位。别把整个字库都编译进 Flash选常用几百字就够剩下的按需从上位机下发。改完之后怎么验证驱动没被改坏我有一套固定流程每次改动都跑一遍步骤目标画面能暴露的问题1全屏纯红/绿/蓝交替颜色通道接反、初始化序列缺命令2四角各一个 10×10 白块偏移量、窗口边界3从上到下的灰阶渐变像素格式、高低字节顺序41 像素间隔的棋盘格单像素寻址、窗口步进5100 帧连贯动画DMA 缓冲复用、撕裂这套流程是吃过亏换来的。早些年调一块方屏前四步全过一上动画就偶尔闪一下白条查了两个晚上才发现是 DMA 缓冲区和一个局部数组地址重叠仿真器看不到只有实际跑才会撞上。从那以后我每次改完刷屏路径都强制把第五步的动画跑满五分钟再提交代码。希望这些能帮你少走两晚弯路剩下的就交给你的板子了。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
Unieap Java简单项目:设备借用登记的事务、并发扣减与验收 简介:Unieap简单小项目是一份面向Java初学者和业务流程开发入门者的示例工程,基于Unieap 3.4框架展示企业级应用的构建思路,适合用来理解工作流引擎、表单设计、数据模型与权限控制等核心组件如何协同。压缩包约82.8MB,文件总数与… · 2026/11/5 6:57:33
基于Unieap的物资申领系统设计:主子表、审批流与分页导出 简介:Unieap 简单小项目面向刚接触业务流程管理框架的 Java 开发者与初学者,提供一份可运行的示例工程,用于理解 Unieap 3.4 在工作流引擎、表单设计、数据模型和权限控制上的基本用法。项目通过实际代码演示应用的启动、流程定义与部署&… · 2026/11/5 6:57:33
C# WinForms 弹出式进度条组件:跨线程进度上报与取消链路设计 简介:这份压缩包面向使用 C# WinForm 开发桌面应用的初中级开发者与自学者,围绕弹出式进度条这一常见交互需求,提供可运行的示例工程与配套代码,帮助理解进度显示、界面响应和任务反馈的实现方式。包内共 28 个文件,以… · 2026/11/5 6:56:33
ADS122U04与dsPIC33EP组合:工业与HVAC多路温度采集实战方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 7:37:45
ADS122U04+STM32L476RG:工业与HVAC精密温度采集系统设计 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 7:37:45
ADS122U04搭配STM32F407ZG:高精度多通道温度采集方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 7:37:45
工业测温实战:ADS122U04与PIC18F85J50的多路温度采集方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 7:37:45
ADS122U04+STM32F765ZI工业温度采集模块:PT100/热电偶精确测量方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 7:37:45
ADS122U04+PIC18F46K42:高精度PT1000测温模块设计与实现 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 7:36:44
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38
多智能体协作系统架构设计与实践优化 1. 多智能体协作系统的时代背景2017年Transformer架构的诞生彻底改变了人工智能的发展轨迹。当行业还沉浸在单一大模型带来的性能突破时,Anthropic的研究团队已经将目光投向了更前沿的领域——如何让多个智能体通过协作产生超越单体能力的系统智能。这就像交响乐团中… · 2026/11/5 0:00:39
大模型系统性入门实战指南:从原理到部署微调的全链路路线 提到大模型的系统性入门,我先说个结论:这事真不难,难的是资料太散、路线太多,今天这个框架明天那个工具,新人很容易在“收藏夹吃灰”和“教程看到一半放弃”之间反复横跳。市面上关于大模型的内容少说有几十万篇&#… · 2026/11/5 0:00:39
鸿蒙文件系统沙箱机制解析与开发实践 1. 鸿蒙文件系统沙箱机制深度解析作为一名从Android转向鸿蒙开发的工程师,我深刻体会到文件系统访问是迁移过程中最先遇到的"拦路虎"。在Android上运行良好的文件读写代码,在鸿蒙真机环境却频频出现文件写入后神秘消失或直接抛出权限拒绝错误的… · 2026/11/5 0:00:39
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38