ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

软件SPI驱动ST7735S 1.8寸TFT-LCD:从GPIO模拟到稳定刷新的完整实战

软件SPI驱动ST7735S 1.8寸TFT-LCD:从GPIO模拟到稳定刷新的完整实战 今年年初我接了个小项目主控板选型时为了控制BOM成本没有单独拉硬件SPI引脚出来做屏接口结果样机阶段屏幕点亮阶段就卡了整整两天。最后干脆把所有SPI时序全部用GPIO翻转模拟来解决过程中踩了一堆关系到信号完整性和时序匹配的坑也把软件SPI驱动1.8寸TFT-LCD这套流程彻底吃透了。今天把完整的思路、代码和调试经验整理出来给正在跟这块屏较劲的朋友做个参考。先说清楚适用范围。1.8寸TFT-LCD多数用的是ST7735S控制器接口支持4线SPI。这里说的“软件SPI”本质上是把SPI控制器换成普通GPIO输出用代码延时控制时钟和数据线的电平变化来模拟通信时序。它的核心优势不是性能而是引脚随便选、不受片上外设映射限制同时代码可移植性极强换任意一款单片机只要把引脚宏定义改掉就能跑。缺点也很直白速度上限低数据吞吐量比硬件SPI差几个量级适合屏刷新频率要求不高、界面元素固定或刷新频率每秒5到10次的场景。我这次做的就是温控面板的静态状态显示对刷新率没有苛刻要求软件SPI完全够用。1. 为什么选软件SPI引脚自由背后的工程逻辑很多初学者和半路转嵌入式的朋友会有个惯性思维单片机自带SPI控制器为什么还要自己用GPIO模拟这不是多此一举吗我刚开始也这么想直到实际做过一轮选型评估才明白这个选择背后有明确的工程考量。首要原因就是引脚自由。以STM32F103C8T6为例硬件SPI1默认引脚是PA5、PA6、PA7SPI2是PB13、PB14、PB15。如果某个项目里这些引脚已经被USART、定时器PWM、ADC采样或者外部中断占用你又不想重新布线改PCB那硬件SPI就等于废了。我踩过的最典型场景是屏幕数据线、控制线、背光引脚一共需要7个GPIO但硬件SPI引脚恰好有一半被温度采集芯片的I2C和蜂鸣器的PWM占用要么换封装更大的芯片要么换屏幕接口要么就用软件SPI把引脚挪到PB12、PB13、PB14这些空闲位置。做硬件的人都知道改PCB比改代码痛苦得多软件SPI直接规避了这个矛盾这是它在项目里最有价值的地方。第二个原因是代码可移植性。硬件SPI的初始化代码里要配置GPIO复用、设置SPI时钟极性和相位、调整预分频系数不同厂商的芯片库函数风格差异非常大。比如你今天用STM32明天换成GD32E230后天换成N76E003如果用的是硬件SPI底层初始化代码基本得重写但软件SPI只要保证GPIO的置高置低宏定义存在整个驱动层几乎原封不动。我做过的几个项目中软件SPI驱动的LCD代码甚至能做到跨平台直接拷贝只是把引脚宏定义替换一下而已这种迁移成本是硬件SPI没法比的。第三个原因有时候会被忽略某些项目需要把SPI引脚重新规划成其他功能做复用。比如一块板子既要驱动LCD显示又要通过SPI通信读取Flash芯片但硬件SPI引脚只有一组普通场景没法做到分时复用两个设备还各自独立片选。软件SPI就可以把LCD和Flash分别挂在不同GPIO上完全规避片选信号冲突的问题。当然软件SPI也有明显的适用边界如果屏幕刷新率要求60fps或者需要频繁播放动画效果软件SPI就完全扛不住了这时候老老实实用硬件SPI加DMA才是正解。从工程角度做个简单对比对比项硬件SPI软件SPI引脚选择自由度受片上复用映射限制任意GPIO均可通信速率上限数十MHz级别通常1到8MHz可稳定运行代码可移植性依赖厂商库跨芯片需改配置宏定义GPIO跨平台极强CPU占用率极低可配合DMA高MCU全程参与时序适用场景高频刷新、大量数据传输静态界面、低频更新的状态屏排错难度信号完整性为主还需关注延时和时序逻辑我最终的结论很简单如果你的项目屏幕数据量不大、刷新率要求不高而且引脚布局紧张软件SPI是一个性价比极高的方案。尤其在做四层板或双层板走线时不限制引脚意味着layout走线可以少打很多过孔这在硬件上省下的成本和时间非常可观。2. ST7735S初始化序列每个命令为什么必须有1.8寸TFT-LCD的驱动核心是ST7735S控制器屏幕显示效果是否正常九成由初始化序列决定。市面上同尺寸屏幕模组虽然外观一样但不同产家的玻璃基板偏置电压、像素格式、Gamma曲线参数都可能不同所以初始化代码并不能盲目从网上复制一个就完事必须逐条理解每一条命令是在干什么出了问题才知道往哪儿排查。这里给出我调试通过并稳定量产的初始化序列代码是标准C语言风格static void ST7735S_Init(void) { LCD_RES_Clr(); delay_ms(50); LCD_RES_Set(); delay_ms(120); /* 1. 软件复位 */ LCD_Write_Cmd(0x01); delay_ms(150); /* 2. 关闭睡眠模式 */ LCD_Write_Cmd(0x11); delay_ms(200); /* 3. 设置像素格式为16位色 */ LCD_Write_Cmd(0x3A); LCD_Write_Data(0x05); /* 4. 设置显示方向为竖屏 */ LCD_Write_Cmd(0x36); LCD_Write_Data(0x00); /* 5. 设置伽马曲线 */ LCD_Write_Cmd(0x26); LCD_Write_Data(0x01); /* 6. 打开显示 */ LCD_Write_Cmd(0x29); delay_ms(20); }很多朋友第一次写初始化代码时会直接从网上下载一段看起来能点亮屏幕的代码跑通了就接着写绘图函数一旦遇到白屏、花屏、颜色偏移就完全摸不着头脑。本质上是因为不理解命令含义。我们来逐条拆解这个序列背后的门道第一条0x01是软复位。它让ST7735S内部所有寄存器恢复默认值保证后续命令在干净状态上执行。上电后硬件复位引脚拉低再拉高的过程属于硬件复位但很多情况下主控已经跑了程序、屏也已经上电此时如果不做软件复位屏的状态就是上次掉电时残留的直接往下初始化可能出现不可预知的显示异常。我在批量测试时发现少发0x01命令的板子有约1%的概率出现半屏花屏补上后消失概率极小但排查成本极高。第二条0x11退出睡眠模式。ST7735S上电后默认是Sleep In状态此时屏幕不工作、内部振荡器关闭。如果跳过这条命令直接写0x29开显示屏幕大概率还是一团黑。0x11后面必须跟足够延时官方手册要求120ms我习惯放到200ms因为部分稳压模块启动偏慢延长这段时间能避免屏幕刚被唤醒就收到后续命令而响应失败。第三条0x3A配合数据0x05设置RGB接口颜色格式为16位色。ST7735S支持12位、16位、18位等多种像素格式。选择16位是因为STM32整字处理效率最高一个uint16_t类型变量刚好对应一个像素点内存占用也可控。如果设置成18位后续发送像素数据时就要做字节对齐处理代码复杂度高不说传输量还增大了三分之一。第四条0x36是行列扫描方向控制。这个寄存器的实质是决定GRAM中每个像素坐标和屏幕物理位置之间的映射关系。工程里常见问题是屏幕画出来的字符是镜像的或者上下颠倒很多人第一反应是改字库其实只要改0x36寄存器的值就能解决。它内部有MY、MX、MV三个方向位和一个RGB位各位组合起来决定扫描方向。我常用的几种配置场景寄存器值效果竖屏接口朝上0x00正常显示横屏0x6090度旋转后正常显示竖屏镜像0xC0左右镜像上下颠倒0x80垂直翻转第五条0x26设置伽马曲线。ST7735S内置了不同厂家预设的Gamma校正曲线0x01是官方推荐的标准曲线颜色饱和度和对比度最均衡。如果读取模组的规格书有些厂商会要求指定特定的0x26参数配合0xE0和0xE1寄存器做正负Gamma微调。我建议直接用官方推荐曲线除非遇到颜色严重偏色或过渡不平滑的情况再去做定制调节。最后一条0x29打开显示。到这里屏幕才会真正把GRAM内容刷新出来上电后如果始终是白屏很多人的第一个怀疑点是引脚接反但这只是可能性之一初始化序列没走到0x29同样会白屏。顺序上要注意0x11必须在0x29之前且中间延时要充分否则显示会有概率打不开。初始化序列看起来短但每一条命令的缺失都会造成不同的异常表现。调试中如果发现白屏、黑屏、花屏、颜色异常先用逻辑分析仪抓一下命令发送过程中是否有掉包确认时序正确后再逐条对比初始化命令是否存在遗漏。这个思路比盲改寄存器省时得多。3. 比特级实现细节时钟沿、数据建立时间与延时控制软件SPI的核心挑战在于用代码精确控制SCL和SDA两个引脚的电平变化来模拟标准SPI时序。理解这部分前你先要在脑袋里固化一个认知SPI通信中时钟极性和时钟相位决定了数据在哪个边沿被采样。ST7735S支持Mode0和Mode3两种模式我习惯用Mode0它的定义是空闲时SCL为低电平数据在SCL上升沿被采样下降沿时数据变化。对应到代码就是先把SDA准备好然后拉高SCL这时控制器把SDA上的电平读走再拉低SCL准备下一个bit。来写具体的代码实现。先定义引脚宏方便后续换板子#define LCD_SCL_PIN GPIO_PIN_13 #define LCD_SDA_PIN GPIO_PIN_14 #define LCD_CS_PIN GPIO_PIN_15 #define LCD_DC_PIN GPIO_PIN_12 #define LCD_RST_PIN GPIO_PIN_11 #define LCD_SCL_Set() HAL_GPIO_WritePin(GPIOB, LCD_SCL_PIN, GPIO_PIN_SET) #define LCD_SCL_Clr() HAL_GPIO_WritePin(GPIOB, LCD_SCL_PIN, GPIO_PIN_RESET) #define LCD_SDA_Set() HAL_GPIO_WritePin(GPIOB, LCD_SDA_PIN, GPIO_PIN_SET) #define LCD_SDA_Clr() HAL_GPIO_WritePin(GPIOB, LCD_SDA_PIN, GPIO_PIN_RESET)然后是最核心的字节发送函数。这里有一个新手经常忽略的细节ST7735S的数据发送顺序是高位在前MSB First也就是一个字节的bit7先发送最后发送bit0。如果你写成了低位在前屏幕显示的内容会完全错乱颜色也会一团糟。static void LCD_SPI_WriteByte(uint8_t data) { for (uint8_t i 0; i 8; i) { if (data 0x80) LCD_SDA_Set(); else LCD_SDA_Clr(); data 1; LCD_SCL_Set(); LCD_SCL_Clr(); } }这段代码背后的时序逻辑是先根据当前bit的值控制SDA电平然后拉高SCL让从设备在上升沿采样这个电平随即拉低SCL。对于大多数运行在72MHz主频下的STM32来说GPIO翻转本身需要几个机器周期天然形成几百纳秒级别的脉冲宽度已经满足ST7735S的时序要求其最小SCL高电平时间约几十纳秒所以不需要额外插入delay。但如果你的主控主频很高比如STM32F412跑到100MHz或者代码开了编译器O3优化GPIO翻转速度过快可能导致SCL高电平时间小于datasheet要求的最小值这时就要在SCL_Set()和SCL_Clr()之间插入几个空指令或delay_us(1)来人为增加时钟周期。这里分享一个我踩过坑之后的经验字节发送函数是被调用频率最高的代码路径每发一个字节会进入循环8次。如果在这个函数里加入延时函数调用会大幅拖慢整体刷新速度。正确做法是把延时控制放在时钟翻转之间而不是循环外层。我的优化思路是先用逻辑分析仪实测波形确认SCL最低时间和最高时间都满足手册要求然后逐步减小延时直到波形临界失真的前一个值这个值就是理论最优解。接下来是命令和数据的区分发送。ST7735S的DC引脚用来标识当前发送的是命令还是数据DC为低电平时SPI总线上的字节被解释为命令DC为高电平时被解释为数据。封装如下static void LCD_Write_Cmd(uint8_t cmd) { LCD_DC_Clr(); LCD_CS_Clr(); LCD_SPI_WriteByte(cmd); LCD_CS_Set(); } static void LCD_Write_Data(uint8_t data) { LCD_DC_Set(); LCD_CS_Clr(); LCD_SPI_WriteByte(data); LCD_CS_Set(); }片选信号CS在通信前拉低、通信后拉高。要注意的是每次命令或数据发送中只允许拉低一次CS不能拆成半个字节再重新拉高否则从设备内部的状态机可能会错乱。有些屏的控制器内部带有FIFO允许命令和连续数据块持续传输时CS一直保持低电平这能减少GPIO翻转次数提升一点效率实际写代码时可以按需优化但初学者建议保持最简结构。为了降低CPU开销我之前还写过一个优化版本用查表法替代逐位判断static const uint8_t bit_mask[8] {0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01}; static void LCD_SPI_WriteByte_Fast(uint8_t data) { for (uint8_t i 0; i 8; i) { if (data bit_mask[i]) LCD_SDA_Set(); else LCD_SDA_Clr(); LCD_SCL_Set(); LCD_SCL_Clr(); } }这个版本比shift mask多了一次数组访问实际测试下来并没有明显提速。真正能带来明显提升的是把SDA赋值改成直接操作BSRR寄存器比如#define LCD_SDA_High() GPIOB-BSRR GPIO_PIN_14 #define LCD_SDA_Low() GPIOB-BRR GPIO_PIN_14BSRR和BRR这两个寄存器是STM32的硬件置位/清零寄存器写1即可实现原子性电平操作无需读改写。相比之下HAL库的HAL_GPIO_WritePin内部要做参数合法性判断一次调用耗费数十个机器周期在每字节8个bit的循环中会被放大8倍。如果你追求更高刷新率可以直接操作寄存器不必纠结是否“够底层”效率才是关键。4. 像素与窗口操作GRAM寻址机制和颜色填充算法初始化完毕、底层字节发送函数就绪之后我们需要考虑的是如何把一张图片或者一行文字显示到屏幕上。ST7735S内部有一块132x162的GRAM但对于1.8寸屏幕实际只显示128x160的区域所以坐标范围是X从0到127Y从0到159。其显示机制是在GRAM中维护一个“当前操作窗口”所有写入的数据按行优先顺序自动填充到窗口内对应位置填充完毕后光标自动移到下一个像素坐标。因此写显示函数前必须先掌握两个API设置地址窗口和写像素点。地址窗口的作用是告诉控制器“接下来哪些像素会被操作”写像素点的作用是“往窗口内按顺序塞颜色数据”。static void LCD_Set_Address(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { LCD_Write_Cmd(0x2A); LCD_Write_Data(x1 8); LCD_Write_Data(x1 0xFF); LCD_Write_Data(x2 8); LCD_Write_Data(x2 0xFF); LCD_Write_Cmd(0x2B); LCD_Write_Data(y1 8); LCD_Write_Data(y1 0xFF); LCD_Write_Data(y2 8); LCD_Write_Data(y2 0xFF); LCD_Write_Cmd(0x2C); }命令0x2A设置列地址范围0x2B设置行地址范围0x2C是存储器写命令。发送完这三个命令后后面的每一个数据字节都会被按顺序写入窗口内的GRAM然后光标自动递增。基于这个机制最简单的画点函数是这样void LCD_Draw_Pixel(uint16_t x, uint16_t y, uint16_t color) { LCD_Set_Address(x, y, x, y); LCD_Write_Data(color 8); LCD_Write_Data(color 0xFF); }颜色值以RGB565格式组织高字节为R[4:0]和G[5:3]低字节为G[2:0]和B[4:0]。一片大区域填充颜色的高效做法是先设置好整个区域的窗口再循环写入颜色数据每写两个字节代表一个像素。如果逐像素地调用LCD_Draw_Pixel每次都要重新发一遍0x2A、0x2B、0x2C命令额外开销非常大。实测下来同样填充一个100x100的矩形逐像素画点耗时约380ms用窗口填充只需要约120ms。窗口填充函数的优化重点是循环内直接调用字节发送函数而不是再次包装void LCD_Fill_Rect(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color) { uint8_t high color 8; uint8_t low color 0xFF; uint32_t pixel_count (x2 - x1 1) * (y2 - y1 1); LCD_Set_Address(x1, y1, x2, y2); for (uint32_t i 0; i pixel_count; i) { LCD_Write_Data(high); LCD_Write_Data(low); } }需要特别注意的是颜色数据必须保持“高字节在前”。如果你颠倒顺序屏幕上的颜色会发生红蓝通道互换之类的错乱。我在调试字体显示时曾遇到一个问题英文字母底色和前景色完全相反排查到最后才发现是填充矩形时把颜色高低字节写反了。如果要在屏幕上显示字符通常是先做一个简单的字模数组每个字符用8x16或16x32的像素点阵表示。显示一个字符的核心逻辑是遍历字模的每一个bitbit为1时用前景色画点bit为0时用背景色画点。一个比较高效的优化是利用窗口机制把单个字符的显示区域一次性设置为8x16的窗口然后按位填充颜色而不是每个像素都调用一次LCD_Set_Address。这样代码虽然看起来多几行但执行效率成倍提升。void LCD_Show_Char(uint16_t x, uint16_t y, char ch, uint16_t fg_color, uint16_t bg_color) { uint8_t i, j; const uint8_t *p (const uint8_t *)font8x16[ch - ][0]; LCD_Set_Address(x, y, x 7, y 15); for (i 0; i 16; i) { uint8_t line p[i]; for (j 0; j 8; j) { if (line 0x80) { LCD_Write_Data(fg_color 8); LCD_Write_Data(fg_color 0xFF); } else { LCD_Write_Data(bg_color 8); LCD_Write_Data(bg_color 0xFF); } line 1; } } }字体数组中每一行代表一个水平行8位数据对应一行的8个像素点bit7是最左列。这种窗口内先循环列、再循环行的填充顺序与ST7735S的GRAM自增方向保持一致可以最大限度避免光标跳变。5. 排查链路复盘白屏、花屏、颜色错乱的定位方法理论再完整实战中总会遇到几个让人抓狂的异常现象。这一节把我调试软件SPI驱动这块屏时实际遇到过的三类高频问题和完整排查链路完整记录下来希望能省下读者自己摸索的时间。5.1 白屏从电压、复位到时序的顺序检查白屏是最常见的初始状态看到白屏时先别怀疑代码按照下面的链路一步步查第一步查供电和背光。用万用表量屏的VCC引脚和背光引脚电压是否正常背光引脚如果是独立控制必须确保它被拉高才有光。很多模组的背光电压和逻辑电压是分开的逻辑电压正常但背光没开屏幕看起来是暗白容易误判成背光硬件损坏。第二步查复位时序。ST7735S的复位脚拉低到拉高之间至少要保证几十毫秒的低电平时间如果复位引脚悬空或者RC复位时间不够屏内部状态机就停在不稳定状态。我建议在初始化函数开头加上至少50ms的低电平复位脉冲有些比较“娇气”的模组需要100ms以上。第三步查DC引脚状态。如果DC引脚在初始化期间被错误拉高后面发送的命令全会被当成数据显示屏幕上会闪出杂乱的彩色条纹而不是白屏。可以用万用表测量代码执行到ST7735S_Init()里打断点手动确认DC引脚电平是否在命令发送阶段为低、数据发送阶段为高。第四步抓SPI波形。把逻辑分析仪探头夹在SCL和SDA引脚上抓初始化阶段的波形确认是否有完整的时钟脉冲序列。如果SCL是完全水平的直线说明GPIO配置成复用功能而非输出模式或者引脚宏定义和实际接线不一致。以上四步都确认没问题后我再回到初始化序列本身做交叉验证。白屏的另一个常见原因是缺少0x29开显示命令或者0x11之后的延时太短。经验法则如果屏幕背光亮但画面全白优先检查是否执行到开显示命令如果背光亮但画面全黑优先检查是否卡在退出睡眠命令。5.2 花屏重点怀疑时序过快和初始化顺序错乱花屏的状态是屏幕有反应但画面里有大量杂色条纹或噪点。这通常说明初始化命令收到了一部分但数据链路中某个环节不稳定。我在一次焊接样板调试中遇到的花屏现象尤其典型屏幕上有淡淡的横向条纹时有时无用手指按压屏幕排线区域时条纹明显减少。用万用表测量各个引脚的接触电阻后发现SDA引脚存在约7Ω的虚焊接触电阻。SPI属于高速数字信号虚焊造成的信号反射和延迟让数据建立时间不满足要求时钟采样时抓到的是不稳定电平。重新焊接排线后花屏彻底消失。碰到花屏现象先用放大镜检查排线焊点是最快的方法。另外软件SPI如果主频太高导致时钟高电平时间太窄也会在显示固定颜色区域时出现像素错位。ST7735S对SCL时钟信号的要求是低电平和高电平都有最小脉宽限制如果软件延时不足数据还没稳定就被采样GRAM里写入的像素值就会产生随机故障。应对方法是把字节发送循环里加两个空操作指令或者用__NOP()插入几个机器周期的延时。我的实际参数是72MHz主频下在LCD_SCL_Set()之后紧跟4个__NOP()实测显示稳定性最好。初始化顺序错误也会导致花屏。网上有不少版本的驱动代码把0x36显示方向放在0x29开显示之后设置这在部分模组上会触发内部寄存器保护机制导致显示方向异常的同时出现花屏。比较稳妥的顺序是软件复位 - 退出睡眠 - 像素格式 - 显示方向 - Gamma曲线 - 开显示。每次调整初始化参数后都要彻底断电重新初始化测试不能只复位主控因为ST7735S内部的寄存器状态可能还残留上一次的设置。5.3 颜色错乱高低字节和坐标方向的双重验证颜色错乱的具体表现是显示内容形状正常但颜色完全不对比如红色变成蓝色白色变成黄色。这种情况几乎都是RGB565字节顺序或坐标方向的问题。颜色字节顺序的排查方法非常直接单独调用LCD_Fill_Rect填一个纯红色矩形。纯红色的RGB565值是0xF800高字节0xF8低字节0x00。如果你的代码把低字节发在前面屏幕上显示的将是0x00F8这个值会被解析成蓝色加一点绿色肉眼可见颜色完全不同。手动填充几个标准色块逐一对比预期颜色能在三分钟内定位是字节顺序还是坐标映射问题。坐标方向问题通常表现为文字位置对了但内容水平翻转或垂直颠倒或者在屏幕边缘出现镜像。这时调整0x36寄存器的值即可。我给个速查表异常现象调整方向左右镜像翻转MX位0x36值的bit6上下颠倒翻转MY位0x36值的bit7旋转90度翻转MV位0x36值的bit5红蓝互换检查RGB565高低字节顺序颜色整体偏色检查0x26伽马曲线或背光色温实际项目中颜色偏色不一定都是代码问题。有一次我们发现屏幕显示的白色偏黄排查了很久才发现是背光LED色温本身就是暖色调。所以在怀疑代码之前先确认一下屏的硬件规格书里有没有标注背光色温否则会浪费很多不必要的调试时间。6. 性能优化思路如何让软件SPI跑得更稳更快软件SPI虽然天生性能上限不高但合理优化后同样有明显提升空间。我在这块屏上做过一轮系统的优化实验把全屏填充颜色从最初的约430ms优化到约210ms字符刷新速度提升了接近一倍。优化的方向主要有四个按效果排序如下6.1 直接寄存器操作替换HAL库调用这是收益最明显的优化手段没有之一。HAL库的HAL_GPIO_WritePin每次调用都要判断引脚编号合法性、做位运算、再写ODR寄存器整个流程有十几条指令。而直接操作BSRR和BRR寄存器只需要一条指令。循环发送一字节数据需要8次SDA设置和8次SCL翻转如果每条指令省下几十个周期累计效应非常可观。推荐做法是在宏定义中直接区分置高置低#define LCD_SCL_H GPIOB-BSRR GPIO_PIN_11 #define LCD_SCL_L GPIOB-BRR GPIO_PIN_11使用volatile方式定义引脚地址还能进一步减少编译优化带来的不确定性不过大部分场景宏定义就够了。6.2 预计算颜色高低字节前面填充矩形函数里已经把颜色拆成高低字节这是一个很细节但很重要的优化。如果每个像素都先执行一次color 8和color 0xFF编译器可能还会多次重复计算。把高位和低位提前计算好放进局部变量循环内只做赋值减少流水线停滞。6.3 块级窗口填充避免重复寻址每次调用LCD_Set_Address都要发送3条命令加6到8字节参数占据不少通信时间。尽量把绘图操作合并成更大块的窗口操作。例如显示一行字符时不要逐个字符发送而是把整行文字的显示区域设置成一个大窗口然后连续填充分段颜色。这样命令开销从N次降低为1次。我实际验证同屏显示8个8x16字符使用单字符独立刷新的耗时约70ms整行窗口合并刷新降到了35ms左右差异非常明显。6.4 合理选择延时策略很多软件SPI代码为了稳妥会在每个bit周期加入固定延时比如delay_us(1)这会严重拖慢速度。正确做法是先确认ST7735S的数据手册时序参数然后让逻辑分析仪实测当前波形是否满足要求再逐步去掉不必要的延时。72MHz主频下GPIO翻转本身产生的延时已经足够满足ST7735S的时序要求大多数情况下可以做到零额外延时。需要格外注意的是编译器优化级别。我在Arm GCC下将优化级别从-O0调成-O2后SCL高电平脉宽从约460ns下降到了约210ns依然在芯片允许范围内手册标称最小高电平时间通常为60ns。但如果在-O2下再启用LTO部分空指令循环会被优化掉此时脉宽进一步变窄极端情况下可能导致信号质量劣化。所以改优化级别后务必用逻辑分析仪复测一次波形不要凭经验拍板。7. 写在最后一块小屏带来的工程思维方式一个软件SPI驱动LCD的完整工程里牵涉的细节远不止“让屏幕亮起来”从时序的成像原理、寄存器的配置逻辑、信号完整性到代码执行效率每一个环节都需要系统性思考。很多初学者拿到一块屏幕第一反应是“抄一段驱动代码点亮就完事”一旦遇到异常现象就束手无策。而真正有价值的恰恰是理解底层机制之后能够根据现象快速缩小问题边界精准定位病灶。我个人的经验是凡是遇到显示异常先不要急着改代码而是拿起万用表和逻辑分析仪把供电、复位、时钟、数据的链路一层层测量一遍。这条面向硬件的调试路径往往比盲改代码快得多。做嵌入式这一行最贵的就是排错时间设备和工具的价值远不止采购成本那么简单。还有一点关于屏幕模组的小提醒不同厂家生产的1.8寸TFT屏虽然控制器都是ST7735S但玻璃面板的初始化参数可能有细微差异。批量采购时务必让供应商提供该批次屏幕匹配的初始化代码并且在来料检验阶段就抽样测试显示效果。一个好的供应商会主动提供这些信息遇到不提供的宁可换一家也不要拿生产风险去赌。软件SPI这颗“螺丝钉”看着不起眼但掌握它的细节能让你的项目在引脚规划、平台迁移和成本控制上收获巨大的灵活性。希望这篇文章能帮你少走一些弯路。如果你在实操中遇到了本文没覆盖到的现象欢迎带着波形图和数据手册来讨论这类问题往往越辩越清晰。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进