目录

  • 1. 前言
  • 2. PCM逻辑设备
    • 2.1. 创建 PCM逻辑设备:
    • 2.2. PCM逻辑设备文件操作函数集:snd_pcm_f_ops[]
    • 2.3. Open PCM逻辑设备
    • 2.4. Write/Read PCM逻辑设备
    • 2.5. Close PCM逻辑设备
  • 3. 结束语

1. 前言

前面几章分析了ASoC音频驱动中Codec、Platform、Machine驱动的组成部分及其注册过程,这三者都是物理设备相关的,大家应该对音频物理链路有了一定的认知。接着分析音频驱动的中间层,由于这些并不是真正的物理设备,故我们称之为逻辑设备。

PCM逻辑设备,我们又习惯称之为PCM中间层或pcm native,起着承上启下的作用:
1.往上是与用户态接口的交互,实现音频数据在用户态和内核态之间的拷贝,即user space <-> kernel space;
2.往下是触发codec、platform、machine的操作函数,实现音频数据在dma_buffer<-> cpu_dai <-> codec之间的传输。

2. PCM逻辑设备

Kernel 版本:3.10
内核源码文件:
         ./kernel-3.10/sound/core/device.c
         ./kernel-3.10/sound/core/init.c
         ./kernel-3.10/sound/core/pcm.c
         ./kernel-3.10/sound/core/pcm_lib.c
         ./kernel-3.10/sound/core/pcm_native.c
         ./kernel-3.10/sound/soc/soc-core.c
         ./kernel-3.10/sound/soc/soc-pcm.c
         ./kernel-3.10/sound/core/sound.c
Tinyalsa源码文件:
         ./external/tinyalsa/pcm.c

2.1. 创建 PCM逻辑设备:

在Linux 音频驱动(四) ASoC音频驱动之Machine驱动中我们给出了注册声卡的完整时序图。现在简化该时序图,重点看一下其中创建PCM逻辑设备的过程。
(为了和原时序图保持一致,没有更改本时序图里的时序编号)

snd_register_device_for_dev()创建pcmCxDxp、pcmCxDxc设备节点的过程如下:

  1. 首先,分配并初始化一个snd_minor结构中的各字段;
    type:SNDRV_DEVICE_TYPE_PCM_PLAYBACK/SNDRV_DEVICE_TYPE_PCM_CAPTURE
    card:card的编号
    device:pcm实例的编号,大多数情况为0
    f_ops:pcmCxDxp、pcmCxDxc设备节点的文件操作函数集,为snd_pcm_f_ops[*]
    private_data:指向该snd_pcm的实例对象
  2. 根据type,card和pcm的编号,确定数组的索引值minor,minor也作为pcm设备的次设备号;
  3. 把该snd_minor结构的地址放入全局数组snd_minors[minor]中;
  4. 最后,调用device_create创建设备节点。
// ./kernel-3.10/sound/core/sound.c, line 57
static struct snd_minor *snd_minors[SNDRV_OS_MINORS];
// ./kernel-3.10/sound/core/sound.c, line 269
int snd_register_device_for_dev(int type, struct snd_card *card, int dev,const struct file_operations *f_ops,void *private_data, const char *name, struct device *device)
{int minor;struct snd_minor *preg;......preg = kmalloc(sizeof *preg, GFP_KERNEL);if (preg == NULL)return -ENOMEM;preg->type = type;preg->card = card ? card->number : -1;preg->device = dev;preg->f_ops = f_ops;preg->private_data = private_data;preg->card_ptr = card;mutex_lock(&sound_mutex);......minor = snd_find_free_minor(type);......snd_minors[minor] = preg;preg->dev = device_create(sound_class, device, MKDEV(major, minor),private_data, "%s", name);......
}

注1:C0D0代表的是Card 0 Device 0,即声卡0中的设备0,pcmC0D0c最后一个c代表capture,pcmC0D0p最后一个p代表playback,这些都是alsa-driver中的命名规则。
注2:snd_minors[]非常重要,用于保存声卡下某个逻辑设备的上下文信息,它在逻辑设备建立阶段被填充,在逻辑设备被使用时就可以从该结构体中得到相应的信息。
注3:sound_class 是sysfs文件系统中的声卡类,在init_soundcore()中被创建。

我们看一下实例:

可以看到这些设备节点的Major=116,Minor则与系统逻辑设备信息文件/proc/asound/devices所列的对应起来,都是字符设备。上层可以通过open/close/read/write/ioctl等系统调用来操作声卡设备,和其他字符设备类似。但是,一般情况下我们使用已封装好的用户接口库如tinyalsa、alsa-lib。

下文将基于tinyslsa详细介绍。Tinyalsa提供了一套完整的pcm逻辑设备用户接口,比如pcm_open()、pcm_start()、pcm_prepare()、pcm_read()、pcm_write()。

2.2. PCM逻辑设备文件操作函数集:snd_pcm_f_ops[]

PCM逻辑设备文件操作函数集对于Playback和Capture是分开定义的,该操作函数集如下:

const struct file_operations snd_pcm_f_ops[2] = {{.owner =     THIS_MODULE,.write =       snd_pcm_write,.aio_write =     snd_pcm_aio_write,.open =          snd_pcm_playback_open,.release =       snd_pcm_release,.llseek =      no_llseek,.poll =          snd_pcm_playback_poll,.unlocked_ioctl =    snd_pcm_playback_ioctl,.compat_ioctl =     snd_pcm_ioctl_compat,....},{.owner =       THIS_MODULE,.read =            snd_pcm_read,.aio_read =       snd_pcm_aio_read,.open =           snd_pcm_capture_open,.release =        snd_pcm_release,.llseek =      no_llseek,.poll =          snd_pcm_capture_poll,.unlocked_ioctl = snd_pcm_capture_ioctl,.compat_ioctl =  snd_pcm_ioctl_compat,......}
};

2.3. Open PCM逻辑设备

User Space:应用程序通过pcm_open()直接呼叫系统调用open()打开pcmCxDxp或pcmCxDxc

// ./external/tinyalsa/pcm.c, line 851
struct pcm *pcm_open(unsigned int card, unsigned int device, unsigned int flags, struct pcm_config *config)
{......snprintf(fn, sizeof(fn), "/dev/snd/pcmC%uD%u%c", card, device, flags & PCM_IN ? 'c' : 'p');pcm->flags = flags;pcm->fd = open(fn, O_RDWR);......
}

Kernel Space:应用层呼叫的系统调用open()到内核空间就是调用PCM逻辑设备的snd_pcm_f_ops.open文件操作函数

// ./kernel-3.10/sound/core/pcm_native.c, line 2131
static int snd_pcm_playback_open(struct inode *inode, struct file *file)
{|--> //./kernel-3.10/sound/core/pcm_native.c, line 2159| snd_pcm_open(file, pcm, SNDRV_PCM_STREAM_PLAYBACK);|--> //./kernel-3.10/sound/core/pcm_native.c, line 2104| snd_pcm_open_file(file, pcm, stream);|--> //./kernel-3.10/sound/core/pcm_native.c, line 2062| snd_pcm_open_substream(pcm, stream, file, &substream);|--> //./kernel-3.10/sound/core/soc-pcm.c, line 132| substream->ops->open(substream); //即soc_pcm_open(), 在soc_new_pcm()函数中配置的|--> cpu_dai->driver->ops->startup()|--> platform->driver->ops->open()|--> codec_dai->driver->ops->startup()|--> rtd->dai_link->ops->startup()
}

2.4. Write/Read PCM逻辑设备

在我的源码包里,tinyalsa write PCM逻辑设备是通过 ioctl() 函数完成的,即应用程序将需要播放的音频数据通过pcm_write() --> ioctl() 传递到内核。

User Space:应用程序write PCM逻辑设备是通过pcm_write() --> ioctl() 来完成的

// ./external/tinyalsa/pcm.c, line 483
int pcm_write(struct pcm *pcm, const void *data, unsigned int count)
{......ioctl(pcm->fd, SNDRV_PCM_IOCTL_WRITEI_FRAMES, &x)......
}

Kernel Space:应用层呼叫的系统调用ioctl()到内核空间就是调用PCM逻辑设备的snd_pcm_f_ops.ioctl文件操作函数

// ./kernel-3.10/sound/core/pcm_native.c, line 2784
static long snd_pcm_playback_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{|--> //./kernel-3.10/sound/core/pcm_native.c, line 2624| snd_pcm_playback_ioctl1(file, pcm_file->substream, cmd, (void __user *)arg);|--> //./kernel-3.10/sound/core/pcm_lib.c, line 2101| snd_pcm_lib_write(substream, xferi.buf, xferi.frames);|--> //./kernel-3.10/sound/core/pcm_lib.c, line 1985| snd_pcm_lib_write1(pcm, stream, file, &substream);|--> //./kernel-3.10/sound/core/pcm_lib.c, line 1962| transfer(substream); //即snd_pcm_lib_write_transfer(). snd_pcm_lib_write1()函数的最后一个入参|--> hwbuf = runtime->dma_area + frames_to_bytes(runtime, hwoff);| //内存和DMA Buffer之间的数据传递, 循环搬送直到播放完毕|--> copy_from_user(hwbuf, buf, frames_to_bytes(runtime, frames)) |--> //./kernel-3.10/sound/core/pcm_native.c, line 904 | snd_pcm_start() //启动DMA传输(只是在开始时,调用一次)|--> //./kernel-3.10/sound/core/pcm_native.c, line 785 | snd_pcm_action(&snd_pcm_action_start, substream, SNDRV_PCM_STATE_RUNNING);|--> //./kernel-3.10/sound/core/pcm_native.c, line 716 or line 765| snd_pcm_action_group()/snd_pcm_action_single()|--> //ops为snd_pcm_action()函数第一个入参| res = ops->do_action(substream, state); //snd_pcm_action_start.do_action(), snd_pcm_do_start()|--> //./kernel-3.10/sound/core/soc-pcm.c, line 609| substream->ops->trigger(); //即soc_pcm_trigger(), 在soc_new_pcm()函数中配置的|--> codec_dai->driver->ops->trigger()|--> platform->driver->ops->trigger()|--> cpu_dai->driver->ops->trigger()
}

snd_pcm_action()的第一个入参很重要,write PCM逻辑设备时,snd_pcm_action()函数的第一个入参为struct action_ops snd_pcm_action_start。

细心读者可能已经注意到,PCM逻辑设备提供了snd_pcm_f_ops[0].write()文件操作函数,那为什么应用程序不是通过系统调用 write()来写音频数据给内核呢 ?
这个问题,我自己的思考:
比较一下PCM逻辑设备的write()和ioctl(),可以发现snd_pcm_write()函数是snd_pcm_playback_ioctl()函数的简化版。
对于单声道音频数据,应用程序使用系统调用write()就可以满足需求。
对于多声道音频数据,应用程序要使用系统调用ioctl()。在多声道时,snd_pcm_playback_ioctl()会调用snd_pcm_lib_writev()。

关于read PCM逻辑设备,应用程序也是通过系统调用 ioctl() 完成的,到内核空间后调用snd_pcm_capture_ioctl()。code flow 和 write非常相似,这里不再赘述。

2.5. Close PCM逻辑设备

User Space:应用程序通过pcm_close() --> pcm_stop() --> ioctl()关闭pcmCxDxp或pcmCxDxc

// ./external/tinyalsa/pcm.c, line 823
int pcm_close(struct pcm *pcm)
{......pcm_stop(pcm);|--> ioctl(pcm->fd, SNDRV_PCM_IOCTL_DROP)......
}

Kernel Space:应用层呼叫的系统调用ioctl()到内核空间就是调用PCM逻辑设备的snd_pcm_f_ops.ioctl文件操作函数

// ./kernel-3.10/sound/core/pcm_native.c, line 2784
static long snd_pcm_playback_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{|--> //./kernel-3.10/sound/core/pcm_native.c, line 2624| snd_pcm_playback_ioctl1(file, pcm, SNDRV_PCM_STREAM_PLAYBACK);|--> //./kernel-3.10/sound/core/pcm_native.c, line 2556| snd_pcm_common_ioctl1(file, substream, cmd, arg);|--> //./kernel-3.10/sound/core/pcm_native.c, line 1565| snd_pcm_drop(substream);|--> //./kernel-3.10/sound/core/pcm_native.c, line 959| snd_pcm_stop(substream, SNDRV_PCM_STATE_SETUP);|--> //./kernel-3.10/sound/core/pcm_native.c, line 785| snd_pcm_action(&snd_pcm_action_stop, substream, state);|--> //./kernel-3.10/sound/core/pcm_native.c, line 716 or line 765| snd_pcm_action_group()/snd_pcm_action_single()|--> //ops为snd_pcm_action()函数第一个入参| res = ops->do_action(substream, state); //snd_pcm_action_stop.do_action(), snd_pcm_do_stop()|--> //./kernel-3.10/sound/core/soc-pcm.c, line 609| substream->ops->trigger(); //即soc_pcm_trigger(), 在soc_new_pcm()函数中配置的|--> codec_dai->driver->ops->trigger()|--> platform->driver->ops->trigger()|--> cpu_dai->driver->ops->trigger()
}

snd_pcm_action()的第一个入参很重要,close PCM逻辑设备时,snd_pcm_action()函数的第一个入参为struct action_ops snd_pcm_action_stop。

注:请注意区分write/close PCM逻辑设备时snd_pcm_action()的第一个入参。

3. 结束语

本文重点介绍了对PCM逻辑设备open、write、close时的code flow。为了直观展示code flow,忽略了音频数据传递的细节。
下一篇文章Linux 音频驱动(六) ALSA音频驱动之PCM Write数据传递过程将重点介绍音频数据PCM data的传递细节。

Linux 音频驱动(五) ALSA音频驱动之PCM逻辑设备相关推荐

  1. Linux(RedHat9.0)下Alsa声卡驱动的安装

    自从装来RedHat9.0就没有听到过声音,也不知道安装了多少次声卡驱动,每种都试验过,都没有成功过,今天终于安装成功,需要的朋友可以参考一下. 首先查看一下,系统声音是否打开,有些朋友电脑没有声音, ...

  2. linux音频框架分析,Alsa音频子系统Codec---al5623.c内核代码框架分析

    驱动代码位于: sound/soc/codec/alc5623.c 随便找个Linux内核都会有. 1.首先进行i2c总线驱动加载在: static int __init alc5623_modini ...

  3. Linux ALSA音频子系统二

    From 本文以高通平台为例,介绍一下android下的音频结构.android使用的是tinyALSA作为音频系统,使用方法和基本框架与linux中常用的ALSA音频子系统是一致的. ALSA音频框 ...

  4. Linux ALSA音频驱动之一:框架概述

    1.ALSA概述 ALSA表示高级Linux声音体系结构(Advanced Linux Sound Architecture).它由一系列内核驱动,应用程序编译接口(API)以及支持Linux下声音的 ...

  5. Linux ALSA音频驱动一:音频系统概述

    音频系统概述 音频系统通过数据+控制总线与CODEC连接,控制通路用I2C,数据通常为I2S,框图如图1所示. I2C:寄存器读写,用于配置CODEC控制通路. I2S:音频数据传输,通常与platf ...

  6. Linux ALSA音频框架分析五:HDA Driver分析

    Linux ALSA音频框架分析五:HDA Driver分析 一 概述 HDA(High Definition Audio)是intel设计的用来取代AC97的音频标准,硬件架构上由hda dodec ...

  7. Linux 音频驱动(一) ASoC音频框架简介

    目录 1. ALSA简介 2. ASoC音频驱动构成 3. PCM数据流 4. 数据结构简介 5. ASoC音频驱动注册流程 1. ALSA简介 Native ALSA Application:tin ...

  8. Linux 音频驱动(四) ASoC音频驱动之Machine驱动

    目录 1. 基本介绍 2. 源码分析 2.1. Machine数据结构 struct snd_soc_dai_link 3. 声卡 3.1. 数据结构struct snd_soc_card 3.2. ...

  9. Linux 音频驱动(三) ASoC音频驱动之Codec驱动

    目录 1. 简介 2. 源码分析 2.1. Codec 2.1.1. 数据结构struct snd_soc_codec_driver 2.1.2. 注册Codec 2.2. Codec DAI 2.2 ...

最新文章

  1. flask 学习实战项目实例
  2. mysql 判断表或字段存不存在
  3. malloc 初始化_关于内存分配malloc、calloc、realloc的区别
  4. HDU 1301 Jungle Roads(裸最小生成树)
  5. java request获取文件_request获取路径方式
  6. HDU 1222 Wolf and Rabbit
  7. 小米手机 - Charles无法安装证书 因为无法读取证书
  8. 顶点 java笔试_网易2014校园招聘杭州Java笔试题--题解--第一天
  9. ❤️缓存集合(一级缓存、二级缓存、缓存原理以及自定义缓存—源码+图文分析,建议收藏) ❤️
  10. Automatic Judge
  11. vue-watch : 深度监控的语法格式--检测数据的tabledata这个数组的变化
  12. Kafka日志清除策略
  13. Python + Selenium 自动发布文章(一):开源中国
  14. 基于Android系统手机通讯录管理软件的设计与开发
  15. 拼多多怎么查看订单详情|盛天海科技
  16. 浏览器点击复制内容并打开微信
  17. matlab中tdma源程序,40MF-TDMA系统中多用户多业务的无线接入控制和时隙分配算法MATLAB源代码...
  18. 十位数连加 c语言,用C语言编写一个简易计算器可实现加减乘除,连加连减,连乖连除....
  19. 一、CSS定位布局[相对定位、绝对定位、固定定位]
  20. 2021年汽车传感器行业研究报告

热门文章

  1. 手动搭建Hadoop环境
  2. 通过guest账户无法进入远程计算机,小经验|guest远程关机设置
  3. 前端框架比比看:bootstrap, bulma, foundation, material-design-lite, materialize-css, semantic-ui
  4. 10.3国庆作业(UART实验)
  5. java工程师的工作述职报告,java程序员述职报告
  6. VMware的 Mount虚拟光驱问题
  7. 怎么用matlab对图片缩小,使用Matlab实现对图片的缩放
  8. 如何让网站HTTPS评级为A或者A+
  9. openvas linux/window漏洞扫描和整改
  10. python画名字廖华兴_python第二周基本图形绘制