4. VDEC模块初始化:解码器设备注册、中断处理函数注册、内存池(MMZ)初始化

好,咱们今天来聊聊VDEC模块的初始化。这部分说白了,就是让解码器从「沉睡」状态进入「待命」状态的过程。我个人习惯把这一步比作「给解码器安家」——你得先给它分配好房子(设备注册)、装好门铃(中断注册)、再准备好仓库(内存池)。

我在项目中遇到过不少次,初始化顺序搞反了,结果解码器死活不干活。嗯,这里面的门道,咱们一个一个说。

4.1 解码器设备注册

设备注册,说白了就是告诉内核:「嘿,我这儿有个VDEC硬件,你管一下。」

海思平台的做法,通常是通过platform driver框架来注册。你想想看,内核怎么知道你的解码器在哪个地址?中断号是多少?这些信息都藏在设备树里。

我一般这么写:

static const struct of_device_id hi_vdec_of_match[] = {
    { .compatible = "hisilicon,hi3559av100-vdec" },
    { /* sentinel */ }
};

static struct platform_driver hi_vdec_driver = {
    .probe  = hi_vdec_probe,
    .remove = hi_vdec_remove,
    .driver = {
        .name  = "hi_vdec",
        .of_match_table = hi_vdec_of_match,
    },
};

module_platform_driver(hi_vdec_driver);

这里有个坑,我提醒一下:compatible属性一定要和设备树里完全一致。我曾经因为少写了一个字母,折腾了一整天。

关键点:probe函数里要完成三件事——获取硬件资源、映射寄存器地址、初始化硬件状态。缺一不可。

4.2 中断处理函数注册

解码器干活的时候,什么时候告诉你「我干完了」?靠中断。

中断注册,我建议放在probe函数的后半段。为什么?因为你要确保硬件已经准备好,否则中断来了你还没准备好处理,那就尴尬了。

代码示例:

static irqreturn_t hi_vdec_irq_handler(int irq, void *dev_id)
{
    struct hi_vdec_dev *dev = (struct hi_vdec_dev *)dev_id;
    u32 int_status;

    // 读中断状态寄存器
    int_status = readl(dev->reg_base + VDEC_INT_STATUS);

    // 判断是哪种中断
    if (int_status & VDEC_INT_DEC_DONE) {
        // 解码完成
        complete(&dev->dec_done);
    }

    if (int_status & VDEC_INT_ERROR) {
        // 解码出错
        dev_err(dev->dev, "VDEC hardware error!\n");
    }

    // 清中断
    writel(int_status, dev->reg_base + VDEC_INT_CLEAR);

    return IRQ_HANDLED;
}

// 在probe中注册
ret = request_irq(dev->irq_num, hi_vdec_irq_handler,
                  IRQF_TRIGGER_HIGH, "hi_vdec", dev);
if (ret) {
    dev_err(dev->dev, "Failed to request IRQ\n");
    return ret;
}

小技巧:中断处理函数里尽量少做事。我习惯只做状态记录和唤醒等待队列,真正的数据搬运放到下半部(tasklet或workqueue)去做。这样能减少中断延迟,系统更稳定。

为什么要用complete而不是wake_up?嗯,这里有个讲究。complete是完成量,适合「一次性通知」的场景。解码器每次解码完成,就是一个典型的「一次性事件」。用wait_for_completion在用户侧等着,干净利落。

4.3 内存池(MMZ)初始化

这是VDEC初始化里最容易被忽视,但也是最容易出问题的地方。

海思平台的内存管理,用的是MMZ(Media Memory Zone)。说白了,就是给多媒体模块单独划一块物理内存,不走内核的常规内存分配。为什么?因为视频解码需要大块连续的物理内存,内核的kmalloc根本满足不了。

我见过不少新手,上来就调hi_mpi_sys_mem_alloc,结果返回-1。为什么?因为MMZ池子还没初始化。

正确的流程是这样的:

  1. 配置MMZ区域大小——在系统启动参数或设备树里指定
  2. 调用MMZ初始化接口——HI_MPI_SYS_Init()
  3. 分配解码器专用内存——HI_MPI_SYS_MemAlloc()

代码示例:

// 1. 初始化系统(包含MMZ)
HI_S32 s32Ret = HI_MPI_SYS_Init();
if (s32Ret != HI_SUCCESS) {
    printf("HI_MPI_SYS_Init failed: 0x%x\n", s32Ret);
    return s32Ret;
}

// 2. 配置VDEC模块内存
VDEC_MOD_PARAM_S stModParam;
memset(&stModParam, 0, sizeof(stModParam));
stModParam.u32MaxPicWidth  = 1920;
stModParam.u32MaxPicHeight = 1080;
stModParam.u32SupportMultiStream = HI_TRUE;

s32Ret = HI_MPI_VDEC_SetModParam(&stModParam);
if (s32Ret != HI_SUCCESS) {
    printf("HI_MPI_VDEC_SetModParam failed: 0x%x\n", s32Ret);
    return s32Ret;
}

// 3. 创建解码通道时分配内存
VDEC_CHN_ATTR_S stChnAttr;
// ... 填充通道属性 ...
s32Ret = HI_MPI_VDEC_CreateChn(0, &stChnAttr);

注意:MMZ内存是物理连续的,但也是有限的。我建议你在初始化时打印一下MMZ的剩余大小:

HI_MPI_SYS_GetMemInfo(&stMemInfo);
printf("MMZ total: %d, free: %d\n", stMemInfo.u32TotalMem, stMemInfo.u32FreeMem);

如果剩余内存不够,解码器会直接报错。我曾经在4K解码项目上,因为MMZ配小了,折腾了两天才找到原因。

4.4 初始化顺序总结

说了这么多,咱们捋一下顺序:

步骤 操作 说明
1 设备树解析 获取寄存器基址、中断号
2 寄存器映射 devm_ioremap_resource
3 中断注册 request_irq
4 MMZ初始化 HI_MPI_SYS_Init
5 VDEC参数配置 HI_MPI_VDEC_SetModParam
6 创建解码通道 HI_MPI_VDEC_CreateChn

这个顺序,我建议你死记硬背下来。顺序错了,轻则功能异常,重则内核崩溃。嗯,我当年就吃过这个亏。

避坑指南:我曾经在项目里把MMZ初始化放在了中断注册之后,结果中断来了,内存还没准备好,解码器直接挂死。从那以后,我都是严格按照「硬件就绪 → 中断就绪 → 内存就绪」的顺序来。

好了,VDEC模块初始化就聊到这儿。下一章咱们讲解码通道的创建和参数配置,那才是真正开始干活的地方。