ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32驱动CS43L22音频编解码器:I2S时序、时钟配置与DMA双缓冲实践

2026/9/11 13:55:50 拓冰建站 浏览量
STM32驱动CS43L22音频编解码器:I2S时序、时钟配置与DMA双缓冲实践 简介面向STM32初学者的I2S音频接口实例教程聚焦如何驱动CS43L22音频编解码器实现立体声数字音频的收发与播放。例程包含完整可编译的C语言源码覆盖GPIO初始化、I2S外设参数配置、DMA缓冲传输、编解码器寄存器初始化等关键环节并示范采样率、位深度、增益与电源管理等常用配置通过代码注释与调用关系可快速理解主从模式选择、帧同步信号、左右声道数据以及DMA与中断协同工作的完整流程。压缩包仅3个文件以2个C源文件加1个头文件组成整体约6KB结构精简、便于直接移植到个人工程中省去从零搭建环境的成本。已有522人学习下载适合正在学习STM32音频应用、规划音乐播放器或语音识别项目的开发者参考可结合CS43L22数据手册快速上手是轻量而完整的入门级示例。1. 这条例程的含金量不在 I2S而在 CS43L22 的时序拿到STM32例程 Tutorial 28 - I2S Audio Codec - CS43L22.7z这类压缩包多数人的第一反应是打开 main.c 找 I2S 初始化代码然后照着抄一遍发现声音出不来或者全是噪声。这不是 I2S 配错了而是 CS43L22 这颗 codec 的初始化序列比想象中更挑剔上电时序要等、寄存器要按组写、MCLK 未稳定前不能动 I2S 外设。例程里真正值钱的不是那几行HAL_I2S_Transmit而是把 codec 从硬件复位到进入正常工作状态的完整握手流程。这篇博文按我平时调试这颗芯片的顺序来讲先理清 I2S 总线上谁做主谁做从再给出一套能在标准库和 HAL 库之间平移的寄存器初始化序列最后处理 DMA 双缓冲和采样率切换这类常见需求。适合手里有探索者/战舰板、或者正在把 CS43L22 接到自绘 PCB 上的工程师也适合刚把 stm32 标准库新建工程跑通、正准备接触音频的新手。2. I2S 引脚连接与时钟树先回答 MCLK 和 SCK 是不是一回事2.1 四线还是五线决定你后面所有配置I2S 接口的 mclk 和 sck 不是同一个接口这是刚接触音频总线时最容易混淆的一点。SCK也叫 BCLK是位时钟每个时钟周期传输一个数据位频率等于采样率 × 位宽 × 通道数MCLK也叫主时钟是系统时钟codec 内部的数字滤波器和 Δ-Σ 调制器都靠它驱动频率通常是采样率的 256 倍或 512 倍。CS43L22 作为从机时这两个时钟都必须由 STM32 提供。STM32 的 I2S 外设在作为主机输出时SCK 和 LRCK即 WS字选择时钟由内部时钟分频产生MCLK 需要单独开启HAL_I2SEx_ConfigTxRx或在 CubeMX 里勾选I2S_MCLKOutput。如果你的 PCB 上只连了四根线SCK、LRCK、SD、GNDCS43L22 会因为没有 MCLK 而完全不工作——这是绝大多数“I2S 没输出”问题的根源。信号名方向STM32→CS43L22说明MCLK输出主时钟256×Fs 或 512×Fs必须存在SCK/BCLK输出位时钟64×Fs16bit×2chLRCK/WS输出字选择频率等于 FsSDIN输出串行数据送给 codecSDOUT输入来自 codec 的数据用于录音2.2 时钟树怎么配才不出爆音CS43L22 需要 MCLK 与 LRCK 保持严格的倍数关系。以 48 kHz 采样率、16 bit 双通道为例LRCK 48 kHzSCK 48k × 16 × 2 1.536 MHzMCLK 如果配置为 256 × Fs 就是 12.288 MHz。STM32 的 I2S 时钟源在 F4 系列上来自 PLLI2SF1 系列上来自 PLL3 或系统时钟分频。在 CubeMX 的 Clock Configuration 里把 PLLI2S 的 R 因子调到输出 12.288 MHz然后在 I2S 配置页选择Master Clock Output这样生成的代码会自动设置好 MCLK 分频。需要留意的坑是改采样率时必须同步改 PLLI2S 和 codec 的 MCLK 分频系数两者不匹配会出现吱吱的噪声。// 标准库中常用的 I2S 时钟初始化片段HAL 库在 MX_I2S1_Init 中对应 RCC_PLLI2SInitTypeDef PLLI2S; // 以 48kHz、16bit 为例I2S 时钟源 48k × 256 12.288 MHz PLLI2S.PLLI2SN 192; PLLI2S.PLLI2SR 2; // 192 / 2 96 MHz再经 I2S 分频得到 12.288M RCC_PLLI2SConfig(PLLI2S); // 使能 PLLI2S 时钟 RCC_PLLI2SCmd(ENABLE); while (RCC_GetFlagStatus(RCC_FLAG_PLLI2SRDY) RESET) {}参数说明PLLI2SN和PLLI2SR的具体值取决于系统主频。上例假设外部晶振为 8 MHz主 PLL 已通过倍频得到 96 MHz 系统时钟PLLI2S 的 VCO 输入频率越高抖动越小。改代码时优先保持 PLLI2SN 在 192~432 之间通过 R 因子调整输出频率避免使用过小的分频系数导致时钟抖动劣化音质。2.3 接线顺序与电平匹配CS43L22 是单电源供电数字引脚兼容 1.8V~3.3V 逻辑STM32 的 3.3V 输出可以直接连接。常见做法是把 STM32 的 I2S 引脚配置为推挽复用输出注意 SDIN 和 SDOUT 不要接反——有一部分开发板的 codec 是挂在 SAI 或 SPI 复用引脚上的原理图里标注的 PB12/PB13/PB14/PB15 可能是 SPI2 的 MISO/MOSI/SCK/NSS需要确认复用功能选的是GPIO_AF_SPI2而不是GPIO_AF_I2S2两者在有些型号上共用一个 AF 编号但在 F1 上完全不同。3. 用 CubeMX 生成 I2S 工程参数表比代码更值得抄3.1 从零配置到生成初始代码打开 CubeMX选好 MCU 型号后依次做以下操作将 CS43L22 的 I2S 引脚对应的外设设置为 I2S2 或 I2S3 全双工主机模式在 Parameter Settings 里按标准 Philips 格式配置。关键参数如下参数项推荐值说明StandardI2S StandardPhilips 格式LRCK 低电平为左声道下降沿对齐数据Data Format16-bitCS43L22 默认支持 16~24 bit例程中多用 16 bitClock PolarityLow数据在 SCK 上升沿稳定与 CS43L22 的数据手册时序图对应Master Clock OutputEnable开启 MCLK 输出频率在 Audio Frequency 中设定Audio Frequency48 kHz由时钟树推导改动后 CubeMX 会重新计算分频系数DMA SettingsTX 与 RX 均开启双缓冲播放和录音都需要见第 4 章生成代码后打开i2s.c检查hsai还是hi2s的句柄命名——CubeMX 不同版本对 I2S 外设的命名不统一有些版本将 I2S 归入 SAI 模块导致 HAL 函数名变成HAL_SAI_Transmit_DMA而非HAL_I2S_Transmit_DMA。如果发现命名不同不要强行改函数名正确做法是在stm32f4xx_hal_conf.h中确认HAL_I2S_MODULE_ENABLED是否被宏定义屏蔽。3.2 生成后的代码必须手动补三件事CubeMX 生成的MX_I2S2_Init只配置了外设寄存器还需要手动补充以下内容第一使能 MCLK 输出。在MX_I2S2_Init中CubeMX 不会自动写I2S2-I2SPR | I2S_PR_OSR这类 MCLK 使能位需要调用// 在 MX_I2S2_Init 末尾添加显式打开 MCLK 输出 hi2s2.Init.MCLKOutput I2S_MCLKOUTPUT_ENABLE; if (HAL_I2S_Init(hi2s2) ! HAL_OK) { Error_Handler(); }第二配置 DMA 句柄与中断。CubeMX 生成的 DMA 配置默认优先级为 Low音频流在系统负载高时会出现欠载underrun把 DMA 优先级提到 High并使能 DMA 传输完成中断// 播放方向 DMA内存到外设循环模式 hdma_i2s2_tx.Instance DMA1_Stream4; hdma_i2s2_tx.Init.Channel DMA_CHANNEL_0; hdma_i2s2_tx.Init.Direction DMA_MEMORY_TO_PERIPHERAL; hdma_i2s2_tx.Init.PeriphInc DMA_PINC_DISABLE; hdma_i2s2_tx.Init.MemInc DMA_MINC_ENABLE; hdma_i2s2_tx.Init.PeriphDataAlignment DMA_PDATAALIGN_HALFWORD; hdma_i2s2_tx.Init.MemDataAlignment DMA_MDATAALIGN_HALFWORD; hdma_i2s2_tx.Init.Mode DMA_CIRCULAR; hdma_i2s2_tx.Init.Priority DMA_PRIORITY_HIGH;参数说明PDATAALIGN_HALFWORD和MDATAALIGN_HALFWORD对应 16 bit 音频采样点如果需要 24 bit 数据则改为 WORD 对齐但 CS43L22 在 I2S 模式下 24 bit 数据实际传输时左对齐到高位DMA 缓冲中需要先把数据左移 8 位否则低 8 位噪声会进入 DAC。第三把 I2S 时钟源从 PLLI2S 切换为系统时钟或外部 PLL。F4 系列上RCC_I2SCLKConfig(RCC_I2S2CLKSOURCE_PLLI2S)这行代码由 CubeMX 生成但如果你把主频从 168 MHz 改为 180 MHzPLLI2S 的 VCO 范围会变化必须回 CubeMX 重新计算否则 I2S 输出频率偏差超过 2% 时codec 内部的 PLL 会失锁声音发闷或变调。3.3 用逻辑分析仪验证 MCLK 是否存在这是最推荐的排查步骤。将逻辑分析仪探头接到 MCLK 引脚采样率设为 50 MHz 以上观察是否有频率约为 12.288 MHz 的方波。没有方波说明时钟树配置或 CubeMX 中 MCLKOutput 未生效有方波但幅度不足 2V说明引脚复用模式配置错误CS43L22 无法识别时钟。如果 MCLK 正常而 LRCK 无信号问题出在 I2S 标准选择——误配成 Left Justified 后 LRCK 极性反转CS43L22 的自动检测会失败表现为解码输出始终为静音。另外确认 SCK 与 MCLK 的相位关系在 Philips 标准下MCLK 的上升沿应超前于 SCK 上升沿约半个 MCLK 周期。逻辑分析仪上若看到两者完全同相可能是 STM32 的 I2S 分频器配置中I2S_PRESCALER_LOW与I2S_PRESCALER_HIGH顺序写反导致 MCLK 与 SCK 由同一级分频产生此时声音虽能输出但左右声道串扰严重。我一般会在初始化后手动读取I2S2-I2SPR寄存器打印到串口确认硬件分频生效后再调 codec。4. CS43L22 寄存器初始化从复位到出声的完整序列4.1 先读 ID再决定要不要继续CS43L22 的 I2C 从机地址是 0x948 位写地址或 0x4A7 位地址取决于AD0引脚电平。上电后必须先等 VLS 和 VLD 电源稳定——数据手册要求至少 10 ms开发板上通常由 RC 延时电路保证自绘板建议在软件里加HAL_Delay(20)。读 ID 的代码uint8_t cs43l22_reg_read(uint8_t reg) { uint8_t val 0; // 7 位地址 0x4A写寄存器地址后重发 START 读取 HAL_I2C_Mem_Read(hi2c1, 0x4A 1, reg, I2C_MEMADD_SIZE_8BIT, val, 1, 100); return val; } void cs43l22_check(void) { uint8_t id cs43l22_reg_read(0x01); // 寄存器 0x01 为 ID 寄存器 // 0xE0 是 CS43L22 的 ID0xE8 是 CS43L33 的 ID if ((id 0xF0) ! 0xE0) { // 打印错误I2C 通信失败或 AD0 电平不符 printf(CS43L22 ID error: 0x%02X\r\n, id); } }参数说明I2C_MEMADD_SIZE_8BIT表示 CS43L22 的寄存器地址是 8 位如果误用 16 位地址模式I2C 时序中会多发送一个字节的地址高字节codec 会把寄存器号解析为 0x01 与 0x00 的组合导致读回的数据全部为 0xFF。ID 寄存器读回 0xE0 后才能确认 I2C 地址、电源和时钟三者同时正常。4.2 初始化序列一组一组写不要只挑重点CS43L22 的初始化不只是把几个控制寄存器写一遍寄存器之间有依赖关系。例如 0x02 的 PDN 位电源关闭必须在配置其他寄存器之后再清除否则 codec 会忽略后续写入。下面给出经过多个板子验证的序列void cs43l22_init(void) { // 1. 先复位写 0x00 寄存器bit7 置 1 触发软件复位 cs43l22_reg_write(0x00, 0x99); // 复位同时开启自动检测 HAL_Delay(10); // 2. 配置电源控制bit61 打开模拟电源bit31 打开数字内核电源 cs43l22_reg_write(0x02, 0x01); // 暂时保持 PDN1让内部 LDO 先稳定 HAL_Delay(5); // 3. 设置 MCLK 分频MCLK12.288MHz 对应除以 128得到 96kHz 内部时钟 cs43l22_reg_write(0x04, 0x80); // MCLK/LRCK256自动检测使能 cs43l22_reg_write(0x05, 0x00); // 主模式I2S 输入 // 4. 配置 DAC 接口I2S 格式16 bitDAC 不衰减 cs43l22_reg_write(0x06, 0x04); // 接口格式I2S cs43l22_reg_write(0x07, 0x00); // DIF00 表示 16 bit 数据 // 5. 配置声道与音量左右声道独立音量0x200dB cs43l22_reg_write(0x20, 0x00); // 左声道音量设为静音前的过渡值 cs43l22_reg_write(0x21, 0x00); cs43l22_reg_write(0x22, 0x00); // 静音控制bit71 取消静音 cs43l22_reg_write(0x23, 0x00); // 左右声道独立模式 // 6. 最后清除 PDN 位codec 开始工作 cs43l22_reg_write(0x02, 0x9E); // bit71PDN0正常模式 HAL_Delay(10); }寄存器说明0x04 寄存器的MCLK/LRCK比值必须与硬件提供的时钟匹配。例程中 MCLK 为 12.288 MHz、LRCK 为 48 kHz比值为 256所以写 0x80 对应自动检测模式。如果你的系统只用 11.2896 MHz256×44.1k需要把寄存器改为 0x80 依然适用CS43L22 会自动检测比值前提是 MCLK 在 4.8 MHz 到 24.576 MHz 之间。0x06 寄存器 bit7:4 控制接口格式0x04 对应标准 I2S改成 0x08 就变成 Left Justified务必与 STM32 侧保持一致。音量寄存器 0x20 和 0x21 是 8 位有效其中高 7 位为增益bit7 是通道软步进控制。初值写 0x00 不代表音量为零CS43L22 的映射关系里 0x00 对应 -102 dB而不是静音。真正的静音由 0x22 寄存器控制。初始化代码需要先设音量再解开静音否则写音量时 codec 处于静音状态寄存器会忽略写入等解除静音后音量依然停留在默认值。4.3 配置失败时看哪几个寄存器的返回值接上 I2C 后如果还是无声按顺序回读以下三个寄存器0x02应读到 0x9E0x06应读到 0x040x20应读到音量设置值。我遇到过的典型案例是 0x02 读回 0x01说明 PDN 清除没有生效——原因是第 1 步复位后延时不足codec 内部的 I2C 接口还没从复位状态恢复后续写入全部丢失。把复位后的延时从 2 ms 增加到 20 ms 即可解决。另一个常见问题是读回 0x06 为 0x00这是 I2S 格式寄存器在 PDN1 时被锁定不能修改需要先配置所有功能寄存器最后再清 PDN。5. DMA 双缓冲播放音频流的正确处理方式5.1 为什么不能用 HAL_I2S_Transmit 阻塞发送示例代码里常出现HAL_I2S_Transmit(hi2s2, buf, size, 1000)在音频应用里这是不可用的。I2S 的 SCK 是连续时钟不发送数据时 SDIN 必须保持为低电平或输出静音编码阻塞式发送在函数返回前会占用 CPU且无法满足实时性要求。正确做法是用 DMA 循环模式让外设自动从内存取数。双缓冲的实现思路准备两个各 4 KB 的缓冲区DMA 在 Circular 模式下自动交替搬运当 DMA 完成一个缓冲区传输时触发半传输中断在中断里填充另一半数据。// 音频缓冲区定义注意按 __attribute__((aligned(4))) 对齐为 4 字节 #define AUDIO_BUF_SIZE 4096 int16_t audio_buf[2][AUDIO_BUF_SIZE] __attribute__((aligned(4))); volatile uint8_t buf_idx 0; void HAL_I2S_TxCpltCallback(I2S_HandleTypeDef *hi2s) { if (hi2s-Instance SPI2) { // STM32 的 I2S2 与 SPI2 共用外设 // 一个缓冲区传完开始填充另一个 buf_idx ^ 1; fill_audio_data(audio_buf[buf_idx], AUDIO_BUF_SIZE); } } void audio_start(void) { // 先填充第一块缓冲区然后启动 DMA 传输 fill_audio_data(audio_buf[0], AUDIO_BUF_SIZE); HAL_I2S_Transmit_DMA(hi2s2, (uint16_t *)audio_buf[0], AUDIO_BUF_SIZE / 2); }参数说明AUDIO_BUF_SIZE / 2是因为 HAL 库的 size 参数以字16 bit为单位DMA 每次搬运一个 halfword缓冲区字节数是 4096halfword 数量是 2048。如果采样率是 48 kHz、单声道 16 bit2048 个采样点对应约 42 ms 音频足够中断回调完成数据填充。buf_idx用异或翻转而不是加 1避免缓冲区索引越界这是嵌入式里常见的环形指针简化写法。5.2 采样率切换的正确顺序播放 44.1 kHz 的 WAV 后再切换为 48 kHz如果只改hi2s.Init.AudioFreq 48000然后重新HAL_I2S_Init声音会变成持续几秒的爆音再恢复。原因有两点STM32 的 I2S 外设重新初始化时 SCK 和 LRCK 会短暂停止DMA 仍在运行此时 SDIN 输出的是缓冲区的旧数据CS43L22 内部 PLL 检测到 MCLK 频率变化后需要几百微秒重新锁定期间 DAC 输出是随机噪声。正确的操作顺序void audio_set_sample_rate(uint32_t new_rate) { // 1. 关闭 DMA 与 I2S让输出静音 HAL_I2S_DMAStop(hi2s2); HAL_I2S_DeInit(hi2s2); // 2. 对 CS43L22 静音防止 DAC 输出噪声 uint8_t mute 0x01; HAL_I2C_Mem_Write(hi2c1, 0x4A 1, 0x22, I2C_MEMADD_SIZE_8BIT, mute, 1, 100); // 3. 重新初始化 I2SCubeMX 的 MX_I2S2_Init 会读取新的 AudioFreq hi2s2.Init.AudioFreq new_rate; MX_I2S2_Init(); __HAL_I2S_ENABLE(hi2s2); // 4. 切换 CS43L22 的 MCLK 分频配置 // 48kHz 对应 0x8044.1kHz 对应 0x81详见数据手册 MCLK/LRCK 比值表 HAL_Delay(5); uint8_t clk_cfg (new_rate 44100) ? 0x81 : 0x80; HAL_I2C_Mem_Write(hi2c1, 0x4A 1, 0x04, I2C_MEMADD_SIZE_8BIT, clk_cfg, 1, 100); HAL_Delay(5); // 5. 解除静音重新启动 DMA uint8_t unmute 0x00; HAL_I2C_Mem_Write(hi2c1, 0x4A 1, 0x22, I2C_MEMADD_SIZE_8BIT, unmute, 1, 100); audio_start(); }逻辑说明第 1 步先停 DMA 再停 I2S避免 DMA 还在向已关闭的外设写数据导致总线冲突第 2 步的静音寄存器 0x22 需要写 0x01 而不是 0xFFCS43L22 的静音控制是低有效位写 1 才静音第 4 步的分频配置取决于 MCLK 与采样率的比值如果 MCLK 固定为 12.288 MHz44.1 kHz 时比值是 278.6不能整除所以需要把 MCLK 改为 11.2896 MHz 才能对应 256×44.1k。硬件设计时应同时引出这两个频率的 MCLK 或使用支持分数分频的 STM32 型号如 F746 的 SAI否则 44.1 kHz 播放始终有微小音调偏移。5.3 DMA 中断里不要做的事HAL_I2S_TxCpltCallback运行在中断上下文注意不要在回调里调用HAL_Delay、printf重定向到串口时尤其危险、或任何可能阻塞超过 1 ms 的操作。我发现比较稳妥的做法是在回调里只做一件事把缓冲区的数据源指针切到另一块然后置一个标志位由主循环根据标志位去解码下一批音频数据。原因是 DMA 的半传输中断与传输完成中断间隔约 21 ms48 kHz 下如果回调执行时间超过这个间隔DMA 已经开始搬运下一块缓冲区此时修改buf_idx会造成数据错位听到的是周期性断裂声。6. 用 Audacity 和示波器验证音质的三个技巧6.1 播放 1 kHz 正弦波用 FFT 看 THD下载一段 1 kHz、-6 dBFS 的正弦波 WAV 存入 SD 卡或数组用例程播放把 CS43L22 的 AOUT_L 引脚接到声卡线路输入在 Audacity 里录制并打开频谱分析。正常的频谱应只在 1 kHz 处有一个主峰左右两侧出现少量谐波二次谐波低于主峰 60 dB 以上。如果看到 500 Hz 下的分频峰说明 MCLK 与 LRCK 的比值配置错误导致内部数字滤波器工作在不同采样率上声音听起来是“金属声”。再叠加上示波器观察 LRCK确认是否为准确的 48 kHz 方波——用示波器的频率测量功能看偏差超过 20 Hz0.04%就需要检查 PLLI2S 配置。6.2 单声道数据的左右对齐问题播放单声道 WAV 时直接把单声道数据送入 I2S左右声道音量会减半且相位相反。原因是 I2S 协议里左右声道各占一个 frame单声道数据必须复制到两个声道且右声道的位序是 LSB 在前。常见做法是用一个扩展函数复制#include string.h void mono_to_stereo(const int16_t *mono, int16_t *stereo, uint32_t frames) { for (uint32_t i 0; i frames; i) { stereo[2 * i] mono[i]; // 左声道 stereo[2 * i 1] mono[i]; // 右声道相位一致 } }这个函数执行速度在 168 MHz 主频下约为 15 MB/s处理 48 kHz 单声道数据只占 CPU 不到 5%可以放在主循环里实时转换。如果想省掉这 5% 的开销可以在音频解码端就把单声道数据交错成双声道例如在处理 WAV 文件时直接读取采样点并写入交错缓冲区避免额外的拷贝循环。6.3 监听 0x02 寄存器的 PDN 状态调试时在初始化完成后每隔 1 秒回读一次0x02打印到串口。正常状态应稳定读回0x9E。如果读回值在0x9E和0x1E之间跳变说明 CS43L22 内部发生了欠压复位brown-out reset通常是 VLS 引脚上电容不够大DAC 输出大动态信号时瞬间拉低电源导致 codec 自动复位。把 VLS 和 VLD 上的去耦电容从 1 µF 加到 4.7 µF并靠近引脚放置这个现象会消失。如果 PDN 一直无法保持清除检查原理图上 PDN 引脚是否直接接了高电平——CS43L22 建议由 GPIO 控制 PDN便于软件复位如果硬件上直接拉高就需要靠写 0x00 寄存器复位但某些批次芯片对软复位支持不完整会出现启动后偶发无声。本文还有配套的精品资源点击获取