Linux音频子系统(九)ASoC架构中的platform

Platform驱动的主要作用是完成音频数据的管理,最终通过CPU的数字音频接口(DAI)把音频数据传送给Codec进行处理,最终由Codec输出驱动耳机或者是喇叭的音信信号。在具体实现上,ASoC有把Platform驱动分为两个部分:snd_soc_platform_driver和snd_soc_dai_driver。其中,platform_driver负责管理音频数据,把音频数据通过dma或其他操作传送至cpu dai中,dai_driver则主要完成cpu一侧的dai的参数配置,同时也会通过一定的途径把必要的dma等参数与snd_soc_platform_driver进行交互。本章的主要内容

  1. 怎么管理音频数据,音频数据通过dma怎么传送到CPU dai中
  2. cpu dai主要做哪些配置

1. platform注册

对于注册,platform_driver中的name与machine中定义的platform一样,就会走到probe函数,该函数就会将snd_soc_platform和snd_soc_dai_driver注册到对应的链表中,下面来看看初始化函数做了些什么?

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;
}

static struct platform_driver s3c24xx_iis_driver = {
	.probe  = s3c24xx_iis_dev_probe,
	.driver = {
		.name = "s3c24xx-iis",
		.owner = THIS_MODULE,
	},
};

该函数主要完成了

  1. i2s控制器的地址映射,dma的地址映射
    2.通过devm_snd_soc_register_component注册一个component组件。传入的参数分别是snd_soc_component_driver和snd_soc_dai_driver
  2. 通过samsung_asoc_dma_platform_register注册dma相关

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芯片进行定义,其主要有以下几个:

  1. probe、remove 回调函数,分别在声卡加载和卸载时被调用;
  2. suspend、resume 电源管理回调函数;
  3. ops 指向snd_soc_dai_ops结构,用于配置和控制该dai;
  4. playback snd_soc_pcm_stream结构,用于指出该dai支持的声道数,码率,数据格式等能力;
  5. 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的主时钟

3. dma注册

对于DMA,首先要配置一些DMA的参数,对于该接口中,在dai的probe函数中有一个初始化dma的接口


static struct s3c_dma_params s3c2412_i2s_pcm_stereo_out = {
	.channel	= DMACH_I2S_OUT,
	.ch_name	= "tx",
	.dma_size	= 4,
};

static struct s3c_dma_params s3c2412_i2s_pcm_stereo_in = {
	.channel	= DMACH_I2S_IN,
	.ch_name	= "rx",
	.dma_size	= 4,
};
samsung_asoc_init_dma_data(dai, &s3c2412_i2s_pcm_stereo_out,
		&s3c2412_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;
}
  1. 此处分配一个dmaengine_pcm结构,然后根据传入的config和flag设置pcm。
  2. 获取dma的传输通道,根据传输的是否是半双工,设置pcm的通道。
  3. 调用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的驱动一般为以下步骤

  1. 分配一个cpu_dai_name的平台驱动,注册。
  2. 分配一个struct snd_soc_dai_driver结构,然后设置相应数据。
  3. 调用snd_soc_register_component函数注册cpu侧的dai结构。
  4. 分配一个struct snd_soc_platform_driver结构,设置相应的数据。
  5. 最终调用snd_soc_add_platform函数添加snd_soc_platform_driver结构。

4.音频数据流

前面两节大致梳理了下平台设备注册了一个dai和dma,那么对于音频的数据流是怎么处理的,本章来接上节来深入看看其数据流向。其数据流向包括

  • 播放时,应用程序把音频数据源源不断地写入dma buffer中,然后相应platform的dma操作则不停地从该buffer中取出数据,经dai送往codec中。
  • 录音时,codec源源不断地把A/D转换好的音频数据经过dai送入dma buffer中,而应用程序则不断地从该buffer中读走音频数据。
    对设备驱动的编写者来说,要基于dma engine提供的Slave-DMA API进行DMA传输的话,需要如下的操作步骤:
  1. 申请一个DMA channel。
  2. 根据设备(slave)的特性,配置DMA channel的参数。
  3. 要进行DMA传输的时候,获取一个用于识别本次传输(transaction)的描述符(descriptor)。
  4. 本次传输(transaction)提交给dma engine并启动传输。
  5. 等待传输(transaction)结束。然后,重复3~5即可。

下面我们来依照上面的流程来梳理下代码,在上章注册platform中,snd_soc_add_platform(dev, &pcm->platform, &dmaengine_pcm_platform)会提供操作函数集

static const struct snd_pcm_ops dmaengine_pcm_ops = {
	.open		= dmaengine_pcm_open,
	.close		= snd_dmaengine_pcm_close,
	.ioctl		= snd_pcm_lib_ioctl,
	.hw_params	= dmaengine_pcm_hw_params,
	.hw_free	= snd_pcm_lib_free_pages,
	.trigger	= snd_dmaengine_pcm_trigger,
	.pointer	= dmaengine_pcm_pointer,
};
static const struct snd_soc_platform_driver dmaengine_pcm_platform = {
	.component_driver = {
		.probe_order = SND_SOC_COMP_ORDER_LATE,
	},
	.ops		= &dmaengine_pcm_ops,
	.pcm_new	= dmaengine_pcm_new,
	.pcm_free	= dmaengine_pcm_free,
}

对于DMA的申请使用dmaengine_pcm_new,而对于配置和开始传输使用dmaengine_pcm_ops的操作函数集来完成,对于具体的在后面从上到下框架介绍中完整介绍,对此,platform做了些什么也基本介绍完。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值