ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

ASoC音频控件开发实战:kcontrol注册、TLV映射与DAPM联动

ASoC音频控件开发实战:kcontrol注册、TLV映射与DAPM联动 1. 音频控件在ASoC架构中的定位与整体设计思路做嵌入式Linux音频驱动开发这些年我接触过不少编解码器芯片从最早玩WM8960、ES8388这类经典Codec到后来搞NAU88C10、TLV320AIC23B再到最近几年RK、全志平台上那些集成度更高的音频子系统。绕来绕去有一个东西始终绕不开就是ASoC框架下的音频控件kcontrol开发。很多刚入行的朋友拿到一块板子声卡能枚举出来aplay也能出声就觉得驱动搞定了。但一旦产品要求加个录音增益调节、或者要做一个数字音量与模拟音量联动的功能立马就懵了——不知道从哪儿下手。这篇文章就是写给这些卡在能出声但不会调阶段的嵌入式Linux驱动工程师的。我会把ASoC编解码器驱动里音频控件开发的核心要点掰开揉碎讲清楚包括控件是怎么注册的、DAPM跟kcontrol什么关系、mixer控件和mux控件在代码上怎么区分、用户空间怎么通过amixer去操作它们以及我自己踩过的那些坑。看完之后你应该能独立给一颗Codec芯片写出完整的控件集并且知道每个控件背后到底发生了什么。先说清楚一个基本认知ASoC里的音频控件本质上是内核向用户空间暴露的一组可读写的音频参数接口。用户空间通过ALSA的control接口也就是我们常用的amixer、alsamixer这些工具来读写这些控件从而实现对音量、增益、通路选择、静音等功能的控制。控件是驱动和用户之间的合同合同定得好不好直接决定了这颗Codec好不好用。那为什么ASoC要单独搞一套kcontrol机制而不是让用户直接读写寄存器原因有三层。第一层是抽象不同Codec的寄存器地址、位定义千差万别用户空间不应该关心这些第二层是安全直接暴露寄存器意味着用户可能写出非法值把芯片搞挂第三层是语义用户想表达的是把播放音量调到80%而不是往寄存器0x0A的bit0-5写0x30。kcontrol就是把这层语义翻译成寄存器操作的那层胶水。在ASoC的代码结构里控件相关的代码主要集中在snd_soc_component_driver结构体里的controls、dapm_widgets、dapm_routes这几个成员上。controls数组定义的是普通的mixer控件dapm_widgets定义的是DAPM动态音频电源管理相关的控件两者配合才能构成一个完整的音频控件体系。很多人搞不清楚这两者的边界我后面会专门用一节来讲。整体设计思路上我一般遵循这么几个原则能用标准宏就不用自定义、能交给DAPM自动管理就不手动控制、控件命名要跟芯片手册保持一致。这三点看起来简单但真正做到位能省掉大量调试时间。举个例子snd_soc_put_volsw这个标准回调能覆盖90%以上的音量控件场景你非要去写自定义的put回调除了增加出错概率没有任何好处。2. 核心细节解析kcontrol注册机制与关键数据结构2.1 snd_kcontrol_new结构体到底填什么要写控件绕不开snd_kcontrol_new这个结构体。它在include/sound/control.h里定义ASoC里我们用的是soc.h里那套宏封装。一个典型的音量控件定义长这样static const struct snd_kcontrol_new wm8960_snd_controls[] { SOC_DOUBLE_R_TLV(Playback Volume, WM8960_LOUT1, WM8960_ROUT1, 0, 127, 0, dac_tlv), SOC_DOUBLE_R_TLV(Capture Volume, WM8960_LINVOL, WM8960_RINVOL, 0, 63, 0, adc_tlv), SOC_SINGLE(Playback Switch, WM8960_LOUT1, 8, 1, 1), };这里面的宏展开之后核心字段有这几个iface接口类型ASoC里一般是SNDRV_CTL_ELEM_IFACE_MIXER、name控件名字用户空间看到的就是它、index同名控件的索引、info告诉用户空间这个控件是什么类型、取值范围多少、get读回调、put写回调、private_value私有数据宏帮你打包好了寄存器地址、位偏移、最大值这些信息、tlv音量映射表可选。我重点说几个容易踩坑的地方。private_value这个字段是宏帮你打包的但打包格式是有讲究的。以SOC_DOUBLE_R为例它把左声道寄存器地址、右声道寄存器地址、位偏移、最大值、是否反向invert这几个信息按位段塞进一个unsigned long里。如果你自己手写private_value而不用宏一旦位段算错控件要么读写错寄存器要么取值范围不对而且这种bug特别隐蔽因为编译不会报错。info回调决定了用户空间能看到什么。标准宏会自动生成合适的info回调比如SOC_SINGLE生成的是SNDRV_CTL_ELEM_TYPE_INTEGER类型范围0到max。如果你要做一个枚举类型的控件比如输入源选择就得用SOC_ENUM系列宏它生成的是SNDRV_CTL_ELEM_TYPE_ENUMERATED类型用户空间看到的是可选项列表而不是数字。2.2 TLV音量映射表为什么不能省很多新手写音量控件的时候直接把tlv参数填成0也就是NULL觉得反正能调就行。这样做出来的控件在amixer里看到的是一个0到127的裸数字用户调音量的时候感觉特别别扭——因为人耳对音量的感知是对数关系的寄存器值线性增加听感上却是前面变化剧烈、后面几乎没感觉。TLVType-Length-Value就是解决这个问题的。它定义了一张寄存器值到分贝值的映射表用户空间拿到这张表之后alsamixer就能显示成dB刻度调节起来线性得多。一个典型的TLV表长这样static const DECLARE_TLV_DB_SCALE(dac_tlv, -12700, 100, 0);这行的意思是最小-127dB每步进1dB最后一个参数0表示不是mute如果是1表示最小值是静音。DECLARE_TLV_DB_SCALE是最常用的宏适合步进均匀的场景。如果步进不均匀就得用DECLARE_TLV_DB_MINMAX或者干脆手写数组。注意TLV表里的数值单位是0.01dB所以-12700实际是-127dB。这个单位换算我第一次写的时候搞错了导致音量刻度差了100倍调了半天才发现。2.3 DAPM Widget与普通kcontrol的边界这是最容易混淆的地方。普通kcontrol就是controls数组里那些是用户主动控制的比如用户调音量、切通路。DAPM widget是内核自动管理的比如播放的时候自动给DAC上电、停止播放自动断电省功耗。但两者又不是完全独立的。DAPM widget里也可以带kcontrol比如一个Mixer widget它内部就包含了一个kcontrol用来控制混音比例。这种widget叫带控件的widget定义的时候用SOC_DAPM_MIXER这类宏宏里会引用一个snd_kcontrol_new数组。我个人的经验是凡是跟电源管理、通路自动切换相关的交给DAPM凡是用户需要手动调节的参数做成普通kcontrol。但有些场景边界模糊比如耳机插入自动静音扬声器这个既可以做成DAPM route自动切换也可以做成一个用户可见的switch控件。我一般倾向于前者因为自动化的东西用户不用操心减少误操作。3. 实操过程从零给一颗Codec写控件集3.1 先读手册把寄存器地图画出来拿到一颗新Codec我第一件事不是写代码而是把数据手册里的寄存器地图整理成一张表。重点看这几类寄存器音量控制寄存器通常分DAC数字音量、ADC数字音量、输出模拟音量、输入增益、通路选择寄存器输入mux、输出mux、电源管理寄存器各种偏置、使能位、静音控制寄存器。以WM8960为例它的输出音量寄存器LOUT1/ROUT1bit0-6是音量值0-127bit8是静音位1表示静音。这个信息决定了你写控件时max填127、shift填0、静音位单独做一个SOC_SINGLE。整理成表格大概是这个感觉功能寄存器位段范围控件类型播放音量LOUT1/ROUT1bit0-60-127SOC_DOUBLE_R_TLV播放静音LOUT1/ROUT1bit80-1SOC_SINGLE录音音量LINVOL/RINVOLbit0-50-63SOC_DOUBLE_R_TLV输入选择LINPUT1bit6-70-3SOC_ENUM这张表画出来控件代码基本就成型了。3.2 控件数组的编写与注册把上面表格里的每一项翻译成宏组成wm8960_snd_controls数组。然后这个数组要挂到snd_soc_component_driver的.controls成员上同时.num_controls填数组长度。注意数组长度用ARRAY_SIZE宏算别手写数字加控件的时候容易忘改。static const struct snd_soc_component_driver soc_component_dev_wm8960 { .controls wm8960_snd_controls, .num_controls ARRAY_SIZE(wm8960_snd_controls), .dapm_widgets wm8960_dapm_widgets, .num_dapm_widgets ARRAY_SIZE(wm8960_dapm_widgets), .dapm_routes wm8960_dapm_routes, .num_dapm_routes ARRAY_SIZE(wm8960_dapm_routes), };注册的时机是在probe函数里调用devm_snd_soc_register_component。这里有个细节控件数组必须是静态常量static const不能是局部变量。因为ASoC框架会长期持有这个指针局部变量出了作用域就没了会导致内核崩溃。这个坑我见过不止一个新手踩。3.3 用户空间验证amixer实操驱动加载之后用amixer controls能看到所有注册的控件。用amixer contents能看到每个控件的详细信息包括类型、范围、当前值。调节音量用amixer cset namePlayback Volume 100读取用amixer cget namePlayback Volume。我习惯用alsamixer这个 curses 界面工具它能直观地显示所有控件用方向键调节特别适合调试阶段快速验证。如果alsamixer里某个控件显示成100这种裸数字而不是dB刻度说明TLV没生效回去检查tlv参数是不是填了NULL。提示调试阶段建议把内核的snd_soc相关debug选项打开/sys/kernel/debug/asoc/下面能看到每个component的控件列表和DAPM状态排查问题非常方便。3.4 参数计算音量步进与dB映射假设你的Codec输出音量寄存器是6位0-63芯片手册说每步进1.5dB最大0dB最小-94.5dB。那么TLV表应该这样算DECLARE_TLV_DB_SCALE(name, -9450, 150, 0)。注意单位是0.01dB所以-94.5dB写成-94501.5dB写成150。如果手册给的是每步进0.75dB那就是75。如果给的是总范围-94.5dB到0dB共64步那步进就是94.5/63≈1.5dB跟上面一致。关键是搞清楚手册里dB值的基准点有些芯片0dB对应寄存器最大值有些对应中间值搞错了整个音量曲线就反了。4. 常见问题与排查技巧实录4.1 控件注册了但amixer看不到这是最常见的问题。排查顺序是这样的先确认num_controls填对了没有如果填0或者填小了后面的控件不会注册。再确认controls指针是不是NULL有些人把数组定义在函数里忘了加static编译器可能优化掉。然后看probe函数有没有真正执行成功如果register_component返回错误但你没检查返回值驱动可能加载了但控件没注册。还有一种情况是控件名字跟已有的冲突了。ALSA允许同名控件存在但需要不同的index区分。如果你注册了两个都叫Playback Volume且index都是0的控件第二个会注册失败。解决办法是给其中一个加index或者改名字。4.2 调节控件没反应控件能看到但调了没效果通常是这几个原因。寄存器地址写错了这个用regmap的debugfs可以验证/sys/kernel/debug/regmap/下面能看到每次读写。位偏移算错了比如音量在bit0-6你shift填了1那调出来的值就整体偏移。控件被DAPM覆盖了有些寄存器DAPM也会写如果DAPM的更新时机在用户操作之后就会把用户的值冲掉。这种情况要检查DAPM widget的reg和mask有没有跟kcontrol重叠。4.3 音量调节有爆音爆音问题一般跟调节时的寄存器写入顺序有关。比如你先写了音量值再取消静音中间那一瞬间可能输出一个突变。正确的做法是先静音、再调音量、最后取消静音或者用芯片支持的零交叉zero-cross功能让芯片在信号过零点时才更新音量。WM8960就有这个功能在音量寄存器里有个ZCL位置1之后音量更新会等到过零点能有效消除爆音。4.4 常见问题速查表现象可能原因排查方法amixer看不到控件num_controls错误/数组非static/注册失败检查probe返回值看dmesg调节无效果寄存器地址或位偏移错误用regmap debugfs看实际写入音量刻度异常TLV表单位或步进算错对照手册重新计算dB值调节有爆音写入顺序问题/未启用zero-cross调整顺序或开启ZCL位控件值被重置DAPM与kcontrol寄存器冲突检查widget的reg/mask枚举控件选项不对enum数组与寄存器值不匹配核对手册的枚举定义4.5 独家避坑经验分享几个文档里不会写的经验。第一控件命名尽量跟芯片手册一致比如手册叫LINVOL你就别自己发明个Left Input Volume后期维护的人对着手册找控件会感谢你。第二调试阶段先把所有控件都注册上哪怕暂时用不到因为后期加控件要重新编译内核调试阶段一次性搞定省事。第三TLV表能用标准宏就用标准宏手写数组容易出错而且标准宏生成的表内核会做优化。还有一个特别隐蔽的坑有些Codec的寄存器写入需要先解锁比如某些寄存器被保护起来了要往一个特定的解锁寄存器写个魔数才能改。这种芯片如果你不知道解锁机制控件调了完全没反应但regmap debugfs里看写入是成功的因为写是写进去了只是被芯片忽略了。遇到这种情况回去翻手册的Register Lock章节。5. 进阶话题自定义控件与DAPM深度联动5.1 什么时候需要自定义put/get回调标准宏覆盖不了的场景就得自己写回调。典型场景有两个一是需要联动多个寄存器比如调音量的同时要更新一个状态寄存器二是需要做范围限制或特殊计算比如把用户输入的百分比转换成芯片的非线性音量值。自定义回调的写法是定义info、get、put三个函数然后手动填snd_kcontrol_new结构体。put回调里用snd_soc_component_update_bits来改寄存器这个函数会自动处理read-modify-write比直接调regmap_write安全。注意put回调的返回值返回0表示值没变返回1表示值变了需要上报事件返回负数表示出错。很多人忘了返回值语义导致用户空间收不到变更通知。5.2 DAPM Route与Mixer控件的配合DAPM的route定义的是widget之间的连接关系比如DAC连接到输出Mixer。而Mixer widget内部的kcontrol控制的是这个Mixer的哪几路输入被打开。两者配合才能实现完整的通路控制。举个例子一个输出Mixer有DAC和Line In两路输入route定义了这两路都能连到Mixer但具体哪路打开由Mixer内部的kcontrol决定。用户空间看到的就是两个switch控件打开哪个哪路就通。这种设计的好处是通路切换不需要改route只需要改switchDAPM会自动根据switch状态决定哪些widget需要上电。5.3 控件与电源管理的时序问题有个容易被忽略的点控件操作和DAPM上下电是有时序依赖的。如果你在DAPM还没给某个模块上电的时候就去写它的寄存器写入可能丢失。ASoC框架一般会处理这个但自定义控件如果绕过了框架直接写寄存器就可能出问题。稳妥的做法是在put回调里先检查component-dapm.bias_level确保芯片处于正常工作状态再操作。我在一个项目里遇到过系统刚启动DAPM还在初始化用户空间就通过一个开机脚本去设置音量结果设置丢失音量一直是默认值。后来在脚本里加了个延时等声卡完全就绪再设置问题解决。这个经验告诉我用户空间的音频初始化脚本要考虑驱动就绪时间不能想当然地认为声卡一出现就能操作。6. 写在最后的一点个人体会搞了这么多年Codec驱动我最大的感受是音频控件开发看着简单实则处处是细节。一个音量控件从寄存器定义到TLV映射到DAPM联动每一环都可能出问题。但反过来只要你把手册读透、把标准宏用对、把调试工具用熟大部分问题都能快速定位。我个人的习惯是每写一个控件就在amixer里验证一遍确认读写都正常再写下一个。不要一口气写完几十个控件再统一调试那样出了问题很难定位是哪个控件的锅。另外regmap的debugfs和ASoC的debugfs是两大神器调试阶段一定要用起来比printk高效得多。最后分享一个小技巧如果你不确定某个控件的TLV该怎么算可以先不填TLV用裸数字调一遍记录下听感合适的那个寄存器值然后反推dB值。虽然土但在手册信息不全的时候特别管用。
返回列表