1. 为什么要在 RK3588 上折腾 librga
RK3588 这颗芯片在边缘计算和嵌入式视觉领域的热度一直没降过。8 核 CPU、6 TOPS NPU、Mali-G610 GPU,再加上独立的 VPU 和 RGA 模块,纸面参数确实漂亮。但真正把板子拿到手之后你会发现,参数好看和跑得流畅完全是两码事。尤其是在做多路摄像头采集、视频编解码、AI 推理结果叠加显示这类典型场景时,CPU 经常被图像格式转换和缩放操作吃满,NPU 算得再快也白搭。
librga 就是解决这个问题的。它是瑞芯微为自家 RGA(Raster Graphic Acceleration)硬件模块提供的用户态库,专门负责图像缩放、格式转换、旋转、裁剪、合成这些 2D 图形操作。你可以把它理解成一块"专门干脏活累活的副卡"——CPU 不用再一个像素一个像素地去搬数据,把任务丢给 RGA,自己该干嘛干嘛。
这篇文章面向的是已经在 RK3588 上跑 Linux(Ubuntu 或 Debian 系)并且需要做图像处理的开发者。不管你是刚拿到板子的新手,还是已经在上面部署过 YOLOv8、做过摄像头适配的老手,只要涉及到图像前处理和后处理,librga 都是绕不开的一环。我会从源码拉取开始,一步步讲到编译、安装、写 Demo、跑通验证,把中间踩过的坑和关键细节都摊开说。
注意:本文所有操作基于 RK3588 平台的 Linux 环境,内核版本建议 5.10 及以上,Buildroot 和 Ubuntu 20.04/22.04 均适用。Android 平台的编译方式略有差异,不在本文讨论范围内。
2. 动手之前:环境确认与依赖梳理
2.1 先搞清楚你的板子到底有没有 RGA
很多人上来就开始编译,结果跑 Demo 的时候报错说找不到设备节点。这种情况十有八九是内核里没开 RGA 驱动,或者设备树里没配置。所以第一步不是急着拉代码,而是先确认硬件和驱动层面是否就绪。
登录到板子的终端,执行:
ls /dev/rga*如果看到/dev/rga这个设备节点,说明驱动已经加载了。如果没有,先检查内核配置:
zcat /proc/config.gz | grep RGA正常应该能看到CONFIG_ROCKCHIP_RGA=y或者=m。如果没有,你需要重新配置内核并编译。对于使用官方 SDK 的开发者,一般在arch/arm64/configs/rockchip_defconfig里已经默认开启了 RGA 相关配置,但如果你用的是第三方板子或者自己裁剪过内核,这一步很容易漏掉。
另外,RGA 模块在 RK3588 上有多个版本(RGA2 和 RGA3),它们各自支持的功能略有差异。RGA3 支持更高的分辨率和更多的格式,但并不是所有操作都能在 RGA3 上跑。librga 会自动根据任务类型选择合适的硬件单元,但你需要知道这个差异的存在,后面排查问题时才不会懵。
2.2 交叉编译工具链的选择
如果你是在板子上直接编译(native 编译),那用系统自带的 gcc 就行。但大多数情况下,我们是在 PC 上交叉编译好再部署到板子上,这样效率高得多。
RK3588 是 ARM64 架构,你需要一套 aarch64 的交叉编译工具链。官方 SDK 里通常会带prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu这样的工具链,直接用就行。如果你没有官方 SDK,也可以从 ARM 官网下载 GNU Toolchain for AArch64。
设置好环境变量:
export CROSS_COMPILE=/path/to/toolchain/bin/aarch64-none-linux-gnu- export CC=${CROSS_COMPILE}gcc export CXX=${CROSS_COMPILE}g++验证一下:
${CC} --version能正常输出版本信息就说明工具链没问题。
实操心得:工具链的版本尽量和板子上运行时的 glibc 版本匹配。我曾经用 gcc 12 编译的库放到 glibc 2.27 的板子上跑,直接报
GLIBC_2.34 not found。后来换成 gcc 10.3 就没事了。如果你不确定板子上的 glibc 版本,用ldd --version查一下。
2.3 依赖库清单
librga 本身依赖不多,但为了编译完整的 Demo 和测试程序,你需要准备以下内容:
| 依赖项 | 用途 | 是否必须 |
|---|---|---|
| libdrm | DRM 显示和缓冲区管理 | Demo 需要 |
| libpng / libjpeg | 图片读写 | 测试程序需要 |
| cmake | 构建系统 | 推荐 |
| meson / ninja | 另一种构建方式 | 可选 |
| OpenCV | 图像处理对比测试 | 可选 |
在 Ubuntu PC 上安装这些依赖:
sudo apt install cmake libdrm-dev libpng-dev libjpeg-dev如果你打算用 OpenCV 做对比测试,再装上libopencv-dev。
3. 源码获取与编译:从零到生成 .so
3.1 拉取 librga 源码
librga 的源码托管在 GitHub 上,仓库地址是https://github.com/airockchip/librga。这个仓库是瑞芯微官方维护的,更新比较活跃。
git clone https://github.com/airockchip/librga.git cd librga拉下来之后先看一下目录结构,心里有个数:
librga/ ├── CMakeLists.txt ├── Makefile ├── include/ │ └── RgaApi.h │ └── im2d.h │ └── im2d.hpp ├── src/ │ ├── rga.cpp │ ├── RgaUtils.cpp │ └── ... ├── samples/ │ ├── demo.cpp │ └── ... └── libs/ └── Linux/ └── aarch64/include/目录下是头文件,src/是核心实现,samples/里有一些示例程序,libs/下预编译好的库文件(但我们要自己编,不用它)。
3.2 编译方式的选择:CMake 还是 Makefile
librga 同时支持 CMake 和 Makefile 两种构建方式。我个人更推荐 CMake,原因是它的交叉编译配置更清晰,而且后续集成到自己的项目里也方便。
先看 Makefile 方式,比较简单粗暴:
make ARCH=arm64 CROSS_COMPILE=aarch64-none-linux-gnu- -j$(nproc)编译完成后会在libs/Linux/aarch64/下生成librga.so。
CMake 方式稍微多几步,但更规范:
mkdir build && cd build cmake .. \ -DCMAKE_SYSTEM_NAME=Linux \ -DCMAKE_SYSTEM_PROCESSOR=aarch64 \ -DCMAKE_C_COMPILER=${CC} \ -DCMAKE_CXX_COMPILER=${CXX} \ -DCMAKE_BUILD_TYPE=Release make -j$(nproc)注意:如果你在 CMake 配置时没有指定
CMAKE_SYSTEM_NAME和CMAKE_SYSTEM_PROCESSOR,CMake 会默认按本机架构编译,生成的 .so 放到板子上跑不了。这个坑我踩过不止一次,每次都是编译完了才发现架构不对。
3.3 编译产物说明
编译成功后,你会得到以下关键文件:
librga.so:核心动态库,应用程序需要链接它librga.a:静态库版本(如果开启了静态编译)RgaApi.h、im2d.h、im2d.hpp:开发用的头文件
验证一下生成的 .so 架构是否正确:
file librga.so输出应该是ELF 64-bit LSB shared object, ARM aarch64,如果是x86-64那就说明交叉编译配置没生效。
再看一下它依赖了哪些库:
aarch64-none-linux-gnu-readelf -d librga.so | grep NEEDED正常情况下应该只依赖libc.so.6、libm.so.6、libdl.so.2这些基础库。如果出现了你 PC 上特有的库,说明链接有问题。
3.4 安装到目标板
把编译好的库和头文件推到板子上:
adb push librga.so /usr/lib/ adb push include/ /usr/include/rga/如果你用的是 NFS 或者 scp,方式类似。推完之后在板子上执行:
ldconfig让系统重新缓存动态库路径。
实操心得:有些板子的根文件系统是只读的,直接 push 到
/usr/lib/会失败。这种情况下可以放到/usr/local/lib/或者/opt/lib/,然后通过LD_LIBRARY_PATH指定路径。另外,如果你用的是 Buildroot 构建的系统,建议直接把 librga 集成到 Buildroot 的 package 里,这样每次重新烧录固件都会自动带上。
4. 写一个能跑的 Demo:从图像缩放到格式转换
4.1 Demo 设计思路
光编译出库没什么意思,得写个程序验证它真的能干活。我设计的 Demo 覆盖三个最常用的场景:
- 图像缩放:把一张 1920x1080 的图缩到 640x360
- 格式转换:把 NV12 格式转成 RGB888
- 旋转:把图像旋转 90 度
这三个操作覆盖了 RGA 最核心的能力,也是实际项目中最常遇到的。Demo 的输入我用程序生成一张渐变图,避免依赖外部图片文件,方便在不同环境下复现。
4.2 核心 API 解析
librga 提供了两套 API:C 风格的RgaApi.h和 C++ 风格的im2d.hpp。C++ 接口更友好,推荐使用。
核心的 C++ 接口是im2d.hpp里的im_rect、im_handle和improcess系列函数。一个典型的调用流程是这样的:
#include "im2d.hpp" #include "RgaUtils.h" // 1. 创建源图像缓冲区 int srcWidth = 1920, srcHeight = 1080; int srcFormat = RK_FORMAT_YCbCr_420_SP; // NV12 int srcSize = srcWidth * srcHeight * 3 / 2; void* srcBuf = malloc(srcSize); // 2. 创建目标图像缓冲区 int dstWidth = 640, dstHeight = 360; int dstFormat = RK_FORMAT_RGB_888; int dstSize = dstWidth * dstHeight * 3; void* dstBuf = malloc(dstSize); // 3. 配置源和目标矩形 im_rect srcRect = {0, 0, srcWidth, srcHeight}; im_rect dstRect = {0, 0, dstWidth, dstHeight}; // 4. 执行缩放 + 格式转换 im_handle srcHandle = importbuffer_virtualaddr(srcBuf, srcSize); im_handle dstHandle = importbuffer_virtualaddr(dstBuf, dstSize); improcess(srcHandle, dstHandle, nullptr, nullptr, srcRect, dstRect, IM_SYNC); // 5. 释放资源 releasebuffer_handle(srcHandle); releasebuffer_handle(dstHandle); free(srcBuf); free(dstBuf);这段代码看起来简单,但里面有几个关键点需要展开说。
4.3 缓冲区管理:虚拟地址 vs DMA 文件描述符
上面的例子用的是importbuffer_virtualaddr,也就是用虚拟地址导入缓冲区。这种方式最简单,适合快速验证。但在实际项目中,尤其是和摄像头、VPU、NPU 配合使用时,更推荐用 DMA 文件描述符(dma_fd)来导入。
原因很简单:虚拟地址导入时,librga 内部需要做一次地址映射和 cache 同步,效率不如直接用 dma_fd。而且当你从 V4L2 摄像头采集到帧之后,拿到的本来就是 dma_fd,直接传进去就行,省去了拷贝。
用 dma_fd 的写法:
im_handle srcHandle = importbuffer_fd(srcDmaFd, srcSize);注意:用 dma_fd 导入时,缓冲区的物理内存必须是连续的,而且要对齐到 RGA 要求的边界(通常是 16 字节或 64 字节)。如果你用
malloc分配的内存,它的物理地址不一定连续,这时候只能用虚拟地址导入。要分配 DMA 缓冲区,可以用dma_heap或者ion驱动。
4.4 格式转换的坑:NV12 到 RGB 的色域问题
NV12 转 RGB 是最常见的操作之一,但很多人转完之后发现颜色不对——要么偏绿,要么偏灰。这通常是色域范围(color range)和色彩空间(color space)没设置对。
RGA 默认使用的是 BT.601 的色域转换矩阵,但如果你摄像头的输出是 BT.709,那转换出来的颜色就会有偏差。librga 提供了设置色域范围的接口:
rga_set_colorkey(...); // 设置色键 // 色域范围通过 improcess 的参数传入具体来说,在improcess的最后一个参数里可以指定IM_COLOR_SPACE_BT709或IM_COLOR_SPACE_BT601。如果你不确定摄像头用的是什么标准,可以两个都试一下,看哪个颜色正常。
另外,NV12 的 UV 排列顺序也要注意。NV12 是 Y 平面在前,UV 交错在后;NV21 则是 VU 交错。如果搞反了,颜色会完全不对。RK3588 的摄像头通常输出 NV12,但有些 sensor 可以配置成 NV21,驱动里要确认清楚。
4.5 完整 Demo 代码
把上面的内容整合成一个完整的 Demo:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include "im2d.hpp" #include "RgaUtils.h" int main() { // 源图像参数 int srcW = 1920, srcH = 1080; int srcFormat = RK_FORMAT_YCbCr_420_SP; int srcSize = srcW * srcH * 3 / 2; // 目标图像参数 int dstW = 640, dstH = 360; int dstFormat = RK_FORMAT_RGB_888; int dstSize = dstW * dstH * 3; // 分配缓冲区 void* srcBuf = malloc(srcSize); void* dstBuf = malloc(dstSize); if (!srcBuf || !dstBuf) { printf("malloc failed\n"); return -1; } // 填充源图像(生成渐变 NV12) memset(srcBuf, 128, srcSize); // UV 置为 128(灰色) uint8_t* yPlane = (uint8_t*)srcBuf; for (int i = 0; i < srcH; i++) { for (int j = 0; j < srcW; j++) { yPlane[i * srcW + j] = (uint8_t)(j * 255 / srcW); } } // 导入缓冲区 im_handle srcHandle = importbuffer_virtualaddr(srcBuf, srcSize); im_handle dstHandle = importbuffer_virtualaddr(dstBuf, dstSize); if (srcHandle == 0 || dstHandle == 0) { printf("importbuffer failed\n"); free(srcBuf); free(dstBuf); return -1; } // 配置矩形 im_rect srcRect = {0, 0, srcW, srcH}; im_rect dstRect = {0, 0, dstW, dstH}; // 执行缩放 + 格式转换 int ret = improcess(srcHandle, dstHandle, nullptr, nullptr, srcRect, dstRect, IM_SYNC); if (ret != IM_STATUS_SUCCESS) { printf("improcess failed: %d\n", ret); } else { printf("improcess success\n"); // 这里可以把 dstBuf 写成 PNG 文件查看结果 } // 释放资源 releasebuffer_handle(srcHandle); releasebuffer_handle(dstHandle); free(srcBuf); free(dstBuf); return 0; }编译这个 Demo:
${CXX} demo.cpp -o demo \ -I/path/to/librga/include \ -L/path/to/librga/libs/Linux/aarch64 \ -lrga -ldl -lpthread推到板子上运行:
adb push demo /tmp/ adb shell chmod +x /tmp/demo adb shell /tmp/demo如果输出improcess success,恭喜你,librga 已经跑通了。
5. 性能实测与优化建议
5.1 实测数据
我在 RK3588 开发板上跑了一组测试,对比 CPU 软件处理和 RGA 硬件加速的耗时:
| 操作 | 分辨率 | CPU 耗时 | RGA 耗时 | 加速比 |
|---|---|---|---|---|
| NV12 缩放 | 1920x1080 -> 640x360 | 12.3ms | 1.8ms | 6.8x |
| NV12 转 RGB888 | 1920x1080 | 18.7ms | 2.4ms | 7.8x |
| RGB 旋转 90 度 | 1280x720 | 9.5ms | 1.2ms | 7.9x |
| NV12 缩放 + 转 RGB | 1920x1080 -> 640x360 | 25.1ms | 2.9ms | 8.7x |
数据很直观:RGA 的加速比在 6 到 9 倍之间。对于需要处理多路视频流的场景,这个差距直接决定了你能不能实时跑满 30fps。
5.2 影响性能的关键因素
缓冲区导入方式:用 dma_fd 比用虚拟地址快 10% 到 20%,因为省去了地址映射和 cache 同步的开销。
任务合并:如果你需要同时做缩放和格式转换,尽量在一次improcess调用里完成,不要分两次。RGA 硬件支持流水线处理,合并调用可以减少硬件上下文的切换开销。
分辨率对齐:RGA 对宽度有对齐要求,通常是 16 字节对齐。如果你的图像宽度不是 16 的倍数,RGA 内部会做 padding,这会增加额外的处理时间。建议在分配缓冲区时就把宽度对齐到 16 或 64。
并发处理:RK3588 有多个 RGA 单元,理论上可以并行处理多个任务。但 librga 默认是串行调度的,如果你有多路视频流需要同时处理,可以考虑用多线程 + 多个im_handle来并行提交任务。
实操心得:我在做四路 1080p 摄像头实时处理时,一开始用单线程串行调用 RGA,结果只能跑到 15fps。后来改成四个线程各自处理一路,每路绑定一个独立的
im_handle,直接跑满 30fps。但要注意,RGA 硬件单元是共享的,线程太多反而会因为竞争而变慢。一般建议线程数不要超过 RGA 硬件单元的数量。
5.3 和 MPP、NPU 的配合
在实际项目中,librga 很少单独使用,通常是和 MPP(视频编解码)和 NPU(AI 推理)配合。一个典型的流水线是这样的:
- 摄像头采集 NV12 帧(V4L2)
- MPP 硬解码 H.264/H.265 视频流
- RGA 做缩放和格式转换(NV12 -> RGB)
- NPU 做推理(YOLOv8 等模型)
- RGA 做后处理(画框、叠加文字)
- MPP 硬编码输出
在这个流水线里,RGA 承担了所有的 2D 图形操作,CPU 只负责调度和逻辑控制。这样整条流水线的效率最高,CPU 占用率可以控制在 20% 以下。
关键点在于缓冲区的复用。从 MPP 解码出来的帧,它的 dma_fd 可以直接传给 RGA,RGA 处理完的 dma_fd 再传给 NPU。整个过程零拷贝,数据一直在 DMA 缓冲区里流转,不需要在 CPU 和硬件单元之间来回搬。
6. 常见问题排查速查表
6.1 编译阶段问题
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
编译报错找不到RgaApi.h | 头文件路径没指定 | 加-I/path/to/librga/include |
链接报错undefined reference to impprocess | 没有链接 librga | 加-lrga |
| 生成的 .so 是 x86 架构 | 交叉编译配置没生效 | 检查CMAKE_SYSTEM_PROCESSOR和CROSS_COMPILE |
编译报错drm.h not found | 缺少 libdrm 开发包 | sudo apt install libdrm-dev |
6.2 运行阶段问题
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
报错open /dev/rga failed | 驱动没加载或设备节点不存在 | 检查内核配置,确认CONFIG_ROCKCHIP_RGA=y |
报错importbuffer failed | 缓冲区大小或对齐不对 | 检查缓冲区大小是否匹配格式要求,宽度对齐到 16 |
| 输出图像颜色偏绿 | 色域范围设置错误 | 尝试 BT.601 和 BT.709 两种设置 |
| 输出图像花屏 | 缓冲区物理地址不连续 | 改用 dma_fd 导入,或使用 DMA 堆分配内存 |
| 程序运行崩溃 | 缓冲区生命周期管理错误 | 确保releasebuffer_handle在free之前调用 |
| 性能不如预期 | 任务没有合并或缓冲区导入方式低效 | 合并操作,改用 dma_fd |
6.3 几个容易忽略的细节
设备权限问题:/dev/rga默认权限是root:root,普通用户访问不了。你可以把用户加到video组,或者改 udev 规则:
echo 'KERNEL=="rga", MODE="0666"' > /etc/udev/rules.d/99-rga.rules内存对齐:RGA 要求缓冲区的物理地址对齐到 64 字节,虚拟地址导入时 librga 内部会处理,但 dma_fd 导入时需要你自己保证。用dma_heap分配的内存默认就是对齐的。
多线程安全:librga 的im_handle不是线程安全的,每个线程应该使用独立的 handle。但improcess函数本身是线程安全的,可以在多个线程中同时调用。
版本兼容性:不同版本的 librga 接口可能有变化。比如早期版本用的是c_RkRgaBlit,新版本推荐用improcess。建议锁定一个稳定版本,不要频繁升级。
7. 从 Demo 到产品:工程化建议
7.1 封装一层自己的接口
直接调用 librga 的 API 虽然能用,但在实际项目中,建议封装一层自己的接口。原因有三个:一是方便替换底层实现(万一以后换平台);二是统一错误处理和日志;三是简化调用流程。
我通常会封装一个RgaProcessor类,提供resize、cvtColor、rotate等方法,内部管理缓冲区的分配和释放。这样上层业务代码只需要关心"我要把这张图缩到多大",不需要关心 RGA 的细节。
7.2 缓冲区池化
在高帧率场景下,频繁地malloc和free缓冲区会导致内存碎片和性能下降。建议实现一个缓冲区池,预先分配好一组 DMA 缓冲区,用的时候从池里取,用完还回去。
缓冲区池的大小可以根据实际需求调整。一般来说,对于 1080p NV12 格式,一个缓冲区大约 3MB,分配 10 个也就 30MB,对 RK3588 的 4GB/8GB 内存来说完全不是问题。
7.3 错误处理与降级
RGA 虽然稳定,但也不是万无一失。在极端情况下(比如硬件忙不过来),improcess可能会返回错误。这时候你需要有一个降级方案,比如用 CPU 软件处理作为备选。虽然慢,但至少不会让整个流水线卡死。
另外,建议在程序启动时做一次 RGA 可用性检测,如果/dev/rga打不开,直接切换到软件处理模式,并打印警告日志。
7.4 性能监控
在生产环境中,建议加一些性能监控点,记录每次 RGA 调用的耗时。如果发现某次调用异常慢,可以及时排查。简单的做法是用clock_gettime记录时间戳,然后统计平均值和最大值。
我在实际项目中会把这些数据通过共享内存暴露出来,方便上位机实时查看。这样一旦性能下降,能第一时间定位到是 RGA 的问题还是其他环节的问题。
8. 个人实操体会
从第一次在 RK3588 上编译 librga 到现在,我大概踩了不下十个坑。最开始的几次都是环境问题——工具链不对、依赖缺失、设备节点不存在。这些问题看起来简单,但如果没有经验,很容易卡住半天。
印象最深的一次是调 NV12 转 RGB 的颜色偏差。当时摄像头出来的画面偏绿,我以为是摄像头驱动的问题,换了三个 sensor 都没解决。后来才发现是 RGA 的色域范围默认用了 BT.601,而摄像头输出的是 BT.709。改了一个参数就正常了。这件事让我意识到,嵌入式开发里很多问题不是"能不能跑",而是"跑得对不对"。
另一个体会是,librga 的文档虽然不算少,但很多细节需要自己看源码才能搞清楚。比如improcess的参数到底怎么传、缓冲区对齐的具体要求是什么、多线程下 handle 怎么管理。这些在官方文档里要么没写,要么写得比较模糊。我的建议是,遇到不确定的地方,直接去看src/目录下的实现,比查文档快得多。
最后说一个实用技巧:如果你在板子上调试,可以用RGA_DUMP环境变量让 librga 输出详细的调试日志。设置export RGA_DUMP=1之后,每次调用都会打印输入输出参数和硬件耗时,排查问题非常方便。这个功能在官方文档里没提,是我看源码的时候发现的。