音频数据流向:
| DMA | | I2S/PCM/AC97 |
RAM -------------> I2SControllerFIFO ---------------------------> CODEC ----> SPK/Headset
PCM模块初始化:
struct snd_soc_platform s3c_soc_platform = {
.name = "s3c-pcm-audio",
.pcm_ops = &s3c_pcm_ops, //OK
.pcm_new = s3c_pcm_new, //OK
.pcm_free = s3c_pcm_free_dma_buffers, //OK
.suspend = s3c_pcm_suspend,
.resume = s3c_pcm_resume,
};
调用snd_soc_register_platform()向ALSA core注册一个snd_soc_platform结构体。成员pcm_new需要调用dma_alloc_writecombine()给DMA分配一块write-combining的内存空间,并把这块缓冲区的相关信息保存到substream->dma_buffer中,相当于构造函数。pcm_free则相反。
snd_pcm_ops结构体如下:
struct snd_pcm_ops {
int (*open)(struct snd_pcm_substream *substream); //OK
int (*close)(struct snd_pcm_substream *substream); //OK
int (*ioctl)(struct snd_pcm_substream * substream, unsigned int cmd, void *arg);
int (*hw_params)(struct snd_pcm_substream *substream, struct snd_pcm_hw_params *params); //OK
int (*hw_free)(struct snd_pcm_substream *substream); //OK
int (*prepare)(struct snd_pcm_substream *substream); //OK
int (*trigger)(struct snd_pcm_substream *substream, int cmd); //OK
snd_pcm_uframes_t (*pointer)(struct snd_pcm_substream *substream); //PCM中间层通过调用这个函数来获取缓冲区的位置。
int (*copy)(struct snd_pcm_substream *substream, int channel,
snd_pcm_uframes_t pos, void __user *buf, snd_pcm_uframes_t count);
int (*silence)(struct snd_pcm_substream *substream, int channel,
snd_pcm_uframes_t pos, snd_pcm_uframes_t count);
struct page *(*page)(struct snd_pcm_substream *substream, unsigned long offset);
int (*mmap)(struct snd_pcm_substream *substream, struct vm_area_struct *vma);
int (*ack)(struct snd_pcm_substream *substream);
};
open函数为PCM模块设定支持的传输模式、数据格式、通道数、period等参数,并为playback/capture stream分配相应的DMA通道。
static int s3c_pcm_open(struct snd_pcm_substream *substream)
{
struct snd_soc_pcm_runtime *rtd = substream->private_data;
struct snd_soc_dai *cpu_dai = rtd->dai->cpu_dai;
struct snd_pcm_runtime *runtime = substream->runtime;
struct audio_stream_a *s = runtime->private_data;
int ret;
if (!cpu_dai->active) {
audio_dma_request(&s[0], audio_dma_callback); //为playback stream分配DMA,audio_dma_callback,这是dma的中断函数。
audio_dma_request(&s[1], audio_dma_callback); //为capture stream分配DMA
}
//设定runtime硬件参数,硬件参数要根据芯片的数据手册来定义。
snd_soc_set_runtime_hwparams(substream, &s3c_pcm_hardware);
/* Ensure that buffer size is a multiple of period size */
ret = snd_pcm_hw_constraint_integer(runtime, SNDRV_PCM_HW_PARAM_PERIODS);
return ret;
}
- static const struct snd_pcm_hardware s3c_pcm_hardware = {
- .info = SNDRV_PCM_INFO_INTERLEAVED |
- SNDRV_PCM_INFO_BLOCK_TRANSFER |
- SNDRV_PCM_INFO_MMAP |
- SNDRV_PCM_INFO_MMAP_VALID |
- SNDRV_PCM_INFO_PAUSE |
- SNDRV_PCM_INFO_RESUME,
- .formats = SNDRV_PCM_FMTBIT_S16_LE |
- SNDRV_PCM_FMTBIT_U16_LE |
- SNDRV_PCM_FMTBIT_U8 |
- SNDRV_PCM_FMTBIT_S8,
- .channels_min = 2,
- .channels_max = 2,
- .buffer_bytes_max = 128*1024,
- .period_bytes_min = PAGE_SIZE,
- .period_bytes_max = PAGE_SIZE*2,
- .periods_min = 2,
- .periods_max = 128,
- .fifo_size = 32,
- };
上层ALSA lib可以通过接口来获得这些参数的,如snd_pcm_hw_params_get_buffer_size_max()来取得buffer_bytes_max。
hw_params函数为substream(每打开一个playback或capture,ALSA core均产生相应的一个substream)设定DMA的源(目的)地址,以及DMA缓冲区的大小。static int s3c_pcm_hw_params(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params)
{
struct snd_pcm_runtime *runtime = substream->runtime;
int err = 0;
/*dma_buffer是DMA缓冲区,它通过4个字段定义:dma_area、dma_addr、dma_bytes和dma_private。其中dma_area是缓冲区逻辑地址,
dma_addr是缓冲区的物理地址,dma_bytes是缓冲区的大小,dma_private是ALSA的DMA管理用到的。dma_buffer是在pcm_new()中初始化的。*/
snd_pcm_set_runtime_buffer(substream, &substream->dma_buffer);
runtime->dma_bytes = params_buffer_bytes(params);
return err;
}
prepare
当pcm“准备好了”调用该函数。在这里根据channels、buffer_bytes等来设定DMA传输参数,跟具体硬件平台相关。注:每次调用snd_pcm_prepare()的时候均会调用prepare函数。trigger
ALSA: Advanced Linux Sound Architecture,它包括内核驱动集合、API库和工具。用户层程序直接调用libsound的API库,不需要打开设备等操作,因此编程者不需要了解底层细节。在嵌入式中,音频数据传输一般用I2S接口,控制一般用I2c或SPI接口。如下仅以嵌入式声卡为例,其驱动代码一般放在sound/soc下面。
struct snd_soc_dai结构体:
/* Digital Audio Interface runtime data.
Holds runtime data for a DAI. */
struct snd_soc_dai {
/* DAI description */
char *name; //模块声卡名称
unsigned int id;
int ac97_control;
struct device *dev;
void *ac97_pdata; /* platform_data for the ac97 codec */
/* DAI callbacks */
int (*probe)(struct platform_device *pdev, struct snd_soc_dai *dai);
void (*remove)(struct platform_device *pdev, struct snd_soc_dai *dai);
int (*suspend)(struct snd_soc_dai *dai);
int (*resume)(struct snd_soc_dai *dai);
/* ops */
struct snd_soc_dai_ops *ops; //声卡操作函数集合指针,实现的有hw_params(硬件参数设定)、digital_mute(静音操作)、set_fmt(格式配置)等,这些函数的实现均与硬件相关,根据硬件的数据手册来实现。
/* DAI capabilities */
struct snd_soc_pcm_stream capture; //录音参数设定
struct snd_soc_pcm_stream playback; //播放参数设定,均包含channel数目、PCM_RATE和PCM_FMTBIT等信息。
unsigned int symmetric_rates:1; //
/* DAI runtime info */
struct snd_pcm_runtime *runtime;
struct snd_soc_codec *codec;
unsigned int active;
unsigned char pop_wait:1;
void *dma_data;
/* DAI private data */
void *private_data;
/* parent platform */
struct snd_soc_platform *platform;
struct list_head list;
};
结构体定义范例:struct snd_soc_dai uda134x_dai = {
.name = "UDA134X",
/* playback capabilities */
.playback = {
.stream_name = "Playback",
.channels_min = 1,
.channels_max = 2,
.rates = UDA134X_RATES,
.formats = UDA134X_FORMATS,
},
/* capture capabilities */
.capture = {
.stream_name = "Capture",
.channels_min = 1,
.channels_max = 2,
.rates = UDA134X_RATES,
.formats = UDA134X_FORMATS,
},
/* pcm operations */
.ops = &uda134x_dai_ops,
};
/* codec device */
struct snd_soc_codec_device {
int (*probe)(struct platform_device *pdev);
int (*remove)(struct platform_device *pdev);
int (*suspend)(struct platform_device *pdev, pm_message_t state);
int (*resume)(struct platform_device *pdev);
};
Probe指声卡的探测与初始化,remove指声卡的卸载,suspend指声卡的休眠,resume指声卡从休眠状态下恢复。详细介绍probe函数。
- static int uda134x_soc_probe(struct platform_device *pdev)
- {
- //获得snd_soc_device结构体
- //在声卡的初始化过程中,其实首先是调用sound/soc/<SOC>下的相关驱动的probe函数,在probe有platform_set_drvdata()的操作,
- //这里有个将指针类型的转换:(struct snd_soc_device *s) ==> (struct platform_device *)。
- struct snd_soc_device *socdev = platform_get_drvdata(pdev);
- struct snd_soc_codec *codec;
- …
- //为codec分配内存
- socdev->card->codec = kzalloc(sizeof(struct snd_soc_codec), GFP_KERNEL);
- if (socdev->card->codec == NULL)
- return ret;
-
- codec = socdev->card->codec;
-
- …
-
- //初始化codec
- codec->name = "uda134x";
- codec->owner = THIS_MODULE;
- codec->dai = &uda134x_dai; //指向上面定义好的dai
- codec->num_dai = 1;
- codec->read = uda134x_read_reg_cache; //控制接口—读
- codec->write = uda134x_write; //控制接口—写
-
- …
-
- mutex_init(&codec->mutex);
- INIT_LIST_HEAD(&codec->dapm_widgets);
- INIT_LIST_HEAD(&codec->dapm_paths);
-
- …
-
- /* register pcms */
- /*创建一个PCM实例以便播放数据流。函数里重要的是如下两句:
- ret = snd_card_create(idx, xid, codec->owner, 0, &codec->card); //create and initialize a soundcard structure
- ret = soc_new_pcm(socdev, &card->dai_link[i], i);//创建播放流/录音流的子流,将所有播放流/录音流的子流操作函数设为soc_pcm_ops。
*/
- ret = snd_soc_new_pcms(socdev, SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1);
-
- …
- /*将操作集合挂到card->control链表上来,这个集合实现了音频播放时各个参数的设置,主要有.info、.get和.set。
- 如playback volume control:SOC_DOUBLE_R_TLV("Playback Volume", SNDCARD_REG_L_GAIN, SNDCARD_REG_R_GAIN, 0, 192, 0, digital_tlv),
- 其中SNDCARD_REG_L_GAIN和SNDCARD_REG_R_GAIN分别是左右声道音量增益寄存器偏移。最终要调用的函数都是在soc-core.c里面的,
- 这里只是提供一些跟硬件相关的参数,大为增加了代码的复用性。*/
- ret = snd_soc_add_controls(codec, uda134x_snd_controls, ARRAY_SIZE(uda134x_snd_controls));
-
- …
-
- /* register card */
- ret = snd_soc_init_card(socdev);
- }
/* SoC Device - the audio subsystem */
struct snd_soc_device {
struct device *dev;
struct snd_soc_card *card;
struct snd_soc_codec_device *codec_dev;
void *codec_data;
};
这个结构体用于向内核注册一个device。初始化一般如下:
- static struct snd_soc_device SOC_SNDCARD_snd_devdata = {
- .card = &snd_soc_s3c24xx_uda134x,
- .codec_dev = &soc_codec_dev_uda134x,//就是CODEC定义的snd_soc_codec_device结构体
- .codec_data = &s3c24xx_uda134x, //私有数据,一般存放SNDCARD控制接口信息,如I2C从设备地址等
- };
对于module_init,其实用platform_driver_register注册一个platform_driver结构体的方式也好,还是直接写一个init也好,都问题不大。前者更贴近Linux的驱动模型。Probe的一般过程如下:
static int s3c24xx_uda134x_probe(struct platform_device *pdev)
{
s3c24xx_uda134x_snd_devdata.codec_dev = &soc_codec_dev_uda134x;
s3c24xx_uda134x_snd_device = platform_device_alloc("soc-audio", -1);
platform_set_drvdata(s3c24xx_uda134x_snd_device, & s3c24xx_uda134x_snd_devdata);
s3c24xx_uda134x_snd_devdata.dev = & s3c24xx_uda134x_snd_device->dev;
platform_device_add(s3c24xx_uda134x_snd_device); //codec中导出的结构体在这里注册
}
/* SoC card */
struct snd_soc_card {
char *name;
struct device *dev;
struct list_head list;
int instantiated;
int (*probe)(struct platform_device *pdev);
int (*remove)(struct platform_device *pdev);
/* the pre and post PM functions are used to do any PM work before and
* after the codec and DAI's do any PM work. */
int (*suspend_pre)(struct platform_device *pdev, pm_message_t state);
int (*suspend_post)(struct platform_device *pdev, pm_message_t state);
int (*resume_pre)(struct platform_device *pdev);
int (*resume_post)(struct platform_device *pdev);
/* callbacks */
int (*set_bias_level)(struct snd_soc_card *,
enum snd_soc_bias_level level);
/* CPU <--> Codec DAI links */
struct snd_soc_dai_link *dai_link;
int num_links;
struct snd_soc_device *socdev;
struct snd_soc_codec *codec;
struct snd_soc_platform *platform;
struct delayed_work delayed_work;
struct work_struct deferred_resume_work;
};
定义这个结构体是让snd_soc_register_card()注册一个card的。初始化范例:
- static struct snd_soc_card snd_soc_s3c24xx_uda134x = {
- .name = "S3C24XX_UDA134X",
- .platform = &s3c24xx_soc_platform,
- .dai_link = &s3c24xx_uda134x_dai_link,
- .num_links = 1,
- };
/* SoC machine DAI configuration, glues a codec and cpu DAI together */
struct snd_soc_dai_link {
char *name; /* Codec name */
char *stream_name; /* Stream name */
/* DAI */
struct snd_soc_dai *codec_dai;
struct snd_soc_dai *cpu_dai;
/* machine stream operations */
struct snd_soc_ops *ops;
/* codec/machine specific init - e.g. add machine controls */
int (*init)(struct snd_soc_codec *codec);
/* Symmetry requirements */
unsigned int symmetric_rates:1;
/* Symmetry data - only valid if symmetry is being enforced */
unsigned int rate;
/* DAI pcm */
struct snd_pcm *pcm;
};
因为一个平台可以运行多个音频设备,snd_soc_dai_link的作用也在这,将CODEC定义的snd_soc_dai挂到一个链表上。
name指定codec名称;.codec_dai指向CODEC定义的snd_soc_dai结构体;.cpu_dai指向I2S定义的snd_soc_dai结构体;.ops接下来分析。
- /* SoC audio ops */
- struct snd_soc_ops {
- int (*startup)(struct snd_pcm_substream *);
- void (*shutdown)(struct snd_pcm_substream *);
- int (*hw_params)(struct snd_pcm_substream *, struct snd_pcm_hw_params *);
- int (*hw_free)(struct snd_pcm_substream *);
- int (*prepare)(struct snd_pcm_substream *);
- int (*trigger)(struct snd_pcm_substream *, int);
- };
底层硬件操作—
CODEC:控制接口及芯片基本初始化
PCM:pcm dma操作
I2S:i2s配置操作
之后i2s和pcm其实都跟codec差不多了,只需要理解alsa-core、<soc>、<codec、pcm、i2s>三层的关系。其中codec、pcm、i2s可以看做同层的,分别对于音频设备的control、dma、i2s接口;<codec、pcm、i2s>会分别export相关结构体给<soc>层,<soc>层将音频设备三部分与CPU Spec联结起来,其probe顺序是<SOC>.probe-><codec, pcm, i2s>.probe;另外<codec、pcm、i2s>在各自的module_init中将自身注册到alsa-core中。