一、概述
ALSA 是 Advanced Linux Sound Architecture 的缩写,目前已经成为了 Linux 的主流音频体系结构。它的主要特点有:① 支持多种声卡设备;② 模块化的内核驱动程序; ③ 支持 SMP 和多线程;④ 提供应用开发函数库(alsa-lib)以简化应用程序开发;⑤ 支持 OSS API,兼容 OSS 应用程序等。
在内核设备驱动层,ALSA 提供了 alsa-driver,同时在应用层,ALSA 为我们提供了 alsa-lib,应用程序只要调用 alsa-lib 提供的 API,即可以完成对底层音频硬件的控制。
图 1.1 alsa的软件体系结构
由图 1.1 可以看出,用户空间的 alsa-lib 对应用程序提供统一的 API 接口,这样可以隐藏了驱动层的实现细节,简化了应用程序的实现难度。内核空间中,alsa-soc 其实是对 alsa-driver 的进一步封装,他针对嵌入式设备提供了一系列增强的功能。
二、ALSA 设备文件结构
查看我们设备中的 ALSA 驱动的设备文件结构:
$ cd /dev/snd
$ ls -l
输出如下:
crw-rw----+ 1 root audio 116, 8 2011-02-23 21:38 controlC0
crw-rw----+ 1 root audio 116, 4 2011-02-23 21:38 midiC0D0
crw-rw----+ 1 root audio 116, 7 2011-02-23 21:39 pcmC0D0c
crw-rw----+ 1 root audio 116, 6 2011-02-23 21:56 pcmC0D0p
crw-rw----+ 1 root audio 116, 5 2011-02-23 21:38 pcmC0D1p
crw-rw----+ 1 root audio 116, 3 2011-02-23 21:38 seq
crw-rw----+ 1 root audio 116, 2 2011-02-23 21:38 timer
我们可以看到以下设备文件:
controlC0 --> 用于声卡的控制,例如通道选择,混音,麦克风的控制等
midiC0D0 --> 用于播放 midi 音频
pcmC0D0c --> 用于录音的 pcm 设备
pcmC0D0p --> 用于播放的 pcm 设备
seq --> 音序器
timer --> 定时器
其中,C0D0 代表的是声卡 0 中的设备 0,pcmC0D0c 最后一个 c 代表 Capture,pcmC0D0p 最后一个 p 代表 Playback,这些都是 alsa-driver 中的命名规则。从上面的列表可以看出,我的声卡下挂了 6 个设备,根据声卡的实际能力,驱动实际上可以挂上更多种类的设备,在 include/sound/core.h 中,定义了以下设备类型:
#define SNDRV_DEV_TOPLEVEL ((__force snd_device_type_t) 0)
#define SNDRV_DEV_CONTROL ((__force snd_device_type_t) 1)
#define SNDRV_DEV_LOWLEVEL_PRE ((__force snd_device_type_t) 2)
#define SNDRV_DEV_LOWLEVEL_NORMAL ((__force snd_device_type_t) 0x1000)
#define SNDRV_DEV_PCM ((__force snd_device_type_t) 0x1001)
#define SNDRV_DEV_RAWMIDI ((__force snd_device_type_t) 0x1002)
#define SNDRV_DEV_TIMER ((__force snd_device_type_t) 0x1003)
#define SNDRV_DEV_SEQUENCER ((__force snd_device_type_t) 0x1004)
#define SNDRV_DEV_HWDEP ((__force snd_device_type_t) 0x1005)
#define SNDRV_DEV_INFO ((__force snd_device_type_t) 0x1006)
#define SNDRV_DEV_BUS ((__force snd_device_type_t) 0x1007)
#define SNDRV_DEV_CODEC ((__force snd_device_type_t) 0x1008)
#define SNDRV_DEV_JACK ((__force snd_device_type_t) 0x1009)
#define SNDRV_DEV_LOWLEVEL ((__force snd_device_type_t) 0x2000)
通常,我们更关心的是 pcm 和 control 这两种设备。
三、驱动的代码文件结构
在 Linux 4.14 代码树中,ALSA 的代码文件结构如下:
Sound
/aoa
/arm
/atmel
/core
/oss
/seq
/drivers
/firewire
/had
/i2c
/isa
/mips
/oss
/parisc
/pci
/pcmcia
/ppc
/sh
/soc
/codecs
/sparc
/spi
/synth
/emux
/usb
/x86
core 该目录包含了 ALSA 驱动的中间层,它是整个 ALSA 驱动的核心部分
core/oss 包含模拟旧的 OSS 架构的 PCM 和 Mixer 模块
core/seq 有关音序器相关的代码
drivers 放置一些与 CPU、BUS 架构无关的公用代码
i2c ALSA自己的 I2C 控制代码
pci pci声卡的顶层目录,子目录包含各种 pci 声卡的代码
isa isa声卡的顶层目录,子目录包含各种 isa 声卡的代码
soc 针对 system-on-chip 体系的中间层代码
soc/codecs 针对 soc 体系的各种 codec 的代码,与平台无关
四、ALSA 初始化
系统启动中 ALSA 初始化过程如下:
alsa_sound_init()
/* 注册 alsa 字符设备 */
register_chrdev(116, "alsa", &snd_fops)
/* 创建 /proc/asound 目录及下属 version、devices、cards、modules 等文件 */
snd_info_init()
const struct file_operations snd_fops =
{
.owner = THIS_MODULE,
.open = snd_open,
.llseek = noop_llseek,
};
从用户空间打开 PCM 设备过程如下:
snd_pcm_open("default", SND_PCM_STREAM_PLAYBACK) // alsa-lib 接口
open("/dev/snd/controlC0") // 打开控制设备; 主设备 116, 次设备 0
open("/dev/snd/pcmC0D0p") // 打开PCM设备; 主设备 116, 次设备 16
snd_open() // 根据主设备号找到该入口
snd_minors[minor] // 根据次设备号找到对应操作集
snd_ctl_f_ops::open() // 控制设备打开方法
snd_ctl_open()
snd_pcm_f_ops::open() // PCM设备打开方法
snd_pcm_playback_open()
snd_lookup_minor_data() // 根据次设备号查找对应 PCM 设备(snd_pcm)
snd_pcm_open() // 打开 PCM 播放子流
五、ALSA 核心层驱动
核心层为用户空间提供逻辑设备接口, 同时为驱动提供接口来驱动硬件设备, 主要位于 sound/core 目录下
5.1 数据结构
该层包含的主要数据结构包括:
- snd_card 表示一个声卡实例, 包含多个声卡设备
- snd_device 表示一个声卡设备部件
- snd_pcm 表示一个 PCM 设备, 声卡设备的一种, 用于播放和录音
- snd_control 表示 Control 设备, 声卡设备的一种, 用于控制声卡
- snd_pcm_str 表示 PCM 流, 分为 Playback 和 Capture
- snd_pcm_substream PCM 子流, 用于音频的播放或录制
- snd_pcm_ops PCM 流操作集
各结构体之间主要关系图如下所示
snd_card 主要字段如下:
struct snd_card {
int number; /* 索引 */
char id[16]; /* 标识符 */
char driver[16]; /* 驱动名称 */
char shortname[32]; /* 短名 */
char longname[80]; /* 名字 */
void *private_data; /* 声卡私有数据*/
void (*private_free) (struct snd_card *); /* 私有数据释放回调 */
struct list_head devices; /* 该声卡下所有设备*/
struct list_head controls; /* 该声卡下所有控制设备*/
struct list_head files_list; /* 声卡管理文件 */
struct device *dev; /* 声卡相关的 device */
struct device card_dev; /* 用于 sysfs, 代表该声卡 */
bool registered; /* 是否注册标记 */
};
snd_device 主要字段如下:
struct snd_device {
struct list_head list; /* 所有注册的声卡设备链表 */
struct snd_card *card; /* 设备所属声卡 */
enum snd_device_state state; /* 设备状态*/
enum snd_device_type type; /* 设备类型*/
void *device_data; /* 指向具体的声卡设备, 如 snd_pcm */
struct snd_device_ops *ops; /* 设备操作集*/ };
snd_pcm 主要字段如下:
struct snd_pcm {
struct snd_card *card; /* 该 PCM 设备所属声卡*/
struct list_head list; /* 所有注册的 PCM 设备链表 */
int device; /* PCM 索引 */
unsigned int info_flags; /* SNDRV_PCM_INFO_ */
char id[64]; /* PCM 设备标识 */
char name[80]; /* PCM 设备名 */
struct snd_pcm_str streams[2]; /* 指向 PCM 设备的capture(1) 和 playback(0) 流 */
void *private_data; /* PCM 设备私有数据*/
void (*private_free) (struct snd_pcm *); /* 私有数据释放回调 */
};
5.2 接口
该层主要接口如下:
/* 创建和初始化声卡结构体 */
int snd_card_new(struct device *parent, int idx, const char *xid, struct module *, int extra_size, struct snd_card **card_ret);
/* 释放声卡结构体 */
int snd_card_free(struct snd_card * card);
/* 注册声卡 */
int snd_card_register(struct snd_card * card);
/* 创建声卡设备部件, 通常由 snd_pcm_new 和 snd_card_new 自动完成 */
int snd_device_new(struct snd_card *, enum snd_device_type type, void *device_data, struct snd_device_ops *ops);
/* 注册声卡设备部件, 通常由 snd_card_register 自动完成 */
int snd_device_register(struct snd_card *card, void *device_data);
/* 创建 PCM 设备 */
int snd_pcm_new(struct snd_card *, const char *id, int device, int playback_count, int capture_count, struct snd_pcm **rpcm);
/* 创建PCM流, 通常 snd_pcm_new 会自动创建 capture 和 playback 两个 PCM 流 */
int snd_pcm_new_stream(struct snd_pcm * pcm, int stream, int substream_count);
/* 设置 PCM 设备操作集 */
void snd_pcm_set_ops(struct snd_pcm *, int direction, const struct snd_pcm_ops *ops);
snd_card_new 完成了如下事宜:
- 分配 snd_card+extra_size 空间大小
- 如果 extra_size 大于 0,将 private_data 指向 extra_size 所在首地址
- 如果指定了 xid, 将其拷贝至 snd_card::id 中, 即声卡标识符
- 根据 idx 获取可用的声卡索引并赋值给 snd_card::number
- 分别将 parent、module 赋值给 snd_card::dev、snd_card::module
- 初始化链表 snd_card::devices、snd_card::controls、snd_card::ctl_files、snd_card::files_list
- 调用 device_initialize() 初始化 snd_card::card_dev, 并设置 snd_card::card_dev 相关成员变量, 用于 sysfs
- 调用 snd_ctl_create() 创建控制接口
8.1 调用 snd_device_initialize 初始化 snd_card::ctl_dev, 并设置相关成员变量, 用于 sysfs
8.2 调用 snd_device_new(SNDRV_DEV_CONTROL, ops) 创建声卡控制设备部件
static struct snd_device_ops ops = {
.dev_free = snd_ctl_dev_free,
.dev_register = snd_ctl_dev_register,
.dev_disconnect = snd_ctl_dev_disconnect,
};
- 调用 snd_info_card_create() 创建 proc 对应文件系统
snd_card_register 完成了如下事宜:
- 如果声卡未注册 (snd_card::registered), 调用 device_add(snd_card::card_dev) 将声卡添加到
sysfs
- 调用 snd_device_register_all(snd_card) 注册该声卡下所有声卡设备,(即 snd_card::devices 链
表), 即完成 snd_device_register 相同的功能
2.1 遍历 snd_card::devices 链表, 依次调用 __snd_device_register 注册声卡设备
2.1.1 调用 snd_device::snd_device_ops::dev_register 注册该设备, 对于 Control 设备, 即snd_ctl_dev_register; 对于 PCM 设备, 即 snd_pcm_dev_register; 最终则都会调用 snd_register_device
2.1.1.1 snd_ctl_dev_register: 调用 snd_register_device(snd_ctl_f_ops) 注册该 Control 设备
2.1.1.2 snd_pcm_dev_register: 调用 snd_pcm_add 将该 PCM 设备添加至全局 PCM 链表snd_pcm_devices 中, 然后调用 snd_register_device(snd_pcm_f_ops) 注册该 PCM 设备
2.1.1.2.1 snd_register_device: 分配 snd_minor 空间, 设置 type、card、device、f_ops、card_ptr 等成员变量; 通过 snd_find_free_minor 找到合适的 minor 并通过 MKDEV(116, minor) 创建设备节点, 然后通过 device_add 向系统添加该设备; 最后将该声卡设备添加至全局声卡主设备的次设备数组 snd_minors 中
- 将该声卡放入全局静态声卡数组 snd_cards 中
- 调用 init_info_for_card() 向 proc 文件系统注册该声卡
snd_pcm_new 完成了如下事宜:
- 分配 snd_pcm 空间, 并设置 snd_pcm::card、snd_pcm::device 等成员变量
- 调用 snd_pcm_new_stream(SNDRV_PCM_STREAM_PLAYBACK) 创建 playback_count 个子流用于播放
- 调用 snd_pcm_new_stream(SNDRV_PCM_STREAM_CAPTURE) 创建 capture_count 个子流用于录制
- 调用 snd_device_new(SNDRV_DEV_PCM, ops) 添加 PCM 设备
static struct snd_device_ops ops = {
.dev_free = snd_pcm_dev_free,
.dev_register = snd_pcm_dev_register,
.dev_disconnect = snd_pcm_dev_disconnect,
};
snd_pcm_new_stream 完成了如下事宜
- 设置 snd_pcm::stream[playback or catpure] 对应 stream、pcm、substream_count 成员变量
- 调用 snd_device_initialize() 初始化 snd_pcm::stream::dev, 并设置相关成员变量, 用于 sysfs
- 调用 snd_pcm_stream_proc_init(snd_pcm_str) 初始化对应 proc 文件系统
- 分配 substream_count 个 snd_pcm_substream 并进行相应初始化
5.3 实现
核心驱动的一般实现步骤如下
- 调用 snd_card_create 创建声卡实例 (struct snd_card)
- 定义声卡的私有结构体用于存放该声卡的一些资源信息, 如中断资源、IO 资源、DMA 资源等
- 硬件初始化, 包括数字音频接口初始化、DMA 控制器初始化、编解码器初始化
- 调用 snd_pcm_new 创建逻辑设备, 并实现其操作集 snd_pcm_ops
- 调用 snd_card_register 注册声卡实例及声卡设备
具体实例可参考 sound/atmel/ac97c 和 sound/spi/at73c213 的实现
六、ALSA ASOC 层驱动
在移动设备中, 为了更好的提供 ALSA 支持, 在核心层的基础上出现了 ASOC(ALSA System on Chip) 层。
ASOC 层代码位于 sound/soc/*, 主要由如下三部分组成:
- Codec: 负责配置编解码器提供音频捕获和回放功能
- Platform: 主要负责 SoC 平台音频 DMA 和音频接口的配置和控制, 包括时钟、DMA、I2S、PCM 等
- Machine: Codec、Platform、输入输出设备提供了一个载体
6.1 DAI
DAI(Digital Audio Interfaces), 即数字音频接口
ASOC 支持三种主流 DAI: AC97、I2S 和 PCM
AC97: 通常用于 PC 声卡, 为 5 线接口, 每个 AC97 帧为 21uS 长, 被分为 13 个时隙
- BCLK: 由 AC97 驱动, 为 12.288 MHz
- SYNC: 同步信号, 由 Controler 驱动, 为 48 kHz
- SDATDIN: 用于 Capture, AC97->Controler
- SDATAOUT: 用于 Playback, Controler->AC97
- RESET: 由 Controler 生成, 用于唤醒 AC97
I2S 是 HiFi、STB 和便携式设备中常用的 4 线 DAI
- SCLK: 串行时钟
- LRCK: 也称 WS, 声道选择线
- Tx: 用于传输音频数据
- Rx: 用于接收音频数据
PCM 是另一种 4 线接口, 与 I2S 非常相似, 可以支持更灵活的协议
- BCLK: 位时钟, 根据采样率而变化
- SYNC: 同步信号
- Tx: 用于传输音频数据
- Rx: 用于接收音频数据
6.2 Codec
Codec 驱动应该实现为通用与硬件无关的,用于配置编解码器、FM、MODEM、BT 或外部 DSP, 以提供 Playback 和 Capture, 这部分代码通常位于 sound/soc/codecs/*
每个 Codec 驱动必须提供如下功能:
- Codec DAI 和 PCM 配置
- 使用 RegMap 实现的 Codec 控制 IO
- Mixers 和 Audio 控制
- Codec 音频操作
- DAPM 描述
- DAPM 事件处理
- DAC 静音控制(可选)
6.2.1 数据结构
Codec 层主要结构体包括 snd_soc_codec、snd_soc_codec_driver、snd_soc_dai、snd_soc_dai_driver
snd_soc_codec 代表一个 Codec 设备, 其主要字段如下:
struct snd_soc_codec {
struct device *dev; /* 指向 Codec 设备的指针 */
const struct snd_soc_codec_driver *driver; /* 该 Codec 对应的驱动 */
struct list_head list;
/* runtime */
unsigned int cache_init:1; /* 指示 Codec cache 是否初始化 */
/* codec IO */
void *control_data; /* 控制 IO 数据 */
hw_write_t hw_write; /* 控制 IO 函数 */
void *reg_cache;
/* component */
struct snd_soc_component component;
};
snd_soc_codec_driver 代表一个 Codec 驱动, 其主要字段如下:
struct snd_soc_codec_driver {
/* 操作集 */
int (*probe)(struct snd_soc_codec *);
int (*remove)(struct snd_soc_codec *);
int (*suspend)(struct snd_soc_codec *);
int (*resume)(struct snd_soc_codec *);
struct snd_soc_component_driver component_driver;
/* codec wide operations */
int (*set_sysclk)(struct snd_soc_codec *codec,
int clk_id, int source, unsigned int freq, int dir);
int (*set_pll)(struct snd_soc_codec *codec, int pll_id, int source,
unsigned int freq_in, unsigned int freq_out);
int (*set_jack)(struct snd_soc_codec *codec, struct snd_soc_jack *jack, void *data);
/* Codec IO 相关函数 */
struct regmap *(*get_regmap)(struct device *);
unsigned int (*read)(struct snd_soc_codec *, unsigned int);
int (*write)(struct snd_soc_codec *, unsigned int, unsigned int);
/* 偏置电压配置函数 */
int (*set_bias_level)(struct snd_soc_codec *, enum snd_soc_bias_level level);
};
snd_soc_dai 代表 DAI 运行时数据, 其主要字段如下:
struct snd_soc_dai {
const char *name; /* 名称 */
int id; /* 索引 */
struct device *dev; /* DAI 设备 */
/* 驱动操作集 */
struct snd_soc_dai_driver *driver;
/* DAI 运行时信息 */
unsigned int capture_active:1;
unsigned int playback_active:1;
unsigned int symmetric_rates:1;
unsigned int symmetric_channels:1;
unsigned int symmetric_samplebits:1;
unsigned int probed:1;
unsigned int active;
struct snd_soc_dapm_widget *playback_widget;
struct snd_soc_dapm_widget *capture_widget;
/* DAI DMA data */
void *playback_dma_data; /* 用于管理 Playback DMA */
void *capture_dma_data; /* 用于管理 Capture DMA */
/* Symmetry data - only valid if symmetry is being enforced */
unsigned int rate; unsigned int channels;
unsigned int sample_bits;
/* parent platform/codec */
struct snd_soc_codec *codec; /* 绑定的 Codec */
struct snd_soc_component *component; /* 绑定的 Platform */
struct list_head list;
};
snd_soc_dai_driver 代表一个 DAI 驱动, 其主要字段如下:
struct snd_soc_dai_driver {
/* DAI 描述 */
const char *name;
unsigned int id;
unsigned int base;
struct snd_soc_dobj dobj;
/* DAI 驱动回调 */
int (*probe)(struct snd_soc_dai *dai);
int (*remove)(struct snd_soc_dai *dai);
int (*suspend)(struct snd_soc_dai *dai);
int (*resume)(struct snd_soc_dai *dai);
/* compress dai */
int (*compress_new)(struct snd_soc_pcm_runtime *rtd, int num);
/* Optional Callback used at pcm creation*/
int (*pcm_new)(struct snd_soc_pcm_runtime *rtd, struct snd_soc_dai *dai);
/* DAI is also used for the control bus */
bool bus_control;
/* 操作集 */
const struct snd_soc_dai_ops *ops;
const struct snd_soc_cdai_ops *cops;
/* DAI 能力 */
struct snd_soc_pcm_stream capture;
struct snd_soc_pcm_stream playback;
unsigned int symmetric_rates:1;
unsigned int symmetric_channels:1;
unsigned int symmetric_samplebits:1;
/* probe ordering - for components with runtime dependencies */
int probe_order;
int remove_order;
};
6.2.2 接口
int snd_soc_register_codec(struct device *dev, const struct snd_soc_codec_driver *, struct
snd_soc_dai_driver *, int num_dai);
snd_soc_register_codec 完成了如下事宜:
- 分配 snd_soc_codec 空间
- 调用 snd_soc_component_initialize() 初始化 snd_soc_codec::snd_soc_component
- snd_soc_codec::snd_soc_component 操作集初始化
- DAPM 相关初始化
- 调用 snd_soc_register_dais() 注册 num_dai 个 DAI
- 将该 Codec 添加至全局 Codec 链表 codec_list 中
6.2.3 实现
Codec 的一般实现步骤如下:
- 获取 Codec 设备资源
- 实现 snd_soc_codec_driver 结构体
- 实现 snd_soc_dai_driver 结构体
- 实现 snd_soc_dai_ops 结构体, 并赋值给 snd_soc_dai_driver::ops
- 调用 snd_soc_register_codec() 注册 Codec
6.3 Platform
Platform 驱动可分为三个部分: 音频 DMA 驱动、SoC DAI 驱动和 DSP 驱动.
这些驱动代码应该只和 SoC CPU 有关而和 Board 无关.
6.4 Machine
Machine/Board 驱动用来将所有的部件驱动 (Codecs、Platforms和DAIs) 进行关联.
七、参考资料:
【1】 Linux ALSA 声卡驱动之一:ALSA 架构简介
【2】 Linux ALSA 详解
【3】 alsa 驱动分析(1)
【4】 Linux 音频设备驱动
评论