尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32H723实时摄像头MJPEG流媒体推流方案详解

STM32H723实时摄像头MJPEG流媒体推流方案详解 用STM32H723VGT6做实时摄像头流媒体这个标题听起来像是个“小马拉大车”的活儿。单片机跑视频流还要被浏览器直接打开看很多人第一反应是不太现实。但这颗料确实把路走通了Cortex-M7跑到550MHz片内带硬件JPEG编解码器又有以太网MAC和USB HS单芯片完成“摄像头采集→JPEG压缩→网络推流”的全链路不需要Linux也不需要额外视频编码芯片。我做完这套方案后第一感受是H723被严重低估了但踩坑也确实不少。这篇文章把我从零开始搭建的完整过程写出来包括DCMI接口怎么接摄像头、硬件JPEG编码器怎么配置、MJPEG流怎么推到浏览器播放以及我在调试中遇到的黑屏、花屏、断流、帧率上不去等一系列问题。适合手里有H723开发板、想自己做一个低成本IP Camera或工业视觉预览方案的工程师参考。1. 为什么选STM32H723VGT6做视频流传输1.1 这颗芯片的定位与传统MCU的差异传统的MCU做图像处理基本就是“采集个静态图然后存下来”很少谈得上实时视频流。原因很直接CPU算力撑不住软编码、内存不够存帧、网络吞吐跟不上。STM32H723VGT6的出现把这几个瓶颈一次解决掉了。核心指标上H723搭载单核Cortex-M7最高主频550MHz内置1MB Flash和564KB SRAM。相比H743、H750它虽然没有双核但把主频拉到550MHz同时保留了硬件JPEG编解码器、DCMI数字摄像头接口、10/100M以太网MAC、USB OTG HS、SDMMC、FMC等一整套外设。单芯片方案里这颗料几乎是为摄像头应用定制的。更重要的是H723在内部总线上做了优化DCMI采集的数据可以直接通过DMA搬运到SRAMJPEG硬件编码器又能把YUV数据压缩成JPEG码流CPU只需要做调度和网络协议栈不需要参与像素运算。这就让“实时视频流”在MCU上变得可行而不是靠主频硬扛。1.2 硬件JPEG编解码器到底解决了什么问题视频流传输最耗资源的环节就是图像压缩。如果不压缩VGA分辨率640x480、30fps、每个像素YUV422占2字节一秒钟的数据量就是 640 × 480 × 2 × 30算下来约18.4MB/s。这个吞吐量放到100M以太网上理论带宽12.5MB/s已经超了而且MCU根本来不及搬运。H723内置的JPEG硬件编解码器可以接受YUV422或YUV444格式的图像数据通过硬件流水线输出标准JPEG码流。编码过程不占用CPU计算资源CPU只需要配置参数、启动编码、等待中断、读取结果。实测下来640x480分辨率的图像从启动编码到拿到JPEG数据大约几毫秒到十几毫秒具体取决于图像复杂度这个速度足够支撑实时推流了。需要特别说明的是这里的JPEG编码器是硬件模块和软件库TinyJPEG、libjpeg完全两码事。硬件编码的延迟低得多也不吃CPU但要正确配置输入格式、量化表、输出缓冲区否则很容易出现编码失败或者花屏。1.3 为什么选择MJPEG而不是H.264MCU方案里有人会问为什么不做H.264H.264压缩率高画质还好。但问题在于STM32H723没有硬件H.264编码器软件H.264编码在550MHz的M7上做VGA分辨率实时编码几乎不可能稳定跑起来编码一帧的时间就足以让帧率掉到个位数。MJPEG本质上是一系列独立的JPEG图像每一帧单独压缩不带帧间压缩。它的缺点是压缩率比H.264低但优点是实现简单、延迟低、每一帧都是完整图像丢一帧不会影响后续帧解码。浏览器端直接支持MJPEG流播放不需要额外插件和协议栈非常适合作为MCU视频流的第一版方案。在实际项目中如果你的带宽有限后续也可以通过添加外部H.264编码芯片来升级但第一版用H723自带的JPEG硬件编码器把通道跑通性价比最高。2. 整体架构与数据通路设计2.1 摄像头采集到网络发送的完整数据流整个系统的数据流可以拆成四条链路我画在自己的设计笔记里大致是这样的走向摄像头OV2640通过DCMI接口输出YUV422并行数据DCMI外设在像素时钟驱动下把数据按行写入DMA缓冲区。DMA使用乒乓缓冲一个缓冲在接收当前帧另一个缓冲已经填满的帧交给JPEG硬件编码器处理。JPEG编码器把YUV数据压缩成JPEG码流存放在输出缓冲区。CPU在收到JPEG编码完成中断后把这段JPEG数据打包成HTTP响应通过lwIP协议栈走以太网发送到浏览器。这里有一个关键设计点DCMI采集和JPEG编码必须并行执行不能先采集完一帧再开始编码。否则采集过程CPU被占用帧率会直接减半。所以乒乓缓冲是必须的采集和编码各自有独立的DMA通道CPU只在关键节点做上下文切换。2.2 两种摄像头数据模式直出JPEG还是输出YUV使用OV2640时你其实有两种选择让OV2640内部JPEG引擎直接输出JPEG数据或者让OV2640输出YUV422原始数据、再由H723硬件JPEG编码器压缩。第一种方式看起来更省事因为OV2640自带JPEG压缩引擎DCMI拿到直接就是JPEG码流MCU完全不用碰压缩。但实际用下来有几个坑OV2640内部压缩质量不可控部分分辨率下JPEG输出不稳定偶尔会出现花帧而且你把JPEG输出配置到DCMI后DCMI需要解析JPEG码流的字节流DMA缓冲区管理会比较麻烦。第二种方式是让摄像头输出YUV422交给H723的硬件JPEG编码器处理这样压缩参数完全可控量化表、分辨率、数据格式都能自定义而且硬件JPEG编码器的输出质量比OV2640内部的压缩引擎稳定得多。我在实际项目中最终选的是第二种方案YUV422走DCMI进入SRAM后由JPEG硬件编码器压缩。2.3 内存规划与DMA缓冲区的坑H723虽然有564KB SRAM但内存是分段的。DMA和JPEG外设对内存有特殊要求在设计阶段如果没规划好后面调试会非常痛苦。H723的内存布局大致是DTCM紧耦合数据内存只有128KB指令紧耦合ITCM也有128KBAXI SRAM有256KB另外还有SRAM1/2/3等。关键点是DCMI的DMA、JPEG编码器的DMA访问必须使用AXI SRAM地址范围0x24000000开始的那段因为外设DMA挂载在AXI总线上访问DTCM会导致数据传输异常。我实际分配是这样做的DCMI采集缓冲用了两块64KB的乒乓缓冲区放在AXI SRAMJPEG编码输出缓冲区预留64KB也放在AXI SRAMlwIP的PBUF池、网络描述符放在SRAM1/2区域剩下的DTCM和剩余SRAM给RTOS任务栈和应用程序。如果只用裸机开发内存规划可以更简单但使用RTOS时一定要确认每个任务栈所在的内存段避免出现DMA访问冲突。2.4 FreeRTOS还是裸机这个项目我用的是FreeRTOS任务拆成三个摄像头采集任务优先级最高、JPEG编码任务、网络发送任务。采集中断通过信号量通知编码任务编码完成中断通过队列把JPEG数据地址传给网络任务。用RTOS的好处是每个环节可以独立调试而且网络发送如果阻塞不会影响摄像头采集。裸机也可以做用一个大循环轮询各状态标志但一旦lwIP的发送函数出现阻塞帧率就会明显波动排查起来很麻烦。所以我建议直接上FreeRTOS工程结构会清晰很多。3. 硬件连接与基础环境搭建3.1 元器件选型与接线摄像头我选的是OV2640模块这是最常用的MCU摄像头传感器200万像素支持YUV422、RGB565、JPEG输出自带DVP并行接口和STM32的DCMI外设正好匹配。OV2640模块市面上很多几块钱到几十块的都有建议选带FIFO的模块还是不带FIFO的这里要区分一下带FIFO的模块内部有AL422B缓冲通常用于没有DCMI接口的MCUH723本身有DCMI直接选不带FIFO的OV2640模块走DVP接口就行。接线方面DCMI需要用到以下信号D0-D78位并行数据线PCLK像素时钟由OV2640输出HSYNC行同步信号VSYNC帧同步信号XCLK主时钟输入由MCU提供通常用MCO输出或定时器产生OV2640的SCCB接口兼容I2C用来配置寄存器用I2C1或I2C2连接。XCLK我用了12MHz到24MHz之间的值实测24MHz没问题OV2640内部有PLL可以倍频。以太网PHY我选的是LAN8720ARMII接口和H723内置的MAC配合。RMII只需要50MHz参考时钟由开发板上的时钟芯片或MCU的MCO输出接线比MII少很多。3.2 CubeMX时钟配置与引脚分配H723的最高主频是550MHz但前提是正确配置电源电压档位和时钟树。在CubeMX里把HSE设为25MHz晶振或根据开发板实际晶振设置PLL1选择倍频到550MHz系统时钟选择PLL1CLK。要注意的是H7系列的VOS电压档位必须设置为VOS0或VOS1否则主频被限制在400MHz以下跑不到550MHz这个在CubeMX的Power Config里直接选就行。外设方面需要使能的模块有DCMI、DMA、JPEG、ETH、I2C、以及串口用于日志输出。引脚分配根据开发板实际丝印走线来一般开发板的原理图上会标明DCMI和以太网的复用引脚直接照着填就好。关键要确认DMA通道DCMI的数据传输建议使用DMA1或DMA2的专用通道并配置为循环模式或双缓冲模式CubeMX里对应的选项是“Circular”或“Double Buffer”。lwIP的配置在CubeMX里可以勾选中间层选择“LWIP”协议栈版本选2.1.2或更高操作系统接口选CMSIS OS如果用了FreeRTOS或无OS。内存池大小先保持默认后面问题排查部分我会专门讲怎么调。3.3 OV2640寄存器初始化要点OV2640的寄存器配置是整个项目里最繁琐的一步因为它不像普通I2C传感器那样简单几行就能出图像。寄存器配置包括时钟分频、输出格式、分辨率、曝光增益等。我这里只强调几个最关键的点。第一必须指定输出为YUV422格式并关闭摄像头内部的JPEG压缩功能JPEG模式寄存器设置为非JPEG模式。因为方案选了H723硬件编码摄像头只负责输出原始YUV数据。第二分辨率我设置为VGA640x480窗口裁剪由OV2640寄存器配置完成。第三HSYNC和VSYNC的极性要配置正确否则DCMI采集到的图像会整体偏移或变形这个和摄像头模块的电路有关调试时用示波器看一下波形最靠谱。我这里给一个精简的初始化流程复位OV2640 → 配置时钟分频 → 设置输出格式YUV422 → 设置分辨率VGA → 设置窗口裁剪 → 开启输出。网上能搜到很多OV2640寄存器表但不同模块寄存器值略有差异最好先用摄像头模块商家提供的配置表再配合SD卡存储一张测试图验证输出格式是否正确这样能少走很多弯路。4. 核心编码链路的完整实现4.1 DCMI DMA乒乓缓冲的配置方法DCMI外设的本质是把并行摄像头数据按像素时钟送入FIFO再通过DMA搬运到内存。乒乓双缓冲的意义在于DCMI正在填充缓冲A时JPEG编码器可以处理缓冲B中已经填满的帧数据两个操作互不阻塞。CubeMX里配置DCMI为8位数据宽度、外部行同步和帧同步模式、PCLK上升沿采样。DMA配置为内存到内存模式方向是从外设到内存模式选择双缓冲Double Buffer。双缓冲模式下DMA需要设置两个内存地址一个地址是当前正在接收数据的缓冲A另一个是待切换的缓冲B。代码层面启动DMA后在DMA传输完成中断里切换缓冲索引并置位一帧数据可用的标志通知RTOS的编码任务。这里有一个细节DCMI在帧同步VSYNC期间会有一段时间没有数据如果缓冲区状态管理不当很容易出现一帧数据里混入两帧的内容。解决方法是在DMA完成中断里读取当前帧号与上一帧号比对确保只在新的帧同步到来后切换缓冲。4.2 硬件JPEG编码器的关键配置硬件JPEG编码器使用前需要配置以下几项输入图像格式、图像尺寸、量化表、输出缓冲区地址、编码模式。H723的JPEG外设编码输入支持YUV422、YUV444等格式关键是输入数据的内存布局必须和摄像头输出一致。OV2640输出的YUV422是YUYV字节序也就是Y、U、Y、V交替排列配置JPEG外设时要把输入格式设置为YUV422并确认识别这个字节序。量化表决定编码质量和输出体积。H723的JPEG外设自带默认量化表也可以手动配置自定义表。我实测默认量化表在VGA分辨率下画质可以接受JPEG文件大小大约在30KB到60KB之间按内容复杂度波动。如果追求更小体积可以调高量化系数但画质会明显下降。输出缓冲区的大小分配需要特别小心因为JPEG编码器输出长度不固定。一个保守的估计公式是输出最大字节数 输入图像宽度 × 输入图像高度 × 2 4096字节。比如640x480的YUV422输入最大输出约614KB对于H723的SRAM来说太大了。但实际编码VGA JPEG很少超过100KB我按100KB为上限预留了128KB的缓冲区实测没有溢出过。如果你跑更高分辨率需要仔细估算并预留余量宁可多留不要少留。启动编码的代码流程大致是先调用HAL_JPEG_ConfigEncoding设置图像尺寸、输入格式、输出数据地址再启动JPEG编码中断等待编码完成。编码完成后从JPEG状态寄存器或回调里获取实际输出的字节数这个值是这次JPEG编码的真实长度后续发送时就按这个长度发送。4.3 用HTTP协议实现MJPEG流推送JPEG数据拿到后剩下的问题是怎么把它送到浏览器并实时播放。最通用的方案是使用HTTP的多部分响应即multipart/x-mixed-replace。响应格式如下HTTP/1.1 200 OK Content-Type: multipart/x-mixed-replace; boundaryframe --frame Content-Type: image/jpeg Content-Length: 43786 [JPEG二进制数据] --frame Content-Type: image/jpeg Content-Length: 45201 [下一帧JPEG二进制数据] ...浏览器收到这种响应后会持续刷新显示最新一帧图像。上面这个格式用浏览器直接打开视频流URL就能播放不需要Flash也不需要WebRTC。如果你要嵌入网页直接在HTML里写一个img srchttp://设备IP/stream即可。lwIP里实现这个推流比较直接的做法是写一个自定义的HTTP回调处理函数判断请求路径是/stream时设置上面的HTTP响应头然后每次获取到一帧JPEG数据就通过TCP连接按格式发送。注意TCP发送要带超时保护否则网络慢时发送缓冲区满进程会卡死。如果不想实现完整的HTTP协议解析可以用lwIP的裸机APIraw API自己管理连接只处理HTTP GET请求行其他路径返回404。这个实现量很小整个HTTP handler大约100行代码就能搞定。4.4 实测帧率与实时性调优整个链路调通后我在VGA分辨率下实测了帧率。OV2640输出YUV422DCMI采集H723硬件JPEG编码以太网MJPEG推流在浏览器播放实测帧率在20fps到30fps之间波动图像越复杂帧率越低因为JPEG编码时间变长了。帧率瓶颈主要在三个方面DCMI采集速度由摄像头输出帧率决定、JPEG编码延迟、网络发送延迟。摄像头这块OV2640在VGA模式下最高可以输出30fps到60fps但实际配置时要根据XCLK频率和内部PLL计算不是所有配置都能稳定60fps。JPEG编码VGA一帧的时间大概在5ms到15ms取决于图像内容。网络发送100M以太网下一帧50KB的数据约4ms30fps完全够用。如果帧率达不到预期优先检查这几个点DMA是否工作在双缓冲模式、JPEG编码是否被串行执行、lwIP的TCP发送窗口是否太小、是否有不必要的图形拷贝。我遇到的最大坑是lwIP默认的PBUF_POOL_SIZE太小发送大帧时缓冲区不足导致丢包严重帧率直接掉到个位数。把PBUF_POOL_SIZE调大并增加TCP_SND_BUF大小后帧率恢复稳定。5. 常见问题与排查技巧实录5.1 画面全黑或全蓝多半是同步信号的问题画面全黑先看DCMI有没有收到VSYNC。用调试器读DCMI的状态寄存器或中断标志如果帧同步中断始终不来大概率是XCLK没配置正确、摄像头没输出时钟或者OV2640的SCCB初始化失败了。如果VSYNC有、图像全蓝那是HSYNC或PCLK的极性配置反了。DCMI有寄存器位可以配置PCLK采样沿和同步信号极性试着把PCLK由上升沿改成下降沿采样或者把HSYNC/VSYNC极性取反。这个在调试时基本靠试我见过5个模块里3个极性不同的情况完全不能用“默认值能用”来想当然。5.2 图像颜色不对从YUV字节序和JPEG格式找原因颜色偏绿偏紫通常是YUV字节序和JPEG输入格式不匹配。OV2640的YUV422输出通常是YUYV顺序也就是相邻两个字节为一个像素的Y和U或Y和V分量。如果JPEG外设配置为YUV422格式但字节序理解反了颜色肯定不对。检查HAL库的JPEG配置结构体里是否有字节序相关的选项或者直接改寄存器值把输入顺序从YUYV改成UYVY试试。另一个容易忽略的点是OV2640的YUV输出范围是有限范围16-235还是全范围0-255如果范围配置不对图像对比度会看起来很奇怪像蒙了一层雾。这个在OV2640寄存器里可以设置通常默认是有限范围但H723 JPEG编码器期望的输入是全范围还是有限范围需要确认。5.3 网络断流优先查PBUF池和TCP超时正常播放几秒后画面停止刷新多半是TCP连接断开了。我用串口打印lwIP的错误统计发现大部分是TCP_WRITE超时或者PBUF分配失败。把cubeMX生成的lwIP配置里PBUF_POOL_SIZE从默认值改大同时TCP_SND_BUF和TCP_WND也相应调大问题基本解决。另外MJPEG流的HTTP响应里如果忘了设置Connection: close有些浏览器会尝试keep-alive复用连接导致播放不稳定。建议HTTP响应头里直接关闭keep-alive每帧数据都从同一个TCP连接里连续发送连接保持到用户断开为止。不要每帧重新建立TCP连接那样开销太大帧率会掉得很厉害。还有一个重要的技巧发送JPEG数据时一定不要一次性调用TCP_WRITE发送几十KB的大缓冲区如果lwIP内部没开启零拷贝它会把数据拷贝到PBUF里再发送大块拷贝容易卡住。最好是分片发送每片2KB左右用TCP_WRITE_FLAG_MORE标记还有后续数据最后一帧发送完成时不加标志这样TCP栈的负担小很多。5.4 帧率上不去先看CPU占用和DMA是否在跑想确认瓶颈在哪最快的方法是用调试器看CPU的空闲时间占比。如果CPU占用率长期大于80%大概率有数据拷贝或软件处理在拖后腿比如JPEG输出缓冲区到发送缓冲区之间的大块memcpy。用零拷贝管理方式直接让lwIP指向JPEG输出缓冲区能省掉一次拷贝。如果CPU占用率不高但帧率还是低检查DCMI的DMA是否真的配置在双缓冲模式还是无意中用了普通模式导致采集和编码串行。另外一个经常被忽略的问题是GPIO速度等级DCMI数据线的GPIO如果配置成Low speed在高像素时钟下会采样出错导致图像数据大量丢位看起来就像花屏。把DCMI数据线和时钟线的GPIO速度全部设置为Very High这个问题通常会消失。5.5 关于调试工具的建议调试这套系统串口日志是必须的建议在采集完成、编码完成、发送完成三个节点各打一条日志记录时间戳这样能直观看到每个环节耗时多少。其次是抓包工具如果设备有以太网直接用Wireshark抓HTTP数据流能看到MJPEG响应是否连续、TCP重传是否严重。我自己在调的时候还借助了一个小技巧把当前帧的JPEG编码耗时和帧大小打印到串口同时在网页上肉眼观察画面卡顿程度两者对照能很快定位是编码瓶颈还是网络瓶颈。这个经验很好用。比如当你发现串口日志显示编码耗时5ms、网络发送耗时20ms但画面还是卡那多半是发送逻辑里有阻塞比如TCP的sndbuf满时会一直等待。遇到这种情况把发送改为非阻塞模式并丢弃最旧一帧反而能让画面更流畅因为实时视频流最重要的是新鲜度不是完整性。6. 后续扩展方向这套基础链路跑通之后可以做的扩展方向不少。如果想把视频质量提上去可以升级摄像头为OV5640支持500万像素和更高帧率但DCMI数据量会变大需要确认H723的DMA吞吐是否足够。如果想把延迟进一步降低可以在lwIP上直接跑RTSP协议实现更标准的流媒体传输但工作量大不少。如果想把视频存储下来可以利用SDMMC接口把JPEG帧直接写SD卡就能做简易的监控摄像头。我个人在实际操作中最深的体会是MCU做视频流的核心不是主频和算力而是数据通路的设计。谁能在“采集、压缩、传输”三条流水线之间把DMA和中断安排得明明白白谁就能在单片机上跑出接近Linux开发板的视频流效果。H723这套方案作为低成本、低功耗、实时性要求高的嵌入式视觉预览和工业监控方案潜力还是很大的。
返回列表