拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32 SAI TDM模式实现8通道麦克风阵列音频采集的完整指南

STM32 SAI TDM模式实现8通道麦克风阵列音频采集的完整指南 接到一个“8通道麦克风阵列音频采集”的需求时最先摆到桌面上的问题不是算法而是数据怎么搬进MCU。我最终选了STM32的SAI接口配TDM模式再用DMA双缓冲把8路音频数据流水线式地送到内存整套系统跑起来之后稳到让我意外。网上聊SAI的帖子不算少但真正把TDM模式、时隙配置、DMA乒乓缓冲这些细节串起来讲透的不多这篇就把我实际调试中踩过的坑和验证过的方法完整写出来给准备做多通道音频采集、麦克风阵列或者声学监测的朋友一个可复现的参考。这条路适合谁你至少用过HAL库跑过GPIO和串口知道CubeMX怎么配时钟树但对SAI和DMA双缓冲还不熟。项目里要采多路音频又不想堆一堆外设和飞线那这套方案值得花两天时间搞清楚。1. 方案选型为什么8通道采集必须走SAI TDM1.1 需求拆解8通道音频采集真正难在哪先说需求本身。8路模拟麦克风、采样率48kHz、每路24bit精度有的场景还要做到各通道严格同步因为后续要做延时求和波束成形或者声源定位通道间的相位差必须稳定可信。8通道听起来只是量变实际上把常规方案全逼到了墙角。如果用STM32内部ADC去采模拟麦克风你得外接8路独立的采样保持电路或者轮询采样通道间采样时刻根本对齐不了而且MCU内部ADC的ENOB做音频采集也偏弱。如果拆成两片4通道ADC芯片分别接你又要解决两片ADC之间的时钟同步和帧同步问题。更麻烦的是中断压力48kHz采样率下每帧只有20.8微秒8通道数据如果全靠CPU在中断里搬运主循环基本什么都干不了。1.2 I2S不够用SAI TDM才是正解I2S天生是双声道协议左右声道靠FS电平高低区分。想传8路你得在板子上堆4颗I2S芯片每颗芯片的MCLK、BCLK、FS还要保证严格同源否则通道间相位就开始飘。这种方案硬件复杂、PCB走线痛苦、软件还要做多路同步校准我是直接放弃的。SAI接口的TDM模式是为了多路时分复用而生的一根数据线在一个音频帧内按固定顺序传8个slot的数据每个slot就是一个通道。FS信号只负责标记帧的起始不再承担左右声道选择功能。对STM32来说这意味着只用一组外设引脚配合一颗支持TDM输出的多通道音频ADC芯片就能把8通道数据全部收进来。硬件连线少软件只维护一路DMA最关键的是8个通道的数据严格同步——它们本来就在同一帧里。1.3 数据量估算为什么DMA是刚需而不是可选先算一笔账这个数字在方案论证阶段就该摆出来。8通道、每通道采样深度32bitTDM时隙常用32bit装载24bit数据采样率48kHz总线数据率就是48kHz × 8 slot × 32bit 12.288Mbit/s ≈ 1.5MB/s一帧数据32字节每20.8微秒就要接收一帧。如果靠中断每来一个FIFO半满信号就去搬一次数据中断频率会到几十甚至上百kHz级别CPU占用率惨不忍睹而且中断响应抖动还会造成音频采样点间隔不均匀。DMA双缓冲的价值就在这里DMA自己把数据从SAI的FIFO搬到内存搬满一个缓冲区后触发一次中断通知CPU处理CPU处理这一块缓冲的同时DMA已经在往另一块缓冲写了。搬运和计算重叠起来CPU才有余力去做滤波、FFT或者网络发送。2. SAI接口与TDM模式的底层逻辑2.1 SAI接口到底是什么SAI是串行音频接口STM32里一般有SAI_A和SAI_B两个块。每个块都有独立的时钟线CK、帧同步线FS、数据线SD可以按全双工或半双工配置支持主从模式还能让A、B两个块同步工作。SAI并不仅仅是一个“更高级的I2S”。它的协议配置非常灵活支持I2S标准、左/右对齐、PCM/DSP、TDM甚至SPDIF和AC97。实际使用中TDM模式通常是在自由协议Free Protocol下把帧长、时隙数、时隙大小按需配置出来的。我用一个生活化的类比帮你理解TDM的工作方式。把数据线想象成一条单车道BCLK是司机踩油门的节拍FS是一声发令枪。枪响之后8台车依次通过这条车道1号车走第1个时间片2号车走第2个时间片依此类推。每一声枪响每个FS脉冲就是一次完整的8通道采样周期。SAI要做的就是告诉DMA哪些时间片的数据需要存下来每个时间片占多少位。2.2 TDM帧与时隙数据在线上是怎么排的一帧TDM数据的时间长度等于采样周期的倒数。采样率48kHz时一帧持续约20.8微秒。一帧内部被等分成长度相同的时隙时隙数必须等于或大于通道数。对于8通道应用最常见的配置是8个slot、每个slot 32bit整帧就是256个BCLK周期。在SAI内部时隙的使能是可以按位控制的。SLOTEN寄存器的每一位对应一个slot你只使能前8个后面的slot即使线上有数据也不会被收进来。这个特性在TDM级联或者部分通道未接时非常有用但也是通道错位的常见来源后面避坑部分会详细说。FS信号在TDM模式下只是一个帧起始标记重点看它的宽度和位置。I2S模式下FS通常是半帧高电平、半帧低电平用于区分左右声道TDM模式下FS往往是一帧开始时的一个短的脉冲典型宽度是1个BCLK周期。配置错极性或者错占空比codec和MCU之间就会出现“鸡同鸭讲”。2.3 必须搞懂的寄存器位FRL、NBSLOT、SLOTSZ、FBOFF这几个寄存器位决定了TDM帧怎么切分也是初始化时最容易配错的地方。FRLFrame Length在FRCR寄存器里定义整帧的BCLK数减1。8个slot、每个slot 32bit时帧长是256个BCLKFRL就填255。NBSLOT在SLOTR寄存器里定义时隙数减18 slot时填7。SLOTSZ定义单个slot的位宽可选8bit、16bit、24bit、32bit这里选32bit。FBOFF定义数据从slot起点偏移几个bit再开始有效许多音频ADC要求1 bit偏移那FBOFF就填1。这几个值之间必须自洽FRL加1要大于等于NBSLOT加1再乘以SLOTSZ对应的位数通常两者正好相等。我的习惯是先用电子表格算好帧长再填进CubeMX的Slot配置里而不是凭感觉随手填后面因为帧长和slot数不匹配导致的错位问题能少一半。3. 硬件方案与引脚连接设计3.1 8通道模拟前端外挂音频ADC芯片TDM模式必须有一个支持TDM输出的音频前端最常见的是多通道音频ADC芯片。我实际用过的两颗是TI的TLV320ADC6140和Cirrus的CS42448都是单芯片8通道ADC支持TDM8输出。控制接口走I2C或SPI数据输出走TDM单线硬件非常简洁。这里插一句芯片选型经验。TLV320ADC6140的动态范围和数字滤波配置更灵活适合录音和声学测量CS42448带DAC输出适合需要同时回放的设备。如果只做采集TLV320ADC6140更省电但它的TDM输出对齐方式需要在寄存器里配置初始化时多留一点时间看数据手册。CS42448的TDM模式通过外部引脚M0/M1选择开机即生效硬件调起来更快。3.2 另一种路线PDM数字麦克风阵列如果前端不想用模拟麦克风可以选PDM接口的数字MEMS麦克风。PDM麦克风直接输出1bit的高速率位流一片STM32的SAI接口在部分系列里带PDM硬件抽滤支持能直接接1到2路PDM麦克风。要接8路通常需要外部再加PDM解调芯片或者用两片支持4路PDM的MCU/FPGA复杂度反而上去了。我的建议是模拟麦克风方案更通用信号链成熟调试工具也好找PDM方案适合成本和空间敏感的极简设计但8通道PDM同步采集对硬件和驱动的压力都不小。这篇博文以模拟前端为主线。3.3 引脚连接一个可以直接抄的接线表以STM32F427的SAI_A和CS42448为例主从关系是STM32做主产生BCLK、FS、MCLKCS42448做从输出TDM数据。核心连接如下STM32引脚CS42448引脚作用SAI_A_CKBCLK位时钟由主机提供SAI_A_FSLRCK/FSYNC帧同步由主机提供SAI_A_MCLKMCLK主时钟部分codec需要SAI_A_SD_ASDTO串行数据输出从ADC到MCUGNDDGND/AGND共地CS42448的MCLK一般要求是采样率的256倍或512倍。48kHz采样时256fs对应12.288MHz512fs对应24.576MHz。这个频率直接用SAI的MCLK引脚输出配置MckOverSampling为256或512即可。如果codec不要求MCLK也可以不用SAI的MCLK输出能省一个引脚。4. SAI初始化实战从CubeMX到寄存器4.1 CubeMX里的关键配置用CubeMX开工程时先选好芯片然后在左侧外设列表里找到SAI启用SAI_Block_A。按下面这个思路去选Audio Mode选Master Receiver表示STM32作为主机接收数据由它产生BCLK和FS。Protocol选Free Protocol这一步很关键只有自由协议下才能把帧结构拆成8个slot来用。Data Size选24bit因为ADC输出的是24bit采样值。FIFO Threshold选Half让FIFO半满时触发DMA请求兼顾实时性和DMA搬运效率。接着配置Slot部分。Slot Size选32bitSlot Number选8Slot Active全部使能First Bit Offset根据codec要求先填0。我在CS42448上实测是0在TLV320ADC6140上则需要按它的TDM offset寄存器再做微调通常也是0或1。时钟树里SAI时钟源选PLLSAI或者外部时钟输入目标频率按“采样率×帧长”来算。48kHz采样、8 slot、32bit时BCLK是12.288MHz如果MCLK走256fs那SAI主时钟输出也是12.288MHz。CubeMX会帮你自动计算PLL参数但外部晶振一定要接别用内部HSI音频时钟对频率精度很敏感HSI温漂会让采样率肉眼可见地偏。4.2 HAL库初始化参数逐项说明CubeMX生成的初始化代码结构大致是下面这样关键参数我加了注释SAI_HandleTypeDef hsai_BlockA; hsai_BlockA.Instance SAI_Block_A; hsai_BlockA.Init.AudioMode SAI_MODEMASTER_RX; // 主机接收 hsai_BlockA.Init.Synchro SAI_ASYNCHRONOUS; // A块独立工作 hsai_BlockA.Init.OutputDrive SAI_OUTPUTDRIVE_DISABLE; hsai_BlockA.Init.NoDivider SAI_MASTERDIVIDER_ENABLE; hsai_BlockA.Init.FIFOThreshold SAI_FIFOTHRESHOLD_HF; // FIFO半满触发DMA hsai_BlockA.Init.AudioFrequency 48000; // 采样率 hsai_BlockA.Init.MckOverSampling SAI_OVERSAMPLING_256; // MCLK48k*256 hsai_BlockA.Init.Protocol SAI_FREE_PROTOCOL; // TDM必须用自由协议 hsai_BlockA.Init.DataSize SAI_DATASIZE_24BIT; // 每通道有效数据24bit hsai_BlockA.Init.FrameSynchro SAI_FRAMESYNC_OUT; hsai_BlockA.Init.FrameSyncPolarity SAI_FRAME_SYNC_ACTIVE_HIGH; hsai_BlockA.Init.FrameSyncOffset SAI_FRAME_SYNC_FIRSTBIT; hsai_BlockA.Init.FrameSyncLength SAI_FRAME_SYNC_LENGTH_1BIT; // FS宽度1个BCLK hsai_BlockA.SlotInit.FirstBitOffset 0; // 数据从slot第0位开始 hsai_BlockA.SlotInit.SlotSize SAI_SLOTSIZE_32BIT; // slot位宽32bit hsai_BlockA.SlotInit.SlotNumber 8; // 8个时隙 hsai_BlockA.SlotInit.SlotActive SAI_SLOTACTIVE_0 | SAI_SLOTACTIVE_1 | SAI_SLOTACTIVE_2 | SAI_SLOTACTIVE_3 | SAI_SLOTACTIVE_4 | SAI_SLOTACTIVE_5 | SAI_SLOTACTIVE_6 | SAI_SLOTACTIVE_7;不同系列HAL库的字段名会有一点差别比如有些芯片没有FrameSyncLength这个选项直接用寄存器设置FSALL。但核心逻辑一致FS要设为短脉冲、数据从slot起始位开始、时隙数和帧长匹配。4.3 初始化顺序的坑DMA必须先于SAI就绪很多人踩过一个现象程序烧进去第一帧数据是乱的之后才正常。这多半是SAI已经使能开始接收但DMA还没有准备好接管FIFO导致FIFO溢出丢了一部分数据。正确的初始化顺序是先初始化GPIO再初始化DMA并准备好接收缓冲最后才使能SAI并启动DMA接收。如果直接调用HAL_SAI_Receive_DMA函数内部会先配好DMA再打开SAI顺序上是安全的但如果你手动改了外设初始化或者在一开始先用HAL_SAI_Receive接收了一帧这个坑就很容易再踩回来。我建议初始化接口里只保留DMA接收的启动不要额外做一次阻塞式接收测试除非你有意要验证FIFO标志。5. DMA双缓冲把带宽留给DMA把时间留给CPU5.1 单缓冲为什么不行单缓冲模式下DMA写满一个缓冲区后触发传输完成中断CPU必须在下一帧数据到来之前把缓冲区里的数据搬走否则DMA会直接覆盖正在处理的数据。48kHz采样时这个时间窗口只有20.8微秒想在中断里做完数据拷贝、格式转换、甚至送FFT几乎是mission impossible。双缓冲做了一个最简单有效的解耦把内存分成两块DMA写一块的时候CPU处理另一块DMA写满后自动切到另一块同时告诉CPU上一块已经写完。两块缓冲轮流倒班谁也不用等谁。只要CPU处理一块的平均时间少于20.8微秒系统就不会丢数据。5.2 方案ADMA循环模式加双区乒乓最稳妥我在F4系列上最常用的实现方式是把DMA配成循环模式并把缓冲区大小设成两帧数据然后利用DMA的半传输中断和传输完成中断来区分前后两个半区。内存里声明两块独立的缓冲区#define SAI_FRAME_WORDS (8) /* 8通道每通道1个字(32bit) */ #define SAI_DMA_LEN (2 * SAI_FRAME_WORDS) /* 双缓冲总长度 */ static volatile uint32_t dma_rx_buf[SAI_DMA_LEN] __attribute__((aligned(32)));DMA启动时把整个dma_rx_buf当作一个连续缓冲区传输长度设为16个字。这样DMA从缓冲区起始写到中间位置时触发半传输中断也就是第一帧数据完成了继续写满到末尾时触发传输完成中断第二帧数据完成。循环模式下DMA写满末尾后地址自动回绕到开头又开始写第一帧如此循环。回调函数里对应处理两块区域void HAL_SAI_RxHalfCpltCallback(SAI_HandleTypeDef *hsai) { /* 前半区 dma_rx_buf[0..7] 已经被DMA填满 */ process_audio_frame((uint32_t *)dma_rx_buf, 0); } void HAL_SAI_RxCpltCallback(SAI_HandleTypeDef *hsai) { /* 后半区 dma_rx_buf[8..15] 已经被DMA填满 */ process_audio_frame((uint32_t *)dma_rx_buf 8, 0); }实际使用中process_audio_frame内部不要做耗时操作只把数据压入自己的环形缓冲区或者直接拷一份到业务缓冲区然后置一个标志位让主循环去处理。音频数据要保证实时性但这不代表所有处理都要堆在中断里中断时间越短系统越稳。5.3 方案BDMA DBM模式真双缓冲的寄存器操作如果你希望两块缓冲区物理上隔开不想依赖“一个连续大缓冲区的两个半区”可以用DMA的双缓冲模式DBM。这个模式在F4/F7/H7的DMA流上是支持的但HAL库封装不完整需要手动设置DMA_SxCR的DBM位。配置思路是先把DMA初始化成循环模式然后给DMA流传两个内存地址M0AR和M1AR打开DBM位。每传完一组数据DMA自动在M0和M1之间切换CT位指示当前使用的是哪个内存区。在传输完成中断里读CT位就能知道刚刚填满的是哪个缓冲区。示例伪代码如下/* 两块独立的32字节对齐缓冲区 */ static volatile uint32_t dma_buf0[8] __attribute__((aligned(32))); static volatile uint32_t dma_buf1[8] __attribute__((aligned(32))); /* DMA初始化时先做常规配置Mode设为DMA_CIRCULAR */ /* 手动设置双缓冲寄存器 */ DMA2_Stream1-M0AR (uint32_t)dma_buf0; DMA2_Stream1-M1AR (uint32_t)dma_buf1; DMA2_Stream1-NDTR 8; /* 每轮传8个字 */ SET_BIT(DMA2_Stream1-CR, DMA_SxCR_DBM); /* 使能双缓冲 */ /* 传输完成中断里判断CT位 */ void DMA2_Stream1_IRQHandler(void) { if (DMA_GetFlagStatus(DMA2_Stream1, DMA_FLAG_TCIF1)) { if (READ_BIT(DMA2_Stream1-CR, DMA_SxCR_CT) ! 0) { /* CT1说明DMA下一次要写buf1当前填满的是buf0 */ process_audio_frame((uint32_t *)dma_buf0, 0); } else { /* CT0说明DMA下一次要写buf0当前填满的是buf1 */ process_audio_frame((uint32_t *)dma_buf1, 0); } } }DBM模式里半传输中断的语义比较绕我建议只用传输完成中断通过CT位判断。这个模式的好处是两块缓冲可以分散在内存的不同位置适合配合MPU把某一块标记为不可缓存坏处是绕开了HAL初始化细节都得自己控。如果项目不是特别在意内存布局方案A已经够用。5.4 缓冲大小、内存对齐与Cache的隐藏规则DMA缓冲区的大小必须与DMA传输的数据宽度对齐。这里所有数据都按32bit字传输每块缓冲至少要按4字节对齐。我习惯直接32字节对齐一方面是为了H7等带D-Cache芯片的Cache line对齐另一方面也给编译器优化留出空间。如果你的芯片是H7这类带D-Cache的DMA往内存写数据时CPU读到的可能是Cache里的旧值这是新手最容易懵的坑。解决方法是在每次DMA填充完一块缓冲后CPU读取之前执行一次Cache无效化SCB_InvalidateDCache_by_Addr((uint32_t *)dma_buf, 32);如果不能接受在中断里做Cache维护也可以在MPU里把DMA缓冲区所在SRAM区域配置成非Cacheable牺牲一点CPU访问速度换取整个系统的简单可靠。6. 从缓冲区到8个通道数据拆包与主流程设计6.1 TDM字节流到通道样本的映射DMA缓冲里存的不是“通道排列整齐的音频数组”而是一帧内按slot顺序排列的原始数据。以8通道、每个slot 32bit为例缓冲区前4字节是通道0接下来4字节是通道1依此类推。如果ADC输出的是24bit数据装在32bit的slot里要分两种情况看。左对齐时24bit有效数据在32bit字的高24位低8位是0提取通道样本时右移8位int32_t sample_left (int32_t)(buf[ch] 8);右对齐时有效数据在低24位提取时要掩码并做符号扩展int32_t raw (int32_t)(buf[ch] 0xFFFFFF); if (raw 0x800000) { raw | 0xFF000000; /* 有符号数扩展 */ } int32_t sample_right raw;实际用哪种对齐取决于ADC芯片的寄存器配置和SAI的FBOFF设置。调试时用一路已知的正弦波输入分别用两种方式解出来看波形非常快就能判断。6.2 字节序陷阱STM32是小端别被字节指针坑了SAI数据线传的是串行位流进入FIFO后按32bit字存到DR寄存器。DMA把DR寄存器的值写入内存时STM32是小端模式也就是说一个字的低字节在低地址。你用uint32_t指针读数据没问题但如果用uint8_t数组去逐字节拼很容易被字节序绕晕。我踩过的一个典型问题是用uint8_t指针把缓冲区的字节导出来看了下发现高位字节和低位字节顺序“反了”以为数据错了实际上uint32_t视角下完全正常。记住一点音频通路上的数据处理全程用uint32_t或者int32_t类型不要在字节层做手工拼接除非你有明确的协议需求。6.3 中断回调里只搬运别做大计算DMA的HT/TC回调触发频率等于采样率48kHz下每20.8微秒一次。如果在回调里做浮点运算、打印日志、刷屏幕任何一个中等复杂度的操作都可能超过这个时间预算。我一个血的教训是在调试时往回调里塞了句printf结果整个采集链路直接被拖死。正确的做法是把回调当“门铃”它只负责告诉主循环“这块缓冲的数据可以用了”并把数据复制到业务环形缓冲区或者DSP输入缓冲。主循环看到标志位以后再慢慢做滤波、FFT、打包发送。如果连memcpy都觉得占时间可以改成指针交换把处理缓冲的指针和填充缓冲的指针互换这样零拷贝速度最快。6.4 采样率稳定性外部晶振是底线TDM数据的真实采样率由SAI的时钟源决定。如果时钟源是内部HSI温度一变化频率就跑ADC采样率跟着漂你的音频数据会变调。音频应用必须用外部无源晶振或晶振模块做HSE再通过PLLSAI或者PLLI2S给SAI提供精确的12.288MHz附近的时钟。CubeMX时钟树里能看到PLLSAI的最终输出我建议把实际算出来的BCLK频率和MCLK频率都写进初始化注释里方便后来人核对。一个能让采样率误差降到非常低的配置是外部晶振25MHz用PLLSAI的输出配置成12.288MHz作为SAI时钟源让SAI内部不分频直接拿它当BCLK。这样48k×256的MCLK正好也是12.288MHz一个频率同时满足数据和主时钟。7. 调试避坑实录这些坑我替你踩过了7.1 全静音/全零先怀疑FS极性和数据线连接如果8个通道全是0先别急着调软件。用逻辑分析仪抓SAI的FS、BCLK、SD三根线确认ADC芯片是不是真的在输出数据。很多时候问题出在FS极性反了SAI把FS设置为高有效但codec认为低有效两边都等不到自己的帧起始数据就一直静音。还有一个低级但高频的坑SAI的SD_A引脚复用错了。F4上同一个引脚可能有多个复用功能GPIO配置成SAI功能后还要确认复用的AF号正确否则数据线根本没接进SAI模块。7.2 通道错位一个正弦波就能定位如果数据不是全0但通道和预期对不上比如正弦波出现在通道3而不是通道0基本可以判断是slot映射错位。常见原因有三个FBOFF偏了一位、只使能了部分slot、ADC芯片的TDM输出offset没有和MCU侧对齐。调试方法很简单只给麦克风输入1的通道接一个1kHz正弦波其余通道悬空或接静音。然后看采回来的8通道数据哪个通道有波形就知道实际偏移了几个slot。偏移1个slot就把FirstBitOffset或者ADC的TDM offset改1偏移多了检查SLOTEN和SlotNumber配置。7.3 串音/数据混乱DataSize和SlotSize不匹配串音现象是通道0的波形在通道1里也隐约出现或者相邻通道数据高几bit混在一起。这种问题多半是DataSize和SlotSize没有对上。SAI的DataSize定义每个有效采样的位数SlotSize定义每个slot的宽度两者可以不同但必须匹配ADC的输出格式。举个例子ADC输出24bit数据SlotSize设成32bit没问题但如果DataSize也设成32bit而ADC只输出24bit那每个slot里读取的32bit中有8bit是无效的而且位对齐很容易错开。最稳的办法是硬件和芯片支持的情况下把DataSize和SlotSize都设成32bit让ADC在TDM模式下输出32bit字宽低8位补0软件上就免去了对齐问题。7.4 DMA中断风暴或者卡死查FIFO阈值和NVIC优先级DMA中断频繁到程序卡死先查SAI的FIFO Threshold。如果阈值配成“空”或者“四分之一”DMA请求非常密集中断几乎连续触发CPU被完全占满。把阈值设到半满或者接近全满DMA单次请求搬运的数据量更大中断频率成倍下降。另一个相关的点是NVIC优先级。DMA传输完成中断和半传输中断的优先级不要设得太低否则它们在响应之前被其他中断打断下一个DMA请求又来了FIFO可能溢出。我的习惯是把DMA中断优先级放在NVIC的中高优先级回调里只做轻量处理重活在主循环。7.5 只有第一包数据正常后面全停DMA模式设错了如果程序跑起来第一帧数据正常之后就再没有新数据大概率是DMA的Mode设成了Normal而不是Circular。Normal模式下DMA传完设定长度就停止工作不会再响应SAI的请求。循环模式才会自动回绕持续填充音频流。这属于配置类的小坑但出现频率非常高。7.6 H7芯片上的数据不更新D-Cache惹的祸把同一套代码从F4挪到H7最常见的问题就是DMA明明在写内存CPU看到的数据却一直是旧值或者数据偶尔更新、偶尔不更新。这是H7的D-Cache造成的缓存一致性问题。方案有两个一是在每个DMA完成回调里对对应缓冲区调用SCB_InvalidateDCache_by_Addr二是用MPU把DMA缓冲区地址区配成non-cacheable。我个人偏向后一种因为音频系统里DMA缓冲本来就是DMA和CPU交替读写与其每次都要记得invalidate不如直接从缓存策略上规避代码也更干净。7.7 快速定位问题的一招先降频再调试遇到复杂的时序问题我会把采样率从48kHz降到8kHz观察帧结构是否依然正确。频率降低后每个BCLK周期变长逻辑分析仪抓时序更容易FIFO溢出概率也小很多。如果8kHz下8通道全部正常再把采样率逐步拉回48kHz问题一般就出在时序紧张或者时钟配置上而不是TDM本身。8. 再补一刀几个别人不会明说的细节调试TDM音频时逻辑分析仪真的比示波器好用。示波器适合看BCLK和FS的波形质量但要看8个slot的数据内容还是逻辑分析仪更方便一次抓一帧256个BCLK按协议解析直接能看到每个slot的二进制值通道错位和极性问题是秒杀的。联调时我的习惯是先用单通道正弦波逐路标定。把8路输入依次接到同一个信号源上每路记下出现波形的实际slot号做一个映射表。这个表不仅验证了软件配置还能反向验证硬件焊接有没有问题。我第一次调CS42448时就是因为PCB上有一颗电阻贴错导致通道2和通道4的slot对调如果没有这张表排查起来要费好几倍时间。最后提醒一句如果是采集和回放同时做的设备可以用SAI_A和SAI_B两个块配置成同步模式这样ADC采样和DAC回放共用一套时钟不会出现采播不同步导致的回声和啸叫。我在一次免提设备项目里就用这个方式替代了外部的时钟同步逻辑效果稳定省了不少事。多通道音频采集这条路只要把TDM帧结构在脑子里理清DMA双缓冲跑顺后面再加算法就会非常顺手。
返回列表