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.新建一个componentcmpnt = 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_listsnd_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_linkret = soc_init_dai_link(card, link);ret = snd_soc_instantiate_card(card); -->//一个for循环, 绑定所有dai_link的dairet = soc_bind_dai_link(card, &card->dai_link[i]); -->//创建runtimertd = soc_new_pcm_runtime(card, dai_link);//绑定cpu_dairtd->cpu_dai = snd_soc_find_dai(&cpu_dai_component);//绑定codec和codec_daicodec_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;//绑定platformlist_for_each_entry(platform, &platform_list, list)...rtd->platform = platformsoc_add_pcm_runtime(card, rtd);//将runtime加到card的rtd_listlist_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 cards0 [I82801AAICH ]: ICH - Intel 82801AA-ICHIntel 82801AA-ICH with AD1980 at irq 211 [mycodec ]: my-codec - my-codecOracleCorporation-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.匹配, 生成节点”
8.声卡驱动02-自己实现alsa驱动-虚拟声卡-匹配相关推荐
- ubuntu14.04安装oss音频驱动,替换掉alsa驱动
环境: ubuntu14.04 64位 前言: 由于不知道怎么操作alsa驱动下的音频设备,所以使用oss替换掉alsa.替换之后,就可以使用open函数打开"/dev/dsp"设 ...
- 虚拟服务器声卡,如何使用虚拟声卡?虚拟声卡安装教程!
在没有声卡的机器上播放音频和视频文件时,将出现诸如"找不到音频设备"的提示,并且无法播放. 虚拟声卡是一种软件的名称,可用于在没有声卡的机器上实现诸如声音回放之类的功能. 如何使用 ...
- 用ALSA驱动声卡流程详解
作者:北南南北 来自:LinuxSir.Org 提要:目前大多数发行版都已经支持主流声卡,声卡的驱动无非是用ALSA:本文主要讲述声卡驱动的流程:目的是帮助遇到声卡的驱动问题的弟兄来弄清楚解决问题的流 ...
- Linux ALSA驱动框架(一)--ALSA架构简介--声卡的创建
(1)ALSA简介 (1) Native ALSA Application:tinyplay/tinycap/tinymix,这些用户程序直接调用 alsa 用户库接口来实现放音.录音.控制 ALSA ...
- Linux中用ALSA驱动声卡流程详解
一.什么是ALSA : Advanced Linux Sound Architecture 的简称为 ALSA ,译成中文的意思是 Linux 高级声音体系(这是我直译的,可能译的不对):一谈到体系就 ...
- 使用最新 ALSA 驱动解决 UBUNTU LINUX INTEL 集成声卡问题
刚开始学习alsa驱动,刚开始装就出现了一大堆问题,所以在网上找解决方案,看到一篇不错的文章,转载以供大家分享. 目前用户所抱怨的 Ubuntu 系列的声卡问题,基本上归结为几类:一,找不到声音设备: ...
- arm linux alsa驱动使用 usb 声卡
这里写目录标题 一:添加alsa驱动 二:添加alsa-lib和alsa-utils 三:录音测试 四:播放测试 五:alsa 库文件与头文件 一:添加alsa驱动 alsa内核配置选项: 二:添加a ...
- arm linux免驱usb声卡,arm linux利用alsa驱动并使用usb音频设备
一.背景: arm linux的内核版本是3.13.0 二.准备工作 添加alsa驱动到内核中,也就是在编译内核的时候加入以下选项: 接下来就重新编译内核即可 三.交叉编译alsa-lib和alsa- ...
- linux系统声卡安装教程,Linux系统下如何安装声卡驱动?
装了几次Linux OS,当然也装了几次声卡驱动,一般来说都是安装ALSA(Adcance Linux Sound Architecture)驱动,多装几次以后就会发现非常的简单的. 首先,先决条件, ...
- alsa 驱动介绍及user层到hw层文件ioctl操作流程分析
您当前位置:首页 > php开源 > 综合技术 > alsa 驱动介绍 alsa 驱动介绍 来源:程序员人生 发布时间:2016-07-02 13:40:22 阅读次数:6838 ...
最新文章
- SuperMap iDesktop Cross 8C 开源桌面GIS下载与扩展开发
- 参与开源项目,结识技术大牛!CSDN“开源加速器计划”招募志愿者啦!
- Q-learning
- 重磅!!kaggle训练, 终于不用怕断网了
- PHP中的urlencode和urldecode
- AOP in dotnet :AspectCore的参数拦截支持
- SpringBoot 使用 Caffeine 本地缓存
- 数字反转(洛谷-P1307)
- Profile配置和加载配置文件
- 随想录(学校研究和公司研发)
- Java基础知识强化38:StringBuffer类之StringBuffer的添加功能
- VOIP通信中影响语音质量的因素
- 一文细数73个Vision transformer家族成员
- 通用数据库连接工具--DbVisualizer的使用
- python打造记账本_Python实现简单的记账本功能
- js求100以内素数的和
- 缺陷检测论文回顾(一)
- android百度定位方式,Android 百度定位SDK
- CodeBlocks中文汉化Code::Blocks 10.05 中文版
- 自我介绍php一句话,面试自我介绍经典语句,经典一句话自我介绍