
1. 从摄像头到码流RK3568 视频编码链路的整体设计思路RK3568 这颗片子做视频采集编码最省心的路径就是 V4L2 抓帧、MPP 硬编、再把码流推出去。整套链路听起来简单但真到板子上跑坑一个接一个。我前后在 RK3568 上折腾过 OV5695、OV8858 几款 sensor也对比过 RK3566 和 RK3588 的 MPP 差异下面把完整流程和踩过的坑一次讲清楚。先说清楚这套方案解决什么问题。你手头有一块 RK3568 的开发板或者自研板接了一颗 MIPI 摄像头想把实时画面编码成 H.264 或 H.265 码流用于本地存储、网络传输或者接 RTMP 推流。整个链路是V4L2 采集原始帧 → NV12 格式转换 → MPP 硬件编码 → 码流封装输出。这条链路的核心价值在于把 CPU 从繁重的视频编码中解放出来RK3568 的 VPU 能同时扛住多路 1080P 编码CPU 占用率可以压到个位数。为什么选 MPP 而不是软件编码x264 软编在 RK3568 的 A55 核心上跑 1080P30 基本就把 CPU 吃满了而且发热感人。MPP 是瑞芯微提供的硬件编解码中间件直接调用 VPU功耗和占用都低一个数量级。至于 V4L2它是 Linux 下摄像头采集的标准框架RK3568 的 ISP 驱动也是走 V4L2 节点暴露出来的用它是顺理成章的事。适合谁来参考如果你已经能在 RK3568 上跑通系统、会用交叉编译、对 Linux 设备树有点概念那这篇内容可以直接抄作业。如果你连 V4L2 的VIDIOC_QBUF都没见过建议先把 V4L2 驱动框架的基础过一遍再回来。1.1 为什么是 V4L2 MPP 这个组合V4L2 负责和 ISP、sensor 打交道MPP 负责和 VPU 打交道两者之间用 DMA-BUF 或者内存拷贝衔接。这个分工不是随便定的。V4L2 的 buffer 管理机制REQBUFS、QBUF、DQBUF天然适合流式采集而 MPP 的输入也支持外部 buffer 导入两边通过 fd 传递可以做到零拷贝。如果你用 OpenCV 的VideoCapture去抓帧再喂给 MPP中间多了一次格式转换和内存拷贝1080P 下每帧多花 3 到 5 毫秒帧率直接掉一截。另一个原因是 RK3568 的 ISP 输出格式。OV5695 这类 sensor 经过 ISP 处理后V4L2 节点吐出来的通常是 NV12 或 NV16。MPP 编码器对 NV12 的支持最好直接喂进去就行不需要额外的色彩空间转换。如果你拿到的是 YUYV那还得先转 NV12这一步用 RGA 做比 CPU 做快得多。1.2 整条链路的模块划分我把整条链路拆成四个模块每个模块独立调试最后串起来采集模块打开/dev/videoX设置格式为 NV12申请 buffer 并入队循环 DQBUF 拿帧。格式处理模块如果 sensor 输出不是 NV12用 RGA 做一次转换如果分辨率需要缩放也在这一步做。编码模块初始化 MPP 编码器上下文配置 H.264/H.265 参数把 NV12 帧送进去拿回码流包。输出模块把码流写到文件、通过 socket 发出去或者交给 RTMP 库推流。这样拆的好处是采集出问题就查 V4L2编码出问题就查 MPP不会一锅粥。我见过不少人一上来就把整条链路写在一起结果画面花屏根本不知道是采集格式不对还是编码参数配错了。2. 环境准备与内核驱动确认在写代码之前先把板子上的环境确认一遍。这一步偷懒后面调试能让你怀疑人生。2.1 确认内核版本和 MPP 库RK3568 的 SDK 一般基于 Linux 4.19 或 5.10。先看内核版本uname -a然后确认 MPP 库是否已经编译安装。瑞芯微的 MPP 源码通常在 SDK 的external/mpp目录下。编译后会生成librockchip_mpp.so头文件在inc/目录。检查一下ls /usr/lib/librockchip_mpp.so ls /usr/include/rockchip/rk_mpi.h如果没有需要自己交叉编译 MPP。编译命令大致是cmake -DCMAKE_TOOLCHAIN_FILE../arm-linux-gnueabihf.toolchain.cmake \ -DHAVE_DRMON -DRKPLATFORMON .. make -j4 make install注意RKPLATFORMON这个选项它决定了 MPP 是否走 DRM 分配器。RK3568 上建议打开这样编码器输入 buffer 可以直接用 DRM 的 DMA-BUF减少一次拷贝。2.2 确认 V4L2 摄像头节点和格式插上摄像头看设备节点v4l2-ctl --list-devicesRK3568 的 ISP 一般会注册多个 video 节点比如rkisp_mainpath、rkisp_selfpath。主路径通常是/dev/video0或/dev/video1。用v4l2-ctl查一下支持的格式v4l2-ctl -d /dev/video0 --list-formats-ext你会看到类似NV12、NV16、YUYV的输出格式列表。记下你需要的分辨率和对应的格式。OV5695 在 RK3568 上通常输出 1920x1080 NV12帧率可以到 30fps。注意有些 sensor 的驱动默认输出格式不是 NV12需要在设备树里配置 ISP 的输出格式或者在应用层用 RGA 转换。设备树里isp节点的rockchip,isp-virtual-dev-name和remote-endpoint配置会影响最终暴露的 video 节点。2.3 设备树里摄像头相关的关键配置RK3568 的设备树里MIPI CSI 和 ISP 的配置集中在rk3568.dtsi和板级 dts 里。以 OV5695 为例关键节点包括i2c1 { ov5695: ov569536 { compatible ovti,ov5695; reg 0x36; clocks cru CLK_MIPIDPHY_REF; clock-names xvclk; power-domains power RK3568_PD_VI; pinctrl-names default; pinctrl-0 cam_clk; reset-gpios gpio3 RK_PA0 GPIO_ACTIVE_HIGH; port { ov5695_out: endpoint { remote-endpoint mipi_in_ucam0; >struct v4l2_format fmt {0}; fmt.type V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; fmt.fmt.pix_mp.width 1920; fmt.fmt.pix_mp.height 1080; fmt.fmt.pix_mp.pixelformat V4L2_PIX_FMT_NV12; fmt.fmt.pix_mp.field V4L2_FIELD_NONE; fmt.fmt.pix_mp.num_planes 1; if (ioctl(fd, VIDIOC_S_FMT, fmt) 0) { perror(VIDIOC_S_FMT); return -1; } // 读回确认 if (ioctl(fd, VIDIOC_G_FMT, fmt) 0) { perror(VIDIOC_G_FMT); return -1; } printf(Actual format: %dx%d, pixelformat: %c%c%c%c\n, fmt.fmt.pix_mp.width, fmt.fmt.pix_mp.height, fmt.fmt.pix_mp.pixelformat 0xff, (fmt.fmt.pix_mp.pixelformat 8) 0xff, (fmt.fmt.pix_mp.pixelformat 16) 0xff, (fmt.fmt.pix_mp.pixelformat 24) 0xff);RK3568 的 ISP 驱动对 NV12 的支持是原生的一般不会拒绝。但如果你请求的是 YUYV驱动可能会返回 NV16 或者别的格式这时候就得用 RGA 转一道。3.2 申请 buffer 与入队V4L2 的 buffer 管理用VIDIOC_REQBUFS申请然后mmap到用户空间再VIDIOC_QBUF入队。RK3568 上建议申请 4 个 buffer太少容易丢帧太多浪费内存。struct v4l2_requestbuffers req {0}; req.count 4; req.type V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; req.memory V4L2_MEMORY_MMAP; if (ioctl(fd, VIDIOC_REQBUFS, req) 0) { perror(VIDIOC_REQBUFS); return -1; } struct buffer { void *start; size_t length; int dmabuf_fd; } buffers[4]; for (int i 0; i req.count; i) { struct v4l2_buffer buf {0}; struct v4l2_plane planes[VIDEO_MAX_PLANES] {0}; buf.type V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; buf.memory V4L2_MEMORY_MMAP; buf.index i; buf.m.planes planes; buf.length VIDEO_MAX_PLANES; if (ioctl(fd, VIDIOC_QUERYBUF, buf) 0) { perror(VIDIOC_QUERYBUF); return -1; } buffers[i].length buf.m.planes[0].length; buffers[i].start mmap(NULL, buf.m.planes[0].length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.planes[0].m.mem_offset); if (buffers[i].start MAP_FAILED) { perror(mmap); return -1; } // 入队 if (ioctl(fd, VIDIOC_QBUF, buf) 0) { perror(VIDIOC_QBUF); return -1; } }这里有个关键点MPP 编码器需要的是 DMA-BUF fd不是 mmap 的虚拟地址。如果你用V4L2_MEMORY_MMAP拿到的是虚拟地址喂给 MPP 时还得再拷贝一次。更好的做法是用V4L2_MEMORY_DMABUF直接拿 fd 传给 MPP。RK3568 的 ISP 驱动支持 DMABUF 导出用VIDIOC_EXPBUF就能拿到 fd。struct v4l2_exportbuffer expbuf {0}; expbuf.type V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; expbuf.index i; expbuf.plane 0; expbuf.flags O_RDWR | O_CLOEXEC; if (ioctl(fd, VIDIOC_EXPBUF, expbuf) 0) { perror(VIDIOC_EXPBUF); return -1; } buffers[i].dmabuf_fd expbuf.fd;这样buffers[i].dmabuf_fd就可以直接传给 MPP 的mpp_frame_set_buffer或者通过mpp_buffer_import导入。3.3 启动流与循环取帧所有 buffer 入队后VIDIOC_STREAMON启动流然后循环VIDIOC_DQBUF拿帧。enum v4l2_buf_type type V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; if (ioctl(fd, VIDIOC_STREAMON, type) 0) { perror(VIDIOC_STREAMON); return -1; } while (running) { struct v4l2_buffer buf {0}; struct v4l2_plane planes[VIDEO_MAX_PLANES] {0}; buf.type V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; buf.memory V4L2_MEMORY_MMAP; buf.m.planes planes; buf.length VIDEO_MAX_PLANES; if (ioctl(fd, VIDIOC_DQBUF, buf) 0) { perror(VIDIOC_DQBUF); break; } // 处理这一帧buf.index 对应 buffers[buf.index] process_frame(buffers[buf.index], buf.m.planes[0].bytesused); // 处理完重新入队 if (ioctl(fd, VIDIOC_QBUF, buf) 0) { perror(VIDIOC_QBUF); break; } }实操心得VIDIOC_DQBUF默认是阻塞的。如果你需要非阻塞模式打开设备时加O_NONBLOCK然后用poll或select等待。但在 RK3568 上阻塞模式配合独立线程更简单不容易出幺蛾子。4. MPP 编码器的配置与调用MPP 的 API 看起来有点绕但理清楚之后其实很规整。核心就是创建上下文、配置参数、循环送帧收包。4.1 MPP 编码器初始化先创建编码器上下文类型选MPP_CTX_ENC编码 ID 选MPP_VIDEO_CodingAVCH.264或MPP_VIDEO_CodingHEVCH.265。MppCtx ctx; MppApi *mpi; MppEncCfg cfg; MPP_RET ret mpp_create(ctx, mpi); if (ret ! MPP_OK) { printf(mpp_create failed\n); return -1; } // 初始化编码器 ret mpp_init(ctx, MPP_CTX_ENC, MPP_VIDEO_CodingAVC); if (ret ! MPP_OK) { printf(mpp_init failed\n); return -1; } // 获取编码配置 ret mpp_enc_cfg_init(cfg); if (ret ! MPP_OK) { printf(mpp_enc_cfg_init failed\n); return -1; }4.2 编码参数配置编码参数直接决定码流质量和带宽。下面是一组 1080P30 H.264 的常用配置// 基础参数 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, 1080); mpp_enc_cfg_set_s32(cfg, prep:format, MPP_FMT_YUV420SP); // NV12 // 码率控制 mpp_enc_cfg_set_s32(cfg, rc:mode, MPP_ENC_RC_MODE_CBR); mpp_enc_cfg_set_s32(cfg, rc:bps_target, 4000000); // 4Mbps mpp_enc_cfg_set_s32(cfg, rc:bps_max, 4800000); mpp_enc_cfg_set_s32(cfg, rc:bps_min, 3200000); 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_num, 30); mpp_enc_cfg_set_s32(cfg, rc:fps_out_denom, 1); mpp_enc_cfg_set_s32(cfg, rc:gop, 60); // I 帧间隔 // H.264 特有参数 mpp_enc_cfg_set_s32(cfg, h264:profile, 100); // High Profile mpp_enc_cfg_set_s32(cfg, h264:level, 40); // Level 4.0 mpp_enc_cfg_set_s32(cfg, h264:cabac_en, 1); // CABAC 熵编码 mpp_enc_cfg_set_s32(cfg, h264:cabac_idc, 0); mpp_enc_cfg_set_s32(cfg, h264:trans8x8, 1); // 8x8 变换 // 应用配置 ret mpi-control(ctx, MPP_ENC_SET_CFG, cfg); if (ret ! MPP_OK) { printf(MPP_ENC_SET_CFG failed\n); return -1; }几个参数的选择逻辑rc:modeCBR 适合网络推流码率稳定VBR 适合本地存储画质优先。推流场景一律 CBR。rc:gopGOP 越大I 帧越少码率越低但丢包后恢复越慢。推流场景 30 到 60 比较合适对应 1 到 2 秒一个 I 帧。h264:profileHigh Profile 比 Baseline 省 20% 左右码率但有些老设备不支持。如果推流给浏览器High Profile 没问题。h264:levelLevel 4.0 支持 1080P30如果分辨率更高要往上调。H.265 的配置类似把h264:前缀换成h265:并且mpp_init时编码 ID 用MPP_VIDEO_CodingHEVC。H.265 在同等画质下比 H.264 省 30% 到 40% 码率但编码复杂度略高RK3568 的 VPU 跑 1080P30 H.265 毫无压力。4.3 送帧与收包MPP 编码器的输入是MppFrame输出是MppPacket。送帧之前要把 V4L2 的 buffer 包装成MppFrame。MppFrame frame NULL; mpp_frame_init(frame); mpp_frame_set_width(frame, 1920); mpp_frame_set_height(frame, 1080); mpp_frame_set_hor_stride(frame, 1920); mpp_frame_set_ver_stride(frame, 1080); mpp_frame_set_fmt(frame, MPP_FMT_YUV420SP); mpp_frame_set_buffer(frame, mpp_buffer); // 送帧 ret mpi-encode_put_frame(ctx, frame); if (ret ! MPP_OK) { printf(encode_put_frame failed\n); } // 收包 MppPacket packet NULL; ret mpi-encode_get_packet(ctx, packet); if (ret MPP_OK packet) { void *ptr mpp_packet_get_pos(packet); size_t len mpp_packet_get_length(packet); // 处理码流 fwrite(ptr, 1, len, output_file); mpp_packet_deinit(packet); }这里的关键是mpp_buffer的来源。如果你用 V4L2 的 DMABUF fd可以用mpp_buffer_import导入MppBuffer mpp_buf NULL; mpp_buffer_import(mpp_buf, (MppBufferInfo){ .type MPP_BUFFER_TYPE_EXT_DMA, .fd buffers[i].dmabuf_fd, .size buffers[i].length, });导入之后mpp_frame_set_buffer直接设置这个 bufferMPP 编码器会通过 DMA 直接读零拷贝。注意mpp_buffer_import导入的 buffer 生命周期要管理好。V4L2 那边 QBUF 之后buffer 可能被驱动复用所以要么在编码完成后再 QBUF要么用mpp_buffer_ref增加引用计数。5. 常见问题与排查技巧实录这部分是我在实际调试中遇到的各种问题按现象分类整理。5.1 采集相关的问题现象VIDIOC_DQBUF一直返回 timeout 或者阻塞不返回。排查思路先确认VIDIOC_STREAMON是否成功再看 sensor 是否正常出流。用v4l2-ctl --stream-mmap --stream-count10测试一下如果这个命令也拿不到帧说明是驱动或硬件问题。检查 MIPI 线序、时钟、供电以及设备树里>mpp_enc_cfg_set_s32(cfg, h264:annexb, 1);现象H.265 编码出来的码流比 H.264 还大。检查rc:mode和gop配置。H.265 在 CBR 模式下如果 GOP 设得太小I 帧频繁码率反而会上去。另外 H.265 的h265:profile要设对Main Profile 比 Main Still Picture 更省码率。5.3 性能优化相关的问题现象CPU 占用率高帧率不稳定。检查是否有不必要的内存拷贝。V4L2 到 MPP 之间如果用 mmap 虚拟地址每帧要拷贝一次 1080P NV12大约 3MB30fps 就是 90MB/s 的拷贝量CPU 占用会很明显。改用 DMABUF 导入可以消除这次拷贝。另外MPP 的encode_put_frame和encode_get_packet可以在不同线程里做。送帧线程只管送收包线程只管收中间用队列衔接。这样 VPU 的流水线能跑满。现象多路编码时帧率下降。RK3568 的 VPU 编码能力有限具体能跑几路取决于分辨率和编码格式。1080P30 H.264 大概能跑 2 到 3 路H.265 会少一些。如果超过能力要么降分辨率要么降帧率。用cat /sys/kernel/debug/mpp_service/session_summary可以看当前 VPU 的负载。5.4 常见问题速查表现象可能原因排查方法解决方式DQBUF 超时sensor 未出流v4l2-ctl --stream-mmap测试检查 MIPI 配置和硬件画面花屏格式或 stride 不匹配VIDIOC_G_FMT读回确认按实际 stride 配置 MPP帧率低曝光过长或串行处理查曝光参数和线程模型固定曝光送帧收包分离mpp_init 失败库与驱动不匹配查 MPP 和内核版本统一用 SDK 源码编译码流无法播放缺 SPS/PPS 或起始码看码流开头字节配置 annexb 输出CPU 占用高内存拷贝过多查是否有 mmap 拷贝改用 DMABUF 导入多路帧率下降VPU 负载超限查 session_summary降分辨率或帧率6. 从编码到推流输出模块的衔接编码出来的码流最终要送到某个地方。本地存储最简单直接写文件。网络传输的话RTMP 推流是最常见的需求。6.1 码流封装与 RTMP 推流RTMP 推流需要把 H.264/H.265 码流封装成 FLV 格式。FLV 的 Video Tag 里H.264 的 NALU 前面要加一个字节的帧类型和编码 IDH.265 类似但格式略有不同。如果你不想自己处理 FLV 封装可以用 librtmp 或者 ffmpeg 的 libavformat。用 ffmpeg 的话流程是创建一个AVFormatContextavformat_alloc_output_context2指定flv格式然后avio_open打开 RTMP URL写 header每拿到一个 MPP packet 就构造一个AVPacket设置好时间戳av_interleaved_write_frame写出去。时间戳的计算很关键。MPP 的 packet 自带 PTS单位是 1/90000 秒。FLV 的时间戳单位是毫秒所以要转换int64_t pts_ms mpp_packet_get_pts(packet) * 1000 / 90000; av_packet_rescale_ts(pkt, (AVRational){1, 1000}, out_stream-time_base);如果时间戳不对播放器会花屏或者卡顿。我见过有人直接用帧序号当时间戳结果推流出去画面一卡一卡的。6.2 低延迟推流的参数调整如果做实时监控或者互动场景延迟要求高需要调整几个参数GOP 设小GOP 从 60 降到 30I 帧更频繁丢包恢复快但码率会上升。关闭 B 帧MPP 默认可能开启 B 帧B 帧会增加编码延迟。用mpp_enc_cfg_set_s32(cfg, h264:bframes, 0)关掉。rc:mode 用 CBRVBR 的码率波动会导致网络拥塞时延迟抖动。送帧收包分离确保 VPU 流水线不空转。实测下来1080P30 H.264 在 RK3568 上从采集到推流出去端到端延迟可以控制在 150 毫秒以内。如果对延迟要求极致可以降到 720P延迟能压到 80 毫秒左右。6.3 本地存储的注意事项如果只是本地录像写文件的时候注意几点文件句柄及时 flushMPP 的 packet 是连续产生的如果写文件不 flush断电时最后几秒的录像会丢。分段存储单个文件不要超过 1GB方便管理和回放。可以按时间分段比如每 10 分钟一个文件。码流格式存文件建议用 Annex-B 格式播放器兼容性好。MPP 的h264:annexb配置打开即可。7. 一些调试经验和后续扩展方向调试这套链路最有效的工具是v4l2-ctl和 MPP 自带的测试程序。MPP 源码里的test/mpp_enc_test可以直接跑编码测试不依赖摄像头先用它确认 MPP 本身没问题再接入 V4L2。这样能把问题范围缩小。另外RK3568 和 RK3566 在 MPP 上的差异不大主要是 VPU 频率和编码能力略有不同。RK3588 的 MPP 支持 8K 编码API 层面兼容但配置参数更多。如果你从 RK3568 迁移到 RK3588大部分代码不用改只需要调整分辨率和码率参数。后续如果要扩展可以考虑几个方向一是加 RGA 做缩放和格式转换支持多分辨率输出二是接入 RTSP 服务用 live555 或者 mediamtx 做服务端三是加 AI 分析把解码后的帧送给 NPU 做推理。RK3568 的 NPU 算力虽然不强但跑个轻量级的目标检测还是够用的。我在实际项目里踩过最大的坑是 V4L2 的 buffer 生命周期管理。一开始没注意 QBUF 的时机编码器还在读 buffer 的时候就 QBUF 了结果画面偶尔出现撕裂。后来改成编码完成后再 QBUF问题就消失了。这个细节在文档里不会写但实际跑起来很容易遇到。