1. 项目概述
手指心跳检测模块是一种基于光电原理的简易生理信号采集装置,它通过红外光穿透手指时血液流动引起的光吸收变化来检测脉搏信号。这种模块在医疗级设备中虽然精度有限,但对于入门学习、教学演示和原型开发来说,是一个极具性价比的选择。
我在实际使用中发现,这类模块特别适合用来理解生物信号采集的基本原理。它的核心部件就是一个红外发射管和一个光敏接收管,当手指放在两者之间时,随着心跳引起的血液脉动,接收管检测到的光强会呈现周期性变化。这种变化经过适当放大和滤波后,就能提取出心率信号。
注意:市面上常见的心跳检测模块有两种工作模式 - 反射式和透射式。本文介绍的属于透射式,需要将手指放在发射和接收元件之间。反射式模块则只需将传感器贴在皮肤表面即可工作。
2. 硬件准备与连接
2.1 所需器材清单
- 核心控制器:STM32F103系列开发板(其他STM32型号也可,但需注意引脚兼容性)
- 传感器模块:手指心跳检测模块(通常标为Pulse Sensor或Heart Rate Sensor)
- 连接线材:杜邦线若干(建议使用母对母线,连接更稳固)
- 电源供应:5V稳定电源(可使用开发板的USB供电或外接电源)
- 辅助工具:万用表(用于调试时检查电压)、示波器(可选,用于观察原始信号)
2.2 模块引脚说明
大多数心跳检测模块都有以下三个基本引脚:
- VCC:供电引脚,接3.3V或5V(具体看模块规格)
- GND:接地引脚
- S/OUT:信号输出引脚,输出模拟电压信号
2.3 接线示意图
以STM32F103C8T6最小系统板为例:
code复制心跳检测模块 STM32开发板
VCC → 3.3V/5V
GND → GND
S → PA0 (ADC1_IN0) 或 PC1 (ADC2_IN11)
实操技巧:如果使用杜邦线连接,建议用热熔胶或胶带固定连接处,避免测量时因线材晃动引入噪声。
3. 工作原理深度解析
3.1 光电体积描记法(PPG)原理
这种心跳检测技术学名叫光电体积描记法(Photoplethysmography)。当红外光穿过手指组织时:
- 心脏收缩期:血液流量增加 → 组织吸光度增加 → 接收端光强减弱
- 心脏舒张期:血液流量减少 → 组织吸光度降低 → 接收端光强增强
这种周期性变化频率就是心率。典型的心跳信号波形包含以下特征:
- 主波(收缩峰):对应心脏收缩
- 重搏波(舒张峰):对应主动脉瓣关闭
- 基线:组织对光的恒定吸收部分
3.2 信号特点与挑战
原始信号具有以下特征:
- 幅值范围:通常只有几十毫伏的变化
- 频率范围:0.5Hz-5Hz(对应30-300BPM心率)
- 主要噪声源:
- 环境光干扰(50/60Hz工频及其谐波)
- 运动伪影(手指微小移动)
- 呼吸引起的低频波动(约0.2-0.3Hz)
3.3 硬件信号调理
优质的心跳模块会包含以下电路:
- 前置放大器:通常采用仪表放大器,增益约100-1000倍
- 带通滤波器:0.5Hz-5Hz,抑制直流偏移和高频噪声
- 环境光补偿:有些模块会加入环境光传感器进行主动补偿
4. 软件实现详解
4.1 ADC配置要点
无论是标准库还是HAL库,ADC配置都需要注意:
-
采样率选择:根据奈奎斯特定理,至少是信号最高频率的2倍。实际建议10-100Hz:
- 太高:浪费资源,增加噪声
- 太低:可能丢失信号细节
-
采样时间:对于高阻抗源(如这类传感器),需要足够长的采样时间:
c复制ADC_SampleTime_55Cycles5 // 标准库 hadc1.Init.SamplingTime = ADC_SAMPLETIME_71CYCLES_5; // HAL库 -
参考电压:确保稳定,必要时使用外部参考
4.2 标准库实现解析
原始代码中的关键点解析:
-
GPIO配置为模拟输入:
c复制PC.GPIO_Mode = GPIO_Mode_AIN; // 必须设为模拟输入 -
ADC独立模式配置:
c复制adc.ADC_Mode = ADC_Mode_Independent; // 单ADC模式 -
单次转换 vs 连续转换:
c复制adc.ADC_ContinuousConvMode = ENABLE; // 连续转换更适合实时监测 -
数据对齐:
c复制adc.ADC_DataAlign = ADC_DataAlign_Right; // 右对齐更常见
4.3 HAL库实现优化
原始HAL库代码可以优化以下几点:
-
增加错误检查:
c复制if(HAL_ADC_Start(&hadc1) != HAL_OK) { Error_Handler(); } -
使用DMA提高效率(对于需要高采样率的应用):
c复制
hadc1.Init.DMAContinuousRequests = ENABLE; -
添加硬件过采样(HAL库特有功能):
c复制
hadc1.Init.OversamplingMode = ENABLE; hadc1.Init.Oversampling.Ratio = ADC_OVERSAMPLING_RATIO_8;
4.4 信号处理算法
原始代码中的指数平滑滤波:
c复制value = 0.75 * oldValue + (1 - 0.75) * adc_light;
这实际上是一个一阶低通滤波器,其截止频率可以通过以下公式计算:
code复制fc = (1-α)/(2πT)
其中α=0.75,T=1s → fc≈0.04Hz
更专业的处理流程建议:
-
直流分量去除:
c复制#define WINDOW_SIZE 10 static float buffer[WINDOW_SIZE]; static int index = 0; buffer[index] = rawValue; float avg = moving_average(buffer, WINDOW_SIZE); // 实现移动平均函数 float acValue = rawValue - avg; // 去除直流分量 index = (index + 1) % WINDOW_SIZE; -
带通滤波(软件实现):
- 先低通(如fc=5Hz)
- 再高通(如fc=0.5Hz)
-
峰值检测算法:
c复制if(current > threshold && rising && current > last){ peakCount++; rising = false; } else if(current < threshold) { rising = true; }
5. 系统优化与调试技巧
5.1 硬件优化方案
- 电源去耦:在模块VCC和GND之间加100nF陶瓷电容
- 信号屏蔽:用铝箔包裹传感器线材减少干扰
- 机械固定:3D打印或制作专用指套保证接触稳定
5.2 软件调试方法
-
原始信号可视化:
c复制// 通过串口发送原始数据 printf("%d\n", rawValue); // 用SerialPlot或Python matplotlib绘制 -
阈值自适应算法:
c复制#define LEARNING_RATE 0.01 threshold = (1-LEARNING_RATE)*threshold + LEARNING_RATE*current; -
心率计算优化:
- 不要简单用60/interval,建议用多个周期平均
- 加入异常值过滤(如>200或<30的明显错误值)
5.3 常见问题排查
-
信号完全平坦:
- 检查电源是否接通
- 用万用表测量信号引脚电压
- 尝试用手指完全遮挡传感器
-
信号噪声过大:
- 尝试在暗室环境中测试
- 检查接地是否良好
- 降低采样率看是否改善
-
心率值不稳定:
- 增加滤波强度
- 检查手指接触压力(适度压力最佳)
- 确保测量时手指保持静止
6. 进阶应用方向
6.1 血氧饱和度(SpO2)监测
利用双波长(通常红光和红外)可以估算血氧:
- 需要两个LED和接收器
- 计算R值:
code复制R = (AC_red/DC_red) / (AC_ir/DC_ir) - 经验公式转换:
code复制SpO2 = 110 - 25*R
6.2 蓝牙/WiFi数据传输
通过HC-05或ESP8266模块将数据发送到手机:
-
数据协议设计:
json复制{"hr":75, "spo2":98, "ts":1625097600} -
手机端APP开发:
- Android可以使用MIT App Inventor
- iOS可以用Swift开发简单接收程序
6.3 机器学习应用
使用TensorFlow Lite for Microcontrollers实现:
- 异常心律检测
- 运动状态分类
- 压力水平评估
需要先采集足够多的样本数据,在PC上训练后部署到MCU。
7. 实测效果与优化记录
在我实际测试中,发现以下几个关键点:
- 手指位置影响:最佳位置是指尖肉垫部分,避免指甲遮挡
- 环境温度影响:寒冷环境下信号较弱,建议保持手指温暖
- 运动干扰:即使微小的手指移动也会引入显著噪声
- 长期稳定性:连续监测30分钟后信号质量会下降,建议间歇测量
一个实用的优化是加入信号质量指数(SQI)评估:
c复制float calculateSQI(float* signal, int length) {
float variance = 0;
float mean = 0;
// 计算均值
for(int i=0; i<length; i++) mean += signal[i];
mean /= length;
// 计算方差
for(int i=0; i<length; i++)
variance += (signal[i]-mean)*(signal[i]-mean);
variance /= length;
return 1.0/(1.0+variance); // 方差越小,SQI越高
}
当SQI低于阈值时,提示用户重新放置手指。