Linux 驱动之 ALSA(二)编解码器类驱动相关概念
编解码器类驱动程序是最基本的,他实现的代码应该利用编解码器设备并公开其硬件属性,以便 amixer 等用户空间工具可以使用它。
由于驱动程序针对特定的编解码器,因此它应该包含音频控制、音频接口功能、编解码器 DAPM 定义和 I/O 功能,每个编解码器必须满足:
- 通过定义 DAI 和 PCM 配置来提供与其他模块的接口
- 提供编解码器控制 I/O hook(使用 I2C、SPI 的 API)
- 根据用户空间的需要,公开其他内核控件(Kernel control,Kcontrol)以动态控制模块行为
- 定义 DAPM widget,为动态电源切换建立 DAPM 路由,并提供 DAC 数字静音控制(option)
Component
包含编解码器的路由、widget、控件金额一组编解码器相关函数回调指针,以及一个或多个 dai 驱动。
1 | /* component interface */ |
- name: 此组件的名称对于编解码器和 platform 来说都是必须的,platform 可能不需要其他字段
- probe:组件驱动 probe 函数,组件 probe 函数,当组件驱动被 mechine 驱动 probe 到时(实际上是当 machine 驱动向 ASOC core 注册一个由该组件组成的 card 时)执行,必要时完成组件初始化
- control:控制接口指针,如控制音量,通道选择等,主要用于编解码器
- set_pll: 设置锁相环的函数指针
- read:读取编解码器寄存器的函数
- write:写入编解码器寄存器的函数
- dapm_widget: dapm 的 widget 指针
- dapm_routes: dapm 路由指针
- ops: platform DMA 相关回调
此结构体同时抽象编解码器和 Platform 机器 DAI 驱动程序
DAI
编解码驱动程序包括编解码设备本身和 DAI 组件,他们在与 platform 绑定时使用。
1 | struct snd_soc_dai_driver { |
- name: DAI 接口名称
- probe:dai probe 函数,当 machine 驱动程序 probe 到该 dai 驱动程序所属的组件驱动程序时(实际是 machine 驱动程序向 ASOC core 注册 card 时)执行
- ops:提供用于配置和控制 DAI 的回调函数
- capture:它表示音频采集的硬件参数,包括通道数、rate、format 等
- playback:它表示音频回放的硬件参数,包括通道数、rate、format 等
1 | struct snd_soc_dai_ops { |
第一类:时钟配置回调函数
- set_sysclk: 设置 DAI 的主时钟
- set_pll: 设置 PLL 函数
- set_clkdiv: 设置时钟分频值
第二类:DAI 的格式配置函数
- set_fmt: 设置 DAI 的格式
- set_tdm_slot: 设置 TDM 的时隙
- set_channel_map: 通道 TDM 的映射设置 machine 驱动通过 snd_soc_dai_set_channel_map 调用
- set_tristate: 设置 DAI 引脚的状态,在于其他 DAI 并行使用同一引脚时需要使用该状态
第三类:普通的标准前端
- startup:打开采集/回放设备时,在打开 PCM 子流时由 ALSA 调用
- hw_params:设置音频流时调用
- prepare:当 PCM 准备好时调用
- trigger: PCM 启动、停止、暂停时调用
Control
编解码器驱动程序通常需要公开一些可以从用户空间更改的编解码器属性,这些就是编解码器控件(control)。
1 | struct snd_kcontrol_new { |
- iface : 控件类型 可能是简单开关控件、立体声控件(Stereo)、混音器(MIXER)、MUX 控件等
- name: 控件名称,命名包含源(Master、PCM、CD、Line)、方向(playback、capture、Bypass)以及功能(switch、volume、route 等)
- access : 控件访问权限,READ、WRITE、VOLATILE 等
- get : 读取控件的当前值并返回给用户空间
- put : 按照应用程序要求设置控件值
- tlv : 为控件提供元数据,有些混音器控件需要以分贝(db)为单位提供信息,可以使用 DECLARE_TLV_xxx 宏定义一些包含这些信息的变量,然后将控制 tlv.p 字段指向的变量,tlv 是类型-长度-值(type-length-value)是一种编码方案。
设置开关控件
1 | SOC_SINGLE(xname, reg,shift.max,invert) |
这种类型的控件只有一个设置,一般用于组件开关
设置带有音量级别的开关
1 | SOC_SINGLE_TLV(name, reg,shift.max,invert,tlv_array) |
用于定义具有增益的控件,如音量控件、EQ 均衡器等
立体声控件
1 | SOC_DOUBLE_R(xname, reg_left,reg_right,xshift,xmax,xinvert) |
在一个寄存器中控制两个相似的变量,因此可以同时控制左右声道
带音量级别的立体声控件
1 | SOC_DOUBLE_R_TLV(xname, reg_left,reg_right,xshift,xmax,xinvert,tlv_array) |
混音器控件
1 | SOC_SINGLE("Input Switch", WM8960_SPEAKER_MIXER,7,1,0) |
DAPM(dynamic audio power management)
widget
普通 kcontrol 具有以下特点:
- 自我描述,无法描述每个 kcontrol 之间的关系
- 缺乏电源管理机制
- 缺乏响应回放、停止、开机、关机等音频事件的时间处理机制
- 缺少爆音防止机制
- 无法自动关闭音频路径中涉及的所有控件
为解决以上问题,DAPM 引入 widget,widget 是 kcontrol 的进一步升级和封装。
1 | struct snd_soc_dapm_widget { |
- id: widget 类型,如 snd_soc_dapm_output、snd_soc_dapm_mixer 等
- name:widget 名称
- shift 和 mask:用于控制 widget 的电源状态,对应寄存器地址 reg
- on_val 和 off_val:表示更改 widget 当前电源状态的值
- event:表示 DAPM 事件处理回调函数指针
- kcontrol_news:组成该 kcontrol 的控件数组
- dirty:当 widget 状态改变时,dirty 用于将这个 widget 插入到 dirty 列表中,然后扫描整个列表执行整个路径的更新
定义 widget
有很多宏定义来定义 widget 控件,包括:
- 编解码域:如 VREF 和 VMID,可以提供参考电压 widget
- platform/machine domain:需要物理连接的 platfoem 或板卡的输入输出接口,如耳机、扬声器、麦克风等
- 音频路径域:指在编解码器中控制音频路径的 MUX、Mixer 和其他 widget
- 音频流域:处理音频流数据,如 ADC、DAC 等
路径的概念--widget 之间的连接器
由 struct snd_soc_dapm_path 结构体表示,该结构体表示两个 widget 之间的连接,他的 source 字段指向连接的开始 widget,而 sink 字段则指向连接到达的 widget。所有输入端点的 snd_soc_dapm_path 连接到 list_node[SND_SOC_DAPM_DIR_IN] 列表中,所有输出端点的 snd_soc_dapm_path 连接到 list_node[SND_SOC_DAPM_DIR_OUT] 列表中。
路由的概念--widget 互联
为了不想处理 path,引入 route 概念,路由至少包括,起始 widget,跳线路径 jumper path 和接收方 widger 组成,使用 struct snd_soc_dapm_route 表示。
1 | struct snd_soc_dapm_route { |
- sink: 指向到达 widget 的名称串
- source:指向起始 widget 的名称串
- control:指向负责控制连接的 kcontrol 名称串
- connected:定义了自定义连接检查的回调函数
source 通过 kcontrol 连接到 sink,可以调用 connected 检查连接状态
数据结构
前面列出了一些关键数据结构,除此之外还有很多其他关键数据结构,以及各数据结构之间的关系没有一一列出来,下面只通过一副图片来将相关数据结构以及他们之间的关系列出来。
原图
参考文献
《Linux 设备驱动开发-约翰-马德奥》