简介:面向STM32初学者与嵌入式显示开发者的实操型驱动代码包,聚焦在STM32F103C8T6上以HAL库配合软件SPI方式点亮并驱动1.8寸ST7735 TFT彩屏,解决硬件SPI资源紧张或引脚受限时的屏幕驱动难题。包内共86个文件、约4.14MB,以Keil MDK工程文件(uvprojx、uvoptx)、C源文件与头文件为核心,配套启动汇编、分散加载sct、map映射、hex与axf固件以及ioc配置和调试记录等,覆盖从工程配置到编译烧录的完整链路。已有5735人学习下载,说明该方案在实际项目中具备较高的可复用性。资源中包含ST7735初始化序列、命令与像素数据发送、GPIO模拟时钟与数据线的软件SPI实现,以及说明文档,读者可据此理解SPI时序模拟要点、屏幕刷屏流程与工程目录组织方式,快速移植到自己的显示项目并排查花屏、无显示等常见问题。
1. 一块 1.8 寸屏接到 F103 上:为什么我最终选了软件 SPI
手里有块 STM32F103C8T6 最小系统板,六十来块买的一包 1.8 寸 ST7735 模块,排针还没焊。第一次点亮的时候我没用硬件 SPI,直接拿四个普通 GPIO 怼上去,很多人觉得这是绕远路,但对 stm32 tft 这类小屏来说,软件 SPI 反而是最省心的起步方式——引脚随便挪、不用抢 SPI1、出问题能一眼一眼数时序。这套「HAL 库 + 软件 SPI + ST7735」的驱动,解决的就是“屏能亮、字能显示、颜色对得上、刷新别太慢”这四个最朴素的需求,适合刚上手 HAL、又不想在 CubeMX 的复用功能里绕圈的人,也适合把屏当调试输出用的老手。
2. 硬件连接与工程骨架:先把一根线一根线落实
2.1 引脚分配与供电判断
拿到模块第一件事不是写代码,是看背面的丝印和排针。1.8 寸 ST7735 模块常见的引脚命名有VCC/GND/CS/RESET(A0)/DC/SDA/SCL/BLK,也有把SDA写成MOSI、SCL写成SCK的,本质一样,因为我们只用单向写。先量一下 VCC 到 GND 之间的电阻,再看板上有没有 AMS1117 或 74HC245 之类的芯片,有稳压就敢给 5V,没有就老老实实 3.3V——STM32 的 IO 是 3.3V 电平,屏的逻辑电平如果是 5V tolerant 倒无所谓,但如果是纯 3.3V 器件,5V 供电会当场烧给你看。
我这次的分配是这样的,全部在 GPIOA 上,方便后面用 BSRR 整组操作:
| 屏引脚 | 功能 | STM32 引脚 | 配置 |
|---|---|---|---|
| CS | 片选,低有效 | PA4 | 推挽输出,初始高 |
| SCL | 时钟 | PA5 | 推挽输出,初始低 |
| SDA | 数据(单向) | PA7 | 推挽输出 |
| DC | 命令/数据选择 | PA6 | 推挽输出 |
| RES | 复位,低有效 | PA3 | 推挽输出,初始高 |
| BLK | 背光 | PA2 | 推挽输出,初始低 |
注意 RES 和 BLK 的初始电平别搞反:RES 上电默认拉高(不复位),BLK 默认拉低(背光关),等初始化走完再开背光,这样调试时能区分“屏没初始化”和“屏初始化了但背光没开”。
提示:GPIO 输出速度一定要设成
GPIO_SPEED_FREQ_HIGH。默认的 Low 档在 72MHz 下上升沿能拖到几百纳秒,软件 SPI 一快就丢位。
2.2 CubeMX 里最小化配置
时钟树按最常规的走:外部 8MHz 晶振 → PLL 9 倍频 → SYSCLK 72MHz,APB2 也是 72MHz,因为我们全部用 GPIO,不分频也无所谓。调试口开 SWD,串口开一个 USART1(PA9/PA10)留着打日志,屏出问题时能第一时间知道程序跑到哪一步。GPIO 按上表逐个点成 Output,初始电平照表格填。
生成工程的时候有个选项要留意:Generate peripheral initialization as a pair of .c/.h files,勾上,后面把 LCD 驱动单独放一对文件里,不跟main.c搅在一起。生成出来先编译一次,确认没报错再往里加东西,这是我最便宜的一条后悔药——很多人一上来就贴驱动,最后分不清是配置错还是代码错。
2.3 软件 SPI 的时序底座
ST7735 的 SPI 是模式 0(CPOL=0,CPHA=0),也就是时钟空闲为低、上升沿采样。软件模拟就三步:拉低 SCL、摆好 SDA、拉高 SCL。这里有一个反直觉的点——数据要在 SCL 上升沿之前就稳定,所以顺序必须是「先摆数据,再抬时钟」,写反了在低速下能凑合,一快就全乱。
/* lcd_port.h:把引脚操作收敛成宏,换板子只改这一个文件 */ #define LCD_CS_L() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) #define LCD_CS_H() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET) #define LCD_SCL_L() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET) #define LCD_SCL_H() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET) #define LCD_SDA_L() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_RESET) #define LCD_SDA_H() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_SET) #define LCD_DC_CMD() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_6, GPIO_PIN_RESET) #define LCD_DC_DAT() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_6, GPIO_PIN_SET) /* 写一个字节,高位先行,MSB first */ void LCD_WriteByte(uint8_t dat) { for (uint8_t i = 0; i < 8; i++) { LCD_SCL_L(); if (dat & 0x80) LCD_SDA_H(); else LCD_SDA_L(); dat <<= 1; LCD_SCL_H(); /* 上升沿,ST7735 在此刻采样 */ } LCD_SCL_L(); /* 收尾拉低,保证空闲电平为低 */ } /* 命令:DC=0,CS 拉低包住整个字节 */ void LCD_WriteCmd(uint8_t cmd) { LCD_DC_CMD(); LCD_CS_L(); LCD_WriteByte(cmd); LCD_CS_H(); } /* 数据:DC=1。批量写时把 CS 一直拉低,别每字节都翻转 */ void LCD_WriteData8(uint8_t dat) { LCD_DC_DAT(); LCD_CS_L(); LCD_WriteByte(dat); LCD_CS_H(); }这段代码里最值钱的是LCD_WriteByte的循环顺序和LCD_CS_H()的位置。CS 在单字节函数里翻转一次没问题,但LCD_WriteData8每调一次就抬高一次 CS,ST7735 会把每次 CS 上升沿当成一次传输结束,连续写像素时这样干,速度直接砍半,后面第 4 章会专门改成批量接口。LCD_DC_CMD/DAT放在 CS 拉低之前设置,是为了避免 DC 跳变被误采样成命令——有些批次的 ST7735 对 DC 建立时间敏感,先设 DC 再拉 CS 是最稳的写法。
3. ST7735 初始化序列拆解:寄存器表不是抄完就完事
3.1 初始化流程的分段理解
ST7735 的初始化动辄四五十条命令,照着抄很容易抄出黑屏。我的习惯是把它切成四段看:复位与唤醒(SWRESET、SLPOUT)、电源与帧率(FRMCTR1~3、PWCTR1~5、VMCTR1)、显示方向与像素格式(MADCTL、COLMOD)、伽马与开显示(GMCTRP1/N1、NORON、DISPON)。前两段错了屏不亮,第三段错了颜色或方向不对,第四段错了画面发灰发白。分段之后,任何一段出问题都能单独回退验证,而不是对着一整坨代码怀疑人生。
下面这张表是我实际用的序列,1.8 寸 128×160 屏通用,不同批次模块可能在小处有差异,但大框架一致:
| 命令 | 名称 | 参数 | 说明 |
|---|---|---|---|
| 0x01 | SWRESET | - | 软复位,之后延时 150ms |
| 0x11 | SLPOUT | - | 退出睡眠,延时 500ms,这条不能省 |
| 0xB1 | FRMCTR1 | 01 2C 2D | 正常模式帧率 |
| 0xB4 | INVCTR | 07 | 反显控制 |
| 0xC0 | PWCTR1 | A2 02 84 | 电压设置,跟随模块厂商 |
| 0xC1 | PWCTR2 | C5 | |
| 0xC5 | VMCTR1 | 0E | VCOM 电压 |
| 0x20 | INVOFF | - | 关反显,颜色不对先查这条 |
| 0x36 | MADCTL | C8 | 方向 + RGB 顺序,见 3.2 |
| 0x3A | COLMOD | 05 | 16bit/pixel,即 RGB565 |
| 0x2A | CASET | 00 00 00 7F | 列地址 0~127 |
| 0x2B | RASET | 00 00 00 9F | 行地址 0~159 |
| 0xE0 | GMCTRP1 | 16 字节 | 正伽马 |
| 0xE1 | GMCTRN1 | 16 字节 | 负伽马 |
| 0x13 | NORON | - | 正常显示模式 |
| 0x29 | DISPON | - | 开显示,延时 100ms |
写成一个带参数的命令表结构体,循环下发,比一条条函数调用好维护:
typedef struct { uint8_t cmd; uint8_t len; uint8_t data[16]; uint16_t delay_ms; } LCD_InitCmd_t; static const LCD_InitCmd_t st7735_init_seq[] = { {0x01, 0, {0}, 150}, {0x11, 0, {0}, 500}, {0xB1, 3, {0x01, 0x2C, 0x2D}, 0}, {0xB4, 1, {0x07}, 0}, {0xC0, 3, {0xA2, 0x02, 0x84}, 0}, {0xC1, 1, {0xC5}, 0}, {0xC5, 1, {0x0E}, 0}, {0x20, 0, {0}, 0}, {0x36, 1, {0xC8}, 0}, {0x3A, 1, {0x05}, 0}, {0x2A, 4, {0x00, 0x00, 0x00, 0x7F}, 0}, {0x2B, 4, {0x00, 0x00, 0x00, 0x9F}, 0}, {0xE0, 16, {0x02,0x1C,0x07,0x12,0x37,0x32,0x29,0x2D, 0x29,0x25,0x2B,0x39,0x00,0x01,0x03,0x10}, 0}, {0xE1, 16, {0x03,0x1D,0x07,0x06,0x2E,0x2C,0x29,0x2D, 0x2E,0x2E,0x37,0x3F,0x00,0x00,0x02,0x10}, 0}, {0x13, 0, {0}, 10}, {0x29, 0, {0}, 100}, }; void LCD_Init(void) { LCD_RST_L(); HAL_Delay(20); LCD_RST_H(); HAL_Delay(120); for (uint32_t i = 0; i < sizeof(st7735_init_seq)/sizeof(st7735_init_seq[0]); i++) { const LCD_InitCmd_t *p = &st7735_init_seq[i]; LCD_WriteCmd(p->cmd); for (uint8_t j = 0; j < p->len; j++) { LCD_WriteData8(p->data[j]); } if (p->delay_ms) HAL_Delay(p->delay_ms); } LCD_BLK_H(); /* 初始化全部走完再开背光 */ }结构体里delay_ms单独列出来,是因为 SWRESET 和 SLPOUT 这两条命令后面跟的延时是硬性要求,少了就是黑屏。硬件复位(RST 拉低再拉高)和软复位(0x01)我两个都做,硬件复位保底,软复位清状态,多花 150ms 换一个确定性,划算。
3.2 窗口与坐标:CASET/RASET 与偏移量
ST7735 的显存是 132×162,而市面上 1.8 寸模块的可视区是 128×160,中间差的那几行几列就是“偏移量”的来源。不同的玻璃封装(俗称红框、绿框、黑框)偏移不一样,常见组合有(0,0)、(2,1)、(0,32)三种。判断方法很土但有效:全屏填红,如果右边或下边漏出一条杂色,说明结束地址少了;如果左边或上边缺一条,说明起始地址没加偏移。
窗口设置函数如下,所有绘图最终都要先落到这个函数上:
/* x0,y0 左上;x1,y1 右下,闭区间 */ void LCD_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { /* 列地址:起始和结束各 16 位,高字节在前 */ LCD_WriteCmd(0x2A); LCD_WriteData8(x0 >> 8); LCD_WriteData8(x0 & 0xFF); LCD_WriteData8(x1 >> 8); LCD_WriteData8(x1 & 0xFF); /* 行地址 */ LCD_WriteCmd(0x2B); LCD_WriteData8(y0 >> 8); LCD_WriteData8(y0 & 0xFF); LCD_WriteData8(y1 >> 8); LCD_WriteData8(y1 & 0xFF); /* 写显存,后续数据从这个窗口左上角开始自增 */ LCD_WriteCmd(0x2C); }0x2C之后的像素数据会按 MADCTL 设定的方向自动走位,列满换行,所以只要窗口设对了,后面连续写(x1-x0+1)*(y1-y0+1)个 16 位颜色值就行,不需要再管坐标。这里有个细节:CASET/RASET 的四个参数顺序是 XS_H, XS_L, XE_H, XE_L,不是起点终点交错,抄错了表现是画面被裁掉一半,很容易误判成屏坏了。
3.3 填屏与字符:从单色块到 8×16 字库
有了窗口函数,填色就三行:
void LCD_Fill(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) { uint32_t n = (uint32_t)(x1 - x0 + 1) * (y1 - y0 + 1); LCD_SetWindow(x0, y0, x1, y1); LCD_DC_DAT(); LCD_CS_L(); /* 整块数据只拉低一次 CS */ while (n--) { LCD_WriteByte(color >> 8); /* RGB565 高字节先发 */ LCD_WriteByte(color & 0xFF); } LCD_CS_H(); }RGB565 的字节序是很多人的第一道坎:颜色值0xF800是红色,发的时候先发0xF8再发0x00,发反了红变蓝、蓝变红,看着像“颜色反了”,其实是字节序反了,跟 MADCTL 的 RGB/BGR 位是两码事。区分方法很简单:纯红0xF800显示成蓝色,是字节序问题;纯红显示成青色,才是 BGR 位设错。
字符显示我一般用 8×16 的 ASCII 点阵,取模软件设置为「阴码、逐列式、逆向(低位在前)」,一个字符 16 字节。绘制函数按列取位、按位画点:
/* 8x16 字库,每个字符 16 字节,列行式 */ extern const uint8_t ascii_8x16[95][16]; void LCD_ShowChar(uint16_t x, uint16_t y, char ch, uint16_t fc, uint16_t bc, uint8_t size) { if (ch < ' ' || ch > '~') return; const uint8_t *p = ascii_8x16[ch - ' ']; LCD_SetWindow(x, y, x + 7, y + 15); LCD_DC_DAT(); LCD_CS_L(); for (uint8_t row = 0; row < 16; row++) { uint8_t bits = p[row]; /* 一行 8 个像素 */ for (uint8_t col = 0; col < 8; col++) { uint16_t c = (bits & 0x01) ? fc : bc; LCD_WriteByte(c >> 8); LCD_WriteByte(c & 0xFF); bits >>= 1; } } LCD_CS_H(); }取模方向必须和代码里的移位方向对上:代码里bits & 0x01然后右移,就是“低位在左”,取模软件里要选逆向。对不上的表现是字符左右镜像或者整个字糊成一团,这类问题不用怀疑硬件,百分之百是取模设置。
4. 刷屏性能优化:软件 SPI 的瓶颈到底在哪
4.1 时间的去向与可接受上限
先算一笔账。72MHz 下,一次HAL_GPIO_WritePin大致是函数调用 + 位带/寄存器写,十几个到二十几个周期;LCD_WriteByte每个 bit 至少两次电平操作加一次判断和移位,8 个 bit 下来保守估计 300~500 个周期,折合 4~7μs 一个字节。128×160 全屏 40960 字节,纯数据时间就奔着 200ms 去了,再加上窗口命令和函数调用开销,全屏刷新 250~300ms 是常态,肉眼可见地“刷一下”。
这个速度能不能用?当调试输出、显示几个传感器数值,完全够;想跑简单动画或者滚动菜单,就得动手。优化的方向只有三个:减少每字节的指令数、减少 CS/DC 翻转、减少函数调用层级。前两个是软件 SPI 的本职,第三个是 C 语言层面的事。
第一步是把HAL_GPIO_WritePin换成直接写 BSRR/BRR。F103 的 GPIO 有置位复位寄存器,一次写操作就能改引脚,不用读-改-写:
/* 直接操作寄存器版本,PA5=SCL,PA7=SDA */ static inline void SCL_H(void) { GPIOA->BSRR = GPIO_PIN_5; } static inline void SCL_L(void) { GPIOA->BRR = GPIO_PIN_5; } static inline void SDA_H(void) { GPIOA->BSRR = GPIO_PIN_7; } static inline void SDA_L(void) { GPIOA->BRR = GPIO_PIN_7; } void LCD_WriteByteFast(uint8_t dat) { /* 循环展开 8 次,去掉 for 的分支判断 */ SCL_L(); if (dat & 0x80) SDA_H(); else SDA_L(); SCL_H(); dat <<= 1; SCL_L(); if (dat & 0x80) SDA_H(); else SDA_L(); SCL_H(); dat <<= 1; SCL_L(); if (dat & 0x80) SDA_H(); else SDA_L(); SCL_H(); dat <<= 1; SCL_L(); if (dat & 0x80) SDA_H(); else SDA_L(); SCL_H(); dat <<= 1; SCL_L(); if (dat & 0x80) SDA_H(); else SDA_L(); SCL_H(); dat <<= 1; SCL_L(); if (dat & 0x80) SDA_H(); else SDA_L(); SCL_H(); dat <<= 1; SCL_L(); if (dat & 0x80) SDA_H(); else SDA_L(); SCL_H(); dat <<= 1; SCL_L(); if (dat & 0x80) SDA_H(); else SDA_L(); SCL_H(); SCL_L(); }展开之后编译器能把if优化成条件传送或者位操作,实测能比 HAL 版本快 3 倍左右,全屏刷新压到 80~100ms。再往上就得考虑把 SCL 和 SDA 放到同一个端口的不同位、用一次 BSRR 同时改两个脚,但这会牺牲引脚自由度,不划算。
注意:优化完一定要回头重新跑一遍初始化。时序变快之后,如果模块的排线比较长或者没有加滤波电容,容易出现“初始化能过、刷屏花屏”的情况,这时候在 SCL_H 之后插一个
__NOP()就能压住。
4.2 区域填充与画线
有了快速写字节,LCD_Fill只需要换掉内部调用。真正影响体验的是局部刷新:屏幕上大部分内容不变时,只重绘变化的矩形区域。比如一个数值从 100 变成 101,只需要重绘那三个字符的位置,而不是整屏重刷。我一般会在应用层维护一个dirty矩形,把所有写操作并进去,主循环末尾统一提交一次。
画线用 Bresenham,不用浮点:
void LCD_DrawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) { int16_t dx = (x1 > x0) ? (x1 - x0) : (x0 - x1); int16_t dy = (y1 > y0) ? (y1 - y0) : (y0 - y1); int16_t sx = (x0 < x1) ? 1 : -1; int16_t sy = (y0 < y1) ? 1 : -1; int16_t err = dx - dy; while (1) { LCD_DrawPoint(x0, y0, color); /* 单点:设 1x1 窗口写一个像素 */ if (x0 == x1 && y0 == y1) break; int16_t e2 = err << 1; if (e2 > -dy) { err -= dy; x0 += sx; } if (e2 < dx) { err += dx; y0 += sy; } } }单点函数每次都要下发 CASET/RASET,画一条 100 像素的线就是 100 次窗口命令,慢得离谱。所以我给LCD_DrawPoint加了一个“相同 y 连续画”的快速路径,或者干脆在画线时按行合并:同一行上的连续像素攒成一个矩形一次填。这个改动看着不起眼,画界面边框时能差出五六倍的体感。
4.3 显存策略与取舍
F103C8T6 只有 20KB RAM,128×160×2 = 40KB 的全屏显存想都别想。剩下的路只有两条:一是“直接写屏”,所有绘制直接落到 ST7735 的 GRAM,优点是零 RAM 开销,缺点是没法做透明度、没法读回、局部刷新要自己算脏矩形;二是“分块缓冲”,比如开 128×16 的一行缓冲(4KB),整行渲染完一次性推给屏,适合做文本终端类的界面。
我这边选的是直接写屏 + 脏矩形,理由是这套代码的使用场景就是参数显示和简单波形,不需要图层合成。如果哪天要上 UI 框架,就得换带 FSMC 的型号或者加外部 SRAM,那是另一个话题了。
5. 常见问题排查:黑屏、花屏、颜色反了、刷屏撕裂
5.1 五条血泪记录
现象一:上电全黑,背光亮着但什么都没有。原因通常出在初始化没走完或者复位时序不对。先量 RES 引脚,上电后应该是低 20ms 再拉高;如果一直是高,说明复位代码没执行到。再查 SLPOUT(0x11)后面的 500ms 延时是不是被删了,很多人为了加快启动把延时改小,结果屏一直躺在睡眠模式里。最后用逻辑分析仪抓 CS 和 DC,看第一条命令 0x01 有没有真的发出去——如果 CS 一直是高,那就是LCD_CS_L()的宏定义写错了端口。
现象二:能显示,但画面整体偏移,右边或下边有一条杂色。这是偏移量没匹配上玻璃。红框屏通常(0,0),绿框常见(2,1),还有些 128×128 的屏要(0,32)。解决方式是在LCD_SetWindow里加两个全局偏移变量,初始化时按模块型号赋值,绘图时统一加上。别硬编码在函数里,换一批模块又要改一遍。
现象三:红色显示成蓝色,蓝色显示成红色。前面提过,先分清楚是字节序还是 BGR。把0xF800发成0x00, 0xF8就是字节序反了,改发送顺序即可;如果字节序对但红蓝还是互换,去改 MADCTL 的 bit3(RGB/BGR),0xC8改成0xC0试试。这两个问题的表现很像,但改的地方完全不同,用一个纯色测试图就能一次定位。
现象四:刷屏时上半屏正常,下半屏撕裂或者错位。典型原因是写数据过程中 CS 被意外拉高,ST7735 把一次连续写拆成了两次,显存指针复位到窗口起点,后面的数据就叠到前面去了。检查批量写函数里有没有在循环内调用了带 CS 翻转的单字节接口。另一个可能是窗口高度算错,比如y1传了 160(越界),实际有效行只有 0~159,多出来的一个像素会把整块数据顶偏。
现象五:换了块板子,同样代码不亮,或者时亮时不亮。先怀疑线。杜邦线超过 15cm、没有共地、SCL 和 SDA 挨得太近,都会让软件 SPI 在高速下丢位。我遇到过最玄学的一次是屏的排针虚焊,按压模块边缘就正常,松开就黑屏,折腾了一下午才用万用表蜂鸣档找出来。软件 SPI 没有硬件外设的采样窗口,对信号质量更敏感,能短则短、能焊则焊。
6. 进阶:把驱动抽成模块,再用一张测试图验收
写到能显示之后,我做的第一件事不是加功能,而是把lcd.c/lcd.h从main.c里彻底剥出来,只暴露六个接口:LCD_Init、LCD_Fill、LCD_DrawPoint、LCD_DrawLine、LCD_ShowChar、LCD_ShowString。所有引脚宏集中在lcd_port.h,换 MCU 只改这一个文件;所有偏移量和方向常量集中在lcd_config.h,换屏只改这一个文件。这个拆分看着是体力活,但它决定了这套代码能不能被你下一次的项目直接抄走。
验收环节我用一张固定的自检图,比“看起来正常”可靠得多:
void LCD_SelfTest(void) { LCD_Fill(0, 0, 127, 159, BLACK); /* 1. 四角打点:验证偏移和方向,四个点必须贴着物理边角 */ LCD_Fill(0, 0, 3, 3, RED); LCD_Fill(124, 0, 127, 3, GREEN); LCD_Fill(0, 156, 3, 159, BLUE); LCD_Fill(124, 156, 127, 159, WHITE); /* 2. 三原色条:验证 RGB565 字节序和 BGR 位 */ LCD_Fill(0, 40, 127, 59, RED); LCD_Fill(0, 60, 127, 79, GREEN); LCD_Fill(0, 80, 127, 99, BLUE); /* 3. 渐变:验证连续写和窗口自增,不能出现台阶或斜纹 */ for (uint16_t x = 0; x < 128; x++) { uint16_t c = (x * 31 / 127) << 11 | (x * 63 / 127) << 5 | (x * 31 / 127); LCD_Fill(x, 110, x, 129, c); } /* 4. 字符:验证字库取模方向和行距 */ LCD_ShowString(4, 134, "0123456789", WHITE, BLACK, 16); }四角打点是这套测试里最关键的一步。四个点如果有一个没贴边,偏移量就是错的,而且能立刻告诉你错在哪个方向:左上角缺一块是 x0/y0 偏大,右下角缺一块是 x1/y1 偏小。渐变条则能抓出“连续写被 CS 打断”的问题——正常的渐变是平滑的,一旦中间有撕裂,会看到明显的竖直分界线。
还有一个我常备的排查手段:把LCD_WriteByte的 SCL 翻转次数用一个全局变量计出来,每刷一屏打印一次。全屏 128×160 理论上是40960 × 8 = 327680次翻转,如果实测对不上,说明有数据被重复发或者漏发,问题一定在循环边界上,比盲猜快得多。这个计数器平时关掉,只在怀疑驱动的时候打开,零成本。
从那以后我每次接一块新屏,都强制先跑一遍这张自检图,四个角、三条色带、一段渐变、一行字符,全过了才往里写业务代码。看起来多花了十分钟,实际省下的是后面几个小时对着花屏怀疑人生的时间。希望帮到你。
本文还有配套的精品资源,点击获取