STM32F767 SPI DMA驱动ILI9341高速刷新实战
简介本资源面向嵌入式开发工程师与STM32学习者提供一套基于STM32F767的SPI DMA高速驱动9341 TFT液晶屏的完整工程方案重点解决刷屏速率低、CPU占用高的问题。工程通过SPI外设配合DMA控制器实现像素数据自动搬运SPI时钟配置至54MHz实测刷屏频率可达20Hz并包含9341初始化序列、颜色模式与扫描方向设置等关键环节。压缩包共1099个文件以591个c源文件、275个h头文件、51个s汇编文件为主另含icf链接脚本、uvprojx与ioc工程配置及hex固件整体约54.23MB目录涵盖Drivers、MDK-ARM与Core等模块结构清晰便于二次开发。目前已有2121人学习下载适合希望掌握SPI DMA刷屏、优化显示性能的中高级开发者参考借鉴。1. STM32F767 SPI DMA 9341高速刷新从刷屏卡顿到流畅动画的分水岭如果你用 STM32F767 驱动过 ILI9341 这类 240×320 的 TFT 屏大概率经历过这个场景主循环里调HAL_SPI_Transmit刷一屏纯色示波器上 SCLK 跑得挺欢但实际帧率只有个位数动画像幻灯片。问题不在 SPI 时钟而在 CPU 被逐字节搬运数据这件事绑死了。STM32F767 的 SPI1 挂 APB2理论可以跑到 54MHz 以上配合 DMA 双缓冲把整屏数据交给硬件搬运CPU 只负责准备下一帧——这才是「高速刷新」的真正含义。这篇笔记面向已经能点亮 9341、但被刷新率卡住的嵌入式开发者把 SPIDMA 的配置、9341 的初始化时序、双缓冲切换和踩坑点一次讲透目标是让你把 240×320 全屏刷新从十几毫秒压到 5ms 以内跑出可用的动画效果。2. 为什么 9341 刷不快SPI 阻塞发送与 DMA 搬运的本质差别2.1 阻塞发送到底慢在哪先算一笔账。240×320 分辨率RGB565 每像素 2 字节一屏就是 153600 字节。SPI 时钟设到 27MHz理论上每秒能传 3.375MB一屏耗时约 45.5ms。但这是理想值实际用HAL_SPI_Transmit阻塞发送时CPU 要等 TXE 标志、写 DR、等 BSY 清零每个字节都有函数调用开销和循环判断。实测下来27MHz 下阻塞发送一屏往往要 60ms 以上帧率不到 16fps而且这期间 CPU 完全被占用没法处理触摸、传感器或动画逻辑。更隐蔽的问题是 HAL 库的HAL_SPI_Transmit内部有超时判断和状态机切换每次调用都有几十个时钟周期的额外开销。如果你还用了HAL_SPI_TransmitReceive或者逐字节while轮询开销更大。所以「SPI 时钟高」和「刷新快」之间隔着一整个 CPU 搬运效率的鸿沟。2.2 DMA 把 CPU 从搬运工变成调度员DMA 的思路很直接把「从内存取数据写到 SPI 数据寄存器」这件事交给 DMA 控制器CPU 只配置一次源地址、目标地址和传输长度然后就可以去干别的。STM32F767 的 DMA2 支持内存到外设的传输SPI1_TX 有对应的 DMA 请求通道。配置好之后SPI 每次 TXE 置位DMA 自动搬一个字节过去CPU 完全不参与。关键收益有两个第一传输期间 CPU 空闲可以准备下一帧数据或处理其他任务第二DMA 搬运没有函数调用开销实际吞吐更接近 SPI 时钟的理论值。27MHz 下单屏 45ms 左右如果 SPI 能跑到 54MHz单屏可以压到 22ms 上下。再配合双缓冲和局部刷新动画帧率能上到 30fps 以上。2.3 9341 的写时序对 DMA 的约束ILI9341 的 8080 并口时序和 SPI 时序不同SPI 模式下它靠 CS 片选、D/C 数据命令切换、SCLK 和 MOSI 四根线工作。写一帧图像前要先发命令0x2CMemory Write然后连续发像素数据。D/C 线在命令阶段为低数据阶段为高。用 DMA 时D/C 线的切换必须由软件在启动 DMA 前完成DMA 传输过程中 D/C 保持不变。这意味着你不能把命令和像素数据混在同一个 DMA 传输里除非用两个 DMA 流或者手动分两次。常见做法是先拉低 D/C 发命令0x2C再拉高 D/C 启动 DMA 发像素数据。CS 线可以在整个过程中保持低电平也可以每帧拉低一次。注意 9341 的 CS 建立时间和保持时间太快会导致命令丢失。3. 用 SPI1DMA2 在 F767 上跑通 9341 的最小工程3.1 CubeMX 配置与时钟树要点在 CubeMX 里选好 STM32F767 芯片后SPI1 配置为 Transmit Only Master数据宽度 8 位CPOLLowCPHA1Edge预分频器先设 4 分频。F767 的 SPI1 挂 APB2如果 APB2 时钟 108MHz4 分频后 SCLK 为 27MHz。想跑更快可以设 2 分频到 54MHz但要注意 9341 模块的走线和排线质量太长或没屏蔽的杜邦线在 54MHz 下容易出错。DMA 配置里给 SPI1_TX 添加 DMA2 Stream3 或 Stream5方向 Memory to Peripheral数据宽度 Byte模式 Normal优先级 High。中断可以先不开用轮询 DMA 传输完成标志。GPIO 方面SCK、MOSI 配复用推挽CS、D/C、RST 配普通推挽输出初始电平拉高。// SPI1 初始化关键参数CubeMX 生成后手动确认 hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; hspi1.Init.Direction SPI_DIRECTION_1LINE; // 只用 MOSI 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; // 27MHz hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; hspi1.Init.TIMode SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation SPI_CRCCALCULATION_DISABLE;这段配置里SPI_DIRECTION_1LINE表示只发不收省一根 MISO 线。NSS用软件控制因为 CS 由 GPIO 手动拉。BaudRatePrescaler是唯一影响刷新速度的参数改小就快但稳定性下降。建议先用 4 分频调通再逐步降到 2 分频。3.2 9341 初始化序列与命令发送封装9341 上电后需要一段初始化序列包括电源控制、伽马校正、内存访问控制等。这些命令通过 SPI 发送D/C 拉低表示命令拉高表示数据。封装两个基础函数// 发送一个命令字节 void LCD_WriteCmd(uint8_t cmd) { LCD_DC_LOW(); // D/C 拉低命令模式 LCD_CS_LOW(); // 片选使能 HAL_SPI_Transmit(hspi1, cmd, 1, 10); LCD_CS_HIGH(); } // 发送一个数据字节 void LCD_WriteData(uint8_t data) { LCD_DC_HIGH(); // D/C 拉高数据模式 LCD_CS_LOW(); HAL_SPI_Transmit(hspi1, data, 1, 10); LCD_CS_HIGH(); }初始化序列里比较关键的是0x36Memory Access Control和0x3APixel Format。0x36的参数决定屏幕扫描方向RGB 顺序和行列交换都在这里设。0x3A设成0x55表示 RGB565。这两个参数设错会导致颜色反了或者图像错位是新手最容易翻车的地方。3.3 启动 DMA 刷一屏纯色的完整代码刷纯色是最简单的验证方式不需要准备图像数据直接用一个填充好的缓冲区。下面代码把 153600 字节的缓冲区通过 DMA 发到屏幕#define LCD_WIDTH 240 #define LCD_HEIGHT 320 #define LCD_PIXELS (LCD_WIDTH * LCD_HEIGHT) // 双缓冲每个缓冲区一屏大小 static uint8_t lcd_buf[2][LCD_PIXELS * 2] __attribute__((aligned(4))); // 用指定颜色填充一屏并启动 DMA 传输 void LCD_FillScreen_DMA(uint8_t buf_idx, uint16_t color) { uint32_t i; uint8_t *p lcd_buf[buf_idx]; // 填充 RGB565 颜色高字节在前 for (i 0; i LCD_PIXELS; i) { p[i * 2] color 8; p[i * 2 1] color 0xFF; } // 设置列地址和页地址限定刷新区域 LCD_SetWindow(0, 0, LCD_WIDTH - 1, LCD_HEIGHT - 1); // 发写内存命令 0x2C LCD_WriteCmd(0x2C); // D/C 拉高CS 拉低启动 DMA LCD_DC_HIGH(); LCD_CS_LOW(); HAL_SPI_Transmit_DMA(hspi1, p, LCD_PIXELS * 2); }LCD_SetWindow是设置 9341 的列地址和页地址对应命令0x2A和0x2B。刷全屏时设成 0 到 239 和 0 到 319。HAL_SPI_Transmit_DMA启动后立即返回CPU 可以继续执行。传输完成后 HAL 会调用HAL_SPI_TxCpltCallback在这个回调里拉高 CS并可以启动下一帧。注意缓冲区要 4 字节对齐DMA 访问非对齐内存会降低效率甚至出错。__attribute__((aligned(4)))是 GCC 的写法IAR 下用#pragma data_alignment。4. 双缓冲与局部刷新把帧率再往上推一档4.1 双缓冲切换的时机与标志位管理单缓冲的问题是DMA 正在发送时CPU 不能改缓冲区否则会撕裂画面。双缓冲让 CPU 在 DMA 发缓冲区 A 时填充缓冲区 BDMA 完成后切换。关键是切换时机——必须在 DMA 传输完成中断里切换不能在主循环里轮询否则可能提前覆盖正在发送的缓冲区。static volatile uint8_t dma_busy 0; static volatile uint8_t front_buf 0; // DMA 传输完成回调 void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { LCD_CS_HIGH(); // 拉高片选结束本次传输 dma_busy 0; // 标记空闲 front_buf ^ 1; // 切换前台缓冲 } } // 主循环里检查并启动下一帧 void LCD_Update(void) { if (!dma_busy) { dma_busy 1; LCD_FillScreen_DMA(front_buf, current_color); } }dma_busy用volatile修饰因为它在中断和主循环里都被访问。front_buf异或 1 实现 0/1 切换。这个结构下CPU 填充缓冲区和 DMA 发送是并行的帧率取决于两者中较慢的那个。4.2 局部刷新怎么算窗口和偏移全屏刷新 153600 字节即使 54MHz 也要 22ms。如果只改屏幕上一小块区域比如一个 64×64 的图标数据量只有 8192 字节耗时不到 1.5ms。局部刷新的关键是正确设置 9341 的窗口// 设置刷新窗口x0,y0 左上角x1,y1 右下角 void LCD_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { LCD_WriteCmd(0x2A); // 列地址设置 LCD_WriteData(x0 8); LCD_WriteData(x0 0xFF); LCD_WriteData(x1 8); LCD_WriteData(x1 0xFF); LCD_WriteCmd(0x2B); // 页地址设置 LCD_WriteData(y0 8); LCD_WriteData(y0 0xFF); LCD_WriteData(y1 8); LCD_WriteData(y1 0xFF); LCD_WriteCmd(0x2C); // 写内存 }窗口设好后后续像素数据会按从左到右、从上到下的顺序填入这个矩形区域。局部刷新时缓冲区只需要存这块区域的数据大小是(x1-x01)*(y1-y01)*2。注意 9341 的窗口坐标是包含边界的x1 和 y1 是闭区间。4.3 用 DMA 传输局部区域的参数计算局部刷新的 DMA 传输长度不是固定的要根据窗口大小算。下面是一个刷新矩形的函数// 刷新一个矩形区域data 里是按行排列的 RGB565 数据 void LCD_BlitRect_DMA(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint8_t *data) { LCD_SetWindow(x, y, x w - 1, y h - 1); LCD_DC_HIGH(); LCD_CS_LOW(); HAL_SPI_Transmit_DMA(hspi1, data, w * h * 2); }w * h * 2是字节数DMA 按字节传输。如果 w 或 h 是奇数总字节数可能是奇数但 DMA 字节传输不要求偶数对齐。注意data的排列顺序必须和窗口扫描顺序一致否则图像会错位。9341 默认是左上到右下逐行扫描如果你的数据是列优先的需要在0x36命令里改扫描方向。5. 高速刷新避坑5 个让画面撕裂、花屏、丢命令的现场5.1 花屏且颜色错乱D/C 线在 DMA 传输中被意外拉低现象是屏幕出现随机彩色条纹或者图像整体偏色。原因通常是 D/C 线在 DMA 传输过程中被其他代码改动比如中断里操作了同一个 GPIO。DMA 传输期间 D/C 必须保持高电平任何翻转都会让 9341 把像素数据当命令解析。解决方法是把 D/C 的 GPIO 操作集中管理DMA 传输期间禁止其他任务碰这个引脚或者用硬件 SPI 的 NSS 加软件 D/C 的组合确保时序不被中断打断。5.2 画面撕裂双缓冲切换发生在 DMA 完成之前现象是屏幕上半部分是旧帧、下半部分是新帧。原因是主循环里提前切换了缓冲区DMA 还在发旧数据时 CPU 已经改了同一块内存。解决方法是严格在HAL_SPI_TxCpltCallback里切换front_buf并且用dma_busy标志阻止重入。如果用了 RTOS注意回调在中断上下文不能调用阻塞 API。5.3 传输卡死DMA 流被重复启动导致状态机锁死现象是第一次刷新正常第二次调用HAL_SPI_Transmit_DMA后没有任何反应。原因是上一次 DMA 还没完成就再次启动HAL 库的状态机卡在 BUSY。解决方法是每次启动前检查hspi1.State是否为HAL_SPI_STATE_READY或者用dma_busy标志做软件互斥。另外DMA 传输完成后要调用HAL_SPI_TxCpltCallback里的清理逻辑确保 SPI 状态回到 READY。5.4 高时钟下偶发丢命令CS 建立时间不足现象是 27MHz 正常切到 54MHz 后初始化偶尔失败屏幕不亮或颜色异常。原因是 CS 拉低到第一个 SCLK 边沿之间的建立时间不够9341 来不及识别命令。解决方法是 CS 拉低后加几个 NOP 延时或者降低 SPI 时钟到 36MHz 左右。另一个办法是 CS 在整帧传输期间保持低电平只在帧间拉高减少切换次数。5.5 缓冲区数据错位DMA 源地址非对齐或长度不是 2 的倍数现象是图像整体偏移几个像素或者最后几个像素颜色不对。原因是 DMA 配置为字节传输时如果源地址不是 4 字节对齐F767 的 DMA 可能按字访问导致数据错位。解决方法是缓冲区用aligned(4)声明传输长度确保是偶数。RGB565 每像素 2 字节只要像素数正确长度自然是偶数。6. 把刷新率再压榨 30%DMA 传输完成中断里的一个隐藏技巧前面所有配置跑通后全屏刷新大概在 22ms 到 45ms 之间取决于 SPI 时钟。但还有一个容易被忽略的优化点DMA 传输完成中断的响应延迟。HAL 库的HAL_SPI_TxCpltCallback是在 DMA 传输完成中断里调用的但从 DMA 完成到 SPI 状态清理、再到回调执行中间有几十个时钟周期的开销。如果每帧都等这个回调再启动下一帧帧间会有固定间隙。我一般会在 DMA 传输快结束时提前准备下一帧。具体做法是用 DMA 的半传输中断HAL_SPI_TxHalfCpltCallback在传输到一半时就开始填充下一块缓冲区。这样当全传输完成中断到来时下一帧数据已经就绪可以立即启动下一次 DMA帧间间隙几乎为零。// 半传输回调提前准备后半段数据 void HAL_SPI_TxHalfCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { // 此时前半段已发完可以安全修改后半段缓冲区 // 如果做动画可以在这里计算下一帧的后半部分 } } // 全传输回调立即启动下一帧 void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { LCD_CS_HIGH(); dma_busy 0; front_buf ^ 1; // 如果下一帧数据已就绪直接在这里启动 if (next_frame_ready) { dma_busy 1; LCD_FillScreen_DMA(front_buf, next_color); } } }这个技巧的代价是代码复杂度上升半传输中断和全传输中断的缓冲区边界要算清楚否则会写出越界访问。我自己的习惯是先用全传输中断把功能跑稳确认帧率瓶颈确实在帧间间隙上再引入半传输优化。另外如果 SPI 时钟已经跑到 54MHz全屏 22ms 里 DMA 传输占大头帧间间隙可能只有 1ms 到 2ms优化收益有限但如果做局部刷新单次传输只有几百微秒帧间间隙占比就很高这时候半传输优化效果明显。还有一个验证方法用 GPIO 翻转做标记在 DMA 启动时拉高一个测试引脚在传输完成回调里拉低用示波器看高电平持续时间。如果高电平时间和理论传输时间差很多说明中断响应或 HAL 状态清理有开销。我曾在某块 F767 开发板上测到 27MHz 全屏传输理论 45ms实际高电平 52ms多出来的 7ms 就是 HAL 库的状态机开销和中断延迟。把 SPI 时钟提到 54MHz 后理论 22ms实际 26ms开销占比反而更小所以高时钟下 HAL 开销相对可接受。最后说一个血泪教训不要在没有示波器的情况下盲目调 SPI 时钟。我曾经把预分频器从 4 改成 2屏幕直接花屏以为是 DMA 配置问题查了半天才发现是杜邦线太长导致信号完整性下降。换短排线后 54MHz 稳定运行。所以调高速刷新时先确认硬件走线再动软件参数。希望帮到你。本文还有配套的精品资源点击获取