3、音频驱动框架:ALSA架构在MTK平台上的移植、ASoC框架(Machine/Codec/Platform驱动)、DMA引擎配置

好,咱们进入第三讲。这一讲,我打算把MTK8676上音频驱动的骨架给你拆开看看。说白了,就是ALSA怎么在MTK上落地,ASoC那三驾马车——Machine、Codec、Platform——到底怎么分工,还有DMA引擎怎么配才能不卡顿。

我个人习惯,讲驱动框架不喜欢上来就贴大段代码。咱们先理清脉络,再动手。你想想看,音频数据从APP发出来,到喇叭出声,中间要经过多少层?嗯,这就是我们今天要聊的。

3.1 ALSA在MTK平台上的移植

ALSA,Advanced Linux Sound Architecture,在MTK平台上移植,其实没那么玄乎。核心就两件事:一是把内核的ALSA框架选上,二是把MTK私有的音频硬件抽象层对接上去。

我在项目中遇到过,很多人一上来就改dts,改得飞起,结果alsa-lib层报错,连个pcm设备都打不开。为什么?因为底层寄存器映射没对上。

移植要点:

  • 内核配置:CONFIG_SOUND、CONFIG_SND、CONFIG_SND_MTK_* 必须打开
  • DTS节点:定义好audio-controller,reg、interrupts、clocks一个不能少
  • 时钟树:MTK的音频时钟源多,要确保APLL、BCLK、LRCK频率匹配

举个例子,MTK8676的I2S接口,时钟源可以来自内部PLL,也可以来自外部晶振。我建议你优先用内部PLL,调试起来省心。外部晶振一旦频率漂移,整个音频链路就崩了。

// DTS示例:MTK8676 I2S节点
i2s0: i2s@11220000 {
    compatible = "mediatek,mt8676-i2s";
    reg = <0 0x11220000 0 0x1000>;
    interrupts = <GIC_SPI 55 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&apmixedsys CLK_APMIXED_I2S0_MCK>,
             <&infracfg CLK_INFRA_I2S0_BCLK>;
    clock-names = "mclk", "bclk";
    dmas = <&audiodma 0>, <&audiodma 1>;
    dma-names = "tx", "rx";
    status = "disabled";
};

避坑指南:我曾经在移植时忽略了dma-names的顺序,结果tx和rx反了,录音变成了放音,排查了整整两天。记住,dma-names要和驱动里request_irq的顺序一致。

3.2 ASoC框架:Machine/Codec/Platform驱动

ASoC,ALSA System on Chip,是专门为嵌入式音频设计的。它把音频驱动拆成三块:Machine、Codec、Platform。说白了,就是各管各的,互不干扰。

我刚开始接触ASoC时,觉得这玩意儿太绕了。后来做多了才发现,这种分层设计太聪明了。你想想看,Codec芯片换了,Platform驱动不用动;DMA引擎换了,Machine驱动不用动。多爽。

3.2.1 Machine驱动

Machine驱动是胶水层。它负责把Codec和Platform粘在一起。具体干三件事:

  • 注册声卡(snd_soc_card)
  • 定义DAI链路(snd_soc_dai_link)
  • 配置音频路由(snd_soc_dapm_route)

我在MTK8676上做多路混音时,Machine驱动里定义了4条DAI链路:主驾、副驾、后排左、后排右。每条链路都有自己的Codec和Platform。

// Machine驱动核心结构
static struct snd_soc_dai_link mt8676_dai_links[] = {
    {
        .name = "Primary",
        .stream_name = "Primary Playback",
        .cpu_dai_name = "mt8676-i2s.0",
        .codec_dai_name = "rt5682-aif1",
        .platform_name = "mt8676-pcm-audio",
        .codec_name = "rt5682.0-001a",
        .ops = &mt8676_ops,
    },
    // ... 其他链路类似
};

注意:Machine驱动里dai_link的name不能重复,否则注册声卡时会报-EEXIST。我见过有人把四个链路都叫"Playback",结果只有第一个生效。

3.2.2 Codec驱动

Codec驱动管的是音频编解码芯片。比如RT5682、ES9018这些。它要实现的接口包括:

  • DAI操作(hw_params、set_fmt、trigger)
  • 控件(音量、静音、EQ)
  • DAPM(动态电源管理)

我个人习惯,Codec驱动里最烦的是DAPM。你想想看,一个Codec可能有几十个widget,每个widget又有自己的电源域。配不好,要么喇叭不出声,要么功耗爆炸。

我记得有一次,RT5682的HPOUT widget没连到DAC,结果耳机没声音。查了三天,最后发现是DAPM路由少了一条线。

3.2.3 Platform驱动

Platform驱动管的是DMA传输和CPU侧的DAI接口。MTK8676的Platform驱动,核心就是配置AudioDMA。

说白了,Platform驱动就是数据搬运工。APP把音频数据写到内存,Platform驱动通过DMA搬到I2S FIFO,然后Codec拿走。

// Platform驱动hw_params回调
static int mtk_pcm_hw_params(struct snd_pcm_substream *substream,
                             struct snd_pcm_hw_params *params)
{
    struct snd_pcm_runtime *runtime = substream->runtime;
    unsigned int channels = params_channels(params);
    unsigned int rate = params_rate(params);
    snd_pcm_format_t format = params_format(params);

    // 配置DMA传输参数
    mtk_audio_dma_config(substream, channels, rate, format);
    return 0;
}

3.3 DMA引擎配置

DMA引擎,是音频性能的关键。MTK8676用的是AudioDMA,支持多通道、多链路并发。

配置DMA,核心就三个参数:

参数 说明 典型值
周期大小 每次中断传输的数据量 1024字节
缓冲区大小 DMA环形缓冲区总大小 65536字节
传输宽度 每次DMA搬运的数据位宽 32位

我在项目中遇到过,周期设得太小,中断太频繁,CPU占用率飙升。设得太大,延迟又高。怎么平衡?我建议你从1024字节开始调,然后根据实际播放效果微调。

经验之谈:多路混音时,每条链路的DMA要分配独立的通道。我曾经图省事,让主驾和副驾共用一条DMA通道,结果声音串了。嗯,这个坑你们别踩。

DMA引擎的初始化代码,一般在Platform驱动的probe函数里完成:

static int mtk_audio_dma_probe(struct platform_device *pdev)
{
    struct mtk_audio_dma *dma;
    int ret;

    dma = devm_kzalloc(&pdev->dev, sizeof(*dma), GFP_KERNEL);
    if (!dma)
        return -ENOMEM;

    // 获取DMA寄存器基址
    dma->regs = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(dma->regs))
        return PTR_ERR(dma->regs);

    // 注册DMA引擎
    ret = devm_snd_dmaengine_pcm_register(&pdev->dev, NULL, 0);
    if (ret) {
        dev_err(&pdev->dev, "Failed to register DMA engine\n");
        return ret;
    }

    dev_info(&pdev->dev, "MTK Audio DMA probed\n");
    return 0;
}

好了,这一讲的内容就这些。ALSA移植、ASoC三件套、DMA配置,这三块是音频驱动的基石。下一讲,咱们聊聊多路混音的具体实现,那才是真正考验架构能力的地方。