第4章:HAL库I2S驱动配置——从CubeMX到函数调用的完整链路

各位同学,咱们今天聊I2S。说实话,I2S这个外设看起来简单,不就是个音频专用的串行接口嘛。但我在项目里栽过跟头——有一次板子打回来,WM8960死活不出声,折腾了两天才发现是I2S的主从模式配反了。所以这一章,咱们把I2S的配置掰开揉碎了讲清楚。

4.1 I2S是什么?为什么音频要用它?

I2S(Inter-IC Sound)是飞利浦在1986年制定的音频总线标准。它把时钟和数据分开,专门用来传输PCM音频数据。你想想看,如果用SPI传音频,帧同步得自己拼,麻烦得很。I2S直接帮你把左右声道分好了。

一个标准的I2S接口有3根线:

  • SCK(连续串行时钟)—— 也叫位时钟,每个bit一个脉冲
  • WS(字选择)—— 左右声道切换,高电平左声道,低电平右声道
  • SD(串行数据)—— 音频数据线,可以是输入或输出

有些芯片还会多一根MCLK(主时钟),WM8960就需要MCLK。这个后面会细说。

核心要点:I2S的WS频率 = 采样率 × 声道数 × 位深。比如44.1kHz、16位立体声,WS就是44.1kHz,SCK = 44.1k × 2 × 16 = 1.4112MHz。这个公式你记牢了,后面调试时钟的时候会反复用到。

4.2 STM32CubeMX配置I2S——手把手操作

我个人习惯先把时钟树搞定,再配外设。因为I2S对时钟精度要求高,尤其是MCLK,必须算准。

4.2.1 时钟树准备

打开CubeMX,进入Clock Configuration。以STM32F407为例,我一般这样配:

  • HSE:8MHz外部晶振
  • PLL:168MHz主频
  • I2S时钟源:选择PLLI2S,输出频率要能整除目标采样率

举个例子,WM8960需要MCLK = 256 × 采样率。采样率44.1kHz时,MCLK = 11.2896MHz。PLLI2S的输出要能精确产生这个频率。STM32的PLLI2S有专门的N、R分频系数,你得手动算一下。

我的经验:别完全相信CubeMX的自动计算。它有时候会给你一个近似值,但音频Codec对时钟抖动很敏感。我习惯在Excel里拉个表,把N、R的所有组合算一遍,挑误差最小的那个。

4.2.2 I2S外设参数配置

在Pinout & Configuration里找到I2S外设(通常是SPI复用为I2S)。双击打开,你会看到这些选项:

参数 可选值 我的推荐
Mode Master Transmit / Master Receive /
Slave Transmit / Slave Receive /
Full-Duplex Master / Full-Duplex Slave
Full-Duplex Master(STM32做主控)
Standard I2S Philips / MSB Justified / LSB Justified / PCM I2S Philips(WM8960默认模式)
Data Format 16-bit / 16-bit extended / 24-bit / 32-bit 16-bit(与WM8960匹配)
Audio Frequency 8k / 11.025k / 16k / 22.05k / 44.1k / 48k / 96k 44.1kHz(CD音质)
Clock Polarity Low / High Low(CK低电平空闲)
MCLK Output Enable / Disable Enable(WM8960需要MCLK)

这里有个坑——Data Format。STM32的I2S数据格式指的是每帧数据的有效位数,不是存储宽度。选16-bit时,每个声道只传输16位数据,但硬件会帮你对齐到32位帧。WM8960那边也要配成16位,两边必须一致。

我曾经踩过的坑:有一次我把STM32配成24位,WM8960配成16位。结果声音全是噪声,因为数据位宽不匹配,Codec把填充位当有效数据解了。记住:发送端和接收端的Data Format必须一模一样。

4.2.3 DMA配置(推荐)

I2S用轮询方式发送数据会占用大量CPU,尤其是高采样率时。我建议用DMA:

  • 添加DMA通道:I2Sx_TX和I2Sx_RX
  • 方向:Memory to Peripheral(发送)/ Peripheral to Memory(接收)
  • 模式:Circular(循环模式,适合音频流)
  • 数据宽度:Half Word(16位)

配好之后,CubeMX会自动生成DMA初始化代码。你只需要在main里调用HAL_I2S_Transmit_DMA就行了。

4.3 主模式 vs 从模式——谁产生时钟?

这个问题我在课上经常被问到。说白了:

  • 主模式:STM32产生SCK、WS和MCLK,WM8960被动接收
  • 从模式:WM8960产生时钟,STM32跟着它的节奏走

大部分场景下,STM32做主模式。因为WM8960的时钟产生电路不如STM32的PLL灵活。但有一种情况例外——如果你用USB音频,PC端会控制采样率,这时候WM8960做主模式更合适。

我的建议:新手先做主模式。主模式调试简单,时钟出问题一眼就能看出来。从模式要等Codec初始化完成才能开始传输,时序要求更严格。

4.4 数据格式详解——16/24/32位怎么选?

STM32的I2S数据格式有4种,但实际常用的就3种:

格式 帧长度 有效数据位 典型应用
16-bit 32位(16位数据 + 16位填充) 16位 CD音质、语音
24-bit 32位(24位数据 + 8位填充) 24位 高保真音频
32-bit 32位(32位数据) 32位 专业音频设备

注意:STM32的I2S硬件总是传输32位帧。选16-bit时,它会在MSB后面补16个0。选24-bit时,补8个0。WM8960那边也要配成对应的接收格式。

我个人习惯用16-bit。原因很简单:16位的数据量小,DMA压力小,而且大部分音频源(比如WAV文件)本身就是16位。除非你做Hi-Fi设备,否则16位足够了。

4.5 音频采样率设置——时钟分频链

采样率设置的核心是PLLI2S的分频计算。STM32的时钟路径是这样的:

HSE (8MHz) → PLLI2S (N倍频) → I2SxCLK (R分频) → I2S prescaler → SCK/WS

具体公式:

  • I2SxCLK = HSE × PLLI2SN / PLLI2SR
  • SCK = I2SxCLK / ( (2 × I2SDIV) + I2SODD )
  • WS = SCK / (帧长度 × 声道数)

举个例子,目标采样率44.1kHz,16位立体声:

  • SCK = 44.1k × 2 × 16 = 1.4112MHz
  • I2SxCLK = 1.4112M × (2 × I2SDIV + I2SODD)
  • 假设I2SDIV=4,I2SODD=0,则I2SxCLK = 1.4112M × 8 = 11.2896MHz
  • PLLI2SN/PLLI2SR = 11.2896M / 8M = 1.4112
  • 取PLLI2SN=192,PLLI2SR=7,得到11.2896MHz(精确)

小技巧:CubeMX的Clock Configuration页面有个“I2S”选项卡,你输入目标采样率,它会自动计算分频系数。但记得点一下“Apply”看看实际误差。误差超过0.1%就可能出现音调偏移。

4.6 HAL_I2S_Transmit/Receive函数详解

HAL库提供了3种传输方式:阻塞、中断、DMA。咱们一个一个看。

4.6.1 HAL_I2S_Transmit(阻塞模式)

HAL_StatusTypeDef HAL_I2S_Transmit(I2S_HandleTypeDef *hi2s, 
                                    uint16_t *pData, 
                                    uint16_t Size, 
                                    uint32_t Timeout);
  • hi2s:I2S句柄,CubeMX生成的
  • pData:发送缓冲区,必须是uint16_t数组(16位数据)
  • Size:要发送的uint16_t数量,不是字节数
  • Timeout:超时时间,单位ms

这个函数会一直等到所有数据发送完才返回。我一般只在初始化阶段用,比如发送一个静音帧。正式播放音频时千万别用——它会卡死CPU。

4.6.2 HAL_I2S_Receive(阻塞模式)

HAL_StatusTypeDef HAL_I2S_Receive(I2S_HandleTypeDef *hi2s, 
                                   uint16_t *pData, 
                                   uint16_t Size, 
                                   uint32_t Timeout);

参数和Transmit一样。同样只适合调试用。

4.6.3 HAL_I2S_Transmit_DMA(DMA模式,推荐)

HAL_StatusTypeDef HAL_I2S_Transmit_DMA(I2S_HandleTypeDef *hi2s, 
                                        uint16_t *pData, 
                                        uint16_t Size);

这个函数启动DMA传输后立即返回。数据在后台自动发送,CPU可以干别的事。我项目中90%的场景都用这个。

注意:DMA传输完成后会触发回调函数:

void HAL_I2S_TxCpltCallback(I2S_HandleTypeDef *hi2s)
{
    // 在这里填充下一批数据
    // 或者翻转标志位通知主循环
}

4.6.4 HAL_I2S_Receive_DMA

HAL_StatusTypeDef HAL_I2S_Receive_DMA(I2S_HandleTypeDef *hi2s, 
                                       uint16_t *pData, 
                                       uint16_t Size);

接收DMA版本。回调函数是HAL_I2S_RxCpltCallback。我一般用双缓冲(ping-pong buffer)来避免数据覆盖。

重要提醒:DMA传输时,缓冲区地址必须是SRAM,不能是Flash或CCM。而且缓冲区要16位对齐。我见过有人把数组定义在const区,结果DMA读不到数据,静音一片。

4.7 本章知识体系总览

下面这张图把I2S配置的完整链路画出来了,从CubeMX到函数调用,一目了然:

I2S配置知识体系图 I2S配置完整链路 CubeMX时钟树配置 I2S外设参数配置 DMA通道配置 PLLI2S N/R分频 MCLK = 256 × Fs 误差 < 0.1% 主/从模式选择 数据格式匹配 采样率设置 Circular模式 Half Word宽度 双缓冲策略 MX_I2Sx_Init() HAL_I2S_MspInit() HAL_DMA_Init() HAL_I2S_Transmit_DMA() / HAL_I2S_Receive_DMA() 回调函数:TxCpltCallback / RxCpltCallback 核心原则:时钟精度 → 参数匹配 → DMA传输 → 回调处理,环环相扣

这张图把整个配置流程串起来了。你从CubeMX开始,一路走到API调用,每一步都有对应的参数和函数。调试的时候,按这个链路倒着查——先看回调有没有触发,再看DMA配置对不对,最后检查时钟频率。

4.8 本章小结

I2S配置说难不难,说简单也不简单。核心就三点:

  • 时钟要算准——PLLI2S的分频系数必须精确,误差控制在0.1%以内
  • 参数要匹配——主从模式、数据格式、采样率,STM32和WM8960必须一致
  • 传输用DMA——别用阻塞模式,除非你只想测试