资讯详情

资讯详情

RK3588视频开发实战:SDK构建与MPP硬编码集成指南

先说个结论RK3588这个平台做嵌入式的视频处理、边缘AI、多路推流类项目时几乎绕不开SDK和MPP这两个词。我前后碰过不少3588的板子从拿到开发板到把视频流稳定跑起来中间最烦的其实不是NPU怎么调也不是内核怎么改而是SDK体系和硬件编解码库MPP之间的那座桥。很多人卡在这个环节明明板子能开机摄像头也能出图可一旦要硬编码H.264/H.265、做低延迟RTSP推流就一头雾水。这篇文章我会把我实际走过的路径完整拆出来从SDK构建、MPP接口原理到把MPP硬编码模块集成进自己的视频应用再到常见问题排查尽量给出一套可以照着做的方案。适合正在搞RK3588开发、尤其是刚接触嵌入式Linux视频链路的朋友。1. 项目拆解为什么RK3588的SDK和MPP是绕不开的两块基石1.1 一个典型项目的真实需求假设你现在接到这样一个需求一块RK3588开发板接一个USB摄像头或者MIPI摄像头要把画面编码成H.264或者H.265然后通过RTSP或者WebRTC推出去同时还要跑一个人脸检测模型。这个需求放在PC上很简单但放到RK3588上就完全是另一套玩法。首先CPU不能一直拿来软编码RK3588虽然有8个核心其中4个还是A76大核但如果1080p30的视频流直接用x264软编码CPU占用能吃掉两三个核心NPU那边还要做人脸检测整个系统很快就变得卡顿。正确做法是把编码工作交给硬件VPU让RK3588内置的视频编解码单元去干活CPU只负责取流、转发、控制逻辑。这里就引出了两个核心概念一个是SDK另一个是MPP。SDK解决的是“让板子能够稳定运行一套操作系统和底层驱动”的问题它包含了U-Boot、Kernel、文件系统、板级配置、各种外设驱动是整个开发的地基。MPP则是Rockchip提供的多媒体处理平台用户态库全称Media Process Platform专门用来向应用程序开放硬件视频编解码能力。1.2 SDK与MPP的职责边界很多刚开始接触RK3588的朋友会把这两者搞混觉得SDK就是一堆编译脚本MPP就是某个API库。我的理解是SDK是集合MPP是集合里的关键组件。SDK作用范围更大它决定你用什么内核、什么设备树、什么根文件系统甚至包括如何把GPU驱动、NPU驱动、视频编解码驱动一起打进去。MPP则属于应用层和内核驱动之间的中间层它通过调用内核里的mpp_service、vpu_service、rga等驱动把硬件能力暴露成一套相对统一的C语言接口。从职责边界来看做嵌入式开发时如果只想业务层调用编码器你完全不去动SDK的构建流程只要在编译自己应用时链接到librkmp.so就行。但如果想让MPP跑得顺畅比如要用到DMA-BUF、ION内存、多路并发那你必须对SDK有什么能力、内核里开了哪些配置有清晰认知。所以这两者不是二选一而是串联的关系。我见过不少项目应用层用OpenCV或者FFmpeg去软编码结果板子一热就掉帧最后才发现是没把硬编能力用起来。也有项目只用GStreamer插件对底层MPP一无所知一旦插件不满足定制需求就彻底卡住。从零到一的过程其实就是在SDK基础上把MPP这套硬编码/硬解码能力训练成自己的基本功。2. 从拿板到能编译SDK环境搭建的几个关键步骤2.1 SDK获取、目录结构与板级配置做RK3588开发第一步是拿到一份可以构建的SDK。Rockchip本身有官方Linux SDK但很多第三方板卡厂商也会在官方基础上二次封装提供自己适配过的版本。早期我拿到手的SDK大多是厂商给的网盘压缩包或者通过repo同步的git仓库。不要小看这一步SDK版本不同后续MPP版本、内核版本、设备树名称都会有差异尽量选择和你手里的板子一致或者相近的版本。SDK解压后常见目录结构大致是这样的u-boot、kernel、buildroot、debian、app、device、rockdev、docs。其中device目录下会按芯片型号分目录里面是各种板级配置文件比如DeviceConfig.mk、BoardConfig.mk里面定义了镜像分区、内核配置、文件系统类型等。kernel目录下一套完整的内核源码设备树文件一般在kernel/arch/arm64/boot/dts/rockchip/下文件名类似rk3588-evb1-lp4-v10.dts、rk3588s-evb.dts这些。构建之前先要确认板级配置。打开device/rockchip/rk3588/下的BoardConfig文件重点看是否有正确的RK_CHIP、RK_TARGET_PRODUCT、RK_ROOTFS_SYSTEM等变量。有的SDK用lunch机制需要运行./build.sh lunch然后在交互菜单中选择自己的板卡配置。我第一次构建时就犯过懒直接执行./build.sh结果编译出来的内核设备树和我手上的底板对不上烧进去后网口和HDMI全部不工作后来才发现是因为没有先选板级。2.2 生成固件U-Boot、Kernel与Rootfs一次说清整体镜像构建流程可以拆成四个并列的部分U-Boot引导、内核、根文件系统、打包操作。构建U-Boot时SDK里的脚本会自动根据板级配置选择对应的defconfig。RK3588的U-Boot一般用rk3588_defconfig然后make最终生成u-boot.itb等文件。内核构建相对简单先使用rockchip_linux_defconfig作为基准配置然后通过make ARCHarm64 rk3588_evb_defconfig或者自定义裁剪。这里要特别留意内核配置里是否打开了mpp service相关模块比如CONFIG_ROCKCHIP_MPP_SERVICE、CONFIG_ROCKCHIP_MPP_RKVDEC、CONFIG_ROCKCHIP_MPP_RKVENC这些。如果没打开后面MPP再怎么写代码也无法工作因为内核根本不提供硬件编解码节点。根文件系统则是另一块大头。RK3588 SDK既可以构建Buildroot也可以直接构建Debian/Ubuntu根文件系统。我个人的观点是如果只是纯业务开发、对系统裁剪要求不高直接用Debian或者Ubuntu的rootfs包整体效率更高因为你可以用apt安装很多依赖库。如果要做量产固件、希望控制体积和安全基线再考虑Buildroot。在SDK里构建Buildroot通常运行./build.sh buildrootDebian则是./build.sh debian最后通过./build.sh updateimg把各个部分打包成统一的update.img。把U-Boot、Kernel、Rootfs三块串起来后首次烧录时可以通过开发板提供的烧录工具(如RKDevTool)把镜像刷进去。注意一点不同开发板的烧录模式不同但都基于Loader parameter分区方案如果你改动了根文件系统大小或者分区信息烧录时也要同步调整parameter.txt。2.3 MPP内联模块与独立编译两种玩法在SDK里MPP通常被放在external或者media目录下也可能以子模块形式存在。如果你跑的是官方完整SDK编译系统会把它一起编译并将生成的库放到rootfs的/usr/lib目录下比如librkmp.so。这种方式我称为“内联模块”好处是省心整个固件构建完开发板上自然就带了MPP环境。但另一种场景更常见你的SDK比较老或者你用的板厂SDK把MPP裁剪掉了又或者你需要在PC上交叉编译某个独立应用然后再放到板上运行。这时候就要单独拉取MPP源码。MPP在GitHub上有Rockchip官方镜像可以直接用git或者repo拉取。拿到源码后MPP支持使用CMake构建。交叉编译时指定工具链为aarch64-linux-gnu-gcc再设置一个安装目录编译后得到头文件和librkmp.so。整个过程不复杂但要注意MPP版本必须和内核驱动的匹配度比如新版MPP可能要求内核中有dma-heap支持如果你内核还是老的ION方案就会出现运行时找不到分配器之类的问题。所以我的建议是前期开发时优先跟随SDK集成好的MPP版本不要在初次阶段就想着自己更新MPP源码到最新版。除非你确定自己需要某个新特性比如AV1硬件解码、新的编码质量策略否则SDK内置版本往往是最匹配内核驱动的。3. MPP核心机制与接口设计不只是会调用就行3.1 用户态API与内核驱动的对应关系MPP的架构可以理解为三层。最底层是内核里的硬件驱动RK3588上有mpp_service这个驱动节点负责管理VPU硬件单元中间是MPP的会话调度层负责把用户态请求转换成对底层驱动的命令最上层才是我们应用里直接调用的C接口也就是rk_mpi.h里暴露出来的一套API。很多教程会直接把mpp_create、mpp_init、mpp_destroy抛出来告诉我们照着写就行。但实际开发中如果不理解这层对应关系碰到疑难问题时很难下手。比如当调用mpp_init(ctx, MPP_CTX_DEC, MPP_VIDEO_CodingAVC)时它其实是在内核mpp_service里创建一个解码会话并绑定到H.264解码硬件单元。如果硬件资源被其他会话占用初始化会不成功这就像一台机器上同时开太多编码任务资源不够。这种问题单纯看API文档是理解不了的你需要知道硬件单元数量是有限的RK3588虽然支持8K解码、多路4K编码但并发路数也有上限。MPP的API风格接近面向对象的会话模型。核心对象是MppCtx它是编解码会话的句柄。每个会话还绑定了一个MppApi结构体里面是一系列函数指针比如decode_put_packet、decode_get_frame、encode_put_frame、encode_get_packet等。之所以这样设计是因为不同硬件平台、不同编解码器底层实现可以不同但上层接口可以保持统一。3.2 解码链路的调用顺序解码是MPP最基础也最常用的功能。不管你是播放本地文件还是接收网络流解码流程基本都是往解码器里塞压缩数据包(MppPacket)然后从解码器里取解码帧(MppFrame)最后处理帧数据。具体调用顺序通常是先mpp_create创建会话。再mpp_init初始化指定方向是解码(MPP_CTX_DEC)指定编码格式例如H.264是MPP_VIDEO_CodingAVCH.265是MPP_VIDEO_CodingHEVC。然后通过mpi-control设置一些控制参数比如是否使用阻塞模式。进入循环mpi-decode_put_packet送入压缩码流mpi-poll等待硬件处理完成mpi-decode_get_frame取出解码后的帧。这里有一个重点poll的含义是轮询硬件是否完成了对当前输入包的处理。如果不调用poll直接反复塞包缓冲区很快会被塞满然后返回错误。正确的姿势是塞一个包poll成功取一帧再塞下一个。这样可以保持硬件流水线高水位运转又不会把队列打爆。解码出来的帧如果后续要显示一般直接提交给DRM/KMS显示接口如果要送去做NPU推理一般转成RGB或者保留NV12然后通过共享内存或者DMA-BUF方式传递给RKNN。这里要提醒这里的解码帧是硬件分配的buffer可能不是连续内存你用普通指针遍历的时候要注意stride对齐的问题不能简单用width * channels去计算偏移。3.3 编码链路与帧缓冲管理编码链路恰恰是解码链路的镜像。它的输入是未压缩的图像数据比如NV12格式的YUV帧输出是H.264/H.265码流。我用过的MPP编码一般步骤是mpp_creatempp_init(ctx, MPP_CTX_ENC, MPP_VIDEO_CodingAVC)。设置编码参数。这里可以走结构体方式也可以使用MPP提供的配置模型接口。核心参数包括分辨率、帧率、GOP大小、码率、QP范围、编码级别等等。分配帧缓冲组(MppBufferGroup)和帧对象(MppFrame)。这一步很关键因为编码器需要一个帧缓冲池来循环存放待编码帧。进入编码循环mpi-encode_put_frame送入帧然后通过mpi-encode_get_packet取出编码后的码流包。帧缓冲的分配和管理是编码链路里最容易出问题的地方。RK3588做硬件编码时建议使用连续物理内存或者DMA-BUF而不是普通的malloc。原因很直白VPU通过DMA读取数据如果地址不连续硬件会频繁做页表映射性能差很多。MPP里可以通过mpp_buffer_group_get_internal创建一个内部buffer group并设置group大小和数量上限。例如4路1080p编码每路需要若干帧的输入缓冲那么每个group可以按一个合理的帧数去限制避免内存膨胀。编码参数的设置也直接影响码流质量。比如H.264编码时我们需要为SPS/PPS设置Header模式通常有两种一种是每个IDR帧前都带SPS/PPS适合实时流另一种是只在流开始处输出一次适合文件存储。常规实时推流场景我一般选择MPP_ENC_HEADER_MODE_EACH_IDR这样即使播放端中途加入也能很快收到关键帧头信息。4. 实操落地把MPP硬编码集成进自己的视频应用4.1 最小工程骨架与链接方式理论说再多不如一段能编译的代码。下面我给出一个最简可用的MPP硬编码流程骨架这个骨架我经常拿来当模板。先看头文件和链接方式。在CMake里需要把头文件路径指向MPP的include目录把库文件指向librkmp.so。简单情况下可以这样include_directories(/path/to/mpp/include/rockchip) add_executable(my_encoder main.cpp) target_link_libraries(my_encoder rockchip_mpp)如果你是在板子上直接编译MPP头文件一般在/usr/include/rockchip下库文件在/usr/lib下直接gcc main.c -lrockchip_mpp都能编过。然后是最小编码初始化流程#include rockchip/rk_mpi.h #include rockchip/mpp_buffer.h #include rockchip/mpp_enc_cfg.h static MppCtx ctx NULL; static MppApi *mpi NULL; static int init_encoder(int width, int height, int fps, int bitrate) { MPP_RET ret; MppEncCfg cfg; 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; } 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_YUV420SP); mpp_enc_cfg_set_s32(cfg, codec:type, MPP_VIDEO_CodingAVC); mpp_enc_cfg_set_s32(cfg, codec:gop, fps * 2); mpp_enc_cfg_set_s32(cfg, codec:rc:mode, MPP_ENC_RC_MODE_CBR); mpp_enc_cfg_set_s32(cfg, codec:rc:bps, bitrate); mpp_enc_cfg_set_s32(cfg, codec:rc:bps_max, bitrate * 17 / 16); mpp_enc_cfg_set_s32(cfg, codec:rc:bps_min, bitrate * 15 / 16); mpi-control(ctx, MPP_ENC_SET_CFG, cfg); mpp_enc_cfg_deinit(cfg); return 0; }注意这个示例里我用了mpp_enc_cfg_set_s32这套配置接口它是MPP较新版本推荐的用法比直接填充一个巨大的结构体要直观。prep:format我设成了MPP_FMT_YUV420SP也就是NV12半平面格式这也是接入摄像头采集时最常见的硬件输出格式之一。4.2 打通V4L2/RGA到MPP编码的数据管道初始化编码器只是万里长征第一步真正的挑战在喂帧环节。实际项目中视频帧通常不是直接从内存里凭空出现的而是来自V4L2摄像头采集或者来自RGA图像处理单元。先说V4L2采集。大多数嵌入式摄像头通过V4L2接口输出YUYV或NV12格式。如果是NV12格式而且分辨率、stride对齐没问题那几乎可以直接送给MPP编码器不需要额外转换。如果是YUYV或者其他Packed格式MPP编码器不一定直接支持这时候一般要用RGA做格式转换。RGA是Rockchip的2D图形加速器可以在硬件层面做色彩空间转换、缩放、旋转。在SDK里RGA对应的库是librga.so使用方式比MPP更简单核心是rga_info_t结构体设置源图、目标图、格式、旋转角度等参数然后调用RGA接口。我踩过的一个坑是V4L2输出缓冲的物理地址问题。V4L2默认分配的buffer如果你走mmap方式拿用户态地址再把这块用户态地址直接填到MPP的输入帧里很可能效率极低甚至出现硬件无法访问的问题。更好的做法是让V4L2使用DMA-BUF导出机制或者让RGA在硬件层直接处理源buffer和目标buffer避免CPU拷贝。MPP的buffer可以用mpp_buffer_import从外部导入DMA-BUF这样可以实现零拷贝。如果实在没有条件用DMA-BUF再用memcpy把数据拷进MPP内部分配的内存性能会差一些但功能上至少是通的。摄像头数据送到MPP编码器之前还要做一次MppFrame构造。大致代码如下static int feed_frame(MppBuffer buf, int width, int height, int64_t pts) { MppFrame frame NULL; mpp_frame_init(frame); mpp_frame_set_width(frame, width); mpp_frame_set_height(frame, height); mpp_frame_set_fmt(frame, MPP_FMT_YUV420SP); mpp_frame_set_buffer(frame, buf); mpp_frame_set_pts(frame, pts); mpi-encode_put_frame(ctx, frame); mpp_frame_deinit(frame); return 0; }这里MppBuffer的来源既可以是mpp_buffer_get从MPP内部group申请也可以是从外部导入的DMA-BUF。取编码后的码流包则这样MppPacket packet NULL; MPP_RET 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); // 把ptr写入文件或者通过RTSP发送 mpp_packet_deinit(packet); }4.3 验证结果与性能观测把代码串起来后先别急着搞复杂业务先在开发板上跑一个最基础的验证。我通常会先写一个测试程序从文件里读取NV12裸流硬编码成H.264文件然后用ffprobe查看信息。如果输出文件能被ffprobe正确识别分辨率和帧率都正确说明MPP编码链路已经通了。接下来再接摄像头做实时流验证。性能观测方面可以用mpstat看CPU占用正常情况下同一路1080p30编码CPU占用应当非常低。如果CPU占用异常高大概率是数据在某个环节走了memcpy或者转换需要把拷贝优化掉。另外要检查编码延迟。RK3588硬编码器的延迟通常很低在Video模式下达几十毫秒量级对实时通信足够用。如果发现延迟很大检查是不是排队帧数太多。encode_put_frame和encode_get_packet之间不要累积太多帧否则就是软件把延迟做大了。5. 踩坑实录SDK与MPP集成中最常见的五个问题5.1 编码初始化失败日志一片空白我遇到过好多次MPP初始化失败但程序又不会报什么具体错误最多打一行mpp_init failed的情况。这种问题多半不是代码写错而是系统环境不对。排查顺序从内核节点开始。先看看板上是否存在/dev/mpp_service节点如果不存在基本可以断定内核里的MPP驱动没加载或没编译进去。再去检查dmesg看看有没有mpp_service相关的报错。常见原因包括设备树里VPU的power domain配置缺失比如供电没开启或者firmware文件没放对地方。RK3588的VPU有时需要额外的固件文件如果rootfs里缺少这些文件硬件单元就会加载失败。另一种情况是MPP库和内核驱动的版本不匹配。比如内核是旧SDK的MPP库是新版调用新接口时底层驱动根本不识别。解决方法是尽量使用SDK内联编译的MPP版本不要随意从外部拉最新源码替换。5.2 解码卡在poll返回超时解码时最常见的问题是调用poll一直拿不到成功状态或者返回超时。我自己的经验是先检查是不是输入数据本身不完整比如H.264码流里没有关键帧解码器无法开始工作。很多IPC摄像头或者文件流一开始不是IDR帧就会出现这种情况。解决方法是确保送入解码器的第一包是包含SPS/PPS和IDR的数据或者在开始解码前先把码流从头扫一遍。如果输入数据没问题那就是硬件解复用资源被占用。RK3588支持同时多路解码但如果你已经把解码器都用在别的会话上了再启一路就会阻塞。这时候用mpp_dec_test这类命令行工具先验一下硬件是否正常可能会发现只要自己程序不放MPP测试工具跑得很顺那就说明是程序里的并发会话冲突了。另外还要看看是不是没有及时释放解码帧。从decode_get_frame拿到的MppFrame如果一直不释放内部buffer pool会被耗尽后续解码就会卡死。正确的用法是处理完帧数据后立刻调用mpp_frame_deinit(frame)把帧对象交回MPP。5.3 花屏、绿屏与格式不对齐MPP和V4L2/RGA集成时花屏绿屏是非常常见的问题。首要怀疑对象是stride对齐。硬件编码时图像的width中width必须是16字节对齐的stride可能大于width。如果你把width1920的NV12数据直接按1920的stride塞给MPP编码器可能去读取硬件要求的stride比如1920本来刚好是16对齐的大多数情况没问题。但如果是320x240这种非典型分辨率stride不对齐就会导致图像在每行结束处错位编码出来就是花屏或撕裂。另一个常见问题是色彩空间不匹配。如果摄像头输出是BT601的YUV而MPP假设成BT709编码出来的画质会偏色发绿。对于不太较真的场景肉眼可能还能接受但想做正规产品就必须在采集端和编码端把色彩空间配一致。还有一个小坑是半平面和打包格式的混用。NV12是半平面Y数据区和UV数据区是分开的YUYV是打包格式Y和UV交错排列。把YUYV的buffer直接当作NV12塞给MPP出来的画面大概率是诡异的条纹。这种问题用眼睛看代码很难发现因为我遇到过太多次最后用xxd打印前几个字节一眼就看出数据排列完全不对。5.4 编码帧率上不去内存分配和排队帧率上不去这个问题在未使用DMA-BUF时最常见。摄像头采集到的buffer本来在硬件缓冲区里如果你用mmap拿到用户态指针再把数据memcpy到MPP输入buffer一路1080p30的数据拷贝就会吃掉不少CPU带宽。优化方案是在V4L2和MPP之间走RGA零拷贝或者干脆让摄像头直接输出到DRM/DMA-BUF buffer。还有一种情况是编码器内部的帧组数量设得太小。如果mpp_buffer_group_limit_config里把缓冲数量限制成2编码器要同时缓冲当前帧、参考帧、临时数据可能因为buffer不足而等待导致帧率波动。我的做法是至少给一个编码通道8到10帧的缓冲上限当然具体要看板子的内存和项目对延迟的要求。缓冲越多越容易稳定帧率但延迟也会增加要平衡。5.5 多路并发与系统稳定性最后说多路并发。RK3588的优势之一就是多路编解码比如4路1080p30编码或者8路1080p30解码但并发不是简单地创建多个会话就能稳定工作的。多路并发时需要为每个会话规划独立的内存预算。MPP的buffer group不是无限分配的你用多少就得在系统内存里预留多少。4路1080p编码每路假设需要4个NV12帧的缓冲一个1080p NV12帧大约是192010801.5约3MB4路就需要48MB再加上其他开销看起来不多但如果你在跑NPU推理时RKNN也占着大块内存系统内存耗尽的风险是真实存在的。再一个是调度优先级。RK3588的VPU硬件在多个会话同时忙时会出现排队某些会话的编码延迟会明显增大。我在实际项目中做过一次简单的优先级控制把实时性要求最高的会话设为低延迟模式其他业务型会话使用默认模式效果还不错。具体方法就是通过MPP的配置接口调整每个会话的超时时间和排队策略。多路并发出问题时优先看内存和系统负载。用free -m看可用内存用cat /proc/meminfo看DMA内存占用用top看CPU负载。很多看起来像MPP本身的bug最后发现都是系统内存耗尽或者中断风暴。最后再分享一个小技巧如果你跟我一样需要在应用层调试MPP建议在工程里保留一个环境变量开关用来输出MPP内部日志。MPP本身支持通过环境变量打开调试打印比如设置mpp_debug相关的变量后运行程序时就能看到硬件编解码时每一步的状态。这个习惯帮我排查过很多诡异的初始化失败问题比不断改代码加printf高效得多。另外刚拿到板子时先用SDK里自带的mpp_enc_test和mpp_dec_test工具把硬件整条链路验证一遍再去写自己的应用。这样才能分清问题到底是在硬件还是在自己写的代码里。希望这篇内容对你正在做的RK3588项目有帮助少走点弯路。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →