☰
RK3588 DMA与RGA图像前处理优化:从CPU到硬件的可复现代码
2026/10/18 19:27:02 网站建设 项目流程

简介:面向RK3588平台的DMA与RGA优化指南项目代码包,专为嵌入式开发、视频流处理与AI推理工程师定制。内容聚焦视频解码、数据预处理和推理链路中的CPU占用过高问题,通过DMA传输减少拷贝开销,借助RGA完成高效格式转换,再结合零拷贝API打通输入输出与推理流程,是一份可直接对照实现的轻量级优化示例。

压缩包采用zip格式,整体仅6KB,共3个文件,分别是html说明页、inscode项目配置与gitignore版本管理文件,便于快速浏览指南内容并在代码环境中加载使用。目前已有165人学习,适合希望降低RK3588平台CPU负载、提升流水线吞吐的开发者参考。

通过代码与配套说明,读者可以复现DMA缓冲区创建、RGA数据包装与预处理、零拷贝API的参数设置,以及推理执行和结果获取的关键步骤;贴出的优化前后CPU消耗对比数据则有助于清晰判断性能提升幅度。

1. RK3588 DMA 与 RGA 优化指南:把图像前处理从 CPU 搬到硬件的一组可复现代码

很多第一次拿到 RK3588 板子的人,视频流处理都会走同一条弯路:摄像头数据先 memcpy 搬运,再用 libyuv 做缩放和 RGB 转换,最后喂给 RKNN。这套方案不是不能用,而是一旦分辨率到 1080p 以上、帧率要求 30 帧时,CPU 已经被吃掉不少算力。这套 RK3588 DMA 与 RGA 优化指南的核心,就是把“内存搬运”和“2D 图像变换”分别交给 DMA 与 RGA 完成:DMA 负责内存分配与 cache 同步,RGA 负责缩放、旋转、格式转换等硬件加速。代码包覆盖 dma-buf 分配、RGA im2d 调用、双缓冲管线三部分。适合做 AI 摄像头、边缘盒子、显示叠加层的嵌入式开发者直接照改。

2. 先用 DMA 把内存管好:dma-buf 分配与 cache 一致性的三个注意点

2.1 为什么这里不推荐直接用 malloc

在 RK3588 这类 ARM SoC 上,CPU 有 cache,外设如 RGA、NPU、ISP 访问内存却走物理地址或经过 IOMMU 映射。两套地址空间之间天然存在 cache 一致性问题:CPU 刚写完的 buffer,硬件可能读到旧数据;硬件刚写完的 buffer,CPU 又可能读到缓存里的旧内容。直接 malloc 一块内存给 RGA 用,不是完全不行,但物理页不一定连续,cache 失效和回写时机也很难控制,结果就是图像偶发花屏、颜色错乱,而且这类问题低帧率时反而不明显,高帧率一上来就频繁冒出来,定位极其费劲。

常见做法是在 Linux 内核里用 dma-buf 作为统一的内存载体。它可以跨驱动传递,也能在用户态通过/dev/dma_heap/system这类堆节点分配,拿到 fd 后再 mmap 出 CPU 可访问的虚拟地址。早几年的 ion 接口在老内核里还能看到,但新平台基本都转到了 dma-heap 框架。选 dma-buf 还有一个实际好处:RGA 和 RKNN 驱动都认识这种 fd,传给硬件时省掉一次地址再映射,避免多一次拷贝。

2.2 分配一块可给 RGA 和 CPU 共同使用的 dma-buf

下面是分配 dma-buf 的最简代码,四步走:打开 heap 节点、填充分配参数、拿到 fd、mmap 出用户态地址。多数项目里这四步会封装成一个 alloc_dma_buffer 函数统一管理。

#include <linux/dma-heap.h> #include <fcntl.h> #include <sys/ioctl.h> #include <sys/mman.h> #include <unistd.h> #include <string.h> #include <stdio.h> static int alloc_dma_buffer(size_t size, int *out_fd, void **va) { struct dma_heap_allocation_data alloc; int heap_fd, ret; heap_fd = open("/dev/dma_heap/system", O_RDWR | O_CLOEXEC); if (heap_fd < 0) { perror("open dma_heap"); return -1; } memset(&alloc, 0, sizeof(alloc)); alloc.len = size; // 按页对齐,内核会自动处理 alloc.fd_flags = O_RDWR | O_CLOEXEC; ret = ioctl(heap_fd, DMA_HEAP_IOCTL_ALLOC, &alloc); close(heap_fd); if (ret) { perror("DMA_HEAP_IOCTL_ALLOC"); return -1; } *out_fd = alloc.fd; // 给 RGA/RKNN 使用的 fd *va = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, *out_fd, 0); // CPU 侧访问地址 if (*va == MAP_FAILED) { perror("mmap"); close(*out_fd); return -1; } return 0; }

这段代码逻辑不复杂,但有三个地方值得确认。第一,/dev/dma_heap/system是内核配置了 dma-heap 后才能看到的节点;如果板子默认没有这个路径,去/sys/kernel/debug/dma_heap下看有哪些堆名,不同 BSP 可能把堆放在不同设备路径,有的叫linux,cma,有的叫system。第二,alloc.len会按页对齐,传视频帧大小时不用担心 4K 对齐,但别传 0 或超过堆容量的值。第三,fd_flags带O_CLOEXEC是防止子进程继承 fd,长时间跑服务的进程少这个标志,偶尔会莫名 fd 泄漏。

分配之后,fd 和虚拟地址要分开用:RGA、NPU 那边只认 fd,CPU 侧只用va。代码里如果频繁把va和fd混用,后面很容易踩 cache 花屏的坑。释放时顺序也要固定,先 munmap 再 close fd,避免映射还在时底层内存被提前释放导致的访问异常。

2.3 cache 同步的时机与方向

dma-buf 并不会自动解决 cache 问题,它只是给了你同步工具。CPU 访问前后需要显式调用DMA_BUF_IOCTL_SYNC,让内核在合适的边界做 cache flush 或 invalidate。这一步经常被第一次接触 DMA 的开发者省略,结果就是硬件读不到刚写的数据,或者 CPU 读到旧帧。

#include <linux/dma-buf.h> /* direction 传 DMA_BUF_SYNC_READ、DMA_BUF_SYNC_WRITE 或两者按位或 */ static void dma_buf_sync_start(int fd, unsigned int direction) { struct dma_buf_sync sync; memset(&sync, 0, sizeof(sync)); sync.flags = direction; ioctl(fd, DMA_BUF_IOCTL_SYNC, &sync); } static void dma_buf_sync_end(int fd, unsigned int direction) { struct dma_buf_sync sync; memset(&sync, 0, sizeof(sync)); sync.flags = direction; ioctl(fd, DMA_BUF_IOCTL_SYNC, &sync); }

需要留意的是,同步方向和数据流方向是反的:CPU 写完数据准备交给 RGA 时,做WRITE方向的 START/END;RGA 写完数据准备让 CPU 读时,做READ方向。如果同一块 buffer 两边都在操作,直接传DMA_BUF_SYNC_RW。另一个常见误区是只 START 不 END,或者只做一遍就把 fd 丢给硬件处理,这样缓存状态其实没有真正刷干净,后续可能隔几帧出现一次图像纹路错乱。我习惯把 START 和 END 封装成一对函数,在关键路径上强制配对调用,宁可多 sync 一次也不留隐患。

3. 让 RGA 扛下缩放与转色:im2d 参数、对齐边界和性能分界

3.1 什么情况用 RGA,什么情况继续用 libyuv

RGA 是 SoC 里的 2D 图形加速引擎,对缩放、旋转、镜像、格式转换、色彩空间调整做了硬件加速,处理时 CPU 基本不参与。而 libyuv 是纯 CPU 优化库,靠 NEON 指令集提速,灵活性强,但每处理一帧 1080p,CPU 占用就很可观;一旦同时还有解码、目标检测、网络传输,主核就开始吃紧。

我一般这样分界:单路 720p 以下、不追求帧率、临时验证,用 libyuv 方便;多路 IPC 输入、模型推理前处理、显示与编码并发,一律走 RGA。这套代码包里重点演示的也是 RGA 路径,因为 RKNN 对输入数据的格式和尺寸要求固定,靠 RGA 把 NV12 转成 RGB888、把 1920x1080 缩到 640x640,是最稳的组合。RGA 的带宽占用也比 CPU 搬运明显更低,长时间跑边缘盒子,功耗和发热都能降一截。

3.2 im2d 的缩放与转色代码结构

librga 提供的接口以 im2d 开头,头文件是im2d.h和rga.h。使用流程固定:用 fd 构造输入输出 buffer,设定宽高和格式,调用 imresize 或 imrotate,最后检查返回值。下面这段把 NV12 输入缩放到任意尺寸并转成 RGB888,适合直接套在推理管线前。

#include "im2d.h" #include "rga.h" int rga_scale_nv12_to_rgb888(int src_fd, int dst_fd, int src_w, int src_h, int dst_w, int dst_h) { rga_buffer_t src, dst; int ret; /* 输入用 NV12,输出用 RGB888 */ src = wrap_fd_to_handle(src_fd, src_w, src_h, RK_FORMAT_YUV_420_SP, 0, 0); dst = wrap_fd_to_handle(dst_fd, dst_w, dst_h, RK_FORMAT_RGB_888, 0, 0); /* fx/fy 传 0,表示按 dst 宽高自动计算缩放比 */ ret = imresize(src, dst, 0, 0, 0, IM_SYNC); if (ret != IM_STATUS_SUCCESS) { fprintf(stderr, "imresize failed: %s\n", imStrError(ret)); return -1; } return 0; }

wrap_fd_to_handle必须传入 dma-buf fd、宽、高、像素格式;宽高是像素单位的逻辑尺寸,不是 buffer 字节长度。后面补的两个 0 表示 stride 与宽高一致,如果你从别的模块拿到了带 padding 的 buffer,这两个参数要填实际行宽。imresize的六个参数里,前两个是 src/dst 描述符,fx、fy 传 0 表示自动按目标尺寸计算,第五个插值参数默认即可,最后一个IM_SYNC表示同步等待完成。调试阶段建议先同步执行,比异步出问题好定位,跑顺后再切异步提高吞吐。

旋转的代码几乎一样,只是换成 imrotate:

ret = imrotate(src, dst, IM_HAL_TRANSFORM_ROT_90, IM_SYNC);

IM_HAL_TRANSFORM_ROT_90、ROT_180、ROT_270分别对应三种旋转角度。90 和 270 度旋转后,输出宽高会交换,创建 dst 时如果仍沿用原宽高,RGA 会直接返回参数错误或者画出压扁的图像。

3.3 对齐边界:十六字节对齐和格式选择是玄学还是硬要求

RGA 的对齐要求不算苛刻,但确实存在。常见做法是输入输出宽度尽量对齐到 16 的整数倍,高度可以宽松一些;如果用 V4L2 采集,很多驱动本身会按硬件对齐,问题不大。NV12 这类 YUV 格式建议至少 2 对齐,不然 Y 平面和 UV 平面边界容易错位,画面出现半个屏的颜色偏移。我遇到过 1280x720 的输入,宽度没问题,画面反复出现右侧一条绿边,最后定位到是 height 没有对齐到 2,UV 平面在 RGA 内部计算时多读了两行。

格式选择上也有常用映射表,先确认输入源的输出格式再选宏。V4L2 采集默认格式往往是 NV12,推理前处理通常要 RGB888,显示叠加层常要 RGBA8888。这几个宏都在rga.h里有定义,不要自己在代码里写数字格式号,不同 BSP 下相同数字可能对应不同格式。如果同时做缩放和转色,一次imresize就能完成,不需要先转格式再缩放。

RGA 格式宏实际格式常见场景
RK_FORMAT_YUV_420_SPNV12摄像头采集
RK_FORMAT_YUV_420_PNV21部分传感器输出
RK_FORMAT_RGB_888RGB888模型推理输入
RK_FORMAT_RGBA_8888RGBA8888显示叠加/UI 合成
RK_FORMAT_BGRA_8888BGRA8888部分 GUI 框架要求

另外,YUV 转 RGB 时存在 limited range 和 full range 的差异。摄像头 sensor 输出一般是 limited range,模型训练一般用 full range,如果不做色彩空间转换,推理结果会整体偏灰或偏白。im2d 里可以设置色彩空间相关参数,代码包里在转换函数上做了封装,直接传场景类型即可。遇到颜色异常,先别急着怀疑对齐,把输入输出的 range 查一下,多数时候问题就在这。

4. 串起整条图像流水线:cache 时序、双缓冲与常见问题排查

4.1 从采集到 RGA 再到 RKNN 的完整数据流

把前两章的代码拼起来,整条链路大概是:摄像头或解码器交出 dma-buf fd,RGA 在完全没有 CPU 拷贝的情况下读到该 buffer,经过缩放和格式转换后写到另一块 dma-buf,最后这张输出的 fd 直接传给 RKNN 推理接口。整个流程中 CPU 只负责启动任务和检查错误,不碰像素数据。这也是这套优化指南最省资源的地方,主核只剩控制逻辑,带宽留给 AI 运算和网络。

实现时建议按“采集 → RGA → 推理”三段分配职责,每段各管一块 dma-buf,不要复用同一块做输入输出。RGA 的 in-place 操作虽然部分场景支持,但一旦跑旋转就会因为宽高变化产生额外判断,反而不省内存。两块 buffer 一进一出的做法更接近稳定的双缓冲模型,后续做性能调优也更容易。代码包里提供了一个最小骨架,采集回调、RGA 处理、推理回调三段分开注册,改起来非常直接。

4.2 双缓冲和同步模式的选择

单 buffer 的处理时间如果刚超过 33ms,下一帧采集就会被卡住。常见做法是分配两块尺寸相同的 dma-buf,一块做 RGA 输入,一块做输出,下一帧再把两块互换。这样采集线程和推理线程各自操作不同的 buffer,不会互相覆盖。双缓冲还需要配一个同步机制:RGA 用IM_SYNC时,调用会阻塞到硬件完成,逻辑最简单;用IM_NO_SYNC时库会立刻返回,你拿到的输出还没就绪,必须等对应的事件信号或 fence。对刚起步的工程,第一版我全用IM_SYNC验证正确性,帧率稳定后再对耗时最大的环节做异步化。

双缓冲换帧时,cache 同步方向也要跟上切换节奏:RGA 输出完成后,CPU 去读结果之前做 READ 方向 sync;CPU 填完输入交给 RGA 前做 WRITE 方向 sync。顺序错了画面不一定会立刻花,但长时间运行会隔一阵子冒出一帧脏数据。这类偶发问题最难排查,所以 sync 一定要封装成固定函数顺序执行。

4.3 常见问题排查

下面几条是我在类似链路里真实处理过的现象,按现象、原因、解决的顺序整理,基本能覆盖常见翻车场景。

现象一:RGA 返回IM_STATUS_INVALID_PARAM,任务完全没执行。 原因:目标尺寸超出 RGA 支持范围,或者宽高没有对齐到偶数。RK3588 的 RGA 对超大宽度有限制,超过一定区间会直接拒绝任务。 解决:先打印imStrError的返回值,再把输入输出的宽高打印出来确认偶数对齐。摄像头输出分辨率一般没问题,往往是手动指定输出大小时抄错了数。

现象二:图像输出大面积绿块或花屏。 原因:源 buffer 的 fd 不是 dma-buf,或者 CPU 侧 sync 没做完整。把 malloc 出来的虚拟地址直接塞给 wrap_fd_to_handle,是 RGA 花屏最常见的原因。 解决:统一走 alloc_dma_buffer 分配的 fd,保证传给 RGA 的是 dma-buf,每次 RGA 读写前后把 cache 同步补上。

现象三:旋转 90 度后图像变形或直接失败。 原因:输出宽高没有交换。旋转 90/270 度时宽和高的语义互换,dst buffer 仍按旧宽高构造。 解决:构造 dst 前判断旋转角度,90/270 时把 dst_w、dst_h 交换后再调用 wrap_fd_to_handle。

现象四:RGA 转出来的 RGB888 送进 RKNN,推理结果偏白或偏色。 原因:色彩空间范围不一致。摄像头 NV12 是 limited range,模型训练通常用 full range 的 RGB,没做 range 转换就会出现颜色偏差;另一种常见原因是模型要 BGR,代码里给成了 RGB。 解决:确认模型输入的 channel 顺序,并在转色时按模型要求设置色彩空间参数。必要时先保存一帧输出图片做像素级对比,排除其他干扰。

现象五:双缓冲后偶发上一帧输出混入下一帧。 原因:输出 buffer 在 RGA 硬件完全写完前就被 CPU 再次访问,或者下一帧任务和上一帧在同一个 buffer 上重叠。 解决:检查IM_SYNC是否生效;如果用了IM_NO_SYNC,必须等事件信号完成后再复用 buffer。最简单省心的方式是在验证阶段把同步切回IM_SYNC。

注意:以上五条里,cache 同步和 buffer 复用是实际能稳定复现的高频问题。代码包里保留了 sync 封装和双缓冲实例,建议逐条对照检查。

5. 把优化验证做成固定动作:时间戳、对齐自检与收尾经验

5.1 用单调时钟区分真实的硬件加速和假提速

优化后的第一个问题永远是“到底快了多少”。我一般不在主流程里 printf,而是用clock_gettime(CLOCK_MONOTONIC)打点,分别记录采集完成、RGA 返回、推理拿到输入这三个时间点,相减得出每一段耗时。同步模式下,1080p NV12 转 640x640 RGB888 通常是几毫秒量级;如果测出来是两位数毫秒,大概率缓冲区仍走了 CPU 拷贝,或者 cache 同步配对没正确执行而被迫回退。看时间戳比看代码更容易让同事信服,也更容易在性能回退时快速定位到具体环节。

5.2 对齐自检清单和 fd 泄漏检查

这套指南的代码包里带了一个自检逻辑,我每次换方案都会跑一遍:检查 dma-buf fd 是否非负、宽高是否偶数对齐到 16、sync 是否成对、RGA 返回值是否全部为成功。新板子或新分辨率合入前,先跑这四步再调帧率。另外,分配出的 fd 要在每个 buffer 释放时统一 close,不要依赖进程退出,否则长时间运行后底层内存会一直挂着,直到崩溃才暴露。fd 数量可以用/proc/self/fd数一下,正常运行时应该稳定在一个固定数字,持续增长就说明有泄漏。

5.3 一点个人经验

从那以后我每次接手新的 RK3588 工程,都会强制先走一遍这套验证:先确认 dma-buf 分配正常,再跑一次 RGA 自检,最后才动线程模型和异步优化。很多看似玄学的图像问题,最后都能在 sync 配对、宽高对齐、buffer 复用三处找到答案。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询