简介:这份基于STM32的声源定位装置毕业设计说明书,面向电子信息、嵌入式与自动化方向的学生及工程师,聚焦如何用STM32F103搭建可定位声源的软硬件系统。全文从总体方案比较入手,依次覆盖555多谐振荡器、电源、AGC、有源高低通滤波、STM32F103最小系统、液晶显示与电平转换等单元电路,并给出参数计算、元器件选型、ILI9320与VCA810等器件说明,以及软件流程图和系统调试记录。资源包仅含1个PDF文件,压缩后约4.23MB,便于直接查阅与打印。已有335人学习,适合作为课程设计、毕业设计或嵌入式竞赛的参考模板,可借此理解声源信号采集、滤波放大、ADC采样、坐标解算和结果显示的完整链路,也能为电路调试与论文撰写提供可复用框架。
1. 从四个驻极体话筒到一组超定方程:这套STM32声源定位装置到底在做什么
多数人第一次看到"声源定位"会条件反射想到麦克风阵列加波束成形,动辄要十几路同步采样和FPGA做互相关。这套西华大学的毕业设计走的完全是另一条路:四个驻极体话筒摆在矩形四个角,用一个555多谐振荡器发出定频方波当"声源靶子",靠测四个通道之间的到达时间差反推坐标,主控只用一颗STM32F103。听起来简陋,但它把被动声测的几个核心问题——信号增益随距离剧烈变化、带外噪声淹没有效频点、时差测量分辨率不足——全都摆到了台面上,逼着你一个个去解。它适合两类人:一是正在做嵌入式毕业设计、需要一份硬件链路完整可复现的参考;二是想搞明白TDOA定位从模拟前端到解方程全链路的人。下面按信号怎么产生、怎么调理、怎么采样、怎么解算、怎么验证的顺序拆开讲。
2. 声源激励与模拟前端:555多谐振荡器、AGC与两级有源滤波
2.1 用555定时器把方波频率钉在500Hz
方案对比里提到过用STC89C51的定时器翻转IO口产生方波,但那样占用一整个单片机只为发一个音,性价比确实低。555接成多谐振荡器只需要两个电阻一个电容,输出占空比和频率都能算得死死的。核心公式是充电时间tpH=0.7(R1+R2)C、放电时间tpL=0.7R2C,振荡频率:
f = 1.43 / ((R1 + 2*R2) * C)按正文,C取100nF、令R1=R2=R,要得到500Hz:
R = 1.43 / (3 * f * C) = 1.43 / (3 * 500 * 100e-9) ≈ 9533 Ω ≈ 9.5 kΩ逻辑说明:555内部两个比较器分别卡在1/3 Vcc和2/3 Vcc,电容在两者之间往复充放电形成弛张振荡。参数说明上,R1同时影响充电支路和占空比,R2只影响放电支路,所以想微调频率优先动R2;C的取值范围受限于漏电流,100nF是稳定性比较好的甜点。实际搭电路时建议R1或R2用10k多圈可调电阻串一个固定电阻,把频率调到实测490~510Hz即可,不必死磕理论值。
提示:555的输出是方波,谐波非常丰富。如果接收端滤波器通带没卡准,三次谐波1500Hz会直接串进来干扰过零检测,这是后面带通设计必须衔接的点。
2.2 声源用无源蜂鸣器而不是扬声器
方案比较里选无源蜂鸣器,理由是功耗低、价格便宜、声音清晰度够。这里有个工程细节值得展开:无源蜂鸣器本质是个小功率压电或电磁换能器,在谐振点附近声压输出效率最高,所以必须用方波驱动而不是直流。扬声器声压更大但需要较大的驱动电流,555的灌电流能力有限,直接推扬声器会出现波形失真,反而让TDOA测量变得不可控。所以低频小功率场景下蜂鸣器是更合适的激励源。
| 器件 | 驱动方式 | 典型功耗 | 声压表现 | 对555的负载 |
|---|---|---|---|---|
| 低音扬声器 | 模拟/方波,需功率级 | 高 | 强,但易失真 | 需加三极管或功放 |
| 无源蜂鸣器 | 方波直接驱动 | 低 | 中等,谐振点清晰 | 可直接挂IO或输出端 |
2.3 VCA810构成的AGC为什么要做,不做会怎样
声源离某个话筒近、离另一个远,接收幅度可能差十几倍。12位ADC的参考电压固定,如果最远的通道信号刚够量程的一半,最近的通道早就削顶了,过零点检测会严重偏移,时差直接算错。所以正文在发射通路后面串了一级基于VCA810的自动增益控制,把进入ADC的信号稳定在一个固定范围内。
VCA810的增益由第3脚的控制电压决定,是压控增益放大器,增益范围用dB线性表示,控制口经接插件引出就是为了方便你在调试时拿一个可调直流电压去标定。外围电路里,两个电源脚各挂一颗2.2~6.8uF的大电容排低频干扰,再并104的小电容做高频去耦,输入端的50Ω接地电阻做阻抗匹配,R10用来消除V−输入端的偏置电流。
常见做法是:先固定一个已知距离测出各通道原始幅度,算出动态范围,再反推AGC需要压多少dB,然后用DAC或PWM滤波产生控制电压。这样比盲调更省时间。
2.4 二阶低通与二阶高通级联出带通
正文里低通截止设550Hz、高通截止设450Hz,通带宽度100Hz,正好把500Hz的方波基频圈在中间,又滤掉工频50Hz、电源纹波和高频谐波。电路采用NE5532搭的压控电压源(VCVS)结构,同相输入、增益通过反馈电阻设定,特点是输入阻抗高、输出阻抗低、Q值可调。
低通部分的电阻电容决定截止频率与品质因数:
fc = 1 / (2*pi*sqrt(R3*R4*C3*C4)) Q = sqrt(R3*R4*C3*C4) / (C4*(R3+R4)) (单位增益同相结构)参数说明:正文取R3=2k、R4=4k、C3=C4=104(即100nF),代入可算出截止频率落在几百赫兹量级。注意这不是教科书上常见的等值设计,因为要让Q略微大于0.707来获得巴特沃斯响应。
高通级增益必须限制在2倍以内,所以NE5532的第6、7脚直接相连(单位增益跟随),否则会自激。两级级联后,通带内的相位一致性非常重要——相位在通带边缘剧烈变化,如果你的频率刚好卡在边缘,四个通道测出来的时间差会带上非线性的相位延迟,这个误差很难事后用算法补。所以实际调试时建议先用信号发生器扫频,把通带中心确认在500Hz附近。
注意:级联滤波器的实际Q值是各级Q的乘积关系,不是简单相加。两级都用高Q会形成尖峰,容易在500Hz附近产生振铃,做TDOA时会带来额外的群延迟,反而误事。
3. STM32F103的采样链路:ADC配置、定时器触发与相位差提取
3.1 引脚与时钟的最基本配置
STM32F103VET6,72MHz主频,12位ADC,这里要用至少四路ADC通道去采四角话筒。核心配置是ADC工作在扫描模式,配合DMA把结果搬到内存,避免CPU轮询。以下是一段可用的初始化骨架(标准库风格,Keil环境下):
// ADC1 扫描 + DMA 搬运,规则通道4路 void ADC_DMA_Init(void) { ADC_InitTypeDef adc; DMA_InitTypeDef dma; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // PA0~PA3 模拟输入 GPIO_InitTypeDef gpio; gpio.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3; gpio.GPIO_Mode = GPIO_Mode_AIN; GPIO_Init(GPIOA, &gpio); dma.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; dma.DMA_MemoryBaseAddr = (uint32_t)adc_buf; // uint16_t adc_buf[4] dma.DMA_DIR = DMA_DIR_PeripheralSRC; dma.DMA_BufferSize = 4; dma.DMA_PeripheralInc = DMA_PeripheralInc_Disable; dma.DMA_MemoryInc = DMA_MemoryInc_Enable; dma.DMA_Mode = DMA_Mode_Circular; // 循环覆盖,实时刷新 DMA_Init(DMA1_Channel1, &dma); DMA_Cmd(DMA1_Channel1, ENABLE); ADC_DeInit(ADC1); adc.ADC_Mode = ADC_Mode_Independent; adc.ADC_ScanConvMode = ENABLE; // 扫描4个通道 adc.ADC_ContinuousConvMode = ENABLE; adc.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; adc.ADC_DataAlign = ADC_DataAlign_Right; adc.ADC_NbrOfChannel = 4; ADC_Init(ADC1, &adc); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); ADC_SoftwareStartConvCmd(ADC1, ENABLE); }逻辑说明:ADC_ScanConvMode打开后,ADC会按规则组顺序依次转换4个通道,每转完一轮DMA自动把DR里的结果搬到数组对应位置,CPU完全不插手。参数说明:采样时间选55.5个ADC周期,是因为四个话筒的信号源阻抗较高(经滤波网络后),采样保持电容需要更长时间充电;如果直接用7.5周期,会看到明显串扰。DMA_Mode_Circular保证缓冲区永远是最新的数据,主循环里随时读adc_buf[0..3]就能拿到四路当前值。
3.2 用定时器做精确采样时钟
500Hz的方波周期是2ms,要做相位差测量,采样率至少要到信号频率的10倍以上,也就是5kHz以上,换算到采样周期要短于200us。更好的做法是用STM32的定时器定时触发ADC,比如TIM2更新事件触发,这样采样时间完全由硬件控制,不受主循环抖动影响。
// TIM2 每 50us 触发一次 ADC 转换(采样率 20kHz) void TIM2_Trig_ADC_Init(void) { TIM_TimeBaseInitTypeDef tim; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); tim.TIM_Prescaler = 72 - 1; // 72MHz/72 = 1MHz,计数单位1us tim.TIM_Period = 50 - 1; // 每50us溢出 = 20kHz触发 tim.TIM_CounterMode = TIM_CounterMode_Up; tim.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInit(TIM2, &tim); // 配置用更新事件作为ADC外部触发 ADC_ExternalTrigConvConfig(ADC1, ADC_ExternalTrigConv_T2_TRGO); TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update); TIM_Cmd(TIM2, ENABLE); }逻辑说明:预分频72把72MHz降到1MHz,周期设为50即50us溢出一次,溢出事件通过TRGO连到ADC的触发输入。参数说明:TIM_SelectOutputTrigger这一步非常关键,很多人只配置了ADC的外部触发源却忘了打开定时器的TRGO输出,结果ADC一直不转换,还以为是ADC坏了。采样率选20kHz,是为了让过零点的位置分辨率落在50us量级,对应声程误差约1.7cm——这个精度对矩形尺寸的定位足够。
3.3 相位差到距离差的换算
采集到四路数据后,用软件找每路的过零点或者互相关计算时间差。简化实现可以测上升沿过零点的样本序号:
// 找某一通道的第一个上升过零点(简化版:过中值判断) int find_zero_cross(uint16_t *buf, int start, int end) { int mid = 2048; // 12位ADC,中值约2048 for (int i = start; i < end - 1; i++) { if (buf[i] < mid && buf[i + 1] >= mid) { return i; // 返回过零点样本索引 } } return -1; }逻辑说明:找到四路的过零索引后,两两相减再乘以采样周期就是时间差t1、t2、t3。参数说明:中值用2048是假设信号已经偏置到Vcc/2;如果没有抬升,就要先算每路的平均值再比较。实际工程里更好的选择是互相关或相位检波,过零法对噪声很敏感,所以前面滤波通带一定要干净。
| 指标 | 典型值 | 影响 |
|---|---|---|
| 采样率 | 20kHz | 决定时间分辨率,误差≈1/fs |
| ADC位数 | 12位 | 幅度分辨率约0.8mV@3.3V |
| 滤波通带 | 450~550Hz | 保证500Hz基频无衰减 |
| 时间差误差 | 50us | 对应约17mm声程误差 |
4. 四点TDOA解算:从距离差到二元二次方程组
4.1 为什么用三角形法加校正点而不是双曲线
正文比较了三种坐标解算方案,最终选了"用三角形法求出一组解,再用第三点距离差校正"。双曲线法的原理是:两个接收点到声源的距离差为常数,轨迹是一条双曲线,四个接收点两两配对可以得三组双曲线,交点即声源。问题在于当声源恰好在矩形中心附近时,多条双曲线几乎退化,交点数值不稳定——这就是所谓"盲区"。
三角形法则是直接构造方程。设矩形四角坐标A(0,0)、B(L,0)、C(L,W)、D(0,W),声源O(x,y)。四点到声源的距离是:
Da = sqrt(x^2 + y^2) Db = sqrt((x-L)^2 + y^2) Dc = sqrt((x-L)^2 + (y-W)^2) Dd = sqrt(x^2 + (y-W)^2)单片机测到的是到达时间Ta、Tb、Tc、Td,两两相减乘以声速v就得到距离差。例如:
Db - Da = v*(Tb - Ta) = v*t1 Dd - Da = v*(Td - Ta) = v*t2 Dc - Da = v*(Tc - Ta) = v*t3三个等式、两个未知数x和y,属于超定方程组。任取两式可以解出一组(x,y),三式组合可解出三组,最后取平均。
4.2 解析求根的简化思路
直接展开平方根式会得到二次方程,手推容易出错,用MATLAB或Python预先符号推导更靠谱。思路是:把Db - Da = k1两边平方式化,利用Db^2 - Da^2 = (Db-Da)(Db+Da) = k1*(Db+Da),可以把Db+Da写成含x、y的线性表达式,从而把非线性方程组转成线性。对A、B两点:
Db^2 - Da^2 = (x-L)^2 + y^2 - x^2 - y^2 = L^2 - 2Lx而右边等于k1*(Db+Da),于是Db+Da = (L^2 - 2Lx)/k1。结合Db - Da = k1,可得:
Da = [(L^2 - 2Lx)/k1 - k1] / 2 = sqrt(x^2 + y^2)再平方一次就得到关于x、y的二次方程。同理对其他点对做同样处理,最后用最小二乘求解。
import numpy as np def solve_source(meas, rect, v=340.0): """ meas: 四个到达时间 [Ta, Tb, Tc, Td] rect: 矩形尺寸 [L, W] 返回声源坐标 (x, y) """ L, W = rect t1 = v * (meas[1] - meas[0]) # Tb - Ta t2 = v * (meas[3] - meas[0]) # Td - Ta t3 = v * (meas[2] - meas[0]) # Tc - Ta # 用A(0,0), B(L,0)得到的一条关系:Db^2 - Da^2 = L^2 - 2Lx # Db - Da = t1,配合可消去 Da 项 A_rows = [] b_vals = [] # A-B 对 A_rows.append([2*L, 0]); b_vals.append(L*L - t1*t1 - 2*t1*0) # A-D 对 A_rows.append([0, 2*W]); b_vals.append(W*W - t2*t2) # A-C 对角对(辅助) A_rows.append([2*L, 2*W]); b_vals.append(L*L + W*W - t3*t3) A = np.array(A_rows, dtype=float) b = np.array(b_vals, dtype=float) sol, *_ = np.linalg.lstsq(A, b, rcond=None) # 最小二乘 return sol[0], sol[1]逻辑说明:把三个距离差方程整理成2Lx + 0*y = b这类线性形式,然后用最小二乘一次性解出x、y,等价于对超定方程组求最优解,精度比任取两式求平均更稳。参数说明:v取340m/s是常温空气中的声速,温度每变化1°C声速变化约0.6m/s,如果测试环境有较大温差,需要现场标定。rcond=None交给numpy自动判断奇异值阈值。
4.3 采样同步与常见误用
超定方程组能不能求出准的解,关键不在算法,而在四路数据是不是同一时刻采的。如果四个通道分时轮流采样,彼此间隔几十微秒,时间差测出来必然带系统性偏差。STM32F103的ADC只有一套采样保持电路,四通道是轮流转换的,所以严格来说并不是同时采样。工程上通常通过以下手段缓解:
- 提高采样率,把通道间间隔压缩到可忽略的量级(比如1us以内需采样率达1MHz,做不到)
- 对500Hz的周期信号,用多周期平均来平滑单周期的量化误差
- 用互相关而非单点过零,把时间差估计精度提升到亚采样级
另一个常见坑是把两个声源同时开着(正文提到有两个声源位置),却不做频率区分或时间分片。两个500Hz源叠加后会形成拍频,过零点漂移,解出的坐标直接跳变。正确做法是一次只发一个源,或者给两个源用不同频率。
提示:如果测出来的坐标总是偏向矩形某个角,先别怀疑算法,用示波器看四路滤波后的信号是否同相——通常是某一级滤波器的相位延迟比其他路大,等于人为制造了一个固定的时间偏置。
5. 验证方法:从单频点扫频到坐标纸实测
5.1 先验证模拟通路,再验证算法
把系统拆成两半分别验证是省时间的做法。模拟通路验证只需要信号发生器和示波器:在话筒位置注入500Hz正弦波,逐级观察555输出、AGC输出、低通输出、高通输出,确认通带中心在500Hz、增益在期望范围内。这一步做完,再上ADC采样看波形是否畸变。
算法验证可以不依赖硬件:在MATLAB或Python里随机生成若干个声源坐标,正向算出理论时间差,加一些高斯噪声,再喂给解算函数,看误差分布。这样能快速找出解算方程在哪些坐标区域病态。
5.2 坐标纸上的误差来源定位
正文附录里有实物图和坐标纸测试方法。实际做的时候,把矩形四个角贴好拾音器,声源放在纸面已知坐标点上逐个测。记录实测值和理论值的偏差,画散点图。常见的误差模式和对症做法如下:
| 误差现象 | 可能的来源 | 排查手段 |
|---|---|---|
| 中心区域误差大、四角小 | 双曲线/方程组在中心区病态 | 增加接收点,或改用最小二乘精化 |
| 整体坐标偏移固定量 | 某一通道固定相位延迟 | 用单点标定修正时间偏置 |
| 远近误差差异大 | AGC未起效,信号削顶 | 示波器看AGC输出摆幅是否恒定 |
| 随机跳变 | 环境噪声或混响 | 多次测量取中位数,加强滤波 |
5.3 一个具体的标定技巧
标定的顺序应当是:先固定声源在矩形中心正对一个已知距离,记录四路的到达时间和绝对幅度;然后保持声源不动,仅改声源频率(比如490、500、510Hz),看时间差读数如何漂移。如果漂移量随频率近似线性,说明滤波器的群延迟不均匀,需要把两级滤波器的中心频率对准声源实际频率,而不是理论值。
另一个技巧是引入参考通道。把四个话筒其中一路当作基准,只测另外三路与它的时间差,这样滤波器的共模相位延迟互相抵消,剩下的只是通道间的相对差异,通常小一个数量级。这是从双通道到多通道TDOA测量里通用的降error手段,值得在毕设之外的实际项目中保留。
本文还有配套的精品资源,点击获取