ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

绕开Rockit,RV1106用V4L2+MPP实现模拟摄像头硬编码

绕开Rockit,RV1106用V4L2+MPP实现模拟摄像头硬编码 1. 项目概述为什么我建议你绕开Rockit做模拟摄像头采集编码RV1106/RV1103这颗芯片做IPC和图像采集方案是真的香价格便宜、外设全、VPU编解码和ISP一条龙。但真正把它拿来干活的时候很多朋友会被Rockit这个官方SDK卡得欲哭无泪——封装太重、调试极不透明、还有一堆业务层逻辑是写死的。如果你只需要“把模拟摄像头的画面采集进来编码成H.264/H.265然后丢给网络或存储”那这条路完全不需要Rockit。我这次要分享的是在RV1106/RV1103上直接走/dev/video0这类V4L2节点做模拟摄像头AHD/TVI/CVI或者CVBS转BT.1120之后的采集再把YUV数据交给MPP硬编码成视频流。用V4L2MPP这条路径绕开Rockit库的所有限制整个链路完全自己可控出问题也能一层一层追。先说结论这条路完全走得通。我在RV1106平台实测640x48030fps的模拟摄像头输入采集到编码整条链路的CPU占用极低VPU编码稳得一批码流直接用ffplay就能拉出来看。下面把从V4L2采集到MPP编码的全过程拆开讲每个环节怎么配参数、为什么这么配、坑在哪全部说透。这套方案适合谁手里有RV1106/RV1103开发板想自己写采集编码程序而不用Rockit那套封装的开发者在Rockit的VI通道上踩了坑、想下钻到驱动层去看问题的朋友还有就是想搞懂V4L2和MPP这两个Linux多媒体基础框架怎么协同工作的人。这篇文的代码和思路可以直接抄作业内核驱动用官方SDK默认配置就行不需要改驱动也不需要打死Rockit——只要不用它的库接口就行。2. 整体链路设计V4L2和MPP到底怎么分工协作2.1 为什么绕开Rockit先搞懂你本来要绕过什么Rockit是瑞芯微给RV1106/RV1103这类IPC芯片做的多媒体中间层它内部封装了VI视频输入、VPSS视频处理子系统、VENC视频编码、VO视频输出等模块。正常情况下你调Rockit的API比如rk_aiq_xxx、rk_mpi_xxx框架会自动帮你完成sensor注册、ISP管线调度、编码器初始化等一系列动作。这个封装的代价就是很多事情你只能按它的规则来。举个例子Rockit里VI通道的buffer管理、帧率控制、通道裁剪方式都有默认策略你想实现“V4L2用户态直接映射缓冲区”这种相对底层一点的操作会发现在Rockit API里根本找不到对应入口。还有Rockit和AIQ图像质量调试工具是深度绑定的如果你没有sensor的IQ文件或者你用的是非标模拟摄像头方案Rockit这套反而成了累赘——它默认会去加载ISP和AE相关的配置但你的信号源根本不存在ISP的sensor模型。以我这次用的模拟摄像头CVBS转BT.1120通过芯片的VIP0口接入为例信号链路的本质是模拟信号从BNC口进来 → CVBS解码器比如TP2825/AHD884等还原成数字YUV → BT.1120格式送给RV1106的VIP接口。这个环节里RV1106只是接收数字YUV流不需要sensor电源控制、不需要IIC配置寄存器、也不需要RAW域ISP处理。这种情况下用Rockit就纯属自找麻烦——它要做的事是“从sensor的RAW图开始处理”而你现在喂给它的是已经成型的YUV。2.2 V4L2这边的数据通路视频输入子系统在Linux侧长什么样在RV1106/RV1103的内核驱动里视频输入子系统是围绕rockchip,rv1106-cif和ISP部分来组织的。因为芯片有内置ISP哪怕你不做RAW处理工具链里也带了所以整个输入链路在内核里注册出来的V4L2节点很是有几个的。像RV1106的SDK在默认配置下会出现/dev/video0rkispp_m_bypass也就是ISP bypass节点输出直通的YUV数据。/dev/video1rkispp_scale0ISP缩放通道0可以用它拿低分辨率的预览流。加上主链路rkisp_mainpath等节点具体数量和SDK版本、dts配置有关。我这次用的是/dev/video0这个bypass节点。为什么用它因为模拟摄像头解码芯片输出的BT.1120 YUV流不需要做3A、不涉及RAW域处理走bypass就是原原本本的YUV出来格式一般是YUYV或NV16/NV12根据解码芯片的输出格式来。V4L2采集一个帧的常规套路大家应该很熟open设备 → 查询能力 → 设置格式 → 申请缓冲区 → 入队 → 用select/poll等事件 →dqbuf拿帧 → 处理 → 再qbuf。但RV1106的CIF子系统有几个跟通用V4L2不太一样的细节它有两个video节点要配合用一个主节点比如video0做数据流控还有一个子节点比如video2或对应的是只出参数的那个节点它就是传说中的PARAM节点。主节点出YUV数据PARAM节点用来配置ISP、曝光、增益等寄存器的。BR感buffer关系不太明显的时候容易搞混后面实操部分细讲。RV1106的VI侧对buffer的对齐有要求一般YUV数据需要按16字节对齐stride这点在VIDIOC_S_FMT设置fmt.fmt.pix.bytesperline时要处理否则解码后的图像是花的或是斜的。因为这是硬核的视频采集不是USB摄像头那种即插即用设备所以帧率由芯片的信号时序决定V4L2投进去的buffer不会主动限制帧率要限制就得在sensor/解码芯片那边或通过下采样去配。2.3 MPP这边的编码任务从YUV裸流到H.264/H.265V4L2采出来的数据通常是NV12根据你配置的pixel format决定这种格式可以直接丢给MPP硬编码省下格式转换的开销。MPP是瑞芯微的视频编解码库全称Media Process Platform它跟Rockit是完全解耦的另一套库直接操作VPU硬件。MPP支持的编码格式涵盖H.264、H.265、JPEG等RV1106的VPU是支持H.264/H.265编码的我们这次主要用H.264 baseline/main profile兼容性最好网络播放也没问题。MPP编码器的工作流程是初始化MPP上下文 → 配置编码参数宽高、像素格式、帧率、码率模式 → 循环喂帧 → 拿码流包 → 喂给下一个环节。这里最关键的一个参数是输入帧率它直接关系到编码器的时间戳计算和码控。因为是模拟摄像头输入帧率是30000/1001这种NTSC制式值或者25fps的PAL制式你还得按实际信号来建议在代码里用浮点计算避免小数精度丢失导致MPP内部时间戳失误。说下为什么MPP和V4L2要分开写代码而不是混在一起。V4L2是Linux内核的框架负责底层设备的控制MPP是用户态库负责VPU驱动和编解码。它们之间的衔接点就是YUV帧和内存buffer——V4L2在mmap模式下直接映射出来的内存地址MPP编码时要求输入帧的内存必须是连续的物理连续或IOMMU映射连续。RV1106默认开IOMMUV4L2映射出来的内存是经过驱动分配的buffer天然满足这个要求。不过有一个很容易踩的坑V4L2缓冲区默认是cache write-back模式的VPU读取的时候可能因为cache一致性问题拿到脏数据。SDK的V4L2驱动一般会处理这种情况——要么在VIDIOC_QBUF时做了cache flush要么buffer类型是V4L2_MEMORY_MMAP时dma_buf是带同步机制的。但为了保险起见在做MPP编码前最好对每个V4L2 buffer调用一次DMA同步VIDIOC_QBUF本身在多数驱动里做了这个事但如果你是直接拿映射地址去硬编而中间绕过了qbuf那需要格外小心。后面我给的参考代码里会体现这个处理。3. 手把手V4L2采集模拟摄像头YUV的完整流程3.1 先确认信号通路通不通用v4l2-ctl快速摸设备情况拿到RV1106板子先把模拟摄像头接到CVBS解码芯片的输入口上确认信号源的制式和分辨率。一般模拟摄像头的输出分辨率有D1720x576对于PAL、720x480对于NTSC、960H960x576/960x480以及AHD/TVI/CVI规格的720P/1080P。如果是常规BT.1120输入注意芯片的输入是16bit还是8bit。接入之后先在板子上跑下面几条命令# 查看V4L2设备节点列表和名字 v4l2-ctl --list-devices # 结果一般会显示类似 # rkisp_mainpath (platform: rkisp): # /dev/video0 # /dev/video1 # /dev/video2 # 查看video0支持的格式 v4l2-ctl -d /dev/video0 --list-formats-ext如果--list-formats-ext出来的格式列表里有YUYV、NV12、NV16这些YUV格式说明节点是通的。如果查询时报Invalid argument或者根本没有格式十有八九是dts里这个节点的通道没配好或者mclk/复位脚没拉对先回去查dts里VIP和CIF的电源时钟。另外一个排查思路是看内核日志dmesg | grep -i cif确认有没有link validate失败之类的信息。CIF的链路涉及media controller比如media节点可能是media0。如果链路没有pipeline起来光在应用层调用S_FMT也会失败。RV1106的SDK一般情况下默认pipeline是配好的但如果你在自己的板子上裁剪过dts就要用media-ctl -p看一下当前的链路状态。3.2 打开设备、设置格式的代码细节下面这段是V4L2采集初始化部分的C代码我直接在SDK的Linux应用层编译验证过。注意几个细节#include stdio.h #include stdlib.h #include string.h #include fcntl.h #include unistd.h #include errno.h #include sys/ioctl.h #include sys/mman.h #include linux/videodev2.h #define CAPTURE_DEV /dev/video0 struct buffer { void *start; size_t length; }; static struct buffer buffers[4]; static int n_buffers 4; int v4l2_capture_init(int width, int height, unsigned int pixfmt) { int fd open(CAPTURE_DEV, O_RDWR | O_NONBLOCK, 0); if (fd 0) { perror(open video0); return -1; } struct v4l2_format fmt; memset(fmt, 0, sizeof(fmt)); fmt.type V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; fmt.fmt.pix_mp.width width; fmt.fmt.pix_mp.height height; fmt.fmt.pix_mp.pixelformat pixfmt; // V4L2_PIX_FMT_NV12 fmt.fmt.pix_mp.field V4L2_FIELD_NONE; if (ioctl(fd, VIDIOC_S_FMT, fmt) 0) { perror(VIDIOC_S_FMT); close(fd); return -1; } // 实际驱动可能会对齐宽高读回来确认 printf(realse width%d, height%d, bytesperline[0]%d\n, fmt.fmt.pix_mp.width, fmt.fmt.pix_mp.height, fmt.fmt.pix_mp.plane_fmt[0].bytesperline); // 这里要把实际宽高和stride保存下来后面传给MPP用 // 特别关注strideRV1106的CIF输出经常是16字节对齐 }这段里两个重要的地方VIDIOC_S_FMT后必须读回实际值。驱动可能把你的宽高调整到对齐尺寸比如要求宽度必须是16的整数倍如果你设置的640是OK的640/1640刚好但如果你设成655驱动就会给你对齐到656或660不回读的话后面MPP编码尺寸就永远对不上。V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE不是单平面的V4L2_BUF_TYPE_VIDEO_CAPTURE。RV1106的CIF/ISP驱动走的是multi-planar API因为YUV420SPNV12虽然只占两个plane但V4L2侧仍然是按mplane处理。很多从USB摄像头转过来的朋友习惯性用单平面API直接ioctl返回EINVAL。你说我拿到的BT.1120输入到底是NV12还是YUYV这取决于CVBS解码芯片的输出格式和CIF的配置。TP2825、NVP6158这类芯片可以配置输出顺序和格式一般CVBS转数字最常用的是YUYVYUV422 8bit双像素交错但RV1106的ISP/CIF处理链在bypass模式下也可以把置换成NV12输出。建议在V4L2层面尽量以NV12为最终采集格式因为VPU编码NV12是最顺的省一次格式转换而且NV12的大小是width * height * 3 / 2内存布局固定MPP喂帧也方便。如果你在--list-formats-ext里看到NV12就直接设如果节点只能输出YUYV那在MPP编码前要自己转一次或者用RGA做转换不过RV1106的SDK里V4L2节点是可以用userspace的NV12输出配置直接跟驱动要的CIF硬件本身支持做这个颜色空间转换。3.3 buffer申请、mmap映射和入队出队的完整循环初始化完格式后申请缓冲区并映射这部分比较公式化int v4l2_request_buffers(int fd) { struct v4l2_requestbuffers req; memset(req, 0, sizeof(req)); req.count n_buffers; 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; } // 注意驱动可能返回比你申请少的buffer以实际为准 n_buffers req.count; printf(reqbuf count%d\n, n_buffers); for (int i 0; i n_buffers; i) { struct v4l2_buffer buf; struct v4l2_plane planes[VIDEO_MAX_PLANES]; memset(buf, 0, sizeof(buf)); memset(planes, 0, sizeof(planes)); 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; } // mplane下至少要拿plane 0的映射信息 buffers[i].length planes[0].length; buffers[i].start mmap(NULL, planes[0].length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, planes[0].m.mem_offset); if (buffers[i].start MAP_FAILED) { perror(mmap); return -1; } } }这里最容易被忽略的是mplane下struct v4l2_buffer里那份planes数组的处理。有很多朋友在单平面API里QUERYBUF后直接用buf.m.offset换成mplane直接把buf.m.planes忽略掉结果mmap的offset拿到一个0整个采集黑屏。正确的做法是给每个v4l2_buffer准备一个长度为VIDEO_MAX_PLANES的v4l2_plane数组指向buf.m.planes然后从planes[0]里拿length和offset。映射完成后把所有buffer入队、STREAMON然后开一个poll循环每次dqbuf到一帧就对那个buffer做处理处理完马上qbuf回去。处理的方式就是把它交给MPP。为了平滑队列深度我建议申请4到6个太少的话一旦应用处理不及时驱动的环形队列就饿死了容易丢帧或者卡顿太多大于8又容易造成画面延迟变大因为每一帧在队列里排队的时间变长了。模拟摄像头本身延迟就比网络摄像头大所以队列深度这块建议实测调。3.4 如果直接用PARAM节点控制曝光和增益的几个冷门操作前面说到RV1106还有一个PARAM节点在主节点采集的同时通过它往内核写ISP控制参数。模拟摄像头虽然不做sensor 3A但解码芯片的输出增益调整和信号相关参数有时会需要调一调。PARAM节点的操作方式比较特殊是通过扩展控制类V4L2_CTRL_CLASS_USER加一些平台自定义ID的方式来控制的。比如在RV1106的SDK里你可以搜到V4L2_CID_CIF_*这类宏具体值在头文件里。用的时候打开PARAM对应的设备节点然后VIDIOC_S_CTRL设置值。比如曝光时间struct v4l2_control ctrl; memset(ctrl, 0, sizeof(ctrl)); ctrl.id V4L2_CID_EXPOSURE; // 或者平台自定义ID ctrl.value 1000; // 你想要的曝光值 ioctl(param_fd, VIDIOC_S_CTRL, ctrl);这块翻SDK的kernel/drivers/media/platform/rockchip/cif和isp代码能找到不少参考。不过说句大实话如果你的CVBS解码芯片自己管理了曝光和白平衡大多数模拟摄像头方案就是芯片内部处理的那RV1106这边根本不需要动任何参数直通就行。我这套代码里把PARAM节点的初始化代码也写进去了但一帧都没实际设置过纯属为了让后面扩展留个口子。4. MPP编码侧用MPP把YUV喂给VPU稳到飞起的编码方案4.1 MPP编码器初始化参数配置里那些要被重点照顾的字段V4L2采集循环跑通后编码侧就是纯粹MPP的事了。MPP编码器初始化的核心配置是一个叫MppEncPrepCfg的结构体外加码率控制相关的MppEncRcCfg。这两个配置搞定编码基本就成了。参考代码#include mpp_enc.h #include mpp_common.h static MppCtx ctx; static MppApi *mpi; static MppEncPrepCfg prep_cfg; static MppEncRcCfg rc_cfg; int mpp_encoder_init(int width, int height, int frame_rate) { MppEncCodecCfg codec_cfg; mpp_create(ctx, mpi); MPP_RET ret mpi-control(ctx, MPP_CMD_SET_BASE_CTX_CFG, NULL); if (ret ! MPP_OK) { mpp_err(MPP_CMD_SET_BASE_CTX_CFG failed\n); return -1; } ret mpi-control(ctx, MPP_CMD_SET_STREAM_TYPE, type); // type MPP_VIDEO_ENCODING_H264 if (ret ! MPP_OK) { mpp_err(MPP_CMD_SET_STREAM_TYPE failed\n); return -1; } // 准备编码配置 memset(prep_cfg, 0, sizeof(prep_cfg)); prep_cfg.change MPP_ENC_PREP_CFG_CHANGE_INPUT | MPP_ENC_PREP_CFG_CHANGE_FORMAT | MPP_ENC_PREP_CFG_CHANGE_WIDTH | MPP_ENC_PREP_CFG_CHANGE_HEIGHT; prep_cfg.width width; prep_cfg.height height; prep_cfg.format MPP_FMT_YUV420SP; // NV12 prep_cfg.change | MPP_ENC_PREP_CFG_CHANGE_INPUT | MPP_ENC_PREP_CFG_CHANGE_FORMAT; ret mpi-control(ctx, MPP_CMD_SET_PREP_CFG, prep_cfg); if (ret ! MPP_OK) { mpp_err(set prep cfg failed\n); return -1; } // 码控配置 memset(rc_cfg, 0, sizeof(rc_cfg)); rc_cfg.change MPP_ENC_RC_CFG_CHANGE_MODE | MPP_ENC_RC_CFG_CHANGE_GOP | MPP_ENC_RC_CFG_CHANGE_FPS_IN_FLEX; rc_cfg.mode MPP_ENC_RC_MODE_CBR; // 恒定码率IPC场景常用 rc_cfg.gop frame_rate * 2; // 2秒一个I帧 rc_cfg.fps_in_flex frame_rate; rc_cfg.fps_in_den 1; rc_cfg.fps_out_flex frame_rate; rc_cfg.fps_out_den 1; ret mpi-control(ctx, MPP_CMD_SET_RC_CFG, rc_cfg); if (ret ! MPP_OK) { mpp_err(set rc cfg failed\n); return -1; } // 编码输出格式细节 memset(codec_cfg, 0, sizeof(codec_cfg)); codec_cfg.coding MPP_VIDEO_ENCODING_H264; codec_cfg.h264.profile 100; // High profile codec_cfg.h264.level 40; codec_cfg.h264.cabac_en 1; codec_cfg.h264.cabac_idc 0; codec_cfg.h264.trans_8x8 1; ret mpi-control(ctx, MPP_CMD_SET_CODEC_CFG, codec_cfg); if (ret ! MPP_OK) { mpp_err(set codec cfg failed\n); return -1; } mpi-control(ctx, MPP_CMD_SET_HEADER_MODE, header_mode); // MPP_ENC_HEADER_MODE_EACH_IDR return 0; }这里面有三个容易被忽略的雷MPP_ENC_PREP_CFG_CHANGE_INPUT到底改变了什么它表示你接下来要设置输入帧的内存布局。如果你不设这个MPP可能默认认为输入是MPP_FMT_YUV420SP但stride是width不按对齐来。如果你之前V4L2侧读到的bytesperline不是width比如640的宽度但bytesperline是656那编码器只认width导致画面右侧有4个像素的条纹。所以如果发现这个问题要在prep_cfg里通过hor_stride和ver_stride告诉MPP真实stride这是很多做MPP的人第一周都会撞上的问题。CBR码控模式下目标码率在哪设置rc_cfg里还有bps_target、bps_max、bps_min这几个字段。忘了设置的话MPP会按默认值跑画面复杂一高就糊成了马赛克。IPC场景我一般设bps_target width * height * 6 / 8左右像素数乘6比特再除以8得到字节/秒实测640x48030fps设1Mbps到2Mbps画质都可以。MPP_ENC_HEADER_MODE_EACH_IDR的作用它让每个I帧都带SPS/PPS。很多播放器比如VLC的某些版本在码流中间开始播放时如果没有SPS/PPS就直接黑屏。这个参数强烈建议设成EACH_IDR代价只是每个I帧多几十字节完全可以忽略。4.2 把V4L2的帧交给MPPBuffer封装、时间戳和码流包回收编码初始化完成后在每次从V4L2拿到一帧YUV后把它封装成一个MppFrame喂给编码器然后取回编码好的码流包MppPacket。关键代码static MppBuffer mpp_buf; static MppFrame frame; int encode_one_frame(void *yuv_vaddr, size_t yuv_size, int64_t pts) { // 如果能复用MppBuffer最好否则要重新import // 这里演示的是从V4L2 buffer的虚拟地址拷贝或者直接引用 mpp_frame_init(frame); mpp_frame_set_width(frame, width); mpp_frame_set_height(frame, height); mp
返回列表