基于STM32的声源定位装置:多路音频同步采集与GCC-PHAT时延估计
2026/9/20 8:06:04 网站建设 项目流程

简介:这是一份面向电子信息、自动化与嵌入式方向学生及工程人员的声源定位系统设计说明书,以西华大学毕业设计为背景,围绕STM32F103展开完整方案论述。内容从总体方案比较入手,分析声源信号的产生方式、声源选择依据与坐标解算策略,进而逐模块讲解555多谐振荡电路、电源电路、自动增益控制电路、有源二阶低通与高通滤波电路、STM32F103最小系统、液晶显示与电平转换电路的设计思路,并给出电源参数、555外围元件参数及音源坐标位置的数学模型与元器件选型过程。软件部分涵盖Keil、IAR、Altium Designer等开发与绘图环境介绍,主程序、液晶初始化、ADC初始化流程图,以及带通滤波器频率响应、多谐振荡器、ADC电压采集和VCA810电路的调测记录,还涉及STM32F103、ILI9320、VCA810等特殊器件的应用说明。资源包内为1个pdf文档,约4.23MB,已有335人学习,适合作为课程设计、毕业设计选题与嵌入式信号处理入门的参考范本。

1. 声源定位装置为什么先卡在采音这一环

会议室里几个人同时说话,摄像头要转头对准正在发言的那一位;车间里一台电机开始异响,维护人员想不拆机就判断声音从哪个方向来。这类需求落到硬件上,最常见的做法就是一块 STM32 加一圈麦克风。标题里的「基于 STM32 的声源定位装置」讲的就是这件事:用 MCU 完成多路音频同步采集、时延估计,最后输出一个方位角,通过串口交给上位机或者直接驱动云台。

它适合两类人:做过 stm32 项目、会点灯会串口,但没处理过多通道同步采样的人;以及想把声源定位从 PC 端搬到嵌入式端、又不想上 FPGA 的人。真正的门槛不在公式,而在采音——四路麦克风之间哪怕差几个采样点,TDOA(到达时间差)就被噪声吃掉了。后面按「原理选型—同步采集—算法落地—精度调优」的顺序展开,每一步都给出能直接抄的配置和代码。

2. 麦克风阵列几何与 TDOA 换算的选型依据

2.1 远场模型下时延差怎么换算成角度

声源距离远大于阵列尺寸时,到达阵列的声波近似平面波。一对间距为 d 的麦克风,声源入射角 θ(以阵列法线为 0°),两路信号的时间差满足:

tau = d · sinθ / c c ≈ 340 m/s

这个式子透露出两件事。第一,d 越大,同样的 θ 产生的时延差越大,单位采样点对应的角度越小,分辨率越高。第二,tau 的量级极小,d = 10 cm 时最大时延差不到 0.3 ms,48 kHz 采样下只有 14 个采样点——采样时钟稍微漂一点,估计值就飞了。

麦克风间距 d最大时延差 d/c@48 kHz 采样点数单采样点角度分辨率空间无混叠上限 c/(2d)
3 cm88 µs4.2约 13.5°5.7 kHz
10 cm294 µs14.1约 4.1°1.7 kHz
20 cm588 µs28.2约 2.0°850 Hz
30 cm882 µs42.4约 1.4°567 Hz

最后一列是关键约束。间距一旦超过半波长,高频分量在相位上就会出现周期模糊,互相关谱上冒出多个高度接近的峰。所以阵列不是越大越好,10~20 cm 是个常见折中:分辨率够看,同时把 3 kHz 以上的分量掩掉就能避开混叠。

2.2 为什么用 GCC-PHAT 而不是直接互相关

直接互相关对幅度敏感。两个麦克风的灵敏度差 3 dB、外壳开孔位置不同、房间混响一叠加,相关峰就会被削平甚至错位。GCC-PHAT 的做法是在频域把幅度信息全部丢掉,只留相位:

R(τ) = ∫ [X1(f)·X2*(f)] / |X1(f)·X2*(f)| · e^{j2πfτ} df

分母的归一化让每个频点权重相同,混响环境下的峰明显更尖锐,这是它在嵌入式声源定位里几乎成了默认选择的原因。代价是低信噪比频点也被同等放大,所以工程上一定要在分母加一个正则项 eps,并且对互谱做 300 Hz~3.5 kHz 的带通掩膜。

先在上位机用 Python 把这个流程跑通,再往 STM32 上搬,能省掉大量调试时间:

import numpy as np def gcc_phat(x1, x2, fs, max_delay, eps=1e-8, band=(300, 3500)): """返回时延(采样点,带符号)、互相关序列、对应时延轴""" n = 1 while n < len(x1) + len(x2): # 补零到 2 的幂,避免循环卷绕 n <<= 1 X1 = np.fft.rfft(x1, n) X2 = np.fft.rfft(x2, n) R = X1 * np.conj(X2) R /= (np.abs(R) + eps) # PHAT 加权 freqs = np.fft.rfftfreq(n, 1 / fs) mask = (freqs < band[0]) | (freqs > band[1]) R[mask] = 0 # 带通掩膜,去掉直流和高频混叠 r = np.fft.irfft(R, n) r = np.concatenate((r[-max_delay:], r[:max_delay + 1])) # 负时延挪到前半段 shift = np.argmax(r) - max_delay return shift, r, np.arange(-max_delay, max_delay + 1) / fs

n取大于两帧长度之和的 2 的幂,是为了让 IFFT 结果等价于线性卷积;max_delay按 2.1 节表格里的最大采样点数取值,一般再留 20% 余量;eps取 1e-8 到 1e-6,太小会让静音段产生伪峰,太大则退化成普通互相关。

2.3 阵列几何怎么选:线阵、L 阵还是圆阵

几何麦克风数可测维度优点主要问题
两麦线阵2一维角度电路最简单,两路 ADC 搞定前后锥面模糊,分不清正前方和正后方
四麦线阵4一维角度三对基线冗余,可解前后模糊仍然测不了俯仰
L 型(十字)阵4方位角 + 俯仰两条正交基线,二维定位两组基线长度必须一致,标定量翻倍
圆阵6 及以上全向方位角各方向精度均匀通道多,走线和同步成本高

四麦 L 阵是性价比最高的一档:两条正交基线各自算一个时延差,再atan2合成方位角。基线长度取相同值时,两个方向的误差特性一致,标定表可以共用一份。

2.4 采样率、帧长和频段限制怎么定

采样率两条路:16 kHz 够覆盖语音主能量(300 Hz~3.4 kHz),数据量和 Flash 占用小;48 kHz 分辨率高,但同样 1024 点只剩 21 ms,帧太短会让频域分辨率不足。帧长常用 1024 点,对应 16 kHz 下 64 ms,声源缓慢移动时估计滞后可以接受。

频段限制不是可选项。以 10 cm 间距为例,1.7 kHz 以上开始相位模糊,所以互谱上要加 300 Hz~3.5 kHz 的掩膜,顺手也把直流偏置和工频干扰滤掉了。这一步放在 STM32 上比放在 Python 上更容易被忽略,值得单独立个函数。

3. STM32 端四路音频同步采集的硬件与配置

3.1 模拟麦克风加 ADC,还是数字 I2S/PDM 麦克风

方案典型器件接口同步性CPU 开销备注
模拟驻极体 + ADCMAX9814 一类ADC 多通道同一触发源,硬件同步需偏置电路,通道间有增益误差
数字 MEMS(I2S)INMP441I2S / TDM共用时钟可同步STM32 一般只有 2~3 个 I2S,四路吃紧
PDM MEMSMP34DT01 一类PDM + 抽取滤波共用同一时钟,严格同步引脚省,适合 6 路以上

在 stm32 系列里挑带 3 个 ADC、且有足够定时器的型号,走「定时器 TRGO 触发 ADC 规则组 + DMA 循环搬运」这条路是最稳的:四路通道由同一个硬件事件发起转换,通道间偏差只有 ADC 的采样保持时间差,量级在百纳秒级,远小于我们关心的微秒级 TDOA。

3.2 定时器触发 ADC 加 DMA 双缓冲的配置骨架

/* TIM3:72 MHz 时钟,目标 fs = 16 kHz */ static void MX_TIM3_Init(void) { htim3.Instance = TIM3; htim3.Init.Prescaler = 0; htim3.Init.Period = 4499; /* (0+1)*(4499+1)/72MHz = 62.5us -> 16kHz */ HAL_TIM_Base_Init(&htim3); TIM3->CR2 |= TIM_CR2_MMS_1; /* MMS=010,更新事件输出到 TRGO */ HAL_TIM_Base_Start(&htim3); } /* ADC1:扫描 4 通道,外部触发,DMA 循环 */ static void MX_ADC1_Init(void) { ADC_ChannelConfTypeDef sConfig = {0}; hadc1.Instance = ADC1; hadc1.Init.ScanConvMode = ENABLE; hadc1.Init.ContinuousConvMode = DISABLE; /* 节奏交给定时器 */ hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T3_TRGO; hadc1.Init.DMAContinuousRequests = ENABLE; hadc1.Init.NbrOfConversion = 4; HAL_ADC_Init(&hadc1); sConfig.SamplingTime = ADC_SAMPLETIME_28CYCLES; sConfig.Channel = ADC_CHANNEL_0; sConfig.Rank = 1; HAL_ADC_ConfigChannel(&hadc1, &sConfig); /* CH1/CH2/CH3 同理,Rank 依次为 2/3/4 */ }

Period的算法就是fs = 时钟 / ((PSC+1)*(ARR+1)),反过来推 ARR。用 TRGO 而不是软件触发,是为了让触发周期严格等于定时器周期,抖动只有时钟源误差,而软件触发会被中断延迟污染。ScanConvMode打开后,每个触发沿转换 4 个通道,DMA 请求在最后一个通道转换完成时才产生,所以 DMA 缓冲里天然就是[ch0, ch1, ch2, ch3, ch0, ch1, ...]的交错排列,取数据时按 4 取模即可。

3.3 双缓冲中断里只做搬运信号,不做算法

DMA 配成循环模式、缓冲长度设为 4×1024×2(两个 1024 点帧),半传输和传输完成各给一个标志:

volatile uint8_t frame_ready = 0; /* 0=无, 1=前半, 2=后半 */ void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) { frame_ready = 1; } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { frame_ready = 2; } /* 主循环里 */ while (1) { if (frame_ready) { uint16_t *p = (frame_ready == 1) ? &adc_buf[0] : &adc_buf[FRAME_LEN * 4]; process_frame(p); /* 解交错、去直流、GCC-PHAT */ frame_ready = 0; } }

中断里只写一个标志位,绝不跑 FFT。一次 1024 点 FFT 在 M4 上要几百微秒到 1 ms,而 16 kHz 下 1024 点只有 64 ms 的采集时间,塞进中断很容易把下一次半传输冲掉,现象就是波形偶尔缺一段、角度随机跳变。

3.4 用串口把原始波形送到上位机核对

标定阶段一定要能看到原始波形。开一路 UART DMA,波特率 921600 或直接上 USB CDC,每帧发固定头 + 4 通道交错的原始数据。上位机画出来,同时播放一段已知方向的白噪声,看四路波形的相对延迟是否符合 2.1 节的公式。这一步能立刻区分「硬件同步没做好」和「算法有问题」——如果波形上就看不到时延,后面全是白忙。

4. GCC-PHAT 与角度解算在 STM32 上的落地

4.1 FFT 长度、窗函数与浮点/定点选择

帧长 1024、补零到 2048 点做 FFT,NFFT=2048。窗函数要慎用:加汉宁窗能压频谱泄漏,但会让两路信号的相关性结构发生变化,PHAT 的峰反而变钝,常见做法是只在信噪比很差时才加,且两路加同样的窗。

数据类型1024 点 FFT 耗时(M4 带 FPU,72 MHz)RAM 占用适用场景
float32约 0.6~1.0 ms每路约 8 KB开发调试、帧率要求不高
q15约 0.4~0.7 ms每路约 4 KB算力和 RAM 紧张,需处理缩放

手头有 CMSIS-DSP 就用arm_rfft_fast_f32,它的实数 FFT 打包格式和 numpy 不一样,不理解格式写出来的互谱一定是错的。

4.2 PHAT 加权和互谱的 C 实现要点

#define NFFT 2048 #define FRAME_LEN 1024 static float X1[NFFT], X2[NFFT], R[NFFT]; static arm_rfft_fast_instance_f32 fft_inst; void gcc_phat_frame(const float *x1, const float *x2, float *corr_out) { memset(X1, 0, sizeof(X1)); memset(X2, 0, sizeof(X2)); memcpy(X1, x1, FRAME_LEN * sizeof(float)); /* 补零到 2048 */ memcpy(X2, x2, FRAME_LEN * sizeof(float)); arm_rfft_fast_f32(&fft_inst, X1, X1, 0); arm_rfft_fast_f32(&fft_inst, X2, X2, 0); /* CMSIS 实数 FFT 打包格式:R[0]=Re0, R[1]=Re(Nyq), R[2k]=Rek, R[2k+1]=Imk */ for (int k = 0; k < NFFT; k += 2) { float a = X1[k], b = X1[k + 1]; /* 通道 1 的 re/im */ float c = X2[k], d = -X2[k + 1]; /* 通道 2 取共轭 */ float re = a * c - b * d; float im = a * d + b * c; float mag = sqrtf(re * re + im * im) + 1e-6f; /* 正则项,别省 */ R[k] = re / mag; /* PHAT:只保留相位 */ R[k + 1] = im / mag; } /* 带通掩膜:按 bin 索引直接把 300Hz 以下、3.5kHz 以上清零 */ int k_lo = (int)(300.0f * NFFT / FS_HZ); int k_hi = (int)(3500.0f * NFFT / FS_HZ); for (int k = 0; k < k_lo; k++) { R[2 * k] = 0; R[2 * k + 1] = 0; } for (int k = k_hi; k <= NFFT / 2; k++) { R[2 * k] = 0; R[2 * k + 1] = 0; } arm_rfft_fast_f32(&fft_inst, R, R, 1); /* 逆变换得到广义互相关 */ memcpy(corr_out, R, NFFT * sizeof(float)); }

三个容易写错的地方:一是取共轭时虚部要取反;二是正则项必须留,否则静音帧会给出满量程的伪峰;三是带通掩膜要在逆变换之前做,逆变换之后再滤就晚了。逆变换结果的前半段对应正时延、后半段对应负时延,取值时要按这个规则搬移。

4.3 从时延差到方位角的解算代码

#define C_SOUND 340.0f /* 声速 m/s */ #define D_BASE 0.12f /* L 阵基线,单位 m */ #define DEG 57.29578f float angle_from_tau(float tau_x, float tau_y) { float sin_x = tau_x * C_SOUND / D_BASE; /* tau 单位为秒 */ float sin_y = tau_y * C_SOUND / D_BASE; if (fabsf(sin_x) > 0.98f || fabsf(sin_y) > 0.98f) return NAN; /* 超出物理可能,判为无效帧 */ return atan2f(sin_y, sin_x) * DEG; }

sin_xsin_y超过 1 意味着算出的时延差对应的路径差比基线还长,物理上不可能,只能来自噪声或错峰。把这类帧直接丢掉,比强行输出一个角度再平滑要干净得多。

4.4 多帧平滑与野值剔除

单帧结果抖动三五度很正常,直接送出去云台会来回抽。常用两级处理:先做 9 点滑动中值滤波滤掉孤立野值,再用一阶低通平滑。

#define WIN 9 static float win[WIN]; static uint8_t win_cnt = 0; static float smooth_angle = 0.0f; float filter_angle(float raw) { if (isnan(raw)) return smooth_angle; /* 无效帧保持上一值 */ win[win_cnt++ % WIN] = raw; if (win_cnt < WIN) return smooth_angle; float tmp[WIN]; memcpy(tmp, win, sizeof(tmp)); /* 简单的选择排序取中值,9 个元素开销可忽略 */ for (int i = 0; i < WIN - 1; i++) for (int j = i + 1; j < WIN; j++) if (tmp[j] < tmp[i]) { float t = tmp[i]; tmp[i] = tmp[j]; tmp[j] = t; } float med = tmp[WIN / 2]; smooth_angle += 0.3f * (med - smooth_angle); /* 一阶低通,系数越小越稳 */ return smooth_angle; }

中值滤波的窗口别超过 9,声源真实移动时会引入明显滞后;低通系数 0.3 是响应速度和稳定性的折中,云台惯量大就调到 0.15 以下。

5. 标定方法与几个立刻见效的调参技巧

装置搭起来第一件事不是看角度输出,是标定。用一只小音箱固定在转台或带刻度的大圆盘上,分别在 0°、±30°、±60°、±90° 各发一段白噪声或扫频,记录每个真实角度对应的tau_xtau_y和最终角度,做一张误差表。同一条母线上两个方向分别标,能顺便发现 L 阵两组基线长度实际不一致的问题——PCB 上差 2 mm,20 cm 基线下就是 1% 的系统误差。

麦克风一致性同样要在标定阶段解决。把音箱放在正前方 0°,理想情况下两两时延差都应该是 0,如果某个通道稳定偏出两三个采样点,基本就是这个麦克风的灵敏度或者外壳开孔位置有差异,可以在代码里给每对通道加一个固定补偿量:

通道对正前方理想时延实测时延补偿值(采样点)
MIC0–MIC10+2.1−2
MIC0–MIC20−0.40
MIC1–MIC30+1.6−2

补偿值四舍五入到整数采样点即可,小数点后的部分留给中值滤波去抹。

几个真正会卡住人的坑,按踩中概率排:

  1. ADC 某一路读数恒为 0。八成是那个引脚复用了 JTAG/SWD 的默认功能。stm32 的 PA13/PA14/PA15/PB3/PB4 上电后归调试口所有,必须在初始化里关掉复用或者改用别的引脚,__HAL_AFIO_REMAP_SWJ_NOJTAG()一类调用要放在 GPIO 配置之前。
  2. 采样率不是精确的 16 kHz。ARR 想当然写成整数、或者时钟树里 APB1 分频和自己的假设不一致,都会让实际采样率和代码里的FS_HZ对不上,时延换算出来的角度整体偏。用定时器输出比较脚接示波器量一下触发周期,比反复怀疑算法快得多。
  3. 偏置电压共用造成串扰。几路驻极体的偏置电阻共用一条走线时,一路的电流波动会窜到其他通道,表现是四路波形高度相关。每路独立偏置、独立退耦电容是最省事的做法。
  4. 把 FFT 塞进 DMA 中断。前面提过,现象是波形偶发缺口、角度随机跳。中断只置标志,算法在主循环跑。

最后一个技巧:算完角度别急着输出,先做一次配对一致性校验。L 阵有四只麦克风、可组成 6 对基线,理论上这些配对给出的时延必须自洽。实际用其中 3 对冗余基线各算一次方位角,若三者两两相差小于 8°,取中值输出;否则判为无效帧保持上一角度。这一步能砍掉大部分由混响和突发噪声引起的错误跳变,代价只是多做两三次 2048 点 FFT。

本文还有配套的精品资源,点击获取

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

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

立即咨询