2. I2S 音频总线协议详解:标准I2S、左对齐、右对齐模式,以及STM32 I2S外设配置

各位同学,咱们今天聊聊I2S。说实话,很多做嵌入式的人一开始都分不清I2S和I2C,名字太像了。但这两个东西完全不一样——I2C是控制总线,I2S是音频数据总线。我当年第一次调WM8960的时候,就是被这个I2S的格式给坑了一把,折腾了两天才出声。

I2S全称是Inter-IC Sound,由飞利浦在1986年提出。它的设计目标很纯粹:在芯片之间传输数字音频数据。你想想看,音频数据是连续的流,不像控制指令那样发一个等一个回复。所以I2S的协议设计,说白了就是围绕「怎么把连续的音频数据,用最少的线,准确无误地传过去」这个问题展开的。

2.1 I2S总线物理层:三根线就够了

I2S总线只需要三根信号线:

  • SCK(串行时钟,也叫BCLK)—— 每一位数据对应一个时钟脉冲
  • WS(字选择,也叫LRCK)—— 区分左声道和右声道
  • SD(串行数据)—— 音频数据就在这根线上传输

有些场景还会多一根MCLK(主时钟),但那是给编解码器内部锁相环用的,不是I2S协议本身的线。嗯,这里要注意:STM32的I2S外设可以输出MCLK,但WM8960也可以自己产生,取决于你的时钟方案。

关键点:WS的频率 = 采样率 × 声道数。比如采样率44.1kHz、立体声,WS就是44.1kHz。SCK的频率 = WS频率 × 位深 × 2。16位立体声44.1kHz,SCK = 44.1k × 16 × 2 = 1.4112MHz。

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

这部分是面试常考题,也是实际项目中容易踩坑的地方。三种格式的区别,说白了就是「数据相对于WS信号的摆放位置不同」。

2.2.1 标准I2S(飞利浦协议)

标准I2S的特点是:数据比WS变化晚一个SCK周期。也就是说,WS切换后,下一个SCK的上升沿才开始传数据。这样做的好处是,接收端可以用WS的边沿来同步,不用太担心时序问题。

我记得第一次看标准I2S的时序图,觉得挺绕的。后来我总结了一个口诀:「WS变,等一拍,数据才出来」。你记住这个,基本就不会搞混了。

2.2.2 左对齐模式

左对齐模式就直白多了:WS变化的同时,数据就开始传。没有那一拍的延迟。数据从MSB开始,紧贴着WS的边沿对齐。

这种模式在早期的DAC里比较常见。我在一个老项目里遇到过,那个DAC只支持左对齐,而STM32默认配置是标准I2S,结果怎么调都没声音。后来查手册才发现是格式不匹配。

2.2.3 右对齐模式

右对齐模式,也叫日本格式或Sony格式。它的特点是:数据的LSB对齐到SCK的最后一个有效位。说白了,数据靠右对齐,左边补零。

这种格式在16位数据、24位SCK周期的情况下比较常见。比如你传16位数据,但SCK每声道给了24个时钟,那数据就放在最后16位,前面8位补0。

格式 数据起始位置 常见应用场景 我遇到的坑
标准I2S WS变化后延迟1个SCK 绝大多数编解码器 WM8960默认就是这个
左对齐 WS变化同时开始 老式DAC、部分ADC 曾经调了2天发现是格式问题
右对齐 数据靠右,左边补零 Sony设备、部分DSP 16位数据用24位帧时容易搞错

注意:WM8960支持标准I2S和左对齐两种模式,通过寄存器(0x04)的BIT[7:6]配置。默认是标准I2S。如果你用STM32的HAL库,默认也是标准I2S,所以一般不用改。但如果你换了编解码器,一定要先确认格式。

2.3 数据格式与位深的关系

这里有个容易混淆的点:I2S的帧长度和数据的实际位深,是两个概念。

  • 帧长度:每个声道占多少个SCK周期。常见的有16位、24位、32位。
  • 数据位深:实际有效数据的位数。比如你采集的是16位音频,但帧长度可以是32位。

为什么会这样?因为有些编解码器内部处理精度更高,或者为了兼容性,会使用更长的帧。WM8960支持16位、20位、24位、32位的数据位深,但帧长度必须和位深匹配,或者大于位深。

我个人习惯是:如果音频源是16位,我就用16位帧长度,省带宽。但如果后续要做算法处理,我会用24位或32位帧,保留更多精度。

2.4 STM32 I2S外设配置实战

好,理论说完了,咱们看看STM32上怎么配。我用的是STM32F4系列,HAL库。其他系列大同小异。

2.4.1 时钟配置

STM32的I2S时钟可以从PLLI2S产生,也可以从外部时钟输入。我建议用PLLI2S,频率更灵活。配置公式是:

I2SxCLK = (PLLI2S_N * HSE_VALUE) / (PLLI2S_R * 预分频系数)

嗯,这个公式看着复杂,其实HAL库有现成的函数帮你算。你只需要告诉它采样率和位深就行。

小技巧:用HAL_I2S_Init()之前,先调用HAL_I2S_GetPrescaler()看看预分频值是否在有效范围内。我遇到过预分频算出来是小数的情况,这时候就得调整PLLI2S的N值。

2.4.2 引脚配置

STM32的I2S引脚通常是复用的。以F407为例,I2S2的引脚是:

  • SCK - PB13
  • WS - PB12
  • SD - PB15
  • MCK - PC6(可选)

配置成复用功能,速度选High。我习惯把GPIO的上下拉设成No Pull,因为I2S是推挽输出,不需要上拉。

2.4.3 初始化结构体

hi2s2.Instance = SPI2;
hi2s2.Init.Mode = I2S_MODE_MASTER_TX;
hi2s2.Init.Standard = I2S_STANDARD_PHILIPS;
hi2s2.Init.DataFormat = I2S_DATAFORMAT_16B;
hi2s2.Init.MCLKOutput = I2S_MCLKOUTPUT_ENABLE;
hi2s2.Init.AudioFreq = I2S_AUDIOFREQ_44K;
hi2s2.Init.CPOL = I2S_CPOL_LOW;
hi2s2.Init.ClockSource = I2S_CLOCK_PLL;
hi2s2.Init.FullDuplexMode = I2S_FULLDUPLEXMODE_DISABLE;

HAL_I2S_Init(&hi2s2);

这里要注意CPOL参数。标准I2S要求时钟空闲时为低电平,数据在上升沿采样。如果你用的是左对齐模式,时钟极性可能不一样。WM8960的手册里明确写了:标准I2S模式下,SCK空闲为低。

2.4.4 发送与接收

HAL库提供了阻塞、中断、DMA三种方式。我推荐用DMA,尤其是音频数据量大、实时性要求高的时候。

// 发送
HAL_I2S_Transmit_DMA(&hi2s2, pData, Size);

// 接收
HAL_I2S_Receive_DMA(&hi2s2, pData, Size);

// 回调函数
void HAL_I2S_TxCpltCallback(I2S_HandleTypeDef *hi2s)
{
    // 数据发送完成,可以填充下一批数据
}

我曾经在一个项目里用中断方式传音频,结果CPU占用率飙到80%,音频还断断续续。换成DMA后,CPU占用率降到5%以下。所以,音频传输,首选DMA。

2.5 三种格式的时序对比图

下面这张图是我用SVG画的,把三种格式放在一起对比,你一看就明白了。

I2S三种数据格式时序对比 WS 左声道 右声道 SCK 标准I2S MSB LSB 延迟1SCK 左对齐 MSB LSB 无延迟 右对齐 补零 MSB→LSB 数据靠右 说明: • 标准I2S:WS变化后,延迟1个SCK周期才开始传数据(MSB优先) • 左对齐:WS变化的同时开始传数据,MSB紧贴WS边沿 • 右对齐:数据放在帧的末尾,前面补零,LSB对齐到帧结束 • 图中以16位数据、16位帧长度为例,实际项目中帧长度可能大于数据位深

2.6 配置时的常见问题

最后,我总结几个实际项目中容易遇到的问题:

经验1:WM8960的I2S格式寄存器是0x04的BIT[7:6]。00=左对齐,01=标准I2S,10=右对齐。默认是01。如果你发现声音不对,先检查这个寄存器。

经验2:STM32的I2S外设和SPI共用同一个外设。如果你初始化了SPI,再初始化I2S,可能会冲突。我建议在CubeMX里直接选I2S功能,不要手动改。

经验3:我曾经遇到一个诡异的问题——声音有杂音。查了半天,发现是SCK和WS的极性搞反了。WM8960要求SCK空闲为低,数据在上升沿采样。如果你的STM32配置了CPOL=High,那数据就全乱了。