基于Arduino与PPG原理的心率监测系统:从硬件搭建到信号处理全解析
1. 项目概述从智能手表到桌面上的心率实验室每次看到手腕上的智能手表亮起显示着实时心率我总会好奇这小小的设备背后究竟是如何工作的。心率监测这个听起来很“医疗”的功能其实离我们这些电子爱好者并不遥远。它的核心原理——光电容积脉搏波描记法PPG本质上是一种利用光学手段“看见”血液流动的巧妙技术。当血液随着心跳泵过血管时血管的容积会发生微小的周期性变化而特定波长的光线通常是绿光穿透皮肤后被血液吸收的程度也会随之改变。传感器捕捉到这个微弱的光强变化信号经过一系列处理就能反推出心跳的节奏。这个项目就是一次将商业产品“黑盒”打开用我们熟悉的Arduino平台和常见模块亲手搭建一个功能完备的心率监测与记录系统的实践。它不仅仅是一个简单的数据读取器更是一个集成了实时显示、本地数据存储的完整数据采集终端。对于嵌入式系统入门者而言这是一个绝佳的综合性项目涵盖了传感器应用、模拟信号处理、实时算法、人机交互OLED显示和数据持久化SD卡存储等多个核心知识点。对于有经验的开发者它则提供了一个深入理解PPG原理、优化滤波算法和设计低功耗可穿戴设备的实践平台。我们将使用Arduino Pro Mini作为大脑Grove心率传感器作为“眼睛”再配上OLED显示屏和Micro SD卡模块最终将它们整合进一个3D打印的外壳里制作出一个既可用于学习研究也能实际使用的桌面心率监测记录仪。2. 核心硬件选型与电路设计解析2.1 主控与传感器为什么是它们项目的核心硬件选型直接决定了系统的性能、功耗和扩展性。我选择了Arduino Pro Mini作为主控制器首要原因是其极佳的性价比与适中的性能。相较于UNOPro Mini去掉了USB转串口芯片和冗余接口体积更小巧价格更低非常适合嵌入到最终产品中。其采用的ATmega328P芯片运行在16MHz主频下处理心率传感器每秒数百次的采样数据并运行简单滤波算法绰绰有余。更重要的是它支持多种休眠模式为未来实现电池供电下的长续航优化留下了空间。传感器方面Grove心率传感器模块是一个明智的“捷径”。它内部集成了专业的光电传感器和前端模拟信号调理电路将复杂的模拟信号初步放大和滤波以模拟电压的形式输出脉搏波形。这省去了我们自己设计光电接收电路、计算偏置电压、设计放大倍数的麻烦极大地降低了入门门槛和调试难度。该模块通常使用绿光LED作为光源因为绿光对于皮下血液中氧合血红蛋白的吸收率变化更敏感且在环境光干扰下比红光表现更稳定。模块输出的信号是一个叠加在直流分量上的交流小信号其交流部分的频率即对应着心率。2.2 电源管理与外围模块设计一个稳定的系统离不开可靠的电源。本项目涉及多个需要3.3V供电的模块如OLED屏、SD卡模块、部分Grove传感器而常用的锂聚合物电池LiPo输出电压通常是3.7V满电约4.2V。直接供电电压不稳可能损坏器件因此一个低压差线性稳压器LDO必不可少。我选用MCP1700-3.3V看中的是其极低的静态电流典型值1.6µA和较低的压差。这意味着在电池电压降到3.6V左右时它仍能稳定输出3.3V最大限度地利用了电池电量延长了使用时间。外围模块中0.96英寸的OLED显示屏用于实时显示心率数值和波形趋势其I2C接口仅需两根信号线节省了宝贵的IO口。Micro SD卡模块则通过SPI接口与Arduino通信负责将带时间戳的心率数据以CSV格式保存下来便于后续在电脑上用Excel或Python进行深入分析。一个简单的滑动开关用于控制整个系统的电源通断这是产品化设计中必不可少的一环。注意在连接电路时务必确认所有模块的逻辑电平。Arduino Pro Mini有5V和3.3V两种版本本项目为统一电源和兼容性应选择3.3V/8MHz版本。若使用5V版本则需注意OLED和SD卡模块是否兼容5V逻辑否则需要电平转换电路徒增复杂度。2.3 电路连接原理图详解整个系统的电路连接遵循清晰的分区原则电源部分、传感器输入部分、人机交互与存储部分。下图是核心连接的思维导图电源通路LiPo电池正极接滑动开关一端开关另一端接MCP1700的输入端Vin。MCP1700的输出端Vout产生系统主电压3.3V并连接到Arduino Pro Mini的VCC引脚、OLED的VCC、SD卡模块的VCC以及心率传感器的VCC如果它也是3.3V工作。地线将所有模块的GND引脚与电池负极、MCP1700的GND、Arduino的GND连接到一起形成统一的参考地这是避免信号噪声的基础。信号连接心率传感器其模拟输出引脚通常是SIG或AO连接到Arduino Pro Mini的任何一个模拟输入引脚例如A0。OLED显示屏SDA引脚接Arduino的A4或对应的SDA数字引脚SCL接A5或对应的SCL数字引脚。这里需要澄清一个常见疑惑在Arduino Pro Mini上I2C接口确实位于引脚A4SDA和A5SCL尽管它们标为模拟输入但在Wire库中已被定义为I2C功能引脚。SD卡模块采用SPI接口。通常连接方式为CS片选接D10MOSI接D11MISO接D12SCK时钟接D13。这是Arduino上最常用的SPI引脚映射。所有连接建议使用杜邦线在面包板上先行测试确认功能无误后再考虑焊接或使用定制PCB。3. 心率监测原理与信号处理算法3.1 深入理解PPG信号不只是“一道光”光电容积脉搏波描记法PPG输出的原始信号远比我们想象的复杂。当传感器紧贴皮肤时LED发出的光穿透组织其中一部分被骨骼、肌肉、静脉血和动脉血吸收另一部分被散射只有一小部分由动脉血反射回来被光电探测器接收。动脉血容积随心脏收缩舒张而周期性变化对光的吸收量也随之变化从而产生了我们需要的脉搏波信号。然而这个信号非常微弱通常在毫伏级别并且混杂着多种噪声运动伪影这是最大的干扰源。手指或手腕的微小移动会改变传感器与皮肤之间的耦合压力及空间位置导致信号基线剧烈漂移幅度可能远大于脉搏波本身。呼吸波呼吸会引起胸腔压力变化进而影响外周血液循环在PPG信号上叠加一个频率较低约0.1-0.3 Hz的波形。环境光干扰尽管传感器有结构设计尽量屏蔽环境光但强光变化仍可能被引入。电源工频干扰50/60 Hz的市电及其谐波可能通过空间耦合或电源线传入。因此从传感器A0引脚读取到的原始ADC值例如0-1023范围是一个包含直流分量、低频漂移、高频噪声和我们所需脉搏波的综合体。直接对这个原始值求波峰或过零点来计算心率结果会极不稳定且不可靠。3.2 从原始数据到心率值滤波算法的实战在嵌入式资源受限的环境下我们需要一套高效、实时的数字滤波算法来提取纯净的脉搏波。以下是一个经典且实用的处理流程完全可以在Arduino上实现第一步去除直流分量与基线漂移原始信号rawSignal可以看作DC Baseline_Wander PPG Noise。我们首先关心的是交流的PPG部分。一个简单有效的方法是计算滑动均值作为动态基线然后减去它。// 假设samples为原始ADC值数组 long sum 0; for(int i0; iWINDOW_SIZE; i){ sum samples[i]; } int movingAverage sum / WINDOW_SIZE; // 动态基线 int acSignal rawSignal - movingAverage; // 得到大致去基线后的交流信号WINDOW_SIZE的选择很关键通常取接近或略大于一个心跳周期采样点数的整数值例如假设心率60-120BPM采样率100Hz则周期对应50-100个点窗口可取100。第二步带通滤波提取脉搏波频段人类心率范围通常在0.7 Hz (42 BPM) 到 3.3 Hz (200 BPM) 之间。我们需要一个通带覆盖此范围的滤波器。在MCU上IIR无限脉冲响应滤波器比FIR有限脉冲响应更节省计算资源。一个二阶巴特沃斯带通滤波器是不错的选择。其差分方程形式为y[n] b0*x[n] b1*x[n-1] b2*x[n-2] - a1*y[n-1] - a2*y[n-2]其中x是输入acSignaly是输出b0, b1, b2, a1, a2是由通带频率和采样率计算出的系数。我们可以使用在线滤波器设计工具如t-filter.engineerjs.com生成针对特定采样率如100Hz和通带如0.7Hz-3.3Hz的系数。在代码中实现这个差分方程即可。第三步峰值检测与心率计算经过滤波后的信号波形清晰波峰突出。心率计算的核心是检测连续波峰之间的时间间隔峰峰间隔PPI。算法步骤如下设置一个动态阈值。可以取最近一段时间内信号最大值的某个比例如70%。遍历滤波后的数据流。当信号值超过阈值且在后续若干点内是局部最大值时判定为一个有效波峰。记录当前波峰的时刻currentPeakTime。计算与前一个波峰的时间差interval currentPeakTime - previousPeakTime。心率BPM, 次/分钟 60000 / interval。这里的60000是将毫秒转换为分钟的系数1分钟60000毫秒。为了显示稳定通常不会每次检测到峰值就更新显示而是计算最近几个如4-8个间隔的平均值或中值再换算成心率。实操心得峰值检测的鲁棒性至关重要。在实际代码中我通常会加入“不应期”判断即在检测到一个峰值后忽略接下来一段时间如对应心率200BPM的300毫秒内的所有信号防止对同一个脉搏波产生多次检测。此外对于因运动导致信号暂时严重失真丢失峰值的情况算法应能判断并给出“信号丢失”提示而不是输出一个荒谬的值。4. 软件实现代码结构与核心功能剖析4.1 程序整体架构与库依赖整个Arduino代码采用模块化设计逻辑清晰便于调试和扩展。主要依赖以下库Wire.h用于驱动I2C接口的OLED显示屏。Adafruit_SSD1306.h和Adafruit_GFX.h这是驱动绝大多数OLED屏的标准库提供了丰富的图形绘制函数。SPI.h用于SD卡模块的通信。SD.hArduino官方SD卡库用于文件操作。程序主体运行在setup()和loop()框架内。在setup()中我们依次完成串口初始化用于调试、OLED屏初始化、SD卡初始化以及心率传感器引脚配置。loop()函数则是一个无限循环其核心是执行“采样-处理-显示-存储”的流水线。为了保证采样率恒定避免loop循环时间不确定带来的问题我会使用millis()函数进行定时采样这是嵌入式系统实现准实时任务的关键技巧。4.2 数据采集与实时滤波的实现首先我们需要设定一个固定的采样率比如100Hz即每10毫秒采样一次。这可以通过比较millis()的差值来实现。#define SAMPLE_INTERVAL_MS 10 // 10ms对应100Hz采样率 unsigned long lastSampleTime 0; void loop() { unsigned long currentTime millis(); // 定时采样任务 if (currentTime - lastSampleTime SAMPLE_INTERVAL_MS) { lastSampleTime currentTime; int rawValue analogRead(HEART_RATE_PIN); // 1. 采集 int filteredValue bandPassFilter(rawValue); // 2. 滤波调用自定义滤波函数 bool isPeak peakDetection(filteredValue, currentTime); // 3. 峰值检测 if(isPeak){ calculateAndUpdateBPM(currentTime); // 4. 计算心率 } updateDisplay(currentBPM, filteredValue); // 5. 更新显示可包含波形绘制 if (currentTime - lastLogTime LOG_INTERVAL_MS) { // 例如每1秒记录一次 logDataToSD(currentTime, currentBPM); lastLogTime currentTime; } } // 其他非实时任务... }其中bandPassFilter()函数实现了前述的IIR带通滤波差分方程。peakDetection()函数实现了带动态阈值和不应期的峰值检测逻辑。calculateAndUpdateBPM()函数维护一个峰值时间队列并计算平均心率。4.3 OLED显示与SD卡数据记录显示部分的目标是直观。我们可以在OLED屏上划分区域顶部大字体显示实时心率值如“72 BPM”中间区域绘制一个实时滚动的脉搏波形图底部可以显示一些状态信息如电池电量图标或信号质量指示条。绘制波形时可以将滤波后的信号值映射到屏幕的Y坐标并让波形从右向左滚动形成动态效果。SD卡数据记录功能旨在为后续分析提供原始数据。在setup()中我们需要检测SD卡是否存在并初始化文件系统。每次记录时以追加模式打开一个文件例如HR_Log.csv写入格式化的数据行。void logDataToSD(unsigned long timestamp, int bpm) { File dataFile SD.open(HR_LOG.CSV, FILE_WRITE); if (dataFile) { dataFile.print(timestamp); // 时间戳毫秒 dataFile.print(,); dataFile.print(bpm); // 心率值 dataFile.print(,); dataFile.println(filteredValue); // 可选同时记录一个最近的滤波后信号值用于调试 dataFile.close(); } else { // 可以在屏幕上显示“SD Error” } }CSV格式的好处是能被Excel、Numbers或任何数据分析软件直接打开。时间戳记录了每次测量的绝对时间便于分析心率随时间的变化趋势。5. 系统集成、调试与优化心得5.1 3D打印外壳设计与装配要点一个定制的外壳不仅能保护电路更能提升项目的完整度和使用体验。使用Fusion 360或Tinkercad等软件进行设计时需要考虑以下几点精确测量使用游标卡尺精确测量所有模块Arduino、电池、屏幕、传感器的尺寸并在模型中留出适当的装配间隙通常单边0.2-0.5mm。传感器开窗为心率传感器设计一个专门的、带限位结构的腔体确保其探测面能紧密、垂直地贴合皮肤。窗口位置要避开任何内部结构的遮挡。散热与透气虽然本项目功耗不大但LDO和MCU在工作时仍会发热。外壳应设计一些通风孔特别是靠近这些芯片的位置。人机交互屏幕窗口要透明或开孔开关和充电接口如果电池不可拆卸的位置要便于操作。打印设置选择PLA材料即可层高0.2mm能平衡打印速度与表面质量。对于需要紧密配合的卡扣或轴承结构可能需要多次测试调整补偿值。装配时建议使用尼龙柱和螺丝固定主板使用双面胶或卡槽固定电池和较小的模块。所有飞线应使用扎带或理线槽固定避免在壳内晃动导致短路。5.2 系统调试与信号质量优化组装完成后上电测试可能不会一帆风顺。以下是系统的调试流程和常见问题排查电源与基础通信测试首先不接传感器只连接OLED屏。上传一个简单的显示测试程序确认屏幕能点亮并显示内容。然后单独测试SD卡模块尝试创建和写入一个文件。这能排除电源问题和基础库驱动问题。传感器信号测试将心率传感器通过杜邦线连接到Arduino上传一个仅通过串口打印原始ADC值的程序。打开串口绘图器Serial Plotter这是一个极其强大的工具。将手指稳定地放在传感器上你应该能看到一个规律起伏的波形。如果信号是一条直线或噪声极大检查传感器连接、供电并确保手指接触良好且施加适当压力。算法参数调优在串口绘图器中同时打印原始信号和滤波后的信号。调整滤波器的系数或峰值检测的阈值、不应期参数观察滤波效果是否干净峰值检测是否准确。可以在代码中设置不同的调试输出级别。整体联调将所有功能集成后长时间运行测试。观察心率显示是否稳定SD卡记录是否完整系统有无死机或重启现象。避坑指南运动伪影是DIY心率监测的最大敌人。在算法层面优化之余硬件上也可以改进一是确保传感器与皮肤接触稳定可以考虑使用弹性腕带二是在传感器背面增加一些柔软的海绵或硅胶垫使其能自适应不同手腕的弧度保持压力均匀。5.3 功耗优化与扩展思路目前的原型机可能功耗较高。若想实现真正的可穿戴续航需进行深度优化降低工作频率将Arduino Pro Mini的内部时钟从16MHz降至8MHz可显著降低功耗。利用休眠模式在两次采样间隔中让MCU进入空闲Idle或掉电Power-down模式。这需要配置定时器中断来唤醒MCU复杂度较高但省电效果极佳。外围模块电源管理不使用OLED和SD卡时可以通过MOSFET开关电路彻底切断它们的电源而不是仅仅软件待机。降低采样率在静态状态下心率变化缓慢可将采样率从100Hz降至50Hz甚至25Hz。在功能扩展上这个项目有无限可能蓝牙传输增加HC-05或HM-10蓝牙模块将实时心率数据发送到手机APP实现无线监控。心率变异性分析HRV是评估压力、疲劳的重要指标。在SD卡记录PPI间隔序列后可在电脑上进行更复杂的时域、频域分析。加入血氧监测有些集成传感器模块如MAX30102能同时测量心率和血氧饱和度SpO2只需更换传感器并修改代码即可。开发上位机软件使用Python的PyQt或Tkinter编写一个桌面程序通过串口实时接收数据并绘制更精美的心率趋势图、频谱图等。这个DIY心率监测器项目就像打开了一扇门。它从最基本的原理出发串联了硬件、软件、算法、机械设计等多个环节。当你看到屏幕上跳动着与自己脉搏同步的数字当你能从SD卡的数据中分析出自己运动前后的心率变化时那种将理论知识转化为实际功能的成就感正是电子制作最大的乐趣所在。它不仅仅是一个工具更是一个理解我们身体与科技如何交互的窗口。