让ST7735飞起来:用DMA-SPI实现丝滑绘图的实战指南
你有没有遇到过这种情况?
在STM32或ESP32上驱动一块1.8英寸的ST7735彩屏,明明代码写得没问题,初始化也成功了,但一动起来就卡顿——文字滚动像拖影,进度条刷新有撕裂,动画更是“一帧一停”。
问题不在你的GUI逻辑,也不怪屏幕质量。真正的瓶颈,藏在SPI数据搬运的方式里。
大多数教程教你用HAL_SPI_Transmit()或者软件模拟SPI逐字节发送像素数据,这在传输几KB还行得通,可当你面对整整40KB(128×160×2)的帧缓冲时,CPU瞬间被锁死。别说跑FreeRTOS了,连看个串口打印都延迟严重。
那有没有办法让MCU“解放双手”,把数据传输这件事彻底甩给硬件?
答案是:DMA + SPI 联动机制。
今天我们就来拆解如何通过DMA增强型SPI方案,让ST7735的绘图性能从“龟速”跃升至“接近理论极限”的高速通道,真正实现流畅渲染。
为什么ST7735总感觉“慢半拍”?
先别急着优化,我们得搞清楚“慢”到底出在哪。
常见误区:以为SPI够快就够了
很多人觉得:“我主频都80MHz了,SPI开到20MHz肯定没问题。”
可现实是:即使SPI时钟拉满,帧率依然只有十几FPS,远低于理论值(理想情况下应可达30+ FPS)。
原因很简单:
不是SPI不够快,而是CPU太忙。
传统方式中,每发送一个字节,都需要CPU参与:
- 写入SPI_DR寄存器
- 等待TXE标志置位
- 中断回调再写下一个字节
这个过程看似自动,实则频繁打断CPU执行流。以40KB图像为例,在10MHz SPI下传输需约32ms,期间若采用中断模式,将触发数万个中断;若轮询,则完全阻塞系统。
结果就是:CPU占用率飙升至90%以上,系统响应迟钝,功耗居高不下。
破局之道:让DMA接管数据搬运
要打破这一困局,关键在于绕过CPU,让硬件自己搬数据—— 这正是DMA的价值所在。
DMA到底做了什么?
DMA(Direct Memory Access)是一种独立于CPU的数据搬运引擎。它能直接从内存读取数据并写入外设寄存器,全程无需CPU干预。
当我们把SPI_TX和DMA通道绑定后,整个流程就变成了这样:
[Framebuffer] → [DMA控制器] → [SPI数据寄存器] → [SCLK/SDA引脚] → [ST7735]CPU只需要做三件事:
1. 配置DMA源地址(指向framebuffer)
2. 设置传输长度(如40960字节)
3. 启动传输
剩下的,全由硬件完成。
传输结束时,DMA会触发一次中断通知CPU:“我干完了。”
中间几十毫秒时间,CPU可以去做别的事:处理传感器、运行状态机、甚至进入低功耗睡眠。
实战解析:DMA-SPI驱动ST7735全流程
下面以STM32平台为例,带你一步步构建高性能绘图系统。
核心目标
- 实现整屏刷新(128×160 RGB565)
- 使用DMA自动发送像素数据
- CPU仅负责启动与收尾
- 支持后续扩展为双缓冲/局部刷新
第一步:理解ST7735的绘图流程
虽然我们要加速的是“发数据”环节,但前后控制命令仍需精准执行。完整的快速绘图流程如下:
设置显示区域(Set Address Window)
c ST7735_SetAddressWindow(0, 0, 127, 159);
告诉ST7735接下来的数据将写入哪个矩形区域。发送“开始写像素”命令(0x2C)
c ST7735_WriteCommand(0x2C); // Write Memory Start切换DC引脚为“数据模式”
c HAL_GPIO_WritePin(DC_PORT, DC_PIN, GPIO_PIN_SET); // RS = 1启动DMA-SPI传输
c HAL_SPI_Transmit_DMA(&hspi1, (uint8_t*)fb, 128*160*2);
注意:这里必须将16位RGB565数组转为8位指针,因为SPI按字节传输。
第二步:配置DMA-SPI联动
使用STM32CubeMX配置更高效,但我们也要明白底层发生了什么。
关键配置项:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| SPI Mode | Master Full-Duplex | 主机模式 |
| Clock Prescaler | fpclk / 4 ~ /8 | 对应10~20MHz速率 |
| Data Size | 8 Bits | 多数DMA不支持直接16位传输 |
| NSS Control | Software | 可控CS片选 |
| DMA Request | TX Only | 仅启用发送DMA |
DMA通道设置:
- 源地址:
framebuffer起始地址(强制转换为uint8_t*) - 目标地址:
&SPI1->DR - 数据宽度:Byte
- 缓冲区大小:40960
- 内存增量模式:Enable
- 外设增量模式:Disable(固定写入SPI_DR)
第三步:核心代码实现(基于HAL库)
// 全局帧缓冲(建议放在SRAM中) uint16_t framebuffer[128][160]; volatile uint8_t dma_busy = 0; // 防止重复启动 /** * @brief 使用DMA刷新整个屏幕 */ void ST7735_FullScreen_DMA(void) { if (dma_busy) return; // 1. 设置全屏区域 ST7735_SetAddressWindow(0, 0, 127, 159); // 2. 发送写像素命令 ST7735_WriteCommand(0x2C); // 3. 切换为数据模式 HAL_GPIO_WritePin(ST7735_DC_PORT, ST7735_DC_PIN, GPIO_PIN_SET); dma_busy = 1; // 4. 启动DMA传输(拆分为字节流) uint8_t *tx_data = (uint8_t *)framebuffer; HAL_SPI_Transmit_DMA(&hspi1, tx_data, 128 * 160 * 2); } /** * @brief DMA传输完成回调函数 */ void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi->Instance == SPI1) { dma_busy = 0; // 可在此触发下一帧更新、唤醒GUI任务等 } }⚠️ 注意事项:
- 必须确保framebuffer位于DMA可访问的内存区域(避免放在stack或特殊SRAM段)
- 若使用RTOS,请在调用前加临界区保护或信号量同步
- 不要在回调中执行耗时操作,防止阻塞中断上下文
性能对比:传统 vs DMA模式
| 指标 | 传统SPI(轮询) | 中断SPI | DMA-SPI |
|---|---|---|---|
| 单帧传输时间(20MHz) | ~19ms | ~20ms | ~19ms |
| CPU占用率 | >95% | ~60% | <5% |
| 是否阻塞系统 | 是 | 否(但频繁中断) | 否 |
| 支持并发任务 | 否 | 有限 | 完全支持 |
| 功耗表现 | 高 | 中 | 低(可休眠) |
| 实际可用帧率 | 20~30 FPS(受限调度) | 30~40 FPS | 接近理论极限 |
看到没?虽然传输时间差不多,但CPU释放带来的系统级收益才是最大亮点。
这意味着你可以同时运行:
- FreeRTOS多任务
- 传感器采集(I2C/BLE)
- UI动画与触摸响应
- 日志输出与调试监控
而这一切都不会影响屏幕刷新的稳定性。
工程优化技巧:不只是“能用”
光实现还不够,以下是我在多个项目中总结的实用经验。
✅ 技巧1:合理管理帧缓冲
方案A:单缓冲(RAM紧张时)
- 直接绘制到
framebuffer - 刷新时DMA发送
- 缺点:可能出现画面撕裂(绘制和刷新同时进行)
方案B:双缓冲(推荐)
uint16_t front_buffer[128][160]; // 当前显示 uint16_t back_buffer[128][160]; // 正在绘制 // 渲染完成后交换 swap_buffers(); ST7735_FullScreen_DMA();- 绘制时不干扰当前画面
- 切换瞬间完成,无撕裂
- 需要80KB RAM → 适合带外部PSRAM的ESP32等平台
方案C:局部刷新(省流量)
只更新变化区域(dirty rect),大幅减少DMA传输量。
例如按钮按下仅刷新32×32区域:
ST7735_UpdateArea(10, 10, 42, 42, &back_buffer[10][10]);特别适合菜单界面、仪表盘等静态背景场景。
✅ 技巧2:SPI时钟调优
不要一上来就怼27MHz!ST7735官方支持最高27MHz,但实际受以下因素制约:
- MCU输出能力
- PCB走线质量(越长越容易反射)
- 屏幕模块内部电容负载
建议调试步骤:
1. 初始设为10MHz,确认图像正常
2. 逐步提升至16MHz、20MHz
3. 观察是否有花屏、偏色、闪屏现象
4. 在稳定前提下尽可能拉高频率
我的经验:多数FPC软排线连接的模块在20MHz下表现最佳,超过后误码率明显上升。
✅ 技巧3:电源与去耦不可忽视
高速传输对电源噪声极为敏感。
务必在ST7735的VCC引脚附近添加:
-0.1μF陶瓷电容(高频去耦)
-10μF钽电容或电解电容(稳压储能)
否则极易出现:
- 开机黑屏
- 传输中途丢帧
- 背光闪烁导致整体失真
✅ 技巧4:结合背光控制进一步节能
DMA传输完成后,MCU其实已经空闲了。此时可以让它“小憩一下”。
HAL_SPI_TxCpltCallback(...) { dma_busy = 0; // 延迟关闭背光或进入Sleep模式 osDelay(50); // 等待人眼感知 Backlight_Off(); // 关闭LED Enter_Stop_Mode(); // 进入低功耗 }对于电池供电设备(如智能手环、环境监测仪),这种协同设计能让续航延长数倍。
常见坑点与避坑指南
| 问题 | 可能原因 | 解决方法 |
|---|---|---|
| 屏幕黑屏但初始化无报错 | DC/RST接线错误或未拉高 | 检查GPIO定义,用示波器抓信号 |
| 图像错位/颜色异常 | 字节顺序颠倒(Big/Little Endian) | 尝试交换高低字节或启用SPI的LSB First |
| DMA只传一半就停止 | 缓冲区地址越界或DMA配置错误 | 检查内存对齐、DMA中断是否被屏蔽 |
| 传输完成后无法再次启动 | dma_busy未正确清除 | 确保回调函数中清零标志位 |
| 使用RTOS时报错 | 任务切换导致DMA源地址失效 | 使用静态分配缓冲区,避免栈变量 |
更进一步:还能怎么榨干性能?
DMA-SPI只是起点。如果你还想继续提速,可以考虑:
🔹 方案1:使用QSPI + RGB接口模拟
部分高端MCU(如STM32H7)可通过QSPI以DDR模式模拟RGB接口,实现真正并行推送。
🔹 方案2:结合LCD控制器专用外设(LTDC/FMC)
如STM32F429自带LTDC,可直接驱动TFT屏,无需任何软件干预。
🔹 方案3:动态压缩+差量更新
对图像内容进行轻量压缩(如RLE),仅传输变化块,极大降低带宽需求。
不过对于绝大多数中小项目来说,DMA-SPI已是性价比最高的选择。
写在最后:这不是炫技,而是必要进化
也许你会说:“我就显示个温度值,有必要搞得这么复杂吗?”
但用户不会管你用了什么芯片、多少资源。他们只关心:
- 界面滑不滑?
- 操作跟不跟手?
- 电池耐不耐用?
而这些体验的背后,正是一个个像“DMA-SPI”这样的细节堆出来的。
嵌入式图形开发的本质,从来都不是“能不能显示”,而是“怎么显示得更好”。
当你掌握了硬件加速的思想,你会发现:
- 同样的MCU,能做的事变多了;
- 同样的屏幕,看起来更舒服了;
- 同样的产品,竞争力更强了。
所以,下次当你面对一块小小的ST7735时,不妨问一句:
“我能把它跑得多快?”
然后动手试试DMA吧。