ARTICLE DETAIL

资讯详情

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

RV1106嵌入式视觉开发:V4L2到H264硬件编码全链路实战指南

RV1106嵌入式视觉开发:V4L2到H264硬件编码全链路实战指南 RV1106 这颗芯片我前前后后调了小半年从最开始裸机点亮 sensor到后面把 H264 编码链路完整跑通中间踩过的坑确实不少。做嵌入式视觉方案的工程师应该都有感触捕获一路视频流不难难的是让它在持续运行时不花屏、不丢帧、延迟可控、码率稳定尤其在资源有限的小型 IPC 和 AI 盒子上。最近刚好有朋友在问 RV1106 上从 sensor 采集到出流的具体流程干脆把完整链路写出来从 V4L2 抓帧、硬件 ISP 处理到 MPP 编码器配置、H264 码流输出每一步的代码逻辑、参数依据和实操经验都摊开讲。这篇文章适合正在做摄像头方案开发的同行也适合刚拿到 RV1106 开发板、想把视频跑起来的新手你不需要有很深的视频编解码基础按照这条链路一步步走基本能把整个流程吃透。1. RV1106 平台概览与视频链路整体架构1.1 为什么选择 RV1106 做视频编码方案瑞芯微 RV1106 是一颗面向智能视觉应用的 SoC它最大的特点就是把“捕获、处理、编码、AI 分析”这几件事整合到了很低的功耗和很小的封装里。芯片内部集成了一颗双核 ARM 处理器主频可以跑到 1.2GHz 左右这个算力跑轻量级 AI 模型没问题跑系统调度和网络协议栈也够用。最核心的部分是它内置的 VPU也就是视频处理单元支持 H.264 和 H.265 的硬件编码最大分辨率能到 5M 像素级别对于目前主流的 300 万像素、500 万像素摄像头方案来说刚好对口。我之前用过好几款同级别的芯片说实话RV1106 的编码器有个非常明确的优势它的 MPPMedia Process Platform软件栈比较干净接口设计得很直观而且提供了完整的多路编码支持。做 IPC 方案时很常见的一路主码流做录像、一路子码流做预览的需求在 RV1106 上可以直接通过 MPP 开两个编码通道实现不需要额外的软件转码省了不少内存和 CPU 开销。编码后的码流质量在同等码率下主观观感和客观 PSNR 指标都做得不错尤其是暗光场景下只要 ISP 调试到位编码器侧不会额外引入明显的块效应和色彩偏差。选择 RV1106 还有一个现实原因它周边配套的 sensor 支持非常全RK 社区的驱动模型对各种常见 sensor比如 IMX、OV、SC 系列适配得都很快基本稍微改改设备树就能点亮。对于做产品而不是做研究的人来说这套生态能省下大量“从零开始调驱动”的时间。1.2 视频从捕获到 H264 码流的完整链路在 RV1106 上一路视频画面从 sensor 进来到最终输出 H264 码流中间会经过这么几个环节sensor 采集 RAW 数据通过 MIPI CSI-2 接口送入 SoC。SoC 内部的 ISP 模块对 RAW 数据进行处理完成去噪、坏点校正、自动曝光、自动白平衡、色彩校正等操作输出 YUV 图像数据。YUV 数据经过 V4L2 驱动框架进入用户空间以 buffer 的方式交给应用层。应用层将 YUV buffer 送入 MPP 编码器由硬件 VPU 完成 H264 编码。编码器输出 H.264 Annex-B 格式的码流应用层负责按帧取出加上时间戳后封装成 RTSP/FLV/MP4 等格式输出。这条链路看起来简单但每一步都有容易出问题的地方。最典型的坑在第三步和第四步之间V4L2 输出的 buffer 格式、对齐方式和 MPP 编码器要求的输入格式不一致如果没做好格式转换或者 stride 对齐编码出来的画面就会出现绿边、花屏甚至编码器直接报错。我在 3.2 节和 4.2 节会专门把这两个环节的参数对齐讲清楚。另外需要说明一点RV1106 的 ISP 通路是可以通过 V4L2 子设备接口来配置的sensor 本身也是一个 V4L2 subdev整个链路是标准的 media controller 框架。所以如果你之前调过其他平台的 V4L2 驱动上手 RV1106 会非常顺畅核心的 open/request buffer/stream on/stream off 流程和 Linux 主线驱动是兼容的。2. 开发环境搭建与底层驱动准备2.1 SDK 目录结构与交叉编译环境拿到 RV1106 开发板后第一件事就是把瑞芯微官方提供的 SDK 拉下来。SDK 里通常包含了 u-boot、kernel、buildroot、app 层示例代码整个编译流程由脚本统一管理。我比较推荐在 Ubuntu 18.04 或 20.04 的 64 位系统上做交叉编译装好repo工具后repo sync拉取全量代码然后执行./build.sh就能构建完整固件。RV1106 的交叉编译工具链是 arm-rockchip830-linux-uclibcgnueabihf这是一个 32 位 ARM 的工具链因为 RV1106 的内核和用户空间都是 32 位的。编译应用层代码时需要把工具链的 bin 目录加进 PATH比如export PATH/opt/arm-rockchip830-linux-uclibcgnueabihf/bin:$PATH然后写 CMakeLists.txt 时指定交叉编译器set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR arm) set(TOOLCHAIN_PREFIX arm-rockchip830-linux-uclibcgnueabihf-) set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}gcc) set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}g)这里有个经验如果只是想验证编码功能直接用 SDK 里自带的mpi_enc_test示例就能跑通。但我们做产品时还是要自己写应用所以我会建议把 MPP 的头文件和库拷贝到自己的工程里或者直接用find_package的方式链接 SDK 生成的 MPP 动态库。MPP 的动态库在 SDK 编译完成后会生成在buildroot/output/rockchip_rv1106/target/usr/lib/下拷贝出来用就行。2.2 设备树与 sensor 驱动配置设备树是 RV1106 视频链路能否正常工作的基础。你需要确保设备树里同时打开了 ISP、MIPI DPHY 和 sensor 三个节点并且 sensor 的 I2C 地址、reset/电源控制引脚配置正确。一个典型的 sensor 节点配置大概是这样的csi2_dphy0 { status okay; ports { #address-cells 1; #size-cells 0; port0 { reg 0; mipi_dphy0_out: endpoint { remote-endpoint sc530ai_csi2_out; }; }; port1 { reg 1; mipi_dphy0_in: endpoint { remote-endpoint isp0_in; }; }; }; };配置好设备树后重新编译内核并烧录固件。上电后先用media-ctl工具查看整个管线是否注册完整media-ctl -p正常情况下你会看到类似这样的拓扑sc530ai 0-0030→csi2-dphy0→rkisp0这样的链路。如果 sensor 节点没有注册先查 I2C 能否读到 sensor ID如果 ISP 节点没有和 sensor 连接上多半是 remote-endpoint 的 phandle 指错了。设备树这块调试比较费时间我的建议是先跑通 SDK 自带的内核配置确认硬件没问题后再按需裁剪。3. V4L2 视频流捕获实现3.1 V4L2 捕获流程的代码骨架视频流捕获是整条链路的第一步代码层面走的是标准 V4L2 框架。整体流程可以总结为“打开设备、设置格式、申请缓冲区、入队、开始采集、出队处理、入队回填”这几步。先打开/dev/video0然后通过 VIDIOC_S_FMT 设置采集格式。RV1106 的 ISP 输出通常是 NV12 格式这个格式在内存里是先存 Y 平面、再交错存储 UV 分量的布局。宽度和高度要注意对齐通常按 16 像素对齐也就是设置 1920x1080 时实际 stride 可能是 1920 或更高这个值会通过v4l2_format.fmt.pix.bytesperline返回给应用。申请缓冲区时我习惯用V4L2_MEMORY_MMAP模式这也是最常用的方式。流程是先调用VIDIOC_REQBUFS申请 4 个 buffer再逐个通过VIDIOC_QUERYBUF获取 buffer 信息并mmap映射到用户空间。然后调用VIDIOC_QBUF把空闲 buffer 放入驱动队列最后VIDIOC_STREAMON开始采集。采集过程中应用层通过VIDIOC_DQBUF等待一个填充好的 buffer拿到 buffer 后里面的图像数据就可以送去编码。处理完后必须马上调用VIDIOC_QBUF把这个 buffer 还给驱动否则 buffer 耗尽画面就会卡住。这个“出队-处理-入队”的循环是整个视频采集的核心循环每个 IPC 方案都跑在这一段逻辑上所以代码务必写得清晰高效避免在这里加任何不必要的拷贝或者延时。3.2 ISP 通路配置与图像的 stride 对齐很多人第一次调 RV1106 都会在 ISP 这步吃亏。ISP 的输入输出格式、裁剪区域和 stride 不对齐是导致编码器报错或画面异常的最高频原因。我在驱动里已经把 ISP 输出设置成 NV12 了但实际从bytesperline拿到的不一定是 1920而可能是 1920 对齐后的 1920 或 1984 之类的值。这是因为 ISP 硬件内部为了保证带宽效率和 SIMD 处理经常会把行像素数对齐到 16 甚至 32 字节。如果你直接以为 stride 等于 width送给编码器的每个 plane 的 stride 就不对编码出来的图像会出现右侧偏色或亮度偏移。解决办法很简单在 V4L2 的VIDIOC_S_FMT之后马上读取v4l2_format.fmt.pix.bytesperline和sizeimage把这两个值缓存起来后面操作 buffer 时全程使用这两个值而不是自己算。另外ISP 还有一个容易忽略的点sensor 输出的图像比例和编码器要的目标分辨率可能不一致。如果 sensor 是 4:3 的 500 万像素 CCD而编码器要输出 16:9 的 1080p那么应该在 ISP 链路中设置裁剪区域crop先在 ISP 阶段裁掉上下多余的部分再输出 YUV而不是直接把整幅图送到编码器里让它硬压。在 ISP 阶段裁剪的好处是省内存带宽编码器输入的图像也干净后续预览和录像两边都能复用同一路 YUV。3.3 缓冲区管理与帧率控制细节缓冲区数量默认设 4 是很多 SDK 示例的惯例但在实际产品中我会根据内存余量和延迟要求来调整如果内存紧张可以减到 3如果采集端有偶发性的处理耗时最好设到 5~6这样能平滑抖动减少因为 buffer 不足导致的丢帧。帧率控制这块我在项目里很少直接依赖 V4L2 设置 sensor 帧率而是更倾向于让 sensor 固定工作在 30fps然后在应用层按需丢弃或者复制帧。原因是 sensor 的帧率切换经常会触发重新稳帧、曝光收敛等过程如果编码端的路数多主码流 子码流一下子切换帧率可能引发几帧的异常。把 sensor 固定在一个稳定帧率由上层决定丢哪些帧整个管线会稳定很多。也有一点要注意ISP 输出的帧时间是持续递增的但 V4L2 buffer 的timestamp默认可能是 monotonic clock也可能是 realtime clock取决于驱动里的配置。在后续封装 RTSP 或者 MP4 时时间戳的基准必须全程一致。我习惯在VIDIOC_QUERYCAP时检查capabilities或者在驱动初始化时统一设定时间戳类型为V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC这样编码器输出和网络发送都能使用同一个时钟源不会出现音视频不同步的诡异问题。4. H264 编码器配置与码流封装4.1 MPP 编码器初始化与关键参数解析RV1106 的 H264 编码器走的是瑞芯微 MPP 框架。MPP 的接口其实非常简单核心结构体是MppCtx和MppEncCfg。初始化大概分三步创建编码器上下文、配置编码参数、准备输入输出分组。创建上下文的代码片段MppCtx ctx; MppEncCfg cfg; mpp_create(ctx, MPP_ENC_H264); mpp_init(ctx, MPP_CTX_ENC, MPP_ENC_H264); mpp_enc_cfg_init(cfg);配置参数时需要设置分辨率、帧率、码率、GOP 间隔、码率控制模式等关键项。以 1080p30 为例典型配置如下mpp_enc_cfg_set_s32(cfg, prep:width, 1920); mpp_enc_cfg_set_s32(cfg, prep:height, 1080); mpp_enc_cfg_set_s32(cfg, prep:hor_stride, 1920); mpp_enc_cfg_set_s32(cfg, prep:ver_stride, 1088); mpp_enc_cfg_set_s32(cfg, prep:format, MPP_FMT_YUV420SP); mpp_enc_cfg_set_s32(cfg, rc:mode, MPP_ENC_RC_MODE_CBR); mpp_enc_cfg_set_s32(cfg, rc:bps_target, 2 * 1024 * 1024); mpp_enc_cfg_set_s32(cfg, rc:bps_max, 2 * 1024 * 1024 * 11 / 10); mpp_enc_cfg_set_s32(cfg, rc:bps_min, 2 * 1024 * 1024 * 9 / 10); mpp_enc_cfg_set_s32(cfg, rc:fps_in_flex, 0); mpp_enc_cfg_set_s32(cfg, rc:fps_in_num, 30); mpp_enc_cfg_set_s32(cfg, rc:fps_in_denom, 1); mpp_enc_cfg_set_s32(cfg, rc:fps_out_flex, 0); mpp_enc_cfg_set_s32(cfg, rc:fps_out_num, 30); mpp_enc_cfg_set_s32(cfg, rc:fps_out_denom, 1); mpp_enc_cfg_set_s32(cfg, rc:gop, 30); mpp_enc_cfg_set_s32(cfg, rc:gop_mode, MPP_ENC_GOP_MODE_NORMAL_P); mpp_enc_cfg_set_s32(cfg, codec:type, MPP_VIDEO_CodingAVC); mpp_enc_cfg_set_s32(cfg, codec:profile, 100); mpp_enc_cfg_set_s32(cfg, codec:level, 40); mpp_enc_cfg_set_s32(cfg, codec:cabac_en, 1); mpp_enc_cfg_set_s32(cfg, codec:cabac_idc, 0); mpp_enc_cfg_set_s32(cfg, codec:trans_8x8, 1);有几个参数我想重点说明一下。prep:ver_stride设置成 1088 而不是 1080是 H264 编码器内部宏块对齐的要求1080 不能被 16 整除所以对齐到 1088。这个值在 V4L2 那边不一定能看到但 MPP 编码器默认会按这个值去解释输入 buffer。如果你传入的 buffer 本身是按 1080 分配的那么最后一行的填充区域是无效数据编码器会自己忽略不影响画面。rc:mode控制码率模式。CBR 适合实时视频预览和录像码率上下波动小VBR 适合追求画质的场景码率会跟着画面复杂度走但带宽不可控。我在做 IPC 时主要用 CBR配合bps_max和bps_min设置成 target 的 ±10%既保证了码率稳定性又给了编码器一定的调整空间。codec:profile设为 100 是 High Profile支持 8x8 变换trans_8x8设为 1和 CABAC 熵编码压缩率更好。如果是低端播放器兼容性要求比较高的场景可以降到 Baseline Profile66但文件体积会增大不少。4.2 帧数据送入编码器的正确姿势编码器配置好之后剩下就是循环从 V4L2 拿 YUV 帧送给 MPP 编码。MPP 的输入输出都是基于MppBuffer和MppPacket的。送一帧数据前需要先调用mpi-dequeue(ctx, FRAME_GROUP, frame)向编码器请求一个空闲输入 frame然后把这个 frame 的 buffer 指针映射到用户空间将 V4L2 出队的 YUV 数据memcpy进去。这里有一个容易忽略的性能点memcpy 一整张 1080p NV12 图像大概要拷贝 3MB 左右的数据这个开销在 ARM 上至少要 1~2ms。如果每帧都做对 CPU 是可见的负担。更高效的做法是使用mpp_buffer_import或者直接使用零拷贝通道。RV1106 的 MPP 支持直接 import 外部 buffer这样 V4L2 出队的 buffer 可以直接交给编码器不需要 memcpy。不过这个方案对 buffer 的内存类型有要求通常需要物理连续内存所以只能和 V4L2 的V4L2_MEMORY_DMABUF模式配合使用。如果项目对延迟和 CPU 占用率特别敏感建议直接走 DMABUF 零拷贝路线能省下整整一段拷贝时间。把帧数据填进 MppFrame 后通过mpi-encode(ctx, packet)触发编码。因为 VPU 是硬件编码encode调用可以异步返回还是同步返回取决于上下文是否设置了异步模式。SDK 里默认是同步也就是encode返回后packet已经包含了一帧编码后的 H264 数据。如果是异步模式需要轮询或等信号量逻辑会复杂一点但对于普通单路编码来说同步模式完全够用代码也更直观。从packet拿数据时注意有个EOS标志位在视频流结束时packet里可能没有数据只带了一个结束标记这个时候不能直接把packet-data当普通码流发送。4.3 H264 码流格式与时间戳处理MPP 编码器输出的 H264 是标准的 Annex-B 格式也就是每个 NALU 前面带起始码00 00 00 01。对 RTSP 分发来说这个格式可以直接用对 MP4 封装来说需要去掉起始码转成长度前缀格式然后写在 stbl box 里这个过程通常在 muxer 层完成。在编码输出过程中需要关注的 NALU 类型主要是 SPS、PPS、IDR 和普通 P 帧。SPS/PPS 通常在编码器刚启动时只输出一次但不少播放器和录制系统要求在关键帧前重新带一遍参数集防止花屏和无法解码。处理办法有两个一是在每次检测到 IDR 帧时把保存过的 SPS/PPS 拼在 IDR 前面二是通过 MPP 的rc:gop_mode配置成智能 GOP让编码器在 I 帧前自动输出参数集。我实测下来第二种方式在 MPP 上更稳设置gop_mode为MPP_ENC_GOP_MODE_SMART_P并配合 IDR 间隔就能让 H264 码流里每个 I 帧前都带上 SPS/PPS。时间戳处理是另一个重灾区。V4L2 buffer 自带 monotonic 时间戳单位是微秒tv_sec * 1000000 tv_usec。编码器输出的 packet 不带时间戳需要应用层在把帧送入编码器前记录一个对应的时间戳编码完成后把这个时间戳传给 packet。我习惯用一个环形队列把输入帧的序号和时间戳绑定编码输出时按顺序取出来这样即使编码器偶尔乱序返回也能保证时间戳跟着正确的帧走。做 RTSP 推流时RTP 时间戳的单位是 90000Hz需要把微秒时间戳做一次换算uint32_t ts_rtp (uint32_t)(ts_us * 90 / 1000);也就是微秒数乘以 90 再除以 1000。这个换算很容易写错一旦写错播放端画面会严重卡顿、音画不同步而且非常隐蔽。5. 实测问题排查与调优心得5.1 编码帧率上不去掉帧频繁我在把编码器跑起来之后遇到的第一个大问题就是帧率不稳20 分钟的测试里时不时掉帧CPU 占用忽高忽低。排查下来有几个原因按出现频率排列如下V4L2 采集侧 buffer 数量不足导致 ISP 输出侧没有空闲 buffer 可写驱动直接丢帧。memcpy 拷贝整帧数据耗时太长在编码和采集循环里形成了串行瓶颈。应用层日志打印过于频繁串口和文件 IO 拉低了整个进程的吞吐。解决办法分别是把 V4L2 buffer 加到 6 个改用 DMABUF 零拷贝重定向打印到内存缓冲区并按需落盘。优化完之后1080p30 的采集编码链路 CPU 占用率稳定在 5% 上下编码本身几乎不消耗 CPU全部由 VPU 完成。还有一个技巧在VIDIOC_S_FMT设置采集参数时把v4l2_pix_format.field设置成V4L2_FIELD_NONE也就是逐行扫描避免驱动或 ISP 走隔行处理路径否则帧率会直接减半。5.2 画面花屏、绿屏和间歇性偏色花屏问题我遇到过两类。第一类是编码器输入 stride 不对。表现为编码后的画面右侧有一道偏色的竖条或者整体右移了几十像素。排查方法很简单把编码器输出的 H264 存成裸流用 ffplay 播放暂停观察画面右侧像素是否错乱。如果错位基本可以确定是hor_stride和 V4L2 的bytesperline不一致严格按第 3.2 节的方式对齐即可。第二类花屏与 IDR 帧或参数集有关。几个播放器里偶尔出现花屏但 VLC 和 ffplay 都能正常播。这个多发生在 RTSP 取流时播放器恰好从非关键帧开始解码且 SPS/PPS 没有随之重发。解决方式就是把每条流里的 SPS/PPS 缓存下来在 RTSP 的配置消息里带出去或者按 4.3 节的方法让编码器智能输出参数集。间歇性偏色则多半是 ISP 白平衡和色彩矩阵没有收敛好跟编码器没有直接关系。这时候不要死磕编码器回头看看 ISP 的 AE/AWB 参数和 sensor 的增益配置往往是在 sensor 曝光切换的瞬间造成色偏。5.3 编码器输入队列阻塞排查MPP 编码器偶尔会出现dequeue拿不到空闲 frame、导致整个采集循环堵住的情况。常见的诱发因素有两个一是应用层在上送帧的速率低于编码器消费速率导致编码器内部堆积了未完成帧二是输出侧 packet 没有及时retrive导致编码器内部 buffer 池耗尽。处理办法在采集循环里增加一个超时保护例如dequeue等待超过 500ms 就丢弃当前帧而不是无限阻塞下去同时在编码输出侧每次encode返回后立刻retrive拿 packet并把 packet 拷贝到自己的发送队列包处理完成后再put_packet释放。严格遵循“dequeue → 填帧 → encode → retrive → 拷贝 → put”这个节奏编码器基本不会堵。5.4 码率波动偏高和 I 帧大小异常设置 CBR 后如果发现实际码率波动超过 ±20%建议检查bps_max和bps_min的设定是否离 target 太远另外rc:gop如果设得太大I 帧之间的 P 帧会积累漂移导致 I 帧瞬间码率暴涨。我常用的做法是 GOP 设为帧率的 1 到 2 倍比如 30fps 下 GOP 设 30即每 1 秒一个 I 帧。这个设置既能保证 seek 响应较快压缩率也合适。如果发现 I 帧瞬间码率还是偏大可以开启rc:scene_chg_thrd之类的场景切换阈值参数让编码器在画面剧烈变化时不强制拉太高码率。不过这些参数对画面质量影响比较微妙需要结合自己的 sensor 场景反复测试没有一个通用最优值。6. 链路调通后的进一步扩展把 H264 采集编码主链路跑通之后很多项目会在此基础上加子码流、加 AI 分析、加存储录像。RV1106 的 MPP 支持多通道编码可以在同一个MppCtx上配置多个编码器实例也可以直接创建独立的编码器上下文来做多路编码。如果只是做一块小开发板验证方案参考本文这套流程足够如果是走向产品建议在应用层加一个流管理模块把采集、编码、推流、存储解耦成独立线程用队列传递帧引用而不是拷贝这样无论是扩展子码流还是叠加 AI 分析都不会反过头来影响主码流的稳定性。再分享一个很实用的经验在项目初期就把整个链路的对齐参数打印出来包括 V4L2 的bytesperline、sizeimage、MPP 的hor_stride、ver_stride、编码器的 GOP 和码率控制参数。不要嫌日志难看这些字段在后续排查花屏、帧率不稳、推流卡顿的时候能帮你直接锁定问题在哪一层省下大量反复试错的时间。我个人在实际调试中的体会是RV1106 这套捕获-编码流程并不复杂难的是每个环节之间的参数一致性。V4L2 管的是采集格式MPP 管的是编码格式两者看似独立实际通过 stride、缓冲区大小、时间戳紧密耦合。把这条耦合关系理清了把每个 buffer 的生命周期管到位整个 H264 视频流从 sensor 到网络侧的输出就会非常稳定。希望这份流程解析能帮你少走一些弯路如果你在调 RV1106 的过程中遇到其他奇怪的现象欢迎随时交流我踩过的坑也许能给你省下一天甚至一周的时间。
返回列表