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画的,把三种格式放在一起对比,你一看就明白了。
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,那数据就全乱了。