2、ALSA框架在MTK上的移植:ALSA核心数据结构、MTK SoC DAI驱动注册、PCM设备创建流程

好,咱们接着聊。上一章我们把ALSA的整体框架搭起来了,这一章要动真格的了——把ALSA移植到MTK平台上。

说实话,我第一次做MTK平台音频移植的时候,也被那一堆结构体搞得有点懵。什么snd_soc_dai、snd_pcm、snd_soc_platform……看着就头大。但后来我发现,只要你抓住了核心的那几个数据结构,整个框架就像拼图一样,一块一块就拼起来了。

2.1 ALSA核心数据结构:你得认识这几个“大佬”

ALSA在SoC这一层,说白了就是三个核心结构体在唱戏:

  • snd_soc_dai —— 数字音频接口,负责数据的收发
  • snd_soc_codec —— 编解码器,负责数模/模数转换
  • snd_soc_platform —— DMA传输,负责内存和DAI之间的数据搬运

嗯,这里要注意,MTK平台通常把platform和DAI合在一起实现,因为MTK的音频控制器本身就集成了DMA引擎。我个人习惯把这种叫“一体化DAI驱动”,省事。

咱们先看看snd_soc_dai这个结构体长什么样:

struct snd_soc_dai_driver {
    const char *name;
    /* 播放和捕获能力 */
    struct snd_soc_pcm_stream playback;
    struct snd_soc_pcm_stream capture;
    /* 操作函数集 */
    int (*probe)(struct snd_soc_dai *dai);
    int (*remove)(struct snd_soc_dai *dai);
    int (*trigger)(struct snd_soc_dai *dai, int cmd, int stream);
    int (*hw_params)(struct snd_soc_dai *dai, 
                     struct snd_pcm_substream *substream,
                     struct snd_pcm_hw_params *params);
    /* ... 还有其他回调 */
};

我在项目中遇到过一个问题:trigger回调里忘记处理SNDRV_PCM_TRIGGER_STOP的情况,结果每次停止播放时DAI的时钟没有关掉,导致功耗一直下不来。嗯,这种坑踩过一次就记住了。

2.2 MTK SoC DAI驱动注册:把硬件“告诉”内核

DAI驱动注册,说白了就是把你硬件的能力描述给ALSA框架。MTK平台的做法一般是这样的:

  1. 定义一个snd_soc_dai_driver结构体,填好你的硬件参数
  2. 定义一个snd_soc_dai结构体,关联上面的driver
  3. 调用snd_soc_register_dai()把它注册到系统中

来看看MTK典型的DAI注册代码:

static const struct snd_soc_dai_ops mtk_dai_ops = {
    .trigger   = mtk_dai_trigger,
    .hw_params = mtk_dai_hw_params,
    .hw_free   = mtk_dai_hw_free,
};

static struct snd_soc_dai_driver mtk_dai_driver = {
    .name = "mtk-dai",
    .playback = {
        .stream_name = "Playback",
        .channels_min = 1,
        .channels_max = 2,
        .rates = SNDRV_PCM_RATE_8000_48000,
        .formats = SNDRV_PCM_FMTBIT_S16_LE |
                   SNDRV_PCM_FMTBIT_S24_LE,
    },
    .capture = {
        .stream_name = "Capture",
        .channels_min = 1,
        .channels_max = 2,
        .rates = SNDRV_PCM_RATE_8000_48000,
        .formats = SNDRV_PCM_FMTBIT_S16_LE,
    },
    .ops = &mtk_dai_ops,
};

小提示:rates和formats这两个字段一定要填准确。我曾经见过有人把SNDRV_PCM_RATE_8000_48000写成SNDRV_PCM_RATE_8000,结果48kHz采样率的音频播放出来全是噪音。你想想看,这种bug查起来多痛苦。

注册的时候,一般在probe函数里调用:

static int mtk_audio_probe(struct platform_device *pdev)
{
    struct snd_soc_dai *dai;
    int ret;

    /* 分配DAI结构体 */
    dai = devm_kzalloc(&pdev->dev, sizeof(*dai), GFP_KERNEL);
    if (!dai)
        return -ENOMEM;

    /* 注册DAI驱动 */
    ret = snd_soc_register_dai(&pdev->dev, &mtk_dai_driver);
    if (ret) {
        dev_err(&pdev->dev, "Failed to register DAI: %d\n", ret);
        return ret;
    }

    dev_info(&pdev->dev, "MTK DAI registered successfully\n");
    return 0;
}

2.3 PCM设备创建流程:让音频“流”起来

PCM设备,就是用户空间看到的那个/dev/snd/pcmC0D0p。创建流程其实不复杂,我把它拆成三步:

  • 第一步:创建PCM实例 —— snd_pcm_new()
  • 第二步:设置操作函数 —— 填充snd_pcm_ops
  • 第三步:注册PCM设备 —— snd_card_register()

等等,这里有个关键点——PCM设备是挂在声卡(snd_card)下面的。所以你得先创建声卡,再创建PCM设备。

来看看完整的流程:

static int mtk_pcm_new(struct snd_soc_pcm_runtime *rtd)
{
    struct snd_card *card = rtd->card->snd_card;
    struct snd_pcm *pcm;
    int ret;

    /* 创建PCM实例,方向为播放+捕获 */
    ret = snd_pcm_new(card, "MTK PCM", 0, 1, 1, &pcm);
    if (ret) {
        dev_err(card->dev, "Failed to create PCM: %d\n", ret);
        return ret;
    }

    /* 设置PCM操作函数 */
    snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &mtk_pcm_ops);
    snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &mtk_pcm_ops);

    /* 分配DMA缓冲区 */
    snd_pcm_set_managed_buffer_all(pcm, SNDRV_DMA_TYPE_DEV,
                                   card->dev, 64*1024, 256*1024);

    return 0;
}

注意:DMA缓冲区的大小要根据你的硬件来定。太小了容易产生XRUN(欠载/过载),太大了又浪费内存。我一般建议先设成64KB,跑起来之后用arecord/aplay测试一下,再根据实际情况调整。

mtk_pcm_ops里最核心的是这几个回调:

static const struct snd_pcm_ops mtk_pcm_ops = {
    .open      = mtk_pcm_open,
    .close     = mtk_pcm_close,
    .ioctl     = snd_pcm_lib_ioctl,
    .hw_params = mtk_pcm_hw_params,
    .hw_free   = mtk_pcm_hw_free,
    .prepare   = mtk_pcm_prepare,
    .trigger   = mtk_pcm_trigger,
    .pointer   = mtk_pcm_pointer,
};

这里我特别想说说pointer回调。这个回调要返回当前DMA读/写的位置,单位是字节。如果这个值算错了,alsa-lib那边就会以为数据没传完或者传过了,结果就是声音断断续续的。

我曾经在MT6765平台上遇到过这个问题——pointer回调里忘记考虑DMA的循环缓冲区的wrap-around情况,结果播放到缓冲区末尾时,pointer突然跳回0,上层应用以为数据还没准备好,直接卡住了。嗯,这种问题查起来真的很费劲。

2.4 整个流程串起来:从驱动加载到设备就绪

好了,咱们把上面这些串起来,看看MTK平台上ALSA移植的完整流程:

  1. 模块加载:platform_driver_register()被调用,进入probe
  2. 创建声卡:snd_card_new()创建snd_card实例
  3. 注册DAI:snd_soc_register_dai()把硬件能力告诉框架
  4. 创建PCM:snd_pcm_new()创建播放/捕获设备
  5. 设置操作函数:snd_pcm_set_ops()绑定回调
  6. 注册声卡:snd_card_register()让用户空间能看到设备

说白了,就是先搭房子(声卡),再装门窗(DAI),最后通水电(PCM)。每一步都有对应的内核API,你只要按顺序调用就行。

核心要点:

  • snd_soc_dai_driver描述硬件能力,snd_soc_dai是运行时实例
  • PCM设备必须挂在snd_card下面,一个声卡可以有多个PCM设备
  • DMA缓冲区大小和pointer回调的正确性,直接影响音频质量
  • 调试时多用tinymix查看控件,用tinycap/tinypcminfo验证PCM设备

下一章我们会深入MTK平台的音频时钟管理和电源管理,这部分才是真正让人头疼的地方。到时候我会分享一些我在MT6779平台上调试时钟同步问题的经验,保证让你少走弯路。