基于linux4.x内核的ALSA框架概述(个人工作总结一)
环境:
linux4.X内核
构建:
基于yocto构建镜像
平台:
芯驰X9系列
硬件框架声音示意图:
如图可知,声音传输由SOC通过总线I2S传输音频PCM数据到ADC芯片CS4344,然后将处理后的数据通过功放电路(此处省略)最后传送给到喇叭。
设备树的匹配
在当前使用的内核版本中,多数采用了设备树来描述硬件,即平台、设备、驱动分离模式,在dts中描述一个声卡注册的硬件设备也可以很简单。
DTS:
`sound_cs4344: sound@cs4344 {
compatible = "semidrive,x9-ref-cs4344";
status = "okay";
};`
在驱动的匹配过程中会根据dts中的compatible属性来进行匹配,status标志着该节点是否可用,如果是okay则证明被使用,如果是disabled则在匹配过程中忽略该节点
驱动文件(XXX.C)
static const struct of_device_id x9_ref_cs4344_dt_match[] = {
{
.compatible = "semidrive,x9-ref-cs4344",
},
};
MODULE_DEVICE_TABLE(of, x9_ref_cs4344_dt_match);
static struct platform_driver x9_ref_cs4344_mach_driver = {
.driver =
{
.name = SND_X9_MACH_DRIVER,
.of_match_table = x9_ref_cs4344_dt_match,
.probe_type = PROBE_PREFER_ASYNCHRONOUS,
},
.probe = x9_ref_cs4344_probe,
.remove = x9_ref_cs4344_remove,
};
module_platform_driver(x9_ref_cs4344_mach_driver);``
初始化一个struct platform_driver变量,其中 x9_ref_cs4344_dt_match为dts中的匹配表,其成员属性compatible与dts中的相等,即证明匹配成功调用x9_ref_cs4344_probe函数。
声卡设备注册
static struct snd_soc_dai_link **snd_x9_ref_soc_dai_links[]** = {
{
.name = "x9_hifi_hs",
.stream_name = "x9 hifi hs",
.cpu_name = "30600000.i2s", //SOC端的i2s物理地址
.cpu_dai_name = "snd-afe-sc-i2s-dai0",
.platform_name = "30600000.i2s", //指定SOC驱动,通常DMA驱动
.codec_name = "snd-soc-dummy", //DAC端的匹配驱动名
.codec_dai_name = "snd-soc-dummy-dai0",
.init = x9_ref_cs4344_init,
.dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_CBS_CFS,
.ops = &x9_ref_cs4344_ops, //音频相关的操作函数集
},
};
#define SND_X9_MACH_DRIVER "x9-ref-ms4344"
#define SND_CARD_NAME SND_X9_MACH_DRIVER
static struct snd_soc_card **x9_ref_cs4344_card** = { //代表ASoc设备
.name = SND_X9_MACH_DRIVER,
.dai_link = snd_x9_ref_soc_dai_links,
.num_links = ARRAY_SIZE(snd_x9_ref_soc_dai_links),
};
static int x9_ref_cs4344_probe(struct platform_device *pdev)
{
struct snd_soc_card *card = &x9_ref_cs4344_card; //初始化一个实例化声卡
struct device *dev = &pdev->dev;
int ret;
struct snd_x9_chip_hs *chip;
card->dev = dev;
/* Allocate chip data */
chip = devm_kzalloc(dev, sizeof(*chip), GFP_KERNEL);
chip->card = card;
platform_set_drvdata(pdev, chip); //将chip保存至私有平台数据中
ret = devm_snd_soc_register_card(dev, card);//注册声卡设备
return ret;
}
首先初始化一个实例化声卡结构体,成员中的dai_link保存了snd_soc_dai_link结构体地址,然后将声卡card嵌入到更大一层结构chip中,调用devm_snd_soc_register_card注册声卡设备。
devm_snd_soc_register_card函数会调用真正的注册函数snd_soc_register_card
int snd_soc_register_card(struct snd_soc_card *card)
{
int i, ret;
struct snd_soc_pcm_runtime *rtd;
for (i = 0; i < card->num_links; i++) {
struct snd_soc_dai_link *link = &card->dai_link[i];//遍历上述声卡中的dai_link数,本文环境为1
//做link检查,分配内存给card->dai_link->codecs初始化name和dai_name
//对link中platform_name、cpu_name、cpu_dai_name、codecs->dai_name等命名做检查
soc_init_dai_link(card, link);
}
/*做基础的初始化、设置链表头、已经连接的links数量等等*/
dev_set_drvdata(card->dev, card);
snd_soc_initialize_card_lists(card);
/*........省略部分初始化.....*/
snd_soc_instantiate_card(card);//主调用函数完成声卡注册
/* deactivate pins to sleep state */
list_for_each_entry(rtd, &card->rtd_list, list) {
struct snd_soc_dai *cpu_dai = rtd->cpu_dai;
int j;
for (j = 0; j < rtd->num_codecs; j++) {
struct snd_soc_dai *codec_dai = rtd->codec_dais[j];
if (!codec_dai->active)
pinctrl_pm_select_sleep_state(codec_dai->dev);
}
if (!cpu_dai->active)
pinctrl_pm_select_sleep_state(cpu_dai->dev);
}
return ret;
}
EXPORT_SYMBOL_GPL(snd_soc_register_card);
snd_soc_card代表着Machine驱动,snd_soc_platform则代表着Platform驱动,snd_soc_codec则代表了Codec驱动,而snd_soc_dai_link则负责连接Platform和Codec。在简要分析snd_soc_instantiate_card函数之前,先了解一下ASoC模型。
ASoC框架简述
ASOC是建立在标准的ALSA驱动上,不能单独存在,ASOC将soc_snd_card逻辑上分为codec/platform/machine三个组件,ASoC对于Alsa来说,就是分别注册PCM/CONTROL类型的snd_device设备,并实现相应的操 作方法集。
硬件示意图如下:
Machine:
抽象为一个主体设备,为SOC、CODEC、输入输出设备提供一个载体,Machine驱动的开发主要工作是向内核注册一个snd_soc_card声卡实体。即上述所创建的结构体card。
Platform :
一般是指某一个SoC平台,比如芯驰X9系列等等,与音频相关的通常包含该SoC中的时钟、DMA、I2S、PCM等等,它只与SoC相关,Machine无关。单独的Platform和Codec驱动是不能工作的,它必须由Machine驱动把它们结合在一起才能完成整个设备的音频处理工作。Platform驱动向ASoC注册snd_soc_platform和snd_soc_dai设备。
Codec :
Codec里面包含了I2S接口、D/A等功能,通常包含多种输入(Mic、Line-in、I2S、PCM)和多个输出(耳机、喇叭、听筒,Line-out),Codec和Platform一样,是可重用的部件,同一个Codec可以被不同的Machine使用。目前主流的codec都是可自动解码,不需要I2C去对寄存器进行配置,所以在芯驰平台中,该设备为一个虚拟设备。
snd_soc_instantiate_card函析
由于该函数中构造复杂,节选部分内容分析,不一一对函数展开。
soc_new_pcm_runtime:
分配内存给struct snd_soc_pcm_runtime *rtd,初始化rtd->card = card; rtd->dai_link = dai_link;,//后续数据流都使用rtd中保存的变量
snd_soc_find_dai:
该函数将搜索所有已注册的组件及其DAI,以找到同名的DAI。包括soc端与codec端,主要通过name匹配,成功找到对端则返回snd_soc_dai结构体指针给rtd(相当于复制在rtd的对应成员中),snd_soc_dai是snd_soc_platform和snd_soc_codec的数字音频接口。
soc_add_pcm_runtime:
将初始化完成的rtd加入card->list链表中(list_add_tail(&rtd->list, &card->rtd_list);),card->num_rtd++;
snd_soc_add_dai_link:
list_add_tail(&dai_link->list, &card->dai_link_list); card->num_dai_links++; 将card中的dai_link动态加入card,不影响静态的snd_soc_dai_link数组初始化
snd_card_new:
创建一个实例化的具体声卡struct snd_card *card;,对各个成员链表头、子设备初始化,创建根文件下各个系统目录,其中
device_initialize(&card->card_dev);
card->card_dev.parent = parent; //指向snd_soc_card的dev
card->card_dev.class = sound_class; // 如下所示
card->card_dev.release = release_card_device;
card->card_dev.groups = card->dev_groups;
card->dev_groups[0] = &card_dev_attr_group;
err = kobject_set_name(&card->card_dev.kobj, “card%d”, idx);//指明card的数量值,初始为0
static char *sound_devnode(struct device *dev, umode_t *mode)
{
.......
return kasprintf(GFP_KERNEL, "snd/%s", dev_name(dev)); //用于在/dev/snd下的目录
}
static int __init init_soundcore(void)
{
........
sound_class = class_create(THIS_MODULE, "sound");
sound_class->devnode = sound_devnode;
......
}subsys_initcall(init_soundcore);//该函数用于创建声卡的class
snd_card_new:
创建一个声卡实例化,其中注册控制子设备(control),/proc目录下的子目录操作
int snd_ctl_create(struct snd_card *card) //创建控制子设备
{
static struct snd_device_ops ops = {
.dev_free = snd_ctl_dev_free,
.dev_register = snd_ctl_dev_register,
.dev_disconnect = snd_ctl_dev_disconnect,
};
........
snd_device_initialize(&card->ctl_dev, card); //初始化声卡control的dev
dev_set_name(&card->ctl_dev, "controlC%d", card->number); //创建该命名
snd_device_new(card, SNDRV_DEV_CONTROL, card, &ops);//创建card的组件,子设备类型为CONTROL,将子设备挂接到card的devices
......
}
snd_info_card_create:
创建声卡的/proc文件
soc_probe_link_components:
//调用 platform->probe codec->probe 即 platform 和 codec的prob函数
static int soc_probe_link_dais(struct snd_soc_card *card,
struct snd_soc_pcm_runtime *rtd, int order)
{
struct snd_soc_dai_link *dai_link = rtd->dai_link;
struct snd_soc_dai *cpu_dai = rtd->cpu_dai;
int i, ret;
.......
/* set default power off timeout */
rtd->pmdown_time = pmdown_time; //默认设置5000ms
soc_probe_dai(cpu_dai, order); //调用SOC侧的probe
for (i = 0; i < rtd->num_codecs; i++)
soc_probe_dai(rtd->codec_dais[i], order); //调用codec侧的probe
if (order != SND_SOC_COMP_ORDER_LAST) //完成5次调用才往下执行
return 0;
if (dai_link->init)//调用dai_link中的init
ret = dai_link->init(rtd);
if (dai_link->dai_fmt)//调用dai_link中的dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_CBS_CFS,
snd_soc_runtime_set_dai_fmt(rtd, dai_link->dai_fmt);
soc_post_component_init(rtd, dai_link->name);
soc_new_pcm(rtd, rtd->num);//创建声卡逻辑设备pcm实例,rtd->pcm = pcm;PCM是由录音和播放二个子流组成,每个子流有若干个substream
//调用drivers中的pcm_new
soc_link_dai_pcm_new(&cpu_dai, 1, rtd);
soc_link_dai_pcm_new(rtd->codec_dais,rtd->num_codecs, rtd);
......
return 0;
}
snd_card_register(card->snd_card);:
最后将挂接在声卡上的子设备进行注册,登记在/dev/snd下。
PCM简析
PCM是目前ALSA中主流用来描述数字的音频流,pcm两大核心任务是:playback(播放) 和 capture(录音).一个pcm设备包含 播放/录制 两个数据流,每个数据流有若干个 substream每一个substream只能被一个进程占用,sub_pcm_substream 才是真正实现声卡设备 播放/录制 功能的结构体。可以在 /proc/asound/cardX/pcmXp/info 可查看pcm信息。
//sub_pcm_substream的操作方法集
struct snd_pcm_ops {
int (*open)(struct snd_pcm_substream *substream); /* 必须实现 */
int (*close)(struct snd_pcm_substream *substream);
int (*ioctl)(struct snd_pcm_substream * substream,
unsigned int cmd, void *arg); /* 用于实现几个特定的IOCTL1_{RESET,INFO,CHANNEL_INFO,GSTATE,FIFO_SIZE} */
int (*hw_params)(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params); /* 用于设定pcm参数,如采样率/位深... */
int (*hw_free)(struct snd_pcm_substream *substream);
int (*prepare)(struct snd_pcm_substream *substream); /* 读写数据前的准备 */
int (*trigger)(struct snd_pcm_substream *substream, int cmd); /* 触发硬件对数据的启动/停止 */
snd_pcm_uframes_t (*pointer)(struct snd_pcm_substream *substream); /* 查询当前的硬件指针 */
int (*wall_clock)(struct snd_pcm_substream *substream,
struct timespec *audio_ts); /* 通过hw获得audio_tstamp */
int (*copy)(struct snd_pcm_substream *substream, int channel,
snd_pcm_uframes_t pos,void __user *buf, snd_pcm_uframes_t count); /* 除dma外的hw自身实现的数据传输方法 */
int (*silence)(struct snd_pcm_substream *substream, int channel,
snd_pcm_uframes_t pos, snd_pcm_uframes_t count); /* hw静音数据的填充方法 */
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); /* 通知硬件写了一次数据 */
};
//表示substream运行时状态及实时信息。 "/proc/asound/*/subX/"
struct snd_pcm_runtime {
/* -- mmap -- */
struct snd_pcm_mmap_status *status; /* 当前硬件指针位置及其状态 */
struct snd_pcm_mmap_control *control; /* 当前的应用指针及其状态 */
/* -- interrupt callbacks -- */ /* HW一次中断传输完毕时的回调,似乎没有哪个模块用到它? */
void (*transfer_ack_begin)(struct snd_pcm_substream *substream);
void (*transfer_ack_end)(struct snd_pcm_substream *substream);
...
struct snd_dma_buffer *dma_buffer_p; /* allocated buffer */
}
static int _snd_pcm_new()
{
struct snd_pcm *pcm;
int err;
static struct snd_device_ops ops = {
.dev_free = snd_pcm_dev_free,
.dev_register = snd_pcm_dev_register,//pcm注册函数
.dev_disconnect = snd_pcm_dev_disconnect,
};
//创建 播放(playback) 录音(capture)任务队列,完成PCM中的streams[2]的初始化,实例化struct snd_pcm_substream结构(主要通信结构),保存在pcm->streams[X].substream指针中
snd_pcm_new_stream(pcm, SNDRV_PCM_STREAM_PLAYBACK, playback_count);
snd_pcm_new_stream(pcm, SNDRV_PCM_STREAM_CAPTURE, capture_count);
//将声卡功能部件PCM 挂接到 声卡上card->devices (与上述中snd_ctl_create函数初始化control字设备一样)
snd_device_new(card, SNDRV_DEV_PCM, pcm, &ops)
}
snd_pcm_set_ops:
实现substream->ops(struct snd_pcm_ops );//该子集会在sound/core/pcm_native.c中的snd_pcm_common_ioctl函数中被上层调用,snd_pcm_common_ioctl是snd_pcm_f_ops.unlocked_ioctl的实现,是pcm子设备的file_operations(文件操作集)
参考连接:
https://blog.csdn.net/LinuxArmbiggod/article/details/80600633