Linux ALSA 音频设备驱动分析

一、概述

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 完成了如下事宜:

  1. 分配 snd_card+extra_size 空间大小
  2. 如果 extra_size 大于 0,将 private_data 指向 extra_size 所在首地址
  3. 如果指定了 xid, 将其拷贝至 snd_card::id 中, 即声卡标识符
  4. 根据 idx 获取可用的声卡索引并赋值给 snd_card::number
  5. 分别将 parent、module 赋值给 snd_card::dev、snd_card::module
  6. 初始化链表 snd_card::devices、snd_card::controls、snd_card::ctl_files、snd_card::files_list
  7. 调用 device_initialize() 初始化 snd_card::card_dev, 并设置 snd_card::card_dev 相关成员变量, 用于 sysfs
  8. 调用 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,

};
  1. 调用 snd_info_card_create() 创建 proc 对应文件系统

 

snd_card_register 完成了如下事宜:

  1. 如果声卡未注册 (snd_card::registered), 调用 device_add(snd_card::card_dev) 将声卡添加到

sysfs

  1. 调用 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 中

  1. 将该声卡放入全局静态声卡数组 snd_cards 中
  2. 调用 init_info_for_card() 向 proc 文件系统注册该声卡

 

snd_pcm_new 完成了如下事宜:

  1. 分配 snd_pcm 空间, 并设置 snd_pcm::card、snd_pcm::device 等成员变量
  2. 调用 snd_pcm_new_stream(SNDRV_PCM_STREAM_PLAYBACK) 创建 playback_count 个子流用于播放
  3. 调用 snd_pcm_new_stream(SNDRV_PCM_STREAM_CAPTURE) 创建 capture_count 个子流用于录制
  4. 调用 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 完成了如下事宜

  1. 设置 snd_pcm::stream[playback or catpure] 对应 stream、pcm、substream_count 成员变量
  2. 调用 snd_device_initialize() 初始化 snd_pcm::stream::dev, 并设置相关成员变量, 用于 sysfs
  3. 调用 snd_pcm_stream_proc_init(snd_pcm_str) 初始化对应 proc 文件系统
  4. 分配 substream_count 个 snd_pcm_substream 并进行相应初始化


5.3 实现

核心驱动的一般实现步骤如下

  1. 调用 snd_card_create 创建声卡实例 (struct snd_card)
  2. 定义声卡的私有结构体用于存放该声卡的一些资源信息, 如中断资源、IO 资源、DMA 资源等
  3. 硬件初始化, 包括数字音频接口初始化、DMA 控制器初始化、编解码器初始化
  4. 调用 snd_pcm_new 创建逻辑设备, 并实现其操作集 snd_pcm_ops
  5. 调用 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 驱动必须提供如下功能:

  1. Codec DAI 和 PCM 配置
  2. 使用 RegMap 实现的 Codec 控制 IO
  3. Mixers 和 Audio 控制
  4. Codec 音频操作
  5. DAPM 描述
  6. DAPM 事件处理
  7. 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 完成了如下事宜:

  1. 分配 snd_soc_codec 空间
  2. 调用 snd_soc_component_initialize() 初始化 snd_soc_codec::snd_soc_component
  3. snd_soc_codec::snd_soc_component 操作集初始化
  4. DAPM 相关初始化
  5. 调用 snd_soc_register_dais() 注册 num_dai 个 DAI
  6. 将该 Codec 添加至全局 Codec 链表 codec_list 中


6.2.3 实现

Codec 的一般实现步骤如下:

  1. 获取 Codec 设备资源
  2. 实现 snd_soc_codec_driver 结构体
  3. 实现 snd_soc_dai_driver 结构体
  4. 实现 snd_soc_dai_ops 结构体, 并赋值给 snd_soc_dai_driver::ops
  5. 调用 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 音频设备驱动

★博文内容均由个人提供,与平台无关,如有违法或侵权,请与网站管理员联系。

★文明上网,请理性发言。内容一周内被举报5次,发文人进小黑屋喔~

评论