1. 概述

Linux 内核中,瑞芯微 I2S 控制器由 ASoC(ALSA System on Chip)框架管理。整条音频通路由三部分组成:

  1. CPU/Codec 平台驱动:实现 snd_soc_dai_driver,描述 I2S 控制器能力;
  2. Machine 驱动:将 CPU DAI 与 Codec DAI 绑定为一条 DAI link;
  3. 设备树 (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 场景;
  • clocksI2S0_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/slaveclk_invframe_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_tableI2S0_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:trace snd_pcm_* 关键路径;
  • dmesg | grep -i i2s:观察驱动日志。

Logo

社区规范:仅讨论OpenHarmony相关问题。

更多推荐