初次接触ALSA感觉其深不可测,涉及到内核驱动代码的分析,架构的理解、应用层的alsa-lib库的使用以及系统运行环境的相关配置管理机制。一开始因为缺乏总体上的认识, 所以只能以项目中的遇到的实际问题为起点,一点一点深入其中。经过差不多两年的积累,现在无论时总体上的理解还是细节上的把握得有了一些收获。
网上关于这块的文章,大部分会先从概念开始介绍alsa,本文通过定位解决实际问题来深入剖析alsa实现细节。
ALSA驱动
定位DMA卡死问题
定位内核层问题的手段
想要有效率的定位解决内核模块的问题,一定要较全面地掌握内核通过的调试技术支持以及相应模块通过的调试机制。如果只是依靠简单的添加printk来定位问题,效率和效果都很差。
使用trace-cmd工具
ALSA PCM core register snd_pcm subsystem to kernel tracepoint system. This subsystem includes two categories of tracepoints; for state of PCM buffer and for processing of PCM hardware parameters.
alsa驱动中有两种Tracepoint, 一个是用于追踪pcm buffer状态的,一个是pcm的硬件参数配置。前者主要涉及hwptr(读指针,内核层更新)、applptr(写指针,应用层更新)、xrun和hw_ptr_error;后者为hw_mask_param和hw_interval_param。使用trace-cmd工具测试发现,4.4.7版本的内核只支持前者中的hwptr、xrun和hw_ptr_error。
分析proc信息
使用perf工具
驱动细节研究
解决问题的前提是对相关驱动的细节有足够的掌握,下面从不同层面、不同角度剖析ALSA驱动的实现细节。而理解驱动代码的前提是对相应的硬件架构有一定了解, 软件完成硬件的抽象,直接理解抽象的东西难度太大,所以需要先理解硬件。
硬件基础
先通过硬件框图来从整体上认识一下HDA内核架构以及HDA在整个系统中的位置。
-
Controler: HDA控制器是一个控制I/O外设的总线,它主要通过PCI总线挂接到CPU上。HDA内部包含多个DMA引擎,每一个引擎用来传输a single audio “stream”。
-
Link: Link这个抽象可以类比为I2S的功能。The controller is physically connected to one or more codecs via the HDA link.
-
Codec: 一个或者多个codecs挂接在Link上。每个codec通过时分复用方式从link上提取属于自己的streams.
下面来介绍一下上面提到的stream概念。
Streams and Channels
HDA中通过抽象出streams和channels来描述link中传输的数据的方式。A stream is a logical or virtual connection created between a system memory buffer and the codec rendering that data, which is driven by a single DMA channel through the link.channels就是 我们很熟悉的声道的概念,所以,一个立体音stream中包含L和R两个channel。
上图描述了上面提到的各个概念之间的关系。每个active stream必须连接到一个DMA引擎上,上面的stream 1,2,3都是active stream,其中1,3是output类型的 stream,2是input类型的stream.4就是一个inactive stream.
上面提到HDA中的Link概念和I2S总线很类似,当然有很大不同,下图描述了link、stream、channels之间的关系。
Link上传输的是一个个frames,而且每个frams的大小是固定的,等于48k采样率20.83us的数据量。因为每个frams occur at a fixed rate,所以 如果传输的音频采样率不是48k,那么there will be more or less than one sample block in each frame for that stream.
另外,和I2S总线最大不同的是,HDA的each frame contains command or control information and then as many stream sample blocks(labeled S-1, S-2.S-3)as are needed.
HDA详细的硬件细节参见这篇资料
HDA与codec之间的交互
当挂接在HDA总线上的codec被正常识别后,HDA控制器就可以向codec发送命令和获取反馈信息。这些控制信息的交互主要通过CORB(Command Outbound Ring Buffer) 和RIRB(Response Input Ring Buffer)这两个key mechanisms。
-
CORB - HDA utilizes the CORB mechanism 传输命令到codec。实际上,CORB就是内存中的一个circular buffer。HDA控制器使用内部的DMA将 CORB中的命令传输到一个Link frame的开始位置,Link frame的数据格式见上面示意图。
-
RIRB - 和CORB对应的就是RIRB,和CORB一样,RIRB也是一个circular buffer,它用来缓存从codec发送过来的responses。
CORB和RIRB的初始化见下面代码段:
HDA中stream的管理
CORB、RIRB是用来传输命令的,对应的数据的传输是通过BDL(Buffer Descriptor List)来管理的。BDL是由BDLE(Buffer Descriptor List Entries)组成的,BDL和BDLE的格式分别如下:
上面关于BDL格式描述中最重要的域是IOC,即Interrupt on Completion。这个标志位决定着DMA传输一定数据后触发中断,中断处理中会唤醒应用层阻塞式的写数据 线程。这个”一定数据量”其实就是我们在设置hw_params时设置的periods值,periods也就是我们将一大块输出缓存进行了均分出来的的一片片buffer。所以内核代码 应该在每个periods的尾部设置这个IOC标志。内核代码中(见下面章节的截图)snd_hdac_stream_setup_periods()函数的407行到419行的for循环正是为每个periods通过setup_bdle()函数设置一个BDLE,并设置IOC标志位1。下面看一下setup_bdle()函数的具体实现:
那么BDL是怎样管理音频数据(stream data)的呢?先来看看HDA中如何抽象细化stream data的。
如上图,samples是最小的单位,即采样点,采样点的数据量和采样精度相关,24bit的采样深度对应的samples就是3个字节大小。Blocks are sets of samples to be rendered at ont point in time.其实就是音频的一个frame,大小为samples * channels,比如24bits立体音的一个Block大小为3x2=6个字节。前面 章节提到,the standard output Link rate id based on a 48-kHz frame time,因此,假如是24bits,96-kHz的立体音,那么multiple blocks must be transmitted at one time。多个一起传输的blocks就构成了packets,而packets又组成buffers,而这个buffers的格式就是BDL来描述管理的。
HDA 驱动已知问题
HD-audio is the new standard on-board audio component on modern PCs after AC97. Although Linux has been supprting HD-audio since long time ago, there are often problems with new machines. A part of the problem is broken BIOS, and the rest is the driver implementation.
笔者参与的项目中的音频驱动一直都是基于intel的HDA接口,定位解决驱动中的问题,熟悉HD-Audio Driver是必不可少的。而且从上面描述来看,这个驱动中还是有不少坑的。
HD-Audio Controller
DMA-Position Problem
The most common problem of the controller is the inaccurate DMA pointer reporting. The DMA pointer for playback and capture can be read in two ways, either via a LPIB register or via a position-buffer map.
默认情况下,驱动会从io-mapped position-buffer中读取缓存指针信息,如果position-buffer死掉了就去读取LPIB寄存器。这部分的代码如下:
我们可以通过模块参数module_param中的position_fix变量来修改上面的行为。
- position_fix=1 means to use LPIB method explicitly.
- position_fix=2 means to use the position-buffer.
- position_fix=3 means to use a combination of both methods, needed for some VIA controllers.
- position_fix=4 is another combination available for all controllers, and uses LPIB for playback,and position-buffer for capture.
- 0 is the default value for all other controllers,就是一开始描述的行为。
position_fix变量作用的代码如下,函数在初始化阶段azx_create()中调用。
另外还有一个坑是HDA中的中断唤醒时间:
Every controller is known to be broken regarding the wake-up timing. It wakes up a few samples before actually processing the data on the buffer, which caused a lot of problem. Since 2.6.27 kernel, the driver puts an artifical delay to the wake up timing. This delay is controlled via bdl_pos_adj option.
bdl_pos_adj也是一个模块参数,默认是负值,不同的控制器会在初始化时将它设为不同值。内核代码中初始化部分的代码如下:
如果bdl_pos_adj的值设置的不合适,内核会打印提示”IRQ timing workaround is actived for card x. Suggest a bigger bdl_pos_adj”。那么这个 bdl_pos_adj具体在哪里发挥重要调节作用呢?答案如下,下面函数定义在sound/hda/hdac_stream.c文件中:
Codec-probing Problem
HDA驱动代码细节
HD-Audio硬件上主要由两部分组成,the controller chip 和 the codec chips on the HD-Audio bus.Linux内核驱动只提供一个snd-hda-intel驱动 来支持所有HDA控制器。另外,对于不同的codec解码器,the snd-hda-intel has a generic parser as a fallback, 但是通常不同的codec会使用 各自的解码器(coded in patch_*.c)。比如,笔者使用的alc662的codec对应的解码器就是由patch_realtek.c提供的。
In design of ALSA PCM core, status and control data for runtime of ALSA PCM substream are shared between kernel/usr spaces by page frame mapping with read-only attribute. Both of hardware-side and application-side position on PCM buffer are maintained as a part of the status data. In a view of ALSA PCM application, these two positions can be updated by executing ioctl(2) with some commands.
调试定位过程
完善HDA中断处理
解决HDMI3没有设备节点问题
解决speaker没有输出问题
了解问题涉及到的知识点
这个问题肯定出在codec内部,所以有必要先理解一下HDA下codec的模型架构。
HDA中codec模型对象
解决问题的手段
这个问题粗略分析应该和codec的配置有关,也就是codec内部通道配置处理问题。下面了解一下HDA通过了哪些途径和工具,让我们可以获取codec配置信息以及 修改这些信息。
HDA Reconfiguration
通过/sys/class/sound/hwCxDy下的文件,我们可以动态地的修改HDA下codec的配置。该目录下主要有如下文件:
- afg - 只读的AFG(audio function group) ID.
- mfg - 只读的MFG ID.
- name - codec的名称,可以直接写入新字符串进行修改。
- init_verbs - 初始化时需要额外执行的verbs,添加需要的vers到这个文件,可以在初始化时被执行。
- hints - 给codec的暗示,例如写入jack_detect = no 就会禁止掉codec的jack dectection功能。
- init_pin_configs - 记录BIOS设置的initial pin default config。
- driver_pin_configs - 记录codec修改掉pin default config值的部分。
- usr_pin_configs - 写入自己设定的配置可以覆盖掉BIOS启动时的设置。
- reconfig - 触发codec重新配置,一旦往这个文件写入任意值,驱动就会re-initialize the codec tree again。
- clear - Resets the codec, removes the mixer elements and PCM stuff of the specified codec, and clear all init verbs and hints
实例:当你想修改pin widget 0x14 值为0x9993013f,并且让驱动re-configure based on that state,执行如下命令:
# echo 0x14 0x9993013f > /sys/class/sound/hwC0Do/usr/pin_configs
# ehco 1 > /sys/class/sound/hwC0D0/reconfig
Code Proc-File
codec的proc file是调试HDA的宝箱,这个文件里面详细记录了codec内的所有widget信息。proc文件是/proc/asound/card/codec#, 每个codec对应一个proc文件。我们可以直接执行cat查看这个文件内的内容,也可以借助codecgraph工具将这个文件生成描述codec内部拓扑结构的svg图。
CodecGraph工具
codecgraph工具主要是一个shell脚本加上一个python脚本,使用方法很简单,执行./codecgraph生成系统中默认路径/proc/asound/card0/codec#0 的拓扑图, 或者执行./codecgraph xxx 生成使用指定的proc file生成拓扑图。生成效果如下:
有了这个codec内部的连接图,对于分析定位音频通道配置问题非常的有帮助。
Hint Strings
这个就是HDA中目前支持的一些写入/sys/class/sound/hwCxDy文件中进行codec parse控制的字符串。The codec parser have severl swithes and adjustment knobs for matching better with the actual codec or device behavior. Many of them can be adjusted dynamically via hints strings.几个主要支持的hint strings如下:
- jack_dectect(bool) - 说明是否使能jack插入检测功能,默认为ture。
- auto_mute(bool) - 是否使能headphone的自动静音功能,默认为ture。
- auto_mic(bool) - 是否使能mic自动切换功能,默认为ture。
- line_in_auto_switch(bool) - 是否使能line-in自动开关切换功能,默认为false。
- vmaster(bool) - 是否使能virtual Master control,默认为ture。
- mixer_nid(int) - specifies the widget NID of the analog-loopback mixer。