【TinyALSA全解析(四)】扩展篇-从TinyALSA到底层音频驱动的全流程分析

news2025/4/7 11:01:54

扩展篇-从TinyALSA到底层音频驱动的全流程分析

  • 第一节 本文说明
  • 第二节 声卡驱动统一入口进行ops替换过程
    • 2.1 tinyalsa到Linux kernel
    • 2.2 Linux Kernel中,由主设备号ops分流到次设备号ops
  • 第三节 次设备中file_operations的open函数
    • 3.1 本节主要内容
    • 3.2 为何次设备的file_operations是snd_pcm_f_ops?
    • 3.3 snd_pcm_playback_open函数访问底层

/*****************************************************************************************************************/

声明: 本博客内容均由https://blog.csdn.net/weixin_47702410原创,转载or引用请注明出处,谢谢!

创作不易,如果文章对你有帮助,麻烦点赞 收藏支持~感谢

/*****************************************************************************************************************/

第一节 本文说明

本文讲一下从HAL层的TinyALSA调到ASOC的platform、codec、machine的完整流程,也就是如何从Tinyalsa调到驱动程序中。

学习本文需要提前掌握ASOC架构、声卡注册过程、ALSA结构体的关系等知识,基础内容不逐个介绍(不然讲不完,后续再出相关教程),就以Tinyalsa中的pcm_open为例讲解。参考的安卓版本为Android 12_r8官方源码,Linux版本为Linux Kernel 5.10官方源码

本文主要内容也可见下面的简图:
本文主要内容

第二节 声卡驱动统一入口进行ops替换过程

2.1 tinyalsa到Linux kernel

当我们open的设备类似为非ALSA plug的时候,实际上调用的open函数应该是pcm_hw_open函数。

// file path :  external/tinyalsa/pcm.c
struct pcm *pcm_open(unsigned int card, unsigned int device,
                     unsigned int flags, struct pcm_config *config)
{
    //...
    pcm->fd = pcm->ops->open(card, device, flags, &pcm->data, pcm->snd_node);
    //...
}

//open非ALSA plug时,pcm->ops->open函数就是pcm_hw_open函数
//file path : external/tinyalsa/pcm_hw.c
static int pcm_hw_open(unsigned int card, unsigned int device,
                unsigned int flags, void **data,
                __attribute__((unused)) void *node)
{
//...
    snprintf(fn, sizeof(fn), "/dev/snd/pcmC%uD%u%c", card, device,
             flags & PCM_IN ? 'c' : 'p');
    fd = open(fn, O_RDWR|O_NONBLOCK);
//...
}

在Linux设备中,音频节点的主设备号都是116,可以用CMD查看这个音频节点"/dev/snd/pcmC%uD%u%c",例如:

# ls -l /dev/snd/pcmC0D0p
crw-rw---- 1 system audio 116,   2 20xx-xx-xx xx:xx /dev/snd/pcmC0D0p

可以见到主设备号是116,次设备号是2
由此需要找Kernel中主设备号为116的设备,看看这个设备的ops是怎么样操作的

2.2 Linux Kernel中,由主设备号ops分流到次设备号ops

在Linux Kernel 5.10中,主设备号为116的设备是如下注册的:

//file path : sound\core\sound.c
static int __init alsa_sound_init(void)
{
    snd_major = major;
    snd_ecards_limit = cards_limit;
    if (register_chrdev(major, "alsa", &snd_fops)) {
/*
这个主设备号major的定义是:
static int major = CONFIG_SND_MAJOR;
这个宏定义在下面:
#define CONFIG_SND_MAJOR    116    // standard configuration
*/
        pr_err("ALSA core: unable to register native major device number %d\n", major);
        return -EIO;
    }
    if (snd_info_init() < 0) {
        unregister_chrdev(major, "alsa");
        return -ENOMEM;
    }
#ifndef MODULE
    pr_info("Advanced Linux Sound Architecture Driver Initialized.\n");
#endif
    return 0;
}

那么对应的open就是:

//file path :sound\core\sound.c
static const struct file_operations snd_fops =
{
    .owner =    THIS_MODULE,
    .open =     snd_open,
    .llseek =   noop_llseek,
};

注意这个file_operations有open函数,但没有close、write、read函数的实现,这个是为什么?
因为后续会在snd_open函数中根据次设备号来替换这个原先的file_operations。

替换file_operations的过程如下:

//file path :sound\core\sound.c

static int snd_open(struct inode *inode, struct file *file)
{
    // 获取设备的次设备号
    unsigned int minor = iminor(inode);
    struct snd_minor *mptr = NULL;
    const struct file_operations *new_fops;
    int err = 0;
    // 检查次设备号是否在有效范围内
    if (minor >= ARRAY_SIZE(snd_minors))
        return -ENODEV;
    // 加锁,防止并发操作
    mutex_lock(&sound_mutex);
    // 获取对应次设备号的设备信息
    mptr = snd_minors[minor];
    // 如果设备信息为空,则尝试自动加载设备
    if (mptr == NULL) {
        mptr = autoload_device(minor);
        if (!mptr) {
            // 如果自动加载失败,则解锁并返回错误
            mutex_unlock(&sound_mutex);
            return -ENODEV;
        }
    }
    // 获取设备的文件操作函数集
    new_fops = fops_get(mptr->f_ops);
    // 解锁
    mutex_unlock(&sound_mutex);
    // 如果获取文件操作函数集失败,则返回错误
    if (!new_fops)
        return -ENODEV;
    // 替换file结构体中的文件操作函数集
    replace_fops(file, new_fops);
    // 如果设备的打开函数存在,则调用该函数打开设备
    if (file->f_op->open)
        err = file->f_op->open(inode, file);
    // 返回结果
    return err;
}

总结:声卡设备的open函数有统一的入口–snd_open函数,任何HW音频函数均是通过这个函数作为跳板访问硬件

但是问题来了,snd_open替换完file_operations后,紧跟着
file->f_op->open(inode, file);这个就是调用次设备的open,那么次设备的open函数在哪里呢?

第三节 次设备中file_operations的open函数

3.1 本节主要内容

先说结论:在写声卡驱动的时候,会调用snd_soc_register_card函数去注册一个声卡,snd_soc_register_card函数会填充snd_minors[minor]数组,最终主设备号在这个数组中取出相应次设备号的file_operations就完成了主设备号ops向次设备的ops转变。次设备号的ops是如下的内容,类似于主设备也是统一入口(这节的主要内容就是这个结论,需要熟悉声卡注册的流程,不懂或者不理解可以记住结论):

// file path: /sound/core/pcm_native.c
const struct file_operations snd_pcm_f_ops[2] = {
    {
        .owner =        THIS_MODULE,
        .write =        snd_pcm_write,
        .write_iter =       snd_pcm_writev,
        .open =         snd_pcm_playback_open,
        .release =      snd_pcm_release,
        .llseek =       no_llseek,
        .poll =         snd_pcm_poll,
        .unlocked_ioctl =   snd_pcm_ioctl,
        .compat_ioctl =     snd_pcm_ioctl_compat,
        .mmap =         snd_pcm_mmap,
        .fasync =       snd_pcm_fasync,
        .get_unmapped_area =    snd_pcm_get_unmapped_area,
    },
    {
        .owner =        THIS_MODULE,
        .read =         snd_pcm_read,
        .read_iter =        snd_pcm_readv,
        .open =         snd_pcm_capture_open,
        .release =      snd_pcm_release,
        .llseek =       no_llseek,
        .poll =         snd_pcm_poll,
        .unlocked_ioctl =   snd_pcm_ioctl,
        .compat_ioctl =     snd_pcm_ioctl_compat,
        .mmap =         snd_pcm_mmap,
        .fasync =       snd_pcm_fasync,
        .get_unmapped_area =    snd_pcm_get_unmapped_area,
    }
};

本节内容会先分析次设备的file_operations为何是snd_pcm_f_ops。

然后举例当执行open设备的时候,分析从snd_pcm_f_ops中的snd_pcm_playback_open函数如何进一步调度到底层的驱动函数。

3.2 为何次设备的file_operations是snd_pcm_f_ops?

这个要说一下声卡注册的流程了(后续会出一个系列讲声卡是如何加载上去的)。

首先声卡注册的函数是snd_soc_register_card函数,这个的函数的内容比较丰富,但对于本文需要掌握如下的调用流程:

snd_soc_register_card
    --snd_soc_bind_card
        --soc_init_pcm_runtime
            --soc_new_pcm
                --snd_pcm_new
                    --_snd_pcm_new
                        --.dev_register =snd_pcm_dev_register,// 定义设备操作函数集
        --snd_card_register
            --snd_device_register_all
                --__snd_device_register
                    --dev->ops->dev_register(dev);//创建dev_ops,调用的是snd_pcm_dev_register

综上,在声卡注册的时候,次设备的ops由snd_pcm_dev_register函数去创建,分析这个函数的内容:

//file path : /sound/core/pcm.c
static int snd_pcm_dev_register(struct snd_device *device)
{
    //...
    // 遍历PCM设备的所有流
    for (cidx = 0; cidx < 2; cidx++) {
        int devtype = -1;
        if (pcm->streams[cidx].substream == NULL)
            continue;
        switch (cidx) {
        case SNDRV_PCM_STREAM_PLAYBACK:
            devtype = SNDRV_DEVICE_TYPE_PCM_PLAYBACK;
            break;
        case SNDRV_PCM_STREAM_CAPTURE:
            devtype = SNDRV_DEVICE_TYPE_PCM_CAPTURE;
            break;
        }
        /* register pcm */
        err = snd_register_device(devtype, pcm->card, pcm->device,
                      &snd_pcm_f_ops[cidx], pcm,
                      &pcm->streams[cidx].dev);
        /* 注意这个参数!snd_pcm_f_ops[cidx]! */
        if (err < 0) {
            list_del_init(&pcm->list);
            goto unlock;
        }
        // 这里是初始化PCM设备的所有流的定时器
        for (substream = pcm->streams[cidx].substream; substream; substream = substream->next)
            snd_pcm_timer_init(substream);
    }
    //...
}

//这个传入的参数为:
//file path :/sound/core/pcm_native.c
const struct file_operations snd_pcm_f_ops[2] = {
    {
        .owner =        THIS_MODULE,
        .write =        snd_pcm_write,
        .write_iter =       snd_pcm_writev,
        .open =         snd_pcm_playback_open,
        .release =      snd_pcm_release,
        .llseek =       no_llseek,
        .poll =         snd_pcm_poll,
        .unlocked_ioctl =   snd_pcm_ioctl,
        .compat_ioctl =     snd_pcm_ioctl_compat,
        .mmap =         snd_pcm_mmap,
        .fasync =       snd_pcm_fasync,
        .get_unmapped_area =    snd_pcm_get_unmapped_area,
    },
    {
        .owner =        THIS_MODULE,
        .read =         snd_pcm_read,
        .read_iter =        snd_pcm_readv,
        .open =         snd_pcm_capture_open,
        .release =      snd_pcm_release,
        .llseek =       no_llseek,
        .poll =         snd_pcm_poll,
        .unlocked_ioctl =   snd_pcm_ioctl,
        .compat_ioctl =     snd_pcm_ioctl_compat,
        .mmap =         snd_pcm_mmap,
        .fasync =       snd_pcm_fasync,
        .get_unmapped_area =    snd_pcm_get_unmapped_area,
    }
};

上面有两个ops,一个是播放使用的,一个是录音使用的。

本文就以录音为例继续分析。录音调用的open函数是snd_pcm_playback_open函数。

3.3 snd_pcm_playback_open函数访问底层

snd_pcm_playback_open的调用流程如下:

snd_pcm_playback_open
    --snd_pcm_open
        --snd_pcm_open_file
            --snd_pcm_open_substream
                --substream->ops->open(substream)

分析到这个语句:substream->ops->open(substream),substream->ops是在注册声卡的时候被指定的,声卡注册的函数是snd_soc_register_card函数,对应流程如下:

snd_soc_register_card
    --snd_soc_bind_card
        --soc_init_pcm_runtime
            --soc_new_pcm:主要内容在这里,分析这个函数

//file path:/sound/soc/soc-pcm.c
int soc_new_pcm(struct snd_soc_pcm_runtime *rtd, int num)
{
    //...
    /* ASoC PCM operations */
    if (rtd->dai_link->dynamic) {
    // 使用了DAPM架构,用下面的ops
        rtd->ops.open       = dpcm_fe_dai_open;
        rtd->ops.hw_params  = dpcm_fe_dai_hw_params;
        rtd->ops.prepare    = dpcm_fe_dai_prepare;
        rtd->ops.trigger    = dpcm_fe_dai_trigger;
        rtd->ops.hw_free    = dpcm_fe_dai_hw_free;
        rtd->ops.close      = dpcm_fe_dai_close;
        rtd->ops.pointer    = soc_pcm_pointer;
    } else {
    //不使用DAPM架构,用下面的ops
        rtd->ops.open       = soc_pcm_open;
        rtd->ops.hw_params  = soc_pcm_hw_params;
        rtd->ops.prepare    = soc_pcm_prepare;
        rtd->ops.trigger    = soc_pcm_trigger;
        rtd->ops.hw_free    = soc_pcm_hw_free;
        rtd->ops.close      = soc_pcm_close;
        rtd->ops.pointer    = soc_pcm_pointer;
    }
    //...
    if (playback)
        //如果设备支持播放,设置substream ops为上述的ops
        snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &rtd->ops);

    if (capture)
        //如果设备支持录音,设置substream ops为上述的ops
        snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &rtd->ops);
    //...
}

//snd_pcm_set_ops的实现
void snd_pcm_set_ops(struct snd_pcm *pcm, int direction,
             const struct snd_pcm_ops *ops)
{
    struct snd_pcm_str *stream = &pcm->streams[direction];
    struct snd_pcm_substream *substream;
    
    for (substream = stream->substream; substream != NULL; substream = substream->next)
        //这里指定了substream->ops = ops
        substream->ops = ops;
}

综上可以知道:

如果使用了DAPM架构,则substream->ops->open就是dpcm_fe_dai_open,
如果不使用DAPM架构,则substream->ops->open就是soc_pcm_open。
可以关注一下,后续将会推出DAPM的详解,本文不讲述DAPM的相关知识。

以不使用DAPM架构为例:
substream->ops->open就是soc_pcm_open函数,对它进行分析:

soc_pcm_open
    --soc_pcm_components_open
        --snd_soc_component_open
            --component->driver->open  遍历component,如果有open调用open, 一般指的是CPU端的open函数
        --snd_soc_link_startup
            --rtd->dai_link->ops->startup  调用DAI_LINK的startup,位于ASOC的machine端
        --snd_soc_dai_startup
            --dai->driver->ops->startup   调用codec端的startup函数

总结,soc_pcm_open会依次调用了CPU端的open函数、machine端的startup函数、codec端的startup函数。这三个函数均是驱动定义的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1260017.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python生成exe文件

Python如何生成exe文件 在终端执行 pip install pyinstaller 在终端执行pyinstaller E:\fund_data\GetFund.py&#xff0c;运行结束后会在D:\Python\Python311\Scripts\dist\目录下生成GetFund.exe文件 3.双击exe文件运行&#xff0c;如果未出现预期结果&#xff0c;可以把e…

深度学习之图像分类(十五)DINAT: Dilated Neighborhood Attention Transformer详解(一)

Dilated Neighborhood Attention Transformer Abstract Transformers 迅速成为跨模态、领域和任务中应用最广泛的深度学习架构之一。在视觉领域&#xff0c;除了对普通Transformer的持续努力外&#xff0c;分层Transformer也因其性能和易于集成到现有框架中而受到重视。这些模…

NAS-DIP: Learning Deep Image Prior with Neural Architecture Search

NAS-DIP: 用神经结构搜索学习深度图像先验 论文链接&#xff1a;https://arxiv.org/abs/2008.11713 项目链接&#xff1a;https://github.com/YunChunChen/NAS-DIP-pytorch Abstract 最近的研究表明&#xff0c;深度卷积神经网络的结构可以用作解决各种逆图像恢复任务的结构…

CGAN原理讲解与源码

1.CGAN原理 生成器&#xff0c;输入的是c和z&#xff0c;z是随机噪声&#xff0c;c是条件&#xff0c;对应MNIST数据集&#xff0c;要求规定生成数字是几。 输出是生成的虚假图片。 判别器的输入是 1.生成器输出的虚假图片x; 2.对应图片的标签c 来自真实数据集&#xff0c;且…

大模型下交互式数据挖掘的探索与发现

在这个数据驱动的时代&#xff0c;数据挖掘已成为解锁信息宝库的关键。过去&#xff0c;我们依赖传统的拖拉拽方式来建模&#xff0c;这种方式在早期的数据探索中起到了作用&#xff0c;但随着数据量的激增和需求的多样化&#xff0c;它的局限性逐渐显露。 >>>> 首…

selenium已知一个元素定位同级别的另一个元素

1.需求与实际情况 看下图来举例 &#xff08;1&#xff09;需求 想点击test22&#xff08;即序号-第9行&#xff09;这一行中右边的“复制”这一按钮 &#xff08;2&#xff09;实际情况 只能通过id或者class定位到文件名这一列的元素&#xff0c;而操作这一列的元素是不…

知识变现的未来:解析知识付费系统的核心

随着数字时代的发展&#xff0c;知识付费系统作为一种新兴的学习和知识分享模式&#xff0c;正逐渐引领着知识变现的未来。本文将深入解析知识付费系统的核心技术&#xff0c;揭示其在知识经济时代的重要性和潜力。 1. 知识付费系统的基本架构 知识付费系统的核心在于其灵活…

为什么要用 Redis 而不用 map/guava 做缓存? Redis为什么这么快 Redis有哪些数据类型 Redis的应用场景

文章目录 为什么要用 Redis 而不用 map/guava 做缓存?Redis为什么这么快Redis有哪些数据类型Redis的应用场景总结一计数器缓存会话缓存全页缓存&#xff08;FPC&#xff09;查找表消息队列(发布/订阅功能)分布式锁实现 总结二 简单的聊聊Redis常见的一些疑问点&#xff1a;具体…

预算削减与经济动荡:2024 年明智且经济地创新

如何在经济衰退周期中保持创新&#xff1f;这篇创新研究提供了实用建议。在经济下行压力下领导者往往会试图降低成本和维持生存。然而&#xff0c;这种二元对立的压力往往会导致领导者做出不够理想的决策&#xff0c;更多地关注生存而不是未来投资。本文提供了一系列实用的建议…

蓝桥杯-平方和(599)

【题目】平方和 【通过测试】代码 import java.util.Scanner; import java.util.ArrayList; import java.util.List; // 1:无需package // 2: 类名必须Main, 不可修改public class Main {public static void main(String[] args) {Scanner scan = new Scanner(System.in);//在…

MATLAB中FFT频谱分析使用详解

文章目录 语法说明语法一&#xff1a;Y fft(X)fft(X)返回X长度的傅里叶变换 语法二&#xff1a;Y fft(X,N)如果 X的长度小于 N&#xff0c;则为 X补上尾零以达到长度 N(FFT插值)双边谱转换为单边谱 如果 X 的长度大于 N&#xff0c;则对 X 进行截断以达到长度 N。 语法三&…

根据密码构成规则检测密码字符串

从键盘输入密码字符串&#xff0c;程序根据给定密码构成规则检测并给出对应提示。 (笔记模板由python脚本于2023年11月27日 19:27:47创建&#xff0c;本篇笔记适合熟悉Python字符串str对象的coder翻阅) 【学习的细节是欢悦的历程】 Python 官网&#xff1a;https://www.python.…

ViLT 论文精读【论文精读】

ViLT 论文精读【论文精读】_哔哩哔哩_bilibili 目录 ViLT 论文精读【论文精读】_哔哩哔哩_bilibili 1 地位 2 ViLT做了什么能让它成为这种里程碑式的工作&#xff1f; 3 ViLT到底把模型简化到了什么程度&#xff1f;到底能加速到什么程度&#xff1f; 2.1 过去的方法是怎…

C++之算术生成算法

C之算术生成算法 accumulate #include<iostream> using namespace std; #include<vector> #include<numeric>void test() {vector<int> v;for (int i 0; i < 10; i){v.push_back(i);}int total accumulate(v.begin(), v.end(),0);cout << t…

论文公式工具

论文公式工具 https://www.latexlive.com/home## 论文图片识别公式网页工具&#xff0c;免费的方便但是有限制次数&#xff0c;一天只能用三次公式图片识别。 先注册登录 我们到论文中截取一张图片 在识别得到的一串码中&#xff0c;删掉前面没用的 输出为这个格式&#x…

从零构建属于自己的GPT系列1:预处理模块(逐行代码解读)、文本tokenizer化

1 训练数据 在本任务的训练数据中&#xff0c;我选择了金庸的15本小说&#xff0c;全部都是txt文件 数据打开后的样子 数据预处理需要做的事情就是使用huggingface的transformers包的tokenizer模块&#xff0c;将文本转化为token 最后生成的文件就是train_novel.pkl文件&a…

【MATLAB】LMD分解+FFT+HHT组合算法

有意向获取代码&#xff0c;请转文末观看代码获取方式~也可转原文链接获取~ 1 基本定义 LMDFFTHHT组合算法是一种基于局部均值分解&#xff08;LMD&#xff09;、快速傅里叶变换&#xff08;FFT&#xff09;和希尔伯特-黄变换&#xff08;HHT&#xff09;的组合算法。 LMD是…

什么是数据增强,为什么会让模型更健壮?

在做一些图像分类训练任务时&#xff0c;我们经常会遇到一个很尴尬的情况&#xff0c;那就是&#xff1a; 明明训练数据集中有很多可爱猫咪的照片&#xff0c;但是当我们给训练好的模型输入一张戴着头盔的猫咪进行测试时&#xff0c;模型就不认识了&#xff0c;或者说识别精度…

87基于matlab的双卡尔曼滤波算法

基于matlab的双卡尔曼滤波算法。第一步使用了卡尔曼滤波算法&#xff0c;用电池电压来修正SOC&#xff0c;然后将修正后的SOC作为第二个卡尔曼滤波算法的输入&#xff0c;对安时积分法得到的SOC进行修正&#xff0c;最终得到双卡尔曼滤波算法SOC估计值。结合EKF算法和安时积分法…

企业联系方式真的那么难获取吗?

企业联系方式的重要性&#xff0c;相信每一个销售人员都是知道的。对于销售来说&#xff0c;获取准确、全面的企业联系方式&#xff0c;无疑是开发客户的基础与保障&#xff0c;任凭能力再高&#xff0c;说服能力多强&#xff0c;没有与客户接触的机会&#xff0c;这些都是无稽…