亲自动手,丰衣足食。本文目的是实现史上最简单的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. SLAM精度测评——rpg_trajectory_evaluatio
  2. power designer 设计数据库生成到oracle数据库
  3. HTML数字自动排序,jquery – HTML中的数字嵌套排序列表
  4. [bzoj2400]Optimal Marks
  5. jee neow_JEE7:展望新时代
  6. centos7 如何使用ReaR进行系统备份(如何使用NFS方法设置ReaR备份)
  7. 【C/C++】一道试题,深入理解数组和指针
  8. Rust: HashMap的用法及其它
  9. 计算机硬件硬盘分区,电脑硬盘分几个区最好?电脑硬盘分区教程
  10. 用计算机怎么计算字节,计算器里面的字节、字、双字、四字
  11. iOS结构化并发---喵神出品。
  12. 【网络】远程连接路由器
  13. 李刚疯狂java抄袭,推荐:疯狂java讲义--李刚著作(3)
  14. Towards End-to-End Lane Detection: an Instance SegmentationApproach
  15. play框架2.5.6教程——使用play控制台
  16. AIR32F103(九) CAN总线的通信和ID过滤机制及实例
  17. pycharm3.6.5安装不了cfg包(求解)
  18. php 运行命令行,命令行运行php报错
  19. Switch分销技术解读
  20. 第三次工业革命(三)

热门文章

  1. win7(SP1 64位)升级IE浏览器升级到IE11
  2. cmd /c和cmd /k 以及CMD命令
  3. 小米笔记本怎么恢复出厂的系统
  4. python游戏dnf_招募:基于python的召唤师全时段全技能(含均值AI)计算器全程测试...
  5. JAVA系统学习之三大版本JavaSE、javaEE、javaME
  6. layer.js之回调销毁对话框
  7. F28335的ADC采集电压不对(ADC采集模块的输入信号的两个端子是:正极:ADCINAx ,负极是:ADCL0)
  8. python异步请求aiohttp_利用aiohttp制作异步爬虫
  9. esxi查看许可过期_ESXi许可证将在 60 天后过期问题
  10. 极市直播丨南京理工大学魏秀参、沈阳:大规模细粒度图像检索