Linux Audio (2)ALSA代码分析-基于Linux3.5

前言

字符设备

声卡在Linux中也是以一种字符设备,经典的Linux字符设备软件架构是:
① 实现file_operation结构体
② 内核注册file_operation结构体
class_createclass_device_create由udev自动创建设备节点

声卡概念

  1. 声卡组成

声卡可以看作是声卡控制芯片和Codec芯片的整合,板载声卡也不例外。
由于信号干扰的原因,声卡控制芯片不可能完全集成于南桥芯片,而是仅仅集成DSP芯片,具体的数模转换以及声音输出输入还得依靠Codec芯片。
集成声卡的弊端在于Codec芯片普遍比较薄弱,而且即便是南桥芯片中集成较为强大的DSP音频功能,其占用的系统资源也还是不小。
我们对于声卡的要求可以分为两点:音质和音效。集成声卡的音效部分则完全依赖于DSP的处理能力,而音质就与Codec芯片有着很大的关系
从一些技术指标来看,我们经常可以看到某某南桥的集成音频单元能够达到很高的水准,但是在缺少API的支持时,其作用也非常有限。声卡发展至今,主要分为板卡式、集成式和外置式三种接口类型,以适用不同用户的需求,三种类型的产品各有优缺点。

Reference: https://www.likecs.com/show-203822381.html

  1. 软声卡和硬声卡

声卡大致可分为软声卡和硬声卡。软声卡一般是集成与主板之上,声音部分的数据处理运算由CPU来完成,也即一般软声卡没有主处理芯片,只有一个解码芯片;硬声卡的声音处理芯片是独立的,声音数据由声音处理芯片独立完成,不需要CPU来协助运算,这样可以很大程度上减轻CPU的运算。

ALSA软件结构

用户空间接口

目前ALSA内核提供给用户空间的接口有:

$ cd /dev/snd
$ ls -l

(1)设备信息接口(/proc/asound)
(2)设备控制接口(/dev/snd/controlCX)
(3)混音器设备接口(/dev/snd/mixerCXDX)
(4)PCM设备接口(/dev/snd/pcmCXDX)
(5)原始MIDI(迷笛)设备接口(/dev/snd/midiCXDX)
(6)声音合成(synthesizer)设备接口(/dev/snd/seq)
(7)定时器接口(/dev/snd/timer)

其中,C0D0代表的是声卡0中的设备0,pcmC0D0c最后一个c代表capture,pcmC0D0p最后一个p代表
playback,这些都是alsa-driver中的命名规则。根据声卡的实际能力,驱动实际上可以挂上更多种类的设备,在include/sound/core.h中。

这些接口被提供给alsa-lib使用,而不是给应用程序使用,应用程序最好使用alsa-lib,或者更高级的接口比如jack提供的接口。

流程分析

接下来我们按照字符设备的软件架构来分析ALSA程序,分析的手法采用倒推法,从sound/core/sound.c的入口函数开始,以file_operation结构体角度分析。以内核v3.4版本分析为例

file_operation结构体

① 实现file_operation结构体
② 内核注册file_operation结构体

(1)sound/core/sound.c的入口函数
sound/core/sound.c的入口函数中有看到向内核注册一个名为snd_fops的结构体,这个file_operation结构体只有open()函数,直觉上来看open()函数用过是一个转接口,在snd_open()函数里应该会注册真正的file_operations。

// sound/core/sound.c
static int __init alsa_sound_init(void)
{
    register_chrdev(major, "alsa", &snd_fops)
}
static const struct file_operations snd_fops =
{
	.owner =	THIS_MODULE,
	.open =		snd_open,
	.llseek =	noop_llseek,
};

(2)真正的file_operations结构体
进入snd_open()函数

// sound/core/sound.c
static int snd_open(struct inode *inode, struct file *file)
{
	unsigned int minor = iminor(inode); //通过inode节点返回设备的主次设备号
	struct snd_minor *mptr = NULL;// 用来暂存file_operation
	const struct file_operations *old_fops; //保存上一个file_operation

	mptr = snd_minors[minor]; //主要语句,从snd_minors数组中获取从设备结构体

	old_fops = file->f_op;
	file->f_op = fops_get(mptr->f_ops); //获取从设备的file_operations结构体
	// 执行从设备的open()函数,这才是真正的file_operation
	if (file->f_op->open) {
		err = file->f_op->open(inode, file);
	}
}

(3)snd_minors[]数组
这个数组在snd_register_device_for_dev()中被调用,这个函数里根据声卡逻辑设备的type获取次设备号,然后把形参中的file_operation结构体填充到snd_minors[minor]中,供step(2)调用。

//Register the ALSA device file for the card
int snd_register_device_for_dev(int type, struct snd_card *card, int dev,
				const struct file_operations *f_ops,
				void *private_data,
				const char *name, struct device *device)
{
	int minor;
	struct snd_minor *preg; //形参填充该结构体,用来赋值给snd_minors[]数组
	preg = kmalloc(sizeof *preg, GFP_KERNEL);
	preg->type = type;
	preg->card = card ? card->number : -1;
	preg->device = dev;
	preg->f_ops = f_ops; // 最关键的file_operation结构体!!!
	preg->private_data = private_data;

#ifdef CONFIG_SND_DYNAMIC_MINORS
	minor = snd_find_free_minor(type); //获取声卡次设备号
#else
	minor = snd_kernel_minor(type, card, dev);//获取声卡次设备号
#endif
	snd_minors[minor] = preg; //填充snd_minors数组
	preg->dev = device_create(sound_class, device, MKDEV(major, minor),
				  private_data, "%s", name); //创建字符设备,这个函数稍后会再次分析
	return 0;
}

(4)snd_register_device_for_dev()函数调用

snd_register_device_for_dev()形参file_operation是从哪里来的呢,这就要分析snd_register_device_for_dev()被调用流程。
在这里插入图片描述
依据code来看,在smd_register_device()snd_pcm_dev_register()中都会调用snd_register_device_for_dev(&snd_ctl_f_ops)
snd_register_device_for_dev(&snd_pcm_f_ops)把各自的file_operation结构体填充到snd_minors[minor]数组中。
图中的某个声卡的驱动程序的一个实例就是/sound/arm/pxa2xx-ac97.c

static int __devinit pxa2xx_ac97_probe(struct platform_device *dev)  
{
	struct snd_card *card; 
	 ret = snd_card_create(SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1,   15.
                  THIS_MODULE, 0, &card);  
	...........
	ret = pxa2xx_pcm_new(card, &pxa2xx_ac97_pcm_client, &pxa2xx_ac97_pcm); 
	ret = snd_card_register(card);  
	............
}

设备节点和类的建立

class_createclass_device_create由udev自动创建设备节点

依据上面的调用过程再从设备节点建立的角度分析:
在这里插入图片描述

声卡创建实例

声卡的建立流程

  • 第一步,创建snd_card的一个实例
    err = snd_card_create(index, id, THIS_MODULE, 0, &card);
  • 第二步,创建声卡的芯片专用数据
     声卡的专用数据主要用于存放该声卡的一些资源信息,例如中断资源、io资源、dma资源等。
  • 第三步,设置Driver的ID和名字
  • 第四步,创建声卡的功能部件(逻辑设备),例如PCM,Mixer,MIDI等
     每一种部件的创建最终会调用snd_device_new()来生成一个snd_device实例,并把该实例链接到 snd_card的devices链表中。通常,alsa-driver的已经提供了一些常用的部件的创建函数,而不必直接调用snd_device_new(),比如:
     PCM ---- snd_pcm_new()
     CONTROL – snd_ctl_create()
  • 第五步,注册声卡
     snd_card_register(card)
函数路径
snd_card_create/sound/core/init.c
snd_pcm_new()/sound/core/pcm.c
snd_ctl_create()/sound/core/control.c
snd_card_register()/sound/core/init.c

示例代码:

// 简化代码
static int __devinit pxa2xx_ac97_probe(struct platform_device *dev)
{
	struct snd_card *card;
	// 逻辑设备:创建控制设备
	ret = snd_card_create(SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1,
			      THIS_MODULE, 0, &card);
	// 逻辑设备:创建pcm设备
	ret = pxa2xx_pcm_new(card, &pxa2xx_ac97_pcm_client, &pxa2xx_ac97_pcm);
	// 创建声卡的芯片专用数据
	// 设置声卡clk,interrupt等信息
	ret = pxa2xx_ac97_hw_probe(dev);
	ret = snd_ac97_bus(card, 0, &pxa2xx_ac97_ops, NULL, &ac97_bus);
	ret = snd_ac97_mixer(ac97_bus, &ac97_template, &pxa2xx_ac97_ac97);
	// 注册声卡
	ret = snd_card_register(card);

	return ret;
}

下面逐一分析创建逻辑设备和注册声卡的过程:
(1)snd_card_create()pxa2xx_pcm_new
该函数的调用流程如图,可以看到最终control和pcm逻辑设备被加到了snd_card结构体的devices列表中。
在这里插入图片描述
(2)snd_card_register(card)
通过snd_device_register_all()注册所有挂在该声卡下的逻辑设备,snd_device_register_all()实际
上是通过snd_card的devices链表,遍历所有的snd_device,并且调用snd_device的ops->dev_register()来实现各
自设备的注册的。

int snd_card_register(struct snd_card *card)
{
	if (!card->card_dev) {
		card->card_dev = device_create(sound_class, card->dev,
					       MKDEV(0, 0), card,
					       "card%i", card->number);
	}
	if ((err = snd_device_register_all(card)) < 0)
		return err;
}

/*
 * register all the devices on the card.
 * called from init.c
 */
int snd_device_register_all(struct snd_card *card)
{
	struct snd_device *dev;
	int err;
	list_for_each_entry(dev, &card->devices, list) {
		if (dev->state == SNDRV_DEV_BUILD && dev->ops->dev_register) {
			if ((err = dev->ops->dev_register(dev)) < 0)
				return err;
			dev->state = SNDRV_DEV_REGISTERED;
		}
	}
	return 0;
}

dev->ops->dev_register(dev)是来自哪里呢?

struct snd_device {
	struct list_head list;		/* list of registered devices */
	struct snd_card *card;		/* card which holds this device */
	snd_device_state_t state;	/* state of the device */
	snd_device_type_t type;		/* device type */
	void *device_data;		/* device structure */
	struct snd_device_ops *ops;	/* operations */
};

struct snd_device_ops *ops搜索该结构体,有两处与ALSA相关:

路径函数
sound/core/control.csnd_ctl_create
sound/core/pcm.c_snd_pcm_new

第一处:

/* sound/core/control.c   */
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,
	};

	if (snd_BUG_ON(!card))
		return -ENXIO;
	return snd_device_new(card, SNDRV_DEV_CONTROL, card, &ops);
}

static const struct file_operations snd_ctl_f_ops =
{
	.owner =	THIS_MODULE,
	.read =		snd_ctl_read,
	.open =		snd_ctl_open,
	.release =	snd_ctl_release,
	.llseek =	no_llseek,
	.poll =		snd_ctl_poll,
	.unlocked_ioctl =	snd_ctl_ioctl,
	.compat_ioctl =	snd_ctl_ioctl_compat,
	.fasync =	snd_ctl_fasync,
};

/*
 * registration of the control device
 */
static int snd_ctl_dev_register(struct snd_device *device)
{
	sprintf(name, "controlC%i", cardnum);
	if ((err = snd_register_device(SNDRV_DEVICE_TYPE_CONTROL, card, -1,
				       &snd_ctl_f_ops, card, name)) < 0)
}
static const struct file_operations snd_ctl_f_ops =
{
	.owner =	THIS_MODULE,
	.read =		snd_ctl_read,
	.open =		snd_ctl_open,
	.release =	snd_ctl_release,
	.llseek =	no_llseek,
	.poll =		snd_ctl_poll,
	.unlocked_ioctl =	snd_ctl_ioctl,
	.compat_ioctl =	snd_ctl_ioctl_compat,
	.fasync =	snd_ctl_fasync,
};

第二处:

/* sound/core/pcm.c */
static int _snd_pcm_new(struct snd_card *card, const char *id, int device,
		int playback_count, int capture_count, bool internal,
		struct snd_pcm **rpcm)
{
	struct snd_pcm *pcm;
	int err;
	static struct snd_device_ops ops = {
		.dev_free = snd_pcm_dev_free,
		// pcm逻辑设备的注册函数,主要是把ALSA逻辑设备的file_operation结构体注册到内核
		.dev_register =	snd_pcm_dev_register,  
		.dev_disconnect = snd_pcm_dev_disconnect,
	};
	if ((err = snd_device_new(card, SNDRV_DEV_PCM, pcm, &ops)) < 0) {
		snd_pcm_free(pcm);
		return err;
	}
	if (rpcm)
		*rpcm = pcm;
	return 0;
}

static int snd_pcm_dev_register(struct snd_device *device)
{
	err = snd_pcm_add(pcm);
	for (cidx = 0; cidx < 2; cidx++) {
		int devtype = -1;
		if (pcm->streams[cidx].substream == NULL || pcm->internal)
			continue;
		switch (cidx) {
		case SNDRV_PCM_STREAM_PLAYBACK:
			sprintf(str, "pcmC%iD%ip", pcm->card->number, pcm->device);
			devtype = SNDRV_DEVICE_TYPE_PCM_PLAYBACK;
			break;
		case SNDRV_PCM_STREAM_CAPTURE:
			sprintf(str, "pcmC%iD%ic", pcm->card->number, pcm->device);
			devtype = SNDRV_DEVICE_TYPE_PCM_CAPTURE;
			break;
		}
		/* register pcm */
		//Register the ALSA device file for the card
		// 这个函数在前面有分析过调用过程,和这里就可以对上了。
		err = snd_register_device_for_dev(devtype, pcm->card,
						  pcm->device,
						  &snd_pcm_f_ops[cidx],
						  pcm, str, dev);
}

snd_register_device_for_dev()这个函数注册PCM逻辑设备的file_operation,当用户空间的Application调用open(/dev/snd/pcmC0D)时,就会通过snd_minors[]数组找到该结构体。

因为PCM设备还会有substream,所以可以继续深挖一下,看下这里的file_operation定义:

const struct file_operations snd_pcm_f_ops[2] = {
	{
		.owner =		THIS_MODULE,
		.write =		snd_pcm_write,
		.aio_write =		snd_pcm_aio_write,
		.open =			snd_pcm_playback_open,
		.release =		snd_pcm_release,
		.llseek =		no_llseek,
		.poll =			snd_pcm_playback_poll,
		.unlocked_ioctl =	snd_pcm_playback_ioctl,
		.compat_ioctl = 	snd_pcm_ioctl_compat,
		.mmap =			snd_pcm_mmap,
		.fasync =		snd_pcm_fasync,
		.get_unmapped_area =	snd_pcm_get_unmapped_area,
	},
	{
		.owner =		THIS_MODULE,
		.read =			snd_pcm_read,
		.aio_read =		snd_pcm_aio_read,
		.open =			snd_pcm_capture_open,
		.release =		snd_pcm_release,
		.llseek =		no_llseek,
		.poll =			snd_pcm_capture_poll,
		.unlocked_ioctl =	snd_pcm_capture_ioctl,
		.compat_ioctl = 	snd_pcm_ioctl_compat,
		.mmap =			snd_pcm_mmap,
		.fasync =		snd_pcm_fasync,
		.get_unmapped_area =	snd_pcm_get_unmapped_area,
	}
};

因为PCM设备有substream的概念,所以再看下.open()函数应该也是一个转接口,最终打开的应是substream的open函数。
snd_pcm_playback_open() --> snd_pcm_open() --> snd_pcm_open_file() --> snd_pcm_open_substream()


使用时的调用流程:
请添加图片描述

内核导出信息

1.devtmpfs信息(设备节点)

shell@tiny4412:/system # ls /dev/snd/ -l                                       
total 0
crw-rw----    1 1000     1005      116,   0 Jan  1 12:00 controlC0
crw-rw----    1 1000     1005      116,  24 Jan  1 12:00 pcmC0D0c
crw-rw----    1 1000     1005      116,  16 Jan  1 12:00 pcmC0D0p
crw-rw----    1 1000     1005      116,  25 Jan  1 12:00 pcmC0D1c
crw-rw----    1 1000     1005      116,  17 Jan  1 12:00 pcmC0D1p
crw-rw----    1 1000     1005      116,  33 Jan  1 12:00 timer

controlC0: 起控制作用,C0表示Card0
pcmC0D0c: Card 0,Device 0 capture,用来录音。
pcmC0D0p: Card 0,Device 0 playback,用来录音。
pcmC0D1c: Card 0,Device 1 capture,用来录音。
pcmC0D1p: Card 0,Device 1 playback,用来录音。
timer: 很少使用,暂时不用管。

pcmC0D1c/pcmC0D1p是一个辅助的备份的音频设备,先不管。

ALSA框架中一个声卡可以有多个逻辑Device,上面的pcmC0D0X和pcmC0D1X就是两个逻辑设备,一个Device又有播放、录音通道。

2.procfs文件信息

shell@tiny4412:/system # ls /proc/asound/                                      
card0     cards     devices   hwdep     pcm       timers    tiny4412  version

3.sysfs文件信息

/sys/class/sound/
有声卡的id,number,pcm_class等信息

4.debugfs文件信息

/sys/kernel/debug/asoc/
导出信息包括:
① Codec各个组件的dapm信息TINY4412_UDA8960/wm8960-codec.0-001a/dapm下的

shell@tiny4412:/sys/kernel/debug/asoc/TINY4412_UDA8960/wm8960-codec.0-001a/dapm # ls
HP_L                  Left Speaker Output   Right Boost Mixer
HP_R                  Left Speaker PGA      Right DAC
LINPUT1               MICB                  Right Input Mixer
LINPUT2               Mic Onboard           Right Output Mixer
LINPUT3               Mono Output Mixer     Right Speaker Output
LOUT1 PGA             OUT3                  Right Speaker PGA
Left ADC              RINPUT1               SPK_LN
Left Boost Mixer      RINPUT2               SPK_LP
Left DAC              RINPUT3               SPK_RN
Left Input Mixer      ROUT1 PGA             SPK_RP
Left Output Mixer     Right ADC             bias_level

② Codec的寄存器信息

shell@tiny4412:/sys/kernel/debug/asoc/TINY4412_UDA8960/wm8960-codec.0-001a # ls
cache_only  cache_sync  codec_reg

③ 为消除pop音的延时时间
/sys/kernel/debug/asoc/TINY4412_UDA8960# ls
dapm_pop_time

④ dapm的bias_level
/sys/kernel/debug/asoc/TINY4412_UDA8960/dapm # cat bias_level
Off

⑤ 系统中所有注册的Codec,dais和Platform驱动的名字

/sys/kernel/debug/asoc # ls
S3C2440_UDA1341  codecs           dais             platforms

从零写Asoc

暂时不表


Asoc 与 ALSA架构的联系

mechine、codec、platform (dma),platform dai (iis) Driver的probe函数里回去调用snd_soc.c中的函数以完成注册:
在这里插入图片描述
sound/soc-core.c中的API如下,这些API都会调用
list_add(&dai->list, &dai_list); list_add(&codec->list, &codec_list);, list_add(&platform->list, &platform_list);
snd_soc_instantiate_cards();

snd_soc_instantiate_cards();函数是Asoc core和ALSA core交互的接口,它的调用流程如下:

把cpu dai添加到dai_list中,在snd_soc_instantiate_cards中会去遍历dai_list 和
snd_soc_instantiate_card--> snd_card_register()--> snd_card_create(),snd_card_register()

/**
 * snd_soc_register_platform - Register a platform with the ASoC core
 *
 * @platform: platform to register
 */
int snd_soc_register_platform(struct device *dev,
		struct snd_soc_platform_driver *platform_drv)
/**
 * snd_soc_register_codec - Register a codec with the ASoC core
 *
 * @codec: codec to register
 */
int snd_soc_register_codec(struct device *dev,
			   const struct snd_soc_codec_driver *codec_drv,
			   struct snd_soc_dai_driver *dai_drv,
			   int num_dai)
/**
 * snd_soc_register_dais - Register multiple DAIs with the ASoC core
 *
 * @dai: Array of DAIs to register
 * @count: Number of DAIs
 */
int snd_soc_register_dais(struct device *dev,
		struct snd_soc_dai_driver *dai_drv, size_t count)
/**
 * snd_soc_register_dai - Register a DAI with the ASoC core
 *
 * @dai: DAI to register
 */
int snd_soc_register_dai(struct device *dev,
		struct snd_soc_dai_driver *dai_drv)

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值