ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Grove耳夹式心率传感器实战:从PPG原理到Arduino/ESP32/树莓派稳定监测方案

2026/8/2 8:01:49 拓冰建站 浏览量
Grove耳夹式心率传感器实战:从PPG原理到Arduino/ESP32/树莓派稳定监测方案 1. 项目概述从“玩具”到“工具”的心率监测方案如果你玩过Arduino或者树莓派大概率接触过一些传感器模块比如温湿度、光照强度或者超声波测距。这些模块大多能直接给出一个明确的物理量读数上手简单成就感来得也快。但心率传感器尤其是耳夹式这种形态给人的感觉就有点不一样了。它测量的不是环境而是我们身体内部最核心的生命体征之一这让它从一个简单的“电子玩具”零件瞬间具备了成为真正“工具”的潜力。我最初接触Grove耳夹式心率传感器就是被这种潜力吸引。市面上很多心率模块要么是手指夹式的体积大、不方便集成要么是腕带式的需要复杂的算法和滤波。而Grove这款耳夹式传感器通过一个标准的三针或四针接口通常是模拟或数字I/O就能直接输出心率数据看起来是连接Arduino或树莓派最快捷的路径。但实际用下来才发现从“能读到数据”到“数据稳定可靠”中间隔着好几道坎。这不仅仅是接上线、跑个示例代码那么简单它涉及到信号质量、供电噪声、运动伪影、算法处理等一系列问题。这篇文章我就想结合自己多次在Arduino UNO R3、ESP32以及树莓派上折腾这款传感器的经历把它从原理到实践从接线到数据稳定彻底讲清楚。你会发现用好这个小模块不仅能做个实时心率监测仪更能深入理解生物信号采集的挑战甚至为后续开发更复杂的健康监测项目打下基础。无论你是想做个智能健身伴侣、情绪状态监测装置还是单纯对生物电信号感兴趣这篇内容都能给你一套可以直接“抄作业”的完整方案。2. 核心原理光电脉搏波PPG与耳垂的“天然优势”在深入接线和代码之前我们必须先搞明白这个小东西到底是怎么工作的。这决定了我们后续如何处理它输出的信号。Grove耳夹式心率传感器本质上是一个**光电体积描记法Photoplethysmography, PPG**传感器。2.1 PPG技术简析用光“看”血流它的原理非常直观传感器一侧有一颗发光二极管LED通常是绿光。绿光对血液中的血红蛋白吸收率较高而对其他组织穿透性较好。当LED发出的绿光照射到皮肤这里是耳垂时一部分光会被皮肤、肌肉、骨骼等组织吸收另一部分会被反射回来。关键在于随着心脏的搏动耳垂毛细血管中的血流量会呈现周期性变化。心脏收缩时血流量大吸收的光多反射回来的光就少心脏舒张时血流量小吸收的光少反射回来的光就多。传感器另一侧的光电探测器如光电晶体管捕捉到的就是这个反射光强度的微弱周期性变化信号。这个原始信号我们称之为脉搏波Pulse Wave。它不是一个干净的正弦波而是一个包含丰富信息的复杂波形其峰值间的间隔就对应着心跳周期。通过计算单位时间内的峰值数量就能得到心率次/分钟。2.2 为什么是耳夹式对比手指与手腕理解了PPG我们再来看“耳夹式”这个形态的优势。常见的PPG传感器位置有手指、手腕和耳垂。手指信号强但运动伪影极大。手部是活动最频繁的部位任何微小的移动都会导致信号剧烈波动不适合长期、动态监测。手腕智能手表佩戴方便但信号弱。手腕处脂肪和骨骼较多血管较深获取清晰脉搏波需要更强的LED和更复杂的算法这也是苹果、华为等公司投入巨大的地方且同样受手腕运动影响严重。耳垂这是一个被很多人忽略的“黄金位置”。耳垂组织薄毛细血管丰富且靠近表面PPG信号非常强。更重要的是在非剧烈运动状态下头部的运动幅度远小于手部和腕部因此运动伪影相对较小。对于静态或轻度活动下的监测耳夹式能提供比手指和手腕更稳定的信号基线。所以选择Grove耳夹式传感器你其实是选择了一个在信号质量和抗干扰性上比较折中且易于集成的方案。它的输出就是那个随着血流变化的、微弱的模拟电压信号。3. 硬件连接与电路设计稳定信号的基石拿到传感器第一步当然是把它和你的主控板连起来。Grove接口简化了物理连接但电路层面的细节决定了数据的“底噪”水平。3.1 接口识别与基础连接常见的Grove心率传感器模块有两种接口三线制信号、VCC、GND和四线制信号、NC、VCC、GND。核心就是三根线电源VCC、地GND和信号输出SIG。对于Arduino如UNO R3这是最经典的搭配。将传感器的Grove线连接到Arduino的模拟输入引脚例如A0。VCC接5V或3.3V具体看模块支持多数兼容GND接GND。这里有一个关键点优先使用3.3V供电。虽然5V也能工作但3.3V能降低模块自身的发热和噪声对微弱的模拟信号更友好。对于ESP32/ESP8266连接方式类似接到任一模拟输入引脚如GPIO34、GPIO35。ESP32的模拟输入电压范围通常是0-3.3V因此务必使用3.3V为传感器供电防止信号电压超标损坏ADC。对于树莓派Raspberry Pi树莓派本身没有模拟输入引脚这是最大的障碍。你需要一个ADC模数转换扩展板比如ADS1115I2C接口精度高。将心率传感器接到ADC的输入通道ADC再通过I2C与树莓派通信。另一种方法是使用带有ADC的MCU如Arduino作为从机通过串口UART将数据发送给树莓派。注意在连接前务必用万用表确认一下你的模块输出信号类型。有些老版本或不同厂家的模块输出可能是经过简单整形的数字脉冲信号心率直接换算成频率但主流Grove模块输出的是原始的模拟PPG波形。本文讨论的是后者因为它更原始可玩性更高。3.2 容易被忽略的硬件“玄学”滤波与供电直接连接后你可能会在串口绘图器Serial Plotter或任何串口调试助手如SSCOM、Putty里看到一个充满毛刺、上下漂移的波形。这很正常原始PPG信号非常脆弱。1. 硬件低通滤波RC滤波这是提升信号质量性价比最高的方法。在传感器的信号输出线SIG和地GND之间并联一个10kΩ电阻和一个0.1μF104的陶瓷电容构成一个简单的RC低通滤波器。它的截止频率大约在160Hz左右足以保留心率信号通常0.5Hz到5Hz同时滤除大部分高频噪声。传感器SIG引脚 ——┬—— 到MCU的ADC引脚 R (10kΩ) | C (0.1μF) | GND这个小电路可以直接焊在杜邦线接头上或者用一个小面包板实现。效果立竿见影波形会平滑很多。2. 电源去耦模拟电路对电源噪声极其敏感。在传感器的VCC和GND引脚之间尽可能靠近传感器模块的地方并联一个10μF的电解电容和一个0.1μF的陶瓷电容。电解电容应对低频波动陶瓷电容滤除高频噪声。如果你的电源来自USB这个步骤尤为重要能有效抑制来自电脑或充电宝的电源噪声。3. 物理固定与避光耳夹的松紧度会影响信号。太紧压迫血管信号失真太松则接触不良信号断续。调整到既稳固又不感到压迫为宜。此外环境光尤其是日光灯、太阳光会干扰光电探测器。确保耳夹闭合良好避免环境光从缝隙射入。有些DIY玩家会用黑色热缩管或电工胶布包裹传感器非接触部分效果很好。4. 软件算法从嘈杂波形中提取心跳硬件搭建好了波形也看到了接下来就是最核心的部分如何用代码从那个依然有些抖动的波形里准确地算出心率。这里我分享一个经过实践验证、适合嵌入式平台的算法流程。4.1 数据采集与滑动平均滤波首先我们需要以一定的频率采样率读取ADC值。对于心率信号100Hz到200Hz的采样率足够了。太高会增加计算负担太低可能丢失波形细节。// Arduino 示例代码片段 - 基础数据采集 const int hrPin A0; // 心率传感器连接的引脚 const long sampleInterval 10; // 采样间隔10ms即100Hz unsigned long previousMillis 0; int rawValue 0; void setup() { Serial.begin(115200); // 初始化串口用于绘图或调试 } void loop() { unsigned long currentMillis millis(); if (currentMillis - previousMillis sampleInterval) { previousMillis currentMillis; rawValue analogRead(hrPin); // 读取原始值 // 1. 滑动平均滤波 (Moving Average) static int filterBuffer[10]; // 缓存数组 static int index 0; static long sum 0; sum - filterBuffer[index]; // 减去最旧的值 filterBuffer[index] rawValue; // 存入新值 sum rawValue; index (index 1) % 10; // 更新索引 int filteredValue sum / 10; // 计算平均值 // 将滤波后的值通过串口输出用于绘图 Serial.println(filteredValue); } }滑动平均能有效平滑随机噪声是信号处理的第一步。4.2 动态阈值峰值检测算法经过滤波的信号已经有了清晰的波形轮廓。接下来是检测波峰或波谷计算峰峰间隔IBI。这里最大的挑战是信号基线可能因为呼吸、身体移动而缓慢漂移。固定阈值检测会失效。我推荐使用动态阈值算法。它的核心思想是阈值不是固定的而是随着信号本身动态调整的。// 接上一段代码 // 全局变量定义 int filteredValue; int threshold; int peak 0; unsigned long lastPeakTime 0; bool belowThreshold true; // 在loop的采样部分之后添加峰值检测逻辑 void loop() { // ... 采样和滑动平均滤波代码 ... // 2. 动态阈值计算与峰值检测 // 阈值由信号的最大值和最小值动态决定 static int signalMax 512; // 初始估计值根据ADC范围调整如1023的一半 static int signalMin 512; // 缓慢追踪信号的最大最小值类似一个低通滤波器 if (filteredValue signalMax) { signalMax filteredValue; } else if (filteredValue signalMin) { signalMin filteredValue; } // 引入衰减让最大值最小值能跟随信号下降/上升 signalMax - 1; // 每采样点衰减1个单位 signalMin 1; // 动态阈值设为最大值和最小值的中间值 threshold (signalMax signalMin) / 2; // 峰值检测逻辑 if (filteredValue threshold belowThreshold) { // 信号从阈值下方穿越到上方可能是一个上升沿 belowThreshold false; if (filteredValue peak) { peak filteredValue; // 记录上升过程中的最大值 } } else if (filteredValue threshold !belowThreshold) { // 信号从阈值上方回落到下方一个完整的脉冲结束 belowThreshold true; // 此时peak变量中存储的就是这个脉搏波的峰值 unsigned long currentPeakTime millis(); if (lastPeakTime 0) { int interBeatInterval currentPeakTime - lastPeakTime; // 峰峰间隔单位ms int heartRate 60000 / interBeatInterval; // 计算心率次/分钟 // 对心率值进行合理性校验例如成年人静息心率通常在40-180bpm之间 if (heartRate 40 heartRate 180) { Serial.print(Heart Rate: ); Serial.print(heartRate); Serial.println( BPM); // 这里可以将心率值用于显示、上传或其他逻辑 } } lastPeakTime currentPeakTime; peak 0; // 重置峰值记录 } }这个算法能很好地适应基线的缓慢变化准确捕捉每一个脉搏波。signalMax和signalMin的衰减常数这里用的是每点±1可以根据信号稳定性调整衰减越快阈值对信号变化越敏感但也可能受突发噪声影响。4.3 中值滤波与心率平滑输出直接计算出的每个IBI峰峰间隔可能仍有跳动。为了得到一个稳定的心率读数我们需要对连续多个心率值进行平滑处理。中值滤波在这里比平均值更有效因为它能剔除偶然的异常值比如因一次误检测产生的极快或极慢心率。// 定义一个心率值缓存数组用于中值滤波 #define HR_BUFFER_SIZE 5 int hrBuffer[HR_BUFFER_SIZE]; int hrBufferIndex 0; // 在计算出心率heartRate后进行中值滤波 if (heartRate 40 heartRate 180) { // 存入缓冲区 hrBuffer[hrBufferIndex] heartRate; hrBufferIndex (hrBufferIndex 1) % HR_BUFFER_SIZE; // 计算中值 int sortedBuffer[HR_BUFFER_SIZE]; for (int i 0; i HR_BUFFER_SIZE; i) sortedBuffer[i] hrBuffer[i]; // 简单冒泡排序对于小数组足够 for (int i 0; i HR_BUFFER_SIZE - 1; i) { for (int j i 1; j HR_BUFFER_SIZE; j) { if (sortedBuffer[i] sortedBuffer[j]) { int temp sortedBuffer[i]; sortedBuffer[i] sortedBuffer[j]; sortedBuffer[j] temp; } } } int smoothedHR sortedBuffer[HR_BUFFER_SIZE / 2]; // 取中值 Serial.print(Smoothed HR: ); Serial.print(smoothedHR); Serial.println( BPM); }经过这套“滑动平均 - 动态阈值检测 - 中值滤波”的组合拳你就能从Grove传感器那原始的、嘈杂的信号中稳定地提取出可信的心率值。5. 进阶应用与系统集成当你能稳定获取心率数据后就可以把它集成到更大的项目中去了。这里分享几个方向。5.1 构建本地心率监测显示系统最简单的应用就是做一个实时心率显示器。你可以使用一个OLED屏幕I2C接口来显示实时心率和简单的脉搏波形。#include Wire.h #include Adafruit_GFX.h #include Adafruit_SSD1306.h // ... 心率检测相关变量和函数 ... Adafruit_SSD1306 display(128, 64, Wire, -1); void setup() { // 初始化OLED if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { Serial.println(F(SSD1306 allocation failed)); for(;;); } display.clearDisplay(); display.setTextSize(1); display.setTextColor(SSD1306_WHITE); // ... 其他初始化 ... } void loop() { // ... 心率检测主循环 ... // 在得到平滑心率smoothedHR后更新显示 display.clearDisplay(); display.setCursor(0,0); display.print(Heart Rate:); display.setCursor(0, 20); display.setTextSize(2); display.print(smoothedHR); display.print( BPM); display.display(); }结合一个锂电池和充电模块你就可以把它做成一个可穿戴的便携设备。5.2 通过串口与上位机通信将Arduino/ESP32作为数据采集端通过串口UART将实时心率数据发送给树莓派或电脑进行更复杂的处理、存储或可视化。这是很多“Arduino智能小车”或机器人项目中常用的主从机通信模式。Arduino端发送void loop() { // ... 计算得到smoothedHR ... Serial.print(HR:); // 定义一个简单协议头 Serial.println(smoothedHR); delay(100); // 控制发送频率例如10Hz }树莓派/Python端接收与处理你可以使用Python的pyserial库读取串口数据然后用matplotlib实时绘图或者存入数据库。import serial import time ser serial.Serial(/dev/ttyUSB0, 115200, timeout1) # 端口名根据实际情况修改 while True: if ser.in_waiting 0: line ser.readline().decode(utf-8).rstrip() if line.startswith(HR:): try: hr_value int(line.split(:)[1]) print(fCurrent Heart Rate: {hr_value} BPM) # 这里可以添加绘图、存储等逻辑 except ValueError: pass time.sleep(0.01)在电脑上你也可以用串口调试助手如SSCOM直接接收数据或者用Processing编写一个更酷炫的可视化界面。5.3 心率变异性HRV的初步探索如果你记录下每一个心跳的准确时间即每个R-R间期在我们的代码里是interBeatInterval你就可以进行更高级的分析比如计算心率变异性HRV。HRV反映了自主神经系统的平衡状态与压力、疲劳、情绪等相关。一个简单的HRV指标是SDNN相邻R-R间期标准差。你需要连续采集几分钟稳定的R-R间期数据然后计算其标准差。这需要在代码中开辟一个数组来存储一段时间内的IBI值然后进行统计分析。这已经进入了生物信号分析的更深领域但对ESP32或树莓派来说计算量完全在可承受范围内。6. 实战避坑指南与稳定性优化纸上得来终觉浅绝知此事要躬行。下面是我在多个项目中总结出的最容易导致心率数据不稳或失败的坑点及解决方案。坑点1数据全是噪声看不到波形检查首先确认硬件连接正确特别是GND共地。然后用万用表测量传感器信号引脚对地电压在佩戴时轻轻按压耳夹看电压是否有规律变化通常在几十到几百毫伏范围。如果没有变化可能是传感器损坏或供电问题。解决确保使用3.3V稳定供电并加上前面提到的RC硬件滤波和电源去耦电容。将采样代码中的analogRead值通过Serial.println输出在Arduino IDE的串口绘图器Serial Plotter中观察这是最直观的调试工具。坑点2波形有规律但心率计算值乱跳检查这通常是峰值检测算法参数不合适。观察串口绘图器中的波形和你的动态阈值线可以把阈值也打印出来画上去。阈值线是否在波形的中间位置是否随着基线漂移而漂移解决调整signalMax和signalMin的衰减速度。如果信号漂移快就增大衰减值如每次±2或±3。如果信号稳定但噪声导致阈值抖动就减小衰减值如每10个采样点衰减±1。同时确保采样间隔稳定避免因为loop中其他耗时操作导致采样时间间隔不均可以使用micros()函数进行更精确的定时采样。坑点3静止时准确一动就失灵检查这是PPG传感器的固有限制运动伪影。解决物理固定确保耳夹佩戴紧密且舒适减少相对运动。可以考虑使用头戴式或耳挂式结构来固定传感器。算法增强实现更复杂的算法如带通滤波器只保留0.5-5Hz的心率信号频段。可以在代码中实现数字带通滤波如IIR滤波器但这会消耗更多计算资源。对于Arduino UNO一个简单的补救方法是结合滑动平均低通和减去直流分量高通来近似。数据融合如果项目允许可以加入一个加速度计如MPU6050。通过加速度数据识别运动状态在剧烈运动时提示数据不可靠或尝试用算法减去运动引起的信号干扰这非常复杂。坑点4与树莓派通信数据断断续续或错误检查串口通信的波特率、数据位、停止位、校验位是否两端一致。树莓派上串口设备名是否正确/dev/ttyUSB0或/dev/ttyAMA0等。解决在Arduino和Python代码中统一使用相同的波特率如115200。在通信协议中加入校验和或帧头帧尾提高数据可靠性。例如发送$HR,75,*23\n这样的格式NMEA协议风格。在Python端增加异常处理和超时重试机制。如果使用USB转串口线如CH340、PL2303、FTDI芯片确保电脑或树莓派上已安装正确的驱动。坑点5长时间运行后数据漂移或死机检查代码中是否存在变量溢出、内存泄漏或看门狗复位问题。解决对于涉及时间间隔的变量如lastPeakTime使用unsigned long类型并注意millis()的回绕问题大约50天回绕一次。比较时间差时使用(currentMillis - lastPeakTime) interval这样的减法形式它是回绕安全的。减少全局变量和大型数组的使用优化内存。对于ESP32如果使用了Wi-Fi或蓝牙注意给看门狗喂食或在长时间计算任务中调用yield()。从一个小小的Grove模块出发我们实际上探讨了嵌入式生物信号采集的完整链路传感器原理、硬件调理、信号处理算法、系统集成和问题调试。这个过程远比调用一个现成的库函数要复杂但也更有价值。当你看到屏幕上稳定跳动的心率曲线与你自己的脉搏同步时那种感觉是完全不同的。这不仅仅是完成了一个项目更是真正理解了数据如何从物理世界被捕获、处理最终变成有意义的数字。基于这个稳定的心率数据源无论是做物联网健康设备、交互式艺术装置还是学术研究你都有了扎实的起点。