3、编码器驱动与固件:驱动层接口设计、固件调度策略、硬件抽象层(HAL)实现、中断处理与DMA传输

编码器驱动与固件,说白了就是连接上层应用和底层硬件的桥梁。我做了这么多年芯片部署,见过太多项目在驱动层翻车——要么中断处理太慢导致丢帧,要么DMA配置不对造成数据错乱。今天咱们就把这块硬骨头啃下来。

核心观点:驱动层设计的好坏,直接决定了编码器能否发挥出硬件应有的性能。一个好的驱动,应该让上层应用感觉不到硬件的存在。

3.1 驱动层接口设计

驱动接口设计,我习惯遵循一个原则:简单、稳定、可预测。你想想看,上层应用开发者不需要知道底层寄存器怎么配,他们只关心怎么把数据送进去、怎么把码流取出来。

3.1.1 接口分层

我个人把驱动接口分成三层:

层级 职责 典型接口
用户层API 面向应用开发者,屏蔽硬件细节 venc_open(), venc_encode(), venc_close()
内核层接口 负责系统调用与资源管理 ioctl(), mmap(), poll()
硬件抽象层 直接操作寄存器,与固件交互 hal_reg_write(), hal_fw_cmd()

我在项目中遇到过,有人把硬件寄存器操作直接暴露给用户层,结果应用层一个错误的参数就把整个编码器搞挂了。嗯,这里要注意:用户层API一定要做参数校验,不能信任任何来自上层的输入。

3.1.2 关键接口设计要点

  • 缓冲区管理:我建议使用mmap()实现零拷贝,避免用户态和内核态之间的数据搬运。高通平台通常支持ION/DMA-BUF,直接用就行。
  • 异步操作:编码是个耗时操作,不能阻塞调用者。用poll()或epoll()实现事件通知,上层可以异步等待编码完成。
  • 错误处理:每个接口都要返回明确的错误码。我曾经见过一个驱动,所有错误都返回-1,调试起来简直噩梦。

小技巧:在高通平台上,驱动接口设计可以参考msm_vidc的架构。它把编码器抽象成vidc设备,通过VIDC_IOCTL_*命令字完成所有操作,结构清晰,值得借鉴。

3.2 固件调度策略

固件是编码器的大脑。它负责把驱动下发的编码任务,拆解成硬件能执行的微操作。我常说,固件调度做得好,编码效率差不了。

3.2.1 任务队列模型

高通编码器固件通常采用生产者-消费者模型:

// 固件任务队列结构示意
struct fw_task_queue {
    struct list_head pending_list;   // 待处理任务
    struct list_head active_list;    // 正在执行的任务
    struct list_head done_list;      // 已完成任务
    spinlock_t lock;
    uint32_t max_concurrent;         // 最大并发数
};

驱动把编码任务塞进pending_list,固件调度器从里面取任务,放到active_list里执行,完成后移到done_list。为什么这么设计?因为编码器硬件通常有多个处理单元(比如多个ME、多个熵编码器),固件需要合理分配任务,避免某个单元空闲、另一个单元忙死。

3.2.2 调度策略对比

策略 原理 适用场景 我踩过的坑
FIFO 先来先服务 低延迟场景 大码率任务会阻塞小任务,导致帧率抖动
优先级调度 高优先级任务先执行 实时编码场景 优先级反转问题,低优先级任务一直得不到执行
时间片轮转 每个任务分配固定时间片 多路编码场景 时间片设置不合理,上下文切换开销大
负载均衡 根据硬件单元负载动态分配 高性能编码场景 负载估计算法不准,反而导致性能下降

我曾经在一个项目中,用FIFO调度处理4K@60fps的编码任务。结果一个I帧编码时间太长,后面的P帧全部超时,最终导致编码器崩溃。后来改成优先级调度,把I帧优先级设高,问题才解决。

注意:固件调度策略不是一成不变的。高通平台通常允许通过firmware_params动态调整调度参数。我建议在系统启动时根据应用场景做一次初始化配置,运行时尽量少改。

3.3 硬件抽象层(HAL)实现

HAL层,说白了就是给驱动和固件之间加一层适配。为什么需要它?因为高通不同型号的编码器硬件,寄存器地址、操作时序可能都不一样。有了HAL,驱动代码就不用改了,换硬件只需要换HAL实现。

3.3.1 HAL接口定义

我习惯把HAL接口分成三类:

  • 初始化/去初始化:hal_init(), hal_deinit()——负责硬件上电、时钟使能、复位等操作。
  • 控制接口:hal_set_param(), hal_get_param()——设置编码参数,比如分辨率、码率、帧率。
  • 数据通道:hal_submit_frame(), hal_get_bitstream()——送原始帧、取编码后的码流。
// HAL层接口示例
struct hal_ops {
    int (*init)(void *hw_priv);
    int (*deinit)(void *hw_priv);
    int (*set_param)(void *hw_priv, uint32_t id, void *value);
    int (*submit_frame)(void *hw_priv, struct frame_buf *buf);
    int (*get_bitstream)(void *hw_priv, struct bs_buf *buf);
    int (*register_isr)(void *hw_priv, irq_handler_t handler);
};

3.3.2 实现要点

嗯,这里有几个坑要注意:

  • 寄存器访问要加锁:HAL层可能被多个线程同时调用,寄存器操作必须是原子的。我建议用spinlock,因为编码器中断上下文不能睡眠。
  • 超时处理:硬件操作可能卡死。我习惯给每个HAL操作加一个超时机制,超时后强制复位硬件。
  • 调试接口:HAL层一定要暴露寄存器dump接口。我曾经调试一个编码器花屏问题,就是通过dump寄存器发现某个配置位没写进去。

经验之谈:高通平台的HAL实现,可以参考venus_hfi(Video EncoNder/Decoder Unified Software Interface)。它把HAL层抽象成HFI协议,驱动通过共享内存和固件通信,效率很高。

3.4 中断处理与DMA传输

中断和DMA,是编码器高性能的关键。我见过太多项目,中断处理写得稀烂,DMA配置一塌糊涂,结果编码器跑起来CPU占用率飙到80%。

3.4.1 中断处理设计

编码器的中断类型通常包括:

中断类型 触发条件 处理优先级
帧完成中断 一帧编码完成 高
错误中断 硬件异常(如超时、溢出) 最高
缓冲区空/满中断 输入缓冲区空或输出缓冲区满 中
DMA完成中断 DMA传输结束 中

我建议把中断处理分成上半部和下半部:

  • 上半部(ISR):只做最紧急的事——清除中断标志、保存关键状态、唤醒下半部。不能在这里做耗时操作。
  • 下半部(Tasklet/Workqueue):处理实际业务——比如把编码完成的帧通知上层、启动下一帧编码。

我曾经犯过一个错误:在ISR里直接调用hal_get_bitstream(),结果这个函数内部有spinlock等待,导致中断响应时间过长,系统直接报irq timeout。后来改成下半部处理,问题解决。

3.4.2 DMA传输优化

DMA是编码器数据搬运的主力。原始帧数据、编码后的码流,都靠DMA在内存和硬件之间传输。我总结了几条优化原则:

  • 使用Scatter-Gather DMA:编码器的输入帧可能分散在多个物理页上。SG-DMA可以一次传输多个不连续的物理块,避免内存拷贝。
  • 对齐要求:高通编码器通常要求DMA缓冲区地址和大小按128字节对齐。不对齐会导致硬件报错。
  • 缓存一致性:DMA传输完成后,一定要做缓存刷新或失效操作。我见过有人忘了做dma_sync_single_for_cpu(),结果CPU读到的是脏数据。
// DMA传输示例
struct dma_buf *buf = dma_alloc_coherent(dev, size, &dma_addr, GFP_KERNEL);
// 配置DMA描述符
struct dma_async_tx_descriptor *desc;
desc = dmaengine_prep_slave_sg(dma_chan, sgl, sg_len, DMA_DEV_TO_MEM,
                                DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
desc->callback = dma_done_callback;
desc->callback_param = buf;
// 提交DMA传输
dmaengine_submit(desc);
dma_async_issue_pending(dma_chan);

重要提醒:DMA传输完成后,一定要检查传输状态。我遇到过DMA传输中途被其他高优先级中断打断,导致数据不完整。建议在回调函数里检查dmaengine_tx_status()的返回值。

3.5 整体架构图

下面这张图,展示了驱动、HAL、固件、中断和DMA之间的协作关系:

编码器驱动与固件架构图 编码器驱动与固件整体架构 用户层应用 (App/Codec Service) 驱动层 (Kernel Driver) 接口管理 | 缓冲区管理 | 系统调用处理 硬件抽象层 (HAL) 寄存器操作 | 参数配置 | 中断注册 固件 (Firmware) 任务调度 | 微操作执行 | 硬件控制 编码器硬件 (Encoder Hardware) DMA引擎 数据传输 中断控制器 事件通知 说明:驱动层通过HAL与固件通信,固件直接控制硬件;DMA负责数据搬运,中断负责事件通知

从这张图可以看出,整个架构是分层解耦的。每一层只关心自己的职责,层与层之间通过明确定义的接口通信。这样做的好处是:哪一层出了问题,可以单独替换或升级,不影响其他层。

总结一下:驱动层接口要简单稳定,固件调度要灵活高效,HAL层要适配解耦,中断和DMA要低延迟高吞吐。把这四点做好了,编码器部署就成功了一大半。