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_device、struct video_device、struct 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_dev和struct 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 知识体系总览
这一章的内容比较多,我画了一张图帮你理清思路:
这张图把整个驱动栈的层次关系画清楚了。从上到下依次是:应用层、V4L2框架、M2M驱动、固件、硬件。每一层都依赖下一层提供的服务。
最后说一句,驱动开发调试起来确实痛苦。没有仿真器,没有printf,全靠printk和示波器。我刚开始做驱动时,一个bug能调一周。但等你把整个流程跑通了,那种成就感也是无可替代的。
本章核心要点:
- V4L2是视频驱动的标准接口,M2M设备是编解码器的驱动模型
- vb2 buffer管理要选对内存类型,DMABUF是零拷贝的关键
- 固件加载用request_firmware,通信协议要简单清晰
- 调试驱动时,先确认硬件寄存器读写正确,再验证buffer流转