4、MTK Codec驱动框架:V4L2驱动模型、MDP架构、IOCTL调用流程

好,咱们今天来啃一块硬骨头——MTK平台的Codec驱动框架。说实话,这块内容我第一次接触的时候也头大,因为MTK的文档写得...嗯,你懂的。但摸爬滚打几年下来,我总结了一套自己的理解方法,今天分享给你。

4.1 V4L2驱动模型:视频的“万能插座”

V4L2,全称Video for Linux 2。说白了,它就是Linux内核给视频设备定的一套标准接口。你想想看,摄像头、解码器、编码器、显示控制器...这些设备五花八门,如果没有统一标准,驱动工程师得疯掉。

V4L2的核心思想就四个字:设备节点化。每个硬件单元都被抽象成一个设备节点,比如/dev/video0/dev/video1。应用程序通过open()ioctl()mmap()这些标准系统调用来操作它。

V4L2在MTK Codec中的角色:

  • 视频解码器:对应/dev/video0,负责将H.264/H.265码流解码为YUV帧
  • 视频编码器:对应/dev/video1,负责将YUV帧编码为H.264/H.265码流
  • M2M设备:Memory-to-Memory,输入输出都是内存,不涉及物理显示

我个人习惯把V4L2驱动分成三层来看:

  1. 字符设备层:最底层,负责注册file_operations,处理open/close/ioctl
  2. V4L2核心层:内核自带的videodev2模块,提供标准的数据结构(如v4l2_formatv4l2_buffer
  3. 硬件驱动层:MTK自己实现的,直接操作寄存器,控制Codec硬件

嗯,这里要注意:MTK的Codec驱动并不是完全按照标准V4L2写的。它做了很多扩展,尤其是buffer管理那块。我在项目中遇到过,直接用标准V4L2的VIDIOC_QBUF去投递buffer,结果驱动返回EINVAL。后来查了半天,发现MTK要求buffer必须从特定的内存池分配。

4.2 MDP架构:Media Data Path的“高速公路”

MDP,Media Data Path。这个名字起得好,它真的就是一条数据高速公路。你想想,视频数据从解码器出来,到显示到屏幕上,中间要经过多少环节?缩放、旋转、色彩空间转换、叠加...如果每个环节都走CPU,那性能就完蛋了。

MDP的解决方案是:硬件流水线化。它把显示相关的处理单元串联起来,数据在硬件层面直接流转,不经过CPU。

MDP组件 功能 我踩过的坑
OVL (Overlay) 图层叠加,支持多个视频层+UI层 曾经图层顺序搞反,视频被UI盖住了
RSZ (Resizer) 硬件缩放,支持任意比例 缩放系数不是整数时,边缘出现锯齿
WDMA (Write DMA) 将处理后的数据写入内存 对齐要求严格,非32字节对齐会崩
RDMA (Read DMA) 从内存读取数据送入显示 带宽不够时画面撕裂

MDP架构最妙的地方在于它的解耦设计。解码器只管输出YUV帧,MDP只管处理显示。两者通过内存buffer连接,互不干扰。这样设计的好处是:你可以同时跑多个解码实例,每个实例的输出都通过MDP合成到屏幕上。

避坑指南:我曾经在调试多路输出时,发现第二路视频始终黑屏。查了两天,最后发现是MDP的RDMA通道没有正确配置源地址。记住:每个显示通道都要独立配置MDP的输入源,不能复用。

4.3 IOCTL调用流程:从APP到硬件的“快递路线”

IOCTL,Input/Output Control。它是用户空间和内核空间通信的主要方式。在MTK Codec驱动中,IOCTL的调用流程大概是这样的:

// 用户空间调用
int fd = open("/dev/video0", O_RDWR);
struct v4l2_format fmt = {0};
fmt.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE;
fmt.fmt.pix_mp.width = 1920;
fmt.fmt.pix_mp.height = 1080;
fmt.fmt.pix_mp.pixelformat = V4L2_PIX_FMT_H264;
ioctl(fd, VIDIOC_S_FMT, &fmt);

这行ioctl调用背后发生了什么?我一步步拆给你看:

  1. 系统调用入口ioctl()触发软中断,进入内核态
  2. V4L2核心分发:内核根据fd找到对应的video_device,调用v4l2_ioctl
  3. 命令解析VIDIOC_S_FMT被解析,找到对应的处理函数
  4. 硬件驱动介入:MTK驱动中的mtk_vcodec_s_fmt()被调用
  5. 寄存器操作:驱动根据fmt参数,配置Codec硬件的分辨率寄存器
  6. 返回用户空间:配置完成,返回0表示成功

注意:MTK的IOCTL调用有严格的顺序要求。必须先调用VIDIOC_S_FMT设置格式,再调用VIDIOC_REQBUFS申请buffer,最后才能VIDIOC_QBUF投递buffer。顺序错了,驱动直接返回EINVAL。我刚开始做的时候就被这个坑过,debug了一整天。

说到buffer管理,MTK有个特殊的地方:它支持DMA-BUF机制。这意味着buffer可以在不同硬件模块之间共享,不需要拷贝。比如解码器输出的YUV帧,可以直接通过DMA-BUF传给MDP的RDMA,零拷贝完成显示。

具体调用流程是这样的:

// 1. 申请DMA-BUF
struct v4l2_exportbuffer expbuf = {0};
expbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
expbuf.index = 0;
ioctl(fd, VIDIOC_EXPBUF, &expbuf);
// expbuf.fd 就是导出的DMA-BUF文件描述符

// 2. 将DMA-BUF传递给MDP
// 通过MDP的IOCTL,将expbuf.fd设置为RDMA的输入源

你想想看,如果没有DMA-BUF,每次显示都要从解码器buffer拷贝到显示buffer,4K分辨率下,一帧就要拷贝8MB数据,60fps就是480MB/s的带宽消耗。有了DMA-BUF,这个开销直接省掉了。

嗯,最后说一句。MTK的IOCTL调用链其实挺深的,我建议你在调试时打开内核的dyndbg动态调试,把V4L2和MTK驱动的日志都打开。命令是这样的:

echo 'file v4l2-ioctl.c +p' > /sys/kernel/debug/dynamic_debug/control
echo 'file mtk_vcodec_dec.c +p' > /sys/kernel/debug/dynamic_debug/control

这样你就能看到每次IOCTL调用时,内核到底执行了哪些函数。对于理解整个流程非常有帮助。

好,这一章就到这里。V4L2是骨架,MDP是血脉,IOCTL是神经。三者配合,才能让MTK8676的硬解码能力真正发挥出来。下一章我们讲具体的解码器初始化流程,到时候我会带你看完整的代码实现。