在前面Linux ALSA 之六:ALSA ASoc 架构已经介绍了什么是 ALSA ASoc
,以及从 Hw & Sw 来看 ASoc Driver 都分为 Platform & Machine & Codec Driver
,在本节中将介绍 Platform Driver。
如前面所述,Platform 指某款 Soc 平台的音频模块,如 Mediatek、Samsung
等。Platform 部件驱动的主要作用是完成音频数据的管理,最终通过 CPU 的数据音频接口(DAI)把音频数据传送给 Codec 进行处理,最终由 Codec 输出驱动耳机或是喇叭的音频信号。
在具体实现上,ASoC 有把 Platform 驱动分为两个部分:
linux-3.0 snd_soc_patform_driver
,其主要负责管理音频数据,把音频数据通过 dma 或其他操作传送到 cpu dai 中,即负责控制 dma buffer & 将 dma buffer 中的音频数据搬运到后端的 FIFO,如 DSP PCM FIFO、I2S tx FIFO 等。音频 pcm dma 驱动需要在 platform 部件驱动中通过 snd_soc_register_component()
来注册。(即将 pcm dma 注册为一个只有 component_driver 的 component(没有 dai)
,在 machine 驱动中会将其绑定到 runtime 中,以便可以访问操作)音频数据从 I2S tx FIFO 搬运到 I2S rx FIFO
(这是回放的情形,录制方向相反)。注意,DAI 是 Digital Audio Interface
的简称,分为 cpu dai 和 codec dai,这两者通过 I2S/PCM 总线连接;AIF 是 Audio Interface 的简称,嵌入式系统中一般是 I2S 和 PCM 接口。【Note】后面描述用 Pcm Dma Driver 代表 Soc Platform Driver;用 Linux Platform Driver 指的是 linux 驱动模型 platform driver(不要被这两个相像的术语所迷惑,前者只是针对 ASoC 子系统的,后者是来自 Linux 的设备驱动模型)
【Note】在上述中会针对 PCM DMA 单独调用 snd_soc_register_component() 注册一个 component(而非使用 dai 对应的 component),猜测是仿照以前需要专门注册 dma platform driver 的 linux kernel 版本的方式,而目前在新的 linux kernel 版本中 PCM DMA & CPU DAI 可以注册在同一 Component
,故将 dma component driver 中需要的操作直接在 dai 对应的 component driver 中。
(下面均以 mt2701-afe-pcm.c
为例进行讲解)
该部分其实就是 /sound/soc/medaitek/mt2701/mt2701-afe-pcm.c
中的 platform driver 与 /arch/arm/boot/dts/mt2701.dtsi
中的 platform device 进行匹配,匹配成功后调用 mt2701_afe_pcm_dev_probe()
函数。
1)mt2701-afe-pcm.c
static const struct of_device_id mt2701_afe_pcm_dt_match[] = {
{ .compatible = "mediatek,mt2701-audio", .data = &mt2701_soc_v1 },
{ .compatible = "mediatek,mt7622-audio", .data = &mt2701_soc_v2 },
{},
};
MODULE_DEVICE_TABLE(of, mt2701_afe_pcm_dt_match);
static struct platform_driver mt2701_afe_pcm_driver = {
.driver = {
.name = "mt2701-audio",
.of_match_table = mt2701_afe_pcm_dt_match,
#ifdef CONFIG_PM
.pm = &mt2701_afe_pm_ops,
#endif
},
.probe = mt2701_afe_pcm_dev_probe,
.remove = mt2701_afe_pcm_dev_remove,
};
module_platform_driver(mt2701_afe_pcm_driver);
在 platform driver
中有注册名为 "mediatek,mt2701-audio"
;
2)mt2701.dtsi
afe: audio-controller {
compatible = "mediatek,mt2701-audio";
interrupts = <GIC_SPI 104 IRQ_TYPE_LEVEL_LOW>,
<GIC_SPI 132 IRQ_TYPE_LEVEL_LOW>;
interrupt-names = "afe", "asys";
};
在设备树文件中有注册名为 "mediatek,mt2701-audio"
的 platform device
,当 platform driver & platform device 匹配之后则会调用 platform_driver 下的 probe() 函数。
1)Platform Driver (PCM DMA)
devm_snd_soc_register_component(&pdev->dev, &mtk_afe_pcm_platform, NULL, 0); //添加一个 dai_drv 为 NULL 的 component_drv => 控制 DMA
在 probe() 函数中会先调用上述函数将 platform driver(struct snd_soc_component_driver mtk_afe_pcm_platform)
注册为一个 component,只有注册后才能被 Machine 驱动使用。它的实现过程:为 snd_soc_component 实例分配内存,初始化相应将 snd_soc_component 实例添加到全局链表 conponent_list
中。
2)DAI Driver (CPU DAI)
//创建 pcm_dais component
devm_snd_soc_register_component(&pdev->dev, &mt2701_afe_pcm_dai_component, mt2701_afe_pcm_dais, ARRAY_SIZE(mt2701_afe_pcm_dais));
在 probe() 函数中后面也会调用上述函数将 dai driver(struct snd_soc_dai_driver mt2701_afe_pcm_dais[])
注册为一个 component,并且通过 snd_soc_register_dais() 循环将所有的 snd_soc_dai_driver register 为 snd_soc_dai,添加到 component->dai_list
中,最后将该 component 添加到全局链表 component_list
以便后面能被 Machine 驱动使用,代码时序框图如下:
# Note:对于 platform driver ops 字段为 snd_pcm_ops
结构,DAI Driver ops 字段为 snd_soc_dai_ops
结构,对于两者的 ops 字段都会在打开 pcm substream 并进行操作时被调用,在 Machine 驱动中调用 soc_new_pcm() 时会对 substream->ops 进行赋值,该 ops 被调用时最终都会 call platform driver ops & dai driver ops 相应的回调函数,详细见后面的 Machine 小节。
1)Platform Driver (PCM DM) ops 字段
该 ops 字段是 snd_pcm_ops 结构,实现该结构中的各个回调函数是 soc platform 驱动的主要工作,它们基本都涉及 dma 操作以及 dma buffer 的管理等工作,如 mtk_afe_pcm_patform 对应的定义如下:
const struct snd_pcm_ops mtk_afe_pcm_ops = {
.ioctl = snd_pcm_lib_ioctl,
//该回调函数返回传送数据的当前位置(pcm 中间层通过调用这个函数来获取 dma 缓冲区的读指针)
//一般情况下,在中断函数中调用 snd_pcm_period_elapsed() 或在 pcm 中间层更新 Buffer 时会调用它,然后 pcm 中间层会更新指针位置和计算缓冲区可用空间,唤醒那些在等待的线程
.pointer = mtk_afe_pcm_pointer,
};
EXPORT_SYMBOL_GPL(mtk_afe_pcm_ops);
const struct snd_soc_component_driver mtk_afe_pcm_platform = {
.name = AFE_PCM_NAME,
.ops = &mtk_afe_pcm_ops,
.pcm_new = mtk_afe_pcm_new,
.pcm_free = mtk_afe_pcm_free,
};
EXPORT_SYMBOL_GPL(mtk_afe_pcm_platform);
其中 component->driver->pcm_new()
会在 Machine 调用 soc_new_pcm() 创建 pcm device 时会调用(详细见 Machine 小节),主要实现是调用 snd_pcm_lib_preallocate_pages_for_all() 函数对 DMA 进行预分配
。
对于 ops->pointer() 返回传送数据的当前位置。pcm 中间层通过调用这个函数来获取 dma 缓冲区的读指针。一般情况下,在中断函数中调用 snd_pcm_period_elapsed() 或在 pcm 中间层更新 Buffer
时会调用它,然后 pcm 中间层会更新指针位置和计算缓冲区可用空间,唤醒那些在等待的线程,代码示例实现如下:
static snd_pcm_uframes_t mtk_afe_pcm_pointer
(struct snd_pcm_substream *substream)
{
struct snd_soc_pcm_runtime *rtd = substream->private_data;
struct snd_soc_component *component = snd_soc_rtdcom_lookup(rtd, AFE_PCM_NAME);
struct mtk_base_afe *afe = snd_soc_component_get_drvdata(component);
struct mtk_base_afe_memif *memif = &afe->memif[rtd->cpu_dai->id];
const struct mtk_base_memif_data *memif_data = memif->data;
struct regmap *regmap = afe->regmap;
struct device *dev = afe->dev;
int reg_ofs_base = memif_data->reg_ofs_base;
int reg_ofs_cur = memif_data->reg_ofs_cur;
unsigned int hw_ptr = 0, hw_base = 0;
int ret, pcm_ptr_bytes;
//读 reg 获取 current offset
ret = regmap_read(regmap, reg_ofs_cur, &hw_ptr);
if (ret || hw_ptr == 0) {
dev_err(dev, "%s hw_ptr err\n", __func__);
pcm_ptr_bytes = 0;
goto POINTER_RETURN_FRAMES;
}
//读 reg 获取 base offset
ret = regmap_read(regmap, reg_ofs_base, &hw_base);
if (ret || hw_base == 0) {
dev_err(dev, "%s hw_ptr err\n", __func__);
pcm_ptr_bytes = 0;
goto POINTER_RETURN_FRAMES;
}
//计算得出在缓冲区中 pcm_ptr
pcm_ptr_bytes = hw_ptr - hw_base;
POINTER_RETURN_FRAMES:
//由于 pcm 中间层都是以 frames 为单位
//For pcm:
// 1 frame => format2bytes*channels;
// 1 sample => format2bytes
return bytes_to_frames(substream->runtime, pcm_ptr_bytes);
}
ops 各个函数均需要取得一个 snd_pcm_runtime
结构体指针,这个指针可以通过 substream->runtime 来获得。snd_pcm_runtime 是运行时的信息,当打开一个 pcm substream
时,pcm 中间层就会为该 pcm substream 分配一个 pcm_runtime
,它拥有很多种信息:hw_params、sw_params 配置拷贝,缓冲区指针信息,mmap 记录,自旋锁等。snd_pcm_runtime 对于驱动程序操作集函数都是只读的,仅 pcm 中间层可以改变或更新这些信息。
2)DAI Driver (CPU DAI) ops 字段
snd_soc_dai_driver
结构需要自己根据不同的 soc 芯片进行定义,关键字介绍如下:
成员 | 作用 |
---|---|
probe、remove | 回调函数,分别在声卡加载和卸载时被调用 |
suspend、resume | 电源管理回调函数 |
ops | 指向 snd_soc_dai_ops 结构,用于配置和控制该 dai |
playback | snd_soc_pcm_stream 结构,用于指出该 dai 支持的声道数、采样率、数据格式等 |
capture | snd_soc_pcm_stream 结构,用于指出该 dai 支持的声道数、采样率、数据格式等 |
mt2701-afe-pcm.c 定义 snd_soc_dai_driver 结构体数据代码示例如下:
static struct snd_soc_dai_driver mt2701_afe_pcm_dais[] = {
/* FE DAIs: memory intefaces to CPU */
{
.name = "PCMO0",
.id = MT2701_MEMIF_DL1,
.suspend = mtk_afe_dai_suspend,
.resume = mtk_afe_dai_resume,
.playback = {
.stream_name = "DL1",
.channels_min = 1,
.channels_max = 2,
.rates = SNDRV_PCM_RATE_8000_192000,
.formats = (SNDRV_PCM_FMTBIT_S16_LE
| SNDRV_PCM_FMTBIT_S24_LE
| SNDRV_PCM_FMTBIT_S32_LE)
},
.ops = &mt2701_single_memif_dai_ops,
},
{
.name = "PCM_multi",
.id = MT2701_MEMIF_DLM,
.suspend = mtk_afe_dai_suspend,
.resume = mtk_afe_dai_resume,
.playback = {
.stream_name = "DLM",
.channels_min = 1,
.channels_max = 8,
.rates = SNDRV_PCM_RATE_8000_192000,
.formats = (SNDRV_PCM_FMTBIT_S16_LE
| SNDRV_PCM_FMTBIT_S24_LE
| SNDRV_PCM_FMTBIT_S32_LE)
},
.ops = &mt2701_dlm_memif_dai_ops,
},
{
.name = "PCM0",
.id = MT2701_MEMIF_UL1,
.suspend = mtk_afe_dai_suspend,
.resume = mtk_afe_dai_resume,
.capture = {
.stream_name = "UL1",
.channels_min = 1,
.channels_max = 2,
.rates = SNDRV_PCM_RATE_8000_48000,
.formats = (SNDRV_PCM_FMTBIT_S16_LE
| SNDRV_PCM_FMTBIT_S24_LE
| SNDRV_PCM_FMTBIT_S32_LE)
},
.ops = &mt2701_single_memif_dai_ops,
},
{
.name = "PCM1",
.id = MT2701_MEMIF_UL2,
.suspend = mtk_afe_dai_suspend,
.resume = mtk_afe_dai_resume,
.capture = {
.stream_name = "UL2",
.channels_min = 1,
.channels_max = 2,
.rates = SNDRV_PCM_RATE_8000_192000,
.formats = (SNDRV_PCM_FMTBIT_S16_LE
| SNDRV_PCM_FMTBIT_S24_LE
| SNDRV_PCM_FMTBIT_S32_LE)
},
.ops = &mt2701_single_memif_dai_ops,
},
...
}
定义如上,其中 ops 字段指向一个 snd_soc_dai_ops 结构,该结构实际上是一组回调函数集合,dai 的配置和控制几乎都是通过这些回调函数来实现的,其定义代码示例如下:
/* FE DAIs */
static const struct snd_soc_dai_ops mt2701_single_memif_dai_ops = {
.startup = mt2701_simple_fe_startup,
.shutdown = mtk_afe_fe_shutdown,
.hw_params = mt2701_simple_fe_hw_params,
.hw_free = mtk_afe_fe_hw_free,
.prepare = mtk_afe_fe_prepare,
.trigger = mtk_afe_fe_trigger,
};
标准的 snd_soc_ops 回调,通常由 soc-core 在进行 pcm 操作时调用:
# Note:关于 DMA 操作一般会涉及源地址 & 目的地址配置
,但是对于 MTK 比较特殊,比如 mt2701-afe-pcm.c,FE DAI 的话仅仅设置了源地址,而没有设置目的地址,理解上是由于使用的 DMA Engine
已经绑定好相应的 I2S/PCM 等 IO 口,即目的地址已被固定,故只要设置了源地址 & Enable Dma Engine 后则将源地址 audio data 搬到 I2S/PCM 等 IO 口,最后由 I2S/PCM 等接口传送到 DSP Input Port
;BE DAI 的话源地址 & 目的地址均不需要要设置,理解上是 cpu dai <-> 也均已经在硬件上固定。
From Linux Kernel Document 一句话:
DMA IO to/from DSP Buffers(if applicable)
如前面“Linux ALSA 之三:简单的 ALSA Driver 实现”之 3.2 设置 PCM 操作中描述所知,当需要分配 DMA Memory 时会在 hw_params 中调用 snd_pcm_lib_malloc_pages()
函数进行分配 DMA Memory,并且将 DMA Memory 对应的物理地址设置给 DMA Engine,即设置 DMA Engine 源地址。同上,在打开 Pcm Substream 时也会调用 Platform Driver ops->hw_params & Dai Driver ops->hw_params
,故在实现时可以在两者之一的 hw_params 操作如上内容。
参考链接:
linux-alsa详解5 ASOC-platform