1. 项目概述:为什么用Arduino做听力测试仪?
几年前,我因为耳鸣去做了次专业的听力测试,拿到那张画着曲线的“听力图”时,心里就在琢磨:这玩意儿原理听起来不复杂啊,不就是发出不同声音让人听,然后记录能听到的最小音量吗?作为一个电子爱好者,手头正好有Arduino,就动了心思,想试试能不能自己攒一个出来。市面上当然有现成的手机App能做类似的事,但它们依赖手机本身的音频硬件,精度和一致性很难保证,更像是个玩具。我的目标是做一个独立的、可重复使用的硬件设备,让它更接近“仪器”的范畴,而不仅仅是“程序”。
这个基于Arduino的DIY听力测试仪,核心就是模拟专业听力计(Audiometer)的基本功能。它通过Arduino控制,产生从低频到高频的一系列“纯音”,并通过耳机播放。测试者通过一个按钮反馈是否听到了声音,设备则记录下在每个频率下,人耳刚好能察觉到声音的音量强度(以分贝dB HL为单位)。最终,这些数据会被可视化成两条曲线(通常左耳红色,右耳蓝色),这就是你的简易听力图。虽然它不能替代专业的医疗诊断设备,但用于个人听力趋势监测、科普教育、或者电子爱好者理解生物医学信号采集与处理,是一个非常棒的项目。整个系统涉及嵌入式编程、数模信号转换、人机交互界面设计以及简单的数据分析与可视化,算是一个综合性的小型工程实践。
2. 核心硬件选型与电路设计解析
一套可靠的硬件是项目成功的基石。我的选型原则是:在满足功能的前提下,优先选择常见、易得、文档丰富的模块,以降低制作门槛和调试难度。
2.1 主控与音频生成:为什么是Arduino Nano?
我选择了Arduino Nano R3作为大脑。原因很简单:它足够小巧,能塞进便携设备里;拥有足够的GPIO引脚来驱动显示屏、编码器和LED矩阵;最重要的是,它内置了硬件PWM(脉冲宽度调制)和定时器,这对于稳定、精确地生成音频信号至关重要。生成纯音的方法,我采用了直接数字合成(DDS)的思想,利用一个高精度定时器中断,不断更新一个数字正弦波表的值,并通过PWM输出。虽然Arduino的PWM频率固定(通常490Hz或980Hz),但通过滤波和后续的放大电路,我们可以得到相对平滑的正弦波。更高级的方案可以用Arduino的DAC引脚(如果MCU支持)或外接DAC芯片,但对于音频范围内的纯音(250Hz-8kHz),PWM经过低通滤波后效果完全可接受。
注意:Arduino Uno/Nano的PWM引脚是3, 5, 6, 9, 10, 11。为确保音质,应选择一个不与核心功能(如定时器1,常用于伺服库)冲突的引脚。我通常使用引脚9或10。
2.2 显示系统:状态与结果的分离呈现
显示部分我用了两块屏,分工明确:
- OLED SSD1306 (128x64):用于显示实时参数。比如当前测试的频率(如1000 Hz)、当前输出的音量等级(如30 dB)、测试的耳朵(L/R)、以及操作提示。它的优点是像素清晰,刷新快,适合显示文字和简单图形,功耗极低。
- WS2812B 8x8 RGB LED矩阵:用于最终结果的可视化。64个LED点阵,X轴代表8个测试频率(例如125, 250, 500, 1000, 2000, 4000, 8000 Hz),Y轴代表8个音量等级(例如从-10 dB到80 dB,每格10dB)。当测试完成,对应的(频率, 阈值)坐标LED就会亮起,左耳用红色,右耳用蓝色,瞬间就能看到两条听力曲线的对比,非常直观。
这种“状态屏+结果屏”的分离设计,让交互逻辑很清晰:测试时你只看OLED,知道现在在干嘛;测试后一眼扫过LED矩阵,结果了然于胸。
2.3 输入与控制:旋转编码器的妙用
调整音量是测试中最频繁的操作。如果用普通按键,需要反复按“加”和“减”,效率低。我选用了一个带按键的旋转编码器。旋转可以无极调节音量,手感顺滑,调节速度快;按下按键则用于确认“在当前频率下,我听到声音了”。一个器件完成两项核心输入,极大简化了面板设计和程序逻辑。
2.4 音频放大与输出:从信号到声音
Arduino的PWM输出是数字信号,且驱动能力很弱,无法直接推动耳机或骨导振荡器。需要经过放大和滤波。
- 滤波电路:PWM输出的是方波,富含高频谐波。我们需要一个低通滤波器(通常是一阶或二阶RC滤波器)将其平滑成近似的正弦波。截止频率应略高于我们需要的最高测试频率(比如10kHz)。
- 功率放大:对于普通的空气传导耳机(阻抗通常32欧姆以上),一个简单的晶体管放大电路或者小功率运放(如LM386)就能驱动。但我为了更好的效果和灵活性,直接选用了一个现成的PAM8403 D类音频放大模块。它效率高、体积小、输出功率足,且自带音量电位器(可用于整体校准)。
- 骨导测试扩展:骨导振荡器(Bone Conduction Oscillator)的阻抗通常很低(4-8欧姆),需要更大的驱动电流。PAM8403模块在低阻抗下也能工作,但为了更可靠和独立控制,我为其单独增加了一个小型的D类功放模块(同样基于PAM8403或类似芯片),并由一个单独的电位器控制增益,方便进行骨导校准。
2.5 电源管理:稳定压倒一切
音频电路对电源噪声比较敏感。我使用两节3.7V锂电串联(约7.4V)供电。为什么不用单节?因为常见的5V稳压芯片(如7805)需要输入电压至少高于输出电压2V(压差)才能稳定工作。7.4V输入,降到5V,压差足够。这个5V为Arduino、OLED、编码器、WS2812(需5V逻辑)供电。7805虽然效率不高(线性稳压),但电路简单,噪声低。对于功放模块(PAM8403),它的工作电压范围是2.5V-5.5V,我直接从前级的5V取电即可,也可以直接从电池取电(通过一个开关),以获得更大的输出摆幅,但要注意模块的耐压。
完整电路连接思路:
- Arduino Nano从7805获得5V供电。
- PWM音频引脚(如D9) → RC低通滤波器 → PAM8403模块音频输入。
- PAM8403模块输出 → 3.5mm耳机插座(用于气导耳机)。
- 另一个PAM8403模块(用于骨导)的输入并联到主音频信号,其输出 → 专用骨导振荡器插座。
- 旋转编码器的CLK, DT, SW引脚分别接Arduino的数字输入引脚,并启用内部上拉电阻。
- OLED屏通过I2C(A4/SDA, A5/SCL)连接。
- WS2812矩阵的数据线接一个带220-470欧姆串联电阻的Arduino数字引脚(如D6)。
3. 软件设计与核心代码剖析
软件是设备的灵魂,需要精心设计测试流程、信号生成算法和数据处理逻辑。
3.1 测试流程状态机设计
程序的核心是一个状态机(State Machine),清晰定义了设备在不同阶段的行为。这比用一堆if-else语句要清晰得多。
enum TestState { STATE_IDLE, // 空闲,显示欢迎界面 STATE_SELECT_EAR, // 选择左耳或右耳测试 STATE_TESTING, // 正在某个频率进行测试 STATE_RECORD, // 受试者按键,记录该频率阈值 STATE_NEXT_FREQ, // 切换到下一个频率 STATE_COMPLETE, // 单耳测试完成 STATE_SHOW_RESULT // 显示最终听力图 };测试一个耳朵的基本流程循环是:STATE_TESTING-> (用户旋转编码器调音量)-> 用户听到声音按下按键 ->STATE_RECORD->STATE_NEXT_FREQ-> 如果还有频率,回到STATE_TESTING,否则进入STATE_COMPLETE。
3.2 精确纯音生成:定时器中断与DDS
在Arduino上产生指定频率的纯音,最稳定可靠的方法是使用定时器中断。以16MHz的AVR芯片为例,我们可以配置一个定时器(如Timer1),使其在精确的时间间隔(由目标频率决定)内触发中断。在中断服务程序(ISR)中,我们从一个预先计算好的正弦波表中读取下一个样本值,并更新到PWM占空比寄存器(如OCR1A)中。
这里有一个关键技巧:正弦波表的大小和定时器频率的配合。假设我们有一个256个点的正弦波表。要产生一个fHz的正弦波,那么更新波表的频率(即采样率)必须是f * 256Hz。定时器中断的频率就需要设置成这个值。通过改变遍历波表的速度(即相位增量),就能改变输出频率。这就是简化的DDS原理。
// 伪代码示例 const int sineTable[256] = {...}; // 0-255的正弦值 volatile uint32_t phaseAccumulator = 0; // 相位累加器 uint32_t phaseIncrement; // 相位增量,决定频率 void setupTimer1ForFrequency(float targetFreq) { // 计算产生targetFreq所需的相位增量 // phaseIncrement = (targetFreq * 2^32) / 采样率 // 采样率 = 定时器中断频率 // 配置Timer1比较匹配中断,设置比较匹配寄存器以得到所需的中断频率 } ISR(TIMER1_COMPA_vect) { phaseAccumulator += phaseIncrement; uint8_t index = (phaseAccumulator >> 24) & 0xFF; // 取高8位作为波表索引 OCR1A = sineTable[index]; // 更新PWM占空比 }实操心得:中断服务程序里的代码必须极其精简,只能做最必要的操作(查表、写寄存器)。任何复杂的计算、函数调用或I2C/SPI通信都不能放在里面,否则会导致中断超时,声音卡顿或系统不稳定。所有状态判断、用户输入处理、显示更新都应在主循环
loop()中完成。
3.3 分贝衰减与音量控制
在听力测试中,我们控制的是声音的强度,单位是分贝听力级(dB HL)。这需要我们将数字音频信号的幅度(振幅)映射到一系列衰减等级上。在硬件上,我们通常通过一个数字电位器或软件控制放大器增益来实现。在这个项目中,我们通过控制PWM的占空比(即正弦波表的输出幅度)来模拟衰减。
我们需要预先定义一组衰减值(例如从0 dB到 -70 dB,每档5 dB)。0 dB对应最大输出(PWM满幅),-70 dB对应一个非常小的输出。在代码中,这通常是一个幅度缩放因子(比如0.0到1.0)。当用户旋转编码器时,实际上是在改变这个缩放因子,然后程序将正弦波表的每个样本乘以这个因子后再输出到PWM。
float currentAttenuationFactor = 1.0; // 对应 0 dB float dBStep = 5.0; // 每档5分贝 int currentVolumeLevel = 0; // 档位索引,0档为0dB // 用户旋转编码器增加音量 void increaseVolume() { if (currentVolumeLevel > -14) { // 假设共15档,0到-70dB currentVolumeLevel--; // 将分贝值转换为线性幅度因子。公式:factor = 10^(dB/20) float dB = currentVolumeLevel * dBStep; currentAttenuationFactor = pow(10, dB / 20.0); updateOLEDDisplay(); // 更新显示的dB值 } }关键点:分贝(dB)是对数单位,而PWM占空比或幅度因子是线性单位。转换时必须用公式幅度因子 = 10^(目标分贝值 / 20)。例如,-20 dB对应的幅度因子是0.1,即信号幅度衰减到原来的十分之一。
3.4 数据记录与可视化
当用户在某个频率下按下确认键时,程序需要记录两个值:当前测试的频率currentFreq和当前的音量等级currentVolumeLevel(或对应的dB值)。我们可以用两个数组来存储左右耳的数据:
int leftEarResults[8]; // 存储8个频率点的阈值(音量等级索引) int rightEarResults[8]; int freqList[8] = {125, 250, 500, 1000, 2000, 4000, 8000, 12000}; // 测试频率列表测试完成后,可视化就很简单了。遍历freqList数组,对于每个频率点i,在WS2812矩阵上,将X坐标设为i,Y坐标设为leftEarResults[i]对应的LED点亮为红色,rightEarResults[i]对应的LED点亮为蓝色。如果左右耳阈值相同,可以用混合色(如紫色)显示。
OLED屏则可以显示更详细的信息,比如具体的频率和dB数值表格。
4. 校准:让DIY设备产生参考价值
这是区分“玩具”和“工具”的关键一步。没有校准,设备输出的分贝值只是任意的数字,没有可比性。校准的目标是:让我们设备上显示的“0 dB”对应国际标准中定义的“正常人平均最小可听声压级”(即0 dB HL)。
4.1 校准的基本原理
你需要一个参考——一个经过计量校准的专业听力计。方法如下:
- 连接:将你的DIY设备的音频输出,连接到专业听力计的输入端口(如果有的话),或者使用一个声级计(需要精度高,且支持频率加权)。
- 设置:在你的设备上选择1000 Hz频率,并将音量调整到“0 dB”档位(即
currentAttenuationFactor = 1.0)。 - 测量:用专业设备测量此时输出的实际声压级(SPL)。假设测出来是90 dB SPL。
- 计算偏移:查阅标准“纯音气导听力零级”表(如ISO 389系列标准)。对于1000 Hz,0 dB HL对应的声压级大约是7.5 dB SPL(具体值因耳机和耦合器而异,此为例)。那么,你设备的“0 dB”档位实际输出是90 dB SPL,比标准零级高了 90 - 7.5 = 82.5 dB。
- 软件修正:这个82.5 dB就是你需要补偿的偏移量。在你的代码中,所有显示给用户的“dB HL”值,都应该在内部计算的基础上减去这个偏移量。或者,更简单的方法是,重新定义你的
currentVolumeLevel与dB HL的映射关系。例如,原来currentVolumeLevel=0对应0 dB(实际输出90 dB SPL),现在让它对应 -82.5 dB HL。这样,当用户调到“0 dB HL”时,你的程序会自动将幅度因子调整到能产生7.5 dB SPL输出的水平。
4.2 实操校准步骤
- 准备工具:专业听力计(或高精度声级计+仿真耳)、用于连接的电线、焊接工具。
- 单点校准(以1kHz为基准):按上述方法,获得在1kHz下的校准偏移量。将这个偏移量作为全局基础偏移写入代码。
- 频率响应补偿:人耳和耳机对不同频率的灵敏度不同。专业设备已经补偿了这一点(即“听力零级”每个频率都不同)。你需要在其他测试频率点(250, 500, 2k, 4k, 8k Hz)重复步骤2,测量出每个频率下,设备输出“0 dB”档位时的实际SPL,与标准零级的差值。你会得到一组频率相关的补偿值(一个数组)。
- 集成到代码:在程序内部,当设置某个频率时,除了应用用户选择的音量衰减,还要自动加上该频率对应的补偿值。这样,用户界面上的“dB HL”才在各个频率上都具有可比性。
- 骨导校准:骨导校准更为复杂,需要标准的力耦合器和仿真乳突。对于DIY项目,可以尝试用气导校准后的设备作为参考,在安静环境下,由听力正常的人主观对比气导和骨导的听阈,进行粗略匹配。但这仅用于趣味探索,不具备医学参考价值。
重要警告:即使经过精心校准,DIY设备也绝不能用于正式的医学诊断或听力损伤评估。其精度、稳定性、安全性与专业医疗设备存在巨大差距。本项目目的仅限于学习、个人兴趣和科普演示。
5. 组装、调试与外壳制作
硬件焊接和软件调试是动手的乐趣所在,也会遇到不少坑。
5.1 焊接与组装注意事项
- 电源先行:先焊接电源部分(7805稳压电路),确保5V输出稳定、无振荡。可以用万用表测量空载和带载(接上Arduino)时的电压。
- 模块化测试:不要一次性焊完所有东西。先焊好Arduino最小系统(包括电源和编码器),上传一个简单的测试程序(如读取编码器值并串口打印),确保核心控制部分工作正常。
- 音频链路调试:
- 先断开功放,用示波器观察PWM引脚经过低通滤波器后的波形。你应该能看到一个平滑度尚可的正弦波,随着代码中频率改变,其周期应相应变化。
- 接上功放,但不接耳机/音箱。测量功放输出,确保没有自激振荡(高频杂波)。
- 最后接上耳机,在最低音量下试听,应能听到纯净的单一频率音调,无杂音、无爆音。
- 显示模块:依次测试OLED和WS2812矩阵。确保I2C地址正确,WS2812数据线串联了电阻(防止过冲损坏第一个LED)。
- 抗干扰:音频信号线尽量短,并远离数字信号线(如WS2812的数据线)和电源线。地线布局要合理,推荐使用“星型接地”或单点接地,避免地环路引入噪声。
5.2 软件调试技巧
- 串口调试助手是你的好朋友:在关键节点(如状态切换、频率改变、按键按下、数据记录时)通过
Serial.print()输出变量值,这是追踪程序逻辑最有效的方法。 - 模拟测试:在没有实际硬件时,可以先用串口指令模拟编码器旋转和按键,测试状态机流程和数据处理是否正确。
- 测试音频:写一个简单的模式,让设备循环播放所有测试频率的音调,用手机音频分析APP(如Spectrum)粗略看一下频率是否准确。
- 内存管理:WS2812库和某些显示库可能比较占内存。注意Arduino Nano的RAM只有2KB,避免使用大型字符串或数组。使用
F()宏将常量字符串存到闪存中,如Serial.print(F(“Hello”));。
5.3 外壳设计与制作
一个得体的外壳能让项目瞬间提升档次。我选用PVC塑料板(雪弗板)来制作,因为它易于切割、打磨和粘合。
- 设计:先用卡尺测量所有模块的尺寸,在纸上画出布局草图。考虑散热(7805会发热)、接口位置(耳机孔、开关、充电口)、屏幕可视角度和按键手感。
- 切割:用勾刀或激光切割机(如果条件允许)切割PVC板。前面板需要为两个屏幕、编码器、开关和耳机接口开孔。开孔尺寸要略小于元件外观尺寸,以便从内部卡住。
- 组装:使用PVC专用胶水或热熔胶固定内部模块。确保线路整齐,避免短路。电池可以用尼龙扎带或电池仓固定。
- 美化:我用的是彩色自粘壁纸贴在外壳表面,既能遮盖接缝,又显得美观。也可以在面板上贴上激光雕刻的标识和刻度。
6. 使用流程与结果解读
设备制作完成后,如何使用它进行一次完整的测试呢?
6.1 标准测试流程
- 环境准备:在一个尽可能安静的环境中进行。本底噪声最好低于30 dB A。
- 设备启动:打开电源,设备初始化,OLED显示欢迎界面,LED矩阵清空。
- 选择测试耳:通常程序会提示先测试左耳(L)或右耳(R),通过旋转编码器选择,按下确认。
- 开始测试:
- OLED屏显示第一个测试频率(如1000 Hz)和当前音量(如-10 dB HL)。
- 将耳机戴在待测耳朵上,另一耳可暂时用耳塞堵住。
- 重要:测试音是间断的“哔哔”声(通常持续1-2秒,间隔1-2秒),而不是持续音。这可以防止听觉疲劳和适应。
- 旋转编码器,从听不到声音的音量开始,逐渐增大。当听到“哔”声时,立即按下编码器按键。
- 设备会记录此阈值,然后自动跳到下一个频率(如500 Hz),音量自动重置到较低水平。
- 重复上述过程,直到所有预设频率测试完毕。
- 换耳测试:程序提示换到另一只耳朵,重复步骤4。
- 结果显示:双耳测试完成后,LED矩阵会点亮红蓝两色曲线。OLED屏可切换显示具体数值表格。
6.2 如何解读你的DIY听力图
在LED矩阵上,Y轴从上到下代表声音从小到大(分贝值递增,即听力损失越严重,点越靠下)。X轴从左到右代表频率从低到高。
- 正常听力:曲线应该集中在矩阵的顶部区域(对应-10 dB HL到20 dB HL之间)。
- 高频下降:这是常见的年龄相关或噪声性听力损失,表现为曲线在右侧(高频部分)向下倾斜。
- 平坦型损失:所有频率的阈值都均匀升高,曲线整体下移。
- 左右耳差异:红蓝曲线分离,表明双耳听力不对称。
再次强调:这张图仅供自我了解和趋势观察。如果你发现阈值 consistently 高于25 dB HL,或者双耳差异很大,正确的做法是寻求专业听力师的帮助,进行全面的听力检查。
7. 常见问题与故障排除
在制作和使用过程中,你可能会遇到以下问题:
| 问题现象 | 可能原因 | 排查与解决思路 |
|---|---|---|
| 没有声音输出 | 1. 电源未接通或电压不足。 2. 音频链路某处断开或虚焊。 3. 程序未正确启动或定时器配置错误。 4. 耳机损坏。 | 1. 检查电池电压、7805输入输出电压。 2. 用示波器或万用表AC档,从PWM输出开始,逐级向后检查信号。 3. 上传一个简单的“蜂鸣器”测试程序,检查MCU是否工作。 4. 更换耳机测试。 |
| 声音失真、有杂音 | 1. 电源噪声大。 2. PWM滤波不充分,高频谐波多。 3. 功放模块自激或接地不良。 4. 信号线受到数字干扰。 | 1. 在7805输入输出端并联更大的滤波电容(如100uF电解+0.1uF瓷片)。 2. 调整RC低通滤波器的参数,降低截止频率。 3. 确保功放模块电源退耦电容(通常靠近电源引脚)已焊接。优化接地布局。 4. 将音频线绞合或使用屏蔽线,远离数字线路。 |
| 旋转编码器操作不灵 | 1. 引脚接触不良或上拉电阻未启用。 2. 防抖处理不当。 3. 中断冲突。 | 1. 检查焊接,在代码中设置输入引脚为INPUT_PULLUP。2. 在代码中加入软件防抖,检测稳定状态后再判断旋转方向。 3. 避免在编码器中断服务程序中做复杂操作,或考虑使用状态轮询法而非中断法。 |
| WS2812矩阵显示错乱 | 1. 数据线串联电阻缺失或阻值不对。 2. 电源功率不足(尤其全白时)。 3. 时序问题,中断干扰了数据发送。 | 1. 在Arduino数据输出引脚和第一个LED的DI之间串联一个220-470欧姆电阻。 2. 为LED矩阵单独供电(5V),并与Arduino电源共地。计算最大电流(64LED * 60mA ≈ 3.84A),确保电源能承受。 3. 在发送WS2812数据时,暂时关闭全局中断 noInterrupts(),发送完再打开interrupts()。 |
| 测试结果重复性差 | 1. 环境噪声干扰。 2. 音量调节步进(分贝档位)太粗。 3. 受试者反应时间不一致或主观判断偏差。 | 1. 在更安静的环境测试,或使用隔音耳罩。 2. 在软件中细化音量衰减的档位,例如从5dB一档改为2dB一档。 3. 采用标准的“升降法”:听到声音就调低一档,听不到就调高一档,反复几次取平均阈值,这需要更复杂的程序逻辑。 |
这个项目从构思到实现,花了我不少周末的时间。最大的收获不是做出了一个能用的设备,而是在这个过程中,把模拟电路、数字信号处理、嵌入式编程和人体生理学( albeit 基础)的知识串了起来。每次调试时听到耳机里传出干净的单频音,或者看到LED矩阵上点亮属于自己的听力曲线时,那种成就感是单纯的买一个产品无法比拟的。它也许不够精密,但足够有趣,也让我对保护听力这件事有了更直观的认识。如果你也感兴趣,不妨动手试试,从最基础的电路焊起,感受一下创造“仪器”的乐趣。