Rockchip 音频驱动代码走读:从内核启动到声卡就绪,到底发生了什么?

电子说

1.4w人已加入

描述

打开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?评论区聊聊。

审核编辑 黄宇

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分