3. ALSA 核心数据结构:snd_card、snd_pcm、snd_control、snd_rawmidi 结构体解析
ALSA 驱动层的核心围绕四个关键数据结构展开:snd_card(声卡实例)、snd_pcm(PCM 设备)、snd_control(控制接口)和 snd_rawmidi(MIDI 设备)。理解这些结构体的层次关系与内部字段,是编写正确 ALSA 驱动的第一步。
核心原则:snd_card 是所有子设备的容器;snd_pcm 负责音频数据流;snd_control 提供用户空间调节接口;snd_rawmidi 处理 MIDI 事件。四者通过链表和指针构成树状拓扑。
3.1 snd_card:声卡实例的根容器
snd_card 是 ALSA 驱动中最高层级的抽象,代表一个物理或逻辑声卡。每个声卡驱动在 probe 阶段必须首先创建并注册一个 snd_card 实例。
驱动开发提示:snd_card 的 number 字段对应 /dev/snd/controlC0 中的数字索引。id 字段用于 alsamixer 等工具识别声卡,建议使用简短且唯一的字符串。
3.2 snd_pcm:音频数据流的核心引擎
snd_pcm 是 ALSA 中最重要的子设备,负责管理音频数据的播放(playback)和录音(capture)。每个 snd_pcm 实例包含两个独立的 substream 链表:一个用于 playback,一个用于 capture。
| 字段/成员 | 类型 | 说明 |
|---|---|---|
| card | struct snd_card * | 所属声卡指针 |
| device | int | 设备索引号(对应 hw:x,y 中的 y) |
| streams[2] | struct snd_pcm_str | 0=playback, 1=capture |
| substream | struct snd_pcm_substream * | 当前活跃的 substream |
| ops | struct snd_pcm_ops * | 硬件操作回调函数集 |
| private_data | void * | 驱动私有数据指针 |
| name | char [32] | 设备名称标识 |
常见错误:驱动开发新手常混淆 snd_pcm 与 snd_pcm_substream。snd_pcm 是逻辑设备,substream 才是真正处理音频数据的实体。多声道声卡通常一个 substream 对应一个声道对。
3.3 snd_control:用户空间的调节旋钮
snd_control 是 ALSA 控制接口的内核表示,用户通过 alsamixer、amixer 或 alsactl 操作的就是这些 control 实例。每个 control 可以是一个音量滑块、开关或枚举选择器。
struct snd_kcontrol {
struct list_head list; // 挂入 snd_card 的 controls 链表
struct snd_ctl_elem_id id; // 控制元素标识(name, index, numid)
unsigned int count; // 该控制器的元素个数
struct snd_kcontrol_volatile volatile[SNDRV_CTL_ELEM_ID_MAX_ELEMS];
struct snd_kcontrol_ops *ops; // 回调函数:info, get, put, tlv
void *private_data; // 驱动私有数据
unsigned int private_value; // 常用整型私有值
};
| 回调函数 | 触发时机 | 典型实现 |
|---|---|---|
| info | 用户查询控制信息 | 填充范围、类型、访问权限 |
| get | 用户读取当前值 | 从硬件寄存器读取音量值 |
| put | 用户写入新值 | 写入硬件寄存器并更新缓存 |
| tlv | 用户查询 TLV 数据 | 返回 dB 刻度映射表 |
调试技巧:使用
amixer controls 列出所有 control,amixer cget numid=N 查看单个 control 的当前值和属性。驱动中建议为每个硬件可调参数创建独立的 snd_kcontrol 实例。
3.4 snd_rawmidi:MIDI 设备的抽象
snd_rawmidi 结构体用于管理 MIDI 输入/输出设备。与 snd_pcm 类似,它也包含独立的 substream 用于输入和输出方向。
struct snd_rawmidi {
struct snd_card *card; // 所属声卡
struct list_head list; // 挂入 snd_card 的 rawmidi 链表
unsigned int device; // 设备索引
unsigned int info_flags; // 设备能力标志
struct snd_rawmidi_ops *ops; // 硬件操作回调
struct snd_rawmidi_substream *substreams[2]; // 0=输出, 1=输入
void *private_data;
char name[32];
};
| 方向 | substream 索引 | 设备节点 | 典型用途 |
|---|---|---|---|
| 输出 (Output) | 0 | /dev/snd/midiC0D0 | 发送 MIDI 事件到外部设备 |
| 输入 (Input) | 1 | /dev/snd/midiC0D1 | 接收来自 MIDI 键盘的事件 |
驱动实现要点:snd_rawmidi 的 ops 回调包括 open、close、trigger 和 drain。trigger 回调在启动/停止 MIDI 传输时被调用,drain 用于等待输出缓冲区排空。对于 USB MIDI 设备,通常还需要处理 4 字节的 USB MIDI 事件包与标准 MIDI 字节流之间的转换。
3.5 四种核心结构体的对比总结
| 结构体 | 创建函数 | 注册函数 | 用户空间节点 | 主要回调集 |
|---|---|---|---|---|
| snd_card | snd_card_new() | snd_card_register() | /dev/snd/controlC0 | —(容器角色) |
| snd_pcm | snd_pcm_new() | snd_pcm_add() | /dev/snd/pcmC0D0p/c | open/close/ioctl/hw_params/trigger/pointer |
| snd_control | snd_ctl_new1() | snd_ctl_add() | 通过 controlC0 访问 | info/get/put/tlv |
| snd_rawmidi | snd_rawmidi_new() | snd_rawmidi_add() | /dev/snd/midiC0D0 | open/close/trigger/drain |
生命周期管理:所有子设备(snd_pcm、snd_control、snd_rawmidi)的生命周期由 snd_card 管理。驱动卸载时只需调用 snd_card_free(),ALSA 核心会自动释放所有已注册的子设备。切勿手动释放子设备内存,否则会导致双重释放。