编辑
2025-03-03
记录知识
0
请注意,本文编写于 116 天前,最后修改于 59 天前,其中某些信息可能已经过时。

目录

前言
一、数字音频
1.1 DAI的音频方案
1.1.1 喇叭原理
1.1.2 麦克风原理
1.1.3 整体框图
1.2 I2S总线
1.3 时钟
1.4 I2C
1.5 设计举例
1.6 其他
二、设备树解析
2.1 设备树举例
3.2 声卡设备驱动
3.3 I2S控制器驱动
四、内核ALSA框架解析
4.1 ASoC介绍
4.2 ASoC框架
五、音频传输分析
5.1 音频传输流程
5.2 环形缓冲区
六、音频路由分析
6.1 DAPM概念
6.2 声卡控件
6.3 播放路由
6.4 通路配置
七、wav格式解析
八、播放流程解析
九、调试错误分享
9.1 proc 声卡信息查看
9.2 mclk和其他clk信息查看
9.4 i2c工具调试
9.5 strace 调试
9.6 amixer控制控件
9.7 XRUN调试
9.8 ftrace
9.9 dma中断
9.10 debugfs
9.11 alsamixer
9.13 驱动文档

音频是内核的一个驱动框架,个人有过音频相关的工作经验,阅读过内核alsa框架代码并具有一些调试技巧,本着分享知识,提高团队技术能力的目的,在此进行音频alsa框架的整体解析,希望阅读此文章的同事后续均能从事音频相关内核开发。当然,于己也是一种回顾和复习,

前言

本文计划将alsa整体全貌介绍一遍,不清楚音频内核的可以走马观花一次,后面接触了可以继续深入,了解音频内核的可以一起精进。本文仅个人经验总结,非权威知识体系,不可全信,如有错误请指出。alsa大概涉及解析点如下:

数字音频 设备树解析 设备驱动解析 内核alsa框架解析 音频路由解析 wav格式解析 播放流程解析 通常错误分享

本文不会涉及pulseaudio及上层中间件和配置文件等系统相关技巧,仅从底层查看和分析问题。

本文面向研发技术人员,需要具备设备树理解能力,C语言阅读能力,内核子系统了解能力,内核调试基本技巧,音频基础知识,操作系统基本概念。文章提到相关基本知识不会详细解释,建议通过网络搜索的方式了解。

一、数字音频

数字音频接口简称DAI(Digital Audio Interfaces),它主要在DSP处理模数和数模转换中和主机进行数据传输,通常DAI指的是I2S、PCM、PDM、DCODEC、VAD、SPDIF等。本文主要讲I2S。

1.1 DAI的音频方案

基于DAI的音频方案如下:

image.png

这里红框内的称之为DAI,而红框外的是模拟电路喇叭和麦克风。我简单解释喇叭和麦克风一下,注意:模拟电路涉及知识深奥,这里只做了解。

1.1.1 喇叭原理

喇叭通常称之为功放,是利用小的电压输出从而激励出大的声压,从而产生人耳能听到的声音。最简单的功放电路如下。

image.png

注意,这里的VCC是PA的Power,通过控制这个电,能够有效的控制PA的控制和扩大功放大小和降低PA的功耗。通常的,代码是作为kcontrol的一个控件设置。

1.1.2 麦克风原理

麦克风通常是电容感应的方式,空气的声压能够影响电容的容值,从而产生电荷的变化,从而转换成数字电压信号。最简单的电容感应电路如下。

image.png

注意,这里的+V是麦克的偏置电压,通常是mic bias,通过控制这个电,能够有效控制mic的噪声和降低麦克风的功耗。通常的,代码是set_bias_level回调设置。

1.1.3 整体框图

综上可知,对于集成ADC+DSP+DAC + .... 组件的设备,通常作为音频codec芯片。芯片和主控直接连接如下:

image.png

这里CLK可以多个,CS是片选,DATA是I2S的数据传输。通过CLK+CS+DATA三类信号连接,通常是I2S总线

1.2 I2S总线

I2S信号有多种变体(PHILIPS模式/左对齐模式/右对齐模式),需要根据芯片手册选择,这里以ES8388举例(PHILIPS模式),其I2S默认如下。

image.png

如上可以知道:LRCK作为声道的片选,LRCK为低为左声道,LRCK为高为右声道,左声道从LRCK下降沿后的第二个时钟上升沿有效,右声道从LRCK上升沿的第二个时钟上升沿有效。

1.3 时钟

值得注意的是,这里通常多个CLK有要求,这里SCLK通常是为BCLK,音频还有一个MCLK。我将三种CLK列表如下

image.png

这里MCLK是输入给CODEC内,通常需要是计算的BCLK的256/128倍,内部通告PLL计算得出。

这里SCLK是I2S信号的位时钟,真正传输I2S信号数据位,故其计算方式为: 音频位时钟 = 采样频率 * 采样位数 * 通道

这里LRCK是作为左右通道选择时钟,也叫采样频率。

1.4 I2C

这里知道,针对数据是通过I2S通信,针对命令大部分可以通过I2C,SPI等常用通信方式,这里只介绍I2C。

这里以ES8388为例,I2C时序如下:

image.png

image.png

可以知道,I2C以起始位,芯片地址,读写标志,应答,寄存器地址,应答,结束位。七个部分组成。

对于声卡,我们知道I2C的芯片地址和寄存器地址即可进行通信。

1.5 设计举例

综上可知,对于CODEC的电路设计,可以如下:

image.png

1.6 其他

其他通用的PCM和PDM这里不做详谈。

至此,可以清楚知道,声卡CODEC和主控的连接关系。

二、设备树解析

2.1 设备树举例

这里具体设备树如下:

image.png

image.png

image.png

可以看到,这里分为三个设备,一个是I2C的控制设备,名字为"everest,es8388",一个为声卡设备,名字为"rockchip,multicodecs-card",一个为I2S控制器,名字为“rockchip,rk3588-i2s-tdm”。我把功能列表如下。

image.png

2.2 设备树节点 这里我列表解释驱动内置的设备树熟悉和其含义

image.png

三、驱动解析 3.1 I2C设备驱动 从设备树可以了解,驱动这边作为component注册到音频,并且分配了mclk时钟。主要流程如下。

image.png

值得注意的两个结构体:

component结构体,实现电源控制和声卡路由控制组件

image.png

dai driver结构体

image.png

3.2 声卡设备驱动

从设备树可以了解,声卡驱动主要注册声卡设备。但还做了其他的事情,如耳机检测,声卡控制等等。具体代码流程如下。

image.png

这里其实需要注意声卡的几个属性的填充,例如snd_soc_dai_link(声卡侧的dai),dapm_widget(电源管理组件),kcontrol(路由控制的回调),jack_report(耳机上报),后面会提到。

3.3 I2S控制器驱动

从设备树可以了解,主要是I2S总线控制器的驱动,其中启用了dma0的0通道和1通道。具体代码流程如下。

image.png

I2S总线控制器也是作为component注册到音频,同时通过pcm dma申请了dma通道。

四、内核ALSA框架解析

4.1 ASoC介绍

内核ALSA实际上也叫ASoC(ALSA System on Chip),早期和普通驱动一样,放在内核driver目录下,后面为了改善如下问题,单独将sound子系统放在kernel根目录

image.png

上述是官方说明,个人可能理解有原因如下:

早期Linux Kernel没有对声卡有过较多和较深入的发展,但是Linux Kernel因为开源的关系广泛用于各个设备,针对每个设备复杂的显示和声音设备,内核不能一一兼容,开源社区旨在将显示和声音剥离成应用层(微内核和模块化的思想),从而诞生X11(显示服务器),但是声卡却迟迟没有一个系统的框架,故Linux Kernel团队主动将ALSA框架从driver目录剥离出来为sound目录,所以我们可以看到,ALSA框架早期是完全由Linux Kernel团队开发的框架,包括了上层的alsa仓库(可以从内核文档找到libalsa和libpcm的编写规范)。

个人观点可能是声卡实在是既算不上很大的框架,又算不上很小的框架,社区没大佬愿意接盘做。所以一直处于这个临界态:明明只是驱动,但是却和arch,fs,kernel,init,virt,net这类大的功能放在一起了。

4.2 ASoC框架

image.png

如上是整个alsa框架示意图。

这里内核ALSA设计理念主要在于抽象,抽象了platform,codec,machine三层关系,platform实际是dma搬运驱动,codec是声卡芯片,而machine完全抽象用作声卡

而三层直接的两个实际层codec和platform通过dai link。当然dai也是抽象,主要抽象了所有的接口设备例如i2s,pcm,pdm等等。其框架图如下:

image.png

对于新的驱动设计,我们主要需要设计machine驱动,修改codec驱动,调试platform驱动。主要原因是

对于machine驱动,每个机器组合不一样,此驱动需要根据实际硬件设计来调整和修改驱动,linux主线提供了sound/soc/generic/simple-card.c样例,瑞芯微提供了sound/soc/rockchip/rockchip_multicodecs.c的驱动样例。

对于codec驱动,每个机器硬件codec可能不一样,但是对于machine是<rockchip,codec>绑定上去的。此驱动只需要根据硬件原理图变更进行修改。如果新的codec驱动,codec芯片厂商能够提供,集成即可。

对于platform驱动,实际上是soc的i2s总线控制器和dma数据搬运,平台默认是良好的,无需修改任何代码,只需要在音频数据出现问题时进行有效调试,例如XRUN问题。

驱动注册api调用流程如下:

对于machine:

image.png

对于codec

image.png

对于platform

image.png

对于上述框架总体,我们可以注册声卡来提供流设备/dev/snd/pcmC%iD%i%c,提供控制设备/dev/snd/controlXX。

这样对于上层使用音频,只需要用/dev/snd/controlXX控制声卡和用/dev/snd/pcmC%iD%i%c来传输音频。

五、音频传输分析

5.1 音频传输流程

对于音频的传输,大致流程如下:

image.png

从userspace到dma和dma到I2S上两条路径,dma都在参与,这里就会涉及到如下两个问题

userspace到DMA的数据量 DMA搬运到i2s的数据量

5.2 环形缓冲区

为了解决两个数据量快慢的方式,通常是用的生产消费者模型来设计缓冲区,内核将其设计成了环形缓冲区,如下所示

image.png

也就是一边读,一边写,只要两边不冲突,这个缓冲区可以永远运行下去。如下是实际的缓冲区设计。

image.png 这里解释如下:

image.png

也就是说,当应用下发一次音频数据,会触发dma开始搬运,这时候先会出现hw_ptr_base和第一个buffer size,应用会主动填充appl_ptr,hw_ptr是实际内核搬运指针。播放的时候,appl_ptr一直在前面,并且超过第一个buffer size时,再新增一个buffer size,而hw_ptr永远在appl_ptr后面追赶。boundary是当前缓冲区的边界,这里代码如下:

image.png

image.png 注意boundary大小一直是buffer_size的倍数如下

image.png

对于上述,我们可以清楚的知道,声卡在数据传输过程中,需要良好的处理好生产者和消费者的冲突。如果生产者和消费者一直没问题,则声卡播放和录音就没问题。

当然通常也不会出问题,除非设置失误(其实也不会设置失误,因为hw_param会refine),也就是说,通常是声卡不支持导致,更多问题还是在codec和硬件。所以如果遇到环形缓冲区的问题,先查codec和硬件本身问题通常不会错,最后再查platform层I2S总线和DMA搬运的问题。

六、音频路由分析

6.1 DAPM概念

上面说了pcm stream 设备/dev/snd/pcmC%iD%i%c的环形缓冲区处理的原理,通过pcm stream设备可以进行音频的播放和录制,但是音频在播放和录制之前,需要设置音频通路,否则codec未设置在一个正常状态下不会正常工作。所以这才有了音频路由的诞生。对于内核,将其封装为DAPM(Dynamic Audio Power Management)。DAPM设计了如下概念:

image.png

image.png

image.png

image.png

这里需要注意的是,route结构体先是sink,再是source。这里会影响大家查看代码理顺音频通路。connected是控件的回调,用于真正的控制Codec寄存器。

6.2 声卡控件

上面可以知道,内核DAPM会设置Codec控件,那么这个控件实际上是对应的声卡内部的基本数字电路单元,主要有Mux和Mixer,PGA,ADC,DAC等等

通常,这些控件会在Codec的datasheet中找到。这里举例如下:

image.png

上图可以看到基本电子电路元器件,我们需要控制的则是这个Codec芯片内部这些电子元器件,使得其内部接通,从而Codec才能正常工作

6.3 播放路由

根据上面可以知道,对于Codec芯片,在播放和录制之前,需要对Codec内部进行控件设置,只有设置对了,声卡才会正常工作。下面举例播放和录制的音频通路需要设置情况

播放情况的控件设置如下

image.png

这边可以发现如果需要其正常工作,通路应该如下,我列表出来,这里只是接出来LOUT接喇叭和INPUT接麦克风,bypass和其他场景不阐述。

image.png

对于代码路由举例,这里只举例一个播放和录音的通路(仅Codec侧)例子。如下:

播放通路

SND_SOC_DAPM_AIF_IN("I2S IN", "Playback", 0, SND_SOC_NOPM, 0, 0), {"Left DAC", NULL, "I2S IN"}, {"Left Mixer", "Left Playback Switch", "Left DAC"}, {"Left Out 1", NULL, "Left Mixer"} {"LOUT1", NULL, "Left Out 1"}, SND_SOC_DAPM_OUTPUT("LOUT1"),

到这里LOUT1已经是SND_SOC_DAPM_OUTPUT类型的widget了

录制通路

SND_SOC_DAPM_INPUT("LINPUT1"), {"Differential Mux", "Line 1", "LINPUT1"}, {"Left PGA Mux", "DifferentialR", "Differential Mux"}, {"Left ADC Mux", "Stereo", "Left PGA Mux"}, {"Left ADC Power", NULL, "Left ADC Mux"}, {"Left ADC", NULL, "Left ADC Power"}, {"I2S OUT", NULL, "Left ADC"}, SND_SOC_DAPM_AIF_OUT("I2S OUT", "Capture", 0, SND_SOC_NOPM, 0, 0),

通路还不止这些,虽然主要是Codec侧的设置,但是Card侧也需要配置DAPM的route。主要如下

声卡侧

image.png

这里可以知道LOUT1的输出是Headphone

也可以知道LINPUT1 的输入是 Main Mic

这样整个链路就直接通了,如下:

播放:Playback---> I2S IN ---> Left DAC ---> Left Mixer ---> Left Out 1 ---> LOUT1 --→ Speaker -→Headphone 录制:Main Mic ---> LINPUT2 ---> Differential Mux ---> Left PGA Mux ---> Left ADC Mux ---> Left ADC Power ---> Left ADC ---> I2S OUT ---> Capture

这里可能有疑问了,原理图上的通路怎么和代码实际有点出入,这里主要原因还是和芯片寄存器设置有关系,有些寄存器Codec芯片并不强制要求你设置,有些寄存器Codec的框图并没有明确写出来,这时候需要具体情况具体分析。

对于Codec,我们需要拿到芯片的user guide/datasheet,例如ES8388的用户手册《ES8388 user Guide.pdf》从文档中找到《BLOCK DIAGRAM》,通常的,我们也只需要关心Mux和Mixer,其他基本不关心,最多关心PGA的gain大小。

对于代码,我们看到驱动内实现的snd_soc_dapm_route 结构体已经把所有的通路已经设置好了。我们只需要关心能通的路即可,没必要专注每个电路元器件。(snd_soc_dapm_route结构体一般是提前提供好的,一般人不会改这个)

6.4 通路配置

根据上述描述,音频路由一旦打通,肯定是要做点动作的,这里主要是两点

根据kcontrol设置codec寄存器,打开mux和mixer开关 设置widget属性,控制上电时序 针对控制寄存器,kcontrol提供了多个宏,主要两个如下:

image.png 通过上述宏和变体,即可正常的控制对于的寄存器开关状态。

针对widget属性,它封装了kcontrol的控件,将其设置为DAPM的属性,主要如下:

image.png

其他的就不列了,对应的组件封装的意义在于上电时序,DAPM默认上电时序和下电时序如下:

上电时序

image.png

下电时序

image.png

从这里可以知道,dapm定义的widget会安装固定的流程进行上电,具象化就例如水流一样, 什么时候流向哪里,最后能够流向终点即成功。

那么什么时候触发水流呢,这时候就是DAPM的事件了,通常的我们知道,声音只有开始,停止,暂停,挂起,唤醒这些事件,实际上DAPM所有事件如下

image.png

这样,在打开音频播放和录制的时候,会主动下发事件,根据对应的事件,按照DAPM的时序进行逐步上电和下电,从而达到DAPM的控制。

这样,从整体看来,音频通过kcontrol来描述组件,在其之上,通过widgt封装了一层,其意义在于实现dapm的电源时序管理。同时为了控制kcontrol控件打开的流程和顺序,又设计了route概念,通过通路来确定声卡工作流程。

七、wav格式解析

wav是一种传统的音频格式保存方法。下面是WAV的整体格式

image.png

下面是具体wav格式分布情况

image.png

对应结构体如下(alsa-utils/aplay/formats.h):

image.png

WaveHeader是包含文件的长度。具体如下

image.png 这里文件大小为:137126,注意这里会主动减去自身的长度8,所以文件长度为137126 + 8 = 137134

image.png

Chunk标识如下

image.png

image.png

具体如下:

image.png

image.png WaveFmtBody包含音频基本信息,具体如下

image.png

这里可以获取到alsa测试音频Front_Center.wav的格式为pcm(01 00),通道为单通道(01 00),采样率为48000(80 bb 00 00),字节率为96000(00 77 01 00),采样字节数(02 00),采样位数(10 00)

这里字节率计算方式为:通道数 * 采样率 * 采样位数 / 8 = (1 * 48000 * 16 / 8 = 96000)

这里采样字节数计算方式为: 通道数 * 采样位数 / 8 = (1 * 16 / 8 = 2)

image.png 最后是data chunk的标识,具体如下,内容为实际音频数据

image.png

这里知道真实的音频数据长度为 13709个字节。加上头的44个字节,就是实际文件大小 13709 + 44 = 137134

至此,通过wav文件格式解析,对于分析声卡是否能够播放能够做基本的判断。

八、播放流程解析

播放录制都是通过pcm流文件来控制,涉及结构体如下

image.png

对于文件的播放,主要流程如下:

image.png

文件打开后需要ioctl,正常用到如下

image.png

这里注意的是,为什么要说refine和params,这两个ioctl涉及到的问题最多,通常是设置参数相关,也就是,音频无法播放,一定是params设置错误,并且refine也refine参数不对导致的。

至于sync_ptr的指针同步,上面缓冲区已经提到了。这里不深究。

综上可以知道,以播放为例子,应用对内核下发的所有动作。

以aplay为例,播放一个wav文件,会先从wav中解析头44个字节,获取到wav文件格式的参数,例如通道,采样率,采样位数等信息,然后根据这些信息会先refine检测一下,然后如果内核有匹配的params,则通过hw_params下发。

根据aplay的例子,可以知道,一个音频无法播放或者播放不对,一定是驱动refine错误或者hw_params下发到codec上不支持导致的。以es8388为例,如下

image.png

这里可以知道,.rate和.formats是codec能支持的声卡。如不在支持内,则hw_param下发后会报错。

九、调试错误分享

根据上述介绍,已经可以大致了解如下

image.png

基本的alsa框架已经讲解清楚,其他细节点这里没有去说明。接下来做错误和调试分享

9.1 proc 声卡信息查看

find /proc/asound/

proc文件系统内有声卡的基本信息查看,主要查看如下

image.png

9.2 mclk和其他clk信息查看

cat /sys/kernel/debug/clk/clk_summary

image.png

9.3 直接修改寄存器

cat /proc/iomem

image.png

image.png

9.4 i2c工具调试

i2cdetect i2cdump i2cset i2cget

9.5 strace 调试

strace aplay -Dhw:1,0 startup.wav > 1.log 2>&1

9.6 amixer控制控件

image.png

9.7 XRUN调试

CONFIG_SND_DEBUG=y CONFIG_SND_PCM_XRUN_DEBUG=y

会出现如下文件

/proc/asound/card0/pcm0p/xrun_debug

设置级别即可

echo 7 > /proc/asound/card0/pcm0p/xrun_debug

9.8 ftrace

cd /sys/kernel/debug/tracing echo "snd_pcm:applptr" >> set_event echo "snd_pcm:hwptr" >> set_event echo "snd_pcm:xrun" >> set_event tail -f trace

9.9 dma中断

image.png

9.10 debugfs

cd /sys/kernel/debug/asoc

image.png

image.png

9.11 alsamixer

mixer调试器

image.png

9.13 驱动文档

image.png

image.png

至此,已经可以具备所有alsa相关的调试技巧。