
做音频设备开发的朋友十有八九都撞上过这种讲不清道不明的同步问题。我手头这块STM32F756的板子当时在做一个USB多通道音频接口的原型计划用SAI1A和SAI2B分别承载四进四出的TDM音频流结果第一次联调就翻车——不是完全没声音而是一出声就夹着嘶嘶啦啦的周期性爆音左右声像还会缓慢漂移。用逻辑分析仪同时抓两块SAI的FS帧同步引脚帧起始沿差出了好几个主时钟周期。这篇文章就围绕这个典型的Synchro issue完整记录我从现象到根因再到解决的排查过程给同样在F756上折腾双SAI的朋友一个参考。1. 音频项目里为什么非要SAI1A和SAI2B一起上阵1.1 单SAI的通道天花板STM32F756的SAI模块设计得确实灵活一个SAI外设内部包含A和B两个独立block每个block可以单独配置成发射或接收支持I2S、LSB/MSB对齐、TDM/PCM等常用音频协议。但真正落到专业音频场景你会很快发现灵活和通道数量是两码事。每个SAI block在纯TDM模式下最多能开16个时隙听起来相当能打。可实际接的音频codec芯片比如Cirrus Logic的CS42448、TI的TLV320ADC6140这类为了抗干扰和兼容性经常在每个时隙里只填充16bit有效数据还要算上PCM同步字、空时隙的占位实际可用的通道数会明显缩水。更麻烦的是如果要做全双工A和B两个block分别被收发占用那单个SAI一次就只能提供4路输入加4路输出想扩展通道就只能打SAI1和SAI2同时使用的主意。1.2 8通道采集卡对双SAI的刚需我当时的项目目标是8通道模拟输入、8通道模拟输出走USB Audio Class 2.0实时传输。F756的USB HS控制器性能完全够用真正的瓶颈就卡在SAI通道数上。假如只用单个SAI的A/B两个blockA跑TDM 4路进、B跑TDM 4路出那就只能做到4进4出离目标差了一半。要凑满8进8出SAI1和SAI2必须协同工作于是很自然的组合就是SAI1A做输入主设备、SAI2B做输出从设备或者反过来。这种双SAI协同在多声道声卡、数字音频路由器、车载音频网关里都很常见也是F746/F756这类芯片做专业音频的常规玩法。问题是两个独立的外设要像同一个外设那样节奏完全一致远比想象中麻烦。2. SAI心跳源头F756时钟树与分频依赖关系2.1 两个SAI的时钟源必须同根排查同步问题第一步必须把时钟树吃透。STM32F756的SAI1和SAI2都可以从同一组PLL获取内核时钟常见做法是选用PLLI2S的Q输出或PLLSAI的R输出。也就是说理论上两个SAI的位时钟SCK和帧同步FS追根溯源都来自同一个PLL分频链这就是两者能对齐的基础。这里有个容易被忽略的细节RCC里不止一个寄存器可以给SAI喂时钟。具体到F756RCC_DCKCFGR寄存器里有SAI1SRC和SAI2SRC两个位域分别控制SAI1和SAI2的时钟源选择。如果SAI1和SAI2的SRC位域选成了不同的PLL输出哪怕采样率算出来一样相位关系也会因为分频链路不同而完全没法保证。我在刚开始排查时就在CubeMX时钟树页面反复确认过两个SAI必须选同一路时钟来源这是后面所有同步操作的前提。2.2 MCKDIV分频差异是怎么制造偏差的即使两个SAI的内核时钟都来自同一个PLL输出也不代表万事大吉。每个SAI block的CR1寄存器里有一个MCKDIV位域用来从内核时钟分频产生主时钟MCK和位时钟SCK。如果SAI1A和SAI2B两个block的MCKDIV取值不一致哪怕最终采样率计算值一样实际得到的MCK频率也会存在微小倍数误差。举个例子假设内核SAI时钟是49.152MHz这是音频里很常见的频率。若SAI1A的MCKDIV设置产生了3.072MHz的MCK而SAI2B的MCKDIV因为复制粘贴错误保留成了别的分频值那两边的位时钟可能一个是3.072MHz、另一个是1.536MHz。这种整数倍的频率差短期听不出太多异常但长时间运行后采样点计数会越差越远帧边界自然就对不齐了。我在代码评审时还真发现过类似问题——SAI1A的FrameLength设成了32SAI2B还留着默认16这就是典型的复制粘贴后没同步修改配置。2.3 同源只是必要条件必须强调一个容易误解的点时钟同源只解决频率一致的问题解决不了相位一致的问题。两块独立的SAI即使吃同一个时钟、用同一组分频参数上电后各自内部的分频器也是从各自的复位状态开始计数的谁先初始化完成、谁先收到启动触发都会让SCK的起始沿差开几个周期。这种相位偏差一旦存在FS帧同步信号就会在错误的边沿被采样音频数据错位几乎是必然结果。3. 爆音、漂移、帧错位——同步失锁的真实症状3.1 症状一周期性爆音我遇到的第一个直观现象就是周期性爆音。爆音不是持续的白噪声而是每隔几十毫秒就咔一下像老式收音机调台时那种杂音。原因分析起来不复杂两个SAI的FS帧起始沿存在相位差DMA读取到的数据流中会出现偶发的半个采样点错位这个错位被音频codec还原后就是一次尖锐的咔嗒声。如果用软件在DMA中断里统计每个SAI的数据块计数会发现SAI1A每收到1000个帧SAI2B可能已经收到1003个帧帧计数差异每隔一段时间就跳变一次。这个跳变周期和爆音出现的周期基本对应算是软件侧最直接的证据。3.2 症状二多通道声像漂移比爆音更难接受的是声像漂移。做多声道音频时如果我们把左前置声道放在SAI1A、右前置声道放在SAI2B正常情况下一左一右两个声道应该严格同步听到的声音应该凝聚在正中间。实际测试中音源的正弦波扫频信号听起来却像在左右声道之间来回移动定位飘忽不定。这背后的道理也简单两个声道的采样数据在时间上没有严格对齐一个声道领先另一个声道零点几个采样点人耳对几十微秒级别的声道间延迟非常敏感尤其是在1kHz-4kHz的中频段。时间差会引起明显的相位差声像自然就飘了。3.3 快速判断示波器看两条FS遇到这类问题我建议先把软件怀疑降到最低用示波器或逻辑分析仪把两个SAI的FS引脚信号拉出来对比。正常情况应该是两条FS的频率完全一致上升沿对齐误差不超过一个SCK周期。如果看到上升沿之间差了整整几个SCK周期或者其中一个FS的脉宽和另一个不同那基本可以锁定是同步配置层面出了问题而不是DMA或中断处理延时不均导致的软件问题。实测时我抓到的波形就是这样SAI1A的FS是标准的1/32帧脉宽SAI2B的FS虽然也是同一个频率但上升沿始终比SAI1A晚大约4个SCK而且这个延迟值在每次上电后都略有变化。这就说明两个block之间完全没有同步机制在工作。4. 一条一条捋寄存器根因定位过程4.1 第一步确认SAI2B没有启用同步把问题锁定在硬件同步层之后我直接读SAI2B的CR1寄存器。STM32的SAI block里有一个SYNCEN字段专门用来配置当前block是否作为从机同步到另一个block。看到这个字段的值是0也就是异步模式我基本就知道问题出在哪了。SYNCEN这个位域的含义在一个SAI内部和两个SAI之间是通用的。我们可以把SAI1A配置成主设备然后让SAI2B的SYNCEN指向SAI1这样SAI2B就不再自己产生SCK和FS而是直接借用SAI1A已经生成好的时钟和帧信号。两块SAI使用同一套时钟时序相位偏差自然就消失了。4.2 第二步核对帧格式参数是否完全一致就算SYNCEN配置正确如果两个block的帧格式参数不一致比如SCK极性、FS极性、时隙数量、时隙大小同步也会被破坏。因为SAI2B虽然在时钟信号上借用了SAI1A但接收的数据解析逻辑仍是按自己的FRCR和SLOTR寄存器配置来执行的。配置不正确数据照样错位。我认真对比了SAI1A和SAI2B的FRCR寄存器SAI1A的FSL和FSALL分别是32和1SAI2B却还是16和8。这个不一致造成的影响是SAI2B即使收到和SAI1A完全一样的FS脉冲也会用不同的帧长度去解析一个16bit帧和一个32bit帧怎么都不可能数到同样的采样边界。这部分配置必须一字不差地统一。4.3 第三步观察初始化顺序是否造成启动相位差还有一种隐蔽情况是两个block都开启了但初始化顺序有讲究。我在调试中尝试把SAI2B先初始化、SAI1A后初始化结果发现SAI2B在SAI1A尚未启动之前虽然处于同步模式但因为没有主时钟输入实际上处于等待状态。而当SAI1A最后启动的那一刻SAI2B从等待状态被带着跑起来帧起点自然就和SAI1A对齐了。反过来如果先初始化SAI1A、再初始化SAI2BSAI2B会捕捉到当前的帧相位作为自己的起点由于初始化本身的时钟延迟它就会比SAI1A晚半个周期。这个顺序问题虽然可能只造成个位数周期的偏差但在严格要求声道一致性的应用里一样不能忍。4.4 进一步缩小怀疑范围其实还有一层更隐蔽的坑值得提如果使用DMA搬运数据两个SAI的DMA请求优先级和FIFO阈值设置不一致也可能导致数据流读取延迟不均匀。这类问题表现出来更像是随机跳变而不是固定相位差在排查时不要和硬件同步问题混在一起。我当时特意把FIFO阈值统一配置成同一档位DMA请求优先级也做了对齐才敢确定纯粹的同步问题。5. 主从同步配置实操从CubeMX到HAL代码5.1 CubeMX时钟树配置在STM32CubeMX里先把时钟树配置成SAI1和SAI2同源。进入Clock Configuration页面找到SAI1和SAI2的时钟源选择把两者都指向PLLI2S_Q或PLLSAI_R不要搞成SAI1用PLLI2S、SAI2用PLLSAI。然后根据实际采样率需求倒推PLL的倍频系数保证两个SAI输入时钟频率严格相等。5.2 SAI1A主设备配置代码HAL库的SAI初始化结构体里有专门的同步字段。SAI1A作为主设备同步配置设为禁用音频模式选主模式帧长度、时隙个数按照前面的统一参数设置。/* SAI1 Block A主设备 */ hsai1.Instance SAI1_Block_A; hsai1.Init.AudioMode SAI_AUDIOMODE_MASTER; hsai1.Init.Synchro SAI_SYNCHRONOUS_DISABLE; hsai1.Init.Protocol SAI_FREE_PROTOCOL; hsai1.Init.DataSize SAI_DATASIZE_16BIT; hsai1.Init.FirstBit SAI_FIRSTBIT_MSB; hsai1.Init.ClockStrobing SAI_CLOCKSTROBING_RISINGEDGE; hsai1.Init.FrameLength 32; hsai1.Init.ActiveFrameLength 1; hsai1.Init.SlotSize SAI_SLOTSIZE_DATASIZE; hsai1.Init.SlotNumber 4; HAL_SAI_Init(hsai1);5.3 SAI2B从设备配置代码SAI2B的关键区别在于两点一是音频模式必须设为从模式二是在Synchro字段里明确指定同步到SAI1。HAL库这个字段在不同版本里名字可能略有差异但基本都有类似SAI_SYNCHRONOUS_EXT_SAI1的选项。/* SAI2 Block B从设备同步到 SAI1 */ hsai2.Instance SAI2_Block_B; hsai2.Init.AudioMode SAI_AUDIOMODE_SLAVE; hsai2.Init.Synchro SAI_SYNCHRONOUS_EXT_SAI1; hsai2.Init.Protocol SAI_FREE_PROTOCOL; hsai2.Init.DataSize SAI_DATASIZE_16BIT; hsai2.Init.FirstBit SAI_FIRSTBIT_MSB; hsai2.Init.ClockStrobing SAI_CLOCKSTROBING_RISINGEDGE; hsai2.Init.FrameLength 32; hsai2.Init.ActiveFrameLength 1; hsai2.Init.SlotSize SAI_SLOTSIZE_DATASIZE; hsai2.Init.SlotNumber 4; HAL_SAI_Init(hsai2);5.4 初始化顺序先备后主初始化顺序在代码层面必须严格遵守一个原则先初始化从设备后初始化主设备。也就是先调用HAL_SAI_Init对SAI2B做配置然后再初始化SAI1A。原因是SAI2B一旦被配置成同步模式会立刻开始等待外部SCK和FS信号主设备SAI1A晚一些启动时整个时钟链从一开始就由主设备接管从设备跟随主设备的起始相位。实际验证下来只要初始化顺序正确用示波器看两条FS上升沿基本是重合的即使有偏差也稳定在一个SCK周期内而且每次上电后偏差值不再随机变化。这个效果已经足够满足绝大多数音频应用的声道一致性要求。5.5 运行阶段不要动同步配置还有一个建议同步关系确认好后运行阶段尽量不要动态去改SAI的同步配置除非你非常清楚当前状态机在干什么。SAI块在使能状态下修改CR1的SYNCEN字段可能导致SCK或FS信号短暂毛刺而这恰恰是音频系统最怕的东西。如果实在需要重新配置先把相关SAI失能DISABLE改完再使能。6. 验证方法与其他容易踩的坑6.1 24小时稳定性测试配置改完之后不能只看声音正常就觉得完事。音频设备的同步问题有很强的偶发性可能运行几分钟才出现一次帧偏移。我习惯做至少24小时的长时间跑机测试用固定频率的正弦波信号输入软件里记录每个SAI的数据块计数并在DMA中断里带时间戳统计。只要24小时内两个SAI的帧计数严格保持相同增量没有出现突然的增减基本可以判定同步问题已经解决。当时我做完这个测试数据块计数差异保持为0爆音和声像漂移都消失了这才敢说问题彻底解决。6.2 FIFO阈值和DMA请求时机硬件同步问题解决后还有一个经常被忽略的软件配置SAI FIFO阈值FTH和DMA请求触发方式。FTH决定FIFO里积累多少个数据后才触发一次DMA请求如果两个SAI的FTH配置不同数据搬运到内存的节奏就会不一致可能造成音频缓冲区里出现周期性前后错位。建议把两个SAI的FTH配置成完全相同的值。比如统一配置为FIFO半满触发配合双缓冲DMA数据流节奏能维持得很好。6.3 中断优先级与缓存一致性F756是Cortex-M7内核有D-Cache做DMA音频传输时必须处理缓存一致性问题。如果DMA写内存后CPU读取的数据是旧的缓存内容就会出现莫名其妙的杂音和循环播放。我当时在音频处理任务里对DMA缓冲区的Cache操作做了严格管理该Clean的Clean、该Invalidate的Invalidate不要偷懒省略这一步。6.4 后来我总结的几条铁律这次调试下来我觉得可以沉淀成几条经验分享给大家两个SAI的输入时钟必须先确认同源不同源谈同步没有基础帧格式四个参数FrameLength、ActiveFrameLength、SlotNumber、SlotSize必须完全一致少一个不一致都白搭从设备初始化必须放在主设备之前顺序错了就要接受随机相位偏差示波器看FS是最快的验证手段别一开始就陷入软件调试的泥潭同步配置完成后运行时不要随意动态调整宁可重启设备也不要在线改同步寄存器。按这个流程走下来SAI1A和SAI2B的同步问题排查起来并不难真正难的是沉住气把时钟树、寄存器、初始化顺序这些基础环节一步步验证到位。搞定了这个F756上做多通道音频项目的大半信心也就有了。