基于PPG原理的DIY心率测量:从硬件搭建到信号处理全解析

📅 2026/7/28 21:45:36
基于PPG原理的DIY心率测量:从硬件搭建到信号处理全解析
1. 从零开始为什么选择自己动手做心率测量如果你对电子制作或者健康监测感兴趣可能早就想过自己动手做一个心率测量装置。市面上从几十块的智能手环到几千块的专业医疗设备选择很多为什么还要费这个劲我最初也是抱着这个疑问开始的但实际做下来发现这个过程的价值远超一个现成的产品。它不仅仅是为了得到一个能显示数字的小玩意更是一次对生物信号采集、模拟电路、数字信号处理乃至嵌入式编程的完整实践。你能亲手触摸到从指尖微弱的血流变化到最终屏幕上稳定跳动的数字之间每一个环节是如何被“翻译”和“计算”出来的。这种从原理到成品的掌控感是任何成品设备都无法给予的。更重要的是自己做的过程让你能完全理解数据的来源和局限。你知道屏幕上那个“75次/分”是怎么算出来的也知道在什么情况下它会不准比如手指没放好、环境光太强以及如何通过调整算法或硬件来改善。这对于学习电子工程、生物医学工程或者单纯想深入理解可穿戴设备原理的朋友来说是一次绝佳的实战机会。我们今天要做的就是一个基于光电容积脉搏波描记法PPG原理的心率测量装置它成本低廉、原理直观非常适合作为入门项目。2. 核心原理拆解光如何“看见”心跳在开始动手之前我们必须先搞清楚最核心的问题不用听诊器不用把电极贴在胸口仅仅靠手指接触是怎么测出心率的答案就藏在你的指尖血管里其技术核心叫做光电容积脉搏波描记法。你可以把这个过程想象成一个非常精密的“透光实验”。我们的血液是红色的因为它反射红光并吸收绿光。心脏每一次搏动都会将一股血液泵入动脉导致指尖的微小动脉血管会发生周期性的扩张和收缩。血管扩张时里面的血液量增多收缩时血液量减少。现在我们在手指的一侧放置一个发光二极管通常是绿光LED在另一侧放置一个光敏传感器如光电晶体管或光电二极管。当LED发出的绿光照射到手指上时大部分光会被皮肤、肌肉等组织吸收和散射但有一部分会穿透组织被血管中的血液吸收。血液量多的时候心脏收缩血液泵出吸收的绿光就多血液量少的时候心脏舒张吸收的绿光就少。因此穿透手指后到达光敏传感器的光强就会随着心跳的节奏发生微弱的周期性变化。这个变化可能只有环境光的百分之几非常微弱。光敏传感器将这个变化的光信号转换成相应变化的电信号通常是电压的微小波动这个电信号就是原始的脉搏波信号。这个原始信号里不仅包含我们想要的心跳节律频率还混杂了大量的“噪声”。比如你的手稍微晃动一下运动伪影传感器接收到的光强就会剧烈变化环境光尤其是日光灯这种有频闪的光源会直接干扰测量甚至呼吸、体温变化都会带来缓慢的基线漂移。所以整个装置的设计和后续的算法处理核心任务就是两件事第一硬件上尽可能清晰地捕捉到这个微弱的脉搏波信号第二用软件算法从充满噪声的信号中准确地找出心跳的周期。注意绿光LED是消费级心率设备如手环的主流选择因为它对血液中氧合血红蛋白的吸收率差异更敏感在皮肤组织中的穿透性也适合浅表血管测量相比红光抗环境光干扰能力稍强。我们DIY也首选绿光LED。3. 硬件选型与电路设计搭建信号的“高速公路”理解了原理我们就可以开始搭建硬件了。我们的目标是设计一个电路能够稳定地驱动LED并高保真地采集光敏传感器传来的微弱信号。整个信号链的每一个环节都至关重要。3.1 核心元器件选型光源LED选择一款高亮度的绿色LED。波长通常在525nm左右。别用普通的指示灯LED亮度不够。我选用的是直径5mm的草帽型高亮绿光LED其典型正向电压约3.2V工作电流20mA。光传感器这是关键中的关键。常见的有光电晶体管和光电二极管。光电晶体管灵敏度高、输出电流大但响应速度相对慢且容易受环境温度影响。光电二极管响应速度极快线性度好更适合需要精确波形分析的场合。对于心率测量光电晶体管已完全足够且电路更简单。我选择的是NPN型光电晶体管例如LTR-329ALS-01这是一款集成度较高的环境光传感器但也可用于此用途或更常见的通用型光电晶体管。微控制器MCU需要负责控制LED闪烁、读取传感器模拟信号、进行数字滤波和计算心率。需要至少一个ADC模数转换器通道和足够的计算能力。Arduino Uno基于ATmega328P是绝佳的入门选择它简单易用社区资源丰富ADC精度为10位0-1023对于这个项目绰绰有余。如果想更小巧或进阶ESP32自带蓝牙Wi-Fi可无线传输数据或STM32系列性能更强也是好选择。本项目以Arduino Uno为例。运算放大器Op-Amp原始信号太微弱可能只有几毫伏的波动Arduino的ADC可能无法有效分辨。我们需要一个运算放大器电路将信号放大到合适的范围例如0-5V。选择一款低噪声、低漂移的通用运放即可如经典的LM358双运放一片搞定放大和滤波或MCP6002。电阻、电容、面包板、导线等若干。电阻用于限流、分压电容用于滤波。3.2 电路原理图与连接详解整个电路可以分为三个部分LED驱动电路、传感器采集电路、信号放大与滤波电路。为了清晰我们分步搭建。第一步LED驱动电路我们不能让LED常亮一方面是为了省电、防止发热更重要的是为了消除环境直流光的影响。采用脉冲驱动方式让LED以一定频率例如100Hz快速闪烁后续我们只处理与这个频率同步的信号就能大幅抑制环境光干扰。Arduino Pin 9 (PWM) ---[220Ω电阻]--- LED阳极 LED阴极 --- GND通过Arduino的PWM引脚控制我们可以轻松调节LED的亮度即发光强度。220Ω电阻用于限制电流保护LED和Arduino引脚。假设电源5VLED压降3.2V则电流约为 (5V-3.2V)/220Ω ≈ 8mA安全范围内。第二步光电晶体管采集电路光电晶体管可以看作一个受光控制的电流源。光照越强集电极和发射极之间流过的电流Ice越大。VCC (5V) --- [光电晶体管集电极C] 光电晶体管发射极E --- [负载电阻R_load (10kΩ)] --- GND Arduino A0 --- 连接到光电晶体管E极与R_load之间这个连接构成了一个共发射极配置。当LED光透过手指照到光电晶体管时Ice增大在负载电阻R_load上产生的压降增大导致E极电压即A0点电压降低。注意这个变化与直觉相反光越强A0电压越低。这个电压信号就包含了脉搏波信息。第三步信号放大与滤波电路基于LM358从A0点直接读取的信号变化幅度太小且包含高频噪声。我们需要用运放搭建一个放大滤波电路。这里设计一个两级电路交流耦合与高通滤波首先用一个电容隔直去除信号的直流分量即环境光强决定的基线只保留交流的脉搏波动。同时电容和电阻构成一个高通滤波器滤除因呼吸等产生的极低频漂移通常低于0.5Hz。A0点 --- [耦合电容C1 (1μF)] --- [电阻R1 (10kΩ)] --- 运放同相输入端() 运放反相输入端(-) --- [电阻R2 (10kΩ)] --- GND 运放反相输入端(-) --- [反馈电阻R3 (100kΩ)] --- 运放输出端 运放输出端 --- Arduino A1这第一级是一个同相放大器放大倍数 A_v 1 R3/R2 1 100k/10k 11倍。耦合电容C1和电阻R1构成的高通滤波器截止频率 f_c 1/(2πR1C1) ≈ 16Hz能有效滤除基线漂移。但请注意心跳频率一般在0.8Hz到3Hz即48-180次/分16Hz的截止频率太高了会把心跳信号也滤掉一部分。实际上我们需要一个截止频率在0.5Hz左右的高通滤波器。这需要非常大的电容或电阻在面包板上不易实现且容易引入噪声。因此在硬件上我们只做初步的隔直和放大把精细的高通滤波留给软件数字滤波器这是更灵活和常见的做法。所以我们可以先将C1增加到10μF将截止频率降到约1.6Hz进行初步处理。低通滤波为了滤除高频噪声如灯光频闪、电路噪声我们需要一个低通滤波器。可以在第一级运放输出后再加一级简单的RC低通滤波或者直接使用运放构建有源低通滤波器。为了简化我们在软件中同样可以实现出色的低通滤波。因此硬件部分我们主要完成信号的线性放大。最终硬件连接思路将光电晶体管电路输出的微弱信号A0送入由LM358构成的第一级放大电路进行交流耦合和11倍放大放大后的信号从运放输出端接到Arduino的另一个模拟引脚A1。这样Arduino通过A1读取的就是经过初步调理的、幅度更大的脉搏波信号。4. 固件编程从模拟信号到心跳数字硬件是躯体软件是灵魂。Arduino程序固件需要完成以下几项核心任务同步驱动LED、采样信号、数字滤波、计算心率。这是整个项目最体现“技术含量”的部分。4.1 程序框架与采样策略我们采用同步检测技术来抑制噪声。即让LED闪烁的频率与我们采样的频率同步并在LED“亮”和“灭”的周期内分别采样通过计算差值来消除共模的环境光干扰。// 定义引脚 const int ledPin 9; // LED控制引脚 const int sensorPin A1; // 连接运放输出的引脚 int sampleRate 100; // 采样率 (Hz)即每秒采样100次 int ledFrequency 25; // LED闪烁频率 (Hz)为了得到足够的采样点通常设为采样率的约数如1/4 int samplesPerCycle sampleRate / ledFrequency; // 每个LED闪烁周期内的采样点数 4 long lastSampleTime 0; int sampleCount 0; int rawValue 0; int ambientLight 0; int pulseSignal 0; // 用于存储一段时间内的信号值用于计算心率 #define BUFFER_SIZE 200 int signalBuffer[BUFFER_SIZE]; int bufferIndex 0; void setup() { Serial.begin(115200); pinMode(ledPin, OUTPUT); // 初始化缓冲区 for (int i 0; i BUFFER_SIZE; i) { signalBuffer[i] 512; // 初始化为中间值 } } void loop() { long currentTime micros(); // 固定频率采样 if (currentTime - lastSampleTime (1000000 / sampleRate)) { lastSampleTime currentTime; // 控制LED闪烁每4个采样点中前2个点LED亮后2个点LED灭 if (sampleCount (samplesPerCycle / 2)) { analogWrite(ledPin, 50); // LED以一定亮度亮起PWM值50 } else { analogWrite(ledPin, 0); // LED熄灭 } // 读取传感器值 rawValue analogRead(sensorPin); // 同步解调在一个完整的亮-灭周期内用“亮”时的信号减去“灭”时的信号 // 这里简化处理将每个周期内前两个采样点视为“信号环境光”后两个视为“环境光” // 实际可以更精细地计算平均值 if (sampleCount 0 || sampleCount 1) { // 积累信号环境光 pulseSignal rawValue; } else if (sampleCount 2 || sampleCount 3) { // 计算差值近似。更准确的做法是分别存储亮、灭周期的值再相减。 // 这里为了简化流程我们先直接使用原始值进行后续滤波。 // 实际上同步解调需要在循环中维护两个累加器。 } // 简易的同步解调实现概念性 static int sumBright 0, sumDark 0; static int countBright 0, countDark 0; if (sampleCount (samplesPerCycle / 2)) { sumBright rawValue; countBright; } else { sumDark rawValue; countDark; } // 当一个完整的闪烁周期结束时计算差值并存入缓冲区 sampleCount; if (sampleCount samplesPerCycle) { sampleCount 0; if (countBright 0 countDark 0) { int brightAvg sumBright / countBright; int darkAvg sumDark / countDark; int filteredValue brightAvg - darkAvg; // 这就是经过同步检测后的脉搏波信号 // 对filteredValue进行软件滤波和心率计算见下文 processSignal(filteredValue); sumBright 0; sumDark 0; countBright 0; countDark 0; } } } }4.2 数字信号处理软件滤波经过硬件初步调理和同步检测信号干净了许多但仍有噪声。我们需要在软件中实施更精确的滤波。一个经典的心率信号处理链包括直流移除软件高通滤波-低通滤波-可能还有工频陷波50/60Hz。1. 直流移除移动平均高通滤波脉搏波信号叠加在一个缓慢变化的直流分量上。我们可以通过计算信号的移动平均线相当于直流分量然后从原始信号中减去它。float alpha 0.1; // 移动平均的平滑系数越小越平滑响应越慢 float dcW 0; // 直流分量估计值 float removeDC(float input) { dcW (alpha * input) ((1 - alpha) * dcW); return input - dcW; }2. 低通滤波一阶无限脉冲响应IIR滤波器为了只保留心跳频率附近的信号0.8-3 Hz我们需要一个低通滤波器。一个简单的一阶IIR低通滤波器实现如下float lowPassFilter(float input) { static float output 0; float cutoffFreq 5.0; // 截止频率设为5Hz略高于心跳最高频率 float dt 1.0 / sampleRate; // 采样间隔 float RC 1.0 / (2 * PI * cutoffFreq); float alpha dt / (RC dt); output (alpha * input) ((1 - alpha) * output); return output; }将removeDC和lowPassFilter函数依次应用于filteredValue就能得到一个相对干净的脉搏波信号。4.3 心率计算算法这是最后一步也是最有挑战性的一步。如何从一波一波的脉搏波形中计算出每分钟有多少次BPM常用方法有时域峰值检测和频域分析。时域峰值检测法寻找波形的峰值或上升沿过零点计算相邻峰值的时间间隔然后转换为心率。这种方法直观但对波形质量要求高容易因噪声产生误判。#define THRESHOLD 50 // 峰值检测阈值需要根据实际信号幅度调整 long lastPeakTime 0; int bpm 0; void calculateBPM_TimeDomain(int signal) { static int lastSignal 0; static bool rising false; // 寻找上升沿信号超过阈值且上一时刻低于阈值 if (signal THRESHOLD lastSignal THRESHOLD rising false) { rising true; long currentTime millis(); if (lastPeakTime 0) { long interval currentTime - lastPeakTime; // 两次心跳的间隔毫秒 bpm 60000 / interval; // 转换为每分钟心跳次数 // 对bpm进行合理性检查例如限制在40-180之间和平滑处理 } lastPeakTime currentTime; } if (signal THRESHOLD) { rising false; } lastSignal signal; }频域分析法更稳健将一段时间如8-10秒的信号缓冲区进行快速傅里叶变换FFT找到能量最强的频率成分这个频率就是心跳频率。Arduino有现成的FFT库如arduinoFFT虽然计算量稍大但抗干扰能力更强。#include arduinoFFT.h arduinoFFT FFT arduinoFFT(); #define SAMPLES 128 // 进行FFT的样本数必须是2的幂 #define SAMPLING_FREQ 100 // 采样频率 double vReal[SAMPLES]; double vImag[SAMPLES]; void calculateBPM_FrequencyDomain() { // 1. 将最新的SAMPLES个滤波后的信号存入vReal数组vImag置零 for (int i 0; i SAMPLES; i) { vReal[i] signalBuffer[(bufferIndex - SAMPLES i BUFFER_SIZE) % BUFFER_SIZE]; vImag[i] 0; } // 2. 执行FFT FFT.Windowing(vReal, SAMPLES, FFT_WIN_TYP_HAMMING, FFT_FORWARD); FFT.Compute(vReal, vImag, SAMPLES, FFT_FORWARD); FFT.ComplexToMagnitude(vReal, vImag, SAMPLES); // 3. 寻找峰值频率避开直流0Hz double peakFreq 0; double peakMag 0; // FFT结果数组的前一半是有效频率每个bin对应的频率 binIndex * (SAMPLING_FREQ / SAMPLES) for (int i 2; i (SAMPLES/2); i) { // 从i2开始忽略直流和极低频 double freq (i * 1.0 * SAMPLING_FREQ) / SAMPLES; if (freq 0.8 freq 3.0) { // 只关心0.8-3Hz范围48-180 BPM if (vReal[i] peakMag) { peakMag vReal[i]; peakFreq freq; } } } // 4. 将频率转换为BPM if (peakFreq 0) { bpm round(peakFreq * 60.0); // 同样进行合理性检查和平滑 } }在实际项目中我推荐结合两种方法使用时域法进行实时性要求高的初步估计同时用频域法定期如每5秒进行校准和验证这样既能快速响应心率变化又能保证结果的稳定性。5. 实测调试与避坑指南让装置真正工作起来电路连好了代码烧录了但把手指放上去串口绘图器里的波形可能还是一团乱麻或者心率值乱跳。别急这是最正常的阶段。调试是DIY的精髓以下是几个关键的调试步骤和常见问题的解决方法。第一步验证硬件信号通路不放手指标在环境光稳定的情况下运行一个只读取A1原始值并打印到串口绘图器的简单程序。用手在传感器上方快速晃动观察波形是否有剧烈变化。这能验证光电传感器和运放电路是否基本工作。遮挡测试用不透明物体完全盖住传感器和LED观察读数是否降到很低接近0。然后移除遮挡读数应回升。这验证了光路是否正常。第二步优化信号质量机械固定与遮光这是影响信号质量的最大因素。必须将LED和光电晶体管紧密、正对地固定在手指夹套或耳夹内确保光路穿过组织。任何微小的相对移动都会产生巨大的运动伪影。使用热熔胶或3D打印一个外壳来固定它们。整个传感部分必须严格遮光避免环境光直射进入传感器。可以用黑色电工胶布或热缩管包裹。调整LED亮度在代码中调整analogWrite(ledPin, value)的value值0-255。亮度太低信号弱亮度太高可能饱和信号顶部变平或使被测者不适。找到一个能产生清晰波动波形且基线稳定的值。调整运放增益如果信号幅度太小在Arduino绘图器里波动只有几个点可以增大运放的放大倍数增大R3。如果信号饱和顶到0或1023则减小增益减小R3或降低LED亮度。软件滤波参数微调这是软件调试的核心。打开Arduino的串口绘图器同时绘制原始信号和经过软件滤波后的信号。如果滤波后信号仍有高频毛刺降低低通滤波器的截止频率cutoffFreq比如从5Hz降到3Hz。如果波形严重畸变或延迟太大可能是高通滤波的截止频率太高或者低通滤波的截止频率太低。需要根据实际波形反复调整。记住我们的目标是看到一个周期性良好、波峰波谷清晰的类正弦波。第三步心率算法调试与优化阈值法时域的痛点THRESHOLD这个值很难设一个通用的。因为不同人、不同佩戴紧度下信号幅度差异很大。可以考虑动态阈值例如将阈值设为最近一段时间信号最大值和最小值中值的某个比例。FFT法频域的要点确保SAMPLES足够多以提供良好的频率分辨率。SAMPLES128SAMPLING_FREQ100时频率分辨率是100/128≈0.78Hz这对于区分心率精度可能不够相邻BPM差可能达到47次。可以增加SAMPLES到256或512但这会增加计算量和延迟。需要在精度和实时性间权衡。数据平滑计算出的BPM值不要直接输出应该进行平滑处理比如使用移动平均或一阶低通滤波避免数值跳变。float smoothedBPM 0; float smoothingFactor 0.1; void smoothBPM(int newBPM) { if (newBPM 40 newBPM 180) { // 合理性检查 smoothedBPM (smoothingFactor * newBPM) ((1 - smoothingFactor) * smoothedBPM); } }常见问题与解决方案问题信号完全没变化是一条直线。检查LED是否亮起光电晶体管引脚是否接反运放供电是否正确用万用表测量关键点电压。问题波形杂乱无章看不出周期性。首要怀疑运动伪影和光泄露加固遮光确保手指稳定不要移动。尝试在安静状态下测量。检查环境光移到日光灯照不到的地方或者用频域法FFT看看噪声主要在哪一频率。如果50Hz或60Hz成分很强说明受到工频干扰需要加强屏蔽或考虑在软件中加入陷波滤波器。问题心率值跳变剧烈时而正常时而翻倍/减半。这是峰值检测中的“倍频/半频”错误噪声导致在一个心跳周期内检测到多个峰值或在两个周期才检测到一个峰值。解决方法是优化滤波让波形更干净同时加入“不应期”在一次心跳检测后设置一个短暂的时间窗口例如200-300毫秒在此窗口内忽略新的峰值这能有效防止因波形震荡产生的误检。#define REFRACTORY_PERIOD 300 // 不应期单位毫秒 long lastPeakTime 0; if (currentTime - lastPeakTime REFRACTORY_PERIOD) { // 执行峰值检测逻辑 }6. 功能扩展与进阶思路不止于心率当你的基础版本能够稳定输出心率值后这个平台就拥有了巨大的扩展潜力。你可以把它从一个简单的DIY玩具升级为一个功能更全面的生理信号监测原型。1. 添加血氧饱和度SpO2监测原理类似但需要两个LED一个红光约660nm一个红外光约880nm。氧合血红蛋白和脱氧血红蛋白对这两种光的吸收率不同。通过计算红光和红外光吸收变化的比率R值可以估算出血氧饱和度。你需要增加一个LED和对应的驱动电路并交替点亮红光和红外光进行测量。算法上需要同时处理两路信号并计算R值复杂度更高但非常具有挑战性和学习价值。2. 实现波形可视化与数据记录本地显示添加一块OLED或LCD屏幕如SSD1306实时显示心率数值和脉搏波形图。无线传输将主控换成ESP32或ESP8266通过Wi-Fi将心率数据和波形实时发送到手机App或电脑上的可视化界面如Processing、Python Matplotlib。甚至可以接入物联网平台实现远程监测。SD卡存储添加一个SD卡模块将长时间的心率数据以CSV格式记录下来用于后续分析。3. 心率变异性HRV分析心率不是绝对规律的每次心跳的间隔有微小的差异这种差异被称为心率变异性是评估自主神经系统功能的重要指标。在准确检测到每个心跳时刻R峰的基础上你可以计算相邻心跳间隔RR间期进而分析时域指标如SDNN、RMSSD或频域指标。这需要更高精度的计时微秒级和更复杂的信号处理算法。4. 改善佩戴体验与外观使用3D建模软件如Fusion 360设计一个贴合手指或手腕的外壳将电路板、电池如锂聚合物电池集成进去做成一个真正的可穿戴设备原型。考虑加入弹性绑带和柔软的硅胶垫提升舒适度。走完从原理认知、硬件搭建、软件编程到调试优化的全过程这个DIY心率测量装置就不仅仅是一个作品了。它更像一把钥匙为你打开了生物信号采集与处理、嵌入式系统开发、数字信号处理这三扇大门。过程中遇到的每一个问题解决的每一个bug都是实实在在的经验。当你看到自己制作的设备稳定地输出与商用产品接近的心率数据时那种成就感是无与伦比的。这个项目最宝贵的产出不是那个能显示数字的小盒子而是你脑子里构建起来的、关于如何让机器理解生命体征的一整套知识体系。