STM32驱动0.96寸ST7735S TFT屏是很多做小体积交互界面的朋友绕不开的一个坑。这块屏幕便宜、接口少、点亮后显示效果也不错一个SPI加几根控制线就能驱动但厂家资料混乱、网上初始化序列五花八门第一次上手直接白屏的概率非常高。这篇文章把完整的驱动代码和接线方式拆开讲清楚从SPI原理到像素写入一点点过适合刚入门的小白也适合想快速移植到自己项目里的朋友照着做基本一遍就能点亮。1. 项目整体思路与方案选型1.1 为什么选0.96寸ST7735S0.96寸TFT屏在市面上带ST7735S驱动IC的模块非常多尺寸小、成本低分辨率常见为80x160或128x160颜色格式支持RGB565显示效果比同尺寸OLED更细腻还能显示彩色图标和照片素材。相比I2C接口的OLEDTFT屏用SPI接口刷新速度快得多做简单的菜单、仪表盘、游戏画面都够用。选择这块屏还有一个现实原因模块资料虽然乱但驱动IC是标准的ST7735S命令集和大部分ST7735系列兼容。这意味着只要你把一套初始化序列调通以后换1.8寸、1.44寸的屏只需要改分辨率参数和MADCTL方向位代码框架完全不用动。我最初是在一个温湿度计项目里用OLED后来需要显示曲线和色块OLED明显吃力换到ST7735S后整个界面活泼了一个量级。1.2 驱动方案的层级划分网上很多教程直接把厂家给的初始化序列丢给你然后说“复制就能用”但出了问题你根本不知道是哪一步错了。我的习惯是把驱动拆成三层底层硬件抽象层管SPI发送、GPIO电平、延时函数中间层ST7735S寄存器操作包含初始化序列、设置显示窗口、写像素点上层绘图API画点、画线、填充矩形、显示字符、显示图片。这种分层的最大好处是便于排查问题。白屏了先确认底层SPI有没有波形颜色不对再怀疑初始化序列和RGB565字节顺序如果只是显示内容错位那就是窗口设置参数写歪了。下面所有代码都按这个结构来写你可以直接抄也可以按自己的工程习惯调整模块划分。1.3 硬件清单与接线我用的主控是STM32F103C8T6也就是最常见的“蓝板”核心板Keil MDK环境驱动库用HAL库。硬件清单如下器件型号/规格备注主控板STM32F103C8T6核心板也可用其他STM32型号TFT屏0.96寸 ST7735S模块注意确认分辨率ST-Link V2下载调试器或用J-Link面包板/杜邦线若干接线用0.96寸模块通常引出8个引脚VCC、GND、CS、RESET、DC/RS、SDI(MOSI)、SCK、LED。不同商家标注略有差异但本质一样。我的接线方式如下TFT引脚STM32引脚说明VCC3.3V部分模块带稳压可接5V建议先看模块丝印GNDGND共地CSPB12片选低有效RESETPB13复位低有效DCPB14数据/命令选择低电平命令高电平数据SDIPA7SPI1_MOSISCKPA5SPI1_SCKLED3.3V或PA6背光直接接3.3V最亮用PA6可以PWM调光注意如果你的模块没有CS引脚说明它已经内部接地那就省掉这根线。另外背光LED引脚不要接到5V长时间工作会发热严重正常3.3V供电就能获得不错的亮度。2. 核心原理ST7735S到底怎么工作2.1 SPI接口与脚位ST7735S的SPI接口是标准的四线模式SCK、SDI、CS、DC加上RESET和背光一共六根控制线。主机通过SPI总线把命令和像素数据发给屏幕控制器控制器再驱动内部TFT面板。这里有个容易绕晕的点屏幕上的“SDI”是从机视角的串行数据输入引脚所以接到主控的MOSI上。SCK是时钟CS是片选DC决定了这一笔传输是命令还是数据。ST7735S的命令字节和数据字节都是8位但像素颜色数据是16位RGB565发送时要分两个字节每个字节都是高位在前。SPI模式方面ST7735S手册要求CPOL0、CPHA0也就是SPI模式0。如果你在CubeMX里把SPI参数配成了模式3初始化序列能跑通但显示颜色会花这是新手最容易踩的坑。后面我会在问题排查里再提一次。2.2 初始化序列在做什么ST7735S上电后并不会自动进入正常显示状态需要主机发送一串配置命令。初始化序列的核心工作可以分成四块软复位、进入睡眠退出、设置显示参数、开启显示。软复位命令0x01让控制器回到出厂状态命令0x11退出睡眠模式内部振荡器开始工作一系列电源控制命令0xC0、0xC1、0xC2等设置LCD的电压、伽马和时序参数命令0x36设置扫描方向和RGB/BGR顺序命令0x3A设置像素格式为16位命令0x2A和0x2B设置列地址和行地址范围最后命令0x29开启显示。网上流传的初始化序列有长有短有些厂家喜欢省略部分电源命令这就会导致同一段代码在这个屏上正常、换个屏就发白或偏色。我建议只要条件允许就用完整的初始化序列不管屏幕商家给没给先用标准序列适配再按实际效果微调。我下面的代码就是一份完整可用的ST7735S初始化序列兼容绝大多数模块。2.3 显存窗口与像素写入ST7735S没有直接“画点”的命令主机要写入一个像素必须先通过命令0x2A设置列地址范围命令0x3A设置行地址范围然后连续发送16位颜色数据。控制器会在设定的窗口内按从左到右、从上到下的顺序自动填充。这就引出一个性能关键点画一屏80x160的画面需要连续发送12800个像素、每像素2字节总共25600字节数据。如果每画一个像素都重复发送一次窗口命令速度会非常慢。所以正确的做法是先用窗口命令锁住整个屏幕区域然后一次性把所有像素数据连续发出去也就是“整屏填充”模式。代码里的LCD_Fill函数就是这么干的LCD_DrawPixel只是作为最基础的绘图原语存在真正刷屏时不会逐点调用它。3. 完整驱动代码实现3.1 代码结构设计代码我分成三个文件来组织lcd.h头文件、lcd.c底层与初始化实现、main.c里的调用示例。如果你是用CubeMX生成的工程直接把lcd.c和lcd.h加进项目即可。所有函数名前缀用LCD_避免和自己业务代码冲突。下面先给出头文件里面定义了引脚宏、常用颜色宏和函数声明。如果你的接线不同只需要修改宏定义里的引脚和SPI句柄。// lcd.h #ifndef __LCD_H #define __LCD_H #include main.h // 屏幕分辨率0.96寸ST7735S常见为80x160 // 如果为128x160修改为128和160即可 #define LCD_WIDTH 80 #define LCD_HEIGHT 160 // 引脚宏定义按你的接线修改 #define LCD_CS_PIN GPIO_PIN_12 #define LCD_CS_PORT GPIOB #define LCD_RST_PIN GPIO_PIN_13 #define LCD_RST_PORT GPIOB #define LCD_DC_PIN GPIO_PIN_14 #define LCD_DC_PORT GPIOB // 控制线电平操作 #define LCD_CS_LOW() HAL_GPIO_WritePin(LCD_CS_PORT, LCD_CS_PIN, GPIO_PIN_RESET) #define LCD_CS_HIGH() HAL_GPIO_WritePin(LCD_CS_PORT, LCD_CS_PIN, GPIO_PIN_SET) #define LCD_RST_LOW() HAL_GPIO_WritePin(LCD_RST_PORT, LCD_RST_PIN, GPIO_PIN_RESET) #define LCD_RST_HIGH() HAL_GPIO_WritePin(LCD_RST_PORT, LCD_RST_PIN, GPIO_PIN_SET) #define LCD_DC_LOW() HAL_GPIO_WritePin(LCD_DC_PORT, LCD_DC_PIN, GPIO_PIN_RESET) #define LCD_DC_HIGH() HAL_GPIO_WritePin(LCD_DC_PORT, LCD_DC_PIN, GPIO_PIN_SET) // 常用颜色 #define LCD_COLOR_BLACK 0x0000 #define LCD_COLOR_WHITE 0xFFFF #define LCD_COLOR_RED 0xF800 #define LCD_COLOR_GREEN 0x07E0 #define LCD_COLOR_BLUE 0x001F #define LCD_COLOR_YELLOW 0xFFE0 #define LCD_COLOR_CYAN 0x07FF #define LCD_COLOR_MAGENTA 0xF81F // RGB888转RGB565 #define LCD_RGB565(r, g, b) \ ((((r) 0xF8) 8) | (((g) 0xFC) 3) | (((b) 0xF8) 3)) void LCD_Init(void); void LCD_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1); void LCD_DrawPixel(uint16_t x, uint16_t y, uint16_t color); void LCD_Fill(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color); void LCD_Clear(uint16_t color); void LCD_ShowChar(uint16_t x, uint16_t y, char ch, uint16_t color, uint16_t bgColor); void LCD_ShowString(uint16_t x, uint16_t y, char *str, uint16_t color, uint16_t bgColor); #endifLCD_RGB565宏里做了一次位掩码和移位把8位的R、G、B分量截断成5位、6位、5位并合并到16位整数里这样你可以直接用LCD_RGB565(255, 0, 0)这种方式写颜色可读性比裸的数字好很多。3.2 底层SPI与引脚初始化如果你用CubeMXGPIO和SPI的初始化代码会自动生成。我这里额外封装两个底层发送函数一个专门发命令一个专门发数据。最关键的一点是SPI发送过程中必须把DC引脚的电平保持正确。发送命令时DC为低发送数据时DC为高而且要在CS拉低之前设置好DC否则时序上会出现命令被当成数据、数据被当成命令的错位。// lcd.c 部分代码 #include lcd.h #include spi.h extern SPI_HandleTypeDef hspi1; static void LCD_WriteCmd(uint8_t cmd) { LCD_DC_LOW(); LCD_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 1, 100); LCD_CS_HIGH(); } static void LCD_WriteData(uint8_t dat) { LCD_DC_HIGH(); LCD_CS_LOW(); HAL_SPI_Transmit(hspi1, dat, 1, 100); LCD_CS_HIGH(); }HAL_SPI_Transmit最后一个参数是超时时间单位毫秒。平时发单字节命令和少量数据100毫秒足够。等到后面刷整屏像素时如果用阻塞方式发送25600字节建议把超时设大一些比如1000毫秒或者改用DMA方式我后面会讲。3.3 初始化函数实现初始化函数的流程一定要严格按照时序走上电后先拉低复位引脚至少10毫秒让控制器彻底复位再拉高并等待至少120毫秒让内部电源稳定。这一步偷懒的话有时屏也能亮但偶尔会出现半屏白、初始化失败这种随机故障特别难查。void LCD_Init(void) { // 复位时序 LCD_CS_HIGH(); LCD_RST_LOW(); HAL_Delay(20); LCD_RST_HIGH(); HAL_Delay(150); // 软复位 LCD_WriteCmd(0x01); HAL_Delay(150); // 退出睡眠模式 LCD_WriteCmd(0x11); 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); 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(0x20); // 设置扫描方向和RGB/BGR顺序 // 0xC8为竖屏0xA8为竖屏上下翻转0x08为横屏 LCD_WriteCmd(0x36); LCD_WriteData(0xC8); // 16位像素格式 LCD_WriteCmd(0x3A); LCD_WriteData(0x05); // 列地址0~79 LCD_WriteCmd(0x2A); LCD_WriteData(0x00); LCD_WriteData(0x00); LCD_WriteData(0x00); LCD_WriteData(0x4F); // 行地址0~159 LCD_WriteCmd(0x2B); LCD_WriteData(0x00); LCD_WriteData(0x00); LCD_WriteData(0x00); LCD_WriteData(0x9F); // 开启反色 LCD_WriteCmd(0x21); // 正常显示 LCD_WriteCmd(0x13); HAL_Delay(10); // 开启显示 LCD_WriteCmd(0x29); LCD_Clear(LCD_COLOR_BLACK); }地址范围那段要重点看列地址结束值0x4F对应十进制79行地址结束值0x9F对应十进制159这是80x160分辨率。如果你买到的是128x160的0.96寸屏把列结束值改成0x7F即可。如果搞错会出现只显示半屏或者画面被拉伸的问题。3.4 像素点与矩形绘制设置窗口是绘图的关键函数所有批量绘制都靠它。它的作用是告诉控制器“接下来我要写入的数据落在哪个矩形区域内”然后你连续发数据控制器会自动按行列顺序填充。void LCD_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { LCD_WriteCmd(0x2A); LCD_WriteData(x0 8); LCD_WriteData(x0 0xFF); LCD_WriteData(x1 8); LCD_WriteData(x1 0xFF); LCD_WriteCmd(0x2B); LCD_WriteData(y0 8); LCD_WriteData(y0 0xFF); LCD_WriteData(y1 8); LCD_WriteData(y1 0xFF); }需要注意的是虽然80x160的分辨率比较小列地址和行地址理论上用8位就够但ST7735S的命令格式仍然要求每个地址用两个字节表示高字节在前。所以x0 8这种写法是必需的不要为了省事只写低字节。有了窗口画矩形填充就很自然了。循环发送颜色数据时我特意用了一个16位的颜色值直接做两个字节的拆分保证高位在前。void LCD_Fill(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) { uint32_t i; uint32_t pixelCount; uint8_t data[2]; LCD_SetWindow(x0, y0, x1, y1); pixelCount (uint32_t)(x1 - x0 1) * (uint32_t)(y1 - y0 1); data[0] color 8; data[1] color 0xFF; LCD_DC_HIGH(); LCD_CS_LOW(); for (i 0; i pixelCount; i) { HAL_SPI_Transmit(hspi1, data, 2, 100); } LCD_CS_HIGH(); }这段代码有一个值得注意的细节先把DC拉高、CS拉低然后在循环里只发数据不要每发一个像素都重新设置DC和CS。假如你的屏幕是80x160分辨率一次LCD_Fill要循环12800次我这样写能省下大量GPIO翻转时间肉眼可见地减少刷新延迟。如果你用LCD_DrawPixel逐点画矩形效果会卡到怀疑人生。3.5 显示字符与字库方案字符显示本质上就是“用字模数组控制像素点”。每个5x7字符用一个长度为5的数组表示数组的每一位对应一列的7个像素位为1就画前景色位为0就画背景色。void LCD_ShowChar(uint16_t x, uint16_t y, char ch, uint16_t color, uint16_t bgColor) { uint8_t i, j; const uint8_t *p; // 这里只列出部分字符完整ASCII字模可以从标准5x7字体生成器获得 static const uint8_t font_5x7[2][5] { {0x00, 0x00, 0x00, 0x00, 0x00}, // 空格 {0x7E, 0x09, 0x09, 0x09, 0x7E}, // A }; if (ch ) p font_5x7[0]; else if (ch A) p font_5x7[1]; else return; for (i 0; i 5; i) { for (j 0; j 7; j) { if (p[i] (0x01 j)) LCD_DrawPixel(x i, y j, color); else LCD_DrawPixel(x i, y j, bgColor); } } }上面的代码只是演示原理实际项目中你要准备完整的ASCII字模数组常见的做法是用“PCtoLCD2002”这类字模软件生成C数组。中文字库更简单取模时设置16x16点阵每个汉字32字节用同样的点阵判断逻辑按行扫描即可。注意中文显示时建议用专门的“汉字显示函数”不要和5x7字符函数混在一起否则代码可读性会变得很差。4. 从CubeMX到点亮实操过程记录4.1 创建工程与参数配置我用的开发环境是Keil MDK配合STM32CubeMX初始化代码。新工程的关键配置如下芯片选择STM32F103C8TxSPI1选择全双工主机模式波特率预分频设置为2分频或4分频也就是SPI时钟在18MHz或9MHz左右SPI参数CPOLLowCPHA1 Edge即模式0帧格式8位先发送MSBPB12、PB13、PB14配置为GPIO输出初始电平设为高。CubeMX生成代码以后把lcd.c和lcd.h加入工程然后在main.c的main函数里调用LCD_Init()和几个测试函数。这里有个小技巧SPI速率先别拉得太高。很多0.96寸模块的PCB走线和排针质量参差不齐18MHz下稳定不代表36MHz也稳定。我的习惯是先9MHz调通所有功能再逐步往上升。4.2 编译下载要点编译时如果报Undefined symbol HAL_SPI_Transmit说明SPI外设没有初始化检查CubeMX生成的MX_SPI1_Init()是否被调用。如果报引脚重定义去CubeMX里看PA5、PA7是否被别的外设占用。下载我用ST-Link V2在Keil里选好调试器后把Settings里的Flash Download勾选上Reset and Run。这样做的好处是每次下载完程序自动复位运行不用手动按复位键。如果你的板子用ST-Link连不上先检查驱动是否安装好。ST-Link V2在Win10/Win11下一般不需要手动装驱动但部分山寨版需要安装WinUSB驱动才能被识别。J-Link同理如果设备管理器里看到黄色感叹号去装一下J-Link官方驱动就能解决。4.3 点亮测试与效果验证第一次点亮我不建议直接跑自己的界面代码。先做一个纯色自检把红、绿、蓝、白四种颜色各显示2秒能快速暴露三类问题接线错误、SPI模式错误、RGB565字节顺序错误。void LCD_TestMode(void) { LCD_Clear(LCD_COLOR_RED); HAL_Delay(2000); LCD_Clear(LCD_COLOR_GREEN); HAL_Delay(2000); LCD_Clear(LCD_COLOR_BLUE); HAL_Delay(2000); LCD_Clear(LCD_COLOR_WHITE); HAL_Delay(2000); LCD_Clear(LCD_COLOR_BLACK); }纯色测试通过后再调用LCD_ShowString显示一行字符。如果颜色正常但字符方向不对比如上下颠倒、左右镜像那就是0x36命令的MADCTL参数需要调。我常用的几个值0xC8竖屏0xA8竖屏旋转180度0x08是横屏0x68是横屏旋转180度。最终用哪个值取决于你安装屏幕的方向。5. 踩坑实录常见问题与排查速查5.1 上电白屏白屏是最常见的故障原因通常是复位时序不对、初始化序列不完整、或者电源电压不稳。排查顺序先用万用表量模块VCC是不是3.3V然后把初始化函数里RST拉低的延时从20毫秒加到100毫秒部分模块对复位脉宽要求更宽最后确认初始化序列里0x11和0x29之间是否间隔足够有些屏幕从退出睡眠到开启显示之间必须有延时我代码里在0x13之后加了10毫秒这个延时不建议去掉。还有一种情况比较隐蔽主控和屏幕没有共地。只接了信号线没接GNDSPI波形完全乱掉屏幕就一直白屏。我早期做实验吃过这个亏提醒一次。5.2 花屏颜色错乱颜色错乱先检查SPI模式。ST7735S要求模式0也就是CPOL0、CPHA0。如果你在CubeMX里配置成了模式3数据和时钟边沿错开控制器读到的每个字节都乱了表现就是花屏、颜色不对。把模式改回0问题立刻消失。RGB565字节顺序也会导致颜色偏色。如果红色变成绿色、蓝色变成橙色多半是发送颜色数据时高低字节反了。ST7735S默认以高字节在前接收颜色数据也就是先发color 8再发color 0xFF。如果你在别的单片机上移植过驱动习惯低字节在前的写法这里一定要改回来。5.3 显示区域偏移或半屏画面只占半屏、或文字被截断这是CASET和RASET的参数和实际分辨率不匹配。0.96寸屏有80x160和128x160两种分辨率代码里LCD_WIDTH和LCD_HEIGHT要跟着改初始化函数里0x2A和0x2B的结束地址也要同步改。80x160时列地址结束值是0x4F128x160时改为0x7F。如果你把80宽的屏硬配成128宽屏幕会读取错误的窗口范围显示内容自然就不对。一个更快的判断方法直接看屏的丝印。如果丝印写着0.96大部分是80x160如果写着1.8寸必然是128x160。下单前也可以问店家要规格书避免拿到手再折腾。5.4 下载器与驱动安装相关这块屏幕本身不带USB转串口一般下载用的是ST-Link或J-Link。如果你用的是带CH340串口的核心板通过串口下载板子上必须有BOOT0拉高才能进入ISP模式下载完再跳回低电平。ST-Link下载时如果提示No target connected优先检查SWDIO、SWCLK两个引脚有没有接反。驱动方面Windows设备管理器里看到“USB Serial Port”但打感叹号就去装对应VCP驱动看到ST-Link设备但无法连接就去装ST官方驱动。这些问题和LCD驱动没有直接关系但会卡住整个开发流程我把它列在这里给新手避坑。6. 进一步优化与扩展方向屏幕点亮只是第一步实际项目中真正能提升体验的是刷新效率。我这套代码用的是阻塞式HAL_SPI_Transmit传整屏数据时CPU被占满刷动画会有可感知的延迟。如果要做流畅的仪表动画或游戏建议改成DMA发送。思路很简单先用LCD_SetWindow设置好整个屏幕区域然后把要显示的数据放进一个缓冲区调用HAL_SPI_Transmit_DMA启动发送CPU在DMA搬运数据的同时可以去跑其他逻辑。要注意DMA发送期间不能再调用其他SPI写操作否则数据会穿插错乱。我的做法是维护一个dmaBusy标志位DMA传输完成中断里把它清掉新一帧数据只有在上一帧发送完成后才更新。如果还想更进一步可以考虑移植LVGL图形库。LVGL自带ST7735S的驱动接口你只需要把底层画点函数和flush回调函数对接到我这套代码上然后用LVGL的控件API做界面开发效率会高很多。像0.96寸这样的小屏做鼠标指针、滑条表盘LVGL 8.3版本跑起来毫无压力。另外一个容易被忽略的点是背光控制。我最初直接把LED引脚接3.3V屏幕亮度恒定后来改成PWM控制在环境光暗的地方可以自动调低亮度既省电又护眼。用定时器的PWM输出通道接到LED引脚占空比50%左右就能获得不错的视觉效果。最后分享一个我调试时的习惯每次改动初始化序列或方向参数都要用纯色自检来验证不要直接跑完整UI。纯色画面能暴露绝大多数硬件和时序问题而且看起来一目了然。我自己踩过很多次花屏和白屏的坑靠的就是这套“先单色、后彩色、再纹理”的验证流程一步步把问题缩小到具体环节。按照这份代码和排查思路你手里的0.96寸ST7735S屏幕应该很快就能亮起来。