☰
STM32 I2S接口采集PDM麦克风:采样率计算与CIC抽取滤波器实现
2026/10/5 7:41:17 网站建设 项目流程

距离上一篇《数字麦克风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 + DATA

INMP441是很多入门教程里的常客,它其实是一个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的音频数据,中断频率不到100Hz,MCU压力很小。

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降到48kHz

PDM数据拿到手之后,下一步就是抽取滤波。这一步是把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^3=32768 // 输入映射为±1024,acc典型峰值约±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/s,48kHz双声道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, dtype='<i2').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, dtype='<i2').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, color='r', 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.536MHz,64倍抽取后实际输出采样率变成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.072MHz;
  • WS翻转频率是不是48kHz;
  • DATA在WS低电平期间是否输出左声道数据,高电平期间是否输出右声道数据;
  • 每个声道时隙内DATA的有效bit数是不是32个。

只要这三路时序和预期一致,基本可以确定硬件和接口配置没问题,问题在软件处理层。如果时序本身就不对,那先解决接线和时钟配置,再回来看CIC代码。逻辑分析仪采样率至少要设到24MHz以上,否则抓3MHz的信号没有足够分辨率,波形会失真。

这些坑我基本都踩过一遍。一开始以为PDM有多神秘,后来发现只要把时序、抽取率、缓冲区这三件事理顺,剩下的就是体力活。如果你照着做还是不正常,拿逻辑分析仪抓一下CLK、WS、DATA,或者用串口打印一段未抽取的原始字出来看一眼,通常一眼就能定位问题。数字音频采集这东西,数据不会说谎,波形也不会说谎。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询