第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到函数调用,一目了然:
这张图把整个配置流程串起来了。你从CubeMX开始,一路走到API调用,每一步都有对应的参数和函数。调试的时候,按这个链路倒着查——先看回调有没有触发,再看DMA配置对不对,最后检查时钟频率。
4.8 本章小结
I2S配置说难不难,说简单也不简单。核心就三点:
- 时钟要算准——PLLI2S的分频系数必须精确,误差控制在0.1%以内
- 参数要匹配——主从模式、数据格式、采样率,STM32和WM8960必须一致
- 传输用DMA——别用阻塞模式,除非你只想测试