- 了解ASoC架构 platform
1.概述
在Platform部分,主要是平台相关的DMA操作和音频管理。流程是先将音频数据从内存通过DMA方式传输到CPU侧的dai接口,然后通过CPU的dai接口(通过I2S总线)将数据送达到Codec中,数据会在Codec侧进行解码操作,最终输出到耳机/音箱中。下图作为参考:
Platform驱动的主要作用是完成音频数据的管理,最终通过CPU的数字音频接口(DAI)把音频数据传送给Codec进行处理,最终由Codec输出驱动耳机或者是喇叭的音信信号。
在具体实现上,ASoC把Platform驱动分为两个部分:
-
snd_soc_dai_driver - 代表cpu侧的dai驱动,其中包括dai的配置(音频格式,clock,音量等)。
-
snd_soc_platform_driver - 代表平台使用的dma驱动,主要是数据的传输等。
其中,snd_soc_platform_driver负责管理音频数据,把音频数据通过dma或其他操作传送至cpu dai中,snd_soc_dai_driver则主要完成cpu一侧的dai的参数配置,同时也会通过一定的途径把必要的dma等参数与snd_soc_platform_driver进行交互。
2.Platform代码分析
2.1.platform注册
- platform_driver
sound/soc/samsung/s3c24xx-i2s.c:
static struct platform_driver s3c24xx_iis_driver = {
.probe = s3c24xx_iis_dev_probe,
.driver = {
.name = "s3c24xx-iis",
},
};
- platform_device
arch/arm/plat-samsung/devs.c:
#ifdef CONFIG_PLAT_S3C24XX
static struct resource s3c_iis_resource[] = {
[0] = DEFINE_RES_MEM(S3C24XX_PA_IIS, S3C24XX_SZ_IIS),
};
struct platform_device s3c_device_iis = {
.name = "s3c24xx-iis",
.id = -1,
.num_resources = ARRAY_SIZE(s3c_iis_resource),
.resource = s3c_iis_resource,
.dev = {
.dma_mask = &samsung_device_dma_mask,
.coherent_dma_mask = DMA_BIT_MASK(32),
}
};
#endif /* CONFIG_PLAT_S3C24XX */
匹配上之后,调用 s3c24xx_iis_dev_probe函数。
static int s3c24xx_iis_dev_probe(struct platform_device *pdev)
{
int ret = 0;
struct resource *res;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) {
dev_err(&pdev->dev, "Can't get IO resource.\n");
return -ENOENT;
}
s3c24xx_i2s.regs = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(s3c24xx_i2s.regs))
return PTR_ERR(s3c24xx_i2s.regs);
s3c24xx_i2s_pcm_stereo_out.dma_addr = res->start + S3C2410_IISFIFO;
s3c24xx_i2s_pcm_stereo_in.dma_addr = res->start + S3C2410_IISFIFO;
ret = devm_snd_soc_register_component(&pdev->dev,
&s3c24xx_i2s_component, &s3c24xx_i2s_dai, 1);
if (ret) {
pr_err("failed to register the dai\n");
return ret;
}
ret = samsung_asoc_dma_platform_register(&pdev->dev);
if (ret)
pr_err("failed to register the dma: %d\n", ret);
return ret;
}
该函数主要完成:
- i2s控制器的地址映射,dma的地址映射;
- 通过samsung_asoc_dma_platform_register注册dma相关;
- 通过devm_snd_soc_register_component注册一个component组件。传入的参数分别是snd_soc_component_driver和snd_soc_dai_driver。
2.2.snd_soc_dai driver驱动注册
首先通过devm_snd_soc_register_component注册一个component组件。传入的参数分别是snd_soc_component_driver和snd_soc_dai_driver,这个跟codec的dai的结构类似。
static const struct snd_soc_dai_ops s3c24xx_i2s_dai_ops = {
.trigger = s3c24xx_i2s_trigger,
.hw_params = s3c24xx_i2s_hw_params,
.set_fmt = s3c24xx_i2s_set_fmt,
.set_clkdiv = s3c24xx_i2s_set_clkdiv,
.set_sysclk = s3c24xx_i2s_set_sysclk,
};
static struct snd_soc_dai_driver s3c24xx_i2s_dai = {
.probe = s3c24xx_i2s_probe,
.suspend = s3c24xx_i2s_suspend,
.resume = s3c24xx_i2s_resume,
.playback = {
.channels_min = 2,
.channels_max = 2,
.rates = S3C24XX_I2S_RATES,
.formats = SNDRV_PCM_FMTBIT_S8 | SNDRV_PCM_FMTBIT_S16_LE,},
.capture = {
.channels_min = 2,
.channels_max = 2,
.rates = S3C24XX_I2S_RATES,
.formats = SNDRV_PCM_FMTBIT_S8 | SNDRV_PCM_FMTBIT_S16_LE,},
.ops = &s3c24xx_i2s_dai_ops,
};
static const struct snd_soc_component_driver s3c24xx_i2s_component = {
.name = "s3c24xx-i2s",
};
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支持的声道数,码率,数据格式等能力;
- 对于devm_snd_soc_register_component这个接口,其最终调用snd_soc_register_component这个接口。
int snd_soc_register_component(struct device *dev,
const struct snd_soc_component_driver *cmpnt_drv,
struct snd_soc_dai_driver *dai_drv,
int num_dai)
{
struct snd_soc_component *cmpnt;
int ret;
cmpnt = kzalloc(sizeof(*cmpnt), GFP_KERNEL);
if (!cmpnt) {
dev_err(dev, "ASoC: Failed to allocate memory\n");
return -ENOMEM;
}
ret = snd_soc_component_initialize(cmpnt, cmpnt_drv, dev);
if (ret)
goto err_free;
cmpnt->ignore_pmdown_time = true;
cmpnt->registered_as_component = true;
ret = snd_soc_register_dais(cmpnt, dai_drv, num_dai, true);
if (ret < 0) {
dev_err(dev, "ASoC: Failed to regster DAIs: %d\n", ret);
goto err_cleanup;
}
snd_soc_component_add(cmpnt);
return 0;
err_cleanup:
snd_soc_component_cleanup(cmpnt);
err_free:
kfree(cmpnt);
return ret;
}
此函数和snd_soc_register_codec的大体流程一致,都是初始化snd_soc_component的实例,然后注册dai,最终将注册的dai放入到component->dai_list中,然后将分配的component放入到component_list链表中。ops字段指向一个snd_soc_dai_ops结构,该结构实际上是一组回调函数的集合,dai的配置和控制几乎都是通过这些回调函数来实现的。
static const struct snd_soc_dai_ops s3c24xx_i2s_dai_ops = {
.trigger = s3c24xx_i2s_trigger,
.hw_params = s3c24xx_i2s_hw_params,
.set_fmt = s3c24xx_i2s_set_fmt,
.set_clkdiv = s3c24xx_i2s_set_clkdiv,
.set_sysclk = s3c24xx_i2s_set_sysclk,
}
- trigger:数据传送的开始,暂停,恢复和停止时,该函数会被调用。
- hw_params:驱动的hw_params阶段,该函数会被调用。通常,该函数会通过snd_soc_dai_get_dma_data函数获得对应的dai的dma参数,获得的参数一般都会保存在snd_pcm_runtime结构的private_data字段。然后通过snd_pcm_set_runtime_buffer函数设置snd_pcm_runtime结构中的dma buffer的地址和大小等参数。要注意的是,该回调可能会被多次调用,具体实现时要小心处理多次申请资源的问题。
- set_fmt:设置dai的格式
- set_clkdiv: 设置分频系数
- set_sysclk: 设置dai的主时钟
2.3.dma注册
对于DMA,首先要配置一些DMA的参数,对于该接口中,在dai的probe函数中有一个初始化dma的接口:
static struct snd_dmaengine_dai_dma_data s3c24xx_i2s_pcm_stereo_out = {
.chan_name = "tx",
.addr_width = 2,
};
static struct snd_dmaengine_dai_dma_data s3c24xx_i2s_pcm_stereo_in = {
.chan_name = "rx",
.addr_width = 2,
};
snd_soc_dai_init_dma_data(dai, &s3c24xx_i2s_pcm_stereo_out,
&s3c24xx_i2s_pcm_stereo_in);
通过该接口会将dma的channel,name,size分别配置到dai的playback和capture中;注册dma的实现,该接口中有samsung_dmaengine_pcm_config结构,是传输pcm数据平台的DMA的相关配置。比如DMA传输之前要做方向,位数,源地址,目的地址的配置。这些都是个具体平台相关的。
int snd_dmaengine_pcm_register(struct device *dev,
const struct snd_dmaengine_pcm_config *config, unsigned int flags)
{
struct dmaengine_pcm *pcm;
int ret;
pcm = kzalloc(sizeof(*pcm), GFP_KERNEL);
if (!pcm)
return -ENOMEM;
pcm->config = config;
pcm->flags = flags;
ret = dmaengine_pcm_request_chan_of(pcm, dev, config);
if (ret)
goto err_free_dma;
ret = snd_soc_add_platform(dev, &pcm->platform,
&dmaengine_pcm_platform);
if (ret)
goto err_free_dma;
return 0;
err_free_dma:
dmaengine_pcm_release_chan(pcm);
kfree(pcm);
return ret;
}
- 此处分配一个dmaengine_pcm结构,然后根据传入的config和flag设置pcm。
- 获取dma的传输通道,根据传输的是否是半双工,设置pcm的通道。
- 调用snd_soc_add_platform函数注册platformd到ASOC core。
snd_soc_add_platform函数:
int snd_soc_add_platform(struct device *dev, struct snd_soc_platform *platform,
const struct snd_soc_platform_driver *platform_drv)
{
int ret;
ret = snd_soc_component_initialize(&platform->component,
&platform_drv->component_driver, dev);
if (ret)
return ret;
platform->dev = dev;
platform->driver = platform_drv;
if (platform_drv->probe)
platform->component.probe = snd_soc_platform_drv_probe;
if (platform_drv->remove)
platform->component.remove = snd_soc_platform_drv_remove;
#ifdef CONFIG_DEBUG_FS
platform->component.debugfs_prefix = "platform";
#endif
mutex_lock(&client_mutex);
snd_soc_component_add_unlocked(&platform->component);
list_add(&platform->list, &platform_list);
mutex_unlock(&client_mutex);
dev_dbg(dev, "ASoC: Registered platform '%s'\n",
platform->component.name);
return 0;
}
该函数主要初始化platform的component, 设置probe, remove回调,将platform->component添加到component_list链表中,最终将platform添加到platform_list中。此时对于platform的驱动一般为以下步骤:
- 分配一个cpu_dai_name的平台驱动,注册。
- 分配一个struct snd_soc_dai_driver结构,然后设置相应数据。
- 调用snd_soc_register_component函数注册cpu侧的dai结构。
- 分配一个struct snd_soc_platform_driver结构,设置相应的数据。
- 最终调用snd_soc_add_platform函数添加snd_soc_platform_driver结构。
refer to
- https://www.cnblogs.com/alan666/p/8311870.html