8.声卡驱动02-自己实现alsa驱动-虚拟声卡-匹配

亲自动手,丰衣足食。本文目的是实现史上最简单的Linux声卡驱动。
如果你是初学者,可能从其他文章了解到声卡驱动,不出意外你可能已经云里雾里了,除非你聪明绝顶(秃顶那种)。
其实生成声卡的节点,子需要几个函数就可以了,它们分别是:

  • platform:snd_soc_register_component()注册CPU DAI, snd_soc_register_platform()注册platform;
  • codec:snd_soc_register_codec()注册CODEC DAI和CODEC;
  • machine:snd_soc_register_card()注册声卡,真正生成节点在这里。

平台:ubuntu 16.04,kernel版本是4.15.0, 本来是想使用qemu测试的,但是电脑配置太低,运行较卡,放弃了。
入口想使用qemu搭建虚拟平台,可参考:【嵌入式Linux驱动入门】一、基于QEMU的IMX6ULL虚拟开发环境搭建

框架图:
在这里插入图片描述

1. codec

1.1 注册codec dai和codec

ret = snd_soc_register_codec(&pdev->dev, &soc_vcodec_drv,
				vcodec_dai, ARRAY_SIZE(vcodec_dai));

先看看soc_vcodec_drv定义

static struct snd_soc_codec_driver soc_vcodec_drv = {
	.probe = vcodec_probe,
	.remove = vcodec_remove,
	//.read = vcodec_reg_read,
	//.write = vcodec_reg_write,
	.ignore_pmdown_time = 1,
};

匹配成功会调用probe()函数, read/write并不是音频数据的读写,而是codec寄存器的读写。
再看看vcodec_dai的定义

static const struct snd_soc_dai_ops vcodec_dai_ops = {
	.startup		= vcodec_startup,	//open之后调用,表示开始,做一下初始化操作
	.hw_params		= vcodec_hw_params,	//设置硬件参数,如采样率等
	.prepare		= vcodec_prepare,	//每次数据传送输之前调用
	.trigger		= vcodec_trigger,	//数据传输的开始,暂停,恢复和停止时,该函数会被调用
	.shutdown		= vcodec_shutdown,
};

static struct snd_soc_dai_driver vcodec_dai[] = {
	{
		.name	= "vcodec_dai",
		.playback = {
			.channels_min = 1,
			.channels_max = 2,
			.rates = SNDRV_PCM_RATE_8000_192000 |	//codec支持的采样率
				SNDRV_PCM_RATE_KNOT,
			.formats = SNDRV_PCM_FMTBIT_S16_LE |	//codec支持的格式,就是数据位宽
				SNDRV_PCM_FMTBIT_S24_LE	|
				SNDRV_PCM_FMTBIT_S32_LE,
		},
		.capture = {
			.channels_min = 1,
			.channels_max = 2,
			.rates = SNDRV_PCM_RATE_8000_48000 |	//codec支持的采样率
				SNDRV_PCM_RATE_KNOT,
			.formats = SNDRV_PCM_FMTBIT_S16_LE |	//codec支持的采样率
				SNDRV_PCM_FMTBIT_S24_LE	|
				SNDRV_PCM_FMTBIT_S32_LE,
		},
		.ops = &vcodec_dai_ops,
	},
};

vcodec_dai是代表codec侧的dai驱动,其中包括dai的配置(音频格式,clock,音量等);
playback表示有播放功能,capture表示录音功能,如果去掉其中一个,表示没有相应功能;
vcodec_dai_ops是由asoc-core调用的函数集,是在open之后调用的,它们的调用顺序是
startup --> hw_params --> prepare --> trigger --> shutdown

另外vcodec_dai_ops还有成员函数digital_mute,功能就是字面意思开关静音,开发过程中经常遇到pop声,可以在这开关功放。

1.2 分析一下snd_soc_register_codec

简略版snd_soc_register_codec(), 留下我们关心的内容

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)
{
	struct snd_soc_codec *codec;

	codec = kzalloc(sizeof(struct snd_soc_codec), GFP_KERNEL);
	codec->component.codec = codec;

	ret = snd_soc_component_initialize(&codec->component,
			&codec_drv->component_driver, dev);
	if (codec_drv->probe)
		codec->component.probe = snd_soc_codec_drv_probe;
	if (codec_drv->remove)
		codec->component.remove = snd_soc_codec_drv_remove;
	if (codec_drv->write)
		codec->component.write = snd_soc_codec_drv_write;
	if (codec_drv->read)
		codec->component.read = snd_soc_codec_drv_read;

	ret = snd_soc_register_dais(&codec->component, dai_drv, num_dai, false);
    list_for_each_entry(dai, &codec->component.dai_list, list)
		dai->codec = codec;

	snd_soc_component_add_unlocked(&codec->component);
	list_add(&codec->list, &codec_list);
}
  • 第6~18行,我们定义的soc_vcodec_drv只是一个副本,重新定义了一个snd_soc_codec指针codec,并将soc_vcodec_drv的回调函数复制到codec->component;
  • 第9行,snd_soc_component_initialize()初始化codec->component, 里面fmt_single_name()生成component->name用于匹配,规则是:dev_name(dev), 如:vcodec.0。如果是I2C设备,却是[dev->driver->name].[bus]-[addr];
  • 第20行,里面调用snd_soc_register_dais()注册codec_dai,会加到codec->component.dai_list
  • 第24行,注册codec->component,会添加到全局链表component_list
  • 第25行,添加codec到全局链表codec_list

2. platform

2.1 注册cpu dai

ret = snd_soc_register_component(&pdev->dev, &vplat_cpudai_component,
					&vplat_cpudai_dai, 1);

看一下vplat_cpudai_component和vplat_cpudai_dai定义

static const struct snd_soc_component_driver vplat_cpudai_component = {
	.name = "vplat-cpudai",
};

static struct snd_soc_dai_driver vplat_cpudai_dai = {
	.name	= "vplat-cpudai",
	.playback = {
		.channels_min = 1,
		.channels_max = 2,
		.rates = SNDRV_PCM_RATE_8000_192000 |
			SNDRV_PCM_RATE_KNOT,
		.formats = SNDRV_PCM_FMTBIT_S16_LE |
			SNDRV_PCM_FMTBIT_S24_LE	|
			SNDRV_PCM_FMTBIT_S32_LE,
	},
	.capture = {
		.channels_min = 1,
		.channels_max = 2,
		.rates = SNDRV_PCM_RATE_8000_48000 |
			SNDRV_PCM_RATE_KNOT,
		.formats = SNDRV_PCM_FMTBIT_S16_LE |
			SNDRV_PCM_FMTBIT_S24_LE	|
			SNDRV_PCM_FMTBIT_S32_LE,
	},
	.ops	= NULL,
};

跟codec dai差不多

分析一下snd_soc_register_component

snd_soc_register_component(...) -->
	struct snd_soc_component *cmpnt;
	//1.新建一个component
	cmpnt = kzalloc(sizeof(*cmpnt), GFP_KERNEL);
	ret = snd_soc_component_initialize(cmpnt, cmpnt_drv, dev); -->
		component->name = fmt_single_name(dev, &component->id);
	
	//2.注册cpu_dai,最终添加cpu_dai到component->dai_list链表
	ret = snd_soc_register_dais(cmpnt, dai_drv, num_dai, true); -->
		soc_add_dai(component, dai_drv + i,count == 1 && legacy_dai_naming); -->
			dai->name = fmt_single_name(dev, &dai->id);
			list_add(&dai->list, &component->dai_list);
	
	//3.注册component,最终添加到全局链表component_list
	snd_soc_component_add(cmpnt); -->
		snd_soc_component_add_unlocked(component); -->
			list_add(&component->list, &component_list);

可见,snd_soc_register_component, 做了3件事

  1. 新建一个component;

  2. 注册cpu_dai,最终添加cpu_dai到component->dai_list链表。关注一下用于匹配的name是怎么来的,是通过fmt_single_name()函数生成的,规则是: dev_name(dev), 那么这里cpu_dai的那么是vplat.0,不清楚的可以这样看:

    vbox@vbox-pc:/sys/bus/platform/drivers/vplat$ ls -l
    total 0
    --w------- 1 root root 4096 1025 10:09 bind
    lrwxrwxrwx 1 root root    0 1025 10:09 module -> ../../../../module/vplatform
    --w------- 1 root root 4096 1025 10:09 uevent
    --w------- 1 root root 4096 1025 10:09 unbind
    lrwxrwxrwx 1 root root    0 1025 10:09 vplat.0 -> ../../../../devices/platform/vplat.0
    
  3. 注册component,最终添加到全局链表component_list;

2.2 注册platform

ret = snd_soc_register_platform(&pdev->dev, &vplat_soc_drv);

看一下vplat_soc_drv定义

static struct snd_pcm_ops vplat_pcm_ops = {
	.open		= vplat_pcm_open,
	.close		= vplat_pcm_close,
	.ioctl		= snd_pcm_lib_ioctl,
	.hw_params	= vplat_pcm_hw_params,
	.prepare    = vplat_pcm_prepare,
	.trigger	= vplat_pcm_trigger,
	.pointer	= vplat_pcm_pointer,
	.mmap		= vplat_pcm_mmap,
	
	//.copy		= vplat_pcm_copy,
};

static struct snd_soc_platform_driver vplat_soc_drv = {
	.ops		= &vplat_pcm_ops,			//由asoc-core回调同codec
	.pcm_new	= vplat_pcm_new,			//分配DMA内存
	.pcm_free	= vplat_pcm_free_buffers,	//释放DMA内存
};

分析一下snd_soc_register_platform

snd_soc_register_platform(...) -->
	struct snd_soc_platform *platform;
	platform = kzalloc(sizeof(struct snd_soc_platform), GFP_KERNEL); -->
	ret = snd_soc_add_platform(dev, platform, platform_drv); -->
		ret = snd_soc_component_initialize(&platform->component,
			&platform_drv->component_driver, dev);
		
		snd_soc_component_add_unlocked(&platform->component);
		list_add(&platform->list, &platform_list);

一样的套路,重新分配一个platform,vplat_soc_drv就是个副本,snd_soc_component_initialize()同样调用fmt_single_name()给platform取个名。

platform也会有一个component,同样注册到全局的component_list链表。

platform最终注册到全局的platform_list链表。

3. machine

3.1 注册soc_card

	struct snd_soc_card *card = &snd_soc_my_card;

	card->dev = &pdev->dev;
	ret = snd_soc_register_card(card);

看一下snd_soc_my_card定义

static struct snd_soc_dai_link my_card_dai_link[] = {
	{
		.name			= "my-codec",
		.stream_name	= "MY-CODEC",		//stream的名字
		.codec_name		= "vcodec.0",		//用于指定codec芯片
		.codec_dai_name = "vcodec_dai",		//用于codec侧的dai名字
		.cpu_dai_name	= "vplat.0",		//用于指定cpu侧的dai名字
		.platform_name	= "vplat.0",		//用于指定cpu侧平台驱动,通常都是DMA驱动,用于传输
		.init			= my_card_init,		//在probe后调用
		.ops			= &my_card_ops,		//asoc-core回调,全是硬件操作
	},
};

static struct snd_soc_card snd_soc_my_card = {
	.name			= "my-codec",
	.owner			= THIS_MODULE,
	.dai_link		= my_card_dai_link,
	.num_links		= ARRAY_SIZE(my_card_dai_link),
};

其中dai_link结构就是用作连接platform和codec的,指明到底用那个codec,那个platfrom。
一个dai_link对应着一个stream,一个stream可能有一个或两个substream,分别是playback或catpure。

3.2 分析一下snd_soc_register_card

snd_soc_register_card(...) -->
	//一个for循环,初始化所有dai_link
	ret = soc_init_dai_link(card, link);
	ret = snd_soc_instantiate_card(card); -->
		//一个for循环, 绑定所有dai_link的dai
		ret = soc_bind_dai_link(card, &card->dai_link[i]); -->
			//创建runtime
			rtd = soc_new_pcm_runtime(card, dai_link);
			//绑定cpu_dai
			rtd->cpu_dai = snd_soc_find_dai(&cpu_dai_component);
			//绑定codec和codec_dai
			codec_dais = rtd->codec_dais;
			codec_dais[i] = snd_soc_find_dai(&codecs[i]);
			rtd->codec_dai = codec_dais[0];
			rtd->codec = rtd->codec_dai->codec;
			//绑定platform
			list_for_each_entry(platform, &platform_list, list)
				...
				rtd->platform = platform
			
			soc_add_pcm_runtime(card, rtd);
				//将runtime加到card的rtd_list
				list_add_tail(&rtd->list, &card->rtd_list);	
		
		//创建snd_card, controlCX节点在此生成
		ret = snd_card_new(card->dev, SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1,
			card->owner, 0, &card->snd_card);
			
		//匹配成功,回调各个的probe函数,soc_new_pcm也会被调到,创建pcmCXDXp和pcmCXDXc节点
		soc_probe_link_dais(card, rtd, order);

通过snd_soc_register_card来注册card, 此函数之后,声卡的相关节点基本生成;

附上多年前在linux 3.X跟的代码:wm8960_note

4. 测试

在ubuntu 16.04上测试,需要另外安装另外3个驱动:
sudo insmod /lib/modules/4.15.0-112-generic/kernel/sound/core/snd-compress.ko
sudo insmod /lib/modules/4.15.0-112-generic/kernel/sound/core/snd-pcm-dmaengine.ko
sudo insmod /lib/modules/4.15.0-112-generic/kernel/sound/soc/snd-soc-core.ko

需要注意的是:/lib/modules/有两个内核版本驱动:

vbox@vbox-pc:/lib/modules$ ls
4.15.0-112-generic  4.15.0-142-generic

笔者的ubuntu出现过问题,手动改过内核版本,用的是4.15.0-112-generic,看可以通过命令uname -a查看一下:

vbox@vbox-pc:/proc/asound$ uname -a
Linux vbox-pc 4.15.0-112-generic #113~16.04.1-Ubuntu SMP Fri Jul 10 04:37:08 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

Makefile改一下KERN_DIR,KERN_DIR就是内核代码路径,Ubuntu下怎么修改,uname -a看一下内核版本,找到/usr/src/下对应的
如:KERN_DIR = /usr/src/linux-headers-4.15.0-112-generic

编译之后接下来就可以安装我们的驱动了
sudo insmod vplatform.ko
sudo insmod vcodec.ko
sudo insmod vmachine.ko

查看打印

vmachine vmachine.0: vcodec_dai <-> vplat.0 mapping ok

说明匹配成功,查看一下系统有哪些声卡

vbox@vbox-pc:/proc/asound$ cat cards
 0 [I82801AAICH    ]: ICH - Intel 82801AA-ICH
                      Intel 82801AA-ICH with AD1980 at irq 21
 1 [mycodec        ]: my-codec - my-codec
                      OracleCorporation-VirtualBox-1.2-VirtualBox

mycodec就是我们的声卡,注册在card1.
查看一下pcm

vbox@vbox-pc:/proc/asound$ cat pcm
00-00: Intel ICH : Intel 82801AA-ICH : playback 1 : capture 1
00-01: Intel ICH - MIC ADC : Intel 82801AA-ICH - MIC ADC : capture 1
01-00: MY-CODEC vcodec_dai-0 :  : playback 1 : capture 1

MY-CODEC就是我们注册的声卡了。
“01-00”:表示声卡1,device 0
这时节点应该生成了

vbox@vbox-pc:/dev/snd$ ls -l
total 0
drwxr-xr-x  2 root root       80 1024 09:39 by-path
crw-rw----+ 1 root audio 116,  2 1024 09:31 controlC0
crw-rw----+ 1 root audio 116,  6 1024 09:39 controlC1
crw-rw----+ 1 root audio 116,  4 1024 09:32 pcmC0D0c
crw-rw----+ 1 root audio 116,  3 1024 09:32 pcmC0D0p
crw-rw----+ 1 root audio 116,  5 1024 09:31 pcmC0D1c
crw-rw----+ 1 root audio 116,  8 1024 09:39 pcmC1D0c
crw-rw----+ 1 root audio 116,  7 1024 09:39 pcmC1D0p
crw-rw----+ 1 root audio 116,  1 1024 09:31 seq
crw-rw----+ 1 root audio 116, 33 1024 09:31 timer

成功生成controlC1、pcmC1D0c、pcmC1D0p。
如果是移植真正codec,到这里基本是能用了,但是这里是要写一个不涉及硬件操作的虚拟声卡,所以是不能用的,后续继续。

另外测试平台不限于ubuntu 16.04,只要内核版本相差不大,应该都能编译通过,是能用的。
现在看看,还是挺简单的,为什么当初学的时候那么费劲?因为没有动手敲。

附上代码位置:https://codechina.csdn.net/u014056414/myalsa/-/tree/2026176f47f4de4ed6aa671a1c9206880b0cf7d2

认准提交“1.匹配, 生成节点

  • 10
    点赞
  • 41
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值