1. 项目概述:当STM32遇见1.8寸TFT-LCD
玩过单片机的朋友都知道,点亮一块屏幕是件很有成就感的事。这次要聊的,就是用STM32的HAL库,通过硬件SPI(或者软件模拟SPI)来驱动一块1.8寸的TFT-LCD彩屏。这类屏幕通常指的是ST7735S或ILI9163这类控制器驱动的128x160或128x128分辨率的小屏,成本低、接口简单,在智能手表、便携式仪表、小型人机交互界面上应用非常广泛。
为什么选择SPI?对于这种分辨率不高、刷新率要求不苛刻的屏幕,SPI接口是性价比最高的选择。它只需要3-4根数据线(MOSI, SCK, CS, DC,有时再加一个RST),相比并口的8位或16位总线,极大地节省了宝贵的单片机IO资源。硬件SPI能解放CPU,让数据传输在后台自动进行;而软件SPI则提供了极致的引脚分配灵活性,当硬件SPI引脚被其他功能占用时,它就是救星。HAL库的封装,让这两种方式的切换和代码编写变得相对统一和便捷。
这篇文章,我会从一个实际项目开发者的角度,带你从零开始,完成引脚连接、HAL库配置、驱动函数编写、图形基础功能实现,再到性能优化和问题排查。无论你是刚接触STM32和HAL库的新手,还是想寻找一个稳定可靠驱动方案的开发者,这里都有你需要的“干货”。我们会深入SPI通信的时序细节,理解TFT屏幕的初始化序列,并分享那些数据手册上不会写的调试技巧和避坑指南。
2. 核心硬件与通信协议解析
2.1 认识我们的“画布”:1.8寸TFT-LCD屏幕
市面上常见的1.8寸TFT模块,其核心驱动芯片大多是ST7735S,也有部分使用ILI9163、ILI9341(裁剪了分辨率)。以ST7735S为例,它内部集成了132x162的显存(GRAM),但我们通常只使用其中心的128x160区域。屏幕每个像素的颜色由16位数据(RGB565格式)表示,即红色占5位,绿色占6位,蓝色占5位。
屏幕模块的引脚通常包括:
- VCC:电源正极(3.3V或5V,需根据模块逻辑电平决定)。
- GND:电源地。
- CS:片选信号,低电平有效。用于选中当前SPI总线上的这个设备。
- RST:复位信号,低电平复位。用于硬件复位屏幕控制器。
- DC(有时标为A0或RS):数据/命令选择线。这是SPI驱动TFT屏幕最关键的一根线。当DC为低电平时,SPI总线发送的是命令(Command);当DC为高电平时,发送的是数据(Data)。
- SCK:SPI时钟线。
- MOSI:SPI主机输出从机输入线,用于STM32向屏幕发送数据。
- MISO:SPI主机输入从机输出线。特别注意:绝大多数TFT屏幕模块在SPI模式下是只写的,不需要读取屏幕数据,因此MISO引脚通常可以悬空不接。这简化了连线。
- BL:背光控制,高电平点亮。
注意:务必查阅你手中屏幕模块的具体资料表,确认其驱动芯片型号、供电电压(接5V可能烧毁3.3V逻辑的模块)、以及引脚定义。这是所有工作的基础。
2.2 SPI通信协议在TFT驱动中的角色
SPI是一种全双工、同步、串行的通信协议。在TFT驱动场景下,我们几乎只使用其“主机发送”功能。
关键时序理解:对于ST7735S这类屏幕,SPI模式通常工作在模式0(CPOL=0, CPHA=0)或模式3(CPOL=1, CPHA=1)。这意味着在时钟线SCK的空闲状态为低电平(模式0)或高电平(模式3),数据在SCK的第一个边沿(上升沿或下降沿)被采样。绝大多数模块默认支持模式0。配置错误会导致屏幕无任何反应。
DC引脚的核心作用:这是区别于其他普通SPI设备(如Flash、传感器)的关键。屏幕控制器需要区分你发送的是一个“设置指令”还是一包“显示数据”。
- 发送命令:拉低DC引脚 -> 通过SPI发送一个字节的命令码(如0x2A, 设置列地址)。
- 发送数据:拉高DC引脚 -> 通过SPI发送命令所需的参数或像素数据(如随后的4个字节的列起始和结束地址)。
所有的屏幕操作,如设置显示区域、写入像素颜色,都遵循“先命令,后数据”的序列。屏幕的初始化,本质上就是发送一长串预先定义好的命令和数据序列。
2.3 硬件SPI vs 软件模拟SPI:如何抉择?
硬件SPI:
- 优点:速度极快,占用CPU资源少(DMA加持下几乎零占用),时序精准由硬件保证。
- 缺点:引脚固定(如STM32F1的SPI1 MOSI/SCK固定在PA7/PA5),可能与其他外设冲突;配置稍复杂。
- 适用场景:对刷屏速度有要求,需要显示动画或频繁刷新局部区域;CPU需要处理其他复杂任务。
软件模拟SPI(Bit-Banging):
- 优点:引脚可以任意指定到任何GPIO,灵活性无敌;代码简单直观,易于理解和调试。
- 缺点:速度慢,占用大量CPU时间进行IO翻转和延时;时序容易受中断干扰。
- 适用场景:硬件SPI引脚被占用;项目初期快速验证;对刷新速度要求极低(如静态图片显示)。
我的经验选择:在资源允许的情况下,优先使用硬件SPI。其性能优势是决定性的。只有当引脚确实冲突,且速度要求不高时,才考虑软件SPI。HAL库的架构使得两者的上层驱动函数(如写命令、写数据)可以抽象成统一的接口,底层实现不同而已,这为我们后续的驱动设计提供了便利。
3. 工程环境搭建与HAL库配置
3.1 STM32CubeMX项目创建与硬件SPI配置
假设我们使用STM32F103C8T6(蓝色药丸核心板)和硬件SPI1。
- 系统核心:在
SYS中,将Debug改为Serial Wire(如果要用ST-Link调试)。 - 时钟:配置RCC,HSE选择Crystal/Ceramic Resonator,然后在Clock Configuration选项卡里将系统时钟(SYSCLK)配置到最高72MHz(对于F1)。
- SPI1配置:
- 找到
SPI1, 将其模式设置为Full-Duplex Master。 - 参数设置:
Prescaler: 时钟分频。这是决定SPI速度的关键。SPI时钟 = APB2总线时钟 / 分频系数。APB2时钟为72MHz。对于屏幕,初期调试可以设大一点,如64分频(约1.125MHz),稳定后再提高。最终可以尝试8分频(9MHz)或4分频(18MHz),具体需看屏幕模块支持的最高SCK频率。Data Size:8-bit。屏幕命令和数据都以字节为单位。First Bit:MSB First。绝大多数设备都是高位在前。Clock Polarity:Low。对应模式0。Clock Phase:1 Edge。对应模式0。
- 此时,CubeMX会自动分配
PA5为SPI1_SCK,PA7为SPI1_MOSI。我们不需要MISO。
- 找到
- GPIO配置:
- 为
CS、DC、RST引脚分配三个普通的GPIO输出口,例如PB0(DC)、PB1(RST)、PB2(CS)。将它们初始输出电平设置为高(对于CS和RST)或低(根据情况)。 - 将背光控制
BL引脚也配置为一个GPIO输出,如PB10, 初始化为高电平点亮背光。
- 为
- 生成代码:在Project Manager中设置好工程名、路径、IDE(Keil MDK或STM32CubeIDE),然后生成代码。
3.2 软件模拟SPI的GPIO初始化
如果选择软件SPI,则无需在CubeMX中配置SPI外设。只需配置用于模拟SCK、MOSI、CS、DC、RST、BL的GPIO引脚为输出模式即可。例如:
PB12->SCKPB13->MOSIPB14->CSPB15->DCPA8->RSTPA9->BL
在生成的代码中,我们需要手动实现SCK和MOSI的翻转时序。
3.3 底层驱动函数抽象与实现
为了代码的通用性和可移植性,我们抽象出几个最基本的操作函数,无论硬件还是软件SPI,都实现它们。
1. 引脚控制宏定义(在头文件中):
// 假设引脚定义如下(硬件SPI情况) #define TFT_CS_PIN GPIO_PIN_2 #define TFT_CS_PORT GPIOB #define TFT_DC_PIN GPIO_PIN_0 #define TFT_DC_PORT GPIOB #define TFT_RST_PIN GPIO_PIN_1 #define TFT_RST_PORT GPIOB #define TFT_BL_PIN GPIO_PIN_10 #define TFT_BL_PORT GPIOB // 控制宏,方便操作 #define TFT_CS_LOW HAL_GPIO_WritePin(TFT_CS_PORT, TFT_CS_PIN, GPIO_PIN_RESET) #define TFT_CS_HIGH HAL_GPIO_WritePin(TFT_CS_PORT, TFT_CS_PIN, GPIO_PIN_SET) #define TFT_DC_LOW HAL_GPIO_WritePin(TFT_DC_PORT, TFT_DC_PIN, GPIO_PIN_RESET) #define TFT_DC_HIGH HAL_GPIO_WritePin(TFT_DC_PORT, TFT_DC_PIN, GPIO_PIN_SET) #define TFT_RST_LOW HAL_GPIO_WritePin(TFT_RST_PORT, TFT_RST_PIN, GPIO_PIN_RESET) #define TFT_RST_HIGH HAL_GPIO_WritePin(TFT_RST_PORT, TFT_RST_PIN, GPIO_PIN_SET) #define TFT_BL_ON HAL_GPIO_WritePin(TFT_BL_PORT, TFT_BL_PIN, GPIO_PIN_SET) #define TFT_BL_OFF HAL_GPIO_WritePin(TFT_BL_PORT, TFT_BL_PIN, GPIO_PIN_RESET)2. 核心通信函数:
- 硬件SPI版本:
// 发送一个字节(8位)数据 static void TFT_WriteByte(uint8_t data) { HAL_SPI_Transmit(&hspi1, &data, 1, HAL_MAX_DELAY); // hspi1是CubeMX生成的SPI句柄 } // 发送一个命令 void TFT_WriteCommand(uint8_t cmd) { TFT_DC_LOW; // 命令模式 TFT_CS_LOW; TFT_WriteByte(cmd); TFT_CS_HIGH; } // 发送一个数据字节 void TFT_WriteData(uint8_t data) { TFT_DC_HIGH; // 数据模式 TFT_CS_LOW; TFT_WriteByte(data); TFT_CS_HIGH; }- 软件SPI版本:
// 模拟SCK和MOSI引脚定义 #define SOFT_SCK_PIN GPIO_PIN_12 #define SOFT_SCK_PORT GPIOB #define SOFT_MOSI_PIN GPIO_PIN_13 #define SOFT_MOSI_PORT GPIOB static void SOFT_SPI_WriteByte(uint8_t data) { for(uint8_t i = 0; i < 8; i++) { HAL_GPIO_WritePin(SOFT_SCK_PORT, SOFT_SCK_PIN, GPIO_PIN_RESET); // SCK拉低 // 根据模式0,在SCK上升沿采样数据,因此我们先设置数据位 if(data & 0x80) { HAL_GPIO_WritePin(SOFT_MOSI_PORT, SOFT_MOSI_PIN, GPIO_PIN_SET); } else { HAL_GPIO_WritePin(SOFT_MOSI_PORT, SOFT_MOSI_PIN, GPIO_PIN_RESET); } // 插入微小延时,确保数据稳定,具体时长需根据主频调整 __NOP(); __NOP(); __NOP(); HAL_GPIO_WritePin(SOFT_SCK_PORT, SOFT_SCK_PIN, GPIO_PIN_SET); // SCK拉高,产生上升沿 __NOP(); __NOP(); // 保持高电平一段时间 data <<= 1; // 移位准备下一位 } // 循环结束后,SCK处于高电平。根据模式0(CPOL=0),空闲时应为低电平 HAL_GPIO_WritePin(SOFT_SCK_PORT, SOFT_SCK_PIN, GPIO_PIN_RESET); } // TFT_WriteCommand和TFT_WriteData函数与硬件版相同,只需将内部的TFT_WriteByte替换为SOFT_SPI_WriteByte。实操心得:软件SPI的延时
__NOP()数量需要根据你的CPU主频进行测试调整。太快了屏幕可能无法识别,太慢了刷屏速度感人。一个简单的测试方法是,先设置较多的__NOP()确保通信成功,然后逐步减少直到出现通信错误,再稍微加回一些作为安全余量。
4. TFT屏幕驱动实现详解
4.1 屏幕初始化序列:唤醒沉睡的“画布”
初始化序列是一系列按照特定顺序发送的命令和数据,用于配置屏幕控制器的各种内部寄存器,如驱动方向、颜色模式、电源控制等。不同厂家、不同批次的ST7735S模块,其初始化序列可能略有差异。最好的来源是屏幕卖家提供的例程或数据手册附录。
一个典型的ST7735S初始化流程如下(通过调用TFT_WriteCommand和TFT_WriteData实现):
- 硬件复位:拉低RST引脚至少10ms,然后拉高,并延时120ms以上等待屏幕内部复位完成。
void TFT_Reset(void) { TFT_RST_LOW; HAL_Delay(20); // 延时20ms,远大于10ms要求 TFT_RST_HIGH; HAL_Delay(120); // 等待复位稳定 } - 退出睡眠模式:发送命令
0x11(SLPOUT)。 - 设置颜色格式:发送命令
0x3A(COLMOD), 后面跟数据0x05或0x06。0x05代表16位RGB565,这是我们最常用的格式。 - 设置显示方向(旋转):发送命令
0x36(MADCTL), 后面的参数决定了显示方向和颜色顺序。例如0xA0或0xC0等,需要根据你的屏幕物理安装方向来调整。 - 设置列和行地址范围:通常初始化时会设置全屏范围,命令为
0x2A(CASET) 和0x2B(RASET), 后面各跟4个字节的起始、结束地址。 - 打开显示:发送命令
0x29(DISPON)。 - 清屏并点亮背光。
关键点:很多初始化序列在关键命令后需要插入HAL_Delay, 例如SLPOUT之后需要等待120ms,DISPON之前可能需要等待。忽略这些延时是导致初始化失败最常见的原因之一。建议将卖家提供的完整初始化序列代码封装成一个TFT_Init()函数。
4.2 设置绘图窗口与像素写入
要在屏幕上某个区域绘图,必须先设置“窗口”,告诉屏幕控制器接下来接收的像素数据要填充到GRAM的哪个矩形区域。
void TFT_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { // 设置列地址范围 TFT_WriteCommand(0x2A); // CASET TFT_WriteData(x0 >> 8); // 起始列高8位 TFT_WriteData(x0 & 0xFF); // 起始列低8位 TFT_WriteData(x1 >> 8); // 结束列高8位 TFT_WriteData(x1 & 0xFF); // 结束列低8位 // 设置行地址范围 TFT_WriteCommand(0x2B); // RASET TFT_WriteData(y0 >> 8); // 起始行高8位 TFT_WriteData(y0 & 0xFF); // 起始行低8位 TFT_WriteData(y1 >> 8); // 结束行高8位 TFT_WriteData(y1 & 0xFF); // 结束行低8位 // 发送写GRAM命令,接下来发送的数据将自动填充到设置的窗口 TFT_WriteCommand(0x2C); // RAMWR }设置好窗口后,调用TFT_WriteData发送的像素数据,就会从窗口的左上角开始,从左到右、从上到下依次填充。每个像素需要两个字节(16位)。
画点函数:这是最基础的图形函数。
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); // 窗口设置为单个像素点 TFT_WriteData(color >> 8); // 发送颜色高字节 TFT_WriteData(color & 0xFF); // 发送颜色低字节 }注意:
TFT_DrawPixel函数每次画点都重新设置窗口,效率极低,仅适用于画散点。画连续图形(线、矩形、填充)时,应该先设置好整个区域的窗口,然后连续发送像素数据。
4.3 基础图形功能实现
基于画点和窗口设置,我们可以构建更高级的函数。
1. 填充矩形:
void TFT_FillRect(uint16_t x0, uint16_t y0, uint16_t w, uint16_t h, uint16_t color) { uint32_t pixelCount = (uint32_t)w * h; if(pixelCount == 0) return; TFT_SetWindow(x0, y0, x0 + w - 1, y0 + h - 1); // 优化:连续发送同一颜色数据 TFT_DC_HIGH; TFT_CS_LOW; for(uint32_t i = 0; i < pixelCount; i++) { TFT_WriteByte(color >> 8); TFT_WriteByte(color & 0xFF); // 可以在此处加入DMA传输优化,见后续章节 } TFT_CS_HIGH; }2. 清屏:TFT_FillRect(0, 0, TFT_WIDTH, TFT_HEIGHT, BACKGROUND_COLOR);
3. 画线、画矩形框、画圆:这些属于算法层面,可以使用Bresenham等经典算法实现,核心依然是调用TFT_DrawPixel或更优化的区域连续写入。网上有大量开源代码可供参考集成。
4.4 显示图片与中英文字符
显示图片:
- 将图片(如BMP、JPG)通过工具(如Img2Lcd, LCD Image Converter)转换为C语言数组,格式为RGB565。
- 图片数组的大小应为
宽度 * 高度 * 2字节。 - 使用
TFT_SetWindow设置图片显示区域。 - 将图片数组数据,通过
TFT_WriteData或更高效的方式(如DMA)连续发送出去。
显示字符(字库):
- 取模:使用PCtoLCD2002等软件,选择字体和大小(如12x12, 16x16),生成点阵字库数组。每个字符对应一个数组,描述其点阵信息。
- 显示函数:根据字符编码(如ASCII或GBK)找到对应的点阵数组,然后遍历点阵,遇到“1”则画前景色点,“0”则画背景色点或跳过。
- 中文字符:原理相同,但字符点阵更大(通常16x16或以上),编码需使用GBK/Unicode,并需要包含更庞大的字库数组。为了节省单片机Flash,通常只提取项目中用到的字符。
实操心得:将字库放在SPI Flash或SD卡中,运行时加载到RAM或直接通过FSMC等接口读取,是显示大量字符的常用方法,但这超出了基础SPI驱动的范畴。对于简单界面,将少量常用字符的点阵直接编译进代码是最直接可靠的。
5. 性能优化与高级技巧
5.1 使用DMA提升硬件SPI刷屏效率
直接使用HAL_SPI_Transmit发送大量像素数据(如整屏填充)是阻塞式的,CPU需要等待每个字节发送完成。DMA(直接存储器访问)可以将数据从内存搬运到SPI数据寄存器的工作交给硬件,解放CPU。
配置步骤(CubeMX):
- 在SPI1的DMA Settings选项卡,添加一个
SPI1_TX的DMA请求。 - 选择通道(如DMA1 Channel3),模式设为
Normal(单次传输),优先级High,内存数据宽度Byte(与SPI的8位匹配)。 - 在代码中,使用
HAL_SPI_Transmit_DMA函数。
uint16_t fillColor = 0xF800; // 红色 uint8_t colorBuffer[320]; // 缓冲区:160个像素 * 2字节 = 320字节 // 将颜色值填充到缓冲区 for(int i = 0; i < 320; i+=2) { colorBuffer[i] = fillColor >> 8; colorBuffer[i+1] = fillColor & 0xFF; } TFT_SetWindow(0, 0, 127, 159); // 设置全屏窗口 TFT_DC_HIGH(); TFT_CS_LOW(); HAL_SPI_Transmit_DMA(&hspi1, colorBuffer, 320); // 启动DMA传输 // 此时CPU可以去做其他事情 // 需要等待DMA传输完成?可以查询标志位或使用回调函数。注意:DMA传输是非阻塞的,你需要管理好传输完成的时机(如通过
HAL_SPI_TxCpltCallback回调函数),才能进行下一次操作(如拉高CS)。对于整屏刷新,可能需要分多次DMA传输,因为一次传输的数据量受限于DMA缓冲区大小和SPI的FIFO。
5.2 双帧缓冲与局部刷新
对于动态显示,频繁全屏刷新(即使使用DMA)也可能造成闪屏。更高级的策略是使用“双帧缓冲”:
- 在单片机RAM中开辟两块与屏幕GRAM大小一致的缓冲区(Buffer A和Buffer B),每个像素占2字节。
- 所有的绘图操作(画点、画线、渲染字符)都只在当前“后台缓冲区”中进行,修改的是内存中的数据。
- 当一帧图像准备好后,通过一次快速的DMA传输,将整个后台缓冲区的内容复制到屏幕GRAM。
- 交换前后台缓冲区指针。
这种方法将耗时的绘图计算与屏幕更新解耦,能提供最流畅的动画效果。但它的代价是消耗大量RAM(对于128x160 RGB565, 需要约40KB * 2 = 80KB),这对于RAM有限的STM32F1系列(如C8T6只有20KB)是不现实的。因此,更实用的优化是局部刷新:只更新屏幕上发生变化的那部分区域,而不是整个屏幕。这需要应用程序层来跟踪脏区域。
5.3 软件SPI的速度极限与优化
软件SPI的瓶颈在于CPU循环和GPIO操作。一些优化思路:
- 使用寄存器直接操作GPIO:代替
HAL_GPIO_WritePin,使用GPIOx->BSRR或ODR寄存器进行位操作,速度更快。#define SOFT_SCK_HIGH (SOFT_SCK_PORT->BSRR = SOFT_SCK_PIN) #define SOFT_SCK_LOW (SOFT_SCK_PORT->BRR = SOFT_SCK_PIN) #define SOFT_MOSI_HIGH (SOFT_MOSI_PORT->BSRR = SOFT_MOSI_PIN) #define SOFT_MOSI_LOW (SOFT_MOSI_PORT->BRR = SOFT_MOSI_PIN) - 循环展开:将8次循环展开成顺序执行的8组语句,消除循环判断开销。
- 减少延时:在保证屏幕能稳定接收的前提下,尽可能减少
__NOP()的数量。 - 使用汇编或内联汇编:对于时间要求极其苛刻的场合,可以用汇编编写最核心的位操作循环。
但无论如何优化,软件SPI的速度都难以与硬件SPI匹敌,更不用说加上DMA了。因此,优化软件SPI更多是一种“救急”或“学习”行为。
6. 常见问题排查与调试心得
驱动屏幕的过程就是与各种奇怪现象斗争的过程。下面是一些典型问题及排查思路。
| 现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 屏幕完全无显示,背光亮 | 1. 电源问题。 2. 复位或初始化序列错误。 3. SPI通信根本未建立。 | 1. 检查VCC/GND电压,确认屏幕逻辑电压(3.3V/5V)。 2. 用逻辑分析仪或示波器抓取RST、CS、DC、SCK、MOSI波形。首先看RST是否有正确的低脉冲,然后看发送初始化命令时,CS和DC是否有对应变化,SCK是否有时钟输出,MOSI是否有数据。 3. 检查SPI模式(CPOL/CPHA)是否与屏幕要求一致。模式0和模式3是常见问题点。 4. 大幅降低SPI波特率(如分频到256)再试。 |
| 屏幕全白、全黑或显示彩色条纹 | 初始化序列部分成功,但某些关键参数(如颜色模式、显示方向、电源配置)设置错误。 | 1. 核对初始化序列,特别是COLMOD(颜色格式)、MADCTL(显示方向)命令的参数。2. 检查 CASET/RASET设置的范围是否超出了屏幕实际物理分辨率。3. 尝试使用卖家提供的“绝对可靠”的初始化代码。 |
| 显示错位、镜像或颜色错误 | MADCTL命令参数设置错误,导致扫描方向、RGB顺序不对。 | 仔细查阅ST7735S数据手册中关于MADCTL寄存器的说明,调整MY、MX、MV、RGB等位的值进行测试。通常有0x00, 0xA0, 0xC0, 0x60等几种常见组合。 |
| 画点位置不正确 | 1. 坐标系理解错误(屏幕原点位置)。 2. 窗口设置函数 TFT_SetWindow有bug。3. 屏幕物理偏移。 | 1. 编写测试代码,分别在(0,0), (maxX,0), (0,maxY), (maxX,maxY)画点,观察位置。 2. 有些屏幕控制器GRAM大小大于可视区域,需要设置偏移量(XSTART, YSTART)。在初始化序列中查找 0x36命令或相关的偏移设置命令。 |
| 使用DMA时显示乱码或错位 | 1. DMA传输未完成就进行了其他操作(如拉高CS)。 2. 内存缓冲区数据格式错误。 3. DMA传输大小或内存地址对齐问题。 | 1. 确保在DMA传输完成回调函数中再进行后续操作(如结束传输、操作其他外设)。 2. 检查颜色缓冲区数据是否是正确的RGB565高低字节顺序。 3. 确保DMA传输的数据长度是偶数(因为每个像素2字节)。 4. 尝试关闭Cache(如果单片机有的话)或使用非缓存内存区域。 |
| 软件SPI显示不稳定,时好时坏 | 时序不满足屏幕要求,受中断干扰。 | 1. 增加软件SPI位之间的延时(__NOP())。2. 在软件SPI的关键传输函数前后关闭全局中断 __disable_irq()和开启__enable_irq(),避免被高优先级中断打断时序。 |
调试必备工具:
- 逻辑分析仪:几十块钱的8通道逻辑分析仪是调试SPI、I2C等数字通信的利器。可以清晰看到CS、DC、SCK、MOSI上的每一个波形,对照数据手册分析是否完全符合时序要求。
- 万用表:检查电源和连接。
- ST-Link + IDE调试器:单步执行,查看变量,设置断点,观察程序流。
我的几点深刻教训:
- 初始化延时是玄学:数据手册上要求
SLPOUT后延迟120ms,我试过有些模块延迟50ms也能工作,有些模块延迟150ms才稳定。宁多勿少,在初始化函数里多加点HAL_Delay,成功后再尝试缩减。 - DC线是关键中的关键:我曾因为一根接触不良的杜邦线,导致DC信号时高时低,屏幕显示乱七八糟的图案,排查了半天。确保所有连接牢固。
- 硬件SPI的NSS(CS)管理:STM32的硬件SPI的NSS引脚有硬件管理模式和软件管理模式。强烈建议使用软件控制GPIO来作为CS,这样更灵活,避免硬件自动控制带来的时序问题。这就是为什么我们在CubeMX中不配置SPI的NSS引脚,而是自己用普通GPIO控制的原因。
- 颜色字节序:RGB565在内存中的存储顺序,是高字节在前(R[4:0]+G[5:3])还是低字节在前(G[2:0]+B[4:0])?这需要和取模软件、屏幕驱动芯片保持一致。常见的错误是红蓝颜色反了。