第4章 I2S驱动配置:STM32的I2S外设详解

好,咱们进入正题。I2S这个外设,说白了就是STM32和音频Codec之间的“嘴巴”。你想想看,WM8960处理的是模拟信号,STM32玩的是数字音频流,这两者怎么对话?全靠I2S总线来传。

我个人习惯把I2S理解成一个“音频快递员”。它不关心你传的是什么音乐,只负责把PCM数据按时按点地送到对面。但这位快递员脾气挺大——时钟、帧同步、数据线,一个都不能乱。

核心要点:I2S配置的成败,直接决定了音频能不能响、音质好不好。我见过太多项目,Codec初始化没问题,DMA也配了,就是I2S的时钟分频算错一位,出来的声音要么变调要么全是噪声。

4.1 STM32的I2S外设架构

STM32的I2S其实是SPI外设的一个“变种”。嗯,准确地说,是SPI在音频领域的专用模式。你打开参考手册,找到SPI章节,里面一定有个I2S模式说明。

它内部包含这几个关键模块:

  • 时钟发生器:通过PLL或系统时钟分频,产生所需的位时钟(BCLK)和主时钟(MCLK)
  • 移位寄存器:负责把并行数据转成串行,按位发送或接收
  • 帧同步控制器:生成左右声道选择信号(LRCK/WS)
  • FIFO/数据寄存器:暂存待发送或已接收的数据

我记得第一次看I2S框图时,被时钟树绕晕了。后来我画了个简化图,才理清楚数据流。

STM32 I2S 数据流与时钟路径 DMA / 内存 音频数据缓冲区 I2S 外设 数据寄存器 移位寄存器 时钟/帧同步控制 WM8960 音频 Codec DMA 请求 I2S 总线 时钟发生器 分频配置 MCLK / BCLK 数据流向说明: 1. DMA 从内存搬运音频数据到 I2S 数据寄存器 2. I2S 移位寄存器按位时钟将数据串行发送到 WM8960 3. 时钟发生器提供 BCLK、LRCK 和可选的 MCLK

4.2 主从模式选择

I2S通信里,主从模式是个绕不开的话题。谁产生时钟,谁就是主设备。在咱们这个课程里,我建议让STM32做主设备,WM8960做从设备。

为什么?因为STM32的时钟源更灵活,我们可以精确控制采样率。WM8960虽然也能做主,但它的PLL配置相对复杂,调试起来更费劲。

模式 时钟产生者 适用场景 注意事项
STM32 主 / WM8960 从 STM32 产生 BCLK、LRCK、MCLK 标准音频播放/录音 时钟精度取决于STM32的PLL配置
STM32 从 / WM8960 主 WM8960 产生 BCLK、LRCK 需要外部时钟同步的场景 WM8960需要外部晶振或PLL锁定
双主模式 双方各自产生时钟 极少使用,容易冲突 不推荐,我踩过这个坑

我的经验:在STM32F4系列上,我习惯用I2S的主模式。配置时记得把I2S的时钟源选为PLLI2S,而不是系统时钟直出。PLLI2S可以精确分频出标准音频采样率,比如44.1kHz、48kHz。

4.3 音频参数配置

音频参数说白了就三个:采样率、位深、声道数。这三个参数决定了I2S的时钟频率。

计算公式很简单:

BCLK = 采样率 × 位深 × 声道数 × 2(如果是I2S标准格式,每声道固定32位时钟周期)

举例:44.1kHz × 16bit × 2声道 = 1.4112 MHz(实际BCLK)
但I2S标准要求每声道32个BCLK周期,所以:
BCLK = 44.1kHz × 32 × 2 = 2.8224 MHz

嗯,这里要注意。STM32的I2S外设支持多种数据格式:

  • I2S标准格式:MSB对齐,左声道在LRCK低电平时传输
  • MSB对齐格式:数据从MSB开始,但帧同步时序略有不同
  • LSB对齐格式:数据从LSB开始,用得少
  • PCM格式:用于DSP或蓝牙音频,帧同步信号不同

我个人建议用I2S标准格式,兼容性最好。WM8960的数据手册里也是默认推荐这个格式。

4.3.1 采样率配置

采样率配置是I2S初始化里最容易出错的地方。STM32的I2S时钟分频器有两个参数:

  • I2SDIV:分频系数,范围2~255
  • ODD:奇数分频使能,0或1

实际输出频率计算公式:

I2SxCLK = 主时钟源频率 / [(I2SDIV + ODD) × 2]

举例:如果PLLI2S输出为 98.304 MHz,要得到 48kHz 采样率:
BCLK = 48kHz × 32 × 2 = 3.072 MHz
I2SDIV + ODD = 98.304 MHz / (3.072 MHz × 2) = 16
所以 I2SDIV = 16, ODD = 0

曾经踩过的坑:我刚开始做音频项目时,直接用系统时钟HSE分频,结果算出来的采样率总是偏一点。播放音乐时音调变高,人声像“唐老鸭”。后来才发现,必须用PLLI2S专用时钟,才能得到精确的音频频率。

4.3.2 位深配置

WM8960支持16位、20位、24位、32位数据宽度。但STM32的I2S外设要注意:

数据位宽 I2S配置位 每声道BCLK周期 实际传输效率
16位 CHLEN=0, DATLEN=00 32 50%(浪费16个时钟周期)
24位 CHLEN=0, DATLEN=10 32 75%(浪费8个时钟周期)
32位 CHLEN=1, DATLEN=11 32 100%(完全利用)

你想想看,16位数据用32位时钟周期传,是不是有点浪费?但这就是I2S标准的规定。好在WM8960内部会自动处理这些填充位,我们不用操心。

4.4 DMA传输设置

音频数据量大,用CPU轮询发送肯定不行。DMA是必须的。STM32的I2S支持DMA请求,每次数据寄存器空或满时触发。

配置DMA时,我建议注意以下几点:

  1. DMA流选择:不同STM32型号的DMA流映射不同,查数据手册
  2. 传输方向:内存到外设(播放),外设到内存(录音)
  3. 数据宽度:必须与I2S数据寄存器宽度一致,通常是半字(16位)
  4. 循环模式:音频播放必须开启,否则一次传输完就停了
  5. 中断使能:传输完成中断、半传输中断,用于更新缓冲区
// 以STM32F407为例,I2S3的DMA配置片段
void I2S3_DMA_Init(void)
{
    DMA_InitTypeDef DMA_InitStructure;

    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1, ENABLE);

    DMA_DeInit(DMA1_Stream7);
    DMA_InitStructure.DMA_Channel = DMA_Channel_0;          // I2S3_TX 对应通道0
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&SPI3->DR;
    DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)audio_buffer;
    DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral;
    DMA_InitStructure.DMA_BufferSize = AUDIO_BUFFER_SIZE;
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
    DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;         // 循环模式,必须
    DMA_InitStructure.DMA_Priority = DMA_Priority_High;
    DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Enable;
    DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;
    DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
    DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
    DMA_Init(DMA1_Stream7, &DMA_InitStructure);

    // 使能DMA传输完成中断
    DMA_ITConfig(DMA1_Stream7, DMA_IT_TC, ENABLE);

    // 使能DMA流
    DMA_Cmd(DMA1_Stream7, ENABLE);
}

小技巧:DMA的FIFO模式建议开启。我遇到过不开FIFO时,数据量大的情况下偶尔丢数据。开启FIFO后,DMA会缓存几个数据再发送,稳定性好很多。

4.5 完整的I2S初始化流程

把上面这些串起来,一个完整的I2S初始化流程应该是这样的:

  1. 使能时钟:GPIO时钟、I2S外设时钟、DMA时钟
  2. 配置GPIO:将I2S引脚复用为AF功能
  3. 配置I2S:设置主从模式、音频标准、数据位宽、时钟分频
  4. 配置DMA:设置数据流、循环模式、中断
  5. 使能I2S:先使能DMA,再使能I2S外设
  6. 启动传输:DMA开始搬运数据,I2S开始发送
// I2S初始化函数示例
void I2S3_Init(uint32_t audio_freq)
{
    I2S_InitTypeDef I2S_InitStructure;

    // 1. GPIO配置(略,参考上一章)
    // 2. I2S配置
    I2S_InitStructure.I2S_Mode = I2S_Mode_MasterTx;        // 主模式发送
    I2S_InitStructure.I2S_Standard = I2S_Standard_Phillips; // I2S标准格式
    I2S_InitStructure.I2S_DataFormat = I2S_DataFormat_16b;  // 16位数据
    I2S_InitStructure.I2S_MCLKOutput = I2S_MCLKOutput_Enable; // 使能MCLK输出
    I2S_InitStructure.I2S_AudioFreq = audio_freq;           // 采样率
    I2S_InitStructure.I2S_CPOL = I2S_CPOL_Low;              // 时钟极性
    I2S_Init(SPI3, &I2S_InitStructure);

    // 3. DMA配置(上面已写)
    I2S3_DMA_Init();

    // 4. 使能I2S的DMA发送请求
    SPI_I2S_DMACmd(SPI3, SPI_I2S_DMAReq_Tx, ENABLE);

    // 5. 使能I2S
    I2S_Cmd(SPI3, ENABLE);
}

重要提醒:使能顺序不能错。先使能DMA,再使能I2S。如果反过来,I2S一启动就发现数据寄存器为空,可能会发送垃圾数据。我曾经因为这个顺序问题,喇叭里传出一声刺耳的爆音,吓得赶紧断电。

好了,I2S的驱动配置就讲到这里。你可能会觉得参数很多,但核心就三个:时钟频率算对、主从模式选对、DMA循环模式开对。把这三点抓住,I2S这块基本不会出大问题。