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驱动分成三层来看:
- 字符设备层:最底层,负责注册
file_operations,处理open/close/ioctl - V4L2核心层:内核自带的
videodev2模块,提供标准的数据结构(如v4l2_format、v4l2_buffer) - 硬件驱动层: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调用背后发生了什么?我一步步拆给你看:
- 系统调用入口:
ioctl()触发软中断,进入内核态 - V4L2核心分发:内核根据
fd找到对应的video_device,调用v4l2_ioctl - 命令解析:
VIDIOC_S_FMT被解析,找到对应的处理函数 - 硬件驱动介入:MTK驱动中的
mtk_vcodec_s_fmt()被调用 - 寄存器操作:驱动根据
fmt参数,配置Codec硬件的分辨率寄存器 - 返回用户空间:配置完成,返回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的硬解码能力真正发挥出来。下一章我们讲具体的解码器初始化流程,到时候我会带你看完整的代码实现。