☰
瑞芯微RGA2实战:NV12缩放与格式转换的硬件加速方案
2026/10/7 10:45:09 网站建设 项目流程

搞嵌入式Linux的视觉项目,最容易被卡脖子的地方往往不是算力不够,而是数据搬运和格式转换太慢。ISP输出NV12,显示要RGB,编码器要YUV,中间还有缩放、裁剪、镜像,这一堆操作如果全扔给CPU,跑到RK3568这种四核A55上,一帧1080p的NV12转RGB888就能吃掉十毫秒上下,再叠加缩放和丢帧,整个视频链路基本没法看。瑞芯微SoC里几乎都内置了一个叫RGA2的2D硬件加速模块,专门消化这类像素级操作,把CPU从这种机械劳动里解放出来。这篇东西从原理到代码一次讲透,重点围绕NV12的缩放与格式转换展开,正在搞IPC、边缘盒子、智能摄像头或者学习嵌入式图像处理的人可以直接照着用。

1. 瑞芯微RGA2到底是什么

1.1 被性能瓶颈逼出来的硬件加速

先说个我自己的场景。有一段时间我在调一块RK3568的板子,接了一个500万像素的摄像头,ISP直接输出NV12,1080p@30fps。当时的处理链路是:先把NV12缩小到720p,再转成RGB888送给算法模块,同时还要把原始NV12送给编码器。一开始这些缩放和转换全在CPU里做,用的还是带NEON优化的代码,结果主线程一跑起来CPU占用直接飙到40%以上,稍微再叠加几路视频流,系统就开始掉帧。

后来把目光转向RGA2,问题一下就解决了。RGA2全称是Raster Graphic Acceleration Unit,是瑞芯微SoC内部专门做2D图像处理的硬件加速器。它不像GPU那样动不动就几十瓦,也不像NPU那样专门跑神经网络,它做的事情非常纯粹——缩放、旋转、裁剪、镜像、格式转换、颜色空间转换。你给它一个源buffer和一个目的buffer,再告诉它你要做什么操作,它通过DMA把数据拉进来,硬件流水线处理完再写回内存,全程几乎不需要CPU参与。

在RK3128、RK3288、RK3399、RV1126、RV1106、RK3568、RK3588这些常见的瑞芯微芯片里,基本都能找到RGA2或者更新一代的RGA。也就是说,只要你用的是瑞芯微主控,这套加速能力几乎是白送的,不用白不用。

1.2 RGA2核心能力与适用边界

我把RGA2在手头几个平台上的能力整理成一张清单,方便你对照自己的需求:

  • 格式转换:NV12、NV21、YUV420、YUYV、RGB565、RGB888、RGBA8888、BGR888等常见格式之间互转,YUV和RGB互转是它最常用的场景。
  • 缩放:任意比例缩放,插值算法由硬件决定,常见的线性插值和双线性插值效果够用。
  • 旋转和镜像:支持0度、90度、180度、270度旋转,支持水平、垂直镜像,这些操作在摄像头方向矫正里非常实用。
  • 裁剪与拼接:可以只取源图的一块区域进行处理,也可以把多个输入合成到一个输出。
  • 颜色填充和Alpha叠加:做UI合成、水印叠加都能用。

但要注意,RGA2不是万能的。3D渲染、复杂的空间滤波(大卷积核)、高精度的图像算法(比如人脸关键点检测、OCR)都不是它的职责范围。这些要么交给GPU,要么交给NPU,要么就在CPU上做。我对RGA2的定位很简单:凡是那种“每个像素都要算一遍、但算法逻辑又不复杂”的操作,优先丢给它。

1.3 RGA2与GPU、VPU的分工差异

很多刚入行的朋友容易把RGA2和GPU、VPU搞混,这里我做一个简单的划分。GPU侧重3D渲染和并行浮点计算,跑游戏、跑OpenGL/Vulkan,做通用计算的也有,但驱动复杂、功耗高;VPU侧重视频编解码,H.264/H.265的编码和解码一般走它;RGA2则是2D图像处理的搬运工和格式转换器,它最擅长的是“把数据从一个格式变成另一个格式、从一个尺寸变成另一个尺寸”。

举个不严谨但好理解的类比:GPU是一个全能型建筑师,VPU是一个标准化工厂,RGA2则是一个高效整理员。你在摄像头采集链路里最需要的其实是整理员,因为它能把ISP吐出来的NV12快速整理成算法模块喜欢的样子,而不必每次都在CPU里手工完成那些重复劳动。

2. NV12图像格式底层拆解

2.1 NV12内存排布与尺寸计算

NV12属于YUV420采样格式中的一种半平面(semi-planar)模式。说到YUV420,核心意思是每4个Y分量共享一组U和V分量,也就是2x2的像素块中只有一个U和一个V。这种降采样方式符合人眼对亮度敏感、对色度不敏感的特性,所以视频领域非常喜欢用它。

具体到NV12的内存布局,它分为两个平面(plane):

  • Y平面:单独存放所有亮度数据,长度是 width * height。
  • UV平面:紧跟在Y平面之后,U和V交错排列,先U后V,一行U接着一行V,总长度是 width * height / 2。

一帧NV12图像的总字节数就是 width * height * 3 / 2。举例来说,1080p(1920x1080)的NV12大小是 1920 * 1080 * 3 / 2 = 3,110,400 字节,约2.97MB。而同样的分辨率下,RGB888是 1920 * 1080 * 3 = 6,220,800 字节,约5.93MB。也就是说,NV12能比RGB888省一半内存带宽,这对嵌入式平台来说非常宝贵。

在代码里,如果你要手动填充一个NV12 buffer,Y平面的数据直接从地址0开始,UV平面则从 offset = width * height 的位置开始。很多新手第一次写代码时容易在UV平面的偏移上出错,导致画面颜色完全错乱。

2.2 stride对齐是绕不过去的坎

NV12还有一个很容易踩的坑,就是stride对齐。stride(也叫pitch)指的是实际存储中一行的字节宽度。由于硬件DMA传输和cache line对齐的要求,一行Y数据的实际字节数往往不是简单的width,而是向上对齐到16字节或64字节的整数倍。

举个例子,一张宽度为640的NV12图片,如果按16字节对齐,那么一行的实际stride可能是640或者640向上对齐后的值。如果640刚好是16的倍数,就没问题;但如果是642,那就要对齐到656了。这时候,Y平面每一行数据的末尾会有几个无用字节,UV平面的stride也要相应变化。

在RGA2驱动的参数里,wstride就对应这个对齐后的宽度值,hstride对应对齐后的高度值。如果wstride设置得和实际buffer不符,轻则画面歪斜,重则直接花屏或返回错误。我遇到的花屏问题里,起码有一半是stride没对上。

2.3 为什么视频链路都默认NV12

从ISP到编码器、从显示到AI预处理,NV12几乎是无处不在的中间格式。硬件ISP天生就输出YUV420类格式,因为传感器采集的是Bayer数据,经过ISP去马赛克后直接转成YUV,NV12又是其中带宽最友好的选择;而H.264/H.265编码器内部做运动估计、变换量化时也是以YUV为处理对象,NV12可以直接作为输入,省去额外转换。

换句话说,在瑞芯微平台上,NV12这种格式是“硬件原生支持”的。你在做系统方案时,尽可能让数据以NV12的形式在模块之间流转,只在必要时做一次转RGB,这样能最大限度减少拷贝和带宽消耗。这也是为什么这篇实战文章要专门围绕NV12来讲——它是整个链路里的“通用语”。

3. RGA2的硬件加速原理拆解

3.1 RGA2内部是怎么工作的

RGA2的工作流程,本质上是一条硬件流水线:数据源头通过DMA从DDR读到内部SRAM,然后经过颜色空间转换、几何变换、缩放滤波等处理单元,最后再通过DMA写回DDR。整个过程由寄存器配置控制,CPU只负责下发任务,不参与像素计算。

从软件视角来说,我们做的就是填充一个任务结构体,告诉RGA2源buffer和目的buffer的地址、格式、宽高、裁剪区域、变换类型,然后通知硬件开始执行。RGA2一旦启动,CPU可以去做别的事情,相当于把原来阻塞式的图像处理变成了异步任务。

这种设计带来的直接好处是,图像处理的时间和CPU负载被解耦了。你用CPU做一次1080p的NV12转RGB888,可能要真实占用一个核心跑好几毫秒;用RGA2的话,CPU只花不到零点几毫秒配置一下寄存器,剩下的时间都在干别的活。对于需要同时处理多路视频的系统,这种解耦非常关键。

3.2 为什么RGA2比CPU快那么多

CPU做图像处理,本质上是取指令、解码、执行,一条SIMD指令哪怕一次能处理16个像素,也还是要一个像素一个像素地过寄存器,而且内存访问要经过cache,cache miss的时候性能会急剧下降。RGA2是专用硬件,它把缩放、格式转换这些操作固化成了专门的电路,数据一进来就按流水线方式流过各个单元,不会有多余的指令开销。

另一个优势是带宽利用率。RGA2的DMA控制器知道整个图像的具体布局和stride,可以按最优的方式突发读取和写入,减少总线冲突。CPU面对未知的内存访问模式,很难做到这么高效。所以同样是NV12转RGB,硬件的吞吐量通常比CPU高好几倍。

我在RK3568平台上做过一个简单的对比测试,结果大致如下:

处理任务(1080p输入)CPU耗时(A55 @ 1.5GHz左右)RGA2耗时
NV12转RGB888约8~15ms约2~3ms
NV12缩放至720p约4~8ms约0.5~1.5ms
NV12转RGB888并缩放至720p约12~20ms约1.5~3.5ms

注意这只是我手头平台的实测经验值,不同芯片主频、内存频率和buffer是否对齐都会影响结果。但趋势是一致的:RGA2耗时基本只有CPU的1/5到1/10,而且CPU基本闲着。

3.3 RGA2与OpenCV在嵌入式环境的选择

很多朋友习惯在电脑上用OpenCV做图像处理,到了嵌入式平台上也想沿用,直接调用OpenCV的cvtColor和resize。在PC上这不是问题,因为PC的CPU性能太强了;但在RK3568这类板子上,OpenCV的CV_8UC3转CV_8UC2这类操作会消耗大量CPU时间,一旦视频流路数多起来,整个系统立刻陷入卡顿。

我的建议是,嵌入式环境下能用RGA2就不动用OpenCV做像素级操作。OpenCV仍然可以用,但应该把它用在RGA2不擅长的上层算法上,比如轮廓分析、特征提取、目标检测的后处理。底层的缩放、格式转换、镜像旋转,统统交给RGA2。这样分工明确,系统性能和开发效率都能兼顾。

4. NV12图像处理实战全流程

4.1 环境准备:从内核驱动到librga

在动手写代码之前,先确认板子上的RGA驱动正常加载。大多数瑞芯微BSP里RGA的设备节点是 /dev/rga,查看是否存在:

ls -l /dev/rga

如果能看到一个字符设备,说明驱动已经加载。再用dmesg看一下RGA驱动的初始化信息:

dmesg | grep -i rga

正常情况下会看到类似“rga2: Driver initializing”的日志。如果没有,可能需要检查设备树里RGA节点的状态,确保status是“okay”。以RK3568为例,设备树里RGA节点大概长这样:

rga: rga@fdb00000 { compatible = "rockchip,rga2"; reg = <0x0 0xfdb00000 0x0 0x1000>; interrupt = <GIC_SPI 91 IRQ_TYPE_LEVEL_HIGH>; clocks = <&cru ACLK_RGA>, <&cru HCLK_RGA>; clock-names = "aclk", "hclk"; status = "okay"; };

如果你用的是较新的SDK,RK3568的RGA节点可能是“rockchip,rga3”或“rockchip,rga2”,具体以实际设备树为准。驱动没问题后,还需要在应用层链接librga(老名字叫librga,新版叫librga-im2d)。大部分瑞芯微BSP会直接带这个库和头文件,如果工程里没有,可以从SDK的external/librga目录下提取源码编译,或者用buildroot/yocto直接添加对应包。

4.2 实战一:NV12缩放并转RGB888

这一节给出一段可以直接落地的代码。场景是:有一块NV12的1080p buffer,需要缩放成720p并转换成RGB888,结果放到另一块buffer里。代码基于老版RGA API的 rga_blit 函数,这套接口在瑞芯微各个SDK里很常见。

#include <stdio.h> #include <string.h> #include <errno.h> #include <rga/RgaApi.h> // 以 1920x1080 NV12 输入,输出为 1280x720 RGB888 int nv12_resize_to_rgb888(int src_fd, int dst_fd) { rga_info_t src, dst; int ret; memset(&src, 0, sizeof(src)); memset(&dst, 0, sizeof(dst)); // 源图像:NV12,1080p src.fd = src_fd; src.mmuFlag = 1; src.wstride = 1920; // 如果实际buffer有对齐,改成对齐后的宽度 src.hstride = 1080; // 同理,改成对齐后的高度 src.format = RK_FORMAT_YCbCr_420_SP; // NV12 src.rect.xoffset = 0; src.rect.yoffset = 0; src.rect.width = 1920; src.rect.height = 1080; // 目标图像:RGB888,720p dst.fd = dst_fd; dst.mmuFlag = 1; dst.wstride = 1280; dst.hstride = 720; dst.format = RK_FORMAT_RGB_888; dst.rect.xoffset = 0; dst.rect.yoffset = 0; dst.rect.width = 1280; dst.rect.height = 720; ret = rga_blit(&src, &dst); if (ret) { printf("rga_blit failed, ret=%d errno=%d(%s)\n", ret, errno, strerror(errno)); return -1; } return 0; }

代码核心就是填充 src 和 dst 两个 rga_info_t 结构体。src_fd 和 dst_fd 建议使用dma-buf的文件描述符,而不是普通的堆内存地址。在Linux平台上,一般通过DMA heap分配,代码如下:

#include <linux/dma-heap.h> #include <fcntl.h> #include <sys/ioctl.h> int alloc_dma_fd(int size) { 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; alloc.fd_flags = O_RDWR | O_CLOEXEC; int ret = ioctl(heap_fd, DMA_HEAP_IOCTL_ALLOC, &alloc); close(heap_fd); if (ret < 0) { perror("dma heap alloc failed"); return -1; } return alloc.fd; }

调用方式也很简单:

int src_fd = alloc_dma_fd(1920 * 1080 * 3 / 2); int dst_fd = alloc_dma_fd(1280 * 720 * 3); nv12_resize_to_rgb888(src_fd, dst_fd); // 用完后关闭fd close(src_fd); close(dst_fd);

注意,NV12的buffer大小是 width * height * 3 / 2,RGB888的buffer大小是 width * height * 3,千万别少申请。

4.3 实战二:裁剪、旋转与镜像操作

缩放和格式转换是最高频的场景,但实际项目中,裁剪和旋转同样常见。比如摄像头是竖着装的,画面方向不对,就需要把图像旋转90度或者180度。

在RGA2里,旋转和裁剪也是通过修改 rga_info_t 里的 rect 和 rotation 相关字段实现的。还是沿用上面的例子,假设我要从1080p的NV12源图中间裁剪出一块 960x540 的区域,并且旋转90度输出到RGB888缓冲区:

src.rect.xoffset = 480; src.rect.yoffset = 270; src.rect.width = 960; src.rect.height = 540; dst.rect.xoffset = 0; dst.rect.yoffset = 0; dst.rect.width = 540; // 旋转90度后宽高互换 dst.rect.height = 960;

旋转参数的设置在不同SDK里略有不同,部分较老的驱动是在 rga_info_t 的 rotation 字段直接填值,比如:

  • 0:不旋转
  • 1:旋转90度
  • 2:旋转180度
  • 3:旋转270度

我建议先查清楚你SDK里rotation枚举的具体定义,再配合简单的小图测试确认效果,因为个别版本对旋转方向的解释不太一样。

4.4 关键参数逐项说明

刚接触RGA的朋友看这些结构体往往一脸懵,这里把最核心的几个字段单独列出来讲清楚。

  • fd:数据所在buffer的dma-buf描述符。建议统一使用fd方式,兼容性最好。
  • mmuFlag:置1表示使能MMU,配合fd使用时通常必须为1。如果虚拟地址方式的话容易出错,不建议在正常项目里这么干。
  • wstride/hstride:实际buffer一行到底有多少像素、一列到底有多少像素。如果buffer做了16字节对齐,这里要填对齐后的值,而不是有效宽度。如果没对齐,可以填成和rect.width/height一样,但后续扩展时容易出问题。
  • rect:真正参与计算的源区域和目标区域。xoffset/yoffset表示从哪个点开始取,width/height表示取多宽多高。目标rect的width/height决定最终输出尺寸。
  • format:像素格式枚举。NV12对应RK_FORMAT_YCbCr_420_SP,NV21对应RK_FORMAT_YCrCb_420_SP,RGB888对应RK_FORMAT_RGB_888。格式填错,输出图像的颜色就会完全不对。

我自己调试时的经验是,先用最简单的crop操作跑通,再逐步增加格式转换和缩放,这样一旦出错,能快速定位是格式问题还是区域问题。

5. 常见问题排查与性能调优

5.1 高频故障速查表

整理了我做RGA2开发时踩过以及帮别人排查过的一些高频问题,直接对照着看能省不少时间:

现象可能原因解决办法
画面偏绿NV12和NV21格式搞混,U/V顺序反了确认使用RK_FORMAT_YCbCr_420_SP,不要错用420_SP对应的NV21
花屏、斜纹wstride设置和实际buffer不对齐把wstride改成对齐后的值,并确保buffer每行stride一致
rga_blit返回错误fd类型不对、mmuFlag未置1使用dma-buf fd,并设置mmuFlag = 1
输出图像比例变形只设置了rect.width/height,未正确设置wstride/hstride按buffer真实对齐宽度设置wstride
图像旋转方向不对SDK不同版本的rotation枚举含义不一致先用小图逐个rotation值测试,确定方向映射关系
转出来的RGB颜色偏暗未做YUV到RGB的full range / limited range处理根据数据源是BT.601还是BT.709、是否limited range手动补充处理,RGA不一定帮你做
多线程调用时偶发失败RGA任务同步问题,同一fd被并发读写对RGA调用加锁,或者保证同一buffer在RGA处理未完成前不被释放和改写

颜色偏绿的问题我碰到过最多次。很多同事从OpenCV转过来,习惯把NV12叫成YUV420,结果在RGA格式枚举里粗心选成了NV21,出来就是一片偏绿。排查时只要把format改回来就正常了。

5.2 调试三板斧

遇到问题别乱猜,按下面三个顺序来查,绝大部分坑都能填上。

第一,确认buffer的物理分配方式。RGA2驱动处理fd模式的buffer最稳定,你如果图省事用malloc出来的虚拟地址,在某些内核版本上会失败,或者出现无法解释的卡顿。好用数据量不大但神秘的错误,优先换dma-buf重新分配。

第二,确认stride。把一幅NV12原图直接保存成文件,用Python或者工具打开,先看原始数据是否完整。再对照驱动日志和wstride设置,检查是否对齐。许多花屏问题本质上是存储布局和计算布局不一致。

第三,最小化测试用例。先做同格式的copy,再做crop,再加缩放,再加格式转换。每一步都单独验证,不要第一次就直接上全功能。这样出问题时,你能立刻知道是哪一步引入的。

5.3 性能优化心得与后续扩展

代码跑通只是第一步,真正上线还需要注意几个细节。

尽量复用dma-buf。RGA2本身很快,但频繁alloct和close dma-buf会带来不必要的开销,对实时性要求高的场景影响尤其明显。正确做法是在初始化阶段就分配好一组输入输出buffer,用来回交替使用。

注意与编码器的配合。在视频编码场景下,RGA2经常被用来把ISP输出的NV12缩放到编码器需要的分辨率。这里可以让RGA2输出直接使用VPU分配的buffer,这样编码器拿到手就能用,省掉一次memcpy。很多瑞芯微方案里,VPU输入和RGA2输出都支持dma-buf,天然具备零拷贝条件。

多路并发时加锁。RGA2在同一时刻只能处理一个任务,多线程同时调用rga_blit需要串行化。我通常用一个简单的互斥锁包住整个调用,或者在主线程里统一提交RGA2任务,避免并发冲突导致偶发的花屏或返回错误。

在更复杂的项目里,你还可以把RGA2与RGA3、RGA等等其他硬件单元组合使用,实现多路视频拼接、画中画、水印叠加等高级功能。但无论功能怎么加,底层思路不变:所有像素级的重复劳动,都尽可能交给专用硬件。

我自己实际操作中的体会是,用RGA2处理NV12最大的价值不是省那几毫秒,而是让CPU的占用率陡然下降,系统整体稳定性明显提升。刚开始接入RGA2时,建议不要急着做复杂功能,先把最简单的缩放跑通,确认颜色和尺寸都正确,再逐步叠加格式转换和旋转。等熟悉了这套接口,你会发现图像处理链路的开发效率比纯CPU方案高出一大截。

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

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

立即咨询