ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32F407用FSMC+DMA驱动LVGL:告别并口屏卡顿,刷新率翻倍实战

STM32F407用FSMC+DMA驱动LVGL:告别并口屏卡顿,刷新率翻倍实战 从一块480×320的3.5寸RGB屏说起吧。之前我用软件模拟8080时序刷一帧全屏数据大概要90多毫秒跑LVGL动画时整个界面像PPT翻页触摸拖动控件根本跟不上手指。后来把总线换成FSMC、再把像素数据交给DMA搬运同样一块屏实测LVGL的刷新率直接翻了一倍多动画才算是真正滑起来。这个过程我在STM32F407上完整踩了一遍CubeMX配置里也埋了好几个默认值陷阱这篇就把整个改造过程和避坑点一次性说清楚。FSMC加DMA驱动LVGL并不是什么冷门操作但网上的教程大多停留在能点亮屏幕或者能跑demo的层面真正把时序参数、内存映射、DMA请求握手、LVGL flush回调这些环节串起来讲的很少。这篇文适合已经能用GPIO模拟方式点亮屏幕、想进一步榨干硬件特性的开发者也适合那些用CubeMX生成工程后面对FSMC一堆时序参数不知道该怎么调的人。1. 为什么3.5寸并口屏卡顿从模拟总线到FSMC的跨越1.1 软件模拟8080时序的瓶颈在哪里先复盘一下最初的方案。屏幕是ILI9488驱动IC的3.5寸屏16位并口像素格式用RGB565。软件模拟的方式很简单把RS、WR、RD、CS这些控制脚用GPIO控制数据线用另外16个GPIO写一个命令或者数据时先把数据放到GPIO的输出寄存器然后拉低WR再拉高一个写周期就完成了。#define LCD_WR_FAST() { WR_GPIO-BRR WR_Pin; WR_GPIO-BSRR WR_Pin; } // 一次16位数据写入 void LCD_WriteData_GPIO(uint16_t data) { GPIO-BSRR data; // 把bit映射到ODR的对应引脚 LCD_WR_FAST(); }问题在于一个像素是2字节480×320分辨率全屏就是307200字节。每次写一个16位数据需要执行一次GPIO赋值拉低WR拉高WR即便编译器优化到极限单次写周期也要4到6条指令。算下来纯刷一帧全屏就要60毫秒以上这还不算命令写入、地址设置、触摸扫描和LVGL本身的开销。还有一个被很多人忽略的隐藏损耗取模运算和地址切换。LVGL的flush回调是按屏幕的一个矩形区域来的每次flush都要重新设置ILI9488的列地址和行地址然后再连续写入若干像素。地址切换本身不贵但如果在flush内部还用软件循环一个像素一个像素地写每一次写都要重新判断边界、做像素格式转换CPU的时间就全耗在这上面了。1.2 FSMC把写屏幕变成了写内存FSMC全称是Flexible Static Memory Controller在F407上它能直接映射NOR Flash、SRAM、PSRAM这类并行存储设备。LCD的8080接口从时序上看和SRAM的写操作高度相似区别只是有没有地址线。LCD这边寄存器选择靠RS引脚对应FSMC的地址线A0数据总线直接挂在FSMC的16位数据线上WR和RD控制信号对应FSMC的写使能和读使能。这样一来最关键的变化产生了CPU向某个内存地址写一个16位数据FSMC硬件会在一个总线周期内自动完成整个8080写时序包括地址建立、数据建立、WR脉冲宽度等。不再需要一条一条指令去翻转GPIO代码里看起来就是普通的指针赋值#define LCD_REG_ADDR ((volatile uint16_t *)0x6C000000) // RS0的时候是命令 #define LCD_DATA_ADDR ((volatile uint16_t *)0x6C000002) // RS1的时候是数据 *LCD_REG_ADDR 0x002A; // 写命令 *LCD_DATA_ADDR 0x0000; // 写数据这个过程中FSMC的Bank1第三区域NE3片选会把LCD映射到0x6C000000起始的地址空间。A0地址线接到LCD的RS引脚所以A00时写入的是命令A01时是数据。对CPU来说LCD成了一个16位宽的内存块操作系统和编译器那一层根本感知不到后面挂的是屏幕。之前模拟方式下写一个像素需要几十条指令现在变成一条STR指令硬件自动处理完WR时序后CPU可以直接做下一件事。等到这一步刷全屏的耗时理论上可以压到几毫秒级别卡顿的主因就被消除了。1.3 DMA参与进来后CPU真正被解放FSMC把写像素从软件循环变成了单条访存指令但一次flush少则几百字节多则几万字节如果全部靠CPU一条一条STR指令往外写虽然比GPIO模拟快得多CPU依然要占用不少时间。尤其LVGL在渲染完成之后flush阶段其实不需要CPU做太多逻辑判断此时正是DMA切入的最佳时机。STM32F407的DMA2是挂在外设总线AHB1上的FSMC的写操作也在这个总线上。于是可以这样设计LVGL渲染好的帧缓冲在内部SRAM或者外部SRAM中DMA把这段缓冲以内存到内存或者内存到外设的模式搬到LCD的数据地址上。这里有个重要的前提DMA搬数据到FSMC的LCD地址时实际上是把内存数据写到0x6C000002这个地址。FSMC的控制器会自动把此次AHB写操作转换成LCD的写时序。也就是说DMA和外设FSMC之间是一种地址驱动的关系不需要专门的硬件外设请求信号。这和串口DMA、SPI DMA完全不同串口需要RXNE/TXE事件来触发DMA请求而FSMC只是AHB总线上的一个从设备DMA只要写地址FSMC就自然产生一个外部总线周期。这种模式下CPU只需要启动一次DMA传输然后可以在剩余时间内去处理触摸、计时器回调或者让LVGL继续渲染下一帧的部分内容。等到DMA传输完成中断触发再把LVGL的flush完成信号发出去。整个渲染-搬运-刷屏流水线就建立起来了。2. CubeMX工程配置全流程与最容易翻车的三处细节2.1 引脚和FSMC外设的基础配置F407上FSMC的引脚是固定的Bank1的NE1到NE4、数据线D0-D15、控制线NOE、NWE、A0-A25这些引脚不能随便改。3.5寸并口屏实际用到的信号有CS、RS、WR、RD、RST以及16根数据线D0-D15。用FSMC驱动时CS接NE片选RS接A0WR接NWERD接NOERST随便找一个GPIO控制。在CubeMX里操作路径是找到FSMC外设使能Bank1 NOR/SRAM Controller选择Bank1的NE1或NE3注意3.5寸屏模块上丝印一般会标CS引脚需要和原理图对应。Memory type选择LCD Interface因为ILI9488这类屏的8080时序和标准SRAM略有差异主要体现在地址建立时间Address Setup Time上LCD模式会自动把读写复用和地址锁存做得更贴合约时序。Data bus width选择16 bit。引脚会自动分配到PG0-PG15这一组数据线以及对应的控制引脚。如果引脚冲突CubeMX左下角会直接报红色错误这时候先检查其他外设是不是占用了同样的引脚。NE片选的选择会影响地址映射。NE1对应0x60000000起始NE2对应0x64000000NE3对应0x68000000NE4对应0x6C000000。上面代码里用的0x6C000000其实就是NE4。如果你在CubeMX里选了NE3地址就要改成0x68000000这个对应关系记错了屏幕肯定点不亮。2.2 时序参数到底怎么填HCLK、周期数、实际t值换算FSMC的时序配置是新手最容易抄一份配置但不知其所以然的地方。ILI9488的8080接口写周期时序参数常见值如下参数符号典型值地址建立时间tAS0~10ns地址保持时间tAH0~5ns数据建立时间tDS15~30ns写脉冲宽度tWP15~30ns写周期时间tWC66~100nsSTM32F407的FSMC时序配置是以HCLK周期数来设置的。HCLK一般是168MHz一个周期约5.95ns。所以一个Write Pulse Width如果是3个HCLK周期大概就是17.85ns这在ILI9488的范围内是可以接受的。CubeMX里对应的字段是Address setup time地址建立建议1个HCLK周期。Address hold time地址保持0或者1个HCLK周期。Data setup time数据建立建议2~3个HCLK周期。Bus turn around time总线周转一般设0。Write pulse width写脉冲宽度建议3个HCLK周期。实际项目中我把Write Operation Timing设成了Address setup1Address hold1Data setup2Write pulse4这个组合在IL9488上跑得很稳。FSMC时序余量不能太大否则刷屏性能会被拖慢这和CPU超频降压的逻辑有些类似。但也不能太激进如果设到0和1的组合在特定的线路长度和电源噪声下可能偶发花屏这种问题很难复现和排查。2.3 DMA请求模式Normal循环还是Memory-to-MemoryCubeMX中DMA配置是另外一个坑。很多人会把LVGL的flush DMA配成Memory-to-Memory模式然后每次flush时调用HAL_DMA_Start之后再人工等传输完成。这种方案能工作但效率不是最优因为你必须在flush函数里忙等或者等待中断DMA和CPU之间其实没有形成流水。对于FSMC写LCD数据更合理的模式是DMA设置成Memory到Peripheral模式Peripheral地址固定为LCD数据地址0x6C000002Memory地址指向LVGL的flush缓冲数据宽度16位方向Memory-to-Peripheral。注意这个Peripheral对FSMC来说不是传统意义上的DMA请求源而是AHB总线上的写目标地址。CubeMX的DMA配置页面里外设选FSMC可能没有直接选项此时你可以在Peripheral里选择Memory-to-Memory的变通方案或者手动在代码里配置。我自己更推荐的写法是不依赖CubeMX的DMA图形化绑定直接在代码里用LL库或者HAL库初始化一个DMA流方向为MemoryToMemory然后在LVGL的flush回调里启动传输。因为我们实际上并不需要一个真正的硬件外设请求信号MemoryToMemory模式下DMA会连续搬运直到计数归零。这里插一个细节AHB总线上的内存到内存DMA源地址和目标地址的增量控制很重要。源地址SRAM缓冲每次传输后地址递增1按16位字为单位就是递增2字节目标地址LCD数据地址固定不变。2.4 CubeMX配置避坑清单把几个高频翻车点集中列出来都是我一个人踩过的或者帮别人排查过的数据宽度不一致。LCD是16位总线DMA的数据宽度也必须配置成HalfWord16位。如果配置成Byte或者Word搬运出来的像素字节序会错乱画面出现颜色通道互换或整个色调偏色。Cache问题。F407没有D-Cache但是在某些板载SRAM或外部SDRAM方案里会存在写缓冲导致DMA搬运后数据没有及时写出。不过F407内部SRAM一般不会有这个问题真正严重的是后面接SDRAM时的Cache一致性这里先提一句。LCD模块上的电平转换芯片。很多3.5寸屏模块板载了74LVC4245之类的电平转换器DIR和OE引脚需要额外接GPIO拉高或拉低不接的话数据方向锁死FSMC写入全被吞掉。这类问题和FSMC本身无关但排障时最容易忽视。NE片选和FSMC地址线的对应。A0接RS之后命令地址是0x6C000000数据地址要加2不是加1。因为FSMC按字节寻址但LCD是16位设备所以数据地址是基地址加2字节偏移。这个偏移到底是1还是2的问题每次都有人问。3. LVGL端口层重构FSMCDMA协同工作的核心逻辑3.1 平铺帧缓冲用单缓冲还是双缓冲LVGL在stm32上的移植一般会分配一个全屏帧缓冲或者若干行缓冲。全屏缓冲的好处是可以在flush回调里一次性把整帧或者一个大矩形交给DMA坏处是如果缓冲放在内部SRAM会挤占LVGL的渲染内存。F407的内部SRAM一共128KB112KB16KB一个480×320 RGB565缓冲区就是300KB显然放不下全屏。因此常用的方案是单缓冲 部分刷新LVGL只分配一个16KB左右的行缓冲每次flush一行或者几行。这个方案内存占用最小但DMA每次搬运的数据量很小对FSMC的连续写优势发挥不出来。多行缓冲分配8到16行的缓冲LVGL渲染完这些行后一次性flushDMA搬运的数据量增大效率更高。实测在3.5寸屏上行缓冲从8行增加到24行帧率提升明显。双缓冲 DMA后台搬运两个缓冲交替一个给LVGL渲染另一个交给DMA搬运。渲染和传输真正并行流畅度最好但内存开销也最大。对于F407这款芯片我会建议在MDK或者CubeIDE的链接脚本里把LVGL的绘图缓冲放到CCM RAM0x10000000之外的空间因为DMA无法访问CCM RAM。很多人直接把LVGL缓冲定义在默认的SRAM里其实没问题但如果你尝试优化时把缓冲塞进CCM RAM就会遇到LVGL渲染正常但屏幕不刷新的诡异现象根源就是DMA压根读不到CCM RAM的数据。3.2 flush回调的正确姿势LVGL的disp_flush_cb需要做到把绘制缓冲的地址和区域信息交给DMA。启动DMA传输。在DMA传输完成中断里调用lv_disp_flush_ready(disp)通知LVGL该缓冲可以继续使用。flush回调返回前不能让LVGL再次改写这块缓冲。static void disp_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) { uint32_t addr (uint32_t)color_p; lcd_set_window(area-x1, area-y1, area-x2, area-y2); // 如果配置成MemoryToMemory模式 HAL_DMA_Start_IT(hdma_memtomem_dma2_stream0, (uint32_t)addr, (uint32_t)LCD_RAM_ADDR, ((area-x2 - area-x1 1) * (area-y2 - area-y1 1))); }这段代码里有个非常关键的点lcd_set_window必须在DMA启动之前完成。因为DMA搬运的数据是线性的一长串LCD的列地址和行地址寄存器只有在设置窗口之后后续写入的数据才会被分配到对应区域。如果顺序反过来DMA数据虽然被刷进了LCD的GRAM但是写到了上一次设置的窗口里画面会错乱甚至出现条纹平铺的效果。另一个点是HAL_DMA_Start_IT和LVGL的缓冲区生命周期管理非常容易冲突。我在第一次接入DMA时直接在flush回调里启动DMA后立即返回LVGL会认为缓冲区可以继续渲染但实际上DMA还在读取这块内存。典型的症状就是画面偶尔出现残影或撕裂。正确的做法是同一个缓冲在使用DMA搬运期间LVGL不能修改它。所以flush回调里必须设置一个flushing标志在DMA完成中断里清除这个标志并调用lv_disp_flush_ready。volatile bool flushing false; void DMA2_Stream0_IRQHandler(void) { if (HAL_DMA_GetITSource(hdma_memtomem_dma2_stream0, DMA_IT_TCIF)) { CLEAR_BIT(hdma_memtomem_dma2_stream0.Instance-HISR, DMA_LISR_TCIF0); flushing false; lv_disp_flush_ready(disp_drv); } }3.3 移植后LVGL的显存配置细节LVGL的lv_conf.h里有几个关键宏需要确认LV_COLOR_DEPTH设为16匹配RGB565。LV_COLOR_16_SWAP这个宏控制字节序如果屏幕显示颜色错乱蓝红交换可以试着打开或者关闭。FSMC是16位并口直连一般不需要SWAP但如果你中间加了串转并的IC或者屏模块做了字节交换就需要调整。LV_MEM_SIZELVGL动态内存池的大小建议至少16KB。如果开了较多控件或者字体可以加到32KB。LV_DISP_DEF_REFR_PERIOD默认30ms即刷新率约33fps。当你把FSMCDMA打通后这个值可以往下降了比如设到15ms甚至10msLVGL会尝试以更高频率刷新脏矩形区域。LV_DPI也很重要它影响控件尺寸和触摸坐标映射。3.5寸屏一般设LV_DPI为100左右如果控件显示太小可以适当提高到120。这个参数不直接和流畅度相关但对使用体验影响很大。3.4 为什么说FSMCDMA的组合对LVGL是天然搭档LVGL的渲染机制是它维护一组脏矩形不需要的时候不重绘。一次重绘可能只涉及一个小按钮的范围比如64×64像素那么数据量只有8KB。这个数据量交给DMA搬运在168MHz的F407上大概几十微秒就完成。此时CPU几乎完全空闲LVGL可以马上进入下一帧的渲染动画的帧率上限取决于渲染耗时而不是总线写屏耗时。FSMC的写入速度上限取决于你配置的时序参数。如果用上面说的Write Pulse 4HCLK周期约5.95ns实际写周期大概40ns。32KB数据搬完理论耗时约1.3ms这里面既有DMA搬运的时间也有FSMC时序列的消耗。相比之前模拟方式的几十毫秒性能提升自然就是翻倍级别的。这套方案最大的价值是LVGL动画的瓶颈从写屏转移到了渲染。而渲染这一块LVGL本身有软件优化配合Cortex-M4的FPU和适当的编译器优化480×320分辨率的简单界面跑到45到50帧并不是不可能。4. 实测数据与踩坑记录流畅度翻倍背后的真实代价4.1 实测帧率和CPU占用别只看跑分我用的测试场景是LVGL官方的lv_demo_widgets以及一个自己做的带滑动列表、图表和开关控件的界面。测试环境STM32F407VET6HCLK 168MHz。外部25MHz晶振PLL倍频到168MHz。ILI9488驱动的3.5寸TFTFSMC 16位并口。LVGL版本8.3编译器为ARM Compiler 6。改动前后的数据方案全屏刷色耗时LVGL帧率简单动画CPU占用率动画运行时GPIO模拟8080约85ms12~15 fps高接近满负荷FSMC直接写无DMA约5ms26~30 fps中等约50%FSMC DMA搬运约2.5msDMA阶段40~50 fps低约25%注意第二行到第三行的变化比较隐蔽。FSMC直接写时LVGL帧率已经很不错但CPU占用率还是偏高。加了DMA之后帧率看起来只从30提高到了45左右没有翻倍那么夸张但CPU占用率显著下降。这些释放出来的CPU时间可以被触摸扫描、通信协议栈或者业务逻辑利用这是流畅度翻倍的另一层含义。如果只盯着帧率看一些demo可能提升不到一倍。但在包含大量控件、同时还有串口通信或者ADC采样的真实项目里多出来的CPU余量直接决定了系统的实时性。这是我个人认为FSMCDMA最有价值的收益点。4.2 花屏问题排查实录DMA传输被中断抢占我在跑lv_demo_music时遇到一个很诡异的偶发花屏不是每次都复现但一旦出现屏幕下半部分会出现水平方向的错位条纹。排查了一个下午最后用逻辑分析仪抓WR和NWE信号发现一个问题DMA正在搬运的过程中系统滴答定时器中断打入中断服务函数正好里调用了HAL_IncTick等短操作这本身不影响DMA。但如果此时其他高优先级中断比如串口中断执行时间较长FSMC总线上可能会出现被打断的现象——不是FSMC被真正打断而是DMA请求优先级和CPU访问FSMC发生总线竞争导致同一帧数据里混入了几条延迟的写操作。这种偶发问题的最简单处理办法把DMA中断优先级设置成最高确保搬运完成时能及时通知LVGL。在启动DMA前临时屏蔽掉不必要的高频中断或者在中断服务函数里不要做耗时操作。给LCD的CS信号加一个RC滤波减少总线竞争导致的信号毛刺。最后我选择的是方案2和3的组合。把DMA中断放在优先级分组2的最高抢占优先级串口中断放低一级同时给CS加了一个100Ω电阻和10pF电容的RC滤波花屏问题消失。4.3 触摸和显示坐标不同步也是DMA造成的系统升级到FSMCDMA之后出现了触摸位置和显示图标错位的现象触摸点在屏幕上方实际高亮的控件在下方。一开始以为是触摸校准参数被改动重新校准了多次还是不行。后来发现原因是屏幕的扫描方向和LCD_RST拉升时序。FSMC写入速度变快之后如果上电初始化时序太快ILI9488在某些批次下会进入错误的扫描模式。以前模拟GPIO方式初始化慢给了屏足够的稳定时间不会触发这个问题。换成FSMC后如果用HAL_Delay只给了10ms的复位时间在低温或者电压偏低的场景下屏控制器偶尔会没完成内部初始化导致GRAM的X/Y寻址方向和触摸面板的坐标方向不一致。解决办法是把LCD_RST的低电平时间从10ms增加到50ms并且在初始化命令sequence结束后追加20ms的空闲时间。这个时间成本在初始化阶段完全可以接受但能省掉后续显示错位这种极其迷惑的排查过程。4.4 另外一个容易被忽视的性能杀手LVGL的内存碎片在优化完FSMCDMA之后有一段时间我发现动画帧率会随着运行时间慢慢下降。比如刚开机跑45fps运行5分钟后变成30fps再过一会儿又恢复呈周期性的波动。这个现象一度让我怀疑是DMA没有得到正确的释放后来用lv_mem_monitor打印内存状态发现LVGL的动态内存碎片率在某个控件频繁创建删除后飙升到30%以上。LVGL的内存分配策略在小内存设备上需要谨慎设计LV_MEM_SIZE设得太小碎片率会快速升高。把LV_MEM_SIZE从默认的16KB提高到32KB并把LV_MEM_ATTR设成在SRAM的连续区域同时避免在动画回调里频繁创建临时控件这个问题就缓解了。LVGL的内存碎片和FSMC没有直接关系但它会反噬你辛辛苦苦省下来的CPU余量。性能优化是一个整体工程单点提速并不能保证最终体验。4.5 如果效果还是不够好下一步还能怎么榨性能FSMCDMA只是第一步再往下走有几个方向值得尝试。一个是开启LV_USE_PERF_MONITOR它在屏幕上显示实时的帧率和CPU占用率这个宏在lv_conf.h里日常开发建议一直开着能直观看到优化是否有效果。另一个是使用外部SRAM。F407可以通过FSMC同时挂载LCD一个NE片选和外部SRAM另一个NE片选把LVGL的绘制缓冲放到外部SRAM这样内部SRAM的压力大大降低。但因为外部SRAM的访问速度比内部SRAM慢DMA从外部SRAM搬运数据到LCD地址时FSMC和DMA都挂在AHB上总线竞争会增加实际性能提升可能不如预期。这时可以尝试让DMA从外部SRAM搬运LVGL渲染缓冲也放外部SRAMCPU的取指和数据访问则尽量留在内部SRAM。还可以尝试把LVGL的disp_drv的full_refresh配置打开强制全屏刷新而不是脏矩形刷新。这在某些场景下可以减少窗口切换时的撕裂感但会牺牲一定性能适合在动画复杂度较低时使用。结语一点个人实操体会整个改造做下来我最深的体会是FSMCDMA在F407上不是单一的外设配置问题而是总线架构、时序参数、LVGL缓冲区生命周期管理、中断优先级四件事的串联。任何一个环节没对齐屏幕要么不亮要么亮了但性能不稳定。如果你正在做类似移植建议按照先FSMC点亮屏幕、再CPU直接写屏、最后上DMA的顺序一步步来每走一步都在LVGL的perf monitor里记录数据确认当前步的性能基线再进入下一步。这样出了问题能快速定位是FSMC配置、DMA配置还是LVGL对接的锅。最后分享一个小技巧调试FSMC时序时不要用LVGL的动画界面来观察流畅度直接写一个全屏刷色循环用示波器或者逻辑分析仪抓WR引脚的频率。全屏刷色频率直接反映FSMC的写吞吐量这个是硬指标排除了LVGL渲染和脏矩形优化的干扰。硬指标达标了再去调LVGL层的缓冲大小和刷新策略效果立竿见影。
返回列表