亲自动手,丰衣足食。本文目的是实现史上最简单的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件事

  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 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
    
  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_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驱动-虚拟声卡-匹配相关推荐

  1. ubuntu14.04安装oss音频驱动,替换掉alsa驱动

    环境: ubuntu14.04 64位 前言: 由于不知道怎么操作alsa驱动下的音频设备,所以使用oss替换掉alsa.替换之后,就可以使用open函数打开"/dev/dsp"设 ...

  2. 虚拟服务器声卡,如何使用虚拟声卡?虚拟声卡安装教程!

    在没有声卡的机器上播放音频和视频文件时,将出现诸如"找不到音频设备"的提示,并且无法播放. 虚拟声卡是一种软件的名称,可用于在没有声卡的机器上实现诸如声音回放之类的功能. 如何使用 ...

  3. 用ALSA驱动声卡流程详解

    作者:北南南北 来自:LinuxSir.Org 提要:目前大多数发行版都已经支持主流声卡,声卡的驱动无非是用ALSA:本文主要讲述声卡驱动的流程:目的是帮助遇到声卡的驱动问题的弟兄来弄清楚解决问题的流 ...

  4. Linux ALSA驱动框架(一)--ALSA架构简介--声卡的创建

    (1)ALSA简介 (1) Native ALSA Application:tinyplay/tinycap/tinymix,这些用户程序直接调用 alsa 用户库接口来实现放音.录音.控制 ALSA ...

  5. Linux中用ALSA驱动声卡流程详解

    一.什么是ALSA : Advanced Linux Sound Architecture 的简称为 ALSA ,译成中文的意思是 Linux 高级声音体系(这是我直译的,可能译的不对):一谈到体系就 ...

  6. 使用最新 ALSA 驱动解决 UBUNTU LINUX INTEL 集成声卡问题

    刚开始学习alsa驱动,刚开始装就出现了一大堆问题,所以在网上找解决方案,看到一篇不错的文章,转载以供大家分享. 目前用户所抱怨的 Ubuntu 系列的声卡问题,基本上归结为几类:一,找不到声音设备: ...

  7. arm linux alsa驱动使用 usb 声卡

    这里写目录标题 一:添加alsa驱动 二:添加alsa-lib和alsa-utils 三:录音测试 四:播放测试 五:alsa 库文件与头文件 一:添加alsa驱动 alsa内核配置选项: 二:添加a ...

  8. arm linux免驱usb声卡,arm linux利用alsa驱动并使用usb音频设备

    一.背景: arm linux的内核版本是3.13.0 二.准备工作 添加alsa驱动到内核中,也就是在编译内核的时候加入以下选项: 接下来就重新编译内核即可 三.交叉编译alsa-lib和alsa- ...

  9. linux系统声卡安装教程,Linux系统下如何安装声卡驱动?

    装了几次Linux OS,当然也装了几次声卡驱动,一般来说都是安装ALSA(Adcance Linux Sound Architecture)驱动,多装几次以后就会发现非常的简单的. 首先,先决条件, ...

  10. alsa 驱动介绍及user层到hw层文件ioctl操作流程分析

    您当前位置:首页 > php开源 > 综合技术 > alsa 驱动介绍 alsa 驱动介绍 来源:程序员人生   发布时间:2016-07-02 13:40:22 阅读次数:6838 ...

最新文章

  1. SuperMap iDesktop Cross 8C 开源桌面GIS下载与扩展开发
  2. 参与开源项目,结识技术大牛!CSDN“开源加速器计划”招募志愿者啦!
  3. Q-learning
  4. 重磅!!kaggle训练, 终于不用怕断网了
  5. PHP中的urlencode和urldecode
  6. AOP in dotnet :AspectCore的参数拦截支持
  7. SpringBoot 使用 Caffeine 本地缓存
  8. 数字反转(洛谷-P1307)
  9. Profile配置和加载配置文件
  10. 随想录(学校研究和公司研发)
  11. Java基础知识强化38:StringBuffer类之StringBuffer的添加功能
  12. VOIP通信中影响语音质量的因素
  13. 一文细数73个Vision transformer家族成员
  14. 通用数据库连接工具--DbVisualizer的使用
  15. python打造记账本_Python实现简单的记账本功能
  16. js求100以内素数的和
  17. 缺陷检测论文回顾(一)
  18. android百度定位方式,Android 百度定位SDK
  19. CodeBlocks中文汉化Code::Blocks 10.05 中文版
  20. 自我介绍php一句话,面试自我介绍经典语句,经典一句话自我介绍

热门文章

  1. pgAdmin III 导出excel数据
  2. windows 超简单实现多用户远程桌面,RDP WRAPPER
  3. RestSharp是什么?
  4. 苹果电脑上好用的五个文档文本编辑工具
  5. Kafka 安装配置及下载地址
  6. 解决数据库左右连接,查询不出为空的解决方案
  7. 下载Postman并且汉化使用
  8. 灰色系统理论与灰色关联分析模型
  9. web前端开发技术(第3版)储久良著课后实验
  10. 浅谈MyBatis持久层框架