一块Arduino,一颗心跳:手把手教你打造脉搏监测系统

你有没有想过,只用一块几十元的开发板和一个指尖传感器,就能实时捕捉自己的心跳?这不是实验室里的高端设备,也不是医院的心电图机——而是你可以亲手实现的嵌入式项目。

在智能手环早已普及的今天,心率监测似乎成了“理所当然”的功能。但当你真正从零开始连接线路、读取信号、解析波形时,那种看到自己心跳在屏幕上跳动的瞬间,依然会让人心跳加速。

本文将带你 从零搭建一个基于 Arduino Uno R3 的脉搏监测系统 ,不靠现成库,不跳步骤,全程剖析每一个技术细节。无论你是电子小白,还是想深入理解生物信号处理的工程师,都能从中获得实战价值。


为什么选它?Arduino + 脉搏传感器的黄金组合

市面上能做心率检测的平台不少,STM32、树莓派、ESP32……但如果你是第一次接触硬件开发, Arduino Uno R3 依然是最友好的起点 。

它的优势不在性能多强,而在于“够用+易上手”:
- 模拟输入引脚(A0-A5)原生支持10位ADC,正好用来采样脉搏传感器输出的微弱电压变化;
- 编程语言基于C/C++简化版,没有复杂的寄存器配置;
- 开发环境(IDE)跨平台、免驱动、一键上传;
- 社区资源丰富,遇到问题基本都能搜到答案。

搭配常见的 Pulse Sensor 或 KY-039 这类光电式脉搏模块 ,整个系统的成本控制在百元以内,却能完成一次完整的生命体征采集流程。

更重要的是: 你能看清每一步发生了什么 。不像某些“黑盒”模块直接返回BPM数值,这里你要自己处理原始信号、识别峰值、计算间隔——这才是学习的本质。


脉搏是怎么被“看见”的?PPG原理一讲就懂

我们用的不是心电图(ECG),而是另一种更轻便的技术: 光电容积描记法(Photoplethysmography, PPG) 。

简单说,就是“用光看血”。

传感器由两部分组成:
- 一个绿色或红外LED,向皮肤发射光线;
- 一个光敏元件(如光敏三极管),接收反射回来的光。

当心脏收缩时,指尖动脉充血增多,吸收更多光,反射回的光变少;舒张时则相反。于是,随着心跳节律,接收到的光强也呈现周期性波动。

这个微小的变化会被内部电路放大,转换为0~5V之间的模拟电压信号输出。典型波形长这样:

    ▲
    │     ●           ●           ●
    │   ↗   ↘       ↗   ↘       ↗   ↘
    │  /     \     /     \     /     \
    │ /       \   /       \   /       \
    └─────────●───────────●───────────●──► t
              ↑           ↑           ↑
           心跳峰值    心跳峰值    心跳峰值

每个上升沿后的峰值对应一次心跳。只要我们能准确抓到这些峰,再算出它们之间的时间差(IBI, Inter-Beat Interval),就能得出心率:

BPM = 60000 / IBI(单位:毫秒)

比如两次心跳相隔800ms,则当前心率为 60000 ÷ 800 = 75 BPM 。

听起来很简单,但现实远没这么理想。


真实世界的问题:噪声、漂移、误判……

如果你把传感器往手指上一夹,马上就能得到稳定的心率?别天真了。实际信号往往是这样的:

  • 环境光干扰 :日光灯闪烁、窗外阳光都会让基线来回漂移;
  • 运动伪影 :手稍微抖一下,就会产生比真实脉搏还大的波动;
  • 接触压力不均 :太松读不到信号,太紧压迫血管反而测不准;
  • 呼吸影响 :深呼吸会引起慢速波动,容易被误认为心跳;
  • 初始不稳定 :前几秒数据混乱,算法还没“热身”。

所以,不能指望 raw data 直接拿来用。我们需要一套 鲁棒的软件处理逻辑 来过滤噪声、锁定有效心跳。

幸运的是,这些问题都有成熟的应对策略,而且完全可以在 Arduino 这种资源有限的平台上实现。


硬件连接:三根线搞定一切

这类脉搏传感器通常只有三个引脚:

引脚 功能 接 Arduino
VCC 电源 5V
GND 地线 GND
SIG 信号输出 A0

就这么简单。不需要额外供电、无需外部滤波电路(模块自带简单放大),插上去就能开始读数。

⚠️ 小贴士:建议使用带遮光罩的传感器(如Pulse Sensor官方版),避免环境光直射。也可以自己用黑色热缩管包裹一下。

此外,我们将使用板载LED(引脚13)作为心跳指示灯,每次检测到心跳就闪一下,让你“看得见”自己的心跳。


核心代码详解:不只是复制粘贴

下面这段代码,是我们整个系统的“大脑”。我会逐段拆解它的设计思路,告诉你每一行为什么这么写。

const int pulsePin = A0;        // 脉搏传感器接A0
const int blinkPin = 13;        // 板载LED用于提示

int threshold = 550;            // 初始阈值
int signal = 0;                 // 当前读取的信号值
int peak = 0;                   // 记录当前脉冲中的最高点
int lastPeak = 0;               // 上一个循环的信号值(用于边沿判断)
long lastBeatTime = 0;          // 上次有效心跳时间(ms)
long currentBeatTime = 0;
int ibi = 0;                    // 本次心跳间隔
int bpm = 0;

变量命名要有意义

别小看变量名。像 signal , peak , lastBeatTime 都清楚表达了用途。这比 val1 , temp , t 强太多,尤其在调试时能省下大量脑力。

threshold = 550 是经验值。因为Arduino ADC是10位(0~1023),对应0~5V。正常脉搏信号峰值大约在600~800之间,设550作为触发起点比较安全。


void setup() {
  pinMode(blinkPin, OUTPUT);
  Serial.begin(9600);
  Serial.println("Pulse Sensor Ready! Send 'S' to start.");
}

串口波特率设为9600是标准做法。打印一句提示语,方便确认程序已启动。

虽然现在没加命令交互功能,但这句留着是个好习惯——将来扩展时可以加入“开始/停止”控制。


void loop() {
  signal = analogRead(pulsePin);

  Serial.print("Signal,");
  Serial.println(signal);

每轮循环先读一次A0引脚的值,并通过串口输出格式为 "Signal,xxx" 的数据。

为什么要加前缀?因为我们要用 Arduino IDE 自带的 Serial Plotter(串口绘图器) 实时查看波形!

打开方式: 工具 → 串口绘图器 ,你会看到一条动态跳动的曲线,就像简易示波器一样。这是调试信号质量的利器。


峰值检测:如何判断“这是心跳”?

这才是最关键的逻辑。

  if (signal > threshold && signal > peak) {
    peak = signal;  // 更新当前脉冲的最大值
  }

这一段的意思是:如果当前信号超过了阈值,并且还在不断升高,那就更新“当前脉冲的最高点”。

注意,我们不是一超过阈值就认定是心跳,而是持续追踪上升过程中的最大值,确保抓到真正的峰值。

接着看下降阶段:

  if (signal < lastPeak && signal < threshold && lastPeak > threshold) {
    currentBeatTime = millis();
    ibi = currentBeatTime - lastBeatTime;
    bpm = 60000 / ibi;

这个条件有点复杂,拆开来看:
- signal < lastPeak :信号已经开始下降(过了顶点);
- signal < threshold :已经回落到阈值以下;
- lastPeak > threshold :之前确实达到了足够高的幅度;

三个条件同时满足,说明我们完整经历了一次“上升→峰值→下降”的脉冲过程,可以判定为一次有效心跳。

然后记录时间差 ibi ,并计算 BPM。


加一道保险:合理范围过滤

人的心率不可能是10 BPM,也不会突然飙到300 BPM(除非濒死)。所以我们加上一道防护:

    if (bpm >= 30 && bpm <= 200) {
      Serial.print("BPM,");
      Serial.println(bpm);

      digitalWrite(blinkPin, HIGH);
      delay(50);
      digitalWrite(blinkPin, LOW);

      lastBeatTime = currentBeatTime;
    }

只有在30~200 BPM之间的结果才被认为是有效的,避免因噪声导致的极端错误值污染数据。

同时点亮LED 50ms,给人一个视觉反馈:“我检测到你的心跳了!”

最后别忘了更新 lastBeatTime ,为下次计算做准备。


  peak = 0;  // 重置峰值,等待下一个脉冲
}

lastPeak = signal;

delay(20);  // 控制采样频率约50Hz

清空 peak 是为了防止旧数据干扰下一轮检测。

lastPeak = signal 放在循环末尾,用于下一回合判断是否出现下降沿。

delay(20) 控制采样周期约为20ms,即每秒采集50次(50Hz)。这对脉搏信号来说足够了(人类心率上限约5Hz),又不会给MCU带来太大负担。

💡 更佳实践:用 millis() 实现非阻塞延时,避免 delay() 影响其他任务响应。但对于单任务系统, delay(20) 简洁可靠。


调试技巧:那些手册不会告诉你的事

1. 如何调阈值?

  • 太高:错过弱信号,尤其对血液循环差的人;
  • 太低:容易误触发,把噪声当心跳。

建议首次运行时关闭BPM输出,只看 "Signal" 波形,观察峰值大致落在哪个区间,再设定略低于平均峰值的阈值。

也可以改为 动态阈值 ,例如取过去10秒信号均值 + 固定偏移量。

2. 启动前几秒不准怎么办?

刚戴上时信号不稳定很正常。可以加入预热机制:

if (millis() < 5000) return;  // 前5秒不处理

或者等连续检测到3个有效心跳后再开启输出。

3. 想要更高精度?试试移动平均滤波

原始信号可能毛刺较多,可以在读取后先平滑一下:

signal = (signal * 3 + analogRead(pulsePin)) / 4;

这是一个简单的IIR低通滤波,能有效抑制高频噪声。


进阶玩法:不止于“看看心跳”

你现在拥有的,不仅仅是一个心率计原型,而是一个可扩展的嵌入式健康终端底座。接下来还能做什么?

✅ 加OLED屏 → 独立设备

用SSD1306屏幕显示实时波形和BPM,做成指夹式心率仪。

✅ 接蓝牙模块(HC-05/HC-06)→ 手机App联动

把数据无线传给手机,记录长期趋势,甚至分析心率变异性(HRV)来评估压力水平。

✅ 结合RTC芯片 → 24小时监测

配合DS3231记录时间戳,实现夜间睡眠心率追踪。

✅ 多传感器融合 → 智能穿戴雏形

叠加温度、血氧(MAX30102)、加速度计(MPU6050),构建多功能健康手环。

甚至可以用 TensorFlow Lite for Microcontrollers 在边缘端跑轻量级异常检测模型,实现“心跳异常预警”。


写在最后:从一块板子出发,走向更大的世界

这个项目看似简单,但它涵盖了嵌入式开发的核心闭环:

感知(Sensor) → 采集(ADC) → 处理(Filter & Algorithm) → 输出(Serial/LED)

你学会了如何与模拟信号打交道,理解了时序控制的重要性,掌握了基本的数字信号处理思想。这些能力,正是通往更复杂系统的钥匙。

也许几年后,你会忘记具体的阈值是多少、延迟该设多久,但你会记得那个第一次在串口绘图器里看到自己心跳波形的时刻——那是一种奇妙的连接感,仿佛科技真的触碰到了生命本身。

而这一切,始于一块Arduino Uno R3,和一颗愿意动手探索的心。

如果你成功实现了这个项目,欢迎在评论区分享你的波形截图或改进方案。我们一起,让每一次心跳都被听见。

更多推荐