2026/9/19 3:57:27

STM32 SAI接口TDM模式实现8通道音频采集与DMA传输详解

STM32 SAI接口TDM模式实现8通道音频采集与DMA传输详解 做多路音频采集的朋友大概率都经历过这种苦I2S一次只能传两声道四个麦克风就得两个外设八个通道甚至要四组SCK/WS同时转画板子都嫌线乱更别说同步问题。后来我把方案换成STM32的SAI接口用TDM模式一根SD线就传完了8个通道DMA一开CPU基本不用管数据搬运。这篇文章就把整个搭建过程完整捋一遍从SAI和TDM的工作原理到CubeMX的配置步骤再到DMA代码和调试经验希望对要做多通道音频采集、麦克风阵列的你有参考价值。先说清楚这篇博文适合谁看你手上有一颗带SAI接口的STM32F446、F746、H743这类都有准备做8通道ADC同步采集或者做麦克风阵列前端但目前对TDM时隙怎么分配、DMA怎么配、数据怎么从寄存器里抠出来还不太清楚。看完这篇你可以直接用里面的思路和代码起步。1. 为什么是SAI和TDM8通道采集的方案选型1.1 I2S做多路采集为什么难受很多初学者第一反应是STM32有多个I2S外设那我做8通道用4个I2S不就行了理论上有一定可行性实际做的时候问题一大堆。先算外设数量。STM32上绝大多数型号的I2S外设就2到3个每个I2S固定支持双声道做8通道至少要4个I2SF4、F1这种根本不够用就算H7这种外设资源丰富的也得去挤引脚和DMA流画PCB时SCK、WS、SD信号满天飞音频信号最容易在这里被引入干扰。再算同步问题。多路音频采集要求通道之间采样时刻一致也就是ADC同时采样。用多个I2S外设分别接多个ADC虽然可以把WS统一接到一起但不同I2S外设的主时钟分频路径、内部FIFO延迟很难做到完全一致时间偏差一两个BCLK周期对阵列测向这种应用就是灾难。还有一层是引脚复用冲突。I2S和SAI经常共用同一组引脚你想把SPI、UART、I2C都放在同一颗芯片上引脚资源很快就炸了。So从工程角度看I2S天生是给2声道设计的强行做多通道属于逆着硬件特性干活。1.2 SAI接口和TDM模式到底厉害在哪SAI全称Serial Audio Interface是STM32上比I2S更灵活的音频接口。它把音频传输拆成了更基础的位时钟、帧同步、串行数据三根线协议层让你自己定义。每个SAI外设内部有A、B两个独立的block每个block都可以配置成发送或接收也可以一个block做主机、另一个做从机两个block之间还能内部同步。TDM模式则是时分复用核心思路是把一个采样周期等分成若干个固定时隙Slot每个Slot传输一个通道的数据。比如8通道、每通道24位数据如果Slot位宽设为32位那一帧就是8乘32等于256个BCLK周期。帧同步信号FS拉高或拉低标志一帧开始之后BCLK一个周期一个周期地把8个Slot从MSB到LSB排着传完下一帧再来。数据线上始终只走一根SD多通道的区分靠的是“位置”也就是时隙序号。这个思路跟你去银行排队办事很像所有人都走同一个入口但窗口按顺序叫号1号窗口办完再叫2号8号办完再回到1号。所有通道共用数据线互不干扰只要主控和ADC都清楚“第几个时隙对应第几个通道”数据就不会乱。TDM模式另一个好处是天然支持多颗ADC级联。一颗支持TDM的4通道ADC你可以把多颗ADC的FS和BCLK并联每颗ADC在固件里配置成不同的起始时隙就能在一条数据线上扩展出更多通道。很多专业音频设备、多麦克风阵列的模拟前端都是这么干的。1.3 8通道系统怎么拆一颗8通道ADC还是两片4通道8通道采集在硬件上一般有两条路。这条路比较简单选一颗原生8通道的音频ADC比如PCM3168A它本身支持TDM 8/16时隙8路ADC和8路DAC都集成在一颗芯片里一个SAI子块直接就能把8个通道接收完。适合对体积敏感、不想板子上摆太多芯片的场景。另一条路是用两颗4通道ADC比如PCM1864、CS5344这类分别接到SAI1的A和B两个block每个block工作在TDM4模式两个block之间通过内部同步保持帧对齐。这种方案的好处是ADC选型灵活、单颗价格可能更低坏处是要同时考两路DMA、两套初始化参数代码量更大PCB上也要多一颗芯片。我自己做项目更推荐第一种单颗8通道方案不是因为它技术上多高级而是它把很多问题挡在了硬件之外。你只需要管一个SAI外设、一条DMA传输流调试的时候心智负担小很多。9.6kHz、48kHz、96kHz采样率切换时也不需要同步调两套时隙参数。两颗4通道ADC的同步问题一旦碰上排查起来非常痛苦。所以后面的配置和代码我都以“一颗支持TDM的8通道ADC STM32的SAI1_Block_A接收”作为主线但TDM4、TDM16的思路完全一样改个时隙数和帧长度就能套到其他芯片上。2. 硬件方案设计ADC芯片和STM32怎么接线2.1 8通道ADC芯片怎么选音频ADC领域选择很多但真正常见于TDM模式、也容易买到的8通道芯片我首推PCM3168A。这芯片是TI家的24位精度信噪比标称112dB带8通道ADC和8通道DAC数据手册里明确写了支持TDM4、TDM8、TDM16模式。如果你不需要回放只需纯采集也可以看PCM1864这种4通道ADC或者AKM的AK5720系列但AKM前几年工厂火灾之后很多型号交期不稳定我后来基本都用TI或Cirrus Logic的片子。选型时重点看三件事是否支持TDM模式、模拟供电电压是否和你的传感器匹配、从机模式下能否正确跟随外部BCLK和FS。另外一定确认ADC是“从机模式”。STM32做主ADC做从时钟由STM32统一产生这样才能保证所有通道采样时刻一致。如果ADC只能做主那STM32就得跟着ADC的时钟走设计会复杂很多同步性也没那么可控。2.2 典型接线表以SAI1_Block_A接收为例STM32与ADC之间的核心信号就四根MCLK主时钟、SCK位时钟、FS帧同步、SD串行数据。具体引脚分配看你的芯片型号和CubeMX里的引脚规划下面是一个典型接线STM32 SAIPin方向ADC Pin说明SAI1_MCLK_A输出MCLK主时钟ADC内部delta-sigma调制器需要SAI1_SCK_A输出BCK位时钟频率等于采样率乘时隙数乘Slot位宽SAI1_FS_A输出LRCK / FSYNC帧同步标志一帧数据的开始SAI1_SD_A输入DOUTADC串行数据输出接到STM32接收端不要漏掉MCLK。很多第一次做音频的人容易只接BCLK和FS结果ADC完全不工作或者输出全是杂音。delta-sigma架构的ADC内部靠过采样技术MCLK是调制器正常工作必需的时钟常见倍数有256fs、512fs也就是采样率48kHz时MCLK要给12.288MHz或24.576MHz。STM32的SAI可以自己输出MCLK这个后面在CubeMX里讲。如果用的是PCM3168A还要注意它的数据输出引脚命名可能是DOUT1/DOUT2或者DOUTTDM模式下一般只需要一根数据线具体看手册里的TDM接法。两颗4通道ADC的方案则要把两片DOUT分别接到SAI1的SD_A和SD_B同时把两片ADC的BCLK、FS并联到同一组引脚保证帧同步。2.3 电源、基准和退耦音频板最容易翻车的地方音频采集对电源噪声非常敏感数字电路的开关噪声如果串到模拟电源上ADC采集的数据会带明显底噪FFT看起来就是整个频段抬高。STM32的3.3V和ADC的模拟供电建议用独立的低噪声LDO供电别直接用DCDC输出的电压DCDC的纹波在音频频段内很难滤干净。PCM3168A这类芯片一般有模拟电源AVDD和数字电源DVDD两者最好各加一颗磁珠和10uF加0.1uF退耦电容。模拟参考电压VREF要接低噪声基准简单做法是LDO输出后加RC滤波但更稳的是用专门的基准芯片。如果你做的是麦克风阵列模拟前端还有偏置电压、放大电路这部分噪声要求更高。还有一点是地的处理。多层板尽量保证模拟地和数字地在ADC芯片底部单点连接不要跨分割区走信号线。有些朋友刚开始用洞洞板飞线做实验噪声大得波形根本没法看大部分是地回路太乱导致的。3. CubeMX配置实操时隙、时钟和DMA怎么填3.1 时钟树先算MCLK和SCLKCubeMX里配置SAI之前建议先把时钟树理清楚否则后面填参数全是凭感觉。音频时钟最核心的约束是SCLK必须严格等于采样率乘以时隙数乘以Slot位宽。FS等于采样率这个不用额外算。以48kHz采样率、TDM8、Slot位宽32位为例SCLK 48000 x 8 x 32 12.288MHzMCLK常见取256fs所以MCLK 48000 x 256 12.288MHz。这个例子里SCLK和MCLK恰好一样但很多情况下它们不同比如TDM4、Slot32位SCLK就等于48k乘4乘32等于6.144MHzMCLK还是12.288MHz。CubeMX里曾经音频时钟主要是PLLI2S或PLLSAI输出的你需要把PLL的N、R配置到能让SAI得到准确SCLK和MCLK的值。不同芯片的PLL结构不太一样但思路一致在Clock Configuration界面里先把SAI的时钟源选成PLLI2S或PLLSAI然后调整PLL参数让最终计算出的Audio Frequency接近48000。CubeMX会显示实际计算值你要确保它没有小数偏差否则长期采集会出现周期性丢采样点。F746这类芯片上SAI1的时钟源也可以选外部时钟或PLL我习惯用PLLI2S因为可以独立于系统主频调整。H7系列新增了PLL2/PLL3灵活度更高但配置逻辑是一样的。3.2 SAI外设参数逐项解读在CubeMX左侧找到SAI1选择Block A然后把它配置成接收模式。这里有几个关键参数你不能照抄网上旧工程的因为你用的芯片、ADC型号不一样参数可能完全不同。Audio Mode选Master Receive也就是STM32做主设备并接收数据。Synchro这里选Synchronous表示A和B两个block之间同步如果你只用A就无所谓但如果你后面要扩展B必须把这个选上否则两路采样点会对不齐。Protocol选Free Protocol因为TDM模式就是协议自由定义选成I2S或者LSB/MSB反而会限制时隙设置。Data Size选24位。这里容易困惑的是ADC明明是24位数据为什么后面Slot Size要选32位因为很多ADC芯片在TDM模式下把24位有效数据放到32位Slot里左边对齐还是右边对齐取决于芯片手册。为了统一和兼容Slot Size选32位Data Size选24位实际接收时高24位或低24位是有效数据后面代码里统一处理。最关键的Frame设置参数值说明Frame Length256 bitTDM8 x 32位SlotActive Frame Length1 bitFS脉冲宽度一颗BCLK周期Frame Sync PolarityActive Low或Active High看ADC手册PCM3168A通常支持配置Frame Sync Offset1 bitFS相对数据起始点的偏移Slot Size32 bit每个时隙位宽Number of Slots8时隙数Slot Enable0到7全使能如果你只用其中几路可以关掉空时隙Frame Length和Frame Sync Polarity这两项是调试时最容易踩坑的后面第五节我会专门展开。Active Frame Length选1 bit是通用做法ADC实现FS检测时一个周期足够。3.3 DMA循环模式配置SAI配置完下面配DMA。打开DMA Settings添加一个SAI1_A_RX通道。传输方向是Peripheral to Memory模式选Circular也就是循环模式。数据宽度这里要注意外设和内存都选Word也就是32位。这跟前面Slot Size选32位是同一个逻辑每次DMA传输搬一个完整时隙的数据32位一次搬完省去字节拼接的麻烦。Memory地址要选递增外设地址固定不变。DMA优先级可以设Medium或High取决于你的系统里还有没有其他高实时性外设。如果你的串口、ADC等都用DMA建议给音频分配High因为音频数据流是持续不断的优先级太低容易被其他突发传输打断造成FIFO溢出。中断方面DMA要勾选Half Transfer和Transfer Complete中断。循环模式下这两个中断会交替触发相当于把一个缓冲区切成两半一半满了就处理另一半这样CPU可以持续消费数据而不用等整个缓冲区存满。CubeMX会自动生成DMA中断回调函数你只需要在代码里重写HAL的弱回调函数。3.4 生成代码后检查初始化顺序CubeMX生成代码后main函数里外设初始化顺序默认是MX_GPIO_Init、MX_DMA_Init、MX_SAI1_Init这样的顺序。有一个细节如果你在代码里手动调整了顺序很可能导致SAI启动后DMA不工作。原因是DMA控制器必须优先于SAI初始化否则SAI在上电时就尝试启动传输但DMA通道还没就绪数据直接丢失。另外检查SAI1的NVIC中断优先级。SAI本身也有FIFO溢出等错误中断如果没使能出问题时会非常难排查。建议在CubeMX里打开SAI全局中断代码里再实现HAL_SAI_ErrorCallback把错误状态打印出来调试阶段会救你很多次。4. 手写代码DMA缓冲区和24位数据提取4.1 缓冲区和头文件定义CubeMX生成的工程里已经包含sai.h、dma.h这些头文件我们这里主要定义采集缓冲区。8通道数据如果每帧每个通道用一个uint32_t存放那一帧就是8个uint32_t。为了DMA的半传输中断和传输完成中断方便处理我们把缓冲区定义成两个半区每个半区能存256帧。#define AUDIO_CH_NUM 8 #define AUDIO_FRAME_HALF 256 #define AUDIO_BUFFER_HALF (AUDIO_CH_NUM * AUDIO_FRAME_HALF) uint32_t audio_rx_buf[2 * AUDIO_BUFFER_HALF] __attribute__((aligned(32)));这里aligned(32)不是必须的但建议加上。SAI的DMA传输如果遇到总线分叉对齐访问可以减少出错概率性能也更好。audio_rx_buf整体大小为2乘8乘256等于4096个uint32_t16KB字节对主流STM32来说负担不大。还有一个重要的变量是当前正在处理哪个半区。我们可以在回调里用一个标志位volatile uint8_t audio_half_ready 0;4.2 启动DMA接收CubeMX生成的MX_SAI1_Init已经完成了所有SAI参数初始化你只需要在main函数里调用HAL_SAI_Receive_DMA启动接收。注意这个函数的Size参数不是字节数而是你要接收的“数据元素”个数。因为我们DMA配置的是32位宽度所以Size就按uint32_t的数量来HAL_SAI_Receive_DMA(hsai1_Block_A, audio_rx_buf, 2 * AUDIO_BUFFER_HALF);启动后DMA会持续往audio_rx_buf里写数据写到半区满时触发Half Transfer中断写满整个缓冲区时触发Transfer Complete中断然后由于是循环模式DMA会重新从头开始写周而复始。你不需要重复调用这个函数除非接收被HAL_SAI_DMAStop停掉了。有些朋友喜欢在while(1)里轮询判断某个标志位再处理数据但更高效的做法是直接在回调函数里处理。HAL库SAI的DMA回调函数有两个我们重写它们void HAL_SAI_RxHalfCpltCallback(SAI_HandleTypeDef *hsai) { if (hsai-Instance SAI1_Block_A) { process_audio_data(hsai, 0); // 处理前半区 } } void HAL_SAI_RxCpltCallback(SAI_HandleTypeDef *hsai) { if (hsai-Instance SAI1_Block_A) { process_audio_data(hsai, 1); // 处理后半区 } }process_audio_data的第二个参数表示哪个半区具体数据地址就是一维数组的起始地址加偏移。处理速度要足够快如果回调里做太多事比如浮点FFT、文件系统写入可能赶不上下一轮覆盖这时候就需要加大半区深度或者优化处理算法。4.3 从32位时隙里抠出24位有效数据DMA搬到内存的是原始32位数据但我们的ADC是24位的所以需要根据芯片手册确认24位有效数据在32位时隙里的位置。不同ADC不一样常见有两种一种左对齐也就是MSB对齐有效数据在高24位低8位填充0另一种右对齐有效数据在低24位。如果是左对齐你可以这样提取有符号24位整数int32_t extract_int24(uint32_t raw) { int32_t val (int32_t)raw; return val 8; }这里直接把32位有符号数右移8位保留符号。为什么因为如果ADC是左对齐有效位在高位低8位是填充0右移8位后就得到了正确的24位有符号值而且符号位也保留住了。如果是右对齐也就是有效数据在低24位这么处理int32_t extract_int24_right(uint32_t raw) { int32_t val (int32_t)(raw 0x00FFFFFF); if (val 0x00800000) // 第24位是符号位 { val | 0xFF000000; // 符号扩展 } return val; }千万不要不管对齐方式直接强转或者移位否则通道数据会出现幅度正确但正负反了、或者数值完全不对的情况。这一点在调试时很容易被忽略因为波形看起来有信号但数值范围不对FFT频谱也有异常。处理8个通道时每来一个半区就循环256帧每帧取8个通道的数据提取之后你可以做后续处理比如写入SD卡、走串口上传、通过DAC回放或者做波束成形算法。4.4 双缓冲区持续采集不丢数双缓冲的核心价值在于DMA往一个半区写数据的同时CPU在处理另一个半区两个动作在时间上重叠谁也不等谁。这种模式下只要CPU处理时间小于DMA填满一个半区的时间理论上才不会丢数据。半区填满所需时间怎么算48kHz采样率256帧每帧8个Slot一个半区就是256乘8等于2048个uint32_tDMA传输结束需要2048个SCLK周期除以12.288MHz大概166微秒再除以2不对这里要理清DMA搬到内存的是每个时隙一个word等于SAI每个BCLK周期搬一个word严格说SAI在接收每个32位Slot时会把它写入FIFO然后DMA把FIFO里的数据搬到内存一个word对应一个Slot一个半区就是2048个word对应2048个BCLK周期。48k乘8乘32等于12.288MHz2048周期约166.7微秒。也就是说你必须在166微秒内把2048个通道样本处理完否则下一个半区数据已经写进来了你还占着CPU没处理完就来不及切换。这个时间窗口对于简单的数值处理来说足够但如果你想在回调里做FFT就必须做一些取舍比如降低采样率或者只对其中几路做FFT或者把数据处理挪到主循环回调里只置标志位。我在实际项目里是把“提取24位数据”这种轻量工作放在回调里完成然后给主循环一个ready标志主循环再把数据往串口或SD卡写。这样把实时性要求高的部分和相对耗时的部分分开系统不容易被卡死。5. 调试实录8通道静音、数据错位、杂音排查5.1 数据全零先查ADC工作状态别急着改代码如果DMA跑起来了但缓冲区里全是0先别怀疑SAI配置拿示波器或逻辑分析仪去量ADC的MCLK、BCLK、FS三根线。很多情况是ADC的MCLK没输出或频率不对或者ADC的电源没供上。PCM3168A这类芯片有软件模式也有硬件模式硬件模式下有些引脚需要上拉到指定电平才能进入TDM从机模式如果这些配置引脚没接对ADC内部可能工作在I2S模式那数据线上只会出两通道数据其他时隙全是0。这时候要仔细读ADC手册里的模式配置表逐脚核对。还有一个很容易忽略的点ADC的DOUT可能默认是三态输出只有在FS有效之后才输出数据。如果FS极性配置反了ADC在错误的时刻才输出DMA接收到的就是空数据或者全是0。逻辑分析仪抓一下FS和SD的时序关系一秒钟就能发现问题。5.2 通道错位和数据跑到高16位采集到数据了但通道顺序不对比如期望通道1的结果出现在通道3的位置这种情况绝大多数是TDM时隙偏移配置错了。SAI的TDM模式支持FirstBitOffset参数它决定第一个时隙的起始位相对FS延迟多少个BCLK周期。ADC手册里一般会画时序图你按图上的说明设置这个偏移就能对上。另一个常见现象是数据看起来有值但幅度特别小或者波形只在零附近抖。你可以打印原始uint32_t看看如果发现每个时隙的数值集中在0xXXXX0000附近说明ADC数据是右对齐的而你把右对齐当成左对齐处理了。用4.3里的右对齐提取函数问题立刻消失。通道之间相互串扰也要重点关注。如果给通道1加了个正弦波结果通道2也出现小幅度的同频信号先检查ADC的模拟输入有没有做好隔离相邻通道的信号会不会通过电源或者地耦合过去。如果是PCB问题代码再怎么调也救不回来。5.3 噪音大、波形对不上采样率采集到波形但噪声巨大最常见的原因是SAI的Clock Strobing配置和ADC输出数据的边沿不匹配。音频ADC一般会在BCLK上升沿更新数据主机应该在下降沿采样但如果配反了采样点落在数据翻转的瞬间采到的值就是不确定的表现为类似白噪声的毛刺。解决办法是切换Clock Strobing也就是把Falling Edge和Rising Edge对调然后看波形毛刺是否消失。这个参数在CubeMX的SAI配置里就是一个下拉框调试时多试一次不费事。如果波形频率不对比如输入1kHz正弦波采出来变成2kHz或500Hz通常是SCLK和采样率的关系算错了。注意检查Frame Length是不是设成了256Number of Slots是不是8。如果你习惯从网上复制一个TDM4的工程忘记改时隙数SCLK就会差一倍表现就是数据速度和实际采样率对不上。5.4 DMA不触发回调的几个原因DMA回调不触发先从最简单的排查DMA中断使能了吗CubeMX的DMA Settings里如果没有勾选Half Transfer和Transfer Complete中断那回调永远不会被调用。其次检查NVIC里DMA中断优先级是否使能很多朋友在CubeMX配好了DMA却没去NVIC页面把DMA中断开关打开。回调函数名字写错也常见。HAL库回调函数名有严格约定HAL_SAI_RxCpltCallback不能写错拼错一个字符就成了一个新的自定义函数不会被HAL库调用。Keil里可以在回调函数里打断点如果没断到八成是函数名写错了。还有一种情况是DMA启动后收到的数据一直在第一个半区因为半传输中断和传输完成中断都触发了但你两个回调函数里处理的是同一份数据导致看起来像没更新。检查process_audio_data里访问的缓冲区索引是不是真的区分了0和1两个半区别两个都用的起始地址。6. 数据验证把8路PCM导出来看波形6.1 串口DMA发送注意连续发送坑要验证采集到的8通道数据是否正确最简单粗暴的办法是把缓冲区数据通过串口发到电脑上。但8通道、48kHz、每通道4字节数据率高达1.5MB/s普通USB转串口芯片跑到921600bps也就是约115KB/s根本传不完。两个方案一个是在调试阶段降低采样率比如把MCLK和SCLK配成8kHz这样8路总数据率降到256KB/s勉强可以在高波特率下传一部分另一个是只回传其中一路或两路数据先确认通道映射和波形正确再扩展到全部通道。串口发送建议用DMA但要注意HAL_UART_Transmit_DMA连续发送时如果上一次还没发完就调用下一次有的MCU会直接返回Busy导致你只看到第一包数据就再也不发了。所以要么加一个发送完成标志位在HAL_UART_TxCpltCallback里置位下一次发送前等待标志位要么用串口空闲中断加DMA的环形发送方案但那个写起来复杂不少调试阶段没必要。6.2 PC端Python解析PCM数据电脑端用Python接收串口数据并解析是最灵活的验证方式。假设你发送时每帧按通道顺序排列每个通道4字节那么接收端按帧切分即可。import serial import numpy as np ser serial.Serial(COM3, 921600, timeout1) CH_NUM 8 FRAME_SIZE CH_NUM * 4 raw ser.read(FRAME_SIZE * 100) data np.frombuffer(raw, dtypenp.uint32) data data.reshape(-1, CH_NUM) samples_signed data.astype(np.int32) 8这里强制把32位无符号改成有符号然后右移8位对应4.3里左对齐提取16位有符号实际是左对齐24位。你也可以用struct模块自己解析成字节再拼但NumPy的frombuffer是最快的。解析完之后用matplotlib画波形每个通道一张图看看应该输出正弦波的通道是不是正弦波没接信号的通道是接近零电平还是杂散噪声。如果通道1和通道2波形完全相同那就要怀疑是不是数据线接错或者ADC内部通道映射配置不对。6.3 实测波形和常见异常判断每一路都接一个1kHz正弦信号源采出来应该是干净的正弦波幅值在满量程范围内。如果波形包络出现周期性起伏可能是有50Hz工频干扰串进去了检查模拟输入线是否过长、是否靠近电源走线。如果8个通道里只有偶数通道有信号奇数通道全是0大概率是TDM的Slot Enable只勾了偶数时隙或者SAI时钟同步到了某个两时就处理的模式。回去查CubeMX的Number of Slots和Slot Enable配置确保8个时隙全部打开。如果波形是方波而不是正弦波可能是输入信号过大导致ADC削波也可能数据提取符号处理有问题比如符号扩展错误导致正半周被变成负半周。把原始值打印出来看看确认是不是最大值和最小值颠倒了。我在实际测试中习惯给每个通道输入不同频率的信号比如通道1给1kHz、通道2给2kHz、通道3给3kHz这样一次性就能确认8个通道的映射关系是否正确。如果通道3采出来是2kHz那说明通道映射整体移位了一个Slot该查FS的极性或者Frame Sync Offset了。这个内容后面还可以继续扩展。比如把TDM8改成TDM16接两颗8通道ADC级联能做16通道的同步采集或者把SAI1_Block_A和Block_B都用起来每个block处理4通道用内部同步保证两路相位一致甚至可以在STM32内部加一个简单的FIR滤波器在DMA回调里直接对8路数据做实时处理做成一个完整的音频前端。串口传输验证只是第一步真正工程化时建议优先考虑SD卡写入或者USB Audio Class这些都能直接基于这篇文章的双缓冲结构继续改。