本文为最终成果贴,将基于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);
}
}
}实现效果如下,由于传感器模块较小,焊接的排针不能让手指完全贴合传感器,因此最终的血氧值略微偏低,心率也不是很稳定
串口输出如下

工程代码如下
我要赚赏金
