一、系统概述与核心功能
1. 系统定位
基于STM32的便携式心率检测仪,通过MAX30102光电传感器采集人体脉搏波信号(PPG),利用STM32内置ADC采集模拟心电/压力信号,结合数字滤波算法与峰值检测算法实时计算心率值(BPM)。系统支持蓝牙数据上传与OLED实时显示,可作为医疗辅助设备或运动健康监测终端。
2. 核心功能模块
| 模块 |
功能描述 |
技术指标 |
| 信号采集 |
MAX30102采集脉搏波(红光+红外),ADS1015采集模拟心电信号 |
采样率100Hz,分辨率16位 |
| 数据处理 |
数字滤波(带通+陷波)、峰值检测、心率计算、运动伪影消除 |
心率误差±3BPM,响应时间<3秒 |
| 人机交互 |
OLED显示实时心率、波形滚动、电池电量;按键控制测量启停 |
128×64分辨率,I2C接口 |
| 数据通信 |
蓝牙BLE上传原始数据/心率值至上位机/手机APP |
传输距离10m,波特率115200 |
| 低功耗设计 |
动态采样率调整,无操作时自动休眠,锂电池续航>48小时 |
待机电流<50μA |
二、硬件设计方案
1. 核心硬件选型
| 模块 |
型号 |
关键参数 |
接口方式 |
| 主控MCU |
STM32L432KC |
80MHz Cortex-M4,超低功耗,256KB Flash,支持DSP指令 |
核心控制器 |
| 心率传感器 |
MAX30102 |
集成红光(660nm)+红外(880nm)LED,16位ADC,I2C接口 |
I2C1(PB6/PB7) |
| 模拟采集 |
ADS1015 |
12位ADC,4通道,I2C接口,支持可编程增益 |
I2C1(复用总线) |
| 显示模块 |
OLED 12864 |
0.96寸,128×64像素,自发光,I2C接口 |
I2C1(地址0x3C) |
| 蓝牙模块 |
JDY-08(BLE4.0) |
超低功耗,透传模式,支持AT指令配置 |
UART1(PA9/PA10) |
| 电源管理 |
MCP73831+TPS61099 |
锂电池充电管理+高效升压,200mAh锂电池 |
3.3V系统供电 |
| 交互模块 |
触摸按键+振动马达 |
单按键控制,振动马达反馈异常心率 |
GPIO(PA0/PA1) |
2. 硬件电路设计要点
2.1 信号采集电路
MAX30102电路:
- VDD → 3.3V(并联0.1μF+10μF电容)
- SDA/SCL → PB6/PB7(4.7kΩ上拉)
- INT → PA2(中断引脚,数据就绪唤醒)
- LED引脚 → 外接100nF电容稳定电流
ADS1015电路(可选模拟心电):
- AIN0 → 心电电极输入(通过仪表放大器AD620)
- AIN1 → 压力传感器输入
- ADDR → GND(I2C地址0x48)
2.2 抗干扰设计
- 电源隔离:模拟电源(AVDD)与数字电源(DVDD)通过磁珠隔离
- 信号屏蔽:MAX30102加装黑色硅胶遮光罩,避免环境光干扰
- 电极设计:心电采集使用银/氯化银电极,降低接触阻抗
- PCB布局:模拟信号线远离数字线,大面积铺地减少EMI
3. 传感器佩戴方案
手指佩戴式:
┌─────────────┐
│ MAX30102 │ ← 食指指尖(指甲面朝下)
│ [传感器] │
└──────┬──────┘
│
FPC软排线(10cm)
│
┌──────▼──────┐
│ STM32主控板 │
│ OLED显示 │
└─────────────┘
手腕佩戴式(可选):
- 集成在手表表带中
- MAX30102朝向手腕内侧
- 松紧度可调节魔术贴
三、软件设计与核心算法
1. 系统架构(FreeRTOS多任务)
采用FreeRTOS实时操作系统,划分6个核心任务:
| 任务 |
优先级 |
功能 |
周期 |
| 信号采集 |
5(最高) |
MAX30102数据读取,ADS1015采样 |
10ms |
| 数字滤波 |
4 |
带通滤波(0.5-3Hz)、50Hz陷波、去基线漂移 |
事件触发 |
| 峰值检测 |
4 |
PPG波形峰值识别,RR间期计算 |
事件触发 |
| 心率计算 |
3 |
BPM计算,数据平滑,异常值剔除 |
1s |
| 显示更新 |
2 |
OLED波形滚动显示,心率数值更新 |
100ms |
| 蓝牙传输 |
2 |
数据包封装,蓝牙透传发送 |
500ms |
2. 核心算法实现(基于HAL库)
2.1 MAX30102驱动与数据采集
#include "max30102.h"
#include "i2c.h"
#define MAX30102_INT_STATUS 0x00
#define MAX30102_FIFO_WR_PTR 0x04
#define MAX30102_FIFO_DATA 0x07
#define MAX30102_MODE_CONFIG 0x09
#define MAX30102_SPO2_CONFIG 0x0A
#define MAX30102_LED_CONFIG 0x0C
uint32_t red_buffer[100];
uint32_t ir_buffer[100];
uint8_t buffer_index = 0;
uint8_t MAX30102_Init(void) {
uint8_t part_id;
HAL_I2C_Mem_Read(&hi2c1, 0x57<<1, 0xFF, 1, &part_id, 1, 100);
if (part_id != 0x15) return 1;
MAX30102_WriteReg(MAX30102_MODE_CONFIG, 0x40);
HAL_Delay(100);
MAX30102_WriteReg(MAX30102_MODE_CONFIG, 0x03);
MAX30102_WriteReg(MAX30102_SPO2_CONFIG, 0x27);
MAX30102_WriteReg(MAX30102_LED_CONFIG, 0x24);
return 0;
}
void MAX30102_ReadFIFO(uint32_t *red, uint32_t *ir) {
uint8_t fifo_data[6];
HAL_I2C_Mem_Read(&hi2c1, 0x57<<1, MAX30102_FIFO_DATA, 1, fifo_data, 6, 100);
*red = ((uint32_t)fifo_data[0] << 16) | ((uint32_t)fifo_data[1] << 8) | fifo_data[2];
*ir = ((uint32_t)fifo_data[3] << 16) | ((uint32_t)fifo_data[4] << 8) | fifo_data[5];
*red &= 0x3FFFF;
*ir &= 0x3FFFF;
}
2.2 数字滤波算法(关键!)
#include "digital_filter.h"
typedef struct {
float b[3];
float a[3];
float x[3];
float y[3];
} BandpassFilter_t;
BandpassFilter_t bp_filter;
void Bandpass_Init(void) {
bp_filter.b[0] = 0.0976f;
bp_filter.b[1] = 0.0f;
bp_filter.b[2] = -0.0976f;
bp_filter.a[0] = 1.0f;
bp_filter.a[1] = -1.7955f;
bp_filter.a[2] = 0.8045f;
memset(&bp_filter.x, 0, sizeof(bp_filter.x));
memset(&bp_filter.y, 0, sizeof(bp_filter.y));
}
float Notch50Hz_Filter(float input) {
static float x1=0, x2=0, y1=0, y2=0;
float output;
output = 0.9317f*input - 1.8634f*x1 + 0.9317f*x2 + 1.8634f*y1 - 0.8692f*y2;
x2 = x1; x1 = input;
y2 = y1; y1 = output;
return output;
}
float Bandpass_Filter(float input) {
float output;
bp_filter.x[2] = bp_filter.x[1];
bp_filter.x[1] = bp_filter.x[0];
bp_filter.x[0] = input;
bp_filter.y[2] = bp_filter.y[1];
bp_filter.y[1] = bp_filter.y[0];
output = bp_filter.b[0]*bp_filter.x[0] + bp_filter.b[1]*bp_filter.x[1] +
bp_filter.b[2]*bp_filter.x[2] - bp_filter.a[1]*bp_filter.y[1] -
bp_filter.a[2]*bp_filter.y[2];
bp_filter.y[0] = output;
return output;
}
2.3 峰值检测与心率计算
#include "heart_rate_algorithm.h"
typedef struct {
float threshold;
uint32_t peak_interval;
uint8_t peak_detected;
uint32_t last_peak_time;
uint32_t rr_intervals[10];
uint8_t rr_index;
} PeakDetector_t;
PeakDetector_t peak_detector;
void PeakDetector_Init(void) {
peak_detector.threshold = 0.5f;
peak_detector.peak_interval = 0;
peak_detector.peak_detected = 0;
peak_detector.last_peak_time = 0;
peak_detector.rr_index = 0;
memset(&peak_detector.rr_intervals, 0, sizeof(peak_detector.rr_intervals));
}
uint8_t Detect_Peak(float filtered_signal) {
static uint8_t above_threshold = 0;
uint8_t peak_found = 0;
if (filtered_signal > peak_detector.threshold) {
if (!above_threshold) {
above_threshold = 1;
peak_detector.peak_detected = 1;
peak_found = 1;
if (peak_detector.last_peak_time > 0) {
uint32_t rr_interval = peak_detector.peak_interval - peak_detector.last_peak_time;
peak_detector.rr_intervals[peak_detector.rr_index] = rr_interval;
peak_detector.rr_index = (peak_detector.rr_index + 1) % 10;
}
peak_detector.last_peak_time = peak_detector.peak_interval;
}
} else {
above_threshold = 0;
}
peak_detector.peak_interval++;
peak_detector.threshold = 0.95f * peak_detector.threshold + 0.05f * filtered_signal;
return peak_found;
}
uint8_t Calculate_HeartRate(void) {
float avg_rr_interval = 0;
uint8_t valid_intervals = 0;
for (uint8_t i = 0; i < 10; i++) {
if (peak_detector.rr_intervals[i] > 0) {
avg_rr_interval += peak_detector.rr_intervals[i];
valid_intervals++;
}
}
if (valid_intervals < 3) return 0;
avg_rr_interval /= valid_intervals;
float heart_rate = 6000.0f / avg_rr_interval;
if (heart_rate < 30 || heart_rate > 180) return 0;
return (uint8_t)heart_rate;
}
2.4 FreeRTOS任务实现
#include "freertos.h"
#include "task.h"
void Signal_Acquisition_Task(void *pvParameters) {
uint32_t red_value, ir_value;
while (1) {
MAX30102_ReadFIFO(&red_value, &ir_value);
red_buffer[buffer_index] = red_value;
ir_buffer[buffer_index] = ir_value;
buffer_index = (buffer_index + 1) % 100;
xQueueSend(filter_queue, &ir_value, portMAX_DELAY);
vTaskDelay(pdMS_TO_TICKS(10));
}
}
void Digital_Filter_Task(void *pvParameters) {
float raw_signal, filtered_signal;
while (1) {
if (xQueueReceive(filter_queue, &raw_signal, portMAX_DELAY) == pdTRUE) {
filtered_signal = Notch50Hz_Filter(raw_signal);
filtered_signal = Bandpass_Filter(filtered_signal);
xQueueSend(peak_queue, &filtered_signal, portMAX_DELAY);
}
}
}
void Heart_Rate_Calculation_Task(void *pvParameters) {
float filtered_signal;
uint8_t heart_rate;
static uint8_t stable_hr[5];
static uint8_t hr_index = 0;
while (1) {
if (xQueueReceive(peak_queue, &filtered_signal, portMAX_DELAY) == pdTRUE) {
if (Detect_Peak(filtered_signal)) {
heart_rate = Calculate_HeartRate();
if (heart_rate > 0) {
stable_hr[hr_index] = heart_rate;
hr_index = (hr_index + 1) % 5;
uint8_t avg_hr = 0;
for (uint8_t i = 0; i < 5; i++) avg_hr += stable_hr[i];
avg_hr /= 5;
xQueueSend(display_queue, &avg_hr, portMAX_DELAY);
}
}
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}
3. 数据采集与蓝牙传输协议
3.1 数据包格式
typedef struct {
uint8_t header;
uint8_t data_type;
uint16_t timestamp;
uint32_t data_value;
uint8_t checksum;
uint8_t footer;
} Bluetooth_Packet_t;
void Send_HeartRate_Data(uint8_t heart_rate, uint16_t timestamp) {
Bluetooth_Packet_t packet;
packet.header = 0xAA;
packet.data_type = 0x01;
packet.timestamp = timestamp;
packet.data_value = heart_rate;
packet.checksum = packet.header ^ packet.data_type ^
(packet.timestamp >> 8) ^ (packet.timestamp & 0xFF) ^
packet.data_value;
packet.footer = 0x55;
HAL_UART_Transmit(&huart1, (uint8_t*)&packet, sizeof(packet), 100);
}
3.2 Python上位机接收程序
import serial
import struct
import matplotlib.pyplot as plt
from collections import deque
class HeartRateReceiver:
def __init__(self, port='COM3', baudrate=115200):
self.ser = serial.Serial(port, baudrate, timeout=1)
self.hr_buffer = deque(maxlen=100)
def parse_packet(self, data):
"""解析蓝牙数据包"""
if len(data) != 7:
return None
header, data_type, timestamp_high, timestamp_low, data_value, checksum, footer = data
if header != 0xAA or footer != 0x55:
return None
calc_checksum = header ^ data_type ^ timestamp_high ^ timestamp_low ^ data_value
if calc_checksum != checksum:
return None
timestamp = (timestamp_high << 8) | timestamp_low
return data_type, timestamp, data_value
def plot_realtime(self):
"""实时绘制心率曲线"""
plt.ion()
fig, ax = plt.subplots(figsize=(10, 6))
while True:
if self.ser.in_waiting >= 7:
data = list(self.ser.read(7))
result = self.parse_packet(data)
if result:
data_type, timestamp, value = result
if data_type == 0x01:
self.hr_buffer.append(value)
ax.clear()
ax.plot(list(self.hr_buffer), 'r-', linewidth=2)
ax.set_title('Real-time Heart Rate Monitor')
ax.set_xlabel('Time (samples)')
ax.set_ylabel('Heart Rate (BPM)')
ax.grid(True, alpha=0.3)
ax.set_ylim(40, 180)
plt.pause(0.01)
plt.pause(0.1)
if __name__ == "__main__":
receiver = HeartRateReceiver('COM3', 115200)
receiver.plot_realtime()
参考代码 基于STM32的数据采集+心率检测仪(原理图、PCB、程序源码等) www.youwenfan.com/contentali/123483.html
四、系统调试与优化
1. 调试步骤
| 阶段 |
操作 |
工具 |
预期结果 |
| 硬件调试 |
测量MAX30102供电电压,验证I2C通信 |
万用表、示波器 |
3.3V稳定,I2C波形正常 |
| 信号调试 |
观察原始PPG波形,验证滤波效果 |
串口打印、MATLAB分析 |
波形清晰,无明显噪声 |
| 算法调试 |
测试峰值检测准确性,验证心率计算 |
秒表计时对比 |
误差<±3BPM |
| 系统测试 |
实际佩戴测试,连续监测稳定性 |
真人测试 |
连续30分钟数据稳定 |
2. 常见问题与解决方案
| 问题 |
原因 |
解决方案 |
| 信号不稳定 |
佩戴松动,环境光干扰 |
调整佩戴松紧度,加装遮光罩 |
| 心率计算错误 |
运动伪影,基线漂移 |
改进滤波算法,增加运动检测 |
| 功耗过高 |
采样率过高,LED电流过大 |
动态调节采样率,优化LED驱动 |
| 蓝牙连接不稳定 |
信号干扰,距离过远 |
更换天线,缩短通信距离 |
3. 性能优化建议
- 动态采样率:静止时降低采样率,运动时提高采样率
- 自适应阈值:根据信号强度自动调整峰值检测阈值
- 运动伪影消除:结合加速度传感器检测运动状态,运动时不计算心率
- 数据压缩:蓝牙传输时使用差分编码压缩数据
五、扩展功能
1. 血氧饱和度测量(SpO2)
float Calculate_SpO2(uint32_t red_ac, uint32_t red_dc, uint32_t ir_ac, uint32_t ir_dc) {
float ratio = (red_ac * ir_dc) / (ir_ac * red_dc);
float spo2 = 110.0f - 25.0f * ratio;
if (spo2 > 100.0f) spo2 = 100.0f;
if (spo2 < 70.0f) spo2 = 70.0f;
return spo2;
}
2. 心率变异性分析(HRV)
void Calculate_HRV(uint32_t *rr_intervals, uint8_t count) {
float mean_rr = 0, sdnn = 0, rmssd = 0;
uint32_t sum_squared_diff = 0;
for (uint8_t i = 0; i < count; i++) {
mean_rr += rr_intervals[i];
}
mean_rr /= count;
for (uint8_t i = 0; i < count; i++) {
float diff = rr_intervals[i] - mean_rr;
sdnn += diff * diff;
}
sdnn = sqrt(sdnn / count);
for (uint8_t i = 1; i < count; i++) {
float diff = rr_intervals[i] - rr_intervals[i-1];
sum_squared_diff += diff * diff;
}
rmssd = sqrt(sum_squared_diff / (count - 1));
}
3. 手机APP开发
- Android端:使用BluetoothLeService接收数据,MPAndroidChart绘制实时波形
- iOS端:CoreBluetooth框架,Charts库显示心率趋势
- Web端:WebSocket实时显示,ECharts绘制历史曲线
六、总结
基于STM32的心率检测仪通过MAX30102光电传感器实现无创心率监测,数字滤波算法有效去除噪声干扰,峰值检测算法精准计算心率值。