十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32软件SPI驱动ST7735S TFT-LCD刷屏实战

STM32软件SPI驱动ST7735S TFT-LCD刷屏实战 手头这块1.8寸TFT-LCD用的是ST7735S控制器分辨率128x160SPI接口。我原本打算用STM32的硬件SPI直接点结果发现SPI1的引脚已经被板子上的其他外设占了重新飞线又嫌麻烦。于是干脆用软件SPI来驱动——也就是拿几个普通GPIO模拟SCK和SDA把屏幕先跑起来再说。实测下来软件SPI虽然刷全屏慢但胜在引脚随便选、接线直观、调试时能单步看波形特别适合前期验证和低速界面。这篇文章就把我从接线、写时序、初始化、画点、填色到字符显示这一整套过程拆开讲清楚顺带把白屏、花屏、偏移、颜色反了这些坑一次性说透。只要你手里有一块STM32和一块1.8寸TFT照着做就能点亮。1. 为什么一块1.8寸TFT-LCD值得用软件SPI去点1.1 模块到手先认清ST7735S与128x160的引脚定义市面上常见的1.8寸TFT模块大多数是红色PCB背面带一个SD卡座正面屏幕分辨率128x160驱动芯片是ST7735S。它和1.44寸、1.8寸、2.0寸的SPI屏基本是一个家族区别主要在分辨率、偏移量和初始化参数。模块引出的引脚通常有8到10个不同厂家丝印略有差别但功能一致。下面这张表是我手头这块的典型定义你可以对照自己的模块确认。模块引脚功能接STM32说明VCC电源正3.3V不要接5V除非模块带电平转换GND电源地GND必须和STM32共地CS片选任意GPIO低电平有效RESET/RST复位任意GPIO低电平复位也可接复位电路DC/A0/RS命令数据选择任意GPIO低命令高数据SDA/MOSI数据线任意GPIO软件SPI时就是普通输出脚SCK/SCL时钟线任意GPIO软件SPI时就是普通输出脚LED/BLK背光3.3V或PWM串联限流电阻或直接3.3V这里最容易忽略的是DC引脚。很多人第一次点屏把SDA和SCK接对了CS也拉低了但屏幕就是不亮或者花屏最后发现DC没接。DC是ST7735S区分“命令”和“数据”的唯一信号DC为低时SPI上传输的字节被当成命令DC为高时被当成数据。没有它屏幕根本不知道你发的是初始化命令还是像素数据。另外背光LED通常已经内置限流电阻直接接3.3V就能亮。如果模块上没有电阻需要自己串一个几十欧到一百欧的电阻否则可能烧背光。想调亮度可以用STM32的定时器输出PWM到BLK脚频率建议1kHz以上避免肉眼看到闪烁。1.2 软件SPI和硬件SPI的取舍我为什么先选软件SPI软件SPI说白了就是用GPIO手动翻转电平来模拟SPI时序。硬件SPI则是STM32内部的SPI外设配置好之后自动产生时钟、自动移位数据。两者没有绝对的好坏关键看当前阶段要解决什么问题。我选软件SPI最直接的原因是硬件SPI的引脚被占了而软件SPI可以把SCK、SDA、CS、DC、RST全都安排到空闲GPIO上甚至可以在PCB上随便飞线。对比维度软件SPI硬件SPI引脚选择任意GPIO固定复用引脚最高速度受CPU主频和代码效率限制可达十几到几十MHzCPU占用高刷屏时CPU一直在翻转低可配合DMA调试难度低可单步、可示波器看波形需要配置外设出错时排查项多冲突风险几乎不占外设可能与其他SPI设备冲突适合场景验证、低速界面、局部刷新刷图、动画、高频刷新我通常的做法是前期用软件SPI把屏幕点亮确认初始化序列、偏移量、颜色格式都对了等界面布局固定、需要提高刷新率时再把底层写字节函数换成硬件SPI发送。这样调试阶段不会因为SPI外设配置分心后期优化也有明确目标。软件SPI的代码结构也很适合理解SPI时序你能亲眼看到每一个SCK上升沿和SDA数据位的关系。1.3 接线表与电源注意事项以STM32F103C8T6为例我选了PA2到PA5这组引脚避开JTAG占用的PA13、PA14、PA15和PB3、PB4。接线如下TFT模块STM32说明VCC3.3V模块和MCU同电源GNDGND共地CSPA4软件片选RESETPA2复位DCPA3命令/数据SDAPA7软件SPI数据SCKPA5软件SPI时钟LED3.3V背光常亮电源部分要特别注意。1.8寸TFT在背光全亮时电流可能达到几十毫安如果和STM32共用LDO要确认LDO余量。屏幕的VCC和MCU的3.3V最好来自同一路避免上电时序不一致导致初始化失败。GND必须共地软件SPI没有差分信号地线不共电平判断就会乱。如果模块背面有SD卡座暂时不用可以不接但SD卡的引脚不要短路到其他信号。还有一点STM32的GPIO输出高电平是3.3VTFT模块的逻辑电平也是3.3V直连没问题。如果你用的是5V单片机比如某些51开发板必须做电平转换否则可能损坏屏幕。软件SPI的SCK频率在前期建议先降到很低比如每个位延时几个微秒等屏幕稳定出图后再逐步提高。2. ST7735S的SPI时序与软件模拟的关键细节2.1 SPI模式0的四个边沿数据到底在哪个沿被采样SPI有四种模式由CPOL和CPHA决定。ST7735S通常使用模式0也就是CPOL0、CPHA0。CPOL0表示时钟空闲时为低电平CPHA0表示在第一个时钟边沿采样数据也就是上升沿采样。这意味着SCK从低变高的一瞬间SDA上的电平必须已经稳定。所以软件SPI的写字节顺序应该是先准备好SDA数据再把SCK拉高然后拉低SCK准备下一位。如果你把顺序写反了先拉高SCK再改SDA屏幕采到的就是上一位的数据结果就是花屏或者完全无显示。很多网上的软件SPI代码看起来五花八门但核心都是围绕这个边沿关系。模式0的时序可以记成一句话数据在上升沿被读走在下降沿改变。我们手动模拟时就是在SCK低电平期间改SDA然后拉高SCK再拉低。还有一点数据是高位先行MSB first。一个字节从bit7开始发最后发bit0。所以循环里通常用“dat 0x80”判断当前位然后左移一位。2.2 命令与数据的区分DC引脚的电平规则ST7735S没有独立的命令通道所有命令和数据都走同一根SDA线靠DC引脚区分。DC为低表示接下来发送的是命令字节DC为高表示发送的是数据字节。比如初始化时发送0x11表示退出睡眠这是命令DC必须拉低。发送颜色值0xF800这是数据DC必须拉高。在实际代码里我一般封装两个函数LCD_WriteCmd和LCD_WriteData。前者拉低DC后者拉高DC。写命令时通常一个字节就够了写数据时可能连续写很多字节。为了效率连续写数据时不要在每一字节之间反复切换DC和CS而是先拉低CS、拉高DC然后连续调用写字节函数最后拉高CS。DC引脚的电平切换要在CS有效期间完成并且要保证在SCK上升沿之前稳定。软件SPI下这很容易控制因为一切都在你的代码顺序里。硬件SPI时DC通常还是用GPIO控制所以同样要注意在发送前切换好。2.3 软件片选CS什么时候拉低、什么时候拉高CS是片选低电平有效。软件SPI下CS也是普通GPIO由代码控制。最简单的写法是每发送一个字节就拉低CS发送完再拉高。这样逻辑清晰但速度慢因为每次都要操作CS引脚。对于初始化阶段完全没问题对于刷屏建议把CS在整个连续传输期间保持低电平只在开始和结束时切换。ST7735S允许CS在命令和数据之间保持低电平只要DC正确。所以优化后的流程是拉低CS发送命令字节切换DC连续发送数据字节最后拉高CS。这样省掉大量CS翻转。注意如果你用硬件SPICS可以由硬件NSS自动控制也可以继续用GPIO软件控制后者更灵活尤其当SPI总线上挂多个设备时。还有一个小细节上电后、初始化前先把CS拉高让屏幕处于未选中状态避免上电瞬间的毛刺被当成命令。RST和DC也建议初始化为高电平或低电平的确定状态不要悬空。2.4 时序参数SCK周期、建立时间与保持时间ST7735S的SPI时钟最高可以到几十MHz具体看数据手册。软件SPI下SCK周期由代码执行时间决定。STM32F103主频72MHz直接操作寄存器时一个SCK周期大概几个机器周期实际频率可能在几MHz到十几MHz之间。前期调试建议主动加延时把SCK降到1MHz以下等屏幕稳定后再去掉延时。建立时间和保持时间在软件SPI里其实就是代码顺序。先改SDA再拉高SCK这中间自然有指令执行时间作为建立时间。拉高SCK后再拉低SCK然后改SDA这中间也有保持时间。只要不在同一个时钟周期里同时改SDA和SCK一般都能满足。如果你用HAL_GPIO_WritePin函数调用开销大反而“天然”满足了低速时序但也拖慢了刷屏速度。我的经验是先用带延时的版本点亮确认初始化序列和偏移都对然后用示波器或者逻辑分析仪看一下SCK频率再决定是否去掉延时。没有仪器也没关系逐步减少延时直到屏幕出现不稳定再退回去一点就是当前接线下的安全速度。3. 从GPIO到写字节软件SPI底层代码逐行拆解3.1 GPIO初始化推挽输出、高速、默认电平软件SPI的GPIO必须配置为推挽输出速度等级选High或Very High具体看STM32型号。推挽输出才能同时输出强高和强低开漏输出需要外部上拉不适合高速SPI。初始化时把所有相关引脚设为输出默认电平SCK低、SDA高、CS高、DC高、RST高。下面是HAL库的初始化示例。static void LCD_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Pin LCD_SCK_PIN | LCD_SDA_PIN | LCD_DC_PIN | LCD_CS_PIN | LCD_RST_PIN; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); LCD_SCK_L(); LCD_SDA_H(); LCD_CS_H(); LCD_DC_H(); LCD_RST_H(); }这里把SCK、SDA、DC、CS、RST都放在GPIOA方便用BSRR和BRR操作。如果你分散在不同端口宏定义里写清楚端口即可。速度等级选High是因为软件SPI翻转频繁低速档位可能让波形边沿变缓影响高速下的可靠性。3.2 宏定义与寄存器操作比HAL_GPIO_WritePin快在哪里HAL_GPIO_WritePin是一个函数内部有参数检查、端口判断、BSRR写入等操作调用一次可能几十个时钟周期。软件SPI每个字节要翻转SCK八次每次还要设置SDA如果用HAL函数光函数调用开销就非常可观。直接操作BSRR和BRR寄存器一条语句就能完成置位或复位速度提升非常明显。#define LCD_SCK_PORT GPIOA #define LCD_SCK_PIN GPIO_PIN_5 #define LCD_SDA_PORT GPIOA #define LCD_SDA_PIN GPIO_PIN_7 #define LCD_DC_PORT GPIOA #define LCD_DC_PIN GPIO_PIN_3 #define LCD_CS_PORT GPIOA #define LCD_CS_PIN GPIO_PIN_4 #define LCD_RST_PORT GPIOA #define LCD_RST_PIN GPIO_PIN_2 #define LCD_SCK_H() (LCD_SCK_PORT-BSRR LCD_SCK_PIN) #define LCD_SCK_L() (LCD_SCK_PORT-BRR LCD_SCK_PIN) #define LCD_SDA_H() (LCD_SDA_PORT-BSRR LCD_SDA_PIN) #define LCD_SDA_L() (LCD_SDA_PORT-BRR LCD_SDA_PIN) #define LCD_DC_H() (LCD_DC_PORT-BSRR LCD_DC_PIN) #define LCD_DC_L() (LCD_DC_PORT-BRR LCD_DC_PIN) #define LCD_CS_H() (LCD_CS_PORT-BSRR LCD_CS_PIN) #define LCD_CS_L() (LCD_CS_PORT-BRR LCD_CS_PIN) #define LCD_RST_H() (LCD_RST_PORT-BSRR LCD_RST_PIN) #define LCD_RST_L() (LCD_RST_PORT-BRR LCD_RST_PIN)BSRR是置位寄存器低16位写1置位高16位写1复位。BRR是复位寄存器写1复位。使用宏比函数调用少了一层栈操作和参数传递。如果你用的是标准库也可以用GPIO_SetBits和GPIO_ResetBits但同样不如直接操作寄存器快。注意宏里的端口和引脚要一一对应别写错端口。3.3 写字节函数的两种写法与实测差异第一种写法是面向初学者的按位判断、拉高拉低逻辑最清楚static void LCD_WriteByte_Slow(uint8_t dat) { for (uint8_t i 0; i 8; i) { if (dat 0x80) LCD_SDA_H(); else LCD_SDA_L(); LCD_SCK_H(); dat 1; LCD_SCK_L(); } }第二种是去掉分支用位运算和条件赋值或者直接用查表速度更快static inline void LCD_WriteByte(uint8_t dat) { for (uint8_t i 0; i 8; i) { if (dat 0x80) LCD_SDA_H(); else LCD_SDA_L(); LCD_SCK_H(); dat 1; LCD_SCK_L(); } }其实两者差别不大关键是把函数声明为inline或者直接写在头文件里让编译器优化。实测在STM32F103 72MHz下直接寄存器版本一个字节大约1微秒左右HAL_GPIO_WritePin版本可能超过10微秒。全屏40960字节前者约40毫秒理论值实际加上循环和窗口设置可能100到200毫秒后者可能超过1秒。这个差距在刷图片时非常明显。3.4 写命令、写数据、写16位数据的封装在写字节的基础上封装三个常用函数void LCD_WriteCmd(uint8_t cmd) { LCD_DC_L(); LCD_CS_L(); LCD_WriteByte(cmd); LCD_CS_H(); } void LCD_WriteData(uint8_t dat) { LCD_DC_H(); LCD_CS_L(); LCD_WriteByte(dat); LCD_CS_H(); } void LCD_WriteData16(uint16_t dat) { LCD_WriteData(dat 8); LCD_WriteData(dat 0xFF); }这是简单版本每个字节都切换CS。初始化阶段完全够用。刷屏时我会用优化版本手动拉低CS设置DC然后连续调用LCD_WriteByte最后拉高CS。注意LCD_WriteByte本身不操作CS和DC只翻转SCK和SDA。这样职责分离优化时更灵活。3.5 复位与延时上电时序不能省ST7735S对上电复位有要求。RST拉低至少10毫秒然后拉高再等待120毫秒让内部稳压和振荡器稳定。之后发送软件复位命令0x01再等120毫秒。很多白屏问题就是因为复位时间不够或者上电后立刻发初始化命令。void LCD_Reset(void) { LCD_RST_H(); HAL_Delay(10); LCD_RST_L(); HAL_Delay(20); LCD_RST_H(); HAL_Delay(120); }这个顺序是先确保RST为高再拉低20毫秒再拉高并等待120毫秒。不同模块可能略有差异但宁可多等不要少等。HAL_Delay在系统时钟配置正确的前提下才准确如果你还没调通SysTick先用简单的循环延时也可以但时间要重新估算。4. ST7735S初始化序列从上电到出图的完整命令链4.1 初始化命令表每条命令解决什么问题ST7735S的初始化看起来是一长串命令其实每条都有明确目的。下面这张表列出了常见命令及其作用理解了这些你就不用盲目抄代码。命令名称作用0x01SWRESET软件复位0x11SLPOUT退出睡眠模式0xB1FRMCTR1正常模式帧率控制0xB2FRMCTR2空闲模式帧率控制0xB3FRMCTR3部分模式帧率控制0xB4INVCTR反显控制0xC0PWCTR1电源控制10xC1PWCTR2电源控制20xC2PWCTR3电源控制30xC3PWCTR4电源控制40xC4PWCTR5电源控制50xC5VMCTR1VCOM控制0x20INVOFF关闭反显0x21INVON开启反显0x36MADCTL扫描方向控制0x3ACOLMOD像素格式0x05为16位0xE0GMCTRP1正极性伽马校正0xE1GMCTRN1负极性伽马校正0x13NORON正常显示模式0x29DISPON开启显示初始化的大致顺序是复位、退出睡眠、设置帧率、设置电源、设置VCOM、设置扫描方向、设置像素格式、设置伽马、开启显示。不同厂家的模块在电源参数和伽马上略有差异但核心命令一致。如果屏幕能亮但颜色不对优先检查COLMOD和MADCTL如果完全白屏先查复位和SLPOUT。4.2 常见红板1.8寸的初始化代码下面是我手头这块红板1.8寸128x160能稳定出图的初始化代码。注意不同批次可能有个别参数差异但大多数ST7735S模块可以直接用。void LCD_Init(void) { LCD_Reset(); LCD_WriteCmd(0x01); // SWRESET HAL_Delay(120); LCD_WriteCmd(0x11); // SLPOUT HAL_Delay(120); LCD_WriteCmd(0xB1); // FRMCTR1 LCD_WriteData(0x01); LCD_WriteData(0x2C); LCD_WriteData(0x2D); LCD_WriteCmd(0xB2); // FRMCTR2 LCD_WriteData(0x01); LCD_WriteData(0x2C); LCD_WriteData(0x2D); LCD_WriteCmd(0xB3); // FRMCTR3 LCD_WriteData(0x01); LCD_WriteData(0x2C); LCD_WriteData(0x2D); LCD_WriteData(0x01); LCD_WriteData(0x2C); LCD_WriteData(0x2D); LCD_WriteCmd(0xB4); // INVCTR LCD_WriteData(0x07); LCD_WriteCmd(0xC0); // PWCTR1 LCD_WriteData(0xA2); LCD_WriteData(0x02); LCD_WriteData(0x84); LCD_WriteCmd(0xC1); // PWCTR2 LCD_WriteData(0xC5); LCD_WriteCmd(0xC2); // PWCTR3 LCD_WriteData(0x0A); LCD_WriteData(0x00); LCD_WriteCmd(0xC3); // PWCTR4 LCD_WriteData(0x8A); LCD_WriteData(0x2A); LCD_WriteCmd(0xC4); // PWCTR5 LCD_WriteData(0x8A); LCD_WriteData(0xEE); LCD_WriteCmd(0xC5); // VMCTR1 LCD_WriteData(0x0E); LCD_WriteCmd(0x20); // INVOFF LCD_WriteCmd(0x36); // MADCTL LCD_WriteData(0xC8); LCD_WriteCmd(0x3A); // COLMOD LCD_WriteData(0x05); LCD_WriteCmd(0xE0); // GMCTRP1 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); // GMCTRN1 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); }这段代码里的MADCTL值0xC8对应某种旋转方向如果你的屏幕方向不对改这个值即可。COLMOD的0x05表示16位RGB565这是最常用的像素格式。4.3 偏移量xoffset/yoffset为什么有人要加2和1ST7735S的显存是132x162但1.8寸屏幕只显示128x160所以存在行列偏移。不同厂家把屏幕贴在玻璃上的位置不同偏移量也不同。常见的1.8寸128x160红板偏移量可能是xoffset0、yoffset0但有些模块需要xoffset0、yoffset0而1.44寸128x128的模块可能需要xoffset2、yoffset1或者xoffset0、yoffset32。如果你发现图像右边或下边有黑边或者图像整体错位就是偏移量不对。解决方法是写一个测试程序先全屏填红再在四个角画不同颜色的方块然后调整xoffset和yoffset直到方块正好出现在屏幕边缘。这个调试过程比反复改初始化命令高效得多。#define LCD_X_OFFSET 0 #define LCD_Y_OFFSET 0 void LCD_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { LCD_WriteCmd(0x2A); // 列地址 LCD_WriteData((x0 LCD_X_OFFSET) 8); LCD_WriteData((x0 LCD_X_OFFSET) 0xFF); LCD_WriteData((x1 LCD_X_OFFSET) 8); LCD_WriteData((x1 LCD_X_OFFSET) 0xFF); LCD_WriteCmd(0x2B); // 行地址 LCD_WriteData((y0 LCD_Y_OFFSET) 8); LCD_WriteData((y0 LCD_Y_OFFSET) 0xFF); LCD_WriteData((y1 LCD_Y_OFFSET) 8); LCD_WriteData((y1 LCD_Y_OFFSET) 0xFF); LCD_WriteCmd(0x2C); // 开始写显存 }注意运算符优先级加偏移后再移位括号不能少。4.4 MADCTL与显示方向旋转90度、180度怎么设MADCTL命令0x36控制显存的扫描方向包括行地址顺序、列地址顺序、行列交换、垂直刷新顺序、RGB/BGR顺序等。常用值如下值方向说明0x00正常MY0, MX0, MV0, BGR00x08正常BGR颜色顺序反过来0xC0旋转180MX1, MY10xC8旋转180BGR很多红板默认0x60旋转90MX1, MV10x68旋转90BGR横屏常用0xA0旋转270MY1, MV10xA8旋转270BGR另一种横屏如果你要横屏显示通常设置0x68或0xA8竖屏显示用0xC8或0x08。设置完MADCTL后LCD_SetWindow里的x和y范围也要跟着变。比如128x160的屏幕横屏后逻辑分辨率变成160x128画点函数的坐标上限也要调整。4.5 颜色格式COLMOD与RGB565的字节顺序COLMOD命令0x3A设置像素格式0x05表示16位每像素也就是RGB565。一个像素占两个字节高字节在前低字节在后。比如红色0xF800先发0xF8再发0x00绿色0x07E0先发0x07再发0xE0蓝色0x001F先发0x00再发0x1F。#define COLOR_RED 0xF800 #define COLOR_GREEN 0x07E0 #define COLOR_BLUE 0x001F #define COLOR_WHITE 0xFFFF #define COLOR_BLACK 0x0000 #define COLOR_YELLOW 0xFFE0 #define COLOR_CYAN 0x07FF #define COLOR_MAGENTA 0xF81F如果显示的颜色和预期是反的比如红色显示成蓝色通常是RGB和BGR顺序反了。改MADCTL里的BGR位或者交换发送的两个字节顺序。注意交换字节顺序会改变所有颜色不如改BGR位来得干净。5. 画点、填充与字符把驱动变成能用的显示层5.1 设置窗口命令0x2A/0x2B/0x2CST7735S画图不是直接指定坐标而是先设置一个矩形窗口然后连续写入数据数据会自动填充窗口内的像素。0x2A设置列地址范围0x2B设置行地址范围0x2C开始写显存。窗口设置好后每写入两个字节列地址自动增加到边界后换行。这个机制让填充矩形非常高效。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); }画点是最基本的操作但逐个画点速度很慢。如果画一条线最好先设置整条线的窗口然后连续发送颜色数据。比如画水平线void LCD_DrawHLine(uint16_t x, uint16_t y, uint16_t len, uint16_t color) { LCD_SetWindow(x, y, x len - 1, y); LCD_CS_L(); LCD_DC_H(); while (len--) { LCD_WriteByte(color 8); LCD_WriteByte(color 0xFF); } LCD_CS_H(); }这样比循环调用LCD_DrawPoint快很多因为窗口只设置一次CS只切换两次。5.2 画点与填充矩形连续写时的CS优化填充矩形是界面开发中最常用的操作比如清屏、画背景、画按钮。优化思路和画线一样设置一次窗口然后连续写数据。void LCD_Fill(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) { uint32_t size (uint32_t)(x1 - x0 1) * (y1 - y0 1); LCD_SetWindow(x0, y0, x1, y1); LCD_CS_L(); LCD_DC_H(); while (size--) { LCD_WriteByte(color 8); LCD_WriteByte(color 0xFF); } LCD_CS_H(); }注意LCD_SetWindow最后发送了0x2C命令此时CS被拉高。然后我们重新拉低CS设置DC为高开始连续写数据。这样避免了每写一个像素都切换CS和DC。实测这个优化对填充速度提升很明显尤其是全屏清屏。清屏可以调用LCD_Fill(0, 0, 127, 159, COLOR_BLACK)。如果你用了横屏坐标范围要相应调整。5.3 ASCII与汉字取模PCtoLCD2002的参数怎么选显示字符需要字库。ASCII字符常用8x16或6x12点阵汉字常用16x16点阵。取模软件我一般用PCtoLCD2002设置如下点阵格式阴码取模方式逐列式取模走向顺向高位在前输出数制十六进制自定义格式C51格式这样生成的数组每个字节代表一列8个像素高位在上。显示时按列读取然后逐位画点。比如8x16的ASCII字符每个字符16个字节每字节一列。显示函数逻辑如下void LCD_ShowChar(uint16_t x, uint16_t y, char ch, uint16_t color, uint16_t bg) { uint8_t i, j, temp; for (i 0; i 16; i) { temp ascii_8x16[ch - ][i]; for (j 0; j 8; j) { if (temp (0x80 j)) LCD_DrawPoint(x j, y i, color); else LCD_DrawPoint(x j, y i, bg); } } }这个版本逐点画速度慢适合少量字符。如果要显示大量文字建议先设置字符矩形窗口然后按行组合字节连续写入。汉字16x16的取模类似每个字32字节每行两个字节。5.4 显示字符串与变量的常用封装显示字符串就是循环调用LCD_ShowChar注意字符宽度和换行。显示变量可以先格式化到缓冲区再调用显示字符串。void LCD_ShowString(uint16_t x, uint16_t y, const char *str, uint16_t color, uint16_t bg) { while (*str) { LCD_ShowChar(x, y, *str, color, bg); x 8; if (x 8 128) { x 0; y 16; } str; } }显示数字void LCD_ShowNum(uint16_t x, uint16_t y, int32_t num, uint16_t color, uint16_t bg) { char buf[12]; sprintf(buf, %ld, (long)num); LCD_ShowString(x, y, buf, color, bg); }sprintf会占用一些Flash和栈空间资源紧张的芯片可以用自己写的整数转字符串函数。显示浮点数时先转成整数和小数两部分避免sprintf的浮点支持带来额外体积。5.5 图片显示Image2Lcd的导出设置显示图片需要先把图片转成点阵数组。常用工具是Image2Lcd设置如下输出灰度16位真彩色最大宽度和高度128和160扫描方式水平扫描字节顺序高位在前包含图像头不包含导出的数组每两个字节一个像素高字节在前和RGB565一致。显示函数void LCD_ShowPicture(uint16_t x, uint16_t y, uint16_t w, uint16_t h, const uint8_t *pic) { LCD_SetWindow(x, y, x w - 1, y h - 1); LCD_CS_L(); LCD_DC_H(); for (uint32_t i 0; i (uint32_t)w * h; i) { LCD_WriteByte(pic[2 * i]); LCD_WriteByte(pic[2 * i 1]); } LCD_CS_H(); }一张128x160的图片数组大小是40960字节。如果放在STM32F103C8T6的Flash里会占用不少空间建议压缩或者放在外部Flash。显示时如果软件SPI速度慢可以看到图像从上到下逐行刷出来这是正常现象。6. 刷屏速度实测软件SPI的性能边界与提速办法6.1 全屏刷新到底要多久按字节估算全屏128x160等于20480个像素每个像素16位共40960字节。每个字节8个SCK周期共327680个SCK周期。如果软件SPI每个SCK周期耗时0.1微秒那么总时间约32.8毫秒如果每个周期0.5微秒总时间约164毫秒。实际代码中还有循环开销、窗口设置、CS切换所以软件SPI全屏刷新通常在100到300毫秒之间。这个速度对于静态界面够了但刷动画、滚动文字就会明显卡顿。我实测过HAL_GPIO_WritePin版本全屏清屏大约1.2秒慢到怀疑人生。换成直接操作BSRR/BRR后降到200毫秒左右。再改成硬件SPI 18MHz全屏约20毫秒。所以软件SPI的定位很明确验证和低速显示可以高速刷图必须换硬件SPI。6.2 直接操作BSRR/BRR省掉函数调用提速的第一招就是前面说的用宏直接操作BSRR和BRR。第二招是减少循环内的分支。比如把判断当前位是高还是低改成查表或者用位运算技巧。第三招是把LCD_WriteByte声明为inline让编译器把它内联到调用处省掉函数调用和返回。static inline void LCD_WriteByte(uint8_t dat) { for (uint8_t i 0; i 8; i) { if (dat 0x80) LCD_SDA_H(); else LCD_SDA_L(); LCD_SCK_H(); dat 1; LCD_SCK_L(); } }如果编译器支持可以加__attribute__((always_inline))或者用宏完全展开。但完全展开会增大代码体积需要权衡。我的经验是先把功能跑通再根据实际需要优化不要一上来就写得很晦涩。6.3 局部刷新与脏矩形不必要不全屏刷界面开发中真正需要刷新的区域往往很小。比如只更新一个温度数值没必要全屏重绘。可以记录哪些区域发生了变化只对这些区域调用LCD_SetWindow和写数据这就是脏矩形思想。软件SPI下局部刷新能把刷新时间从几百毫秒降到几毫秒体验提升巨大。实现上可以给每个UI元素记录上一次的位置和内容变化时只重绘该元素。或者简单一点把屏幕分成几个固定区域比如标题栏、数据区、按钮区哪个区域变了就重绘哪个区域。这样代码不复杂但效果很明显。6.4 什么时候必须换硬件SPI或DMA如果你需要显示动态波形、视频、流畅动画或者界面刷新率要求超过10帧每秒软件SPI基本不够用。这时候应该换硬件SPI最好再配上DMA。硬件SPI的引脚是固定的需要提前规划。STM32F103的SPI1在PA5、PA6、PA7SPI2在PB13、PB14、PB15。如果这些引脚被占用可以考虑重映射或者换芯片型号。换硬件SPI后DC和CS仍然可以用GPIO控制只有SCK和SDA交给SPI外设。发送函数改成HAL_SPI_Transmit一次发送一个字节或一串字节。配合DMA可以在后台刷屏CPU去处理其他任务。这个迁移过程不难但要注意SPI模式、数据宽度和字节顺序下一章会详细说。7. 白屏、花屏、偏移、颜色反了实机排查链路7.1 白屏与背光先确认屏有没有亮白屏是最常见的问题但原因可能完全不同。第一步先看背光有没有亮。如果背光不亮屏幕一片黑不是白屏。背光不亮查LED/BLK引脚电压、限流电阻、供电。如果背光亮但屏幕全白说明屏幕没有收到有效初始化命令或者复位失败。排查顺序先测VCC是否3.3V再测RST在上电后是否有低电平脉冲再测CS、DC、SCK、SDA是否有波形。如果SCK完全没有波形说明代码没跑到发送函数或者GPIO配置错了。如果有波形但仍然白屏重点查初始化序列里的SLPOUT和DISPON有没有发延时够不够。7.2 花屏与初始化失败从复位和SPI速率查起花屏通常是数据错位或者初始化不完整。先降低软件SPI速度加一些延时看是否稳定。如果降速后正常说明之前SCK太快超出了模块的时序要求。如果降速后仍然花屏检查接线是否松动尤其是SDA和SCK有没有接反。软件SPI下接反不会烧但会花屏。另一个常见原因是初始化序列不完整比如缺少电源控制命令或者伽马校正。不同厂家的ST7735S模块对这些参数敏感程度不同最好用厂家提供的初始化代码。如果找不到可以多试几个版本的初始化序列找到能稳定显示的那个。7.3 图像偏移与显示不全窗口偏移与分辨率图像偏移表现为左边或上边有黑边或者图像显示不完整。这是窗口偏移量不对。前面说过调整LCD_X_OFFSET和LCD_Y_OFFSET用全屏填充和边角方块来定位。注意如果MADCTL设置了旋转偏移方向也会变需要重新调整。还有一种情况是显示不全比如只显示了前128行中的一部分。检查LCD_SetWindow里的y1是否超过159以及MADCTL的MV位是否把行列交换了。横屏时逻辑分辨率是160x128如果代码里还按128x160设置窗口就会出错。7.4 颜色颠倒与反色BGR位和INVON颜色颠倒通常有两种一种是RGB顺序反了红色变蓝色绿色不变另一种是整体反色黑变白。前者改MADCTL的BGR位或者交换发送字节顺序后者改INVON/INVOFF命令。注意反色和颜色顺序是两回事不要混在一起调。如果只有部分颜色不对比如红色正常但蓝色偏紫可能是伽马校正参数不匹配。可以先注释掉伽马校正命令用默认值显示看颜色是否正常。如果正常再逐步加回伽马参数。7.5 常见问题速查表现象优先检查解决方法白屏背光、复位、电源测BLK电压、RST时序、3.3V供电花屏接线、SPI速率降低SCK速度、检查SDA/SCK是否接反偏移窗口偏移调整xoffset/yoffset颜色反BGR/INV改MADCTL的BGR位或INVON/INVOFF方向不对MADCTL改MX/MY/MV组合显示条纹电源、刷新加滤波电容、降低刷新率汉字乱码取模设置逐列式、顺向、高位在前刷屏慢软件SPI硬件SPI、DMA、局部刷新上电偶尔不亮复位时序增加复位低电平时间和上电延时8. 从软件SPI迁移到硬件SPI引脚、配置与代码改动8.1 硬件SPI的引脚复用与时钟配置以STM32F103的SPI1为例SCK在PA5MISO在PA6MOSI在PA7。TFT只用MOSIMISO不用可以不配置。配置步骤开启GPIOA和SPI1时钟GPIOA5和PA7设为复用推挽输出SPI1设为主机模式、8位数据、CPOL0、CPHA0、MSB first、分频2或4。分频2对应36MHz分频4对应18MHz先放慢一点稳定后再提速。void LCD_SPI_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; SPI_HandleTypeDef hspi1; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_SPI1_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_5 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; hspi1.Init.Direction SPI_DIRECTION_1LINE; hspi1.Init.DataSize SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity SPI_POLARITY_LOW; hspi1.Init.CLKPhase SPI_PHASE_1EDGE; hspi1.Init.NSS SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_4; hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; hspi1.Init.TIMode SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation SPI_CRCCALCULATION_DISABLE; HAL_SPI_Init(hspi1); __HAL_SPI_ENABLE(hspi1); }实际项目中我会把hspi1设为全局变量方便其他地方调用。注意SPI_DIRECTION_1LINE表示只用MOSI节省一根线。8.2 软件片选与硬件片选的区别硬件SPI的NSS可以配置为软件NSS也就是由代码控制CS引脚也可以是硬件NSS由SPI外设自动控制。我建议用软件NSS因为TFT的CS需要在命令和数据之间保持低电平硬件NSS在每字节后可能自动拉高反而不方便。软件NSS就是把CS当普通GPIO和软件SPI时一样控制。DC仍然用GPIO控制和软件SPI一样。发送命令时拉低DC调用HAL_SPI_Transmit发送一个字节发送数据时拉高DC调用HAL_SPI_Transmit发送数据。CS在连续传输期间保持低电平。8.3 迁移时最容易忽略的SPI模式与数据宽度最容易忽略的是SPI模式和软件SPI不一致。软件SPI用的是模式0硬件SPI也必须配置成CPOL0、CPHA0否则数据错位。还有一个坑是数据宽度STM32 SPI可以配成8位或16位。如果配成16位发送一个16位数据会自动产生16个时钟适合刷像素但初始化命令是8位需要切换数据宽度比较麻烦。我建议统一用8位自己拆分高低字节代码简单兼容性好。另外硬件SPI发送函数是阻塞的HAL_SPI_Transmit会等到发送完成。如果刷大图CPU会被占用。要真正释放CPU需要配合DMA。DMA配置稍微复杂但效果明显尤其是刷图片和动画。8.4 迁移后的性能提升与实测对比我做过一个简单测试同样填充全屏红色软件SPI直接寄存器版本约200毫秒硬件SPI 18MHz约20毫秒提升接近10倍。如果开DMACPU占用几乎为零刷屏期间还能处理按键和串口。对于需要实时显示波形或者刷新率要求高的场景硬件SPI是必须的。不过硬件SPI也有代价引脚固定可能与其他外设冲突配置项多调试时如果不出图排查起来比软件SPI麻烦。所以我的习惯是保留软件SPI的底层函数作为备用在硬件SPI调不通时切回软件SPI验证屏幕本身没问题这样能快速定位是硬件SPI配置问题还是屏幕问题。最后再分享一个小技巧如果你在软件SPI和硬件SPI之间切换可以把写命令、写数据、设置窗口这些上层函数保持不变只替换底层的写字节实现。这样上层显示代码完全不用改迁移成本很低。我平时会把底层SPI操作单独放在一个文件里用宏选择软件或硬件实现调试和量产都很方便。
返回列表