2、硬件加速架构概览:SoC视频编解码模块(VPU/CODEC)的典型架构、硬件Pipeline与软件交互模型
好,我们直接进入正题。上一章聊了视频编解码的基础概念,这一章我们来看看,这些算法在芯片里到底是怎么跑的。
说白了,视频编解码硬件加速,就是把那些计算密集的活儿——比如运动估计、DCT变换、熵编码——从CPU手里抢过来,交给专用的硬件模块去干。这个模块,业界通常叫它VPU(Video Processing Unit)或者CODEC(Coder-Decoder)。
我个人习惯把VPU看作一个“小型的、高度定制化的处理器”。它不跑通用指令,而是跑硬件流水线。你想想看,一个1080p@60fps的视频流,每秒要处理超过1.2亿个像素点。CPU要是干这活,功耗直接起飞。所以,必须上硬件。
2.1 SoC中的VPU/CODEC:它到底长什么样?
我们先从宏观上看看,一个典型的SoC里,视频编解码模块是怎么嵌入进去的。
核心要点:VPU不是孤岛。它通过系统总线(如AXI总线)与DDR内存、CPU、以及其他外设(如Display Controller、Camera ISP)相连。
下面这张图是我用SVG画的,展示了一个典型的SoC视频子系统架构。你可以把它当作一个“地图”,后面讲的所有细节,都能在这张图上找到位置。
这张图里,CPU通过控制寄存器接口(MMIO)给VPU下指令,比如“开始解码第5帧”。VPU自己则通过AXI总线,从DDR里拉取码流数据,再把解码后的YUV帧写回DDR。数据流和控制流是分开的,这是硬件加速设计的核心思想之一。
我的经验: 我在做一款4K解码芯片时,发现VPU和DDR之间的带宽经常成为瓶颈。后来我们在VPU内部加了一个128KB的SRAM作为临时缓冲区,专门缓存参考帧的局部数据。就这么一个小改动,总线带宽占用直接降了40%。所以,别小看内部缓存的设计。
2.2 硬件Pipeline:编解码的“流水线工厂”
VPU内部是怎么工作的?答案是:流水线(Pipeline)。
你可以把硬件Pipeline想象成一条汽车装配线。每个工位只干一件事,干完就传给下一个。这样,虽然单个任务耗时不变,但整体吞吐量可以做到每个时钟周期出一个结果。
一个典型的视频解码硬件Pipeline,大致包含以下几个阶段:
- 码流解析(Parsing):从DDR读取压缩码流,解析出语法元素(如宏块类型、运动矢量)。
- 熵解码(Entropy Decoding):对CAVLC或CABAC进行解码,得到量化后的变换系数。
- 反量化/反变换(IQ/IDCT):把系数变回残差像素块。
- 运动补偿(Motion Compensation):根据运动矢量,从参考帧中取出预测块。
- 重建(Reconstruction):预测块 + 残差块 = 重建块。
- 环路滤波(Loop Filter):去块效应滤波,消除方块效应。
编码的Pipeline基本是反过来的,但多了个运动估计(Motion Estimation)的步骤。这步最耗计算,通常占编码器70%以上的算力。
关键点:硬件Pipeline的深度和平衡性,直接决定了VPU的性能。如果某个阶段处理得慢,它就会成为整个流水线的“瓶颈”,拖慢整体帧率。
我记得有一次调试一个H.265解码器,发现帧率始终上不去。用示波器抓了各个模块的握手信号,发现是熵解码模块的CABAC引擎处理一个宏块需要300个时钟,而运动补偿只需要80个时钟。这就是典型的流水线不平衡。后来我们给CABAC引擎增加了双发射能力,才把帧率拉回来。
2.3 软件交互模型:驱动怎么“指挥”硬件?
硬件再强,也得有软件来调度。VPU的软件交互模型,说白了就是驱动(Driver)和固件(Firmware)之间的配合。
我习惯把这种交互分成三个层次:
| 层次 | 运行位置 | 职责 | 典型接口 |
|---|---|---|---|
| 应用层 | CPU (Linux/Android) | 调用V4L2或Media API | ioctl, mmap |
| 内核驱动层 | CPU (Kernel Space) | 管理硬件资源、提交任务 | 中断、DMA、寄存器读写 |
| 硬件固件层 | VPU内部微控制器 | 调度硬件Pipeline、处理错误 | Mailbox、共享内存 |
具体流程是这样的:
- 应用程序(比如播放器)通过V4L2接口,向驱动提交一个解码请求。
- 驱动把码流缓冲区的物理地址、输出帧缓冲区的地址,打包成一个“任务描述符”,写入VPU的寄存器。
- 驱动写一个“启动”寄存器,触发VPU开始工作。
- VPU内部的固件(一个轻量级的RISC-V或ARM核)解析任务描述符,然后按顺序启动硬件Pipeline的各个模块。
- 解码完成后,VPU通过中断通知CPU。驱动在中断处理函数中,把完成的任务返回给应用层。
避坑指南: 我曾经遇到过一个非常隐蔽的bug。驱动在提交任务后,立即修改了码流缓冲区的数据。结果VPU正在读取那块内存,读到的数据是错的,导致解码花屏。后来我们强制要求:驱动在VPU工作期间,绝对不能碰正在使用的缓冲区。这个叫“缓冲区的所有权转移”。
2.4 一个简单的交互代码示例
下面是一个伪代码,展示了驱动如何向VPU提交一个解码任务。注意,这不是真实的Linux驱动代码,而是为了说明交互逻辑。
// 驱动侧:提交解码任务
struct vpu_task {
uint32_t bitstream_addr; // 码流物理地址
uint32_t bitstream_size; // 码流大小
uint32_t output_addr[2]; // YUV输出地址(双缓冲)
uint32_t pic_width;
uint32_t pic_height;
};
// 1. 填充任务描述符
struct vpu_task task;
task.bitstream_addr = dma_map_single(dev, bitstream_buf, size, DMA_TO_DEVICE);
task.output_addr[0] = dma_map_single(dev, frame_buf0, size, DMA_FROM_DEVICE);
task.output_addr[1] = dma_map_single(dev, frame_buf1, size, DMA_FROM_DEVICE);
// 2. 写入VPU寄存器(MMIO)
writel(VPU_REG_TASK_ADDR, &task);
writel(VPU_REG_CMD, VPU_CMD_START_DECODE);
// 3. 等待中断
wait_for_completion(&vpu_done);
// 4. 中断处理函数中
irq_handler() {
// 读取状态寄存器
status = readl(VPU_REG_STATUS);
if (status & VPU_STATUS_DECODE_DONE) {
// 通知应用层
complete(&vpu_done);
}
}
这段代码看起来简单,但实际工程中要考虑的事情多得多。比如:多个任务如何排队?硬件出错时怎么恢复?如何支持H.264和H.265两种模式的切换?这些我们后面会慢慢展开。
2.5 总结一下
这一章我们聊了VPU在SoC中的位置、硬件Pipeline的典型阶段、以及软件驱动的交互模型。说白了,硬件加速就是“CPU下命令,VPU干苦力,DDR当仓库”。理解了这个框架,后面再深入细节时,你就知道每个模块是干什么用的了。
嗯,这里要注意一点:不同厂商的VPU架构差异很大。高通、联发科、海思,各有各的玩法。但万变不离其宗,核心的Pipeline和交互模型是相通的。你只要吃透了我上面讲的这套逻辑,换任何平台都能快速上手。