ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

深入解析fsl-asoc-card.c:理解ASoC机器驱动probe全流程

深入解析fsl-asoc-card.c:理解ASoC机器驱动probe全流程 把一块 i.MX6ULL 板子上的声卡整明白最绕不开的就是fsl-asoc-card.c这个驱动。它是 NXP 平台 ASoC 机器驱动的通用实现负责把 CPU 侧的 SAI 和外部 Codec “缝”成一张完整的 HiFi 声卡。网上讲 ALSA 和 ASoC 的资料不少但真到probe函数级别一段一段跟过源码的人其实不多。这篇文章就拿fsl-asoc-card.c的 probe 流程开刀从 compatible 匹配到snd_soc_register_card把每一步在干什么、为什么要这么干讲清楚。如果你正在调 imx6ull 的音频驱动或者想弄懂机器驱动怎么和 DTS 配合生成声卡这篇逐段拆解应该能帮你少走不少弯路。1. 先明白 fsl-asoc-card.c 到底在缝什么1.1 ASoC 的三角关系machine 就是那根线ASoC 框架把音频驱动拆成三个角色CPU DAI、Codec、Machine。CPU DAI 在 i.MX6ULL 上就是 SAI同步音频接口它负责把内存里的 PCM 数据用 I2S 格式发出去Codec 是板子上的编解码芯片比如 WM8960、SGTL5000负责把数字信号转成模拟信号或者反过来Machine 驱动则负责把这两者连接起来告诉 ASoC哪块 CPU DAI 和哪颗 Codec 配对用什么采样率、什么声道格式。可以这样理解CPU 侧就像是一个只讲数字协议的搬运工Codec 是一个只懂模拟信号的翻译官而 machine 驱动是那根电话线它要确保两边说的都是同一种“方言”否则音频数据就传不过去。fsl-asoc-card.c在 i.MX6ULL 上扮演的正是 machine 驱动这个角色。它本身不处理音频数据也不配置硬件寄存器它只负责“牵线搭桥”告诉内核这三者之间的连接关系。这也是为什么很多初学者上来就去找音频数据流经的代码路径找半天找不到——因为机器驱动里根本没有音频数据只有绑定关系和初始化顺序。理解了这点你再去看probe函数里的那些结构体赋值就不会觉得乱了。1.2 fsl-asoc-card.c 和 simple-card 的取舍内核里其实早就有一个通用机器驱动simple-card很多平台用它也能跑起来。那为什么 NXP 还要单独维护一个fsl-asoc-card.c因为它比 simple-card 多做了几件 NXP 平台特定的事。比如说不同 Codec 对 MCLK 的频率要求不一样WM8960 需要 24MHz 或者 12MHz 的 SYSCLK而 SGTL5000 可能对时钟的使能时序更敏感。fsl-asoc-card 里针对常见 Codec 做了适配能在hw_params阶段正确设置 clock 和格式。另一个典型场景是 ESAI 接口的 TDM 模式simple-card 不一定能正确处理每条 DAI 的 slot 配置。加上 fsl-asoc-card 直接绑定了fsl,imx-audio-wm8960、fsl,imx-audio-sgtl5000这类 compatibleDTS 里写起来更直观对 NXP 参考板上常见的 Codec 型号几乎做到开箱即用。当然这不是说 simple-card 不行。如果你的板子用的是通用 Codec且不需要特定时钟时序simple-card 反而更轻量。但既然你点开了fsl-asoc-card.c大概率就是用 NXP 官方的参考设计那跟着这套驱动走下去是最省事的路径。1.3 probe 在整条链路里的位置一个声卡从无到有要经过设备树节点匹配、platform driver probe、dai_link 填充、snd_soc_register_card 注册、card instantiate 这一连串过程。probe函数是其中的总调度它做的事情可以归纳为三件拿到硬件信息、填充描述结构体、把结构体交给 ASoC 框架注册。后面几个章节会逐段展开这三件事。先给读者一个心理预期fsl_asoc_card_probe本身的代码量并不大几十行到一百多行但它牵扯出的数据结构很多。真正理解它的人不是记住了每一行而是明白了它往dai_link里填的那些字符串最终如何被 ASoC 框架用来查找和绑定设备。2. probe 前半场从 compatible 到私有数据就位2.1 入口函数与设备树匹配以 5.4 版本内核为例fsl-asoc-card.c的驱动入口用的是一个标准platform_driver结构static const struct of_device_id fsl_asoc_card_dt_ids[] { { .compatible fsl,imx-audio-sgtl5000, }, { .compatible fsl,imx-audio-wm8960, }, { .compatible fsl,imx-audio-cs42888, }, { .compatible fsl,imx-audio-wm8962, }, { .compatible fsl,imx-audio-ak4458, }, { .compatible fsl,imx-audio-ak5558, }, { .compatible fsl,imx-audio-esai, }, { .compatible fsl,imx-audio-sai, }, {}, }; static struct platform_driver fsl_asoc_card_driver { .probe fsl_asoc_card_probe, .driver { .name fsl-asoc-card, .pm snd_soc_pm_ops, .of_match_table fsl_asoc_card_dt_ids, }, };probe函数的入口长这样代码有省略但核心逻辑保留static int fsl_asoc_card_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct fsl_asoc_card_priv *priv; struct device_node *np dev-of_node; int ret; priv devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; priv-dev dev; dev_set_drvdata(dev, priv); /* 获取 CPU DAI 信息 */ ret fsl_asoc_card_get_dai(priv); if (ret) return ret; /* 获取 Codec 信息 */ ret fsl_asoc_card_get_codec(priv); if (ret) return ret; /* 填充 dai_link */ ret fsl_asoc_card_fill_dai_link(priv); if (ret) return ret; /* 注册声卡 */ ret snd_soc_register_card(priv-card); if (ret) { dev_err(dev, snd_soc_register_card failed: %d\n, ret); return ret; } return 0; }入口的逻辑非常直白devm_kzalloc分配一个私有数据结构体然后依次调用几个辅助函数去获取 DAI、获取 Codec、填充 dai_link最后注册。这种写法在 Linux 驱动里非常典型——先准备数据再提交注册。注意这里用的是devm_kzalloc它会在驱动解绑时自动释放内存省掉了手动kfree的麻烦也让remove函数变得很干净。2.2 fsl_asoc_card_priv 这个“总管”里有什么私有数据结构体fsl_asoc_card_priv是整个声卡驱动的“总管”它贯穿了 probe 后续的所有阶段。不同内核版本结构体略有差异但核心字段是稳定的struct fsl_asoc_card_priv { struct device *dev; struct snd_soc_card card; struct snd_soc_dai_link dai_link; struct snd_soc_dai_link_component cpu_dai; struct snd_soc_dai_link_component codec_dai; struct snd_soc_dai_link_component platform_dai; struct snd_soc_dai_link_component codec_component; struct fsl_asoc_dai_priv dai_priv; ... };这个结构体的设计意图很明显把声卡描述拆成 CPU DAI、Codec、Platform 三块每一块用snd_soc_dai_link_component记录名字和设备节点。snd_soc_card和snd_soc_dai_link是 ASoC 框架要求填的两大核心结构前者代表声卡本身后者代表一组数据链路。从snd_soc_dai_link_component这个命名就能看出现代内核把“Codec”和“DAI”分开描述了不再像老内核那样把 codec 直接挂在 dai_link 上。这是一种更灵活的模型一个 Codec 芯片里可以存在多个 DAI比如 WM8960 有 AIF1、AIF2 两个音频接口通过 codec_dai 的名字来指定用哪一个。2.3 fsl_asoc_card_get_dai先把 CPU 侧的信息抠出来fsl_asoc_card_get_dai的作用是从设备树节点拿到 CPU DAI 的 phandle 和名字。DTS 里通常这么写sound { compatible fsl,imx6ull-evk-wm8960, fsl,imx-audio-wm8960; model wm8960-audio; cpu-dai sai2; audio-codec wm8960; codec-dai-name wm8960-hifi; audio-routing Headphone Jack, HP_L, Headphone Jack, HP_R, LINPUT1, AMIC, LINPUT3, AMIC; mux-int-pin 1; mux-ext-pin 1; };cpu-dai sai2指向 SAI2 节点fsl_asoc_card_get_dai内部会调用of_parse_phandle找到这个节点再读取节点的名字最终拼出 CPU DAI 的设备名。这个设备名会填进cpu_dai组件里以后 ASoC 框架就是靠它去找已经注册的 DAI 设备。这里比较容易忽略的一点是CPU DAI 的设备节点必须已经有一个对应的平台驱动在跑。对 SAI2 来说驱动是fsl-sai.c如果 SAI2 节点没有status okay或者 SAI 驱动没有成功 probe后面再怎么做声卡匹配都会失败而且报错信息可能要到很晚才出现。2.4 fsl_asoc_card_get_codecCodec 的设备节点也要拿到与 CPU DAI 类似fsl_asoc_card_get_codec负责解析audio-codec属性。codec 通常在 I2C 总线上比如 WM8960 挂在 I2C1 上设备节点是一个 i2c_client。驱动读取audio-codec的 phandle同样拿到 codec 设备的完整名字然后把它填进 codec 组件。这一步需要特别注意Codec 本身也是一个独立驱动的设备。如果 I2C 上的 WM8960 没有成功 probefsl_asoc_card_get_codec可能不会立刻报错因为这里只是在解析设备树名字并没有去检查设备是否活跃。等到后面snd_soc_register_card真正去绑定 codec 时才会发现找不到对应的 codec 设备那时候的报错就让人一头雾水了。3. probe 中场dai_link 是怎么被填出来的3.1 dai_link 的每个字段都有来历fsl_asoc_card_fill_dai_link是 probe 里信息密度最高的一段。它把前面解析到的 CPU DAI、Codec 信息逐一填进struct snd_soc_dai_link结构体。核心代码大概长这样static int fsl_asoc_card_fill_dai_link(struct fsl_asoc_card_priv *priv) { struct snd_soc_dai_link *dai_link priv-dai_link; dai_link-name HiFi; dai_link-stream_name HiFi; dai_link-cpus priv-cpu_dai; dai_link-num_cpus 1; dai_link-codecs priv-codec_component; dai_link-num_codecs 1; dai_link-platforms priv-platform_dai; dai_link-num_platforms 1; dai_link-ops fsl_asoc_card_ops; dai_link-init fsl_asoc_card_dai_init; return 0; }这里name和stream_name都叫 “HiFi”这个字符串会出现在声卡的 PCM 设备名里。cpus、codecs、platforms这三组指针分别指向前面解析出的组件结构体。ops指向的是一个回调集里面有hw_params、startup、shutdown等函数这些回调会在声卡运行时被 ASoC 调用。init回调在 dai_link 初始化时执行通常用于设置 codec 的私有数据或初始化 kcontrol。注意一点在老版本内核里dai_link 直接使用cpu_dai_name、codec_name、codec_dai_name这种字符串字段而新版本改成了snd_soc_dai_link_component数组。如果你在网上搜到旧代码看到字符串赋值不要奇怪那是内核 API 演进的结果。理解这一点你查代码时就不容易犯迷糊。3.2 codec_dai_name 是从哪来的DTS 里codec-dai-name wm8960-hifi这个属性最终会被解析进 codec_dai 组件的dai_name字段。这个字符串必须和 codec 驱动内部注册的 DAI 名字完全一致多一个空格都不行。WM8960 驱动里会这样注册 DAIstatic struct snd_soc_dai_driver wm8960_dai { .name wm8960-hifi, .playback { ... }, .capture { ... }, };也就是说Machine 驱动说“我要用名字叫 wm8960-hifi 的 DAI”ASoC 框架就去 Codec 驱动已注册的 DAI 列表里找这个名字。如果 DTS 里写成了wm8960_hifi或者wm8960-hifi1那匹配就会失败声卡注册就会报错。这个设计既是 ASoC 灵活性的体现也是初学者最容易踩的坑。内核打印的错误信息有时不够直观比如只显示一个ASoC: DAI wm8960-hifi not registered如果不清楚匹配规则很难快速定位到是 DTS 里一个短横线和下划线的区别。3.3 ops 里藏着的 HiFi 参数控制fsl_asoc_card_ops里的核心回调是hw_params。以 WM8960 为例它需要对 codec 设置系统时钟否则 Codec 内部的 DSP 和 DAC/ADC 都起不来static int fsl_asoc_card_hw_params(struct snd_pcm_substream *substream, struct snd_pcm_hw_params *params) { struct snd_soc_pcm_runtime *rtd substream-private_data; struct fsl_asoc_card_priv *priv ...; unsigned int sample_rate params_rate(params); unsigned int mclk; /* 根据采样率计算合适的 MCLK */ mclk fsl_asoc_card_get_mclk_rate(sample_rate, priv); /* 设置 CPU DAI 和 Codec 的系统时钟 */ snd_soc_dai_set_sysclk(rtd-cpu_dai, 0, mclk, 0); snd_soc_dai_set_sysclk(rtd-codec_dai, 0, mclk, 0); return 0; }为什么要根据采样率计算 MCLK因为常见的音频采样率是 8k 的整数倍和 48k 的整数倍这两组频率对 MCLK 的要求不同。比如 44.1kHz 系常用 22.5792MHz 或 11.2896MHz48kHz 系常用 24.576MHz 或 12.288MHz。WM8960 的 SYSCLK 要求通常是采样率的 256 倍、384 倍、512 倍等这里 fsl-asoc-card 会优先选择固定的 PLL 输出频率让 Codec 工作在最优状态。如果你在移植中改了 Codec比如把 WM8960 换成 SGTL5000就一定要关注这个fsl_asoc_card_get_mclk_rate函数。不同 Codec 的 MCLK 范围、PLL 倍频系数都不一样直接套用会出现“播放有声音但明显变调”或者“录音全是噪音”的怪问题。3.4 dai_init拨码开关和私有设置的入口dai_link 的init回调在 ASoC 框架实例化这条路时会被调用fsl-asoc-card 里通常会在此时做一些 Codec 专属的设置。比如从设备树读取audio-routing属性创建音频路由或者通过snd_soc_dapm_force_enable_pin强制开启某个引脚。有些参考板上音频路径里有一个外部模拟开关切换 Line-in 和 Mic 输入由 GPIO 控制。这时就可以在dai_init里读取 DTS 中自定义的mux-int-pin、mux-ext-pin属性通过snd_soc_dapm_new_controls注册自定义开关。这也是为什么dai_init往往比hw_params更容易让人困惑——它做的事情高度依赖具体板卡设计没有一个统一套路。拿到一份不熟悉的 DTS最好的办法是在dai_init里打一个dev_info把接收到的节点内容打印出来再对照原理图确认每一个 pin 的含义。4. probe 后半场snd_soc_register_card 和那根“针线”4.1 注册前的自检fsl_asoc_card_fill_dai_link完成后probe 还剩最后一个关键动作调用snd_soc_register_card。不过在此之前有些版本会先检查card结构体的完整性比如 num_dapm_widgets 是否合法、driver_name 是否为空等。这些自检逻辑散落在 ASoC 框架内部probe 本身反而看不到太多。实际上当 fsl-asoc-card 走到snd_soc_register_card时它正在做的事情可以概括为把一张“还没通电”的声卡描述注册到 ALSA 核心。注册成功之后ALSA 核心会为它创建逻辑设备节点但真正的硬件初始化还要等到 card instantiate 过程。这里有一个容易混淆的概念snd_soc_register_card和后面声卡真正变得“可用”之间还有一段路要走。它更像是在社保局登记了一个人的户口但这个人还没开始上班。真正的上班也就是 DAC 初始化、codec bias 上电是在 instantiate 阶段完成的。4.2 snd_soc_register_card 之后发生了什么snd_soc_register_card会触发snd_soc_instantiate_card这个过程做几件大事第一遍历card-dai_link数组对每个 dai_link 调用soc_bind_dai_link把 cpu_dai、codec_dai、platform 从内核已注册的设备列表里找出来并绑定。这就是最常报错的地方如果找不到匹配的 CPU DAI 或 Codec这里会输出ASoC: CPU DAI ... not registered之类的错误。第二对所有绑定的 DAI 调用snd_soc_dai_probe这个函数会调用 DAI 驱动自己的probe回调完成硬件初始化。对 SAI 来说就是配置引脚、时钟、DMA 通道对 Codec 来说就是复位、初始化寄存器。第三调用card-probe回调也就是 fsl-asoc-card 里fsl_asoc_card_probe注意这个和 platform driver 的 probe 不是同一个函数只是名字容易混淆。这个回调里通常会做snd_soc_card_late_probe前的最后准备比如设置 Codec 的输出功率。这个“延迟绑定”机制是 ASoC 比老式 ALSA 驱动先进的地方。CPU DAI 和 Codec 设备不需要严格按顺序 probe只要在snd_soc_register_card之后的某个时刻两边都已经注册好框架就能动态完成绑定。所以你会看到有时候 dmesg 里 I2C Codec 的 probe 日志出现在声卡日志之后这是正常现象。4.3 late_probe修音量的最后机会fsl-asoc-card 里还有一个late_probe回调通常在声卡基本成型后调用用于处理代码执行顺序的问题。例如 WM8960 的输出偏置电压需要等时钟稳定之后再配置放在late_probe里就比放在普通probe里更稳妥。实战中late_probe常被用来打印声卡状态、补充创建 kcontrol 等操作。如果你在调试中遇到“声卡注册成功但 alsamixer 里找不到某个控件”可以考虑是不是控件创建早了或者创建晚了。把控件创建挪到late_probe往往能解决这类时序问题。5. 实际操作中的坑与排查5.1 常见报错速查表调声卡驱动时dmesg 里的错误信息是最直接的线索。我整理了一份出现频率最高的报错和对应的排查方向报错信息含义排查方向ASoC: CPU DAI ... not registeredCPU DAI 没有被绑定检查 SAI 节点 status、fsl-sai 驱动是否 probe 成功ASoC: CODEC DAI ... not registeredCodec DAI 名字不匹配核对codec-dai-name和 codec 驱动中的 dai 名字Failed to set sysclk时钟设置失败检查 MCLK 引脚、PLL 配置用示波器测时钟输出wm8960: ASoC: error at soc_codec_probe on wm8960Codec 驱动 probe 失败检查 I2C 地址、I2C 通信是否正常snd_soc_register_card failed: -517依赖设备未 ready检查是否是 -EPROBE_DEFER等待 Codec 或 SAI 驱动就绪-517这个返回值特别有意思。它对应-EPROBE_DEFER意思是“这次先不干了等依赖的设备注册好了再试一次”。内核会把该 probe 插入延迟队列等 Codec 或 SAI probe 完成后再次尝试。所以你看到-517不代表失败而是一种基于依赖的排队等待机制。5.2 我踩过的三个具体坑第一个坑是codec-dai-name写错。曾经把wm8960-hifi写成wm8960_HiFi大小写加下划线全错了开机后 dmesg 一直报wm8960_HiFi not registered。当时围着 I2C 和 GPIO 排查了半天最后用cat /sys/kernel/debug/asoc/dais查了系统里实际注册的 DAI 名字才恍然大悟。这个调试节点的信息非常全强烈建议遇到 DAI 绑定问题先看一眼。第二个坑是 SAI2 的 MCLK 出不来。DTS 里明明配了fsl,sai-synchronous-rx之类的属性但用示波器量 SAI2_MCLK 引脚始终没有时钟输出。最后发现是 IOMUX 里少了SAI2_MCLK的 pinmux 配置引脚还处于 GPIO 模式。这个问题在 dmesg 里不会报任何错误因为内核认为时钟已经使能只是物理引脚没通。第三个坑是声卡注册成功但没有 PCM 设备。这种情况往往出现在 platform DAI 没配对好导致 PCM 设备没有创建成功。排查方法是aplay -l如果显示**** List of PLAYBACK Hardware Devices ****下面一片空白多半是 codec 或 platform 的 DMA 配置有问题需要回到 SAI 的fsl-sai.c驱动里核对 DMA 绑定。5.3 怎么快速确认声卡已经“缝”好确认声卡状态最快的方法是看/proc/asound/cardsrootimx6ull:~# cat /proc/asound/cards 0 [wm8960audio ]: wm8960-audio - wm8960-audio wm8960-audio看到[wm8960audio]说明声卡已经注册成功了。再用aplay -l查看 PCM 设备rootimx6ull:~# aplay -l **** List of PLAYBACK Hardware Devices **** card 0: wm8960audio [wm8960-audio], device 0: HiFi wm8960-hifi-0 [] Subdevices: 1/1 Subdevice #0: subdevice #0这里的HiFi就是 dai_link 里的stream_namewm8960-hifi是 Codec 的 DAI 名字。看到这一行说明从 CPU DAI 到 Codec DAI 的整条链路已经绑定成功。如果还是想深入看绑定情况可以检查 debugfsrootimx6ull:~# ls /sys/kernel/debug/asoc/ dais componentscomponents文件会列出 codec、platform、cpu_dai 等所有组件格式类似snd-soc-dummy、wm8960.0-001a、5a020000.sai这样。逐个对照很容易发现问题出在哪个环节。6. 从 fsl-asoc-card 出发怎么改造成私有驱动6.1 复制代码要克制理解后再动手很多人拿到一块定制板子第一反应是直接把fsl-asoc-card.c复制一份改个名字然后开始堆自己的代码。我建议克制一下这个冲动。因为 fsl-asoc-card 里的逻辑和 DTS 的绑定关系已经比较成熟随意复制再修改往往会遗失掉一些隐性的处理比如特定 Codec 的时钟表格、DAPM 路由初始化等。更稳妥的做法是先在原版驱动上跑通声音确认链路没有问题再把需要扩展的部分以回调或者独立文件的方式加入。比如新 Codec 需要额外的 PLL 配置可以先在fsl_asoc_card_ops里增加一个自定义的hw_params回调而不是整个替换驱动。6.2 一个简单例子增加自定义 DAPM 控件假设你的板子有一个 GPIO 控制的功放需要在声卡初始化时注册一个“功放开关”。可以在 card 的 probe 回调里这样写static const struct snd_kcontrol_new amp_controls[] { SOC_GPIO_SWITCH(Amp Switch, amp_gpio, 0, 0), }; static int fsl_asoc_card_my_probe(struct snd_soc_card *card) { struct snd_soc_card *card ...; int ret; ret snd_soc_add_card_controls(card, amp_controls, ARRAY_SIZE(amp_controls)); if (ret) return ret; return 0; }把这个回调挂到card-probe上声卡注册时就会自动创建这个 kcontrol。这样在 tinymix 里就能看到Amp Switch可以直接控制功放通断。6.3 个人经验调声卡驱动要从“时钟、格式、路由”三件套入手做了这么多年嵌入式音频调试我总结出一个经验绝大多数声卡问题出在三个地方——时钟不对、格式不匹配、路由没通。时钟不对的表现是播放变调、录音频率漂移优先查 MCLK 和 BCLK 的配置格式不匹配的表现是播放或录音完全无声但无报错优先查 I2S 格式是 I2S 还是 LEFT_J、位宽是 16bit 还是 24bit路由没通的表现是声卡注册正常但某个通路没声音优先查 DAPM 路由和 kcontrol 的开关状态。这次拆解fsl-asoc-card.c的 probe回头看其实就是围绕这三件事在准备数据因为只有把时钟信息、格式信息、设备绑定信息全部填进 dai_link 和 opsASoC 框架才能在后来的hw_params阶段正确执行硬件配置。所以下次你再看到snd_soc_register_card成功但没声音时不用慌把这“时钟、格式、路由”三件事各查一遍很多问题能自己浮出水面。驱动代码只是替你把这三件事描述给了内核真正出问题时硬件链路反而更容易暴露出真相。
返回列表