
1. RV1106这块板子为什么会成为视频捕获的首选做嵌入式视频开发这些年我经手过不少方案海思的Hi3516系列、君正的T系列、全志的V系列各有各的脾气。去年因为一个户外低功耗视频终端的项目我拿到了瑞芯微的RV1106老实说一开始没抱太大期望——这颗料看起来就是一颗IPC专用SoC规格表不花哨参数也不算激进。但真正把H264视频流捕获和编码的完整流程跑通之后我发现自己对这颗芯片的判断有些保守了。先说说RV1106在视频链路里到底扮演什么角色。它是一颗专门面向视觉处理的应用处理器内部集成了自研的ISP和H264/H265硬件编码器也带了一个不错的NPU。你如果只把它当成一颗能跑Linux的MCU来看那就浪费了它最核心的价值——它真正擅长的事情是接收摄像头sensor的原始图像数据经过ISP处理再通过硬件编码器压缩成H264码流。整个过程中CPU几乎不需要参与像素级计算这让CPU可以腾出大量算力去做业务逻辑比如跑AI算法、处理网络协议栈、管理存储。我实测下来RV1106的H264编码能力在1080p60fps级别跑得很稳如果只要求1080p30fpsCPU占用率可以压得很低。对于大多数视频流捕获场景比如网络摄像头、工业视觉终端、车载记录仪这个性能储备是足够的。而且这颗芯片的封装和功耗控制做得不错在低功耗场景下比很多同类方案有优势单板甚至可以靠PoE供电直接跑完整套视频链路。不过说实话RV1106的软件生态和它的硬件能力有一定差距。官方SDK的文档比较开发板风格很多细节要靠自己去读源码和寄存器手册。这篇文章我就把从零开始搭建RV1106视频流捕获与H264编码链路的完整流程写出来附带我踩过的坑和验证过的参数配置给正在调研这块板子的人一个参考。适合读这篇文章的人正在评估RV1106作为视频产品主控的硬件工程师或嵌入式工程师已经拿到开发板但不知道从哪里开始跑通视频链路的入门者想了解H264硬件编码器实际工程配置的开发者不适合读这篇文章的人没有Linux和C语言基础想通过图形化界面点一点就完成开发的朋友。RV1106的整个开发流程还是命令行为主SDK也是Linux体系下的工具链。2. 环境搭建与开发板初始化最容易卡住的三道门槛2.1 SDK选型用哪个版本决定你后面少踩多少坑RV1106的SDK在瑞芯微的官方仓库里通过repo方式管理名字叫luckfox-pico因为瑞芯微官方有对应的Luckfox Pico开发板。我第一次拉代码的时候就遇到了这里的一个坑repo工具需要Python 2.7环境但当前系统默认的Python已经是3.x了直接执行repo init会报语法错误。我的解决方法是直接在用户目录下建一个Python 2.7的虚拟环境专门用来执行repo工具。这个做法在老版本SDK上很管用新版的SDK2024年之后的master分支已经解决了Python 3的兼容问题所以如果你现在拉代码建议直接拉最新的release分支不要用老版本否则后面编译工具链也会有一堆兼容性麻烦。SDK拉下来之后整个目录结构大概是这样sdk/ ├── kernel/ # Linux内核源码5.10版本 ├── u-boot/ # 引导加载程序 ├── buildroot/ # 根文件系统构建系统 ├── app/ # 板级应用示例 ├── tools/ # 烧录工具、交叉编译工具链 ├── output/ # 编译产物这里有个经验之谈核心开发阶段尽量不要改动kernel和u-boot的默认配置。RV1106的SDK默认配置是经过官方验证的能跑通大多数基础功能。贸然去裁剪内核或者调整DTS设备树很容易引入视频帧中断丢失、编码器时钟频率异常这类疑难杂症。2.2 交叉编译环境的三个隐藏坑RV1106用的是arm-rockchip830-linux-uclibcgnueabihf这套工具链跟常见的arm-linux-gnueabihf有一些细微差别。这套工具链是Buildroot定制的所以它的glibc/uclibc版本和系统库路径跟Ubuntu自带的交叉编译工具链不一样。如果你图省事直接用Ubuntu的gcc-arm-linux-gnueabihf编译应用程序可能会出现编译通过、板子上跑不起来或者运行报错说缺libgcc库的情况。我的建议是SDK里自带的tools/linux/toolchain目录下已经准备好了工具链把这个路径加到PATH里不要自己去装新的交叉编译器。同时要注意工具链的名称带uclibc意味着板子上的根文件系统是精简过的很多动态库依赖和宿主机不同编译时要加-static或者显式链接你需要的库不然上板执行会报symbol找不到。还有一个小细节SDK的Buildroot默认把stripped过的二进制打包到根文件系统里所以如果你要gdb调试记得在编译应用时关掉-O2优化用-Og并且不要strip。这样调试体验会好很多。我一开始图省事没关优化结果在gdb里看变量全是优化后的假值排查了一个下午。2.3 烧录与启动从SD卡到SPI Nor的差异RV1106支持从SD卡、SPI Nor Flash、EMMC三种介质启动。开发调试阶段我强烈建议用SD卡启动因为替换内核和根文件系统都很方便只要把SD卡拔下来插到电脑上重新分区拷贝就行。而SPI Nor启动每次烧录都要用瑞芯微的RKDevTool工具开发模式下频繁烧录很容易把Flash的寿命和扇区擦写磨损搞掉一截。SD卡启动的准备步骤说起来不复杂但第一次操作容易搞混分区先用SDK里的打包脚本生成update.img再解包出boot.img和rootfs.img把SD卡分成两个分区第一个分区放boot.img内核dtb第二个分区放rootfs.img把分区表标记为可启动插卡上电启动之后确认系统起来了先用cat /proc/cpuinfo和cat /proc/meminfo看一眼系统状态。接下来就是本文的核心——视频流捕获与编码。3. 视频捕获链路拆解从sensor到内存的完整旅程3.1 sensor选型与DTS配置的对应关系RV1106的Camera接口支持DVP和MIPI CSI两种输入方式。我用的是SC3336这颗sensor它是一颗200万像素、1/2.7英寸的CMOS图像传感器输出RAW10格式通过MIPI CSI-2接口连接。SC3336在RV1106 SDK里已经内置了驱动所以DTS配置相对简单只需要在board dts里把对应的sensor节点使能并配置I2C地址和MIPI通道数。DTS里有一个关键参数是sensor的link-freq和pixel-rate这两个值必须和sensor的驱动代码一致。如果配错了sensor初始化会失败或者采集出来的图像是花屏、绿屏。我的经验是直接用SDK自带的面板配置文件rk1106-bb.dts里的ov5647节点改参数不要从零自己写节点。SC3336和OV5647的寄存器配置逻辑不同但DTS框架可以复用只需要改I2C地址和reset引脚的GPIO编号。i2c2 { status okay; clock-frequency 400000; sc3336: sc333630 { compatible smartsens,sc3336; reg 0x30; pinctrl-names default; pinctrl-0 csi_pwdn; reset-gpios gpio2 RK_PB5 GPIO_ACTIVE_LOW; pwdn-gpios gpio2 RK_PB4 GPIO_ACTIVE_HIGH; rockchip,camera-module-index 1; rockchip,camera-module-facing back; rockchip,camera-module-name default; rockchip,camera-module-lens-name default; port { sc3336_out: endpoint { remote-endpoint csi2dphy1_input; >MppEncCfg cfg; mpp_enc_cfg_init(cfg); mpp_enc_cfg_set_s32(cfg, prep:width, width); mpp_enc_cfg_set_s32(cfg, prep:height, height); mpp_enc_cfg_set_s32(cfg, prep:hor_stride, width); mpp_enc_cfg_set_s32(cfg, prep:ver_stride, height); mpp_enc_cfg_set_s32(cfg, prep:format, MPP_FMT_NV12); mpp_enc_cfg_set_s32(cfg, rc:mode, MPP_ENC_RC_MODE_CBR); // 或VBR mpp_enc_cfg_set_s32(cfg, rc:bps, width * height * 4); // 目标码率 mpp_enc_cfg_set_s32(cfg, rc:bps_max, width * height * 8); mpp_enc_cfg_set_s32(cfg, rc:bps_min, width * height * 2); mpp_enc_cfg_set_s32(cfg, codec:type, MPP_VIDEO_CodingAVC); // H264这里最关键的是hor_stride和ver_stride。它们不是简单的width和height而是sensor输出时每行像素的align值。RV1106要求stride按16字节对齐有些平台要求64字节如果你的width是19201920本身已经是16的倍数但是如果分辨率是1080×1920这种竖屏产品你用1080作为stride而不做对齐编码器输出的码流就会花屏。正确做法是int aligned_w (width 15) / 16 * 16; int aligned_h (height 1) / 2 * 2;第一个stride用于NV12的Y平面ver_stride其实对应的是uv缓冲区的行数对齐。记得把这两个值传给编码器而不是直接传分辨率。4.2 码率控制模式CBR还是VBR别拍脑袋码率控制是H264编码里很影响产品体验的一个模块。RV1106的硬件编码器支持CBR固定码率和VBR可变码率两种模式还支持CQP固定量化参数模式。CBR模式目标码率固定适合网络传输带宽受限的场景比如RTSP推流到公网码率波动小但画面复杂度高时会出现质量下降。VBR模式允许码率在设定范围内波动复杂场景可以临时拉高码率保证质量适合本地录存储文件不会因为复杂画面产生明显失真。CQP模式固定QP值不关心码率大小适合项目调试阶段观察原始编码质量不适合产品发布。我的一个项目做的是双码流主码流用CBR 4Mbps推RTSP子码流用VBR 512Kbps做本地录像。主码流保证网络流畅子码流保证本地文件不至于在暗光场景下糊成一团。这里要提醒的是CBR模式下如果你设置的码率远低于场景所需编码器会强制丢细节画面会出现马赛克块状物这是正常现象不要误以为编码器硬件坏了。如果需要诊断码率设置是否合理可以直接统计每帧的编码字节数与目标对比。// 编码帧回调里统计 if (packet-len target_frame_bytes * 1.5) { printf(warning: frame too large, idr%d size%d\n, packet-is_intra, packet-len); }还有一个细节GOPGroup of Pictures长度。如果你做的是低延时视频流传输I帧间隔设置过大会导致关键帧等待时间过长设置过小虽然拉流快但码率会被拉高。我的默认配置是GOP2×fps也就是2秒一个I帧大部分场景够用了。如果做的是监控存储类产品GOP可以放到4秒甚至更长因为存储场景对关键帧频繁程度不敏感码率更宝贵。4.3 编码循环把V4L2帧送入MPPI的零拷贝思路把V4L2采集到的NV12帧送进编码器这里有一个零拷贝的优化空间值得注意。V4L2采集到的是内核空间的DMA bufferMPPI编码器需要的输入也是物理内存地址。如果你把每个采集帧都memcpy到用户空间一块普通内存里再交给编码器那么每一帧会多出两次拷贝操作——一次从内核buffer拷贝到用户空间一次从用户空间拷贝到编码器输入。1080p30fps下每帧3.1MB的数据量两次拷贝对CPU的带宽占用是3.1MB×30×2186MB/s这个数字在一些低主频的CPU上会让CPU占用率直接飙到40%以上。RV1106的V4L2驱动支持通过DMA_BUF导出缓冲区VIDIOC_EXPBUFMPPI可以通过传递fd的方式直接引用这块物理内存从而实现零拷贝。整个流程变成V4L2申请缓冲区时用VIDIOC_EXPBUF导出对应的dma-buf fd将fd告诉MPPI的MppBuffer作为编码器输入V4L2把采集出的帧放入queueMPPI直接从对应fd读取数据编码这样CPU几乎不参与图像数据的搬运CPU占用率可以控制在5%以内。早期我图省事直接mmapV4L2拷贝1080p30fps下CPU占用率在40%左右后来改成dma-buf零拷贝降到3%~5%这个优化对整机功耗和稳定性影响非常大。实现零拷贝的步骤有几个细节VIDIOC_EXPBUF需要内核驱动支持RV1106的V4L2驱动默认支持MPP的buffer需要通过MPP_BUFFER_DMA类型分配并且用mpp_buffer_import传入外部fd开发过程中如果遇到buffer busy或编码帧卡住的情况大概率是buffer生命周期管理问题需要确保V4L2的QBUF和MPPI的编码入队操作严格顺序化5. 码流处理与推流本地存储和RTSP消息流方案实战对比5.1 H264码流的正确打包别把裸流直接塞进文件里编码器输出的是一段一段的H264原始码流Annex B格式带有起始码(00 00 00 01)和多个NAL单元。如果直接把这个裸流按顺序写入文件虽然可以用ffplay直接播放ffplay能自动解析裸流但文件没有时间戳索引、没有封装无法做定位和切片。正常做法是把H264裸流封装进MP4或FLV容器。RV1106 SDK的app目录下有一个mpp_enc_test例子它输出的就是裸流文件。要存储成MP4有两种方案用FFmpeg库在应用层封装用SDK自带的Muxer模块瑞芯微有封装MP4的库FFmpeg方式更通用功能完整但需要裁剪库体积、处理可能的license问题。我推荐用SDK自带的Muxer至少对RV1106平台优化过。下面的代码是MPP Muxer模块封装MP4的关键流程MppMuxer* muxer NULL; MppMuxerCreate(muxer); MppMuxerConfig config { .type MPP_MUXER_TYPE_MP4, .filename (const char*)out_path, }; mpp_muxer_config(muxer, config); // 每次编码返回一个packet MPP_RET ret mpp_muxer_write(muxer, packet);这个模块内部会解析H264的SPS/PPS和I帧信息自动生成MP4的moov box。唯一需要注意的是MP4封装要求知道总时长或者使用fmp4模式如果做的是流式录制录制时间不定长建议用fragmented MP4fMP4这样即使录制中断之前的视频片段也完整可播放。5.2 自己写一个轻量RTSP推流什么时候值得做项目需要把实时视频流送到局域网客户端查看最常见的方案就是RTSP推流。市面上的选项无非是在RV1106上跑一个Live555在RV1106上跑一个GStreamer的RTSP服务在RV1106上用一个现成的RTSP服务器二进制但这些方案都比较重。Live555体积不小GStreamer更是重量级RV1106的Flash资源和内存资源并不宽裕。而且这些方案引入的依赖多了维护起来也是负担。所以如果产品形态比较简单没有多路并发需求自己实现一个极简RTSP服务器模块是完全可行的。RTSP协议本身不复杂核心是RTSP信令HTTP风格 RTP承载H264码流。H264在RTP中打包要符合RFC3984规范I帧要拆分成FU-A分片P帧如果小于MTU通常1400字节则直接单包发送。编码器输出的H264帧很大一个1080p的I帧可能达到100KB必须分片。自己实现时优先级最高的是一个H264 NAL拆包器。从MPP拿到的packet是一整帧的码流内部可能包含多个NAL如SPS/PPS/SEI/SLICE你需要遍历NAL根据类型做分类NAL type7SPS和type8PPS通过RTSP的sdp描述sprop-parameter-sets发送给客户端NAL type5IDR和type1非IDR slice通过RTP打包发送一个简化版的NAL切分逻辑可以这样写static void clip_h264_nal(const void* data, size_t len) { const uint8_t* p data; size_t offset 0; while (offset len) { // 找起始码 if (p[offset] 0 p[offset1] 0 p[offset2] 1) { // 前一个NAL结束 } } }这里有一个很多人忽略的细节MPP输出的H264 packet有时是分片的一个packet只包含部分NAL单元有时是完整帧。调试RTSP丢包、花屏问题时先确认对端拿到的码流是正确的不一定要怀疑网络问题。5.3 本地RTSP服务器测试工具的选择在没有自研RTSP之前我评估过网上几个能在RV1106上跑的现成工具。实测下来有一个轻量方案值得推荐mediamtx原rtsp-simple-server它是一个Go编译的单一二进制交叉编译到ARM Linux非常容易运行内存占用很低支持RTSP推拉流还支持WebRTC转推。用它可以快速把RV1106编码出的H264流推起来方便在局域网内用VLC等工具预览验证。使用方法很简单在RV1106上以rtsp地址做推流端把编码器输出的H264裸流通过FFmpeg或自研程序以RTSP方式推到mediamtx监听的端口局域网内的客户端用VLC打开rtsp://192.168.x.x:8554/stream直接预览我自己写了一个小工具从MPP拿到的packet通过RTSP库推流到mediamtx整体延迟大约300ms以内无线环境稳定性不错。在项目早期这个方案能帮我快速验证编码链路和画质等业务成熟后再把真正产品化的播放逻辑放进去。下面是我实测过的几种工具搭配工具体积内存占用交叉编译难度适用场景mediamtx约50MB约20MB低Go交叉编译简单开发验证、原型测试Live555约200MB含依赖约40MB中需要稳定成熟的RTSP库时GStreamer rtspsrc约300MB约80MB高需要完整的媒体处理管线自研简易RTSP约50KB约1MB低产品落地控制体积和依赖6. 性能优化与实际项目踩坑记录6.1 CPU占用率优化从40%到5%的三个步骤前文提到零拷贝把CPU占用率大幅降下来这是第一大步。但完整的优化链路不止这一处。我整理三个实际操作中最有效的优化点第一关闭V4L2采集线程的CPU调频策略优先绑定到固定CPU核心。RV1106有双核A7默认内核调度会在两个核之间来回迁移线程迁移带来的cache失效对视频缓冲区的操作很不友好。我在采集线程里设置了sched_setaffinity把采集线程绑定在CPU0编码线程绑定在CPU1中断响应也做了对应调整。这个改动之后画面的丢帧率明显降低而且CPU调度延迟不再导致偶发的卡一顿。第二用DPDK思路优化内存分配预分配、复用避免在热路径上频繁malloc/free。V4L2缓冲区本身是预分配的编码器的packet buffer也需要预分配。MPP的mpp_buffer_get接口支持从buffer group里申请申请后可复用不释放。我在初始化阶段一次性申请了5个编码输入buffer和5个packet buffer之后整个采集编码循环里不再调用任何malloc。这样做除了减少开销更重要的是避免长时间运行后内存碎片化对7×24小时连续运行的设备来说内存碎片化是隐性杀手。第三降低编码器的延迟而不是盲目追求码率。视频系统的端到端延时由三部分组成sensor曝光时间ISP处理时间编码器延迟网络传输延迟。编码器本身存在帧级延迟frame delay通过MPP可以设置是否开启低延迟模式low delay P帧虽然会在一定程度上降低压缩率但对交互型视频应用比如无人机图传、远程遥控来说延迟降低的收益远大于码率增加的成本。RV1106的编码器低延迟模式我实测过可以降低约一个帧周期33ms代价是码率上升约15%。具体取舍看业务场景。6.2 常见问题排查画面花屏、卡顿、RTSP无法拉流的定位思路这里记录几个我在RV1106上真实遇到、排查了很久的问题希望后来者少走弯路问题A编码器输出的码流播放时画面间歇性花屏且每次花屏都伴随I帧排查过程一开始怀疑编码器参数配置错误反复调整比特率、GOP长度无效。后来把编码器的原始码流保存到本地文件用ffprobe逐帧分析发现花屏时间点对应的NCU单元数量异常进一步查发现V4L2采集到的那一帧本身就不是完整帧——是因为ISP在输出时遇到了帧同步问题导致缓冲区里写入了一半新帧一半旧帧编码器并不知道照常编码就产生了一个完整但不一致的错误帧。修复方案在V4L2采集侧增加帧同步锁保证每次读出的缓冲区是完整一帧同时在应用层对采集帧数做计数如果两次V4L2_DQBUF间隔异常明显大于1/fps丢弃导致的问题帧不让它进入编码器。加上之后花屏问题基本不再出现。问题B长时间运行后编码器输入队列阻塞画面卡在最后一帧排查过程跑720p30fps连续运行48小时后画面突然冻结。看内核日志发现编码器的中断频率异常且进程状态是D状态不可中断睡眠跟踪到是编码器在等一个buffer释放而这个buffer被应用层拿着没有归还。原因是应用代码里有个分支当网络断连时推流模块抛异常导致编码线程的写buffer流程中断没有调用mpp_buffer_put。修复方案在编码线程的外层while循环里加try-catch逻辑确保无论网络如何编码buffer都会归还到MPP缓冲池。同时给编码线程加上喂狗机制如果连续10秒没有成功编码出一帧自动复位编码器防止系统在跑飞后彻底卡死。问题C通过mediamtx推流后VLC首帧黑屏过几秒才出画面排查过程VLC播放RTSP时需要等待SPS/PPS和第一个I帧才能开始解码。如果RTSP服务器在推流时把SPS/PPS信息只放在RTSP DESCRIBE响应里而客户端没有正确解析就会一直等。此外如果视频源不做循环发送SPS/PPS新连接的客户端只有等下一个I帧到来才能看到画面。修复方案在RTP传输中周期性插入SPS/PPS NAL单元比如每2秒一次或者在每个IDR帧前面强制附带SPS/PPS。这个操作在自研RTSP里加上不难但对于使用现成库的朋友标准做法是设置sprop-parameter-sets参数并要求推流程序定期发送。6.3 帧率统计与丢帧检测让设备真正可运维的细节视频设备的稳定性不能靠感觉要有数据支撑。我在RV1106的主循环里加了三组统计指标V4L2采集帧率实际每秒从sensor拿到的帧数反映相机链路健康度编码器帧率每秒实际编码成功的帧数反映编码链路健康度丢帧数采集到但未编码成功的帧数可以统计v4l2 buffer溢出次数这些指标通过一个简单的共享内存结构体暴露给外部管理接口我在Linux里写了一个watchdog脚本每60秒读取一次指标如果发现连续多次编码帧率为0自动重启应用并记录错误日志。有了这套机制即使设备在无人值守的环境运行也能快速通过日志远程定位问题。统计代码很简单核心就是两个原子计数器static atomic_t v4l2_frame_count; static atomic_t enc_frame_count; void v4l2_capture_thread(void) { while (1) { // dequeue frame atomic_inc(v4l2_frame_count); } } void encoder_thread(void) { while (1) { // encode packet atomic_inc(enc_frame_count); } }这些计数器的精度不需要太高够判断链路是否存活即可。7. 一些个人建议开发RV1106视频链路时的整体思路最后分享几点实际的开发经验。第一把视频链路拆成三个独立模块来开发采集、编码、推流/存储。三个模块之间用清晰的数据灌接口环形队列或者MPP buffer直接传递衔接。这样做的好处是调试时可以单独替换任何一个模块不影响其他测试。例如编码有问题可以直接用SDK自带的mpp_enc_test喂一个静态图并不需要先把sensor配好。第二在前期就用真实的sensor和镜头跑通完整链路不要只用测试图调试。很多画质问题暗角、偏色、噪点都是sensor和镜头模组引入的测试图检测不出来这些。第三不要忽视电源设计。RV1106在进行硬件编码时瞬时功耗会增加约0.5W如果供电设计不够强壮会出现sensor图像闪烁、编码器偶发crash。我早期在一套面包板上跑的时候经常莫名其妙掉帧后来确认是3.3V供电被拉垮所致换用DC-DC模块独立供电后稳定了很多。第四把日志系统当成产品一部分来设计。RV1106的编码链路涉及sensor驱动、ISP、编码器驱动、设备树、MPP库、应用层任何一个环节出问题日志分离不清就无法定位。建议给日志加上模块前缀和级别过滤线上设备只打印error开发设备打印debug。我习惯在应用层启动时打印一个版本字符串包含SDK commit hash线上出问题能立刻知道跑的是哪个版本的代码。RV1106的H264视频流捕获与编码链路整体上手难度中等但硬件编码器一旦跑通性能表现相当扎实在低功耗和性价比上很能打。如果你正在用这颗料做产品原型我建议把前两周的时间主要花在SDK编译和基础环境上一旦环境跑通视频链路反而比预想的更顺利。最终如果你也跑通了同样的链路欢迎交流具体的参数配置和粒子优化方式。视频开发这条路细节永远比你想的多但每解决一个问题对这套系统的理解就更深一层。