简介:面向RK3588平台多媒体与AI推理开发者的优化代码包,重点解决视频流解码、数据预处理及推理环节中CPU占用过高的问题。内容完整演示DMA缓冲区创建流程,说明其如何减少CPU参与的数据拷贝;随后介绍将DMA缓冲区包装为RGA可处理格式的方法,实现缩放、格式转换等预处理;最后给出零拷贝API的输入输出设置、与RGA及DMA的组合调用方式,并附优化前后的CPU消耗对比数据,帮助评估实际收益。包内共3个文件,以inscode配置、html说明文档和gitignore工程文件为主,整体仅6KB,轻量易读。目前已有165人学习下载。通过这份代码,读者可直接复用DMA+RGA+零拷贝的衔接逻辑,理解输入输出数据的设置细节、与推理框架的集成方法,以及常见踩坑点的规避思路,适合已在RK3588上开展视频处理或边缘推理的工程师参考。
1. 先把结论放在前面:DMA没理清之前,谈RGA优化都是空话
做RK3588的DMA与RGA优化之前,先承认一个现实:如果你对dma-buf的分配、映射和缓存同步没有概念,那RGA这个硬件加速器在你手里多半只是个“偶尔能用、经常翻车”的黑匣子。很多人把4K缩放、NV12转RGBA这类操作直接扔给RGA,却忽略了底层内存是连续物理内存还是虚拟地址、CPU和RGA之间有没有做同步,结果性能不升反降。
这篇笔记我会从一次真实踩坑经历讲起,目标是让你拿到一份能跑的代码和一套判断问题的思路:RGA负责像素搬运,DMA负责让这些像素在CPU、RGA和其他硬件之间共享时不丢缓存一致性。读完你可以自己搭最小Demo,也能定位为什么RGA调用返回Invalid argument、为什么图像会出现条纹或花屏。适合正在做实时视觉、显示合成、视频后处理的开发者。
2. DMA-BUF与RGA的关系:内存怎么安排,决定了加速能兑现多少
2.1 RGA处理的是像素,DMA处理的是内存可见性
RGA全名是Raster Graphic Acceleration,字面意思是“光栅图形加速”,落到RK3588上,它本质上是一个支持2D像素级操作的硬件引擎。常见的操作包括缩放、旋转、镜像、格式转换、色域转换、Alpha合成。它的输入和输出都是图像缓冲区,而这些缓冲区在系统里通常表现为dma-buf文件描述符。相比CPU逐像素搬运,RGA在1920×1080这个量级上可以把单次操作延迟做到个位数毫秒,而且几乎不吃CPU核。
但这里说的“DMA优化”不是去配DMA控制器状态机,而是指通过DMA-BUF机制管理物理内存。RK3588这类多核异构芯片上,RGA、显示控制器、ISP、VPU都能拿到同一个dma-buf fd,在各自需要时对同一片物理页发起访问。这样做的核心收益是零拷贝:视频解码输出的NV12帧直接作为RGA的源,RGA处理完的结果再直接作为显示平面的buffer,中间不需要CPU参与复制。
零拷贝不是白拿的。dma-buf里的那一片内存,CPU可能已经写入了数据,而一级缓存还没来得及刷回物理内存;硬件引擎只能看见真实物理内存里的内容。反过来,RGA写入了一片区域,CPU再去mmap读取时可能读到的是缓存里的旧值。这就是缓存一致性问题。优化RGA之前如果不先把这一层理顺,后面所有努力都可能砸在带宽和数据错乱上。
2.2 选dma-heap还是CMA:按连续性和生命周期来
RK3588平台在现代内核上一般使用dma-heap框架分配dma-buf,设备节点常见的有/dev/dma_heap/system和/dev/dma_heap/cma。system堆从普通系统内存分配,返回的物理页可能不连续;cma堆从CMA区域分配,返回的是连续物理内存。RGA虽然有自己的MMU可以处理不连续地址,但连续内存让一轮总线burst能更饱和,实际吞吐更高,延迟也更稳定。
从优化角度,我的习惯是:需要长时间复用的帧缓冲尽量用cma堆,并且初始化时多分配几块做缓冲池;临时小尺寸、调试用途的分配可以用system堆。这里有一个容易忽略的坑:CMA区域如果被其他驱动占用太满,分配会阻塞甚至直接失败。所以不要在运行过程中频繁分配释放,尤其是大块buffer。常见做法是启动阶段把整个池子建好,后面只做拿fd和放fd。
另外要注意dma-buf的生命周期。以往有个项目按帧分配dma-buf,一秒30帧连续跑,几分钟后系统卡死。排查下来不是CPU内存泄露,而是某个fd在驱动之间引用计数没理清楚,导致CMA内存始终不被释放。处理办法很简单:明确buffer的ownership,谁最后用完谁负责close,不要在中断回调里回收fd。
2.3 什么时候别用RGA:和GPU/VPU/CPU的分工
RGA不是万能加速器。遇到3D变换、逐像素复杂算法、大规模卷积这类需求,应该去用GPU或NPU;视频编解码本身也由VPU负责,不应该用RGA硬扛。RGA的强项是规则、重复、数据量大的2D搬运:YUV与RGB互转、等比缩放、裁剪、旋转、简单叠加。如果算法分支判断多到一定程度,CPU加SIMD反而更快,因为RGA的流水线是为固定模式设计的。
选型时有一个判断标准:一份数据在一帧里被处理两次以上,就值得把它从“CPU写一遍、RGA读一遍”改成“RGA一次做完”。例如视频帧要先缩小、再转格式、再旋转,RGA能在一次blit里同时完成;如果你拆成三次CPU操作,总线压力和CPU占用都会上去。反过来,如果只是一次性低频率操作,比如切个壁纸,RGA带来的复杂度反而不值得,直接把任务丢给显示控制器处理更省事。
3. 可复现的第一步:dma-buf分配与一次RGA缩放/旋转
3.1 在C语言里从CMA heap分配dma-buf
先写一个最底层的分配函数。内核提供的dma-heap接口通过ioctl对外暴露,调用方只需要打开堆设备节点,填好长度和fd_flags,就能拿到一个代表物理缓冲区的fd。下面这段代码可以直接编译进你的工具里:
#include <linux/dma-heap.h> #include <sys/ioctl.h> #include <fcntl.h> #include <unistd.h> static int alloc_dma_buf(size_t len, int *fd_out) { struct dma_heap_allocation_data data = { .len = len, .fd_flags = O_CLOEXEC | O_RDWR, }; int heap_fd = open("/dev/dma_heap/cma", O_RDONLY); if (heap_fd < 0) { perror("open cma heap"); return -1; } int ret = ioctl(heap_fd, DMA_HEAP_IOCTL_ALLOC, &data); close(heap_fd); if (ret != 0) { perror("dma heap alloc"); return -1; } *fd_out = data.fd; // data.fd 就是和这块buffer绑定的文件描述符 return 0; }这里有几个参数要注意。len是字节数,不是像素数,1920×1080的NV12帧大约需要1920×1080×3/2字节。fd_flags里加O_RDWR意味着后续可以拿到读写映射;如果只给RGA做只读源,只给O_RDONLY也行,但很多驱动实现会对读写fd做相同映射,所以保持O_RDWR最稳。heap_fd打开后要立刻close,因为真正引用内存的是data.fd,而不是堆节点本身。
如果内核还没有dma-heap框架,而是老的ION接口,命令和数据结构都不一样。不过RK3588的主流内核已经切到dma-heap,你只需要确认/dev/dma_heap/cma节点存在。还有一个细节:CMA区域大小可以在设备树或内核cmdline里配,跑起来后通过/proc/meminfo里的CmaTotal和CmaFree查看余量。CmaFree长期偏小,就说明有其他驱动把CMA占满了。
3.2 用mmap和DMA_BUF_IOCTL_SYNC完成CPU写入
分配出dma-buf fd之后,CPU并不天然能访问它。需要mmap拿到虚拟地址,同时用DMA_BUF_IOCTL_SYNC来控制缓存同步。下面这段代码演示了把一片RGBA buffer清零,并确保写入被硬件看见:
#include <linux/dma-buf.h> #include <sys/mman.h> static void *map_dma_buf(int buf_fd, size_t len) { void *ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, buf_fd, 0); if (ptr == MAP_FAILED) { perror("mmap dma buf"); return NULL; } return ptr; } static void sync_dma_buf(int buf_fd, int start_or_end, int rw) { struct dma_buf_sync sync = {0}; if (start_or_end) sync.flags |= DMA_BUF_SYNC_START; else sync.flags |= DMA_BUF_SYNC_END; if (rw & 1) sync.flags |= DMA_BUF_SYNC_READ; if (rw & 2) sync.flags |= DMA_BUF_SYNC_WRITE; int ret = ioctl(buf_fd, DMA_BUF_IOCTL_SYNC, &sync); if (ret != 0) perror("sync dma buf"); }使用顺序很重要:CPU要写数据时,先SYNC_START_WRITE,然后memset,再SYNC_END_WRITE。这个END不只是告诉内核“我写完了”,它同时负责把CPU cache里的脏数据刷回物理内存。RGA去读这片缓冲之前,你不需要额外再做一次START,因为硬件直接访问物理内存;但如果紧接着用CPU读RGA的输出,就必须补一轮START_READ和END_READ。
如果省略这组同步,最常见的结果是:第一次运行图像正常,第二次开始偶尔出现半帧残影,换一次buffer后彻底花屏。这都是缓存一致性引发的问题,不是RGA计算错误。你把RGA当成一个异步引擎来看,它自己不知道CPU缓存里有什么,只知道物理地址。谁负责刷缓存,谁就要承担时序正确的责任。
3.3 调RGA的核心参数:从im2d一条调用到细节
缓存同步做完,就能把两个dma-buf交给RGA了。用户态库的常见接口是im2d,下面用一次1920×1080 RGBA到1280×720 RGBA的旋转加缩放来演示:
#include "im2d.h" #include "RgaUtils.h" int src_fd, dst_fd; unsigned char *src_map, *dst_map; size_t src_size = 1920 * 1080 * 4; size_t dst_size = 1280 * 720 * 4; alloc_dma_buf(src_size, &src_fd); alloc_dma_buf(dst_size, &dst_fd); src_map = map_dma_buf(src_fd, src_size); dst_map = map_dma_buf(dst_fd, dst_size); sync_dma_buf(src_fd, 1, 2); // START | WRITE memset(src_map, 0x80, src_size); sync_dma_buf(src_fd, 0, 2); // END | WRITE rga_buffer_t src = wrapbuffer_fd(src_fd, 1920, 1080, RK_FORMAT_RGBA_8888); rga_buffer_t dst = wrapbuffer_fd(dst_fd, 1280, 720, RK_FORMAT_RGBA_8888); IM_STATUS status = imrotate(src, dst, IM_HAL_ROTATE_90); if (status != IM_STATUS_SUCCESS) { printf("RGA rotate failed: %d\n", status); }注意imrotate的旋转参数IM_HAL_ROTATE_90表示顺时针90度旋转。如果只想缩放,用imresize更直接,参数是两个浮点比例。想在旋转同时缩放,就把它拆成两个调用,或者用task接口一次性提交多个子任务。wrapbuffer_fd里的宽高和像素格式必须与实际buffer布局严格一致,尤其NV12这类带UV平面的格式,高度要按2对齐,否则RGA会读取越界。
这个demo虽然短,但已经把整条链路跑通了:DMA-BUF分配、CPU写入、缓存同步、RGA变换。后面要做的优化和排障都基于这个最小工程展开。建议你把它封装成一个rga_test.c,留好命令行参数,后面每次调参直接跑它看返回值。
4. 性能优化三板斧:对齐、同步与流水线
4.1 对齐和stride:硬件限制 vs 驱动默认
RGA对缓冲区布局有隐式约束。不同驱动版本要求不一样,但有一条保底经验:源宽度按16像素对齐,高度按2像素对齐;stride(行字节数)按16字节对齐。业务分辨率如果是1922×1082这种非对齐值,直接丢给RGA,它可能报错或输出乱码。解决办法是把stride设成1922×4向上取整到16的倍数,但width仍写业务真实值。
| buffer类型 | 建议对齐 | 不处理时的现象 |
|---|---|---|
| RGBA/RGB源 | width按16字节对齐,stride按16字节对齐 | 边缘出现斜向条纹 |
| NV12源 | width按16像素对齐,height按2像素对齐 | 彩色通道错位,UV平面偏移 |
| 旋转目标 | 按目标stride对齐,不能直接按旋转后宽高算 | 输出尺寸正确但内容被裁剪 |
一个比较稳妥的做法是分配buffer时直接按对齐后的stride分配内存,然后用wrapbuffer_align接口把stride显式传给RGA。驱动会把stride当作实际行字节数,程序员则用逻辑宽高表示图像范围。不要偷懒只用wrapbuffer_fd,全凭驱动去猜,结果就是“本地正常、量产翻车”。
还要留意分辨率变化。旋转会把宽高交换,但stride是独立的。分配dst buffer时,dst的stride要按目标分辨率换算,不能用源的stride沿用。这个坑在从横向视频切到纵向视频时尤其明显,表现为旋转后每一行图像都有偏移,越往底部越乱。
4.2 缓存同步的正确姿势:START/END放哪里
DMA_BUF_IOCTL_SYNC的使用要围绕“CPU访问边界”来放。CPU写之前放START_WRITE,写完后放END_WRITE;CPU读之前放START_READ,读完后放END_READ。RGA本身不需要这些操作,但如果你在RGA之后用CPU去校验输出,漏了READ同步,读到的可能是旧的缓存内容。
常见的错误是把END放到了mmap之后、memset之前。想想就能明白,START和END必须成对出现,且必须包裹住实际的CPU读写区域。两个fd之间也要隔离:src同步完再end,dst在RGA完成后才start_read,顺序不能乱。
另外,不要在同一片buffer上同时做START_READ和START_WRITE,除非你明确知道自己在做read-modify-write。常见做法是分两个阶段:先READ完成校验,再WRITE准备下一次输入。合并RRWW顺序虽然合法,但会让驱动判断困难,极端情况下性能反而下降。
这里补充一个更容易翻车的细节:dma-buf的SYNC操作是per-fd的,不是per-mapping的。如果同一个fd被两个线程各自mmap,两个线程都要各自做START和END,不能认为一个线程sync完,另一个线程就能直接访问。CPU cache是按核和虚拟地址域管理的,换核跑一样可能出现脏数据。
4.3 双缓冲与批量操作:把RGA忙等时间打满
RGA每一次blit都是硬件行为,如果采用同步模式,调用返回时CPU会等待引擎完成。假设一个RGA操作耗时2ms,业务帧率30fps,CPU有大约60ms的时间在空等。优化办法是双缓冲:准备两个dma-buf,CPU往A写,RGA读B;下一帧CPU写B,RGA读A,两端交错开。
// 假设有两个已分配的buffer,交替使用 int current = 0; for (;;) { // 写current对应的src并同步 sync_dma_buf(src_fd[current], 1, 2); memset(src_map[current], value, src_size); sync_dma_buf(src_fd[current], 0, 2); // 等前一轮的RGA完成,再加下一个 wait_for_rga_idle(); // 提交当前轮次,RGA读src[current],写dst[current] submit_rga(src_fd[current], dst_fd[current]); current = 1 - current; }真正要把带宽打满,还可以一次提交多个无关的RGA操作,让引擎内部流水线重叠。比如同时缩放两路视频、旋转一路图片,把它们拼到一个任务列表提交,比逐次同步调用快很多。前提是src和dst的dma-buf互不重叠,并且CPU侧已完成所有写同步。
这里有个细节:双缓冲的CPU端必须等到RGA真正读完了上一版本才能写,否则会出现“写了一半,RGA读到新旧混合”的竞态。常用的握手信号是等待RGA的completion事件,或者用一个固定循环次数的fence变量。不要用sleep去猜,休眠时间依赖系统调度,猜不准,特别是在多核负载波动时。
5. 避坑记录:DMA/RGA调试里最容易翻车的地方
5.1 先按“现象→原因→解决”建一套定位方法
调试DMA/RGA,最大的敌人是“现象不稳定”。你看到的是花屏、偶尔黑帧、IOCTL报错,但根因可能分散在内存分配、缓存同步、RGA参数三处。我的习惯是把输出分三类。第一类是ioctl直接返回错误码,最好查,多半是参数格式或对齐问题。第二类是调用成功但图像内容错乱,多半是stride、宽高或颜色空间不匹配。第三类是调用成功且单帧正确,但连续运行偶发卡顿或崩溃,多半是缓存同步或fd生命周期问题。
按这个分类去查,不要一开始就怀疑硬件坏了。RGA是相对简单可靠的数字模块,只要电源和时钟正常,出问题几乎都是上层传参问题。花半小时把日志格式统一起来,比闷头改参数高效得多。
5.2 五个高频坑的逐个复盘
第一个坑:RGA调用返回Invalid argument。现象是imresize返回EINVAL,log里能看见驱动打印“width out of range”。原因是width小于2或高度小于2,或者格式枚举非法。解决方法是进入调用前先校验width和height,并把RGB888和RGB8888这类容易混淆的枚举分开。NV12要确保三平面长度一致,否则驱动读UV越界后返回错误。
第二个坑:输出图像有规律性条纹。现象是缩放后的图像每隔几行出现一条斜向缺线。原因是stride不对。例如你按1920×1080给buffer分配了len,但stride没有按16字节对齐,RGA按错误的行步长读取时就会错位。解决方法是分配时用ALIGN_UP(width * bpp, 16),调用im2d时把stride显式传过去。修完这个坑,条纹会立刻消失,不需要怀疑是硬件问题。
第三个坑:旋转后图像方向对,但坐标反了。现象是内容完整但呈镜像,或者左上角内容跑到右下角。原因是旋转参数和驱动版本的坐标定义不一致。解决方法是改用im2d的task接口,在task里分别设置src_rect和dst_rect,明确指定目标起始坐标,不要依赖默认rect。
第四个坑:用普通malloc申请内存传给RGA,性能不升反降。现象是同样大小的buffer,RGA操作耗时从2ms涨到20ms。原因是没有通过dma-buf分配,驱动为了让它能被硬件访问,内部做了同步拷贝,或者MMU每次都要重新映射。解决方法是统一改成dma-buf fd传入,不要用virAddr裸传。virAddr方式不是完全不能用,但那是给性能要求低的调试场景准备的。
第五个坑:双缓冲跑了几分钟开始黑帧。现象是前1000帧正常,之后突然出现整帧黑屏,随后又恢复。原因是某个fd在同一帧里被CPU和RGA同时访问,缓存同步批次错位。解决方法是检查日志里的同步顺序,确保CPU写src后END_WRITE,再交给RGA;RGA写dst完成后,CPU读dst前补START_READ。同时检查dma-buf引用计数,千万不要重复close同一个fd,否则会发生use-after-free,表现就是偶尔黑一帧后驱动彻底卡死。
6. 把优化的账算清:验证带宽之后还能怎么继续榨
6.1 先做两件验证:用真实耗时和dmesg确认改动生效
任何优化改动,都要用同一份数据跑A/B对比,不要凭感觉。我在本地写了一个最小benchmark:分配两块dma-buf,循环100次RGA缩放,分别统计总耗时。在RK3588上,1920×1080 RGBA缩到1280×720,同步调用单帧大概在2ms到4ms之间波动。如果某次改动后这个数字突然降到10ms以上,基本可以断定走了CPU回退路径,立刻检查stride和cache sync。
同时要养成看dmesg的习惯。RGA驱动在遇到非法参数或内部错误时,会打印类似“rga2 job timed out”的日志。这些日志能直接告诉你失败发生在哪一层,比在业务代码里printf猜高效得多。把dmesg和业务日志都带上时间戳,才能把黑帧那类偶发问题对齐到同一时刻分析。
6.2 和V4L2/NPU衔接:零拷贝还要跨模块共享
走到这一步,你已经能控制好RGA本身。真正落到项目里,还需要把dma-buf fd递交给其他模块。常见做法是:VPU解码输出一个dma-buf,RGA把它作为源做缩放,然后同一片buffer再作为V4L2输出平面的fd,或直接给NPU做输入。关键是把buffer的所有权设计成“谁在用谁负责引”。我在项目里定了两条铁律:一是在同一帧内,任何模块都不能在另一个模块还没结束时就释放fd;二是每次跨模块传递,接收方都要立刻增加一次fd引用。这几条规则能避免大多数偶发崩溃。
再往前一步,如果DPU支持直接显示RGA的输出,你可以把RGA的dst buffer直接设置成显示plane的framebuffer,这样连“RGA完成后拷贝给显示”都省了。这就是RK3588平台上真正的零拷贝闭环。跑通这条链路之后,再去调整分辨率切换、多路输入这些边界场景,你会觉得整个内存模型尽在掌握。
回忆我踩过最深的一次坑,就是误以为dma-buf的mmap地址可以跨线程随便传,结果缓存同步线程和RGA线程没有统一顺序,项目联调时花了两天才定位到是SYNC_END漏了一次。后来我把所有dma-buf操作封装成一个自测工具函数,每次提交前自动校验sync标志位,再没在黑帧上折腾过。希望这些经验能帮你在RK3588上少走几步弯路,把DMA和RGA真正打磨成顺手的东西。
本文还有配套的精品资源,点击获取