
简介面向STM32初学者与嵌入式显示开发者的STM32F103C8T6 HAL库驱动ST7735 1.8寸TFT彩屏工程重点解决硬件SPI资源紧张或需要软件模拟时序时的点屏问题适合具备C语言与STM32基础、希望掌握小屏驱动移植的读者。压缩包共86个文件、约4.14MB包含6个C源文件与7个头文件承担HAL初始化、GPIO配置和ST7735驱动另附Keil工程、启动汇编、链接脚本、map与hex等编译调试文件便于直接打开工程、烧录验证和二次移植。已有5735人学习。资料围绕软件SPI通过GPIO模拟时钟、MOSI与NSS信号的实现以及ST7735初始化序列、显示分辨率与颜色模式设置、像素数据和控制命令发送展开读者可据此理解屏幕驱动分层、对照CubeMX配置、排查通信时序也可将工程作为小尺寸彩屏在物联网设备中的显示参考并可用于课程设计或毕业设计中的显示模块验证。1. 从一块 1.8 寸小屏说起STM32F103C8T6 上的软件 SPI 点亮 ST7735手头只有一块 STM32F103C8T6 最小系统板外加一张 1.8 寸 ST7735 的 TFT 彩屏偏偏 SPI1 的引脚已经被别的外设占死了——这种局面下软件 SPI 是最省事的解法随便挑两个空闲 GPIO把时钟和数据一位一位地手工做出时序来屏照样能亮。这套驱动就是干这件事的基于 HAL 库用软件 SPI 驱动 128×160 的 ST7735 彩屏覆盖了从引脚宏定义、初始化寄存器序列、开窗刷屏到画点画线、ASCII 与汉字显示的一整条链路。搜 stm32 tft 的人多半卡在两个地方第一次点屏不知道初始化表怎么写或者屏亮了却白屏、花屏、颜色发蓝。这套代码的配置都是显式的出问题时能一行一行对着修改比那些封装好的库更容易用来排查。2. 工程骨架HAL 库 软件 SPI 的目录与引脚怎么排先把物理连接固定下来再谈代码。软件 SPI 最大的好处是引脚自由但自由也意味着容易接错我一般会先把接线和引脚宏写在一张纸上后面所有调试都围绕这张纸来。2.1 目录结构与文件职责这套驱动的核心文件不多刻意没有做太重的封装目的就是让改屏、改引脚的成本压到最低。Project/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── st7735.h # 引脚宏、屏幕尺寸、对外 API 声明 │ │ ├── lcd_gui.h # 图形层 API点、线、框、字符 │ │ └── lcd_font.h # 字模表6x12 ASCII 16x16 汉字 │ └── Src/ │ ├── main.c # 初始化 测试画面 │ ├── st7735.c # 驱动层位操作时序、命令/数据、初始化表 │ ├── lcd_gui.c # 图元与文本绘制全部基于开窗接口 │ ├── lcd_font.c # 点阵数据必须加 const 放 Flash │ └── stm32f1xx_it.c ├── Drivers/ │ └── STM32F1xx_HAL_Driver/ └── STM32F103C8TX_FLASH.ld分层只有两层st7735.c只管“怎么把一个字节、一个颜色送到屏里”lcd_gui.c只管“画成什么形状”。这样后期换硬件 SPI 或者换屏型号只改驱动层图形层一行不用改。很多新手喜欢把画点、画线、初始化全塞进一个.c几百行之后想换个引脚都得翻半天改起来很不方便。lcd_font.c里的点阵一定要加const。F103C8T6 只有 20KB RAM一张 16×16 汉字点阵是 32 字节几百个常用字就是十几 KB放 RAM 直接崩而且启动时还得从 Flash 拷一遍。放 Flash 之后用const uint8_t配合链接脚本把它们留在.rodata里RAM 占用几乎为零。2.2 引脚分配与接线表1.8 寸 ST7735 模块常见的引出脚是 GND / VCC / SCL / SDA / RES / DC / CS / BLK 八个。这套驱动用的分配如下可以直接照着接线屏引脚ST7735 功能接到 MCU说明GND地GND必须和 MCU 共地VCC电源3.3V别接 5V模块上虽有 LDO 但便宜货未必有SCLSPI 时钟PB13软件模拟任意 GPIO 都行SDASPI 数据PB15单向只写不需要 MISORES硬件复位PB12上电必须拉低一次DC命令/数据选择PB14高数据低命令CS片选PB11单屏系统可省直接接地也行BLK背光3.3V 或 PWM串联限流电阻别直接灌有两点容易被忽略。一是BLK背光引脚很多模块内部只是把 LED 阳极引出来直接接 3.3V 电流会偏大串个 10Ω33Ω 电阻更稳妥二是CS如果你只挂一块屏把它固定拉低能省掉每次传输的片选翻转软件 SPI 的吞吐会明显好看一些。代价是同一组 GPIO 上不能再挂别的 SPI 从设备。2.3 CubeMX 里的关键配置用 CubeMX 建工程时这套驱动只需要配置 GPIO不需要开任何 SPI 外设。具体做法是把 PB11PB15 全部配成GPIO_Output模式选推挽输出Output Push Pull、无上下拉、速度选High初始电平按下面的约定设置。SCK 初始低电平ST7735 是 SPI 模式 0时钟空闲为低第一个上升沿采样。SDA 初始低电平无所谓但别悬空。DC 初始高电平随便第一次写命令前会被拉低。RES 初始高电平让屏处于非复位状态真正的复位在初始化函数里做。CS 初始高电平如果用了片选空闲时保持高。时钟树配到 72MHz外部 8MHz 晶振 ×9。软件 SPI 的位翻转速度直接受主频影响72MHz 下一条BSRR写操作大约几十纳秒加上循环开销实际字节能跑到 24MHz 等效速率对 128×160 这种分辨率够用了。如果手里是 8MHz 内部 RC 的板子刷屏会明显变慢这不一定是代码问题。3. 驱动层拆解一个像素从内存到屏幕的完整链路驱动层要解决的问题其实就三个怎么产生时钟、怎么区分命令和数据、按什么顺序配置寄存器。把这三件事拆开看ST7735 就没那么难对付了。3.1 软件 SPI 的字节收发与时钟边沿软件 SPI 就是手工做出“数据稳定 → 时钟上升沿 → 时钟下降沿”的循环。ST7735 在 SCK 上升沿采样 SDA所以数据必须在时钟拉低期间就准备好。/* st7735.h —— 换板子只改这一段 */ #define LCD_SCK_PORT GPIOB #define LCD_SCK_PIN GPIO_PIN_13 #define LCD_SDA_PORT GPIOB #define LCD_SDA_PIN GPIO_PIN_15 #define LCD_DC_PORT GPIOB #define LCD_DC_PIN GPIO_PIN_14 #define LCD_RST_PORT GPIOB #define LCD_RST_PIN GPIO_PIN_12 #define LCD_CS_PORT GPIOB #define LCD_CS_PIN GPIO_PIN_11 #define SCK_H() HAL_GPIO_WritePin(LCD_SCK_PORT, LCD_SCK_PIN, GPIO_PIN_SET) #define SCK_L() HAL_GPIO_WritePin(LCD_SCK_PORT, LCD_SCK_PIN, GPIO_PIN_RESET) #define SDA_H() HAL_GPIO_WritePin(LCD_SDA_PORT, LCD_SDA_PIN, GPIO_PIN_SET) #define SDA_L() HAL_GPIO_WritePin(LCD_SDA_PORT, LCD_SDA_PIN, GPIO_PIN_RESET) #define DC_H() HAL_GPIO_WritePin(LCD_DC_PORT, LCD_DC_PIN, GPIO_PIN_SET) #define DC_L() HAL_GPIO_WritePin(LCD_DC_PORT, LCD_DC_PIN, GPIO_PIN_RESET) #define CS_H() HAL_GPIO_WritePin(LCD_CS_PORT, LCD_CS_PIN, GPIO_PIN_SET) #define CS_L() HAL_GPIO_WritePin(LCD_CS_PORT, LCD_CS_PIN, GPIO_PIN_RESET)这一堆宏看着啰嗦但它换来的是移植成本极低换一块板子只改这十几行下面的代码一个字不用动。我见过把引脚号硬编码在函数体里的写法换板子时要全文搜索替换稍不留神就漏一处非常容易出错。下面的字节发送函数是可读版先用宏牺牲一点速度换清晰/* 发送一个字节高位在前模式 0SCK 空闲低上升沿采样 */ static void LCD_WR_BYTE(uint8_t dat) { for (uint8_t i 0; i 8; i) { if (dat 0x80) SDA_H(); else SDA_L(); dat 1; SCK_L(); /* 时钟拉低数据在此期间保持稳定 */ __NOP(); /* 等待数据线上的电平稳定下来 */ SCK_H(); /* 上升沿ST7735 在这一刻采样 */ __NOP(); } }dat 0x80判断最高位然后左移一位八次循环正好把一个字节从高到低送出去。ST7735 是高位先出MSB First顺序反了整屏就是乱码。两个__NOP()是给上升沿留建立时间杜邦线一长、屏的输入电容一大去掉这两条指令就可能在示波器上看到边沿塌陷表现就是偶发花屏。追求速度的时候我会换成直接操作BSRR的版本。BSRR一次写就能置位或复位指定引脚不需要读改写也不会影响同端口的其他引脚/* 快速版绕过 HAL 函数调用直接写寄存器 */ static inline void LCD_WR_BYTE_FAST(uint8_t dat) { for (uint8_t i 0; i 8; i) { if (dat 0x80) LCD_SDA_PORT-BSRR LCD_SDA_PIN; /* 置高 */ else LCD_SDA_PORT-BSRR (uint32_t)LCD_SDA_PIN 16; /* 置低 */ dat 1; LCD_SCK_PORT-BSRR (uint32_t)LCD_SCK_PIN 16; /* SCK 拉低 */ LCD_SCK_PORT-BSRR LCD_SCK_PIN; /* SCK 拉高采样 */ } }BSRR低 16 位置位、高 16 位复位这个技巧在 F1 系列上很好用。实测同样的循环快速版比 HAL 版快三倍以上全屏刷新的时间从 1.2 秒降到 0.4 秒左右。注意BSRR写 0 的位不受影响所以 SCK 和 SDA 挂在同一个 GPIOB 端口上也不冲突。3.2 命令与数据的分流DC 引脚的作用ST7735 只有一根数据线它靠 DC 引脚区分这次送过去的是命令还是数据DC 为低是命令寄存器地址DC 为高是参数或显存数据。所以每一次“写命令 若干参数”都是一个独立的小事务。/* 写命令DC0屏把这一个字节当地址 */ void ST7735_WriteCmd(uint8_t cmd) { DC_L(); CS_L(); LCD_WR_BYTE_FAST(cmd); CS_H(); } /* 写 8 位参数DC1屏把字节写进上一条命令对应的寄存器 */ void ST7735_WriteData8(uint8_t dat) { DC_H(); CS_L(); LCD_WR_BYTE_FAST(dat); CS_H(); } /* 写 16 位颜色ST7735 是高位在前先发高字节再发低字节 */ void ST7735_WriteData16(uint16_t dat) { DC_H(); CS_L(); LCD_WR_BYTE_FAST((uint8_t)(dat 8)); LCD_WR_BYTE_FAST((uint8_t)(dat 0xFF)); CS_H(); }三个函数的区别只在 DC 电平和字节数。RGB565 是 16 位色红 5 位、绿 6 位、蓝 5 位一个像素两个字节。这里顺序千万不能反反了之后红色会变成蓝色、白色还是白色非常容易误判成“屏的色序有问题”实际是自己把高低字节发颠倒了。3.3 ST7735 初始化表逐条说明初始化是整个驱动里最容易出问题的一段。ST7735 上电后处于睡眠状态必须按顺序退出睡眠、设置色彩格式、设置扫描方向、打开显示。少一步或者延时不够结果就是白屏。void ST7735_Init(void) { /* 1. 硬件复位拉低 10ms 再拉高之后必须等 120ms */ HAL_GPIO_WritePin(LCD_RST_PORT, LCD_RST_PIN, GPIO_PIN_RESET); HAL_Delay(10); HAL_GPIO_WritePin(LCD_RST_PORT, LCD_RST_PIN, GPIO_PIN_SET); HAL_Delay(120); /* 2. 软件复位 退出睡眠这两条后面都要等 */ ST7735_WriteCmd(0x01); /* SWRESET */ HAL_Delay(120); ST7735_WriteCmd(0x11); /* SLPOUT退出睡眠 */ HAL_Delay(120); /* 3. 帧率控制正常 / 空闲 / 局部刷新三组 */ ST7735_WriteCmd(0xB1); ST7735_WriteData8(0x01); ST7735_WriteData8(0x2C); ST7735_WriteData8(0x2D); ST7735_WriteCmd(0xB2); ST7735_WriteData8(0x01); ST7735_WriteData8(0x2C); ST7735_WriteData8(0x2D); ST7735_WriteCmd(0xB3); ST7735_WriteData8(0x01); ST7735_WriteData8(0x2C); ST7735_WriteData8(0x2D); ST7735_WriteCmd(0xB4); ST7735_WriteData8(0x07); /* 反显控制 */ /* 4. 电源控制不同批次屏的推荐值略有差异 */ ST7735_WriteCmd(0xC0); ST7735_WriteData8(0xA2); ST7735_WriteData8(0x02); ST7735_WriteData8(0x84); ST7735_WriteCmd(0xC1); ST7735_WriteData8(0xC5); ST7735_WriteCmd(0xC2); ST7735_WriteData8(0x0A); ST7735_WriteData8(0x00); ST7735_WriteCmd(0xC3); ST7735_WriteData8(0x8A); ST7735_WriteData8(0x2A); ST7735_WriteCmd(0xC4); ST7735_WriteData8(0x8A); ST7735_WriteData8(0xEE); ST7735_WriteCmd(0xC5); ST7735_WriteData8(0x0E); /* 5. 色彩格式0x05 16 位 RGB565 */ ST7735_WriteCmd(0x3A); ST7735_WriteData8(0x05); /* 6. 扫描方向0xC8 MX|MY|BGR竖屏 128x160 常用值 */ ST7735_WriteCmd(0x36); ST7735_WriteData8(0xC8); /* 7. 伽马校正两组各 16 字节照抄即可 */ ST7735_WriteCmd(0xE0); ST7735_WriteData8(0x02); ST7735_WriteData8(0x1C); ST7735_WriteData8(0x07); ST7735_WriteData8(0x12); ST7735_WriteData8(0x37); ST7735_WriteData8(0x32); ST7735_WriteData8(0x29); ST7735_WriteData8(0x2D); ST7735_WriteData8(0x29); ST7735_WriteData8(0x25); ST7735_WriteData8(0x2B); ST7735_WriteData8(0x39); ST7735_WriteData8(0x00); ST7735_WriteData8(0x01); ST7735_WriteData8(0x03); ST7735_WriteData8(0x10); ST7735_WriteCmd(0xE1); ST7735_WriteData8(0x03); ST7735_WriteData8(0x1D); ST7735_WriteData8(0x07); ST7735_WriteData8(0x06); ST7735_WriteData8(0x2E); ST7735_WriteData8(0x2C); ST7735_WriteData8(0x29); ST7735_WriteData8(0x2D); ST7735_WriteData8(0x2E); ST7735_WriteData8(0x2E); ST7735_WriteData8(0x37); ST7735_WriteData8(0x3F); ST7735_WriteData8(0x00); ST7735_WriteData8(0x00); ST7735_WriteData8(0x02); ST7735_WriteData8(0x10); /* 8. 打开显示最后一步 */ ST7735_WriteCmd(0x13); /* NORON普通显示模式 */ HAL_Delay(10); ST7735_WriteCmd(0x29); /* DISPON */ HAL_Delay(100); ST7735_FillScreen(COLOR_BLACK); /* 清一次 GRAM避免上电残留雪花 */ }几个参数值得单独说说。0x3A的0x05是 RGB565如果写成0x06就是 18 位色一次要发三个字节颜色会全错。0x36的0xC8拆成二进制是1100 1000bit7 是 MY、bit6 是 MX、bit5 是 MV、bit3 是 BGR。MX/MY 决定扫描方向MV 决定行列交换BGR 位置 1 表示按 BGR 顺序解释颜色。这四个位是花屏和颜色错乱的首要排查点。代码里的HAL_Delay(120)不是随便写的。SWRESET 之后手册要求至少 120msSLPOUT 之后也是 120msDISPON 之后 20ms。这类延时一旦不满足屏内部状态机还没准备好接收命令你发过去的初始化表就被丢掉了表现就是背光亮着但一片白。3.4 开窗与连续写从 SetWindow 到 RAMWRST7735 内部有一块 132×162 的 GRAM比可见的 128×160 大一圈这个差异就是后面偏移量的来源。往显存写数据之前必须先“开窗”告诉屏我要写的矩形区域之后的像素数据会按从左到右、从上到下的顺序自动填充坐标由屏自己递增。/* 屏幕可见尺寸与显存偏移不同批次模块的偏移不一样 */ #define LCD_W 128 #define LCD_H 160 #define LCD_XOFF 2 #define LCD_YOFF 1 /* 开窗设定矩形区域之后的写数据会落在这个区域里 */ void ST7735_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { ST7735_WriteCmd(0x2A); /* CASET列地址 */ ST7735_WriteData16(x0 LCD_XOFF); ST7735_WriteData16(x1 LCD_XOFF); ST7735_WriteCmd(0x2B); /* RASET行地址 */ ST7735_WriteData16(y0 LCD_YOFF); ST7735_WriteData16(y1 LCD_YOFF); ST7735_WriteCmd(0x2C); /* RAMWR后续都是显存数据 */ }x0/x1、y0/y1是包含端点的闭区间比如SetWindow(0, 0, 127, 159)就是整屏。开窗之后不需要再发0x2C连续调用ST7735_WriteData16就会自动往下一个像素走。但一旦换了区域必须重新发0x2A/0x2B再发0x2C。有了开窗填充矩形就是一句话的事把每个像素往窗口里写就行/* 填充矩形内部用开窗 连续写比逐个画点快得多 */ void ST7735_FillRect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color) { if (x LCD_W || y LCD_H || w 0 || h 0) return; if (x w LCD_W) w LCD_W - x; /* 自己裁剪别指望屏帮你 */ if (y h LCD_H) h LCD_H - y; ST7735_SetWindow(x, y, x w - 1, y h - 1); DC_H(); CS_L(); for (uint32_t i 0; i (uint32_t)w * h; i) { LCD_WR_BYTE_FAST((uint8_t)(color 8)); /* 每个像素两个字节 */ LCD_WR_BYTE_FAST((uint8_t)(color 0xFF)); } CS_H(); }注意CS_L()提到了循环外面。如果照搬ST7735_WriteData16每个像素都要拉一次片选四千多个像素就是四千多次多余的翻转全屏填充直接慢一倍。这种“把片选打开一次连续写一批数据”的做法在软件 SPI 下是性价比最高的一步优化。w * h要转成uint32_t再比较uint16_t相乘溢出是新手常犯的错128×160 正好是 20480勉强没溢出一旦填整屏的 16 位色在别的地方乘三就出问题了。4. 图形层落地图元、字模与局部刷新驱动层把像素送到屏上图形层决定送什么。这一层最考验的是取舍Flash 和 RAM 都紧张字库放多大、刷新怎么裁都需要提前规划。4.1 画点、画线、画框所有图元最后都塌缩成“给某个矩形窗口填什么颜色”。画点是最小的情况直接开一个 1×1 的窗口/* 画点越界直接丢弃避免把屏的 GRAM 写乱 */ void LCD_DrawPoint(uint16_t x, uint16_t y, uint16_t color) { if (x LCD_W || y LCD_H) return; ST7735_SetWindow(x, y, x, y); ST7735_WriteData16(color); }用软件 SPI 逐点画线开销主要在每次开窗都要发三条命令加六个参数字节。所以画线时我一般用 Bresenham 求点但每画一个点都开一次窗这在短线上可接受长线就明显卡。真要画大量线条应该改成“先算出一整行的起止点按行开窗一次填一行”。/* 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); if (x0 x1 y0 y1) break; int16_t e2 err 1; /* 乘 2 用移位省一次乘法 */ if (e2 -dy) { err - dy; x0 sx; } if (e2 dx) { err dx; y0 sy; } } }err是误差累积项e2是它的两倍两个if决定这一步往哪个方向走。全程整数没有float在 M3 内核上跑起来很利索。这里用了int16_t而不是uint16_t因为err会变成负数用无符号类型会得到一个巨大的正数循环直接跑飞这类 overflow 用调试器很难一眼看出来。矩形框就是四条线实心矩形直接用ST7735_FillRect。如果要做圆角、圆形用中点画圆法同样只有整数运算原理和 Bresenham 类似这里不铺开。4.2 ASCII 与汉字字模从哪来、怎么画6×12 的 ASCII 字模每个字符 12 个字节每字节的低 6 位表示一行。字模可以用常见的取模软件生成参数选“阴码、逐行式、逆向、十六进制”生成出来的数组直接贴进lcd_font.c。/* 6x12 ASCII 字模片段A 的 12 行点阵 */ const uint8_t ascii_6x12[~ - 1][12] { /* ... 前面省略 ... */ {0x00,0x00,0x10,0x38,0x6C,0xC6,0xC6,0xFE,0xC6,0xC6,0xC6,0x00}, /* A */ /* ... 后面省略 ... */ }; /* 显示一个 ASCII 字符fc 为前景色bc 为背景色 */ void LCD_ShowChar(uint16_t x, uint16_t y, char ch, uint16_t fc, uint16_t bc) { if (ch || ch ~) ch ; /* 非法字符兜底 */ const uint8_t *p ascii_6x12[ch - ]; for (uint8_t row 0; row 12; row) { uint8_t bits p[row]; for (uint8_t col 0; col 6; col) { /* 从高位往低位判断bit5 对应最左边一列 */ uint16_t c (bits (0x20 col)) ? fc : bc; LCD_DrawPoint(x col, y row, c); } } }0x20 col依次取出 bit5 到 bit0。取模软件里“逆向”这个选项就是决定这个方向的选错了字符会左右镜像一眼就能看出来。汉字用 16×16 点阵每个字 32 字节前 16 字节是上半部分后 16 字节是下半部分每个字节的 bit7 对应最左一列。为了不让 Flash 爆掉我一般只放界面里真正用到的字用一个结构体数组按字符串查找typedef struct { const char *text; /* 这个字本身UTF-8 编码占 3 字节 */ const uint8_t *dot; /* 32 字节点阵 */ } cn_glyph_t; /* 只收录界面上用得到的几十个字别整 GB2312 全量搬进来 */ static const uint8_t dot_ni[32] { /* ... 32 字节 ... */ }; static const uint8_t dot_hao[32] { /* ... 32 字节 ... */ }; static const cn_glyph_t cn_table[] { { 你, dot_ni }, { 好, dot_hao }, }; /* 查找并用前景/背景色画一个 16x16 汉字 */ void LCD_ShowChinese(uint16_t x, uint16_t y, const char *utf8, uint16_t fc, uint16_t bc) { for (uint32_t i 0; i sizeof(cn_table)/sizeof(cn_table[0]); i) { if (utf8[0] cn_table[i].text[0] utf8[1] cn_table[i].text[1] utf8[2] cn_table[i].text[2]) { const uint8_t *p cn_table[i].dot; for (uint8_t row 0; row 16; row) { for (uint8_t col 0; col 16; col) { uint8_t byte p[row * 2 (col 3)]; uint16_t c (byte (0x80 (col 7))) ? fc : bc; LCD_DrawPoint(x col, y row, c); } } return; } } /* 找不到就画一个方框占位别静默什么都不画 */ ST7735_DrawRect(x, y, 16, 16, fc); }按 UTF-8 前三个字节比对是因为一个汉字的 UTF-8 编码固定占三字节这样不用做编码转换。找不到字时画个方框占位比什么都不画好——至少你能立刻知道是字库缺字而不是屏坏了。我见过太多人对着空白屏折腾半天硬件最后发现是字库里根本没那个字。4.3 局部刷新别每次都全屏重画128×160 全屏 20480 个像素、40960 字节软件 SPI 下大约要 0.30.5 秒。如果你的界面里有个数字每秒变一次每次都全屏重画画面会明显闪CPU 也被占死。常见做法是记录一个“脏矩形”所有绘制操作只更新这块区域等到一帧结束再统一提交/* 脏矩形记录本帧实际被改动的范围 */ static uint16_t dirty_x0, dirty_y0, dirty_x1, dirty_y1; void LCD_MarkDirty(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { if (x0 dirty_x0) dirty_x0 x0; if (y0 dirty_y0) dirty_y0 y0; if (x1 dirty_x1) dirty_x1 x1; if (y1 dirty_y1) dirty_y1 y1; }实际使用时更简单的办法是先用背景色把要更新的文字区域擦掉再画新内容这一小块区域就是“局部刷新”。比如一个 6×12 的数字擦除加重画是 144 像素比全屏刷新快 140 倍肉眼完全看不出闪。如果你要做进度条、波形图这类连续更新的界面值得再往上走一步维护一块和屏等大的 RAM 显存128×160×2 40KB可惜 F103C8T6 只有 20KB RAM根本放不下。折中方案是只对变化的那一行开窗用ST7735_FillRect填新数据画面滚动效果靠调整行号实现不搬整个缓冲区。5. 避坑排查白屏、花屏、偏色的血泪记录这一章是我自己踩过的、也是被问得最多的几类问题。每一个都按“现象 → 原因 → 解决”记录遇到了直接对号入座。5.1 白屏与整屏雪花现象一背光亮着屏上一片纯白没有任何画面。原因基本锁定在初始化时序。ST7735 上电后内部状态机需要时间尤其是 SWRESET 和 SLPOUT 之后。很多人把延时写成HAL_Delay(10)甚至干脆省掉命令发过去时屏还没准备好整张初始化表被静默丢弃。另一种情况是 RES 引脚压根没接或者接了个上拉电阻没做真正的低电平复位屏停在上电默认状态。解决办法是按手册把时序补足RES 拉低至少 10ms拉高后等 120msSWRESET 后 120msSLPOUT 后 120msDISPON 后 20ms 以上。同时确认0x29DISPON确实发出去了没有这一条命令前面配得再对也是白屏。现象二屏上出现雪花噪点或者整屏随机彩色条纹偶尔复位后正常。原因通常是软件 SPI 的时序太快或者信号质量太差。软件 SPI 的“速度”取决于主频和指令条数快速版BSRR循环在 72MHz 下单次位翻转只有几十纳秒如果杜邦线超过 15cm、又没有共地、还没串限流电阻上升沿会明显变形屏采到的就是随机值。解决分三步先把快速版换回带__NOP()的慢速版验证确认能稳定显示后再逐步去掉延时接线尽量短SDA/SCK 各自有一根地线回流更好。如果一定要用长线把 SCK 的翻转速率压到 1MHz 以下稳定性会明显改善。5.2 图像偏移与颜色错乱现象三画面整体偏右下两个像素右边或底部多出一条彩色窄边。这是 ST7735 的 GRAM 尺寸造成的。它内部是 132×162而 1.8 寸模块只引出 128×160 的可见区多出来的部分不会有显示。如果开窗时直接写x0 0就会把显存最左边那两列不可见区域也写上画面整体左移或者右边缺一条。处理办法就是加偏移。红排线red tab的模块常见值是 XOFF2、YOFF1绿排线的模块可能是 0/0 或者 1/2。这没有统一答案只能试填一个整屏纯色看四边有没有不均匀的窄条或者画一个 1 像素的边框看是否完整可见。试出来之后把LCD_XOFF、LCD_YOFF固定到宏里。现象四颜色全反了红色显示成蓝色或者整屏像负片白变黑、黑变白。两个不同的原因别混在一起。红色和蓝色互换是0x36MADCTL里的 BGR 位没配好0xC8里的 bit3 是 BGR 位去掉它就是 RGB 顺序。整屏像负片是反显开关的问题0x20是 INVOFF0x21是 INVON有些批次屏出厂默认就是反显状态代码里发0x20才能正常。还有一种容易被误判的情况颜色发灰、对比度低。这多半是电源电压偏低或者背光限流电阻过大不是寄存器问题拿万用表量一下 VCC 是不是稳定在 3.3V。5.3 刷屏卡死与偶发异常现象五刷到一半进入 HardFault或者画面闪一下就停了。最常见的三个原因字模数组越界、栈溢出、uint16_t乘法溢出。字模越界在汉字显示里特别隐蔽UTF-8 中文占三字节如果拿char去索引并按单字节长度遍历指针会跑到数组外面去。栈溢出通常发生在主循环里放了大数组比如uint8_t buf[128*160]40KB 直接压在默认 1KB 的栈上链接阶段不报错跑起来才崩。解决办法字模全部加const放 Flash大缓冲区声明成static或全局别放函数内所有坐标运算先转uint32_t再乘。另外如果在刷屏过程中被中断打断太久个别屏会出现一行错位关键区域刷新时可以用__disable_irq()/__enable_irq()包一下代价是中断响应延迟几十毫秒自己权衡。现象六全屏刷新慢得离谱感觉像死机。这不是故障是软件 SPI 的物理上限。20480 个像素、每个两块字节、每个字节八次位操作算下来就是 32 万次 GPIO 操作72MHz 下几百毫秒很正常。判断方法很简单把ST7735_FillScreen换成ST7735_FillRect(0,0,16,16,...)如果小方块刷得飞快那就是数据量问题不是死机。真嫌慢就得考虑上硬件 SPI 加 DMA或者减少刷新面积。6. 二次开发把驱动搬进自己的工程并继续提速驱动跑通只是起点真正花时间的是把它接到自己的项目里让它跟其他外设共存并且在需要的时候把性能再压一压。6.1 移植清单换板子、换屏要动哪几个文件移植这件事只要分层做对了工作量其实很小。我一般的顺序是先改引脚、再改尺寸偏移、最后改字库。改动项需要修改的位置说明换 MCU 引脚st7735.h里的LCD_*_PORT/PIN宏唯一需要动的地方换屏尺寸LCD_W/LCD_H宏0.96 寸是 80×1601.44 寸是 128×128偏移不对LCD_XOFF/LCD_YOFF宏靠试色块确定没有通用值换扫描方向ST7735_Init里0x36的参数0xC8/0x08/0x68/0xA8 对应四个方向换字库lcd_font.c只放用得到的字控制 Flash 占用换 HAL 版本st7735.c里的HAL_GPIO_WritePin用 BSRR 版本的话完全无关如果是横屏使用除了改0x36的值还要把LCD_W和LCD_H对调并且LCD_XOFF/LCD_YOFF也要跟着换——横屏时行列被 MV 位交换过原来的偏移量作用在另一条轴上这一步漏掉就会偏。我第二次做横屏界面时就漏了调了快两个小时才发现是偏移量没换。6.2 从软件 SPI 平滑升级到硬件 SPI DMA软件 SPI 的好处是引脚自由、调试直观坏处是占 CPU。当真要做动画或者视频级别的刷新还是得换硬件 SPI。好消息是只要前面分层干净这步改造只发生在驱动层内部。做法是把字节发送函数换掉保持ST7735_WriteCmd/ST7735_WriteData16的接口不变/* 硬件 SPI 版接口不变只换底层实现 */ static void LCD_WR_BYTE_HW(uint8_t dat) { HAL_SPI_Transmit(hspi1, dat, 1, 100); /* 阻塞发送超时 100ms */ } /* 批量发颜色配合 DMA 时把这段换成 HAL_SPI_Transmit_DMA */ void ST7735_FillRect_DMA(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color) { static uint8_t line_buf[LCD_W * 2]; /* 一行的缓冲区放静态区 */ for (uint16_t i 0; i w; i) { line_buf[i*2] (uint8_t)(color 8); line_buf[i*2 1] (uint8_t)(color 0xFF); } ST7735_SetWindow(x, y, x w - 1, y h - 1); DC_H(); CS_L(); for (uint16_t row 0; row h; row) { HAL_SPI_Transmit(hspi1, line_buf, w * 2, 100); /* 逐行发缓冲区只占一行 */ } CS_H(); }这里用“一行缓冲区”而不是整屏缓冲区是因为 RAM 只有 20KB。整屏 16 位色需要 40KB一行只要 256 字节代价是多几次HAL_SPI_Transmit调用。真要上 DMA就把逐行发送改成HAL_SPI_Transmit_DMA加传输完成回调在回调里发下一行CPU 占用能降到几乎为零。注意硬件 SPI 的时钟极性必须配成 CPOL0、CPHA0模式 0预分频从 4 开始试72MHz / 4 18MHz 往往跑不稳一般 8 分频即 9MHz 比较稳妥。ST7735 手册标称最高 15MHz但模块的排线、电平转换芯片都会拖后腿还是要按实际能跑稳的速率来。6.3 怎么确认驱动真的没问题调试屏的时候最怕“看着能用”实际上有隐藏问题。我习惯用三个测试画面做验收每次改完驱动都跑一遍。第一个是四色块屏幕四等分分别填红、绿、蓝、白。这一张能同时验证偏移量、颜色顺序和开窗范围如果四个色块边界不齐或者有一边被切掉就是偏移没对。第二个是 1 像素边框加对角线。边框能看出有没有行列镜像对角线能看出画线算法和坐标方向是否一致。第三个是渐变条从 0x0000 到 0xFFFF 分 16 段显示。人眼对渐变的台阶很敏感如果中间出现色带断裂或者顺序跳跃说明 RGB565 的高低字节发反了。三个都过了再往上叠 UI 才靠谱。跳过这一步直接写业务代码后面一旦花屏你根本分不清是驱动问题还是界面逻辑问题白白多花几个小时。说个我自己的教训。早些年调一块新屏白屏了两天示波器、逻辑分析仪全上了最后发现是 RES 引脚焊盘虚焊万用表静态量是通的一动就断。从那以后我每次点新屏都强制先用万用表蜂鸣档把八根线逐根量一遍再跑四色块测试确认硬件没问题才动代码。软件再优雅也救不了虚焊这个顺序千万别反过来。希望帮到你。本文还有配套的精品资源点击获取