4. I2S音频总线配置:深入理解I2S协议,配置STM32的I2S外设与WM8960进行音频数据交换

好,咱们进入第四章节。前面我们把WM8960的硬件连接和电源管理搞定了,现在要解决一个核心问题:音频数据怎么在STM32和WM8960之间跑起来?

答案就是I2S总线。说实话,I2S这玩意儿看着简单,就三根线加一根MCLK,但坑不少。我最早做音频项目时,以为I2S就是SPI换了个马甲,结果调试了整整两天声音都是嘶哑的。后来才发现是数据格式没对齐。

这一章,咱们就把I2S协议掰开揉碎,然后手把手配置STM32的I2S外设,让它和WM8960愉快地聊天。

4.1 I2S协议核心要点

I2S(Inter-IC Sound)是飞利浦公司定义的音频数据传输总线标准。它专门用来连接音频编解码器和数字信号处理器。说白了,就是给数字音频铺一条专用高速公路。

标准I2S总线有四根信号线:

  • SCK(串行时钟,也叫BCLK)—— 每一位数据的同步时钟
  • WS(字选择,也叫LRCK)—— 左右声道切换信号
  • SD(串行数据)—— 音频数据线,可以是输入或输出
  • MCLK(主时钟)—— 可选,用于Codec内部锁相环和采样率生成

这里有个关键点:WS的频率等于采样率。比如你播放44.1kHz的音频,WS就是44.1kHz。SCK的频率等于采样率 × 位宽 × 2(双声道)。16位数据、44.1kHz采样率,SCK就是44.1k × 16 × 2 = 1.4112MHz。

嗯,这个计算你心里要有数,后面配置时钟时会用到。

4.2 I2S数据格式:别让左右声道串了

I2S协议规定,WS为低电平时传输左声道数据,高电平时传输右声道数据。数据在SCK的上升沿锁存,MSB先传输。

但这里有个容易搞混的地方:数据相对于WS的偏移位置。常见的格式有三种:

格式 数据与WS关系 典型应用
标准I2S(飞利浦格式) 数据在WS变化后的第二个SCK上升沿开始 WM8960、CS43L22等主流Codec
左对齐(Left Justified) 数据在WS变化后的第一个SCK上升沿开始 部分老式DAC
右对齐(Right Justified) 数据在SCK周期的最后几位开始 较少见,兼容性差

WM8960默认使用标准I2S格式。我建议你也用这个格式,兼容性最好。STM32的I2S外设支持这三种格式,咱们选标准I2S就行。

关键配置:WM8960的I2S格式通过寄存器 IFACE_CTRL1 (0x04) 的 FORMAT[1:0] 位设置。00 为标准I2S格式。STM32侧通过 I2S_CFG 寄存器的 I2SSTD[1:0] 位选择,00 对应标准I2S。

4.3 主时钟MCLK:被忽视的隐形杀手

很多初学者会忽略MCLK。我刚开始做项目时也犯过这个错——只接了SCK、WS和SD三根线,结果WM8960死活不出声。

WM8960内部有一个数字信号处理引擎,它需要MCLK来产生内部时钟。MCLK的频率通常是采样率的整数倍,常见的有256fs、384fs、512fs等。fs就是采样率。

举个例子:采样率44.1kHz,MCLK用256fs,那么MCLK = 44.1k × 256 = 11.2896MHz。

WM8960支持多种MCLK分频比,通过寄存器 CLOCKING1 (0x01) 的 MCLKDIV[2:0] 位设置。STM32的I2S外设可以输出MCLK,也可以由外部晶振提供。我个人习惯用STM32的I2S外设直接输出MCLK,省一个晶振。

注意:MCLK频率必须与采样率匹配。如果MCLK频率不对,WM8960内部锁相环无法锁定,音频会失真或完全无声。我曾经遇到过MCLK频率偏差超过1%导致声音变调的情况,排查了整整一个下午。

4.4 STM32 I2S外设配置实战

好,理论说完了,咱们动手配置。以STM32F407为例,I2S外设可以复用在SPI引脚上。我用的引脚分配如下:

I2S信号 STM32引脚 对应SPI引脚
SCK PB13 SPI2_SCK
WS PB12 SPI2_NSS
SD_OUT PB15 SPI2_MOSI
SD_IN PB14 SPI2_MISO
MCLK PC6 I2S2_MCK

配置步骤分为四步:

  1. 使能时钟:GPIOB、GPIOC、SPI2的时钟都要打开
  2. 配置GPIO:将PB12~PB15和PC6设置为复用功能
  3. 配置I2S外设:设置模式、数据格式、时钟极性等
  4. 使能I2S:启动传输

下面是HAL库的配置代码:

// I2S2初始化结构体
I2S_HandleTypeDef hi2s2;

void MX_I2S2_Init(void)
{
    hi2s2.Instance = SPI2;
    hi2s2.Init.Mode = I2S_MODE_MASTER_TX;  // 主机发送模式
    hi2s2.Init.Standard = I2S_STANDARD_PHILIPS;  // 标准I2S格式
    hi2s2.Init.DataFormat = I2S_DATAFORMAT_16B;  // 16位数据
    hi2s2.Init.MCLKOutput = I2S_MCLKOUTPUT_ENABLE;  // 使能MCLK输出
    hi2s2.Init.AudioFreq = I2S_AUDIOFREQ_44K;  // 44.1kHz采样率
    hi2s2.Init.CPOL = I2S_CPOL_LOW;  // 时钟低电平空闲
    hi2s2.Init.ClockSource = I2S_CLOCK_PLL;  // 使用PLL作为时钟源
    hi2s2.Init.FirstBit = I2S_FIRSTBIT_MSB;  // MSB先传输

    if (HAL_I2S_Init(&hi2s2) != HAL_OK)
    {
        Error_Handler();
    }
}

这里有几个参数要特别注意:

  • Mode:STM32作为主机,WM8960作为从机。所以STM32产生SCK和WS时钟。
  • AudioFreq:HAL库提供了 I2S_AUDIOFREQ_44K、I2S_AUDIOFREQ_48K 等宏定义。但实际频率取决于系统时钟配置。我建议你配置完后用示波器量一下WS的频率,确认是否准确。
  • ClockSource:STM32的I2S时钟可以从PLLI2S产生,也可以从系统时钟分频。PLLI2S可以产生更精确的音频时钟,特别是44.1kHz这种非整数倍频率。

经验之谈:如果你发现声音播放速度不对,十有八九是采样率配置错了。用示波器量WS引脚,频率应该等于你设置的采样率。比如44.1kHz,WS周期大约是22.7微秒。

4.5 时钟树配置:PLLI2S的精确计算

STM32F407的I2S时钟源可以选择PLLI2S。PLLI2S的配置公式如下:

I2S_xCLK = (HSE / PLL_M) × PLLI2S_N / PLLI2S_R

其中HSE是外部高速晶振,通常是8MHz或25MHz。PLL_M、PLLI2S_N、PLLI2S_R是分频系数。

以8MHz HSE、44.1kHz采样率、16位数据为例:

  • 目标I2S时钟 = 44.1k × 256 = 11.2896MHz
  • PLL_M = 8(HSE不分频)
  • PLLI2S_N = 271
  • PLLI2S_R = 3
  • 计算结果:8M / 8 × 271 / 3 = 11.2917MHz
  • 误差:0.02%,完全可接受

HAL库的配置代码:

// 配置PLLI2S
RCC_PeriphCLKInitTypeDef PeriphClkInitStruct = {0};
PeriphClkInitStruct.PeriphClockSelection = RCC_PERIPHCLK_I2S;
PeriphClkInitStruct.PLLI2S.PLLI2SN = 271;
PeriphClkInitStruct.PLLI2S.PLLI2SR = 3;
HAL_RCCEx_PeriphCLKConfig(&PeriphClkInitStruct);

嗯,这里要注意:PLLI2S_N的取值范围是50~432,PLLI2S_R的取值范围是2~7。如果算出来的值不在范围内,需要调整PLL_M或换一个分频组合。

4.6 I2S与WM8960的时钟同步

配置好STM32的I2S外设后,还需要让WM8960的时钟设置与之匹配。WM8960的时钟路径如下:

WM8960时钟路径与I2S同步关系 MCLK输入 MCLKDIV分频 内部时钟域 BCLK(SCK)输入 LRCLK(WS)输入 数字音频接口 ADC/DAC处理 关键配置 • MCLKDIV 根据采样率 选择分频比 • BCLK与LRCLK 由STM32产生 • 采样率必须 与MCLK匹配 • 数据格式 双方一致

WM8960的寄存器配置如下:

// 配置WM8960时钟
// 寄存器 CLOCKING1 (0x01)
// MCLKDIV[2:0] = 000 (MCLK不分频,直接使用)
uint16_t clocking1_val = 0x0000;
wm8960_write_reg(0x01, clocking1_val);

// 配置I2S接口格式
// 寄存器 IFACE_CTRL1 (0x04)
// FORMAT[1:0] = 00 (标准I2S格式)
// WL[1:0] = 00 (16位数据)
uint16_t iface_ctrl1_val = 0x0000;
wm8960_write_reg(0x04, iface_ctrl1_val);

这里有个细节:WM8960的MCLKDIV分频比需要根据MCLK频率和采样率来选择。如果MCLK = 256fs,分频比选1(不分频)。如果MCLK = 512fs,分频比选2。具体对应关系在WM8960数据手册的Table 16中有详细说明。

4.7 数据发送与接收:DMA是王道

配置好I2S外设后,就可以传输音频数据了。有两种方式:轮询和DMA。

轮询方式简单,但会占用CPU。对于音频应用,我强烈建议用DMA。原因很简单:音频数据是连续的,用轮询方式CPU啥也干不了,只能不停地往I2S数据寄存器里塞数据。

DMA方式配置如下:

// 启动I2S2的DMA发送
// 使用DMA1 Stream4,通道0
// 数据从audio_buffer发送到I2S2_DR寄存器
HAL_I2S_Transmit_DMA(&hi2s2, (uint16_t*)audio_buffer, buffer_size);

// DMA传输完成回调
void HAL_I2S_TxCpltCallback(I2S_HandleTypeDef *hi2s)
{
    if (hi2s->Instance == SPI2)
    {
        // 填充下一帧数据
        // 或者切换缓冲区(双缓冲模式)
        audio_buffer_ready = 1;
    }
}

双缓冲模式是音频处理的标配。一个缓冲区在DMA传输,另一个缓冲区由CPU填充数据。这样就不会出现数据断流的情况。

我的习惯:缓冲区大小设置为采样率的一半。比如44.1kHz采样率,缓冲区大小设为22050个采样点(约500ms数据)。这样即使CPU偶尔被其他中断打断,也不会导致音频卡顿。

4.8 常见问题与调试方法

配置I2S时,你可能会遇到以下问题:

现象 可能原因 排查方法
完全无声 MCLK未输出或频率错误 用示波器量MCLK引脚,确认频率
声音嘶哑 数据格式不匹配(位宽或对齐方式) 检查WM8960和STM32的I2S格式设置
左右声道颠倒 WS极性配置错误 检查CPOL位设置,尝试取反
声音有杂音 时钟抖动或电源噪声 检查电源滤波,增加去耦电容
播放速度异常 采样率配置错误 用示波器量WS频率,确认等于采样率

我曾经遇到过一个特别隐蔽的问题:声音播放正常,但每隔几秒会出现一次爆音。排查了三天,最后发现是DMA中断优先级设置太低,被其他高优先级中断抢占了导致数据断流。把DMA中断优先级调到最高后问题解决。

避坑指南:调试I2S时,不要只依赖逻辑分析仪。逻辑分析仪只能看到数字信号,但MCLK的模拟特性(如抖动、幅度)会影响Codec的工作。有条件的话,用示波器看MCLK和BCLK的波形质量。

4.9 本章小结

I2S配置是整个音频系统的骨架。骨架搭好了,后面的音频处理才能顺利进行。核心要点就三个:时钟频率要对、数据格式要匹配、DMA传输要流畅。

配置时,我建议你按这个顺序来:先确认MCLK频率,再确认BCLK和WS频率,最后检查数据格式。每一步都用示波器验证,不要凭感觉。