0%

ALSA SoC 框架

ALSA SoC 框架 编解码器和平台类驱动程序

ALSA是什么?

​ ALSA(advanced linux sound architecture) 高级linux声音架构。但是这个架构主要为计算机设置,没有怎么考虑嵌入式,后来就出现ASoC(ALSA system on chip),为嵌入式处理器和各种编解码器提供更好的ALSA支持。
​ ASoC将嵌入式音频系统划分为3个可重用的组件驱动程序,即机器类(machine class)、平台类(platform class)和编解码器类(codec class)。其中平台类和编解码器类具有跨平台属性,机器类则是板级的。即平台类和编解码类一个就可以在不同的板子上使用,而机器就是这块板子上配置编解码器和平台的,所以是专属于板子的。

ASoC

​ 在当前的内核实现中,使用一种通用方法——组件(component),来进行平台和编码器的描述。有两个结构体来辅助进行,struct snd_soc_component(即编码器或平台)和struct snd_soc_component_driver(他们各自的音频接口驱动程序)。
​ 数字音频接口(digital audio interface,DAI)是一种总线控制器,可以将音频数据从一端(比如SoC)送到另一端(编解码器)。ASoC支持大多数DAI

​ 一个ASoC分为三个元素(平台、编解码器、机器),每一个元素都有自己的专用驱动程序。

  1. 平台(platform): SoC 平台相关的驱动部分,也就是“芯片原厂”提供的、与具体 SoC 平台绑定的音频驱动模块。包含SoC的音频DMA引擎(负责音频数据的内存搬运);控制 数字音频接口(DAI),如 I2S、PCM、AC97 等;配置 SoC 内部的音频相关硬件资源(如 FIFO、时钟、DMA 通道),各个芯片原厂的产品比如stm,qcom,mtk。Platform 类是 ASoC 中“CPU 一侧”的驱动,负责把音频数据从内存搬到 SoC 的音频接口(DAI),再通过 DMA 等方式交给 Codec。平台的驱动程序分为两部分:

    • CPU DAI: cpu的音频总线控制器,比如I2S、pcm等总线控制器。平台驱动程序定义DAI并将它们注册到ASoC内核中。描述 SoC 内部数字音频接口(I²S、PCM、AC97…)的寄存器配置、时钟、格式、slot、TDM 等。实现 startup / hw_params / set_fmt / trigger 等 DAI 操作回调。

    • PCM DMA: pcm驱动程序通过覆盖由struct snd_soc_component_driver结构体公开的指正帮助执行DMA操作。pcm驱动程序与平台无关,仅仅与引擎上游的API交互。然后dma引擎与特定平台的dma驱动程序交互获得正确dma配置。这里分为两部分,DMA引擎和PCM运行库。

      DMA 引擎驱动(dmaengine 实现)
      • 封装 SoC 的专用音频 DMA 控制器,向内核 dmaengine 子系统注册通道。
      • 提供 device_prep_slave_sg / device_config / device_tx_status 等回调,完成把环形 buffer 搬到 Tx FIFO(或从 Rx FIFO 搬进 buffer)的全部硬件操作。
      • 负责 burst、width、FIFO threshold、中断/周期回调等细节。

      PCM 运行库(snd_soc_platform_driver)
      • 把上面两者“粘”在一起:
      – 用 snd_pcm_ops 封装 open / close / ioctl / pointer / mmap 等 PCM 运行时接口;
      – 借助 dmaengine API 为每一次 trigger(START) 向 DMA 引擎申请描述符并启动传输;
      – 通过 DAI 驱动在 hw_params 里把接口时钟、格式配置下去

      pcm和dma它负责的是将dma缓冲区中的音频数据传送到总线Tx FIFO。

    • 平台类驱动 = CPU-DAI 驱动 + DMA 引擎驱动 + PCM 运行库

  2. 编解码器(codec):功能就是编码和解码,还包括一些回声消除的功能和其他组件。将来自声源的模拟信号转换为处理可以操作的数字信号,或者反过来。会对音频信号进行相应的调整并控制音频信号之间的路径。

  3. 机器(machine): 板级表示,链接两个音频接口(cpu_dai 和 codec_dai)。该链接在内核中通过struct snd_soc_dai_link实例抽象出来。配置好链接后,机器驱动将注册一个struct snd_soc_card对象,它是linux对于声卡的抽象。

编解码器

编解码器的驱动程序需要满足一些规范:

​ • 通过定义DAI 和 PCM 配置来提供与其他模块的接口。
​ • 提供编解码器控制IO挂钩?什么意思?
​ • 根据用户空间实用程序的需要,公开其他内核空间(kernel control, kcontrol)以动态控制模块行为。(向上提供接口?)
​ • 定义DPAM weight,为动态电源切换建立DAPM路由,并提供DAC数字静音控制。

​ 知道了规范,我们来看看编解码器的结构。它的驱动程序根据上面的了解,很明显就是包括了编解码设备(组件)本身和DAI组件,它们在与平台绑定时使用。通过devm_snd_soc_register_component(),驱动程序注册一个struct snd_soc_component_driver对象(实际上是编解码器驱动程序的实例,包含指向编解码器的路由、weight、控件和一组编解码器相关函数回调的指针)以及一个或多个struct snd_soc_dai_driver(编解码器DAI驱动程序的一个实例,可能包含一个音频流。如下就注册了一个组件devm_snd_soc_register_component(dev,&component_driver, &dai, 1))。看下它们的结构体,接下来会跟着这个结构体来了解编解码驱动程序和平台驱动程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
struct snd_soc_component_driver {
/* 1. 组件名字,dmesg/debugfs/topology 均可见 */
const char *name;

/* 2. 默认 kcontrol 模板表 */
const struct snd_kcontrol_new *controls;
unsigned int num_controls;

/* 3. DAPM widget & 路由表 */
const struct snd_soc_dapm_widget *dapm_widgets;
unsigned int num_dapm_widgets;
const struct snd_soc_dapm_route *dapm_routes;
unsigned int num_dapm_routes;

/* 4. 生命周期钩子 */
int (*probe)(struct snd_soc_component *component);
void (*remove)(struct snd_soc_component *component);
int (*suspend)(struct snd_soc_component *component);
int (*resume)(struct snd_soc_component *component);

/* 5. 寄存器统一读写(Codec/ADC 常用) */
unsigned int (*read)(struct snd_soc_component *component,
unsigned int reg);
int (*write)(struct snd_soc_component *component,
unsigned int reg, unsigned int val);

/* 6. PCM 设备手动创建/销毁 */
int (*pcm_construct)(struct snd_soc_component *component,
struct snd_soc_pcm_runtime *rtd);
void (*pcm_destruct)(struct snd_soc_component *component,
struct snd_pcm *pcm);

/* 7. 全局时钟 & PLL & Jack */
int (*set_sysclk)(struct snd_soc_component *component,
int clk_id, int source, unsigned int freq, int dir);
int (*set_pll)(struct snd_soc_component *component, int pll_id,
int source, unsigned int freq_in, unsigned int freq_out);
int (*set_jack)(struct snd_soc_component *component,
struct snd_soc_jack *jack, void *data);
int (*get_jack_type)(struct snd_soc_component *component);

/* 8. 设备树解析 */
int (*of_xlate_dai_name)(struct snd_soc_component *component,
const struct of_phandle_args *args,
const char **dai_name);
int (*of_xlate_dai_id)(struct snd_soc_component *component,
struct device_node *endpoint);

/* 9. 事件通知 */
void (*seq_notifier)(struct snd_soc_component *component,
enum snd_soc_dapm_type type, int subseq);
int (*stream_event)(struct snd_soc_component *component, int event);
int (*set_bias_level)(struct snd_soc_component *component,
enum snd_soc_bias_level level);

/* 10. PCM 流操作(等价 snd_pcm_ops) */
int (*open)(struct snd_soc_component *component,
struct snd_pcm_substream *substream);
int (*close)(struct snd_soc_component *component,
struct snd_pcm_substream *substream);
int (*ioctl)(struct snd_soc_component *component,
struct snd_pcm_substream *substream,
unsigned int cmd, void *arg);
int (*hw_params)(struct snd_soc_component *component,
struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params);
int (*hw_free)(struct snd_soc_component *component,
struct snd_pcm_substream *substream);
int (*prepare)(struct snd_soc_component *component,
struct snd_pcm_substream *substream);
int (*trigger)(struct snd_soc_component *component,
struct snd_pcm_substream *substream, int cmd);
int (*sync_stop)(struct snd_soc_component *component,
struct snd_pcm_substream *substream);
snd_pcm_uframes_t (*pointer)(struct snd_soc_component *component,
struct snd_pcm_substream *substream);
int (*get_time_info)(struct snd_soc_component *component,
struct snd_pcm_substream *substream, struct timespec64 *system_ts,
struct timespec64 *audio_ts,
struct snd_pcm_audio_tstamp_config *audio_tstamp_config,
struct snd_pcm_audio_tstamp_report *audio_tstamp_report);
int (*copy)(struct snd_soc_component *component,
struct snd_pcm_substream *substream, int channel,
unsigned long pos, struct iov_iter *iter,
unsigned long bytes);
struct page *(*page)(struct snd_soc_component *component,
struct snd_pcm_substream *substream,
unsigned long offset);
int (*mmap)(struct snd_soc_component *component,
struct snd_pcm_substream *substream,
struct vm_area_struct *vma);
int (*ack)(struct snd_soc_component *component,
struct snd_pcm_substream *substream);
snd_pcm_sframes_t (*delay)(struct snd_soc_component *component,
struct snd_pcm_substream *substream);

/* 11. 压缩音频 (VoIP/BT offload) */
const struct snd_compress_ops *compress_ops;

/* 12. probe/remove 顺序 */
int probe_order;
int remove_order;

/* 13. trigger 顺序 */
enum snd_soc_trigger_order trigger_start;
enum snd_soc_trigger_order trigger_stop;

/* 14. 模块引用策略:PCM open 时再 get 模块 */
unsigned int module_get_upon_open:1;

/* 15. 电源标志位 */
unsigned int idle_bias_on:1; /* idle 时保持偏置 */
unsigned int suspend_bias_off:1; /* suspend 时断电 */
unsigned int use_pmdown_time:1; /* 播放停止后延迟 */

/* 16. 字节序/拓扑/后端修正 */
unsigned int endianness:1; /* 不关心 endian,自动 LE/BE */
unsigned int legacy_dai_naming:1; /* 使用旧命名规则 */
const char *ignore_machine; /* 忽略 machine 固定 FE/BE */
const char *topology_name_prefix; /* topology 节点前缀 */
int (*be_hw_params_fixup)(struct snd_soc_pcm_runtime *rtd,
struct snd_pcm_hw_params *params);
bool use_dai_pcm_id; /* 用 DAI link PCM ID 作为设备号 */
int be_pcm_base; /* 后端 PCM 起始设备号 */

#ifdef CONFIG_DEBUG_FS
const char *debugfs_prefix; /* debugfs 节点前缀 */
#endif
};
成员 作用
name 组件名称,用于调试和匹配
controls / num_controls 该组件提供的 ALSA kcontrols(音量、开关、增益等)
dapm_widgets / dapm_routes DAPM(动态音频电源管理)所需的 widget 和路径
probe / remove 组件被绑定/解绑时的回调
ops 指向 struct snd_pcm_ops,用于实现 PCM 流操作(open、hw_params、trigger、pointer 等)
pcm_construct / pcm_destruct 可选:手动创建/销毁 PCM 设备
compress_ops 用于支持压缩音频(如 aptX、AAC)
of_xlate_dai_name 在设备树中解析 DAI 名称
seq_notifier / stream_event 监听 DAPM 或音频流事件,用于电源管理
probe_order / remove_order 多组件时的加载顺序控制

​ 一个组件驱动由他本身的驱动程序和DAI驱动程序组成,当然也要看struct snd_soc_dai_driver这个结构。这个结构体是干啥的?它是 ASoC(ALSA System on Chip)框架里专门描述 一个数字音频接口(DAI)能力 的结构体。在一条音频链路中,无论是 SoC 内部 I²S/PCM 端口,还是 Codec 的 DAC/ADC 端口,都有一个对应的 snd_soc_dai_driver 实例,用来告诉 ALSA/ASoC:

​ “我这个接口支持哪些格式、采样率、时钟角色、bclk/lrclk 分频能力、通道数等”。

​ 在驱动中,这个结构体的实例应该和编解码器上的DAI数量相同,它们需要填充和注册(填充在驱动程序里,注册在probe函数中,同时也必须使用devm_snd_soc_register_component()导出(实际上是插入到ASoC全局组件列表里),以便它可以在注册声卡之前由机器驱动程序注册到核心。(各 Component 的 probe 阶段先把自身“挂”到 ASoC 全局表;Machine 驱动在真正注册声卡时再去表里匹配并绑定它们。

​ 看看结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct snd_soc_dai_driver {
/* === DAI 描述信息 === */
const char *name; /* 该 DAI 的名字,ASoC / debugfs / 用户空间可见 */
unsigned int id; /* 在同一组件内的序号,0,1,2…,用于区分多路 DAI */
unsigned int base; /* 寄存器基地址偏移(少数驱动用,平台相关,可忽略) */
struct snd_soc_dobj dobj; /* ASoC 内部对象管理头,驱动不直接操作 */
struct of_phandle_args *dai_args; /* 指向设备树 phandle 解析结果,自动填充 */

/* === 操作函数指针 === */
const struct snd_soc_dai_ops *ops; /* 通用 DAI 操作:set_fmt、set_sysclk、trigger … */
const struct snd_soc_cdai_ops *cops; /* 压缩音频/回环专用操作,通常置 NULL */

/* === DAI 能力声明 === */
struct snd_soc_pcm_stream capture; /* 录音能力:采样率、格式、通道范围 */
struct snd_soc_pcm_stream playback; /* 播放能力:同上 */

/* 以下 3 个布尔位表示“对称”要求:所有通道/流必须一致 */
unsigned int symmetric_rate:1; /* 所有流必须同一采样率 */
unsigned int symmetric_channels:1; /* 所有流必须同一通道数 */
unsigned int symmetric_sample_bits:1; /* 所有流必须同一位深 */
};

​ 我们要理解其他数据结构和struct snd_soc_pcm_stream 和 struct snd_soc_dai_ops,这两个结构体和dai配置息息相关。

  • DAI操作

    上面的dai_driver中包含了两个ops结构体实例抽象。struct snd_soc_dai_ops 中包含了一组回调函数,这些回调函数的作用基本上分为三类,时钟配置回调函数,DAI格式配置回调函数,PCM校正回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
	struct snd_soc_dai_ops {
/* 1. 生命周期 */
int (*probe)(struct snd_soc_dai *dai); // DAI实例绑定到声卡时调用,做一次性初始化
int (*remove)(struct snd_soc_dai *dai); // DAI实例从声卡移除时调用,释放资源
int (*compress_new)(struct snd_soc_pcm_runtime *rtd, int num); // 创建压缩音频offload实例(BT/VoIP用)
int (*pcm_new)(struct snd_soc_pcm_runtime *rtd, struct snd_soc_dai *dai); // 每创建PCM子流时调用,可分配DMA私有缓冲区

/* 2. 时钟&PLL -- 通常在hw_params阶段被soc-card调用 */
int (*set_sysclk)(struct snd_soc_dai *dai, int clk_id, unsigned int freq, int dir); // 选时钟源、设频率、指定方向
int (*set_pll)(struct snd_soc_dai *dai, int pll_id, int source,
unsigned int freq_in, unsigned int freq_out); // 配置PLL输出频率
int (*set_clkdiv)(struct snd_soc_dai *dai, int div_id, int div); // 设置分频器(bclk/lrclk/fs等)
int (*set_bclk_ratio)(struct snd_soc_dai *dai, unsigned int ratio); // 手动指定BCLK与LRCLK比值

/* 3. 接口格式 -- 设置I²S/PCM/TDM等时序 */
int (*set_fmt)(struct snd_soc_dai *dai, unsigned int fmt); // 格式、极性、主从模式
int (*xlate_tdm_slot_mask)(unsigned int slots, unsigned int *tx_mask, unsigned int *rx_mask); // 辅助生成TDM掩码
int (*set_tdm_slot)(struct snd_soc_dai *dai, unsigned int tx_mask, unsigned int rx_mask,
int slots, int slot_width); // TDM时隙数量、宽度、收发掩码
int (*set_channel_map)(struct snd_soc_dai *dai, unsigned int tx_num, unsigned int *tx_slot,
unsigned int rx_num, unsigned int *rx_slot); // 多通道映射(HDMI/SLIMbus)
int (*get_channel_map)(struct snd_soc_dai *dai, unsigned int *tx_num, unsigned int *tx_slot,
unsigned int *rx_num, unsigned int *rx_slot); // 获取当前映射
int (*set_tristate)(struct snd_soc_dai *dai, int tristate); // 把数据线置高阻

/* 4. 流描述符绑定(平台驱动常用) */
int (*set_stream)(struct snd_soc_dai *dai, void *stream, int direction);
void *(*get_stream)(struct snd_soc_dai *dai, int direction);

/* 5. 数字静音 -- 防止pop */
int (*mute_stream)(struct snd_soc_dai *dai, int mute, int stream); // mute=1静音

/* 6. PCM运行期 -- 由soc-core在打开/关闭/传输时自动调用 */
int (*startup)(struct snd_pcm_substream *, struct snd_soc_dai *); // 检查约束、使能时钟
void (*shutdown)(struct snd_pcm_substream *, struct snd_soc_dai *); // 反向清理
int (*hw_params)(struct snd_pcm_substream *, struct snd_pcm_hw_params *, struct snd_soc_dai *); // 参数设置(FIFO/DMA突发等)
int (*hw_free)(struct snd_pcm_substream *, struct snd_soc_dai *); // 释放硬件资源
int (*prepare)(struct snd_pcm_substream *, struct snd_soc_dai *); // 启动前最后准备(阈值/预填充)
int (*trigger)(struct snd_pcm_substream *, int cmd, struct snd_soc_dai *); // START/STOP/PAUSE/RESUME
int (*bespoke_trigger)(struct snd_pcm_substream *, int cmd, struct snd_soc_dai *); // 特殊trigger序列
snd_pcm_sframes_t (*delay)(struct snd_pcm_substream *, struct snd_soc_dai *); // 返回硬件FIFO延迟(帧数)

/* 7. 自动格式选择 -- 按优先级尝试格式 */
u64 *auto_selectable_formats;
int num_auto_selectable_formats;

/* 8. probe/remove顺序控制 */
int probe_order;
int remove_order;

/* 9. 行为开关 */
unsigned int no_capture_mute:1; /* 录音流不调用mute */
unsigned int mute_unmute_on_trigger:1; /* trigger START/STOP时自动mute/unmute */
};

了解这个结构体能提供什么操作,绑定到driver中,能怎么用。

  • 采集和回放的硬件配置
    在采集和回放的操作期间,要设置DAI和功能,以便于允许配置底层PCM流。实现这个目标,就要在编解码器和平台驱动程序中为每个操作和每个DAI填充struct snd_soc_pcm_stream 的一个实例。(采集和播放操作?怎么填充)
1
2
3
4
5
6
7
8
9
10
struct snd_soc_pcm_stream {
const char *stream_name;
u64 formats; /* SNDRV_PCM_FMTBIT_* */
unsigned int rates; /* SNDRV_PCM_RATE_* */
unsigned int rate_min; /* min rate */
unsigned int rate_max; /* max rate */
unsigned int channels_min; /* min channels */
unsigned int channels_max; /* max channels */
unsigned int sig_bits; /* number of bits of content */
};

控件

上面列出的都是驱动中所需要的结构体,按照组件结构体中的第一个`struct snd_kcontrol_new *controls;` 控件!他的结构体就是struct snd_kcontrol_new,编解码器的驱动程序用控件公开一些可以从用户空间可以修改的编解码器属性。当编解码器初始化的时候,这些音频控件会注册到ALSA核心。
让ai介绍控件类型和如何使用
-------------本文结束感谢您的阅读-------------