2. I2S总线协议详解:物理层、数据格式与帧同步

I2S(Inter-IC Sound)总线是嵌入式Linux音频系统中应用最广泛的数字音频接口标准,由飞利浦半导体(现恩智浦)于1986年制定。它专门用于连接音频编解码器(CODEC)、DSP、FPGA或SoC内部的I2S控制器,实现立体声PCM音频数据的串行传输。本章从物理层信号、数据格式(左对齐、右对齐、标准I2S)以及帧同步与位时钟三个维度进行深入剖析。

2.1 I2S物理层:三线制与四线制

标准I2S总线使用三条主要信号线,部分实现会额外增加一条主时钟线(MCLK),形成四线制。物理层信号定义如下表所示:

信号线 缩写 方向(典型) 功能描述
位时钟 BCLK / SCK 主机→从机 每个脉冲对应一位数据的移位时钟,频率 = 采样率 × 位宽 × 通道数
帧同步 LRCK / WS 主机→从机 指示当前传输的是左声道还是右声道,频率等于采样率
串行数据 SD / SDATA 双向(半双工) 按位串行传输PCM采样数据,通常为二进制补码格式
主时钟(可选) MCLK / MCK 主机→从机 为CODEC内部Δ-Σ调制器提供高精度时钟,频率通常为256×Fs或384×Fs
设计要点:在嵌入式Linux系统中,SoC的I2S控制器通常作为主机(Master)提供BCLK和LRCK,CODEC作为从机(Slave)接收时钟并同步输出数据。MCLK并非I2S标准强制要求,但大多数高性能音频CODEC需要MCLK来驱动内部数字滤波器,否则会产生时钟抖动或无法锁定。

下图展示了I2S物理层三线制连接的基本拓扑结构:

I2S物理层三线制连接拓扑 SoC / CPU I2S 控制器(主机) 音频 CODEC I2S 接口(从机) BCLK LRCK (WS) SD (双向) 主机提供BCLK和LRCK,从机同步采样 SD线为半双工,收发分时复用或使用独立TX/RX线

2.2 数据格式:左对齐、右对齐与标准I2S

I2S协议定义了多种数据对齐方式,核心差异在于串行数据(SD)相对于帧同步信号(LRCK)和位时钟(BCLK)的相位关系。三种主流格式的对比如下:

格式名称 数据与LRCK对齐关系 MSB位置 LSB填充 典型应用
标准I2S格式 数据延迟1个BCLK周期,MSB在LRCK跳变后第2个BCLK上升沿 MSB固定在第2个BCLK LSB后多余BCLK填充0 飞利浦标准、主流CODEC
左对齐格式 数据与LRCK跳变同时开始,MSB在LRCK跳变后的第1个BCLK上升沿 MSB在第1个BCLK LSB后多余BCLK填充0 DSP模式、部分ADI CODEC
右对齐格式 数据对齐到LRCK跳变前的最后一个BCLK,MSB在数据帧末尾 MSB靠近帧结束 MSB前多余BCLK填充0 日本电子协会(JEITA)标准
常见配置陷阱:在Linux ALSA驱动中,snd_soc_dai_set_fmt() 函数通过 SND_SOC_DAIFMT_I2S、SND_SOC_DAIFMT_LEFT_J、SND_SOC_DAIFMT_RIGHT_J 枚举值选择格式。如果CODEC数据手册标注为“I2S格式”,但实际波形显示MSB与LRCK同时开始,则可能是左对齐格式,务必用示波器或逻辑分析仪确认。

下图以16位数据、32位BCLK周期为例,对比三种格式的时序差异:

I2S三种数据格式时序对比(16位数据,32BCLK/帧) LRCK 左声道(LRCK=低) 右声道(LRCK=高) BCLK SD (I2S) MSB ... 16位数据 ... LSB MSB ... 16位数据 ... LSB ▲ 延迟1BCLK SD (左对齐) MSB ... 16位数据 ... LSB MSB ... 16位数据 ... LSB ▲ 与LRCK同时开始 SD (右对齐) MSB ... 16位数据 ... LSB MSB ... 16位数据 ... LSB ▲ 对齐到帧末尾 三种格式的BCLK和LRCK时序完全相同,仅SD数据相对于LRCK的相位不同 标准I2S:延迟1BCLK;左对齐:无延迟;右对齐:对齐到帧结束 多余BCLK周期(16位数据使用32BCLK时)填充0

2.3 帧同步与位时钟的时序关系

帧同步信号(LRCK/WS)和位时钟(BCLK)共同决定了音频数据的采样率、位宽和通道数。其核心计算公式如下:

BCLK频率 = 采样率(Fs) × 位宽(bits) × 通道数(channels)

示例:
  Fs = 48000 Hz, 位宽 = 16 bits, 立体声(2通道)
  BCLK = 48000 × 16 × 2 = 1.536 MHz

  Fs = 48000 Hz, 位宽 = 32 bits, 立体声(2通道)
  BCLK = 48000 × 32 × 2 = 3.072 MHz

帧同步信号LRCK的频率始终等于采样率Fs,其占空比通常为50%(方波),但部分CODEC也支持非50%占空比。LRCK的跳变沿(上升沿或下降沿)用于标识新数据帧的开始:

  • 标准I2S约定:LRCK为低电平时传输左声道数据,高电平时传输右声道数据。数据在LRCK跳变后的第2个BCLK上升沿开始移位。
  • 左对齐约定:LRCK为低电平时传输左声道,高电平时传输右声道。数据在LRCK跳变后的第1个BCLK上升沿开始移位。
  • 右对齐约定:数据在LRCK跳变前的最后一个BCLK周期完成传输,MSB靠近帧结束位置。
Linux内核配置要点:在设备树(Device Tree)中,I2S控制器节点通常通过 clock-frequency 属性指定BCLK频率,或通过 frame-master 和 bitclock-master 属性指定时钟主从关系。例如:
cpu_dai: &i2s0 {
    #sound-dai-cells = <0>;
    clocks = <&clk IMX6QDL_CLK_SSI1>;
    clock-names = "ipg", "per";
    dmas = <&sdma 14 7 1>,
           <&sdma 15 7 1>;
    dma-names = "rx", "tx";
    status = "okay";
};

2.4 数据位宽与BCLK周期数的匹配

实际系统中,BCLK周期数(即每帧的BCLK脉冲数)必须大于或等于位宽 × 通道数。常见配置如下:

采样率 (Fs) 位宽 通道数 理论最小BCLK 常用BCLK配置 每帧BCLK数
48000 16 2 1.536 MHz 1.536 MHz 32
48000 24 2 2.304 MHz 3.072 MHz 64
48000 32 2 3.072 MHz 3.072 MHz 64
96000 24 2 4.608 MHz 6.144 MHz 64
192000 32 2 12.288 MHz 12.288 MHz 64
工程经验:当BCLK周期数大于实际数据位数时(例如16位数据使用64BCLK/帧),多余位在标准I2S和左对齐格式中填充0,在右对齐格式中则填充在MSB之前。这会导致数据帧中存在无效位,但不会影响有效数据的传输。Linux ALSA驱动通过 snd_pcm_hw_params_set_format() 设置物理格式,硬件会自动处理填充位。

下图展示了BCLK、LRCK与数据位宽之间的匹配关系:

BCLK与LRCK时序匹配关系(16位数据,32BCLK/帧) LRCK 左声道 (16位有效数据) 右声道 (16位有效数据) BCLK 1 5 9 13 17 21 25 29 32 1 5 9 13 17 21 25 29 32 16位有效数据 填充0 16位有效数据 填充0 每帧32个BCLK周期,其中前16个BCLK传输有效数据 后16个BCLK为填充位(标准I2S/左对齐填充0,右对齐填充在MSB前)

2.5 I2S协议在Linux内核中的抽象

在Linux ALSA框架中,I2S协议参数通过 snd_soc_dai_set_fmt() 和 snd_soc_dai_set_tdm_slot() 等API进行配置。关键参数包括:

  • 格式(Format):I2S、左对齐、右对齐、DSP_A、DSP_B等。
  • 时钟极性(Clock Polarity):BCLK和LRCK的有效沿(上升沿/下降沿)可配置。
  • 帧长度(Frame Length):每帧的BCLK周期数,通常为32、48、64等。
  • 数据偏移(Data Offset):数据相对于LRCK跳变沿的延迟BCLK数。
内核配置示例:在ASoC机器驱动中,通常通过以下代码片段配置I2S格式:
/* 设置DAI格式:标准I2S,主机模式 */
ret = snd_soc_dai_set_fmt(cpu_dai,
                          SND_SOC_DAIFMT_I2S |
                          SND_SOC_DAIFMT_NB_NF |
                          SND_SOC_DAIFMT_CBS_CFS);
if (ret < 0) {
    dev_err(rtd->dev, "Failed to set CPU DAI format: %d\n", ret);
    return ret;
}

/* 设置TDM时隙:立体声,16位数据,32BCLK/帧 */
ret = snd_soc_dai_set_tdm_slot(cpu_dai, 0x3, 0x3, 2, 16);
if (ret < 0) {
    dev_err(rtd->dev, "Failed to set TDM slot: %d\n", ret);
    return ret;
}