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 |
配置步骤分为四步:
- 使能时钟:GPIOB、GPIOC、SPI2的时钟都要打开
- 配置GPIO:将PB12~PB15和PC6设置为复用功能
- 配置I2S外设:设置模式、数据格式、时钟极性等
- 使能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的寄存器配置如下:
// 配置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频率,最后检查数据格式。每一步都用示波器验证,不要凭感觉。