多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

STM32F4+ES8388音频驱动:I²S时钟与DMA流设计实战

STM32F4+ES8388音频驱动:I²S时钟与DMA流设计实战 一拿到“STM32F4驱动ES8388音频芯片的I²S时钟与DMA流设计”这个题目我就想起自己第一次调通ES8388录音播放链路时的状态示波器挂在I²S的BCLK上看了半天音频Codec不出声DMA却在满速搬运最后发现是MCLK分频比例没对上。做嵌入式音频开发的都知道这种问题最容易让人头大。STM32F4作为主控配上ES8388这颗低功耗立体声编解码器中间走I²S传输数字音频再用DMA把数据流从CPU手里解放出来这套组合在开发板、智能音箱、录音笔里非常常见。这篇文章就围绕这个项目把I²S时钟树、PLLI2S配置、DMA流映射、ES8388寄存器初值这些环节拆开讲清楚尤其是哪些坑必须避开。无论你是刚接触音频Codec的新手还是想快速复现一套可用的录放系统这篇内容都值得你花十分钟看完。我调过的板子里ES8388一般挂在STM32F407或者F429上采样率常用48kHz或者44.1kHz音频数据位深16bitI²S走标准飞利浦格式。整套设计的核心其实就一句话MCU的I²S外设负责把数字音频按时序送出去DMA负责把数据从内存搬到I²S发送寄存器、或者从I²S接收寄存器搬到内存CPU只在缓冲半满和全满的时候介入一下。听起来不复杂但时钟一旦配错轻则声音变调重则完全无声。下面我按项目落地顺序把每个关键点逐个说透。1. 项目整体设计与方案选型1.1 这套音频链路要解决什么问题嵌入式设备一旦开始处理音频就绕不开两个现实问题第一音频是连续数据流中断里一个字节一个字节地往I²S数据寄存器里写CPU会被拖死第二I²S接口本身是个同步串行口它需要精确的位时钟、帧时钟和主时钟这三路时钟只要有一点偏差音频Codec就可能罢工。STM32F4的I²S外设加上DMA控制器天生就是为解决这两个问题设计的。I²S负责把并行内存数据转成串行音频时序DMA负责数据搬运CPU只做少量控制和管理工作。这样设计之后一个48kHz、16bit、双声道的音频流每秒要搬运约192KB数据CPU占用率可以控制在极低水平剩下的算力还能拿去跑界面、跑控制逻辑、跑音频算法。1.2 为什么选择ES8388市面上I²S接口的音频Codec不少常见的有WM8978、CS43L22、SGTL5000但ES8388在很多项目里更讨喜。它是一颗集成度很高的低功耗立体声Codec内部有两路ADC和两路DAC还带了耳机放大器支持8kHz到192kHz的采样率数字接口支持标准I²S、左对齐、右对齐和DSP格式控制接口走I²C或者SPI。对于做便携式设备、录音笔、智能音箱开发板这些场景一颗ES8388就能搞定录音和放音不需要额外加运放和耳机驱动电路。还有一个很现实的好处是这芯片在国产开发板上用得特别多资料相对好找价格也稳。真要说缺点就是寄存器数量比CS43L22多一点初始化顺序不对容易踩坑这一点后面我会重点讲。1.3 整体信号连接与主从关系MCU和ES8388之间本质上是两条链路一条是控制链路用I²C读写ES8388的内部寄存器另一条是音频数据链路用I²S传输PCM数据。I²S这边一般需要四根线SCK也叫BCLK位时钟、WS也叫LRCK左右声道帧时钟、SD发送数据线、SD接收数据线。如果启用主时钟输出还要加一根MCLK也就是给Codec提供内部Delta-Sigma调制器参考时钟的Master Clock。绝大多数情况下推荐让STM32F4作为I²S主设备由它产生MCLK、BCLK、WSES8388作为从设备只负责接收时钟和收发数据。这样MCU可以主动控制音频时序也方便用PLLI2S精确合成所需频率。ES8388端也要配成从模式两边一旦都是主模式等于有两套时钟源在打架系统直接乱套。2. I²S时钟系统设计从PLLI2S到MCLK2.1 STM32F4时钟树里的I2S时钟来源STM32F4系列有一个独立的PLL叫PLLI2S专门给I²S和SAI生成音频时钟这是很多初学者容易忽略的地方。主PLL负责给CPU、AHB、APB提供时钟PLLI2S则独立负责音频外设时钟两者互不干扰。这样设计的好处是你可以一边让CPU跑168MHz一边让I²S跑出一个和音频采样率精确匹配的时钟不会因为调整系统主频而破坏音频采样率。PLLI2S的输入一般来自HSE经过PLLM分频后再做PLLI2SN倍频最后由PLLI2SR分频输出得到I2SxCLK。这个I2SxCLK再进入I²S外设经过内部的分频器最终生成BCLK、WS以及MCLK。我在调试时习惯在CubeMX的Clock Configuration页面里看这一路的参数变化尤其是修改采样率之后PLLI2SN和PLLI2SR会自动重新计算这一点非常方便。2.2 采样率与MCLK、BCLK、WS的关系I²S的几路时钟之间有严格倍数关系。以最常见的48kHz、16bit、立体声为例WS频率正好等于采样率48kHzBCLK是位时钟STM32F4在标准I²S模式下即便数据只有16bit每个声道对应的BCLK周期数通常仍按32bit帧宽计算所以BCLK等于64倍采样率也就是3.072MHzMCLK主时钟一般取256倍采样率也就是12.288MHz。如果是44.1kHz采样对应的MCLK就是11.2896MHzBCLK是2.8224MHz。这个倍数关系不是随便定的ES8388内部的数字滤波器和Delta-Sigma调制器需要依靠MCLK来产生内部时钟MCLK与LRCK的比值必须落在它支持的范围内一般是256、384或者512。如果MCU端生成的是256fs而ES8388寄存器里配置的是384fs两边就匹配不上出来的声音要么变调要么直接是白噪声。2.3 CubeMX里怎么安全配置I2S时钟我现在的习惯是先用CubeMX把I²S外设和DMA搭好再手动调整ES8388的寄存器。CubeMX里配置I²S的步骤不难开启SPI2或者SPI3把它设置成I²S主模式Standard选PhilipsData Format选16bitMCLKOutput选EnableAudio Frequency选48kHz或者44.1kHz。然后在Clock Configuration页面确认I2S时钟源这一路有值通常CubeMX会自动算好PLLI2S的参数。有一个小提示如果你的板载晶振不是标准的8MHz或者25MHz一定要在CubeMX里填对HSE频率否则算出来的采样率会有偏差听起来就是音调不对。CubeMX生成工程后我还会打开i2s.c文件看一眼hi2s-Init.AudioFreq和MCLKOutput确认和预期一致。2.4 与ES8388侧时钟匹配ES8388的寄存器里有一项是配置MCLK和LRCK比率的也就是Master Clock Ratio。常见配置是256fs、384fs、512fs。这一项必须和MCU实际输出的MCLK一致。举个例子STM32F4这边通过CubeMX配置MCLK输出为256×48kHz也就是12.288MHz那ES8388端也要把MCLK/LRCK Ratio配成256比1。如果ES8388配成了384比1它内部会认为输入的MCLK频率不匹配实际表现往往是白噪声或者只有非常微弱的底噪听不到正常人声。这种问题在代码层面很难查出来因为它不会报错DMA也在正常跑示波器看波形也都有就是声音不对。所以我在初始化ES8388时一定会先把这个比率写死再加注释提醒自己。3. DMA流设计从零拷贝到双缓冲3.1 为什么音频流必须靠DMA很多人会问48kHz、16bit、双声道的数据量并不大每秒不到200KBCPU中断里搬数据真的不行吗真不行。因为音频流是持续不断的假如你在主循环里用阻塞方式往I²S寄存器里灌数据整个程序就别想干别的事了假如你靠中断搬运每发送一个采样要进一次中断48kHz双声道意味着每秒要进96000次中断每次中断还要压栈、判断、写寄存器、出栈开销非常大。而且音频一旦出现断流表现比网络丢包还难接受直接就是爆音和卡顿。DMA的价值在于它可以在没有CPU参与的情况下把内存里的PCM数据自动搬运到I²S的数据寄存器这个动作是硬件完成的CPU只需要在处理完一个缓冲块之后把下一块数据准备好。这就像一个自动化流水线传送带不停转工人只需要在两端定时补料就行。3.2 DMA通道映射与初始化要点STM32F4有两个DMA控制器DMA1和DMA2每个控制器有8个Stream每个Stream又支持多个Channel映射。I²S这种高速外设通常挂在DMA2上。具体是哪个Stream、哪个Channel要以参考手册里的DMA请求映射表为准不同型号的F4会有差异。我强烈建议在CubeMX里直接添加I2S的DMA请求让工具自动分配Stream和Channel它会自动处理这些映射关系。如果你非要手动配需要注意几个通用规则外设地址增量关闭因为I²S数据寄存器地址是固定的内存地址增量打开因为要顺序读取缓冲区数据宽度统一设成HalfWord对应16bit音频采样DMA模式选Circular循环模式这样DMA会不断从缓冲区搬运数据地址到尾部后自动回到起点。3.3 HAL库代码启动DMA与回调写法使用HAL库时I²S的DMA启动非常简单。CubeMX初始化好I2S和DMA之后代码里只需要一句话HAL_I2S_Transmit_DMA(hi2s2, (uint16_t *)audio_tx_buf, TX_BUF_SIZE);如果是录音就调用HAL_I2S_Receive_DMA(hi2s2, (uint16_t *)audio_rx_buf, RX_BUF_SIZE);接下来要处理的重点是回调函数。HAL库提供了两个很有用的回调半传输回调和传输完成回调。在实际工程里我一般这样写void HAL_I2S_TxHalfCpltCallback(I2S_HandleTypeDef *hi2s) { if (hi2s-Instance SPI2) { // 前半段缓冲已经发送完往 audio_tx_buf[0 ... N/2-1] 填入新数据 fill_audio_data(audio_tx_buf, TX_BUF_SIZE / 2); } } void HAL_I2S_TxCpltCallback(I2S_HandleTypeDef *hi2s) { if (hi2s-Instance SPI2) { // 后半段缓冲已经发送完往 audio_tx_buf[N/2 ... N-1] 填入新数据 fill_audio_data(audio_tx_buf TX_BUF_SIZE / 2, TX_BUF_SIZE / 2); } }这里有一个很关键的注意事项回调函数是在中断上下文里执行的千万不要在里面放耗时操作比如printf、HAL_Delay、Flash写入、大块memcpy。我以前在回调里加过调试打印结果导致半传输中断处理时间太长DMA循环模式下的缓冲区已经被覆盖声音卡顿得一塌糊涂。看起来像DMA坏了其实是调试代码把中断拖死了。正确做法是在回调里只做标记或者做一次紧凑的内存拷贝把数据准备好就立刻返回。3.4 双缓冲的实际落地I²S DMA最常用的搭档就是双缓冲。思路是准备一个长度是2N的数组DMA以循环模式工作。当DMA发送完前半段N个采样时触发半传输中断CPU往前半段填新数据当DMA发送完后半段N个采样时触发完成中断CPU往后半段填新数据。因为DMA和CPU操作的是不同的缓冲区域所以在理想情况下不会互相踩踏。对于接收方向也一样DMA循环把I²S接收到的数据写入2N数组CPU在半传输和完成中断里取走数据去处理。这个方案非常经典代码写起来也简单不需要自己管理环形队列的读写指针。实际选缓冲大小时要平衡延迟和CPU负担。N太小比如64个采样中断频率太高CPU压力大N太大比如8192个采样延迟会变大放音时调节音量或者切歌时响应也慢。48kHz采样率下我一般取N等于512或者1024这样每秒钟中断几十次CPU占用很低延迟也在可接受范围内。如果你需要更灵活的数据管理可以把双缓冲的数据再拷到一个环形队列里供上层音频处理模块使用但要注意数组下标和取模运算别弄错。4. 从断流到爆音的排查实录4.1 无声这类问题的排查思路无声是最常见的现象排查顺序非常关键。我的习惯是先看I²S时钟波形用示波器量STM32输出的MCLK、BCLK、WS确认这三路时钟存在且频率正确。如果时钟没有问题在MCU侧检查CubeMX配置、PLLI2S初始化、引脚复用设置。如果时钟存在再看ES8388寄存器读写是否正常最简单的办法是读芯片ID寄存器确认I²C通信没问题。接着检查ES8388的初始化顺序必须先复位再开电源再配音频接口格式最后开DAC和耳机通路。很多无声问题出在初始化顺序上比如音频接口格式都设好了才发现DAC的静音位还没解除或者耳机放大器的使能位没打开。还有一个容易被忽略的坑DMA缓冲区不能定义成函数内的局部变量因为局部变量在栈上栈地址在优化后可能失效DMA访问时会直接进入硬件错误。缓冲区一定要定义成全局数组必要时加__attribute__((aligned(4)))做对齐。4.2 爆音、卡顿、杂音的排查爆音和卡顿往往是两个不同方向的问题。开机爆音一般是因为DAC输出从0电平突然跳到第一个采样值产生了一个电压阶跃。解决思路是在播放开始时做淡入处理或者先向ES8388输出几毫秒静音数据再切换音频数据。卡顿则和DMA缓冲生命周期有关常见原因是缓冲区太小、中断优先级不够高、回调处理耗时太长。如果只是监听48kHz的音频我建议把DMA中断优先级提上来至少高过普通外设中断并且把半传输和完成中断对应到同一个高优先级组里。杂音的问题更偏向硬件ES8388的模拟电源和数字电源如果不做滤波或者模拟地和数字地连得不好ADC/DAC输出很容易带上底噪。PCB布线时I²S的BCLK和WS尽量短串接33Ω电阻能改善信号沿过冲MCLK附近不要走大电流电源线。4.3 用示波器看I2S时钟波形调试I²S时示波器是最好的朋友。48kHz采样率下标准的时钟组合是这样的MCLK约12.288MHzBCLK约3.072MHzWS正好是48kHz。对于44.1kHz采样MCLK约11.2896MHzBCLK约2.8224MHz。如果测出来的BCLK频率接近预期WS频率也对但MCLK有毛刺那就是PLLI2S或者供电的问题如果MCLK完全不存在回头检查CubeMX里MCLKOutput是否使能。需要注意你的逻辑分析仪采样率必须足够高至少25MHz以上否则12.288MHz的MCLK根本抓不出完整波形。我一般用示波器看MCLK和BCLK用逻辑分析仪抓WS和SD数据重点确认数据位与WS的偏移关系也就是标准I²S格式下SD数据要比WS变化晚一个BCLK周期如果这个偏移不对解码出来的左声道右声道就会反掉。4.4 常见问题速查表我把调试中容易遇到的问题整理成一张速查表方便你排查时直接对号入座现象可能原因处理办法完全无声ES8388复位失败或I²C地址不对读芯片ID寄存器确认I²C通信正常完全无声主从模式不匹配STM32配MasterES8388配Slave完全无声DAC静音位未解除初始化ES8388时显式关闭DAC静音完全无声DMA缓冲区定义在局部栈上改成全局数组注意对齐白噪声MCLK与LRCK比率不匹配ES8388配置成和MCU一致的256fs或384fs声音变调采样率参数与音频数据不一致核对I2S AudioFreq播放前统一重采样播放有爆音开机瞬间DAC电平阶跃加淡入或者先播静音缓冲区声音卡顿DMA循环缓冲太小中断被抢占增大缓冲提高DMA中断优先级录音音量小ES8388 ADC增益配置不对检查ADC PGA寄存器和自动增益控制只有单声道I2S标准/TDM格式配置错两边都用同一时钟极性和标准I2S格式我记得有一次调试放音DMA确实在循环跑缓冲区数据我也确认过没问题但耳机里就是只有持续的噝噝声。折腾了很久最后发现是ES8388的MCLK分频配成了384fs而STM32实际输出的是256fs一个比例不匹配内部滤波器就完全跑偏了。从那以后每次初始化ES8388我都先确认MCLK比率再调其他寄存器。还有一次是录音通道的ADC PGA增益设得太大输入信号稍微大点就削波失真听起来像有杂音其实早就是削波了这个问题用示波器看ADC输入波形最直观。调这类问题别急着怀疑芯片坏了先用示波器把I2S三路时钟量一遍再把ES8388寄存器回读一遍大部分问题都能定位到具体环节。再分享一个我后来一直沿用的调试顺序先做一个最小测试工程不跑FreeRTOS不做文件系统只用一块固定正弦波数组通过I²S DMA循环发送到ES8388耳机能听到连续平顺的正弦声再往上加业务逻辑。这样一旦出问题你就能确定是基础链路还是上层逻辑的锅。如果你能听到正弦声但听不到人声那就是数据处理的问题和I²S、DMA、ES8388都没关系。这套思路帮我省了大量排查时间强烈建议你试试。
返回列表