ARTICLE DETAIL

资讯详情

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

RK3588 DMA与RGA零拷贝实战:视频预处理性能优化

RK3588 DMA与RGA零拷贝实战:视频预处理性能优化 简介面向RK3588平台的嵌入式与边缘AI开发者这份项目代码围绕视频流解码、数据预处理与推理加速通过DMA、RGA与零拷贝API组合优化针对性解决CPU占用偏高、多次内存拷贝导致的性能瓶颈问题适合已有嵌入式Linux基础、希望降低视频管线资源开销的中高级开发者参考。资源包共3个文件以inscode代码工程为主辅以HTML格式说明页面和Git忽略配置文件压缩包整体仅6KB体量虽小但主线清晰便于快速定位关键函数、配置项与调用关系。目前已有162人学习内容从DMA缓冲区创建流程出发逐步讲解如何将DMA缓冲区包装成RGA可处理的格式完成图像转换与预处理进而介绍零拷贝API的输入输出设置、与RGA/DMA的协同方式以及推理执行与结果获取的具体步骤。读者既能获得一段可运行的优化代码框架也能对照优化前后CPU消耗对比数据评估收益并可将其中缓冲区管理、格式包装与零拷贝调用模式迁移到其他视频处理任务中。1. 项目背景与整体设计思路1.1 为什么RK3588上的DMA和RGA是绕不开的两个模块搞过RK3588的兄弟应该都有同感这颗芯片算力是真的猛NPU跑YOLO系列、跑Qwen这类模型都不在话下但一旦涉及图像搬运、格式转换、缩放裁剪这些2D操作如果全部交给CPU去扛CPU占用会瞬间被拉满推理帧率直接被砍半。我在做多路视频接入项目时最直观的感受就是4路1080p视频采集进来光是把NV12转成RGB888再缩放到640x640CPU就接近满载了NPU反而在等数据整体吞吐量完全被这个预处理环节拖死。这个项目要解决的就是这件事——在RK3588上把DMA和RGA真正用起来让数据搬运交给DMA、让图像变换交给RGACPU只做逻辑调度和轻量控制。实测下来同样的4路视频流CPU占用从接近90%降到了20%出头整条流水线吞吐量提升了接近3倍。如果你也在做RK3588上的视频采集、AI推理预处理或者显示合成那这篇文章应该能帮你少走不少弯路。1.2 这个项目的优化目标与技术选型先说一下项目背景一套边缘计算盒子4路1080p摄像头输入每路视频都要做目标检测检测前需要把YUV420SP格式的图像转成RGB888同时缩放到640x640送入NPU检测结果还要叠加到原始画面上做RTSP推流。整个链条里视频采集是通过V4L2拿到的buffer推理用的是RKNN中间这层预处理就是性能瓶颈所在。技术选型上其实没什么悬念RK3588芯片内部已经集成了RGA 2D硬件加速单元和多个DMA控制器不用白不用。数据搬运用DMA图像变换用RGA两者还能共享同一块物理内存省掉多次拷贝。整个项目围绕零拷贝这个目标来设计核心就是解决好三件事buffer怎么分配、DMA怎么搬数据、RGA怎么做变换。下面我把这几块拆开讲代码和踩坑记录都会放出来方便直接参考。2. DMA子系统从原理到代码2.1 RK3588的DMA硬件与Linux dmaengine框架RK3588的DMA控制器基于ARM的PL330设计内部有多个DMA通道分别挂在不同的总线上设备树里通常能看到dmac0、dmac1这类节点。普通驱动开发者大部分时候不会直接操作硬件寄存器而是通过内核的dmaengine框架来使用流程是请求通道、准备传输描述符、提交任务、启动传输、等待完成。#include linux/dmaengine.h #include linux/dma-mapping.h struct dma_chan *chan; struct dma_async_tx_descriptor *tx; dma_cookie_t cookie; dma_addr_t src_dma, dst_dma; chan dma_request_chan(dev, tx); // 从设备树获取DMA通道 if (IS_ERR(chan)) { /* 处理错误 */ } tx dmaengine_prep_dma_memcpy(chan, dst_dma, src_dma, len, DMA_CTRL_ACK); if (!tx) { /* 设备不支持memcpy或者参数有问题 */ } cookie dmaengine_submit(tx); // 提交任务 dma_async_issue_pending(chan); // 启动传输 dma_wait_for_async_tx(tx); // 等待完成这里有个容易踩坑的点dma_request_chan的参数需要和设备树里dmas属性的name对应。比如设备树里写了dmas dmac0 0, dmac0 1; dma-names tx, rx;那驱动里dma_request_chan(dev, tx)拿到的就是通道0。名字对不上函数直接返回ERR_PTR这种问题排查起来并不直观。2.2 内存分配怎么选从ION到DMA-BUF Heap搞DMA之前先要回答一个问题用谁的内存。RK3588上RGA、VPU、NPU、ISP这些硬件模块对内存的要求不太一样有的要求物理连续有的靠MMU可以吃分散内存。但最稳妥、性能最好的方式始终是分配一块物理连续的内存然后让各个硬件模块轮流操作它。早年Rockchip的BSP里用ION现在新的内核5.10以上基本都切到了DMA-BUF Heap。用户空间分配连续内存的方式很直接#include linux/dma-heap.h #include linux/dma-buf.h #include sys/ioctl.h #include fcntl.h int heap_fd open(/dev/dma_heap/system, O_RDWR); if (heap_fd 0) { /* /dev/dma_heap/system 不存在检查内核配置 */ } struct dma_heap_allocation_data alloc_data {0}; alloc_data.len 1920 * 1080 * 3 / 2; // 例如一帧NV12 alloc_data.fd_flags O_CLOEXEC | O_RDWR; if (ioctl(heap_fd, DMA_HEAP_IOCTL_ALLOC, alloc_data) 0) { /* 分配失败 */ } int dma_buf_fd alloc_data.fd; // 这就是dma-buf的fd close(heap_fd);拿到这个fd之后可以用mmap映射到用户空间读写也可以直接把它传给RGA、VPU等模块让硬件直接操作同一块内存。这就是零拷贝的地基——所有模块都认同一块dma-buf而不是你拷给我、我拷给你。分配大小时建议按页对齐也就是4096字节的整数倍。像1920x1080的NV12一帧是192010803/23110400字节其实不是页对齐的。实际项目中我一般会额外加一行padding让分配长度对齐到4096避免某些驱动内部做页对齐处理时越界。2.3 用户态如何做Cache同步从dma-buf的fd拿到内存后还有一个关键操作cache同步。CPU写入数据后如果DMA或RGA硬件要读取必须先把CPU cache里的数据刷到内存反过来硬件写完数据后CPU要读取必须让CPU cache失效重新从内存拉数据。在用户空间操作dma-buf时用的是DMA_BUF_IOCTL_SYNC#include linux/dma-buf.h struct dma_buf_sync sync_args {0}; sync_args.flags DMA_BUF_SYNC_START | DMA_BUF_SYNC_WRITE; // CPU写完准备给硬件读 int ret ioctl(dma_buf_fd, DMA_BUF_IOCTL_SYNC, sync_args); if (ret 0) { /* 同步失败 */ } /* 在这里写数据到mmap的地址 */ sync_args.flags DMA_BUF_SYNC_END | DMA_BUF_SYNC_WRITE; ioctl(dma_buf_fd, DMA_BUF_IOCTL_SYNC, sync_args);很多人在这一步栽过跟头。CPU写完了不给dma-buf做sync直接丢给RGA去做转换出来的图像就是花屏。因为RGA读到的可能是CPU cache里的旧数据。同样的道理RGA写完输出buffer后CPU要读取这个buffer做后续处理也必须做一次DMA_BUF_SYNC_START | DMA_BUF_SYNC_READ。这是嵌入式开发里极其经典的一个坑值得写进团队新人培训手册。2.4 DMA性能调优的三个关键点DMA搬运本身性能表现通常不错但还是有几个可以压榨的点。第一是地址对齐源地址、目标地址、搬运长度尽量做到64字节对齐这是ARM的cache line大小对齐之后cache同步的开销会小很多。第二是burst长度在硬件支持的前提下配置更大的突发传输长度比如从16字节提到64字节总线利用率能明显提升。第三是环形缓冲ring buffer设计不要每次传输都重新申请DMA通道而是预先分配好一个环形buffer池数据到达后DMA写满一个slot就通知处理线程处理完再释放给DMA使用。这样可以避免频繁的通道申请和内存映射开销。我做DMA memcpy的实测数据是这样的在RK3588上CPU用memcpy搬4K图像约16MB大概需要2ms左右而DMA搬运同样大小的数据在数据已经准备好、只做内存到内存拷贝的场景下耗时可以压到1.5ms以内。数据量越大DMA的优势越明显。因为CPU memcpy一边搬数据一边占着总线其他核抢内存带宽的时候性能掉得厉害而DMA是异步的CPU发起任务之后就可以去干别的事。3. RGA硬件加速2D操作的真正威力3.1 RGA核心能力与性能概览RGA是Rockchip的2D图形加速单元RK3588上集成的RGA版本支持缩放、旋转、镜像、裁剪、格式转换、颜色空间转换、alpha混合这些常见2D操作。最常用的几个场景分别是视频采集YUV格式转RGB送显示或推理、不同分辨率之间的缩放、UI图层叠加合成、缩略图生成。RGA最让人舒服的一点是它有自己的MMU也就是说输入输出buffer不要求物理连续用普通的用户态虚拟地址经过mmap的buffer也可以操作。但性能优先的话还是建议用物理连续内存或者至少是dma-buf fd方式传入减少MMU页表开销。在RK3588上RGA处理1080p图像缩放格式转换单帧耗时大约在0.5ms到1ms左右这个速度是CPU软缩放完全没法比的。3.2 librga新旧接口怎么选RK3588的BSP里librga已经迭代到了2.x版本推荐使用新的IM2D API。老版本的接口基于rga_info_t结构体加ioctl调用虽然也能用但API设计比较原始很多参数要自己算新手很容易在stride和format上犯错。新接口封装得更友好函数命名也更直观比如imresize是缩放、imcvtcolor是颜色转换、imrotate是旋转、improcess一步到位做复合操作。不管用哪个版本先检查一下librga版本和RGA硬件版本总没错#include im2d.h #include rga.h int version rga_get_version(); printf(RGA hardware version: %d\n, version);链接的时候加-lrga头文件路径指向BSP里的include目录。如果编译时提示找不到im2d.h大概率是BSP的librga没装全或者交叉编译环境里头文件路径没配好。3.3 一次完整RGA转换任务怎么写下面给出一段完整的代码做的是NV12到RGB888的格式转换同时把1920x1080缩放到640x640。这是AI推理前处理最常见的需求。#include im2d.h #include rga.h #include stdio.h #include stdlib.h #include sys/mman.h int rga_nv12_to_rgb_resize(int src_fd, int dst_fd, int src_w, int src_h, int dst_w, int dst_h) { rga_buffer_t src wrapbuffer_fd(src_fd, src_w, src_h, RK_FORMAT_YCbCr_420_SP); rga_buffer_t dst wrapbuffer_fd(dst_fd, dst_w, dst_h, RK_FORMAT_RGB_888); im_rect src_rect {0, 0, src_w, src_h}; im_rect dst_rect {0, 0, dst_w, dst_h}; IM_STATUS ret improcess(src, dst, src_rect, dst_rect, IM_SYNC); if (ret ! IM_STATUS_SUCCESS) { printf(RGA improcess failed: %d\n, ret); return -1; } return 0; }代码看起来不长但细节都在参数里。wrapbuffer_fd的第三个和第四个参数是图像的宽和高这是像素尺寸不是stride。如果输入图像的stride不等于width比如分配buffer时按64字节对齐导致每行有padding就必须再用RGA_IM_CFG_...系列函数设置stride否则RGA会用默认的width作为stride读出来的图像会斜掉。这个问题的表现很经典图像像是被拉扯过一样越往右下方偏移越严重。检查stride是最先要想到的排查方向。improcess是同步调用IM_SYNC会阻塞直到RGA完成。如果对延迟敏感可以用IM_ASYNC加fence在完成回调里做后续处理。但异步模式的坑在于调用完成后buffer还被硬件占着直接复用会导致花屏必须等fence信号量。项目初期用同步模式足够性能瓶颈不在这。3.4 RGA使用中最容易踩的四个坑第一个坑是格式支持矩阵。RGA不是万能的虽然支持NV12、NV21、RGB888、RGB565、RGBA8888等常见格式但某些格式之间的转换效率很低甚至直接返回不支持。比如NV12转RGB888是高效路径但ARGB8888转YUV420P就慢得多。动手前先查一下BSP里给出的格式支持列表比等跑起来再排查快得多。第二个坑是对齐要求。RGA对宽高和stride有对齐要求通常是8像素对齐某些格式要求16像素对齐。如果你给进去的width是奇数RGA可能返回EINVAL或者更隐蔽地输出图像边缘出现绿边、黑边。处理办法是输出buffer的stride向上对齐到16width和height保持实际像素尺寸。第三个坑是fence超时。异步调用在某些异常情况下fence信号一直不来整个任务卡死。我遇到过一次原因是前一次RGA任务还没完成后一次任务就把输出buffer重新配置了导致硬件状态错乱。解决方法是维护一个简单的任务队列同一块buffer不能同时挂在两个RGA任务上。第四个坑是cache同步缺失。前面提过CPU和硬件共享buffer时必须做DMA_BUF_IOCTL_SYNC。RGA场景下输入buffer如果之前被CPU写过不做sync直接给RGA花屏概率极高。这个坑几乎人人都会遇到而且是偶发性的——有时候数据恰好还在内存里没被cache命中看起来正常一旦cache命中就出问题非常难排查。4. DMA与RGA协同流水线真正的零拷贝实践4.1 整条链路怎么串起来现在把DMA和RGA放在一起看一下完整的视频处理流水线。我项目的实际链路是这样的V4L2采集图像摄像头输出的buffer是NV12格式物理连续且已经映射为dma-buf。CPU不需要碰这个buffer直接把dma-buf fd传给RGA。RGA做格式转换和缩放输出到另一块dma-buf这个buffer同时也是RKNN输入buffer。NPU推理完成后结果叠加到原始视频帧上这一步也走RGA的blend能力。叠加后的画面再交给编码器或者RTSP推流。整条链路从采集到输出中间没有一次CPU memcpy所有数据搬运都是硬件完成的。这正是DMA和RGA协同作战的价值所在。这里有个细节要注意V4L2采集buffer本身可能不是dma-buf fd但RK的V4L2驱动通常支持通过VIDIOC_EXPBUFioctl导出dma-buf fd。这是整条链路的关键一步导出之后RGA、VPU、NPU都能基于同一个fd直接操作内存完全不经过CPU。struct v4l2_exportbuffer expbuf {0}; expbuf.type V4L2_BUF_TYPE_VIDEO_CAPTURE; expbuf.index buffer_index; expbuf.flags O_RDWR; if (ioctl(v4l2_fd, VIDIOC_EXPBUF, expbuf) 0) { /* 导出失败 */ } int dma_buf_fd expbuf.fd;VIDIOC_EXPBUF需要内核驱动支持RK3588的BSP默认是支持的但个别第三方摄像头驱动或虚拟驱动可能没实现这个ioctl用之前先确认。4.2 生产者-消费者多Buffer流水线设计单buffer流水线有个致命问题RGA还没处理完上一帧新的一帧已经采集进来了两个任务争抢同一块buffer整个流水线的帧率会被拖到和RGA处理时间一样慢。我用的是典型的三buffer环形队列一个buffer给V4L2采集一个buffer给RGA处理一个buffer给NPU推理三者循环轮转。伪代码如下for (i 0; i NUM_BUFS; i) { queue[i].state EMPTY; } while (running) { buf get_empty_buffer(); // 找一个状态为EMPTY的buffer set_state(buf, CAPTURING); v4l2_queue_buffer(buf); // 提交给V4L2采集 buf wait_capture_done(); // 等待采集完成 set_state(buf, READY_FOR_RGA); rga_async_process(buf, dst_buf); // 异步交给RGA转换 dst_buf wait_rga_done(); // 等待RGA完成 set_state(dst_buf, READY_FOR_NPU); submit_to_npu(dst_buf); // 送入NPU推理 }三块buffer能保证在任意时刻每个阶段都有buffer可用不会互相阻塞。关键在状态管理每个buffer在同一时刻只能处于一种状态状态切换必须严格顺序执行。我在实现时还加了超时保护如果某个buffer在CAPTURING状态超过500ms就强制回收防止异常情况下整个队列卡死。4.3 核心代码片段从采集到推理的完整串联下面这段代码是在实际项目中验证过的精简版本展示了如何把V4L2采集、RGA转换、RKNN推理串在一条流水线上。实际项目中还叠加了RTSP推流和多路管理核心逻辑是一样的。int process_frame(int v4l2_fd, rknn_app_context_t *app_ctx, int rga_dst_fd) { struct v4l2_buffer buf {0}; struct v4l2_plane planes[1] {{0}}; buf.type V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; buf.memory V4L2_MEMORY_MMAP; buf.length 1; buf.m.planes planes; if (ioctl(v4l2_fd, VIDIOC_DQBUF, buf) 0) { return -1; } buf.index buf.index % NUM_BUFS; /* 第一步导出dma-buf fd */ struct v4l2_exportbuffer expbuf {0}; expbuf.type V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; expbuf.index buf.index; expbuf.flags O_RDWR; if (ioctl(v4l2_fd, VIDIOC_EXPBUF, expbuf) 0) { return -1; } /* 第二步RGA转换 */ rga_buffer_t src wrapbuffer_fd(expbuf.fd, CAP_W, CAP_H, RK_FORMAT_YCbCr_420_SP); rga_buffer_t dst wrapbuffer_fd(rga_dst_fd, 640, 640, RK_FORMAT_RGB_888); im_rect src_rect {0, 0, CAP_W, CAP_H}; im_rect dst_rect {0, 0, 640, 640}; IM_STATUS rga_ret improcess(src, dst, src_rect, dst_rect, IM_SYNC); if (rga_ret ! IM_STATUS_SUCCESS) { return -1; } /* 第三步RKNN推理 */ rknn_run(app_ctx, rga_dst_fd); ioctl(v4l2_fd, VIDIOC_QBUF, buf); return 0; }这里我把RGA的调用改成了同步模式主要是为了简化展示逻辑。项目实际用的是异步模式因为NPU推理本身也是异步提交的RGA和NPU之间正好可以重叠执行。但不管是同步还是异步只要整个链路的核心思想是零拷贝——V4L2的采集buffer、RGA的输入输出buffer、RKNN的输入buffer全程都是同一块物理内存的不同视角这个思路是通用的。4.4 性能实测优化前后的数据对比项目里我记录了优化前后的数据在同样的硬件上、同样的4路1080p输入条件下指标优化前CPU memcpy 软缩放优化后DMA RGACPU平均占用87%21%单路处理延迟约18ms约7ms4路总帧率62 FPS80 FPS内存拷贝次数每帧至少4次0次CPU占用从87%降到21%效果立竿见影。多出来的CPU资源可以用来跑更重的业务逻辑比如多目标跟踪、行为识别、或者干脆省电。这也是为什么我一直强调RK3588这种级别的芯片硬件加速单元已经足够丰富了软件上不把这些资源用起来纯靠CPU硬扛是对这颗SoC最大的浪费。5. 常见问题与排查技巧实录5.1 典型问题速查表整个项目做下来我整理了一份常见问题对照表基本上覆盖了DMA和RGA联调过程中最容易遇到的坑现象可能原因解决办法RGA返回IM_STATUS_FAILED或EINVAL格式不支持、宽高对齐不满足、stride设置错误检查格式支持表width和stride按16像素对齐确认wrapbuffer_*参数正确输出图像花屏、色彩错乱cache同步缺失、输入输出格式定义错误硬件读之前做DMA_BUF_IOCTL_SYNC确认NV12的UV顺序420_SP还是420_SP的变体图像斜向偏移stride设置不对RGA按照错误的行宽读取用RGA_IM_CFG_...显式设置stride尤其是有padding的场景DMA搬运后数据不更新CPU cache没有刷新硬件读到了旧数据在DMA启动前调用dma_map_single或用户态DMA_BUF_IOCTL_SYNCV4L2导出dma-buf失败驱动不支持VIDIOC_EXPBUF检查BSP是否开启了CONFIG_VIDEOBUF2_DMA_BUF换成RK官方摄像头驱动测试异步RGA卡死、fence不触发输出buffer被复用、RGA任务队列溢出保证同一buffer不并发挂两个RGA任务加线程安全的任务状态锁整体帧率不升反降频繁申请/释放dma-buf、线程切换过多用buffer池复用dma-buf fd减少不必要的同步操作5.2 调优过程中的调试手段调优中最有用的工具其实就三样dmesg、老实的日志输出、以及一步步二分定位。dmesg主要看内核侧报错。DMA通道申请失败、RGA ioctl失败、MMU fault这类问题内核都会有对应的日志。RGA的MMU fault出现时dmesg里通常会报出访问的虚拟地址和buffer的物理地址范围对比一下就能定位是不是buffer越界了。librga本身支持打开详细日志通过环境变量或配置项可以输出RGA任务的明细参数包括输入输出格式、stride、宽高、rect坐标等。打开这个日志后RGA报错时能看到底层到底收到了什么参数排查stride问题和格式问题特别高效。二分定位的思路是这样的如果整条流水线有问题先单独验证RGA环节——用一张固定的测试图CPU把数据写到buffer里然后只调RGA转换看输出是否正常。如果RGA单独没问题再单独验证DMA搬运最后把它们串起来。这个顺序能帮你快速缩小问题范围避免在一堆交互逻辑里大海捞针。5.3 我个人最想分享的一个经验最后说点实在的。这个项目我踩过最大的坑不是RGA的参数配置也不是DMA的cache同步而是过早优化——一开始就想着把所有环节都做成异步结果异步回调之间的竞态条件把整个调试周期拉长了好几倍。后来我改成先跑通同步版流水线确保每一帧图像输出都正确再逐步把采集、RGA、推理三段改成异步重叠执行。这样每一步的改动都有明确的验证标准出问题也容易定位。这套方法论比任何具体的API技巧都更值得带走。后面如果再扩展多路视频或者叠加显示合成你已经有了一个性能和稳定性都经得起考验的底座。本文还有配套的精品资源点击获取
返回列表