1. 从四路摄像头到一块屏幕:这个项目到底在解决什么问题
四路1080P摄像头同时接入,每一路都要做畸变校正、色彩空间转换、缩放,然后拼成一张4K画面输出到HDMI或者MIPI屏上——这个需求在车载环视、工业多目视觉、安防NVR、医疗内窥镜这些场景里反复出现。RK3588这颗芯片本身接口够多,MIPI CSI最多能接六路,HDMI输入也有,但真正把四路视频流拼起来还能跑满帧率,光靠CPU软处理是绝对扛不住的。我实测过,四路1080P@30fps的NV12数据,光是从内核态搬到用户态再逐像素做拼接,CPU占用直接飙到400%以上,帧率还稳不住。
所以这个项目的核心命题就一句话:怎么让RGA和GPU各干各擅长的事,把多路视频的预处理和拼接显示做成一条低延迟、低占用的硬件加速流水线。RGA(Raster Graphic Acceleration)是瑞芯微芯片里的2D硬件加速单元,专门干缩放、裁剪、格式转换、旋转、合成这类像素搬运的活;GPU(Mali-G610)则负责需要3D运算或者复杂混合模式的部分。两者协同,才能把CPU从繁重的像素处理里解放出来。
这篇文章适合谁看?如果你正在RK3588上做多路摄像头接入、视频拼接、或者任何需要把多路视频流合成一路输出的项目,并且已经意识到纯CPU方案走不通,那这篇内容就是给你写的。我会从流水线设计、RGA的配置细节、GPU混合的介入时机、内存带宽的瓶颈分析、到实际调试中踩过的坑,完整地讲一遍。代码层面以C/C++和Rockchip的librga、OpenGL ES为主,但思路对任何嵌入式视频处理项目都通用。
先给一个整体架构的轮廓,后面再逐层拆开:
- 输入层:四路MIPI CSI摄像头,通过V4L2采集,输出NV12格式,分辨率1920x1080。
- 预处理层:每路视频用RGA做畸变校正(LUT映射)、缩放(如果拼接目标分辨率需要)、色彩空间转换(NV12到RGB)。
- 拼接层:根据布局(2x2、1+3、横向1x4等)计算每路的目标区域,用RGA做区域合成或者GPU做纹理混合。
- 输出层:合成后的4K画面通过DRM/KMS直接送显示,或者编码后推流。
这个架构里,RGA承担了80%以上的像素处理工作量,GPU只在需要alpha混合、抗锯齿、或者非规则拼接时才介入。下面我从为什么这么分工开始讲。
2. 为什么RGA和GPU要分工:算力特性与带宽账
2.1 RGA到底能做什么,不能做什么
RGA在RK3588上是独立于GPU和VPU的硬件模块,它的设计目标就是高效地做2D像素操作。具体能力包括:
- 缩放:支持从1/8到8倍的缩放,双线性插值。四路1080P缩到960x540做2x2拼接,RGA单路耗时大约1.2ms。
- 裁剪与旋转:90/180/270度旋转,镜像翻转。
- 格式转换:NV12、NV21、RGB565、RGBA8888、YUV420P等之间的转换。
- 合成:支持两路源的alpha混合,但注意,RGA的混合模式比较有限,主要是全局alpha和逐像素alpha。
- LUT映射:可以做简单的色彩查找表,用于畸变校正的近似。
RGA不能做的:复杂的3D变换、多路(超过2路)同时混合、非线性的几何校正(比如鱼眼展开需要更复杂的映射)。这些就是GPU的活。
2.2 GPU在拼接里的角色定位
Mali-G610有四个核心,支持OpenGL ES 3.2和Vulkan 1.2。在视频拼接场景里,GPU的优势在于:
- 多纹理混合:可以同时绑定四路纹理,在fragment shader里做任意混合逻辑。
- 几何变换:顶点着色器可以做透视变换,适合非平面拼接。
- 抗锯齿:MSAA或者FXAA,让拼接边缘更平滑。
但GPU的劣势也很明显:功耗高、延迟大。一次GPU渲染的提交到完成,即使是很简单的quad绘制,也有几毫秒的驱动开销。所以我的原则是:能用RGA搞定的,绝不走GPU。
2.3 内存带宽的账要算清楚
四路1080P NV12,每帧数据量是1920x1080x1.5 = 3.1MB。四路就是12.4MB。30fps下,每秒的原始数据量是372MB。如果每一路都做一次RGA读+写,再GPU读+写,带宽消耗翻倍都不止。
RK3588的DDR带宽理论上是LPDDR4x-4266,约34GB/s。但实际可用带宽受限于总线仲裁和内存控制器效率,通常能到60%就不错了。所以流水线设计里,减少不必要的内存往返是核心优化点。具体做法:
- RGA直接输出到GPU能用的纹理格式,避免中间格式转换。
- 使用DMA-BUF做零拷贝共享,RGA的输出buffer直接作为GPU的纹理输入。
- 如果拼接布局允许,尽量让RGA做最终合成,GPU只做最后的显示送显。
我实测过一组数据,四路1080P到4K 2x2拼接,纯RGA方案CPU占用约15%,GPU占用5%以下;RGA+GPU混合方案CPU占用12%,GPU占用25%。看起来纯RGA更优,但纯RGA在拼接边缘有锯齿,且不支持非规则布局。所以选择取决于你的画质要求和布局复杂度。
3. 流水线搭建:从V4L2采集到DRM显示的完整链路
3.1 采集端:V4L2的多路管理
RK3588的MIPI CSI接口在Linux下通过V4L2框架管理。四路摄像头通常对应/dev/video0到/dev/video3,但具体编号取决于设备树配置。我建议用media-ctl工具先确认拓扑:
media-ctl -p -d /dev/media0这会打印出从sensor到ISP到video节点的完整链路。确认每路sensor的entity名称和对应的video设备。
采集时用非阻塞IO+mmap,每个摄像头一个独立的线程。buffer数量建议设4个,太少容易丢帧,太多增加延迟。格式统一设成NV12,因为这是RGA和ISP都最友好的格式。
struct v4l2_format fmt = {0}; fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; fmt.fmt.pix_mp.width = 1920; fmt.fmt.pix_mp.height = 1080; fmt.fmt.pix_mp.pixelformat = V4L2_PIX_FMT_NV12; fmt.fmt.pix_mp.field = V4L2_FIELD_NONE; ioctl(fd, VIDIOC_S_FMT, &fmt);注意:NV12在V4L2里是两平面格式,Y平面和UV平面分开。用MPLANE接口时,planes[0]是Y,planes[1]是UV。RGA导入时要用对应的格式标识。
3.2 RGA预处理:畸变校正与缩放
每路摄像头如果用的是广角镜头,畸变校正是必须的。RGA本身不支持完整的畸变模型,但可以通过LUT做近似。具体做法是:离线用OpenCV标定出畸变参数,生成一张映射表,把映射表转成RGA能用的LUT格式。
RGA的LUT映射通过im2d接口的imcrop或者imresize配合immap实现。但更实用的做法是:如果畸变不严重,直接用RGA做一次缩放+裁剪,把有效区域取出来,边缘的黑边用GPU或者RGA的填充功能补掉。
缩放的配置示例:
rga_info_t src = {0}; src.fd = src_dma_fd; src.mmuFlag = 1; src.rect.xoffset = 0; src.rect.yoffset = 0; src.rect.width = 1920; src.rect.height = 1080; src.format = RK_FORMAT_YCbCr_420_SP; rga_info_t dst = {0}; dst.fd = dst_dma_fd; dst.mmuFlag = 1; dst.rect.xoffset = 0; dst.rect.yoffset = 0; dst.rect.width = 960; dst.rect.height = 540; dst.format = RK_FORMAT_YCbCr_420_SP; imresize(&src, &dst, NULL, 0);这段代码把1080P缩到960x540,为2x2拼接做准备。实测单次耗时约1.2ms,四路并行(RGA支持多实例)总耗时约2.5ms,完全在33ms的帧周期内。
3.3 拼接合成:RGA的imcomposite与GPU的纹理混合
2x2拼接最简单的方式是用RGA的imcomposite,把四路缩放后的画面依次合成到一张4K的buffer上。但RGA的合成一次只能处理两路,所以需要三次合成操作:
- 左上+右上 -> 上半部分
- 左下+右下 -> 下半部分
- 上半+下半 -> 完整4K
每次合成都有内存读写,三次下来带宽消耗不小。优化方法是:如果四路画面没有重叠,可以直接用RGA的imblit把每路拷贝到目标buffer的对应区域,不需要混合。这样只需要四次blit,每次都是纯拷贝,带宽效率更高。
// 把缩放后的左上画面blit到4K buffer的(0,0)位置 rga_info_t src = {0}; src.fd = left_top_fd; src.rect.width = 960; src.rect.height = 540; src.format = RK_FORMAT_YCbCr_420_SP; rga_info_t dst = {0}; dst.fd = canvas_fd; dst.rect.xoffset = 0; dst.rect.yoffset = 0; dst.rect.width = 960; dst.rect.height = 540; dst.format = RK_FORMAT_YCbCr_420_SP; imblit(&src, &dst, NULL, 0);如果拼接布局有重叠区域,或者需要做羽化融合(比如全景拼接),那就必须上GPU。GPU的做法是把四路画面作为四个纹理,在fragment shader里根据坐标计算混合权重。
// fragment shader 简化示例 precision mediump float; uniform sampler2D texLeft; uniform sampler2D texRight; varying vec2 vTexCoord; void main() { vec4 left = texture2D(texLeft, vTexCoord); vec4 right = texture2D(texRight, vTexCoord); float blend = smoothstep(0.4, 0.6, vTexCoord.x); gl_FragColor = mix(left, right, blend); }这个shader在重叠区域做线性羽化,边缘过渡自然。但GPU渲染的延迟比RGA高,所以只在必要时用。
3.4 显示输出:DRM/KMS的直接送显
合成后的4K buffer通过DRM/KMS直接送显示,避免经过X11或Wayland的额外拷贝。RK3588的显示控制器支持多层叠加,可以把合成buffer作为primary plane,OSD作为overlay plane。
drmModeSetPlane(fd, plane_id, crtc_id, fb_id, 0, 0, 0, 3840, 2160, 0, 0, 3840 << 16, 2160 << 16);这里的关键是零拷贝:RGA输出的DMA-BUF直接转成DRM framebuffer,不需要CPU参与。我用drmPrimeHandleToFD把DMA-BUF fd转成DRM handle,然后创建framebuffer。
提示:DRM的格式要和RGA输出格式匹配。RGA输出NV12,DRM这边要用
DRM_FORMAT_NV12,并且注意UV平面的offset。
4. 性能调优:那些文档里不会写的参数和技巧
4.1 RGA的并行度与优先级设置
RK3588有两个RGA核心(RGA2和RGA3),但默认驱动可能只用一个。通过/dev/rga的ioctl可以查询和设置核心亲和性。我通常把四路预处理分配到两个核心上,避免单核心排队。
// 查询RGA能力 struct rga_hw_cap cap; ioctl(rga_fd, RGA_GET_HW_CAP, &cap); // 设置使用核心1 int core = 1; ioctl(rga_fd, RGA_SET_CORE, &core);另外,RGA的任务提交是异步的,用imsync或者RGA_BLIT_SYNC标志控制同步方式。如果四路预处理之间没有依赖,用异步提交然后统一等待,能提高吞吐。
4.2 内存分配:DMA-HEAP vs ION vs CMA
RK3588的Linux内核里,DMA-BUF的分配后端有几种选择。我推荐用DMA-HEAP的system和cma区域:
- system heap:适合小buffer,分配快,但物理不连续。
- CMA:适合大buffer,物理连续,RGA和GPU访问效率高。
四路1080P的buffer建议用CMA,每路约3.1MB,四路12.4MB,加上4K输出buffer约12MB,总共不到32MB,CMA区域设64MB足够。
# 查看CMA大小 cat /proc/meminfo | grep Cma # 设备树里调整CMA reserved-memory { cma: cma { compatible = "shared-dma-pool"; size = <0x4000000>; // 64MB reusable; }; };4.3 帧率不稳的排查思路
如果实测帧率波动大,按这个顺序排查:
- V4L2丢帧:用
v4l2-ctl --stream-mmap --stream-count=100测试单路采集稳定性。如果单路都丢帧,检查MIPI时钟和sensor配置。 - RGA排队:用
cat /sys/kernel/debug/rga/status查看RGA任务队列长度。如果队列经常满,说明RGA成为瓶颈,需要减少每路的处理量或者提高并行度。 - DDR带宽:用
devfreq或者rockchip_dmc的debugfs节点查看带宽占用。如果接近上限,考虑降低中间格式的位深或者减少合成次数。 - 显示vsync:如果显示端有撕裂,检查DRM的page flip是否及时。用
drmModePageFlip的异步模式,避免阻塞。
我遇到过一种情况:四路采集都正常,RGA也正常,但显示帧率只有20fps。最后发现是DRM的atomic commit里每次都在重新创建framebuffer,导致内核频繁分配内存。改成复用framebuffer后,帧率稳定在30fps。
4.4 GPU混合的延迟隐藏
如果必须用GPU做混合,尽量把GPU渲染和RGA处理流水线化。具体做法:GPU渲染第N帧的同时,RGA处理第N+1帧的预处理。用两个buffer做ping-pong,GPU的fence通过DMA-BUF的sync文件描述符传递给RGA,实现跨硬件的同步。
// GPU渲染完成后生成fence int fence_fd = glFinishFence(); // 把fence传给RGA,RGA等待fence后再开始 rga_info_t src = {0}; src.fd = gpu_output_fd; src.sync_fd = fence_fd;这样GPU和RGA可以并行工作,整体延迟从串行的15ms降到约8ms。
5. 实际项目中的坑与应对:从花屏到性能悬崖
5.1 花屏问题:格式与stride的陷阱
RGA对stride(行跨距)有要求,必须是16的倍数。如果V4L2采集出来的buffer stride不是16对齐,RGA导入后会花屏。解决办法是在V4L2的格式设置里显式指定stride,或者用RGA的wstride参数手动指定。
src.wstride = 1920; // 必须是16的倍数 src.hstride = 1080;另一个花屏原因是NV12的UV平面offset计算错误。NV12的UV平面紧跟在Y平面后面,offset = stride * height。如果RGA导入时offset设错,UV会错位,画面颜色异常。
5.2 性能悬崖:当RGA遇到非对齐尺寸
RGA在处理非2的幂次尺寸时,性能会明显下降。比如把1920x1080缩到1000x560,RGA的内部算法会退化。我的经验是:尽量让缩放后的尺寸是16的倍数,如果业务要求不能改,那就用GPU做这一步缩放,虽然GPU延迟高,但至少性能稳定。
5.3 多路同步:时间戳对齐的实践
四路摄像头如果不同步,拼接出来的画面会有运动物体撕裂。硬件上可以用MIPI的同步信号,但软件层面需要在V4L2采集时打时间戳,然后在拼接前做对齐。
struct v4l2_buffer buf = {0}; ioctl(fd, VIDIOC_DQBUF, &buf); // buf.timestamp 是内核时间戳我通常取四路时间戳的中位数作为基准,把其他路的帧缓存到队列里,等齐了再一起送RGA。这样会增加一帧的延迟,但画面同步性好很多。
5.4 内存泄漏:DMA-BUF的引用计数
DMA-BUF的引用计数很容易搞错。每次dma_buf_get都要对应的dma_buf_put,否则内存泄漏。在长时间运行的项目里,我建议用/sys/kernel/debug/dma_buf/bufinfo定期检查未释放的buffer。
注意:RGA和GPU导入DMA-BUF后,驱动内部会持有引用。释放时要确保RGA任务已完成,否则会触发内核警告。
6. 从能跑到好用:稳定性与可维护性的最后几公里
项目能跑起来只是第一步,真正交付还要考虑长时间运行的稳定性。我在这类项目里通常会加几个保障机制:
看门狗与自动恢复:每个硬件模块(V4L2、RGA、GPU、DRM)都有独立的健康检查线程。如果某个模块超过500ms没有响应,触发重新初始化。RGA的重新初始化比较简单,关闭fd再打开即可;GPU的context重建代价大一些,但比整个进程重启要好。
日志与性能计数器:在关键路径上打时间戳,记录每帧的采集耗时、RGA耗时、GPU耗时、显示耗时。这些数据通过/proc或者共享内存暴露出来,方便现场调试。我习惯用一个环形buffer存最近1000帧的耗时,出问题时直接dump出来分析。
配置热更新:拼接布局、缩放比例、混合权重这些参数做成配置文件,支持运行时重载。这样现场调整不需要重新编译和重启。
温度与频率监控:RK3588在高负载下会降频。用/sys/class/thermal/thermal_zone*/temp监控温度,如果接近阈值,主动降低帧率或者减少GPU参与,避免触发硬件保护导致画面卡死。
最后分享一个我在实际项目里总结的参数组合,四路1080P到4K 2x2拼接,30fps稳定运行:
| 模块 | 配置 | 耗时 |
|---|---|---|
| V4L2采集 | NV12, 4 buffer, 非阻塞 | 每路约2ms |
| RGA缩放 | 1080P->960x540, 双线性 | 每路1.2ms |
| RGA拼接 | 四次blit到4K canvas | 总3ms |
| DRM显示 | 零拷贝, page flip | 每帧1ms |
| CPU占用 | 四线程采集+主线程调度 | 约15% |
| GPU占用 | 仅显示合成 | 5%以下 |
这套配置在室温25度下连续跑了72小时,没有丢帧,没有花屏,温度稳定在65度左右。如果你的场景需要更高的画质或者更复杂的拼接,把GPU加进来做混合,性能余量也足够。关键是先把RGA的流水线调通,GPU作为补充,而不是反过来。