瑞芯微 I2S 学习笔记:驱动开发与设备树配置
1. 概述
Linux 内核中,瑞芯微 I2S 控制器由 ASoC(ALSA System on Chip)框架管理。整条音频通路由三部分组成:
- CPU/Codec 平台驱动:实现
snd_soc_dai_driver,描述 I2S 控制器能力; - Machine 驱动:将 CPU DAI 与 Codec DAI 绑定为一条 DAI link;
- 设备树 (DTS):描述硬件拓扑、时钟、引脚、格式参数。
本篇基于 RK3588 + ES8388 典型方案,结合 kernel/sound/soc/rockchip/ 源码,逐层拆解如何从设备树到驱动打通 I2S 通路。
2. 软件分层结构
┌──────────────────────────────────────────┐
│ user-space │ tinyplay / aplay / arecord
└──────────────────────────────────────────┘
│ ALSA / ioctl
┌──────────────────────────────────────────┐
│ ALSA core │ /sound/core
└──────────────────────────────────────────┘
┌──────────────────────────────────────────┐
│ ASoC core │ /sound/soc/soc-core.c
│ ┌──────────────┐ ┌──────────────┐ │
│ │ soc-dai.c │ │ soc-pcm.c │ │
│ └──────────────┘ └──────────────┘ │
└──────────────────────────────────────────┘
┌──────────────────┬───────────────────────┐
│ rockchip-i2s.c │ es8388.c (Codec) │
│ (CPU DAI) │ (Codec DAI) │
└──────────────────┴───────────────────────┘
┌──────────────────────────────────────────┐
│ rockchip-sound.c (machine) │
└──────────────────────────────────────────┘
│ DTS
┌──────────────────────────────────────────┐
│ rk3588-i2s.dtsi / board.dts │
└──────────────────────────────────────────┘
从 RK5.10 kernel 开始,瑞芯微 BSP 主要使用
simple-audio-card风格的rockchip-sound兼容驱动,新平台(RK3588)则引入audio-graph-card。
3. 设备树配置
3.1 I2S 控制器节点
RK3588 I2S0 DTS(节选自 arch/arm64/boot/dts/rockchip/rk3588-i2s.dtsi):
i2s0_8ch: i2s@fe470000 {
compatible = "rockchip,rk3588-i2s";
reg = <0x0 0xfe470000 0x0 0x1000>;
interrupts = <GIC_SPI 220 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&cru I2S0_MCLKOUT>, <&cru I2S0_MCLK>, <&cru I2S0_LRCK>,
<&cru I2S0_SCLK>, <&cru PCLK_AUDIO_ROOT>;
clock-names = "i2s_clk", "i2s_hclk", "i2s_lrclk",
"i2s_sclk", "hclk";
dmas = <&dmac1 0>, <&dmac1 1>;
dma-names = "tx", "rx";
pinctrl-names = "default";
pinctrl-0 = <&i2s0_lrck>, <&i2s0_sclk>, <&i2s0_sdi0>, <&i2s0_sdo0>;
#sound-dai-cells = <0>;
rockchip,no-dma-tx;
rockchip,no-dma-rx;
status = "disabled";
};
说明:
i2s0_8ch:节点名 + 后缀8ch表示该 I2S 控制器支持 8 通道。也有i2s0_2ch用于 2ch-only 场景;clocks:I2S0_MCLKOUT是输出给 Codec 的主时钟;I2S0_MCLK是内部 clock;I2S0_LRCK/SCLK是 LRCLK/BCLK 内部名称;dmas:DMA 通道绑定,必须与dma-names一一对应;pinctrl:复用功能与电气属性(bias、drive strength)。
3.2 Board DTS:使能 I2S0 并绑定引脚
&i2s0_8ch {
status = "okay";
pinctrl-0 = <&i2s0_lrck
&i2s0_sclk
&i2s0_sdo0
&i2s0_sdi0
&i2s0_mclk>;
#sound-dai-cells = <0>;
};
3.3 Codec 节点
ES8388 标准节点:
es8388: es8388@10 {
reg = <0x10>;
compatible = "everest,es8388";
clocks = <&i2s0_8ch>;
clock-names = "mclk";
pinctrl-names = "default";
pinctrl-0 = <&i2s0_mclk>;
#sound-dai-cells = <0>;
status = "okay";
};
3.4 Sound Card(Machine 节点)
RK3588 默认采用 audio-graph-card 风格的 sound card:
&i2s0_8ch {
status = "okay";
};
sound {
compatible = "audio-graph-card";
label = "rockchip-es8388";
dais = <&i2s0_8ch_port>, <&es8388_port>;
};
i2s0_8ch_port: port@0 {
reg = <0>;
i2s0_8ch_endpoint: endpoint@0 {
remote-endpoint = <&es8388_endpoint>;
format = "i2s";
mclk-fs = <256>;
bitclock-master = <&i2s0_8ch_endpoint>;
frame-master = <&i2s0_8ch_endpoint>;
};
};
es8388_port: port@0 {
reg = <0>;
es8388_endpoint: endpoint@0 {
remote-endpoint = <&i2s0_8ch_endpoint>;
format = "i2s";
};
};
关键属性:
format = "i2s" | "left_j" | "right_j" | "dsp_a" | "dsp_b";bitclock-master/frame-master:用 endpoint 句柄指向主端;mclk-fs:MCLK 与 LRCLK 频率比;bitclock-inversion/frame-inversion:边沿极性反转。
3.5 兼容老 BSP:simple-audio-card
旧 BSP 通常使用 simple-audio-card:
sound {
compatible = "simple-audio-card";
simple-audio-card,format = "i2s";
simple-audio-card,name = "rockchip,es8388";
simple-audio-card,mclk-fs = <256>;
simple-audio-card,bitclock-master = <&cpu_dai>;
simple-audio-card,frame-master = <&cpu_dai>;
cpu_dai: simple-audio-card,cpu {
sound-dai = <&i2s0_8ch>;
};
codec_dai: simple-audio-card,codec {
sound-dai = <&es8388>;
clocks = <&i2s0_8ch>;
system-clock-frequency = <12288000>;
};
};
两种风格不能并存,否则 ASoC 会拒绝绑定。
4. CPU DAI 驱动
4.1 驱动注册骨架
sound/soc/rockchip/rockchip_i2s.c:
static struct snd_soc_dai_driver rockchip_i2s_dai = {
.playback = {
.channels_min = 2,
.channels_max = 8,
.rates = SNDRV_PCM_RATE_8000_192000,
.formats = SNDRV_PCM_FMTBIT_S8 |
SNDRV_PCM_FMTBIT_S16_LE |
SNDRV_PCM_FMTBIT_S24_LE |
SNDRV_PCM_FMTBIT_S32_LE,
},
.capture = {
.streams = "Capture",
.channels_min = 1,
.channels_max = 8,
.rates = SNDRV_PCM_RATE_8000_192000,
.formats = SNDRV_PCM_FMTBIT_S16_LE |
SNDRV_PCM_FMTBIT_S24_LE |
SNDRV_PCM_FMTBIT_S32_LE,
},
.ops = &rockchip_i2s_dai_ops,
.symmetric_rate = 1,
};
static int rockchip_i2s_probe(struct platform_device *pdev)
{
struct device_node *np = pdev->dev.of_node;
struct rk_i2s_dev *i2s;
int ret;
i2s = devm_kzalloc(&pdev->dev, sizeof(*i2s), GFP_KERNEL);
if (!i2s)
return -ENOMEM;
i2s->dev = &pdev->dev;
i2s->variant = of_device_get_match_data(&pdev->dev);
spin_lock_init(&i2s->lock);
i2s->sclk = devm_clk_get(&pdev->dev, "i2s_sclk");
i2s->hclk = devm_clk_get(&pdev->dev, "i2s_hclk");
i2s->mclk = devm_clk_get(&pdev->dev, "i2s_clk");
i2s->regmap = devm_regmap_init_mmio(&pdev->dev,
devm_platform_ioremap_resource(pdev, 0),
i2s->variant->regmap_config);
i2s->playback_dma_data.addr = res->start + I2S_TXDR;
i2s->playback_dma_data.addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
i2s->playback_dma_data.maxburst = 8;
i2s->capture_dma_data.addr = res->start + I2S_RXDR;
i2s->capture_dma_data.addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
i2s->capture_dma_data.maxburst = 8;
ret = devm_snd_soc_register_component(&pdev->dev,
&rockchip_i2s_component,
&rockchip_i2s_dai, 1);
return ret;
}
4.2 关键 ops 回调
static int rockchip_i2s_hw_params(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params,
struct snd_soc_dai *dai)
{
struct rk_i2s_dev *i2s = to_info(dai);
unsigned int mclk_rate, bclk_rate, lrclk_rate;
int ret;
lrclk_rate = params_rate(params);
bclk_rate = lrclk_rate * params_channels(params) *
params_width(params);
mclk_rate = lrclk_rate * i2s->mclk_fs;
ret = clk_set_rate(i2s->mclk, mclk_rate);
if (ret) return ret;
ret = clk_set_rate(i2s->sclk, bclk_rate);
if (ret) return ret;
rockchip_i2s_set_fmt(i2s, dai->active_fmt);
rockchip_i2s_set_clk(i2s, bclk_rate, lrclk_rate);
rockchip_i2s_txrx_config(i2s, substream->stream);
return 0;
}
其他常用 ops:
set_fmt:协商 I2S/PCM/LJ/RJ、master/slave、clk_inv、frame_inv;set_sysclk:设置 MCLKFS;trigger:在 START/RESUME 时解复位 FIFO + 启动 DMA;startup:复位 FIFO、reset controller;shutdown:关闭 DMA、关闭 MCLK。
4.3 中断处理
static irqreturn_t rockchip_i2s_irq(int irq, void *dev_id)
{
struct rk_i2s_dev *i2s = dev_id;
u32 val;
val = regmap_read(i2s->regmap, I2S_ISR);
if (val & I2S_ISR_TX_UR)
dev_warn(i2s->dev, "TX underrun\n");
if (val & I2S_ISR_RX_OR)
dev_warn(i2s->dev, "RX overrun\n");
regmap_write(i2s->regmap, I2S_CLR, val);
return IRQ_HANDLED;
}
4.4 与 ASoC 注册
static struct snd_soc_component_driver rockchip_i2s_component = {
.name = "rockchip-i2s",
.pcm_new = rockchip_i2s_pcm_new,
.pointer = rockchip_i2s_pointer,
.ops = &rockchip_i2s_dai_ops,
};
static const struct of_device_id rockchip_i2s_match[] = {
{ .compatible = "rockchip,rk3588-i2s",
.data = &rk3588_i2s_variant },
{ .compatible = "rockchip,rk3568-i2s",
.data = &rk3568_i2s_variant },
{},
};
MODULE_DEVICE_TABLE(of, rockchip_i2s_match);
5. Codec 驱动(CODEC 侧视角)
5.1 DAI
static struct snd_soc_dai_driver es8388_dai = {
.name = "es8388-hifi",
.playback = es8388_hw_rules_playback,
.capture = es8388_hw_rules_capture,
.ops = &es8388_ops,
};
5.2 控件(kcontrols)
static const struct snd_kcontrol_new es8388_snd_controls[] = {
SOC_DOUBLE_TLV("Playback Volume", ES8388_DACCONTROL1, 0, 4, 192, 1,
es8388_tlv),
SOC_SINGLE("Playback Switch", ES8388_DACCONTROL3, 0, 1, 1),
SOC_DOUBLE("Capture Volume", ES8388_ADCCONTROL1, 4, 0, 15, 1),
};
5.3 DAPM 路由
static const struct snd_soc_dapm_route es8388_dapm_routes[] = {
{"DAC Input", NULL, "DAC Mux"},
{"DAC Mux", "Diff", "DAC Mixer"},
{"DAC Mixer", NULL, "DAC"},
{"HP Out", NULL, "DAC"},
};
DAPM 通过 widget 自动上电通路,未启用的器件保持断电,从而降低功耗。
6. Machine 驱动
6.1 simple-audio-card 路径
新 BSP 推荐 audio-graph-card:
sound {
compatible = "audio-graph-card";
dais = <&i2s0_8ch_port>, <&es8388_port>;
};
对应驱动位于 sound/soc/generic/audio-graph-card.c,它会自动解析 graph 并注册 sound card。
6.2 自定义 Machine 驱动(高级场景)
在多 Codec、多 DAI link 的项目中仍然需要:
static struct snd_soc_dai_link rk3588_dai_links[] = {
{
.name = "I2S0 ES8388",
.stream_name = "Main",
.codec_dai_name = "es8388-hifi",
.cpu_dai_name = "i2s0-hifi",
.platform_name = "i2s0-hifi",
.codec_name = "es8388.0-0010",
.dai_fmt = SND_SOC_DAIFMT_I2S |
SND_SOC_DAIFMT_NB_NF |
SND_SOC_DAIFMT_CBS_CFS,
.init = rk3588_es8388_init,
},
};
static struct snd_soc_card rk3588_card = {
.name = "rockchip-rk3588",
.owner = THIS_MODULE,
.dai_link = rk3588_dai_links,
.num_links = ARRAY_SIZE(rk3588_dai_links),
.dapm_widgets = rk3588_dapm_widgets,
.num_dapm_widgets = ARRAY_SIZE(rk3588_dapm_widgets),
};
其中 .init 通常用于:
- 设置 Codec 初始寄存器(如 PA 偏置、PLL、AGC);
- 打开耳机/扬声器开关;
- 设置 TDM slot 映射。
6.3 私有数据透传
static int rk3588_es8388_init(struct snd_soc_pcm_runtime *rtd)
{
struct rk3588_machine_priv *priv = snd_soc_card_get_drvdata(rtd->card);
return snd_soc_component_set_sysclk(rtd->codec_dai, 0,
priv->mclk_rate,
SND_SOC_CLOCK_IN);
}
machine 私有结构体保存于 snd_soc_card->drvdata,可承载 board-level 信息(喇叭数、EQ、默认音量)。
7. 时钟与复位
7.1 Cru(Clock Reset Unit)节点
cru: clock-controller@fd7c0000 {
...
assigned-clocks = <&cru I2S0_MCLK>;
assigned-clock-rates = <12288000>;
};
7.2 I2S 控制器内部 PLL
static const struct clk_div_table div_bclk_table[] = {
{ .val = 0, .div = 1 }, { .val = 1, .div = 2 },
{ .val = 2, .div = 4 }, { .val = 3, .div = 8 },
{ .val = 4, .div = 16 }, { .val = 5, .div = 32 },
{ .val = 6, .div = 64 }, { .val = 7, .div = 128 },
{ }
};
驱动使用 clk_register_divider_table 把 I2S0_SCLK 暴露成 BCLK;LRCK 由 BCLK 经另一级 divider 生成。
7.3 复位
ret = of_get_named_gpio(np, "reset-gpios", 0);
if (ret > 0)
devm_gpio_request_one(i2s->dev, ret, GPIOF_OUT_INIT_LOW, "es8388-reset");
外部 Codec 复位可在 machine 驱动 probe 中执行,I2S 控制器复位由 CRU 的 SRST_I2S0 完成。
8. 常见驱动开发问题
| 现象 | 根因 | 排查 |
|---|---|---|
aplay: pcm_write:2030: write error: Input/output error |
TX FIFO underrun | 检查 DMA buffer、period_size、snd_soc_dai_trigger |
提示 snd_soc_register_card failed |
DAI link CPU/Codec 不匹配 | 检查 DTS sound-dai 名、dai_fmt |
| 注册成功但无声 | 时钟未起 / DAPM widget 未 enable | amixer cget numid 查看开关、tinymix 控制 DAPM |
| 底噪大 | MCLK 偏移 | 验证 PLL 是否整除 Fs |
| HDMI 输出无声音 | TDM slot 错误 | cat /proc/asound/*/status,确认 format = "dsp_a" 等 |
| 启动慢 | Codec reset 时序长 | 优化 i2c 设备 probe 顺序 |
9. 常用工具
tinymix:列出与控制 ALSA kcontrol;tinyplay/tinycap:脱离 PulseAudio 直接操作 PCM;aplay/arecord -l:列出 card/device;amixer -c <card> cget/cset:CLI 控制;alsactl store/restore:保存状态;/sys/kernel/debug/asoc/:查看 DAI link 状态;ftrace:tracesnd_pcm_*关键路径;dmesg | grep -i i2s:观察驱动日志。
更多推荐
所有评论(0)