ARTICLE DETAIL

资讯详情

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

STM32F7 SDRAM RGB屏幕偶发花屏根因分析与完整修复指南

STM32F7 SDRAM RGB屏幕偶发花屏根因分析与完整修复指南 先说结论这块板子最后能稳定运行不是因为某个一锤定音的补丁而是把SDRAM时序、刷新率、LTDC带宽、甚至MPU缓存策略全部重新捋了一遍。这类问题最坑的地方在于“偶发”两个字你盯着它的时候它不出现你合上屏幕开始量产测试它又跳出来。STM32F7 SDRAM RGB屏幕的组合在项目里非常常见但大多数开发者把SDRAM初始化跑通、屏幕能点亮之后就默认一切正常实际上隐患从那一刻就埋下了。如果你也遇到类似的现象——屏幕上随机位置出现一条几像素宽的错位横带或者某个矩形区域出现雪花般的噪点而且频率不固定、重启之后可能消失——那这篇文章应该能帮你少走不少弯路。我会从现象描述、根因分析、排查方法到最终方案完整复盘一次这个问题的处理过程。1. 问题现象与最初的排查方向1.1 故障表现描述项目硬件平台是STM32F746外挂一颗16位数据宽度的SDRAM容量32MB屏幕上跑的是800x480的RGB接口LCD通过LTDC控制器驱动。软件上使用了ST官方的TouchGFX来做界面底层是FreeRTOS 自己封装的一个显示驱动层。问题现象非常典型系统长时间运行后屏幕会出现偶发性的局部画面错乱。表现形式有三种一种是整行错位画面水平方向出现一条或者多条带状的偏移宽度在2到10像素不等另一种是局部噪点某个矩形区域内出现类似电视雪花一样的点状干扰持续时间几百毫秒到几秒不等还有一种最隐蔽是颜色异常某个图标的颜色突然变成错误的色块但下一帧又恢复正常这三种现象的共同点是没有固定出现的时间点也没有固定的位置区域。有时候系统跑2小时才出现一次有时候开机5分钟就触发。更头疼的是重启之后可能连续几个小时都不再出现极具欺骗性。1.2 最初怀疑的目标遇到这种问题第一反应一般是查LTDC配置。毕竟画面显示异常最容易联想到的是像素时钟、同步极性、前后肩这些参数配错了。我最初也是沿着这个方向排查把LTDC的时序参数对着屏幕数据手册逐一确认甚至用示波器测量了HSYNC、VSYNC、DE、PCLK这几根线的波形和屏幕要求的时序完全吻合。接着怀疑是DMA2D和LTDC同时访问显存时总线仲裁出了问题。因为TouchGFX的UI刷新会频繁触发DMA2D搬运如果DMA2D和LTDC在同一个AHB总线上抢带宽理论上确实可能导致LTDC读取显存时出现断续。但这个猜想很快被否定了——我做了个测试把DMA2D的搬运完全停掉只在屏幕上显示一张静态图片问题依然出现了。到了这一步我才意识到问题可能出在更底层的地方SDRAM本身。因为LTDC读取的像素数据最终都要经过FMC总线从SDRAM中取出来如果SDRAM这一层偶发返回错误数据屏幕表现出的现象就是噪点或色块。而像行错位这种问题看起来像是地址跳变也有可能是SDRAM的行激活或预充电时序出现了异常。2. SDRAM与LTDC之间的数据链路到底怎么回事2.1 FMC与SDRAM的基本工作机制STM32F7的FMCFlexible Memory Controller带有SDRAM控制器支持常见的SDRAM颗粒。FMC负责产生SDRAM所需的所有控制信号包括时钟、片选、行地址选通、列地址选通、写使能、数据掩码等。我们不需要一个一个寄存器去手动控制这些信号FMC会自动完成预充电、行激活、读写、刷新这样的状态机转换。SDRAM本身是动态存储器靠电容存储电荷来保持数据电容会漏电所以必须周期性刷新。SDRAM内部的存储阵列按行组织每次刷新操作会刷新一整行控制器需要在规定时间内把所有的行都刷一遍。对于常见的SDRAM颗粒规格通常是8192行不同容量有差异刷新周期为64ms也就是说每64ms内必须把8192行全部刷新一遍。FMC的SDRAM控制器提供两种刷新方式自动刷新和自刷新。正常运行时使用自动刷新控制器会根据你配置的刷新周期寄存器每隔一定时间发出一次刷新命令。这个刷新间隔的计算非常关键也是后续排查的重点之一。2.2 LTDC如何从SDRAM取数据STM32F7的LTDCLCD-TFT Display Controller是一个直接连接RGB接口屏幕的控制器。LTDC内部有多个FIFO每个显示层一个。它通过AHB总线向FMC发起读请求FMC把SDRAM中的数据返回给LTDC的FIFOFIFO再按照像素时钟持续输出给屏幕。关键点在于LTDC对显存的读取不是突发式的批量读取而是按照像素时钟周期连续读取。对一块800x480、24位色深的屏幕像素时钟约30MHz到40MHz取决于刷新率设置每个像素3字节意味着LTDC每秒要从SDRAM中取走大约120MB的数据。这个数据量对SDRAM来说并不算大但问题在于LTDC对时序的敏感度极高任何一次读请求返回延迟过大FIFO就可能出现下溢表现出来就是屏幕上的一条杂色带。正常来说FMC控制器会优先保证LTDC的带宽需求但如果SDRAM本身处于刷新操作期间FMC需要等待刷新完成才能执行读请求。SDRAM的刷新操作需要占用一定的时间如果刷新频率过高或者刷新窗口安排不合理就会挤占正常读写的带宽。2.3 为什么偶发问题最难查嵌入式系统的偶发性问题最让人头疼因为它往往不是单一原因造成的而是多个因素叠加之后刚好在某一个极窄的条件下触发。以SDRAM刷新和LTDC读取的冲突为例正常情况下FMC会在SDRAM刷新间隙穿插执行LTDC的读请求两者的时间很难正好撞上。但如果SDRAM的时序参数设置了临界值再加上系统负载升高导致总线占用繁忙就可能出现某一秒内刷新请求和LTDC读请求的冲突次数超过了FIFO的容忍能力导致画面出现短暂异常。更麻烦的是这种冲突不一定会留下明显痕迹。LTDC的FIFO有一定的缓冲深度短时间内的延迟可以通过FIFO吸收只有当延迟超过了FIFO能承受的时间才会表现为画面异常。而FIFO的缓冲时间取决于像素时钟和FIFO深度通常只有几十微秒级别这也是为什么异常现象往往只持续几百毫秒就会消失。3. 根因排查的完整过程3.1 第一步确认SDRAM本身是否稳定在排查一切和LTDC相关的问题之前我必须先确认SDRAM本身有没有问题。这个道理很简单如果SDRAM本身就不稳定那无论LTDC配置得多完美画面都照样花。我写了一个SDRAM压力测试程序把SDRAM的全部地址空间按固定模式写入数据然后读回来比对。测试模式包括全0、全1、0xAA55、0x55AA、递增地址数据、随机数据等循环跑了几千万次。正常情况下如果SDRAM的时序参数不对这种压力测试很容易暴露出读写错误。测试结果出乎意料在CPU频率216MHz、SDRAM时钟108MHz的情况下连续跑了3个小时SDRAM读写错误数为0。这说明SDRAM本身的读写功能是正常的时序参数也没有严重偏差。3.2 第二步加入总线并发因素既然纯CPU读写没问题那问题大概率出在并发访问场景。我把测试程序改了一下CPU在SDRAM里持续做读写操作同时DMA2D也在SDRAM和内部SRAM之间搬运数据屏幕上显示一张不断变化的动画。这样模拟了实际运行时的总线竞争环境。这个测试很快暴露了问题。当三种访问同时进行时屏幕上开始出现和实际故障完全一致的噪点和色块。我加了一些调试手段在LTDC的中断里统计FIFO错误标志果然每次画面出现异常时LTDC的FIFO下溢标志位都被置位。FIFO下溢意味着LTDC在一个像素时钟周期内没拿到该显示的数据被迫输出了上一个数据的重复值或者空值。这就解释了一切不是LTDC配置错了也不是SDRAM坏了而是SDRAM的响应时间在某些时候超过了LTDC FIFO能承受的范围。3.3 第三步锁定SDRAM时序的影响为什么纯CPU读写没问题一旦加上并发访问就出问题答案在于SDRAM的时序参数。我初始化SDRAM的时序配置如下FMC_SDRAM_TimingTypeDef SdramTiming; SdramTiming.LoadToActiveDelay 2; // tMRD SdramTiming.ExitSelfRefreshDelay 7; // tXSR SdramTiming.SelfRefreshTime 4; // tRAS SdramTiming.RowCycleDelay 7; // tRC SdramTiming.WriteRecoveryTime 2; // tWR SdramTiming.RPDelay 2; // tRP SdramTiming.RCDDelay 2; // tRCD这些值是在参考手册的推荐范围内选的理论上是合法的。但问题在于合法的配置和稳定的配置之间还有一段距离。SDRAM的时序参数以时钟周期为单位每个参数都有最小值和最大值限制。手册上给的是最小值实际使用时如果只按照这个最小值配置留给颗粒的余量就只有理论上的几个纳秒。在温度变化、电压波动或者总线繁忙的情况下这个余量很可能被吃掉导致一次读写操作实际上还没完成控制器就认为已经完成了产生数据错误。根据我个人的经验SDRAM时序必须留足余量不能卡着最小值配。这是我后面真正解决问题时最重要的一步。3.4 第四步排查刷新率配置除了时序参数SDRAM的刷新率设置也是关键。FMC的SDRAM控制器需要你告诉它“多久刷新一行”这个值通过SDCMR寄存器的COUNT字段设置。计算公式如下刷新周期 (刷新时间 / 总行数) × 时钟频率以常见的64ms刷新8192行计算每行刷新间隔为64ms / 8192 7.8125us。如果SDRAM时钟是108MHz也就是时钟周期约9.26ns那么每行刷新间隔对应的时钟周期数为7.8125us / 9.26ns ≈ 843但实际配置时还要减去控制器本身的开销约20个时钟周期所以我原来配置的COUNT值是820左右。这个值算下来刷新率大约在每秒12.8万次刷新命令看起来合理但问题在于SDRAM控制器在执行刷新命令时会阻塞正常的读写请求刷新率设置的越高阻塞时间就越长。我做了个实验把COUNT值调大延长刷新间隔屏幕上异常现象明显减少把COUNT值调小加快刷新频率异常现象变得频繁。这进一步印证了刷新操作和LTDC读取之间确实存在争抢。3.5 第五步审查整个时钟树还有一个一直被忽略的疑点时钟树。STM32F7的SDRAM时钟和LTDC像素时钟来自不同的PLLSDRAM时钟由PLLQ输出LTDC像素时钟由PLLSAI的P分频输出。如果这两个PLL之间存在互锁或者是公用同一个分频系数调整其中一个可能影响另一个。我检查了RCC配置确认这两个时钟是独立的没有共用分频器。但发现了一个细节SDRAM时钟虽然理论上可以配置到108MHz但这颗SDRAM颗粒在108MHz下的访问时间指标已经很接近极限。SDRAM的数据手册上标明的最大时钟频率通常是在特定负载条件下测得的实际PCB走线长度、阻抗匹配等因素会让它打折扣。4. 最终的解决方案与代码级修复4.1 重新整定SDRAM时序参数解决这个问题我做的第一件事是把SDRAM的时序参数全部重新配置不再卡着最小值。调整后的配置如下FMC_SDRAM_TimingTypeDef SdramTiming; SdramTiming.LoadToActiveDelay 3; // tMRD从2调到3 SdramTiming.ExitSelfRefreshDelay 10; // tXSR从7调到10 SdramTiming.SelfRefreshTime 6; // tRAS从4调到6 SdramTiming.RowCycleDelay 10; // tRC从7调到10 SdramTiming.WriteRecoveryTime 3; // tWR从2调到3 SdramTiming.RPDelay 3; // tRP从2调到3 SdramTiming.RCDDelay 3; // tRCD从2调到3每个参数增加1到3个时钟周期换算成时间大约是多出10到30ns。这个调整对SDRAM的整体性能影响很小但极大地提高了读写操作的可靠性。SDRAM颗粒在时序上留出的余量越大在极端情况下的容错能力就越强。4.2 调整刷新率设置刷新率COUNT值我进行了重新计算。为了更好地说明我换一种计算方式SDRAM行数为4096行这是我这颗颗粒的实际行数刷新时间64ms那么每行的刷新周期为64ms / 4096 15.625us。在108MHz时钟下这个时间对应的时钟周期数为15.625us × 108MHz ≈ 1687减去控制器建议的约20个时钟周期开销COUNT值设置为1667。这个值比原来我用的820大了一倍意味着刷新命令的频率减少了一倍对总线的占用时间也就减少了一半。这里可能有人会问刷新频率减半会不会导致数据丢失答案是不会。只要在64ms内完成全部4096行的刷新也就是每行刷新间隔不超过15.625us数据就能保持。我把COUNT设为1667对应每行刷新间隔约15.4us仍然在规格范围内但留给正常读写操作的时间窗口就宽裕很多了。4.3 显存缓存策略调整除了SDRAM控制器本身的配置MPU存储保护单元的缓存策略也值得检查。STM32F7的内核有L1 Cache如果SDRAM被配置为Write-Back模式那么CPU写入SDRAM的数据会先缓存在Cache里等到合适的时机再写回。这时候如果LTDC去读取显存可能读到的是SDRAM中还没更新到的最新数据导致画面出现残留或者错位。我的做法是把SDRAM的缓存策略改为Write-Through模式。Write-Through模式下CPU每次写入都会同步写到SDRAM不再经过Cache回写虽然写入性能略有下降但保证了LTDC读取和CPU写入的一致性。对于显示类应用来说写入一致性远比写入性能重要。MPU配置代码示例MPU_Region_InitTypeDef MPU_InitStruct; MPU_InitStruct.Enable MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress 0xC0000000; // SDRAM基地址 MPU_InitStruct.Size MPU_REGION_SIZE_32MB; MPU_InitStruct.AccessPermission MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable MPU_ACCESS_NOT_BUFFERABLE; MPU_InitStruct.IsCacheable MPU_ACCESS_NOT_CACHEABLE; MPU_InitStruct.IsShareable MPU_ACCESS_NOT_SHAREABLE; MPU_InitStruct.Number MPU_REGION_NUMBER0; MPU_InitStruct.SubRegionDisable 0x00; MPU_InitStruct.TypeExtField MPU_TEX_LEVEL0; MPU_InitStruct.DisableExec MPU_INSTRUCTION_ACCESS_DISABLE; HAL_MPU_ConfigRegion(MPU_InitStruct);我把SDRAM设置为完全不缓存Not Cacheable Not Bufferable这样CPU每次读写都直接访问SDRAM彻底回避了Cache一致性问题。对显示缓冲这种大块数据缓存的性能提升本来就不明显牺牲一点速度换取稳定物有所值。4.4 调整LTDC层配置减少带宽压力LTDC每个显示层都会占用SDRAM带宽层数越多、颜色格式越深带宽占用越高。我的项目用了两层一层是背景层ARGB8888格式一层是前景层ARGB8888格式。两层叠加之后LTDC每个像素需要从SDRAM读取8字节的数据。我把两层的颜色格式都改成了RGB565。人眼对颜色深度的敏感度远没有想象的那么高在800x480分辨率的屏幕上RGB565的色彩表现足够应对绝大多数界面场景。修改之后LTDC每像素读取的数据量从8字节减半到4字节SDRAM带宽压力直接下降一半。如果界面设计实在需要ARGB8888可以考虑另一条路把不需要Alpha透明效果的层改成RGB888格式省掉Alpha通道占用的字节同样能减少带宽占用。4.5 修改后的完整初始化流程修复后的SDRAM初始化流程和之前相比主要差别在于时序参数、刷新率、MPU配置三个部分。整体流程代码如下void BSP_SDRAM_Init(void) { // 1. 配置FMC时钟 __HAL_RCC_FMC_CLK_ENABLE(); // 2. 配置SDRAM控制器时序 static FMC_SDRAM_TimingTypeDef SdramTiming {0}; SdramTiming.LoadToActiveDelay 3; SdramTiming.ExitSelfRefreshDelay 10; SdramTiming.SelfRefreshTime 6; SdramTiming.RowCycleDelay 10; SdramTiming.WriteRecoveryTime 3; SdramTiming.RPDelay 3; SdramTiming.RCDDelay 3; // 3. 初始化SDRAM FMC_SDRAM_InitTypeDef SdramInit {0}; SdramInit.SDBank FMC_SDRAM_BANK1; SdramInit.ColumnBitsNumber FMC_SDRAM_COLUMN_BITS_NUM_8; SdramInit.RowBitsNumber FMC_SDRAM_ROW_BITS_NUM_12; SdramInit.MemoryDataWidth FMC_SDRAM_MEM_BUS_WIDTH_16; SdramInit.InternalBankNumber FMC_SDRAM_INTERN_BANKS_NUM_4; SdramInit.CASLatency FMC_SDRAM_CAS_LATENCY_3; SdramInit.WriteProtection FMC_SDRAM_WRITE_PROTECTION_DISABLE; SdramInit.SDClockPeriod FMC_SDRAM_CLOCK_PERIOD_2; // HCLK/2 108MHz SdramInit.ReadBurst FMC_SDRAM_RBURST_ENABLE; SdramInit.ReadPipeDelay FMC_SDRAM_RPIPE_DELAY_1; HAL_SDRAM_Init(hsdram, SdramInit, SdramTiming); // 4. 配置刷新率 SDRAM_RefreshConfig(); // 5. 配置MPU缓存策略 SDRAM_MPU_Config(); }4.6 验证结果改完这些配置后我重新跑了之前的并发压力测试CPU持续读写SDRAMDMA2D不停搬运屏幕播放动画。连续跑了72小时屏幕上没有再出现过任何一次异常。后面让系统带载运行了整整一周包括反复开关机、休眠唤醒、界面高频率切换等场景之前的偶发花屏再也没有复现。5. 常见问题与排查技巧实录5.1 偶发花屏问题排查速查表我整理了一个排查速查表方便大家对照检查排查方向检查内容判断标准SDRAM基础读写使用多种测试图案全地址读写连续运行1小时以上无错误SDRAM时序各时序参数是否留足余量参数是否接近手册最小值SDRAM刷新率COUNT值计算是否准确刷新间隔是否在规格范围内总线仲裁CPU、DMA2D、LTDC并发访问无FIFO下溢错误MPU缓存策略SDRAM区域是否配置了缓存Write-Through或Not Cacheable时钟树SDRAM时钟是否超频颗粒最大频率是否满足需求PCB走线SDRAM数据线/地址线等长信号完整性测试通过5.2 我踩过的坑和总结的经验排查这个问题的过程中有几个坑值得一提。第一个坑是盲目相信参考代码。ST官方的例程提供了SDRAM初始化代码但这些代码通常是在官方评估板上验证过的评估板的PCB布局、走线长度、供电质量都和你自己的板子不一样。直接把官方例程的参数套到自己的板子上未必能保证稳定。参考代码只能作为起点不能作为终点。第二个坑是忽略了SDRAM的供电稳定性。SDRAM对电源纹波非常敏感尤其是VDDQ和VREF引脚。我后来检查了板子的电源设计发现SDRAM的VREF电压是由两个电阻分压得到的没有加去耦电容纹波偏大。在电源VDD和VREF之间加了一个0.1uF的MLCC电容后花屏现象又有一定改善。如果遇到类似问题别忘记检查SDRAM的电源质量。第三个坑是没有尽早做并发压力测试。如果我没有在项目早期就编写SDRAM DMA2D LTDC同时工作的压力测试程序而是等到界面功能开发完再排查定位问题的时间至少要翻三倍。嵌入式开发里底层硬件的压力测试应该和应用开发同步进行越早做越好。第四个经验是分步修改、单变量验证。我在调试过程中每次只改一个参数改完就重新跑压力测试。这样可以明确知道哪个参数的改动有效果哪个没有。千万不要同时改多个参数否则出了问题根本没法判断是哪一步引入的。我把时序参数、刷新率、缓存策略三个方面的修改分成了三次独立的验证每次都在干净的代码基础上修改单一变量这才最终锁定了问题范围。5.3 最后的实用建议如果你现在正面临和这个项目类似的问题我建议按照这个顺序来操作先做SDRAM基础压力测试确认SDRAM本身在纯CPU读写场景下的稳定性如果基础测试没问题加入DMA2D和LTDC的并发访问压力测试如果并发测试暴露问题优先调整SDRAM的时序参数把每个参数从最小值往上加1到2个时钟周期检查刷新率COUNT值按照公式重新计算确保刷新间隔略小于颗粒规格的最大值查看MPU配置把SDRAM区域的缓存策略设置为Not Cacheable或者Write-Through最后检查电源和PCB布局确认SDRAM的供电质量和信号完整性SDRAM花屏这类问题本质上不是“哪里坏了”而是“哪里不够稳”。把系统里每一个潜在的风险点都排查一遍把每一处参数都调整到留有余量的位置问题自然就消失了。这也是我在这次调试中最大的体会在嵌入式系统里稳定不是一种状态而是一种设计。
返回列表