
距离上一篇《数字麦克风PDM信号采集与STM32 I2S接口应用一》写完差不多一个多月。那篇里我把PDM数字麦克风接到STM32的I2S接口跑通了CubeMX工程和DMA搬运很多朋友留言说“能出声了但声音不太对”有的嘶嘶响有的音量小得离谱有的听起来像机器人。这篇就把这些“能出声但不好用”的问题一次性说清楚重点讲采样率怎么算、为什么接收位宽建议用32位、CIC抽取滤波器到底怎么写以及我踩过的几个坑。先说明一下这套方案主要适合两类人一类是手上没有带DFSDM外设的STM32比如F1/F4系列但又想接PDM数字麦克风另一类是项目里已经把I2S口占用了想通过软件抽取的方式复用I2S来采集PDM信号。整个过程不依赖特殊外设只要芯片有I2S、有DMA、主频别太低就能跑起来。1. 两类“数字麦克风”先分清别把PCM当PDM很多人买“数字麦克风”的时候其实没搞清楚自己买的是什么。市面上常见的数字麦克风模块从输出格式上分其实是两大类一类输出PDM单比特流另一类输出I2S格式的PCM数据。这两者在接线上看起来很像但处理方式完全不同搞混了后面全是坑。1.1 常见型号与接口差异我列一个对比表大家买完模块先对着看一下自己到底是哪一种类别典型型号输出内容需要抽取滤波吗接口形式PDM数字麦克风MP34DT05-M、ICS-43434、SPH0641LM4H1bit Sigma-Delta调制流需要MCU侧必须做抽取CLK DATA部分有L/R选择脚I2S/PCM数字麦克风INMP441、ICS-4343224bit PCM数字音频不需要拿来就是PCMBCLK WS DATAINMP441是很多入门教程里的常客它其实是一个I2S接口的PCM麦克风输出的是已经处理好的24位音频数据。如果你用的是INMP441那直接用STM32的I2S接收就行完全不需要CIC抽取滤波器。但如果你手里是MP34DT05或者ICS-43434这种纯PDM输出的麦克风接收到的原始数据是一串0和1的高速比特流必须自己想办法把它变成PCM这正是本篇要解决的核心问题。1.2 PDM信号为什么能靠I2S外设接收PDM的全称是Pulse Density Modulation脉冲密度调制。它的原理用一句话说就是用固定频率下“1”的密度来表示模拟信号的幅度。信号幅度高的时候输出的一串比特里1多幅度低的时候0多。这和PWM有些神似只不过PWM是调占空比PDM是调密度。那为什么PDM比特流能接到I2S接口上因为I2S外设本质上就是一个带帧同步的串行移位寄存器它并不关心线上跑的是PCM数据还是PDM数据只要按位把数据收进来就行。PDM麦克风输出的是1bit串行流I2S接收端只要在时钟边沿把这些bit全部采下来再通过DMA搬进内存任务就完成了一大半。后续的抽取、滤波、转PCM都是MCU软件的事。接线也简单。以常见的PDM麦克风为例把它的CLK脚接STM32的I2S_CK也就是BCLK输出脚DATA脚接I2S_SD如果有L/R选择脚按照模块手册接到对应电平决定它输出到左声道时隙还是右声道时隙。很多麦克风支持双麦克风共用一个数据引脚一个拉高一个拉低分别占用左右声道这样一根DATA线就能传两路音频。2. 采样率与CubeMX配置把“3.072MHz”算明白如果说第一篇是“让信号能进来”那这一篇的关键就是“让进来的信号数据量正确”。PDM采集最迷惑人的地方就在这里麦克风的物理采样率其实是几MHz但最终音频采样率只要44.1k或48k中间的抽取关系必须自己算清楚否则出来的声音不是变调就是噪声。2.1 采样率链路推导为什么一个字对应一个采样点我以最常见的48kHz采样率、I2S主模式接收为例把整条采样率链路拆开算一遍。目标音频采样率FS 48000Hz使用标准I2S Philips模式每个Frame一帧包含左右两个声道时隙每个时隙32个BCLK周期所以一帧共64个BCLK周期。I2S_CK输出频率就是BCLK FS × 64 48000 × 64 3072000Hz 3.072MHz这和很多PDM麦克风手册里推荐的主时钟范围是吻合的。PDM位流在3.072MHz下传输对于其中某一个声道来说这个声道只在32个BCLK周期内有效因此单个声道的有效PDM位率是f_PDM_ch BCLK / 2 1536000Hz 1.536MHz从1.536MHz抽取到48kHz抽取系数就是R 1536000 / 48000 32这个推导结果很重要在I2S接口场景下单声道的PDM抽取率不是64而是32。因为左右声道把一个Frame平分了每个声道拿到了一半的位。换句话说每接收一个32bit的声道数据就应该输出一个PCM采样点。这也是为什么我强烈建议在CubeMX里把I2S数据格式配置成32bit。使用32bit格式时每个声道的32位时隙恰好对应一个PCM采样周期一个字就是一份PDM位流处理起来非常清爽。如果用默认的16bit格式一个声道时隙里只有前16位是有效PDM数据后16位基本是0抽取的结果会带明显的直流偏置和音量损失。2.2 CubeMX配置清单从参数到DMA回调下面是我在STM32F407上验证过的一套配置其他系列大同小异重点看参数含义。配置项推荐值说明I2S2 ModeMaster Receive主模式由STM32产生时钟StandardI2S Phillips标准I2S时序Data Format32-bit让每个声道时隙完整承载PDM位流Audio Frequency48000 Hz用于CubeMX自动计算时钟分频Clock PolarityLow默认即可不对再调Clock SourcePLL I2S用PLLI2S不要直接用系统时钟DMA ModeCircular循环模式持续接收DMA Data WidthHalf Word或Word取决于芯片和HAL库版本DMA传输这里有个特别容易踩的细节STM32F4系列I2S的数据寄存器是16位的即使你配置了32bit数据格式HAL库里DMA缓冲区也是按半字管理的。也就是说一个声道的32bit时隙在DMA缓冲区里实际占了两个uint16_t接收顺序是先高半字、后低半字。开发的时候先用串口打印原始数据确认一下拼接顺序别默认所有芯片都一致。中断部分我建议开启DMA的半传输中断和完成中断两个中断回调分别处理缓冲区的前半块和后半块。缓冲区大小可以这样设目标是每块数据大约包含512个PCM采样点也就是512个左声道字加512个右声道字共2048个半字。DMA循环缓冲区总长度就是4096个半字半传输中断处理2048个半字约等于10.6ms的音频数据中断频率不到100HzMCU压力很小。2.3 DMA环形缓冲与半满/全满消费模型环形DMA的好处是采集和消费可以流水线化。当DMA写到缓冲区一半时触发半传输中断主循环处理前半块DMA继续写后半块等写满时触发完成中断再处理后半块。只要处理速度比DMA写入速度快音频流就是连续的不会有间隔。我习惯在中断回调里只做个标志位真正的CIC抽取放到主循环里做。理由很简单中断里做耗时计算容易干扰其他实时任务而且一旦加调试打印中断响应时间不可控。标志位方式最稳。volatile uint8_t g_data_ready 0; void HAL_I2S_RxHalfCpltCallback(I2S_HandleTypeDef *hi2s) { if (hi2s-Instance SPI2) { g_data_ready 1; } } void HAL_I2S_RxCpltCallback(I2S_HandleTypeDef *hi2s) { if (hi2s-Instance SPI2) { g_data_ready 2; } }主循环里根据标志位决定处理哪一块缓冲区。记住DMA一直在循环写入主循环处理完当前块之前DMA可能已经开始写下一块了所以必须保证“当前块”在DMA写回来之前处理完。缓冲区设计得够大、处理够快这个风险就很小。3. CIC抽取滤波器从1.536MHz降到48kHzPDM数据拿到手之后下一步就是抽取滤波。这一步是把3MHz级别的位流变成48kHz音频的关键也是很多教程一句话带过、但实际工作量最大的地方。3.1 为什么选CIC而不是FIR常用的抽取滤波器有两种FIR和CIC。FIR系数多每个输出样本要算几十上百次乘加在STM32这种没有硬件乘法器加速的单片机上跑3MHz数据率基本不现实。CIC滤波器是IIR结构但它的系数很简单整个计算过程只有加法和减法没有乘法非常适合MCU。CIC全称是Cascaded Integrator-Comb级联积分梳状滤波器。它的结构分两段前半段是积分器链后半段是梳状器链中间靠抽取来降低数据率。积分器负责累积低频信号梳状器负责抵消积分器的直流漂移两者配合就形成了一个高效的抽取低通滤波器。对单声道32倍抽取来说我用了3阶CIC。3阶的阻带衰减对于一般的语音采集、环境录音已经够用再高的阶数会显著增加积分器内部数据宽度和计算量收益不大。在168MHz主频下3.072M bit/s的数据率每个bit大约能分到50个主频周期CIC的3次加法和3次减法绰绰有余。3.2 3阶32倍抽取器的实现代码核心代码结构如下。我先把CIC的积分器和梳状器两个环节封装成一个函数然后对每个声道的32bit数据循环执行32次最后取梳状输出作为该声道的PCM样本。typedef struct { int32_t i1, i2, i3; // 积分器三个级 int32_t d1, d2, d3; // 梳状器延迟寄存器 } PDM_CIC; static int32_t cic_execute(PDM_CIC *cic, int32_t x) { // 积分级 cic-i1 x; cic-i2 cic-i1; cic-i3 cic-i2; // 梳状级 int32_t y1 cic-i3 - cic-d1; cic-d1 cic-i3; int32_t y2 y1 - cic-d2; cic-d2 y1; int32_t y3 y2 - cic-d3; cic-d3 y2; return y3; } int16_t cic_process_word(PDM_CIC *cic, uint32_t word) { int32_t acc 0; // 一个32bit声道字从MSB开始逐bit处理 for (int bit 31; bit 0; bit--) { // 把PDM bit映射为有符号值防止直流偏置 int32_t x (word (1u bit)) ? 1024 : -1024; acc cic_execute(cic, x); } // 归一化3阶CIC直流增益约等于32^332768 // 输入映射为±1024acc典型峰值约±33M右移10位后回到16bit范围 int32_t pcm acc 10; if (pcm 32767) pcm 32767; if (pcm -32768) pcm -32768; return (int16_t)pcm; }这段代码里最容易被忽略的是输入映射那一步。PDM位流本质上只有0和1如果你直接拿0和1去做积分整个输出会带一个很大的直流分量听起来就是持续的隆隆声。把0映射为负值、1映射为正值相当于把位流变成真正的有符号信号直流分量自然就消除了。另外要注意积分器内部变量一定用int32_t不能用int16_t。3阶CIC在抽取周期内累积的数值远超过16bit范围用窄类型会直接溢出出来的声音全是爆音。3.3 双声道处理和后续DC去除如果是双麦克风方案左右两个声道各需要一个独立的PDM_CIC实例千万不能共用同一个结构体。因为两个声道的位流是交替进入的共用状态会互相污染。第一个麦克风接左声道第二个麦克风接右声道数据引脚可以并在一起靠L/R选择脚区分时隙。处理DMA缓冲区时按偶奇索引拆开PDM_CIC cic_left, cic_right; void process_pdm_block(uint16_t *buf, uint32_t half_len) { // 假设每4个半字为一组左高、左低、右高、右低 for (uint32_t i 0; i 3 half_len; i 4) { uint32_t left ((uint32_t)buf[i] 16) | buf[i 1]; uint32_t right ((uint32_t)buf[i 2] 16) | buf[i 3]; int16_t pcm_l cic_process_word(cic_left, left); int16_t pcm_r cic_process_word(cic_right, right); // 写入PCM环形缓冲区 } }CIC抽取后的信号可能还有一个非常低的直流残余大部分场景不用管但如果你要用于精密测量可以再加一个一阶高通滤波器。一种简单的DC Block实现static int16_t dc_block(int16_t input) { static int32_t y_prev 0; static int32_t x_prev 0; int32_t y input - x_prev (y_prev - (y_prev 8)); x_prev input; y_prev y; return (int16_t)y; }这个高通滤波器的截止频率大约在几十Hz只是为了滤掉麦克风本身的直流偏差不会影响正常的语音和音乐频段。4. 把数据导出分析确认采样率和音质折腾完代码下一步一定是想办法验证输出到底对不对。经验是不要靠耳朵猜把PCM数据导出来用PC端工具做频谱分析一眼就能看出问题。4.1 用串口把PCM数据发到PC最简单粗暴的方式就是串口直出。把CIC处理后的PCM数据按小端格式通过串口发到电脑用串口助手存成bin文件然后用Python解析。// 主循环里把PCM缓冲区通过串口发送波特率至少921600 for (int i 0; i pcm_count; i) { uint8_t b[2]; b[0] pcm_buf[i] 0xFF; b[1] (pcm_buf[i] 8) 0xFF; HAL_UART_Transmit(huart1, b, 2, 10); }串口波特率越高越好921600波特率大约能传90KB/s48kHz双声道16bit数据是192KB/s直接实时传会来不及。我一般会降低要传输的音量先把数据存入一个大数组采集完一段后再统一发送或者只发送单声道这样带宽就够用了。4.2 用Python脚本转成WAV并看频谱PC端我习惯先用Python把bin数据读出来再转成WAV文件方便用Audacity或Adobe Audition直接听。转WAV的时候采样率必须写48kHz如果实际采集频率不是48kHz听感会不对但稍后用频谱就能查出来。import struct import numpy as np raw open(pcm.bin, rb).read() samples np.frombuffer(raw, dtypei2).astype(np.float32) samples / 32768.0 # 转成单声道WAV文件16bit import wave wav wave.open(output.wav, w) wav.setnchannels(1) wav.setsampwidth(2) wav.setframerate(48000) wav.writeframes((samples * 32767).astype(np.int16).tobytes()) wav.close()要想快速看频谱可以用numpy做FFT。比如让麦克风对着手机扬声器播放一段1kHz正弦波录几秒钟数据后做频谱分析import numpy as np import matplotlib.pyplot as plt samples np.frombuffer(raw, dtypei2).astype(np.float32) n len(samples) # 加汉宁窗减少频谱泄漏 window np.hanning(n) spec np.abs(np.fft.rfft(samples * window)) freq np.fft.rfftfreq(n, 1 / 48000.0) plt.semilogy(freq, spec) plt.axvline(1000, colorr, linestyle--) plt.xlabel(Frequency (Hz)) plt.ylabel(Amplitude) plt.show()如果正常频谱峰值应该出现在1000Hz附近。如果峰值出现在2000Hz附近说明抽取率少了一半实际采样率变成96kHz如果峰值出现在500Hz附近说明抽取率多了一倍实际采样率变成24kHz。这个现象在产品验证阶段特别有用能够迅速暴露配置错误。4.3 采样率偏了怎么办音频采样率偏差超过1%人耳就能明显感觉到变调偏高了声音变尖偏低了声音发闷。常见的采样率不准原因有两个。第一个是时钟树配置不对。I2S的时钟源必须用PLL I2S并且要按CubeMX的自动计算设置分频不能随便选。检查时钟树配置里I2S时钟是否真的接近3.072MHz很多看起来“差不多”的配置实际上差了百分之几。第二个是麦克风本身需要的主时钟频率和实际BCLK不匹配。PDM麦克风虽然一般支持较宽的主时钟范围但不同主时钟频率下内部调制器的信噪比会变化。如果BCLK偏离推荐值太多抽取出来的音频会明显劣化甚至出现电流声。5. 常见异常与排查实录这部分是我在实际调试过程中反复踩过的坑每一个都对应了一个真实的定位经过。建议直接把这张表收藏起来遇到问题逐条对照。5.1 数据全是0或全部是1先看I2S_CK引脚有没有时钟输出没有时钟就查CubeMX时钟树和I2S使能。再看DATA引脚的电平是否正确PDM麦克风在无信号时输出应该是接近50%占空比的随机翻转序列用示波器看是有变化的。如果DATA一直拉高或一直拉低多半是麦克风供电问题、L/R选择脚悬空、或者麦克风没有正常上电。其次检查共地。数字麦克风和STM32之间的地如果没有可靠连接数据线就是悬空状态采集结果几乎必然异常。这个问题在面包板搭电路时特别常见。5.2 有数据但嘶嘶响像广播噪声这种最典型的原因是抽取率不对。我遇到过有人把CIC抽取率按64倍设置结果BCLK配的是3.072MHz单声道有效位率只有1.536MHz64倍抽取后实际输出采样率变成24kHz。采样率减半后所有高频噪声全部混叠到低频段表现出来就是刺耳的嘶嘶声。解决方法是重新按第二节的推导过程算一遍BCLK除以目标采样率再除以2就是单声道的CIC抽取率。在绝大多数I2S接PDM的应用里这个值是32不是64。另外一个容易忽视的是数据格式。如果CubeMX里选的Data Format是16bit而代码按32bit消息处理数据位序就会完全错乱。确认你的DMA缓冲区分组方式和CubeMX配置完全对应。5.3 音量特别小或者一直有隆隆的直流声音量小大概率是CIC归一化系数没调好。我给出代码里的acc 10只是一个起始参考实际音量取决于麦克风灵敏度、输入映射幅度、抽取率三个因素。如果发现整体音量只有正常值的几分之一把右移位数减少1位就是2倍增益减少2位就是4倍增益。直流声多半是PDM输入没有做负向映射直接用0/1参与了积分。按前面说的把0映射为-1024而不是0立刻就能消除。还有一个低频隆隆声来源是时钟极性不匹配导致每个采样字里有效数据偏移了若干个bit看起来数据“有”但频谱上全是低频伪信号这种可以尝试把CubeMX里的Clock Polarity从Low改成High或者代码里把bit循环起始位置调整一位试试。5.4 DMA中断风暴与系统卡死问题在DMA中断回调里做CIC抽取、串口打印或者调用HAL_Delay是我见过最多的卡死原因。HAL_Delay依赖SysTick中断如果你的音频DMA中断优先级比SysTick高SysTick就永远得不到执行delay永远不会返回。这就是为什么有的朋友报“stm32延时函数delay卡死”的实际场景之一。遇到HardFault也别慌进去看CFSR寄存器的值。我之前调试时遇到0x00008200这种值它对应的是UsageFault多数情况下和未对齐访问、非法指令有关。优先查DMA缓冲区地址是否4字节对齐、回调函数里是否有数组越界。给DMA缓冲区加上对齐属性基本能解决一大半__attribute__((aligned(4))) uint16_t pdm_buf[4096];经验法则中断回调里只置标志位和记录状态CIC这类计算全部放到主循环。宁可主循环偶尔来不及处理也不要让中断处理时间过长影响整个系统的实时性。5.5 逻辑分析仪是最后的仲裁者靠耳朵猜问题效率很低。我调试PDM采集时示波器两个探头同时抓BCLK和DATA逻辑分析仪抓BCLK、WS、DATA三路信号确认下面的信息BCLK频率是不是3.072MHzWS翻转频率是不是48kHzDATA在WS低电平期间是否输出左声道数据高电平期间是否输出右声道数据每个声道时隙内DATA的有效bit数是不是32个。只要这三路时序和预期一致基本可以确定硬件和接口配置没问题问题在软件处理层。如果时序本身就不对那先解决接线和时钟配置再回来看CIC代码。逻辑分析仪采样率至少要设到24MHz以上否则抓3MHz的信号没有足够分辨率波形会失真。这些坑我基本都踩过一遍。一开始以为PDM有多神秘后来发现只要把时序、抽取率、缓冲区这三件事理顺剩下的就是体力活。如果你照着做还是不正常拿逻辑分析仪抓一下CLK、WS、DATA或者用串口打印一段未抽取的原始字出来看一眼通常一眼就能定位问题。数字音频采集这东西数据不会说谎波形也不会说谎。