拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32+I2S驱动WM8978音频Codec:移植实战与调试经验

STM32+I2S驱动WM8978音频Codec:移植实战与调试经验

做嵌入式音频相关的项目,第一步往往就是“让单片机出声”。前阵子我把一颗 WM8978 音频 Codec 移植到了基于 STM32F407 的 HAL 库工程里,用 I2S 接口传音频数据,用 I2C 配置寄存器,从查手册、对原理图、调时钟,到最终耳机能听到歌、麦克风能录音,整个过程花了不少时间也踩了不少坑。这篇文章把整套移植过程的思路和实操记录完整写下来,适合正在用 STM32 接 WM8978、或者准备接其他 I2S Codec 的朋友参考。

先说清楚这个项目到底做了什么:STM32 通过 I2S 总线把数字音频流发给 WM8978,WM8978 内部完成 DAC 数模转换后驱动耳机或扬声器;反向的录音通路则通过 WM8978 的 ADC 把模拟麦克风信号转成数字流送回 STM32。这套驱动搞定之后,后面做语音识别前端、录音笔、数字对讲机、蓝牙音箱主机,都可以直接复用同一份底层代码,扩展性很强。

1. 项目全貌:WM8978在系统中到底扮演什么角色

1.1 选型逻辑:为什么是STM32+I2S+WM8978

有朋友问过,STM32 不是自带 DAC 吗,直接输出模拟信号不就行了?严格来说,STM32 内置 DAC 精度和驱动能力都有限,只能输出很弱的模拟信号,而且采样率、信噪比和音频应用差距很大。要真正做音频设备,几乎都要外接一颗音频 Codec 芯片。

Codec 的全称是 Coder/Decoder,作用就是把数字音频转成模拟音频(播放),或者把模拟音频转成数字音频(录音)。WM8978 是欧胜(Wolfson)推出的一颗低功耗立体声 Codec,内部集成了立体声 ADC、DAC、耳机放大器、麦克风偏置电路、混音器、音量和 EQ 控制,非常适合 MCU 类的嵌入式音频项目。一颗芯片基本涵盖了音频前端到后端的全部模拟环节,外围电路只需要少量阻容和参考电压配置。

为什么选 I2S 而不是 I2C 来传音频数据?因为 I2C 是低速控制总线,标准模式才 100kbps,传个寄存器配置没问题,但传连续的音频采样流完全不够用。I2S 本身是专门为数字音频设计的串行总线,以位时钟同步方式连续传输采样数据,配合 DMA 几乎不占 CPU 资源。所以常见的方案就是:I2S 传数据,I2C 传配置,两条总线各干各的。

选 WM8978 还有个现实原因,很多 STM32F407 开发板上已经贴好了 WM8978 和配套的耳机座、麦克风、喇叭接口,硬件不用自己画,直接拿来练手最合适。即使是你自己画板,WM8978 的封装和外围电路也不复杂,数据手册里给了完整的参考设计,照着抄就行。

1.2 整体数据通路:一块Codec的前半生

从宏观上看,WM8978 左边是模拟世界,右边是数字世界,中间是 ADC/DAC 和混音开关。播放通路的信号流向是这样:STM32 从 SD 卡或者 Flash 里读出 WAV/MP3 数据,送入 I2S 发送寄存器,通过 I2S_SD 数据线逐位发送;WM8978 收到数字流后经过数字滤波、DAC 转换,变成模拟信号,再经过内部耳放或者外部功放输出到喇叭。整个过程中,I2S 的位时钟 BCLK 和帧时钟 LRCK 由 STM32 产生,WM8978 作为从机跟随时序。

录音通路的流向刚好相反:麦克风拾取的微弱模拟信号,经过 WM8978 内部的 PGA 可编程增益放大,进入 ADC 采样量化,再经过数字滤波后打包成 I2S 帧数据,由 WM8978 输出,STM32 通过 I2S 接口接收。理解了这条主链路,后面所有寄存器配置的意图就很好猜了:哪个寄存器管电源,哪个管时钟,哪个管音频接口格式,哪个管音量,本质上都是在为这条链路各环节开关和设置参数。

2. 底层原理:把I2S和WM8978之间的约定讲清楚

2.1 I2S四根线,每一根都别接错

I2S 总线在 STM32F4 上常用的是四根线:SCK(也叫 BCLK,位时钟)、WS(也叫 LRCK,左右声道帧时钟)、SD(串行数据线)和 MCK(主时钟,可选)。BCLK 的频率等于采样率乘上每个声道的位数,再乘上声道数。举个例子,标准 I2S 模式、16 位数据、双声道,BCLK 就是fs * 16 * 2 = 32 * fs,但实际传输时多数 Codec 每声道会占 32 个位时钟槽位,所以 BCLK 通常是64 * fs。

LRCK 用来区分左右声道,频率等于采样率。比如采样率 48kHz,LRCK 就是 48kHz 的方波。标准飞利浦 I2S 协议里,LRCK 低电平传输左声道,高电平传输右声道,而且数据要比 LRCK 沿晚一个 BCLK 周期开始,最高位先发。移植驱动时不需要从零实现这个时序,STM32 的 SPI 外设复用成 I2S 模式之后,硬件会自动处理位对齐和帧同步,你要做的只是把采样数据按顺序往发送寄存器里丢。

MCK 这根线最容易被忽略。WM8978 内部需要主时钟来驱动数字滤波器和 Σ-Δ 调制器,这个主时钟可以来自外部有源晶振,也可以由 STM32 的 I2S_MCK 引脚直接输出。绝大多数开发板采用的是后者,所以 CubeMX 里需要把 I2S 的 Master Clock Output 使能。MCK 频率一般设计为采样率的 256 倍,也就是256 * fs,但实际工程里因为 PLL 分频取整问题,这个值往往不是刚好等于理论值,后面我会专门讲这个误差怎么处理。

2.2 WM8978是“两个世界之间的翻译官”

WM8978 这类 Codec 芯片的内部结构,一句话概括就是:一个数字音频接口,一组模拟输入输出通道,中间夹着 ADC、DAC、混音器和各种增益/音量控制模块。数字音频接口负责跟 STM32 的 I2S 对接,支持标准 I2S、左对齐、右对齐和 DSP 格式,数据位宽支持 16/20/24/32 位。初始化时,STM32 和 WM8978 两端的音频接口格式必须完全一致,否则采样数据错位,声音会变成刺耳的爆音。

模拟部分比较常用的是三路输出:HPOUT_L/HPOUT_R 给耳机,LOUT1/ROUT1 和 LOUT2/ROUT2 可以接功放或者 Line Out。输入部分有两路麦克风输入和一个辅助输入。实际项目里,要看开发板原理图确认你的耳机和麦克风具体接到了哪些引脚,然后去配置对应的电源管理寄存器和混音开关,只开你用到的那几个通道就够了,不用的通道关掉还能省电。

音量控制也是 WM8978 的一个强项。耳机输出和 DAC 输出都有独立的数字音量寄存器,从 +6dB 到 -73dB 可调,步进 0.5dB。移植驱动时很多人会忽略音量寄存器的默认值,结果是初始化完喇叭没声音,其实不是没信号,而是默认音量为静音状态。这个坑我在调配时踩过,后面问题排查部分会细说。

2.3 控制接口与寄存器读写机制

WM8978 的控制接口支持两线 I2C 和三线 SPI 两种模式,由芯片引脚决定,开发板上几乎都是 I2C 模式。I2C 地址有讲究:WM8978 的 7 位从机地址通常是0x1A,但在 STM32 HAL 库函数HAL_I2C_Master_Transmit()中传地址参数时,要注意 HAL 已经帮你去掉了 R/W 位,所以直接填0x1A而不是0x34。不少新手在这里卡很久,逻辑分析仪一抓全是 NACK,其实地址就错了。

写寄存器的格式是三字节:第一个字节是寄存器地址,后面两个字节是 16 位数据,高位在前。比如要写寄存器 R2,数据是0x01BF,那么发送序列就是0x02 0x01 0xBF。封装一个基础写寄存器函数之后,所有上层配置都用这个函数操作。

#define WM8978_I2C_ADDR 0x1A static void WM8978_WriteReg(uint8_t reg, uint16_t val) { uint8_t buf[3]; buf[0] = reg; buf[1] = (val >> 8) & 0xFF; buf[2] = val & 0xFF; HAL_I2C_Master_Transmit(&hi2c1, WM8978_I2C_ADDR, buf, 3, 100); }

读寄存器稍微麻烦一点,需要先发寄存器地址,再发起读操作,同样注意 HAL 里的地址参数填 7 位地址。不过实际移植过程中,绝大部分场景只需要写寄存器,读寄存器主要是用来回读芯片 ID 验证通信链路是否正常。

3. 驱动移植实操:CubeMX配置到代码落地

3.1 硬件连接与引脚规划

我用的是一块 STM32F407ZET6 核心板加 WM8978 子板,它们之间通过排针连接。如果你也用类似的开发板,先别急着写代码,拿起原理图对照一下 I2S 和 I2C 引脚到底接到了 STM32 的哪些 IO 上。

以最常见的 F407 开发板为例,SPI2/I2S2 和 I2C1 的引脚对应关系如下:

功能STM32引脚连接对象
I2S2_SCK / BCLKPB13WM8978 BCLK
I2S2_WS / LRCKPB12WM8978 LRCK
I2S2_SDPB15WM8978 DIN / DOUT
I2S2_MCKPC6WM8978 MCLK
I2C1_SCLPB6WM8978 I2C_SCL
I2C1_SDAPB7WM8978 I2C_SDA

你的板子引脚未必完全一样,比如有些板子把 WM8978 接到了 SPI3/I2S3,有些 I2C 用的是 I2C2,这些都要以原理图为准。需要特别注意的是 I2S_SD 的方向:播放时 STM32 作为主机发送,SD 引脚是输出;录音时 WM8978 作为从机发送数据,同一个物理引脚要切换方向。好在 WM8978 在标准 I2S 从机模式下,DIN 接收 MCU 数据、DOUT 发送数据给 MCU 是分开的,如果引脚定义只连了 DIN,那录音就得改硬件。我用的这块子板把 DIN 和 DOUT 都引出来了,只是默认只用了 DIN。

3.2 CubeMX工程配置要点

CubeMX 里配置项目时,有几个地方容易出问题。首先是 RCC,外部高速晶振 HSE 要打开,因为内部 HSI 精度不够,会影响音频采样率的准确性。然后是 I2C1,模式选 I2C,速度选 Standard Mode 或者 Fast Mode 都行,WM8978 完全跟得上 400kHz。

SPI2 这一路是重点。在 CubeMX 的 Pinout 视图里找到 SPI2,模式那里不要选 "Disabled",而是选 "I2S2"。具体模式要看你的应用:

  • 只做播放:选Transmit Only Master
  • 只做录音:选Receive Only Master
  • 播放录音切换使用:CubeMX 里先按其中一个配置,后面代码里改模式,或者直接选Half-Duplex Master自己切换

音频参数方面,Standard 选 Philips 标准,Data Format 选 16 位,Audio Frequency 填 48kHz,Clock Polarity 保持 Low。Master Clock Output 这一项要看 MCK 来源,如果 WM8978 的 MCLK 是 STM32 的 PC6 提供,就必须 Enable;如果是外部晶振提供,就 Disable。

时钟树配置是整个工程最关键的一环。F407 的 I2S 时钟源来自 PLLI2S,如果 PLLI2S 配得不对,后面的 AudioFreq 再准也没用。我常用的配置是:HSE=8MHz,PLLM=8,PLLI2SN=192,PLLI2SR=2,这样 PLLI2S 输出时钟是 96MHz,I2S2 的外设时钟就是 96MHz。CubeMX 会自动帮你计算 I2S 分频系数,生成代码后你可以在HAL_I2S_MspInit()里看到 PLLI2S 的配置。

3.3 时钟计算:我想要的那个采样率怎么来

很多人移植 WM8978 失败,不是 I2C 不通,也不是初始化序列不对,而是 I2S 主时钟和采样率对不上,导致声音变调或者干脆是刺耳的噪声。这里把计算方法完整展开一次。

STM32F4 的 I2S 分频系数由 I2SDIV 和 ODD 两个参数决定,实际分频数 =2 * I2SDIV + ODD。当 I2S 作为主机并输出 MCK 时,I2S 输入时钟先经过这个分频,得到256 * fs的 MCK;如果不输出 MCK,这个分频直接得到 BCLK 的整数倍关系,具体要看 STM32 参考手册的 I2S 章节。

以我实际用的 96MHz I2S 时钟源,目标采样率 48kHz 来计算。采用标准 I2S、16 位数据格式时,每声道占 32 个 BCLK,BCLK 频率应该是64 * 48kHz = 3.072MHz。96MHz 除以 3.072MHz 等于 31.25,这不是整数,怎么办?I2S 分频寄存器只能写整数,所以取最接近的 31,即2 * 15 + 1 = 31,此时实际 BCLK = 96MHz / 31 = 3.0968MHz,实际采样率 = 3.0968MHz / 64 = 48.387kHz,和标称 48kHz 偏差约 0.8%。

0.8% 的偏差对音乐播放来说耳朵基本听不出来,但如果直接把 MCK 给 WM8978 当系统主时钟,WM8978 内部的数字滤波器可能会因为时钟频率偏移而表现不稳定。稳妥的办法是开启 WM8978 内部 PLL,让 Codec 从 MCLK 恢复出精确的内部时钟。很多开发板的例程其实没有严格计算 MCK,照样能出声,就是因为 WM8978 的 PLL 把误差吸收掉了。

如果你不想依赖 PLL 兜底,也可以改 PLLI2SR 来让分频数更理想。比如 PLLI2SR=5 时 I2S 时钟为 38.4MHz,对于 48kHz:38.4MHz / 3.072MHz = 12.5,取 12 或 13 误差都很大,反而不如 96MHz 的方案。我的建议是:先用 96MHz 这套参数把声音调通,再去优化时钟精度。

常见采样率在 96MHz 时钟源下的分频表如下,方便直接抄:

目标采样率目标BCLK(64fs)分频计算I2SDIV / ODD实际采样率偏差
48kHz3.072MHz96/3.072=31.2515 / 148.387kHz+0.8%
44.1kHz2.8224MHz96/2.8224=34.0117 / 044.118kHz+0.04%
8kHz512kHz96/0.512=187.593 / 18.021kHz+0.26%

实际代码里你不需要手动写 I2SDIV,CubeMX 会根据你填的 AudioFreq 自动计算。但你要会看生成代码里的参数,如果偏差太大,手动改 AudioFreq 或者 PLLI2SR 都可以。

3.4 WM8978初始化序列参考

WM8978 一共 48 个寄存器,但不是每个都要动。初始化一般遵循固定的顺序:软件复位、电源管理、音频接口配置、时钟配置、输出通路配置、音量配置。下面是我在工程里实测可用的一份精简初始化序列,寄存器值以 WM8978 数据手册为标准,如果你手里的板子电路有差异,需要根据原理图调整对应通道开关。

void WM8978_Init(void) { // 1. 软件复位 WM8978_WriteReg(0x00, 0x0000); // 2. 电源管理:打开 DAC、ADC、VMID 和偏置 WM8978_WriteReg(0x01, 0x0000); // 电源管理1 先全关 WM8978_WriteReg(0x02, 0x01BF); // 电源管理2:DACL/DACR/ADCL/ADCR/Bias 上电 WM8978_WriteReg(0x03, 0x006F); // 电源管理3:输出级和输入缓冲使能 // 3. 音频接口:标准I2S,16位数据 WM8978_WriteReg(0x04, 0x0000); // 4. 时钟:MCLK作为主时钟 WM8978_WriteReg(0x05, 0x0000); // 5. 关闭未用到的通路,防止串音 WM8978_WriteReg(0x06, 0x0000); WM8978_WriteReg(0x0B, 0x0000); WM8978_WriteReg(0x0D, 0x0000); // 6. 耳机音量先设到一个适中值 WM8978_WriteReg(0x09, 0x0030); // 左耳机音量,+0dB 附近 WM8978_WriteReg(0x0A, 0x0030); // 右耳机音量 // 7. DAC 通路音量软复位,避免静音 WM8978_WriteReg(0x0E, 0x0002); WM8978_WriteReg(0x0F, 0x00FF); // 左 DAC 音量最大 WM8978_WriteReg(0x10, 0x00FF); // 右 DAC 音量最大 // 8. 关闭 3D、EQ 等附加功能 WM8978_WriteReg(0x12, 0x0000); WM8978_WriteReg(0x13, 0x0000); WM8978_WriteReg(0x14, 0x0000); }

这份序列只保证核心通路能出声、能录音。实际调板子时,建议对照数据手册把每个寄存器的 bit 含义看一遍,尤其是电源管理寄存器,哪个 bit 对应哪一路放大器,开关错了声音路径就断了。寄存器 R2/R3 的数值在不同开发板上可能有差异,比如有些板子的耳机功放由 GPIO 控制,还需要额外拉高那个 GPIO。

3.5 播放与录音的收发实现

初始化搞定后,播放一首音频就很简单了。HAL 库提供了HAL_I2S_Transmit()的阻塞发送和HAL_I2S_Transmit_DMA()的非阻塞发送。实际项目基本都用 DMA,否则 CPU 会被中断打断太多,系统其他任务都没法跑。

DMA 播放的初始化在 CubeMX 里可以直接配好,I2S2_TX 的 DMA 请求默认映射到 DMA1 Stream3。发送时只需要准备一个 uint16_t 数组,里面按左、右、左、右的顺序填充采样点:

static uint16_t audio_buf[2048]; void Audio_Play(uint16_t *buf, uint32_t len) { HAL_I2S_Transmit_DMA(&hi2s2, buf, len); } void HAL_I2S_TxCpltCallback(I2S_HandleTypeDef *hi2s) { // 一帧发送完成,可以在这里补充下一帧数据 }

如果是单个固定数目的 WAV 数组,阻塞发送也行:

HAL_I2S_Transmit(&hi2s2, (uint16_t *)wav_data, wav_len / 2, HAL_MAX_DELAY);

有些 WAV 是 16 位数据,但 I2S 每个声道槽位是 32 位,会不会丢数据?不会。标准 I2S 模式下,STM32 硬件会自动把 16 位数据放到 32 位槽的最高有效位,WM8978 配置成 16 位接收格式后会从高位取数据,低 16 位自动忽略。你只需要保证数据缓冲区是 16 位对齐即可。

录音实现和播放类似,差别在于调用HAL_I2S_Receive_DMA(),DMA 把 I2S 接收到的数据搬到内存数组。但要注意 F4 的 I2S 是半双工模式,同一时刻只能发送或者接收。想要边放边录(比如做回音消除),F4 单路 I2S 是做不到的,需要用两路 I2S 配合或者换带 SAI 接口的芯片。做普通录音笔或者语音唤醒项目,先录后放完全够用。

4. 踩坑记录与调试方法:声音不对时别慌

4.1 常见问题速查表

移植过程中我遇到和帮朋友排查过的问题,整理成下面这张表,基本涵盖了大多数 WM8978 驱动失败场景。

现象可能原因解决办法
I2C 通信失败,读取 ID 错误I2C 地址填成了 8 位地址HAL 函数传 7 位地址 0x1A
喇叭完全无声电源管理寄存器没开对应通道检查 R2/R3 的 DAC/耳放使能位
有数据但音量极小音量寄存器默认是静音或最低将 R9/R10、R0F/R10 音量调到合适值
声音变调、播放速度快I2S 采样率配置与实际音频不一致核对 AudioFreq 和 WAV 文件采样率
声音夹杂爆音时钟偏差过大或者 GND 连接不良检查 PLLI2S 配置,分频尽量取整
播放正常,录音全零I2S 工作模式是只发送切到接收模式,并检查 I2S_SD 方向
初始化后立刻发热模拟电源配置异常或短路检查 WM8978 供电和电路焊接

4.2 用示波器和正弦波快速定位问题

调音频最怕的问题就是“没有声音”,因为你在听筒里什么都听不到,根本没法判断是哪一环断了。我的经验是:不要用音乐去测试初始化结果,先用一段单频正弦波。

在电脑上用 Python 或者直接生成一个 1kHz、持续 1 秒的正弦波 WAV 文件,转成 C 数组放到工程里播放。如果 1kHz 正弦波能听到纯音,说明 DAC 通路是通的;如果声音发闷或者带杂音,说明采样率或者滤波配置有问题。用音乐测试很难听出这种细微差别,正弦波就像音频工程师的万用表,能快速定位是哪一环节出错。

示波器在这个阶段也非常有用。重点测几组波形:I2C 的 SCL/SDA 有没有 ACK,I2S 的 BCLK 频率对不对,LRCK 是不是等于目标采样率,SD 引脚有没有数据脉冲。只要数据线有波形,WM8978 理论上就能收到数据,剩下的问题基本都在寄存器配置和模拟通路上。

4.3 几个调音频的通用心得

第一,每次只改一个变量。音频链路太长,电源、时钟、格式、音量、通路开关,任何一环都影响最终结果。很多人习惯一次改十几个寄存器然后上电试听,出问题根本不知道是哪一步引起的。我一般是初始化后先固定音量,然后逐个通路验证,确认 DAC 出声后再去开麦克风通路,每一步都确保稳定才继续下一步。

第二,先缩小链路范围。有时候耳机没声音,可以先在 WM8978 的模拟输入端手动灌一个信号,看耳机能不能听到直通的声音。如果直通有声音而 DAC 没声音,问题就锁定在数字侧;如果直通都没声音,问题在模拟侧或者后端功放。

第三,多看数据手册而不是只抄例程。网上 WM8978 的例程很多,但每个开发板的电路设计不一样,例程里的寄存器值不一定适用你的硬件。认真读一遍数据手册里每个寄存器的说明,之后遇到问题你会有自己的判断力,而不是陷入“照着敲代码还是不出声”的困境。

最后分享一个实际经验:调音频环境时,先用耳机听,别急着接喇叭功放。耳机阻抗高、驱动电流小,容易调试;喇叭功放一接,满板子电流噪声和地弹会把真实问题盖住。等耳机通路完全正常了,再去接功放和喇叭,这时候如果还有底噪,重点查电源纹波和地线布局。音频调试是个慢活,但只要你把数据链路和时钟摸透了,WM8978 这颗芯片其实非常稳定,很适合作为嵌入式音频的入门和主力方案。

返回列表