1. 从一次"声卡不出声"的调试经历说起
嵌入式Linux下面搞音频,最让人抓狂的不是写驱动本身,而是你明明把codec驱动加载了、DMA也配好了、时钟也算对了,结果aplay一跑,要么是静默无声,要么是满耳噪声,要么干脆报个-22错误码让你怀疑人生。我最早接触ALSA SoC框架是在一块i.MX6的板子上,当时用的是WM8960这颗codec,硬件同事拍着胸脯说"线路绝对没问题",结果我调了整整三天才让喇叭发出第一声。后来复盘才发现,问题根本不在codec驱动,也不在CPU的DAI驱动,而是卡在了machine驱动这一层——那个看起来最"薄"、最像"胶水"的驱动,恰恰是整条音频链路能不能跑通的关键。
ALSA SoC(ASoC)这套框架把嵌入式音频系统拆成了三块:Platform(平台/CPU侧DAI与DMA)、Codec(编解码芯片)、Machine(机器/板级胶水)。前两块在Linux内核里都有大量现成的驱动,很多时候你只需要在设备树里改改参数就能用。但Machine驱动不一样,它是板级专属的,每一块板子的走线、时钟源、codec与CPU的连接方式都可能不同,所以内核社区没法给你一个通用的。这也是为什么很多新手移植音频时,codec和platform都搞定了,最后卡在machine这一层——因为这块代码往往得自己写,或者至少得自己改。
这篇内容我打算把Machine驱动从"它到底在干什么"到"怎么写一个能跑的"完整讲一遍。适合已经能编译内核、会看设备树、但对ASoC三层结构还比较模糊的嵌入式Linux开发者。我会结合snd_soc_dai_link、snd_soc_card、snd_soc_ops这几个核心结构体,把Machine驱动里每一行代码的意图讲清楚,再补上几个我实际踩过的坑。看完之后,你应该能独立给一块新板子写出可用的machine驱动,而不是对着内核里的示例代码照抄还抄不明白。
2. Machine驱动到底在ASoC三层里扮演什么角色
2.1 三层结构的分工边界
先把ASoC的三层结构用一句话说清楚,不然后面写代码会一直糊里糊涂。
Platform驱动管的是CPU这一侧的数字音频接口(DAI)和DMA传输。比如i.MX的SAI、SSI,全志的I2S,瑞芯微的I2S/TDM,这些控制器怎么配置、DMA怎么搬数据、FIFO水位怎么设,都是Platform驱动的事。它对应内核里的snd_soc_platform_driver和snd_soc_dai_driver(CPU DAI部分)。
Codec驱动管的是那颗编解码芯片。WM8960、ES8323、RT5651、NAU8822这些芯片,内部有寄存器、有增益控制、有各种mixer和mux,怎么初始化、怎么设采样率对应的时钟分频、怎么控制音量,都是Codec驱动的事。它对应snd_soc_codec_driver和snd_soc_dai_driver(Codec DAI部分)。
Machine驱动管的是"这两者怎么连起来"。它要回答几个问题:这块板子上CPU的哪个DAI连到了codec的哪个DAI?用的是哪一路时钟(MCLK从哪来、BCLK和LRCLK谁做master)?DAI的格式是I2S还是左对齐还是DSP模式?板子上有没有额外的功放、耳机检测、麦克风偏置需要控制?这些信息全部由Machine驱动来描述。
打个比方:Platform是"发动机",Codec是"变速箱",Machine就是"传动轴和安装支架"——它决定了发动机和变速箱怎么对接、装在车架的哪个位置。发动机和变速箱本身再好,对接错了车也跑不起来。
2.2 为什么Machine驱动不能通用
有人会问:既然都是把CPU DAI和Codec DAI连起来,为什么内核不做一个通用的Machine驱动?
原因在于板级差异太大。同样是WM8960,A板子可能用I2S接CPU的SAI1,MCLK由codec自己产生;B板子可能用左对齐接SAI2,MCLK由CPU提供;C板子可能在codec和喇叭之间还挂了一颗功放,需要额外GPIO控制使能;D板子可能codec的MCLK是从一颗外部晶振来的,跟CPU完全无关。这些差异没法用参数穷举,所以内核的做法是:把Machine驱动做成板级专属的,放在sound/soc/<厂商>/目录下,比如sound/soc/fsl/imx-wm8960.c、sound/soc/rockchip/rockchip_rt5651.c。
这也意味着,当你拿到一块新板子,如果内核里没有对应的machine驱动,你就得自己写一个。好消息是,Machine驱动的代码量通常不大,核心就是填几个结构体,难的是把时钟和DAI格式搞对。
2.3 Machine驱动的核心数据结构全景
在动手写之前,先把几个关键结构体的关系理清楚。这是后面所有代码的基础。
| 结构体 | 作用 | 关键字段 |
|---|---|---|
snd_soc_dai_link | 描述一条CPU DAI到Codec DAI的连接 | cpu_dai_name、codec_dai_name、platform_name、codec_name、dai_fmt、ops |
snd_soc_card | 描述整张声卡 | name、dai_link、num_links、dapm_widgets、dapm_routes、controls |
snd_soc_ops | DAI链路的回调操作 | hw_params、hw_free、prepare、trigger、set_sysclk |
snd_soc_dapm_widget | DAPM电源管理部件 | 板级额外widget(功放、麦克风等) |
snd_soc_dapm_route | DAPM连接路由 | widget之间的连接关系 |
snd_kcontrol_new | 板级自定义控件 | 功放开关、耳机检测等 |
一张声卡(snd_soc_card)可以包含多条DAI链路(dai_link),比如同时有播放和录音、或者有多个codec。每条链路描述一对CPU DAI和Codec DAI的连接。ops里的回调是Machine驱动介入音频流程的钩子,最常用的是hw_params(配置时钟和格式)和set_sysclk(设置主时钟)。
3. 手写一个Machine驱动的完整流程
3.1 从设备树开始:把板级连接信息描述清楚
现代内核(4.x以后)强烈推荐用设备树来描述Machine驱动,而不是像老内核那样在C代码里硬编码。设备树里主要描述三件事:CPU DAI是哪个、Codec是哪个、两者怎么连。
以一块i.MX6 + WM8960的板子为例,设备树大概长这样:
sound { compatible = "fsl,imx6q-sabresd-wm8960", "fsl,imx-audio-wm8960"; model = "wm8960-audio"; cpu-dai = <&ssi2>; audio-codec = <&codec>; audio-routing = "Headphone Jack", "HP_L", "Headphone Jack", "HP_R", "Ext Spk", "SPK_LP", "Ext Spk", "SPK_LN", "LINPUT1", "Mic Jack", "Mic Jack", "MICB"; mux-int-port = <2>; mux-ext-port = <3>; hp-det-gpios = <&gpio5 4 GPIO_ACTIVE_LOW>; mic-det-gpios = <&gpio1 9 GPIO_ACTIVE_LOW>; };这里几个字段值得说明。cpu-dai指向CPU侧的DAI控制器节点,audio-codec指向codec节点。audio-routing是DAPM路由,描述板级物理连接,比如"Headphone Jack"这个输出口连到了codec的"HP_L"和"HP_R"引脚。mux-int-port和mux-ext-port是i.MX特有的,因为i.MX的SSI内部有个audmux,需要配置内部端口和外部端口的映射。hp-det-gpios和mic-det-gpios是耳机和麦克风检测引脚。
注意:
audio-routing里的名字必须和codec驱动里定义的widget名字完全一致,大小写都不能错。我见过有人把"Headphone Jack"写成"Headphone jack",结果DAPM路由死活建不起来,音频路径不通,查了半天才发现是大小写问题。
设备树写完之后,Machine驱动的C代码要做的事就是:解析这些属性,填充snd_soc_dai_link和snd_soc_card,然后注册声卡。
3.2 填充snd_soc_dai_link:把CPU和Codec接起来
snd_soc_dai_link是Machine驱动最核心的结构。老内核里这些字段是静态填的,新内核推荐用devm_snd_soc_register_card配合设备树动态解析。但不管哪种方式,核心字段就那几个。
static struct snd_soc_dai_link imx_wm8960_dai = { .name = "wm8960", .stream_name = "WM8960", .cpu_dai_name = "ssi2", .codec_dai_name = "wm8960-hifi", .platform_name = "imx-pcm-audio", .codec_name = "wm8960.0-001a", .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF | SND_SOC_DAIFMT_CBS_CFS, .ops = &imx_wm8960_ops, };逐个字段解释。name是这条链路的逻辑名字,stream_name是给用户空间看的流名字。cpu_dai_name要和CPU DAI驱动注册的名字对上,codec_dai_name要和codec驱动里snd_soc_dai_driver的name对上。platform_name指向DMA平台驱动,i.MX上通常是imx-pcm-audio。codec_name是codec在I2C总线上的设备名,格式一般是<i2c总线名>-<地址>,比如wm8960.0-001a表示I2C0上地址0x1a的wm8960。
dai_fmt是重点,它由三部分组成,用或运算组合:
- 格式部分:
SND_SOC_DAIFMT_I2S、SND_SOC_DAIFMT_LEFT_J、SND_SOC_DAIFMT_RIGHT_J、SND_SOC_DAIFMT_DSP_A、SND_SOC_DAIFMT_DSP_B。这决定了数据相对于帧同步信号的对齐方式。 - 时钟反转部分:
SND_SOC_DAIFMT_NB_NF(正常BCLK、正常LRCLK)、SND_SOC_DAIFMT_IB_NF(反转BCLK)、SND_SOC_DAIFMT_NB_IF(反转LRCLK)、SND_SOC_DAIFMT_IB_IF(都反转)。 - 主从部分:
SND_SOC_DAIFMT_CBS_CFS(codec做BCLK和LRCLK的master)、SND_SOC_DAIFMT_CBM_CFM(codec做master)、SND_SOC_DAIFMT_CBS_CFM(codec做BCLK slave、LRCLK master)等。
主从配置是最容易搞错的地方。原则是:一条总线上只能有一个master。如果codec做master,那CPU就得做slave,反之亦然。具体谁做master,取决于硬件设计——通常看MCLK是谁提供的,以及板子上有没有外部晶振。如果codec有独立晶振,一般让codec做master;如果MCLK从CPU来,通常CPU做master。
3.3 实现snd_soc_ops:时钟配置的关键回调
ops里的回调是Machine驱动真正"干活"的地方。最核心的是hw_params和set_sysclk。
static int imx_wm8960_hw_params(struct snd_pcm_substream *substream, struct snd_pcm_hw_params *params) { struct snd_soc_pcm_runtime *rtd = substream->private_data; struct snd_soc_dai *codec_dai = rtd->codec_dai; struct snd_soc_dai *cpu_dai = rtd->cpu_dai; int ret; /* 根据采样率设置codec的sysclk */ ret = snd_soc_dai_set_sysclk(codec_dai, WM8960_SYSCLK_AUTO, 0, SND_SOC_CLOCK_IN); if (ret < 0) { dev_err(rtd->dev, "failed to set codec sysclk: %d\n", ret); return ret; } /* 设置CPU DAI的时钟分频 */ ret = snd_soc_dai_set_sysclk(cpu_dai, IMX_SSI_SYSCLK, 0, SND_SOC_CLOCK_IN); if (ret < 0) { dev_err(rtd->dev, "failed to set cpu sysclk: %d\n", ret); return ret; } return 0; }hw_params在每次音频流参数确定时被调用,采样率、位宽、声道数都在params里。这里要根据采样率去配置codec的PLL和分频,让MCLK能产生正确的BCLK和LRCLK。WM8960的WM8960_SYSCLK_AUTO表示让codec自动根据采样率算PLL,省得手动算分频。
set_sysclk回调则是在需要设置主时钟频率时被调用:
static int imx_wm8960_set_sysclk(struct snd_soc_dai *dai, int clk_id, unsigned int freq, int dir) { /* 如果MCLK从CPU的SSI输出,需要配置SSI的时钟源 */ if (dir == SND_SOC_CLOCK_OUT) { /* 配置CPU输出MCLK */ } return 0; }这里有个经验:MCLK的频率通常是采样率的256倍或384倍。比如44.1kHz采样率,MCLK常见是11.2896MHz(256倍)或22.5792MHz(512倍)。如果MCLK频率不对,codec内部PLL锁不住,出来的声音就会变调或者全是噪声。我调WM8960时就遇到过MCLK给成了12MHz(对应48kHz的256倍),但实际播放44.1kHz,结果声音明显偏快,改成11.2896MHz就正常了。
3.4 注册声卡与DAPM路由配置
把dai_link和card填好之后,最后一步是注册。
static struct snd_soc_card imx_wm8960_card = { .name = "wm8960-audio", .owner = THIS_MODULE, .dai_link = &imx_wm8960_dai, .num_links = 1, .dapm_widgets = imx_wm8960_dapm_widgets, .num_dapm_widgets = ARRAY_SIZE(imx_wm8960_dapm_widgets), .dapm_routes = imx_wm8960_dapm_routes, .num_dapm_routes = ARRAY_SIZE(imx_wm8960_dapm_routes), .controls = imx_wm8960_controls, .num_controls = ARRAY_SIZE(imx_wm8960_controls), }; static int imx_wm8960_probe(struct platform_device *pdev) { struct snd_soc_card *card = &imx_wm8960_card; card->dev = &pdev->dev; return devm_snd_soc_register_card(&pdev->dev, card); }DAPM widget和route是板级特有的。比如板子上有一颗外部功放,需要GPIO使能,就得定义一个widget:
static const struct snd_soc_dapm_widget imx_wm8960_dapm_widgets[] = { SND_SOC_DAPM_HP("Headphone Jack", NULL), SND_SOC_DAPM_SPK("Ext Spk", NULL), SND_SOC_DAPM_MIC("Mic Jack", NULL), SND_SOC_DAPM_LINE("Line In", NULL), };这些widget名字要和设备树audio-routing里的名字对应。DAPM的好处是,它会在音频路径被使用时自动上电相关部件,不用时自动断电,省电又省心。
4. 时钟与DAI格式:Machine驱动最容易翻车的地方
4.1 MCLK、BCLK、LRCLK三者的关系
这三个时钟是音频接口的基础,搞不清它们的关系,Machine驱动基本没法调。
MCLK(Master Clock)是codec的主时钟,通常是采样率的256倍或384倍。它可以是CPU提供的,也可以是外部晶振提供的,还可以是codec自己PLL产生的。MCLK是codec内部所有时钟的源头。
BCLK(Bit Clock)是位时钟,每个bit传输一个BCLK周期。BCLK频率 = 采样率 × 位宽 × 声道数。比如48kHz、16bit、双声道,BCLK = 48000 × 16 × 2 = 1.536MHz。
LRCLK(Left/Right Clock,也叫帧同步)是声道时钟,频率等于采样率。每个LRCLK周期传输一个左声道和一个右声道的数据。
三者的关系是:MCLK经过codec内部分频产生BCLK,BCLK再分频产生LRCLK。所以MCLK必须足够高,才能分出所需的BCLK。如果MCLK太低,分不出高采样率所需的BCLK,就会出问题。
提示:计算MCLK时,先确定最高采样率和位宽,算出所需BCLK,再确保MCLK至少是BCLK的若干倍(通常codec要求MCLK ≥ 2×BCLK)。比如要支持192kHz/24bit/双声道,BCLK = 192000 × 24 × 2 = 9.216MHz,MCLK至少得18.432MHz,实际常用24.576MHz。
4.2 主从模式选错的典型症状
主从模式配错,症状很有特点,可以帮你快速定位。
如果双方都做master,BCLK和LRCLK会有两个驱动源在打架,结果是时钟波形混乱,声音要么完全没声,要么是刺耳的噪声。
如果双方都做slave,没有时钟源,BCLK和LRCLK一直是低电平,codec收不到时钟,aplay会卡住或者报DMA超时。
如果BCLK和LRCLK的master不一致(比如一个CBS_CFM),可能出现能播放但采样率不对、声音变调的情况。
排查方法:用示波器量BCLK和LRCLK引脚。正常工作时,BCLK应该是干净的方波,LRCLK是占空比50%的方波。如果量不到波形,说明master配置有问题;如果波形频率不对,说明分频算错了。
4.3 用示波器和aplay快速验证链路
调音频时,我习惯用一套固定的验证流程,能快速定位问题出在哪一层。
第一步,aplay -l看声卡有没有注册成功。如果看不到声卡,说明Machine驱动probe失败,去dmesg里找错误。
第二步,aplay -D hw:0,0 test.wav播放一个已知正常的wav文件。如果报错,看错误码。-22(EINVAL)通常是格式或参数不支持,-16(EBUSY)是设备被占用,-5(EIO)是DMA传输错误。
第三步,用示波器量MCLK、BCLK、LRCLK。MCLK应该在codec的MCLK引脚上,BCLK和LRCLK在CPU和codec之间的连线上。三个时钟都正常,基本就能出声了。
第四步,如果时钟正常但没声,检查DAPM路由。amixer里看看有没有"Headphone"、"Speaker"这些控件,用amixer sset 'Headphone' 100%把音量拉满,再amixer sset 'Headphone' unmute取消静音。很多时候不是驱动问题,是默认静音了。
5. 调试实战:几个我踩过的坑和排查链路
5.1 声卡注册失败:从dmesg倒推问题
有一次给一块新板子写machine驱动,aplay -l死活看不到声卡。dmesg | grep -i snd看到这么一行:
imx-wm8960 sound: ASoC: failed to instantiate card -517-517是-EPROBE_DEFER,意思是依赖的某个设备还没准备好。这通常是因为codec驱动还没probe,或者CPU DAI还没注册。排查方法是看probe顺序:dmesg里codec的probe信息在machine之前还是之后。如果codec还没probe,machine就会defer,等codec好了再重试。
但有时候会一直defer,那就要查codec为什么没probe。常见原因是I2C地址不对、codec的reset GPIO没配、或者codec的供电没打开。我遇到过一次是codec的VDD没使能,I2C能读到ID但probe失败,加上regulator配置就好了。
5.2 有噪声但无音乐:DMA与格式的锅
还有一次,声卡注册成功了,aplay也不报错,但喇叭里只有"沙沙"的噪声,没有音乐。这种情况通常是数据格式对不上。
排查思路:先确认dai_fmt里的格式(I2S/左对齐/右对齐)和codec、CPU DAI双方是否一致。如果CPU按I2S发,codec按左对齐收,数据就会错位,出来就是噪声。然后确认位宽,16bit和24bit混了也会出噪声。
我那次的问题是CPU DAI的dai_fmt配的是I2S,但codec驱动默认是左对齐,两边不一致。在machine驱动的hw_params里显式调用snd_soc_dai_set_fmt把两边都设成I2S就好了。
5.3 播放变调:采样率与MCLK不匹配
变调问题我前面提过,这里展开说。声音变快或变慢,本质是实际输出的采样率和文件采样率不一致。
原因通常是MCLK频率和采样率不匹配。比如MCLK固定给12MHz,播放44.1kHz的文件,codec按12MHz去分频,算出来的实际采样率是46.875kHz,声音就偏快。解决办法是让MCLK随采样率变化,或者在hw_params里根据采样率重新配置codec的PLL。
WM8960这类codec支持WM8960_SYSCLK_AUTO,会自动根据采样率算PLL,比较省事。但有些codec需要手动算分频系数,这时候就得查codec手册里的PLL公式,把MCLK、采样率代进去算。
5.4 耳机检测与功放使能的GPIO处理
板级GPIO处理是Machine驱动的另一个重点。耳机检测(hp-det)和功放使能(amp-enable)这两个最常见。
耳机检测一般用gpio_request加中断,检测到耳机插入时,切换DAPM路由,把输出从喇叭切到耳机。功放使能则是在播放时拉高GPIO,停止时拉低,避免功放一直工作产生底噪。
static int imx_wm8960_hp_detected(struct snd_soc_jack *jack, int status, void *data) { struct snd_soc_card *card = data; if (status & SND_JACK_HEADPHONE) { /* 耳机插入,切换到耳机输出 */ snd_soc_dapm_disable_pin(&card->dapm, "Ext Spk"); snd_soc_dapm_enable_pin(&card->dapm, "Headphone Jack"); } else { /* 耳机拔出,切回喇叭 */ snd_soc_dapm_disable_pin(&card->dapm, "Headphone Jack"); snd_soc_dapm_enable_pin(&card->dapm, "Ext Spk"); } snd_soc_dapm_sync(&card->dapm); return 0; }这里要注意,snd_soc_dapm_sync必须调用,否则路由切换不会立即生效。另外GPIO的中断触发方式(上升沿/下降沿/双边沿)要和硬件设计匹配,配错了会检测不到插拔。
6. 从能跑到好用:Machine驱动的进阶优化
6.1 多DAI链路与多codec场景
一块板子上有多个音频接口很常见,比如同时有WM8960(耳机+喇叭)和另一颗codec(比如蓝牙音频或HDMI音频)。这时候card里就要配多条dai_link。
static struct snd_soc_dai_link imx_multi_dai[] = { { .name = "wm8960", .stream_name = "WM8960", .cpu_dai_name = "ssi2", .codec_dai_name = "wm8960-hifi", .platform_name = "imx-pcm-audio", .codec_name = "wm8960.0-001a", .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF | SND_SOC_DAIFMT_CBS_CFS, .ops = &imx_wm8960_ops, }, { .name = "bt-sco", .stream_name = "Bluetooth SCO", .cpu_dai_name = "bt-sco-dai", .codec_dai_name = "bt-sco-codec", .platform_name = "imx-pcm-audio", .codec_name = "bt-sco", .dai_fmt = SND_SOC_DAIFMT_DSP_A | SND_SOC_DAIFMT_IB_NF | SND_SOC_DAIFMT_CBS_CFS, }, };多条链路时,每条链路的dai_fmt和ops可以不同。用户空间会看到多个PCM设备,aplay -l里会列出hw:0,0、hw:0,1等。要注意的是,多条链路共享同一个platform时,DMA资源要分配好,避免冲突。
6.2 低功耗设计:DAPM与runtime PM配合
嵌入式设备对功耗敏感,音频子系统的功耗优化主要靠DAPM和runtime PM。
DAPM会自动管理音频路径上各部件的电源,不用时自动关闭。但前提是widget和route配得完整。如果route漏了,DAPM可能误判某个部件还在使用,导致关不掉,功耗下不来。
runtime PM则是在整个声卡空闲时把相关时钟、电源域关掉。Machine驱动里可以在ops的trigger回调里处理:
static int imx_wm8960_trigger(struct snd_pcm_substream *substream, int cmd) { switch (cmd) { case SNDRV_PCM_TRIGGER_START: /* 播放开始,打开时钟和功放 */ break; case SNDRV_PCM_TRIGGER_STOP: /* 播放停止,关闭时钟和功放 */ break; } return 0; }实测下来,配好DAPM和runtime PM之后,音频空闲时的功耗能从几十毫安降到几毫安,对电池供电的设备很关键。
6.3 用amixer和tinyplay做回归验证
驱动写完不是终点,得有一套回归验证方法。我习惯用amixer和tinyplay(tinyalsa工具集里的)做快速验证。
amixer用来检查控件和路由:
amixer -c 0 controls # 列出所有控件 amixer -c 0 sset 'Headphone' 80% # 设置耳机音量 amixer -c 0 sset 'Headphone' unmute # 取消静音 amixer -c 0 cget name='Headphone' # 查看当前值tinyplay用来播放测试:
tinyplay test.wav -D 0 -d 0 # 播放到card0 device0 tinycap rec.wav -D 0 -d 0 -c 2 -r 48000 -b 16 # 录2声道48kHz 16bit回归验证的清单:播放、录音、耳机插拔、音量调节、采样率切换(44.1k/48k/96k)、多声道。每一项都过一遍,基本能覆盖大部分问题。
6.4 设备树与驱动代码的对应关系梳理
最后强调一下设备树和驱动代码的对应关系,这是新手最容易乱的地方。
设备树里的cpu-dai、audio-codec、audio-routing、mux-int-port这些属性,在驱动里通过of_property_read_*或snd_soc_of_parse_*系列函数解析。比如snd_soc_of_parse_audio_routing解析audio-routing,snd_soc_of_parse_card_name解析model。
驱动的probe函数里,一般先解析设备树,填充dai_link和card,再注册。如果设备树属性名写错,解析会失败,probe就挂了。所以改设备树之后,一定要看dmesg里有没有解析失败的报错。
我个人的习惯是,写machine驱动时先把设备树和驱动的字段对应关系列个表,逐个核对,能省很多调试时间。这个表大概长这样:
| 设备树属性 | 驱动解析函数 | 填充到 |
|---|---|---|
model | snd_soc_of_parse_card_name | card->name |
cpu-dai | of_parse_phandle | dai_link->cpu_of_node |
audio-codec | of_parse_phandle | dai_link->codec_of_node |
audio-routing | snd_soc_of_parse_audio_routing | card->dapm_routes |
hp-det-gpios | devm_gpiod_get_optional | 耳机检测GPIO |
把这张表填满,驱动基本就成型了。剩下的就是调时钟、调格式、调路由,这些靠实测和示波器慢慢磨。我调一块新板子的音频,从写驱动到出声,快的话半天,慢的话两三天,主要时间都花在时钟和格式的反复验证上。但只要把Machine驱动这层的逻辑吃透,后面再遇到新板子,基本就是套模板改参数的事。