ARTICLE DETAIL

资讯详情

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

STM32F769 MJPEG视频播放与Bootloader升级实战

STM32F769 MJPEG视频播放与Bootloader升级实战 最近给一个用STM32F769做的中控面板做了一次大版本升级核心诉求就一条在现有UI里加视频播放。这个项目从需求评审到稳定量产前后折腾了三个多月踩了不少坑包括怎么选视频方案、怎么把升级通道做安全、怎么在MCU的硬件资源限制下把帧率跑上去。今天把整个实现思路和关键代码细节整理出来给有同样需求的朋友一个参考。适合正在做HMI/中控/家电面板类产品、准备给STM32项目加动效或视频功能、或者刚接触MCU图形开发想了解全流程的工程师看。先说结论STM32上跑视频绝大多数场景不是真的去解码H.264而是用MJPEG或帧序列方案配硬件解码器和DMA2D做流水线。真正难的不是解码本身而是帧数据从Flash到解码器再到屏幕的带宽管理以及和UI事件、软件升级机制的协同。这篇文章会把选型思路、Bootloader分区设计、视频解码链路、调优手段、典型问题排查全部讲清楚。1. 整体设计思路STM32跑视频先别急着写代码1.1 先搞清楚你要的是真视频还是伪视频这是整个项目第一个分岔路口。很多产品经理说给界面加个视频实际想要的效果差异很大如果只是开机动画、操作引导、动态Logo这类需求本质是帧序列动画。把画面预渲染成一组图片按固定节奏播放F103这种入门级MCU也能扛得住320x240分辨率。如果是播放一段产品宣传片、演示视频或者摄像头的实时画面回显这才是真正意义上的视频解码需要处理压缩数据流对主频、内存、总线带宽都有硬要求。我在项目启动前用一张表把这些需求列清楚建议大家设计评审时也这么做。需求类型典型场景最低MCU门槛建议方案帧序列伪视频开机动画、动态图标、翻页动效STM32F103预渲染PNG/JPEG帧按序播放真视频MJPEG宣传片、操作演示、动画剧情STM32F429/F769/H743独立JPEG帧连续解码真视频H.264摄像头回显、流媒体一般不推荐MCU方案外置解码芯片或应用处理器这次项目的需求是播放一段45秒的产品演示动画分辨率要到达320x240以上最后选了MJPEG方案。理由很简单MCU没有H.264硬解模块的话纯软解H.264在F769上跑到15fps都很吃力还会把CPU占满导致UI卡死而MJPEG每一帧都是独立JPEG图F769自带的硬件JPEG编解码器可以直接解解码速度快CPU占用低还能随机跳帧非常适合交互式播放。1.2 从F103到H7的选型跨度性能差距在哪很多朋友一开始习惯性想用F103觉得我以前用F103做过LCD显示加个视频应该也行。这个认知得纠正一下视频功能不是简单加个播放器库的事它对硬件外设有明确需求LTDC/LCD控制器F103没有LTDC只能用FSMCRGB屏或者SPI方式刷屏带宽严重不足。F429开始有LTDC和DMA2DF769/H743在此基础上还有ART加速器和硬件JPEG解码器。DMA2D做图像搬运、像素格式转换、混合叠加的关键外设。没有它的话每一帧从解码缓冲复制到显示缓冲都要CPU参与内存带宽直接被榨干。外部存储接口视频素材动辄几MB甚至十几MBF103用SPI NOR Flash读帧最多每秒1~2MB这连320x240x15fps的RGB565裸数据带宽约2.3MB/s都不够更别提JPEG解码输入。F429/F769支持SDRAM和更高速的QSPI Flash能显著缓解瓶颈。我这次用的F769主频216MHz内部有硬件JPEG解码器外扩了一片SDRAM和一片W25Q256 QSPI Flash。这个组合放320x240甚至640x360的MJPEG视频都够用实测320x24015fps解码时CPU占用大约25%UI交互依然流畅。如果预算有限用F429没有硬解JPEG得用NanoJPEG软解同样分辨率帧率会掉到8~10fps但也不算完全不能用。1.3 为什么还要单独设计Bootloader和分区这可能是整个项目里最容易被低估的部分。我们做的是软件升级加视频功能视频资源文件会跟着固件一起更新但视频素材和程序代码的更新频率完全不同。如果每次UI改个按钮都要把几百KB的视频资源重新全量下发用户升级时间会非常长而且网络抖动一次就前功尽弃。我采用的做法是把Bootloader、App、资源文件分区管理。App分区放程序固件资源分区放视频、图片、字体两者独立升级。Bootloader负责启动引导和接收升级包校验通过后再把新数据写入对应分区。这样即使视频资源升级失败App本身还是旧版本系统能正常运行不会变砖。2. 软件升级通道设计把Bootloader做得像运输队而不是门卫2.1 MCU启动流程与分区地图规划做Bootloader之前必须理解MCU的启动流程。STM32上电后从0x08000000读取栈顶指针和复位向量跳到SystemInit再进main。如果我们要让Bootloader跳转到App就得在App里重映射中断向量表用SCB-VTOR把向量表地址指到App起始位置。这里有一个特别容易踩的坑App工程的起始地址和链接脚本必须和分区表一致否则跳转后直接HardFault。下面是我这次项目的Flash和外部Flash分区方案可以直接参考分区地址范围大小用途Bootloader0x08000000 ~ 0x0800FFFF64KB升级管理、启动引导App当前版本0x08010000 ~ 0x0807FFFF448KB主程序固件App备份区0x08080000 ~ 0x080EFFFF448KB上一个版本的副本用于回滚系统配置0x080F0000 ~ 0x080FFFFF64KB升级标志、版本号、校验信息外部Flash资源区W25Q256 独立分区视素材大小视频、图片、音频资源全部放在外部FlashApp分区为什么留到448KB这么大因为F769本身有1MB内部Flash我一开始试图把代码压到256KB以内结果功能一多编译体积就失控后来干脆放宽到448KB。实际项目中这个尺寸还会根据功能模块动态调整但记住一个原则App分区宁大勿小备份区必须有。备份区的存在是为了实现升级失败自动回滚。升级流程是先把新固件写入备份区完整写入并校验通过后再切换启动标志下次重启由Bootloader决定运行哪个版本。如果中途断电备份区和当前运行区都还能正常工作这是A/B升级的基本思路虽然费一点Flash空间但极大降低变砖风险。2.2 升级协议设计用串口空闲中断收包最顺手升级通道我选了串口因为产品本身有串口调试口而且不需要额外硬件。协议没有用Y-Modem而是自定义了一套简单的分帧协议帧头0xAA55 包序号 长度 命令字 数据 CRC32。用串口空闲中断配合DMA接收收满一帧就解析解析完回ACK下一帧再发发完所有数据后再发结束包。这里重点说下为什么用串口空闲中断。如果只做逐字节接收任何一个字符延迟都会导致整包超时调试时特别折磨人。开启HAL库的UART空闲中断后一帧数据接收完毕会触发IDLE中断我只需要在中断里记录当前DMA接收长度就能精准定位帧边界不需要自己拼包。这个思路不仅适合升级协议任何串口帧协议都适用。// 接收完一帧后进入该函数 void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart huart1) { rx_frame_len Size; parse_frame_flag 1; // 重新启动空闲中断DMA接收等待下一帧 HAL_UARTEx_ReceiveToIdle_DMA(huart1, rx_buf, RX_BUF_SIZE); } }固件校验层面我用了CRC32加SHA256双保险。CRC32做快速初筛SHA256在固件完全接收后做最终校验。有条件的话还建议加签名业界常用做法是对固件包做AES-GCM加密或者Ed25519签名防止被篡改。这里不展开密码学细节但提醒一句凡是能联网的升级机制签名从第一天就要考虑别等出问题再补。2.3 视频资源怎么升级独立资源分区的优势视频素材是这次升级的核心但它的升级路径和App不一样。我的方案是把视频编码成统一格式后放到外部QSPI Flash的资源分区App代码里通过文件系统抽象层读取。这样视频资源升级时只需要把旧的资源分区擦除、写入新素材再更新一下资源索引表即可。App固件本身完全不用动。实际操作中资源升级和对App升级的链路是共用的Bootloader收到资源升级命令后就进入资源接收模式接收完普通校验、存到外部Flash。一旦中途异常退出Bootloader会检查资源区的魔数magic number和版本号发现无效就保持旧资源不切换这是很实用的安全兜底。3. 视频播放核心模块实现解码、缓冲、渲染一条流水线3.1 把视频预处理成MJPEG分辨率、帧率、体积的平衡视频源通常是MP4格式工程上我们会先用ffmpeg把它转成MJPEG帧序列。转码时的参数选择直接影响播放效果和存储体积。下面是我经过多轮实测后比较稳的参数组合适用于界面展示类视频ffmpeg -i input.mp4 \ -vf scale640:360 \ -r 15 \ -q:v 8 \ -pix_fmt yuvj420p \ output_%04d.jpg几个参数的含义要理解清楚才能根据需求调整scale分辨率。640x360是我给F769定的上限再高解码压力大且MJPEG文件体积会暴涨。如果是320x240体积能缩小一半以上播放更顺滑。-r 15帧率。10fps属于能看但有点卡15fps是流畅和开销的平衡点20fps以上对MCU压力就大了。-q:vJPEG质量数值越小质量越高。实际测试中8左右画质可接受文件比原始视频小很多如果素材是卡通动画可以适当提高压缩率。pix_fmt用yuvj420p而不是yuv444虽然画质略降但解码更快文件更小。转完之后把这些JPEG帧按顺序打包进一个资源文件再生成一个帧索引表每帧的偏移量和长度一并烧录到QSPI Flash。帧索引表的意义是支持随机访问播放比如循环播放、跳转到某一段时间不用每次从头部顺序扫描。3.2 解码链路DMA读Flash到JPEG解码器再到SDRAM启动播放时我的流水线分为三个阶段读取通过QSPI DMA把当前帧的JPEG数据从外部Flash搬进内存中的输入缓冲。解码把输入缓冲交给硬件JPEG解码器F769的JPEG模块解码输出RGB565像素数据到SDRAM中的帧缓冲1。显示把帧缓冲1中的像素通过DMA2D搬运到LTDC的当前帧缓冲或者直接切换LTDC的显示地址。这里有个很重要的技巧是双缓冲。解码器正在写帧缓冲1的同时LTDC在显示帧缓冲0下一帧解码完成后交换角色。这样显示端的刷新和解码端的写入不会互相干扰画面上不会出现撕裂。如果能做三缓冲还能进一步降低因解码抖动造成的掉帧但内存消耗大本项目的SDRAM够用我最终用了三缓冲。// 帧解码完成后的缓冲切换逻辑 void jpeg_decode_cplt_callback(void) { // 把刚解码完成的缓冲索引交给显示层 display_index decode_index; // 解码器立刻开始解码下一帧到另一个缓冲 decode_index 1 - decode_index; start_next_decode(decode_index); }3.3 硬件JPEG解码器的使用要点STM32F769的硬件JPEG解码器在HAL库里有现成驱动但有一点必须注意输出格式要在初始化时配置好。我使用RGB565输出这样LTDC可以直接显示不涉及像素格式转换。如果输出RGBA8888界面叠加透明通道更方便但内存占用翻倍且DMA2D转换会多一步。解码前要确保JPEG数据是完整的不能只给一部分。硬件JPEG模块的内部状态机对数据边界很敏感如果帧数据不完整解码结果可能是花屏或者直接报错。我遇到过一次从QSPI Flash读帧时因为DMA配置错误导致只读了一半数据画面就一直闪色块这个问题的排查花了大半天。解码性能方面320x24015fps时F769硬件JPEG解码器几乎不占CPU但DMA和总线带宽还是要关注。如果同时做视频解码和UI重绘建议给DMA2D分配高优先级LTDC刷新不能等。另外打开了数据缓存D-Cache的情况下解码缓冲和显示缓冲的Cache一致性要处理好否则画面出现随机色点。3.4 UI与视频叠加用好DMA2D的混合能力UI部分我用的LVGL。本来的设计是把视频解码后的帧直接当LVGL的图片对象显示后来发现这个做法性能不行。LVGL内部有自己的刷新机制如果视频帧以图片形式进入LVGL对象树每次刷新都会走LVGL的绘图管线CPU开销和内存复制都很大。更高效的做法是把视频播放窗口直接从LVGL的管理中摘出来。用LTDC的两个图层图层0显示视频帧图层1显示UI控件两个图层在LTDC内部做硬件混合。UI界面上需要显示视频的区域在视觉上留空视频图层就从那个区域透出来。这样LVGL只刷新UI层视频层由解码流水线独立驱动两者互不阻塞。如果你需要在视频上方叠加半透明控件或者字幕可以利用LTDC的ALPHA混合功能。具体做法是给UI层配置一个全局透明度或者在控件上单独使用DMA2D做alpha混合。我的经验是能用硬件层混合解决的就别用软件混合否则一帧图像叠加多个控件会把CPU拖垮。3.5 帧率控制与播放进度管理视频播放不能是死循环穷刷也不能让UI中断打断播放。我用一个FreeRTOS定时器任务来管理播放时钟每帧间隔用时间戳计算比如15fps的间隔是66.7ms。定时器到时检查是否该显示下一帧如果解码还没完成就跳过这一帧跳帧策略避免累积时间的卡顿。播放进度管理上我额外维护了一个播放时间戳音频如果有也用它来同步。这次项目不需要音频如果后续要加配音或背景音乐建议用PWMDAC播放WAV通过播放时间戳对齐否则音画不同步会非常明显。4. 调试实测与性能调优从能播到顺滑的距离4.1 实测数据与瓶颈定位工程做到一半我先用默认配置跑了一版测量结果是这样的320x24015fps MJPEG视频解码器工作正常但UI刷新明显变慢滑动列表的时候掉帧明显。用逻辑分析仪和STM32的性能计数器一抓发现瓶颈不在CPU频率而在SDRAM带宽。问题出在视频解码输出帧和UI显示帧都在SDRAM里DMA2D搬运数据也要经过SDRAM多个外设同时访问SDRAM时带宽被争抢。后来我把视频帧缓冲区的一部分移到内部RAMF769有较大SRAM但无法覆盖所有缓冲再用DMA2D分块搬运把一次大块搬运拆成多个小块交错执行画面帧率和UI流畅度都上来了。优化项优化前优化后说明视频帧缓冲位置全部SDRAM部分内部SRAM SDRAM减少外部总线争抢DMA2D搬运方式整帧搬运分块交错搬运降低单次总线占用时间MPU/ART加速器未开启开启Flash预取提升指令和只读数据访问速度帧率10fps左右稳定15fps体验明显提升4.2 常见问题与排查技巧调试过程中积累了几个高频问题和对应的排查思路直接整理成表方便大家遇到类似情况快速定位。现象可能原因排查方向与解决ST-Link连接不上无法烧录芯片读保护RDP开启或调试引脚被复用先用ST-Link Utility连接并解除读保护确认没有禁用JTAG/SWD引脚程序跳转到App后跑飞中断向量表未重映射确认App工程里SCB-VTOR指向App起始地址且全局中断在跳转前已关闭视频画面撕裂显示端在清屏时解码端在写同一缓冲启用双缓冲/三缓冲保持显示缓冲与解码缓冲分离画面偶发花屏/色块帧数据不完整或Cache一致性问题检查DMA读取Flash的传输长度对解码缓冲执行Cache Clean/Invalidate视频播放时UI卡顿SDRAM带宽争抢或LVGL刷新过于频繁把UI帧率限制在30fps以内视频层和UI层分图层显示优化DMA2D调度特别说一下禁止JTAG释放引脚这个点很多人会踩。F769的某些调试引脚默认复用为GPIO如果产品需要这些引脚接外部设备就得在代码里调用GPIO_PinLockConfig或者配置AFIO来禁用JTAG只保留SWD。但是调试阶段千万别禁用否则连不上调试器只能用ST-Link Utility的Connect Under Reset模式恢复非常麻烦。我一般是软件发布前才加上禁JTAG的配置。4.3 嵌入式环境里的升级与调试工具链开发过程中用到的工具链也简单分享一下。代码工程基于STM32CubeMX生成IDE用的Keil MDK和VS Code交叉使用CubeMX负责引脚配置和时钟树具体逻辑代码在VS Code里写最后用STM32CubeCLT的命令行工具做持续集成构建。如果你在Linux下做STM32开发现在用STM32CubeCLT加OpenOCD加VS Code的组合已经很成熟不再需要依赖Windows环境。烧录和调试方面日常调试用IAR/Keil的在线调试批量生产用ST-Link批量烧录脚本或者让产线利用Bootloader的串口升级通道烧录。STM32 ST-Link Utility这个老工具虽然官方停止更新了但用来做整片擦除、读保护解除、手动下载hex仍然很顺手生产返修时我经常用它。5. 工程化落地与扩展方向5.1 视频资源的包管理与版本控制视频资源不能只管一次。产品后续迭代会不断换宣传片、换引导动画因此资源包要有明确的版本号、素材格式约定和打包脚本。我这边做了一个小工具把FFmpeg转出的JPEG帧、音频WAV、帧索引表打成一个res_pack_v3.bin同时生成一个JSON描述文件描述版本、分辨率、帧率、时长。Bootloader在升级资源时校验JSON里的CRC和版本号只有版本递增时的资源才允许覆盖旧的。如果你维护的产品线很多建议把视频素材统一放在git仓库里做版本管理由CI自动打包资源文件。这比在开发工程师本机手工转码可靠得多至少不会出现程序里调用第32帧但资源包只有30帧这种低级错误。5.2 进阶玩法远程升级、触摸手势、视频音频这次项目做完后我还在往三个方向扩展第一是远程升级。MCU通过以太网或Wi-Fi模块联网用HTTP拉取新固件和资源包然后走已有的Bootloader升级通道。热词里提到了STM32的HTTP库一般配合lwIP用注意HTTP下载要做断点续传和超时重试不能一断就从头来。第二是触摸手势与视频联动。在视频播放层和UI层混合的前提下可以做到滑动手势切换视频场景或者在视频某帧停留时自动弹出控件。触控用的是电容触摸芯片通过I2C把坐标送给MCU在UI层做区域命中判断。这里涉及触摸校准和ADC采样因为有些电容屏的坐标原始值是ADC值需要做线性变换可以参考常规的ADC多点校准方法保证点击位置和UI控件对应准确。第三是视频音频同步。F769没有专用音频DAC我计划用I2S外接一颗音频Codec播放WAV文件播放进程和视频播放进程通过时间戳对齐。同步实现的关键是维护一个共同的媒体时钟不能用两个任务各自计时否则跑几秒钟之后音画就会差一截。5.3 项目复盘做带视频的MCU UI哪些坑是可以提前绕开的最后把经验集中复盘一下。第一视频方案要在硬件选型阶段就决定。如果产品规划里已经明确了要播放演示动画直接选带LTDC、DMA2D和硬件JPEG解码器的型号F429起步、F769/H743更好别抱着F103不放后面再迁移代价更高。第二Bootloader和App必须解耦。很多项目一开始不做Bootloader后期要升级时再补就会碰到Flash空间不够、启动向量冲突、升级协议与现有代码纠缠不清等问题。这次项目因为一开始就设计了独立Bootloader后面加视频资源升级几乎是无痛迁移。第三帧率和清晰度是最典型的产品语言。给产品经理评审的时候我直接提供从转码参数矩阵导出的几条demo320x24010fps、320x24015fps、640x36015fps、640x36020fps让业务方自己挑哪个效果能接受再反推硬件成本。这比口头沟通高效很多建议同行的朋友都用这个办法。第四升级失败率要当作核心质量指标。视频资源包通常比固件大得多几MB的数据在串口或网络传输中出错的概率不低。协议要带分帧重传和断点续传校验要够强升级标志位要有看门狗兜底。宁可升级慢一点也不能升级后变成砖。写在后面做MCU上的视频播放最大的感受是性能不是靠堆配置堆出来的而是靠合理规划总线、缓冲和调度省出来的。很多朋友拿到F769/H743觉得很强大随手就把视频和UI都塞进LVGL结果帧率上不去再去优化绕了不少弯路。我个人现在做这类项目的标准顺序是先定视频源和播放场景再定数据存放介质和缓冲方案最后才考虑UI框架怎么集成。把流水线设计清楚了剩下的代码其实都是套路。如果你也在做类似项目遇到具体的解码花屏、升级回滚、帧率上不去这类问题欢迎按文章里的排查表逐项对一遍大概率能少走我走过的弯路。
返回列表