1. 项目概述:从点灯到刷屏的进阶之路
玩STM32的朋友,从点灯入门后,第一个有成就感的项目往往就是驱动一块屏幕。当字符、图形甚至动画在屏幕上流畅显示时,那种满足感是无可替代的。而SPI接口的屏幕,因其引脚少、驱动相对简单,成为了许多嵌入式开发者的首选。但“驱动起来”和“驱动得好”之间,隔着好几座技术大山。这次,我就结合自己多次在项目里折腾SPI屏幕的经验,从最基础的GPIO模拟,到硬件SPI,再到终极的DMA+硬件SPI组合,来一次彻底的刷图方案总结。这不仅仅是三种方法的罗列,更是对嵌入式开发中如何平衡资源、性能和复杂度的深度思考。无论你是刚接触SPI的新手,还是正在为刷屏速度或CPU占用率发愁的老手,相信这篇总结都能给你带来一些直接的参考和启发。
2. 三种刷图方案的核心思路与选型考量
驱动SPI屏幕刷图,本质上就是向屏幕的显存(或GRAM)高速、准确地写入像素数据。根据STM32提供的不同硬件资源和我们对性能的需求,可以演化出三种典型的实现路径。选择哪一种,从来不是单纯追求性能最强,而是要放在具体项目的上下文里权衡。
2.1 GPIO模拟SPI:极致的灵活性与可控的代价
GPIO模拟,顾名思义,就是完全不用STM32内部的SPI硬件外设,而是手动控制几个通用输入输出引脚的电平变化,来模拟出SPI协议的时序。这种方法听起来很“原始”,但在很多场景下却是首选甚至唯一选择。
为什么需要GPIO模拟?首要原因就是硬件资源冲突。一块复杂的板卡上,SPI外设可能已经被其他更高优先级的设备(如Flash、传感器)占用了。其次是为了极致的时序控制。有些“非标”的屏幕或者器件,其SPI时序可能比较特殊,比如时钟极性和相位不是标准的0或1,或者需要在不该有时钟跳变的地方插入特殊延时,硬件SPI的固定模式可能无法满足,而GPIO模拟则可以精细操控每一个时钟边沿。最后,对于教学和深度理解协议而言,亲手用代码“画”出SPI的波形,是理解其本质的最佳方式。
它的核心思路很简单:用一组GPIO分别模拟SPI的SCK(时钟)、MOSI(主设备输出)、DC(数据/命令选择,通常也用GPIO模拟)和CS(片选)。然后,通过循环和位操作,将每一个数据位按照协议时序“吐”出去。例如,发送一个字节(8位)数据,就需要循环8次,每次先根据数据位的值(1或0)设置MOSI引脚电平,然后拉高再拉低SCK引脚产生一个时钟脉冲。
注意:GPIO模拟SPI时,务必仔细查阅屏幕数据手册对时序参数的要求,特别是
tSU(建立时间)和tHD(保持时间)。你的延时函数(Delay_us或nop循环)精度必须满足这些要求,否则可能导致通信失败。这是模拟方式最大的调试难点。
2.2 硬件SPI:解放CPU的标准化加速器
当你受够了GPIO模拟中那些精心调校的延时循环,或者屏幕刷新的速度成为瓶颈时,硬件SPI就该登场了。STM32内置的SPI外设是一个专为串行外设接口通信设计的硬件模块,你只需要配置好几个寄存器,它就能自动帮你完成时钟生成、数据移位和收发。
硬件SPI的核心优势在于“解放CPU”。在GPIO模拟中,CPU需要全程参与每一个时钟、每一个数据位的操作,期间几乎不能做其他事情。而硬件SPI模式下,CPU只需要把要发送的数据填进SPI的数据寄存器(DR),或者从DR里读出接收到的数据,剩下的时钟生成、数据移入移出等底层操作都由SPI外设自动完成。CPU在数据搬运间隙可以处理其他任务,或者进入低功耗模式。
选择硬件SPI,意味着你接受了标准的SPI协议(模式0/1/2/3,MSB/LSB先行等)。你需要根据屏幕手册,正确配置STM32 SPI的CPOL(时钟极性)、CPHA(时钟相位)、BaudRate(波特率)以及数据帧格式。一旦配置正确,其通信稳定性和速度上限远非GPIO模拟可比。以STM32F103在72MHz主频下,SPI时钟可以轻松达到18Mbps或36Mbps(取决于分频),而GPIO模拟受限于软件延时,通常很难超过2-3Mbps。
2.3 DMA+硬件SPI:追求极致吞吐量的终极方案
硬件SPI解放了CPU在比特位操作上的负担,但数据搬运的负担还在。例如,你要刷一张320x240的RGB565图片,那就是3202402 = 153,600字节的数据。CPU需要重复执行“从内存取一个数据 -> 写入SPI数据寄存器 -> 等待发送完成”这个循环15万次,这依然是一个巨大的开销。
此时,DMA(直接存储器访问)就该出场了。DMA是一个独立于CPU的数据搬运工,它可以在外设(如SPI的发送数据寄存器)和内存(如存放图片数据的数组)之间直接建立数据传输通道,完全不需要CPU介入。
DMA+硬件SPI的组合,是实现刷图性能飞跃的关键。你的操作流程变成了:1. 配置好SPI和DMA(告诉DMA数据从哪里来,到哪里去,传多少)。2. 启动DMA传输。然后,CPU就可以完全“撒手不管”,去处理其他更重要的任务,或者干脆休眠。SPI外设会通过DMA自动从内存中索取数据并发送出去,直到整个数据块传输完毕,DMA会产生一个中断通知CPU“活儿干完了”。
这种方案将CPU从繁重的、重复性的数据搬运工作中彻底解放出来,刷图过程对CPU的占用率几乎为0。这对于需要屏幕刷新与复杂业务逻辑(如用户交互、网络通信、算法处理)并行的系统至关重要,也是实现流畅动画和复杂UI的基石。当然,它的配置复杂度也是最高的,涉及到SPI、DMA、可能还有中断的协同工作,调试的难度也相应增加。
3. 核心细节解析与实操要点
理解了三种方案的宏观思路,接下来我们深入到每一种方案的实现细节、配置要点和那些容易踩坑的地方。
3.1 GPIO模拟SPI的精细控制与性能瓶颈
用GPIO模拟SPI,代码的掌控感最强,但魔鬼都在细节里。首先,引脚初始化必须设置为推挽输出模式,并且输出速度建议设置为最高(如GPIO_Speed_50MHz),以确保电平翻转的速度足够快。
发送一个字节的函数是核心。以SPI模式0(CPOL=0, CPHA=0)为例,通常的写法是时钟空闲为低,在时钟上升沿采样数据。那么发送一位的流程是:先设置MOSI为数据位电平 -> 短暂延时(满足建立时间tSU)-> 拉高SCK产生上升沿 -> 屏幕在上升沿采样 -> 拉低SCK完成一个时钟周期 -> 短暂延时(满足保持时间tHD)。循环8次。
// 简化示例,未包含DC和CS控制 void SPI_WriteByte(uint8_t data) { for(uint8_t i = 0; i < 8; i++) { if(data & 0x80) { MOSI_GPIO_Port->BSRR = MOSI_Pin; // 输出高 } else { MOSI_GPIO_Port->BRR = MOSI_Pin; // 输出低 } // 短暂延时,可用__NOP()或简易延时函数 Delay_Nanos(50); // 假设需要50ns建立时间 SCK_GPIO_Port->BSRR = SCK_Pin; // SCK 拉高,产生上升沿 // 这里屏幕采样数据 Delay_Nanos(50); // 时钟高电平保持时间 SCK_GPIO_Port->BRR = SCK_Pin; // SCK 拉低 data <<= 1; // 准备下一位 } }实操要点与坑点:
- 延时精度:
Delay_Nanos这种纳秒级延时很难用循环精确实现,通常需要根据系统时钟频率计算__NOP()指令的个数。更可靠的方法是使用定时器产生精确延时,但这又增加了复杂性。很多时候,我们是通过示波器测量波形,反复调整__NOP()的数量来逼近时序要求。 - DC和CS信号:对于屏幕,DC(Data/Command)引脚决定当前发送的是命令还是数据,通常也需要一个GPIO控制。CS(片选)引脚在传输开始时拉低,结束后拉高。务必注意CS的拉低和拉高时机,有些屏幕要求CS在整帧数据期间保持有效,而有些则要求每发送一个字节或一个命令/数据包都要翻转一次。
- 性能天花板:GPIO模拟的速度受限于循环、判断、位操作和软件延时的开销。即使将所有操作优化到极致,在百兆主频的MCU上,稳定的SCK频率也很难超过5MHz。对于刷全屏图片这种大数据量操作,会明显感觉到卡顿。
3.2 硬件SPI的配置迷宫与效率提升
切换到硬件SPI,首先要在CubeMX或直接操作寄存器进行正确配置。以下是关键配置项:
- 模式与极性:根据屏幕手册选择
CPOL和CPHA。最常见的是Mode 0(CPOL=0, CPHA=0)和Mode 3(CPOL=1, CPHA=1)。选错会导致屏幕无显示或显示乱码。 - 数据大小:通常是8位或16位。对于发送像素数据(RGB565),使用16位模式可以一次发送两个字节,效率更高。但要注意屏幕的GRAM接口是否支持16位连续写入。
- 波特率预分频:这是控制SCK速度的关键。公式为
SPI_SCK = APBx_CLK / Prescaler。在保证屏幕能稳定接收的前提下(参考屏幕手册的最大SCLK频率),尽可能提高波特率。例如,APB2时钟为72MHz,选择4分频,则SPI时钟为18MHz。 - NSS(片选)管理:硬件SPI的NSS引脚可以用硬件模式(输出)或软件模式。对于驱动屏幕,强烈建议使用软件模式,即把NSS配置为普通GPIO输出,手动控制。因为硬件NSS模式的行为可能不符合屏幕对CS信号的要求(例如,硬件NSS可能在每次数据传输间隙自动变高)。
- 全双工与只发送:刷屏只需要发送数据,不需要接收。因此可以将SPI配置为只发送(Transmit Only Master)模式,或者即使配置成全双工,也忽略接收到的数据。这可以避免不必要的接收缓冲区处理。
配置好后,发送数据的核心函数就是往SPI->DR寄存器写数据。在查询方式下,你需要先检查TXE(发送缓冲区空)标志,为空后才能写入下一个数据;或者等待BSY(忙)标志结束。在中断方式下,可以在TXE中断里写入下一个数据。
效率提升技巧:
- 使用16位数据模式:如果屏幕支持,将SPI数据宽度设为16位,并准备
uint16_t类型的像素数据数组。这样一次传输就能发送一个像素(RGB565),传输次数减半。 - FIFO与连续写入:充分利用SPI的TXFIFO。不要等发送完一个字节再写下一个,可以连续写入多个数据到
DR寄存器,让FIFO缓冲起来,提高总线利用率。 - 避免频繁检查标志位:在循环发送大量数据时,可以使用
while((SPI->SR & SPI_FLAG_TXE) == RESET);这样的忙等待,虽然CPU占用高,但代码简单。对于更复杂的系统,应采用中断或DMA。
3.3 DMA+硬件SPI的协同作战与内存管理
这是性能最优的方案,但配置环节多,需要理清SPI、DMA和内存三者之间的关系。
配置流程:
- SPI配置:与纯硬件SPI类似,但需要使能SPI的DMA发送请求。在CubeMX中,勾选“SPIx_TX DMA Request”。在标准库或HAL库中,调用
SPI_DMACmd(SPIx, SPI_DMAReq_Tx, ENABLE)。 - DMA配置:这是核心。
- 通道选择:每个SPI的TX请求对应固定的DMA通道(如SPI1_TX对应DMA1_Channel3)。必须查数据手册正确选择。
- 传输方向:内存到外设(Memory to Peripheral)。
- 外设地址:设置为SPI数据寄存器的地址(
(uint32_t)&(SPIx->DR))。 - 内存地址:设置为你的像素数据数组的首地址。这个地址必须是物理地址,且数据在内存中连续存放。
- 数据宽度:外设和内存的数据宽度要匹配。如果SPI是8位模式,这里都选Byte;如果是16位模式,都选HalfWord。不匹配会导致数据错乱。
- 传输模式:通常使用非循环模式(Normal),传输指定数量后停止。如果是要实现双缓冲或循环刷屏,可以考虑使用循环模式(Circular),但逻辑更复杂。
- 内存地址递增:必须使能,因为我们要连续发送数组中的多个数据。
- 外设地址非递增:SPI数据寄存器地址是固定的,所以不递增。
- 中断配置(可选但推荐):使能DMA传输完成中断(TCIE)。这样可以在传输结束后,在中断服务函数里进行后续操作,比如切换下一帧图像缓冲区,或者通知主程序刷屏完成。
启动传输:配置完成后,先启动DMA通道(DMA_Cmd(DMAy_Channelx, ENABLE)),然后启动SPI发送(SPI_Cmd(SPIx, ENABLE))。对于有些屏幕,需要在启动传输前先发送命令(如设置写GRAM的地址),这个命令发送通常还是用查询或中断方式,然后再用DMA发送像素数据。
内存管理的坑:
- 数据对齐:如果你的像素数组是
uint16_t类型,且首地址是2字节对齐的,那么DMA配置为HalfWord宽度就能高效工作。如果地址是奇数,可能导致对齐错误或性能下降。确保数组定义时编译器会将其对齐。 - 缓存一致性(对于Cortex-M7等带Cache的芯片):这是大坑!如果你使用了D-Cache(数据缓存),CPU写入内存(数组)的数据可能还留在Cache里,并未真正写入物理内存。此时DMA直接从物理内存取数据,拿到的是旧数据或随机数据。必须在启动DMA传输前,对发送数据缓冲区执行Cache清理(Clean)或无效化(Invalidate)操作。使用
SCB_CleanDCache_by_Addr()等函数。 - 缓冲区生命周期:确保在DMA传输期间,存放像素数据的数组(缓冲区)不能被释放或覆盖。通常需要将缓冲区定义为全局数组或静态数组。
4. 实操过程与核心环节实现
下面我将以STM32F407驱动一款常见的240x320 SPI屏幕(ILI9341)为例,分别展示三种方案的核心代码片段和关键操作。假设屏幕使用SPI Mode 0, 8位数据线,DC和CS为GPIO控制。
4.1 GPIO模拟SPI的完整发送函数
首先定义并初始化好SCK_Pin,MOSI_Pin,DC_Pin,CS_Pin及其对应的GPIO端口。
// 假设系统主频168MHz,一个__NOP()约6ns #define SPI_DELAY_FAST() do{ __NOP(); __NOP(); __NOP(); }while(0) // 约20ns延时 void SPI_WriteByte(uint8_t data) { for(uint8_t i = 0; i < 8; i++) { // 设置MOSI电平 if(data & 0x80) { HAL_GPIO_WritePin(MOSI_GPIO_Port, MOSI_Pin, GPIO_PIN_SET); } else { HAL_GPIO_WritePin(MOSI_GPIO_Port, MOSI_Pin, GPIO_PIN_RESET); } SPI_DELAY_FAST(); // 建立时间 // 产生时钟上升沿 HAL_GPIO_WritePin(SCK_GPIO_Port, SCK_Pin, GPIO_PIN_SET); SPI_DELAY_FAST(); // 数据采样时间 HAL_GPIO_WritePin(SCK_GPIO_Port, SCK_Pin, GPIO_PIN_RESET); SPI_DELAY_FAST(); // 保持时间 data <<= 1; } } void LCD_Write_Cmd(uint8_t cmd) { HAL_GPIO_WritePin(DC_GPIO_Port, DC_Pin, GPIO_PIN_RESET); // DC=0 命令 HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); // CS拉低 SPI_WriteByte(cmd); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); // CS拉高 } void LCD_Write_Data(uint8_t data) { HAL_GPIO_WritePin(DC_GPIO_Port, DC_Pin, GPIO_PIN_SET); // DC=1 数据 HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); SPI_WriteByte(data); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); } // 刷一个像素点(RGB565,两个字节) void LCD_DrawPoint(uint16_t x, uint16_t y, uint16_t color) { LCD_SetWindow(x, y, x, y); // 设置窗口为单个点(需实现此函数) LCD_Write_Data(color >> 8); // 发送高字节 LCD_Write_Data(color & 0xFF); // 发送低字节 }刷全屏就是循环调用LCD_DrawPoint或更高效地设置全屏窗口后连续发送数据,但用GPIO模拟刷全屏会非常慢。
4.2 硬件SPI(查询方式)刷屏示例
使用STM32CubeMX配置SPI1为8位数据,主模式,只发送,软件NSS。生成代码后。
// 发送一个字节(查询方式) void SPI_WriteByte(uint8_t data) { while((SPI1->SR & SPI_FLAG_TXE) == RESET); // 等待发送缓冲区空 SPI1->DR = data; // 可以不等待接收,因为我们只发送 // while((SPI1->SR & SPI_FLAG_RXNE) == RESET); // 等待接收完成(如果需要) // volatile uint8_t dummy = SPI1->DR; // 读走数据以清除标志 } // LCD写命令/数据的函数与GPIO模拟版本相同,只是底层SPI_WriteByte换成了硬件版本。 // 更高效的连续发送函数(用于刷一块区域) void LCD_Write_MultiData(uint8_t *pData, uint32_t Size) { HAL_GPIO_WritePin(DC_GPIO_Port, DC_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); for(uint32_t i = 0; i < Size; i++) { while((SPI1->SR & SPI_FLAG_TXE) == RESET); SPI1->DR = pData[i]; } // 可选:等待最后一个字节发送完成 while((SPI1->SR & SPI_FLAG_BSY) == SET); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); }这个版本比GPIO模拟快很多,但CPU仍然在while循环中等待。
4.3 DMA+硬件SPI刷屏实现
这是最复杂的,我们分步骤实现。假设使用SPI1,DMA1的Channel3(Stream3 for F4)用于SPI1_TX。
步骤1:CubeMX配置
- 在
Connectivity->SPI1中,模式为Transmit Only Master,硬件NSS选Disable。 - 在
DMA Settings标签页,点击Add,选择SPI1_TX,方向为Memory To Peripheral。 - 在
System Core->DMA中,配置添加的流(Stream)。模式为Normal,优先级High,内存地址递增,外设地址不递增,数据宽度根据SPI配置选择(如Half Word如果SPI是16位)。 - 在
NVIC Settings中,使能DMAx Streamx global interrupt。
步骤2:代码实现
uint16_t lcd_buffer[320*240]; // 定义一个全屏缓冲区(RGB565) void LCD_Write_MultiData_DMA(uint16_t *pData, uint32_t Size) { // 1. 等待上一次DMA传输完成(如果有) while(__HAL_DMA_GET_FLAG(&hdma_spi1_tx, DMA_FLAG_TCIF3_7)); // 2. 清理DMA传输完成标志(可选) __HAL_DMA_CLEAR_FLAG(&hdma_spi1_tx, DMA_FLAG_TCIF3_7); // 3. 停止DMA(如果正在运行) __HAL_DMA_DISABLE(&hdma_spi1_tx); // 4. 重新配置DMA:源地址、目标地址、数据量 hdma_spi1_tx.Instance->PAR = (uint32_t)&(SPI1->DR); // 外设地址 hdma_spi1_tx.Instance->M0AR = (uint32_t)pData; // 内存地址 hdma_spi1_tx.Instance->NDTR = Size; // 数据数量(单位取决于数据宽度) // 5. 清理Cache(对于M7内核至关重要!) #if defined (__DCACHE_PRESENT) && (__DCACHE_PRESENT == 1U) SCB_CleanDCache_by_Addr((uint32_t*)pData, Size * sizeof(uint16_t)); #endif // 6. 设置DC为数据,CS拉低 HAL_GPIO_WritePin(DC_GPIO_Port, DC_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); // 7. 使能DMA流,然后使能SPI的DMA发送请求 __HAL_DMA_ENABLE(&hdma_spi1_tx); __HAL_SPI_ENABLE(&hspi1); SET_BIT(hspi1.Instance->CR2, SPI_CR2_TXDMAEN); // 8. 主程序在此处返回,DMA在后台传输数据 } // DMA传输完成中断服务函数 void DMA1_Stream3_IRQHandler(void) { if(__HAL_DMA_GET_FLAG(&hdma_spi1_tx, DMA_FLAG_TCIF3_7)) { __HAL_DMA_CLEAR_FLAG(&hdma_spi1_tx, DMA_FLAG_TCIF3_7); // 传输完成,拉高CS HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); // 可以在这里设置一个标志,通知主程序刷屏完成,或者准备下一帧数据 g_lcd_dma_finish_flag = 1; } } // 主程序中使用 void main(void) { // ... 初始化 LCD_Init(); // 初始化屏幕,设置SPI为16位模式等 fill_screen_buffer(lcd_buffer, RED); // 将缓冲区填充为红色 LCD_SetWindow(0, 0, 239, 319); // 设置全屏窗口 LCD_Write_MultiData_DMA(lcd_buffer, 320*240); // 启动DMA传输 while(1) { if(g_lcd_dma_finish_flag) { // 刷屏完成,可以处理其他任务或准备下一帧 g_lcd_dma_finish_flag = 0; // ... 更新缓冲区内容 // ... 再次启动DMA传输 } // 主循环处理其他任务,刷屏过程不占用CPU时间 } }5. 常见问题与排查技巧实录
在实际操作中,无论采用哪种方案,都会遇到各种各样的问题。下面我整理了一份从现象到原因的排查清单,涵盖了从无显示到显示异常的大部分情况。
5.1 屏幕完全无显示(白屏、花屏、乱码)
这是最常见的问题,排查需要有条理。
| 现象 | 可能原因 | 排查步骤 |
|---|---|---|
| 上电后白屏 | 1. 电源或背光问题。 2. 复位或初始化序列不正确。 3. 根本未与MCU通信成功。 | 1. 用万用表测量屏幕VCC、GND、背光电压是否正常。 2. 用逻辑分析仪或示波器抓取SPI总线(SCK, MOSI)波形,看初始化命令(如软件复位、退出睡眠、像素格式设置)是否发出。与数据手册时序对比。 3. 检查CS、DC引脚电平在通信时是否正确变化。 |
| 显示全屏随机彩色斑点 | 1. SPI模式(CPOL/CPHA)设置错误。 2. 数据位顺序(MSB/LSB)错误。 3. 初始化未完成就发送像素数据。 | 1.这是高频问题!用示波器同时抓取SCK和MOSI。确定空闲时SCK电平(CPOL),以及数据在SCK的哪个边沿稳定(CPHA)。与屏幕手册核对。 2. 尝试修改SPI的 LSBFIRST位,或调整GPIO模拟中发送字节的位顺序(先发高位还是低位)。3. 确保发送了完整的初始化序列,并留有足够延时(参考手册的 tRES等参数)。 |
| 显示内容错位、撕裂 | 1. 刷屏速度太快,屏幕GRAM刷新跟不上。 2. DMA传输未完成就修改了显存缓冲区。 3. 设置显示窗口(X, Y坐标)错误。 | 1. 在发送像素数据命令后,增加适当延时,或等待屏幕的TE(撕裂效应)信号(如果支持)。2. 确保在DMA传输完成中断标志置位后,再更新或复用发送缓冲区。 3. 仔细检查设置窗口坐标的代码,确认起始和结束地址计算正确,特别是屏幕的坐标系原点(通常是左上角)。 |
5.2 通信不稳定,时好时坏
| 现象 | 可能原因 | 排查步骤 |
|---|---|---|
| 偶尔花屏或数据错误 | 1. 电源噪声干扰。 2. SPI时钟频率过高,信号质量差。 3. 导线过长或接触不良。 4. (DMA模式)Cache一致性问题。 | 1. 在屏幕电源引脚就近加一个10uF和一个0.1uF的电容。 2. 降低SPI波特率,看问题是否消失。用示波器观察SCK和MOSI波形,看是否有过冲、振铃或边沿不陡峭。 3. 检查杜邦线连接,尽量使用短线和排线。 4.(M7等核心)检查并确保在启动DMA前,对发送缓冲区执行了 SCB_CleanDCache_by_Addr。 |
| DMA传输部分数据后停止 | 1. DMA传输完成中断过早进入,数据未发完。 2. DMA缓冲区溢出或错误。 3. SPI或DMA时钟未使能。 | 1. 检查DMA传输完成(TC)中断标志是否在正确时刻置位。可以在中断里读取DMA的CNDTR寄存器,看剩余数据量是否为0。2. 检查DMA配置的源/目标地址、数据宽度、传输数量是否正确。检查是否有其他高优先级中断长时间阻塞导致DMA被暂停。 3. 检查 __HAL_RCC_SPI1_CLK_ENABLE()和__HAL_RCC_DMA1_CLK_ENABLE()是否被调用。 |
5.3 性能不达预期
| 现象 | 可能原因 | 优化方向 |
|---|---|---|
| GPIO模拟刷屏极慢 | 软件延时过长,循环开销大。 | 1. 使用寄存器直接操作(GPIOx->BSRR/BRR)代替HAL库函数,减少函数调用开销。2. 用 __NOP()内联汇编实现最小延时,并通过示波器校准,找到能满足屏幕时序要求的最少__NOP()数量。3. 如果可能,改用硬件SPI。 |
| 硬件SPI CPU占用高 | 使用查询方式(while(TXE))发送大量数据。 | 1. 改用DMA传输。 2. 如果不能用DMA,考虑使用SPI发送中断,在中断中填充下一个数据,主程序可以执行其他任务。 |
| DMA刷屏仍有卡顿 | 1. 准备下一帧数据的时间过长,超过了DMA传输时间。 2. 内存拷贝(如从图像解码到显存)耗时太长。 3. SPI时钟频率设置过低。 | 1. 使用双缓冲区(Ping-Pong Buffer):DMA在传输缓冲区A时,CPU准备缓冲区B的数据,传输完成后立即切换。 2. 优化图像生成或解码算法,使用硬件加速(如Chrom-ART DMA2D,如果MCU支持)。 3. 在屏幕允许范围内,提高SPI时钟分频。检查APB总线时钟是否已配置到最高。 |
5.4 特殊芯片与高级话题
对于STM32H7、GD32等更高性能或有细微差异的MCU,还有一些额外注意事项:
- STM32H7的Cache与MPU:除了前面提到的D-Cache清理,如果使用了MPU(内存保护单元),还需要确保DMA访问的内存区域具有正确的属性(通常是
Device或Normal Non-cacheable类型)。 - GD32的SPI与DMA:GD32与STM32高度兼容,但外设寄存器地址和部分行为可能有差异。例如,GD32的SPI可能需要不同的时钟使能位或状态标志检查方式,务必参考对应的GD32参考手册和库函数。
- SPI硬件片选(NSS)的坑:除非屏幕时序严格要求硬件NSS,否则建议用软件GPIO控制。硬件NSS可能在每次8位或16位数据传输后自动变高,这与很多屏幕要求CS在整帧数据期间保持低电平的特性不符。
- 与LVGL等图形库配合:当使用LVGL时,其
flush_cb回调函数需要尽快将指定区域的像素数据发送到屏幕。此时,DMA+硬件SPI几乎是必须的。你需要在这个回调函数中,根据lv_disp_drv_t参数计算出脏区(damaged area)的内存地址和大小,然后启动DMA传输。同时,要使用双缓冲和lv_disp_flush_ready()在DMA完成中断中通知LVGL刷新完成,以实现最流畅的UI体验。
折腾SPI屏幕刷图的过程,就是一个不断在资源、性能和复杂度之间做权衡和突破的过程。从GPIO模拟的知其然,到硬件SPI的知其所以然,再到DMA运用的游刃有余,每一步都伴随着问题的解决和理解的加深。我最深的体会是,工具很重要(逻辑分析仪和示波器是必备的),但更重要的是系统性的排查思路:电源->时钟->配置->时序->数据。先确保最基础的通信能建立(能看到正确的初始化命令波形),再去解决性能和稳定性的问题。最后,别忘了数据手册永远是你最好的朋友,屏幕的脾气都写在里面了。