ARTICLE DETAIL

资讯详情

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

Rockit VI模块开发实战:初始化细节与数据流处理全解析

Rockit VI模块开发实战:初始化细节与数据流处理全解析 做多媒体中间件开发这几年Rockit框架下的VI模块我前前后后调了不下十次每次换一颗sensor、换一块板子初始化部分总是最先出问题的地方。VI模块的全称是Video Input它负责把MIPI、LVDS、BT.1120这类接口进来的视频数据接入内存是Rockit体系里整个视频处理链路的源头。如果在初始化阶段没有把通道属性、buffer数量、像素格式之间的关系理顺后面的数据流处理就会跟着遭殃丢帧、花屏、取帧超时全都会冒出来。这篇文章把我在Rockit VI模块开发上踩过的坑和沉淀下来的流程完整讲一遍从初始化到底层数据流处理的闭环适合刚接手Rockit平台、正在被视频输入搞得焦头烂额的嵌入式开发者参考也适合有经验的同学对照自己的工程习惯做一次checklist式的自查。1. 先搞清VI模块在整个多媒体链路中的位置1.1 Rockit框架中的VI角色定位很多人拿到Rockit平台后第一件事就是急着点一颗摄像头、出一路视频结果被一堆以VI开头的结构体和接口绕晕。要理解这些接口先得搞清楚VI在整个框架里到底扮演什么角色。Rockit是Rockchip平台上的多媒体处理中间件屏蔽掉了大部分底层驱动细节给应用层提供一套统一的MPIMedia Process Interface接口。VI是其中的视频输入单元对应驱动层里的video input pipeline负责从物理接口接收外部视频源数据完成必要的格式整理后写到内存buffer里供后级模块继续消费。它不负责图像算法也不负责编码核心使命就是把外部视频信号稳定、完整地搬进内存。从系统视角看VI模块处在整个音视频处理链路的最前端后面通常会跟着VPSS做缩放、去噪、旋转等后处理再往后接VENC做编码或者接RKNN做推理。它不像算法模块那样有那么多花活但它的稳定性直接决定整条链路能不能转起来。很多做应用层的同事觉得VI只是“开个通道、收个帧”但实际出问题时丢帧、帧率不对、画面错位根因往往都在VI侧或者VI和sensor的对接处。所以我把VI看作整个链路的地基地基歪了上层越努力越折腾。你在初始化上多花十分钟把参数理清楚后面可以省下好几个小时的排障时间。1.2 VI与VPSS/VENC的数据交接方式VI往后送数据主要有两种模式一种是bind模式一种是取帧模式。bind模式通过rk_mpi_sys_bind把VI通道绑定到一个或多个后级模块比如VPSS或VENC绑定之后底层自动搬运帧数据业务代码基本不用碰帧。取帧模式则相反由用户线程主动去VI通道里拿帧处理完再release回去。这两种模式没有绝对好坏主要看使用场景。走编码和RTSP推流的项目我基本都用bind模式省心需要做AI检测、抓拍、图像拼接的项目就得用取帧模式因为你必须拿到帧里的像素内容去做分析。为什么说这个选择要放在初始化之前考虑因为bind和取帧模式下VI通道的配置细节差别很大。bind模式下buffer数量可以适当压得低一些因为数据搬运是系统自动做的取帧模式下你要考虑应用处理帧的耗时buffer数量就要多留一点。另外bind模式下要考虑绑定层级和通道间的fflush清帧时机取帧模式则要考虑取帧超时和线程调度。后面初始化参数怎么填很多是由这个数据流模型决定的。我见过有人一开始定了取帧模式后面又直接改成bind结果buffer数量和线程模型完全没跟过来跑起来各种丢帧这就是前期没把数据流方向想清楚的结果。2. VI模块初始化全细节从通道绑定到buffer分配2.1 初始化前必须理清的参数清单VI初始化本质上是把硬件能力和业务需求对齐的过程。所以在动代码之前我会先列一张参数清单逐个核对输入接口是MIPI CSI、LVDS还是BT.1120接口下挂的sensor或HDMI接入芯片输出的是什么格式RAW Bayer还是YUV分辨率、帧率、数据位深是多少MIPI模式下用了几个lane对应到芯片的哪个CSI口。这些参数大多可以从sensor datasheet或者参考设计的DTS里抄到但一定要逐项确认。DTS里的csi2_dphy节点、mipi_csi2节点、sensor节点是否使能决定后面VI_DEV_ATTR_S配完有没有意义。我遇到过很多次sensor本身已经输出数据了但DTS没开对应laneVI侧初始化照样失败。除了硬件链路参数开发环境侧也有一个初始化配置要提前做干净。Rockit的库、依赖的头文件、交叉编译工具链的路径、板子上设备节点的权限这些如果没对齐会出现代码明明编译过了运行起来却加载不到库的尴尬问题。这类问题不属于VI逻辑本身但是会大量占用排查时间所以我在环境初始化阶段会用一份固定的脚本统一设置环境变量把LD_LIBRARY_PATH、RK_MPI_SYS_PATH这些一次性配好避免每次开新终端都重新踩坑。磨刀不误砍柴工环境初始化做扎实了后面调VI的时候心态会稳很多。2.2 结构体初始化的细节坑进入代码层面第一个最常见的坑就是结构体初始化。C语言里定义VI_CHN_ATTR_S、VI_DEV_ATTR_S这类结构体时很多同学只给低层字段赋值比如设置了宽高和像素格式但其他字段直接靠局部变量默认值这在栈上是随机的脏数据。实际表现很诡异用YUV420格式时一切正常切到NV12就花屏切到RAW格式干脆报错。原因不是格式枚举写错了而是结构体里某些保留字段、对齐字段、压缩模式字段的值是乱的驱动读取时行为完全不可预期。所以我的习惯是每个结构体定义后先memset清零再逐字段赋值最后再拿sizeof检查一下结构体版本是否和SDK头文件匹配。VI_CHN_ATTR_S stChnAttr; memset(stChnAttr, 0, sizeof(stChnAttr)); stChnAttr.enPixFmt RK_FMT_YUV420SP; stChnAttr.u32Width 1920; stChnAttr.u32Height 1080; stChnAttr.enCompressMode COMPRESS_MODE_NONE; stChnAttr.enMirror MIRROR_NONE; stChnAttr.u32FrameRate 30; // 创建VI通道 rk_mpi_vi_chn_create_chn(0, 0, stChnAttr); // 使能VI通道 rk_mpi_vi_chn_enable_chn(0, 0);注意我这里的接口命名是常见SDK版本的写法不同版本可能叫RK_MPI_VI_CreateChn之类以你手头头文件为准。关键是结构体必须先清零这个习惯能挡掉至少三分之一不明所以的初始化问题。在嵌入式这种内存环境里不要指望局部变量初始值为0那只是理论上的侥幸。2.3 初始化顺序为什么不能乱VI初始化有一个固定的顺序先配置设备属性dev再创建逻辑通道chn最后使能通道enable。设备属性描述的是物理接口的工作模式比如MIPI模式下要不要做lane翻转、时钟极性怎么样逻辑通道是在设备之上抽象出来的一层用于裁剪、镜像、设置帧率等业务属性。为什么顺序不能乱因为通道使能时驱动会去读取设备属性配置MIPI PHY如果设备属性没配好enable阶段会在PHY跟前置芯片握手时卡死或者返回错误表现就是时钟没起来、PLL压根不锁。顺序乱导致的经典现象是rk_mpi_vi_chn_enable_chn返回成功后取帧一直超时但代码逻辑看起来没有毛病。这时候去查状态往往发现PHY层没起来罪魁祸首就是初始化时直接创建了通道就enable跳过了设备属性配置。所以哪怕你只改一个分辨率也要把设备属性一起过一遍确保和通道属性是对应的。我习惯把整个初始化过程封装成一个函数入参是sensor型号、分辨率、帧率函数内部按固定顺序执行尽量减少手动拼装带来的顺序风险。这个函数写完基本可以一直复用换板子改参数就行。2.4 buffer数量到底怎么定才不浪费内存buffer数量是VI初始化里最常被随手填的一个参数很多人直接填3因为参考代码里是3。但buffer数量直接影响两个事视频延迟和内存开销。以1080p30为例一帧NV12的数据量是1920x1080x1.5字节大约3.1MB如果是RGB888就是6.2MB。你开4路视频输入每路buffer填4光VI侧就占掉近50MB内存这对很多内存只有1GB到2GB的嵌入式设备来说不是小数字。buffer数量理论上要满足生产者速率和消费者速率的匹配。最简单的估算方式buffer数量要大于等于后级单帧处理平均耗时乘以输入帧率再加2。比如后级处理一帧平均耗时20ms输入帧率30fps那么一帧间隔约33ms处理耗时20ms意味着每帧处理时新帧可能刚到或者还没到buffer数量理论上是2个左右但为了抗突发和调度抖动我一般会再加1到2个余量落到3到4个。调试阶段我会先给足4个跑稳之后慢慢往下压压到出现丢帧的临界值再回调一档这样内存利用率和稳定性都兼顾。这里还要提一个细节buffer数量不是光填通道属性里的一个数字就完事还要确认你有没有给这个通道分配对应的内存池。Rockit里一般通过buffer group或mpp buffer pool来管理如果你手动调了内存池的大小要和buffer数量对应起来否则可能出现缓冲区申请失败然后初始化又悄悄失败。这个问题隐藏得很深我建议在初始化流程里加一层返回值检查每一步rk_mpi调用都要判断返回值并打印错误码不要一错到底。初始化阶段的报错不可怕可怕的是它不直接报错而是留到数据流阶段以丢帧的方式暴露出来。3. 数据流处理实战取帧、缓存与释放3.1 bind模式把数据流交还给框架bind模式下VI通道不需要业务线程去主动取帧。你只需要把VI通道当作源端把VPSS或VENC当作目的端调一次rk_mpi_sys_bind内部就会自动把数据搬运过去。这个模式的好处是代码量少、时序不容易乱系统内部会用独立线程处理帧的流转。坏处是你拿不到帧内容也没法对帧做自定义修改。所以这个模式适合采集之后要做编码、推流、存储的纯视频链路。如果你用的是实时视频流场景bind模式是非常省心的选择。我在使用bind模式时会额外注意两点一是源端和目的端的格式、分辨率要匹配否则VPSS接收后还要做不必要的转换浪费带宽二是绑定后如果要修改参数比如切换分辨率一定先unbind、disable通道、改属性、enable、再重新bind。直接热改属性在部分版本下会导致底层状态机错乱后面取帧或者编码就异常。这个经验我在线升级项目里反复遇到过改成标准流程后就再没出过事。前期多写几行标准流程后期能省掉很多线上问题。3.2 取帧模式标准循环与释放时机取帧模式是很多AI项目的主战场。标准的取帧循环其实不长设置一个帧超时时间调用rk_mpi_vi_get_chn_frame从VI通道拿一帧拿到后做业务处理或直接送推理处理完调用rk_mpi_vi_release_chn_frame把帧还回去。为什么必须还因为VI通道内部是可复用的环形缓冲帧不归还缓冲池很快被占满后续sensor新来的帧无处安放只能丢弃。现实中的表现很典型程序刚启动正常跑了十几分钟后画面开始卡顿再过一会儿取帧直接超时一看内存占用也没怎么涨其实是被你自己耗死的。释放时机也是细节。你拿到帧指针后如果只是读像素内容应该尽快release如果你要异步处理比如扔给另外一个线程做算法那帧就不能立刻release得等处理完。但别一直攥着不放尤其是实时视频流场景下每个buffer都被占用了整个pipeline都会停下来。对于这种情况我一般会在应用层做一次浅拷贝只拷贝图像数据到自己的buffer随后立即release VI帧从而把VI通道的缓冲占用时间压缩到最短。VI_FRAME_INFO_S stFrame; rk_s32 s32Ret; while (g_bRunning) { // 100ms超时等待 s32Ret rk_mpi_vi_get_chn_frame(0, 0, stFrame, 100); if (s32Ret RK_SUCCESS) { // 业务处理推理/抓拍/显示等 ProcessFrame(stFrame); // 处理完立即释放 rk_mpi_vi_release_chn_frame(0, 0, stFrame); } else { // 超时,统计一下 stats.timeouts; } }注意这里的100毫秒超时不是随便定的它应该大于一帧间隔。30fps的sensor每帧间隔约33ms超时给100ms意味着最多容忍约3帧的空窗如果你给10ms线程会频繁被超时打断CPU空转白白浪费。反过来给太大比如1000mssensor真的挂了你也难以及时发现。我用的是50到200ms区间具体看场景对延迟的容忍度。处理帧的线程栈大小也要给足有些图像处理库临时变量硕大栈太小容易溢出而且是在运行一段时间后才崩溃很难查。3.3 帧率波动、丢帧与线程模型数据流跑起来之后最常遇到的问题就是帧率不达标或者偶尔丢帧。先说一个容易忽略的点VI取帧线程不是一个独立的定时器它的节奏完全由sensor输出帧率驱动。如果sensor那边帧率本身不稳定比如没有锁定在30fps而是上下浮动VI侧能做的只是尽量保持不丢帧不会帮你补帧。所以排查帧率问题时第一步永远是用示波器或者查询接口确认sensor的实际输出帧率别一上来就在VI侧找问题。很多所谓VI丢帧其实是sensor源头就不稳定。丢帧通常有两个来源一种是物理链路丢帧比如MIPI传输信号质量差、带宽不足、buffer分配不够另一种是应用层没及时取帧或释放帧导致缓冲被占满。怎么区分Rockit的通道状态查询接口通常会返回一些计数包括已采集帧数、已丢帧数、当前缓冲占用数。如果已采集帧数一直增长、已丢帧数也在涨多半是物理链路或buffer不够如果已采集帧数都不涨那就是sensor或PHY链路的问题。下面是我整理的一张排查对照表实际用下来效率不错。现象可能原因排查动作取帧超时缓冲被占满、后级release不及时检查release时机统计超时次数已采集帧数不增长MIPI链路没起来、sensor无数据查PHY状态、sensor输出时钟丢帧计数持续增长buffer数量不足、总线带宽不够调大buffer数量或降分辨率花屏/画面错位lane配置错误、时序信号异常核对DTS lane数/极性、PHY状态线程模型方面取帧模式我强烈推荐单线程消费。也就是说同一时刻只有一个线程在get/release同一个VI通道。Rockit部分版本对并发get/release并不友好多线程抢帧轻则导致帧顺序错乱重则直接卡死在驱动里。如果你确实需要多消费者正确做法是在应用层自己再做一层分发一个线程从VI取帧收到帧后按业务需求复制多份或分发给不同队列而不是让多个线程都去调rk_mpi_vi_get_chn_frame。这个设计能极大规避底层并发带来的不确定性虽然多一次拷贝但换来的是稳定。4. 常见问题与排查技巧实录4.1 初始化失败读出固定寄存器和PLL不锁这类信号做视频输入开发少不了一类和硬件强相关的问题初始化时通过I2C去读外部芯片的寄存器读出来的值永远是同一个固定数。在VI场景里这通常发生在sensor、HDMI转接芯片、或者某些模拟前端芯片上。你反复改寄存器地址、改I2C速率都没有用因为问题根本不在寄存器配置。我自己调过不止一种带配置寄存器的前端出现过某个地址的寄存器永远读同一个固定值、接收端PLL始终不锁的现象最后定位到的原因基本集中在三处芯片电源没有正确上电、外部时钟没起来、I2C地址写错或者sensor的reset引脚一直处于复位态。这类问题的排查顺序不要上来就改驱动代码。先量电源域确认各路供电在规格要求范围内再示波器量MCLK或者参考时钟频率和幅度要达标接着核对I2C地址特别是8位和7位地址的转换很多sensor手册给的是8位地址你在驱动里写的是7位就会差一位导致读写无响应最后检查reset和power down引脚电平。当这些硬件前提都确认了寄存器再读出固定值那才轮得到怀疑软件配置或芯片本身。这个排查思路在VI初始化上完全通用MIPI的sensor这样查其他带I2C配置的输入芯片也一样。另一个高频信号是接收端PLL没有锁定。在MIPI CSI链路里PLL失锁说明接收端没有检测到稳定连续的时钟信号。大概率是sensor那边还没开始输出、或者MIPI clock lane的通道号配置和DTS对不上、或者lane polarity正好反了。排查手段仍然是从信号源头往接收端逐级确认确认sensor寄存器进入视频输出模式DTS中lane映射和实际PCB走线逐一核对PHY配置里的lane交换开关是否打开。VI初始化阶段出现的PLL问题十有八九是配置顺序和映射错位不是芯片本身坏了。遇到这种问题别慌一级一级量信号原因很快就浮出来。4.2 数据流阶段的高频故障帧率异常、花屏、卡帧处理完初始化数据流阶段也有自己的一套高频故障。帧率异常最典型的场景实况输出只有设计帧率的一半比如配置30fps结果稳定在15fps。这种整数除二的降频通常不是随机丢帧而是缓冲数量严重不足导致每两帧丢一帧或者sensor配置里工作模式就是15fps但应用侧还在按30fps去取。前者通过调大buffer数量能解决后者需要回到sensor配置确认驱动里设置的帧率枚举值和sensor实际寄存器值一致。我遇到过一个项目应用层按30fps上报但sensor实际上被写成了15fps查了一个下午才发现。花屏和画面错位问题几乎是MIPI链路误码或者配置不匹配的标志。常见原因包括lane数量配置多了或少了、clock lane和data lane的极性反了、sensor输出的数据位宽和VI输入数据位宽不一致、像素格式里色彩空间和位深不匹配。排查时先按只看灰度的方法排除颜色空间问题再把lane数量从多到少逐个切换配合驱动打印错误计数基本能定位到具体配置项。这里要特别提醒直接对着一个花屏画面去猜驱动代码效率很低务必先把PHY和MIPI接收端的状态计数和错误计数打出来它们会指出是哪个lane出现了CRC或ECC错误。卡帧和取帧超时在业务侧感知非常明显。除了之前说的release不及时还有一类是系统内存低导致的buffer申请失败。嵌入式设备上同时跑编码、推理、显示内存本身紧张VI在取帧过程中申请新buffer失败时不会主动告诉你只是在状态查询里把错误计数加一。我的经验是在这种大负载场景下VI通道的buffer数量要按后端最大耗时来评估而不是按平均耗时宁多勿少优先保证不丢帧。等到整体系统稳定了再去一点点优化内存这种收着来的方式最稳。4.3 快速定位三板斧日志、统计、最小化复现遇到VI问题我基本按三板斧来推进。第一板斧是把Rockit相关模块日志级别调到DEBUG驱动和中间件会打印具体的失败节点和错误码很多问题在这一步就能看到是PHY配置失败还是帧缓冲申请失败。第二板斧是写一个小的状态查询工具周期性打印VI通道状态里的已采集帧数、已丢帧数、缓冲占用数和错误计数不要等崩了再去看要在复现过程中实时观察数据变化。第三板斧是构造最小化复现环境只保留一个sensor、一个VI通道关掉所有后级模块单线程取帧把所有非必要路径砍到最简。这三板斧听着简单但能过滤掉绝大部分看似复杂、实际定位简单的问题。比如我遇到过多路启动后某一路画面不出的问题一开始怀疑是性能问题后来按最小化复现逐步加回其他路才发现是某一处在初始化时共用了同一个buffer group导致两路互相覆盖。这种问题如果不做减法光看代码很难发现。另外排查问题过程中我强烈建议保留现场记录当时改了哪个DTS节点、哪个结构体字段前后现象是什么。VI相关的问题往往依赖时序和状态记录不完整的话稍微绕一下就会回到原路。最后补充一个开发环境层面的经验。每次新开一个调试终端记得把动态库路径、日志级别这些初始化配置固定到脚本里。我见过太多同事因为环境变量没配对拿着一个旧库在反复排查最后发现跑的版本根本不是自己刚编译的那个。把环境配置脚本化是节省调试时间最划算的一笔投入。开发环境初始化配置这件事看起来不疼不痒但它直接影响你后续所有VI调试的效率。最后再分享一个我个人的操作习惯。每调完一套VI链路我都会把当时用过的一整套参数组合完整记录下来sensor型号、DTS节点状态、VI设备属性、通道属性、buffer数量、取帧超时、线程栈大小存成一个独立配置文件。下次换平台、换板子直接拿配置和新的参考代码逐项对比改动点一目了然。VI模块的调试最怕的就是“感觉改了这个好了”没有记录就没有复现能力也就没有真正稳定的系统。祝大家在Rockit的VI模块上少踩坑多出片。
返回列表