干了几年嵌入式Linux驱动,音频这一块永远是最容易让人头皮发麻的。寄存器手册厚得像砖头,数据手册里一句话能让你查一整天,好不容易调通I2C读写,接上喇叭又没声音。后来把ASoC框架捋顺了,再看Codec驱动和音频控件,其实就是一个套路:组件、控件、DAPM路径、DAI配置,四件事而已。
这篇文章我结合自己做过的几颗Codec芯片,把ASoC音频驱动里最核心的控件注册与Codec驱动开发从头到尾拆一遍。内容包括框架设计思路、kcontrol控件机制的完整说明、DAPM路径的接法,以及实际调试中常见的坑。适合正在做嵌入式Linux音频驱动、或者刚接触ALSA想搞懂ASoC到底在干什么的朋友。
1. ASoC音频子系统的设计思路拆解
1.1 为什么要拆成Codec、Platform、Machine三块
早期Linux音频驱动并不是现在这个样子。那时候每个板子的音频驱动都是一个大杂烩:Codec芯片的初始化、I2S控制器的寄存器配置、DMA缓冲区的申请、板级音频路径的切换,全部揉在同一个文件里。换一颗Codec,改板子,甚至换一个DMA通道,都可能把整个驱动推翻重写。大量重复代码散落在各个平台,维护起来非常痛苦。
ASoC(ALSA System on Chip)就是为了解决这个问题而诞生的。它把音频驱动按职责拆成三个独立角色:
- Codec:负责音频编解码芯片本身,比如音量、EQ、ADC/DAC开关、采样率配置。你写的驱动主要就是这一块。
- Platform:负责SoC内部的音频控制器,比如I2S/TDM外设、DMA引擎。它不管Codec芯片上有什么寄存器,只负责把数据从内存搬到总线。
- Machine:负责把上面两者匹配起来。它定义一个dai_link,说清楚“CPU侧DAI接到Codec侧DAI,使用I2S格式,主从关系谁是主”。板级的喇叭、耳机、MIC的物理连接也归它管。
打个比方,就像一套家庭音响系统。Platform是播放器,负责读碟、输出数字流;Codec是功放加解码器,负责把数字信号变成模拟信号驱动喇叭;Machine就是那根连接线加音频切换器,决定播放器该接到哪个功放、功放该去推哪个音箱。
这套拆分的好处是:换一颗Codec只需要写新的Codec驱动,Platform和Machine基本不动;换平台则只需要重写Platform层,Codec驱动甚至可以被多个平台复用。这是整个ASoC架构最核心的设计理念。
1.2 从播放一首歌看音频数据的实际流向
理解了三个角色,再看一条完整的播放路径就非常清晰了。用户在应用层调用ALSA库播放一首MP3,解码之后得到PCM数据,这些数据进入内核后大致经历这么几个环节:
- Platform层的DMA把内存里的PCM数据搬运到I2S控制器的FIFO;
- I2S控制器按设定的帧格式(比如16bit、双声道、44.1kHz)把数据一位一位送到总线上的Codec芯片;
- Codec芯片的DAI(Digital Audio Interface)接收数据,经过内部通路,比如DAC转换、模拟加法器、可编程增益放大器(PGA),最后从SPK_OUT或HP_OUT引脚输出到喇叭或耳机。
这一整条链路,在ASoC里就是一组dai_link加上一组DAPM路径(route)。dai_link负责描述“I2S总线两端是哪两个DAI在通信”,DAPM路径负责描述“Codec内部模拟信号从哪个输入走到哪个输出”。调试音频驱动,绝大部分时间就是在查这条链路断在哪。
对应的驱动代码里,hw_params回调是所有参数协商的核心。比如Codec驱动里的hw_params函数,需要根据Linux ALSA传入的采样率、声道数、格式,去设置Codec内部与时钟、数据格式相关的寄存器;Platform层的hw_params则设置DMA的传输格式。两边都配置好了,PCM流才能真正跑起来。
2. 控件(kcontrol)系统:用户看到的旋钮和开关
2.1 内核控件与用户空间的映射关系
在用户空间调整音量、切换输入源、打开或关闭某个功放,你操作的其实是一系列“控件”(control)。在ASoC框架里,这些控件在内核侧就叫kcontrol,每个控件有一个名字、一组属性,以及读/写寄存器的回调函数。
用一条命令就能看到一个声卡上注册了哪些控件。在设备终端执行:
amixer contents输出会像这样:
numid=1,iface=MIXER,name='PCM Volume' ; type=INTEGER,access=rw------,values=2,min=0,max=255,step=0 : values=128,128 numid=2,iface=MIXER,name='Speaker Switch' ; type=BOOLEAN,access=rw------,values=1 : values=on其中'PCM Volume'就是内核里注册的一个kcontrol,type是INTEGER,范围0到255,双声道所以values=2。audio_hw层面用tinymix操作更直观:
tinymix "PCM Volume" 200 tinymix "Speaker Switch" 1如果把嵌入式Linux音频驱动比作一个调音台,控件就是调音台上的推子和开关。用户不需要关心底层寄存器地址是0x1F还是0x3A,只需要知道“PCM Volume”这个名字,内核替他把对应寄存器的位域改好。
2.2 常用控件宏与参数计算
ASoC提供了一组便捷宏,定义控件时不需要手动写完整结构体。最常见的几个是:
SOC_SINGLE:单个寄存器单个位域,比如一个通道的音量。SOC_DOUBLE:两个寄存器或同一寄存器的两个位域,通常用于左右声道音量。SOC_SWITCH:单个开关位,控制某个通路的开与关。SOC_ENUM:枚举型控件,比如输入源选择、数据格式选择。
拿一个典型场景举例。假设Codec芯片手册写着:“Volume Register,地址0x1F,bit 8到bit 12,共5位,控制左声道音量,0表示静音,0x1F表示最大”,那么用SOC_SINGLE就能非常简洁地描述:
static const struct snd_kcontrol_new vol_controls[] = { SOC_SINGLE("PCM Volume", 0x1F, 8, 0x1F, 0), };这里四个关键参数分别是:控件名、寄存器地址、位偏移、最大值。最后一个参数是是否反转,0表示寄存器值越大音量越大。如果芯片设计反了,音量越大寄存器值越小,就把这里的0改成1。
位域的计算逻辑内核会帮你搞定,内部调用的是snd_soc_component_update_bits(component, reg, mask, val)。mask根据位偏移和位数自动生成,比如上面这个5位字段,mask就是0x1F << 8。所以你在驱动里哪怕写裸代码,也推荐用这个函数而不是直接regmap_write,因为update_bits天然就是“读-改-写”,不会破坏同一寄存器里其他位域的值。
SOC_ENUM稍微特殊一点,它的定义包含一个soc_enum结构体和字符串数组:
static const char * const input_texts[] = {"LINE_IN", "MIC_IN", "AUX_IN"}; static SOC_ENUM_SINGLE_DECL(input_enum, 0x20, 0, input_texts); static const struct snd_kcontrol_new input_controls[] = { SOC_ENUM("Input Source", input_enum), };用户空间读到的是字符串,而不是0/1/2。这个设计对应用层非常友好,也减少了驱动里到处做数字映射的麻烦。
2.3 get/put回调的工作细节
用宏定义控件时,get和put回调是由框架自动生成的,内部调用了snd_soc_component_read和snd_soc_component_update_bits。但有些控件的逻辑不是简单的读写,例如需要跨多个寄存器算出一个值、开关控件与后续DAPM路径联动、或者寄存器含义和用户空间数字不一致时,就必须自己实现get和put回调。
手动实现时,关键点有三个。第一,get回调的任务是读寄存器并把值按ucontrol->value.integer.value[0]的格式填回去,注意如果实际寄存器值和用户空间期望的线性值不一致,要做换算。第二,put回调的任务是校验用户传入的值是否越界,然后写寄存器,并且如果写失败了要返回错误码。第三,返回值有讲究:写了并且值变化,返回1;写了但值和原来一样,返回0;出错返回负数。很多初学者统一return 0或return 1,结果某些ALSA上层工具只能得到错误反馈,表现为“控件能读不能写”或“写后自动还原”。
举个例子,一个去重音效开关,寄存器bit3=1表示开启,0表示关闭。用户空间约定的是布尔值:
static int deemp_get(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol) { struct snd_soc_component *component = snd_kcontrol_chip(kcontrol); unsigned int val; snd_soc_component_read(component, 0x21, &val); ucontrol->value.integer.value[0] = !!(val & (1 << 3)); return 0; } static int deemp_put(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol) { struct snd_soc_component *component = snd_kcontrol_chip(kcontrol); unsigned int val, old; snd_soc_component_read(component, 0x21, &old); if (ucontrol->value.integer.value[0]) val = old | (1 << 3); else val = old & ~(1 << 3); if (val != old) return snd_soc_component_update_bits(component, 0x21, 1 << 3, val & (1 << 3)) ? 1 : -EIO; return 0; }注意snd_kcontrol_chip(kcontrol)这个用法。老版本代码里常见snd_kcontrol_chip返回的是注册控件时传入的data指针,通常就是component,拿到它之后再做寄存器访问。
3. Codec驱动开发的完整实操
3.1 第一步:定义regmap和I2C挂载
绝大多数Codec芯片都挂在I2C或SPI总线上,先让内核能找到芯片是第一件事。以I2C接口的Codec为例,驱动文件的基本骨架:
static const struct regmap_config codec_regmap = { .reg_bits = 8, .val_bits = 8, .max_register = 0x7F, }; static int codec_i2c_probe(struct i2c_client *i2c) { struct regmap *regmap; regmap = devm_regmap_init_i2c(i2c, &codec_regmap); if (IS_ERR(regmap)) return PTR_ERR(regmap); return devm_snd_soc_register_component(&i2c->dev, &codec_component_driver, &codec_dai, 1); } static const struct i2c_device_id codec_i2c_id[] = { { "mycodec", 0 }, { } }; MODULE_DEVICE_TABLE(i2c, codec_i2c_id); static const struct of_device_id codec_of_match[] = { { .compatible = "vendor,mycodec" }, { } }; static struct i2c_driver codec_i2c_driver = { .driver = { .name = "mycodec", .of_match_table = codec_of_match, }, .probe = codec_i2c_probe, .id_table = codec_i2c_id, }; module_i2c_driver(codec_i2c_driver);这里reg_bits = 8, val_bits = 8表示寄存器地址和值都是8位。绝大多数小Codec芯片都是这种结构,如WM8960、ES8316这类芯片基本符合。遇到寄存器地址按16位甚至更大范围寻址的芯片,修改reg_bits和max_register即可。
devm_snd_soc_register_component是标准注册接口,第三个参数表示这个Codec带几个DAI。大多数Codec至少提供两个DAI,一个给Playback,一个给Capture,有的芯片还有辅助DAI用于连接蓝牙或语音通道。
3.2 第二步:注册控件和DAPM widget
组件驱动结构体需要填充控件数组和DAPM widget数组。以我们上面定义的音量控件为例,完整的注册逻辑如下:
static const struct snd_soc_component_driver codec_component_driver = { .name = "mycodec", .controls = vol_controls, .num_controls = ARRAY_SIZE(vol_controls), .dapm_widgets = codec_dapm_widgets, .num_dapm_widgets = ARRAY_SIZE(codec_dapm_widgets), .dapm_routes = codec_dapm_routes, .num_dapm_routes = ARRAY_SIZE(codec_dapm_routes), .idle_bias_on = 1, };控件数组和DAPM数组需要注册到组件驱动里,ASoC框架会在probe过程中自动创建对应的ALSA control对象。控件数量不能为0,哪怕芯片只有一个DAC静音开关,也得注册至少一个控件,否则声卡创建出来之后将会几乎没有可操作的东西。
DAPM widget这里先不展开,下一节详细讲。现在只需要知道,芯片内部所有需要电源管理的模块,比如DAC、ADC、PGA、功放、偏置电压,都应该用DAPM widget来描述,而不是用普通控件。
3.3 第三步:配置DAI和PCM回调
Codec驱动的DAI部分是一个snd_soc_dai_driver结构体,里面最关键的是ops和capture/playback成员的配置:
static const struct snd_soc_dai_ops codec_dai_ops = { .hw_params = codec_hw_params, .set_fmt = codec_set_fmt, .set_sysclk = codec_set_sysclk, .mute_stream = codec_mute_stream, }; static struct snd_soc_dai_driver codec_dai = { .name = "mycodec-hifi", .playback = { .stream_name = "Playback", .channels_min = 2, .channels_max = 2, .rates = SNDRV_PCM_RATE_8000_48000, .formats = SNDRV_PCM_FMTBIT_S16_LE, }, .capture = { .stream_name = "Capture", .channels_min = 2, .channels_max = 2, .rates = SNDRV_PCM_RATE_8000_48000, .formats = SNDRV_PCM_FMTBIT_S16_LE, }, .ops = &codec_dai_ops, };rates和formats字段的作用是告诉ASoC内核这套DAI支持什么采样率和数据格式。实际运行时,应用程序请求的格式如果不在这里面,内核会直接返回错误,根本不会调用hw_params。所以这两行的过滤条件很重要,宁可写全一点,也不要漏掉你在驱动的hw_params里真正支持的格式。
hw_params的典型实现,是根据采样率设置Codec内部的时钟分频器:
static int codec_hw_params(struct snd_pcm_substream *substream, struct snd_pcm_hw_params *params, struct snd_soc_dai *dai) { struct snd_soc_component *component = dai->component; int rate = params_rate(params); switch (rate) { case 8000: snd_soc_component_update_bits(component, 0x22, 0x07, 0x00); break; case 44100: snd_soc_component_update_bits(component, 0x22, 0x07, 0x03); break; case 48000: snd_soc_component_update_bits(component, 0x22, 0x07, 0x04); break; default: return -EINVAL; } return 0; }这段代码写的只是示例,真实芯片的分频寄存器要根据MCLK频率和fsync频率计算。比如系统MCLK是12.288MHz,要得到48kHz采样率,BCLK通常是采样率乘位数乘通道数,比如16bit双声道就是48k乘32等于1.536MHz。Codec内部还需要从MCLK分频得到Bit Clock,分频系数就是12.288MHz除1.536MHz等于8。计算清楚再填寄存器,不怕音频无声或跑调。
3.4 第四步:设备树与Machine层关联
Codec驱动本身可以被注册,但要让系统真正使用这颗Codec,需要设备树里描述硬件连接。以常见的I2C挂载方式为例,设备树片段如下:
&i2c1 { mycodec: codec@1a { compatible = "vendor,mycodec"; reg = <0x1a>; #sound-dai-cells = <0>; }; }; sound { compatible = "simple-audio-card"; simple-audio-card,name = "myboard"; simple-audio-card,format = "i2s"; simple-audio-card,bitclock-master = <&dai_cpu>; simple-audio-card,frame-master = <&dai_cpu>; dai_cpu: simple-audio-card,cpu { sound-dai = <&i2s1>; }; dai_link: simple-audio-card,codec { sound-dai = <&mycodec>; }; };设备树里compatible要跟驱动中of_match_table对应,reg要跟芯片硬件地址一致。Machine层的simple-audio-card帮我们省了写Machine驱动的功夫,它通过sound-dai里引用的phandle自动寻找Codec的DAI和CPU的DAI,并把两者绑定成dai_link。
如果不想用simple-audio-card,也可以手写Machine驱动,核心是构造snd_soc_dai_link数组,并指定cpu_dai_name、codec_dai_name、codec_of_node或codec指针等字段。这种方式更灵活,但代码量也更多。
4. DAPM与音频路径:驱动“通”才是硬道理
4.1 DAPM控件的本质
DAPM(Dynamic Audio Power Management,动态音频电源管理)是ASoC里最巧妙也最容易劝退新人的部分。它的核心思想是:根据音频信号的流向,自动开关音频链路中的各个模块电源。比如你只播放音乐,不上录音,ADC和相关模拟电路就不需要供电;你把耳机拔了,功放就可以休眠。
DAPM控件和我们在第2节讲的普通控件不一样。普通控件只是简单映射寄存器和用户空间,DAPM控件除了映射寄存器之外,还要参与“电源状态”的传播。每个DAPM widget(控件)都有一个power状态,框架通过遍历音频路径,判断某个widget是否需要开启。如果路径上游有信号源、下游有终端,中间所有widget都会自动开启;反之则自动关闭,并在关闭时调用你注册的event回调去做所谓“延迟断电”处理。
常见的DAPM widget类型有:
SND_SOC_DAPM_INPUT:外部输入引脚,比如MIC、LINE_IN。SND_SOC_DAPM_OUTPUT:外部输出引脚,比如HP_OUT、SPK_OUT。SND_SOC_DAPM_DAC/SND_SOC_DAPM_ADC:数模/模数转换器。SND_SOC_DAPM_PGA:可编程增益放大器,往往和音量控件配合。SND_SOC_DAPM_MUX/SND_SOC_DAPM_MUX:输入源选择。SND_SOC_DAPM_SWITCH:通路的开关。SND_SOC_DAPM_SUPPLY:供电项,不作为音频信号路径,只给其他widget供电。
定义DAPM widget的宏类似:
static const struct snd_soc_dapm_widget codec_dapm_widgets[] = { SND_SOC_DAPM_INPUT("MIC_IN"), SND_SOC_DAPM_INPUT("LINE_IN"), SND_SOC_DAPM_DAC("DAC", "Playback", 0x23, 0, NULL, 0), SND_SOC_DAPM_ADC("ADC", "Capture", 0x24, 0, NULL, 0), SND_SOC_DAPM_PGA("SPK PGA", NULL, 0x25, 0, NULL, 0), SND_SOC_DAPM_OUTPUT("SPK_OUT"), };注意DAC那个widget后面的"Playback",它和DAI里的stream_name对应,表示DAC放在播放链路上。如果名字不匹配,DAPM的电源传播会断掉,导致有声卡设备但实际没有声音输出。
4.2 把播放路径串起来的routes
widget定义好之后,要用route把它们连起来。route用snd_soc_dapm_route描述,每个route由三个字段组成:source、sink、control。比如:
static const struct snd_soc_dapm_route codec_dapm_routes[] = { { "Input Source", "MIC_IN", "MIC_IN" }, { "Input Source", "LINE_IN", "LINE_IN" }, { "ADC", NULL, "Input Source" }, { "DAC", NULL, "ADC" }, /* 这是错的,只是演示说明 */ { "SPK PGA", NULL, "DAC" }, { "SPK_OUT", NULL, "SPK PGA" }, };注意route的方向是从信号源到信号接收者。{ "ADC", NULL, "Input Source" }表示信号从Input Source流向ADC。NULL代表这条路径不需要开关控制;如果路径经过一个MUX或SWITCH,第三个字段就是对应的枚举控件名,DAPM会根据用户空间选择控件状态来自动决定是否连通这条路径。
一个极常见的坑:MCU新手会把route顺序写反,写成{ "MIC_IN", NULL, "Input Source" },于是信号永远无法从模拟输入传到ADC,录音全是静音。调试时如果波形无论如何都进不来,先检查route方向。
音频路径的完整度直接决定芯片能不能出声。拿播放来说,数据进入Codec后,至少要走:DAI输入 -> DAC -> 模拟混音器或PGA -> SPK_OUT。其中任何一环没有route连接,信号就会断开。检查时可以在内核调试目录里查看整条链路:
cat /sys/kernel/debug/asoc/soc-name/dapm_widgets cat /sys/kernel/debug/asoc/soc-name/dapm_paths这两个文件会列出所有widget及连接关系。如果播放时某一行显示DAC: off,说明DAPM没有把你需要的DAC通电。这时候要顺着route往上查,看看是不是某个input信号没有“联通”,导致路径根本没建立起来。
4.3 DAPM事件回调与电源时序
DAPM不只是一个静态的路径图,widget在上下电时还会触发事件。典型场景是功放:功放启动时如果DAC已经输出信号,会产生POP音;功放关闭时如果DAC还在播放,也会突然截断产生爆音。所以功放widget往往会加上event回调,在供电前后设置一个延时。
定义带事件的widget:
static int spk_pga_event(struct snd_soc_dapm_widget *w, struct snd_kcontrol *kcontrol, int event) { switch (event) { case SND_SOC_DAPM_PRE_PMD: /* 先关功放 */ snd_soc_component_update_bits(w->dapm->component, 0x26, 0x01, 0x00); break; case SND_SOC_DAPM_POST_PMU: /* 等DAC稳定后开功放 */ msleep(50); snd_soc_component_update_bits(w->dapm->component, 0x26, 0x01, 0x01); break; } return 0; } SND_SOC_DAPM_PGA_E("SPK PGA", 0x25, 0, NULL, 0, spk_pga_event, SND_SOC_DAPM_PRE_PMD | SND_SOC_DAPM_POST_PMU),这里的时序逻辑是DAPM非常值钱的地方。事件类型很多,常见有PRE_PMU、POST_PMU、PRE_PMD、POST_PMD。实际项目中,在POST_PMU里做延时启动,在PRE_PMD里先做静音,能有效解决大部分POP音问题。事件回调里允许调用msleep,因为DAPM初始化是在线程上下文中执行的,不必担心原子环境。
5. 实战问题排查与经验记录
5.1 用户空间找不到控件
这个问题的出现频率最高。驱动加载正常,声卡也出现了,但tinymix里看不到你注册的控件名。排查顺序建议这样:
- 确认控件有没有写进组件的控件数组,以及
num_controls是否用了ARRAY_SIZE。少写一个控件数组入口,后面所有控件都不会被注册。 - 确认控件名是否和用户空间期望一致。内核允许重名控件,但如果同一个卡里出现两个同名控件,tinymix操作时可能只会操作到第一个,容易产生“改了没反应”的错觉。
- 检查设备树里Codec节点有没有
#sound-dai-cells。如果这个属性缺失,Machine层可能无法正确绑定Codec,控件注册流程没有走完。 - 用
cat /proc/asound/card0/codec#0或cat /sys/kernel/debug/asoc/cards查看当前ASoC识别到的组件和控件状态。
还有一种很迷惑的现象:新版内核用DAPM控件代替了一些普通控件,你在tinymix里看到的名字可能是DAPM自动生成的,比如DAC Playback Volume。这种名字往往带前缀或后缀。遇到这种情况直接在/proc/asound/里搜索关键字更靠谱。
5.2 无声、爆音和POP音的排查顺序
没有声音是最难定位的问题,因为它既可能是软件配置问题,也可能是硬件线路问题。我的调试习惯是按下述顺序逐一排查,不要一上来先怀疑寄存器配置:
- 第一步:确认硬件通路。用示波器看I2S总线的BCLK、FSYNC、DATA波形。如果DATA完全没有波形,问题在DMA和Platform层;如果波形正常再去查Codec内部。
- 第二步:确认控件状态。用tinymix把所有涉及到的控件都打开,音量调到最大,尤其注意PCM Playback Volume和Speaker Switch这两个最常见的卡控。
- 第三步:确认DAPM路径。查看
/sys/kernel/debug/asoc/.../dapm_widgets,看DAC、PGA、功放是否都处于on状态。路径断点往往在这里一目了然。 - 第四步:确认Codec寄存器。通过regmap debugfs或串口打印读取关键寄存器实际值。有些控件被用户空间改了,但寄存器没有变化,说明regmap配置或缓存有问题。
POP音和爆音的逻辑稍微不同。POP音多数是模拟偏置建立时间不足,功放上电太快。这时可以拉长PGA事件回调的启动延时,或者把功放放在DAPM路径的更下游,让DAC先稳定。爆音往往和电源突然断开有关,优化的方向是断电前先切到静音状态。
5.3 regmap、时钟与缓存问题
用regmap做Codec寄存器管理是大势所趋,但也带来几个隐藏点。第一个是regmap cache,默认是用regmap_config.cache_type控制的。如果你在probe阶段写过寄存器初始化序列,后来用户空间改控件,再读取时寄存器缓存和硬件真实值可能不一致。排查这类问题最简单粗暴的方法是临时把cache_type设为REGCACHE_NONE,看现象是否变化。
第二个是时钟问题。很多Codec在上电时需要一个稳定的MCLK信号,否则I2C上哪怕寄存器写对了,芯片内部PLL也锁不住,采样率会乱,表现为“有声音但明显变调”或者“异常抖动”。可以先用固定频率的晶振或外部信号源测试,排除MCLK波动因素。系统侧确认clk_enable是否正常,以及set_sysclk里的频率参数是否传对了。
第三个是信号格式问题。I2S、左对齐、右对齐、DSP格式,Codec侧和CPU侧必须一致。在set_fmt回调里,主机/从机角色(CBS/CFS、CBM/CFM)也必须匹配。如果simple-audio-card,bitclock-master定义错误,Codec侧会自动切换主从角色。这种问题通常表现为BCLK没有输出或连续报帧同步错误,内核日志里会有ASoC: failed to start之类的报错。
写在最后
我自己调试Codec驱动时,最大的感悟有两点。第一是ASoC框架里每个抽象都不是白做的:kcontrol管用户可见的操作,DAPM管电源和路径,dai_link管数据连接。遇到问题先归类,比盲目翻寄存器手册效率高得多。第二是音频调试一定要会看波形,没有示波器就去查逻辑分析仪,光靠眼睛瞪内核日志,排查无声问题的效率会非常低。
如果手里的芯片是常见的WM、ES、TLV、AK系列,先找同系列芯片的现有驱动看结构,再对着自己手册改寄存器。照着框架搭一遍,比看十篇文章都管用。希望这篇笔记能帮你少走几步弯路。