SPI LCD DMA 编程与调试记录
目录一、前言二、SPI 三种传输方式回顾三、DMA 方式的 CubeMX 配置四、核心代码SPI_WriteDatas 改造五、深入理解DMA 两级中断链六、CubeMX 的 GPDMA 代码生成误区七、调试三板斧八、移植到你的工程九、总结十、结尾一、前言大家好这里是Hello_Embed。上一篇我们完成了 UART 的三种编程方式查询/中断/DMA理解了 DMA 异步传输的基本模式。本篇把这个模式迁移到 SPI LCD——让 LCD 显示也跑在 DMA 方式下。本文在 STM32H5DShanMCU-H5 开发板上实操借助Claude Code辅助调试。核心改动只有spi_lcd.c里的一个函数但踩坑过程覆盖了 HAL 库中断链、CubeMX 代码生成偏差、FreeRTOS 任务栈等多个知识点。二、SPI 三种传输方式回顾与 UART 完全对称HAL 库为 SPI 提供了三种 API方式HAL API阻塞/非阻塞需要 NVIC需要 GPDMA查询HAL_SPI_Transmit阻塞❌❌中断HAL_SPI_Transmit_IT非阻塞✅❌DMAHAL_SPI_Transmit_DMA非阻塞✅✅关键点HAL_SPI_Transmit_DMA是非阻塞的——调用后立即返回DMA 在后台搬运数据。如果不等传输完就拉高 CSLCD 只能收到残缺信号。三、DMA 方式的 CubeMX 配置在 CubeMX 中打开.ioc文件给 SPI2 添加两项配置缺一不可3.1 SPI2 global interruptNVICSPI2 →NVIC Settings→ 勾选SPI2 global interrupt作用提供SPI2_IRQHandler处理 EOTEnd of Transfer中断。DMA 传完后EOT 中断负责把 SPI 状态恢复到 READY。3.2 SPI2_TX DMA 请求SPI2 →DMA Settings→ Add → 选择SPI2_TXDirection: Memory to Peripheral作用分配一个 GPDMA 通道配置为从内存搬运数据到 SPI TX 数据寄存器。3.3 生成代码后的验证CubeMX Generate Code 后检查以下内容是否齐全文件应包含位置spi.cDMA Init __HAL_LINKDMA SPI2 NVICHAL_SPI_MspInitstm32h5xx_it.cSPI2_IRQHandler外设中断区stm32h5xx_it.cGPDMA1_ChannelX_IRQHandlerGPDMA 中断区gpdma.cHAL_NVIC_EnableIRQ(GPDMA1_ChannelX_IRQn)MX_GPDMA1_Init四、核心代码SPI_WriteDatas 改造spi_lcd.c中只需改SPI_WriteDatas一个函数。改前查询方式staticintSPI_WriteDatas(uint8_t*TxData,uint16_tsize){interr;LCD_Select();errHAL_SPI_Transmit(hspi2,TxData,size,100);// 阻塞LCD_DeSelect();return-err;}改后DMA 方式staticintSPI_WriteDatas(uint8_t*TxData,uint16_tsize){interr;LCD_Select();errHAL_SPI_Transmit_DMA(hspi2,TxData,size);// 非阻塞if(errHAL_OK){while(HAL_SPI_GetState(hspi2)!HAL_SPI_STATE_READY){}// 必须等 State 变 READY——意味着 DMA 传完 EOT 中断已处理}LCD_DeSelect();return-err;}改动点只有三处HAL_SPI_Transmit→HAL_SPI_Transmit_DMA去掉 timeout 参数DMA 模式下不需要加while等待HAL_SPI_STATE_READY移植要点其他文件spi.h、main.c、draw.c全部不用动。CubeMX 生成的初始化代码自动包含了 DMA 通道配置。五、深入理解DMA 两级中断链这是本篇最核心的知识点。HAL 库把 DMA 传输完成后的收尾工作拆成了两步① HAL_SPI_Transmit_DMA() ├── 配置 GPDMA 通道源内存 buf, 目的SPI TXDR ├── SET TXDMAEN ← 允许 SPI 发送 DMA 请求 ├── SET SPE / CSTART ← 使能 SPI 并开始主机传输 └── 返回 HAL_OK ← 此时 DMA 还在后台跑 ··· GPDMA 逐字节搬运数据到 SPI TXDR ··· ② GPDMA TC 中断DMA 搬运完成 └── GPDMA1_ChannelX_IRQHandler └── HAL_DMA_IRQHandler └── SPI_DMATransmitCplt() └── 开 EOT 中断 ← 只是开State 仍是 BUSY_TX ③ SPI EOT 中断SPI 最后一帧移出到总线 └── SPI2_IRQHandler └── HAL_SPI_IRQHandler └── 清 EOT / TXDMAEN / CSTART └── State HAL_SPI_STATE_READY ← 这里才恢复 while(HAL_SPI_GET_STATE ! READY) → 终于退出 LCD_DeSelect() → 安全拉高 CS理解要点如果只配 DMA 而不配 SPI2 中断DMA 传完后 State 永远回不到 READYwhile 循环死等如果只配 SPI2 中断而不配 DMAHAL_SPI_Transmit_DMA 会因为hdmatx为 NULL 而返回 HAL_ERROR两者必须同时配置六、CubeMX 的 GPDMA 代码生成误区STM32H5 的 CubeMX当前版本对 GPDMA 代码生成存在系统性偏差参数SPI_TX 正确值CubeMX 可能生成影响DirectionDMA_MEMORY_TO_PERIPHDMA_PERIPH_TO_MEMORY方向反了SrcIncDMA_SINC_INCREMENTEDDMA_SINC_FIXED源地址不自增→多字节数据只有第一字节正确DestIncDMA_DINC_FIXEDDMA_DINC_FIXED✅ 正确SPI TXDR 地址不变实践建议每次 CubeMX Generate Code 后手动检查spi.c中 GPDMA 通道的 Direction 和 SrcInc。CubeMX 不会保存你对这些字段的手动修改。七、调试三板斧今天踩坑全程用到的调试方法适用于任何 HAL 函数调用失败的情况7.1 死循环标记法在if-else每个分支里放独特的死循环if(errHAL_ERROR){volatileintdbg1;while(dbg){}}// ← 停在这 HAL_ERRORelseif(errHAL_BUSY){volatileintdbg2;while(dbg){}}// ← 停在这 HAL_BUSYelseif(errHAL_OK){/* 正常路径 */}停在哪个死循环就知道走了哪个分支——不需要看变量只需要看 PC 停在哪个行号。7.2 逐步断点法在main()每个外设初始化后面打断点看断在哪一行后进入 Error_Handler / HardFault。7.3 隔离验证法先确认基础功能正常LCD 查询模式能显示再逐个添加新配置加 DMA、加中断、加 FreeRTOS 任务每加一步验证一步不跳步八、移植到你的工程从零开始给 SPI LCD 添加 DMA完整步骤Step 1 — CubeMX 配置SPI2 →NVIC Settings→ 勾选SPI2 global interruptSPI2 →DMA Settings→ Add →SPI2_TXGENERATE CODEStep 2 — 手动修复 CubeMX 生成代码打开Core/Src/spi.c在HAL_SPI_MspInit中找到 GPDMA 通道初始化// 修改前CubeMX 可能生成的错误值handle_GPDMA1_ChannelX.Init.DirectionDMA_PERIPH_TO_MEMORY;handle_GPDMA1_ChannelX.Init.SrcIncDMA_SINC_FIXED;// 修改后SPI TX 的正确值handle_GPDMA1_ChannelX.Init.DirectionDMA_MEMORY_TO_PERIPH;handle_GPDMA1_ChannelX.Init.SrcIncDMA_SINC_INCREMENTED;Step 3 — 删除 MX_ICACHE_Init()打开Core/Src/main.c删除或注释掉MX_ICACHE_Init();SystemInit 已初始化 ICACHE重复调用会导致 Error_Handler。Step 4 — 修改 SPI_WriteDatas打开Drivers/Module_driver/spi_lcd.c按 第四节 修改。Step 5 — 验证编译烧录先做简单测试确认 DMA 链路正常Draw_Clear(0x00FF0000);// 红屏 — 验证全屏填充Draw_Line(0,0,100,100,0x0000FF00);// 绿线 — 验证画线Draw_String(0,0,OK,0x0000FF00,0);// 绿字 — 验证文字九、总结要点说明SPI DMA APIHAL_SPI_Transmit_DMA— 非阻塞和 UART DMA 模式完全一致CS 时序必须等 State→READY 再拉高 CS两级中断链DMA TC → SPI_DMATransmitCplt 开 EOT → SPI2_IRQHandler → State READYCubeMX 配置SPI2 NVIC SPI2_TX DMA两项缺一不可CubeMX 坑SrcInc、Direction 生成可能错误每次 Generate 后手动检查调试方法死循环标记 逐步断点 隔离验证核心模式HAL_xxx_Transmit_DMAwhile(HAL_GET_STATE ! READY)→ 适用于任何外设的 DMA 发送。十、结尾本文借助Claude Code辅助编写源码在百问网课程第 3-6 章的基础上扩展——将 UART DMA 的知识迁移到了 SPI LCD。