STM32 HAL库驱动TFT-LCD:硬件SPI与软件SPI实现详解
2026/7/29 4:20:00 网站建设 项目流程

1. 项目概述:为什么选择STM32 HAL库驱动TFT-LCD?

在嵌入式开发中,图形化人机界面(HMI)的需求越来越普遍,而1.8寸TFT-LCD屏幕因其小巧、色彩丰富、价格低廉,成为了许多STM32项目的首选显示模块。很多朋友拿到屏幕后,第一反应是找现成的库,比如Arduino上的Adafruit或U8g2库。但当你切换到STM32平台,特别是使用ST官方主推的HAL库时,往往会发现“水土不服”——要么库不兼容,要么底层驱动需要自己重写。

这个项目,就是来解决这个痛点的。我们将基于STM32的HAL库,从零开始构建一个驱动1.8寸TFT-LCD屏幕的完整方案。这里有个关键点:硬件SPI与软件SPI。硬件SPI效率高,占用CPU资源少,是首选;但在某些引脚资源紧张、或者硬件SPI被其他设备占用的场景下,软件模拟SPI(即“位碰撞”)就成了救星。我将带你深入这两种方式的实现细节,不仅仅是调通,更要理解其背后的时序逻辑、HAL库的调用机制以及如何写出稳定高效的驱动代码。

无论你是刚接触STM32的新手,还是想优化现有显示驱动的老手,这篇文章都将提供可直接“抄作业”的代码框架和大量避坑经验。我们不止于点亮屏幕,更要实现流畅的图形绘制、文本显示和图像刷新。

2. 核心需求解析与方案选型

在动手写代码之前,我们必须明确目标和约束条件,这决定了后续所有技术路线的选择。

2.1 明确屏幕型号与驱动芯片

市面上常见的1.8寸TFT-LCD屏幕,其核心驱动芯片多为ST7735SILI9163。虽然引脚和初始化序列略有不同,但通信协议(SPI)和基本的图形指令集是相似的。在开始前,务必确认你的屏幕型号。通常卖家会提供资料,或者你可以通过观察初始化代码中的命令序列来推断。本文将以ST7735S为例进行讲解,因为它的资料更公开,也更普遍。

为什么确认驱动芯片如此重要?不同的驱动芯片,其初始化命令集、颜色格式(如RGB565或RGB666)、GRAM(显存)的扫描方向等都可能不同。用错初始化代码,轻则显示颜色异常、花屏,重则完全无法点亮。我的经验是,拿到屏幕第一件事,就是找到并备份好其对应的数据手册(Datasheet)和初始化代码示例。

2.2 硬件SPI vs 软件SPI:如何抉择?

这是本项目的核心决策点。我们来做个详细的对比:

特性硬件SPI软件SPI (GPIO模拟)
通信速率。取决于MCU的SPI时钟配置,轻松达到几十MHz。。受限于CPU翻转GPIO的速度,通常很难超过几MHz。
CPU占用率。数据传输由硬件完成,CPU可处理其他任务。。CPU需要不断操作GPIO来模拟时钟和数据,在传输大量数据时(如图片)会非常繁忙。
代码复杂度。直接调用HAL库的HAL_SPI_Transmit/Receive即可。。需要自己实现时钟、数据位的精确时序控制。
引脚灵活性。必须使用MCU指定的SPI引脚(如MOSI, MISO, SCK)。。任意GPIO引脚均可模拟,自由度极大。
稳定性与可靠性。硬件保证时序精确,抗干扰能力强。。时序由软件延时控制,容易受中断干扰,在高主频或复杂任务下可能出错。
典型应用场景刷屏、显示动画、刷新率高、系统负载重的应用。引脚冲突、硬件SPI被占用、仅需显示少量静态信息或对刷新率无要求的场景。

我的选型建议:

  1. 首选硬件SPI:只要你的MCU有富余的SPI外设,且对应引脚可以连接屏幕,无脑选硬件SPI。它带来的性能提升和开发便利性是决定性的。
  2. 备用软件SPI:当硬件SPI被SD卡、无线模块等占用时,或者你的PCB布局导致硬件SPI引脚走线困难时,软件SPI是优秀的备选方案。在驱动小尺寸屏幕显示简单UI时,其性能通常也足够。

本项目将实现一个驱动层,同时支持硬件和软件两种模式,通过宏定义轻松切换。这增强了代码的复用性和项目的适应性。

2.3 引脚定义与连接

无论哪种SPI方式,屏幕除了SPI线,还有几个关键的控制引脚:

  1. CS (Chip Select / 片选):低电平有效,用于选中当前SPI设备。当总线上有多个设备时,此引脚至关重要。
  2. DC (Data/Command / 数据命令):这个引脚决定了你发送的是“命令”还是“数据”。通常低电平表示命令(如设置地址、开关显示),高电平表示要写入GRAM的像素数据。这是SPI驱动TFT屏幕与驱动其他简单SPI设备(如Flash)最大的区别之一
  3. RST (Reset / 复位):硬件复位引脚,低电平有效。可靠的复位是屏幕正常工作的第一步。
  4. BL (Backlight / 背光):控制背光开关,通常是高电平点亮。有些屏幕可能直接接VCC常亮。

连接示意图(以STM32F103C8T6和硬件SPI1为例):

  • SPI1_SCK (PA5)->TFT_SCK
  • SPI1_MOSI (PA7)->TFT_SDA(即MOSI)
  • PA4(任意GPIO) ->TFT_CS
  • PA3(任意GPIO) ->TFT_DC
  • PA2(任意GPIO) ->TFT_RST
  • PA1(任意GPIO) ->TFT_BL
  • VCC (3.3V)->TFT_VCC
  • GND->TFT_GND

注意:务必确认你的屏幕逻辑电平是3.3V。绝大多数1.8寸屏是3.3V,如果接5V可能会永久损坏。

3. 底层驱动框架设计与实现

一个健壮的驱动框架应该将底层通信细节与上层的图形操作分离。这样,当我们想更换通信方式(硬件SPI切软件SPI)或甚至更换屏幕型号时,只需要修改底层驱动文件,上层应用代码几乎不用动。

3.1 硬件SPI驱动实现

首先,使用STM32CubeMX配置硬件SPI。

  1. 选择对应的SPI外设(如SPI1)。
  2. 模式设置为“Full-Duplex Master”或“Transmit Only Master”(因为我们主要向屏幕写数据)。
  3. 时钟分频(Baud Rate)需要计算。ST7735S的SPI时钟最高典型值为15MHz。为了稳定,我们可以先配置为较低频率,如PCLK/4(假设系统时钟72MHz,则SPI时钟为18MHz)。初始化成功后可以尝试提高。
  4. 数据大小(Data Size)设置为8位。
  5. 时钟极性(CPOL)和相位(CPHA)通常设置为Mode 0(CPOL=Low, CPHA=1Edge)或Mode 3(CPOL=High, CPHA=2Edge)。ST7735S通常使用Mode 0。这一点必须在屏幕资料中确认,如果不对,数据会错位。
  6. 生成代码后,我们开始编写驱动函数。

核心的写命令和写数据函数如下:

// tft_spi.h #define TFT_CS_LOW() HAL_GPIO_WritePin(TFT_CS_GPIO_Port, TFT_CS_Pin, GPIO_PIN_RESET) #define TFT_CS_HIGH() HAL_GPIO_WritePin(TFT_CS_GPIO_Port, TFT_CS_Pin, GPIO_PIN_SET) #define TFT_DC_CMD() HAL_GPIO_WritePin(TFT_DC_GPIO_Port, TFT_DC_Pin, GPIO_PIN_RESET) #define TFT_DC_DATA() HAL_GPIO_WritePin(TFT_DC_GPIO_Port, TFT_DC_Pin, GPIO_PIN_SET) #define TFT_RST_LOW() HAL_GPIO_WritePin(TFT_RST_GPIO_Port, TFT_RST_Pin, GPIO_PIN_RESET) #define TFT_RST_HIGH() HAL_GPIO_WritePin(TFT_RST_GPIO_Port, TFT_RST_Pin, GPIO_PIN_SET) // 声明SPI句柄,例如 extern SPI_HandleTypeDef hspi1; extern SPI_HandleTypeDef hspi1; void TFT_WriteCommand(uint8_t cmd); void TFT_WriteData(uint8_t data); void TFT_WriteData_Burst(uint16_t *data, uint32_t count);
// tft_spi.c #include "tft_spi.h" // 写命令 void TFT_WriteCommand(uint8_t cmd) { TFT_DC_CMD(); // 设置DC为命令模式 TFT_CS_LOW(); // 选中屏幕 HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY); TFT_CS_HIGH(); // 取消选中 } // 写单字节数据 void TFT_WriteData(uint8_t data) { TFT_DC_DATA(); // 设置DC为数据模式 TFT_CS_LOW(); HAL_SPI_Transmit(&hspi1, &data, 1, HAL_MAX_DELAY); TFT_CS_HIGH(); } // 写多字节数据(用于快速填充颜色) void TFT_WriteData_Burst(uint16_t *data, uint32_t count) { TFT_DC_DATA(); TFT_CS_LOW(); // 注意:HAL_SPI_Transmit 要求数据地址为 uint8_t* // 而我们的颜色数据是uint16_t,需要转换指针类型并乘以2 HAL_SPI_Transmit(&hspi1, (uint8_t*)data, count * 2, HAL_MAX_DELAY); TFT_CS_HIGH(); }

关键点解析:

  • HAL_MAX_DELAY:这是一个宏,表示阻塞式等待直到传输完成。在显示驱动中,我们通常使用阻塞模式,因为后续操作依赖本次传输完成。如果系统实时性要求极高,可以考虑使用DMA+中断,但复杂度会大大增加。
  • 指针类型转换TFT_WriteData_Burst函数直接传输uint16_t数组,因为屏幕颜色数据通常是16位的RGB565格式。HAL_SPI_Transmit接收的是uint8_t指针和字节数,所以需要做指针转换,并且数量count要乘以2。
  • 片选(CS)控制:每次传输前后都要操作CS引脚。虽然有些屏幕在不与其他SPI设备共享总线时可以常低,但规范的操作是每次通信都拉低和拉高,这能保证时序的清晰和代码的通用性。

3.2 软件SPI(GPIO模拟)驱动实现

软件SPI的核心就是“模仿”硬件SPI的时序。我们需要定义四个GPIO:SCK、MOSI、CS、DC(RST和BL与控制方式无关)。然后通过置高、置低这些GPIO,并配合精确的延时来产生时钟和数据。

首先,在头文件中定义这些引脚,并实现基本的位操作宏或函数。

// tft_soft_spi.h #define TFT_SOFT_SPI_SCK_PIN GPIO_PIN_5 #define TFT_SOFT_SPI_SCK_PORT GPIOA #define TFT_SOFT_SPI_MOSI_PIN GPIO_PIN_7 #define TFT_SOFT_SPI_MOSI_PORT GPIOA // CS, DC, RST, BL 引脚定义与硬件SPI相同 // 内联函数或宏,用于快速GPIO操作,减少函数调用开销 __STATIC_INLINE void TFT_SOFT_SPI_SCK_HIGH(void) { HAL_GPIO_WritePin(TFT_SOFT_SPI_SCK_PORT, TFT_SOFT_SPI_SCK_PIN, GPIO_PIN_SET); } __STATIC_INLINE void TFT_SOFT_SPI_SCK_LOW(void) { HAL_GPIO_WritePin(TFT_SOFT_SPI_SCK_PORT, TFT_SOFT_SPI_SCK_PIN, GPIO_PIN_RESET); } __STATIC_INLINE void TFT_SOFT_SPI_MOSI_HIGH(void){ HAL_GPIO_WritePin(TFT_SOFT_SPI_MOSI_PORT, TFT_SOFT_SPI_MOSI_PIN, GPIO_PIN_SET); } __STATIC_INLINE void TFT_SOFT_SPI_MOSI_LOW(void) { HAL_GPIO_WritePin(TFT_SOFT_SPI_MOSI_PORT, TFT_SOFT_SPI_MOSI_PIN, GPIO_PIN_RESET); } // 软件SPI写一个字节的核心函数 void SOFT_SPI_WriteByte(uint8_t data);
// tft_soft_spi.c #include "tft_soft_spi.h" // 一个简单的微秒级延时函数,可以用SysTick或定时器实现更精确的延时 static void delay_us(uint32_t us) { uint32_t ticks = us * (SystemCoreClock / 1000000) / 5; // 粗略计算,需要根据实际CPU频率校准 while(ticks--) { __NOP(); } } void SOFT_SPI_WriteByte(uint8_t data) { // 假设使用SPI Mode 0: CPOL=0, CPHA=0 // 数据在SCK上升沿被采样,因此我们在SCK低电平时设置数据,然后拉高SCK,再拉低。 for(uint8_t i = 0; i < 8; i++) { TFT_SOFT_SPI_SCK_LOW(); delay_us(1); // 短暂延时,建立数据稳定时间 if(data & 0x80) { TFT_SOFT_SPI_MOSI_HIGH(); } else { TFT_SOFT_SPI_MOSI_LOW(); } data <<= 1; // 移出最高位 delay_us(1); TFT_SOFT_SPI_SCK_HIGH(); // 产生上升沿,屏幕在此刻采样数据 delay_us(1); // 循环末尾SCK为高,下次循环开始会拉低,符合Mode0时序 } // 循环结束后,SCK为高电平。根据Mode0,空闲时SCK为低,所以最后拉低。 TFT_SOFT_SPI_SCK_LOW(); } // 基于SOFT_SPI_WriteByte,实现与硬件SPI接口一致的TFT写命令/数据函数 void TFT_WriteCommand(uint8_t cmd) { TFT_DC_CMD(); TFT_CS_LOW(); SOFT_SPI_WriteByte(cmd); TFT_CS_HIGH(); } void TFT_WriteData(uint8_t data) { TFT_DC_DATA(); TFT_CS_LOW(); SOFT_SPI_WriteByte(data); TFT_CS_HIGH(); } void TFT_WriteData_Burst(uint16_t *data, uint32_t count) { TFT_DC_DATA(); TFT_CS_LOW(); uint8_t *p = (uint8_t*)data; for(uint32_t i = 0; i < count * 2; i++) { SOFT_SPI_WriteByte(p[i]); } TFT_CS_HIGH(); }

软件SPI的注意事项:

  1. 时序是关键delay_us的精度直接影响通信速率和稳定性。上述代码中的延时值(1us)需要根据你的CPU主频调整。如果主频很高(如168MHz),__NOP()循环次数需要大幅增加。最好用示波器测量SCK周期,确保它符合屏幕数据手册的要求(通常>66ns)。
  2. 中断干扰:软件SPI的时序容易被中断打断。如果传输过程中发生中断,可能导致SCK或MOSI信号脉宽异常,从而传输错误数据。在要求高的场合,可以在SOFT_SPI_WriteByte函数前后关闭全局中断(__disable_irq()__enable_irq()),但会影响系统实时性。
  3. 性能瓶颈:软件SPI写一个字节需要几十条指令,而硬件SPI只需一条DMA或中断请求。在填充全屏(160*128=20480像素,即40960字节)时,软件SPI会消耗大量CPU时间,导致屏幕刷新缓慢,系统无法响应其他任务。

3.3 统一的驱动接口

为了让上层应用无需关心底层是硬件还是软件SPI,我们可以创建一个头文件tft_driver.h,用宏来切换模式。

// tft_driver.h // 选择驱动方式:取消注释其中一行 #define USE_HARDWARE_SPI // #define USE_SOFTWARE_SPI #ifdef USE_HARDWARE_SPI #include "tft_spi.h" // 将通用函数名映射到硬件SPI实现 #define TFT_WriteCmd TFT_WriteCommand #define TFT_WriteData TFT_WriteData #define TFT_WriteBurst TFT_WriteData_Burst #elif defined(USE_SOFTWARE_SPI) #include "tft_soft_spi.h" // 软件SPI的函数名可能与硬件SPI相同,这里直接使用 // 如果不同,同样需要映射 #else #error "Please select either USE_HARDWARE_SPI or USE_SOFTWARE_SPI in tft_driver.h" #endif // 提供统一的初始化、画点、画线等函数声明 void TFT_Init(void); void TFT_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1); void TFT_DrawPixel(uint16_t x, uint16_t y, uint16_t color); // ... 其他图形函数

这样,在应用层,我们只需要包含tft_driver.h,调用TFT_WriteCmdTFT_DrawPixel等函数即可。切换SPI模式只需修改tft_driver.h中的一个宏定义。

4. ST7735S屏幕初始化与基础图形功能

有了底层通信能力,下一步就是和屏幕对话,按照它的“规矩”进行设置,然后才能在上面作画。

4.1 初始化序列详解

初始化是一系列预先定义好的命令和参数的组合,用于唤醒屏幕、设置工作模式、颜色格式、扫描方向等。ST7735S的初始化序列通常可以在卖家提供的例程或官方数据手册中找到。下面是一个典型的初始化函数片段:

void TFT_Init(void) { // 1. 硬件复位 TFT_RST_HIGH(); HAL_Delay(5); TFT_RST_LOW(); HAL_Delay(20); TFT_RST_HIGH(); HAL_Delay(120); // 等待复位完成,时间很关键,太短可能初始化失败 // 2. 发送初始化命令序列 TFT_WriteCmd(0x01); // Software Reset HAL_Delay(150); // 等待软件复位完成 TFT_WriteCmd(0x11); // Sleep Out HAL_Delay(120); // 退出睡眠模式 TFT_WriteCmd(0x3A); // Interface Pixel Format TFT_WriteData(0x05); // 16 bits/pixel (RGB565) TFT_WriteCmd(0x36); // Memory Data Access Control (MADCTL) // 参数设置屏幕旋转方向。0x00为默认,0xA0等用于旋转。 // 例如:0xC8 表示MY=1, MX=1, MV=1, RGB顺序,常用于横屏模式 TFT_WriteData(0xC8); TFT_WriteCmd(0xB2); // PORCH Setting TFT_WriteData(0x0C); TFT_WriteData(0x0C); TFT_WriteData(0x00); TFT_WriteData(0x33); TFT_WriteData(0x33); TFT_WriteCmd(0xB7); // Gate Control TFT_WriteData(0x35); // ... 这里省略了伽马校正、电源控制等更多命令,通常有十几到二十条命令 TFT_WriteCmd(0x29); // Display ON HAL_Delay(100); // 3. 清屏并开启背光 TFT_FillScreen(0x0000); // 填充黑色 TFT_BL_ON(); // 背光控制宏 }

初始化过程中的坑:

  • 延时至关重要:命令之间的HAL_Delay不是随意的。数据手册或例程中给出的延时是屏幕内部处理所需的最短时间。如果缩短,可能导致后续命令被忽略,屏幕状态异常。宁可等久一点,也不要偷这点时间
  • MADCTL命令:这个命令控制显示方向(旋转)和颜色格式的字节顺序。如果你发现屏幕显示是镜像的、颜色不对(红蓝互换),问题大概率出在这里。需要根据你的屏幕安装方向和硬件接线来调整这个参数。常见的值有0x00(默认),0xC0,0xA0,0x60等,需要结合MY,MX,MV,RGB等位去理解数据手册。
  • 颜色格式0x3A命令设置像素格式。我们常用0x05代表16位RGB565(即一个像素点用2字节表示)。也有屏幕支持0x06(18位RGB666)。必须和后续发送的颜色数据格式匹配。

4.2 设置窗口与画点函数

屏幕的GRAM可以看作一个二维数组。我们通过“设置窗口”命令来告诉屏幕,接下来要写入的数据对应屏幕上的哪个矩形区域。然后连续发送颜色数据,屏幕就会自动填充该区域。这是高效填充矩形或绘制图像的基础。

// 设置绘图窗口 void TFT_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { // 发送列地址设置命令 (CASET) TFT_WriteCmd(0x2A); TFT_WriteData(x0 >> 8); // 起始列高8位 TFT_WriteData(x0 & 0xFF); // 起始列低8位 TFT_WriteData(x1 >> 8); // 结束列高8位 TFT_WriteData(x1 & 0xFF); // 结束列低8位 // 发送行地址设置命令 (RASET) TFT_WriteCmd(0x2B); TFT_WriteData(y0 >> 8); TFT_WriteData(y0 & 0xFF); TFT_WriteData(y1 >> 8); TFT_WriteData(y1 & 0xFF); // 发送写GRAM命令 (RAMWR),准备接收颜色数据 TFT_WriteCmd(0x2C); }

有了TFT_SetWindow,画点函数就很简单了:将窗口设置为单个像素点,然后发送一个颜色值。

// 画一个像素点 void TFT_DrawPixel(uint16_t x, uint16_t y, uint16_t color) { // 检查坐标是否在屏幕范围内 if(x >= TFT_WIDTH || y >= TFT_HEIGHT) return; TFT_SetWindow(x, y, x, y); // 发送颜色数据,注意高低字节顺序。ST7735S通常先发高字节。 TFT_WriteData(color >> 8); TFT_WriteData(color & 0xFF); }

这里有一个非常重要的细节:颜色字节顺序。color是一个16位的uint16_t变量,在内存中存储为两个字节。例如红色0xF800,在内存中可能是0xF8(高地址)和0x00(低地址)(小端模式)。但屏幕接收数据时,是通过SPI一个字节一个字节发送的。是先发高8位(0xF8)还是低8位(0x00)?这由屏幕驱动芯片决定,并且有时可以通过MADCTL命令的某一位来设置。常见的ST7735S屏幕,通常需要先发送颜色值的高8位。如果你发现画出的颜色不对(比如红色显示成浅蓝色),很可能就是字节顺序反了。解决方法是调整TFT_DrawPixel中发送数据的顺序,或者调整TFT_WriteData_Burst函数中处理uint16_t数组的方式。

4.3 高级图形功能:填充、画线、显示字符

基于画点函数,我们可以构建更复杂的图形功能,但直接循环调用TFT_DrawPixel效率极低。利用TFT_SetWindow进行区域操作是关键。

1. 全屏填充(最快的方式)

void TFT_FillScreen(uint16_t color) { uint32_t total_pixels = TFT_WIDTH * TFT_HEIGHT; uint16_t color_buffer[32]; // 一个小缓冲区 uint32_t i; // 用目标颜色填充缓冲区 for(i = 0; i < 32; i++) { color_buffer[i] = color; } // 设置整个屏幕为窗口 TFT_SetWindow(0, 0, TFT_WIDTH-1, TFT_HEIGHT-1); // 分批次发送颜色数据,避免一次性分配过大数组 for(i = 0; i < total_pixels; i += 32) { uint32_t count = (total_pixels - i) > 32 ? 32 : (total_pixels - i); TFT_WriteBurst(color_buffer, count); } }

技巧:这里没有一次性发送total_pixels个颜色数据,而是用一个小的缓冲区循环发送。这样做有两个好处:一是节省RAM(对于内存紧张的MCU如STM32F103很重要),二是如果使用软件SPI,可以避免单次传输时间过长导致看门狗复位等问题。

2. 画水平/垂直线(优化版)画线算法(如Bresenham算法)本身不复杂,但优化其绘制过程能显著提升速度。画水平线时,我们可以设置一个水平方向的窗口,然后连续发送颜色数据,而不是画一个点设置一次窗口。

void TFT_DrawFastHLine(uint16_t x, uint16_t y, uint16_t w, uint16_t color) { // 边界检查 if((x >= TFT_WIDTH) || (y >= TFT_HEIGHT) || (w == 0)) return; if((x + w - 1) >= TFT_WIDTH) w = TFT_WIDTH - x; // 限制宽度 TFT_SetWindow(x, y, x + w - 1, y); for(uint16_t i = 0; i < w; i++) { TFT_WriteData(color >> 8); TFT_WriteData(color & 0xFF); } }

垂直线同理。对于斜线,优化空间较小,但仍可使用Bresenham算法减少乘除运算。

3. 显示位图与字符显示一张图片,本质就是将图片的像素数据(通常是RGB565数组)搬运到屏幕的指定窗口。你需要一个工具(如Img2Lcd, LCD Image Converter)将图片转换成C语言数组。

// 显示一张宽w高h的位图,数据数组为pixelData void TFT_DrawBitmap(uint16_t x, uint16_t y, uint16_t w, uint16_t h, const uint16_t *bitmap) { TFT_SetWindow(x, y, x + w - 1, y + h - 1); TFT_WriteBurst((uint16_t*)bitmap, w * h); // 注意强制类型转换和数量 }

显示字符则是通过字模。你需要一个包含ASCII字符点阵数据的数组(字库)。显示时,根据字符编码找到对应的字模数据,然后遍历点阵,在屏幕上画点(对于小字体)或使用块操作(对于大字体)。

5. 性能优化与高级技巧

当基础功能实现后,我们往往会遇到性能瓶颈,尤其是用软件SPI或者需要显示动态内容时。

5.1 使用DMA提升硬件SPI效率

对于硬件SPI,使用DMA(直接存储器访问)传输数据可以彻底解放CPU。CPU只需要配置好SPI和DMA,启动传输,就可以去处理其他任务,直到传输完成中断发生。

配置步骤(以STM32CubeMX为例):

  1. 在SPI配置中,开启“DMA Transmit”。
  2. 在DMA设置中,为SPI_Tx添加一个DMA流(Stream),方向为“Memory To Peripheral”,数据宽度为“Byte”(因为SPI数据寄存器是8位的)。
  3. 生成代码后,使用HAL_SPI_Transmit_DMA函数。
// 使用DMA填充区域 void TFT_FillRect_DMA(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color) { uint32_t total_pixels = w * h; static uint16_t dma_buffer[320]; // 定义一个静态缓冲区,大小适中 // 注意:DMA缓冲区必须是全局或静态变量,或者确保其生命周期覆盖整个DMA传输过程。 for(uint32_t i = 0; i < 320; i++) { dma_buffer[i] = color; } TFT_SetWindow(x, y, x + w - 1, y + h - 1); uint32_t transferred = 0; while(transferred < total_pixels) { uint32_t chunk = (total_pixels - transferred) > 320 ? 320 : (total_pixels - transferred); // 等待上一次DMA传输完成(如果有)。这里简单使用阻塞等待,实际应用可用信号量。 while(hspi1.State == HAL_SPI_STATE_BUSY_TX) { } HAL_SPI_Transmit_DMA(&hspi1, (uint8_t*)dma_buffer, chunk * 2); transferred += chunk; } // 等待最后一次DMA完成 while(hspi1.State == HAL_SPI_STATE_BUSY_TX) { } }

DMA使用的注意事项:

  • 缓冲区生命周期:传递给HAL_SPI_Transmit_DMA的数据缓冲区,必须保证在DMA传输完成前一直有效。因此通常使用全局数组或静态局部数组。
  • 传输完成回调:更好的做法是使用SPI传输完成中断回调函数HAL_SPI_TxCpltCallback,在回调中准备下一批数据或设置完成标志,避免CPU空等。
  • 数据对齐:确保缓冲区地址和长度符合DMA要求(通常是字节对齐)。

5.2 双帧缓冲与局部刷新

对于动画或快速变化的UI,频繁全屏刷新(即使使用DMA)也会导致明显的闪烁。一种高级技巧是使用双帧缓冲

  1. 在MCU的内部RAM或外扩RAM中开辟两块和屏幕分辨率一样大小的缓冲区(buffer1[128][160],buffer2[128][160])。
  2. 所有的图形绘制操作(画点、画线、渲染字符)都只在“后台缓冲区”上进行。
  3. 当一帧画面渲染完成后,通过DMA快速将整个后台缓冲区的内容一次性搬运到屏幕的GRAM中。
  4. 交换前后台缓冲区,开始渲染下一帧。

这样,屏幕始终显示完整的一帧,避免了绘制过程中的闪烁。但这需要消耗大量RAM(对于160*128的16位色深,一帧需要40KB),STM32F103系列可能无法承受,但STM32F4或H7系列则可以。

更实际的方法是局部刷新:只更新屏幕上发生变化的那部分区域。这需要应用层记录哪些区域是“脏”的,然后只刷新这些脏区域对应的窗口。

5.3 软件SPI的极限优化

如果被迫使用软件SPI,又想提高速度,可以尝试以下方法:

  • 使用寄存器直接操作:代替HAL_GPIO_WritePin,直接写GPIOx->BSRRGPIOx->BRR寄存器来置位/复位引脚,速度更快。
  • 展开循环:将SOFT_SPI_WriteByte函数中的8次循环展开,减少循环判断开销。
  • 汇编优化:对时序最关键的位操作部分,用内联汇编编写,实现纳秒级精确控制。
  • 提高CPU主频:这是最直接有效的方法。

但无论如何优化,软件SPI的瓶颈在于CPU需要逐位处理数据。对于1.8寸屏,如果只是显示静态文本或简单图形,优化后的软件SPI完全可以胜任。但如果需要播放动画或刷新复杂界面,硬件SPI是唯一的选择。

6. 常见问题排查与调试心得

驱动屏幕的过程就是与各种奇怪现象斗争的过程。下面是我总结的一些常见问题及解决方法。

6.1 屏幕白屏、花屏、乱码

这是最常见的问题,排查思路如下:

  1. 电源与连接

    • 首要检查:用万用表测量屏幕VCC和GND引脚是否为稳定的3.3V?背光电压是否正确?所有连接线是否牢固?虚焊和接触不良是头号杀手
    • 电流是否足够:屏幕全亮时功耗较大,确保你的电源或LDO能提供至少200mA的电流。
  2. 复位时序

    • 复位引脚(RST)的时序非常关键。确保复位低电平保持时间足够长(通常>10ms),复位后等待足够长时间(>120ms)再发送初始化命令。可以尝试延长这些延时。
  3. 初始化序列

    • 命令或参数错误:逐条核对初始化命令和参数,是否与你的屏幕型号完全一致?不同批次的屏幕初始化代码可能有细微差别。
    • 延时不足:命令之间的延时是否足够?尝试将所有HAL_Delay加倍。
  4. SPI通信问题

    • 时钟极性/相位(CPOL/CPHA):这是最容易出错的地方。如果模式不对,数据位会全部错位。尝试四种模式(0,1,2,3)的组合。Mode 0是最常见的
    • 时钟速度过快:尤其是软件SPI,降低SCK的频率试试。硬件SPI也先从低速(如1MHz)开始测试。
    • 字节顺序:发送颜色数据时,高低字节顺序是否正确?尝试交换TFT_WriteData(color >> 8)TFT_WriteData(color & 0xFF)的顺序。
    • 用逻辑分析仪抓取波形:这是终极调试手段。查看CS、DC、SCK、MOSI的波形,看命令和数据是否按预期发出,时序是否符合数据手册要求。

6.2 显示颜色异常(红蓝互换、偏色)

  1. 颜色格式设置:检查0x3A命令的参数是否为0x05(RGB565)。
  2. MADCTL命令0x36命令的某些位控制RGB顺序。尝试修改其参数,常见的如0x00,0xC0,0xA0等。
  3. 颜色数据格式:确认你定义的颜色常量是否正确。RGB565格式下,红色是0xF800,绿色是0x07E0,蓝色是0x001F

6.3 显示位置错乱、镜像或旋转不正确

这几乎完全由MADCTL(0x36命令)控制。它里面的MY,MX,MV,RGB,ML,MH,D3-D0位分别控制垂直镜像、水平镜像、行列交换、颜色顺序、垂直刷新顺序等。你需要仔细阅读ST7735S数据手册中关于MADCTL寄存器的说明,并结合你的屏幕物理安装方向,计算出正确的参数值。一个笨办法但有效:在网上搜索“ST7735S 旋转参数”,找到别人测试好的值进行尝试。

6.4 刷新速度慢,动画卡顿

  1. 确认使用硬件SPI:这是最大的性能差异来源。
  2. 提高SPI时钟:在屏幕允许的范围内(ST7735S通常最高15-20MHz),逐步提高SPI的波特率分频系数。
  3. 优化底层函数
    • 减少函数调用开销,将TFT_WriteData等函数内联(static inline)。
    • 对于硬件SPI,使用TFT_WriteData_Burst进行块传输,而不是单字节发送。
    • 使用DMA传输。
  4. 优化应用层算法
    • 避免频繁设置窗口。一次设置窗口,然后发送大量数据。
    • 使用局部刷新,只更新变化的区域。
    • 对于字符显示,使用预渲染的字模位图,而不是实时计算画点。

6.5 软件SPI下系统无响应

这是因为软件SPI长时间占用CPU。解决方法:

  1. 分时操作:将一屏数据的传输分成多个小片段,在片段之间调用HAL_Delay(1)或执行一次其他任务。
  2. 使用RTOS:将显示刷新任务放在一个低优先级的线程中,这样高优先级的任务(如按键响应)仍然可以打断它。
  3. 降低刷新率:非必要不刷新全屏。

驱动一个TFT屏幕,从点亮到稳定高效运行,是一个系统工程,涉及硬件连接、时序理解、驱动编写和性能优化。硬件SPI提供了坚实的性能基础,而软件SPI则展现了在资源受限下的灵活性。通过构建一个统一的驱动接口,你的代码可以轻松适配不同的项目需求。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询