
簡介面向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 寄存器復位但某些批次芯片對軟復位支持不完整會出現啟動后偶發無聲。本文還有配套的精品資源點擊獲取