ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

嵌入式Linux音频驱动开发:ASoC控件与Codec驱动调试实战

嵌入式Linux音频驱动开发:ASoC控件与Codec驱动调试实战 音频驱动这块很多人第一次接触ALSA/ASoC的时候都会被那一堆card、dai、codec、platform、machine的概念绕晕。我在做嵌入式Linux音频子系统的头两年最怕的就是板子跑起来之后aplay没声音然后对着dmesg一行行翻翻半天也看不出到底哪一环断了。后来把ASoC的注册链路和控件模型彻底捋了一遍才发现大部分问题其实都出在machine驱动的dai_link配置和codec的widget连接上跟硬件本身关系不大。这篇内容就围绕ASoC音频控件和Codec驱动开发展开把从设备树描述到控件注册、从dai_link匹配到调试手段的完整链路拆开讲适合正在做嵌入式Linux音频驱动、或者准备接手音频子系统维护的同行参考。不管你是刚上手的新人还是已经调过几块板子的老手下面这些实操细节和踩坑记录应该都能对上你的某些经历。1. ASoC分层模型到底分的是什么1.1 从一次没声音的排查说起板子上的音频codec芯片是ES8323I2S接的是SoC的i2s0控制器设备树里codec节点、dai节点、sound节点都写了内核启动之后/proc/asound/cards也能看到声卡但aplay播放时DMA中断计数在涨示波器量I2S的BCLK和LRCLK都有波形唯独codec的模拟输出端一片安静。这种情况如果只盯着codec驱动看很容易陷入死胡同因为问题往往不在codec本身而在ASoC的machine层没有把codec的DAPM路径正确上电。ASoC把音频系统拆成三块Platform、Codec、Machine。Platform负责SoC侧的DMA和I2S/PCM控制器也就是把音频数据从内存搬到I2S FIFOCodec负责芯片内部的寄存器配置、时钟管理、混音器、增益控制Machine则是把前两者粘起来的板级胶水层描述这块板子上哪个I2S接哪个codec、用哪个时钟、走哪条dai_link。很多人调不通音频本质上是没搞清楚这三者的职责边界把本该在machine里做的事写到了codec驱动里或者反过来。1.2 三个组件的注册顺序与依赖关系ASoC的注册顺序是有讲究的。Platform驱动和Codec驱动可以独立注册各自往系统里挂一个snd_soc_component但Machine驱动必须等前两者都就绪之后才能完成绑定。内核里通过component_list和card_list来管理这个匹配过程machine驱动在probe时会遍历所有已注册的component根据dai_link里指定的cpu_dai_name和codec_dai_name去匹配。这里有个容易忽略的点dai_link的匹配是靠名字字符串不是靠指针或者设备树phandle。如果你的codec驱动里注册的dai名字叫es8323-hifi而machine的dai_link里写的是es8323那匹配就会失败声卡注册会返回-EPROBE_DEFER或者直接报no backend DAIs enabled。我见过不止一个项目因为这个名字对不上白白浪费一整天排查硬件。/* machine驱动中dai_link的典型写法 */ static struct snd_soc_dai_link my_dai_link { .name ES8323, .stream_name ES8323 HiFi, .cpu_dai_name i2s0, /* 必须与platform驱动注册的dai名一致 */ .codec_dai_name es8323-hifi, /* 必须与codec驱动注册的dai名一致 */ .platform_name rockchip-i2s, /* 与platform驱动name一致 */ .codec_name es8323.1-0010, /* i2c设备名格式为name.bus-addr */ .dai_fmt SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF | SND_SOC_DAIFMT_CBS_CFS, .ops my_ops, };上面这段代码里codec_name的格式特别容易写错。它遵循I2C设备命名规则是device_name.bus_num-addr比如codec挂在i2c1上、地址0x10那名字就是es8323.1-0010。如果设备树里codec节点的reg写的是0x10但驱动里i2c_device_id的name写的是别的这个字符串就对不上machine probe会直接失败。1.3 dai_fmt的四个维度与常见误配dai_fmt是machine驱动里最核心的配置之一它决定了I2S总线的时序格式。这个字段由四部分组成用或运算组合维度可选值含义格式SND_SOC_DAIFMT_I2S / LEFT_J / RIGHT_J / DSP_A / DSP_B数据对齐方式时钟反转SND_SOC_DAIFMT_NB_NF / NB_IF / IB_NF / IB_IFBCLK和LRCLK的极性主从SND_SOC_DAIFMT_CBS_CFS / CBM_CFM / CBS_CFM / CBM_CFScodec是主还是从帧同步SND_SOC_DAIFMT_IB_NF等组合帧同步信号极性最常见的误配是主从关系。如果SoC的I2S配置成masterCBM_CFM即codec bit clock master、codec frame master的反义而codec也配置成master两边都在驱动时钟结果就是BCLK上出现两个源打架波形畸变声音要么没有要么全是噪声。正确做法是一方做master另一方做slave通常让SoC做masterCBM_CFM表示codec是bit和frame的master反过来CBS_CFS表示codec是slave具体看硬件设计。提示dai_fmt配错不一定导致probe失败但一定导致音频异常。如果声卡能注册、aplay能跑但没声音或全是杂音优先检查dai_fmt的主从和极性配置。2. Codec驱动里那些必须自己填的寄存器2.1 regmap配置与I2C读写封装Codec驱动跟SoC平台驱动最大的区别在于codec通常挂在I2C或SPI上寄存器访问要走总线。ASoC推荐用regmap来统一管理寄存器读写好处是能自动处理缓存、位域操作和调试接口。以ES8323为例它挂在I2C上regmap配置大概是这样static const struct regmap_config es8323_regmap_config { .reg_bits 8, .val_bits 8, .max_register 0x3F, .cache_type REGCACHE_RBTREE, .volatile_reg es8323_volatile_register, };reg_bits和val_bits要根据芯片手册来定。有些codec是16位寄存器地址、8位数据有些是8位地址、16位数据写错了regmap读写会全部失败但错误信息往往只是regmap read failed不会告诉你位宽不对。volatile_reg回调用来标记哪些寄存器不能被缓存比如状态寄存器、中断标志寄存器这些每次读都要走真实总线。2.2 DAPM widget的定义与连接DAPMDynamic Audio Power Management是ASoC里最精妙也最容易出错的部分。它的核心思想是根据音频流的路径动态地给路径上的每个widget上电或断电没在用的模块自动关掉省电。widget分很多种类型snd_soc_dapm_input、snd_soc_dapm_output、snd_soc_dapm_mixer、snd_soc_dapm_mux、snd_soc_dapm_pga、snd_soc_dapm_dai等等。定义widget的时候名字必须和route里的名字完全一致否则DAPM图就连不起来。我踩过的一个坑是widget名字里带了个空格route里写的时候没注意结果DAPM路径死活不通codec的模拟部分一直不上电。这种问题dmesg里不会有明显报错只能通过/sys/kernel/debug/asoc/card/dapm来看每个widget的状态。static const struct snd_soc_dapm_widget es8323_dapm_widgets[] { SND_SOC_DAPM_INPUT(MIC), SND_SOC_DAPM_INPUT(LINEIN), SND_SOC_DAPM_OUTPUT(HPOL), SND_SOC_DAPM_OUTPUT(HPOR), SND_SOC_DAPM_PGA(HP Driver, ES8323_HP_CTRL, 7, 0, NULL, 0), SND_SOC_DAPM_MIXER(Output Mixer, ES8323_DAC_CTRL, 0, 0, es8323_output_mixer, ARRAY_SIZE(es8323_output_mixer)), }; static const struct snd_soc_dapm_route es8323_dapm_routes[] { { HP Driver, NULL, Output Mixer }, { HPOL, NULL, HP Driver }, { HPOR, NULL, HP Driver }, { Output Mixer, DAC, DAC }, };route的格式是{ sink, control, source }。如果control是NULL表示无条件连接如果指定了control名字那这个连接受对应的mux或mixer控件控制。上面这段里{ Output Mixer, DAC, DAC }表示Output Mixer的DAC输入来自DAC widget这个连接受名为DAC的mixer控件控制。2.3 kcontrol的定义与用户空间可见性kcontrol是用户空间通过amixer能看到的控件。ASoC提供了几种标准宏SOC_SINGLE、SOC_DOUBLE、SOC_ENUM、SOC_SINGLE_TLV等。带TLV的控件能显示音量刻度不带TLV的只能显示原始寄存器值。static const DECLARE_TLV_DB_SCALE(hp_tlv, -4650, 150, 0); static const struct snd_kcontrol_new es8323_snd_controls[] { SOC_DOUBLE_R_TLV(Headphone Playback Volume, ES8323_HPOL_VOL, ES8323_HPOR_VOL, 0, 0x21, 1, hp_tlv), SOC_SINGLE(Headphone Switch, ES8323_HP_CTRL, 7, 1, 0), };SOC_DOUBLE_R_TLV里的参数依次是控件名、左声道寄存器、右声道寄存器、寄存器内偏移、最大值、是否反转、TLV数组。那个是否反转参数很容易搞错如果音量调大反而变小就是反转位设反了。TLV的-4650, 150表示最小-46.5dB每步1.5dB这个数值必须跟芯片手册的增益表对应不能随便填。注意kcontrol的名字会直接出现在amixer的列表里命名要规范。同一个card里不能有重名控件否则注册会失败并报control already exists。3. Machine驱动的dai_link与时钟配置3.1 simple-card与自定义machine驱动的取舍内核提供了simple-audio-card这个通用machine驱动设备树里配好就行不用写C代码。对于大多数标准I2S接codec的场景simple-card完全够用。但如果你需要动态切换dai_link、或者codec的时钟需要特殊处理、或者有多个codec共享一个I2S那就得自己写machine驱动。我一般的判断标准是能用simple-card就用simple-card省事且不容易出错。只有当simple-card的固定模型满足不了需求时才自己写。自己写machine驱动的成本主要在调试上因为你要自己处理hw_params、set_sysclk、set_fmt这些回调任何一个环节出问题都可能导致音频异常。3.2 MCLK与PLL的时钟树梳理Codec要工作必须给它提供MCLK主时钟通常是12.288MHz或11.2896MHz这种音频专用频率。MCLK的来源可能是SoC的I2S控制器分频出来的也可能是独立的晶振。如果MCLK频率不对codec内部的PLL锁不住采样率就会偏声音会变调或者有周期性的爆音。在machine驱动的hw_params回调里通常要调用clk_set_rate来设置MCLK频率static int my_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; unsigned int mclk; /* 根据采样率计算MCLK通常是采样率的256倍或384倍 */ switch (params_rate(params)) { case 8000: case 16000: case 32000: case 48000: case 96000: mclk params_rate(params) * 256; break; case 44100: case 88200: case 176400: mclk params_rate(params) * 256; break; default: return -EINVAL; } return snd_soc_dai_set_sysclk(codec_dai, 0, mclk, SND_SOC_CLOCK_IN); }这里params_rate(params) * 256是常见做法但具体倍数要看codec手册。有些codec要求MCLK是采样率的512倍有些支持自动检测。如果MCLK设错最典型的现象是播放44.1kHz的文件正常播放48kHz的就变调因为两个采样率对应的MCLK不同PLL没锁对。3.3 多dai_link场景下的路由管理一块板子上有两个codec、或者一个codec同时接了两个I2S的情况并不少见。这时候machine驱动里要定义多个dai_link每个link对应一条独立的音频路径。关键是要保证每个link的cpu_dai_name和codec_dai_name组合唯一不能有两条link指向同一个dai对。多link场景下DAPM的路由会跨link连接。比如蓝牙SCO的音频从BT codec进来经过mixer混到主codec输出这条路径就涉及两个link。这时候route的定义要特别小心sink和source必须分属不同的widget域否则DAPM图会形成环导致上电逻辑死循环。4. 调试手段从dmesg到dapm目录4.1 声卡注册失败的典型dmesg信息解读声卡注册失败时dmesg里的信息往往很隐晦。下面列几个我实际遇到过的报错和对应原因dmesg信息根本原因排查方向no backend DAIs enabled for carddai_link匹配失败检查cpu_dai_name/codec_dai_name字符串ASoC: CODEC DAI name not registeredcodec驱动未加载或dai名不对确认codec驱动probe成功dai名一致ASoC: CPU DAI name not registeredplatform驱动未加载确认I2S控制器驱动已注册Failed to create card: -517依赖的component还没就绪检查probe顺序可能需要EPROBE_DEFERcontrol name already existskcontrol重名检查codec和machine的控件命名-517就是-EPROBE_DEFER表示machine驱动probe时依赖的codec或platform还没注册好。内核会自动重试但如果一直重试失败说明依赖的驱动根本没加载或者加载了但probe失败。4.2 用debugfs看DAPM路径状态/sys/kernel/debug/asoc/目录是调试ASoC的利器。挂载debugfs之后进去能看到所有已注册的card、component、dai和dapm信息。mount -t debugfs none /sys/kernel/debug ls /sys/kernel/debug/asoc/ cat /sys/kernel/debug/asoc/card_name/dapmdapm文件会列出每个widget的当前状态On/Off、引用计数和电源状态。如果播放时某个关键widget还是Off说明DAPM路径没走通。这时候要顺着route往回查看是哪一段连接断了。我常用的一个技巧是播放音频的同时cat这个文件对比播放前后哪些widget从Off变成了On。如果预期应该上电的widget没变那问题就在它前面的route上。4.3 寄存器级别的验证方法当DAPM路径看起来正常但声音还是不对时就要下到寄存器级别了。用regmap的debugfs接口可以直接读写codec寄存器# 查看所有寄存器 cat /sys/kernel/debug/regmap/1-0010/registers # 读取单个寄存器 echo 0x00 /sys/kernel/debug/regmap/1-0010/registers对比芯片手册检查关键寄存器的值是否符合预期。比如codec的电源管理寄存器、时钟配置寄存器、DAC使能寄存器。这一步能排除掉驱动逻辑看起来对但寄存器实际没写进去的情况比如I2C通信失败但驱动没报错。提示regmap的debugfs路径是bus-addr格式比如i2c1上地址0x10的codec就是1-0010。如果找不到这个目录说明regmap没注册成功或者debugfs没挂载。5. 那些年踩过的坑与排查链路5.1 时钟极性反了导致全是噪声有一次调一块新板子codec是WM8960I2S接法跟参考设计一样dai_fmt也照抄的结果播放出来全是刺耳的噪声但能听出旋律。这种情况基本可以断定是时钟极性或者数据对齐的问题。我把SND_SOC_DAIFMT_NB_NF改成SND_SOC_DAIFMT_IB_NF噪声消失声音正常。排查链路是这样的先确认BCLK和LRCLK有波形示波器再确认数据线上有信号示波器然后怀疑极性。因为数据在传、时钟在跑只是采样点错位所以能听到变调的音频。这种问题不看波形很难定位光看代码是看不出来的。5.2 codec上电顺序不对导致pop音pop音是音频系统里最烦人的问题之一。WM8960的HP输出在上电瞬间会有啪的一声原因是HP驱动器和输出耦合电容的充放电时序不对。解决办法是在DAPM的route里加一个延时或者用codec的soft-start功能。具体做法是在codec驱动的set_bias_level回调里从SND_SOC_BIAS_STANDBY切到SND_SOC_BIAS_ON时加一段延时让内部参考电压稳定后再开HP驱动器。有些codec有专门的pop抑制寄存器配置一下就能解决。5.3 多声卡场景下的card id冲突系统里同时有HDMI音频和codec音频时两个card的id可能冲突。ALSA默认按注册顺序分配card id但如果两个驱动都请求id 0第二个就会失败。解决办法是在machine驱动里指定card-driver_name和card-name或者用snd_card_new的idx参数显式指定。我遇到过一次HDMI声卡先注册占了card 0codec声卡注册时拿不到id结果aplay -l只看到一个card。后来在codec的machine驱动里把card name改成唯一的问题解决。这个坑的隐蔽性在于dmesg里不会有明显报错只是声卡消失了。5.4 采样率切换时的时钟重配问题播放完44.1kHz再播放48kHz时如果MCLK没有重新配置codec的PLL会失锁声音变调或者直接静音。这个问题在hw_params回调里处理每次打开PCM流时都要根据新的采样率重设MCLK。但有些驱动只在第一次hw_params时设置后续切换采样率就不管了导致问题。正确的做法是在hw_params里无条件重设MCLK不要做如果没变就跳过的优化。因为PCM流关闭再打开时codec可能已经掉电MCLK需要重新建立。6. 从零写一个Codec驱动的完整骨架6.1 驱动结构体与probe流程一个最小可用的codec驱动包含这几个部分snd_soc_component_driver、snd_soc_dai_driver、regmap配置、DAPM widget和route、kcontrol数组。probe函数里主要做三件事初始化regmap、注册component和dai、配置初始寄存器。static int my_codec_probe(struct i2c_client *i2c, const struct i2c_device_id *id) { struct my_codec_priv *priv; int ret; priv devm_kzalloc(i2c-dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; priv-regmap devm_regmap_init_i2c(i2c, my_codec_regmap_config); if (IS_ERR(priv-regmap)) return PTR_ERR(priv-regmap); i2c_set_clientdata(i2c, priv); ret devm_snd_soc_register_component(i2c-dev, my_codec_component_drv, my_codec_dai, 1); if (ret) return ret; return 0; }devm_snd_soc_register_component会自动处理component的注册和注销不用手动管理生命周期。dai数组里定义playback和capture的stream_name、channels_min/max、rates、formats这些会直接影响用户空间能打开的PCM参数。6.2 dai_ops里必须实现的回调snd_soc_dai_ops里有一堆回调但不是所有都必须实现。最核心的是hw_params、set_fmt、set_sysclk、set_bias_level。hw_params在每次PCM流打开时调用用来配置codec的采样率、位宽、通道数。set_fmt配置I2S格式set_sysclk设置MCLKset_bias_level管理codec的偏置电压。如果某个回调不实现ASoC会用默认行为但默认行为不一定适合你的codec。比如set_bias_level不实现的话codec的偏置电压可能一直处于某个中间状态导致功耗偏高或者有底噪。6.3 注册之后的验证清单驱动写完烧进去按这个清单逐项验证dmesg | grep codec_name确认probe成功没有报错ls /sys/kernel/debug/asoc/确认component和dai已注册aplay -l确认声卡和PCM设备可见amixer controls确认kcontrol列表符合预期cat /sys/kernel/debug/asoc/card/dapm确认widget和route正确播放测试音频用示波器确认I2S波形用cat /sys/kernel/debug/regmap/dev/registers对比关键寄存器值这七步走完基本能覆盖90%的音频驱动问题。剩下的10%通常是硬件问题比如codec供电不对、I2S走线干扰、晶振不起振这些就得拿万用表和示波器上硬件排查了。我个人在实际操作中的体会是ASoC的调试最忌讳猜。dmesg、debugfs、regmap这三样工具配合使用能把问题定位到具体的widget或寄存器。与其反复改代码试不如先把DAPM路径和寄存器状态看清楚往往一眼就能看出哪里断了。另外codec手册一定要对着看尤其是寄存器位定义和时钟要求很多问题手册里其实写得很清楚只是容易被忽略。
返回列表