4、硬件驱动与固件开发:Linux V4L2框架、M2M设备驱动、固件加载与通信协议

好,咱们进入第四章。这一章讲的是驱动和固件,说白了就是让硬件跑起来的那层“胶水”。很多做算法的人觉得驱动是纯软件活,做硬件的人又觉得这是嵌入式的事。我个人觉得,做视频编解码器开发,不懂这层,你连硬件报错都看不懂。

咱们从Linux V4L2框架讲起,再到M2M设备驱动,最后聊聊固件加载和通信协议。嗯,这里要注意,这三块是层层递进的。

4.1 Linux V4L2框架:视频驱动的“普通话”

V4L2,全称Video for Linux 2。它是Linux内核里视频设备的统一接口。你想想看,如果没有V4L2,每个芯片厂商都得自己搞一套ioctl,那应用层得疯掉。

V4L2把设备抽象成三类:

  • Capture设备:从摄像头采集数据
  • Output设备:把数据送出去显示
  • M2M设备:内存到内存的转换,编解码器就属于这类

我在项目中遇到过一个问题:某个芯片的编码器在V4L2框架下注册成了Capture设备,结果应用层用标准API调用时,死活拿不到编码后的码流。查了半天,原来是驱动里把V4L2_CAP_VIDEO_M2M标志位漏掉了。这种坑,你光看文档是看不出来的。

核心数据结构struct v4l2_devicestruct video_devicestruct vb2_queue。这三个结构体构成了V4L2驱动的骨架。

V4L2的buffer管理用的是videobuf2(vb2)框架。它支持三种内存类型:

内存类型 说明 适用场景
V4L2_MEMORY_MMAP 内核分配物理连续内存,用户态mmap映射 大多数硬件编解码器
V4L2_MEMORY_USERPTR 用户态分配内存,传给内核 软件处理、GPU共享
V4L2_MEMORY_DMABUF 基于dma-buf,跨设备共享 多硬件流水线、零拷贝

我个人习惯用DMABUF。为什么?因为编解码器往往要和显示、GPU打交道,DMABUF能实现真正的零拷贝。我曾经在一个4K解码项目里,用MMAP方式每帧要多花3ms拷贝时间,换成DMABUF后直接省掉了。

4.2 M2M设备驱动:编解码器的“心脏”

M2M,Memory-to-Memory。说白了就是从一个内存缓冲区读数据,处理后写到另一个缓冲区。编解码器就是典型的M2M设备。

M2M驱动的核心是struct v4l2_m2m_devstruct v4l2_m2m_ctx。驱动里需要实现两个队列:

  • OUTPUT队列:存放原始数据(编码输入/解码输入)
  • CAPTURE队列:存放处理后的数据(编码输出/解码输出)

驱动的工作流程大概是这样的:

// 伪代码,展示M2M驱动的核心逻辑
static void device_run(void *priv) {
    struct my_dev *dev = priv;
    
    // 从OUTPUT队列取一个buffer
    struct vb2_buffer *src = v4l2_m2m_next_src_buf(dev->m2m_ctx);
    // 从CAPTURE队列取一个buffer
    struct vb2_buffer *dst = v4l2_m2m_next_dst_buf(dev->m2m_ctx);
    
    // 配置硬件寄存器
    writel(src->planes[0].m.fd, dev->regs + REG_SRC_ADDR);
    writel(dst->planes[0].m.fd, dev->regs + REG_DST_ADDR);
    writel(FRAME_SIZE, dev->regs + REG_SIZE);
    
    // 启动硬件
    writel(1, dev->regs + REG_START);
    
    // 硬件完成后触发中断,在中断处理函数中调用
    // v4l2_m2m_buf_done(src, VB2_BUF_STATE_DONE);
    // v4l2_m2m_buf_done(dst, VB2_BUF_STATE_DONE);
    // v4l2_m2m_job_finish(dev->m2m_dev, dev->m2m_ctx);
}

嗯,这里要注意一个关键点:M2M驱动必须处理好流控。硬件编解码器通常有多个处理单元(比如多个编码通道),你不能一股脑把所有任务都扔给硬件。我曾经见过一个驱动,在4路编码同时启动时,因为没做流控,硬件直接挂死。

避坑指南:我曾经在调试一个H.264编码器驱动时,发现编码出来的码流每隔几帧就花屏。查了三天,最后发现是驱动里没有等硬件完成就释放了buffer。记住:v4l2_m2m_buf_done()必须在硬件真正完成后调用。

4.3 固件加载与通信协议

固件,就是跑在硬件内部处理器上的代码。编解码器芯片里通常有个小CPU(比如RISC-V或ARM Cortex-M),负责处理控制逻辑、码流解析等任务。

固件加载的方式主要有两种:

  • 通过SPI Flash加载:固件烧录在外部Flash里,上电后硬件自动加载
  • 通过驱动加载:驱动在初始化时,把固件从文件系统读到内存,然后通过DMA写到硬件的SRAM里

我建议用第二种方式。为什么?因为方便升级。你想想看,如果固件烧在Flash里,升级就得重新烧录,生产线上麻烦得很。通过驱动加载,只需要替换文件系统里的固件文件就行。

驱动加载固件的典型代码:

static int load_firmware(struct device *dev, const char *fw_name) {
    const struct firmware *fw;
    int ret;
    
    // 从文件系统请求固件
    ret = request_firmware(&fw, fw_name, dev);
    if (ret) {
        dev_err(dev, "Failed to load firmware %s\n", fw_name);
        return ret;
    }
    
    // 把固件写入硬件SRAM
    // 这里用memcpy_toio或者dma_map_single
    memcpy_toio(dev->iomem_base, fw->data, fw->size);
    
    // 通知硬件固件加载完成
    writel(1, dev->regs + REG_FW_READY);
    
    release_firmware(fw);
    return 0;
}

固件和驱动之间的通信协议,通常用共享内存+门铃寄存器的方式。驱动把命令写到共享内存,然后写门铃寄存器通知固件。固件处理完后,写状态寄存器通知驱动。

我参与过一个项目,通信协议设计得特别复杂,用了12个寄存器做握手。结果调试的时候,固件和驱动两边对寄存器定义的理解不一致,浪费了两周时间。后来我学乖了,通信协议一定要有明确的文档,而且驱动和固件两边要共用头文件。

警告:固件加载时一定要检查固件的版本和校验和。我曾经遇到过生产线上烧录了错误版本的固件,导致一批设备解码出来的视频全是绿屏。从那以后,我在驱动里加了固件版本检查,版本不匹配直接拒绝加载。

4.4 知识体系总览

这一章的内容比较多,我画了一张图帮你理清思路:

硬件驱动与固件开发知识体系 应用层 (FFmpeg/GStreamer) V4L2 框架 video_device / vb2_queue / v4l2_device M2M 设备驱动 OUTPUT队列 → 硬件处理 → CAPTURE队列 device_run / job_finish / buf_done 固件加载与通信协议 request_firmware → DMA写入SRAM → 门铃通知 共享内存命令 / 状态寄存器 / 中断处理 硬件编解码器 (ASIC/FPGA) 用户态 内核态 固件层 硬件层

这张图把整个驱动栈的层次关系画清楚了。从上到下依次是:应用层、V4L2框架、M2M驱动、固件、硬件。每一层都依赖下一层提供的服务。

最后说一句,驱动开发调试起来确实痛苦。没有仿真器,没有printf,全靠printk和示波器。我刚开始做驱动时,一个bug能调一周。但等你把整个流程跑通了,那种成就感也是无可替代的。

本章核心要点

  • V4L2是视频驱动的标准接口,M2M设备是编解码器的驱动模型
  • vb2 buffer管理要选对内存类型,DMABUF是零拷贝的关键
  • 固件加载用request_firmware,通信协议要简单清晰
  • 调试驱动时,先确认硬件寄存器读写正确,再验证buffer流转

专注资料整理