基于 Linux Kernel 源码深度分析 路径:
sound/|include/sound/分析版本:Linux 6.x mainline
- ALSA 架构层次:从用户态到硬件
- snd_card:声卡抽象
- PCM 子系统
- 环形缓冲区与双指针模型
- DMA 传输机制
- ALSA Control 接口
- ASoC 框架:嵌入式音频
- DAPM:动态音频电源管理
- MIDI 子系统
- 用户空间接口
- sound/ 目录主要文件一览
- 调试工具与 procfs
Linux 最早的音频接口是 OSS(Open Sound System),它以 /dev/dsp、/dev/mixer 等设备文件为界面。OSS 存在几个根本性缺陷:
- 单一打开:同一时间只允许一个进程使用
/dev/dsp,无法混音。 - API 贫乏:没有标准的硬件参数协商机制,应用难以获知硬件能力。
- 无法热插拔:设备号静态分配,USB 声卡等动态设备支持很差。
- 无法细粒度控制:mixer 接口过于简单,无法表达现代 SoC 复杂的音频路径。
ALSA(Advanced Linux Sound Architecture) 由 Jaroslav Kysela 于 1998 年发起,2.6 内核将其纳入主线。ALSA 解决了上述问题:
- 引入 card/device/substream 三级结构,支持多声卡并存。
- 设计基于约束集的硬件参数协商(
snd_pcm_hw_params)。 - 提供
/dev/snd/pcmC*D*p(playback)和pcmC*D*c(capture)命名规则,支持热插拔。 - 通过
snd_kcontrol框架统一 mixer/音量/开关控制。
+===========================================================================+
| 用 户 空 间 |
| |
| +------------------+ +-------------------+ +------------------------+ |
| | 应用程序 | | GStreamer / mpv | | aplay / arecord | |
| | (自行调用 API) | | (GStreamer插件) | | (alsa-utils 工具) | |
| +--------+---------+ +---------+---------+ +-----------+------------+ |
| | | | |
| +--------v----------------------v-------------------------v------------+ |
| | PipeWire (新一代音频服务器) | |
| | 会话管理 / 策略路由 / 低延迟混音 / 蓝牙 / JACK 兼容 | |
| +----------------------------------+----------------------------------+ |
| | |
| +----------------------------------v----------------------------------+ |
| | alsa-lib (libasound.so) | |
| | pcm 插件链 / ctl API / 配置解析(.asoundrc / alsa.conf) | |
| +----------------------------------+----------------------------------+ |
+========================================|==================================+
| syscall: open/ioctl/mmap/read/write
+========================================v==================================+
| 内 核 空 间 |
| |
| +----------------------------------------------------------------------+ |
| | ALSA Core (sound/core/) | |
| | snd_card | snd_device | snd_minor | proc | power management | |
| +-------------------------------+--------------------------------------+ |
| | |
| +-----------+ +-----------+ +-----------+ +-----------+ |
| | PCM 子系 | | Control | | RawMIDI | | Timer | |
| | 统(sound/ | | 子系统 | | 子系统 | | 子系统 | |
| | core/pcm) | |(core/ctl) | |(core/mid) | |(core/tim) | |
| +-----+-----+ +-----+-----+ +-----+-----+ +-----+-----+ |
| | | | | |
| +-----v---------------v---------------v---------------v----------------+ |
| | ASoC Framework (sound/soc/) | |
| | snd_soc_card | CPU DAI | Codec | Platform | DAPM | |
| +-------------------------------+--------------------------------------+ |
| | |
| +-------------------------------v--------------------------------------+ |
| | 低层驱动 (sound/drivers/ + vendor BSP) | |
| | Intel HDA | USB Audio | ARM PL041 | Qualcomm Q6DSP | ... | |
| +-------------------------------+--------------------------------------+ |
+========================================|==================================+
| DMA / I2S / AC97 / HDA / USB
+========================================v==================================+
| 硬 件 层 |
| DAC/ADC Codec芯片 | I2S总线 | HDMI音频 | USB声卡 | PCIe HDA |
+===========================================================================+
| 层次 | 代表组件 | 主要职责 |
|---|---|---|
| 应用层 | GStreamer、mpv、aplay | 产生/消费 PCM 数据 |
| 音频服务器 | PipeWire、PulseAudio | 策略路由、混音、低延迟调度 |
| alsa-lib | libasound.so | 封装 ioctl、实现 PCM 插件链 |
| ALSA Core | sound/core/ |
声卡抽象、设备注册、proc 文件 |
| PCM/CTL/MIDI | 各子系统 | 数据传输、控制、MIDI 协议 |
| ASoC | sound/soc/ |
嵌入式 SoC 三件套 + DAPM |
| 低层驱动 | HDA、USB Audio | 操作具体硬件寄存器、DMA |
| 硬件 | Codec、DAC/ADC | 模数转换、物理信号 |
ALSA 在 /dev/snd/ 下创建如下设备节点,major 号为 116(include/sound/core.h:26):
/dev/snd/
controlC0 -- 声卡 0 的控制接口
pcmC0D0p -- 声卡 0, 设备 0, 播放(p)
pcmC0D0c -- 声卡 0, 设备 0, 录音(c)
pcmC0D1p -- 声卡 0, 设备 1, 播放
midiC0D0 -- 声卡 0, MIDI 设备 0
seq -- ALSA sequencer
timer -- 全局 timer 设备
命名格式:pcmC<card>D<device><direction>,其中 p = playback,c = capture。
snd_card 是整个 ALSA 最顶层的对象,代表一块物理(或逻辑)声卡。定义于 include/sound/core.h:80:
/* include/sound/core.h:80 */
struct snd_card {
int number; /* 声卡编号,对应 snd_cards[] 数组下标 */
char id[16]; /* 用户可见的 id 字符串,如 "HDA-Intel" */
char driver[16]; /* 驱动名称 */
char shortname[32]; /* 短名称 */
char longname[80]; /* 完整名称,出现在 /proc/asound/cards */
char mixername[80]; /* mixer 名称 */
char components[128];/* 组件列表,空格分隔 */
struct module *module;
void *private_data; /* 驱动私有数据 */
void (*private_free)(struct snd_card *card);
struct list_head devices; /* 挂载的 snd_device 链表 */
struct device *ctl_dev; /* /dev/snd/controlC* 对应的 device */
unsigned int last_numid; /* 最近分配的数字 ID */
struct rw_semaphore controls_rwsem;
rwlock_t controls_rwlock;
int controls_count;
struct list_head controls; /* 所有 kcontrol 链表 */
struct list_head ctl_files; /* 已打开的 control 文件 */
struct snd_info_entry *proc_root; /* /proc/asound/cardX/ 根目录 */
struct proc_dir_entry *proc_root_link;
spinlock_t files_lock;
int shutdown;
struct device *dev;
struct device card_dev; /* sysfs cardX 对象 */
bool registered;
bool managed;
int sync_irq; /* 用于 PCM sync 的 IRQ */
size_t total_pcm_alloc_bytes; /* 已分配的 PCM 缓冲区总量 */
struct mutex memory_mutex;
unsigned int power_state; /* 电源状态 */
};字段分类说明:
| 字段组 | 字段 | 作用 |
|---|---|---|
| 身份信息 | id, shortname, longname, driver |
声卡描述,出现在 /proc/asound/cards |
| 设备管理 | devices 链表 |
挂载 PCM/CTL/MIDI/Timer 等 snd_device |
| 控制管理 | controls, controls_count, last_numid |
管理所有 snd_kcontrol |
| procfs | proc_root, proc_root_link |
暴露调试信息到 /proc/asound/ |
| 电源 | power_state, power_ref, power_sleep |
支持 suspend/resume |
| 内存 | total_pcm_alloc_bytes, memory_mutex |
限制 DMA 缓冲区总量 |
每个挂载到声卡的子系统(PCM、CTL、MIDI 等)都通过 snd_device 结构体链接(include/sound/core.h:67):
/* include/sound/core.h:38 */
enum snd_device_type {
SNDRV_DEV_LOWLEVEL,
SNDRV_DEV_INFO,
SNDRV_DEV_BUS,
SNDRV_DEV_CODEC,
SNDRV_DEV_PCM,
SNDRV_DEV_COMPRESS,
SNDRV_DEV_RAWMIDI,
SNDRV_DEV_TIMER,
SNDRV_DEV_SEQUENCER,
SNDRV_DEV_HWDEP,
SNDRV_DEV_JACK,
SNDRV_DEV_CONTROL, /* 必须是最后一个,注册顺序决定 */
};
/* include/sound/core.h:67 */
struct snd_device {
struct list_head list; /* 链接到 snd_card.devices */
struct snd_card *card;
enum snd_device_state state;
enum snd_device_type type;
void *device_data; /* 指向具体子系统(snd_pcm 等)*/
const struct snd_device_ops *ops;
};snd_device_type 的枚举值同时定义了注册顺序,SNDRV_DEV_CONTROL 最后注册,确保控制接口在所有设备就绪后再暴露给用户空间。
snd_card_new(parent_dev, idx, xid, module, extra_size, &card)
|
+-- 分配 snd_card + extra_size 内存(kzalloc)
|
+-- 初始化各锁:controls_rwsem, controls_rwlock, files_lock
|
+-- 分配声卡编号(snd_cards[] 数组,最多 SNDRV_CARDS=8 或动态上限)
|
+-- 初始化 card->card_dev(kobject 等)
|
+-- snd_info_card_create() -> 创建 /proc/asound/cardX/ 目录
|
+-- 返回 card,驱动继续初始化各子设备
关键 API(include/sound/core.h:282):
int snd_card_new(struct device *parent, int idx, const char *xid,
struct module *module, int extra_size,
struct snd_card **card_ret);extra_size 允许驱动在 snd_card 末尾额外分配私有数据,通过 card->private_data 访问,避免二次分配。
snd_card_register(card)
|
+-- 遍历 card->devices 链表,按 snd_device_type 顺序调用 dev->ops->dev_register()
| SNDRV_DEV_PCM -> snd_pcm_dev_register() -> 创建 /dev/snd/pcmC*D*
| SNDRV_DEV_RAWMIDI -> snd_rawmidi_dev_register()
| SNDRV_DEV_CONTROL -> snd_ctl_dev_register() -> 创建 /dev/snd/controlC*
|
+-- 在 /proc/asound/cards 中添加条目
|
+-- 在 sysfs 中注册 card_dev(/sys/class/sound/cardX)
|
+-- card->registered = true
/proc/asound/ 是 ALSA 最主要的调试入口:
/proc/asound/
cards -- 所有声卡列表
devices -- 所有设备(pcm/ctl/midi)
modules -- 加载的 ALSA 模块
card0/
id -- 声卡 ID 字符串
pcm0p/ -- PCM 设备 0 播放
info -- 硬件信息
sub0/
hw_params -- 当前硬件参数
status -- 运行时状态(ptr / xrun)
sw_params -- 软件参数
pcm0c/ -- PCM 设备 0 录音
...
snd_card.components 字段(128 字节,空格分隔)用于描述声卡能力,例如:
"HDA:10EC0269,17AA21FA,00100103 AC97a:AD1981"
ASoC 声卡在探测完成后还会通过 snd_soc_card.components 填充类似信息。
PCM 子系统有三层核心结构体:
snd_pcm -- PCM 设备实体
|
+-- snd_pcm_str[2] -- 方向流(playback=0, capture=1)
|
+-- snd_pcm_substream -- 具体子流(支持多个同方向子流)
|
+-- snd_pcm_runtime -- 运行时状态(open 时分配)
/* include/sound/pcm.h:534 */
struct snd_pcm {
struct snd_card *card;
struct list_head list;
int device; /* 设备编号(0-based) */
unsigned int info_flags;
char id[64];
char name[80];
struct snd_pcm_str streams[2]; /* [0]=playback, [1]=capture */
struct mutex open_mutex;
wait_queue_head_t open_wait;
void *private_data;
bool internal; /* 内部使用,不对用户空间暴露 */
bool nonatomic; /* 非原子模式(如 USB Audio) */
};/* include/sound/pcm.h:513 */
struct snd_pcm_str {
int stream; /* SNDRV_PCM_STREAM_PLAYBACK 或 _CAPTURE */
struct snd_pcm *pcm;
unsigned int substream_count;
unsigned int substream_opened;
struct snd_pcm_substream *substream; /* 子流链表头 */
struct snd_kcontrol *chmap_kctl; /* 通道映射控件 */
struct device *dev;
};/* include/sound/pcm.h:464 */
struct snd_pcm_substream {
struct snd_pcm *pcm;
struct snd_pcm_str *pstr;
void *private_data;
int number;
char name[32];
int stream; /* 方向 */
struct pm_qos_request latency_pm_qos_req;
size_t buffer_bytes_max; /* 环形缓冲区最大字节数 */
struct snd_dma_buffer dma_buffer; /* DMA 缓冲区 */
size_t dma_max;
const struct snd_pcm_ops *ops; /* 驱动操作集 */
struct snd_pcm_runtime *runtime; /* 运行时状态,open 后分配 */
struct snd_timer *timer;
unsigned timer_running: 1;
long wait_time;
struct snd_pcm_substream *next; /* 同方向下一个子流 */
struct list_head link_list;
struct snd_pcm_group self_group;
struct snd_pcm_group *group;
int ref_count;
atomic_t mmap_count;
unsigned int f_flags;
void (*pcm_release)(struct snd_pcm_substream *);
struct pid *pid;
};运行时结构体在 open 时分配,包含完整的状态信息(include/sound/pcm.h:362):
/* include/sound/pcm.h:362 */
struct snd_pcm_runtime {
/* 状态 */
snd_pcm_state_t state;
snd_pcm_state_t suspended_state;
int overrange;
snd_pcm_uframes_t avail_max;
snd_pcm_uframes_t hw_ptr_base;
snd_pcm_uframes_t hw_ptr_interrupt;
unsigned long hw_ptr_jiffies;
snd_pcm_sframes_t delay;
/* HW 参数 */
snd_pcm_access_t access;
snd_pcm_format_t format;
unsigned int rate;
unsigned int channels;
snd_pcm_uframes_t period_size;
unsigned int periods;
snd_pcm_uframes_t buffer_size;
size_t byte_align;
unsigned int frame_bits;
unsigned int sample_bits;
/* SW 参数 */
snd_pcm_uframes_t start_threshold;
snd_pcm_uframes_t stop_threshold;
snd_pcm_uframes_t silence_threshold;
snd_pcm_uframes_t silence_size;
snd_pcm_uframes_t boundary;
/* mmap */
struct snd_pcm_mmap_status *status;
struct snd_pcm_mmap_control *control;
/* 等待队列 */
snd_pcm_uframes_t twake;
wait_queue_head_t sleep;
wait_queue_head_t tsleep;
/* 硬件描述 */
struct snd_pcm_hardware hw;
struct snd_pcm_hw_constraints hw_constraints;
/* DMA */
unsigned char *dma_area; /* 内核虚拟地址 */
dma_addr_t dma_addr; /* 物理总线地址 */
size_t dma_bytes;
struct snd_dma_buffer *dma_buffer_p;
}; open()
|
v
+-------+-------+
| OPEN |
+-------+-------+
| hw_params()
v
+-------+-------+
| SETUP |
+-------+-------+
| prepare()
v
+-------+-------+
xrun -----> | PREPARED | <-------- drain done (playback)
+-------+-------+
| trigger(START)
v
+-------+-------+
| RUNNING | <-- period elapsed loop
+-------+-------+
/ \
trigger(PAUSE) trigger(STOP)
/ \
+------+------+ +------+------+
| PAUSED | | XRUN |
+------+------+ +------+------+
|
trigger(PAUSE_RELEASE)
|
v
RUNNING
ALSA PCM 状态定义在 include/uapi/linux/soundcard.h 和 include/uapi/sound/asound.h 中。驱动通过 snd_pcm_stop_xrun() 将状态推入 XRUN。
驱动通过实现 snd_pcm_ops 向 ALSA 核心提供回调(include/sound/pcm.h:55):
/* include/sound/pcm.h:55 */
struct snd_pcm_ops {
int (*open)(struct snd_pcm_substream *substream);
int (*close)(struct snd_pcm_substream *substream);
int (*ioctl)(struct snd_pcm_substream *substream,
unsigned int cmd, void *arg);
int (*hw_params)(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params);
int (*hw_free)(struct snd_pcm_substream *substream);
int (*prepare)(struct snd_pcm_substream *substream);
int (*trigger)(struct snd_pcm_substream *substream, int cmd);
int (*sync_stop)(struct snd_pcm_substream *substream);
snd_pcm_uframes_t (*pointer)(struct snd_pcm_substream *substream);
int (*get_time_info)(...);
int (*fill_silence)(...);
int (*copy)(...);
struct page *(*page)(...);
int (*mmap)(...);
int (*ack)(struct snd_pcm_substream *substream);
};各回调调用时机:
| 回调 | 触发时机 | 主要任务 |
|---|---|---|
open |
用户打开设备文件 | 分配私有数据,设置 runtime->hw |
close |
用户关闭设备文件 | 释放 open 中的资源 |
hw_params |
SNDRV_PCM_IOCTL_HW_PARAMS |
配置 DMA、时钟、格式 |
hw_free |
SNDRV_PCM_IOCTL_HW_FREE |
释放 hw_params 中的资源 |
prepare |
SNDRV_PCM_IOCTL_PREPARE |
复位 DMA 指针,准备开始传输 |
trigger |
START/STOP/PAUSE 命令 | 原子上下文,启停 DMA |
sync_stop |
等待 DMA 真正停止 | 非原子,可睡眠 |
pointer |
周期性被核心查询 | 返回 hw_ptr 当前位置(帧数) |
ack |
用户更新 appl_ptr 后 |
通知驱动更新 FIFO/预取 |
snd_pcm_hardware 描述驱动支持的范围(include/sound/pcm.h:32):
/* include/sound/pcm.h:32 */
struct snd_pcm_hardware {
unsigned int info; /* SNDRV_PCM_INFO_* flags */
u64 formats; /* SNDRV_PCM_FMTBIT_* 位图 */
u32 subformats;
unsigned int rates; /* SNDRV_PCM_RATE_* 位图 */
unsigned int rate_min; /* 最小采样率 */
unsigned int rate_max; /* 最大采样率 */
unsigned int channels_min;
unsigned int channels_max;
size_t buffer_bytes_max; /* 环形缓冲区最大字节数 */
size_t period_bytes_min;
size_t period_bytes_max;
unsigned int periods_min; /* 最少 period 数 */
unsigned int periods_max;
size_t fifo_size; /* 硬件 FIFO 大小 */
};协商流程使用约束传播算法,由 snd_pcm_hw_constraints 维护参数区间(include/sound/pcm.h:256):
/* include/sound/pcm.h:256 */
struct snd_pcm_hw_constraints {
struct snd_mask masks[...]; /* 格式、访问模式等离散参数 */
struct snd_interval intervals[...]; /* 采样率、周期大小等连续区间 */
unsigned int rules_num;
struct snd_pcm_hw_rule *rules; /* 参数间的依赖规则 */
};协商步骤:
- 驱动在
open()中设置runtime->hw,并可调用snd_pcm_hw_constraint_*()添加额外约束(如对齐要求)。 - 用户空间调用
SNDRV_PCM_IOCTL_HW_PARAMS,传入期望参数。 - ALSA 核心对每个参数依次调用约束规则,缩小可行域(refinement loop)。
- 若所有参数收敛到单点,则参数确定;否则返回
-EINVAL。 - 调用驱动的
hw_params()将确定的参数写入硬件。
+----------------------------------------------------------+
| 环 形 缓 冲 区 |
| buffer_size = periods * period_size (单位:帧) |
| |
| |<---- period_size ---->|<---- period_size ---->|... |
| +----------+------------+----------+------------+---+ |
| | period 0 | period 1 | period 2 | period 3 |...+ |
| +----------+------------+----------+------------+---+ |
| |
| ^appl_ptr ^hw_ptr |
+----------------------------------------------------------+
buffer_size:整个环形缓冲区的帧数(通常是 period_size 的整数倍)。period_size:每次 DMA 中断/wakeup 的帧数。应用每消费/产生period_size帧,驱动产生一次中断。periods:缓冲区中 period 的个数,通常为 2-8。
典型配置(44100 Hz,立体声,S16):
period_size = 1024 帧 = 4096 字节
periods = 4
buffer_size = 4096 帧 = 16384 字节
延迟 = 1024/44100 ≈ 23ms
DMA 中断处理函数完成一个 period 传输后,调用(include/sound/pcm.h:1181):
void snd_pcm_period_elapsed(struct snd_pcm_substream *substream);
void snd_pcm_period_elapsed_under_stream_lock(struct snd_pcm_substream *substream);内部流程:
DMA 中断 (IRQ context)
|
v
驱动中断处理函数
|
+-- 更新内部计数
|
+-- snd_pcm_period_elapsed(substream)
|
+-- 调用 substream->ops->pointer() 获取 hw_ptr
|
+-- 更新 runtime->hw_ptr_interrupt
|
+-- 检查 xrun(appl_ptr 追不上 hw_ptr)
|
+-- 唤醒 runtime->sleep 等待队列
| (应用的 poll/read/write 在此等待)
|
+-- 更新 runtime->status->hw_ptr(mmap 可见)
ALSA PCM 使用环形缓冲区(ring buffer)实现零拷贝音频传输。缓冲区在物理上是一块连续(或 SG)DMA 内存,逻辑上形成环形:
物理内存(DMA 可访问):
+--------+--------+--------+--------+--------+--------+
| frame0 | frame1 | frame2 | frame3 | frame4 | frame5 | ...
+--------+--------+--------+--------+--------+--------+
offset=0 offset=N-1
逻辑环形视图(buffer_size=6帧,periods=3,period_size=2帧):
hw_ptr=4
|
+------+------+------+v-----+------+------+
| 4 | 5 | 0 | 1 | 2 | 3 | <- 帧编号
+------+------+------+------+------+------+
^
appl_ptr=2
可用空间(playback)= appl_ptr - hw_ptr = 2 - 4 + 6 = 4帧(mod 6)
可用空间(capture) = hw_ptr - appl_ptr = 4 - 2 = 2帧
两个指针均以帧(frame)为单位,存在绕回(wrap-around)处理(include/sound/pcm.h:371-376):
/* include/sound/pcm.h:371 */
snd_pcm_uframes_t hw_ptr_base; /* 缓冲区重启时的基准位置 */
snd_pcm_uframes_t hw_ptr_interrupt; /* 最近中断时的 hw_ptr */
unsigned long hw_ptr_jiffies; /* hw_ptr 上次更新的 jiffies */
u64 hw_ptr_wrap; /* 因绕回累积的偏移量 */| 指针 | 含义 | 更新者 |
|---|---|---|
hw_ptr |
硬件 DMA 当前位置(已传输到哪里) | 驱动(pointer() 回调) |
appl_ptr |
应用程序当前读/写位置 | 用户空间(write/read syscall) |
播放(Playback):
- 应用写数据 →
appl_ptr前进 - DMA 从缓冲区读走数据 →
hw_ptr前进 - 可用空间 =
buffer_size - (appl_ptr - hw_ptr) - Underrun(xrun):
hw_ptr追上appl_ptr,数据消费完
录音(Capture):
- DMA 写入数据 →
hw_ptr前进 - 应用读走数据 →
appl_ptr前进 - 可用数据 =
hw_ptr - appl_ptr - Overrun(xrun):
appl_ptr追不上hw_ptr,数据被覆盖
ALSA 支持将 DMA 缓冲区直接 mmap 到用户进程地址空间,实现真正的零拷贝:
用户进程地址空间:
0x7f000000 +--------+--------+--------+
| frame0 | frame1 | frame2 | ... mmap 映射的 DMA 缓冲区
0x7f001000 +--------+--------+--------+
(与内核 DMA 缓冲区 runtime->dma_area 指向同一物理页)
0x7f002000 +----+----+----+----+
|hw_p|app_| | | mmap 的 status/control 结构体
0x7f002000 |tr |ptr | | |
+----+----+----+----+
^ ^
snd_pcm_mmap_status snd_pcm_mmap_control
mmap 流程:
- 用户
mmap(fd, 0, buffer_size, PROT_RW, MAP_SHARED, 0) - 内核调用
snd_pcm_mmap()→snd_pcm_mmap_data()→remap_pfn_range()或dma_mmap_coherent() - 用户直接读写缓冲区,用
snd_pcm_mmap_control->appl_ptr通知内核 - 无需
read/write系统调用,延迟极低
xrun 是音频传输中的严重事件:
/* include/sound/pcm.h:106 */
#define SNDRV_PCM_POS_XRUN ((snd_pcm_uframes_t)-1)当 pointer() 返回 SNDRV_PCM_POS_XRUN 或核心检测到指针越界时,snd_pcm_stop_xrun() 被调用,将 substream 状态推入 SNDRV_PCM_STATE_XRUN。
xrun 检测路径:
snd_pcm_period_elapsed()
-> snd_pcm_update_hw_ptr0()
-> 比较 hw_ptr 和 appl_ptr
-> 若 hw_ptr 超过 appl_ptr + buffer_size (playback xrun)
或 hw_ptr - appl_ptr > buffer_size (capture xrun)
-> snd_pcm_stop_xrun()
-> snd_pcm_stop(substream, SNDRV_PCM_STATE_XRUN)
-> 通知用户空间(POLLPRI / EPOLLPRI)
应用恢复:调用 SNDRV_PCM_IOCTL_PREPARE 重新准备,再 SNDRV_PCM_IOCTL_START 重新开始。
xrun 调试通过 /proc/asound/card0/pcm0p/sub0/xrun_debug 启用详细日志(include/sound/pcm.h:505-507):
/* include/sound/pcm.h:527 */
#ifdef CONFIG_SND_PCM_XRUN_DEBUG
unsigned int xrun_debug; /* 0=disabled, 1=verbose, 2=stacktrace */
#endifDMA 缓冲区的元数据由 snd_dma_buffer 管理(include/sound/memalloc.h:55):
/* include/sound/memalloc.h:55 */
struct snd_dma_buffer {
struct snd_dma_device dev; /* 设备类型和 DMA 方向 */
unsigned char *area; /* 内核虚拟地址(CPU 访问) */
dma_addr_t addr; /* 物理总线地址(设备访问) */
size_t bytes; /* 缓冲区字节数 */
void *private_data; /* SG 表等扩展信息 */
};对应关系:
runtime->dma_area = dmab->area; /* 内核虚拟地址 */
runtime->dma_addr = dmab->addr; /* 设备物理地址 */
runtime->dma_bytes = dmab->bytes;include/sound/memalloc.h:32-50 定义了多种 DMA 内存类型:
#define SNDRV_DMA_TYPE_UNKNOWN 0 /* 未定义 */
#define SNDRV_DMA_TYPE_CONTINUOUS 1 /* 连续内存,无 DMA(如 vmalloc 模拟) */
#define SNDRV_DMA_TYPE_DEV 2 /* 设备连续 DMA 内存(dma_alloc_coherent) */
#define SNDRV_DMA_TYPE_DEV_WC 5 /* Write-Combined 连续内存 */
#define SNDRV_DMA_TYPE_DEV_IRAM 4 /* 片上 SRAM */
#define SNDRV_DMA_TYPE_VMALLOC 7 /* vmalloc 分配,不可 DMA */
#define SNDRV_DMA_TYPE_NONCONTIG 8 /* 非连续 SG 缓冲区 */
#define SNDRV_DMA_TYPE_DEV_SG 3 /* Scatter-Gather DMA 页 */
#define SNDRV_DMA_TYPE_DEV_WC_SG 6 /* WC + SG */主 API(include/sound/memalloc.h:76):
/* include/sound/memalloc.h:76 */
static inline int snd_dma_alloc_pages(int type, struct device *dev,
size_t size, struct snd_dma_buffer *dmab)
{
return snd_dma_alloc_dir_pages(type, dev, DMA_BIDIRECTIONAL, size, dmab);
}内部路由:
snd_dma_alloc_dir_pages(type, dev, dir, size, dmab)
|
+-- type == SNDRV_DMA_TYPE_DEV
| dma_alloc_coherent(dev, size, &dmab->addr, GFP_KERNEL)
| -> 分配物理连续、CPU/设备均可缓存一致的内存
|
+-- type == SNDRV_DMA_TYPE_DEV_SG
| 分配 scatter-gather 表
| 每页独立分配,通过 sg_table 链接
| dmab->area = vmap(pages, ...) (连续虚拟地址)
|
+-- type == SNDRV_DMA_TYPE_VMALLOC
| vmalloc(size) (不可 DMA,仅用于软件模拟)
|
+-- type == SNDRV_DMA_TYPE_NONCONTIG
snd_dma_alloc_pages_fallback 或 SG 方式
当物理内存碎片化无法分配连续大块内存时,使用 SG DMA:
scatter-gather 表(sg_table):
+-------+ 物理地址
| sg[0] | -> [0x10000000 ~ 0x10000FFF] (4KB page)
+-------+
| sg[1] | -> [0x20003000 ~ 0x20003FFF] (4KB page)
+-------+
| sg[2] | -> [0x30007000 ~ 0x30007FFF] (4KB page)
+-------+
...
虚拟地址(连续):
area = vmap(pages, n, VM_MAP, pgprot_noncached(...))
-> [0xFFFF800010000000 ~ 0xFFFF800010002FFF] (连续虚拟)
SG 地址查询辅助函数(include/sound/memalloc.h:97):
dma_addr_t snd_sgbuf_get_addr(struct snd_dma_buffer *dmab, size_t offset);
struct page *snd_sgbuf_get_page(struct snd_dma_buffer *dmab, size_t offset);
unsigned int snd_sgbuf_get_chunk_size(struct snd_dma_buffer *dmab,
unsigned int ofs, unsigned int size);ASoC Platform 驱动通常通过 Linux 通用 dmaengine 框架驱动 DMA 控制器:
ASoC Platform 驱动(如 sound/soc/fsl/imx-pcm-dma.c)
|
+-- dma_request_channel(mask, filter, param) 请求 DMA 通道
|
+-- dmaengine_prep_dma_cyclic(chan, 准备循环 DMA
| runtime->dma_addr, DMA 物理地址
| runtime->dma_bytes, 缓冲区大小
| runtime->period_size * frame_bytes, 每次传输大小
| direction, flags)
| -> 返回 dma_async_tx_descriptor
|
+-- descriptor->callback = snd_pcm_period_elapsed 注册完成回调
|
+-- dmaengine_submit(descriptor) 提交到 DMA 引擎
|
+-- dma_async_issue_pending(chan) 触发 DMA 开始
DMA 完成后,snd_pcm_period_elapsed() 被从 DMA 中断上下文调用,核心更新 hw_ptr 并唤醒应用。
当 DMA 类型需要显式 cache 同步时(include/sound/memalloc.h:90):
void snd_dma_buffer_sync(struct snd_dma_buffer *dmab,
enum snd_dma_sync_mode mode);
/* mode: SNDRV_DMA_SYNC_CPU -> 同步到 CPU(读之前调用)
SNDRV_DMA_SYNC_DEVICE -> 同步到设备(DMA 前调用) */用户空间: /dev/snd/controlC0
|
ioctl(SNDRV_CTL_IOCTL_*)
|
内核: sound/core/control.c
|
snd_card.controls 链表
| |
snd_kcontrol snd_kcontrol ...
(音量) (静音开关)
Control 接口的主要用途:
- 音量控制(Volume)
- 静音开关(Mute Switch)
- 枚举选择(Enum,如输入源)
- 路由选择(Mux)
- EQ 参数
- 音效开关
/* include/sound/control.h:70 */
struct snd_kcontrol {
struct list_head list; /* 链入 snd_card.controls */
struct snd_ctl_elem_id id; /* 控件标识(iface/device/name/index) */
unsigned int count; /* 相同元素的数量 */
snd_kcontrol_info_t *info; /* 查询控件信息 */
snd_kcontrol_get_t *get; /* 读取当前值 */
snd_kcontrol_put_t *put; /* 写入新值 */
union {
snd_kcontrol_tlv_rw_t *c; /* 动态 TLV(回调方式) */
const unsigned int *p; /* 静态 TLV 数据 */
} tlv;
unsigned long private_value; /* 驱动私有值(常用来编码寄存器地址/位) */
void *private_data;
void (*private_free)(struct snd_kcontrol *kcontrol);
struct snd_kcontrol_volatile vd[] __counted_by(count);
};三个核心回调函数类型(include/sound/control.h:17-23):
typedef int (snd_kcontrol_info_t)(struct snd_kcontrol *kcontrol,
struct snd_ctl_elem_info *uinfo);
typedef int (snd_kcontrol_get_t)(struct snd_kcontrol *kcontrol,
struct snd_ctl_elem_value *ucontrol);
typedef int (snd_kcontrol_put_t)(struct snd_kcontrol *kcontrol,
struct snd_ctl_elem_value *ucontrol);每个控件有唯一的 snd_ctl_elem_id,包含:
/* include/uapi/sound/asound.h */
struct snd_ctl_elem_id {
unsigned int numid; /* 数字 ID(内核分配,唯一) */
snd_ctl_elem_iface_t iface; /* 接口类型:CARD/HWDEP/MIXER/PCM/RAWMIDI/TIMER/SEQUENCER */
unsigned int device; /* 设备编号(用于 PCM/RAWMIDI) */
unsigned int subdevice; /* 子设备编号 */
unsigned char name[44]; /* 人类可读名称(如 "Master Playback Volume") */
unsigned int index; /* 同名控件的索引 */
};ALSA 控件命名规范(alsa-lib 约定):
"Master Playback Volume" -- 主音量
"Master Playback Switch" -- 主静音
"PCM Playback Volume" -- PCM 播放音量
"Capture Volume" -- 录音音量
"Capture Switch" -- 录音开关
"Input Source" -- 输入源选择(枚举)
"Headphone Jack" -- 耳机插入检测(开关)
/* include/sound/control.h:47 */
struct snd_kcontrol_new {
snd_ctl_elem_iface_t iface;
unsigned int device;
unsigned int subdevice;
const char *name;
unsigned int index;
unsigned int access; /* SNDRV_CTL_ELEM_ACCESS_* 权限位 */
unsigned int count;
snd_kcontrol_info_t *info;
snd_kcontrol_get_t *get;
snd_kcontrol_put_t *put;
union {
snd_kcontrol_tlv_rw_t *c;
const unsigned int *p;
} tlv;
unsigned long private_value;
};典型使用(以 AC97 音量控件为例):
static const struct snd_kcontrol_new my_volume = {
.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
.name = "Master Playback Volume",
.info = snd_ac97_info_volsw,
.get = snd_ac97_get_volsw,
.put = snd_ac97_put_volsw,
.private_value = AC97_MASTER | (31 << 8) | (0 << 16),
};
snd_ctl_add(card, snd_ctl_new1(&my_volume, ac97));/* include/sound/control.h:134-144 */
struct snd_kcontrol *snd_ctl_new1(const struct snd_kcontrol_new *kcontrolnew,
void *private_data);
void snd_ctl_free_one(struct snd_kcontrol *kcontrol);
int snd_ctl_add(struct snd_card *card, struct snd_kcontrol *kcontrol);
int snd_ctl_remove(struct snd_card *card, struct snd_kcontrol *kcontrol);
int snd_ctl_replace(struct snd_card *card, struct snd_kcontrol *kcontrol,
bool add_on_replace);
int snd_ctl_rename_id(struct snd_card *card, struct snd_ctl_elem_id *src_id,
struct snd_ctl_elem_id *dst_id);
struct snd_kcontrol *snd_ctl_find_numid(struct snd_card *card, unsigned int numid);
struct snd_kcontrol *snd_ctl_find_id(struct snd_card *card,
const struct snd_ctl_elem_id *id);控件查找优化:当 CONFIG_SND_CTL_FAST_LOOKUP 开启时,snd_card 内维护两张 XArray 哈希表(include/sound/core.h:107-110):
#ifdef CONFIG_SND_CTL_FAST_LOOKUP
struct xarray ctl_numids; /* numid -> kcontrol */
struct xarray ctl_hash; /* id hash -> kcontrol */
bool ctl_hash_collision;
#endifTLV(Tag-Length-Value)用于描述音量与 dB 的映射关系,支持非线性音量曲线。常见 TLV 类型:
| TLV 类型 | 含义 |
|---|---|
SNDRV_CTL_TLVT_DB_SCALE |
线性 dB 刻度(步进固定) |
SNDRV_CTL_TLVT_DB_LINEAR |
线性增益(非 dB) |
SNDRV_CTL_TLVT_DB_RANGE |
分段不同步进 |
SNDRV_CTL_TLVT_DB_MINMAX |
最小最大 dB |
声卡驱动定义 TLV:
/* 从 -9450 mB 到 0 mB,步进 150 mB(约 1.5 dB) */
static const DECLARE_TLV_DB_SCALE(master_tlv, -9450, 150, 0);
static const struct snd_kcontrol_new my_vol = {
...
.access = SNDRV_CTL_ELEM_ACCESS_TLV_READ | SNDRV_CTL_ELEM_ACCESS_READWRITE,
.tlv.p = master_tlv,
};alsa-lib 读取 TLV 后,将寄存器值转换为 dB 显示给用户。PulseAudio/PipeWire 用此实现感知均匀的音量调节。
当控件值改变时,驱动应调用(include/sound/control.h:131-132):
void snd_ctl_notify(struct snd_card *card, unsigned int mask,
struct snd_ctl_elem_id *id);
void snd_ctl_notify_one(struct snd_card *card, unsigned int mask,
struct snd_kcontrol *kctl, unsigned int ioff);用户空间通过 poll() 或 read() /dev/snd/controlC* 接收事件(SNDRV_CTL_EVENT_MASK_VALUE / SNDRV_CTL_EVENT_MASK_INFO)。
标准 ALSA 驱动将 CPU 侧逻辑、Codec 逻辑和电源管理混在一起,难以复用。ASoC(Audio System on Chip)框架将嵌入式音频系统分解为三个独立可复用的组件:
+------------------------------------------------------------------+
| ASoC 三大组件 |
| |
| +------------------+ +------------------+ +----------------+ |
| | CPU DAI | | Codec | | Platform | |
| | (SoC 音频接口) | | (编解码芯片) | | (DMA/内存) | |
| | | | | | | |
| | - I2S/TDM/PDM | | - ADC/DAC | | - DMA 引擎 | |
| | - 时钟生成 | | - 音量控制 | | - PCM 缓冲区 | |
| | - BCLK/LRCLK | | - 混音器 | | - Compress | |
| +--------+---------+ +--------+---------+ +--------+-------+ |
| | | | |
| +----------+-----------+--------------------+ |
| | |
| snd_soc_dai_link |
| (三者的连接描述) |
+------------------------------------------------------------------+
|
snd_soc_card(机器驱动,描述整个音频系统)
|
snd_card(ALSA 核心声卡对象)
/* include/sound/soc.h:972 */
struct snd_soc_card {
const char *name;
const char *long_name;
const char *driver_name;
const char *components;
struct device *dev;
struct snd_card *snd_card; /* 指向 ALSA snd_card */
struct module *owner;
struct mutex mutex;
struct mutex dapm_mutex;
/* 回调 */
int (*probe)(struct snd_soc_card *card);
int (*late_probe)(struct snd_soc_card *card);
void (*fixup_controls)(struct snd_soc_card *card);
int (*remove)(struct snd_soc_card *card);
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);
int (*set_bias_level)(struct snd_soc_card *,
struct snd_soc_dapm_context *dapm,
enum snd_soc_bias_level level);
/* DAI 链接 */
struct snd_soc_dai_link *dai_link;
int num_links;
struct list_head rtd_list;
int num_rtd;
/* 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;
struct snd_soc_dapm_context *dapm;
/* 控件 */
const struct snd_kcontrol_new *controls;
int num_controls;
long pmdown_time;
unsigned int instantiated:1;
unsigned int fully_routed:1;
void *drvdata;
};/* include/sound/soc.h 中的 snd_soc_dai_link(约 750 行区域)*/
struct snd_soc_dai_link {
const char *name;
const char *stream_name;
/* CPU / Codec / Platform 组件 */
struct snd_soc_dai_link_component *cpus;
unsigned int num_cpus;
struct snd_soc_dai_link_component *codecs;
unsigned int num_codecs;
struct snd_soc_dai_link_component *platforms;
unsigned int num_platforms;
int id; /* 可选的 ID,用于 DPCM */
const struct snd_soc_ops *ops;
const struct snd_soc_compr_ops *compr_ops;
/* 格式协商 */
unsigned int dai_fmt; /* SND_SOC_DAIFMT_* */
/* 特性标志 */
unsigned int dynamic:1; /* DPCM 动态链接 */
unsigned int dpcm_playback:1;
unsigned int dpcm_capture:1;
unsigned int ignore_pmdown_time:1;
unsigned int ignore:1; /* 后端 BE,不创建 PCM 设备 */
};使用宏定义 DAI Link(include/sound/soc.h:912):
/* include/sound/soc.h:925 */
SND_SOC_DAILINK_DEFS(primary,
DAILINK_COMP_ARRAY(COMP_CPU("i2s-controller")),
DAILINK_COMP_ARRAY(COMP_CODEC("wm8960.1-001a", "wm8960-hifi")),
DAILINK_COMP_ARRAY(COMP_PLATFORM("imx-pcm-audio")));
static struct snd_soc_dai_link my_card_dai_links[] = {
{
.name = "HiFi",
.stream_name = "HiFi",
.dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF
| SND_SOC_DAIFMT_CBP_CFP,
SND_SOC_DAILINK_REG(primary),
},
};/* include/sound/soc.h:1143 */
struct snd_soc_pcm_runtime {
struct device *dev;
struct snd_soc_card *card;
struct snd_soc_dai_link *dai_link;
struct snd_pcm_ops ops; /* 覆盖 PCM ops(包装驱动 ops) */
struct snd_pcm *pcm; /* 关联的 ALSA PCM 设备 */
struct snd_soc_dai **dais; /* CPU DAI + Codec DAI 数组 */
/* 延迟工作(pmdown_time 后关闭电源) */
struct delayed_work delayed_work;
/* function mark(防止重入) */
struct snd_pcm_substream *mark_startup;
struct snd_pcm_substream *mark_hw_params;
struct snd_pcm_substream *mark_trigger;
int num_components;
struct snd_soc_component *components[] __counted_by(num_components);
};
/* 访问宏:include/sound/soc.h:1196 */
#define snd_soc_rtd_to_cpu(rtd, n) (rtd)->dais[n]
#define snd_soc_rtd_to_codec(rtd, n) (rtd)->dais[n + (rtd)->dai_link->num_cpus]/* include/sound/soc.h:449 */
int snd_soc_component_initialize(struct snd_soc_component *component,
const struct snd_soc_component_driver *driver,
struct device *dev);
int snd_soc_add_component(struct snd_soc_component *component,
struct snd_soc_dai_driver *dai_drv,
int num_dai);组件驱动(snd_soc_component_driver)包含:
struct snd_soc_component_driver {
const char *name;
/* PCM 操作 */
int (*open)(struct snd_soc_component *, struct snd_pcm_substream *);
int (*close)(struct snd_soc_component *, struct snd_pcm_substream *);
int (*hw_params)(struct snd_soc_component *, struct snd_pcm_substream *,
struct snd_pcm_hw_params *);
int (*hw_free)(struct snd_soc_component *, struct snd_pcm_substream *);
int (*prepare)(struct snd_soc_component *, struct snd_pcm_substream *);
int (*trigger)(struct snd_soc_component *, struct snd_pcm_substream *, int);
snd_pcm_uframes_t (*pointer)(struct snd_soc_component *,
struct snd_pcm_substream *);
/* 控件和 DAPM */
const struct snd_kcontrol_new *controls;
int num_controls;
const struct snd_soc_dapm_widget *dapm_widgets;
int num_dapm_widgets;
const struct snd_soc_dapm_route *dapm_routes;
int num_dapm_routes;
/* 电源管理 */
int (*suspend)(struct snd_soc_component *);
int (*resume)(struct snd_soc_component *);
...
};dai_fmt 字段通过位掩码描述 I2S 总线格式:
SND_SOC_DAIFMT_I2S 标准 I2S(左通道数据在 LRCLK 上升沿后)
SND_SOC_DAIFMT_LEFT_J Left-Justified
SND_SOC_DAIFMT_RIGHT_J Right-Justified
SND_SOC_DAIFMT_DSP_A DSP/PCM 模式 A
SND_SOC_DAIFMT_DSP_B DSP/PCM 模式 B
SND_SOC_DAIFMT_PDM 脉冲密度调制
SND_SOC_DAIFMT_NB_NF 正常极性(BCLK正常, LRCLK正常)
SND_SOC_DAIFMT_NB_IF LRCLK 反转
SND_SOC_DAIFMT_IB_NF BCLK 反转
SND_SOC_DAIFMT_CBP_CFP Codec 提供 BCLK 和 LRCLK(旧: CODEC_MASTER)
SND_SOC_DAIFMT_CBC_CFC CPU 提供 BCLK 和 LRCLK(旧: CPU_MASTER)
嵌入式音频系统中,Codec 芯片内部有大量可独立开关的模块(ADC、DAC、PGA、Mixer、PLL 等),这些模块的开关状态直接影响功耗。
DAPM(Dynamic Audio Power Management)的目标:
- 根据当前激活的音频路径自动计算哪些 widget 需要上电
- 在路径不再使用时自动下电
- 所有操作对应用透明,无需手动干预
Widget 是 DAPM 图中的节点,每个 widget 对应 Codec 内部的一个功能模块(include/sound/soc-dapm.h:516):
/* include/sound/soc-dapm.h:516 */
struct snd_soc_dapm_widget {
enum snd_soc_dapm_type id; /* widget 类型 */
const char *name;
const char *sname; /* 关联的 PCM 流名称 */
struct list_head list;
struct snd_soc_dapm_context *dapm;
/* 硬件控制 */
int reg; /* 负值 = 无直接寄存器控制 */
unsigned char shift;
unsigned int mask;
unsigned int on_val;
unsigned int off_val;
/* 状态标志 */
unsigned char power:1;
unsigned char active:1;
unsigned char connected:1;
unsigned char force:1;
unsigned char ignore_suspend:1;
unsigned char is_supply:1;
unsigned char is_ep:2; /* 是否为端点(source/sink) */
int subseq; /* 同类型内的排序 */
int (*power_check)(struct snd_soc_dapm_widget *w);
/* 事件回调 */
unsigned short event_flags;
int (*event)(struct snd_soc_dapm_widget *, struct snd_kcontrol *, int);
/* 关联的控件 */
int num_kcontrols;
const struct snd_kcontrol_new *kcontrol_news;
struct snd_kcontrol **kcontrols;
/* 图连接(边) */
struct list_head edges[2]; /* edges[0]=inputs, edges[1]=outputs */
struct list_head work_list;
struct list_head power_list;
struct list_head dirty;
int endpoints[2]; /* 可达端点数 */
struct clk *clk;
int channel;
};Widget 类型(snd_soc_dapm_type):
端点类型:
snd_soc_dapm_input 外部输入引脚(如 MIC_IN)
snd_soc_dapm_output 外部输出引脚(如 HP_OUT)
snd_soc_dapm_mic 麦克风
snd_soc_dapm_hp 耳机
snd_soc_dapm_spk 扬声器
snd_soc_dapm_line Line 输入/输出
信号处理:
snd_soc_dapm_pga 可编程增益放大器(PGA)
snd_soc_dapm_mixer 混音器
snd_soc_dapm_mux 多路选择器
snd_soc_dapm_adc ADC(模数转换)
snd_soc_dapm_dac DAC(数模转换)
供电:
snd_soc_dapm_supply 电源域(如 AVDD、MICBIAS)
snd_soc_dapm_regulator_supply 调压器
snd_soc_dapm_clock_supply 时钟源
snd_soc_dapm_vmid 虚地参考
流域:
snd_soc_dapm_aif_in AIF 输入(来自 CPU DAI)
snd_soc_dapm_aif_out AIF 输出(到 CPU DAI)
/* include/sound/soc-dapm.h:473 */
struct snd_soc_dapm_route {
const char *sink; /* 目标 widget 名称 */
const char *control; /* 控制此路由的 kcontrol 名称(NULL = 总是连通) */
const char *source; /* 来源 widget 名称 */
int (*connected)(struct snd_soc_dapm_widget *source,
struct snd_soc_dapm_widget *sink);
struct snd_soc_dobj dobj;
};典型路由定义示例(WM8960 Codec):
static const struct snd_soc_dapm_route wm8960_dapm_routes[] = {
/* ADC 路径:MIC -> PGA -> ADC */
{ "Left Input Mixer", "Boost Switch", "LINPUT1" },
{ "Left Boost Mixer", NULL, "Left Input Mixer" },
{ "Left ADC", NULL, "Left Boost Mixer" },
/* DAC 路径:DAC -> Mixer -> HP */
{ "Left Output Mixer", "PCM Playback Switch", "Left DAC" },
{ "HP_L", NULL, "Left Output PGA" },
{ "Left Output PGA", NULL, "Left Output Mixer" },
/* 电源路径:MICBIAS 供给所有输入混音器 */
{ "MICB", NULL, "MICBIAS" },
};路由图可视化(播放路径示例):
AIF IN (I2S)
|
v
Left DAC --------+
|
Left Output Mixer (kcontrol: "PCM Playback Switch")
|
Left Output PGA (寄存器控制增益)
|
HP_L (耳机左声道输出 pin)
|
[物理引脚 HP_OUT_L]
当用户打开 PCM 设备或调整路由控件时,DAPM 执行路径分析:
触发事件(PCM open / kcontrol change)
|
+-- 将受影响的 widget 加入 dirty 列表
|
+-- dapm_power_widgets()
|
+-- 遍历所有 widget,调用 widget->power_check()
| 对每个 widget 检查:是否存在从 source 端点
| 到 sink 端点的活跃路径?
|
+-- 计算需要上电和下电的 widget 集合
|
+-- 按 subseq 排序(电源域先于信号路径)
|
+-- 对需要上电的 widget:
| 调用 widget->event(SND_SOC_DAPM_PRE_PMU)
| 写寄存器(reg/shift/on_val)
| 调用 widget->event(SND_SOC_DAPM_POST_PMU)
|
+-- 对需要下电的 widget:
调用 widget->event(SND_SOC_DAPM_PRE_PMD)
写寄存器(reg/shift/off_val)
调用 widget->event(SND_SOC_DAPM_POST_PMD)
事件标志(event_flags):
SND_SOC_DAPM_PRE_PMU 上电前
SND_SOC_DAPM_POST_PMU 上电后
SND_SOC_DAPM_PRE_PMD 下电前
SND_SOC_DAPM_POST_PMD 下电后
SND_SOC_DAPM_PRE_REG 寄存器写前
SND_SOC_DAPM_POST_REG 寄存器写后
DAPM 还管理 Codec 的偏置电平(snd_soc_bias_level):
SND_SOC_BIAS_OFF 完全关闭(无电流)
SND_SOC_BIAS_STANDBY 待机(保持充电,快速恢复)
SND_SOC_BIAS_PREPARE 准备中(时钟运行但无信号)
SND_SOC_BIAS_ON 完全开启(信号路径活跃)
snd_soc_card.set_bias_level() 回调允许机器驱动在偏置电平切换时执行额外操作(如控制外部 LDO)。
为避免频繁上下电导致的 pop/click 噪声,ASoC 引入 pmdown_time 延迟机制:
/* include/sound/soc.h:1029 */
long pmdown_time; /* 停止播放后延迟下电的时间(ms,默认 5000ms)*/PCM 关闭时,通过 delayed_work 延迟 pmdown_time 毫秒后才真正执行 DAPM 下电。
MIDI(Musical Instrument Digital Interface)是乐器与计算机之间的数字通信协议,传递的不是 PCM 音频数据,而是演奏事件(音符开/关、音量、音色等)。
MIDI 消息格式:
Status byte: 1TTTNNNN
T = 类型 (0=Note Off, 1=Note On, 2=After Touch, 3=Control, 4=Program, 5=Channel Pressure, 6=Pitch Bend, 7=系统消息)
N = 通道编号 (0-15)
Data bytes: 0DDDDDDD (最高位=0, 7位数据)
典型消息:
Note On: 0x9n key velocity (n=channel)
Note Off: 0x8n key velocity
Control: 0xBn ctrl value (ctrl=7: 音量, ctrl=10: 声像)
Program: 0xCn program (切换音色)
Pitch: 0xEn LSB MSB (弯音, 14bit)
/* include/sound/rawmidi.h */
struct snd_rawmidi_runtime {
struct snd_rawmidi_substream *substream;
unsigned int drain: 1;
unsigned int oss: 1;
/* MIDI 流缓冲区 */
unsigned char *buffer; /* 环形缓冲区 */
size_t buffer_size;
size_t appl_ptr; /* 应用指针 */
size_t hw_ptr; /* 硬件指针 */
size_t avail_min;
size_t avail;
size_t xruns;
size_t align; /* 对齐(0=字节流, 3=UMP 4字节对齐) */
wait_queue_head_t sleep;
void (*event)(struct snd_rawmidi_substream *substream);
struct work_struct event_work;
};MIDI 操作集(include/sound/rawmidi.h:39):
/* include/sound/rawmidi.h:39 */
struct snd_rawmidi_ops {
int (*open)(struct snd_rawmidi_substream *substream);
int (*close)(struct snd_rawmidi_substream *substream);
void (*trigger)(struct snd_rawmidi_substream *substream, int up);
void (*drain)(struct snd_rawmidi_substream *substream);
};接收(硬件→软件,include/sound/rawmidi.h:166):
int snd_rawmidi_receive(struct snd_rawmidi_substream *substream,
const unsigned char *buffer, int count);发送(软件→硬件):
/* include/sound/rawmidi.h:168 */
int snd_rawmidi_transmit_empty(struct snd_rawmidi_substream *substream);
int snd_rawmidi_transmit_peek(struct snd_rawmidi_substream *substream,
unsigned char *buffer, int count);
int snd_rawmidi_transmit_ack(struct snd_rawmidi_substream *substream, int count);
int snd_rawmidi_transmit(struct snd_rawmidi_substream *substream,
unsigned char *buffer, int count);
int snd_rawmidi_proceed(struct snd_rawmidi_substream *substream);发送流程(peek+ack 模式,适用于 FIFO 驱动):
驱动发送中断:
1. snd_rawmidi_transmit_peek(sub, buf, max) -- 窥视数据,不移动指针
2. 将 buf 写入硬件 FIFO
3. snd_rawmidi_transmit_ack(sub, actually_sent) -- 确认已发送字节数
4. 若缓冲区清空,停止发送中断
接收流程:
硬件接收中断:
1. 从硬件读取 MIDI 字节
2. snd_rawmidi_receive(sub, buf, count) -- 写入环形缓冲区
3. 环形缓冲区满时,xruns++ 并丢弃数据
4. 唤醒 runtime->sleep(用户 read 等待)
Linux 6.x 新增对 MIDI 2.0 UMP 协议的支持,使用 4 字节对齐(align=3)的包格式,通过 snd_ump 设备暴露。
ALSA 在 /dev/snd/ 下创建多种字符设备:
/dev/snd/controlC<N> 声卡控制接口
/dev/snd/pcmC<N>D<M>p 播放 PCM
/dev/snd/pcmC<N>D<M>c 录音 PCM
/dev/snd/midiC<N>D<M> RAW MIDI
/dev/snd/seq ALSA Sequencer
/dev/snd/timer 全局 Timer
主设备号 116(include/sound/core.h:26),次设备号由 snd_minor 结构体管理(include/sound/core.h:215):
/* include/sound/core.h:215 */
struct snd_minor {
int type; /* SNDRV_DEVICE_TYPE_XXX */
int card; /* 声卡编号 */
int device; /* 设备编号 */
const struct file_operations *f_ops;
void *private_data;
struct device *dev;
struct snd_card *card_ptr;
};用户程序通过 ioctl 与 PCM 设备交互,主要命令(定义于 include/uapi/sound/asound.h):
SNDRV_PCM_IOCTL_PVERSION -- 获取协议版本
SNDRV_PCM_IOCTL_INFO -- 获取 PCM 设备信息
SNDRV_PCM_IOCTL_TSTAMP -- 设置时间戳模式
SNDRV_PCM_IOCTL_TTSTAMP -- 设置时间戳类型
SNDRV_PCM_IOCTL_HW_REFINE -- 精化硬件参数(不应用)
SNDRV_PCM_IOCTL_HW_PARAMS -- 应用硬件参数
SNDRV_PCM_IOCTL_HW_FREE -- 释放硬件参数
SNDRV_PCM_IOCTL_SW_PARAMS -- 设置软件参数
SNDRV_PCM_IOCTL_STATUS -- 获取运行状态(hw_ptr/appl_ptr)
SNDRV_PCM_IOCTL_DELAY -- 获取延迟帧数
SNDRV_PCM_IOCTL_HWSYNC -- 同步 hw_ptr
SNDRV_PCM_IOCTL_SYNC_PTR -- 同步 hw_ptr 和 appl_ptr(mmap 模式)
SNDRV_PCM_IOCTL_PREPARE -- 准备播放/录音
SNDRV_PCM_IOCTL_RESET -- 重置指针
SNDRV_PCM_IOCTL_START -- 开始传输(trigger)
SNDRV_PCM_IOCTL_DROP -- 立即停止,丢弃剩余数据
SNDRV_PCM_IOCTL_DRAIN -- 等待播放完成后停止
SNDRV_PCM_IOCTL_PAUSE -- 暂停/继续
SNDRV_PCM_IOCTL_REWIND -- 回退 appl_ptr
SNDRV_PCM_IOCTL_FORWARD -- 前进 appl_ptr
SNDRV_PCM_IOCTL_WRITEI_FRAMES -- 交织写入 PCM 帧
SNDRV_PCM_IOCTL_READI_FRAMES -- 交织读取 PCM 帧
SNDRV_PCM_IOCTL_WRITEN_FRAMES -- 非交织写入
SNDRV_PCM_IOCTL_READN_FRAMES -- 非交织读取
SNDRV_PCM_IOCTL_LINK -- 链接多个 substream
SNDRV_PCM_IOCTL_UNLINK -- 解链
用户程序:
fd = open("/dev/snd/pcmC0D0p", O_RDWR)
|
v
ioctl(fd, SNDRV_PCM_IOCTL_HW_PARAMS, &hw_params)
-- 设置 format=S16_LE, channels=2, rate=44100
-- period_size=1024帧, periods=4
|
v
ioctl(fd, SNDRV_PCM_IOCTL_SW_PARAMS, &sw_params)
-- 设置 start_threshold, stop_threshold, avail_min
|
v
mmap(NULL, buffer_size, PROT_RW, MAP_SHARED, fd, 0)
-- 获得 DMA 缓冲区的虚拟地址(可选)
|
v
ioctl(fd, SNDRV_PCM_IOCTL_PREPARE)
|
v
循环:
poll(fd, POLLIN, timeout) -- 等待有空间可写
写入数据到 mmap 缓冲区(或用 write() 系统调用)
ioctl(fd, SNDRV_PCM_IOCTL_SYNC_PTR, &sync_ptr) -- 更新 appl_ptr
|
v(首次写入后超过 start_threshold,自动触发)
ioctl(fd, SNDRV_PCM_IOCTL_DRAIN) -- 等待播放完成
|
v
close(fd)
alsa-lib(libasound)对上述 ioctl 进行了高层封装:
/* alsa-lib API 示例(非内核代码) */
snd_pcm_t *pcm;
snd_pcm_open(&pcm, "default", SND_PCM_STREAM_PLAYBACK, 0);
snd_pcm_hw_params_t *params;
snd_pcm_hw_params_alloca(¶ms);
snd_pcm_hw_params_any(pcm, params);
snd_pcm_hw_params_set_format(pcm, params, SND_PCM_FORMAT_S16_LE);
snd_pcm_hw_params_set_channels(pcm, params, 2);
snd_pcm_hw_params_set_rate(pcm, params, 44100, 0);
snd_pcm_hw_params(pcm, params);
snd_pcm_writei(pcm, buf, frames); /* 阻塞写入 */alsa-lib 的核心特性:
- PCM 插件链:
plughw(自动格式转换)、dmix(软件混音)、dsnoop(共享录音)、null(丢弃)、file(写文件)等。 - 配置文件解析:
~/.asoundrc和/etc/alsa/alsa.conf定义设备别名和插件链。 - 设备命名:
default、hw:0,0、plughw:0,0、pulse(通过 PulseAudio 插件)等。
PipeWire 是下一代 Linux 音频/视频服务器,目标是统一 PulseAudio(桌面音频)和 JACK(专业低延迟音频)的使用场景。
+--------------------------------------------------------+
| 应用程序 |
| PulseAudio APP | JACK APP | GStreamer | Pipewire APP |
+----+------------+---+------+----+------+----+----------+
| PA兼容 | JACK兼容 | GStreamer插件 | 原生
v v v v
+--------------------------------------------------+
| PipeWire 守护进程 |
| |
| Session Manager (WirePlumber) |
| 策略路由 / 设备枚举 / 权限管理 |
| |
| Filter Graph: |
| source -> resample -> mix -> sink |
| |
| 低延迟调度 (SCHED_FIFO / memfd) |
+--------------------+-----------------------------+
|
+-----------+----------+
| |
ALSA (PCM) Bluetooth (BlueZ)
/dev/snd/pcmC0D0p A2DP / HFP
PipeWire 相比 PulseAudio 的优势:
| 特性 | PulseAudio | PipeWire |
|---|---|---|
| 最低延迟 | ~50ms | <1ms(JACK 兼容模式) |
| 视频支持 | 无 | 支持(V4L2、libcamera) |
| Flatpak 隔离 | 有限 | 完整 portal 支持 |
| Wayland 集成 | 需适配 | 原生支持 |
| 蓝牙 Codec | SBC | aptX/LDAC/LC3 |
| 配置复杂度 | 复杂 | 统一 WirePlumber 脚本 |
PipeWire 通过 ALSA PCM mmap 模式 与内核交互,使用 POSIX timerfd 和 memfd(共享内存)在进程间零拷贝传递音频数据。
ALSA Sequencer(/dev/snd/seq)提供 MIDI 事件的时序调度和路由,支持:
- 事件队列(带时间戳的 MIDI 事件)
- 端口(port)和连接(connection)管理
- 与 snd_rawmidi 桥接
- 软件合成器(fluidsynth)连接
sound/
core/ ALSA 核心
init.c -- snd_card_new(), snd_card_register()
pcm.c -- snd_pcm_new(), PCM 设备管理
pcm_native.c -- PCM ioctl 处理(hw_params/trigger等)
pcm_lib.c -- snd_pcm_period_elapsed(), xrun 处理
pcm_memory.c -- DMA 缓冲区分配/释放
control.c -- snd_ctl_add(), ioctl 处理
rawmidi.c -- MIDI 子系统
timer.c -- 全局 timer
info.c -- /proc/asound/ 接口
sound.c -- 字符设备注册,major=116
memalloc.c -- snd_dma_alloc_pages() 实现
soc/ ASoC 框架
soc-core.c -- snd_soc_register_card(), 组件管理
soc-pcm.c -- ASoC PCM ops 实现
soc-dapm.c -- DAPM 图算法
soc-component.c -- snd_soc_component 管理
soc-dai.c -- DAI 操作封装
soc-topology.c -- 固件拓扑加载
codecs/ -- Codec 驱动(wm8960, rt5651, ...)
generic/ -- 通用平台驱动
drivers/ 通用驱动
dma-engine.c -- dmaengine 平台驱动
dummy.c -- Dummy 声卡(测试用)
pci/ PCI 声卡驱动
hda/ -- Intel HD Audio
hda_intel.c -- HDA 控制器驱动
hda_codec.c -- HDA Codec 驱动框架
patch_realtek.c -- Realtek Codec 补丁
usb/ USB 音频
card.c -- USB Audio Class 驱动
pcm.c -- USB PCM 子系统
mixer.c -- USB 混音器
arm/ ARM 平台驱动(legacy)
x86/ x86 平台特有(HDMI/DP audio)
# 查看所有声卡
cat /proc/asound/cards
0 [HDAIntel ]: HDA-Intel - HDA Intel PCH
HDA Intel PCH at 0xdf340000 irq 139
# 查看所有 PCM 设备
cat /proc/asound/devices
2: [ 0- 0]: digital audio playback
3: [ 0- 0]: digital audio capture
4: [ 0- 1]: digital audio playback
8: [ 0] : control
# 查看 PCM 状态(播放时)
cat /proc/asound/card0/pcm0p/sub0/status
state: RUNNING
owner_pid : 1234
trigger_time: 12345678.012345678
tstamp : 12345678.023456789
delay : 1024
avail : 3072
avail_max : 3072
hw_ptr : 49152
appl_ptr : 50176
# 查看当前硬件参数
cat /proc/asound/card0/pcm0p/sub0/hw_params
access: MMAP_INTERLEAVED
format: S16_LE
subformat: STD
channels: 2
rate: 44100 (44100/1)
period_size: 1024
buffer_size: 4096
# 开启 xrun 调试(stacktrace)
echo 2 > /proc/asound/card0/pcm0p/xrun_debug# 列出所有播放设备
aplay -l
# 测试播放(WAV 文件)
aplay -D hw:0,0 test.wav
# 测试录音
arecord -D hw:0,0 -f S16_LE -r 44100 -c 2 out.wav
# 查看/修改混音器
alsamixer # TUI 界面
amixer set Master 80% # 命令行设置音量
amixer contents # 列出所有控件
# 显示声卡信息
alsainfo
# MIDI 播放
aplaymidi -p 20:0 test.mid与 ALSA 相关的 Kconfig 调试选项:
CONFIG_SND_DEBUG -- 开启 ALSA 调试信息
CONFIG_SND_DEBUG_VERBOSE -- 详细调试输出
CONFIG_SND_PCM_XRUN_DEBUG -- PCM xrun 调试
CONFIG_SND_CTL_DEBUG -- Control 接口校验
CONFIG_SND_PCM_OSS -- OSS 兼容层
CONFIG_SND_DYNAMIC_MINORS -- 动态次设备号(> 8 块声卡)
CONFIG_SND_MAX_CARDS -- 最大声卡数(动态模式)
当 CONFIG_SND_DEBUG 开启时,/sys/kernel/debug/sound/ 提供额外调试信息:
/sys/kernel/debug/sound/
card0/
hda-codec-0/ -- HDA Codec 寄存器 dump
hda-codec-1/
seq/ -- Sequencer 内部状态
# 检查 ALSA PCM 子流是否被打开
ls -la /proc/$(pidof pulseaudio)/fd | grep snd
# 通过 strace 观察 ioctl 调用
strace -e ioctl aplay test.wav 2>&1 | grep SNDRV
# 查看 DMA 缓冲区分配情况(总量限制)
cat /proc/asound/card0/pcm0p/sub0/hw_params
# 使用 latency-test 评估 RTT 延迟(JACK 工具)
jack_iodelaysnd_card 声卡顶层对象(include/sound/core.h:80)
snd_device 挂载到声卡的子设备(include/sound/core.h:67)
snd_pcm PCM 设备(include/sound/pcm.h:534)
snd_pcm_str PCM 方向流(include/sound/pcm.h:513)
snd_pcm_substream PCM 子流(include/sound/pcm.h:464)
snd_pcm_runtime PCM 运行时状态(include/sound/pcm.h:362)
snd_pcm_hardware 硬件能力描述(include/sound/pcm.h:32)
snd_pcm_ops 驱动操作集(include/sound/pcm.h:55)
snd_pcm_hw_constraints 参数约束集(include/sound/pcm.h:256)
snd_dma_buffer DMA 缓冲区元数据(include/sound/memalloc.h:55)
snd_dma_device DMA 设备描述(include/sound/memalloc.h:22)
snd_kcontrol 控件实例(include/sound/control.h:70)
snd_kcontrol_new 控件模板(include/sound/control.h:47)
snd_ctl_file 已打开的控制文件(include/sound/control.h:105)
snd_soc_card ASoC 声卡(include/sound/soc.h:972)
snd_soc_dai_link DAI 连接描述(include/sound/soc.h ~750行区域)
snd_soc_pcm_runtime ASoC PCM 运行时(include/sound/soc.h:1143)
snd_soc_dapm_widget DAPM 节点(include/sound/soc-dapm.h:516)
snd_soc_dapm_route DAPM 路由(include/sound/soc-dapm.h:473)
snd_rawmidi_runtime MIDI 运行时(include/sound/rawmidi.h:57)
snd_rawmidi_ops MIDI 操作集(include/sound/rawmidi.h:39)
SNDRV_PCM_FMTBIT_S8 8位有符号
SNDRV_PCM_FMTBIT_U8 8位无符号
SNDRV_PCM_FMTBIT_S16_LE 16位有符号小端(CD 标准)
SNDRV_PCM_FMTBIT_S24_3LE 24位有符号小端(3字节紧凑)
SNDRV_PCM_FMTBIT_S24_LE 24位有符号小端(4字节左对齐)
SNDRV_PCM_FMTBIT_S32_LE 32位有符号小端(32位容器,可能24/20有效位)
SNDRV_PCM_FMTBIT_FLOAT_LE 32位浮点小端
SNDRV_PCM_FMTBIT_DSD_U8 DSD(直接流数字)1位PDM,8位打包
SNDRV_PCM_FMTBIT_DSD_U32_LE DSD 32位打包小端
// include/sound/pcm.h:148-202 中完整定义
每帧(frame)字节数 = channels × sample_bits / 8。
例:S16_LE, stereo → 2 × 16 / 8 = 4 字节/帧。
// include/sound/pcm.h:109-129
SNDRV_PCM_RATE_8000 = (1U<<1) // 电话
SNDRV_PCM_RATE_16000 = (1U<<3) // 语音识别
SNDRV_PCM_RATE_44100 = (1U<<6) // CD 标准
SNDRV_PCM_RATE_48000 = (1U<<7) // 专业音频/数字广播
SNDRV_PCM_RATE_96000 = (1U<<10) // Hi-Res Audio
SNDRV_PCM_RATE_192000 = (1U<<12) // Hi-Res Audio
SNDRV_PCM_RATE_384000 = (1U<<14) // DSD 等效
SNDRV_PCM_RATE_768000 = (1U<<16) // DSD512
// 6.12 新增(include/sound/pcm.h:127-129)
SNDRV_PCM_RATE_12000 = (1U<<17)
SNDRV_PCM_RATE_24000 = (1U<<18)
SNDRV_PCM_RATE_128000 = (1U<<19)由 Claude Code 分析生成