转载请标注原文地址:http://blog.youkuaiyun.com/uranus_wm/article/details/11595647
平台配置:samsung exynos4412 + wm8994(wolfson audio codec) + lsu6300v(龙尚wcdma modem)
半年前调试wm8994语音通话和录音功能时,参考了droidphone和spenic两位博友的帖子
他们对linux alsa架构有深入的了解,帖子也写得比较详细,可惜对于新手上手调试还是碰到很多问题不理解,特别是调试手段方面
在此我将自己的调试过程做个备忘录,也希望对广大苦逼的程序猿有所帮助,难免有错,还望不吝指出:
这篇文章主要介绍linux alsa的数据结构和初始化过程
后面一篇文章重点介绍soundrecord的控制和数据流程
1.说明下demo板的音频系统框图:
3G音频接口PCM直接与codec相连,通话时音频数据走向和AP没有关闭,通话录音除外
2. 介绍下注册一个alsa类型声卡涉及到的几个paltform_device和关键数据结构的系统框图:
注册声卡,文件位置:/sound/soc/samsung/smdk_wm8994.c
- static struct snd_soc_dai_link smdk_dai[] = {
- { /* Primary DAI i/f */
- .name = "WM8994 AIF1",
- .stream_name = "Pri_Dai",
- .cpu_dai_name = "samsung-i2s.0",
- .codec_dai_name = "wm8994-aif1",
- .platform_name = "samsung-audio",
- .codec_name = "wm8994-codec",
- .init = smdk_wm8994_init_paiftx,
- .ops = &smdk_ops,
- }, { /* Sec_Fifo DAI i/f */
- .name = "Sec_FIFO TX",
- .stream_name = "Sec_Dai",
- .cpu_dai_name = "samsung-i2s.4",
- .codec_dai_name = "wm8994-aif1",
- #if defined(CONFIG_SND_SAMSUNG_NORMAL) || defined(CONFIG_SND_SAMSUNG_RP)
- .platform_name = "samsung-audio",
- #else
- .platform_name = "samsung-audio-idma",
- #endif
- .codec_name = "wm8994-codec",
- .init = smdk_wm8994_init_paiftx,
- .ops = &smdk_ops,
- }, { /* Second DAI i/f */
- .name = "WM8994 AIF2",
- .stream_name = "Voice_Dai",
- .cpu_dai_name = "smdk-voice-dai",
- .codec_dai_name = "wm8994-aif2",
- .platform_name = "snd-soc-dummy",
- .codec_name = "wm8994-codec",
- .init = smdk_wm8994_init_paiftx,
- .ops = &smdk_voice_ops,
- }
- };
- static struct snd_soc_card smdk = {
- .name = "SMDK-WM8994",
- .dai_link = smdk_dai,
- .num_links = ARRAY_SIZE(smdk_dai),
- };
static struct snd_soc_dai_link smdk_dai[] = {
{ /* Primary DAI i/f */
.name = "WM8994 AIF1",
.stream_name = "Pri_Dai",
.cpu_dai_name = "samsung-i2s.0",
.codec_dai_name = "wm8994-aif1",
.platform_name = "samsung-audio",
.codec_name = "wm8994-codec",
.init = smdk_wm8994_init_paiftx,
.ops = &smdk_ops,
}, { /* Sec_Fifo DAI i/f */
.name = "Sec_FIFO TX",
.stream_name = "Sec_Dai",
.cpu_dai_name = "samsung-i2s.4",
.codec_dai_name = "wm8994-aif1",
#if defined(CONFIG_SND_SAMSUNG_NORMAL) || defined(CONFIG_SND_SAMSUNG_RP)
.platform_name = "samsung-audio",
#else
.platform_name = "samsung-audio-idma",
#endif
.codec_name = "wm8994-codec",
.init = smdk_wm8994_init_paiftx,
.ops = &smdk_ops,
}, { /* Second DAI i/f */
.name = "WM8994 AIF2",
.stream_name = "Voice_Dai",
.cpu_dai_name = "smdk-voice-dai",
.codec_dai_name = "wm8994-aif2",
.platform_name = "snd-soc-dummy",
.codec_name = "wm8994-codec",
.init = smdk_wm8994_init_paiftx,
.ops = &smdk_voice_ops,
}
};
static struct snd_soc_card smdk = {
.name = "SMDK-WM8994",
.dai_link = smdk_dai,
.num_links = ARRAY_SIZE(smdk_dai),
};
BSP初始化的时候,注册了一个名为"soc-audio"的平台设备,该设备有一个私有数据结构体snd_soc_dai_link
本例中smdk_dai总共有三个dai_link,具体说明一下:
前两个dai_link分别是Primary DAI和Sec_Fifo DAI在物理层上其实都是exynos4412的I2S0与wm8994的aif1相连接
只不过软件上用了多路复用,表示层两个dai_link。作用在4412这边Sec_Fifo DAI即"samsung-i2s.4"这个虚拟端口支持更高的采样率
但它只能用于playback,不能用于capture;因此soundrecord只能用Primary DAI。另外Sec_Fifo DAI在使用dma时用的是idma,
即snd_dma_buffer来自于iram。
第三个dai_link是用于电话语音,物理层上由codec wm8994的aif2与lsu6300v的pcm接口相连
再说明下snd_soc_dai_link的结构体成员:
.name = "WM8994 AIF2"指定dai_link的名字,可以随便取
.stream_name = "Voice_Dai"指定cpu侧stream名称,可以随便取
.cpu_dai_name = "smdk-voice-dai" 指定与codec_dai对应连接的cpu_dai的名字,本例中我们必须为lsu6300v声明注册一个cpu_dai,如:
- {
- .name = "smdk-voice-dai",
- .id = 3,
- .playback = {
- .channels_min = 1,
- .channels_max = 2,
- .rates = WM8994_RATES,
- .formats = WM8994_FORMATS,
- //.rates = SNDRV_PCM_RATE_8000,
- //.formats = SNDRV_PCM_FMTBIT_S16_LE,
- },
- .capture = {
- .channels_min = 1,
- .channels_max = 2,
- .rates = WM8994_RATES,
- .formats = WM8994_FORMATS,
- //.rates = SNDRV_PCM_RATE_8000,
- //.formats = SNDRV_PCM_FMTBIT_S16_LE,
- },
- .ops = &smdk_voice_dai_ops,
- },
{
.name = "smdk-voice-dai",
.id = 3,
.playback = {
.channels_min = 1,
.channels_max = 2,
.rates = WM8994_RATES,
.formats = WM8994_FORMATS,
//.rates = SNDRV_PCM_RATE_8000,
//.formats = SNDRV_PCM_FMTBIT_S16_LE,
},
.capture = {
.channels_min = 1,
.channels_max = 2,
.rates = WM8994_RATES,
.formats = WM8994_FORMATS,
//.rates = SNDRV_PCM_RATE_8000,
//.formats = SNDRV_PCM_FMTBIT_S16_LE,
},
.ops = &smdk_voice_dai_ops,
},
由于lsu630v和4412本身没有关联,注册cpu_dai完全是为了满足alsa的需要,所以注册的位置随便放在哪里都可以,如"snd-soc-dummy"设备里,甚至可以放在wm8994注册的dai之后。
.codec_dai_name = "wm8994-aif2" 指定用哪个codec_dai,名字必须与codec里面注册的dai name相同
.platform_name = "snd-soc-dummy"指定cpu lsu6300v的platform,可以不写默认用的就是"snd-soc-dummy"这个虚拟设备,但最好另写一个,因为后面注册snd_pcm_hardware时需要用到这个设备
.codec_name = "wm8994-codec":指定codec的名称,需与wm8994注册的名字一样
.init = smdk_wm8994_init_paiftx:指定初始化函数,open此dai_link时会调用此函数,主要用来开启关闭一些dapm pin
.ops = &smdk_voice_ops:指定此dai_link的操作函数open此dai_link时会调用这类函数
再说明一下框图中的snd_soc_pcm_runtime和snd_pcm_hardware:
整个soc_core都是以snd_soc_pcm_runtime为桥梁来操作,可以这么理解:每一个物理连接对应一个或多个dai_link
每个dai_link对应一个snd_pcm_hardware设备,而snd_soc_pcm_runtime就是这个设备的驱动私有数据,snd_soc_pcm_runtime包含一个snd_pcm成员
snd_pcm才是真正保存音频数据的结构体,其成员stream内部有多个substream分别用于playback和capture
因此我们在增加dai_link时一定要注意注册snd_pcm_hardware。
- static const struct snd_pcm_hardware dummy_dma_hardware = {
- .formats = 0xffffffff,
- .channels_min = 1,
- .channels_max = UINT_MAX,
- /* Random values to keep userspace happy when checking constraints */
- .info = SNDRV_PCM_INFO_INTERLEAVED |
- SNDRV_PCM_INFO_BLOCK_TRANSFER,
- .buffer_bytes_max = 128*1024,
- .period_bytes_min = PAGE_SIZE,
- .period_bytes_max = PAGE_SIZE*2,
- .periods_min = 2,
- .periods_max = 128,
- };
- static int dummy_dma_open(struct snd_pcm_substream *substream)
- {
- snd_soc_set_runtime_hwparams(substream, &dummy_dma_hardware);
- return 0;
- }
static const struct snd_pcm_hardware dummy_dma_hardware = {
.formats = 0xffffffff,
.channels_min = 1,
.channels_max = UINT_MAX,
/* Random values to keep userspace happy when checking constraints */
.info = SNDRV_PCM_INFO_INTERLEAVED |
SNDRV_PCM_INFO_BLOCK_TRANSFER,
.buffer_bytes_max = 128*1024,
.period_bytes_min = PAGE_SIZE,
.period_bytes_max = PAGE_SIZE*2,
.periods_min = 2,
.periods_max = 128,
};
static int dummy_dma_open(struct snd_pcm_substream *substream)
{
snd_soc_set_runtime_hwparams(substream, &dummy_dma_hardware);
return 0;
}
说到snd_pcm_hardware这个结构体,后面讲alsa init的时候还要讲到这个结构体的用途
3.linux alsa初始化流程,先看图:
从1到5是设备的probe过程:
1:dma_probe,由于soundrecorder用到dma,所以其platform其实就是dma,常用的是arm的pl330
2:snd_soc_dummy_probe,由于语音通话的platform是3G模块,它和4412没有连接,所以用了一个dummy设备作为platform
8:snd_soc_dapm_add_route,关于dapm有必要专门一章介绍,这里不做详述
10:soc_new_pcm之后才是alsa初始化的关键:
- 创建pcm device
- 为capture和playback创建substream
- 设置操作函数snd_pcm_ops
- dma_new时分配snd_dma_buffer,录音时dma控制器会从i2s的rx_fifo中拷贝数据到这个buffer,然后hal层通过iotcl请求数据时,回调用copy_to_user将音频数据拷贝到用户空间。buffer中area是mmap之后的虚地址,addr是对应物理地址,dma需要访问物理地址,而应用访问的是虚地址。
12:probe_dai_link,这里会创建设备节点/dev/snd/pcmCxDxp,Cx表示系统第几个codec,Dx表示第几个dai_link,最有一个字母p表示playback,c表示cpature
android HAL层通过tinyalsa访问这些设备节点,以实现和codec的数据通信。
至此alsa init过程基本完成,录音的实现请看下面一篇文章!
http://blog.youkuaiyun.com/uranus_wm/article/details/12748559