ST7789初始化必须禁用SPI 3线模式,DC引脚需CPU手动控制,SPI配置为Mode 3(CPOL=0, CPHA=0),使用双缓冲+半传输中断并配合DCache清理,发送像素前须发0x2C指令及dummy字节。

ST7789 初始化必须禁用 SPI 3 线模式,否则 DMA 传输会卡死
ST7789 默认上电是 4 线 SPI(SCL/SDA/DC/CS/RES),但很多移植者误用 3 线(SCL/SDA/CS)并依赖 DC 引脚电平切换命令/数据——这会导致 HAL_SPI_Transmit_DMA() 在发送多段数据时无法同步 DC 状态,DMA 中断里没法动态切 DC,最终屏幕显示错乱或完全无响应。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 初始化前拉低
RES至少 10ms,再拉高并延时 120ms,确保芯片退出复位状态 -
DC引脚必须接 GPIO,且全程由 CPU 控制(不能交给 SPI 外设模拟),每次发命令前手动置低,发数据前手动置高 - SPI 配置中关闭
HAL_SPI_Init()的CPOL=1和CPHA=1(ST7789 要求 Mode 3),波特率建议 ≤ 20MHz(STM32H7 可到 36MHz,但需验证信号完整性) - 避免使用
HAL_SPI_TransmitReceive_DMA()——它隐含全双工读写,而 ST7789 是单向写设备,MISO 悬空易引入噪声干扰 DMA 流程
DMA 刷新帧缓冲区要用双缓冲 + 半传输中断,否则屏幕撕裂明显
单缓冲直接刷 frame_buffer 会导致 DMA 正在传第 n 行时,CPU 已开始改写第 n+1 行,画面出现横向错位。ST7789 不支持硬件 VSYNC,只能靠软件同步。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 分配两块大小一致的帧缓冲区:
frame_buffer_a和frame_buffer_b,用指针current_fb切换 - 启动 DMA 时调用
HAL_SPI_Transmit_DMA(&hspi1, current_fb, sizeof(frame_buffer_a)),同时开启HAL_SPI_RegisterCallback(&hspi1, HAL_SPI_TX_HALF_COMPLETE_CB_ID, ...) - 在半传输回调里翻转
current_fb指针,并把新图像数据写入另一块缓冲区(此时 DMA 正在传前半部分,后半部分可安全更新) - 注意:
sizeof必须是 2 的整数次幂(如 240×320×2 = 153600 字节 → 扩展到 153600 对齐到 153600?不,要对齐 DMA 传输单元,实际建议用 153600 或补零至 153600+16,但更稳妥是直接用__ALIGN_BEGIN uint16_t frame_buffer_a[240*320] __ALIGN_END配合SCB_CleanInvalidateDCache_by_Addr()刷缓存
发送像素数据前必须先发内存写入指令 0x2C,且不能省略 dummy byte
很多驱动代码在 set_window() 后直接调用 HAL_SPI_Transmit_DMA() 发送 RGB565 数据,但 ST7789 要求在收到 0x2C 后,SPI 总线上第一个字节是 dummy(值任意,常为 0x00),否则后续像素被丢弃或错位。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 封装一个函数:
st7789_write_pixels(uint16_t *data, size_t len),内部先拉低DC,发0x2C;再拉高DC,发一个0x00(dummy),再发data数组 - dummy byte 必须作为 DMA 传输首字节 —— 把它和像素数据拼成临时缓冲区,或用
HAL_SPI_Transmit_DMA()分两次调用(不推荐,增加中断开销) - 若使用
HAL_DMAEx_MultiBufferStart(),需确保 dummy byte 在主缓冲区头部,且两个缓冲区结构完全一致(包括 dummy 位置) - 检查示波器上 SCL/SDA 时序:0x2C 后至少等待 1 个 SPI 周期再拉高 DC,否则部分批次 ST7789 会忽略后续数据
STM32H7 上启用 D-Cache 后必须手动清理,否则 DMA 看到的是脏数据
这是最隐蔽的坑:CPU 写完 frame_buffer,以为数据已落内存,但实际还在 L1 D-Cache 里;DMA 控制器直连 AXI 总线,取的是物理内存旧值,导致屏幕显示上一帧内容或花屏。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 定义帧缓冲区时加属性:
uint16_t frame_buffer_a[240*320] __attribute__((section(".fb_a"), aligned(32))),并在链接脚本里将其放在非 cacheable 区域(如 DTCM 或 AXI SRAM),或 - 保留 cacheable 属性,但在每次更新缓冲区后、启动 DMA 前调用:
SCB_CleanDCache_by_Addr((uint32_t*)frame_buffer_a, sizeof(frame_buffer_a))(注意地址需 32 字节对齐) - 不要用
__DSB()替代缓存清理 —— 它只保证指令顺序,不刷新 cache line - 如果用
HAL_DMAEx_MultiBufferStart()切换双缓冲,每个缓冲区都必须单独 clean,不能只 clean 当前指针指向的那块
DMA 刷新率瓶颈往往不在 SPI 速度,而在 CPU 缓存管理和帧缓冲区拷贝路径。没做 cache clean 的代码,在仿真器下可能“看起来正常”,一拔调试器就出问题。



















