ARTICLE DETAIL

资讯详情

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

RKMPP硬编硬解实战:从交叉编译到性能调优的完整链路

RKMPP硬编硬解实战:从交叉编译到性能调优的完整链路 RKMPP 这个名字凡是做过瑞芯微平台音视频开发的基本都绕不开。它是 Rockchip 平台上的硬件编解码媒体处理框架向上对接 FFmpeg、GStreamer 这类常用多媒体框架向下直接调用芯片里的 VPU视频处理单元做 H.264/H.265 的硬编硬解。我第一次在 RK3588 上跑通 RKMPP 的时候最大的感受是这东西性能确实猛但环境搭建和参数调优的坑也是真不少。从交叉编译工具链的版本匹配到运行时的缓冲区配置再到多路并发时的性能瓶颈定位每一步都有讲究。这篇内容我打算把 RKMPP 从零到跑通、再到性能调优的完整链路梳理一遍。适合正在用 RK3568、RK3588、RV1106 这类瑞芯微芯片做音视频产品的开发者也适合刚接触嵌入式交叉编译、想搞清楚 MPP 框架到底怎么用的人。我会尽量把每个环节为什么这么做讲清楚而不是只丢一堆命令让你照抄。毕竟嵌入式这行环境千差万别理解原理比记住步骤重要得多。1. 先搞清楚 RKMPP 到底解决什么问题1.1 软编软解和硬编硬解的本质差异很多人刚接触音视频开发时习惯用 FFmpeg 自带的 libx264 做软编码。在 PC 上跑没问题但一旦搬到嵌入式板子上CPU 算力根本扛不住。以 RK3588 为例如果用软编跑 1080p30fps 的 H.264CPU 占用率轻松飙到 400% 以上多核累加而且帧率还不稳定。原因很简单视频编码本质上是大量矩阵运算和运动估计这些活儿让通用 CPU 干效率极低。硬编硬解的思路完全不同。瑞芯微芯片内部集成了专门的 VPU 硬件模块这个模块的电路就是为视频编解码设计的做 DCT 变换、运动补偿这些操作是天生就会。CPU 只需要把码流或原始帧丢给 VPU剩下的活儿硬件自己干完CPU 占用率能降到个位数。RKMPP 就是这套硬件能力的软件封装层它把 VPU 的寄存器操作、DMA 传输、中断处理这些底层细节封装成一套 C 接口让上层应用不用直接跟硬件打交道。这里有个关键点要理解RKMPP 不是 FFmpeg 的替代品它更像是 FFmpeg 的一个硬件加速后端。你在 FFmpeg 里用h264_rkmpp这个解码器底层实际调用的就是 RKMPP 的接口。所以整个调用链路是应用层 → FFmpeg → RKMPP → VPU 驱动 → 硬件。1.2 MPP 框架的模块划分RKMPP 的代码结构其实挺清晰的主要分这么几块mpp_dec / mpp_enc解码器和编码器的核心逻辑负责码流解析、帧管理、码率控制。mpp_hal硬件抽象层不同芯片RK3568、RK3588、RV1106的 VPU 寄存器操作差异都在这一层屏蔽掉。mpp_buf缓冲区管理处理 DMA 内存分配、物理地址映射、缓存一致性。osal操作系统抽象层把 Linux 的线程、锁、内存操作封装起来方便移植。理解这个分层很重要因为后面排查问题时你得知道错误是出在应用层参数配置、还是 HAL 层硬件交互。比如解码花屏可能是码流本身有问题应用层也可能是 DMA 缓冲区没对齐buf 层还可能是 VPU 固件版本不匹配HAL 层。1.3 什么场景下必须用 RKMPP不是所有场景都值得上 RKMPP。如果你的应用只是偶尔解一两帧图片用软解完全够用引入 RKMPP 反而增加复杂度。但以下几种情况基本没有别的选择第一多路视频实时解码。比如 NVR 设备要同时解 8 路 1080p软解根本跑不动必须靠 VPU 并行处理。第二高分辨率编码。4K 甚至 8K 的编码CPU 软编的延迟和功耗都无法接受。第三低功耗场景。RV1106 这种用在电池供电设备上的芯片CPU 性能弱但 VPU 能效比极高硬编硬解是唯一可行方案。提示判断要不要用 RKMPP先看你的分辨率和路数。单路 720p 以下、帧率不高的场景软解可能更省事。2. 交叉编译环境的搭建与工具链选择2.1 为什么交叉编译是第一个拦路虎嵌入式开发和 PC 开发最大的区别就是编译环境和运行环境是分开的。你的开发机是 x86 架构的 Ubuntu但目标板子是 ARM 架构的 RK3588两者指令集完全不同。交叉编译就是在一台机器上生成另一台机器能跑的代码这中间的工具链配置、库依赖、头文件路径任何一个环节出错都会导致编译失败或者运行时崩溃。我见过太多人卡在这一步编译出来的二进制文件拷到板子上一运行就报 No such file or directory其实不是文件不存在而是动态链接器路径不对或者依赖的 so 库版本不匹配。这类问题的根源往往是交叉编译时用的工具链和板子上的运行环境不一致。2.2 工具链的获取与验证瑞芯微官方推荐用他们 SDK 里自带的工具链通常在prebuilts/gcc/linux-x86/aarch64/目录下。以 RK3588 为例工具链名字类似gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu。我强烈建议直接用官方工具链不要自己去找 Linaro 或者 ARM 官方的通用工具链因为官方工具链里的 glibc 版本、内核头文件都是和板子固件匹配过的。拿到工具链后第一件事是验证它能正常工作# 解压工具链 tar -xf gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu.tar.xz -C /opt/ # 加入环境变量 export PATH/opt/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin:$PATH # 验证 aarch64-none-linux-gnu-gcc --version如果能看到版本信息说明工具链本身没问题。接下来要验证它能不能正确链接目标平台的库。写一个最简单的 hello world用工具链编译然后用file命令看生成的二进制架构aarch64-none-linux-gnu-gcc hello.c -o hello file hello # 应该输出ELF 64-bit LSB executable, ARM aarch64如果file显示的是 x86-64那说明你调用的还是本机 gcc环境变量没生效。2.3 依赖库的交叉编译顺序RKMPP 本身依赖的库不多主要是 libdrm 和 libion用于 DMA 缓冲区管理。但如果你要在 FFmpeg 里用 RKMPP那依赖链就长了FFmpeg 依赖 RKMPPRKMPP 依赖 libdrmlibdrm 又依赖内核头文件。这个编译顺序不能乱否则会出现找不到符号的错误。我一般按这个顺序来libdrm从源码编译--hostaarch64-none-linux-gnu安装到独立的 sysroot 目录。RKMPP指定--sysroot指向刚才的安装目录确保能找到 libdrm。FFmpeg配置时加--enable-rkmpp并把 RKMPP 的头文件和库路径传进去。这里有个容易忽略的细节编译 libdrm 时--prefix最好设成一个独立的目录比如/opt/rk3588-sysroot而不是直接装到工具链的默认路径。这样做的目的是保持环境干净不同项目之间不会互相污染。等所有库都编译完再把这个 sysroot 目录整体打包作为后续开发的统一依赖环境。2.4 pkg-config 的交叉编译配置很多库用 pkg-config 来管理编译参数交叉编译时如果不配置好pkg-config 会去本机的/usr/lib/pkgconfig找库导致链接到 x86 的版本。解决办法是设置环境变量export PKG_CONFIG_PATH/opt/rk3588-sysroot/lib/pkgconfig export PKG_CONFIG_SYSROOT_DIR/opt/rk3588-sysroot export PKG_CONFIG_LIBDIR/opt/rk3588-sysroot/lib/pkgconfigPKG_CONFIG_SYSROOT_DIR这个变量特别关键它会让 pkg-config 输出的-I和-L路径自动加上 sysroot 前缀避免指向本机路径。我第一次交叉编译 FFmpeg 时就是因为没设这个链接阶段报了一堆 undefined reference查了半天才发现是链接到了本机的 libdrm。3. RKMPP 核心接口的使用逻辑3.1 解码流程的四个关键步骤RKMPP 的解码接口用起来其实不复杂核心就四步初始化、送码流、取帧、释放。但每一步的参数配置都有讲究。第一步创建解码器上下文。调用mpp_create创建 MPP 上下文然后mpp_init指定工作模式为MPP_CTX_DEC编码格式为MPP_VIDEO_CodingAVCH.264或MPP_VIDEO_CodingHEVCH.265。这里要注意编码格式必须和实际码流一致否则解码器初始化会失败。第二步配置解码器参数。通过mpp_dec_cfg_set设置输出格式、帧缓冲区数量等。其中MPP_DEC_SET_PARSER_SPLIT_MODE这个参数值得说一下它控制码流是按帧送还是按任意长度送。如果设成按帧送你需要自己保证每次送的是一帧完整数据如果设成非分帧模式MPP 内部会自己找帧边界。对于实时流场景我建议用非分帧模式容错性更好。第三步循环送码流取帧。用mpp_packet封装码流数据调用decode_put_packet送入然后decode_get_frame取解码后的帧。这里有个坑decode_get_frame是异步的送进去一帧不一定马上能取出来因为 VPU 需要时间处理。所以正确的做法是送一帧、取一帧如果取不到就继续送而不是死等。第四步释放资源。每取到一帧处理完后必须调用mpp_frame_deinit释放否则缓冲区很快耗尽。整个流程结束后mpp_destroy销毁上下文。3.2 缓冲区管理的核心概念RKMPP 的缓冲区管理和普通内存分配不太一样它用的是 DMA 缓冲区。为什么要用 DMA因为 VPU 是独立于 CPU 的硬件模块它访问内存不经过 CPU 的 MMU需要的是物理地址连续的缓冲区。普通malloc分配的内存物理上可能是分散的VPU 没法直接访问。MPP 内部通过 ION 或者 DMA-BUF 来分配这类内存。你在应用层拿到的MppFrame里有一个MppBuffer对象它封装了文件描述符fd和虚拟地址。如果要把解码后的帧显示出来通常需要把 fd 传给显示模块比如 DRM 或者 RGA让显示硬件直接读取这块内存避免一次内存拷贝。这里有个性能优化的关键点零拷贝。理想情况下解码后的帧应该直接从 VPU 的输出缓冲区送到显示模块中间不做任何 memcpy。RKMPP 支持这种模式但需要显示模块也支持 DMA-BUF 导入。如果你用的是 RGA 做缩放或格式转换RGA 同样支持 DMA-BUF整条链路可以做到零拷贝。3.3 编码器的码率控制模式编码这块码率控制是重点。RKMPP 支持三种模式CBR固定码率、VBR可变码率、FIXQP固定 QP。选哪种取决于你的场景。CBR 适合网络传输因为码率稳定不会突然撑爆带宽。但它的缺点是画面复杂时质量会下降因为码率被限制死了。VBR 适合本地存储画面复杂时给高码率简单时给低码率整体质量更好但瞬时码率可能很高。FIXQP 则是固定量化参数码率完全不可控一般只在特殊调试场景用。配置码率的接口是mpp_enc_cfg_set_s32关键参数包括参数含义典型值MPP_ENC_RC_MODE码率控制模式CBR / VBRMPP_ENC_RC_BITRATE目标码率bps40000004MbpsMPP_ENC_RC_FPS_IN输入帧率30MPP_ENC_RC_FPS_OUT输出帧率30MPP_ENC_RC_GOPGOP 长度60MPP_ENC_RC_QP_INIT初始 QP26GOP 长度这个参数影响很大。GOP 越长I 帧越少压缩率越高但随机访问的延迟也越大。对于实时监控场景GOP 一般设成帧率的 2 倍左右也就是 30fps 对应 GOP 60。对于点播场景可以设得更长一些。4. 性能优化的实战切入点4.1 从 CPU 占用率反推瓶颈性能优化第一步是找到瓶颈在哪。最直接的方法是看 CPU 占用率。如果解码时 CPU 占用率很高说明数据在软件层被大量拷贝或者格式转换了。正常情况下硬解 1080p 的 CPU 占用应该在 5% 以下单核。用top命令观察如果发现某个线程占用特别高可以用perf top进一步定位热点函数。我遇到过一种情况解码本身很快但帧从 MPP 缓冲区拷贝到应用缓冲区这一步花了大量时间。后来改成直接传递 fdCPU 占用直接降了一半。另一个常见瓶颈是格式转换。VPU 解码输出的格式通常是 NV12但显示模块可能需要 RGB 或者 YUV420P。如果这个转换用 CPU 做开销很大。正确做法是用 RGA瑞芯微的 2D 加速器做硬件转换RGA 支持 NV12 到 RGB 的快速转换而且能和 VPU 共享 DMA 缓冲区。4.2 多路并发的资源分配策略做多路解码时资源分配是个大学问。RK3588 的 VPU 支持多路并行但并不是路数越多越好。每一路解码都需要独立的 MPP 上下文和缓冲区这些都会消耗内存和 VPU 内部资源。我的经验是先确认芯片的 VPU 规格。RK3588 的 VPU 支持 8K60fps 解码换算成 1080p 大概是 32 路。但实际能跑多少路还要看内存带宽和 CPU 处理能力。一般建议留 30% 的余量不要跑满。多路场景下缓冲区数量要合理配置。每路解码如果配太多缓冲区内存占用会很大配太少又容易丢帧。我一般从每路 4 个缓冲区开始调观察是否有丢帧再逐步增加。4.3 内存带宽的隐形限制很多人优化性能时只盯着 CPU 和 VPU 占用忽略了内存带宽。视频编解码是内存带宽消耗大户尤其是高分辨率多路场景。RK3588 的内存带宽大概是 10GB/s 左右一路 4K60fps 的解码大概消耗 1.5GB/s 带宽8 路就把带宽吃满了。如果发现 VPU 占用不高但帧率上不去很可能就是内存带宽瓶颈。这时候可以考虑降低分辨率、减少不必要的内存拷贝、使用更紧凑的像素格式比如 NV12 比 YUV420P 省 1/3 带宽。注意内存带宽瓶颈很难通过软件优化解决本质上是硬件限制。选型阶段就要算好带宽预算。5. 那些年我踩过的坑5.1 解码失败的排查链路mpp 解码失败是搜索频率很高的问题但失败的原因五花八门。我总结了一套排查顺序第一确认码流本身是否合法。用ffprobe检查码流信息看编码格式、分辨率、帧率是否正常。有时候码流是半截的或者 SPS/PPS 缺失MPP 自然解不了。第二确认编码格式配置是否匹配。如果码流是 H.265但初始化时设的是 H.264肯定失败。这个错误很隐蔽因为初始化不一定报错但送码流后取不到帧。第三检查缓冲区配置。如果MPP_DEC_SET_PARSER_SPLIT_MODE设成了按帧送但实际送的数据不是完整帧解码器会一直等。这种情况表现为送了很多数据但一帧都取不出来。第四看内核日志。dmesg里如果有 VPU 相关的报错比如 vpu reset 或者 iommu fault说明是硬件层面的问题可能是固件版本不匹配或者内存地址越界。5.2 交叉编译时的符号找不到问题交叉编译 FFmpeg 链接 RKMPP 时最常见的错误是 undefined reference tompp_create。这个错误的本质是链接器找不到 RKMPP 的库文件。排查步骤先确认--extra-ldflags里的-L路径是否正确指向了 RKMPP 的 lib 目录。然后确认库文件名对不对RKMPP 编译出来通常是librockchip_mpp.so。最后确认链接顺序FFmpeg 的 configure 里RKMPP 的库要放在依赖它的库后面。还有一个隐蔽的坑如果 RKMPP 是用 C 编译的而 FFmpeg 是 C 代码链接时可能需要-lstdc。这个错误信息不会直接提示而是报一堆 C 标准库的符号找不到。5.3 运行时库版本不匹配编译通过了拷到板子上运行报 version GLIBC_2.29 not found这是典型的工具链 glibc 版本高于板子固件版本。解决办法有两个要么换用和板子固件匹配的旧版工具链要么把板子的根文件系统升级到匹配的版本。我一般建议用官方 SDK 里的工具链因为官方已经做过版本匹配。如果自己找的工具链一定要用strings命令检查板子上/lib/libc.so.6的版本确保工具链的 glibc 版本不高于它。6. 几个提升开发效率的实用技巧6.1 用 mpi_dec_test 快速验证环境RKMPP 源码里自带了一些测试程序在test/目录下。其中mpi_dec_test是最有用的它能直接读一个 H.264/H.265 文件解码后输出 YUV。环境搭好后第一件事就是用这个测试程序验证./mpi_dec_test -i test.h264 -t 7 -n 100 -o output.yuv-t 7表示 H.264-n 100表示解 100 帧。如果这个能跑通说明 RKMPP 本身没问题接下来再排查自己的应用代码。这个工具帮我省了大量时间因为它把环境问题和代码问题隔离开了。6.2 日志分级与调试开关RKMPP 内部有日志系统通过环境变量控制输出级别export MPP_LOG_LEVELdebug调试阶段打开 debug 日志能看到码流解析、缓冲区分配、硬件交互的详细信息。但正式发布时要关掉因为日志本身也有性能开销。我一般会在代码里根据编译选项控制日志级别debug 版本开 verboserelease 版本只保留 error。6.3 性能测试的标准化流程做性能优化必须有可复现的测试流程。我的做法是固定测试素材同一段码流、固定测试命令、固定观察指标CPU 占用、内存占用、帧率、延迟。每次改动只动一个变量这样才能准确判断优化效果。测试时用time命令统计总耗时用top -H看线程级 CPU 占用用cat /proc/meminfo看内存变化。如果条件允许用示波器或者硬件计数器测端到端延迟比软件打点更准确。7. 关于 RKMPP 后续演进的一些观察从我这几年的使用体验看RKMPP 的迭代方向主要在几个方面。一是对新编码格式的支持比如 AV1 的硬解已经在部分新芯片上落地。二是和上层框架的集成越来越紧密FFmpeg 的 rkmpp 插件现在维护得比较活跃。三是 API 的稳定性在提升早期版本接口变动比较频繁现在基本稳定了。对于正在做技术选型的朋友我的建议是如果项目周期紧直接用官方 SDK 里配套的 RKMPP 版本不要追新。如果要做长期产品关注一下 RKMPP 的社区动态但升级要谨慎因为硬件相关的库升级往往牵一发动全身。另外RKMPP 和 RGA 的配合使用是个值得深入的方向。很多场景下解码只是第一步后续的缩放、旋转、格式转换、叠加都可以用 RGA 硬件加速。把 VPU 和 RGA 串起来做零拷贝流水线是瑞芯微平台音视频处理的性能天花板。这块我还在持续摸索有新的心得再分享。
返回列表