1. 项目背景与核心价值
作为一名嵌入式开发工程师,我最近在优化一个基于STM32的TFT显示屏驱动项目时,发现传统的SPI传输方式严重制约了屏幕刷新率。经过反复测试验证,最终通过DMA+SPI的组合方案实现了性能突破,屏幕刷新率提升近8倍。这个方案特别适合需要高速刷新的嵌入式GUI、仪器仪表等场景。
ST7789是一款常见的240x320分辨率TFT驱动芯片,采用SPI接口时通常只能达到15-20fps的刷新率。而通过STM32F103的DMA控制器自动搬运数据到SPI外设,可以释放CPU资源,实现后台数据传输。实测在72MHz系统时钟下,SPI时钟配置为18MHz时,刷新率可稳定达到60fps以上。
2. 硬件设计与接口配置
2.1 硬件连接示意图
ST7789与STM32F103的典型连接方式如下:
code复制ST7789 STM32F103
VCC -> 3.3V
GND -> GND
SCL -> PA5(SPI1_SCK)
SDA -> PA7(SPI1_MOSI)
RES -> PB0
DC -> PB1
CS -> GND(常使能)
BLK -> 3.3V
2.2 GPIO初始化配置
c复制// 复位和DC引脚配置
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_0|GPIO_PIN_1;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
// SPI引脚自动配置
__HAL_RCC_SPI1_CLK_ENABLE();
注意:ST7789的CS引脚建议直接接地使能,节省一个GPIO资源。如果必须控制CS,需在DMA传输完成中断中处理CS电平变化。
3. SPI与DMA的协同配置
3.1 SPI外设参数优化
c复制hspi1.Instance = SPI1;
hspi1.Init.Mode = SPI_MODE_MASTER;
hspi1.Init.Direction = SPI_DIRECTION_1LINE;
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;
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
HAL_SPI_Init(&hspi1);
关键参数说明:
- BaudRatePrescaler=4:在72MHz主频下得到18MHz SPI时钟
- Direction=1LINE:使用单线模式节省IO资源
- 关闭CRC和TI模式提升传输效率
3.2 DMA控制器配置技巧
c复制hdma_spi1_tx.Instance = DMA1_Channel3;
hdma_spi1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_spi1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_spi1_tx.Init.MemInc = DMA_MINC_ENABLE;
hdma_spi1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_spi1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_spi1_tx.Init.Mode = DMA_NORMAL;
hdma_spi1_tx.Init.Priority = DMA_PRIORITY_HIGH;
HAL_DMA_Init(&hdma_spi1_tx);
__HAL_LINKDMA(&hspi1, hdmatx, hdma_spi1_tx);
经验分享:将DMA优先级设为HIGH可防止其他外设中断导致传输卡顿。如果使用内存到内存模式,需要额外配置FIFO阈值。
4. 显存管理与传输优化
4.1 双缓冲机制实现
c复制#define BUF_SIZE 240*40
uint8_t frame_buffer[2][BUF_SIZE];
void refresh_screen(void) {
// 后台传输当前非显示缓冲区
HAL_SPI_Transmit_DMA(&hspi1, frame_buffer[display_idx], BUF_SIZE);
// 切换缓冲区索引
display_idx ^= 0x01;
// 在前台准备下一帧数据
prepare_frame(frame_buffer[display_idx]);
}
4.2 数据传输协议优化
ST7789的显存写入需要先发送命令字再发送数据。通过预格式化数据包可以减少传输过程中的GPIO操作:
c复制#pragma pack(push, 1)
typedef struct {
uint8_t cmd;
uint8_t data[0];
} lcd_cmd_packet;
#pragma pack(pop)
void send_cmd(uint8_t cmd, uint8_t* data, uint16_t len) {
lcd_cmd_packet* packet = malloc(sizeof(lcd_cmd_packet) + len);
packet->cmd = cmd;
memcpy(packet->data, data, len);
HAL_GPIO_WritePin(GPIOB, DC_Pin, GPIO_PIN_RESET);
HAL_SPI_Transmit(&hspi1, &packet->cmd, 1, HAL_MAX_DELAY);
HAL_GPIO_WritePin(GPIOB, DC_Pin, GPIO_PIN_SET);
HAL_SPI_Transmit_DMA(&hspi1, packet->data, len);
}
5. 性能测试与优化记录
5.1 不同配置下的性能对比
| 传输方式 | SPI时钟 | CPU占用率 | 实测刷新率 |
|---|---|---|---|
| 轮询模式 | 9MHz | 98% | 12fps |
| 中断模式 | 18MHz | 45% | 28fps |
| DMA模式(本文) | 18MHz | <5% | 65fps |
| DMA双缓冲 | 36MHz | <3% | 82fps |
5.2 关键延迟优化点
- 减少DC引脚切换延迟:将DC和RESET引脚配置为高速模式(GPIO_SPEED_FREQ_HIGH)
- SPI时钟相位调整:CLKPhase=1EDGE比2EDGE节省约0.5us每字节
- 内存对齐优化:确保DMA源地址4字节对齐可提升搬运效率
6. 常见问题与解决方案
6.1 DMA传输不启动
症状:调用HAL_SPI_Transmit_DMA()后无反应
排查步骤:
- 检查__HAL_LINKDMA是否执行
- 确认DMA通道时钟已使能
- 验证缓冲区地址是否在有效内存区域
6.2 屏幕显示错位
典型原因:
- 显存大小与屏幕分辨率不匹配
- 颜色格式设置错误(RGB565 vs RGB888)
- 扫描方向寄存器配置错误(MADCTL)
解决方法:
c复制// 正确的初始化序列示例
static const uint8_t init_cmds[] = {
0x36, 0x00, // MADCTL: RGB顺序设置
0x3A, 0x55, // 像素格式: 16bit/pixel
...
};
6.3 屏幕闪烁问题
可能原因及对策:
- 电源不稳定:在VCC与GND间加100uF电容
- 刷新不同步:启用VSYNC同步信号
- DMA缓冲区被修改:使用MPU保护显存区域
7. 进阶优化方向
对于需要更高性能的场景,可以尝试以下优化:
- 使用STM32F4系列芯片,SPI时钟可达50MHz
- 采用硬件SPI FIFO(需芯片支持)
- 实现区域刷新机制,只更新变化部分
- 使用LTDC接口替代SPI(需屏幕支持)
我在实际项目中发现,通过合理配置DMA传输完成中断,可以在帧传输间隙插入其他任务处理。例如:
c复制void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) {
if(hspi == &hspi1) {
// 在此处执行低优先级任务
update_touch_data();
check_battery();
}
}
这种设计使得系统在保持高刷新率的同时,还能及时响应其他外设事件。经过三个项目的实际验证,这套方案在工业HMI、医疗设备显示等场景下表现稳定可靠。
