资讯详情

资讯详情

嵌入式Linux ASoC机器类驱动实战:从踩坑到精通

1. 从一个踩坑故事说起为什么机器类驱动值得单独拎出来讲几年前接手一个车载中控项目主芯片是i.MX6Q音频编解码器用的是SGTL5000。硬件同事把原理图丢过来I2S走线、I2C控制线、MCLK时钟全部连好了我心想这活儿不难照着内核里现成的dts改改就行。结果板子回来一上电aplay能跑但声音出来全是爆音采样率一切换直接卡死。折腾了整整三天最后发现问题出在机器类驱动里dai_link的platform和codec名字对不上导致ASoC在注册时把两个不匹配的组件硬绑在一起时钟配置全乱了。这件事让我意识到ASoC这套框架里机器类驱动Machine Driver是最容易被忽视、但出问题最难查的一环。CPU DAI驱动和Codec驱动通常芯片原厂都给你写好了你基本不用动但机器类驱动是板级专属的每一块新板子都得自己写或者大改它决定了“哪块CPU的哪个I2S接口”跟“哪颗Codec的哪个DAI”配对以及时钟怎么走、DAPM路由怎么连。说白了它就是音频子系统的“婚介所”加“交通调度中心”。这篇内容适合谁看如果你正在做嵌入式Linux音频开发手上有块新板子要bringup音频或者你已经在用ASoC但只会抄dts、出了问题不知道怎么排查那这篇就是写给你的。我会从ASoC的整体架构切入把机器类驱动的核心结构体、注册流程、时钟配置、DAPM路由、常见故障排查全部拆开讲一遍尽量做到你照着就能改出自己的板级驱动。2. ASoC三层架构拆解机器类驱动到底站在哪个位置2.1 三层各管什么Platform、Codec、MachineASoC把嵌入式音频系统拆成三个独立驱动层这个设计思路跟ALSA老框架最大的区别就是解耦。老框架里一个snd_card把所有东西揉在一起换个Codec就得大改ASoC把职责分清楚之后CPU侧的东西归PlatformCodec侧的东西归Codec板级连接关系归Machine。Platform驱动管的是SoC内部的音频接口也就是CPU DAI比如i.MX的SSI、SAI全志的I2S瑞芯微的I2S/TDM。它负责配置DMA、FIFO、时钟分频这些跟SoC强相关的东西。对应的结构体是snd_soc_platform_driver和snd_soc_dai_driverCPU侧那部分。Codec驱动管的是外部编解码芯片比如WM8960、SGTL5000、ES8388、NAU8822这些。它负责芯片内部的寄存器配置、增益控制、静音开关、DAPM widget定义。对应snd_soc_codec_driver和snd_soc_dai_driverCodec侧那部分。现在新内核里Codec驱动逐渐被snd_soc_component_driver统一但老代码里还是Codec那套。Machine驱动管的是“这两者怎么连”。它不碰具体寄存器只负责三件事第一声明dai_link把CPU DAI和Codec DAI绑在一起第二定义板级时钟MCLK从哪来、给谁用第三建立DAPM路由把Codec内部的widget和板子上的物理连接喇叭、耳机、麦克风串起来。对应snd_soc_card和snd_soc_dai_link这两个核心结构体。注意很多人以为Machine驱动只是“胶水”随便写写就行。实际上时钟配置和DAPM路由这两块如果写错表现出的症状千奇百怪——爆音、无声、单声道、采样率切换失败、休眠唤醒后失声全都可能跟Machine有关。2.2 为什么要有Machine这一层解耦带来的实际好处举个实际例子。假设你用的是同一颗i.MX6ULL但做了两款产品A款配SGTL5000B款配WM8960。如果没有Machine层你得为两款产品各写一份完整的音频驱动里面CPU部分和Codec部分混在一起代码重复率极高。有了Machine层之后CPU DAI驱动和两颗Codec驱动都是现成的你只需要写两份很薄的Machine驱动每份可能就一两百行只描述“怎么连”就行。再往深了说Machine层还承担了一个关键职责时钟树描述。嵌入式音频的时钟来源很灵活MCLK可能来自SoC的PLL也可能来自外部晶振还可能由Codec自己产生再反喂给SoC。这些板级差异没法在CPU驱动或Codec驱动里写死只能放在Machine层。所以Machine驱动虽然代码量小但信息密度极高每一行都关键。2.3 数据流与控制流一次播放到底经过了什么理解数据流对排查问题特别有帮助。当你执行aplay test.wav时大致经过这些环节ALSA lib把PCM数据通过/dev/snd/pcmC0D0p写入内核。ASoC的PCM中间层soc-pcm.c接管根据dai_link找到对应的CPU DAI和Codec DAI。CPU DAI驱动配置I2S控制器启动DMA把数据从内存搬到I2S FIFO。I2S控制器按配置的格式I2S/左对齐/右对齐、位宽、采样率把数据串行输出到物理引脚。Codec DAI接收I2S数据流按自己的配置解码成模拟信号。Codec内部的DAPM根据当前通路状态把模拟信号路由到耳机或喇叭输出。控制流则是另一条路amixer设置音量时走的是kcontrol路径直接操作Codec寄存器dapm的电源管理是自动的当PCM流启动时ASoC会根据路由表自动上电相关widget。Machine驱动在这两条流里都有参与数据流里它提供了dai_link的绑定关系控制流里它提供了DAPM路由表和板级kcontrol。所以一旦Machine写错两条路都会出问题。3. 机器类驱动核心结构体逐字段精讲3.1 snd_soc_dai_link绑定关系的核心snd_soc_dai_link是Machine驱动里最重要的结构体它描述了一条音频链路。我拿一个实际项目里的配置来逐字段讲static struct snd_soc_dai_link my_board_dai_link { .name SGTL5000, .stream_name SGTL5000 HiFi, .cpu_dai_name 202c000.ssi, .codec_dai_name sgtl5000, .platform_name 202c000.ssi, .codec_name sgtl5000.0-000a, .dai_fmt SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF | SND_SOC_DAIFMT_CBM_CFM, .ops my_board_ops, };name和stream_name是给用户空间看的aplay -l列出来的就是stream_name。这两个字段随便起但建议起得有意义方便调试时辨认。cpu_dai_name必须跟CPU DAI驱动里注册的名字完全一致。这个名字通常是设备树里节点的名字比如202c000.ssi。怎么确认去/sys/kernel/debug/asoc/下面看或者直接看CPU DAI驱动里snd_soc_dai_driver.name字段。名字对不上是新手最常犯的错误症状是Machine注册时直接报-ENODEVprobe失败。codec_dai_name是Codec内部DAI的名字比如SGTL5000就是sgtl5000WM8960是wm8960-hifi。这个也得跟Codec驱动里注册的一致。platform_name通常跟cpu_dai_name一样因为DMA引擎是挂在CPU DAI上的。但有些SoC把DMA单独抽出来做成一个platform驱动这时候名字就不一样了得查具体SoC的文档。codec_name是I2C设备名格式是i2c_client_name.bus-addr比如sgtl5000.0-000a表示I2C0上地址0x0a的sgtl5000。这个字段在新内核里可以省略ASoC会自动匹配但老内核里必须写对。dai_fmt是格式配置这个字段极其关键写错了直接爆音或者无声。它由三部分组成用或运算组合配置项可选值含义协议格式SND_SOC_DAIFMT_I2S标准I2SWS变化后一个BCLK才出数据SND_SOC_DAIFMT_LEFT_J左对齐WS变化同时出数据SND_SOC_DAIFMT_RIGHT_J右对齐SND_SOC_DAIFMT_DSP_A/BDSP模式TDM常用时钟极性SND_SOC_DAIFMT_NB_NFBCLK正常WS正常SND_SOC_DAIFMT_IB_NFBCLK反相WS正常SND_SOC_DAIFMT_NB_IFBCLK正常WS反相SND_SOC_DAIFMT_IB_IF都反相主从模式SND_SOC_DAIFMT_CBM_CFMCodec是BCLK和WS的主SND_SOC_DAIFMT_CBS_CFSCodec是BCLK和WS的从SND_SOC_DAIFMT_CBM_CFSCodec出BCLKSoC出WSSND_SOC_DAIFMT_CBS_CFMCodec出WSSoC出BCLK主从模式这块特别容易搞混。CBM是Codec Bit MasterCFM是Codec Frame Master。如果Codec是主SoC就是从那SoC的I2S控制器要配置成从模式接收外部BCLK和WS。实际项目里大多数情况是SoC做主CBS_CFS因为SoC的时钟更稳定。但有些Codec比如某些USB音频芯片只能做主那就得配成CBM_CFM。3.2 snd_soc_card整张声卡的容器snd_soc_card代表一整张声卡它把多个dai_link、DAPM路由、kcontrol、时钟配置全部装在一起static struct snd_soc_card my_board_card { .name my-board-sgtl5000, .owner THIS_MODULE, .dai_link my_board_dai_link, .num_links 1, .dapm_widgets my_board_dapm_widgets, .num_dapm_widgets ARRAY_SIZE(my_board_dapm_widgets), .dapm_routes my_board_dapm_routes, .num_dapm_routes ARRAY_SIZE(my_board_dapm_routes), .controls my_board_controls, .num_controls ARRAY_SIZE(my_board_controls), .set_bias_level my_board_set_bias_level, .late_probe my_board_late_probe, };name是声卡名会出现在/proc/asound/cards里。dai_link和num_links指定链路单链路就一个多链路比如同时有播放和录音走不同I2S就多个。dapm_widgets和dapm_routes是板级DAPM定义这个后面单独讲。controls是板级kcontrol比如“Speaker Switch”这种板子上的GPIO控制功放使能的开关。set_bias_level是个回调Codec的bias电平变化时会调用一般用来控制外部功放的电源。late_probe在Codec和CPU DAI都probe完之后调用适合做一些需要两者都就绪才能做的初始化。3.3 snd_soc_opsPCM操作回调snd_soc_ops定义了PCM流的回调最常用的是hw_paramsstatic int my_board_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; unsigned int mclk; int ret; /* 根据采样率计算MCLK */ switch (params_rate(params)) { case 8000: case 16000: case 32000: case 64000: mclk 8192000; break; case 11025: case 22050: case 44100: case 88200: mclk 11289600; break; case 48000: case 96000: mclk 12288000; break; default: return -EINVAL; } /* 设置Codec的sysclk */ ret snd_soc_dai_set_sysclk(codec_dai, 0, mclk, SND_SOC_CLOCK_IN); if (ret 0) { dev_err(rtd-dev, failed to set codec sysclk: %d\n, ret); return ret; } /* 设置CPU DAI的sysclk */ ret snd_soc_dai_set_sysclk(cpu_dai, 0, mclk, SND_SOC_CLOCK_OUT); if (ret 0) { dev_err(rtd-dev, failed to set cpu sysclk: %d\n, ret); return ret; } return 0; }这个回调是时钟配置的主战场。每次PCM参数变化采样率、位宽、声道数都会调用一次你得在这里根据采样率重新计算并设置MCLK。为什么不能写死因为44.1kHz系列和48kHz系列的MCLK必须是各自基频的整数倍44.1k系列用11.2896MHz48k系列用12.288MHz写死了切换采样率就会失锁。4. 时钟配置机器类驱动里最容易翻车的部分4.1 MCLK从哪来三种常见方案对比嵌入式音频的MCLK来源主要有三种选哪种直接决定了Machine驱动怎么写方案一SoC的PLL直接提供。比如i.MX6的SAI可以直接从PLL4分频出MCLK通过set_sysclk设置。这种方案最省事不需要外部晶振但要求SoC的PLL能分出音频所需的频率。i.MX6的PLL4就是专门给音频用的能分出11.2896MHz和12.288MHz。方案二外部独立晶振。板子上放两颗晶振一颗11.2896MHz给44.1k系列一颗12.288MHz给48k系列通过GPIO或时钟mux切换。这种方案时钟质量最好jitter最小但成本高、占板面积大。HiFi播放器常用这种。方案三Codec自己产生再反喂。有些Codec内置PLL可以从BCLK或外部低频晶振倍频出MCLK然后输出给SoC。这种方案适合SoC时钟资源紧张的情况但Codec的PLL jitter通常比SoC的差。在Machine驱动里方案一和方案三的区别体现在set_sysclk的最后一个参数SND_SOC_CLOCK_OUT表示SoC输出MCLK给CodecSND_SOC_CLOCK_IN表示SoC接收Codec送来的MCLK。4.2 采样率与MCLK的对应关系必须记住的几组数字音频时钟有个基本约束MCLK必须是采样率乘以一个整数这个整数通常是256、384或512。为什么是256因为I2S每帧有2个声道×32位64个BCLKMCLK通常是BCLK的4倍所以MCLK64×4×fs256×fs。采样率44.1k系列MCLK48k系列MCLK8kHz2.048MHz2.048MHz16kHz4.096MHz4.096MHz22.05kHz5.6448MHz-32kHz-8.192MHz44.1kHz11.2896MHz-48kHz-12.288MHz88.2kHz22.5792MHz-96kHz-24.576MHz注意8k和16k两个采样率在44.1k和48k系列里是重合的因为2.048MHz和4.096MHz既是44.1k系列2.048M/8k256也是48k系列4.096M/16k256的整数倍。实际配置时按哪个系列走都行但建议统一按48k系列处理避免混乱。4.3 set_sysclk调用时机与参数含义snd_soc_dai_set_sysclk的原型是int snd_soc_dai_set_sysclk(struct snd_soc_dai *dai, int clk_id, unsigned int freq, int dir);clk_id是时钟源ID不同SoC和Codec定义不同。比如i.MX的SAI有IMX_SAI_CLK_PLL4、IMX_SAI_CLK_PLL3等SGTL5000有SGTL5000_SYSCLK、SGTL5000_LRCLK等。这个ID必须查具体芯片的驱动头文件写错了要么报错要么静默失败。freq是频率值单位Hz。dir是方向SND_SOC_CLOCK_IN表示这个时钟是输入给DAI的SND_SOC_CLOCK_OUT表示DAI输出这个时钟。调用时机上hw_params里调用是最常见的但有些Codec需要在startup里就设置好时钟因为它的内部PLL需要时间锁定。我一般会在hw_params里设置如果遇到Codec PLL锁定慢的问题再挪到startup里提前设置。实操心得调试时钟问题时一定要用示波器量MCLK、BCLK、WS三根线的实际频率。我遇到过SoC的PLL配置正确但时钟输出引脚被复用成GPIO的情况软件层面完全看不出来量了才知道引脚没配对。5. DAPM路由让声音走对路的关键5.1 DAPM是什么动态音频电源管理DAPM全称Dynamic Audio Power Management是ASoC里最精妙的设计之一。它的核心思想是根据当前音频流的状态自动上电或下电音频通路上的各个组件达到省电的目的。比如你在放音乐DAPM会自动把DAC、混音器、功放上电把ADC、麦克风偏置下电当你停止播放它又把DAC和功放下电。DAPM的运作依赖三个东西widget组件、route连接关系、path通路。widget是音频通路上的一个节点比如“DAC”、“Mixer”、“Speaker Output”route描述两个widget之间的连接path是实际激活的通路。Codec驱动里已经定义了大量widget和route但板级特有的连接必须由Machine驱动补充。比如Codec的“HP_OUT”引脚连到板子上的耳机座这个连接关系Codec驱动不知道得在Machine里加一条route。5.2 板级widget定义喇叭、耳机、麦克风板级widget通常用SND_SOC_DAPM_OUTPUT、SND_SOC_DAPM_INPUT、SND_SOC_DAPM_MIC、SND_SOC_DAPM_HP、SND_SOC_DAPM_SPK这些宏定义static const struct snd_soc_dapm_widget my_board_dapm_widgets[] { SND_SOC_DAPM_SPK(Ext Speaker, NULL), SND_SOC_DAPM_HP(Headphone Jack, NULL), SND_SOC_DAPM_MIC(Onboard Mic, NULL), SND_SOC_DAPM_MIC(Headset Mic, NULL), };SND_SOC_DAPM_SPK和SND_SOC_DAPM_HP的区别在于SPK类型的widget在DAPM上电时会触发SND_SOC_DAPM_POST_PMU事件可以用来控制外部功放的使能GPIO。HP类型则不会。所以如果你板子上有个独立功放芯片需要GPIO使能喇叭widget必须用SND_SOC_DAPM_SPK。5.3 板级route定义把Codec引脚连到物理接口route用snd_soc_dapm_route数组定义每条route是{sink, control, source}三元组static const struct snd_soc_dapm_route my_board_dapm_routes[] { /* 耳机 */ {Headphone Jack, NULL, HP_OUT}, /* 喇叭 */ {Ext Speaker, NULL, SPK_OUT}, /* 板载麦克风 */ {MIC_IN, NULL, Onboard Mic}, /* 耳麦麦克风 */ {MIC_IN, NULL, Headset Mic}, };sink是接收端source是发送端control是中间的控制开关一般填NULL表示直连。上面第一条route的意思是Codec的HP_OUT引脚连接到板子上的Headphone Jack。这里有个坑HP_OUT和SPK_OUT这些名字必须跟Codec驱动里定义的widget名字完全一致。怎么查去Codec驱动源码里搜SND_SOC_DAPM_OUTPUT或者运行时看/sys/kernel/debug/asoc/card/dapm/目录下的widget列表。5.4 用DAPM debugfs验证路由是否正确内核开了CONFIG_SND_SOC_DAPM_DEBUG之后/sys/kernel/debug/asoc/下面会有详细的DAPM信息。我常用的几个命令# 查看所有widget及其状态 cat /sys/kernel/debug/asoc/my-board-sgtl5000/dapm/*/dapm_widgets # 查看当前激活的通路 cat /sys/kernel/debug/asoc/my-board-sgtl5000/dapm/*/dapm_paths # 查看bias电平 cat /sys/kernel/debug/asoc/my-board-sgtl5000/dapm/*/bias_level如果播放时发现某个widget没上电或者通路没激活基本就是route写错了。我遇到过一次喇叭无声查DAPM发现Ext Speaker一直是off状态最后发现是route里source名字写成了SPKOUT少了下划线跟Codec里的SPK_OUT对不上。6. 完整实操从零写一个机器类驱动6.1 设备树配置Machine驱动的另一种写法现在新内核里Machine驱动越来越多用设备树描述而不是写C文件。以i.MX6ULL SGTL5000为例设备树大概长这样i2c1 { sgtl5000: codec0a { compatible fsl,sgtl5000; reg 0x0a; clocks clks IMX6UL_CLK_SAI2; clock-names mclk; VDDA-supply reg_3p3v; VDDIO-supply reg_3p3v; }; }; sai2 { pinctrl-names default; pinctrl-0 pinctrl_sai2; assigned-clocks clks IMX6UL_CLK_SAI2_SEL, clks IMX6UL_CLK_SAI2; assigned-clock-parents clks IMX6UL_CLK_PLL4_AUDIO_DIV; assigned-clock-rates 0, 12288000; status okay; }; sound { compatible simple-audio-card; simple-audio-card,name my-board-sgtl5000; simple-audio-card,format i2s; simple-audio-card,bitclock-master dailink_master; simple-audio-card,frame-master dailink_master; simple-audio-card,widgets Headphone, Headphone Jack, Speaker, Ext Speaker, Microphone, Onboard Mic; simple-audio-card,routing Headphone Jack, HP_OUT, Ext Speaker, SPK_OUT, MIC_IN, Onboard Mic; simple-audio-card,cpu { sound-dai sai2; }; dailink_master: simple-audio-card,codec { sound-dai sgtl5000; clocks clks IMX6UL_CLK_SAI2; }; };simple-audio-card是ASoC提供的一个通用Machine驱动适合简单场景。assigned-clock-rates里把SAI2的时钟设成12.288MHz对应48k系列。bitclock-master和frame-master指向Codec节点表示Codec是主。widgets和routing就对应C代码里的DAPM定义。用设备树的好处是不用写C代码改起来快。但复杂场景比如多Codec、复杂时钟切换、需要自定义kcontrol还是得写C驱动。6.2 C驱动完整框架注册、probe、回调如果设备树搞不定就得写C驱动。一个最小的Machine驱动框架#include linux/module.h #include linux/platform_device.h #include sound/soc.h static struct snd_soc_dai_link my_dai_link { .name SGTL5000, .stream_name SGTL5000 HiFi, .cpu_dai_name 202c000.ssi, .codec_dai_name sgtl5000, .platform_name 202c000.ssi, .codec_name sgtl5000.0-000a, .dai_fmt SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF | SND_SOC_DAIFMT_CBS_CFS, .ops my_ops, }; static const struct snd_soc_dapm_widget my_widgets[] { SND_SOC_DAPM_SPK(Ext Speaker, NULL), SND_SOC_DAPM_HP(Headphone Jack, NULL), }; static const struct snd_soc_dapm_route my_routes[] { {Ext Speaker, NULL, SPK_OUT}, {Headphone Jack, NULL, HP_OUT}, }; static struct snd_soc_card my_card { .name my-board-sgtl5000, .owner THIS_MODULE, .dai_link my_dai_link, .num_links 1, .dapm_widgets my_widgets, .num_dapm_widgets ARRAY_SIZE(my_widgets), .dapm_routes my_routes, .num_dapm_routes ARRAY_SIZE(my_routes), }; static int my_probe(struct platform_device *pdev) { struct snd_soc_card *card my_card; int ret; card-dev pdev-dev; ret devm_snd_soc_register_card(pdev-dev, card); if (ret) dev_err(pdev-dev, register card failed: %d\n, ret); return ret; } static const struct of_device_id my_of_match[] { { .compatible my,board-sgtl5000, }, { } }; MODULE_DEVICE_TABLE(of, my_of_match); static struct platform_driver my_driver { .driver { .name my-board-sgtl5000, .of_match_table my_of_match, }, .probe my_probe, }; module_platform_driver(my_driver); MODULE_LICENSE(GPL); MODULE_DESCRIPTION(My Board SGTL5000 Machine Driver);这个框架里devm_snd_soc_register_card是注册入口它会触发ASoC去匹配dai_link里指定的CPU DAI和Codec DAI。匹配成功就创建声卡失败就返回错误。6.3 编译进内核还是做成模块两种方式都行看项目需求。做成模块CONFIG_SND_SOC_MY_BOARDm的好处是调试方便改完insmod就行不用重启。但要注意模块加载顺序Codec驱动和CPU DAI驱动必须先加载Machine驱动最后加载否则匹配不上。编译进内核y的好处是启动就绪适合量产。但调试时改一行就得重新烧录效率低。我的习惯是开发阶段做成模块量产时改成y。注意如果做成模块dai_link里的codec_name和cpu_dai_name必须写对因为模块加载时ASoC会立即去匹配匹配不上直接probe失败。编译进内核的话ASoC会延迟匹配等所有驱动都注册完再试容错性稍好。7. 常见问题排查从症状反推原因7.1 声卡注册失败-ENODEV、-EPROBE_DEFER-ENODEV通常表示dai_link里的名字对不上。排查步骤先看/sys/kernel/debug/asoc/下有没有已注册的CPU DAI和Codec DAI确认名字拼写。再看设备树里Codec节点有没有被正确probe/sys/bus/i2c/devices/下有没有对应的设备。-EPROBE_DEFER表示依赖的驱动还没加载ASoC会稍后重试。这个错误本身不是问题但如果一直defer说明某个依赖驱动始终没probe成功。常见原因是Codec的供电regulator没就绪或者I2C总线没通。7.2 播放无声从DAPM和时钟两条线查无声是最常见的症状排查思路分两条线DAPM线查/sys/kernel/debug/asoc/card/dapm/看播放时HP_OUT或SPK_OUT有没有上电。如果没上电说明route断了或者widget名字对不上。如果上电了但还没声查Codec的模拟输出寄存器看是不是静音了或者增益为0。时钟线用示波器量MCLK、BCLK、WS。如果MCLK没有查set_sysclk有没有被调用、PLL有没有配好。如果BCLK没有查CPU DAI的I2S控制器有没有使能。如果WS频率不对查采样率配置。7.3 爆音、杂音时钟抖动与上电时序爆音通常有两个原因时钟抖动和上电时序。时钟抖动多半是MCLK质量差。如果MCLK来自SoC的PLL检查PLL的配置是否稳定如果来自外部晶振检查晶振的负载电容是否匹配。我遇到过一颗晶振负载电容焊错导致MCLK抖动大播放时全是“滋滋”声。上电时序问题表现为播放开始或结束时“啪”一声。这是因为功放上电瞬间的直流冲击。解决办法是在DAPM的POST_PMU事件里加延时等Codec输出稳定后再开功放。具体做法是给喇叭widget加一个自定义的event回调static int spk_event(struct snd_soc_dapm_widget *w, struct snd_kcontrol *k, int event) { if (SND_SOC_DAPM_EVENT_ON(event)) { msleep(50); /* 等Codec输出稳定 */ gpio_set_value(SPK_EN_GPIO, 1); } else { gpio_set_value(SPK_EN_GPIO, 0); } return 0; } SND_SOC_DAPM_SPK(Ext Speaker, spk_event),7.4 采样率切换失败MCLK没跟着变症状是44.1kHz的歌能放切到48kHz就卡死或者变调。原因基本是hw_params里没有根据采样率重新设置MCLK或者设置了但Codec的PLL没重新锁定。排查方法在hw_params里加打印看每次切换采样率时mclk变量有没有变。如果没变说明switch语句漏了某个采样率。如果变了但Codec没响应查Codec的PLL配置寄存器看是不是需要先关PLL再重配。7.5 常见问题速查表症状可能原因排查方法声卡注册失败-ENODEVdai_link名字对不上查debugfs下已注册DAI名字声卡注册失败-EPROBE_DEFER依赖驱动未就绪查Codec供电、I2C总线播放无声DAPM route断/时钟没出查dapm_paths、量MCLK/BCLK爆音杂音时钟抖动/上电时序量MCLK质量、加延时采样率切换失败MCLK没重配查hw_params打印、Codec PLL单声道I2S格式配错查dai_fmt协议格式休眠唤醒后失声bias电平没恢复查set_bias_level回调8. 进阶技巧多链路、TDM与动态时钟切换8.1 多dai_link配置同时支持播放和录音有些板子播放走一个I2S录音走另一个I2S或者一个Codec有多个DAI。这时候num_links要大于1每个link独立配置static struct snd_soc_dai_link my_links[] { { .name Playback, .stream_name Playback, .cpu_dai_name 202c000.ssi, .codec_dai_name sgtl5000, .playback_only 1, ... }, { .name Capture, .stream_name Capture, .cpu_dai_name 202c000.ssi, .codec_dai_name sgtl5000, .capture_only 1, ... }, };playback_only和capture_only标记这条链路只支持单向。如果Codec的播放和录音共用同一个DAI那其实一个link就够了不用拆成两个。8.2 TDM模式多通道音频的配置要点TDMTime Division Multiplexing用于多通道场景比如车载的8声道输出。配置TDM的关键是dai_fmt里用SND_SOC_DAIFMT_DSP_A或DSP_B然后通过set_tdm_slot设置时隙ret snd_soc_dai_set_tdm_slot(cpu_dai, 0xff, 0xff, 8, 32);参数含义第一个0xff是发送时隙掩码第二个是接收时隙掩码8是时隙数32是每个时隙的位宽。TDM模式下WS信号的频率会变成采样率的8倍8个时隙BCLK频率也相应提高。TDM最容易出问题的地方是时隙对齐。如果SoC和Codec对“第一个时隙从哪开始”的理解不一致就会出现声道错位。解决办法是查两边的手册确认是DSP_AWS上升沿后第一个BCLK开始还是DSP_BWS下降沿后开始。8.3 动态时钟切换根据采样率自动切PLL高端播放器需要支持44.1k和48k双系列且要求jitter最小。做法是板子上放两颗晶振Machine驱动里根据采样率动态切换时钟源static int my_hw_params(...) { ... if (params_rate(params) % 8000 0) { /* 48k系列用12.288MHz晶振 */ clk_set_parent(mclk_clk, clk_12m); } else { /* 44.1k系列用11.2896MHz晶振 */ clk_set_parent(mclk_clk, clk_11m); } ... }clk_set_parent是时钟框架的API需要设备树里把两颗晶振都描述成时钟源然后在Machine驱动里通过clk_get拿到句柄。这种方案比SoC PLL分频的jitter低一个数量级但成本和复杂度都高。9. 我个人在实际项目中的几点体会做嵌入式音频这几年踩过的坑比写过的代码还多。有几点体会特别深第一先量波形再改代码。我早期遇到问题就埋头看代码后来发现80%的音频问题用示波器量一下MCLK、BCLK、WS就能定位。软件层面一切正常但引脚没输出这种问题看代码永远看不出来。第二DAPM debugfs是宝藏。/sys/kernel/debug/asoc/下面的信息比任何文档都全widget状态、path激活情况、bias电平一目了然。养成习惯遇到音频问题先看debugfs。第三dai_fmt的主从配置要跟硬件设计对齐。软件里写CBS_CFS还是CBM_CFM取决于板子上MCLK、BCLK、WS是谁驱动的。这个必须跟硬件工程师确认不能猜。我见过软件配成SoC做主、硬件却把Codec的BCLK输出连到SoC的输入结果两边都在输出时钟直接短路。第四采样率切换的MCLK计算别偷懒。有人图省事MCLK固定12.288MHz44.1kHz的歌也用它。结果就是44.1k播放时MCLK不是采样率的整数倍Codec内部PLL锁不住声音变调或者断断续续。老老实实按表格算44.1k系列用11.2896MHz48k系列用12.288MHz。最后分享一个小技巧调试新板子时先在设备树里用simple-audio-card把基本通路跑通确认硬件没问题再换成自定义C驱动做精细控制。这样能把硬件问题和软件问题分开省很多时间。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →