4、Codec驱动:Codec抽象层、snd_soc_dai_driver、snd_soc_codec_driver、控件与kcontrol注册
好,咱们今天来聊聊Codec驱动。说实话,这是ASoC子系统里最「接地气」的一块。你想想看,CPU和DMA把音频数据搬过来了,最后总得有个东西把数字信号变成模拟声音吧?这个活儿,就是Codec干的。
在ASoC框架里,Codec被抽象成了两层:一个是硬件描述层,一个是控制接口层。我刚开始接触时也犯过迷糊,总觉得这两层分得太细。后来踩了几个坑才明白——这种设计,说白了就是为了让驱动代码能复用。你换一颗Codec芯片,只需要改硬件描述部分,控制逻辑基本不用动。
4.1 Codec抽象层:把硬件「翻译」给内核听
Codec抽象层干的事,就是把芯片手册里的寄存器、引脚、音频接口这些物理概念,翻译成内核能理解的数据结构。核心就是两个结构体:snd_soc_dai_driver 和 snd_soc_codec_driver。
我个人习惯先看 snd_soc_dai_driver。它描述的是Codec的数字音频接口(DAI),也就是跟CPU那边对接的物理层。比如采样率支持哪些、格式支持哪些、时钟极性怎么配。
static struct snd_soc_dai_driver my_codec_dai = {
.name = "my-codec-hifi",
.playback = {
.stream_name = "Playback",
.channels_min = 2,
.channels_max = 2,
.rates = SNDRV_PCM_RATE_8000_48000,
.formats = SNDRV_PCM_FMTBIT_S16_LE,
},
.capture = {
.stream_name = "Capture",
.channels_min = 1,
.channels_max = 2,
.rates = SNDRV_PCM_RATE_8000_48000,
.formats = SNDRV_PCM_FMTBIT_S16_LE,
},
.ops = &my_codec_dai_ops,
};
你看,这个结构体把Codec能干什么事说得清清楚楚。我在项目中遇到过一个问题:某颗Codec的Playback支持48kHz,但Capture只支持16kHz。当时没仔细看手册,直接复制了Playback的参数给Capture,结果录音全是噪声。嗯,这个坑我替你们踩过了。
rates 和 formats 用的是位掩码,不是数值。别写成 48000,要写成 SNDRV_PCM_RATE_48000。我见过有人直接写数字,编译不报错,但运行时内核根本不认。
4.2 snd_soc_codec_driver:Codec的「身份证」
接下来是 snd_soc_codec_driver。这个结构体描述的是Codec芯片本身的属性,比如有多少个寄存器、读写回调函数、偏置电压控制等。
static struct snd_soc_codec_driver my_codec_driver = {
.probe = my_codec_probe,
.remove = my_codec_remove,
.suspend = my_codec_suspend,
.resume = my_codec_resume,
.controls = my_codec_controls,
.num_controls = ARRAY_SIZE(my_codec_controls),
.dapm_widgets = my_codec_dapm_widgets,
.num_dapm_widgets = ARRAY_SIZE(my_codec_dapm_widgets),
.dapm_routes = my_codec_dapm_routes,
.num_dapm_routes = ARRAY_SIZE(my_codec_dapm_routes),
};
这里有个关键点:controls 和 dapm_widgets 是分开注册的。为什么?因为控件(kcontrol)是给用户空间用的,比如调节音量、切换输入源。而DAPM widget是给内核电源管理用的,控制Codec内部哪些模块上电、哪些下电。
我曾经犯过一个错误:把音量控制写成了DAPM widget。结果每次调音量,内核都以为要切换电源状态,搞得Codec频繁上下电,声音断断续续。你想想看,这多尴尬。
4.3 控件与kcontrol注册:让用户能「拧旋钮」
控件,也就是kcontrol,是Codec驱动里最贴近用户的部分。用户空间的工具(比如alsamixer、amixer)就是通过kcontrol来调节音量、切换通道的。
注册kcontrol有两种方式:一种是手动定义 snd_kcontrol_new 结构体,另一种是用宏定义。我个人推荐用宏,代码更简洁,也不容易出错。
static const struct snd_kcontrol_new my_codec_controls[] = {
SOC_SINGLE("Master Playback Volume", MY_CODEC_REG_VOL, 0, 255, 0),
SOC_SINGLE("Master Capture Volume", MY_CODEC_REG_CAP_VOL, 0, 255, 0),
SOC_DOUBLE("Headphone Playback Volume", MY_CODEC_REG_HP_VOL,
0, 8, 127, 0),
SOC_ENUM("Input Source", my_codec_input_enum),
};
你看,SOC_SINGLE 是单通道控件,SOC_DOUBLE 是双通道(左右声道独立控制),SOC_ENUM 是枚举选择(比如切换麦克风还是线路输入)。
注册过程其实很简单。在 snd_soc_codec_driver 里填好 .controls 和 .num_controls,ASoC框架会在probe阶段自动帮你注册。但要注意一点:控件的注册顺序会影响它们在用户空间的排列。我习惯把常用的控件(比如主音量)放在前面,不常用的(比如数字滤波器模式)放在后面。
4.4 实战中的避坑指南
好,最后分享几个我实际项目中踩过的坑,希望能帮你省点时间。
- 寄存器地址别搞错:Codec芯片的寄存器地址通常是8位或16位,但有些芯片的地址是分页的。我遇到过一颗芯片,音量寄存器在Page 0,但静音控制却在Page 1。如果你不切换页面,写进去的数据根本不对。
- 控件范围要匹配:
SOC_SINGLE的最后一个参数是偏移量(shift),中间那个是最大值。比如音量寄存器是8位,最大值255,但实际芯片只用了低7位(0-127)。如果你写成255,用户调到128以上时,芯片可能产生爆音。 - 别忘了DAPM路由:控件注册好了,但如果没有对应的DAPM路由,音频路径可能不通。比如你注册了"Headphone Playback Volume",但没把DAPM widget连到输出引脚上,耳机就是不出声。
我曾经在一个项目里,花了整整两天排查耳机无声的问题。最后发现,DAPM路由里少了一条从DAC到HP_OUT的连接。加上去之后,声音就出来了。嗯,这种低级错误,犯过一次就再也不会忘了。
总结一下:Codec驱动其实不难,关键是理解抽象层的设计思路。把硬件描述和控制逻辑分开,代码才能干净、可维护。下一章咱们聊聊DAPM,那才是ASoC的精髓所在。