Linux音频子系统(3) - ALSA ASoC

1.概述

  ASoC是Alsa System on Chip的缩写,用于实现那些集成声音控制器的CPU,它的设计目标如下:

  • 解耦codec, codec的驱动不依赖具体的平台。
  • 简单易用的I2S/PCM配置接口,让soc和codec的配置相匹配。
  • 动态的电源管理DAPM,实现对用户空间透明的电源管理,各个widget按需供电,实现功耗最小化。
  • 消除pop音,控制各个widget上下电的顺序消除pop音。
  • 添加平台相关的控制,如earphone, speaker。

2.ASoC架构
在这里插入图片描述
  以播放为例, 在这样一个硬件结构下, 涉及到几个模块:

  • DMA : 负责把用户空间的音频数据搬移至I2S的FIFO.

  • I2S : 负责以某个采样频率、采样深度、通道数发送音频数据, 也叫dai (Digital Audio Interface).

  • AFIx : 负责以某个采样频率、采样深度、通道数接收音频数据, 也称作dai.

  • DAC : 并把数据通过DAC转换后送给耳机等播放.

  为了解决复用性问题, 内核引入了ASoC架构,在底层ASoC抽象了如下三个模块:

  • Platform : 该模块负责DMA的控制和I2S的控制, 由CPU厂商负责编写此部分代码.
  • Codec : 该模块负责AFIx的控制和DAC部分的控制(也可以说是芯片自身的功能的控制), 由Codec厂商负责编写此部分代码.
  • Machine : 用于描述一块电路板, 它指明此块电路板上用的是哪个Platform和哪个Codec, 由电路板商负责编写此部分代码.

  从数据结构的角度来说, ASoC核心层内部定义了如下数据结构, 注意它们是由核心层内部创建和维护的:

  • struct snd_soc_platform : 用于抽象一个platform, 作用是描述一个CPU的DMA设备及操作函数. 系统中可能有多个platforms, 它们都挂载在全局链表头static LIST_HEAD(platform_list)下面, 不同的platform以name区分.

  • struct snd_soc_codec : 用于抽象一颗Codec. 一颗Codec可能会有多个dai接口, 该结构体的作用是描述与具体dai无关的、Codec内部的工作逻辑, 例如控件/微件/音频路由的描述信息、时钟配置、IO 控制等. 系统中可能有多个Codecs, 它们都挂载在全局链表头static LIST_HEAD(codec_list) 下面, 不同的Codec以name区分.

  • struct snd_soc_dai : 用于描述一个dai, 既可以是CPU侧的dai(I2S), 也可以是Codec侧的dai(AFIx). 一颗CPU可能有多个I2S, 一个Codec也可能有多个AFIx, 因此系统中会有很多个dai, 它们都挂载在全局链表头static LIST_HEAD(dai_list)下面, 不同的dai以name区分.

  从底层驱动的角度来说, ASoC定义了一些需要底层实现的interface以及相应的注册函数:

  • 针对DMA (platform): CPU厂商需要填充struct snd_soc_platform_driver 和struct snd_pcm_ops, 然后调用snd_soc_register_platform向ASoC核心层注册, 例如atmel-pcm-pdc.c.

  • 针对I2S (cpu_dai): CPU厂商需要填充struct snd_soc_dai_driver 和 struct snd_soc_dai_ops, 然后调用snd_soc_register_dai向ASoC核心层注册, 例如atmel_ssc_dai.c.

  • 针对Codec (codec_dai) : Codec厂商需要填充struct snd_soc_codec_driver(用于描述Codec内部工作逻辑)和struct snd_soc_dai_driver、struct snd_soc_dai_ops(用于描述AFIx), 然后调用snd_soc_register_codec向ASoC核心层注册, 例如wm9081.c.

  • 针对Machine (codec): 电路板商需要填充struct snd_soc_dai_link、struct snd_soc_ops, 然后准备一个struct snd_soc_card把dai_link包裹起来, 然后调用snd_soc_register_card注册此snd_soc_card. 例如atmel_wm8904.c.

  当底层调用了snd_soc_register_card时, ASoC核心层会从全局链表中找到dai_link指定的platform、cpu_dai、codec_dai、codec, 并建立一个struct snd_soc_pcm_runtime来保存这些对应关系. 然后核心层会snd_card_new创建声卡, snd_pcm_new创建pcm逻辑设备, 最后snd_card_register注册声卡. 此后, 用户空间就可以看到设备节点了. 当用户空间访问设备节点时, 最终由ASoC核心层响应, 核心层会通过snd_soc_pcm_runtime找到对应的platform、cpu_dai、codec_dai、codec, 并根据需要回调它们实现的接口函数.

  ASoC系统架构也在不停的演化, 从v4.18开始, 虽然ASoC还是划分为platform、cpu_dai、codec_dai、codec这几个模块, 但描述它们的数据结构发生了较大变化:

  • 首先, ASoC用统一的数据结构来描述platform和codec : 核心层用struct snd_soc_component替代原来的struct snd_soc_platform(描述platform)和struct snd_soc_codec(描述codec); 底层驱动则用struct snd_soc_component_driver替换原来的struct snd_soc_platform_driver和struct snd_soc_codec_driver.

  • 其次, 描述dai的数据结构虽然没有变化, 核心层依然用struct snd_soc_dai, 底层驱动依然用struct snd_soc_dai_driver、struct snd_soc_dai_ops. 但dai相关的数据结构被包裹到snd_soc_component下来统一管理了. 当底层驱动注册一个dai时, 核心层会创建一个snd_soc_component, 然后把dai挂载到这个数据结构下面.

  • 也就是说, 不管是platform、codec还是dai, 核心层现在都统一用struct snd_soc_component来管理了, 一个component对应一个模块. 因此原先的platform_list、codec_list、dai_list这3个表头都不存在了, 只有一个static LIST_HEAD(component_list)表头.

  • 最后, 核心层提供给底层的注册API也统一了, 不管是platform、codec还是dai, 现在都统一用devm_snd_soc_register_component / snd_soc_register_component注册即可.

  • Machine层面没有什么变化, machine驱动还是构建一个struct snd_soc_card然后调用snd_soc_register_card注册即可. 而且核心层在注册函数的实现逻辑上也没有太大变化.

ASoC架构如下图:
在这里插入图片描述
  ASoC对于Alsa来说,就是分别注册PCM/CONTROL类型的snd_device设备,并实现相应的操作方法集。图中DAI是数字音频接口,用于配置音频数据格式等。

  • Codec驱动向ASoC注册snd_soc_codec和snd_soc_dai设备。
  • Platform驱动向ASoC注册snd_soc_platform和snd_soc_dai设备。
  • Machine驱动通过snd_soc_dai_link绑定codec/dai/platform。

  Widget是各个组件内部的小单元。处在活动通路上电,不在活动通路下电。ASoC的DAPM 正是通过控制这些Widget的上下电达到动态电源管理的效果。

  • path描述与其它widget的连接关系。
  • event用于通知该widget的上下电状态。
  • power指示当前的上电状态。
  • control实现空间用户接口用于控制widget的音量/通路切换等。

  对驱动开者来说,就可以很好的解耦了:

  • codec驱动的开发者,实现codec的IO读写方法,描述DAI支持的数据格式/操作方法和Widget的连接关系就可以了;
  • soc芯片的驱动开发者,Platform实现snd_pcm的操作方法集和DAI的配置如操作 DMA,I2S/AC97/PCM的设定等;
  • 板级的开发者,描述Machine上codec与platform之间的总线连接, earphone/Speaker的布线情况就可以了。

2.1.struct snd_soc_card

include/sound/soc.h:
struct snd_soc_card {
    const char *name;
    const char *long_name;
    const char *driver_name;
    struct device *dev;
    struct snd_card *snd_card;       //card结构体,最终是要注册card
    struct module *owner;

    struct list_head list;
    struct mutex mutex;
    struct mutex dapm_mutex;

    bool instantiated;

    int (*probe)(struct snd_soc_card *card);
    int (*late_probe)(struct snd_soc_card *card);
    int (*remove)(struct snd_soc_card *card);

    /* the pre and post PM functions are used to do any PM work before and
     * after the codec and DAI's do any PM work. */
    int (*suspend_pre)(struct snd_soc_card *card);
    int (*suspend_post)(struct snd_soc_card *card);
    int (*resume_pre)(struct snd_soc_card *card);
    int (*resume_post)(struct snd_soc_card *card);

    /* callbacks */
    int (*set_bias_level)(struct snd_soc_card *,
                  struct snd_soc_dapm_context *dapm,
                  enum snd_soc_bias_level level);
    int (*set_bias_level_post)(struct snd_soc_card *,
                   struct snd_soc_dapm_context *dapm,
                   enum snd_soc_bias_level level);

    long pmdown_time;

    /* CPU <--> Codec DAI links  */----------------附属部件链接表
    struct snd_soc_dai_link *dai_link;
    int num_links;
    struct snd_soc_pcm_runtime *rtd;
    int num_rtd;

    /* optional codec specific configuration */
    struct snd_soc_codec_conf *codec_conf;
    int num_configs;

    /*
     * optional auxiliary devices such as amplifiers or codecs with DAI
     * link unused
     */
    struct snd_soc_aux_dev *aux_dev;
    int num_aux_devs;
    struct snd_soc_pcm_runtime *rtd_aux;
    int num_aux_rtd;

    const struct snd_kcontrol_new *controls;
    int num_controls;

    /*
     * Card-specific routes and widgets.----------------------DAPM内容
     */
    const struct snd_soc_dapm_widget *dapm_widgets;
    int num_dapm_widgets;
    const struct snd_soc_dapm_route *dapm_routes;
    int num_dapm_routes;
    bool fully_routed;

    struct work_struct deferred_resume_work;

    /* lists of probed devices belonging to this card */--------附属部件链表
    struct list_head codec_dev_list;
    struct list_head platform_dev_list;
    struct list_head dai_dev_list;

    struct list_head widgets;
    struct list_head paths;
    struct list_head dapm_list;
    struct list_head dapm_dirty;

    /* Generic DAPM context for the card */---------------DAPM内容
    struct snd_soc_dapm_context dapm;
    struct snd_soc_dapm_stats dapm_stats;

#ifdef CONFIG_DEBUG_FS
    struct dentry *debugfs_card_root;
    struct dentry *debugfs_pop_time;
#endif
    u32 pop_time;

    void *drvdata;
};

2.2.struct snd_card

  struct snd_card可以说是整个ALSA音频驱动最顶层的一个结构, 整个声卡的软件逻辑结构开始于该结构, 几乎所有与声音相关的逻辑设备都是在snd_card的管理之下,声卡驱动的第一个动作通常就是创建一个snd_card结构体。

include/sound/core.h:
/* main structure for soundcard */    
struct snd_card {  
    int number;         /* number of soundcard (index to 
                                snd_cards) */  
  
    char id[16];            /* id string of this card */  
    char driver[16];        /* driver name */  
    char shortname[32];     /* short name of this soundcard */  
    char longname[80];      /* name of this soundcard */  
    char mixername[80];     /* mixer name */  
    char components[128];       /* card components delimited with 
                                space */  
    struct module *module;      /* top-level module */  
  
    void *private_data;     /* private data for soundcard */  
    void (*private_free) (struct snd_card *card); /* callback for freeing of 
                                private data */  
    struct list_head devices;   /* devices */  
  
    unsigned int last_numid;    /* last used numeric ID */  
    struct rw_semaphore controls_rwsem; /* controls list lock */  
    rwlock_t ctl_files_rwlock;  /* ctl_files list lock */  
    int controls_count;     /* count of all controls */  
    int user_ctl_count;     /* count of all user controls */  
    struct list_head controls;  /* all controls for this card */  
    struct list_head ctl_files; /* active control files */  
  
    struct snd_info_entry *proc_root;   /* root for soundcard specific files */  
    struct snd_info_entry *proc_id; /* the card id */  
    struct proc_dir_entry *proc_root_link;  /* number link to real id */  
  
    struct list_head files_list;    /* all files associated to this card */  
    struct snd_shutdown_f_ops *s_f_ops; /* file operations in the shutdown 
                                state */  
    spinlock_t files_lock;      /* lock the files for this card */  
    int shutdown;           /* this card is going down */  
    int free_on_last_close;     /* free in context of file_release */  
    wait_queue_head_t shutdown_sleep;  
    struct device *dev;     /* device assigned to this card */  
#ifndef CONFIG_SYSFS_DEPRECATED  
    struct device *card_dev;    /* cardX object for sysfs */  
#endif  
 
#ifdef CONFIG_PM  
    unsigned int power_state;   /* power state */  
    struct mutex power_lock;    /* power lock */  
    wait_queue_head_t power_sleep;  
#endif  
 
#if defined(CONFIG_SND_MIXER_OSS) || defined(CONFIG_SND_MIXER_OSS_MODULE)  
    struct snd_mixer_oss *mixer_oss;  
    int mixer_oss_change_count;  
#endif  
};  
  • struct list_head devices 记录该声卡下所有逻辑设备的链表
  • struct list_head controls 记录该声卡下所有的控制单元的链表
  • void *private_data 声卡的私有数据,可以在创建声卡时通过参数指定数据的大小

2.3.card注册

  通常有两种方法注册一个card:

  • 在machine驱动中注册一个名为 “soc-audio” 的 platform device,并将 struct snd_soc_card 结构变量作为这个device的私有数据,在 soc-core.c 调用 名为 “soc-audio” 的 platform drv的probe函数,probe中调用 snd_soc_register_card 注册这个card.

  • 直接在machine驱动中调用 snd_soc_register_card 函数注册card.

snd_soc_register_card流程如下:
在这里插入图片描述

  • A:绑定系统中相关的platform/cedec
  • B:声卡对象的创建
  • C:声卡注册
  • control:这是control相关的一些初始化函数

2.4.逻辑设备创建与注册

  struct snd_device 隶属于card, 通常用来实现某一功能,一个逻辑设备一般来说会对应用户空间的一个或多个设备节点(也有某些仅在内核使用的逻辑设备不会创建设备节点)。

  • struct snd_device:
struct snd_device {
	struct list_head list;		/* list of registered devices */
	struct snd_card *card;		/* card which holds this device */
	enum snd_device_state state;	/* state of the device */
	enum snd_device_type type;	/* device type */
	void *device_data;		/* device structure */
	struct snd_device_ops *ops;	/* operations */
};
  • struct snd_device_ops

  每个逻辑设备都有一个对应的snd_device_ops, 在新增一个逻辑设备时, 需要为此设备准备好该数据结构。

struct snd_device_ops {
	int (*dev_free)(struct snd_device *dev);
	int (*dev_register)(struct snd_device *dev);
	int (*dev_disconnect)(struct snd_device *dev);
};

2.4.1.API

  • snd_device_new
    该API用于创建一个逻辑设备,主要内容是分配一个struct snd_device空间, 初始化相关字段, 并把该逻辑设备添加到card->devices链表下。
  • snd_device_register
    一般在注册声卡时(snd_card_register)会自动调用此处的API; 不过也可以在card注册完毕后, 在手动调用此API注册一个新的逻辑设备。、
  • snd_device_register_all
    相当于对snd_device_register的封装: 针对card下的每一个逻辑设备, 调用__snd_device_register注册此逻辑设备。
  • snd_device_disconnect
    一般在调用snd_card_disconnect时会调用此API.

2.4.2. 字符设备驱动的创建

  逻辑设备与用户空间是通过字符设备驱动交互的。一个逻辑设备可以创建一个或多个字符设备节点,节点创建的时机如下:

  当声卡注册时(snd_card_register), 会针对其下的每一个逻辑设备调用snd_device_register,后者会回调逻辑设备的回调函数(snd_device_ops->dev_register)。逻辑设备在实现dev_register时, 一般会调用snd_register_device, 后者会通过device_add创建一个设备节点. 所以调用几次snd_register_device, 就会存在几个设备节点。

  所有ALSA字符设备的主设备号都是CONFIG_SND_MAJOR (116), ALSA系统在初始化时, 会在alsa_sound_init中调用register_chrdev,定义此类字符设备的统一处理函数snd_fops。snd_fops中只实现了snd_open函数, 当用户空间打开任一ALSA设备节点时, 都会进入到该open函数中。

  在snd_open中, 会根据次设备号(每个逻辑设备都有自己的次设备号), 选择对应的逻辑设备的ops函数, 然后替换file->f_op, 此后用户空间的任何操作都是直接与逻辑设备的ops函数交互的。ALSA系统中有一个全局数组snd_minors[], 数组的下标就是次设备号, 每个元素对应一个逻辑设备, snd_minor[i]-> f_ops存储的就是逻辑设备自己的ops函数.有了它, snd_open的实现就比较容易了, 只需用次设备号作为下标, 从snd_minors[]中找到对应的元素, 然后用元素的f_ops替换file->f_op即可.

2.4.3.逻辑设备中间层

  在当前的系统中, 有很多逻辑设备中间层, 它们相当于对《逻辑设备创建与注册》中的步骤进行了封装, 然后对外提供了更加简洁的API来创建对应的逻辑设备。
在这里插入图片描述
  除了Info设备外, 其它几个中间层都是对snd_device_new进行了一次封装. 当调用这些中间层提供的API时, 最终会在card下添加一个新的逻辑设备。

  在card的注册阶段, 会扫描它下面的每一个逻辑设备, 然后调用snd_device_register来注册该逻辑设备. 当逻辑设备注册完成后, 核心层会回调snd_device_ops.dev_register函数, 如果dev_register里面调用了snd_register_device, 则会在用户空间出现字符设备节点.

重点介绍:PCM逻辑设备

  • PCM逻辑设备是以字符设备的形式呈现给用户空间;
  • 内核实现了一个‘PCM中间层’, 该中间层对上向ALSA核心层注册, 将自己注册为一个逻辑设备, 并最终创建了字符设备节点, 负责与用户空间交互(snd_pcm_f_ops); 对下则提供API给底层驱动, 供底层驱动注册一个PCM设备.

  从功能的角度来说, PCM逻辑设备的作用是供用户空间播放/录制PCM音频. PCM音频本质上来说就是一块Buffer, ‘PCM中间层’的作用就是从用户空间接收这块Buffer, 然后把Buffer的数据转给底层驱动播放. 底层驱动一般对应的是I2S控制器, 它收到数据后, 会通过I2S总线把数据传给codec, 然后codec经过DA转换后送到喇叭播放.

  除了Buffer的传输, 还要解决配置的问题, 也就是配置I2S控制器和Codec芯片, 告诉它们应该按照什么样的clock、采样深度、通道数等来播放Buffer中的数据. 因此‘PCM中间层’也提供了一些ioctl给用户空间, 供它们设置这些参数.

  从分工的角度来说, PCM中间层和底层驱动各自应该专注于哪些事情?

  PCM中间层主要完成一些公共性的事情, 这些事情一般与具体的硬件无关(例如存储音频数据时, 肯定需要一个Buffer, 这个Buffer很显然用环形缓冲区描述比较好, 怎么管理这个环形缓冲区的读写指针? 另外, 用户空间设置的音频参数要检查其合法性. 还有, 用户空间可能启停音频播放/录制, 这意味着我们最好实现一个状态机, 来管理用户空间的切换操作. 等等). 对于与具体硬件相关的操作, PCM中间层会pass给底层驱动去处理(这就意味着PCM中间层要定义interface, 让底层驱动去实现这些interface).

  底层驱动则处理与具体硬件相关的事情, 例如I2S控制器的配置, Codec芯片的配置, 时钟的初始化等等.

refer to

  • http://www.mysixue.com/?p=134
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值