第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框图时,被时钟树绕晕了。后来我画了个简化图,才理清楚数据流。
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时,我建议注意以下几点:
- DMA流选择:不同STM32型号的DMA流映射不同,查数据手册
- 传输方向:内存到外设(播放),外设到内存(录音)
- 数据宽度:必须与I2S数据寄存器宽度一致,通常是半字(16位)
- 循环模式:音频播放必须开启,否则一次传输完就停了
- 中断使能:传输完成中断、半传输中断,用于更新缓冲区
// 以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初始化流程应该是这样的:
- 使能时钟:GPIO时钟、I2S外设时钟、DMA时钟
- 配置GPIO:将I2S引脚复用为AF功能
- 配置I2S:设置主从模式、音频标准、数据位宽、时钟分频
- 配置DMA:设置数据流、循环模式、中断
- 使能I2S:先使能DMA,再使能I2S外设
- 启动传输: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这块基本不会出大问题。