ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32驱动ST7735S 1.8寸TFT-LCD实战:软件SPI点亮与驱动详解

2026/9/21 1:34:19 拓冰建站 浏览量
STM32驱动ST7735S 1.8寸TFT-LCD实战:软件SPI点亮与驱动详解 一块1.8寸的小彩屏接在STM32上通电后白屏按网上的代码敲了一遍还是不亮——这个场景恐怕很多新手都经历过。最近我刚好在某宝淘了几块ST7735S控制器的1.8寸TFT-LCD分辨率128x160用软件SPI在STM32F103C8T6上把它从零点亮顺手把显示文字、画线、填充这些基础功能全部调通。这篇文章就把完整的思路、代码、踩坑过程都记录下来希望能帮你少走点弯路。这篇文章适合正好手上有这类屏幕、正在纠结怎么点亮的人也适合想搞懂SPI时序和LCD初始化原理的初学者。内容不依赖特定开发板接线用普通GPIO模拟SPI代码以HAL库为主标准库用户也能看得懂因为核心时序逻辑是一样的。1. 项目整体设计与思路拆解1.1 先认识这块屏市面上常见的1.8寸TFT-LCD屏幕绝大多数用的控制器是ST7735S或者老一点的ST7735R。两者寄存器基本兼容驱动代码差别不大。屏幕本身是128x160像素也就是说竖着放的时候宽128像素高160像素。每个像素点用RGB565格式表示占2个字节红色是0xF800绿色是0x07E0蓝色是0x001F这个后面写填充函数时要用到。模块的外观一般是8个引脚或者9个引脚SPI接口的丝印通常长这样VCC、GND、CS、RESET、DC有的叫RS、SDA有的叫DIN或MOSI、SCK、LED。有些模块把LED背光脚放在VCC旁边有些则单独拉出来。如果你买的模块还有MISO脚那很可能还支持读操作但ST7735S的读功能实际中用得少普通显示根本用不上。关于供电这里得特别注意模块板载了LDO的可以把VCC接到5V没板载LDO的就只能接3.3V。判断方法很简单看模块背面有没有一颗小的三端稳压芯片。如果没有接5V大概率直接烧掉。我第一次用的时候没注意这个细节差点报废一块屏后来养成习惯拿到模块先翻背面看一眼电路。1.2 为什么选用软件SPI关于驱动方式其实有两种选择硬件SPI和软件SPI。硬件SPI是STM32芯片自带的SPI外设由硬件自动产生时钟信号和移位数据CPU只需把数据丢进寄存器就行。速度快不占CPU时间但缺点是引脚绑定死了。比如STM32F103C8T6的SPI1只能用PA4-PA7这组引脚如果你这些引脚已经被其他外设占了就比较麻烦。另外硬件SPI的配置选项多时钟极性、相位、分频系数、数据帧格式任何一个配错都点不亮新手排查起来头大。软件SPI则是用GPIO直接模拟时序想接哪只脚就接哪只脚完全绕开外设引脚映射问题配置也极其简单。而且代码在裸机上逻辑透明出了问题一眼就能看出是时序问题还是接线问题非常适合学习。对于1.8寸这种小屏软件SPI的速度完全够用。我实际测试过将GPIO翻转延时控制在微秒级SCK频率能做到1MHz左右刷新一帧128x160的满屏数据大约需要300多毫秒显示静态信息、菜单、仪表数字完全没问题。如果做动态波形显示也能勉强跑起来只是会有肉眼可见的刷新过程。下面是两种方式的简单对比方便你做选择对比项软件SPI硬件SPI引脚选择任意GPIO灵活固定引脚受外设映射限制配置复杂度低只要GPIO输出就行高需配置极性、相位、分频等传输速度较低SCK 1MHz以内高可达18MHz甚至更高CPU占用高每个bit都要CPU翻转引脚低硬件自动搬运适合场景小屏、低速、学习调试大屏、高速刷新、复杂UI代码可移植性极高换单片机只需要改引脚宏与具体MCU外设绑定1.3 软件SPI的时序基础与速度估算SPI协议的核心就三根线SCK时钟、SDA数据、CS片选。传输的时候主机在时钟的某个边沿把数据位放到数据线上从机在另一个边沿采样。ST7735S支持多种SPI模式但最常用的是模式0也就是时钟空闲为低电平数据在上升沿采样。软件模拟模式0的字节发送逻辑可以这样描述对于要发送的8个bit从最高位开始每个bit先拉低SCK然后在SDA线上输出电平再拉高SCK形成一个完整的时钟脉冲最后把数据左移一位继续处理下一个bit。这里有一个非常容易踩的坑SDA电平必须在SCK拉高之前稳定下来。如果先拉高SCK再改SDA从机可能在错误的时间采样到不稳定的数据导致显示错乱。所以代码里一定要保证顺序是“拉低SCK - 设置SDA - 拉高SCK”。速度方面一次完整的字节发送需要8个SCK脉冲再加上函数调用和GPIO翻转的系统开销。STM32F103在72MHz主频下GPIO翻转一次大约需要几十纳秒。我测试过不加延时的软件SPISCK频率能到2MHz左右但如果加上HAL库的引脚操作封装实际速度会慢一些。如果你在延时函数里加了太长的nop循环一帧数据可能会刷新到接近1秒。所以软件SPI虽然简单速度还是要心里有数。2. 核心细节解析与实操要点2.1 引脚规划与GPIO初始化点亮屏幕之前先规划好引脚。我的接法是随便挑了几个闲置GPIOPB13做SCKPB15做SDAPB14做DCPB12做CSPB11做RESET背光LED用3.3V直接点亮。这个接法没有特殊原因仅仅说明软件SPI的灵活性——完全绕开了硬件SPI的固定引脚。实际接线对照表屏幕丝印功能说明STM32引脚VCC电源正极3.3VGND电源地GNDCS片选低电平有效PB12RESET复位低电平有效PB11DC数据/命令选择PB14SDA数据线MOSIPB15SCK时钟线PB13LED背光控制3.3VGPIO初始化代码用HAL库写所有引脚配置为推挽输出速度设置为HIGH。速度为什么要设成HIGH因为软件SPI是靠GPIO快速翻转产生时钟的翻转速度太慢会导致时序信号上升沿不够陡峭影响从机采样。虽然软件模拟本身速度也不快但GPIO的驱动能力还是要给足。void LCD_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_PULLUP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); }初始化完成后最好先用宏把各引脚定义好后面写代码会清爽很多移植到别的板子也只改这一处。我用的是这种风格#define LCD_SCK_PIN GPIO_PIN_13 #define LCD_SCK_PORT GPIOB #define LCD_SDA_PIN GPIO_PIN_15 #define LCD_SDA_PORT GPIOB #define LCD_CS_PIN GPIO_PIN_12 #define LCD_CS_PORT GPIOB #define LCD_DC_PIN GPIO_PIN_14 #define LCD_DC_PORT GPIOB #define LCD_RST_PIN GPIO_PIN_11 #define LCD_RST_PORT GPIOB #define LCD_SCK_H() HAL_GPIO_WritePin(LCD_SCK_PORT, LCD_SCK_PIN, GPIO_PIN_SET) #define LCD_SCK_L() HAL_GPIO_WritePin(LCD_SCK_PORT, LCD_SCK_PIN, GPIO_PIN_RESET) #define LCD_SDA_H() HAL_GPIO_WritePin(LCD_SDA_PORT, LCD_SDA_PIN, GPIO_PIN_SET) #define LCD_SDA_L() HAL_GPIO_WritePin(LCD_SDA_PORT, LCD_SDA_PIN, GPIO_PIN_RESET) #define LCD_CS_H() HAL_GPIO_WritePin(LCD_CS_PORT, LCD_CS_PIN, GPIO_PIN_SET) #define LCD_CS_L() HAL_GPIO_WritePin(LCD_CS_PORT, LCD_CS_PIN, GPIO_PIN_RESET) #define LCD_DC_H() HAL_GPIO_WritePin(LCD_DC_PORT, LCD_DC_PIN, GPIO_PIN_SET) #define LCD_DC_L() HAL_GPIO_WritePin(LCD_DC_PORT, LCD_DC_PIN, GPIO_PIN_RESET) #define LCD_RST_H() HAL_GPIO_WritePin(LCD_RST_PORT, LCD_RST_PIN, GPIO_PIN_SET) #define LCD_RST_L() HAL_GPIO_WritePin(LCD_RST_PORT, LCD_RST_PIN, GPIO_PIN_RESET)这里多说一句如果你用的引脚恰好是PB3、PB4、PA15这些就要先处理JTAG复用问题。具体我放到后面常见问题里细说因为这是很多人点不亮屏幕的隐形杀手。2.2 字节发送的精髓时序函数逐行拆解软件SPI最底层的函数就是发送一个字节。所有命令、数据的传输最终都会落到这个函数上。把这个函数理解透了整个软件SPI就算学会了一半。void LCD_SPI_WriteByte(uint8_t data) { for (uint8_t i 0; i 8; i) { LCD_SCK_L(); if (data 0x80) LCD_SDA_H(); else LCD_SDA_L(); data 1; LCD_SCK_H(); } }这个函数的逻辑就是前面说的模式0时序先拉低SCK把当前bit的数据放到SDA上然后拉高SCK让从机在上升沿采样。每循环一次处理一个bit共8次完成一个字节的传输。这里有个细节值得反复强调判断用的是data 0x80先发最高位也就是大端模式。这是SPI协议的标准做法ST7735S也是这么要求的。不要写反成从最低位开始发否则屏幕显示出来的内容会完全错乱。我在调试时发现过一种情况数据发送顺序是反的结果屏幕上显示的颜色和字符像被镜像了一样全是乱的。排查了很久才发现是位发送顺序搞反了。从这以后我写软件SPI都固定按“高字节先出”来。如果你的屏幕刷新速度太慢可以在LCD_SCK_H()和LCD_SCK_L()之间加几个nop指令来降低时钟频率。但测试下来ST7735S对时序要求并不苛刻不加延时也能正常工作。如果你用的是74LS系列逻辑芯片电平转换模块就要考虑增加延时因为逻辑芯片本身有传输延迟。2.3 命令与数据模式的切换SPI发送字节只解决了“怎么把数据传输出去”的问题屏幕上还分不清你发的是命令还是数据。这就需要DC引脚出场了。DC引脚的高电平表示当前传输的是数据低电平表示当前传输的是命令。也就是说在发送命令字节之前先把DC拉低让屏幕知道“接下来给我的是命令请解析到命令寄存器里”发完再拉高。发送数据时则相反。对应的两个基础函数void LCD_WriteCmd(uint8_t cmd) { LCD_DC_L(); LCD_CS_L(); LCD_SPI_WriteByte(cmd); LCD_CS_H(); } void LCD_WriteData(uint8_t data) { LCD_DC_H(); LCD_CS_L(); LCD_SPI_WriteByte(data); LCD_CS_H(); }注意CS片选的用法。CS是低电平有效所以传输期间要拉低传输完成后拉高。在整个字节传输过程中CS保持低电平不变。实际开发中有些工程师会让CS一直接地或者一直拉低不切换这种做法也能工作毕竟SPI是一主一从的情况。但严格来说每次传输的前后切换CS更规范也为以后挂多个SPI设备做准备。我习惯是每个字节都控制CS额外开销就两条GPIO语句无伤大雅。写命令和写数据这两个函数是屏幕驱动的地基后面所有功能——初始化、设置坐标、填充颜色、显示字符——最终都靠它们来完成。2.4 初始化序列到底在干什么屏幕拿到手以后不能直接就开始填充像素。ST7735S内部的状态机需要走一遍上电初始化流程把显示模式、颜色格式、电源参数、伽马曲线这些寄存器配置好屏幕才能正常显示。初始化的代码看起来就是一堆命令加数据的组合很多教程直接让你复制粘贴但不告诉你每一条在干什么。我拆开讲解几个关键命令0x11是Sleep Out让屏幕从睡眠模式唤醒。唤醒之后需要延时120毫秒左右等待内部振荡器稳定。这个延时不能省否则后续命令可能不被响应。0x36是MADCTL寄存器控制显示方向和RGB/BGR颜色顺序。这个寄存器非常关键屏幕横屏还是竖屏、颜色对不对都由它决定。常见取值有0x00竖屏RGB、0x08竖屏BGR、0xC0横屏、0xA0另一种横屏。不同厂家的屏默认值不一样如果你发现颜色反了或者方向不对重点改这里。0x3A是像素格式设置。写成0x05表示16位色RGB565写成0x06表示18位色RGB666。对于大多数应用场景选0x05就够了。如果你设成18位色却只发2字节数据画面颜色会完全混乱。后面的0xB1、0xB4、0xC0、0xC1、0xC5、0xE0、0xE1这些是电源控制、帧率设置和伽马校正。理论上不写也能出画面但官方推荐序列能保证显示效果和稳定性。我一直用一套比较完整的初始化序列简单总结就是唤醒 - 设置方向 - 设置颜色格式 - 设置电源参数 - 打开显示。完整初始化函数如下void LCD_Init(void) { LCD_GPIO_Init(); LCD_RST_H(); HAL_Delay(50); LCD_RST_L(); HAL_Delay(50); LCD_RST_H(); HAL_Delay(50); LCD_WriteCmd(0x11); HAL_Delay(120); LCD_WriteCmd(0x36); LCD_WriteData(0x08); LCD_WriteCmd(0x3A); LCD_WriteData(0x05); LCD_WriteCmd(0xB1); LCD_WriteData(0x05); LCD_WriteData(0x3C); LCD_WriteData(0x3C); LCD_WriteCmd(0xB4); LCD_WriteData(0x03); LCD_WriteCmd(0xC0); LCD_WriteData(0x28); LCD_WriteData(0x08); LCD_WriteData(0x04); LCD_WriteCmd(0xC1); LCD_WriteData(0xC0); LCD_WriteCmd(0xC5); LCD_WriteData(0x00); LCD_WriteData(0x1C); LCD_WriteCmd(0xE0); LCD_WriteData(0x04); LCD_WriteData(0x1F); LCD_WriteData(0x1A); LCD_WriteData(0x0D); LCD_WriteData(0x04); LCD_WriteData(0x08); LCD_WriteData(0x07); LCD_WriteData(0x01); LCD_WriteData(0x04); LCD_WriteData(0x08); LCD_WriteData(0x0D); LCD_WriteData(0x0D); LCD_WriteData(0x0D); LCD_WriteData(0x1C); LCD_WriteData(0x1B); LCD_WriteCmd(0xE1); LCD_WriteData(0x04); LCD_WriteData(0x1F); LCD_WriteData(0x1A); LCD_WriteData(0x0D); LCD_WriteData(0x04); LCD_WriteData(0x08); LCD_WriteData(0x07); LCD_WriteData(0x01); LCD_WriteData(0x04); LCD_WriteData(0x08); LCD_WriteData(0x0D); LCD_WriteData(0x0D); LCD_WriteData(0x0D); LCD_WriteData(0x1C); LCD_WriteData(0x1B); LCD_WriteCmd(0x29); }最后一条0x29是Display On执行到这里屏幕应该已经被点亮了即使不填充任何像素背景也会变成默认颜色而不是白屏。这里补充一个经验初始化之后立刻显示全屏纯色是验证屏幕是否正常的最快方法。我一般会先调用填充函数把整个屏幕刷成红色如果颜色正确、没有条纹再继续往下写功能代码。这样能把“屏幕不亮”和“代码有问题”这两个问题快速分开。3. 实操过程与核心环节实现3.1 底层驱动文件编写底层驱动是整个屏幕项目的根基。我建议把代码拆成两个文件lcd_io.c和lcd.c。lcd_io.c负责最底层的GPIO操作和字节发送lcd.c负责初始化、坐标设置、颜色填充这些功能。这样拆开的好处是如果以后你想把屏幕换成硬件SPI只需要改lcd_io.c不用动上层任何代码。lcd_io.h里把刚才那几个宏和字节发送函数声明放进去。lcd.c里包含lcd_io.h然后实现初始化、窗口设置、画点、填充、字符显示等函数。窗口设置函数是所有绘图操作的核心它的作用是告诉屏幕“接下来我要往哪个矩形区域内写像素”。ST7735S的窗口设置由三个命令完成0x2A设置列地址范围0x2B设置行地址范围0x2C是开始写显存。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); LCD_WriteCmd(0x2C); }注意坐标是用16位表示的所以高8位和低8位要分开发送。对于只有128x160分辨率的屏幕来说坐标值不可能超过255高8位永远为0但按规范还是要发别偷懒。设置完窗口之后紧接着发送的每一个数据字节都会按顺序写入窗口内的像素。写入顺序是从窗口左上角开始从左到右从上到下。当窗口内的像素写满后显存地址会自动回卷到窗口起点。3.2 基础图形与颜色填充有了窗口设置函数画点就很简单了。把窗口设置成单个像素然后发送2字节的颜色数据void LCD_DrawPoint(uint16_t x, uint16_t y, uint16_t color) { LCD_SetWindow(x, y, x, y); LCD_WriteData(color 8); LCD_WriteData(color 0xFF); }这里又有一个细节RGB565颜色是2字节发送时高字节在前低字节在后。这一点容易和很多人的习惯相反导致颜色不对。如果发现屏幕显示的颜色和预期差很多优先检查这里。全屏填色就更直接了把窗口设置成整个屏幕或者某个区域然后循环发送颜色数据void LCD_Fill(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) { uint32_t pixelCount (uint32_t)(x1 - x0 1) * (y1 - y0 1); LCD_SetWindow(x0, y0, x1, y1); for (uint32_t i 0; i pixelCount; i) { LCD_WriteData(color 8); LCD_WriteData(color 0xFF); } }这里有一个性能优化点在循环里调用LCD_WriteData每个字节都会操作一遍DC、CS、SPI发送开销比较大。空间允许的情况下可以把LCD_WriteData封装成非切换CS的版本在填充循环之外一次拉低CS循环内只发送数据字节全部发完再拉高CS。这样能省掉大量CS切换的时间。画线函数我用的是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 2 * err; if (e2 -dy) { err - dy; x0 sx; } if (e2 dx) { err dx; y0 sy; } } }这个函数支持任意方向画线斜线、水平线、垂直线都能处理。实际项目中坐标转换、仪表刻度、波形曲线都能靠它扩展出来。3.3 显示字符与简单UI屏幕能点亮、能填充颜色之后下一步就是显示文字。显示字符的原理是把字模数组中的每个bit映射成一个像素点bit为1画前景色bit为0画背景色或者跳过不画。我常用的是8x16的ASCII字模。这种字模每个字符用16个字节表示每字节是一行中的8个像素每行从左到右对应bit7到bit0。取模软件生成字模时需要注意参数设置一般选“阴码、列行式、逆向、逐列式”。如果你发现显示的字符是横向的、乱的或者反的基本都是取模设置不对。显示一个字符的代码逻辑如下void LCD_ShowChar(uint16_t x, uint16_t y, char ch, uint16_t color) { uint8_t idx ch - ; uint8_t temp; for (uint8_t i 0; i 16; i) { temp ASCII8x16[idx][i]; for (uint8_t j 0; j 8; j) { if (temp 0x80) LCD_DrawPoint(x j, y i, color); temp 1; } } }这种逐点绘制的方式简单直观但缺点是慢。显示一个字符需要调用16x8次画点函数每次画点都设置窗口、发送数据。如果你追求速度可以先把一行8个像素拼成一个字节再用窗口填充的方式一次写入。不过对于静态界面来说逐点绘制完全够用速度差异感知不明显。字符串显示就是循环调用字符显示函数遇到换行符就换一行继续void LCD_ShowString(uint16_t x, uint16_t y, const char *str, uint16_t color) { while (*str) { if (*str \n) { y 16; x 0; } else { LCD_ShowChar(x, y, *str, color); x 8; if (x 128 - 8) { x 0; y 16; } } str; } }到这里一个简单的UI雏形就出来了。你可以用填充函数画一个背景色块再在上面显示文字做一个简单的菜单界面。我实际做温湿度计界面的时候就是先全屏填充深蓝色背景然后画一个白色边框矩形再在固定坐标显示温度数值和单位效果还挺像那么回事。3.4 主函数与完整流程把以上代码串起来主函数就非常简洁了。基本流程是初始化系统时钟和滴答定时器初始化屏幕然后进入主循环。int main(void) { HAL_Init(); SystemClock_Config(); LCD_Init(); LCD_Fill(0, 0, 127, 159, 0x001F); // 蓝色背景 LCD_ShowString(20, 60, Hello STM32, 0xFFFF); LCD_ShowString(20, 80, TFT-LCD OK, 0x07E0); while (1) { HAL_Delay(10); } }SystemClock_Config函数根据不同板子的晶振频率需要微调这块在CubeMX里生成一次就不会出问题。如果你用的是正点原子、野火或者其他开发板时钟配置代码直接拿现成的就行。我在测试这段代码时屏幕显示蓝色背景白色文字“Hello STM32”绿色文字“TFT-LCD OK”整个效果稳定没有闪烁或者花屏。这说明初始化序列、软件SPI时序、颜色格式这几个关键点全部没问题。4. 常见问题与排查技巧实录4.1 白屏屏幕白屏是最常见的问题也是最让人头疼的因为白屏意味着背光亮了但屏幕没有接收到有效信号。我把实际踩过的坑整理成几个排查方向首先是供电。用万用表量一下VCC引脚电压是不是3.3V。不是的话先解决电源问题。我见过一个案例是杜邦线内部断了导致屏幕的VCC虚接屏幕偶尔白屏偶尔正常。这种间歇性故障最坑人最好检查每一根杜邦线是否导通。摸一下屏幕表面有没有微热如果完全冰凉大概率是电源没进去。其次是复位时序。检查RESET引脚有没有按照“拉低-延时-拉高”的顺序操作。有些教程的初始化代码里复位延时太短屏幕还没稳定就开始发命令也会导致白屏。我一般把复位低电平时间维持50毫秒以上确保屏幕彻底复位完成。最后是初始化序列是否完整。如果跳过了唤醒命令或者延时不够屏幕就会一直白屏。网上流传的初始化代码五花八门我建议用本文中的这套完整序列如果还不行多半是硬件接线问题。4.2 花屏花屏和程序的关联度更高一些。我遇到的花屏主要有三种情况第一种是接线松动。杜邦线接触不良导致某几位数据传输错误显示出来就是花屏带噪点偶尔还有横纹。这种情况下把杜邦线重新插紧或者换成排线焊接一般就能解决。第二种是SCK速度过快。虽然软件SPI的速度不算高但如果你的GPIO翻转速度很快而模块上的走线又比较长信号完整性就会出问题。可以试着在字节发送函数里加几个延时降低SCK频率看花屏是否消失。我在调试一块接了很多杜邦线的屏时就遇到这种情况速度降下来就好了。第三种是窗口坐标设置错乱。如果填充函数传递的坐标值大于屏幕的最大分辨率屏幕会接收到超出范围的地址产生显示错乱。这种问题只要仔细检查传入参数就能发现。4.3 颜色不对颜色不对通常有两种表现一种是颜色完全混乱红的变蓝、蓝的变红另一种是颜色偏色看起来不对但说不清哪里不对。第一种情况是RGB565字节序问题。发送颜色数据时高字节在前低字节在后这个顺序错了颜色就会完全错乱。检查color 8和color 0xFF的发送顺序。第二种情况是屏幕本身的RGB/BGR顺序问题。ST7735S屏的出厂设置可能跟代码里写的相反这时候需要修改初始化序列里0x36命令的数据值。比如你设置的是0x00但颜色反了改成0x08就能恢复正常。同理横竖屏方向切换也是通过修改这个寄存器的值来实现。4.4 显示方向不对屏幕显示的内容是横着的或者反着的别以为屏幕坏了这是0x36寄存器里的方向控制位在起作用。0x36寄存器里MY位控制行扫描方向MX位控制列扫描方向MV位控制行列是否交换。通过这三个位的组合可以实现四种显示方向MADCTL值效果适用场景0x00竖屏从左上角开始默认竖屏显示0xC0横屏MX和MV置位横屏显示0xA0另外方向的横屏根据物理安装方向调整0x08竖屏但RGB/BGR对调解决颜色反的问题实际调试时我一般写一个测试函数循环切换这几个值每次延时1秒看哪个方向符合需求就固定下来。这比每次改代码烧录要高效得多。4.5 JTAG复用踩坑这个坑是很多人的噩梦。STM32F103的PB3、PB4、PA15这三个引脚默认被JTAG占用。如果你把屏幕的控制线接到了这几个引脚上GPIO配置完成后根本无法正常输出因为引脚内部还在复用为JTAG功能你写的电平根本出不去。此时必须在初始化代码中禁用JTAG保留SWDHAL库的写法是__HAL_AFIO_CLK_ENABLE(); __HAL_AFIO_REMAP_SWJ_NOJTAG();标准库的写法是GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);这两行代码执行之后PB3、PB4、PA15才能当普通GPIO用。如果你用的是PA13、PA14、PA15这几个SWD调试引脚禁用JTAG后保留了SWD程序烧录、在线调试都不受影响。我之前就因为这个问题卡了一晚上屏幕怎么都不亮最后用示波器量PB3才发现引脚电平一直在跳根本不受控制。4.6 其他坑再补充几个实际开发中容易忽略的点关于背光引脚如果你用的是需要软件控制的背光模式记得在初始化之前先把背光置高。否则屏幕可能一直是黑屏让你误以为屏幕坏了。最简单的做法是把LED脚直接接3.3V先验证显示功能再做背光控制。关于屏幕模块的接口顺序不同商家卖的1.8寸屏引脚顺序可能不一样。有的是VCC-GND-CS-RESET-DC-SDA-SCK-LED有的把LED放在最前面。如果是排针直插到开发板上的接反了不会烧但会完全不显示。这时候先别怀疑代码照着丝印重新捋一遍接线。关于RAM容量STM32F103C8T6只有20KB RAM而1.8寸屏整屏帧缓冲需要128x160x2约40KB所以不能把整屏的帧缓冲都放内存里。这也是为什么很多人做小屏项目时要么用局部窗口刷新要么外挂SPI RAM。如果你做动画或波形刷新要注意这个限制别申请一个大数组直接塞内存编译能过但运行后程序会崩溃。最后再分享一个调试小技巧如果你手头有逻辑分析仪把SCK和SDA两根线夹上发送一个固定字节比如0x55观察波形是不是1、0、1、0、1、0、1、0的排列。如果不是说明GPIO方向配置或者软件时序有问题。没有逻辑分析仪也没关系用示波器看SCK有没有方波SDA有没有数据脉冲这是排查软件SPI最直接的手段。信号在就说明底层驱动没问题问题多半在上层逻辑信号没有就从底层GPIO配置开始查。我靠这个方法很多次都是几分钟内就定位了问题所在。