
1. 项目概述从“心跳”到“数据”的奇妙旅程最近在捣鼓一些健康监测的小玩意儿发现心率这个指标真是既基础又关键。无论是运动爱好者想了解自己的训练强度还是普通上班族想监测一下工作压力下的身体状态一个能实时、准确测量心率的装置都很有用。市面上的智能手环、手表固然方便但总感觉像个黑盒子数据怎么来的、准不准心里没底。于是我就琢磨着自己动手做一个心率测量装置核心目标就两个原理要搞懂数据要能看。这不只是一个简单的DIY更像是一次对生物信号采集和处理的微型探索。这个DIY心率测量装置本质上是一个光电体积描记法PPG传感器应用。简单来说就是利用血液对特定波长光线的吸收率会随着心脏搏动血液容积变化而周期性变化的特性来间接测量心率。听起来有点绕你可以把它想象成用手指轻轻捏住一个微型手电筒和光敏电阻血液多的时候透过去的光少血液少的时候透过去的光多这个“多”和“少”的波动频率就是你的心率。整个项目非常适合对电子制作、嵌入式编程或者生物医学工程感兴趣的朋友无论你是想验证一个物理原理还是想为某个具体的应用比如低成本健康监测、教学演示打个样都能从中获得清晰的路径和实用的经验。2. 核心方案选型与设计思路拆解2.1 传感器选型为什么是光电式而非电极式心率测量主流有两大技术路线心电图ECG/电极式和光电体积描记PPG/光电式。ECG通过测量皮肤表面的电信号来捕捉心脏的电活动医疗级设备多用此法精度高。PPG则通过光学手段检测皮下血液容积的变化。对于我们DIY而言PPG方案优势明显。首先实现难度和成本是决定性因素。ECG需要高输入阻抗、高共模抑制比的精密放大电路来处理微伏级的心电信号极易受工频干扰和运动伪影影响电路设计和调试门槛很高。而PPG的核心就是一个发光二极管LED和一个光电探测器电路相对简单成本极低。其次使用便利性上PPG传感器可以做成非接触式或轻接触式如指尖、耳垂佩戴而ECG通常需要多个电极与皮肤保持良好导电接触DIY起来佩戴体验不佳。因此基于PPG方案是业余条件下最务实、成功率最高的选择。在PPG传感器里又分透射式和反射式。透射式要求光源和探测器在组织两侧比如夹在指尖或耳垂信号强度通常更大。反射式则是光源和探测器在同一侧检测反射回来的光更适合贴在手腕、胸口等平坦部位。考虑到我们DIY常用指尖作为测量部位透射式是更直接的选择。市面上有集成好的PPG模块比如MAX30102它集成了红光和红外LED、光电探测器、环境光消除电路甚至部分数字处理逻辑能极大简化我们的设计。但为了彻底搞懂原理我决定从最基础的分离元件开始一个高亮度绿光LED和一个光电晶体管。绿光波长约550nm相对于红光和红外光对血液中氧合血红蛋白的吸收特性更敏感在指尖等部位能获得信噪比更好的原始信号。2.2 信号链路设计从微弱光变到清晰脉搏波确定了传感器接下来就是设计整个信号处理的链路。从光电晶体管出来的信号非常微弱且混杂着各种噪声我们的任务就是把它放大、滤波变成一个干净的、能明显看到脉搏波起伏的模拟电压信号。整个链路可以拆解为以下几个关键环节光电转换与一级放大光电晶体管将光信号转换为微弱的电流信号。我们需要一个跨阻放大器TIA电路将其转换为电压信号并进行初步放大。这是整个链路中最关键的一步放大倍数需要仔细计算既要保证小信号能被有效放大又要防止饱和。高通滤波去除直流分量PPG信号包含一个很大的直流DC分量它反映了组织的总体吸光度而我们需要的是叠加在上面的交流AC分量即由脉搏引起的波动。一个截止频率在0.5Hz左右的高通滤波器如一阶RC高通可以有效地剥离直流分量只留下脉搏波。低通滤波去除高频噪声环境光变化、电源纹波、电路热噪声等会引入高频干扰。我们需要一个截止频率在5-10Hz左右的低通滤波器因为成年人的心率范围通常在0.8Hz到3.3Hz即48-200 BPM之间保留这个频带就够了。二级放大与电平调整经过滤波后的AC信号幅度可能仍然较小不足以被微控制器的ADC模数转换器很好地分辨。需要再进行一次可调增益的放大并将信号整体抬升到一个合适的电压范围例如围绕1.65V波动如果ADC参考电压是3.3V。这个模拟信号处理部分我选择使用一颗通用双运放芯片如TL082或LM358来搭建。虽然性能比不上仪表放大器但对于这个项目已经足够且成本低廉便于理解每一级电路的作用。2.3 微控制器与数据处理方案处理后的模拟脉搏波信号需要送入微控制器进行数字化和心率计算。这里有几个选择Arduino、ESP32或者STM32。Arduino Uno简单易用社区资源丰富但其ADC精度10位和主频较低对于需要一定实时性的信号处理可能稍显吃力。ESP32功能强大自带Wi-Fi/蓝牙双核处理ADC精度也可接受12位如果想做无线数据传输比如到手机APP是绝佳选择。STM32系列性能最强外设丰富但开发环境相对复杂。考虑到首次实现以可靠和直观为首要目标我选择了折中的方案使用Arduino Nano基于ATmega328P因为它体积小巧引脚兼容Uno且拥有足够的处理能力来完成我们的核心算法。心率计算算法将在后文详细展开其核心思想是在时域内检测脉搏波的峰值计算峰值间隔时间IBI再转换为每分钟心跳次数BPM。3. 硬件电路搭建与核心参数计算3.1 元器件清单与电路原理图以下是构建核心模拟前端所需的主要元器件传感器部分5mm高亮度绿光LED 1个光电晶体管如PT333-3C1个。运放芯片TL082CP双运放1片。电阻电容若干阻值的电阻如10kΩ, 100kΩ, 1MΩ, 10MΩ和电容如1μF, 10μF电解电容 0.1μF陶瓷电容。微控制器Arduino Nano 开发板 1块。其他面包板、杜邦线、用于手指固定的夹子或软套。电路原理图分为三个主要部分传感器偏置与TIA电路绿光LED通过一个限流电阻如220Ω连接到3.3V电源。光电晶体管的集电极接运放的反相输入端发射极接地。在反相输入端和输出端之间连接一个反馈电阻R_f例如10MΩ。这就是经典的跨阻放大器。R_f的阻值决定了转换增益V_out I_photo * R_f。光电晶体管产生的光电流通常在微安级别因此需要兆欧级的反馈电阻来获得伏特级别的输出电压。同相输入端通过一个电阻连接到地或一个偏置电压这里简单接地。注意反馈电阻R_f的阻值极高容易引入噪声。建议使用金属膜电阻并保持该部分电路走线简短避免干扰。可以在R_f两端并联一个小电容如1-10pF以抑制高频振荡增加稳定性。滤波与放大电路第一级运放的输出首先经过一个高通滤波器。用一个1μF的电容C_hp串联信号路径后接一个到地的电阻R_hp例如1MΩ截止频率f_c 1/(2πR_hpC_hp) ≈ 0.16Hz足以去除直流。高通滤波后的信号进入第二级运放构成一个同相放大器。放大倍数由两个电阻决定Gain 1 (R_f2 / R_g2)。这里R_f2和R_g2可以选用可变电阻如100kΩ电位器以便调试。同时在同相输入端提供一个偏置电压例如通过两个等值电阻对3.3V分压得到1.65V将交流信号抬升到以1.65V为中心。电源去耦在运放的电源引脚正负电源附近务必各接一个0.1μF的陶瓷电容到地以滤除电源噪声这对模拟小信号处理至关重要。3.2 关键参数计算与调试心得跨阻增益计算假设在手指透射条件下光电晶体管产生约0.5μA的峰值光电流变化AC分量。为了得到足够幅度的电压信号比如希望AC分量有0.5V的波动。那么所需的跨阻 R_f V / I 0.5V / 0.5μA 1MΩ。实际中由于个体差异皮肤厚度、颜色和佩戴松紧度光电流变化范围很大因此可能需要尝试不同的R_f值例如从1MΩ到10MΩ观察输出信号幅度。实操心得开始时可以用一个5MΩ或10MΩ的电阻如果输出饱和接近电源电压说明增益太大需要减小R_f如果信号太小几十毫伏则需要增大R_f或增加后续放大倍数。滤波器截止频率设定高通目标去除接近直流的缓慢漂移。心率低至40 BPM时频率约为0.67Hz。设置截止频率在0.1-0.5Hz比较安全。例如取R_hp1MΩ, C_hp1μF f_c0.16Hz。低通目标抑制高频噪声。心率高达200 BPM时频率约为3.3Hz。考虑到脉搏波并非纯正弦波包含一定谐波为了保持波形形状截止频率可以设得稍高如5-10Hz。可以在第二级放大后加一个简单的RC低通如R10kΩ, C1μF f_c≈16Hz或者依靠软件数字滤波。二级放大倍数调试经过高通滤波后纯粹的AC信号可能只有100-200mV。为了充分利用Arduino Nano的ADC量程0-3.3V 10位精度即约3.2mV/LSB我们希望信号峰值达到1V左右。因此二级放大倍数设置在5-10倍是可调的。重要提示务必先确保高通滤波器工作正常去除直流分量否则巨大的直流电压会被二级放大并导致输出饱和。搭建与调试顺序建议先在面包板上搭建TIA第一级用示波器或Arduino的ADC读取后通过串口绘图观察输出。用手指轻轻按住传感器LED和光电晶体管对准指尖两侧应该能看到一个随着心跳缓慢波动的电压信号上面叠加着很多噪声。然后接入高通滤波观察波形是否被“拉”回到中心零点附近。最后调整二级放大倍数得到一个幅度适中、零点居中的清晰脉搏波信号。这个过程需要耐心也是理解PPG信号本质的最佳时机。4. 固件编程与心率算法实现4.1 数据采集与预处理硬件信号准备好后就需要用Arduino来读取和处理了。首先将模拟信号输出端连接到Arduino Nano的一个模拟输入引脚如A0。const int ppgPin A0; // 脉搏波信号输入引脚 int sensorValue 0; long lastBeatTime 0; // 上次检测到心跳的时间 int peak 0; // 峰值 int trough 1024; // 谷值 int threshold 512; // 动态阈值 int bpm 0; // 计算得到的心率 bool pulseDetected false; void setup() { Serial.begin(115200); // 开启串口用于调试和绘图 } void loop() { sensorValue analogRead(ppgPin); // 读取原始值 (0-1023) // ... 后续处理 }原始数据噪声很大直接进行峰值检测不可靠。我们需要进行软件滤波。一个简单有效的方法是移动平均滤波它可以平滑掉随机高频噪声。#define SAMPLE_SIZE 10 int sampleBuffer[SAMPLE_SIZE]; int sampleIndex 0; int runningTotal 0; int getFilteredValue(int rawValue) { runningTotal runningTotal - sampleBuffer[sampleIndex]; // 减去最旧的值 sampleBuffer[sampleIndex] rawValue; // 存入新值 runningTotal runningTotal rawValue; sampleIndex (sampleIndex 1) % SAMPLE_SIZE; // 循环索引 return runningTotal / SAMPLE_SIZE; // 返回平均值 }对于更复杂的噪声可以考虑在移动平均后再实施一个软件带通滤波只保留心率相关的频率成分例如0.7Hz - 4Hz。但作为入门移动平均结合合理的阈值算法已经能取得不错的效果。4.2 心率检测算法时域峰值检测法心率算法的核心是实时检测脉搏波的峰值点计算相邻峰值的时间间隔IBI进而计算BPM。这里介绍一种结合动态阈值的稳健算法。信号跟踪与极值更新持续跟踪滤波后信号的局部最大值峰值和最小值谷值。if (filteredValue peak) { peak filteredValue; // 更新峰值 } if (filteredValue trough) { trough filteredValue; // 更新谷值 }动态阈值计算阈值不应是固定值。一个常用的方法是根据最近的峰谷值动态计算threshold trough (peak - trough) * 0.5;即取当前波动幅度的中点。也可以加入衰减因子让阈值随时间缓慢下降以应对信号基线的漂移。峰值检测与防抖当信号值超过阈值并且处于上升沿时标记一个“可能的心跳”。但需要防止噪声引起的多次触发。常见的防抖逻辑是一旦检测到一个心跳在接下来的一段时间例如200-300毫秒对应心率200BPM内忽略任何新的超过阈值的事件。if (filteredValue threshold !pulseDetected (millis() - lastBeatTime) 300) { // 检测到有效心跳 pulseDetected true; int interBeatInterval millis() - lastBeatTime; // 计算间隔毫秒 lastBeatTime millis(); // 计算BPM60000毫秒 / 间隔毫秒 if (interBeatInterval 0) { bpm 60000 / interBeatInterval; // 可以对bpm进行范围限制例如 40-200 BPM if (bpm 40 || bpm 200) { bpm 0; // 视为无效数据 } } // 重置峰谷值为下一个周期准备 peak filteredValue; trough filteredValue; } if (filteredValue threshold) { pulseDetected false; // 信号回落重置检测标志 }输出与显示计算出的BPM可以通过串口发送到电脑用串口绘图器或Processing编写的程序显示波形和数字心率。也可以连接一个OLED屏幕如SSD1306来本地显示心率值这样就是一个完整的独立设备了。算法优化心得单纯的阈值法在运动或测量不当时容易误检或漏检。可以引入信号质量指数SQI的概念例如计算最近几个IBI的标准差如果变异过大则认为信号不可靠不更新显示的心率值或者显示“信号弱”提示。更高级的算法可以在频域进行如FFT但在Arduino上实时计算负担较重时域法在静态或轻度活动下足够有效。5. 系统集成、测试与优化5.1 机械结构与佩戴设计可靠的信号获取离不开稳定的测量环境。你需要将LED和光电晶体管固定在一个夹子上确保它们正对且距离合适通常几毫米并且能够稳固地夹住指尖或耳垂避免环境光漏入。可以用黑色热缩管或胶带将传感器部分包裹起来起到遮光作用。佩戴时手指不要用力按压自然放松即可过度压迫反而会阻碍血液流动影响信号。对于整个电路可以先在面包板上调试确认所有功能正常后可以考虑用万用板焊接一个更稳定的版本或者设计一块简单的PCB。电源方面Arduino Nano可以通过USB供电或者用一块9V电池配合稳压模块供电。如果要做成可穿戴的一块小容量的锂电池如3.7V 500mAh加上充电模块是更优雅的方案。5.2 校准与准确性验证DIY设备的准确性需要验证。最直接的方法是与一个你认为可靠的设备如医用指夹式血氧仪、口碑较好的智能手环进行同步对比测量。在静坐状态下同时记录两者显示的心率值持续几分钟观察趋势是否一致数值差异是否在可接受范围内如±5 BPM。校准主要针对两个方面硬件基线在未佩戴传感器时读取ADC值这个值应该对应环境的直流光强。可以记录这个值作为“零点”参考在软件中做偏移校正。算法参数阈值系数上文中的0.5、防抖时间窗口、滤波窗口大小等可能需要根据实际信号特征进行微调。最好的办法是录制一段包含良好脉搏波信号的ADC数据通过串口输出到电脑文件然后在电脑上用Python或MATLAB编写脚本离线调试这些参数找到最优组合后再固化到Arduino代码中。5.3 常见问题与排查实录在制作过程中你几乎一定会遇到以下问题。这里是我的排查记录问题现象可能原因排查与解决思路输出信号完全饱和接近电源电压或0V1. 跨阻放大器反馈电阻R_f过大增益太高。2. 环境光太强光电晶体管饱和。3. 运放电路接线错误形成正反馈。1. 减小R_f阻值或降低LED电流增大限流电阻。2. 确保传感器部分遮光良好在暗处测试。3. 检查运放反相/同相输入端是否接反反馈网络是否连接正确。信号幅度非常小50mV波动1. R_f阻值太小增益不足。2. LED亮度不够或光电晶体管距离太远。3. 测量部位血流不丰富或佩戴不当。1. 增大R_f阻值尝试5MΩ, 10MΩ。2. 确保LED正常发光尝试减小LED限流电阻但注意别烧毁调整传感器对齐。3. 尝试耳垂、指尖等不同部位确保轻微接触压力。信号中有规律的50Hz/100Hz工频干扰电源噪声或空间电磁干扰。1.最有效方法为运放电源增加高质量的退耦电容0.1μF陶瓷电容并联10μF电解电容并尽量靠近运放电源引脚。2. 尝试使用电池供电隔离市电干扰。3. 在软件中增加50Hz陷波滤波。脉搏波形混乱无法稳定检测峰值1. 运动伪影手抖。2. 滤波参数不当噪声未有效滤除。3. 算法阈值设置不灵敏。1. 保持测量时身体和手指静止。2. 调整软件移动平均的窗口大小或引入低通滤波。3. 优化动态阈值算法引入信号质量判断在信号差时暂停更新心率。心率读数跳动剧烈不稳定1. 单次IBI计算受偶然误差影响大。2. 信号质量时好时坏。1. 采用滑动平均法平滑心率输出例如存储最近4-8个有效的BPM值求平均后显示。2. 增加IBI有效性检查剔除明显不合理如间隔小于300ms或大于1500ms的数据。一个关键的实操心得示波器是你的最佳朋友。如果没有硬件示波器强烈利用Arduino的串口绘图器Serial Plotter。将原始ADC值、滤波后的值、甚至计算出的阈值实时打印出来你能直观地看到每一级电路、每一个算法步骤的效果这对于调试来说是无价之宝。6. 项目延伸与进阶思考完成基础版本后这个项目还有很多可以玩和深化的方向无线化与可视化将主控换成ESP32通过Wi-Fi将实时心率数据和波形发送到电脑或手机APP可以用简单的WebSocket或HTTP服务器实现甚至上传到物联网平台进行长期记录。多参数监测使用像MAX30102这样的集成模块它同时提供红光和红外光通道理论上可以计算血氧饱和度SpO2。算法更复杂需要计算两个波长的AC/DC比值。心率变异性HRV分析心率不仅仅是平均值其瞬间波动HRV是反映自主神经系统活动的重要指标。在能稳定获取IBI序列的基础上可以计算SDNN、RMSSD等时域指标这需要更稳定的信号和更复杂的算法。低功耗设计如果想做成长期佩戴的设备需要考虑功耗。可以周期性地唤醒传感器进行测量如每秒一次其他时间让MCU和传感器进入休眠模式并使用纽扣电池供电。做这个项目的最大收获不仅仅是得到了一个能闪数字的小装置而是亲手验证了一个生物医学测量的基本原理经历了从模拟信号拾取、放大滤波、数字转换到软件算法处理的完整链路。每一个环节的微小调整都会影响最终结果这种“看得见摸得着”的反馈比单纯看理论要有趣得多。下次如果你看到智能手环上的心率数字脑海里大概就能浮现出背后那条微弱光信号所经历的奇妙旅程了。最后一个小建议在焊接最终版电路时模拟部分的地线走线要尽量粗短形成“星型接地”或单点接地能有效降低噪声这是从多次调试中得来的宝贵经验。