Linux VPU 驱动

目录1. 前言2. 概述3. VPU 工作原理3.1 VPU 编码工作流程3.2 VPU解码工作流程4. Linux 下的 VPU4.1 驱动架构4.2 用户空间编程框架(Encoder编码示例)4.3 VPU 驱动工作流程小结4.4 示例4.4.1 FrienlyARM的方案内核NX VPU驱动补丁5. 参考资料
1. 前言
限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
2. 概述
VPU 是用来进行图像、视频数据进行硬件编、解码的硬件模块。内部集成了 Encoder、Decoder 功能部件进行图像、视频数据进行硬件编、解码,以加速处理。
3. VPU 工作原理
3.1 VPU 编码工作流程
---------------
| --------- |
输入数据 -->|->| Encoder |->|-> 编码后的输出数据
| --------- |
| |
| --------- |
| | Decoder | |
| --------- |
---------------
3.2 VPU解码工作流程
---------------
| --------- |
| | Encoder | |
| --------- |
| |
| --------- |
输入数据 -->|->| Decoder |->|-> 解码后的输出数据
| --------- |
---------------
4. Linux 下的 VPU
4.1 驱动架构
VPU驱动 可基于 V4L2子系统 框架完成。
1. 分别为 Encoder 和 Decoder 各注册1个 /dev/videoX 设备(总共2个video设备)。
/* 注册 Encoder 设备 */
vfd->vfl_dir = VFL_DIR_M2M;
video_register_device(vfd, VFL_TYPE_GRABBER, ...)
/* 注册 Decoder 设备 */
vfd->vfl_dir = VFL_DIR_M2M;
video_register_device(vfd, VFL_TYPE_GRABBER, ...)
设备数据传输方向为 VFL_DIR_M2M , 表明设备是设备完成的功能内存间的数据传输拷贝。
2. 在 open() 调用中,在打开文件句柄的私有数据 file_private 绑定设备 buffer 队列(vb2_queue)的类型、接口、IO模式、数据传输方向等。
这里以 Encoder 的 open() 调用为例加以说明:
/* Encoder【输入】数据队列初始化 */
encoder_vq_input.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
encoder_vq_input.io_modes = VB2_MMAP | VB2_USERPTR | VB2_DMABUF;
encoder_vq_input.ops = &xxx_vpu_encoder_qops;
encoder_vq_input.mem_ops = &vb2_dma_contig_memops;
...
vb2_queue_init(&encoder_vq_input);
/* Encoder【输出】数据队列初始化 */
encoder_vq_output.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
encoder_vq_output.io_modes = VB2_MMAP | VB2_USERPTR | VB2_DMABUF;
encoder_vq_output.ops = &xxx_vpu_encoder_qops;
encoder_vq_output.mem_ops = &vb2_dma_contig_memops;
...
vb2_queue_init(&encoder_vq_output);
...
4.2 用户空间编程框架(Encoder编码示例)
/* 打开设备(/dev/videoX为Encoder设备) */
fd = open("/dev/videoX", O_RDWR);
/* 设置输入、输出数据格式 */
/* 设置编码【输入】数据格式 */
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
...
ioctl(fd, VIDIOC_S_FMT, &fmt);
/* 设置编码【输出】数据格式 */
fmt.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE;
...
ioctl(fd, VIDIOC_S_FMT, &fmt);
/* 请求输入、输出buffer,然后映射内核buffer到用户空间(IO模式为 V4l2_MEMORY_MMAP) */
/* 请求【输入】buffer并映射到用户空间 */
rb.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
rb.memory = V4l2_MEMORY_MMAP;
rb.count = 1;
ioctl(fd, VIDIOC_REQBUFS, &rb);
buf.index = i;
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
buf.memory = V4l2_MEMORY_MMAP;
buf.length = num_input_planes;
buf.m.planes = input_planes;
ioctl(fd, VIDIOC_QUERYBUF, &buf);
input_buffer.start = mmap(0, ..., PROT_READ|PROT_WRITE, ...);
input_buffer.length = ...;
/* 请求【输出】buffer并映射到用户空间 */
rb.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
rb.memory = V4l2_MEMORY_MMAP;
rb.count = 1;
ioctl(fd, VIDIOC_REQBUFS, &rb);
buf.index = i;
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
buf.memory = V4l2_MEMORY_MMAP;
buf.length = num_output_planes;
buf.m.planes = output_planes;
ioctl(fd, VIDIOC_QUERYBUF, &buf);
output_buffer.start = mmap(0, ..., PROT_READ|PROT_WRITE, ...);
output_buffer.length = ...;
/* 将【输出】buffer入队,然后开启【输出流】 */
buf.index = i;
buf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE;
buf.memory = V4l2_MEMORY_MMAP;
buf.length = num_output_planes;
buf.m.planes = output_planes;
output_planes[i].bytesused = output_planes[i].length;
ioctl(fd, VIDIOC_QBUF, &buf);
type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE;
ioctl(fd, VIDIOC_STREAMON, &type);
/* 设置编码输入数据,将【输入】buffer入队,然后开启【输入流】 */
/* 设置编码输入数据 */
memcpy(input_buffer.start, input_data, input_data_size);
buf.index = i;
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
buf.memory = V4l2_MEMORY_MMAP;
buf.length = num_input_planes;
buf.m.planes = input_planes;
input_planes[i].bytesused = input_planes[i].length;
ioctl(fd, VIDIOC_QBUF, &buf);
type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
ioctl(fd, VIDIOC_STREAMON, &type);
/* 出队编码队列(vb2_queue)中就绪的【输出缓冲】 */
(vb2_buffer/v4l2_buffer, vb2_plane/v4l2_plane)
buf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE;
buf.memory = V4L2_MEM_TYPE;
buf.length = num_output_planes;
buf.m.planes = output_planes;
ioctl(fd, VIDIOC_DQBUF, &buf);
/* 拷贝编码好的数据到目的缓冲(假定 output plane 数目为1) */
memcpy(output_data, output_buffer.start, buf.m.planes[0].bytesused);
/* 关闭设备 */
close(fd);
4.3 VPU 驱动工作流程小结
VPU
-----------------------------
| ----------------------- |
| | Encoder | |
| | ----------------- | |
--->|->|->| encoding buffer |->|->|--->
^ | | ----------------- | | |
输入数据队列(vb2_queue) | | ----------------------- | | 输出数据队列(vb2_queue)
----------------------- | | | | -----------------------
| vb2_buffer[] |-->| | ----------------------- | |--> | vb2_buffer[] |
----------------------- | | | Decoder | | | -----------------------
v | | ----------------- | | |
--->|->|->| decoding buffer |->|->|--->
| | ----------------- | |
| ----------------------- |
-----------------------------
Encoder/Decoder完成编、解码动作后:
(1) 拷贝编、解码后的数据到输出队列中某个vb2_buffer的缓冲:
memcpy(output_buffer, input_buffer, size);
(2) 标记输入数据队列中某个vb2_buffer中的数据编、解码完成:
vb2_buffer_done(&in_vb, VB2_BUF_STATE_DONE);
(3) 设置输出缓冲负载(输出数据大小):
vb2_set_plane_payload(&out_vb, 0, size);
(4) 标记输出数据队列中某个vb2_buffer中的数据编、解码输出数据就绪:
vb2_buffer_done(&out_vb, VB2_BUF_STATE_DONE);
4.4 示例
这是一个实际的范例,来自 FrienlyARM 的方案 :NanoPC-T3 Plus 。该方案基于 S5P6818 的 SoC 。
4.4.1 FrienlyARM的方案内核NX VPU驱动补丁
官方自带的VPU驱动编解码的部分有些问题,我对它做了如下修改:
/*
* drivers/media/platform/nx-vpu/nx_vpu_enc_v4l2.c
*/
void vpu_enc_get_seq_info(struct nx_vpu_ctx *ctx)
{
...
/* 注释下面这一段代码 */
/*{
struct nx_vpu_buf *dst_mb;
unsigned long flags;
spin_lock_irqsave(&ctx->dev->irqlock, flags);
dst_mb = list_entry(ctx->strm_queue.next, struct nx_vpu_buf,
list);
list_del(&dst_mb->list);
ctx->strm_queue_cnt--;
vb2_set_plane_payload(&dst_mb->vb, 0, ctx->strm_size);
vb2_buffer_done(&dst_mb->vb, VB2_BUF_STATE_DONE);
spin_unlock_irqrestore(&ctx->dev->irqlock, flags);
}*/
}
static void nx_vpu_enc_buf_queue(struct vb2_buffer *vb)
{
...
if (vq->type == V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE) {
...
} else if (vq->type == V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE) {
buf->used = 0;
if (ctx->img_fmt.num_planes == 1)
NX_DbgMsg(INFO_MSG, "adding to src: %p(%08lx)\n",
vb, (unsigned long)nx_vpu_mem_plane_addr(ctx, vb, 0));
else if (ctx->img_fmt.num_planes == 2)
NX_DbgMsg(INFO_MSG, "adding to src: %p(%08lx, %08lx)\n",
vb, (unsigned long)nx_vpu_mem_plane_addr(ctx, vb, 0),
(unsigned long)nx_vpu_mem_plane_addr(ctx, vb, 1));
else if (ctx->img_fmt.num_planes == 3)
NX_DbgMsg(INFO_MSG, "adding to src: %p(%08lx, %08lx, %08lx)\n",
vb, (unsigned long)nx_vpu_mem_plane_addr(ctx, vb, 0),
(unsigned long)nx_vpu_mem_plane_addr(ctx, vb, 1),
(unsigned long)nx_vpu_mem_plane_addr(ctx, vb, 2));
}
...
}
int nx_vpu_enc_open(struct nx_vpu_ctx *ctx)
{
...
ctx->vq_img.io_modes = VB2_MMAP | VB2_USERPTR | VB2_DMABUF;
...
...
ctx->vq_strm.io_modes = VB2_MMAP | VB2_USERPTR | VB2_DMABUF;
...
}
/*
* drivers/media/platform/nx-vpu/nx_vpu_v4l2.c
*/
#define DST_QUEUE_OFF_BASE (1 << 30)
int vidioc_querybuf(struct file *file, void *priv, struct v4l2_buffer *buf)
{
struct nx_vpu_ctx *ctx = fh_to_ctx(file->private_data);
int ret = 0;
FUNC_IN();
...
if (buf->type == V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE) {
...
} else if (buf->type == V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE) {
...
//buf->m.planes[0].m.mem_offset += DST_QUEUE_OFF_BASE;
/* Adjust MMAP memory offsets for the CAPTURE queue */
if (buf->memory == V4L2_MEMORY_MMAP /*&&
!V4L2_TYPE_IS_OUTPUT(ctx->vq_img->type)*/) {
if (V4L2_TYPE_IS_MULTIPLANAR(ctx->vq_img.type)) {
int i;
for (i = 0; i < buf->length; ++i)
buf->m.planes[i].m.mem_offset += DST_QUEUE_OFF_BASE;
} else {
buf->m.offset += DST_QUEUE_OFF_BASE;
}
}
} else {
...
}
return ret;
}
我为 S5P6818 的 VPU 编写了一个测试程序 nxvpu-yuv2jpg.c ,该程序用于将 YUV420 或 GREY 格式数据转换为 MJEPG 格式数据,实现代码见 这里 或 这里 。
5. 参考资料
https://wiki.friendlyelec.com/wiki/index.php/NanoPC-T3_Plus/zh