8.声卡驱动02-自己实现alsa驱动-虚拟声卡-匹配
亲自动手,丰衣足食。本文目的是实现史上最简单的Linux声卡驱动。如果你是初学者,可能从其他文章了解到声卡驱动,不出意外你可能已经云里雾里了,除非你聪明绝顶(秃顶那种)。其实生成声卡的节点,子需要几个函数就可以了,它们分别是:platform:snd_soc_register_component()注册CPU DAI, snd_soc_register_platform()注册platform;c
亲自动手,丰衣足食。本文目的是实现史上最简单的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件事
-
新建一个component;
-
注册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 10月 25 10:09 bind lrwxrwxrwx 1 root root 0 10月 25 10:09 module -> ../../../../module/vplatform --w------- 1 root root 4096 10月 25 10:09 uevent --w------- 1 root root 4096 10月 25 10:09 unbind lrwxrwxrwx 1 root root 0 10月 25 10:09 vplat.0 -> ../../../../devices/platform/vplat.0
-
注册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 10月 24 09:39 by-path
crw-rw----+ 1 root audio 116, 2 10月 24 09:31 controlC0
crw-rw----+ 1 root audio 116, 6 10月 24 09:39 controlC1
crw-rw----+ 1 root audio 116, 4 10月 24 09:32 pcmC0D0c
crw-rw----+ 1 root audio 116, 3 10月 24 09:32 pcmC0D0p
crw-rw----+ 1 root audio 116, 5 10月 24 09:31 pcmC0D1c
crw-rw----+ 1 root audio 116, 8 10月 24 09:39 pcmC1D0c
crw-rw----+ 1 root audio 116, 7 10月 24 09:39 pcmC1D0p
crw-rw----+ 1 root audio 116, 1 10月 24 09:31 seq
crw-rw----+ 1 root audio 116, 33 10月 24 09:31 timer
成功生成controlC1、pcmC1D0c、pcmC1D0p。
如果是移植真正codec,到这里基本是能用了,但是这里是要写一个不涉及硬件操作的虚拟声卡,所以是不能用的,后续继续。
另外测试平台不限于ubuntu 16.04,只要内核版本相差不大,应该都能编译通过,是能用的。
现在看看,还是挺简单的,为什么当初学的时候那么费劲?因为没有动手敲。
附上代码位置:https://codechina.csdn.net/u014056414/myalsa/-/tree/2026176f47f4de4ed6aa671a1c9206880b0cf7d2
认准提交“1.匹配, 生成节点”
更多推荐
所有评论(0)