STM32F4标准库 DMA FSMC驱动TFT-LCD:从CPU瓶颈到DMA高效刷屏实战

张开发
2026/4/15 14:17:36 15 分钟阅读

分享文章

STM32F4标准库 DMA FSMC驱动TFT-LCD:从CPU瓶颈到DMA高效刷屏实战
1. 为什么需要DMAFSMC方案优化TFT-LCD刷新当你用STM32F4驱动TFT-LCD时是否遇到过画面刷新卡顿、CPU占用率飙升的情况这通常是因为传统的逐点描画方式比如LCD_DrawPoint函数需要CPU频繁介入每个像素的写入操作。我最初用9341驱动芯片的屏幕时刷个全屏动画CPU使用率直接冲到80%以上系统其他任务根本跑不动。FSMCFlexible Static Memory Controller本身已经比GPIO模拟总线快很多但单纯靠CPU通过FSMC写数据仍然存在瓶颈。这时候DMADirect Memory Access就派上用场了——它就像个专职快递员能在不打扰CPU的情况下自动把显存数据搬运到LCD显存。实测下来同样的320x240屏幕刷新使用DMA后CPU占用从80%降到不足5%帧率还提升了3倍。2. 硬件连接与地址映射关键点2.1 FSMC引脚配置实战以常见的FSMC_NE1Bank1接LCD_CSFSMC_A16接LCD_RS为例硬件连接需要特别注意数据线FSMC_D0~D15接LCD_D0~D15地址线FSMC_A16作为命令/数据选择线RS信号写使能FSMC_NWE接LCD_WR读使能FSMC_NOE接LCD_RD这里有个坑我踩过STM32内部会对地址右移1位对齐所以A16实际对应的是HADDR[17]。这直接影响到后面基地址的计算。2.2 地址计算魔鬼细节LCD寄存器与显存需要映射到两个不同的FSMC地址#define LCD_BASE ((u32)(0x60000000 | 0x0001FFFE)) typedef struct { volatile uint16_t LCD_REG; volatile uint16_t LCD_RAM; } LCD_TypeDef; #define LCD ((LCD_TypeDef *) LCD_BASE)这个0x0001FFFE怎么来的因为A16对应的是bit17右移1位后是0x00010000再减去2结构体对齐要求得到0x0000FFFE。实际使用时写命令LCD-LCD_REG cmd写数据LCD-LCD_RAM data3. DMA核心配置详解3.1 初始化DMA2 Stream3我用的是DMA2_Stream3通道0因为它与FSMC有专用通路。初始化时要特别注意这些参数DMA_InitStructure.DMA_Channel LCD_DMA_Channel; DMA_InitStructure.DMA_PeripheralBaseAddr 0; //动态设置 DMA_InitStructure.DMA_Memory0BaseAddr (uint32_t)LCD-LCD_RAM; DMA_InitStructure.DMA_DIR DMA_DIR_MemoryToMemory; DMA_InitStructure.DMA_BufferSize 0; //动态设置 DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Enable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Disable; DMA_InitStructure.DMA_PeripheralDataSize DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord;关键点MemoryToMemory模式虽然LCD是外设但FSMC被映射到内存空间外设地址递增连续写入颜色数据存储器地址固定始终指向LCD_RAM3.2 传输触发机制启动DMA传输前需要三步准备void LCD_Start_DMA_Transfer(uint16_t x1,uint16_t y1,uint16_t x2,uint16_t y2,uint16_t *color) { LCD_Address_Set(x1,y1,x2,y2); //设置坐标 LCD_DMA_Stream-NDTR (x2-x11)*(y2-y11); //数据量 LCD_DMA_Stream-PAR (uint32_t)color; //数据源地址 DMA_Cmd(LCD_DMA_Stream, ENABLE); }注意NDTR的单位是字节数还是字数我遇到过因为搞错这个导致传输数据不全的问题。4. 与LVGL的深度整合4.1 替换底层驱动接口LVGL的刷新回调可以这样改造void my_disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p) { LCD_Start_DMA_Transfer(area-x1, area-y1, area-x2, area-y2, (uint16_t*)color_p); // 不要立即调用lv_disp_flush_ready } void DMA2_Stream3_IRQHandler(void) { if(DMA_GetITStatus(LCD_DMA_Stream, DMA_IT_TCIF3)) { DMA_ClearITPendingBit(LCD_DMA_Stream, DMA_IT_TCIF3); lv_disp_flush_ready(NULL); //在DMA完成中断中通知LVGL } }4.2 性能优化实测数据对比三种刷新方式方式320x240全屏刷新时间CPU占用率逐点描画120ms85%FSMC直接写入45ms60%DMAFSMC本文方案15ms5%特别提醒如果使用双缓冲记得在DMA传输完成中断里切换缓冲区否则会出现撕裂现象。我在项目中就因为没处理好这个导致屏幕上半部分和下半部分显示不同帧的内容。

更多文章