Rockchip 音频驱动代码走读:从内核启动到声卡就绪,到底发生了什么? 电子说
|
打开kernel/sound/soc/rockchip/ 目录,十几二十个 C 文件摆在你面前:rockchip_i2s_tdm.c、rockchip_pdm.c、rockchip_multi_dais.c...... 想搞清楚声卡是怎么注册出来的,应该从哪开始看? 这篇文章带你沿着一条清晰的主线走读:从驱动加载→ DAI 初始化 → 时钟配置 → DMA 注册 → Machine Driver 组装 → 声卡诞生。看完你就知道每一步在做什么,以及出了问题该盯哪段代码。 |

一、先建立全局视角:文件地图
Rockchip 音频驱动主要分布在两个目录:
CPU DAI + Platform 驱动(kernel/sound/soc/rockchip/):
| 文件 | 功能 | 主流芯片 |
| rockchip_i2s_tdm.c | I2S-TDM DAI 驱动 | RK3568/RK3588/RK3576 |
| rockchip_sai.c | SAI DAI 驱动 | RK3576/RK3562 |
| rockchip_pdm.c | PDM 数字麦克风 | RK3308/RK3568 |
| rockchip_spdif.c | SPDIF 光纤输出 | 通用 |
| rockchip_multi_dais.c | Combo DAI 组合 | 通用 |
| rockchip_multicodecs.c | Multi Codecs Machine | 通用 |
| rockchip_hdmi.c | HDMI Audio Machine | 通用 |
CODEC 驱动(kernel/sound/soc/codecs/):
| 文件 | 功能 |
| rk817_codec.c | RK817/RK809 PMIC 内置 CODEC |
| rk3308_codec.c | RK3308 内置 8ch ADC |
| hdmi-codec.c | 通用 HDMI CODEC |
| dmic.c | 数字麦克风占位 CODEC |
你不需要全部看懂。抓住一条主线:I2S-TDM 驱动的 probe 流程,就能理解 80% 的套路。
+%26+CODEC (codecs 目录))
二、主线:I2S-TDM 驱动从加载到就绪
2.1 驱动是怎么被加载的?
每个平台驱动都有一个compatible 匹配表:
static const struct of_device_id rockchip_i2s_tdm_match[] = { { .compatible = "rockchip,rk3308-i2s-tdm", .data = &i2s_tdm_rk3308 }, { .compatible = "rockchip,rk3568-i2s-tdm", .data = &i2s_tdm_rk3568 }, { .compatible = "rockchip,rk3588-i2s-tdm", .data = &i2s_tdm_rk3588 }, { .compatible = "rockchip,prv-i2s-tdm", .data = &i2s_tdm_prv }, {},};
内核启动时,设备树中的compatible = "rockchip,rk3588-i2s-tdm" 会和这个表匹配上,然后调用probe() 函数。所以DTS 里的 compatible 字符串必须和驱动里的一致,这是最常见的 "驱动没加载" 原因之一。
2.2 Probe 函数里在做什么?
rockchip_i2s_tdm_probe() 是整个驱动初始化的核心,可以概括为 6 个步骤:
probe() │ ├── ① 分配私有数据结构(存时钟、regmap、配置参数) │ ├── ② 映射寄存器地址(通过设备树 reg 属性) │ ├── ③ 获取所有时钟 │ ├── mclk_tx / mclk_rx(收发主时钟) │ ├── mclk_tx_src / mclk_rx_src(时钟源选择) │ └── mclk_root0 / mclk_root1(根 PLL:48k 系/44.1k 系) │ ├── ④ 获取复位控制器 + 初始化 regmap │ ├── ⑤ 注册 ASoC Component(包含 DAI 定义和操作函数) │ ├── hw_params(设置采样率、通道数、位深) │ ├── set_fmt(设置 I2S/PCM 格式、主从关系) │ ├── set_sysclk(设置 MCLK 频率) │ ├── trigger(启动/停止 DMA 传输) │ └── startup/shutdown(资源申请/释放) │ └── ⑥ 注册 DMAEngine PCM(配置 DMA 搬运参数)
这 6 步走完后,DAI 驱动就就绪了,等待 Machine Driver 来 "认领" 它。
三、五个关键操作函数,逐个拆解
3.1 hw_params:每一次播放 / 录音前的 "参数协商"
应用程序调用pcm_open() 后,ALSA 框架会自动调用 hw_params(),把采样率、通道数、位深等参数传下来。驱动在这里做三件事:
(1)计算并设置 MCLK 频率
freq = params_rate(params) * mclk_fs; // 例如 48000 * 256 = 12.288MHzclk_set_rate(i2s_tdm->mclk_tx, freq);
(2)根据采样率选择 PLL 根时钟
这是 I2S-TDM 驱动的一个关键设计:48kHz 系列和 44.1kHz 系列使用不同的 PLL,保证时钟精度。
if (params_rate(params) % 11025 == 0) { // 44.1k、88.2k、176.4k、11.025k、22.05k → 走这条 clk_set_parent(i2s_tdm->mclk_tx_src, i2s_tdm->mclk_root1);} else { // 48k、96k、192k、16k、32k、8k → 走这条 clk_set_parent(i2s_tdm->mclk_tx_src, i2s_tdm->mclk_root0);}
(3)配置通道数和位深,写入寄存器
// 通道数val = params_channels(params); // 2 / 4 / 6 / 8// 位深switch (params_width(params)) {case 16: txcr_val = I2S_TXCR_VDW(16); break;case 24: txcr_val = I2S_TXCR_VDW(24); break;case 32: txcr_val = I2S_TXCR_VDW(32); break;}// 写入 TXCR(发送控制)、RXCR(接收控制)、CKR(时钟控制)寄存器regmap_write(i2s_tdm->regemap, I2S_TXCR, txcr_val);
什么时候会出问题?
•MCLK 计算错误 → 时钟频率不对 → 播放速度异常
•PLL 选错 → 44.1k 的内容用 48k 的时钟播 → 音调变高
•通道数配置和 CODEC 不一致 → 数据错位 → 声道反转或只有单声道
3.2 set_fmt:初始化时的 "通信协议约定"
这个函数在声卡初始化时调用一次,约定好 CPU 和 CODEC 之间的通信规则:
(1)主从关系
switch (fmt & SND_SOC_DAIFMT_CLOCK_PROVIDER_MASK) {case SND_SOC_DAIFMT_CBC_CFC: // CPU 为 Master,CODEC 为 Slave(最常见) ckr |= I2S_CKR_MSS_SLAVE; break;case SND_SOC_DAIFMT_CBM_CFM: // CPU 为 Slave,CODEC 为 Master ckr &= ~I2S_CKR_MSS_SLAVE; break;}
(2)数据格式
switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) {case SND_SOC_DAIFMT_I2S: txcr = I2S_TXCR_MODE(I2S_MODE_I2S); break;case SND_SOC_DAIFMT_LEFT_J: txcr = I2S_TXCR_MODE(I2S_MODE_LEFT_J); break;case SND_SOC_DAIFMT_DSP_A: txcr = I2S_TXCR_MODE(I2S_MODE_PCM); break;}
(3)时钟极性
switch (fmt & SND_SOC_DAIFMT_INV_MASK) {case SND_SOC_DAIFMT_NB_NF: /* 正常 */ break;case SND_SOC_DAIFMT_IB_NF: /* BCLK 反相 */ ckr |= I2S_CKR_IBP; break;}
3.3 trigger:播放 / 暂停的那一瞬间
这是用户感知最直接的操作—— 点击播放或暂停时,触发的就是这个函数:
switch (cmd) {case SNDRV_PCM_TRIGGER_START:case SNDRV_PCM_TRIGGER_RESUME:case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: // 启动 TX/RX DMA 传输 regmap_update_bits(i2s_tdm->regemap, I2S_XFER, I2S_XFER_TXS_START, I2S_XFER_TXS_START); break;case SNDRV_PCM_TRIGGER_STOP:case SNDRV_PCM_TRIGGER_SUSPEND:case SNDRV_PCM_TRIGGER_PAUSE_PUSH: // 停止 DMA 传输 regmap_update_bits(i2s_tdm->regemap, I2S_XFER, I2S_XFER_TXS_START, I2S_XFER_TXS_STOP); break;}
没有什么复杂逻辑,就是往I2S_XFER 寄存器写开始 / 停止位。但如果这个函数没被调到,检查上层 ALSA 的状态机是否正确转换到了 RUNNING。
→trigger ()→写 I2S_XFER 寄存器→启停 DMA 传输)
四、时钟树:为什么 44.1k 和 48k 要分两路?
I2S-TDM 驱动管理着一棵精密的时钟树:
VPLL0 (48k 系列) ──┐ ├──► mclk_root0 ──► mclk_tx_src ──► mclk_tx ──► 输出到 CODECVPLL1 (44.1k 系列)─┘ mclk_tx ──► 分频器 ──► BCLK(位时钟) │ └──► 分频器 ──► LRCK(帧时钟 = 采样率)
为什么要分两个 PLL?
因为 48kHz × 256 = 12.288MHz,44.1kHz × 256 = 11.2896MHz。如果用同一个 PLL 分频,很难同时精确得到这两个频率。Rockchip 干脆用两个独立的 PLL,各管各的家族。
MCLK 校准(可选功能)
某些场景对时钟精度要求极高(比如专业音频),DTS 可以开启 rockchip,mclk-calibrate。驱动会根据实际 LRCK 频率反推 MCLK,在 ±1000 ppm 范围内微调。
五、DMAEngine:数据搬运工
CPU 不直接参与音频数据的搬运,而是交给 DMA(直接内存访问)引擎:
static const struct snd_pcm_hardware rockchip_i2s_tdm_pcm_hardware = { .formats = SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S24_LE | SNDRV_PCM_FMTBIT_S32_LE, .channels_min = 2, .channels_max = 8, .rates = SNDRV_PCM_RATE_8000_192000, .period_bytes_min = 128, .period_bytes_max = 8192, .periods_min = 2, .periods_max = 128,};
这段代码声明了该 DAI 的能力范围:支持 16/24/32bit、28 通道、8k192k 采样率。应用程序请求超出这个范围的参数时,ALSA 会直接拒绝。
六、PDM 驱动:和 I2S 有什么不同?
PDM(数字麦克风)驱动在 rockchip_pdm.c 里,套路和 I2S 类似,但有三个关键差异:
| 方面 | I2S-TDM | PDM |
| 数据方向 | TX + RX | 仅 RX(只有麦克风输入) |
| 时钟 | BCLK + LRCK | 只有 PDM_CLK |
| 数据格式 | 多 bit PCM | 1-bit PDM,芯片内部抽取成 PCM |
PDM 的时钟计算很简单:
PDM_CLK = 采样率 × 抽取比(通常是 64 或 128)// 例:16kHz 采样率,抽取比 64 → PDM_CLK = 1.024 MHz
七、Machine Driver 的注册:最后一块拼图
DAI 驱动和 CODEC 驱动都就绪后,Machine Driver(Simple Card / Multi Codecs / HDMI)把它们组装成声卡。
Simple Card 的注册流程:
DTS sound 节点 │ ▼simple-card.c probe() │ ├── 解析 CPU DAI phandle → 获取 DAI 名称 ├── 解析 CODEC DAI phandle ├── 解析 format(I2S/PCM/Left-J 等) ├── 解析 mclk-fs(MCLK 倍频) ├── 解析主从关系 ├── 初始化 dai_link └── devm_snd_soc_register_card() → 注册声卡
注册成功后,你会在/proc/asound/cards 里看到新声卡,/dev/snd/ 下出现pcmC0D0p、controlC0 等设备节点。
八、关键函数速查表
| 函数 | 作用 | 触发时机 |
| probe() | 驱动初始化:映射寄存器、获取时钟、注册 Component | 内核启动,设备树匹配成功 |
| hw_params() | 配置 BCLK/LRCK 频率、通道数、位深 | 应用打开 PCM 设备时 |
| set_fmt() | 配置 DAI 格式(I2S/PCM)、主从关系 | 声卡初始化时 |
| set_sysclk() | 配置 MCLK 频率 | 声卡初始化或 hw_params 时 |
| trigger() | 启动 / 停止 DAI 数据传输 | 播放 / 录音开始 / 暂停 / 停止 |
| startup/shutdown() | 分配 / 释放 DAI 资源 | PCM 打开 / 关闭时 |
、hw_params (参数配置)、set_fmt (格式约定)、trigger (启停) 等)
九、给代码阅读者的建议
1.先看 probe ():理解驱动初始化时做了什么,时钟从哪来
2.再看 hw_params ():理解播放前参数怎么传递、怎么配置寄存器
3.然后看 trigger ():理解数据流怎么启停
4.最后扫一眼 set_fmt ():理解主从和格式的配置逻辑
其他文件(regmap、DMA、DAPM)可以先放一放,等主线通了再回来看细节。
十、总结
Rockchip 音频驱动的完整启动链路:
1.DAI 驱动 probe → 映射寄存器、获取时钟、注册 ASoC Component
2.DMAEngine PCM 注册 → 配置 DMA 参数、声明能力范围
3.Machine Driver probe → 解析 DTS、组装 dai_link
4.声卡注册 → 创建 /dev/snd/ 设备节点,用户空间可见
每一步都有明确的输入(设备树属性)和输出(注册的数据结构),出了问题按这个链路倒着查,通常能快速定位。
你在走读驱动代码时,哪个环节最让你困惑?probe、时钟、还是 DMA?评论区聊聊。
审核编辑 黄宇
全部0条评论
快来发表一下你的评论吧 !