尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

0.96寸ST7735S TFT屏驱动实战:STM32 SPI初始化与代码全解析

0.96寸ST7735S TFT屏驱动实战:STM32 SPI初始化与代码全解析 如果你跟我一样第一批作品用 SSD1306 那个 0.96 寸单色 OLED 点阵屏吊了半年胃口那么第一次看到 0.96 寸 ST7735S TFT 点亮的时候会有一种突然从传呼机时代跨进智能手环时代的错觉。这篇文章把整个过程拆开讲从硬件选型、SPI 协议、初始化序列到完整的 STM32 驱动代码和排错手法全部走一遍。无论你是刚开始玩 STM32 的学生还是想给毕业设计加一个稍微好看点的界面都能照着抄。网上关于 ST7735S 的资料很零散尤其是 0.96 寸这个“非标”尺寸很多人按着 1.8 寸的教程改结果要么白屏、要么花屏、要么颜色整体错位。我把它踩过的坑都整理出来顺便给一份可以直接用的驱动框架。先说明一点ST7735S 和 ST7735R 的寄存器大部分通用但初始化序列里的 Gamma 参数、电源参数、扫描方向都不一样所以阅读屏背面的标识或者问卖家要初始化脚本是最省事的一条路。我这里会给你一套通用性较好的参数以及当它失效时怎么排查的完整思路。1. 选型背后的门道为什么这块ST7735S的TFT值得单独折腾很多人的第一块彩屏都是 1.8 寸 ST7735S因为资料最多。但 0.96 寸的版本价格更低、体积更小尤其适合做手腕设备、小型仪表、或者传感器数据展示面板。1.1 从单色 OLED 到 TFT 的真实体验差异SSD1306 那类 OLED 屏本质是 128×64 个像素点每个像素只能亮或不亮。显示字符、波形、简单图标完全够用但它要做图形界面就非常吃力没有颜色层次没法做圆角阴影中文字库点阵一做就是大几十 KB。换成 ST7735S TFT 后同样 0.96 寸的尺寸分辨率能做到 80×160也有的批次是 128×160 的物理面板裁切每像素 16 位 RGB565 彩色意味着你可以直接跑小型的 GUI 框架画曲线、做动画、放图标都不再有色彩上的妥协。功耗上 TFT 这类透射屏需要背光待机功耗比 OLED 高所以做电池供电时背光要单独控制。但大多数基于 STM32 的桌面设备或开发板项目这一点功耗完全在可接受范围内。1.2 ST7735S 与 ST7735R、ILI9341 的血缘关系ST7735S 是矽创电子针对小尺寸 TFT 面板推出的驱动控制器内部显存GRAM通常是 132×162物理面板裁切后映射到屏幕上。同一颗控制器可以驱动 0.96 寸、1.44 寸、1.8 寸不同的面板差别在于面板分辨率、行列起始偏移以及初始化参数。ST7735R 是更早期型号两者命令集 95% 相同但 Gamma 校正寄存器和部分电源时序寄存器位置不一样。如果你手头的屏幕丝印写的是 ST7735V 或者 ST7735建议先把它当 ST7735S 或者 ST7735R 分别试很多卖家标注都不严谨。ILI9341 则是 2.4 寸甚至更大屏幕的主流控制器分辨率 240×320驱动原理类似但初始化序列完全不同。你要是以后换大屏代码框架可以复用底层寄存器要重写。1.3 0.96寸“特殊规格”的两个关键坑先说第一个坑分辨率。市面上 0.96 寸 ST7735S 大多数是 80×160但有些面板是 128×160 或者 128×128。如果你按照买回来的屏写死成 80×160显示会截断反过来又会扫描到面板黑边区。第二个坑行列偏移。ST7735S 的 GRAM 是 132 列 × 162 行而屏幕可见区域可能只用了 80×160。如果初始化时没有正确设置起始偏移画面整体会偏上、偏左或出现一条不规则的黑边。解决方法是修改驱动里的 GRAM 偏移宏通常偏移量是 0~2 之间的一个小整数具体以实际屏幕为准。控制器常见面板尺寸分辨率颜色接口价格区间SSD13060.96寸OLED128x64单色I2C/SPI低ST7735S0.96寸TFT80x160RGB565SPI中低ST7735R1.8寸TFT128x160RGB565SPI中低ILI93412.4寸TFT240x320RGB565SPI/并行中这块屏适合什么样的项目我给你一个判断标准如果你的界面只需要展示温度和湿度几个大字加图标那 SSD1306 就够如果你要做一个带波形曲线的便携示波器、调试器或者想给小车加一个类似手机桌面的菜单那 0.96 寸 TFT 的性价比就体现出来了。2. ST7735S的驱动原理SPI时序、GRAM映射与初始化序列点亮一块 ST7735S 屏幕本质上就是通过 SPI 往内部的寄存器里写配置然后往 GRAM 里搬像素数据。这个过程不复杂但每一步都要清楚在干什么否则初始化序列配错了都不知道从哪查。2.1 4线SPI通信的读写流程ST7735S 支持 SPI 写数据不支持 SPI 读数据有些寄存器支持读但需要 MISO 引脚普通模块一般不接。它的 SPI 是 4 线制SCK时钟、SDAMOSI 数据、CS片选、DC数据/命令选择。RES复位严格说不算 SPI 的线但驱动初始化离不开它。写命令和写数据是两个流程DC 拉低时SPI 传输的是命令字节DC 拉高时SPI 传输的是数据。这就是为什么驱动里要有两个独立函数WriteCmd和WriteData。CS 的作用是把屏幕从总线上隔离出来如果你板子上只接了这一块 SPI 设备可以直接拉低但建议还是受控后面接多个 SPI 设备时会省事很多。2.2 初始化序列到底做了什么初始化序列看起来是一大段看不懂的魔数但拆开看就三类唤醒和复位类SWRESET0x01、SLPOUT0x11让控制器从睡眠状态回到正常模式。面板时序和电源类FRMCTR1/2/30xB1/B2/B3、PWCTR1~50xC0~0xC5设置行扫描频率、电荷泵电压、VCOM 电平。不同面板对电压和时序要求不一样这就是为什么 1.8 寸的初始化参数放在 0.96 寸上可能偏色。显示方向和格式类MADCTL0x36、COLMOD0x3A、CASET/RASET0x2A/0x2B分别设置扫描方向、颜色深度、显存行列映射范围。最容易出问题的其实就是最后一类。MADCTL的每一位控制一个方向的映射位含义设置值bit7 MY行方向翻转0从上到下1从下到上bit6 MX列方向翻转0从左到右1从右到左bit5 MV行列交换0正常1XY交换bit4 ML列刷新方向0/1 视面板而定bit3 BGRRGB还是BGR0RGB1BGRbit2-0保留固定值比如MADCTL 0xC0表示MY1, MX1, BGR0即行列都翻转。不同面板的排线方向不同所以“正确”值不是唯一的而是要让画面原点出现在你期望的角上。调这个参数最好配合一个只画四个角的测试程序扫一遍方向就出来了。2.3 地址窗口与GRAM偏移的计算逻辑ST7735S 画任何一个矩形区域都要先告诉它“我要往这个范围写像素”。这是 CASET 和 RASET 干的事。窗口范围越小传输的数据越少画一个点只传一个点的数据填充整屏就要传 80×160×2 字节。0.96 寸面板的 GRAM 偏移问题也出在这里。比如某块面板的物理裁切从 GRAM 的第 2 行开始那么你设置 RASET 时起点要加上 2。写成代码就是LCD_WrCmd(0x2B); LCD_WrData((ROW_START y0) 8); LCD_WrData((ROW_START y0) 0xFF); LCD_WrData((ROW_START y1) 8); LCD_WrData((ROW_START y1) 0xFF);这里ROW_START就是我前面说的偏移宏。先把它设成 0如果画出来的内容整体错位就逐个加 1、2、3 测试。这个调试过程看起来笨但在没有逻辑分析仪的情况下最有效。3. 硬件连接与引脚选择避开电平、电源、布线的隐性坑硬件连线是整个流程里最容易出错但最容易被忽略的部分。屏没点亮很多人第一时间怀疑 SPI 配置实际上八成是某个引脚没接对或者电平不对。3.1 最小连接方案与引脚对照表我以最常见的 STM32F103C8T6 蓝色开发板为例给出一组稳定的引脚配置ST7735S模块引脚功能STM32引脚说明VCC电源3.3V严禁接5VGND地GND共地必须SCLSPI时钟PA5SPI1_SCKSDASPI数据PA7SPI1_MOSIRES复位PA1软件控制复位DC数据/命令PA2软件控制CS片选PA3软件控制BLK背光PA4高电平点亮如果你用的是其他 MCU或者板子上 SPI1 已经被占用就直接把这个表里的引脚改成你自己的。注意一点硬件 SPI 的 SCK/MOSI 引脚是固定的比如 F103 的 SPI1_SCK 只有 PA5SPI2_SCK 只能从 PB13 等引脚上选。如果引脚分配冲突要么换 SPI 外设要么用软件 SPI后者在下一节会讲。3.2 3.3V电平匹配、背光电路与去耦电容ST7735S 的 IO 耐压一般在 3.3V 级别模块上如果没有电平转换芯片VCC 接 5V 有烧毁风险。STM32F103 的引脚输出也是 3.3V电平匹配没问题。如果你用 51 单片机或者 Arduino 的 5V 引脚最好加一个 1k 串联电阻分压或者用逻辑电平转换模块。背光引脚 BLK 务必单独控制。如果直接接 3.3V屏幕会一直全亮做成软件控制后可以 PWM 调亮度也可以配合传感器息屏。有些模块的 BLK 是低电平点亮这个要看你买的那块板子的背面丝印或者直接量一下引脚对地电压。电源去耦这点很容易被人忽略。TFT 在刷新时需要瞬间电流特别是整屏填充时如果供电线太长或者没有滤波电容画面边缘会出现水波纹或者亮度不均的现象。我会在 VCC 和 GND 之间并一个 10uF 电解电容和一个 100nF 陶瓷电容尽量靠近模块引脚。3.3 软件SPI与硬件SPI的取舍这是一个实战中必须面对的问题。硬件 SPI 使用外设自动时钟F103 的 SPI1 最高可以跑到 36MHzST7735S 的数据手册显示 SPI 时钟极限一般在 15.5MHz 左右所以实际建议设 10MHz 上下留足裕量。硬件 SPI 配合 DMA 传输可以把 CPU 从单纯的像素搬运中解放出来做动画时帧率提升非常明显。软件 SPI 则是用 GPIO 翻转模拟时序好处是引脚任意、代码可读性高新手能直观理解每一位怎么被时钟采样。坏处是速度慢72MHz 主频下无优化的软件 SPI 通常只能跑到 1~2Mbps全屏填充可能要 200ms 以上。我建议是先把软件 SPI 跑通确认屏幕和初始化没问题再切到硬件 SPI 提升性能。这样排查问题时变量少很多。4. 完整代码实战从SPI底层到绘图API一次跑通说明一下我会以 STM32 HAL 库 软件 SPI 作为示例因为不管哪个型号的 STM32这套代码都套得上。代码核心是st7735s.c和st7735s.h主函数里调用它完成画线、填充、显示字符。4.1 工程配置CubeMX里只需做对的几件事用 STM32CubeMX 建工程时不需要配置 SPI 外设因为软件 SPI 只是普通 GPIO。你只需要做这几步将 SCL、SDA、RES、DC、CS、BLK 对应的引脚设为 GPIO 输出初始电平全部拉低。时钟配置为 72MHz。串口可选方便调试打印。如果你决定用硬件 SPI那就开启 SPI1Mode 选 Transmit Only MasterPrescaler 设到使得 SCK 约为 9MHzCPOLLow、CPHA1Edge模式 0MSB First。配置完后生成 Keil 工程即可。4.2 驱动头文件宏定义决定移植成本驱动里我尽量用宏把引脚和屏幕参数集中管理。你换一块板子只需要改这段#ifndef __ST7735S_H #define __ST7735S_H #include main.h // 软件SPI引脚宏定义按自己板子修改 #define LCD_SCK_PIN GPIO_PIN_5 #define LCD_SCK_PORT GPIOA #define LCD_SDA_PIN GPIO_PIN_7 #define LCD_SDA_PORT GPIOA #define LCD_RES_PIN GPIO_PIN_1 #define LCD_RES_PORT GPIOA #define LCD_DC_PIN GPIO_PIN_2 #define LCD_DC_PORT GPIOA #define LCD_CS_PIN GPIO_PIN_3 #define LCD_CS_PORT GPIOA #define LCD_BLK_PIN GPIO_PIN_4 #define LCD_BLK_PORT GPIOA // 屏幕参数0.96寸常见规格为80x160 #define LCD_WIDTH 80 #define LCD_HEIGHT 160 // GRAM行列偏移某批次面板可能需要调整 #define LCD_GRAM_ROW_START 0 #define LCD_GRAM_COL_START 0 // RGB565常用颜色 #define COLOR_BLACK 0x0000 #define COLOR_WHITE 0xFFFF #define COLOR_RED 0xF800 #define COLOR_GREEN 0x07E0 #define COLOR_BLUE 0x001F #define COLOR_YELLOW 0xFFE0 #define COLOR_CYAN 0x07FF #define COLOR_MAGENTA 0xF81F #define COLOR_ORANGE 0xFD20 typedef uint16_t lcd_color_t; void ST7735S_Init(void); void ST7735S_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1); void ST7735S_DrawPixel(uint16_t x, uint16_t y, lcd_color_t color); void ST7735S_FillRect(uint16_t x0, uint16_t y0, uint16_t w, uint16_t h, lcd_color_t color); void ST7735S_Clear(lcd_color_t color); void ST7735S_DrawHLine(uint16_t x, uint16_t y, uint16_t len, lcd_color_t color); void ST7735S_DrawVLine(uint16_t x, uint16_t y, uint16_t len, lcd_color_t color); void ST7735S_DrawCircle(uint16_t cx, uint16_t cy, uint16_t r, lcd_color_t color); void ST7735S_DrawString(uint16_t x, uint16_t y, const char *str, lcd_color_t color, uint16_t bg, uint8_t scale); #endif4.3 驱动实现SPI底层、初始化序列、绘图API下面是st7735s.c的核心实现。我实现了软件 SPI重点关注初始化序列和地址窗口设置。#include st7735s.h #include string.h // 软件模拟SPI发送一个字节MSB先行 static void SPI_WriteByte(uint8_t dat) { for (int i 7; i 0; i--) { HAL_GPIO_WritePin(LCD_SCK_PORT, LCD_SCK_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(LCD_SDA_PORT, LCD_SDA_PIN, (dat (1 i)) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(LCD_SCK_PORT, LCD_SCK_PIN, GPIO_PIN_SET); } } // 写命令DC0CS0 static void LCD_WriteCmd(uint8_t cmd) { HAL_GPIO_WritePin(LCD_CS_PORT, LCD_CS_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(LCD_DC_PORT, LCD_DC_PIN, GPIO_PIN_RESET); SPI_WriteByte(cmd); HAL_GPIO_WritePin(LCD_CS_PORT, LCD_CS_PIN, GPIO_PIN_SET); } // 写数据DC1CS0 static void LCD_WriteData(uint8_t dat) { HAL_GPIO_WritePin(LCD_CS_PORT, LCD_CS_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(LCD_DC_PORT, LCD_DC_PIN, GPIO_PIN_SET); SPI_WriteByte(dat); HAL_GPIO_WritePin(LCD_CS_PORT, LCD_CS_PIN, GPIO_PIN_SET); } // 初始化ST7735S void ST7735S_Init(void) { HAL_GPIO_WritePin(LCD_BLK_PORT, LCD_BLK_PIN, GPIO_PIN_SET); // 硬件复位 HAL_GPIO_WritePin(LCD_RES_PORT, LCD_RES_PIN, GPIO_PIN_RESET); HAL_Delay(20); HAL_GPIO_WritePin(LCD_RES_PORT, LCD_RES_PIN, GPIO_PIN_SET); HAL_Delay(150); LCD_WriteCmd(0x01); // SWRESET HAL_Delay(150); LCD_WriteCmd(0x11); // SLPOUT HAL_Delay(200); // 帧率设置 LCD_WriteCmd(0xB1); LCD_WriteData(0x05); LCD_WriteData(0x3A); LCD_WriteData(0x3A); LCD_WriteCmd(0xB2); LCD_WriteData(0x05); LCD_WriteData(0x3A); LCD_WriteData(0x3A); LCD_WriteCmd(0xB3); LCD_WriteData(0x05); LCD_WriteData(0x3A); LCD_WriteData(0x3A); LCD_WriteData(0x05); LCD_WriteData(0x3A); LCD_WriteData(0x3A); LCD_WriteCmd(0xB4); // INVCTR LCD_WriteData(0x03); // 电源控制 LCD_WriteCmd(0xC0); LCD_WriteData(0x62); LCD_WriteData(0x02); LCD_WriteData(0x04); LCD_WriteCmd(0xC1); LCD_WriteData(0xC0); LCD_WriteCmd(0xC2); LCD_WriteData(0x0D); LCD_WriteData(0x00); LCD_WriteCmd(0xC3); LCD_WriteData(0x8D); LCD_WriteData(0x6A); LCD_WriteCmd(0xC4); LCD_WriteData(0x8D); LCD_WriteData(0xEE); LCD_WriteCmd(0xC5); LCD_WriteData(0x0E); // 扫描方向与颜色格式按屏实际调整 LCD_WriteCmd(0x36); // MADCTL LCD_WriteData(0xC0); LCD_WriteCmd(0x3A); // COLMODRGB565 LCD_WriteData(0x05); // 显存行列窗口先设为全区域 LCD_WriteCmd(0x2A); // CASET LCD_WriteData(0x00); LCD_WriteData(0x00); LCD_WriteData(0x00); LCD_WriteData(0x4F); LCD_WriteCmd(0x2B); // RASET LCD_WriteData(0x00); LCD_WriteData(0x00); LCD_WriteData(0x00); LCD_WriteData(0x9F); // Gamma校正 LCD_WriteCmd(0xE0); LCD_WriteData(0x02); LCD_WriteData(0x1C); LCD_WriteData(0x07); LCD_WriteData(0x12); LCD_WriteData(0x37); LCD_WriteData(0x32); LCD_WriteData(0x29); LCD_WriteData(0x2D); LCD_WriteData(0x29); LCD_WriteData(0x25); LCD_WriteData(0x2B); LCD_WriteData(0x39); LCD_WriteData(0x00); LCD_WriteData(0x01); LCD_WriteData(0x03); LCD_WriteData(0x10); LCD_WriteCmd(0xE1); LCD_WriteData(0x03); LCD_WriteData(0x1D); LCD_WriteData(0x07); LCD_WriteData(0x06); LCD_WriteData(0x2E); LCD_WriteData(0x2C); LCD_WriteData(0x29); LCD_WriteData(0x2D); LCD_WriteData(0x2E); LCD_WriteData(0x2E); LCD_WriteData(0x37); LCD_WriteData(0x3F); LCD_WriteData(0x00); LCD_WriteData(0x00); LCD_WriteData(0x02); LCD_WriteData(0x10); LCD_WriteCmd(0x13); // NORON HAL_Delay(10); LCD_WriteCmd(0x29); // DISPON HAL_Delay(100); ST7735S_Clear(COLOR_BLACK); }这里要注意我给的初始化序列是常见 ST7735S 方案的集合对绝大多数模组能正常点亮。如果你的屏幕出现偏色严重或者显示残缺优先怀疑 Gamma 和电源参数不要盲目全抄。卖家的初始化脚本里通常会有针对型号的差异对比后你会发现差别往往就在0xC0~0xC5和0xE0/0xE1这两段。4.4 绘图API地址窗口是性能分水岭画点函数DrawPixel是最底层的操作但实际做界面时绝不要逐点调用它。为什么因为每调用一次就要完整发送 CASET/RASET/RAMWR 三组命令SPI 传输命令字节虽然开销不大但全屏 12800 个点逐点画命令开销是像素数据的数倍速度完全不能看。正确的用法是设置地址窗口后连续灌数据。以填充矩形为例void ST7735S_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { LCD_WriteCmd(0x2A); LCD_WriteData((x0 LCD_GRAM_COL_START) 8); LCD_WriteData((x0 LCD_GRAM_COL_START) 0xFF); LCD_WriteData((x1 LCD_GRAM_COL_START) 8); LCD_WriteData((x1 LCD_GRAM_COL_START) 0xFF); LCD_WriteCmd(0x2B); LCD_WriteData((y0 LCD_GRAM_ROW_START) 8); LCD_WriteData((y0 LCD_GRAM_ROW_START) 0xFF); LCD_WriteData((y1 LCD_GRAM_ROW_START) 8); LCD_WriteData((y1 LCD_GRAM_ROW_START) 0xFF); LCD_WriteCmd(0x2C); // RAMWR启动写显存 } void ST7735S_FillRect(uint16_t x0, uint16_t y0, uint16_t w, uint16_t h, lcd_color_t color) { if (x0 LCD_WIDTH || y0 LCD_HEIGHT) return; if (x0 w LCD_WIDTH) w LCD_WIDTH - x0; if (y0 h LCD_HEIGHT) h LCD_HEIGHT - y0; ST7735S_SetWindow(x0, y0, x0 w - 1, y0 h - 1); uint32_t pixels (uint32_t)w * h; for (uint32_t i 0; i pixels; i) { LCD_WriteData((color 8) 0xFF); LCD_WriteData(color 0xFF); } }RGB565 各一位代表一个颜色分量color 8是高字节先发送低字节后发送。这个顺序千万别搞反否则颜色块会变成色块渐变的花纹。画线的原理和填充类似直线可以拆成一个一个像素但对长直线更高效的做法是一整段设置地址窗口。我代码里的DrawHLine/DrawVLine就是这种实现void ST7735S_DrawHLine(uint16_t x, uint16_t y, uint16_t len, lcd_color_t color) { if (y LCD_HEIGHT || x LCD_WIDTH) return; if (x len LCD_WIDTH) len LCD_WIDTH - x; ST7735S_SetWindow(x, y, x len - 1, y); for (uint16_t i 0; i len; i) { LCD_WriteData((color 8) 0xFF); LCD_WriteData(color 0xFF); } }4.5 字符显示字库的取舍与实现方式TFT 上显示字符通常有两种方案小型 ASCII 点阵字库和全字库。0.96 寸屏做中文界面的时候中文字库在 F103C8T6 上几乎装不下因为 64KB Flash 里塞一套 GB2312 字库非常紧张。比较常见的做法是只提取你要用到的几十个汉字做成取模数组。ASCII 字符用 5×7 点阵每个字符 5 字节96 个可见字符约 480 字节非常省资源。你可以直接从 Adafruit GFX 库的glcdfont.c里把 5×7 字库挖出来格式是 5 字节一行每个字节的低 5 位对应一列。下面是DrawChar的简化实现思路static const unsigned char font5x7[][5] { // 这是示例数据实际需要完整的96个字符 {0x00, 0x00, 0x00, 0x00, 0x00}, // 空格 // 其余字符... }; void ST7735S_DrawChar(uint16_t x, uint16_t y, char ch, lcd_color_t color, uint16_t bg, uint8_t scale) { if ((uint8_t)ch 0x20 || (uint8_t)ch 0x7F) return; const unsigned char *glyph font5x7[ch - 0x20]; for (int row 0; row 7; row) { for (int col 0; col 5; col) { uint16_t px x col * scale; uint16_t py y row * scale; if ((glyph[col] row) 1) ST7735S_FillRect(px, py, scale, scale, color); else if (bg ! COLOR_BLACK) ST7735S_FillRect(px, py, scale, scale, bg); } } }注意我的字库数据是按列优先还是行优先决定glyph[col] row的读取方式。如果你是直接从文本取模软件里导出的通常更常见的是每行 5 字节、行优先具体看取模软件设置。调试时先显示一个 H验证字库方向对不对不对就转置数组。4.6 主函数示例直接烧录就能看到效果主函数里的演示逻辑是清屏、显示三原色彩条、画一个移动的小方块。跑通这一套屏幕就说明没有问题了。#include main.h #include st7735s.h void SystemClock_Config(void); static void GPIO_Init(void); int main(void) { HAL_Init(); SystemClock_Config(); GPIO_Init(); ST7735S_Init(); // 三原色彩条 ST7735S_FillRect(0, 0, 80, 40, COLOR_RED); ST7735S_FillRect(0, 40, 80, 40, COLOR_GREEN); ST7735S_FillRect(0, 80, 80, 40, COLOR_BLUE); ST7735S_FillRect(0, 120, 80, 40, COLOR_WHITE); HAL_Delay(1000); // 画边框再做一个像素级移动方块 ST7735S_Clear(COLOR_BLACK); ST7735S_DrawHLine(0, 0, 80, COLOR_CYAN); ST7735S_DrawHLine(0, 159, 80, COLOR_CYAN); ST7735S_DrawVLine(0, 0, 160, COLOR_CYAN); ST7735S_DrawVLine(79, 0, 160, COLOR_CYAN); for (uint16_t x 10; x 60; x 2) { ST7735S_FillRect(x, 70, 8, 8, COLOR_YELLOW); HAL_Delay(30); if (x 10) ST7735S_FillRect(x - 2, 70, 8, 8, COLOR_BLACK); } while (1) { HAL_Delay(100); } }4.7 引入硬件SPI和DMA提速如果你对现在的刷新速度不满意可以把软件 SPI 换成硬件 SPI。CubeMX 里打开 SPI1然后在驱动里替换SPI_WriteByteextern SPI_HandleTypeDef hspi1; static void SPI_WriteByte(uint8_t dat) { HAL_SPI_Transmit(hspi1, dat, 1, HAL_MAX_DELAY); }这样改完之后全屏填充速度会有数量级提升。注意 HAL 的HAL_SPI_Transmit是阻塞式每字节都要等待仍然不够快。真正的提速要配合 DMAvoid ST7735S_FillRectDMA(uint16_t x0, uint16_t y0, uint16_t w, uint16_t h, uint16_t *buf, uint32_t len) { ST7735S_SetWindow(x0, y0, x0 w - 1, y0 h - 1); HAL_SPI_Transmit_DMA(hspi1, (uint8_t *)buf, len * 2); }使用 DMA 时缓冲区必须是全局数组不能是局部变量因为函数退出后局部变量就失效了。另外DMA 传输期间 STM32 不能进入低功耗模式这个细节在低功耗项目里要注意。5. 亮屏排错实录白屏、花屏、偏色的完整排查链路点亮这块屏的过程中几乎每个人都会遇到一种或者几种症状。我把最常见的几类问题整理成排查链路遇到问题从前往后检查比盲改参数高效得多。5.1 白屏先查电源和复位再查串口波形白屏意味着背光亮了但是控制器没有输出内容。这种情况要先确认背光是否正常然后看 RES 引脚有没有按初始化顺序复位。我用一个简单方法判断是单片机没把控制器唤醒还是 SPI 数据没传到在初始化代码里故意把某个命令参数改错如果画面从白屏变成花屏或者颜色块错乱说明 SPI 链路是通的如果始终纯净地白屏问题往往在电源或复位。接下来查复位时序。ST7735S 要求复位信号拉低至少 10us然后释放等待 120ms 以上再发命令。如果 MCU 的 RES 引脚悬空或复位后延时太短控制器可能还在启动中命令直接被吞了。最后用示波器或者逻辑分析仪抓 SDA 和 CS 的波形。把初始化代码跑一遍看看 CS 有没有正常拉低、SDA 上到底有没有字节流。如果波形根本没有检查 GPIO 配置很多人缺少GPIO_MODE_OUTPUT_PP或者时钟没使能导致引脚一直是高阻态。5.2 花屏或斜纹MADCTL方向和地址窗口的问题画面能亮但是出现斜向的条纹、颜色一团乱这基本是 CASET/RASET 设置和物理面板不匹配。典型情况是初始化里写死了整个 132×162 的 GRAM但物理面板只用了 80×160 的某一区域SPI 流入的数据填到了 GRAM 的空白区画面就像被切开重新拼接一样。处理方法是把 CASET/RASET 的范围先写到最小可见区域。比如 80×160 的屏先设LCD_WriteCmd(0x2A); LCD_WriteData(0x00); LCD_WriteData(0x00); LCD_WriteData(0x00); LCD_WriteData(0x4F); // 0~79 LCD_WriteCmd(0x2B); LCD_WriteData(0x00); LCD_WriteData(0x00); LCD_WriteData(0x00); LCD_WriteData(0x9F); // 0~159如果这样做了还是花屏说明面板的 GRAM 偏移不为 0。把上面0x00改成0x01或者0x02试试再用四个角分别画不同颜色观察颜色坐标是否对得上。花屏还有一个常见原因是 SPI 极性配置错误。ST7735S 通常工作在 SPI Mode 0CPOL0CPHA1Edge但个别模组需要 Mode 2CPOL1CPHA2Edge。改一下 CubeMX 里的 SPI 设置试一遍只需要编译下载一次比抓波形快。5.3 偏色或颜色反相RGB-BGR序与COLMOD位宽颜色整体是反的比如红色变蓝色问题几乎肯定出在MADCTL的 bit3 BGR 位。置 1 时是 BGR 颜色序置 0 时是 RGB 颜色序。每块模组因为 FPC 排线定义差异正确值不一样。你只要把0xC0改成0xC8或者反过来就能看到颜色是否归位。颜色发灰、层次感不强则要检查COLMOD设置。ST7735S 支持 12bit/16bit/18bit 颜色0x05表示 16 位色。如果你的代码用 RGB565 填充控制器却设置成了 12bit 模式颜色就会偏得离谱。另外注意12bit 模式下每个像素 1.5 字节数据拼接方式和 16bit 完全不同所以COLMOD必须和绘制代码一致。还有一种少见的情况初始化序列里执行了INVON0x21而不是INVOFF0x20导致颜色反转。如果你看到的是类似负片的效果查初始化里有没有多出一条反色命令。5.4 画面残影和随机闪烁时钟裕量与DMA节奏刷新时有残影或者偶尔闪烁一种原因是 SPI 时钟太高。我实测过某块 0.96 寸屏在 18MHz 以上时出现随机竖条纹降到 9MHz 完全正常。ST7735S 的理论极限虽然不低但廉价模组的 PCB 走线和排线质量参差不齐保守一点没有坏处。闪烁的另一个原因是 DMA 和 MCU 更新显存内容不同步。如果你在 DMA 传输还没完成时就修改了缓冲区数据屏幕上会出现撕裂感。解决办法是加传输完成回调在回调里置一个标志主循环只在这个标志置位后才更新下一帧内容。6. 性能优化与进阶方向从“能显示”到“显示得好”点亮只是第一步。实际项目中我们关心的是帧率、CPU 占用、代码可维护性。这块小屏虽然分辨率低但同样的优化思路换到大屏一样适用。6.1 显存缓冲的取舍F103的20KB RAM塞得下吗80×160×2 字节等于 25600 字节F103C8T6 只有 20KB SRAM整屏帧缓冲放不下。所以不能像很多大型 GUI 那样把整个屏幕数据都缓存到内存里再一次性刷到 LCD。常用做法是行缓冲。比如定义一个uint16_t linebuf[80]每次只准备一行的像素数据通过 DMA 发送后再准备下一行。这样内存只消耗 160 字节还可以在数据准备阶段做软件抗锯齿、渐变等计算显示效果比逐点写丰富很多。如果确实需要整屏缓冲区芯片必须升级到 F103RE、F407 或者带外部 SRAM 的型号。也可以选择 Y 方向分块刷新像 LVGL 的 partial refresh 机制一样每次只刷新一个 1/4 屏的缓冲区域。6.2 局部刷新与脏矩形做仪表盘的核心思路做波形显示或者数值刷新时整屏重绘是最容易实现但效率最低的方案。以 80×160 为例每秒刷新 30 帧每帧 25600 字节SPI 按 9MHz 计算是 25600×8/9M ≈ 22.7ms占一帧时间的 68%。再加上数据准备时间CPU 几乎全花在刷新上。更合理的做法是记录上一次绘制的区域只对变化区域调用FillRect或者窗口裁剪。做温度显示时温度值每次只变化几个像素宽没必要刷整屏。做曲线图时新旧曲线之间只有最右一格需要更新把画布向左平移的操作改成移动窗口配合局部重绘帧率能翻几倍。6.3 接入GUI框架的思路u8g2与LVGL如果你不想自己维护绘图函数可以直接把屏幕接到成熟 GUI 库上。u8g2 对 80×160 这类小屏支持很好底层接口只需要提供写字节回调。它支持多种字库中文支持也不错适合做静态界面和简单菜单。LVGL 做 0.96 寸屏幕有点重但如果你有现成的 LVGL 项目想移植到小屏做验证只需实现 flush 回调void lcd_flush_cb(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p) { ST7735S_SetWindow(area-x1, area-y1, area-x2, area-y2); // 将color_p指向的缓冲区按RGB565字节序送入SPI lv_disp_flush_ready(drv); }LVGL 默认的lv_color_t在 RGB565 配置下就是 16 位直接和 ST7735S 的 16 位模式匹配。需要留意大小端字节序STM32 是小端SPI 发送时要把每个像素拆成高低字节顺序不要搞反。6.4 我实测中保存下来的一套稳定模板最后分享一下我目前最常用的稳定模板。主控用 STM32F103C8T6SPI1 硬件 SPI时钟 9MHzMADCTL 0xC0COLSTART 偏移 0ROWSTART 偏移 0背光用 PWM 控制。这套配置在某型号 0.96 寸 ST7735S 模块上连续运行了两个多月没有出现过掉帧、花屏或者偏色。如果你换了一家门买的屏幕第一件事就是跑 Demo 程序分别打印0xC0、0xA0、0xC8三个 MADCTL 值把颜色和方向都校准后再开始做界面。校准屏幕比写界面重要得多因为所有坐标计算都建立在原点位置准确的基础上。方向不对后续的触摸坐标映射、图片显示、字库排版全都会跟着错到时候排查起来更痛苦。另外ST7735S_Init里我只调用了一次ST7735S_Clear(COLOR_BLACK)但如果在清屏后立刻快速切换页面屏上可能出现残留的上一帧。这种情况下可以在每次切页前用ST7735S_SetWindow(0,0,LCD_WIDTH-1,LCD_HEIGHT-1)后连续灌一到两帧纯色数据相当于强制清空 GRAM。虽然会多花几毫秒但视觉效果更干净。
返回列表