欢迎光临
我们一直在努力

ESP32-S3健康监测手环开发实战(三)

ESP32-S3健康监测手环开发实战(三):MAX30102心率血氧算法与PPG信号处理

本篇深入讲解基于MAX30102的心率血氧测量实现,包括PPG信号原理、中断驱动架构、过零检测心率算法、R值血氧公式以及稳定性检测机制。

1. MAX30102传感器原理

在这里插入图片描述

1.1 PPG光电容积脉搏波原理

MAX30102是一款集成了红光LED(660nm)和红外LED(880nm)的反射式光电传感器,通过光电容积描记法(PPG)测量心率和血氧。

工作原理:

┌─────────────────────────────────────┐
│ MAX30102 │
│ ┌────┐ ┌────┐ │
│ │红光│ ────► │红外│ │
│ │LED │ 皮肤 │LED │ │
│ └────┘ 反射 └────┘ │
│ ↓ ↓ │
│ ┌────────────────────────┐ │
│ │ 光电二极管 │ │
│ │ (接收反射光) │ │
│ └────────────────────────┘ │
└─────────────────────────────────────┘

心率测量原理:

  • 心脏跳动时,血液流经毛细血管
  • 血红蛋白吸收光线,导致反射光强度变化
  • 光强变化周期 = 心跳周期
  • 通过检测脉冲周期计算心率

血氧测量原理:

  • 氧合血红蛋白(HbO2)和脱氧血红蛋白(Hb)对红光/红外光吸收率不同
  • 红光:Hb吸收更多(脱氧时信号大)
  • 红外光:HbO2吸收更多(氧合时信号大)
  • 通过两种光的吸收比值计算血氧饱和度

1.2 双通道信号特性

通道波长特性用途
红光 660nm 脱氧血红蛋白吸收强 血氧计算
红外光 880nm 信号稳定,脉搏明显 心率检测

1.3 传感器配置

// 硬件配置(独立I2C端口)
#define APP_MAX30102_I2C_PORT I2C_NUM_1 // 独立I2C端口
#define APP_MAX30102_I2C_SDA_PIN GPIO_NUM_5 // SDA
#define APP_MAX30102_I2C_SCL_PIN GPIO_NUM_15 // SCL
#define APP_MAX30102_I2C_FREQ_HZ 100000 // 100kHz
#define APP_MAX30102_INT_PIN GPIO_NUM_3 // 中断引脚

// 算法参数
#define SAMPLE_RATE 100 // 采样率 100Hz
#define BUFFER_SIZE 500 // 5秒缓冲(500样本)
#define FILTER_WINDOW_SIZE 5 // 移动平均窗口 50ms

2. GPIO中断驱动架构

2.1 为什么使用中断驱动?

MAX30102内置FIFO缓冲区,当有新数据时会拉低INT引脚产生中断。中断驱动方式的优势:

  • 实时性高:数据就绪立即处理
  • CPU占用低:不需要轮询
  • 不丢数据:FIFO + 中断确保数据完整
  • 2.2 中断处理架构

    ┌─────────────────────────────────────────────────────────────┐
    │ 中断驱动架构 │
    ├─────────────────────────────────────────────────────────────┤
    │ │
    │ MAX30102 INT引脚 (GPIO3) │
    │ │ │
    │ │ 下降沿触发 │
    │ ▼ │
    │ ┌────────────────┐ │
    │ │ gpio_isr_handler │ ISR (中断服务程序) │
    │ │ (IRAM_ATTR) │ 在中断上下文执行,必须快速 │
    │ └────────┬───────┘ │
    │ │ │
    │ │ xQueueSendFromISR │
    │ ▼ │
    │ ┌────────────────┐ │
    │ │ 事件队列 │ 异步解耦 │
    │ │ (10个元素) │ │
    │ └────────┬───────┘ │
    │ │ │
    │ │ xQueueReceive (阻塞等待) │
    │ ▼ │
    │ ┌────────────────┐ │
    │ │ gpio_intr_task │ FreeRTOS任务 │
    │ │ (优先级10) │ 独立栈4KB(支持浮点运算) │
    │ │ │ 执行:读FIFO → 滤波 → 算法 │
    │ └────────────────┘ │
    │ │
    └─────────────────────────────────────────────────────────────┘

    2.3 中断初始化代码

    static esp_err_t gpio_intr_init(void)
    {
    // GPIO 配置
    gpio_config_t io_conf = {
    .intr_type = GPIO_INTR_NEGEDGE, // 下降沿触发
    .mode = GPIO_MODE_INPUT,
    .pin_bit_mask = (1ULL << APP_MAX30102_INT_PIN),
    .pull_down_en = GPIO_PULLDOWN_DISABLE,
    .pull_up_en = GPIO_PULLUP_ENABLE, // 启用上拉
    };
    ESP_ERROR_CHECK(gpio_config(&io_conf));

    // 创建事件队列(容量10个事件)
    s_gpio_evt_queue = xQueueCreate(10, sizeof(uint32_t));

    // 创建中断处理任务
    xTaskCreate(
    gpio_intr_task,
    "max30102_intr",
    4096, // 栈大小4KB(浮点运算需要)
    NULL,
    10, // 优先级10(较高)
    NULL
    );

    // 安装GPIO ISR服务并注册处理函数
    gpio_install_isr_service(0);
    gpio_isr_handler_add(APP_MAX30102_INT_PIN, gpio_isr_handler,
    (void *)APP_MAX30102_INT_PIN);

    return ESP_OK;
    }

    2.4 ISR处理函数

    // ISR必须加IRAM_ATTR放入IRAM,确保中断时可访问
    static void IRAM_ATTR gpio_isr_handler(void *arg)
    {
    uint32_t gpio_num = (uint32_t)arg;
    // 从ISR发送事件到队列(不阻塞)
    xQueueSendFromISR(s_gpio_evt_queue, &gpio_num, NULL);
    }

    2.5 中断处理任务

    static void gpio_intr_task(void *arg)
    {
    uint32_t io_num;
    uint32_t red_raw, ir_raw;
    bool finger_on;

    for (;;) {
    // 阻塞等待中断事件
    if (xQueueReceive(s_gpio_evt_queue, &io_num, portMAX_DELAY)) {
    // 读取FIFO数据
    if (max30102_read_fifo(&red_raw, &ir_raw) != ESP_OK) {
    max30102_clear_interrupt();
    continue;
    }

    // 检测是否有手指(信号强度判断)
    finger_on = (red_raw >= APP_MAX30102_FINGER_THRESHOLD);

    // 更新原始数据
    s_latest_data.red_raw = red_raw;
    s_latest_data.ir_raw = ir_raw;
    s_latest_data.finger_on = finger_on;

    // 状态机处理
    state_machine_update(finger_on, red_raw, ir_raw);

    // 清除中断标志
    max30102_clear_interrupt();
    }
    }
    }

    3. PPG信号预处理

    3.1 环形缓冲区设计

    // PPG 数据缓冲区结构
    typedef struct {
    uint32_t red_raw[BUFFER_SIZE]; // 原始红光数据
    uint32_t ir_raw[BUFFER_SIZE]; // 原始红外数据
    float red_filtered[BUFFER_SIZE]; // 滤波后红光
    float ir_filtered[BUFFER_SIZE]; // 滤波后红外
    uint16_t write_idx; // 写入索引
    uint16_t count; // 有效数据数量
    } ppg_buffer_t;

    #define BUFFER_SIZE 500 // 5秒 × 100Hz = 500样本

    环形缓冲区工作原理:

    索引: 0 1 2 3 4 … 497 498 499
    ┌───┬───┬───┬───┬───┬───┬───┬───┬───┐
    数据: │ D │ D │ D │ D │ D │…│ D │ D │ D │
    └───┴───┴───┴───┴───┴───┴───┴───┴───┘
    ↑
    write_idx (循环递增)

    当write_idx达到500时,回绕到0,覆盖最老的数据

    3.2 移动平均滤波

    // 向缓冲区添加数据并进行滤波
    static void ppg_buffer_add(uint32_t red_raw, uint32_t ir_raw)
    {
    uint16_t idx = s_ppg_buffer.write_idx;

    // 存储原始数据
    s_ppg_buffer.red_raw[idx] = red_raw;
    s_ppg_buffer.ir_raw[idx] = ir_raw;

    // 移动平均滤波(5点窗口 = 50ms)
    float red_sum = 0, ir_sum = 0;
    uint8_t filter_count = 0;

    for (int i = 0; i < FILTER_WINDOW_SIZE; i++) {
    int buf_idx = (idx – i + BUFFER_SIZE) % BUFFER_SIZE;
    if (s_ppg_buffer.count > (uint16_t)i) {
    red_sum += s_ppg_buffer.red_raw[buf_idx];
    ir_sum += s_ppg_buffer.ir_raw[buf_idx];
    filter_count++;
    }
    }

    // 计算平均值作为滤波后的值
    if (filter_count > 0) {
    s_ppg_buffer.red_filtered[idx] = red_sum / filter_count;
    s_ppg_buffer.ir_filtered[idx] = ir_sum / filter_count;
    }

    // 更新索引(环形)
    s_ppg_buffer.write_idx = (idx + 1) % BUFFER_SIZE;
    if (s_ppg_buffer.count < BUFFER_SIZE) {
    s_ppg_buffer.count++;
    }
    }

    移动平均滤波效果:

    • 窗口大小:5样本 = 50ms(100Hz采样率)
    • 作用:平滑高频噪声,保留心率信号(0.5-3Hz)
    • 延迟:25ms(窗口大小的一半)

    4. 心率计算算法

    4.1 过零检测原理

    心率计算的核心是检测PPG信号的周期。本项目采用过零检测法:

    信号值
    ▲
    │ ╱╲ ╱╲ ╱╲
    │ ╱ ╲ ╱ ╲ ╱ ╲
    DC─┼──╱────╲──╱────╲──╱────╲── ←── DC水平线
    │ ╱ ╲╱ ╲╱ ╲
    │ ↑ ↑ ↑
    │ 过零点 过零点 过零点
    │ (上升) (上升) (上升)
    └─────────────────────────────► 时间
    │←─间隔1─→│←─间隔2─→│

    心率 = 60 × 采样率 / 平均间隔

    4.2 算法实现

    static uint8_t calculate_heart_rate(void)
    {
    if (s_ppg_buffer.count < MIN_SAMPLES_FOR_CALC) {
    return 0; // 数据不足3秒,无法计算
    }

    uint16_t analyze_count = (s_ppg_buffer.count < BUFFER_SIZE) ?
    s_ppg_buffer.count : BUFFER_SIZE;

    // 1. 计算IR信号的DC分量(平均值)
    float dc_sum = 0;
    for (uint16_t i = 0; i < analyze_count; i++) {
    uint16_t buf_idx = (s_ppg_buffer.write_idx – 1 – i + BUFFER_SIZE) % BUFFER_SIZE;
    dc_sum += s_ppg_buffer.ir_filtered[buf_idx];
    }
    float dc_level = dc_sum / analyze_count;

    // 2. 检测过零点(从下到上穿越DC水平)
    uint16_t peak_intervals[32];
    uint8_t interval_count = 0;
    uint16_t last_crossing_idx = 0;
    bool below_dc = true;

    for (uint16_t i = analyze_count – 1; i > 0; i—) {
    uint16_t buf_idx = (s_ppg_buffer.write_idx – 1 – i + BUFFER_SIZE) % BUFFER_SIZE;
    float value = s_ppg_buffer.ir_filtered[buf_idx];

    if (below_dc && value > dc_level) {
    // 检测到上升过零点
    if (last_crossing_idx > 0) {
    uint16_t interval = (analyze_count – 1 – i) – last_crossing_idx;
    // 间隔有效性检查(40-200 BPM范围)
    if (interval >= MIN_PEAK_INTERVAL && // 30样本 = 200BPM
    interval <= MAX_PEAK_INTERVAL) { // 150样本 = 40BPM
    if (interval_count < 32) {
    peak_intervals[interval_count++] = interval;
    }
    }
    }
    last_crossing_idx = analyze_count – 1 – i;
    below_dc = false;
    } else if (!below_dc && value < dc_level) {
    below_dc = true; // 进入DC以下区域
    }
    }

    if (interval_count < 2) {
    return 0; // 峰值太少,无法计算
    }

    // 3. 计算平均峰值间隔
    uint32_t interval_sum = 0;
    for (uint8_t i = 0; i < interval_count; i++) {
    interval_sum += peak_intervals[i];
    }
    float avg_interval = (float)interval_sum / interval_count;

    // 4. 计算心率:HR = 采样率 × 60 / 平均间隔
    float hr = (SAMPLE_RATE * 60.0f) / avg_interval;

    // 5. 范围限制
    if (hr < HR_MIN || hr > HR_MAX) {
    return 0;
    }

    return (uint8_t)(hr + 0.5f); // 四舍五入
    }

    4.3 心率计算参数

    #define HR_MIN 40 // 最小有效心率
    #define HR_MAX 200 // 最大有效心率
    #define MIN_PEAK_INTERVAL 30 // 最小间隔 = 100Hz × 60 / 200BPM
    #define MAX_PEAK_INTERVAL 150 // 最大间隔 = 100Hz × 60 / 40BPM

    5. 血氧饱和度计算

    5.1 R值公式原理

    血氧计算基于红光和红外光的吸收比值(R值):

    R = (AC_red / DC_red) / (AC_ir / DC_ir)

    各分量含义:

    • DC_red:红光信号的平均值(直流分量)
    • AC_red:红光信号的交流分量(脉动部分)
    • DC_ir:红外信号的平均值
    • AC_ir:红外信号的交流分量

    R值与血氧的关系:

    • R ≈ 0.4 时,SpO2 ≈ 100%
    • R ≈ 1.0 时,SpO2 ≈ 82%
    • R ≈ 2.0 时,SpO2 ≈ 50%

    5.2 SpO2经验公式

    // SpO2 = 104 – 17×R – 11×R² (参考 Maxim AN6409)
    float spo2 = 104.0f – 17.0f * R – 11.0f * R * R;

    这是一个经验公式,来自Maxim官方应用笔记AN6409,适用于大多数人群。

    5.3 算法实现

    static float calculate_spo2(void)
    {
    if (s_ppg_buffer.count < MIN_SAMPLES_FOR_CALC) {
    return 0;
    }

    uint16_t analyze_count = (s_ppg_buffer.count < BUFFER_SIZE) ?
    s_ppg_buffer.count : BUFFER_SIZE;

    // 1. 计算DC分量(平均值)
    float red_dc_sum = 0, ir_dc_sum = 0;
    for (uint16_t i = 0; i < analyze_count; i++) {
    uint16_t buf_idx = (s_ppg_buffer.write_idx – 1 – i + BUFFER_SIZE) % BUFFER_SIZE;
    red_dc_sum += s_ppg_buffer.red_filtered[buf_idx];
    ir_dc_sum += s_ppg_buffer.ir_filtered[buf_idx];
    }
    float red_dc = red_dc_sum / analyze_count;
    float ir_dc = ir_dc_sum / analyze_count;

    if (red_dc < 1.0f || ir_dc < 1.0f) {
    return 0; // 防止除零
    }

    // 2. 计算AC分量(使用标准差近似)
    float red_ac_sum = 0, ir_ac_sum = 0;
    for (uint16_t i = 0; i < analyze_count; i++) {
    uint16_t buf_idx = (s_ppg_buffer.write_idx – 1 – i + BUFFER_SIZE) % BUFFER_SIZE;
    float red_diff = s_ppg_buffer.red_filtered[buf_idx] – red_dc;
    float ir_diff = s_ppg_buffer.ir_filtered[buf_idx] – ir_dc;
    red_ac_sum += red_diff * red_diff;
    ir_ac_sum += ir_diff * ir_diff;
    }
    float red_ac = sqrtf(red_ac_sum / analyze_count); // RMS
    float ir_ac = sqrtf(ir_ac_sum / analyze_count);

    if (ir_ac < 0.001f) {
    return 0; // AC分量太小,信号无效
    }

    // 3. 计算R值
    float R = (red_ac / red_dc) / (ir_ac / ir_dc);

    // 4. 计算SpO2
    float spo2 = 104.0f – 17.0f * R – 11.0f * R * R;

    // 5. 范围限制
    if (spo2 < SPO2_MIN) spo2 = SPO2_MIN; // 70%
    if (spo2 > SPO2_MAX) spo2 = SPO2_MAX; // 100%

    return spo2;
    }

    6. 稳定性检测机制

    6.1 为什么需要稳定性检测?

    PPG信号容易受到干扰:

    • 手指移动导致运动伪影
    • 环境光干扰
    • 接触压力变化

    稳定性检测确保输出数据可靠。

    6.2 方差判断法

    // 稳定性检测历史
    typedef struct {
    uint8_t hr_history[STABILITY_HISTORY_SIZE]; // 5个心率历史值
    float spo2_history[STABILITY_HISTORY_SIZE]; // 5个血氧历史值
    uint8_t history_idx;
    uint8_t history_count;
    } stability_tracker_t;

    #define STABILITY_HISTORY_SIZE 5 // 历史记录数量
    #define STABILITY_HR_THRESHOLD 10.0f // 心率方差阈值
    #define STABILITY_SPO2_THRESHOLD 2.0f // 血氧方差阈值

    6.3 稳定性检测实现

    static bool stability_check(uint8_t hr, float spo2)
    {
    // 添加到历史记录
    s_stability.hr_history[s_stability.history_idx] = hr;
    s_stability.spo2_history[s_stability.history_idx] = spo2;
    s_stability.history_idx = (s_stability.history_idx + 1) % STABILITY_HISTORY_SIZE;
    if (s_stability.history_count < STABILITY_HISTORY_SIZE) {
    s_stability.history_count++;
    }

    // 需要至少5个样本才能判断
    if (s_stability.history_count < STABILITY_HISTORY_SIZE) {
    return false;
    }

    // 计算方差
    float hr_sum = 0, hr_sq_sum = 0;
    float spo2_sum = 0, spo2_sq_sum = 0;

    for (uint8_t i = 0; i < STABILITY_HISTORY_SIZE; i++) {
    float hr_val = (float)s_stability.hr_history[i];
    float spo2_val = s_stability.spo2_history[i];

    hr_sum += hr_val;
    hr_sq_sum += hr_val * hr_val;
    spo2_sum += spo2_val;
    spo2_sq_sum += spo2_val * spo2_val;
    }

    // 方差 = E[X²] – E[X]²
    float hr_mean = hr_sum / STABILITY_HISTORY_SIZE;
    float hr_var = (hr_sq_sum / STABILITY_HISTORY_SIZE) – (hr_mean * hr_mean);

    float spo2_mean = spo2_sum / STABILITY_HISTORY_SIZE;
    float spo2_var = (spo2_sq_sum / STABILITY_HISTORY_SIZE) – (spo2_mean * spo2_mean);

    // 检查是否稳定
    return (hr_var < STABILITY_HR_THRESHOLD && // 心率方差 < 10
    spo2_var < STABILITY_SPO2_THRESHOLD); // 血氧方差 < 2
    }

    7. 状态机设计

    7.1 状态定义

    typedef enum {
    MAX30102_STATE_IDLE, // 空闲:等待手指
    MAX30102_STATE_DETECTING, // 检测中:采集数据等待稳定
    MAX30102_STATE_STABLE, // 稳定:输出有效数据
    } max30102_state_t;

    7.2 状态转换图

    ┌────────────────────────────────┐
    │ │
    │ 手指移开 │
    ▼ │
    ┌──────────┐ │
    │ IDLE │◄──────────────────┐ │
    │ (空闲) │ │ │
    └────┬─────┘ │ │
    │ │ │
    检测到手指 │ │
    │ │ │
    ▼ │ │
    ┌─────────────┐ 手指移开 │
    │ DETECTING │─────────────────┘ │
    │ (检测中) │ │
    └──────┬──────┘ │
    │ │
    数据稳定 │
    │ │
    ▼ │
    ┌──────────┐ │
    │ STABLE │──────────────────────────┘
    │ (稳定) │ 数据不稳定时回到DETECTING
    └──────────┘

    7.3 状态机实现

    static void state_machine_update(bool finger_on, uint32_t red_raw, uint32_t ir_raw)
    {
    uint32_t current_time = get_time_ms();

    switch (s_state) {
    case MAX30102_STATE_IDLE:
    // 空闲状态:等待手指放上
    if (finger_on) {
    state_transition(MAX30102_STATE_DETECTING);
    }
    break;

    case MAX30102_STATE_DETECTING:
    // 检测中:采集数据并判断稳定性
    if (!finger_on) {
    state_transition(MAX30102_STATE_IDLE);
    break;
    }

    // 添加数据到缓冲区
    ppg_buffer_add(red_raw, ir_raw);

    // 尝试计算心率血氧
    if (s_ppg_buffer.count >= MIN_SAMPLES_FOR_CALC) {
    uint8_t hr = calculate_heart_rate();
    float spo2 = calculate_spo2();

    if (hr > 0 && spo2 > 0) {
    // 检查稳定性
    if (stability_check(hr, spo2)) {
    stability_get_average(&s_latest_data.heart_rate,
    &s_latest_data.spo2);
    state_transition(MAX30102_STATE_STABLE);
    }
    }
    }
    break;

    case MAX30102_STATE_STABLE:
    // 稳定状态:定期输出数据
    if (!finger_on) {
    state_transition(MAX30102_STATE_IDLE);
    break;
    }

    ppg_buffer_add(red_raw, ir_raw);

    // 每500ms更新一次输出
    if ((current_time – s_last_output_time) >= STABLE_OUTPUT_INTERVAL_MS) {
    s_last_output_time = current_time;

    uint8_t hr = calculate_heart_rate();
    float spo2 = calculate_spo2();

    if (hr > 0 && spo2 > 0) {
    if (stability_check(hr, spo2)) {
    stability_get_average(&s_latest_data.heart_rate,
    &s_latest_data.spo2);
    s_data_ready = true;
    } else {
    // 数据不稳定,回到检测状态
    state_transition(MAX30102_STATE_DETECTING);
    }
    }
    }
    break;
    }
    }

    8. 完整数据流

    ┌─────────────────────────────────────────────────────────────┐
    │ MAX30102 数据处理流程 │
    ├─────────────────────────────────────────────────────────────┤
    │ │
    │ MAX30102 FIFO ──────► GPIO中断 ──────► ISR │
    │ │ │
    │ ▼ │
    │ 事件队列 │
    │ │ │
    │ ▼ │
    │ 中断处理任务 │
    │ │ │
    │ ┌─────────────────┴─────────────┐ │
    │ ▼ ▼ │
    │ 原始数据存储 滤波处理 │
    │ (red_raw, ir_raw) (移动平均) │
    │ │ │ │
    │ └─────────────────┬─────────────┘ │
    │ │ │
    │ ▼ │
    │ ┌─────────────────┴─────────────┐ │
    │ ▼ ▼ │
    │ 心率计算 血氧计算 │
    │ (过零检测) (R值公式) │
    │ │ │ │
    │ └─────────────────┬─────────────┘ │
    │ │ │
    │ ▼ │
    │ 稳定性检测 │
    │ (方差判断) │
    │ │ │
    │ ┌───────┴───────┐ │
    │ ▼ ▼ │
    │ 稳定 不稳定 │
    │ │ │ │
    │ ▼ ▼ │
    │ 输出数据 继续采集 │
    │ │
    └─────────────────────────────────────────────────────────────┘

    9. 总结

    本篇详细讲解了MAX30102心率血氧测量的完整实现:

  • PPG原理:红光/红外双通道测量,反射式光电检测
  • 中断驱动:GPIO下降沿中断 + FreeRTOS队列 + 独立任务
  • 信号预处理:500样本环形缓冲 + 5点移动平均滤波
  • 心率算法:DC水平计算 + 过零点检测 + 平均间隔 → BPM
  • 血氧算法:AC/DC分量提取 + R值公式 + SpO2经验公式
  • 稳定性检测:5样本历史 + 方差阈值判断
  • 状态机:IDLE → DETECTING → STABLE,确保数据可靠
  • 下篇预告

    在最后一篇文章中,我们将讲解软件I2C和OLED显示的实现:

    • 为什么需要软件I2C
    • GPIO模拟I2C协议详解
    • SSD1306 OLED驱动开发
    • 界面布局与局部刷新优化

    系列文章导航:

    • 第一篇:项目架构设计与双I2C总线实现
    • 第二篇:MPU6050跌倒检测算法与状态机设计
    • 本篇:MAX30102心率血氧算法与PPG信号处理
    • 第四篇:软件I2C实现与OLED显示优化

    开发环境:ESP-IDF 5.3.2 + VS Code
    硬件平台:ESP32-S3 + MAX30102

    赞(0)
    未经允许不得转载:171主机测评 » ESP32-S3健康监测手环开发实战(三)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址