ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

RK3588 DMA与RGA实战:从DMA-BUF到零拷贝图像处理

RK3588 DMA与RGA实战:从DMA-BUF到零拷贝图像处理 简介面向RK3588平台多媒体与边缘推理场景的优化指南配套项目代码适合具备一定RK3588使用经验或Linux驱动基础的嵌入式开发者在视频流解码、数据预处理与模型推理环节显著降低CPU占用。压缩包内共3个文件包括inscode项目配置、html说明文档及gitignore工程配置整体仅6KB轻量便于快速查阅。目前已有162人学习。具体内容先梳理DMA缓冲区的创建与映射流程涵盖内存分配、文件描述符映射及设备访问强调其减少数据搬运对CPU消耗的原理再说明将DMA缓冲区包装为RGA可处理格式借助RGA实现缩放、旋转、格式转换等高效预处理最后结合零拷贝API演示输入输出数据设置、与RGA和DMA的衔接、推理执行及结果获取。资源还提供优化前后CPU消耗对比数据便于开发者量化收益、预判优化空间为RK3588上的更多业务功能预留算力。 搞嵌入式异构计算的朋友最近两三年应该都绕不开RK3588这颗芯片。8核CPU加上6T算力的NPU还有一堆外设控制器做AI视觉盒子、边缘计算网关、视频处理板卡的方案里基本都能看到它的影子。我调试这块板子的时间不算短了有个很深的感受很多项目遇到性能瓶颈或者动不动CPU占用率飙高往往不是CPU不够快而是数据搬运和图像前后处理没做好。这里面的核心就是DMADirect Memory Access直接内存访问和RGARockchip Graphics Accelerator瑞芯微2D图形加速单元这两个功能模块。这篇文章就把我实际调试RK3588 DMA与RGA过程中用过、踩过、验证过的东西结合项目代码一次性讲清楚。这篇内容适合谁看第一类是在RK3588上做视频采集、显示、编解码发现CPU扛不住、帧率上不去的开发第二类是刚入手RK3588被DMA-BUF、mmap、RGA格式转换、cache一致性问题搞得头大的兄弟第三类是手上没有项目但想提前把这个平台吃透准备做技术预研的。不管你是哪一种这篇文章都按“为什么这么设计—关键API怎么用—实际代码怎么跑—坑在哪里”这条线来写可以直接对着操作也可以当工具文档查。1. 整体设计与思路拆解1.1 为什么在RK3588上要重点抠DMA和RGA先说一个很多人的误区认为RK3588的CPU性能强图像处理、格式转换、缩放这些事让CPU算就行了。实际上如果你真用CPU去处理一帧4K NV12转RGB再缩放到1080P在ARM核心上跑起来代价非常大。单纯内存拷贝的带宽占用都会把DDR带宽吃满更别提像素级的转换运算了。而RK3588里面的RGA硬件单元就是专门干这个活的缩放、裁剪、旋转、格式转换都是硬件完成不占用CPU算力。DMA的作用就更基础了。包括RK3588在内所有SoC的数据通路都离不开DMA——外设DMA把数据从硬件FIFO搬到内存;DMA-BUF机制则用来在多个硬件模块之间共享同一块物理内存做到零拷贝。比如摄像头sensor把一帧图像写进一块dma-buf, VPU直接从这个buf里读出来做编解码RGA再把这个buf里的图像做格式转换整个过程不需要CPU参与复制这才是高性能视频链路的正确打开方式。1.2 DMA与RGA的职责划分一个管搬运一个管处理很多新手容易把DMA和RGA搞混觉得两者都是处理数据的。我习惯用一句话区分DMA负责把数据从一个地方原封不动搬到另一个地方RGA负责把数据“加工”成另一种形态。在一条典型的视频处理链路里分工是这样的先说采集侧。RK3588的MIPI CSI或者ISP输出的原始图像通常由硬件DMA引擎把sensor的一帧数据写到指定的内存缓冲区里这就是DMA干的活。然后是处理侧由于采集到的裸数据通常是RAW、NV12这类格式而算法推理或者显示环节需要的可能是RGB888或者需要把1080P变成640x640的模型输入尺寸这一步就是RGA做的它从DMA-BUF里读原始帧经过硬件加速转换后写入另一块输出缓冲区。最后是分发侧处理完的数据还要通过DMA或者显示控制器送往目的设备比如编码器、显示屏、或者内存中的其他模块。这里想强调一点在RK3588上DMA和RGA并不是两个独立孤立的模块它们通过dma-buf这个机制紧密联动。RGA的输入输出都要求是dma-buf fd意味着你要让数据“流经”RGA就必须先学会怎么分配、导入、同步dma-buf。所以这篇文章讲的DMA优化重点不在那个搬运的外设本身而是DMA-BUF这套内存管理和同步机制它是整个零拷贝架构的地基。2. 核心细节解析与实操要点2.1 DMA-BUF一切零拷贝方案的地基在RK3588的Linux系统里你要让多个硬件设备共享内存最常见的载体就是dma-buf。它本质上是Linux内核提供的一个共享缓冲区抽象可以让GPU、VPU、ISP、RGA、Display控制器等不同设备通过文件描述符fd访问同一块物理内存而不需要每经过一个设备就复制一份数据。dma-buf是怎么分配出来的在RK3588平台上实际用得比较多的是DMA-BUF heaps接口。调用的路径大致是应用层打开/dev/dma_heap/system这样的设备节点然后用DMA_HEAP_IOCTL_ALLOC这个ioctl来分配一块指定大小的内存拿到一个fd。这个fd就是一块dma-buf的句柄后面传给RGA、VPU、编码器都可以。另一个常见途径是DRMDirect Rendering Manager的GEM接口用drmPrimeHandleToFD把handle转为fd。对一般应用开发来说DMA-BUF heaps的接口更直接、依赖更少我用得最多。使用dma-buf时有件事很多人容易忽略fd不是普通的文件fd它的生命周期管理要特别谨慎。它背后对应的是内核里的一份内存引用计数当你把fd传给别的设备驱动时驱动内部通常会用dma_buf_get来增加引用计数用完了再dma_buf_put释放。应用层这边close(fd)只代表你放弃了这份引用但如果硬件设备还在使用这块buffer内存不会真正释放这既是好事也是隐患——好处是不会因为用户提前关闭fd导致硬件访问悬空内存坏处是如果驱动实现有bug引用计数减不到零内存就会泄漏。所以我在工程上习惯在分配之后统一用一个结构体记录fd、size、映射地址等信息方便统一管理避免泄漏。2.2 RGA硬件单元它究竟能做什么、不能做什么RK3588上的RGA能力其实挺强的从spec来看RGA2和RGA3两个版本配合使用支持最大8192x8192分辨率的处理支持缩放、旋转、镜像、裁剪、格式转换、颜色空间转换等2D图形操作常见的格式像NV12、NV21、RGB565、RGB888、RGBA8888都能处理。实际项目里用得最多的几个场景我列一下把摄像头采集的NV12转成RGB888喂给NPU或者OpenCV把大分辨率图像等比缩放到模型需要的输入尺寸把图像旋转90度比如竖屏摄像头采集的画面转为横屏显示多个输入源合成为一张图画中画场景把4K画面裁剪出ROI区域再放大送到编码器但是RGA不是万能的它主要处理2D图像数据不能做卷积、滤波这类复杂的3D计算。我见过有人试图用RGA做图像增强算法这就不太合理那部分是NPU或者GPU的工作。RGA能保证的是一帧4K图像做一次NV12转RGB加缩放的耗时通常在几毫秒以内完全够实时视频链路使用。2.3 两种加速模型同步调用vs异步调用RGA的使用方式上有两种思路一种是一次调用就干完一件事比如转格式完了再缩放。另一种是把多个操作合并到一次RGA调用中比如一次blit同时完成格式转换、缩放、裁剪效率更高。我建议在应用代码里都按“一次RGA调用尽量多做几件事”的思路来写因为多次调用RGA意味着多次提交任务、多次同步等待中间的调度开销不可小觑。像im2col、直接转换这种场景能一次完成的事情就不要再拆开。还有一个点需要注意librga提供了c_RkRgaBlit这种同步接口调用之后会阻塞直到RGA完成也有RgaBlitAsync这类异步接口。同步接口逻辑清晰适合原型验证但在高帧率流水线里我一般会把多个帧的任务提交到RGA队列里异步执行然后用RgaFlush或查询完成状态来同步。这样可以让RGA一直在工作CPU在做其他事情流水线的吞吐量才会上去。3. 实操过程与核心环节实现3.1 环境准备确认librga和内核配置RK3588官方SDK里已经集成了librga路径一般在external/librga如果你用的是Ubuntu或者Debian系统可以从瑞芯微的Github仓库拉取编译。需要注意版本匹配问题librga的用户态版本要和内核里的RGA驱动版本对应否则会出现ioctl参数结构体不匹配直接返回错误。我踩过一次坑SDK的内核RGA驱动是新版但系统里装的librga还是老版本结果调用c_RkRgaBlit一直返回-22EINVAL排查了很久才发现是版本不一致。编译时在CMake里加上librga的链接路径通常还需要加上-lrga。另外确认内核里这些配置有打开CONFIG_DMABUF_HEAPSy CONFIG_DMABUF_HEAPS_SYSTEMy CONFIG_DMABUF_HEAPS_CMAy CONFIG_ROCKCHIP_RGAy可以用下面的命令检查设备节点是否存在ls /dev/dma_heap/ ls /dev/rga正常情况下会看到system、linux,cma等heap节点和/dev/rga设备节点。3.2 分配DMA缓冲区dma-buf heaps还是DRM我项目里最常用的分配方式是通过DMA-BUF heaps接口。下面这段代码展示分配一块NV12格式帧所需的缓冲区并拿到dma-buf fd#include linux/dma-heap.h #include fcntl.h #include sys/ioctl.h #include unistd.h int alloc_dma_buf(size_t size, int *fd_out) { int heap_fd open(/dev/dma_heap/system, O_RDWR); if (heap_fd 0) { perror(open dma_heap failed); return -1; } struct dma_heap_allocation_data alloc; memset(alloc, 0, sizeof(alloc)); alloc.len size; // fd_flags 可以带 O_CLOEXEC防止子进程误继承 alloc.fd_flags O_CLOEXEC; int ret ioctl(heap_fd, DMA_HEAP_IOCTL_ALLOC, alloc); close(heap_fd); if (ret 0) { perror(DMA_HEAP_IOCTL_ALLOC failed); return -1; } *fd_out alloc.fd; return 0; }如果要用CMA分配连续物理内存某些场景下硬件要求连续内存就把打开的节点换成/dev/dma_heap/linux,cma。RK3588平台上有4G和8G内存的版本CMA大小一般由内核命令行参数cma指定分配大buffer前先看一下/proc/meminfo里的CmaTotal免得分配失败。另外提一句systemheap分配的是不连续物理内存但大部分RK3588外设都支持SMMU/IOMMU所以不连续内存反而更常用也更省CMA资源。3.3 把DMA-BUF映射到用户空间拿到dma-buf fd后如果在用户态要直接读写这块内存比如填充一幅图像、读出来显示需要把fd映射到进程地址空间。这里不是用普通的mmap直接对设备节点映射而是对fd本身做mmap。操作方式如下#include sys/mman.h void *map_dma_buf(int fd, size_t size) { void *addr mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (addr MAP_FAILED) { perror(mmap dma_buf failed); return NULL; } return addr; }映射成功后就可以像操作普通内存一样往里面写数据了。但是这里有个关键问题——cache一致性。RK3588的CPU是ARM架构默认情况下CPU访问内存是有cache的而DMA设备比如RGA、VPU访问的是物理内存。如果CPU写了一份数据到buffer里没有做cache flush硬件设备去读的时候读到的可能是cache里的旧数据或物理内存里的未更新数据反过来硬件写完了数据CPU如果不做invalidate也可能读到cache里残留的旧数据。这个问题的解决方案就是dma-buf提供的sync接口#include linux/dma-buf.h static void sync_dma_buf(int fd, bool start, bool write) { struct dma_buf_sync sync; memset(sync, 0, sizeof(sync)); sync.flags start ? DMA_BUF_SYNC_START : DMA_BUF_SYNC_END; sync.flags | write ? DMA_BUF_SYNC_WRITE : DMA_BUF_SYNC_READ; int ret ioctl(fd, DMA_BUF_IOCTL_SYNC, sync); if (ret 0) { perror(DMA_BUF_IOCTL_SYNC failed); } }在任何CPU访问buffer之前调用DMA_BUF_SYNC_START访问完之后调用DMA_BUF_SYNC_END就能保证cache和硬件设备之间的数据一致性。我给团队定的规矩是用户态填充数据前startwrite填充完成后endwrite硬件处理完成后、CPU读取结果前startread读完endread。这个习惯养成之后cache导致的花屏问题基本不会再出现。3.4 调用RGA完成格式转换和缩放现在到了核心步骤用RGA把一块NV12的dma-buf转成RGB888并缩放。这里使用librga提供的接口。我给出一个完整的示例代码#include rga/RgaApi.h #include rga/RgaApi.h // 部分新版本头文件路径是 rockchip/rga.h视SDK版本而定 int rga_convert_and_scale(int src_fd, int src_w, int src_h, int dst_fd, int dst_w, int dst_h) { rga_info_t srcInfo, dstInfo; memset(srcInfo, 0, sizeof(rga_info_t)); memset(dstInfo, 0, sizeof(rga_info_t)); srcInfo.fd src_fd; srcInfo.mmuFlag 1; // 使用MMU驱动内部处理地址映射 srcInfo.rect.xoffset 0; srcInfo.rect.yoffset 0; srcInfo.rect.width src_w; srcInfo.rect.height src_h; srcInfo.rect.wstride src_w; // 注意实际stride如果宽度有对齐需要传stride srcInfo.rect.hstride src_h; srcInfo.format RK_FORMAT_YCbCr_420_SP; // NV12 dstInfo.fd dst_fd; dstInfo.mmuFlag 1; dstInfo.rect.xoffset 0; dstInfo.rect.yoffset 0; dstInfo.rect.width dst_w; dstInfo.rect.height dst_h; dstInfo.rect.wstride dst_w; dstInfo.rect.hstride dst_h; dstInfo.format RK_FORMAT_RGB_888; // 同步接口调用后阻塞直到完成 int ret c_RkRgaBlit(srcInfo, dstInfo, NULL); if (ret ! 0) { fprintf(stderr, RGA blit failed, ret%d\n, ret); return ret; } return 0; }这里几个字段容易出错我重点说明。第一是mmuFlag。设置为1时RGA驱动会通过MMU访问你把fd传进去的buffer不需要用户自己再做物理地址转换这在新版内核上是推荐的用法如果设为0你可能需要额外获取物理地址一般不建议除非你知道自己在干什么。第二是wstride和hstride。这两个字段表示的是缓冲区在内存中的实际宽度和高度单位是像素它们不一定等于width和height。比如解码器通常会把一帧1920x1080的数据按1920对齐到64的倍数也就是1920的stride存放但如果你从VPU拿到的buffer实际是按2048字节对齐的wstride要传实际的那个值否则图像会歪斜。这个坑我在从VPU解码输出直接喂给RGA时踩过VPU输出的NV12宽度stride经常不等于实际宽度导致图像错位排查半天。第三是format枚举名。RK_FORMAT_YCbCr_420_SP对应的是NV12对应NV21是RK_FORMAT_YCbCr_420_SP_10B千万不要把UV顺序搞反否则转换出来的颜色是偏的。3.5 完整链路打通DMA采集到RGA处理把前面的部分串起来一个视频链路的核心流程大致是这样一个顺序分配两块dma-buf一块存放原始采集帧一块存放RGA输出帧——把两个fd传给采集模块和RGA——采集模块的DMA把sensor图像写入原始buf——CPU端通过sync标记硬件写入完成——把src_fd和dst_fd传给RGA驱动调用一次c_RkRgaBlit完成格式转换和缩放——RGA完成后再sync一下保证CPU读取结果是新的——CPU或者NPU开始使用dst buffer。这里有一个性能关键点在整个流水线中尽量减少CPU对buffer的读写。如果只是把采集的帧转成NV12再送去编码器那么CPU根本不需要碰bufferRGA在硬件层面直接完成搬运和转换CPU只负责提交任务和接收完成信号。这时候CPU占用率几乎可以忽略带宽瓶颈主要在DDR和DMA引擎上。如果程序逻辑里有“读出buffer数据做分析”这种需求就意味着有一次DMA方向的内存拷贝性能会明显下降设计时要想清楚是否值得。4. 常见问题与排查技巧实录4.1 cache一致性导致的花屏、颜色错乱这是项目初期最典型的问题。现象是CPU往dma-buf里写入了图像数据然后调用RGA做格式转换输出的图像出现花屏、颜色错乱、偶尔有拖影。原因前面已经说过——CPU没有在写入后执行cache flush。解决办法是在写入完成后调用DMA_BUF_IOCTL_SYNC并且flag设为DMA_BUF_SYNC_WRITE。同样RGA处理完数据后如果CPU直接读取比如显示在屏幕上也要先做DMA_BUF_SYNC_READ。我在实际代码里是在RGA调用之后统一做一次read方向的sync效果稳定。这里有个小技巧有时候频繁sync会影响性能尤其是在高帧率链路里。如果缓冲区是从dma_heap system分配的而且整个链路中不存在CPU并发读写的情况可以尝试用DMA_BUF_SYNC_START只做一次直到整个生命周期结束再做END但这样做有风险除非你确认链路是单写单读且没有真正并发否则不要这么干稳定性优先。4.2 RGA调用返回-22或者其他负值c_RkRgaBlit返回-22是我碰到最多的错误也就是EINVAL。绝大多数情况下是这三个原因参数结构体没初始化比如忘记memset导致某些字段是垃圾值版本不匹配内核驱动和librga版本对应不上格式或分辨率不合法比如把宽度设成了奇数NV12要求宽度为偶数某些RGA版本还要求宽度按16/64对齐排查方法很简单在调用前把srcInfo、dstInfo的所有字段打印出来看一遍确认没有无效值其次把librga更新到和内核匹配的版本最后检查输入输出宽高是否满足要求。举个例子NV12转RGB888时如果src宽度是1920没问题但如果你做ROI且xoffset2某些RGA版本会直接报错因为RGB/YCbCr数据的xoffset要求是偶数对齐的。4.3 fd关闭导致数据丢失或崩溃有人会想我把fd传给RGA之后RGA内部是不是已经把数据复制走了答案是没有。RGA驱动只是记录了这个fd对应的dma-buf引用真正的数据还在那块buffer里RGA是直接去读那部分物理内存的。如果用户在RGA调用之后立刻close(fd)并重新分配其他buffer老buffer内存可能会被释放但硬件可能还没处理完轻则画面闪一下重则驱动崩溃。正确做法是调用RGA的同步接口后等待返回成功再close fd用异步接口时要在确认RGA完成之后再close。4.4 内存泄漏排查dma-buf泄漏比较隐蔽因为LEAK不会直接在log里打出来。我常用的排查方法是每分配一次就记录fd和大小定时查看/proc/self/fd的fd数量或者用/sys/kernel/debug/dma_buf/bufinfo查看内核里dma-buf的引用统计。如果发现分配的buffer数量只增不减优先检查驱动导入导出的路径是否正确尤其注意有没有在异常分支上漏掉close。4.5 RGA性能不达预期时的排查思路如果你发现RGA计算耗时远超理论值先确认是不是链路问题是不是CPU在每次帧处理时都在同步等待RGA完成是不是buffer没有复用每帧都重新分配是不是在RGA调用前后做了无意义的memcpy另外RK3588有两个RGA硬件单元RGA2和RGA3在多路视频流项目中可以尝试把不同流的任务分别提交到RGA2和RGA3上利用多硬件单元并行处理。这个在librga里面是通过不同的rga_info_t中的core字段来指定的默认会自动分配但手动指定在某些场景下能更好地做负载均衡。5. 写在最后的几点项目体会如果你自己在RK3588上调试过DMA和RGA肯定能理解我说的“看起来简单用起来细节决定成败”这句话。DMA-BUF的分配一个是套路化的固定模板真正体现工程水平的是对cache一致性、fd生命周期、stride对齐这些边缘情况的处理RGA调用同理几个关键参数写对就能跑但要在流水线上保持稳定不掉帧就得考虑异步提交、buffer复用、多RGA核心的调度。最后再分享一个很实际的经验。项目排期紧的时候很多人会先拿CPU做图像处理把DMA和RGA的优化放后面。这个思路我不太赞成因为一旦链路建立了再想改成零拷贝架构工作量几乎是翻倍的。我建议从第一天起就把DMA-BUF和RGA的框架搭好——即使第一版只是简单地缩放和格式转换也要让数据通路走在正确的架构上。后面优化NPU输入、接编码器、加显示通路都变得顺理成章。RK3588的潜力很大但潜力能不能兑现成产品性能往往就取决于这些底层数据通路的设计提前布局后面会轻松很多。本文还有配套的精品资源点击获取
返回列表