ARTICLE DETAIL

资讯详情

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

TouchGFX外部Flash OTA无感升级方案:分时复用与双区切换实践

TouchGFX外部Flash OTA无感升级方案:分时复用与双区切换实践 做TouchGFX项目的朋友应该都有过这种经历产品跑着跑着要升级固件升级包通过串口或网络传过来然后界面就卡住了更惨一点的是花屏、黑屏、按键点了没反应用户看着像设备变砖了一样。我们最近在LAT1499这个型号上就碰到了这个问题。这个项目的TouchGFX资源图片、字库、语言包体积已经超过了内部Flash容量全部放在外部Flash里升级固件时又必须写同一颗Flash。如果照搬传统做法升级期间把GUI完全停掉先用外部Flash写升级包再恢复A/B测试阶段就被客户否了——操作员正盯着设备界面你直接把画面冻结几十秒体验完全说不过去。后来我们把方案改成基于外部Flash分时复用的无感升级整个升级过程中界面照常刷新、触摸照常响应升级完成后重启一次切换资源区用户几乎感知不到升级发生。这就是标题里写的LAT1499无感升级方案。下面我把设计思路、时间片量化方法、状态机设计和工程上踩过的坑都摊开讲给正在做TouchGFX加外部Flash OTA的朋友一个可复现的参考。1. 为什么升级必须动外部Flash资源和固件共存的现实1.1 TouchGFX资源为什么被放进外部Flash现在TouchGFX项目里UI资源体积增长非常快。一块480x272的LCD纯RGB565全屏一帧就是250KB左右一套界面下来几十张图片再加上多语言字库、文本翻译表几MB是很常见的。而主流MCU的内部Flash除去Bootloader和应用程序代码能留给资源的空间往往只有几百KB到1MB出头完全不够用。把资源放外部Flash就成了标配。外部QSPI Flash现在8MB、16MB非常便宜一片W25Q128也就几块钱容量是内部Flash的好几倍而且TouchGFX原生支持从外部Flash直接加载资源。具体做法是把图片、字库等通过TouchGFX Designer编译成Bin文件运行时映射到MCU的外部存储器地址空间代码可以直接像读内部Flash一样访问这些资源速度也够快。这就是“读取外部Flash”这个动作的主要来源也是后面所有麻烦的根源。1.2 传统升级方案的三个硬伤触屏设备升级固件如果按老式做法先把GUI停掉然后擦除外部Flash、写入新固件、再恢复GUI会撞上三个很实际的问题。第一个是体验断层。升级一启动屏幕就冻结在某个画面快的十几秒慢的一两分钟用户不知道发生了什么只能干等。如果升级过程中来了个电话或者紧急报警操作员根本没法处理这在工业HMI、医疗设备、门禁机这些场景里是不能接受的。第二个是恢复风险。外部Flash擦写不是原子操作升级包写入一半掉电资源区就被破坏了一半。如果资源区里还混着当前正在运行的程序要用的图片和字库重启后直接就花屏或者卡死在启动界面。传统方案里要么做整片Flash的完整备份要么赌掉电不发生两者都不省心。第三个是操作窗口问题。升级时必须把外部Flash让出来但GUI渲染随时都在读资源这两个动作天然冲突。强行暂停GUI是回避冲突而不是解决冲突。分时复用就是冲着这个问题去的把冲突的两个操作在时间上错开让升级写入一点一点“挤”进GUI渲染的空隙里既不停GUI也不耽误升级。1.3 分时复用要解决的四个关键问题真正要把分时复用落地需要回答四个问题Flash怎么分区才能保证GUI数据和升级数据互不干扰GUI的“空闲窗口”在哪里有多大一次写入一个多大的块才能安全落进窗口如果升级中途掉电或者写入出错怎么保证设备还能启动、还能恢复。这四个问题贯穿整个方案设计后面每一节都会围绕它们展开。想清楚再动手写代码比拿到需求直接开搞要稳妥得多。2. 外部Flash分时复用的核心设计2.1 Flash分区双资源区加暂存区分时复用的前提是Flash分区逻辑清晰。我建议直接把外部Flash划分为五个区域其中关键在于资源区做成双份也就是A区和B区。区号起始地址示例大小功能10x00000064KBBoot参数、版本信息、升级标志20x0100004MBGUI资源区A当前激活30x4100004MBGUI资源区B备份/升级目标40x8100002MB升级包暂存区50xA10000剩余运行日志、参数备份等资源区A和B内容一样但同一时刻只有一个被激活。当前GUI从A区读取资源升级时新资源写到B区写完后Bootloader切换启动标志设备重启后使用B区A区变成旧版本留作回滚。这样的好处是升级过程中永远有一个完整可用的资源区在服务界面任何时刻都不会出现“读到一半”的数据从根本上消除了花屏风险。暂存区用来接住传输过来的升级包。升级包先完整下载到暂存区校验CRC通过后再拷贝到非激活资源区。这么做是为了避免边下载边擦写的复杂性传输网络不稳定时随时可以中断重来反正暂存区是垃圾数据不影响当前运行。Boot参数区放在最前面单独划出来存放当前激活的资源区编号、升级状态、CRC校验值。这个区域需要加写保护禁止普通代码直接修改只有Bootloader在严格校验后才能写入。2.2 找准“读取外部Flash”的空闲窗口分时复用的关键是吃透TouchGFX一帧的渲染节奏。TouchGFX通常以VSync为节拍每个帧周期做四件事接收垂直同步信号、更新UI状态、渲染当前帧到帧缓冲、等待下一帧信号。在这四件事里真正需要从外部Flash读资源的只有渲染阶段其他阶段CPU和QSPI总线相对空闲。以30fps为例一个帧周期约33ms。渲染阶段根据界面复杂度不同可能需要10ms到25ms剩下的就是空闲窗口。这个窗口就是升级写入的安全时间片。需要特别注意的是渲染过程中读取外部Flash也不是持续占满总线的TouchGFX会把位图读到缓存里很多小图读完一次就命中缓存不会反复读Flash。所以实际空闲窗口比理论值更大需要通过打点实测。我建议在工程里加一个调试钩子在TouchGFX渲染回调的入口打一个GPIO翻转或者用一个32位定时器记录渲染开始和结束的时间戳然后把耗时统计值通过串口或调试器导出来。跑几个典型界面包括静止页面、列表滚动、全屏动画找出最坏情况下的渲染耗时。这个最坏值决定了安全窗口的下限写Flash的每次操作必须小于这个下限才能保证不拖累渲染。2.3 计算一帧能挤出多少写入时间动手设计之前先把账算清楚。我的经验是取最坏渲染耗时作为基准而不是平均值。假设实测最坏情况下渲染耗时22ms帧周期33ms那么理论空闲窗口只有11ms。别把11ms全用满给渲染和系统调度留足余量我一般只取50%也就是每次升级写入的操作窗口控制在5ms左右。然后看Flash写入速度。以常见的W25Q64JV为例页编程256字节典型耗时0.4ms擦除一个4KB扇区典型耗时45ms读写一个字节的QSPI指令开销大约几微秒。5ms窗口内如果不擦除只做页编程能写入大约2KB到3KB。30fps下一秒钟就是60KB到90KB的实际写入速度。假设升级包有效数据5MB被拆成1280个扇区。先算擦除时间1280个扇区乘以45ms约57.6秒。再算页编程时间5MB除以每窗口2.5KB约2048个窗口每个窗口0.4ms到1ms累计约1到2秒相比擦除可以忽略。总体升级耗时约1分钟加上暂存区分页校验和传输时间整个过程控制在2到3分钟内。这个速度看起来不快但用户体验上完全没有断点用户可以继续翻页面、点按钮代价是升级时间比暂停GUI方案长了一些但换来无感体验完全值得。需要提醒的是实际写入速度还受QSPI时钟频率、DMA模式、Flash型号影响。比如把QSPI时钟从40MHz提到80MHz读取和写入都快一点但信号完整性也要重新验证。后文会讲到如果实测升级速度远低于预期优先查哪几个地方。3. 工程落地任务划分、状态机与关键代码3.1 GUI任务与升级任务如何协同代码层面我用了FreeRTOS加TouchGFX的经典组合。系统里跑三个与升级相关的任务GUI任务保持最高优先级负责渲染和事件响应传输任务中等优先级负责从串口或网络接收升级包升级任务最低优先级负责把数据写入外部Flash。三个任务之间用事件标志组和信号量协同。GUI任务每渲染完一帧在TouchGFX的帧完成回调里发送一个事件表示“这一帧已经消费完外部Flash读操作QSPI总线暂时空闲”。升级任务阻塞等待这个事件收到后才执行一次Flash写入操作写完后立即把总线让回来等待下一个帧完成事件。这个机制保证了写入操作永远只发生在渲染间隙不会与GUI抢总线。核心是事件通知不是简单的延时。靠sleep或者延时来做时间片分配一旦渲染耗时抖动就会撞车必须在帧完成回调里做同步。3.2 升级状态机设计升级任务内部用一个状态机管理整个流程。状态切换如下typedef enum { OTA_IDLE 0, // 空闲 OTA_DOWNLOAD, // 下载升级包到暂存区 OTA_VERIFY, // 校验暂存区CRC OTA_COPY_RES, // 将暂存区数据拷贝到非激活资源区 OTA_MARK_SWITCH, // 写入切换标志 OTA_UPDATE_DONE, // 升级完成等待重启 } OtaState;OTA_DOWNLOAD阶段每接收到一包数据就写入暂存区但写入动作同样要等帧完成事件不能一收到数据就直接写Flash。OTA_COPY_RES阶段是分时复用的核心把暂存区的数据按块拷贝到非激活资源区每帧拷贝一块直到全部完成。OTA_MARK_SWITCH阶段只在拷贝完成后执行一次写入目标资源区编号和CRC值到Boot参数区然后通知系统可以重启。这里有一个非常重要的设计细节升级状态必须持久化。如果设备在OTA_COPY_RES中途掉电重启后Bootloader要能判断出“上次升级没完成资源区B是坏的”然后自动回退到资源区A并把升级任务状态恢复到“继续拷贝”或“重新下载”。我的做法是每拷完一个扇区就在Boot参数区更新一次偏移量这样断点续传和回滚都有据可依而不是简单记录一个“正在升级”的标志。3.3 关键代码分时写入与双区切换核心代码逻辑可以抽成两个函数一个是GUI帧完成回调一个是升级任务主循环。这里给出一个可运行的简化版本。// GUI任务TouchGFX主循环 void TouchGFX_Task(void) { while (1) { UI.update(); // 渲染一帧 UI.requestRedraw(); BaseType_t hpw pdFALSE; // 帧渲染完成通知升级任务可以占用Flash xTaskNotifyFromISR(otaTaskHandle, OTA_EVT_FRAME_READY, eSetBits, hpw); portYIELD_FROM_ISR(hpw); } }// 升级任务等待帧完成事件后执行写Flash void OtaTask(void *param) { uint32_t evt 0; while (1) { xTaskNotifyWait(0, 0xFFFFFFFF, evt, pdMS_TO_TICKS(200)); if (!(evt OTA_EVT_FRAME_READY)) { continue; // 没有帧完成事件不碰Flash } switch (otaState) { case OTA_DOWNLOAD: otaDownloadNextPacket(); // 写暂存区 break; case OTA_COPY_RES: otaCopyNextChunk(); // 每次拷贝4KB break; case OTA_MARK_SWITCH: otaMarkBootSwitch(); break; default: break; } } }otaCopyNextChunk函数里我按4KB对齐拷贝一个窗口处理一个扇区。但前面算过一个扇区擦除需要45ms远超过5ms的安全窗口所以不能在一个窗口里既擦除又写入。实际我采用拆分策略先在一个窗口里执行扇区擦除擦除期间GUI任务如果要读Flash会短暂阻塞这个风险需要规避。更稳妥的做法是把擦除也拆成小片先把目标扇区4KB数据读回SRAM暂存再把擦除操作放到一个“长窗口”。所谓长窗口是指画面静止时帧周期会被拉长比如TouchGFX在无触摸变化时可能进入低刷新模式帧间隔从33ms变成100ms甚至更长这时候有充足的时间做完一次完整的扇区擦除。代码里可以通过检测帧完成事件的间隔来判断当前是不是长窗口只有间隔超过50ms才执行擦除否则只做页编程。双区切换的逻辑在otaMarkBootSwitch里static void otaMarkBootSwitch(void) { BootParam_t param; param.activeRes currentTargetRes; // 目标资源区编号 param.crc calcBootParamCrc(param, sizeof(param) - 4); qspi_write_enable(); qspi_sector_erase(BOOT_PARAM_SECTOR); qspi_wait_busy(); qspi_write_enable(); qspi_page_program(BOOT_PARAM_ADDR, (uint8_t *)param, sizeof(param)); qspi_wait_busy(); otaState OTA_UPDATE_DONE; }Bootloader在启动时读取这个参数校验CRC如果合法就跳转到对应资源区不合法则强制使用资源区A。注意参数写入时最好使用双字冗余比如同一个结构体写两份一份损坏还有另一份兜底。3.4 掉电保护与恢复流程掉电保护是整个方案里最容易被忽略但又最关键的部分。我按三个阶段分别做了防护。下载阶段掉电暂存区里的数据不完整下次启动时Bootloader发现没有合法的升级标志就直接进入正常模式不影响当前GUI运行。拷贝阶段掉电非激活资源区可能只写了一半。此时Bootloader读到Boot参数区里的偏移量发现上次拷贝没有完成就把激活区仍然指向A区同时保留偏移量。设备正常启动后升级任务发现还有未完成的拷贝任务可以选择继续拷贝或者重新开始。我建议重新开始因为继续拷贝要检查已拷贝块的CRC逻辑复杂且收益低一个4MB资源包重拷也就一两分钟。切换标志写入阶段掉电是最危险的窗口。如果我已经把资源区编号改成了B但B区其实没写完整设备重启就会跑一个坏资源。所以顺序必须是先确保目标资源区完整再写切换标志。标志本身还要带CRCBootloader发现CRC错误就回退A区。这样即使掉电点落在标志写入的瞬间最坏情况也只是回到旧版本而不是变砖。4. 常见问题与排查技巧实录4.1 升级过程中界面卡顿怎么查实测下来界面卡顿绝大多数不是Flash写入本身慢而是写窗口没有控制好挤占了渲染时间。排查时先去掉升级任务单独跑GUI用定时器测出基准帧耗时然后把升级任务加回来逐步把每次写入的块从1KB调到8KB观察帧耗时拐点。拐点出现的位置就是当前硬件平台的安全窗口上限。还有一个容易被忽略的坑QSPI写入期间的CPU轮询。如果写Flash时用了阻塞式轮询状态寄存器即使每次只写2KBQSPI传输过程中CPU也会被占住触摸响应和UI动画都会受影响。解法是写入改成DMA方式CPU在DMA传输完成后收到中断再把Flash的WIP位检查放到周期任务里做不要死等。我们项目里就是因为这个卡顿排查了很久最后把写入改成DMA加中断回调帧率才恢复稳定。4.2 写入失败和校验失败的常见原因写外部Flash失败先查三件事地址是否4KB对齐页编程是否跨256字节边界擦除后是否等了WIP位清除。页编程跨边界是新手最容易犯的错Flash的页编程一次最多写256字节如果地址跨页必须拆成两次。另外一个高频坑是QSPI时钟频率过高信号完整性变差写入偶尔失败。把时钟从80MHz降到40MHz再试如果问题消失就是布线或上拉电阻的问题。校验失败还要区分是传输错误还是写入错误。我的做法是在升级包头带上CRC32下载阶段实时校验拷贝阶段写入后回读一次关键扇区再校验。两者都通过才允许切换标志。如果下载阶段CRC失败直接要求重传对应分片如果拷贝后回读失败说明Flash写入异常需要检查供电电压是否满足Flash工作条件尤其注意擦写瞬间电流尖峰是否导致电压跌落。4.3 资源切换后花屏怎么办升级完成后重启界面花屏是最令人生气的bug因为提示你升级成功但是界面没法看。大多数情况下都是缓存问题QSPI映射到MCU地址空间后CPU的I-Cache和D-Cache缓存了旧资源区的数据。切换资源区后如果缓存没有失效CPU仍然命中旧缓存读出来的图片自然就是乱的。解决方法是在Bootloader跳转前执行一次完整的Cache无效化操作包括I-Cache和D-Cache同时在TouchGFX初始化外部Flash资源前再对资源映射地址段做一次SCB_InvalidateDCache_by_Addr。还有一个间接原因是TouchGFX内部的位图缓存旧资源区A的热点图仍然存在RAM里。切换资源区后强制调用TextureCache的清理接口或者直接重启后做一次全界面重绘可以避免残留缓存导致的花屏。4.4 实测数据与调优建议最后给一组我们项目里的实测数据帮助大家建立一个直观感受。测试环境是STM32H750 W25Q128 480x272 LCDTouchGFX跑30fpsQSPI时钟80MHzDMA方式写入。指标传统暂停GUI方案LAT1499分时复用方案升级期间界面状态全程冻结正常刷新触摸正常4MB升级包总耗时约40秒约2分10秒失败后设备恢复需要进入恢复模式自动回退旧资源区掉电导致花屏风险高低双区保护代码复杂度低中等偏高从表格可以看到分时复用最大的代价是升级耗时拉长到两倍以上但换来的是用户体验和可靠性的大幅提升。如果产品对升级速度有硬性要求我建议优先优化安全窗口的利用率比如在界面允许的前提下升级期间把刷新率从30fps降到20fps每帧能挤出的窗口会大不少升级耗时能压到1分半左右。再配合DMA和乒乓缓冲实测可以做到近乎无感的同时升级速度不明显拖延。最后说一点我自己的体会。无感升级看起来是个功能点本质上是对系统时间资源的管理——把一帧33ms掰开用既要保证界面渲染又要把Flash擦写一点点塞进去非常考验工程细节。这套分时复用方案我们在LAT1499上完整跑过流程可以复现但具体时间参数一定要以你自己板子的实测为准。Flash型号不同、QSPI时钟不同延迟差异会很明显。如果你们项目里也碰到TouchGFX升级卡界面的问题可以按这个思路先做分区和时间片量化再用数据说话调参数会比反复试错靠谱得多。
返回列表