音频是内核的一个驱动框架,个人有过音频相关的工作经验,阅读过内核alsa框架代码并具有一些调试技巧,本着分享知识,提高团队技术能力的目的,在此进行音频alsa框架的整体解析,希望阅读此文章的同事后续均能从事音频相关内核开发。当然,于己也是一种回顾和复习,
本文计划将alsa整体全貌介绍一遍,不清楚音频内核的可以走马观花一次,后面接触了可以继续深入,了解音频内核的可以一起精进。本文仅个人经验总结,非权威知识体系,不可全信,如有错误请指出。alsa大概涉及解析点如下:
数字音频 设备树解析 设备驱动解析 内核alsa框架解析 音频路由解析 wav格式解析 播放流程解析 通常错误分享
本文不会涉及pulseaudio及上层中间件和配置文件等系统相关技巧,仅从底层查看和分析问题。
本文面向研发技术人员,需要具备设备树理解能力,C语言阅读能力,内核子系统了解能力,内核调试基本技巧,音频基础知识,操作系统基本概念。文章提到相关基本知识不会详细解释,建议通过网络搜索的方式了解。
数字音频接口简称DAI(Digital Audio Interfaces),它主要在DSP处理模数和数模转换中和主机进行数据传输,通常DAI指的是I2S、PCM、PDM、DCODEC、VAD、SPDIF等。本文主要讲I2S。
基于DAI的音频方案如下:
这里红框内的称之为DAI,而红框外的是模拟电路喇叭和麦克风。我简单解释喇叭和麦克风一下,注意:模拟电路涉及知识深奥,这里只做了解。
喇叭通常称之为功放,是利用小的电压输出从而激励出大的声压,从而产生人耳能听到的声音。最简单的功放电路如下。
注意,这里的VCC是PA的Power,通过控制这个电,能够有效的控制PA的控制和扩大功放大小和降低PA的功耗。通常的,代码是作为kcontrol的一个控件设置。
麦克风通常是电容感应的方式,空气的声压能够影响电容的容值,从而产生电荷的变化,从而转换成数字电压信号。最简单的电容感应电路如下。
注意,这里的+V是麦克的偏置电压,通常是mic bias,通过控制这个电,能够有效控制mic的噪声和降低麦克风的功耗。通常的,代码是set_bias_level回调设置。
综上可知,对于集成ADC+DSP+DAC + .... 组件的设备,通常作为音频codec芯片。芯片和主控直接连接如下:
这里CLK可以多个,CS是片选,DATA是I2S的数据传输。通过CLK+CS+DATA三类信号连接,通常是I2S总线
I2S信号有多种变体(PHILIPS模式/左对齐模式/右对齐模式),需要根据芯片手册选择,这里以ES8388举例(PHILIPS模式),其I2S默认如下。
如上可以知道:LRCK作为声道的片选,LRCK为低为左声道,LRCK为高为右声道,左声道从LRCK下降沿后的第二个时钟上升沿有效,右声道从LRCK上升沿的第二个时钟上升沿有效。
值得注意的是,这里通常多个CLK有要求,这里SCLK通常是为BCLK,音频还有一个MCLK。我将三种CLK列表如下
这里MCLK是输入给CODEC内,通常需要是计算的BCLK的256/128倍,内部通告PLL计算得出。
这里SCLK是I2S信号的位时钟,真正传输I2S信号数据位,故其计算方式为: 音频位时钟 = 采样频率 * 采样位数 * 通道
这里LRCK是作为左右通道选择时钟,也叫采样频率。
这里知道,针对数据是通过I2S通信,针对命令大部分可以通过I2C,SPI等常用通信方式,这里只介绍I2C。
这里以ES8388为例,I2C时序如下:
可以知道,I2C以起始位,芯片地址,读写标志,应答,寄存器地址,应答,结束位。七个部分组成。
对于声卡,我们知道I2C的芯片地址和寄存器地址即可进行通信。
综上可知,对于CODEC的电路设计,可以如下:
其他通用的PCM和PDM这里不做详谈。
至此,可以清楚知道,声卡CODEC和主控的连接关系。
这里具体设备树如下:
可以看到,这里分为三个设备,一个是I2C的控制设备,名字为"everest,es8388",一个为声卡设备,名字为"rockchip,multicodecs-card",一个为I2S控制器,名字为“rockchip,rk3588-i2s-tdm”。我把功能列表如下。
2.2 设备树节点 这里我列表解释驱动内置的设备树熟悉和其含义
三、驱动解析 3.1 I2C设备驱动 从设备树可以了解,驱动这边作为component注册到音频,并且分配了mclk时钟。主要流程如下。
值得注意的两个结构体:
component结构体,实现电源控制和声卡路由控制组件
dai driver结构体
从设备树可以了解,声卡驱动主要注册声卡设备。但还做了其他的事情,如耳机检测,声卡控制等等。具体代码流程如下。
这里其实需要注意声卡的几个属性的填充,例如snd_soc_dai_link(声卡侧的dai),dapm_widget(电源管理组件),kcontrol(路由控制的回调),jack_report(耳机上报),后面会提到。
从设备树可以了解,主要是I2S总线控制器的驱动,其中启用了dma0的0通道和1通道。具体代码流程如下。
I2S总线控制器也是作为component注册到音频,同时通过pcm dma申请了dma通道。
内核ALSA实际上也叫ASoC(ALSA System on Chip),早期和普通驱动一样,放在内核driver目录下,后面为了改善如下问题,单独将sound子系统放在kernel根目录
上述是官方说明,个人可能理解有原因如下:
早期Linux Kernel没有对声卡有过较多和较深入的发展,但是Linux Kernel因为开源的关系广泛用于各个设备,针对每个设备复杂的显示和声音设备,内核不能一一兼容,开源社区旨在将显示和声音剥离成应用层(微内核和模块化的思想),从而诞生X11(显示服务器),但是声卡却迟迟没有一个系统的框架,故Linux Kernel团队主动将ALSA框架从driver目录剥离出来为sound目录,所以我们可以看到,ALSA框架早期是完全由Linux Kernel团队开发的框架,包括了上层的alsa仓库(可以从内核文档找到libalsa和libpcm的编写规范)。
个人观点可能是声卡实在是既算不上很大的框架,又算不上很小的框架,社区没大佬愿意接盘做。所以一直处于这个临界态:明明只是驱动,但是却和arch,fs,kernel,init,virt,net这类大的功能放在一起了。
如上是整个alsa框架示意图。
这里内核ALSA设计理念主要在于抽象,抽象了platform,codec,machine三层关系,platform实际是dma搬运驱动,codec是声卡芯片,而machine完全抽象用作声卡
而三层直接的两个实际层codec和platform通过dai link。当然dai也是抽象,主要抽象了所有的接口设备例如i2s,pcm,pdm等等。其框架图如下:
对于新的驱动设计,我们主要需要设计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:
对于codec
对于platform
对于上述框架总体,我们可以注册声卡来提供流设备/dev/snd/pcmC%iD%i%c,提供控制设备/dev/snd/controlXX。
这样对于上层使用音频,只需要用/dev/snd/controlXX控制声卡和用/dev/snd/pcmC%iD%i%c来传输音频。
对于音频的传输,大致流程如下:
从userspace到dma和dma到I2S上两条路径,dma都在参与,这里就会涉及到如下两个问题
userspace到DMA的数据量 DMA搬运到i2s的数据量
为了解决两个数据量快慢的方式,通常是用的生产消费者模型来设计缓冲区,内核将其设计成了环形缓冲区,如下所示
也就是一边读,一边写,只要两边不冲突,这个缓冲区可以永远运行下去。如下是实际的缓冲区设计。
这里解释如下:
也就是说,当应用下发一次音频数据,会触发dma开始搬运,这时候先会出现hw_ptr_base和第一个buffer size,应用会主动填充appl_ptr,hw_ptr是实际内核搬运指针。播放的时候,appl_ptr一直在前面,并且超过第一个buffer size时,再新增一个buffer size,而hw_ptr永远在appl_ptr后面追赶。boundary是当前缓冲区的边界,这里代码如下:
注意boundary大小一直是buffer_size的倍数如下
对于上述,我们可以清楚的知道,声卡在数据传输过程中,需要良好的处理好生产者和消费者的冲突。如果生产者和消费者一直没问题,则声卡播放和录音就没问题。
当然通常也不会出问题,除非设置失误(其实也不会设置失误,因为hw_param会refine),也就是说,通常是声卡不支持导致,更多问题还是在codec和硬件。所以如果遇到环形缓冲区的问题,先查codec和硬件本身问题通常不会错,最后再查platform层I2S总线和DMA搬运的问题。
上面说了pcm stream 设备/dev/snd/pcmC%iD%i%c的环形缓冲区处理的原理,通过pcm stream设备可以进行音频的播放和录制,但是音频在播放和录制之前,需要设置音频通路,否则codec未设置在一个正常状态下不会正常工作。所以这才有了音频路由的诞生。对于内核,将其封装为DAPM(Dynamic Audio Power Management)。DAPM设计了如下概念:
这里需要注意的是,route结构体先是sink,再是source。这里会影响大家查看代码理顺音频通路。connected是控件的回调,用于真正的控制Codec寄存器。
上面可以知道,内核DAPM会设置Codec控件,那么这个控件实际上是对应的声卡内部的基本数字电路单元,主要有Mux和Mixer,PGA,ADC,DAC等等
通常,这些控件会在Codec的datasheet中找到。这里举例如下:
上图可以看到基本电子电路元器件,我们需要控制的则是这个Codec芯片内部这些电子元器件,使得其内部接通,从而Codec才能正常工作
根据上面可以知道,对于Codec芯片,在播放和录制之前,需要对Codec内部进行控件设置,只有设置对了,声卡才会正常工作。下面举例播放和录制的音频通路需要设置情况
播放情况的控件设置如下
这边可以发现如果需要其正常工作,通路应该如下,我列表出来,这里只是接出来LOUT接喇叭和INPUT接麦克风,bypass和其他场景不阐述。
对于代码路由举例,这里只举例一个播放和录音的通路(仅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。主要如下
声卡侧
这里可以知道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结构体一般是提前提供好的,一般人不会改这个)
根据上述描述,音频路由一旦打通,肯定是要做点动作的,这里主要是两点
根据kcontrol设置codec寄存器,打开mux和mixer开关 设置widget属性,控制上电时序 针对控制寄存器,kcontrol提供了多个宏,主要两个如下:
通过上述宏和变体,即可正常的控制对于的寄存器开关状态。
针对widget属性,它封装了kcontrol的控件,将其设置为DAPM的属性,主要如下:
其他的就不列了,对应的组件封装的意义在于上电时序,DAPM默认上电时序和下电时序如下:
上电时序
下电时序
从这里可以知道,dapm定义的widget会安装固定的流程进行上电,具象化就例如水流一样, 什么时候流向哪里,最后能够流向终点即成功。
那么什么时候触发水流呢,这时候就是DAPM的事件了,通常的我们知道,声音只有开始,停止,暂停,挂起,唤醒这些事件,实际上DAPM所有事件如下
这样,在打开音频播放和录制的时候,会主动下发事件,根据对应的事件,按照DAPM的时序进行逐步上电和下电,从而达到DAPM的控制。
这样,从整体看来,音频通过kcontrol来描述组件,在其之上,通过widgt封装了一层,其意义在于实现dapm的电源时序管理。同时为了控制kcontrol控件打开的流程和顺序,又设计了route概念,通过通路来确定声卡工作流程。
wav是一种传统的音频格式保存方法。下面是WAV的整体格式
下面是具体wav格式分布情况
对应结构体如下(alsa-utils/aplay/formats.h):
WaveHeader是包含文件的长度。具体如下
这里文件大小为:137126,注意这里会主动减去自身的长度8,所以文件长度为137126 + 8 = 137134
Chunk标识如下
具体如下:
WaveFmtBody包含音频基本信息,具体如下
这里可以获取到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)
最后是data chunk的标识,具体如下,内容为实际音频数据
这里知道真实的音频数据长度为 13709个字节。加上头的44个字节,就是实际文件大小 13709 + 44 = 137134
至此,通过wav文件格式解析,对于分析声卡是否能够播放能够做基本的判断。
播放录制都是通过pcm流文件来控制,涉及结构体如下
对于文件的播放,主要流程如下:
文件打开后需要ioctl,正常用到如下
这里注意的是,为什么要说refine和params,这两个ioctl涉及到的问题最多,通常是设置参数相关,也就是,音频无法播放,一定是params设置错误,并且refine也refine参数不对导致的。
至于sync_ptr的指针同步,上面缓冲区已经提到了。这里不深究。
综上可以知道,以播放为例子,应用对内核下发的所有动作。
以aplay为例,播放一个wav文件,会先从wav中解析头44个字节,获取到wav文件格式的参数,例如通道,采样率,采样位数等信息,然后根据这些信息会先refine检测一下,然后如果内核有匹配的params,则通过hw_params下发。
根据aplay的例子,可以知道,一个音频无法播放或者播放不对,一定是驱动refine错误或者hw_params下发到codec上不支持导致的。以es8388为例,如下
这里可以知道,.rate和.formats是codec能支持的声卡。如不在支持内,则hw_param下发后会报错。
根据上述介绍,已经可以大致了解如下
基本的alsa框架已经讲解清楚,其他细节点这里没有去说明。接下来做错误和调试分享
find /proc/asound/
proc文件系统内有声卡的基本信息查看,主要查看如下
cat /sys/kernel/debug/clk/clk_summary
9.3 直接修改寄存器
cat /proc/iomem
i2cdetect i2cdump i2cset i2cget
strace aplay -Dhw:1,0 startup.wav > 1.log 2>&1
CONFIG_SND_DEBUG=y CONFIG_SND_PCM_XRUN_DEBUG=y
会出现如下文件
/proc/asound/card0/pcm0p/xrun_debug
设置级别即可
echo 7 > /proc/asound/card0/pcm0p/xrun_debug
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
cd /sys/kernel/debug/asoc
mixer调试器
至此,已经可以具备所有alsa相关的调试技巧。