Linux音频驱动之Asoc框架
背景
最近,把Linux的音频驱动梳理了下,以现在正在用的RK3399平台为基础。
ASoC(ALSA System on Chip)
详细参考内核文档: kernel\documentation\sound\alsa\soc\Overview.txt
ASOC由来
ALSA存在的问题:
- Codec驱动与SOC中断CPU耦合严重,这将导致代码重复,一个Codec驱动每个cpu上会出现不同的版本,很难维护
- 当音频事件发生时(插拔耳机,音箱)没有标准的方法通知用户,尤其在移动端此事件非常常见
- 当播放/录制音频时,驱动会让整个codec处于上电状态,这样会在移动端非常浪费电量。同时也不支持改变采样频率/偏置电流来节约功耗
针对以上问题,提出了ASOC(ALSA System on Chip)来力争解决上述问题。解决方法如下:
- Codec代码独立,不再与CPU耦合,这样可以增加Codec代码重复利用。
- 在Codec和Soc之间通过简单的I2S/PCM音频接口通信,这样SOC和Codec只需要注册自己相关的接口到ASOC Code即可。
- 动态的电源管理(Dynamic Audio Power Management)DAPM。DAPM始终将Codec自动设置在最低功耗状态运行。
- 消除pop音。控制各个widget上下电的顺序消除pop音。
- 添加平台相关的控制,运行平台添加控制设备到声卡。
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核心函数,注册声卡;