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

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

声明: 本博客内容均由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函数。这三个函数均是驱动定义的。

  • 20
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 为了帮助您更好地理解 tinyalsa 中 pcm.c 文件中的 pcm_open 函数调用到驱动层的流程,我已经为您创建了一张 UML 图。 请注意,为了保证信息的完整性,我在 UML 图中包含了 pcm.c 文件中的几个关键函数: ``` +----------------+ | | | pcm_open | | | +-------+--------+ | | v +-------+--------+ | | | pcm_native_open | | | +-------+--------+ | | v +-------+--------+ | | | snd_pcm_open | | | +----------------+ ``` 其中,pcm_open 是 pcm.c 文件中的函数,pcm_native_open 和 snd_pcm_open 分别是驱动层中的函数。 请注意,UML 图中的箭头表示函数的调用关系。例如,pcm_open 调用了 pcm_native_open 函数,pcm_native_open 函数又调用了 snd_pcm_open 函数。 ### 回答2: 在绘制tinyalsa pcm.c中pcm_open函数调用到驱动层的流程图时,需要先了解pcm_open函数的具体实现逻辑。 pcm_open函数主要负责打开PCM设备并进行初始化设置。具体流程如下: 1. 检查PCM设备是否已经打开,如果已经打开则返回错误代码。 2. 检查PCM设备类型是否支持,在PCM设备类型不支持时返回错误代码。 3. 根据给定的PCM设备类型,初始化相应的pcm设备描述符。 4. 设置设备的采样参数,如采样率、声道数、数据格式等。 5. 打开PCM设备,并通过ioctl系统调用与设备驱动程序进行交互,传递初始化参数。 6. 检查打开设备是否成功,如果失败则返回错误代码。 7. 将已打开的PCM设备信息保存到pcm设备描述符结构体中,并返回该结构体指针。 根据以上流程,我们可以绘制出如下的流程图: ``` 开始 -> 检查PCM设备是否已打开? -> 是 -> 返回错误代码 | | 否 否 | | 检查PCM设备类型是否支持? -> 是 -> 初始化pcm设备描述符 | | 否 否 | | 返回错误代码 设置设备采样参数 | | 初始化pcm设备描述符 | | | 打开PCM设备 打开PCM设备失败? | | 结束 <- 是 <- 返回错误代码 | 返回pcm设备描述符指针 ``` 在上述流程图中,箭头表示流程的前后关系,圆角矩形表示判断和决策点,矩形表示具体操作步骤,菱形表示判断条件。流程图可以更清晰地展示函数的执行流程和判断逻辑,有助于我们更好地理解和调试代码。 ### 回答3: UML(Unified Modeling Language)是一种用于软件工程的建模语言,可以用于描述系统的结构、行为和交互。在绘制tinyalsa pcm.c中的pcm_open函数调用到驱动层的流程图时,我们可以使用UML中的活动图(Activity Diagram)来表示函数调用的流程。 活动图的基本元素包括活动(Action)、控制流(Control Flow)和决策(Decision)等。以下是使用活动图来描述pcm_open函数调用到驱动层的流程: 1. 开始:表示函数调用的起点。 2. 进入pcm_open函数:表示开始执行pcm_open函数。 3. 执行初始化:在pcm_open函数中进行与pcm设备的初始化操作,例如设置设备参数。 4. 打开pcm设备:通过驱动程序打开pcm设备。 5. 发送配置信息:将配置信息发送给驱动程序。 6. 检查打开状态:检查驱动程序是否成功打开pcm设备。 7. 打开成功:如果打开成功,执行下一步;否则,执行错误处理。 8. 设置流模式:根据配置信息设置pcm设备的流模式,例如录音还是播放。 9. 开始数据传输:开始进行数据的传输。 10. 结束数据传输:数据传输完成后结束传输。 11. 关闭pcm设备:关闭已打开的pcm设备。 12. 结束:表示函数调用的终点。 上述流程图描述了pcm_open函数调用到驱动层的主要步骤和执行顺序。在实际绘制活动图时,可以根据具体情况增加细节和标注,更面地表示函数调用的流程
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

芯心智库

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值