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平台的做法一般是这样的:
- 定义一个snd_soc_dai_driver结构体,填好你的硬件参数
- 定义一个snd_soc_dai结构体,关联上面的driver
- 调用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移植的完整流程:
- 模块加载:platform_driver_register()被调用,进入probe
- 创建声卡:snd_card_new()创建snd_card实例
- 注册DAI:snd_soc_register_dai()把硬件能力告诉框架
- 创建PCM:snd_pcm_new()创建播放/捕获设备
- 设置操作函数:snd_pcm_set_ops()绑定回调
- 注册声卡: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平台上调试时钟同步问题的经验,保证让你少走弯路。