DMA驱动LCD:让STM32的屏幕真正“活”起来
你有没有遇到过这样的场景?
在调试一个基于STM32F4的工业HMI面板时,明明主频168MHz,FreeRTOS跑得飞快,可一打开GUI界面,滑动列表就卡顿、触控响应像隔了一层毛玻璃;用逻辑分析仪抓SPI波形,发现CPU在疯狂轮询SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE)——每发一个像素,都要等一次标志位,再搬一个16-bit数据……全屏刷新要40多毫秒,帧率刚过20,用户还没点第二下,系统已经忙着调度看门狗喂食了。
这不是代码写得烂,而是掉进了嵌入式图形开发最经典的陷阱:把显存当GPIO来刷。
而破局的关键,不在算法优化,也不在换更快的MCU,而在打开那个常年被忽略的外设——DMA。
为什么CPU刷屏注定是瓶颈?
先看一组硬数据(实测于STM32F407VGT6 + ILI9341 SPI接口LCD):
| 刷新方式 | 分辨率 × 色深 | 全屏耗时 | CPU占用率 | 实际帧率 |
|---|---|---|---|---|
| 软件轮询(GPIO模拟SPI) | 320×240 × 16bpp | ~115 ms | >95% | <9 FPS |
| 标准HAL_SPI_Transmit() | 同上 | ~42 ms | ~87% | ~24 FPS |
| DMA + Circular Mode | 同上 | ≤8.3 ms | <12% | ≥120 FPS(理论吞吐) |
注意:这里的“≤8.3ms”不是指DMA传输完一帧的时间,而是从CPU发起刷新请求,到最后一行像素稳定显示在LCD上的端到端延迟。它包含DMA搬运+SPI物理层建立+LCD控制器内部锁存+像素点亮全过程。而CPU在这段时间里,可以去算PID、解Modbus、收CAN报文,甚至睡个回笼觉。
关键在哪?
不是DMA本身有多神,而是它把“搬运工”的角色,从需要思考、判断、等待的CPU,换成了只认地址、长度、触发信号的纯硬件状态机。
CPU负责“决策”,DMA负责“执行”——这才是嵌入式实时系统的本分。
真正决定体验上限的,从来不是带宽,而是同步
很多工程师配置完DMA,发现画面撕裂、颜色错乱、偶尔闪屏,第一反应是:“DMA配置错了?”
其实更大概率是:没管好“谁在什么时候改显存”这件事。
举个最典型的例子:
你在VSYNC信号刚到来时,调用memcpy()往前台显存写新内容,而LTDC DMA正在同一块内存里读像素——结果就是前半帧是旧图,后半帧突然跳成新图,画面中间一道清晰的横线,俗称“撕裂”。
所以,双缓冲不是可选项,而是必选项;而VSYNC同步,不是建议做法,而是唯一可靠路径。
STM32 LTDC的精妙之处在于:它把“显存地址切换”这个动作,做成了寄存器级原子操作。你只需在VSYNC中断里改一行寄存器:
// 切换图层0的帧缓冲基址(硬件立即生效,无指令周期延迟) LTDC_Layer1->CFBAR = (uint32_t)new_front_buffer;这行代码执行完,下一帧开始,LTDC DMA就自动从新地址取数据。整个过程不依赖内存屏障、不需要关中断、不涉及Cache刷新——因为LTDC的DMA引擎和CPU的AXI总线是并行挂载在同一个互连矩阵上的,地址更新对DMA控制器是即时可见的。
但这里埋着一个极易被忽视的坑:
如果你的显存放在开启了D-Cache的SRAM中(比如H7的AXI-SRAM),而CPU刚用memcpy()写完后台缓冲区,DMA却从Cache里读到了旧值——那切过去的就是一堆脏数据。
解决方案不是关Cache(性能损失太大),而是精准清理:
// 写完后台缓冲区后,强制将对应内存区域写回并失效Cache行 uint32_t addr = (uint32_t)back_buffer; uint32_t size = WIDTH * HEIGHT * 2; // 16bpp SCB_CleanInvalidateDCache_by_Addr((uint32_t*)&addr, size);这行代码干了两件事:先把CPU修改过的缓存行写回内存(Clean),再让后续DMA读取时必须从内存取(Invalidate)。少了任意一步,都可能看到诡异的“局部花屏”。
SPI LCD也能玩转DMA?关键在“伪并行”时序控制
有人会说:“LTDC是高端货,我用的是F4系列+SPI接口的ILI9341,没LTDC,难道只能认命?”
完全不必。SPI LCD的DMA优化,核心思路是:把SPI外设当成一个‘可编程的并行总线’来用。
ILI9341这类芯片,本质上是通过SPI接收命令+数据流,内部有一个并行RGB接口连接到TFT面板。它的关键时序约束只有一个:WR引脚(或等效的SPI SCLK边沿)必须满足最小脉冲宽度与周期。
而STM32的SPI外设,在DMA配合下,能极精准地控制SCLK频率与占空比。例如配置SPI为:
BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2(主频168MHz → SCLK=84MHz)DataSize = SPI_DATASIZE_16BITFirstBit = SPI_FIRSTBIT_MSB(匹配ILI9341的高位先行)TIMode = ENABLE(启用TI模式,使SCLK严格跟随DMA请求)
此时,DMA每送一个16-bit像素,SPI硬件就自动发出一个完整SCLK周期——相当于用串行线模拟出了并行WR/Strobe信号。
更进一步,你可以利用SPI的NSS(片选)信号,配合DMA传输完成中断,实现“命令+数据”流水线:
// 先发命令(如0x2C,开始写GRAM) HAL_SPI_Transmit(&hspi1, cmd_buf, 1, HAL_MAX_DELAY); // 立即启动DMA发送显存数据(自动拉低NSS,保持片选) HAL_DMA_Start(&hdma_spi_tx, (uint32_t)framebuffer, (uint32_t)&hspi1.Instance->TXDR, pixel_count); __HAL_SPI_ENABLE(&hspi1); // 此刻SPI开始工作,DMA自动供数这样,命令和数据之间零延迟,避免了传统方式中因软件延时导致的命令解析错误。
不是所有DMA通道都生而平等:总线仲裁才是隐藏Boss
当你把LTDC、SDMMC、USB、ETH全开DMA,却发现LCD突然开始掉帧,别急着骂驱动——先看一眼DMA请求优先级与总线带宽分配。
STM32H7的DMA架构是分层的:
- 专用DMA(如LTDC DMA)直连AXI总线,带宽独享,延迟最低;
- 通用DMA(如DMA1/DMA2)走AHB总线,需与CPU、Cache、其他外设争抢带宽;
- 更致命的是:如果SDMMC在DMA读SD卡,同时LTDC DMA也在刷屏,而两者都挂在同一AHB从设备上,就会触发总线仲裁,造成LTDC突发传输被打断,表现为画面横向撕裂或局部闪烁。
解决办法很直接:
- 将LTDC DMA通道设为DMA_PRIORITY_HIGH(H7上最高为DMA_PRIORITY_VERY_HIGH);
- 在RCC_PeriphCLKInitStruct中,给LTDC时钟源(如PLL2_Q)预留足够裕量(建议≥120MHz);
- 若使用外部SDRAM作显存,务必启用FMC_SDRAM->BTCR[1]中的WRITE_PROTECTION位,并确保SDRAM刷新周期(REFRESH_RATE)设置合理(H7典型值:8192 refreshes/64ms → 7.8μs间隔),否则DMA突发读取会与刷新冲突,导致显存数据损坏。
这些细节不会写在HAL库文档里,但它们真实决定了你的屏幕是丝滑还是幻灯片。
显存怎么放?这是比DMA配置更值得深思的问题
新手常问:“我要支持480×272分辨率,显存该malloc多大?”
答案不是简单算480×272×2,而是要回答三个问题:
显存放在哪?
- 内部SRAM?太小(F4只有192KB,双缓冲直接爆掉);
- CCM RAM?H7有256KB,但不支持DMA访问(除非用AXI-SRAM);
- 外部SDRAM?带宽足,但需FMC初始化、时序校准、刷新管理;
- AXI-SRAM(H7特有)?最佳选择:64KB~512KB,支持DMA+Cache+零等待,但需在链接脚本中显式分配.lcd_fb段。要不要压缩布局?
对于静态UI(如仪表盘背景图),可预存为RLE编码或索引色图(CLUT),运行时由LTDC硬件解码——显存占用直降60%,且LTDC的CLUT查找是纯硬件流水线,不占CPU。局部刷新真省时间吗?
表面看,只刷一个按钮区域(如100×50像素)比全刷快5倍。但实际中,你要:
- 维护脏矩形链表(CPU开销);
- 计算DMA起始地址与长度(乘法+偏移);
- 可能触发多次DMA重配置(比循环模式启动慢3~5倍);
- 若脏区域分散,DMA突发传输效率暴跌。
工程经验:当脏区域面积 > 显存15%,全刷反而更快;当<5%且位置集中,局部刷才有意义。这个阈值,必须用示波器实测DMA启动延迟+传输时间才能标定。
最后一句实在话
DMA驱动LCD,技术门槛其实不高——HAL库几行配置就能点亮。
但把它用到产品级稳定、流畅、低功耗,考验的是你对存储器映射、总线协议、时序约束、Cache行为、中断嵌套、电源模式切换这一整套底层机制的理解深度。
它不教你怎么画按钮,但它决定了你画的按钮,能不能在16ms内出现在用户眼前;
它不帮你写通信协议,但它腾出的CPU资源,让Modbus、CAN FD、BLE Mesh能同时跑满而不丢包;
它不承诺续航多久,但当你把CPU塞进Stop模式,只留LTDC和DMA维持待机画面时,那多出来的2.3倍电池寿命,就是用户对你产品的无声认可。
如果你正在为HMI卡顿发愁,不妨今晚就打开CubeMX,勾选LTDC+DMA,把那块闲置已久的LCD,真正变成系统的眼睛,而不是拖垮实时性的累赘。
你试过DMA驱动LCD后,帧率提升最明显的是哪个场景?欢迎在评论区聊聊你的实战踩坑与破局时刻。