背景

最近,把Linux的音频驱动梳理了下,以现在正在用的RK3399平台为基础。

ASoC(ALSA System on Chip)
详细参考内核文档:  kernel\documentation\sound\alsa\soc\Overview.txt

ASOC由来

ALSA存在的问题:

  1.  Codec驱动与SOC中断CPU耦合严重,这将导致代码重复,一个Codec驱动每个cpu上会出现不同的版本,很难维护
  2.  当音频事件发生时(插拔耳机,音箱)没有标准的方法通知用户,尤其在移动端此事件非常常见
  3.  当播放/录制音频时,驱动会让整个codec处于上电状态,这样会在移动端非常浪费电量。同时也不支持改变采样频率/偏置电流来节约功耗

针对以上问题,提出了ASOC(ALSA System on Chip)来力争解决上述问题。解决方法如下:

  1.  Codec代码独立,不再与CPU耦合,这样可以增加Codec代码重复利用。
  2.  在Codec和Soc之间通过简单的I2S/PCM音频接口通信,这样SOC和Codec只需要注册自己相关的接口到ASOC Code即可。
  3.  动态的电源管理(Dynamic Audio Power Management)DAPM。DAPM始终将Codec自动设置在最低功耗状态运行。
  4.  消除pop音。控制各个widget上下电的顺序消除pop音。
  5.  添加平台相关的控制,运行平台添加控制设备到声卡。

ASOC架构

ASOC分为 Platform、Machine、Codec三大部分

  • Codec:  ASoC中的一个重要设计原则就是要求Codec驱动是平台无关的,它包含了一些音频的控件(Controls),音频接口,DAMP(动态音频电源管理)的定义和某些Codec IO功能。为了保证硬件无关性,任何特定于平台和机器的代码都要移到Platform和Machine驱动中。
  • Platform:  它包含了该SoC平台的音频DMA和音频接口的配置和控制(I2S,PCM,AC97等等);它也不能包含任何与板子或机器相关的代码。
  • Machine:  Machine驱动负责处理机器特有的一些控件和音频事件(例如,当播放音频时,需要先行打开一个放大器);单独的Platform和Codec驱动是不能工作的,它必须由Machine驱动把它们结合在一起才能完成整个设备的音频处理工作。
@startuml

box "Machine" #LightBlue
participant Platform
participant Codec
end box

Platform -> Codec : cpu_dai
Codec -> Platform : codec_dai

@enduml

Codec部分:

路径:kernel/sound/soc/codecs/
过程:DTS("ti,tlv320aic32x4")->I2C.compatible(of_device)->probe->snd_soc_register_codec(snd_soc_codec_driver,snd_soc_dai_driver)

i2c_driver:

static const struct i2c_device_id aic32x4_i2c_id[] = {
	{ "tlv320aic32x4", 0 },
	{ }
};
MODULE_DEVICE_TABLE(i2c, aic32x4_i2c_id);

static const struct of_device_id aic32x4_of_id[] = {
	{ .compatible = "ti,tlv320aic32x4", },
	{ /* senitel */ }
};
MODULE_DEVICE_TABLE(of, aic32x4_of_id);

static struct i2c_driver aic32x4_i2c_driver = {
	.driver = {
		.name = "tlv320aic32x4",
		.of_match_table = aic32x4_of_id,
	},
	.probe =    aic32x4_i2c_probe,
	.remove =   aic32x4_i2c_remove,
	.id_table = aic32x4_i2c_id,
};

module_i2c_driver(aic32x4_i2c_driver);

snd_soc_codec_driver

static struct snd_soc_codec_driver soc_codec_dev_aic32x4 = {
	.probe = aic32x4_probe,
	.set_bias_level = aic32x4_set_bias_level,
	.suspend_bias_off = true,

	.controls = aic32x4_snd_controls,
	.num_controls = ARRAY_SIZE(aic32x4_snd_controls),
	.dapm_widgets = aic32x4_dapm_widgets,
	.num_dapm_widgets = ARRAY_SIZE(aic32x4_dapm_widgets),
	.dapm_routes = aic32x4_dapm_routes,
	.num_dapm_routes = ARRAY_SIZE(aic32x4_dapm_routes),
};

codec snd_soc_dai_driver:

主要操作:

  • 工作时硬件寄存器配置(时钟分频、数据长度等)-hw_params,
  • 主时钟设置-set_sysclk
  • 主从模式设置,数据接口(I2S、PCM)-set_fmt
static const struct snd_soc_dai_ops aic32x4_ops = {
	.hw_params = aic32x4_hw_params,
	.digital_mute = aic32x4_mute,
	.set_fmt = aic32x4_set_dai_fmt,
	.set_sysclk = aic32x4_set_dai_sysclk,
};
static struct snd_soc_dai_driver aic32x4_dai = {
	.name = "tlv320aic32x4-hifi",
	.playback = {
		     .stream_name = "Playback",
		     .channels_min = 1,
		     .channels_max = 2,
		     .rates = AIC32X4_RATES,
		     .formats = AIC32X4_FORMATS,},
	.capture = {
		    .stream_name = "Capture",
		    .channels_min = 1,
		    .channels_max = 2,
		    .rates = AIC32X4_RATES,
		    .formats = AIC32X4_FORMATS,},
	.ops = &aic32x4_ops,
	.symmetric_rates = 1,
};

Platform 部分:

路径:kernel/sound/soc/xxx(平台)/
过程:DTS("rockchip,rk3399-i2s")->i2s.compatible(of_device)->probe->devm_snd_soc_register_component(snd_soc_dai_driver)

devm_snd_soc_register_component:
(1)新建一个snd_soc_component
(2)注册component和dai

platform_driver

static const struct dev_pm_ops rockchip_i2s_pm_ops = {
	SET_RUNTIME_PM_OPS(i2s_runtime_suspend, i2s_runtime_resume,
			   NULL)
	SET_SYSTEM_SLEEP_PM_OPS(rockchip_i2s_suspend, rockchip_i2s_resume)
};

static struct platform_driver rockchip_i2s_driver = {
	.probe = rockchip_i2s_probe,
	.remove = rockchip_i2s_remove,
	.driver = {
		.name = DRV_NAME,
		.of_match_table = of_match_ptr(rockchip_i2s_match),
		.pm = &rockchip_i2s_pm_ops,
	},
};
module_platform_driver(rockchip_i2s_driver);

snd_soc_dai_driver(cpu_dai)

主要操作:

  • 工作时硬件寄存器配置(主要是I2S的)-hw_params,
  • 主时钟设置-set_sysclk
  • 主从模式设置,数据接口(I2S、PCM)-set_fmt
  • I2S操作-trigger
static const struct snd_soc_dai_ops rockchip_i2s_dai_ops = {
	.hw_params = rockchip_i2s_hw_params,
	.set_sysclk = rockchip_i2s_set_sysclk,
	.set_fmt = rockchip_i2s_set_fmt,
	.trigger = rockchip_i2s_trigger,
};
static struct snd_soc_dai_driver rockchip_i2s_dai = {
	.probe = rockchip_i2s_dai_probe,
	.playback = {
		.stream_name = "Playback",
		.channels_min = 2,
		.channels_max = 8,
		.rates = SNDRV_PCM_RATE_8000_192000,
		.formats = (SNDRV_PCM_FMTBIT_S8 |
			    SNDRV_PCM_FMTBIT_S16_LE |
			    SNDRV_PCM_FMTBIT_S20_3LE |
			    SNDRV_PCM_FMTBIT_S24_LE |
			    SNDRV_PCM_FMTBIT_S32_LE),
	},
	.capture = {
		.stream_name = "Capture",
		.channels_min = 2,
		.channels_max = 2,
		.rates = SNDRV_PCM_RATE_8000_192000,
		.formats = (SNDRV_PCM_FMTBIT_S8 |
			    SNDRV_PCM_FMTBIT_S16_LE |
			    SNDRV_PCM_FMTBIT_S20_3LE |
			    SNDRV_PCM_FMTBIT_S24_LE |
			    SNDRV_PCM_FMTBIT_S32_LE),
	},
	.ops = &rockchip_i2s_dai_ops,
	.symmetric_rates = 1,
};

Machine 部分:

路径:kernel/sound/soc/xxx(平台)/,注:RK3399使用的是 simple-audio-card
过程:DTS("simple-audio-card")->compatible(of_device)->probe->devm_snd_soc_register_card(snd_soc_card)->检查匹配dai(snd_soc_dai_link)

simple-audio-card中的 snd_soc_dai_link是根据DTS配置去指定解析的

static const struct of_device_id asoc_simple_of_match[] = {
	{ .compatible = "simple-audio-card", },
	{},
};
MODULE_DEVICE_TABLE(of, asoc_simple_of_match);

static struct platform_driver asoc_simple_card = {
	.driver = {
		.name = "asoc-simple-card",
		.pm = &snd_soc_pm_ops,
		.of_match_table = asoc_simple_of_match,
	},
	.probe = asoc_simple_card_probe,
	.remove = asoc_simple_card_remove,
};

module_platform_driver(asoc_simple_card);
snd_soc_dai_link

主要注册过程

devm_snd_soc_register_card
    snd_soc_register_card —— 注册声卡
        snd_soc_instantiate_card —— 声卡初始化
            soc_bind_dai_link —— 检查struct snd_soc_dai_link中,dai_list/codec_list/platform_list中是否存在;
            snd_card_create —— ALSA核心函数,创建声卡,同时内部自动创建了control逻辑设备;
            snd_soc_dapm_new_controls():DAPM音频电源动态管理;
            soc_probe_link_components():调用soc_probe_codec/soc_probe_platform,分别初始化snd_soc_dai_link中指定的codec和platform驱动代码里的东西,包括DAPM,但不包括cpu_dai/codec_dai;
            soc_probe_link_dais():初始化snd_soc_dai_link中指定的cpu_dai和codec_dai驱动里的东西;调用soc_new_pcm(),这个函数首先创建了pcm逻辑设备,调用的是snd_device_new()的PCM类型的封装函数snd_pcm_new(),接着调用snd_pcm_set_ops()来给创建的pcm逻辑设备的substream提供pcm操作,最后调用platform中的pcm_new()函数,这个函数用来分配初始化dma;
            snd_soc_dai_set_fmt():如果提供回调,分别设置snd_soc_dai_link中的cpu_dai、codec_dai中的接口格式;
            snd_card_regster():ALSA核心函数,注册声卡;

参考

https://blog.csdn.net/u012830148/article/details/80079782