STM32 HAL库驱动TFT-LCD:硬件SPI与软件SPI实现详解 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屏幕其核心驱动芯片多为ST7735S或ILI9163。虽然引脚和初始化序列略有不同但通信协议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被占用、仅需显示少量静态信息或对刷新率无要求的场景。我的选型建议首选硬件SPI只要你的MCU有富余的SPI外设且对应引脚可以连接屏幕无脑选硬件SPI。它带来的性能提升和开发便利性是决定性的。备用软件SPI当硬件SPI被SD卡、无线模块等占用时或者你的PCB布局导致硬件SPI引脚走线困难时软件SPI是优秀的备选方案。在驱动小尺寸屏幕显示简单UI时其性能通常也足够。本项目将实现一个驱动层同时支持硬件和软件两种模式通过宏定义轻松切换。这增强了代码的复用性和项目的适应性。2.3 引脚定义与连接无论哪种SPI方式屏幕除了SPI线还有几个关键的控制引脚CS (Chip Select / 片选)低电平有效用于选中当前SPI设备。当总线上有多个设备时此引脚至关重要。DC (Data/Command / 数据命令)这个引脚决定了你发送的是“命令”还是“数据”。通常低电平表示命令如设置地址、开关显示高电平表示要写入GRAM的像素数据。这是SPI驱动TFT屏幕与驱动其他简单SPI设备如Flash最大的区别之一。RST (Reset / 复位)硬件复位引脚低电平有效。可靠的复位是屏幕正常工作的第一步。BL (Backlight / 背光)控制背光开关通常是高电平点亮。有些屏幕可能直接接VCC常亮。连接示意图以STM32F103C8T6和硬件SPI1为例SPI1_SCK (PA5)-TFT_SCKSPI1_MOSI (PA7)-TFT_SDA(即MOSI)PA4(任意GPIO) -TFT_CSPA3(任意GPIO) -TFT_DCPA2(任意GPIO) -TFT_RSTPA1(任意GPIO) -TFT_BLVCC (3.3V)-TFT_VCCGND-TFT_GND注意务必确认你的屏幕逻辑电平是3.3V。绝大多数1.8寸屏是3.3V如果接5V可能会永久损坏。3. 底层驱动框架设计与实现一个健壮的驱动框架应该将底层通信细节与上层的图形操作分离。这样当我们想更换通信方式硬件SPI切软件SPI或甚至更换屏幕型号时只需要修改底层驱动文件上层应用代码几乎不用动。3.1 硬件SPI驱动实现首先使用STM32CubeMX配置硬件SPI。选择对应的SPI外设如SPI1。模式设置为“Full-Duplex Master”或“Transmit Only Master”因为我们主要向屏幕写数据。时钟分频Baud Rate需要计算。ST7735S的SPI时钟最高典型值为15MHz。为了稳定我们可以先配置为较低频率如PCLK/4假设系统时钟72MHz则SPI时钟为18MHz。初始化成功后可以尝试提高。数据大小Data Size设置为8位。时钟极性CPOL和相位CPHA通常设置为Mode 0CPOLLow CPHA1Edge或Mode 3CPOLHigh CPHA2Edge。ST7735S通常使用Mode 0。这一点必须在屏幕资料中确认如果不对数据会错位。生成代码后我们开始编写驱动函数。核心的写命令和写数据函数如下// 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 软件SPIGPIO模拟驱动实现软件SPI的核心就是“模仿”硬件SPI的时序。我们需要定义四个GPIOSCK、MOSI、CS、DCRST和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: CPOL0, CPHA0 // 数据在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的注意事项时序是关键delay_us的精度直接影响通信速率和稳定性。上述代码中的延时值1us需要根据你的CPU主频调整。如果主频很高如168MHz__NOP()循环次数需要大幅增加。最好用示波器测量SCK周期确保它符合屏幕数据手册的要求通常66ns。中断干扰软件SPI的时序容易被中断打断。如果传输过程中发生中断可能导致SCK或MOSI信号脉宽异常从而传输错误数据。在要求高的场合可以在SOFT_SPI_WriteByte函数前后关闭全局中断__disable_irq()和__enable_irq()但会影响系统实时性。性能瓶颈软件SPI写一个字节需要几十条指令而硬件SPI只需一条DMA或中断请求。在填充全屏160*12820480像素即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 表示MY1, MX1, MV1, 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字节表示。也有屏幕支持0x0618位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为例在SPI配置中开启“DMA Transmit”。在DMA设置中为SPI_Tx添加一个DMA流Stream方向为“Memory To Peripheral”数据宽度为“Byte”因为SPI数据寄存器是8位的。生成代码后使用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也会导致明显的闪烁。一种高级技巧是使用双帧缓冲。在MCU的内部RAM或外扩RAM中开辟两块和屏幕分辨率一样大小的缓冲区buffer1[128][160],buffer2[128][160]。所有的图形绘制操作画点、画线、渲染字符都只在“后台缓冲区”上进行。当一帧画面渲染完成后通过DMA快速将整个后台缓冲区的内容一次性搬运到屏幕的GRAM中。交换前后台缓冲区开始渲染下一帧。这样屏幕始终显示完整的一帧避免了绘制过程中的闪烁。但这需要消耗大量RAM对于160*128的16位色深一帧需要40KBSTM32F103系列可能无法承受但STM32F4或H7系列则可以。更实际的方法是局部刷新只更新屏幕上发生变化的那部分区域。这需要应用层记录哪些区域是“脏”的然后只刷新这些脏区域对应的窗口。5.3 软件SPI的极限优化如果被迫使用软件SPI又想提高速度可以尝试以下方法使用寄存器直接操作代替HAL_GPIO_WritePin直接写GPIOx-BSRR或GPIOx-BRR寄存器来置位/复位引脚速度更快。展开循环将SOFT_SPI_WriteByte函数中的8次循环展开减少循环判断开销。汇编优化对时序最关键的位操作部分用内联汇编编写实现纳秒级精确控制。提高CPU主频这是最直接有效的方法。但无论如何优化软件SPI的瓶颈在于CPU需要逐位处理数据。对于1.8寸屏如果只是显示静态文本或简单图形优化后的软件SPI完全可以胜任。但如果需要播放动画或刷新复杂界面硬件SPI是唯一的选择。6. 常见问题排查与调试心得驱动屏幕的过程就是与各种奇怪现象斗争的过程。下面是我总结的一些常见问题及解决方法。6.1 屏幕白屏、花屏、乱码这是最常见的问题排查思路如下电源与连接首要检查用万用表测量屏幕VCC和GND引脚是否为稳定的3.3V背光电压是否正确所有连接线是否牢固虚焊和接触不良是头号杀手。电流是否足够屏幕全亮时功耗较大确保你的电源或LDO能提供至少200mA的电流。复位时序复位引脚RST的时序非常关键。确保复位低电平保持时间足够长通常10ms复位后等待足够长时间120ms再发送初始化命令。可以尝试延长这些延时。初始化序列命令或参数错误逐条核对初始化命令和参数是否与你的屏幕型号完全一致不同批次的屏幕初始化代码可能有细微差别。延时不足命令之间的延时是否足够尝试将所有HAL_Delay加倍。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 显示颜色异常红蓝互换、偏色颜色格式设置检查0x3A命令的参数是否为0x05RGB565。MADCTL命令0x36命令的某些位控制RGB顺序。尝试修改其参数常见的如0x00,0xC0,0xA0等。颜色数据格式确认你定义的颜色常量是否正确。RGB565格式下红色是0xF800绿色是0x07E0蓝色是0x001F。6.3 显示位置错乱、镜像或旋转不正确这几乎完全由MADCTL0x36命令控制。它里面的MY,MX,MV,RGB,ML,MH,D3-D0位分别控制垂直镜像、水平镜像、行列交换、颜色顺序、垂直刷新顺序等。你需要仔细阅读ST7735S数据手册中关于MADCTL寄存器的说明并结合你的屏幕物理安装方向计算出正确的参数值。一个笨办法但有效在网上搜索“ST7735S 旋转参数”找到别人测试好的值进行尝试。6.4 刷新速度慢动画卡顿确认使用硬件SPI这是最大的性能差异来源。提高SPI时钟在屏幕允许的范围内ST7735S通常最高15-20MHz逐步提高SPI的波特率分频系数。优化底层函数减少函数调用开销将TFT_WriteData等函数内联static inline。对于硬件SPI使用TFT_WriteData_Burst进行块传输而不是单字节发送。使用DMA传输。优化应用层算法避免频繁设置窗口。一次设置窗口然后发送大量数据。使用局部刷新只更新变化的区域。对于字符显示使用预渲染的字模位图而不是实时计算画点。6.5 软件SPI下系统无响应这是因为软件SPI长时间占用CPU。解决方法分时操作将一屏数据的传输分成多个小片段在片段之间调用HAL_Delay(1)或执行一次其他任务。使用RTOS将显示刷新任务放在一个低优先级的线程中这样高优先级的任务如按键响应仍然可以打断它。降低刷新率非必要不刷新全屏。驱动一个TFT屏幕从点亮到稳定高效运行是一个系统工程涉及硬件连接、时序理解、驱动编写和性能优化。硬件SPI提供了坚实的性能基础而软件SPI则展现了在资源受限下的灵活性。通过构建一个统一的驱动接口你的代码可以轻松适配不同的项目需求。