本文为最终成果贴,将基于LCD屏幕驱动工程实现脉搏血氧的显示
首先定义基本的IIC读写功能
static int i2c_write_reg(uint8_t reg, const uint8_t* data, int len) { uint8_t tx[1 + 16]; if (len > 16) return -1; tx[0] = reg; if (len) memcpy(&tx[1], data, len); mxc_i2c_req_t req = { .i2c = I2C_PORT, .addr = MAX30102_ADDR_7BIT, .tx_buf = tx, .tx_len = 1 + len, .rx_buf = NULL, .rx_len = 0, .restart = 0, .callback = NULL }; return MXC_I2C_MasterTransaction(&req); } static int i2c_read_reg(uint8_t reg, uint8_t* data, int len) { mxc_i2c_req_t req = { .i2c = I2C_PORT, .addr = MAX30102_ADDR_7BIT, .tx_buf = ®, .tx_len = 1, .rx_buf = data, .rx_len = len, .restart = 1, .callback = NULL }; return MXC_I2C_MasterTransaction(&req); }
接着定义MAX30102的设备操作函数,包括软件复位、清空FIFO缓冲区、数据读取
/* ===================== MAX30102 设备操作 ===================== */ static void max30102_soft_reset(void) { uint8_t v = 0x40; /* RESET=1 */ (void)i2c_write_reg(REG_MODE_CONFIG, &v, 1); MXC_Delay(MXC_DELAY_MSEC(10)); } static void max30102_clear_fifo(void) { uint8_t z = 0x00; (void)i2c_write_reg(REG_FIFO_WR_PTR, &z, 1); (void)i2c_write_reg(REG_OVF_COUNTER, &z, 1); (void)i2c_write_reg(REG_FIFO_RD_PTR, &z, 1); } static void max30102_config_spo2_50sps(void) { /* 1) 使能 FIFO Almost Full & PPG_RDY 中断(可选) */ uint8_t ien1 = 0xC0; /* bit7=A_FULL_EN=1, bit6=PPG_RDY_EN=1 */ (void)i2c_write_reg(REG_INT_ENABLE1, &ien1, 1); uint8_t ien2 = 0x00; (void)i2c_write_reg(REG_INT_ENABLE2, &ien2, 1); /* 2) FIFO:avg=1、rollover=0、almost_full=15 */ uint8_t fifo_cfg = 0x0F; (void)i2c_write_reg(REG_FIFO_CONFIG, &fifo_cfg, 1); /* 3) 模式:SpO2(0b011) */ uint8_t mode = 0x03; (void)i2c_write_reg(REG_MODE_CONFIG, &mode, 1); /* 4) SpO2配置:ADC Range=4096nA(01),SampleRate=50sps(001),PulseWidth=411us(11) */ uint8_t spo2 = 0x27; /* 0b0010_0111 */ (void)i2c_write_reg(REG_SPO2_CONFIG, &spo2, 1); /* 5) LED 电流:~10mA(0x32) */ uint8_t ired = 0x32, iir = 0x32; (void)i2c_write_reg(REG_LED1_PA, &ired, 1); /* RED */ (void)i2c_write_reg(REG_LED2_PA, &iir, 1); /* IR */ /* 6) 清 FIFO 指针 */ max30102_clear_fifo(); } /* 读取一对样本(RED/IR),返回 1=成功,0=无数据 */ static int max30102_read_one_pair(void) { uint8_t s1=0, s2=0; (void)i2c_read_reg(REG_INT_STATUS1, &s1, 1); (void)i2c_read_reg(REG_INT_STATUS2, &s2, 1); if ((s1 & 0x40) == 0) { /* PPG_RDY=bit6 */ return 0; /* 暂无新样本 */ } /* 读取 6 字节:RED[23:0], IR[23:0](高 2 位无效) */ uint8_t d[6]; if (i2c_read_reg(REG_FIFO_DATA, d, 6) != 0) return 0; uint32_t red = ((uint32_t)d[0] << 16) | ((uint32_t)d[1] << 8) | d[2]; uint32_t ir = ((uint32_t)d[3] << 16) | ((uint32_t)d[4] << 8) | d[5]; red &= 0x03FFFF; /* 18-bit */ ir &= 0x03FFFF; /* 轻度阈值去噪(环境光/空读) */ if (ir <= 10000) ir = 0; if (red <= 10000) red = 0; g_fifo_red = (uint16_t)(red & 0xFFFF); g_fifo_ir = (uint16_t)(ir & 0xFFFF); return 1; }
血氧浓度的测量是较为容易的,心率的计算则需要用到一些算法和数据处理
为了能分析更多的心率信息,用环形缓冲区和滑动窗口实现更多数据的读取
/* ===================== 工具:滑动写入环形缓冲 ===================== */ static inline void ring_push(float red, float ir) { s_ring_red[s_ring_widx] = red; s_ring_ir [s_ring_widx] = ir; s_ring_widx = (s_ring_widx + 1) % RING_N; if (s_ring_count < RING_N) s_ring_count++; }
信号滤波:去除直流和带通滤波,其中带通滤波使用的是一阶高通滤波和一阶低通滤波串联
/* ===================== 滤波:去直流 + 带通(0.5~5 Hz) ===================== */ static void remove_dc_and_bandlimit(const float *x, float *y, int n) { /* 高通:x_hp = x - LPF(x, fc=0.5Hz);再低通:LPF(x_hp, fc=5Hz) */ const float dt = 1.0f / (float)SAMPLES_PER_SECOND; /* fc=0.5Hz -> RC=1/(2πfc) */ const float RC_hp = 1.0f / (2.0f * (float)M_PI * 0.5f); const float alpha_hp = dt / (RC_hp + dt); /* fc=5Hz -> RC=1/(2πfc) */ const float RC_lp = 1.0f / (2.0f * (float)M_PI * 5.0f); const float alpha_lp = dt / (RC_lp + dt); float lpf_dc = x[0]; float lpf_bp = 0.0f; for (int i = 0; i < n; ++i) { /* DC 低通(跟踪基线)*/ lpf_dc += alpha_hp * (x[i] - lpf_dc); float x_hp = x[i] - lpf_dc; /* 带通后再低通限制高频噪声 */ lpf_bp += alpha_lp * (x_hp - lpf_bp); y[i] = lpf_bp; } }
计算归一化自相关函数用于心率周期检测,其中x为信号数据、n为数据长度、energy为信号能量、L为滞后点数
/* ===================== 辅助:计算归一化自相关 R(L) —— 代替 C++ lambda ===================== */ static double sample_R(const float *x, int n, double energy, int L) { if (L < LAG_MIN || L > LAG_MAX) return -1e9; double acc = 0.0; int Nuse = n - L; for (int i = 0; i < Nuse; ++i) acc += (double)x[i] * (double)x[i + L]; return acc / energy; }
通过自相关分析估计心率,其流程为:搜索最佳滞后点 -> 二次插值细化 -> 转换为BPM
/* ===================== 自相关 + 二次插值估计主周期 ===================== */ static float estimate_hr_bpm_from_acf(const float *x, int n) { if (n < (LAG_MAX + 2)) return 0.0f; /* 归一化能量(简单 rms)用于自相关归一化 */ double energy = 0.0; for (int i = 0; i < n; ++i) energy += (double)x[i] * (double)x[i]; if (energy <= 1e-12) return 0.0f; /* 搜索最佳滞后 L */ double bestR = -1e9; int bestL = LAG_MIN; for (int L = LAG_MIN; L <= LAG_MAX; ++L) { double R = sample_R(x, n, energy, L); if (R > bestR) { bestR = R; bestL = L; } } /* 二次插值细化 —— 使用 L-1, L, L+1 */ double R_prev = sample_R(x, n, energy, bestL - 1); double R_curr = sample_R(x, n, energy, bestL); double R_next = sample_R(x, n, energy, bestL + 1); double delta = 0.0; if (R_prev > -1e8 && R_next > -1e8) { double denom = (R_prev - 2.0 * R_curr + R_next); if (fabs(denom) > 1e-9) { delta = 0.5 * (R_prev - R_next) / denom; if (delta > 1.0) delta = 1.0; if (delta < -1.0) delta = -1.0; } } double Lf = (double)bestL + delta; if (Lf < (double)LAG_MIN || Lf > (double)LAG_MAX) return 0.0f; double freq_hz = (double)SAMPLES_PER_SECOND / Lf; double hr_bpm = 60.0 * freq_hz; if (hr_bpm < 30.0 || hr_bpm > 220.0) return 0.0f; return (float)hr_bpm; }
基于比率比值法(SpO2线性映射)估计血氧饱和度
/* ===================== SpO₂ 估计(Ratio-of-Ratios) ===================== */ static float estimate_spo2_percent(const float *red, const float *ir, int n) { if (n < 32) return 0.0f; /* DC:滑动平均;AC:RMS */ double sum_red = 0.0, sum_ir = 0.0; for (int i = 0; i < n; ++i) { sum_red += red[i]; sum_ir += ir[i]; } double dc_red = sum_red / n; double dc_ir = sum_ir / n; double ac2_red = 0.0, ac2_ir = 0.0; for (int i = 0; i < n; ++i) { double xr = (double)red[i] - dc_red; double xi = (double)ir [i] - dc_ir; ac2_red += xr * xr; ac2_ir += xi * xi; } double rms_red = sqrt(ac2_red / n); double rms_ir = sqrt(ac2_ir / n); /* 信号质量保护 */ if (dc_red <= 1.0 || dc_ir <= 1.0) return 0.0f; if (rms_red/dc_red < 0.002 || rms_ir/dc_ir < 0.002) return 0.0f; double R = (rms_red / dc_red) / (rms_ir / dc_ir); double est = 104.0 - 17.0 * R; /* 简单线性近似 */ if (est > 100.0) est = 100.0; if (est < 0.0) est = 0.0; return (float)est; }
主流程函数中,采集一窗口数据(50个样本)并填充环形缓冲区
static void acquire_window_and_fill_ring(void) { int got = 0; while (got < WINDOW_SAMPLES) { if (!max30102_read_one_pair()) { MXC_Delay(MXC_DELAY_MSEC(2)); continue; } /* 写入环形缓冲(原始 16-bit 转 float) */ ring_push((float)g_fifo_red, (float)g_fifo_ir); got++; } }
从环形缓冲区计算心率和血氧指标,其流程为:数据复制 -> 信号滤波 -> 心率估计 -> 血氧估计 -> 有效性检测
static void compute_metrics_from_ring(void) { if (s_ring_count < RING_N/2) { /* 先累积至少 2 秒再输出有效值 */ g_hr = 0; g_spo2 = 0.0f; return; } /* 将环形缓冲复制成线性数组(时间顺序) */ float red[RING_N], ir[RING_N]; int n = s_ring_count; int start = (s_ring_widx - n + RING_N) % RING_N; for (int i = 0; i < n; ++i) { int idx = (start + i) % RING_N; red[i] = s_ring_red[idx]; ir [i] = s_ring_ir [idx]; } /* 仅用 IR 做心率估计(更稳健);RED/IR 用于 SpO₂ */ float ir_bp[RING_N]; remove_dc_and_bandlimit(ir, ir_bp, n); /* 自相关 + 二次插值估计心率 */ float hr_bpm = estimate_hr_bpm_from_acf(ir_bp, n); /* SpO₂ 估计(RMS/均值比值) */ float spo2 = estimate_spo2_percent(red, ir, n); /* 最终有效性校验 */ if (hr_bpm < 30.0f || hr_bpm > 220.0f) hr_bpm = 0.0f; if (spo2 < 50.0f || spo2 > 100.0f) spo2 = 0.0f; g_hr = (int)(hr_bpm + 0.5f); g_spo2 = spo2; }
将上述数据采集和指标计算汇总成一个函数,便于后续调用
static void blood_Loop(void) { acquire_window_and_fill_ring(); /* 约采 1 秒(50 点),写入滑动缓冲 */ compute_metrics_from_ring(); /* 基于最近 4 秒数据输出一次结果 */ }
最后定义对外API函数,便于用户调用
int sensor_init(void) { if (MXC_I2C_Init(I2C_PORT, 1, 0) != 0) return -1; MXC_I2C_SetFrequency(I2C_PORT, 400000); max30102_soft_reset(); max30102_config_spo2_50sps(); /* 保持 50sps/411us 配置不变 */ uint8_t part = 0; (void)i2c_read_reg(REG_PART_ID, &part, 1); if (part != 0x15) { printf("WARN: MAX30102 PartID=0x%02X (expect 0x15)\n", part); } g_hr = 0; g_spo2 = 0.0f; s_ring_widx = 0; s_ring_count = 0; memset(s_ring_red, 0, sizeof(s_ring_red)); memset(s_ring_ir, 0, sizeof(s_ring_ir)); return 0; } void sensor_run_once(void) { blood_Loop(); /* 约采 1 秒并完成一轮计算 */ } int sensor_get_hr(void) { return g_hr; } float sensor_get_spo2(void) { return g_spo2; } /* ===================== 可选:P0_19 下拉沿中断(演示三参 IntConfig) ===================== */ static void max30102_int_cb(void *cbdata) { (void)cbdata; /* 算法仍采用轮询 */ } void GPIO0_IRQHandler(void) { MXC_GPIO_Handler(MXC_GPIO0); } void sensor_int_init(void) { mxc_gpio_cfg_t cfg = { .port = MXC_GPIO0, .mask = (1u << 19), /* P0_19 */ .func = MXC_GPIO_FUNC_IN, .pad = MXC_GPIO_PAD_NONE, .vssel = MXC_GPIO_VSSEL_VDDIO }; MXC_GPIO_Config(&cfg); MXC_GPIO_RegisterCallback(&cfg, max30102_int_cb, cfg.mask); MXC_GPIO_IntConfig(&cfg, MXC_GPIO_INT_FALLING); MXC_GPIO_EnableInt(MXC_GPIO0, (1u << 19)); NVIC_EnableIRQ(GPIO0_IRQn); }
观察脉搏血氧计传感器模块,可以看到共有5个引脚需要与评估板连接,其中INT引脚为中断引脚,下降沿触发,用于传感器数据就绪通知
在主函数中,初始化LCD和脉搏血氧计传感器,随后在屏幕上和串口输出当前血氧和心率值,如果手指离开传感器,则屏幕上两个参数均显示为0,串口输出相关警告
#include <stdio.h> #include "mxc_device.h" #include "mxc_delay.h" #include "board.h" #include "lcd_init.h" #include "lcd.h" #include "sensor.h" int main(void) { printf("\nMAX78000FTHR + MAX30102 SpO2/HR demo (50 sps, window=50 ~1Hz)\n"); // 初始化 LCD LCD_Init(); LCD_Fill(0, 0, LCD_WIDTH, LCD_HEIGHT, BLACK); if (sensor_init() != 0) { printf("sensor_init failed.\n"); while (1) { } } while (1) { /* 采满一窗并完成一次计算(~1 秒) */ sensor_run_once(); int hr = sensor_get_hr(); float spo2 = sensor_get_spo2(); uint8_t buffer[32] = {0}; sprintf((char *)buffer, "MAXREFDES117#"); LCD_ShowString(5, 10, (uint8_t *)buffer, WHITE, BLACK, 16, 0); sprintf((char *)buffer, "Oximeter"); LCD_ShowString(5, 30, (uint8_t *)buffer, WHITE, BLACK, 16, 0); if (hr > 0 && spo2 > 0.0f) { printf("SpO2: %5.2f %% HR: %3d bpm\n", spo2, hr); sprintf((char *)buffer, "SpO2: %5.2f %% ", spo2); LCD_ShowString(5, 60, (uint8_t *)buffer, WHITE, BLACK, 16, 0); sprintf((char *)buffer, "HR: %3d bpm ", hr); LCD_ShowString(5, 80, (uint8_t *)buffer, WHITE, BLACK, 16, 0); } else { printf("SpO2/HR invalid (no finger or weak PPG)\n"); sprintf((char *)buffer, "SpO2: 0 "); LCD_ShowString(5, 60, (uint8_t *)buffer, WHITE, BLACK, 16, 0); sprintf((char *)buffer, "HR: 0 "); LCD_ShowString(5, 80, (uint8_t *)buffer, WHITE, BLACK, 16, 0); } } }
实现效果如下,由于传感器模块较小,焊接的排针不能让手指完全贴合传感器,因此最终的血氧值略微偏低,心率也不是很稳定
串口输出如下
工程代码如下