1. 项目概述与核心思路
几年前,我在一个健康监测相关的创客项目里,第一次接触到用光电法测心率。当时市面上成品的医用指夹式血氧仪已经非常成熟,但作为一个喜欢折腾硬件的开发者,我更想亲手从传感器信号开始,理解数据是如何被采集、处理,最终变成一个直观的数字显示在屏幕上的。这个用Arduino制作心率监测器的项目,就是那个想法的落地。它不仅仅是一个简单的“连线-上传代码”的玩具,其背后涉及了模拟信号采集、数字滤波算法、嵌入式系统实时性以及产品化外壳设计等多个层面的考量。
这个设备的核心功能很明确:实时测量并显示人体的心率(BPM,每分钟心跳次数)。它特别适合几类朋友:一是嵌入式开发的初学者,想通过一个完整的项目串联起传感器、微控制器和显示模块;二是对生物信号测量感兴趣的学生或爱好者,用于课程设计或兴趣探索;三是需要快速搭建健康监测原型的创客,它的模块化设计便于二次开发。整个项目的硬件成本可控,软件生态成熟,最关键的是,当你看到自己的脉搏跳动被实时转化成屏幕上的数字时,那种亲手实现一个“生命体征监测仪”的成就感,是无可替代的。
2. 核心硬件选型与电路设计解析
2.1 主控与传感器:为什么是Arduino Uno和这款脉搏传感器?
选择Arduino Uno作为主控几乎是入门嵌入式项目的首选,原因在于其极低的入门门槛和丰富的生态。对于心率监测这个任务,Uno的ATmega328P微控制器拥有6路模拟输入(A0-A5),这正好满足了我们需要一路模拟信号(脉搏传感器)的需求。其16MHz的主频和2KB的SRAM,处理简单的滤波算法和驱动一个小型TFT屏绰绰有余。更关键的是,Arduino IDE和庞大的库支持,让我们可以避开繁琐的寄存器配置,专注于应用逻辑。
脉搏传感器的选择是项目的灵魂。市面上常见的光电式脉搏传感器主要分透射式和反射式。我们项目中使用的是类似MAX30102或Pulse Sensor Amped这类反射式传感器。它的工作原理是:传感器上的LED发出特定波长(通常是绿光,因为其对血液中的含氧血红蛋白吸收率差异敏感,且受皮肤表层影响小)的光照射到皮肤(如指尖),皮肤下的毛细血管会随着心跳周期性地充血和收缩,导致反射回传感器的光强发生微小变化。这个变化被光电二极管接收,转化为微弱的电流信号,再经过传感器板载的运放电路放大和滤波,输出一个模拟电压信号。这个信号的波形(光电容积脉搏波,PPG)的峰值间隔时间,就对应了心跳周期。
注意:传感器的佩戴方式对信号质量影响巨大。手指必须适度按压传感器感光区域,太轻则接触不良信号微弱,太重则会压迫血管导致信号失真甚至消失。最佳状态是手指自然放置,感受到轻微压力即可。
2.2 显示模块:1.8寸TFT LCD的驱动考量
选用1.8寸TFT LCD(通常驱动芯片为ST7735或ILI9163)而非更简单的1602字符液晶,是为了实现更好的用户体验。心率值是一个动态变化的数字,有时可能需要显示波形或简单的图标,TFT屏的像素级控制能力为此提供了可能。这类屏幕通常采用SPI接口与主控通信,占用引脚少(通常只需CS、DC、RST、SCK、MOSI),速度快,非常适合Arduino这种IO资源有限的板子。
在电路连接上,需要特别注意电平匹配和电源噪声。TFT屏的VCC接5V,但有些屏的逻辑电平可能是3.3V,如果其IO口非5V耐受,则需要在数据线(如MOSI、SCK)上加电平转换电路,不过大多数兼容Arduino的模块已内置电平转换芯片。另一个关键是背光(LED/BL)的供电,直接接5V可能导致电流过大,最好串联一个限流电阻(如100Ω),或者通过一个三极管由PWM引脚控制,以实现亮度调节,这在夜间使用时很实用。
2.3 电路连接详解与电源管理
完整的电路连接如下,我建议在面包板上先完成所有接线并测试,再考虑装入外壳:
TFT LCD连接 (SPI接口):
- VCC-> Arduino5V
- GND-> ArduinoGND
- CS(片选) ->D10(可自定义,需在代码中对应)
- DC(数据/命令选择) ->D9(可自定义)
- RST(复位) ->D8(可自定义,或接至Arduino的RESET,但独立控制更灵活)
- SCK(时钟) ->D13(Arduino的SPI时钟引脚)
- MOSI(主出从入) ->D11(Arduino的SPI数据输出引脚)
- MISO(主入从出) ->不连接(本例中TFT屏仅接收数据)
- LED/BL(背光) -> 通过一个220Ω电阻接至5V(或接至一个PWM引脚如D3,用于调光)。
脉搏传感器连接:
- VCC(正极) -> Arduino3.3V。这里是一个关键点:虽然传感器模块通常支持3.3V-5V供电,但使用3.3V供电可以有效降低传感器自身发热,减少因发热导致的基线漂移和信号噪声,获得更稳定的波形。
- GND(负极) -> ArduinoGND。
- S(信号输出) -> ArduinoA0(模拟输入引脚)。这个引脚将读取随心跳变化的模拟电压值(通常在0-3.3V之间波动)。
电源部分:
- 整个系统可由USB口供电(5V/500mA),足够驱动Arduino、传感器和屏幕。如果追求便携性,使用移动电源是完美方案。切勿使用电压不稳定或电流不足的电源适配器,屏幕在启动瞬间电流较大,劣质电源可能导致系统不断重启或显示异常。
实操心得:在连接所有线缆时,尤其是杜邦线,尽量使用不同颜色区分电源(红)、地(黑)、信号(黄、绿等)。在面包板阶段,可以用热熔胶或胶带轻轻固定关键连接点,防止因线缆松动导致间歇性故障,这种故障最难排查。
3. 核心程序设计与信号处理算法
3.1 程序框架与库依赖
代码的核心任务很清晰:周期性读取A0口的模拟值,对这个值进行滤波以提取出有效的脉搏波,检测波峰,计算波峰间隔时间,最后换算成BPM并显示在TFT屏上。我们需要用到两个主要的库:Adafruit_ST7735(或类似的TFT驱动库)用于驱动屏幕,以及可能需要的Adafruit_GFX库用于图形和文字绘制。
首先,在Arduino IDE中通过库管理器安装这些库。初始化部分包括:定义引脚、创建TFT对象、设置屏幕旋转方向、初始化串口(用于调试)以及定义一些全局变量,如存储心率值的变量、用于滤波的数组、记录时间的变量等。
3.2 模拟信号读取与软件滤波
直接读取的A0原始值噪声很大,混杂了50/60Hz的工频干扰、呼吸引起的缓慢漂移以及随机噪声。因此,滤波是必不可少的一步。在资源有限的Arduino上,我们通常采用轻量级的软件滤波算法:
移动平均滤波:这是最简单有效的方法。创建一个数组,持续存储最近N个采样值,每次计算时取这个数组的平均值作为输出。它能平滑随机噪声,但会引入一定的延迟。N值越大,越平滑,延迟也越大。对于心率信号,N取8-16是比较合适的起点。
// 示例:简易移动平均滤波 const int numReadings = 10; int readings[numReadings]; // 存储读数的数组 int readIndex = 0; // 当前读数索引 int total = 0; // 总和 int average = 0; // 平均值 void setup() { for (int i = 0; i < numReadings; i++) readings[i] = 0; } int smooth(int newReading) { total = total - readings[readIndex]; // 减去最旧的读数 readings[readIndex] = newReading; // 存入新读数 total = total + readings[readIndex]; // 加上新读数 readIndex = (readIndex + 1) % numReadings; // 循环索引 return total / numReadings; // 返回平均值 }带通滤波:心率信号的有效频率范围大致在0.5 Hz到3.0 Hz之间(对应30到180 BPM)。我们可以设计一个简单的数字带通滤波器,例如通过一个高速滤波器去除基线漂移,再经过一个低通滤波器去除高频噪声。这可以通过计算信号的一阶或二阶差分等近似实现,但实现起来比移动平均复杂。
动态阈值峰值检测:滤波后的信号是一个近似周期性的波形。检测心率的关键是准确找到每个波峰的时刻。一个鲁棒的方法是使用动态阈值。算法可以这样工作:持续追踪信号的最大值和最小值,设定一个阈值,比如
threshold = min + (max - min) * 0.7。当信号值从低于阈值上升到高于阈值时,标记为一个上升沿;之后当信号值达到一个局部最大值并开始下降时,可以认为是一个波峰。检测到波峰后,记录当前时间(millis()),并与上一个波峰的时间比较,得到心跳间隔(IBI, Inter-Beat Interval)。
3.3 心率计算与显示逻辑
得到IBI(单位:毫秒)后,计算心率就很简单了:BPM = 60000 / IBI。但是,单次IBI计算容易受偶然误差影响(比如一次误检测或漏检测)。因此,通常采用滑动平均或中值滤波来处理连续多个BPM值,得到一个稳定的输出。
在TFT屏幕上显示,我们需要考虑用户体验。初始化时,显示一个静态的界面,比如标题“Heart Rate Monitor”。主循环中,在计算得到稳定的BPM值后,清空之前显示数字的区域(避免残影),然后用大字体刷新显示新的BPM值。还可以增加一些视觉反馈,比如当手指放置正确且信号良好时,屏幕边框显示绿色;信号弱或未检测到时,显示红色。
注意事项:代码中必须加入“超时重置”逻辑。如果超过一定时间(例如3秒)没有检测到新的有效波峰,应将BPM显示清零或显示“---”,并提示用户重新放置手指。这避免了显示一个过时且可能错误的心率值。
4. 3D打印外壳的设计与优化
4.1 使用Tinkercad进行快速建模
Tinkercad是一款在线的、面向初学者的三维建模工具,非常适合这类简单的外壳设计。设计思路是制作一个分为“底壳”和“面壳”的两部分结构。底壳用于固定Arduino Uno主板,面壳则用于固定TFT屏幕和脉搏传感器。
开始设计前,必须进行精确测量。使用卡尺测量Arduino Uno的长宽高(约68.6mm x 53.4mm x 15mm,不含引脚)、TFT屏幕模块的尺寸、脉搏传感器的尺寸。在Tinkercad中,使用“长方体”基本体构建外壳主体,然后使用“孔”形状来挖出需要的空间:
- 底壳:内部需要凸起的支柱或卡槽,用于固定Arduino的四个安装孔。底部可以设计一些加强筋,防止打印变形。侧壁需要为USB-B接口、电源接口、ICSP接口开孔。
- 面壳:需要开一个矩形窗口以露出TFT屏幕,并在旁边开一个小圆孔或方孔,用于固定脉搏传感器,使其感光部分能恰好对准外壳表面。面壳与底壳的接合处,可以采用简单的卡扣配合或螺丝柱配合。对于螺丝柱配合,需要在底壳和面壳对应位置设计圆柱体作为螺丝柱,并在面壳的螺丝柱上预留沉孔。
4.2 打印设置与后处理
将设计好的STL文件导入切片软件(如Cura)。打印参数设置直接影响成品质量:
- 材料:PLA是最佳选择,它易于打印、无异味、强度足够。选择红色、白色或其他你喜欢的颜色。
- 层高:0.2mm可以在打印速度和表面光洁度之间取得良好平衡。
- 填充密度:15%-20%的填充率足以提供必要的结构强度,同时节省材料和时间。
- 支撑:如果外壳有悬空结构(如面壳内侧固定屏幕的卡扣),需要生成支撑。但设计时应尽量避免大面积的悬垂,以减少对支撑的依赖,便于拆除并获得更好的内壁质量。
- 底座(Raft/Brim):如果担心模型翘边,可以启用Brim(裙边),它比Raft(底座)更节省材料且易于剥离。
打印完成后,小心地移除支撑和底座。用砂纸打磨结合面,确保底壳和面壳能平整地对齐。对于螺丝孔,可能需要用手动钻或合适尺寸的钻头进行通孔,以确保螺丝能顺利穿过。
4.3 内部布局与组装技巧
组装顺序很关键:
- 底壳内部:首先将Arduino Uno放入底壳。不要急于使用永久性胶水。可以先使用双面泡棉胶或蓝丁胶临时固定,方便后期调试或更换。将连接TFT屏和传感器的杜邦线预先整理好,用扎带或胶带固定在底壳内壁,留出合适的长度。
- 面壳内部:将TFT屏幕和脉搏传感器放入面壳对应的位置。这里我强烈推荐使用尼龙双面胶或VHB胶带来固定。它们粘性足够强,又具有一定的弹性,可以缓冲轻微震动,并且未来如果需要拆卸,相对容易清理。确保传感器感光区域与外壳的开孔完全对准,且没有胶体遮挡。
- 最终合体:将面壳上引出的线缆与底壳内的Arduino对应引脚连接。再次上电测试,确保一切功能正常。最后,将面壳扣到底壳上。如果采用卡扣设计,听到“咔哒”声即可;如果采用螺丝固定,均匀拧紧四颗螺丝(如M3x10mm)。切勿过度拧紧,以免压裂打印件。
实操心得:在面壳内侧,围绕屏幕开窗和传感器开孔的位置,设计一圈1mm高、1mm宽的围栏。这样在粘贴屏幕和传感器时,它们可以靠在这个围栏上,确保与外壳表面平齐,不会内陷或歪斜,外观会精致很多。
5. 系统调试、校准与性能优化
5.1 上电调试与信号质量评估
组装完成后首次上电,不要急于测量。首先观察TFT屏幕是否正常点亮,并显示预期的初始界面(如等待信号提示)。接下来,将指尖轻轻、稳定地覆盖在脉搏传感器上。此时,打开Arduino IDE的串口绘图器(Serial Plotter),这是最强大的调试工具。
在代码中,除了输出计算后的BPM,还应将滤波后的原始模拟值(analogRead(A0))也通过串口发送。在串口绘图器中,你应该能看到一个清晰的、周期性的波形。一个健康的信号波形应该具有以下特征:有明显的波峰和波谷,波形相对平滑(噪声毛刺少),基线稳定(没有缓慢的上下漂移)。如果波形噪声很大,像一团杂草,说明滤波参数需要调整(比如增加移动平均的窗口大小)。如果基线持续上漂或下漂,可能是传感器压力不稳定或环境光干扰。
5.2 心率计算的校准与验证
得到稳定波形后,就需要验证心率计算的准确性。最直接的方法是与一个可靠的参考设备进行对比,比如智能手环、智能手表或专业的指夹式脉搏血氧仪。让自己处于静坐状态,同时用自制设备和参考设备测量心率,记录多组数据。
你可能会发现两种常见偏差:
- 数值系统性偏快或偏慢:这可能是峰值检测算法过于敏感或迟钝。可以调整动态阈值中的比例系数(上文提到的0.7),比如尝试0.65或0.75,观察哪个值得到的BPM更接近参考值。
- 数值不稳定,跳动剧烈:这通常是信号噪声大或峰值检测算法在噪声影响下误触发导致的。除了优化硬件(确保供电稳定、传感器接触良好),应在软件中增加“有效信号判断”。例如,只有当检测到的IBI在一个合理的生理范围内(如对应心率30-180 BPM),并且连续几次检测到的IBI值相差不大时,才更新最终显示的BPM值。这被称为“置信度检查”。
5.3 功耗优化与便携性增强
虽然USB供电很方便,但如果你希望它真正成为一个便携设备,功耗就值得关注了。Arduino Uno在默认情况下功耗并不低。我们可以进行一些软件优化:
- 降低屏幕亮度:通过PWM控制背光引脚,将亮度调整到刚好清晰可见的程度,能显著降低电流。
- 使用睡眠模式:在等待测量或长时间无操作时,可以让Arduino进入空闲(Idle)或掉电(Power-down)模式,通过外部中断(比如传感器信号变化)唤醒。但这需要修改硬件电路,将传感器信号连接到支持外部中断的引脚(如D2, D3)。
- 选用更省电的主控:如果进行二次迭代,可以考虑使用Arduino Nano Every或基于ARM Cortex-M0+的Seeed Studio XIAO系列,它们原生功耗更低,性能更强。
此外,可以考虑在底壳上集成一个小型锂电池(如18650)和充电管理模块(如TP4056),实现真正的无线便携。这时需要增加一个电源开关,并注意将整个系统的电压稳定在5V。
6. 常见问题排查与进阶扩展思路
6.1 问题速查表
在实际制作过程中,你几乎一定会遇到下面这些问题。这里是一个快速排查指南:
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 屏幕无显示 | 1. 电源未接通或接触不良。 2. 背光未点亮。 3. SPI引脚接错。 4. 代码中屏幕初始化失败(型号/引脚定义错误)。 | 1. 检查USB线、5V和GND连接,用万用表测量电压。 2. 检查背光引脚(LED/BL)是否接电,或尝试直接短接到5V看是否亮起。 3. 逐一核对CS、DC、RST、SCK、MOSI引脚定义是否与代码中一致。 4. 检查代码中 Adafruit_ST7735库的初始化语句,确认型号(ST7735_BLACKTAB等)和引脚号正确。 |
| 屏幕显示花屏或错乱 | 1. SPI通信速率过快或受干扰。 2. 电源功率不足。 3. 复位(RST)时序问题。 | 1. 在初始化代码中尝试降低SPI时钟频率(如果库支持)。检查杜邦线是否过长,尽量缩短。 2. 换用输出电流更大的电源(如电脑USB口或2A以上的适配器)。 3. 确保RST引脚在上电后有正确的复位脉冲,可尝试在 setup()中手动拉低再拉高该引脚。 |
| 传感器读数始终为0或恒定值 | 1. 传感器供电错误(接反或电压不对)。 2. 信号线未连接或接错。 3. 手指未正确覆盖感光元件。 | 1. 确认VCC接3.3V,GND接GND。用万用表测量传感器输出引脚电压,在有无手指覆盖时应有变化。 2. 确认信号线接在了A0(或你定义的)引脚。 3. 调整手指按压的位置和力度,确保感光区域被完全覆盖。 |
| 心率读数不稳定(数值乱跳) | 1. 信号噪声大(环境光、运动伪影)。 2. 滤波算法参数不佳。 3. 峰值检测算法过于敏感。 | 1. 在暗光环境下测试,保持手指静止。尝试用不透明材料遮挡传感器周围缝隙。 2. 增加移动平均滤波的窗口大小( numReadings)。3. 提高峰值检测的动态阈值比例,或增加“有效IBI”的范围判断。 |
| 心率读数明显偏快或偏慢 | 1. 峰值检测算法误将噪声峰或次峰当作主峰。 2. 计算BPM的公式或单位有误。 | 1. 观察串口绘图器波形,确认算法检测到的波峰位置是否正确。可能需要结合信号斜率进行更智能的检测。 2. 检查代码: BPM = 60000 / IBI,其中IBI单位必须是毫秒。 |
| 3D打印件组装不严或开裂 | 1. 打印尺寸有误差(收缩)。 2. 卡扣设计过紧或过松。 3. 螺丝孔尺寸不对或拧螺丝用力过猛。 | 1. 在Tinkercad设计时预留适当的公差(如0.2mm间隙)。打印后可用砂纸轻微打磨结合面。 2. 优化卡扣的厚度和倾角。可先打印一个小样测试。 3. 螺丝孔直径应略大于螺丝直径(如M3螺丝,孔设计为3.2-3.4mm)。拧螺丝时使用合适的力度。 |
6.2 项目进阶扩展方向
当基础功能稳定实现后,这个项目还有巨大的扩展空间:
- 数据记录与可视化:为Arduino增加一个SD卡模块,将实时的心率数据(带时间戳)记录到CSV文件中。之后可以将数据导入电脑,用Python(Matplotlib)或Excel绘制长时间的心率变化曲线,观察运动、休息前后的心率恢复情况。
- 无线传输与手机App:用蓝牙模块(如HC-05/06)或Wi-Fi模块(如ESP8266)替换Arduino Uno,将心率数据实时发送到手机。你可以用MIT App Inventor或更专业的平台开发一个简单的手机App来接收和显示数据,甚至实现异常心率报警。
- 多参数监测:升级传感器。使用MAX30102模块,它可以同时测量心率和血氧饱和度(SpO2)。只需修改代码,利用相应的库解析传感器数据,就能在TFT屏上同时显示两个生命体征参数。
- 算法优化与心率变异性(HRV):深入信号处理领域,实现更先进的数字滤波(如巴特沃斯滤波器)和峰值检测算法。更进一步,可以计算连续心跳间隔的微小变化,即心率变异性(HRV),这是一个反映自主神经系统活动的重要指标,对压力评估有一定参考意义。
- 产品化外观设计:使用更专业的3D建模软件(如Fusion 360)重新设计外壳,加入圆角、纹理、品牌Logo等,使用双色打印或后期喷涂,让它看起来更像一个市售的消费电子产品。
这个项目从传感器原理到代码实现,再到结构设计,覆盖了一个嵌入式产品原型开发的完整链条。我个人的体会是,最难的不是让屏幕亮起来或代码跑起来,而是如何让设备在各种情况下(不同肤色、不同体温、轻微移动)都能稳定可靠地工作。这需要反复地调试滤波参数、优化检测算法、改进机械结构。每一次调试后看到波形变得更干净、读数变得更稳定,那种攻克难题的快乐,正是DIY项目最吸引人的地方。最后一个小建议,在将电路装入外壳前,务必在面包板上进行长达数小时的老化测试,模拟各种使用场景,这能帮你提前发现很多潜在的不稳定因素。