ALSA-ASOC音频驱动框架简述

##ALSA-ASOC音频驱动框架简述
注意:本文只限于讲解ALSA-ASOC音频驱动框架,不深入到寄存器、时序等配置,文章有不足之处,日后逐渐完善补充

另外多谢两位前辈的博客,学到了很多,多谢。
https://blog.csdn.net/droidphone
https://blog.csdn.net/longwang155069

####目录:
####一:ALSA概述

1.1  ALSA概述
1.2  ALSA文件框架
1.3  从proc文件系统开始入手分析ALSA

####二:ALSA子设备PCM概述
2.1 PCM概述
2.2 PCM代码分析
####三:ALSA子设备control概述
####四:Asoc框架概述
4.4 ASOC-ALSA 之Machine框架分析
4.4.1 Machine概述
4.4.2 Machine驱动分析
4.5 ASOC-ALSA 之Codec框架分析
4.5.1 Codec概述
4.5.2 Codec代码框架
4.6 ASOC-ALSA 之Platform 框架分析
4.6.1 Platform 概述
4.6.2 Platform代码框架

.
###一 : ALSA概述
#####1.1 ALSA概述
ALSA(Advanced Linux Sound Architecture)是linux上主流的音频结构,在没有出现ALSA架构之前,一直使用的是OSS(Open Sound System)音频架构主要的区别就是在OSS架构下,App访问底层是直接通过Sound设备节点访问的。而在ALSA音频架构下,App是通过ALSA提供的alsa-lib库访问底层硬件的操作,不再访问Sound设备节点了。这样做的好处可以简化App实现的难度。

#####1.2 ALSA文件框架
不像其它的系统框架,声音子系统ALSA不在内核的driver中,而是在/kernel/sound 单独的目录中,不知道是不是因为代码结构过于庞大,所以单独分出sound目录。Alsa架构目录结构如下:

/kernel/sound
├── core
│   ├── compress_offload.c 实现某些可硬件解码的音频流的用户接口。alsa-lib上已有封装好的编程接口。
│   ├── control.c 通过设备节点 /dev/snd/controlCx 实现声卡的音量控制/声道切换等。
│   ├── device.c 各种子设备如 pcm/control/timer... 的注册接口。
│   ├── info.c 实现procfs下文件访问接口。
│   ├── init.c 实现声卡设备的创建/销毁等。
│   ├── pcm_*.c 实现pcm设备的文件系统接口。
│   ├── sound.c Alsa子系统。
│   └── timer.c
|   |__ codec 编解码codec声卡IC驱动文件
├── drivers 其它的声卡如虚拟声卡(dummp/loopback)等。
├── oss
├── isa/pci/pcmcia 各种总路线上的声卡在alsa上的实现。
├── ppc/sh/mips 平台自己的实现。
├── soc ASOC子系统的实现。
└── spi/usb spi/usb声卡。

内核中用 snd_card来描述一个声卡设备。

// kernel/include/sound/core.h
	struct snd_card {
	int number;			/* number of soundcard (index to snd_cards) */
	char id[16];			/* id string of this card */
	char driver[16];		/* driver name */
	char shortname[32];		/* short name of this soundcard */
	char longname[80];		/* name of this soundcard */
	char mixername[80];		/* mixer name */
	char components[128];		/* card components delimited with space */
...
	struct list_head devices;	//所有snd_device的链表头. snd_device是alsa的内部表示,既与设备模型中 的设备无关,也与文件系统中的字符设备无关。一个声卡就是若干个snd_device 的集合,各个snd_device实现自己的操作方法集。常用的设备类型有的 CONTROL/PCM/TIMER…

	void *private_data;		//声卡的私有数据,可以在创建声卡时通过参数指定数据的大小 
...							
	struct list_head devices;	//记录该声卡下所有逻辑设备的链表,链接各个功能部件的链表
...
	struct list_head ctl_files;     //若干个进程打开"/dev/snd/controlCx"时生成ctl_file的链 表头。一般用于pcm有多个substream时配置选用哪个substream.
	struct list_head controls;	//通过设备节点"/dev/snd/controlCx"提供给用户空间的controls. 常用于控制音量,音频流的通路等

	struct device *dev;		//此声卡的父设备,通过何种方式挂载声卡。如usb/pci/platform…
	struct device *card_dev;	//此些声卡自己的设备,用于注册到设备模型。如pci声卡,位于 "/sys/devices/pci0000:00/0000:00:1b.0/sound/card0/".	
};

.

#####1.3 从proc文件系统开始入手分析ALSA
分析一个驱动总要有一点起点,我们从 proc文件系统入手开始分析ALSA系统

cd /proc/asound 
ls

card0  //声卡0
cards  //系统可用的声卡
devices  //alsa下所有注册的声卡子设备,包括 control pcm timer seq等等
version //ALSA版本信息
...等等


cd /dev/snd
ls
controlC0 //用于声卡的控制,比如通道选择,混音,麦克风控制等等
midiC0D0  //用于播放midi音频
pcmC0D0c  //用于录音的pcm设备
pcmC0D0p  //用于播放的pcm设备
seq  //音序器
timer //定时器

Alsa的proc接口信息在 sound/core/sound.c 中实现
主要工作:
1 注册声卡字符设备
2 在proc下创建声卡相关文件
3 为声卡子设备创建设备节点

static struct snd_minor *snd_minors[SNDRV_OS_MINORS];

//为声卡子设备创建设备节点
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)
{
struct snd_minor *preg;

//设置 初始化功能部件上下文信息
...
snd_minors[minor] = preg;

//创建设备节点
device_create();
...

}
EXPORT_SYMBOL(snd_register_device_for_dev);


static int snd_open(struct inode *inode, struct file *file)
{
	//首先从设备节点中取出设备号
	unsigned int minor = iminor(inode);

	struct snd_minor *mptr = NULL;
	const struct file_operations *old_fops;
	int err = 0;

	//从 snd_minors[minor]全局数组中取出当初注册的对应的子设备snd_minor,
	mptr = snd_minors[minor];

	...

	//然后从snd_minor结构中取出对应子设备设备的f_ops
	old_fops = file->f_op;

	//并且把file->f_op替换为对应子设备的f_ops
	file->f_op = fops_get(mptr->f_ops);
	if (file->f_op == NULL) {
		file->f_op = old_fops;
		err = -ENODEV;
	}

	//直接调用对对应子设备的f_ops->open()
	if (file->f_op->open) {
		err = file->f_op->open(inode, file);
		if (err) {
			fops_put(file->f_op);
			file->f_op = fops_get(old_fops);
		}
	}

	...
	return err;
}


//字符设备,只有一个open 说明此处的open只是一个转接口,只起到中转换作用,后面一定会调用各个子设备的回调函数
static const struct file_operations snd_fops =
{
	.owner =	THIS_MODULE,
	.open =		snd_open,
	.llseek =	noop_llseek,
};

//注册声卡设备
static int __init alsa_sound_init(void)
{
	snd_major = major;
	snd_ecards_limit = cards_limit;
	
	//注册 声卡设备 为字符设备
	if (register_chrdev(major, "alsa", &snd_fops)) {
		snd_printk(KERN_ERR "unable to register native major device number %d\n", major);
		return -EIO;
	}

	//在proc下创建 asound 目录,并且创建version、devices、module、cards等信息
	if (snd_info_init() < 0) {
		unregister_chrdev(major, "alsa");
		return -ENOMEM;
	}
	snd_info_minor_register();
	return 0;
} 

综上所述,我们可以看出Alsa系统实际是一个用于管理若干个声卡设备的子系统,一个声卡 snd_card 由多个 snd_device子设备组成(pcm control等等),在snd_card 注册/销毁的同时,回调所有子设备的 snd_devices_ops 方法,用于支持各种子设备的注册和热拔插等操作。此外 snd_card还为子设备提供了注册字符设备的注册接口,需要注册到字符设备的子设备,比如control、pcm根据 minor 通过snd_card的 f_op->open方法 snd_open(),传递到各自的 file_operations->open函数上。

###二 ALSA子设备PCM概述:
#####2.1 PCM概述
pcm用来描述ALSA中的数字音频流,简单的说 ALSA声卡设备的 播放/录制 功能就是通过 pcm 实现的,声音是连续模拟量,计算机将它离散化之后用数字表示我们人的耳朵听到的信号是模拟信号,PCM就是要把声音从模拟信号转换成数字信号的技术,pcm两大核心任务是:playback(播放) 和 capture(录音).一个pcm设备包含 播放/录制 两个数据流,每个数据流有若干个 substream每一个substream只能被一个进程占用,sub_pcm_substream 才是真正实现声卡设备 播放/录制 功能的结构体。可以在 /proc/asound/cardX/pcmXp/info 可查看pcm信息。

内核用snd_pcm结构用于表征一个PCM类型的snd_device,以下是pcm关键结构体

struct snd_pcm {
	struct snd_card *card;//指向所属的 声卡设备
	struct list_head list;
	int device; /* device number */
	unsigned int info_flags;
	unsigned short dev_class;
	unsigned short dev_subclass;
	char id[64];
	char name[80];
	struct snd_pcm_str streams[2];//播放和录音两个数据流
	struct mutex open_mutex;
	wait_queue_head_t open_wait;//打开pcm子设备时等待打开一个可获得的substream
	void *private_data;
	void (*private_free) (struct snd_pcm *pcm);
	struct device *dev; /* actual hw device this belongs to */
	bool internal; /* pcm is for internal use only */
	struct snd_pcm_oss oss;
	};

struct snd_pcm_str {
	int stream;		/* stream (direction) */
	struct snd_pcm *pcm; /* 指向所属的pcm设备 */
	/* -- substreams -- */
	unsigned int substream_count; /* 个数 */
	unsigned int substream_opened; /* 在使用的个数 */
	struct snd_pcm_substream *substream; /* 指向substream单链表 */
}

于 /proc/asound/cardX/pcmXp/subX/info 可查看substream信息
struct snd_pcm_substream {
	...
	const struct snd_pcm_ops *ops; //substream 操作集
	...
	struct snd_pcm_runtime *runtime;
        ...
};

//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 */
}

#####2.2 PCM代码分析
/kernel/sound/core/pcm.c
功能:
提供声卡功能部件PCM的创建接口:int snd_pcm_new()
提供声卡功能部件PCM任务队列创建接口:int snd_pcm_new_stream():用来创建播放(playback)任务 和 录音(capture)任务//pcm注册函数

static int snd_pcm_dev_register(struct snd_device *device)
{
struct snd_pcm_substream *substream;
struct snd_pcm_notify *notify;
char str[16];
struct snd_pcm *pcm;
struct device *dev;

//对于一个pcm设备,可以生成两个设备文件,一个用于playback,一个用于capture,
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:
		//pcm功能部件(逻辑设备)命名规则
		sprintf(str, "pcmC%iD%ip", pcm->card->number, pcm->device);
		devtype = SNDRV_DEVICE_TYPE_PCM_PLAYBACK;
		break;
	case SNDRV_PCM_STREAM_CAPTURE:
		//pcm功能部件(逻辑设备)命名规则
		sprintf(str, "pcmC%iD%ic", pcm->card->number, pcm->device);
		devtype = SNDRV_DEVICE_TYPE_PCM_CAPTURE;
		break;
	}


//注册PCM功能部件 snd_pcm_f_ops是一个标准的文件系统file_operation结构数组位于  /sound/core/pcm_native.c :const struct file_operations snd_pcm_f_ops[2] ,snd_pcm_f_ops 作为参数被注册,记录在 snd_minors中,
 
err = snd_register_device_for_dev(devtype, pcm->card,pcm->device,&snd_pcm_f_ops[cidx],pcm, str, dev);

}

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)任务队列
	snd_pcm_new_stream(pcm, SNDRV_PCM_STREAM_PLAYBACK, playback_count);
	snd_pcm_new_stream(pcm, SNDRV_PCM_STREAM_CAPTURE, capture_count);

//将声卡功能部件PCM 挂接到 声卡上
	snd_device_new(card, SNDRV_DEV_PCM, pcm, &ops)

}

//创建一个新的PCM实例
int snd_pcm_new()
{
	_snd_pcm_new();
}

###三 control概述
Control接口主要让用户空间的应用程序(alsa-lib)可以访问和控制音频codec芯片中的多路开关,挂动控制等等,对于Mixer(混音)来说,Control接口显得更加重要,从ALSA 0.9.x开始,所有的mixer工作都是通过control接口的api来实现。
日后补充 control部分的分析

###四 Asoc框架概述
#####4.1 Asoc概述
ASOC(Alsa System on Chip) 用于实现那些集成了声音控制器 的CPU,是建立在标准ALSA驱动层上,为了更好的支持嵌入式处理器和移动设备的音频Codec的一套软件体系,在ASoc出现之前,内核对SOC中的音频已经有了部分的支持,不过会有一些局限性:

局限性1	Codec驱动与SoC CPU的底层耦合过于紧密,这种不理想会导致代码的重复,例如,仅是wm8731的驱动,当时Linux中有分别针对4个平台的驱动代码。
局限性2	音频事件没有标准的方法来通知用户,例如耳机、麦克风的插拔和检测,这些事件在移动设备中是非常普通的,而且通常都需要特定于机器的代码进行重新对音频路劲进行配置。
局限性3	当进行播放或录音时,驱动会让整个codec处于上电状态,这对于PC没问题,但对于移动设备来说,这意味着浪费大量的电量。同时也不支持通过改变过取样频率和偏置电流来达到省电的目的。

ASoC正是为了解决上述种种问题而提出的,目前已经被整合至内核的代码树中:sound/soc。ASoC不能单独存在,他只是建立在标准ALSA驱动上的一个它必须和标准的ALSA驱动框架相结合才能工作。ASOC将soc_snd_card逻辑上分为codec/platform/machine三个组件,ASoC对于Alsa来说,就是分别注册PCM/CONTROL类型的snd_device设备,并实现相应的操 作方法集。

#####4.2 Asoc硬件架构:

这里写图片描述

重点内容
一些平台的 codec IC 集成在Soc内部,比如高通平台(msm8909),但是常见的Codec声卡芯片都是独立于 soc外部,即CPU 和 Codec 是两个独立的IC,本文讲述Asoc框架的基础是 codec ic独立于soc外部;

Machine :
是指某一款机器,可以是某款设备,某款开发板,又或者是某款智能手机,由此可以看出Machine几乎是不可重用的,每个Machine上的硬件实现可能都不一样CPU不一样,Codec不一样,音频的输入、输出设备也不一样,Machine为CPU、Codec、输入输出设备提供了一个载体。

Platform :
一般是指某一个SoC平台,比如pxaxxx,s3cxxxx,omapxxx等等,与音频相关的通常包含该SoC中的时钟、DMA、I2S、PCM等等,只要指定了SoC,那么我们可以认为它会有一个对应的Platform,它只与SoC相关,Machine无关,这样我们就可以把Platform抽象出来,使得同一款SoC不用做任何的改动,就可以用在不同的Machine中。实际上,把Platform认为是某个SoC更好理解。

Codec :
字面上的意思就是编解码器,Codec里面包含了I2S接口、D/A、A/D、Mixer、PA(功放),通常包含多种输入(Mic、Line-in、I2S、PCM)和多个输出(耳机、喇叭、听筒,Line-out),Codec和Platform一样,是可重用的部件,同一个Codec可以被不同的Machine使用。嵌入式Codec通常通过I2C对内部的寄存器进行控制。音频数据传输一般用I2S接口,控制一般用I2c或SPI接口

#####4.3 Asoc软件架构:
嵌入式音频系统软件就架构同样同样分为三大部分,Machine,Platform,Codec

这里写图片描述

Machine:
驱动主要是针对设备的,实现Codec和Platform耦合,Machine驱动是实现板级上的Codec 和SoC中间的桥梁。描述两者如何连接。一般的板卡设计者只需要实现这部分的驱动。Machine驱动的开发主要工作是向内核注册一个snd_soc_card声卡实体。

Codec:
Codec驱动ASoC中的一个重要设计原则就是要求Codec驱动是平台无关的,它包含了一些音频的控件(Controls),音频接口,DAMP(动态音频电源管理)的定义和某些Codec IO功能。为了保证硬件无关性,任何特定于平台和机器的代码都要移到Platform和Machine驱动中。所有的Codec驱动都要提供以下特性:

Codec DAI 和 PCM的配置信息;
Codec的IO控制方式(I2C,SPI等);
Mixer和其他的音频控件;
Codec的ALSA音频操作接口;

Codec的配置一般通过简单的串行总线如I2C,SPI来实现,SoC DAI (SoC Digital Audio Interface)有时候也被称作同步串行接口,实现SoC 和CodeC之间的数据传输。通常为I2S,PCM,AC97,DSP A/B接口。有的SoC也有复杂的SPDIF接口。CodeC一般为SoC外部的独立IC,但也有SoC 厂家在SoC内部实现CodeC功能。Linux 系统中的CodeC驱动部分定义了codec的功能,包括dai类型,控制,模拟输入输出。驱动一般都是IC 厂家实现。平台驱动定义的是SoC的音频接口。同时还是有DMA调用。实现的是数据的发送和接收。音频数据传输一般用I2S接口,控制一般用I2c或SPI接口。

Platform:
Platform驱动 它包含了该SoC平台的音频DMA和音频接口的配置和控制(I2S,PCM,AC97等等);它也不能包含任何与板子或机器相关的代码。Machine驱动 Machine驱动负责处理机器特有的一些控件和音频事件(例如,当播放音频时,需要先行打开一个放大器);单独的Platform和Codec驱动是不能工作的,它必须由Machine驱动把它们结合在一起才能完成整个设备的音频处理工作。Platform驱动向ASoC注册snd_soc_platform和snd_soc_dai设备

snd_soc_card代表着Machine驱动,snd_soc_platform则代表着Platform驱动,snd_soc_codec则代表了Codec驱动,而snd_soc_dai_link则负责连接Platform和Codec。

重点内容
snd_soc_dai是snd_soc_platform和snd_soc_codec的数字音频接口。
snd_soc_codec的dai是codec_dai
snd_soc_platform的dai为cpu_dai,
snd_pcm是snd_soc_card实例化后注册的声卡类型.

重点内容
注意 snd_soc_dai_link结构就指明了该Machine所使用的Platform和Codec。在Codec这边通过codec_dai和Platform侧的cpu_dai相互通信,既然相互通信,就需要遵守一定的规则,其中codec_dai和cpu_dai统一抽象为struct snd_soc_dai结构,而将dai的相关操作使用snd_soc_dai_driver抽象。同时也需要对所有的codec设备进行抽象封装,linux使用snd_soc_codec进行所有codec设备的抽象,而将codec的驱动抽象为snd_soc_codec_driver结构。

#####4.4 ASOC-ALSA 之Machine框架分析:
######4.4.1 Machine概述
由上面可知,ASoC被分为Machine、Platform和Codec三大部分,其中的Machine驱动负责Platform和Codec之间的耦合以及部分和设备或板子特定的代码,Machine为CPU、Codec、输入输出设备提供了一个载体,再次引用上一节的内容:单独的Platform和Codec驱动是不能工作的,它必须由Machine驱动把它们结合在一起才能完成整个设备的音频处理工作。ASoC的一切都从Machine驱动开始,包括声卡的注册,绑定Platform和Codec驱动等等,接下来我们从Machine开始分析。

Machine关键数据结构如下

//代表ASoc设备
struct snd_soc_card {
	struct device *dev;
	struct snd_soc_dai_link *dai_link;
	int num_links;
}

//用于创建CPU DAI 和 Codec DAI的连接,代码上就是对 cpu_of_node,platform_of_node和codec_of_node分配对应的节点
struct snd_soc_dai_link {
/* config - must be set by machine driver */
const char *name;			/* Codec name */
const char *stream_name;		/* Stream name */

const char *cpu_name;
const struct device_node *cpu_of_node;
const char *cpu_dai_name; //指定CPU侧的数字音频接口,一般都是I2S接口

const char *codec_name; //指定codec芯片
const struct device_node *codec_of_node;
const char *codec_dai_name; //指定codec侧的dai名称

const char *platform_name; //指定cpu侧平台驱动,通常是DMA驱动,用于传输
const struct device_node *platform_of_node;

const struct snd_soc_ops *ops; //音频相关的操作函数集
...
};

######4.4.2 Machine驱动分析
kernel/sound/soc 此目录下就是当前支持ASOC架构的平台以 三星平台 kernel/sound/soc/smdk_wm8994.c为例
功能:
1 创建声卡 card 实例,指定 platform、codec、cpu_dai、codec_dai的名字,

2 注册声卡 card 实例 snd_soc_register_card(card)

2.2 为snd_soc_pcm_runtime数组申请内存 每一个dai_link对应snd_soc_pcm_runtime数组的一个单元
2.3 调用snd_soc_instantiate_card(card);
	2.3.1 遍历 snd_soc_card 的dai_link,并且将 codec, dai,  platform相关信息存储在  struct snd_soc_pcm_runtim
	2.3.2 创建声卡card实例 并且设置card
	2.3.3 调用  platform->probe  codec->probe 即 platform 和 codec的prob函数
	2.3.4 调用了codec_dai,cpu_dai 的probe函数
	2.3.5 创建声卡逻辑设备pcm实例 ret = soc_new_pcm(rtd, num);
	2.3.6 注册声卡设备ret = snd_card_register(card->snd_card);

3 将声卡实例card 注册为平台设备

小结:
概括来说就是 将machine驱动的注册过程会将 调用codec codec_dai platform cpu_dai(platform_dai)各部分的prob函数,由此注册各部分驱动,声卡驱动注册成功后最后将machine注册为平台设备;

kernel/sound/soc/smdk_wm8994.c ,代码中相关函数调用于 kernel/sound/soc/soc_core.c文件,一并贴出,请留意

//声卡card实例操作集
static struct snd_soc_ops smdk_ops = {
.hw_params = smdk_hw_params,
};

//dai_link中指定了 platform codec cpu_dai codec_dai的名字,用来匹配系统中已注册的platform、codec、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-i2s.0",
	.codec_name = "wm8994-codec",
	.init = smdk_wm8994_init_paiftx,
	.ops = &smdk_ops,
}, { /* Sec_Fifo Playback i/f */
	.name = "Sec_FIFO TX",
	.stream_name = "Sec_Dai",
	.cpu_dai_name = "samsung-i2s-sec",
	.codec_dai_name = "wm8994-aif1",
	.platform_name = "samsung-i2s-sec",
	.codec_name = "wm8994-codec",
	.ops = &smdk_ops,
},
};

//声卡实例
static struct snd_soc_card smdk = {
.name = "SMDK-I2S",
.owner = THIS_MODULE,
.dai_link = smdk_dai,
.num_links = ARRAY_SIZE(smdk_dai),
};


static int smdk_audio_probe(struct platform_device *pdev)
{
int ret;
struct device_node *np = pdev->dev.of_node;
struct snd_soc_card *card = &smdk;

//将声卡card实例 绑定 platform平台
card->dev = &pdev->dev;

if (np) {
	smdk_dai[0].cpu_dai_name = NULL;
	smdk_dai[0].cpu_of_node = of_parse_phandle(np,
			"samsung,i2s-controller", 0);
	if (!smdk_dai[0].cpu_of_node) {
		dev_err(&pdev->dev,
		   "Property 'samsung,i2s-controller' missing or invalid\n");
		ret = -EINVAL;
	}

	smdk_dai[0].platform_name = NULL;
	smdk_dai[0].platform_of_node = smdk_dai[0].cpu_of_node;
}

//注册声卡card实例
ret = snd_soc_register_card(card);

if (ret)
	dev_err(&pdev->dev, "snd_soc_register_card() failed:%d\n", ret);

return ret;
}


static const struct of_device_id samsung_wm8994_of_match[] = {
{ .compatible = "samsung,smdk-wm8994", },
{},
};
MODULE_DEVICE_TABLE(of, samsung_wm8994_of_match);

static struct platform_driver smdk_audio_driver = {
.driver		= {
	.name	= "smdk-audio",
	.owner	= THIS_MODULE,
	.of_match_table = of_match_ptr(samsung_wm8994_of_match),
},
.probe		= smdk_audio_probe,
.remove		= smdk_audio_remove,
};
module_platform_driver(smdk_audio_driver);

kernel/sound/soc/soc_core.c

//依次调用了cpu_dai codec_dai 的probe函数,创建 pcm 实例
static int soc_probe_link_dais(struct snd_soc_card *card, int num, int order)
{
struct snd_soc_dai_link *dai_link = &card->dai_link[num];
int ret;

	...

//调用cpu_dai->probe
ret = cpu_dai->driver->probe(cpu_dai);

	...
//调用codec_dai->probe
ret = codec_dai->driver->probe(codec_dai);

	...
//创建声卡逻辑设备pcm实例
ret = soc_new_pcm(rtd, num);
}

static int soc_probe_platform(struct snd_soc_card *card,
		   struct snd_soc_platform *platform)
{
int ret = 0;
const struct snd_soc_platform_driver *driver = platform->driver;
struct snd_soc_dai *dai;

...
//调用platform->probe
ret = driver->probe(platform);

...
}

static int soc_probe_codec(struct snd_soc_card *card,
		   struct snd_soc_codec *codec)
{
int ret = 0;
const struct snd_soc_codec_driver *driver = codec->driver;
struct snd_soc_dai *dai;

...
//调用codec->probe
ret = driver->probe(codec);

...
}

static int soc_probe_link_components(struct snd_soc_card *card, int num,
			     int order)
{
struct snd_soc_pcm_runtime *rtd = &card->rtd[num];
struct snd_soc_dai *cpu_dai = rtd->cpu_dai;
struct snd_soc_dai *codec_dai = rtd->codec_dai;
struct snd_soc_platform *platform = rtd->platform;
int ret;

/* probe the CPU-side component, if it is a CODEC */
if (cpu_dai->codec &&
    !cpu_dai->codec->probed &&
    cpu_dai->codec->driver->probe_order == order) {
	ret = soc_probe_codec(card, cpu_dai->codec);
	if (ret < 0)
		return ret;
}
/* probe the CODEC-side component */
if (!codec_dai->codec->probed &&
    codec_dai->codec->driver->probe_order == order) {
	ret = soc_probe_codec(card, codec_dai->codec);
	if (ret < 0)
		return ret;
}
/* probe the platform */
if (!platform->probed &&
    platform->driver->probe_order == order) {
	ret = soc_probe_platform(card, platform);
	if (ret < 0)
		return ret;
}
return 0;
}


static int snd_soc_instantiate_card(struct snd_soc_card *card)
{
struct snd_soc_codec *codec;
struct snd_soc_codec_conf *codec_conf;
enum snd_soc_compress_type compress_type;
struct snd_soc_dai_link *dai_link;
int ret, i, order, dai_fmt;

mutex_lock_nested(&card->mutex, SND_SOC_CARD_CLASS_INIT);

/* bind DAIs */
//确认 struct snd_soc_dai_link 中的 cpu codec platform 的 node name dai是否存在 
//并且将 codec, dai,  platform相关信息存储在  struct snd_soc_pcm_runtime 中 
//遍历 把snd_soc_card中的dai_link配置复制到相应的snd_soc_pcm_runtime中
for (i = 0; i < card->num_links; i++) {
	ret = soc_bind_dai_link(card, i);
	if (ret != 0)
		goto base_error;
}

//创建声卡card实例 并且设置card
ret = snd_card_create(SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1,
		card->owner, 0, &card->snd_card);


//调用  platform->probe  codec->probe 即 platform 和 codec的prob函数
for (order = SND_SOC_COMP_ORDER_FIRST; order <= SND_SOC_COMP_ORDER_LAST;
		order++) {
	for (i = 0; i < card->num_links; i++) {
		ret = soc_probe_link_components(card, i, order);
		if (ret < 0) {
			dev_err(card->dev,
				"ASoC: failed to instantiate card %d\n",
				ret);
			goto probe_dai_err;
		}
	}
}

//调用了codec_dai,cpu_dai 的probe函数
for (order = SND_SOC_COMP_ORDER_FIRST; order <= SND_SOC_COMP_ORDER_LAST;
		order++) {
	for (i = 0; i < card->num_links; i++) {
		ret = soc_probe_link_dais(card, i, order);
		if (ret < 0) {
			dev_err(card->dev,
				"ASoC: failed to instantiate card %d\n",
				ret);
			goto probe_dai_err;
		}
	}
}
ret = snd_card_register(card->snd_card);
}


int snd_soc_register_card(struct snd_soc_card *card)
{
int i, ret;

if (!card->name || !card->dev)
	return -EINVAL;

for (i = 0; i < card->num_links; i++) {
	struct snd_soc_dai_link *link = &card->dai_link[i];

	//检测 struct snd_soc_dai_link 中的 codec 节点是否存在 
	if (!!link->codec_name == !!link->codec_of_node) {
		dev_err(card->dev, "ASoC: Neither/both codec"
			" name/of_node are set for %s\n", link->name);
		return -EINVAL;
	}
	
	//检测 struct snd_soc_dai_link 中的 codec_dai name 是否存在 
	if (!link->codec_dai_name) {
		dev_err(card->dev, "ASoC: codec_dai_name not"
			" set for %s\n", link->name);
		return -EINVAL;
	}

	//struct snd_soc_dai_link 中的 platform 节点是否存在
	if (link->platform_name && link->platform_of_node) {
		dev_err(card->dev, "ASoC: Both platform name/of_node"
			" are set for %s\n", link->name);
		return -EINVAL;
	}

	//struct snd_soc_dai_link 中的 platform name 和 node  是否存在
	if (link->cpu_name && link->cpu_of_node) {
		dev_err(card->dev, "ASoC: Neither/both "
			"cpu name/of_node are set for %s\n",link->name);
		return -EINVAL;
	}

	//struct snd_soc_dai_link 中的 cpu node name 存在
	if (!link->cpu_dai_name &&
	    !(link->cpu_name || link->cpu_of_node)) {
		dev_err(card->dev, "ASoC: Neither cpu_dai_name nor "
			"cpu_name/of_node are set for %s\n", link->name);
		return -EINVAL;
	}
}

...
//为snd_soc_pcm_runtime数组申请内存 每一个dai_link对应snd_soc_pcm_runtime数组的一个单元
card->rtd = devm_kzalloc(card->dev,
			 sizeof(struct snd_soc_pcm_runtime) *
			 (card->num_links + card->num_aux_devs),
			 GFP_KERNEL);

...

ret = snd_soc_instantiate_card(card);

}
4.5 ASOC-ALSA 之Codec框架分析
4.5.1 Codec概述

ASOC的出现是为了让Codec独立于CPU,减少和CPU之间的耦合,这样同一个Codec驱动无需修改就可以适用任何一款平台,在Machine中已经知道,snd_soc_dai_link结构就指明了该Machine所使用的Platform和Codec。在Codec这边通过codec_dai和Platform侧的cpu_dai相互通信,既然相互通信,就需要遵守一定的规则,其中codec_dai和cpu_dai统一抽象为struct snd_soc_dai结构,而将dai的相关操作使用snd_soc_dai_driver抽象。同时也需要对所有的codec设备进行抽象封装,linux使用snd_soc_codec进行所有codec设备的抽象,而将codec的驱动抽象为snd_soc_codec_driver结构。

描述codec的几个关键数据结构如下:
struct snd_soc_codec
struct snd_soc_codec_driver
struct snd_soc_dai
struct snd_soc_dai_driver

//codec设备
struct snd_soc_codec {
const char *name; //Codec的名字
struct device *dev;//指向Codec设备的指针
const struct snd_soc_codec_driver *driver;//指向该codec的驱动的指针
struct snd_soc_card *card;//指向Machine驱动的card实例
int num_dai; //该Codec数字接口的个数,目前越来越多的Codec带有多个I2S或者是PCM接口 
...
void *control_data; //该指针指向的结构用于对codec的控制,通常和read,write字段联合使用
enum snd_soc_control_type control_type;//可以是SND_SOC_SPI,SND_SOC_I2C,SND_SOC_REGMAP中的一种
...
};

//codec设备驱动
struct snd_soc_codec_driver {
/* driver ops */
int (*probe)(struct snd_soc_codec *);//codec驱动的probe函数,由snd_soc_instantiate_card回调
int (*remove)(struct snd_soc_codec *);
int (*suspend)(struct snd_soc_codec *);//电源管理
int (*resume)(struct snd_soc_codec *);//电源管理

//codec寄存器操作函数
...
...
};

//数字音频接口运行时数据 
struct snd_soc_dai {
const char *name;//dai的名字
struct device *dev;//设备指针
struct snd_soc_dai_driver *driver;// 指向dai驱动结构的指针
void *playback_dma_data;//用于管理playback dma
void *capture_dma_data;//用于管理capture dma
struct snd_soc_platform *platform;//如果是cpu dai,指向所绑定的平台
struct snd_soc_codec *codec;//如果是codec dai指向所绑定的codec
struct snd_soc_card *card;//指向Machine驱动中的crad实例
...
};

//数字音频接口驱动程序
struct snd_soc_dai_driver {
/* DAI description */
const char *name;//dai驱动名字
...
/* DAI driver callbacks */
int (*probe)(struct snd_soc_dai *dai);//dai驱动的probe函数,由snd_soc_instantiate_card回调
int (*remove)(struct snd_soc_dai *dai);
int (*suspend)(struct snd_soc_dai *dai);//电源管理
int (*resume)(struct snd_soc_dai *dai);
...
struct snd_soc_pcm_stream capture;//描述capture的能力
struct snd_soc_pcm_stream playback;//描述playback的能力 
...
};

//实现该dai的控制与参数配置
struct snd_soc_dai_ops {
...
}

重点内容
注意:如何搜索codec代码呢? 可以通过 machine 中的 snd_soc_dai_link中的 codec_name = “xxx” 来搜索codec代码的位置

4.5.2 Codec代码框架

因为Codec驱动的代码要做到平台无关性,要使得Machine驱动能够使用该Codec,Codec驱动的首要任务就是确定snd_soc_codec和snd_soc_dai的实例,并把它们注册到系统中,注册后的codec和dai才能为Machine驱动所用。以WM8994为例,对应的代码位置:/sound/soc/codecs/wm8994.c

/sound/soc/codecs/wm8994.c ,代码中很多函数调用于kernel/sound/soc/soc_core.c,相关函数一并贴出,请留意

static const struct snd_soc_dai_ops wm8994_aif1_dai_ops = {
.set_sysclk	= wm8994_set_dai_sysclk,
.set_fmt	= wm8994_set_dai_fmt,
.hw_params	= wm8994_hw_params,
.digital_mute	= wm8994_aif_mute,
.set_pll	= wm8994_set_fll,
.set_tristate	= wm8994_set_tristate,
};

static const struct snd_soc_dai_ops wm8994_aif2_dai_ops = {
.set_sysclk	= wm8994_set_dai_sysclk,
.set_fmt	= wm8994_set_dai_fmt,
.hw_params	= wm8994_hw_params,
.digital_mute   = wm8994_aif_mute,
.set_pll	= wm8994_set_fll,
.set_tristate	= wm8994_set_tristate,
};

static const struct snd_soc_dai_ops wm8994_aif3_dai_ops = {
.hw_params	= wm8994_aif3_hw_params,
};

//创建 snd_soc_dai_driver实例 
static struct snd_soc_dai_driver wm8994_dai[] = {
{
	.name = "wm8994-aif1",
	.id = 1,
	.playback = {
		.stream_name = "AIF1 Playback",
		.channels_min = 1,
		.channels_max = 2,
		.rates = WM8994_RATES,
		.formats = WM8994_FORMATS,
		.sig_bits = 24,
	},
	.capture = {
		.stream_name = "AIF1 Capture",
		.channels_min = 1,
		.channels_max = 2,
		.rates = WM8994_RATES,
		.formats = WM8994_FORMATS,
		.sig_bits = 24,
	 },
	.ops = &wm8994_aif1_dai_ops,
},
{
	.name = "wm8994-aif2",
	.id = 2,
	.playback = {
		.stream_name = "AIF2 Playback",
		.channels_min = 1,
		.channels_max = 2,
		.rates = WM8994_RATES,
		.formats = WM8994_FORMATS,
		.sig_bits = 24,
	},
	.capture = {
		.stream_name = "AIF2 Capture",
		.channels_min = 1,
		.channels_max = 2,
		.rates = WM8994_RATES,
		.formats = WM8994_FORMATS,
		.sig_bits = 24,
	},
	.probe = wm8994_aif2_probe,
	.ops = &wm8994_aif2_dai_ops,
},
{
	.name = "wm8994-aif3",
	.id = 3,
	.playback = {
		.stream_name = "AIF3 Playback",
		.channels_min = 1,
		.channels_max = 2,
		.rates = WM8994_RATES,
		.formats = WM8994_FORMATS,
		.sig_bits = 24,
	},
	.capture = {
		.stream_name = "AIF3 Capture",
		.channels_min = 1,
		.channels_max = 2,
		.rates = WM8994_RATES,
		.formats = WM8994_FORMATS,
		.sig_bits = 24,
	 },
	.ops = &wm8994_aif3_dai_ops,
}
};

//创建 snd_soc_codec_driver实例
static struct snd_soc_codec_driver soc_codec_dev_wm8994 = {
.probe =	wm8994_codec_probe,
.remove =	wm8994_codec_remove,
.suspend =	wm8994_codec_suspend,
.resume =	wm8994_codec_resume,
.set_bias_level = wm8994_set_bias_level,
};

static int wm8994_probe(struct platform_device *pdev)
{
...

//注册 codec声卡IC 以及 DAI
return snd_soc_register_codec(&pdev->dev, &soc_codec_dev_wm8994,
		wm8994_dai, ARRAY_SIZE(wm8994_dai));
}


static const struct dev_pm_ops wm8994_pm_ops = {
SET_SYSTEM_SLEEP_PM_OPS(wm8994_suspend, wm8994_resume)
};

static struct platform_driver wm8994_codec_driver = {
.driver = {
	.name = "wm8994-codec",
	.owner = THIS_MODULE,
	.pm = &wm8994_pm_ops,
},
.probe = wm8994_probe,
.remove = wm8994_remove,
};
module_platform_driver(wm8994_codec_driver);

总的来说就是创建 snd_soc_codec_driver snd_soc_dai_driver实例,然后当作platform设备注册,执行prob时进行 snd_soc_codec_driver和snd_soc_dai_driver的注册

kernel/sound/soc/soc_core.c

kernel/sound/soc/soc_core.c
//注册 codec_dai 
static int snd_soc_register_dais(struct device *dev,
	struct snd_soc_dai_driver *dai_drv, size_t count)
{
struct snd_soc_codec *codec;
struct snd_soc_dai *dai;
int i, ret = 0;

//初始化 codec_dai
...
//把codec实例链接到全局链表codec_list
list_add(&dai->list, &dai_list);
}

//注册 codec 和 codec_dai
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)
{
size_t reg_size;
struct snd_soc_codec *codec;
int ret, i;

dev_dbg(dev, "codec register %s\n", dev_name(dev));

//申请 snd_soc_codec 空间
codec = kzalloc(sizeof(struct snd_soc_codec), GFP_KERNEL);
if (codec == NULL)
	return -ENOMEM;

//确认 snd_soc_codec的名字,这个很重要,Machine驱动定义的snd_soc_dai_link中会指定每个link的codec和dai的名字,进行匹配绑定时就是通过和这里的名字比较,从而找到该Codec的
codec->name = fmt_single_name(dev, &codec->id);

//初始化 codec 

codec->write = codec_drv->write;
codec->read = codec_drv->read;
codec->volatile_register = codec_drv->volatile_register;
codec->readable_register = codec_drv->readable_register;
codec->writable_register = codec_drv->writable_register;
codec->ignore_pmdown_time = codec_drv->ignore_pmdown_time;
codec->dapm.bias_level = SND_SOC_BIAS_OFF;
codec->dapm.dev = dev;
codec->dapm.codec = codec;
codec->dapm.seq_notifier = codec_drv->seq_notifier;
codec->dapm.stream_event = codec_drv->stream_event;
codec->dev = dev;
codec->driver = codec_drv;
codec->num_dai = num_dai;
mutex_init(&codec->mutex);

...

//通过snd_soc_register_dais函数对本Codec的dai进行注册
ret = snd_soc_register_dais(dev, dai_drv, num_dai);
}
EXPORT_SYMBOL_GPL(snd_soc_register_codec);

#####4.6 ASOC-ALSA 之Platform 框架分析
######4.6.1 Platform 概述

在ASOC在Platform部分,主要是平台相关的DMA操作和音频管理。大概流程先将音频数据从内存通过DMA方式传输到CPU侧的dai接口,然后通过CPU的dai接口(通过I2S总线)将数据从达到Codec中,数据会在Codec侧会解码的操作,最终输出到耳机/音箱中。Platform驱动的主要作用是完成音频数据的管理,最终通过CPU的数字音频接口(DAI)把音频数据传送给Codec进行处理,最终由Codec输出驱动耳机或者是喇叭的音信信号。它包含了该SoC平台的音频DMA和音频接口的配置和控制(I2S,PCM,AC97等等);它也不能包含任何与板子或机器相关的代码。在具体实现上,ASoC有把Platform驱动分为两个部分:

1:snd_soc_platform_driver : 负责管理音频数据,把音频数据通过dma或其他操作从内存传送至cpu dai中,代表平台使用的dma驱动,主要是数据的传输等

2:snd_soc_dai_driver: 完成cpu一侧的dai的参数配置,同时也会通过一定的途径把必要的dma等参数与snd_soc_platform_driver进行交互。代表cpu侧的dai驱动,其中包括dai的配置(音频格式,clock,音量等)

注意:和Machine一样,使用snd_soc_platform结构对所有platform设备进行统一抽象

如何找到Machine对应的Platform呢?
答案也是通过Machine中的snd_soc_dai_link中 platform_name(cpu_dai_name) “samsung-i2s-sec” 。在内核中搜素platform_name所对应的name。搜索到 /sound/soc/samsung/i2s.c
由于资料原因,我们以 /sound/soc/samsung/s3c24xx-i2s.c为例

######4.6.2 Platform 代码框架

功能:
1 创建平台设备 static struct platform_driver
2 创建 struct snd_soc_dai_driver 代表cpu侧的dai驱动其中包括dai的配置(音频格式,clock,音量等)
3 注册 struct snd_soc_dai_drive
4 注册 struct snd_soc_platform_driver :asoc_dma_platform_register(&pdev->dev);(代表平台使用的dma驱动,主要是数据的传输等)

//cpu_dai 操作函数集
static const struct snd_soc_dai_ops s3c24xx_i2s_dai_ops = {
.trigger	= s3c24xx_i2s_trigger,
.hw_params	= s3c24xx_i2s_hw_params,
.set_fmt	= s3c24xx_i2s_set_fmt,
.set_clkdiv	= s3c24xx_i2s_set_clkdiv,
.set_sysclk	= s3c24xx_i2s_set_sysclk,
};

//创建 cpu侧 的cpu_dai 实例
static snd_soc_dai_driver s3c24xx_i2s_dai = {
.probe = s3c24xx_i2s_probe,
.suspend = s3c24xx_i2s_suspend,
.resume = s3c24xx_i2s_resume,
.playback = {
	.channels_min = 2,
	.channels_max = 2,
	.rates = S3C24XX_I2S_RATES,
	.formats = SNDRV_PCM_FMTBIT_S8 | SNDRV_PCM_FMTBIT_S16_LE,},
.capture = {
	.channels_min = 2,
	.channels_max = 2,
	.rates = S3C24XX_I2S_RATES,
	.formats = SNDRV_PCM_FMTBIT_S8 | SNDRV_PCM_FMTBIT_S16_LE,},
.ops = &s3c24xx_i2s_dai_ops,

};

//创建 snd_soc_component_driver 实例
static const struct snd_soc_component_driver s3c24xx_i2s_component = {
.name		= "s3c24xx-i2s",
};

static int s3c24xx_iis_dev_probe(struct platform_device *pdev)
{
int ret = 0;

//初始化 snd_soc_component ,注册一个component实例,传入的参数分别是snd_soc_component_driver和snd_soc_dai_drive(代表cpu侧的dai驱动,其中包括dai的配置)
ret = snd_soc_register_component(&pdev->dev, &s3c24xx_i2s_component,
				 &s3c24xx_i2s_dai, 1);

//注册 struct snd_soc_platform_driver(代表平台使用的dma驱动,主要是数据的传输)
ret = asoc_dma_platform_register(&pdev->dev);
}

static struct platform_device_id samsung_i2s_driver_ids[] = {
{
	.name           = "samsung-i2s",
	.driver_data	= TYPE_PRI,
}, {
	.name           = "samsung-i2s-sec",
	.driver_data	= TYPE_SEC,
},
{},
};
MODULE_DEVICE_TABLE(platform, samsung_i2s_driver_ids);

static struct samsung_i2s_dai_data samsung_i2s_dai_data_array[] = {
[TYPE_PRI] = { TYPE_PRI },
[TYPE_SEC] = { TYPE_SEC },
};

static const struct of_device_id exynos_i2s_match[] = {
{ .compatible = "samsung,i2s-v5",
  .data = &samsung_i2s_dai_data_array[TYPE_PRI],
},
{},
};
MODULE_DEVICE_TABLE(of, exynos_i2s_match);

static const struct dev_pm_ops samsung_i2s_pm = {
SET_RUNTIME_PM_OPS(i2s_runtime_suspend,
			i2s_runtime_resume, NULL)
};

//平台设备
static struct platform_driver samsung_i2s_driver = {
.probe  = samsung_i2s_probe,
.remove = samsung_i2s_remove,
.id_table = samsung_i2s_driver_ids,
.driver = {
	.name = "samsung-i2s",
	.owner = THIS_MODULE,
	.of_match_table = of_match_ptr(exynos_i2s_match),
	.pm = &samsung_i2s_pm,
},
};
module_platform_driver(samsung_i2s_driver);
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Linux老A

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值