一、成品简介1.实物成品成品展示二、项目简介1.功能详解心率血氧体温监测系统基础版PPG 信号采集MAX30102光学传感器采集指尖光电容积脉搏波PPG信号使用 IIC 通信。滑动窗口滤波处理使用双通道滑动缓冲区动态窗口剔除启动不稳定采样数据对 PPG 原始信号进行降噪处理抑制运动干扰与环境光噪声。血氧饱和度与心率解算基于郎伯 - 比尔定律官方库对有效 PPG 信号进行特征提取计算血氧饱和度、实时心率。阈值自定义设置通过按键切换参数支持设置血氧下限、心率上下限阈值。OLED 显示0.96 寸 OLED 采集界面展示血氧、心率、电池电量、运行时长设置界面用于调整报警阈值数据周期性刷新。电池电压采集与电量监测用电阻分压用 ADC 采集锂电池电压平均值低通滤波估算剩余电量。声光异常报警当血氧饱和度、心率测量值超出用户设定阈值时蜂鸣器与 LED实现声光预警。锂电池充放电管理TP4056 充电模块实现锂电池充电。硬件框图流程图2.主要器件STM32F103C8T6 单片机MAX30102 光电血氧传感器0.96 寸 OLED 显示屏TP4056 锂电池充电管理模块3.7V 锂电池有源蜂鸣器 状态LED独立按键3.原理图设计4.PCB设计三、主要模块1.MAX30102 血氧传感器模块MAX30102光电传感器是款专门为健康监测设计的集成式光学传感器模块。由ADI公司推出内部集成红光660nm和红外光880nm双波长的LED光源、高灵敏度光电探测器、环境光抑制电路以及18位精度的模数转换器能够通过反射式或透射式的方式采集人体指尖或耳垂处的光电容积脉搏波PPG信号。内部集成的FIFO数据缓冲区可暂存32个样本减轻主控MCU的数据处理负担传统的脉搏测量方法主要分为三种一是从心电信号中提取二是从测量血压时计算压力传感器测到的波动来完成脉率测量三是光电容积法。前两种方法测量血氧时候会限制病人的活动如果长时间使用会增加病人生理和心理上的不舒适感。所以光电容积法脉搏测量成为了监护测量中最普遍的方法其使用方法简单、可靠性高、佩戴方便等。光电容积法的基本原理是利用人体组织在血管搏动时造成透光率的不同来进行脉搏和血氧饱和度测量的[5]。通过有选择性的发射特定波长红外光采集信号。当光束透过人体外周血管由于动脉搏动充血容积变化导致光的透光率发生变化此时由光电变换器接收经人体组织反射的光线转变为电信号并将其放大和输出。由于脉搏是随心脏的搏动而周期性变化的信号动脉血管容积也周期性变化因此光电变换器的电信号变化周期就是脉搏率。四、写作内容参考1.摘要参考内容随着我国人口老龄化的加剧、慢性病患病率持续上升以及全民健康意识的显著提升血氧饱和度作为人体健康的关键指标在家庭健康管理和疾病早期预警中的重要性日益凸显。然而当前市场上的消费级血氧监测设备普遍存在精度不足、续航能力弱、缺乏本地实时报警等缺陷难以满足居家养老、慢病管理及运动健康等场景下的实际需求。为此本设计提出一种基于嵌入式技术的便携式血氧饱和度监测仪旨在实现高精度、低功耗、本地化智能预警的健康监测解决方案。本设计包含两大部分第一部分系统硬件选型与电路设计。以STM32单片机作为核心集成MAX30102光电传感器实现血氧饱和度与心率的同步检测OLED显示屏实时显示参数血氧或心率异常时触发预警声光报警使用TP4056锂电池充电管理模块完成系统供电。第二部分系统软件功能设计。使用C语言在STM32平台上开发程序实现IIC通信驱动MAX30102、OLED界面刷新、按键阈值设置及报警逻辑控制。血氧与心率算法采用滑动窗口动态校准机制在资源受限的单片机上完成PPG信号滤波、特征提取与生理参数计算确保测量结果的稳定性与可靠性。系统支持用户自定义报警阈值并具备实时数据显示与异常即时响应能力。2.目录参考内容五、项目主要源码5.1.单片机源码5.1.1主程序//KEYKEY1-设置修改的阈值参数 KEY2 阈值 KEY3 阈值-- KEY4确定 //阈值分为血氧参数低在80-95 心率高参数120-150 uint8_t xue_low75;uint8_t heart_high145;//参数阈值设置 uint8_t mode;//界面 设置界面还是运行界面 设置界面0 uint8_t can_mode;//修改参数选择设置 0修改血氧下限 1修改心率上限 int main(void) { unsigned char i; uint8_t key0; DelayInit(); NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级2位响应优先级 DelayMs(200); I2C_Configuration(); //IIC初始化 OLED_Init(); //OLED初始化 OLED_CLS();//清屏 DelayMs(1000); OLED_CLS();//清屏 DelayMs(1000); IIC_init();//IIC初始化 Init_MAX30102();//MAX30102初始化 memset(STM32_RX1_BUF, 0, STM32_RX1BUFF_SIZE);//清除缓存 STM32_Rx1Counter 0; TIM2_Init(99,719); //定时器初始化定时1ms //Tout ((arr1)*(psc1))/Tclk ; //Tclk:定时器输入频率(单位MHZ) //Tout:定时器溢出时间(单位us) /* NEW 上面完成了NVIC Delay OLED MAX30102 */ /* LED BEEP uart DS18b20 KEY */ beep_init();led_init(); led_off();beep_off(); uart1_Init(115200); // DS18B20_Init(); KEY_Init(); Adc_Init(); DelayMs(200); beep_on();led_on(); DelayMs(200); beep_off();led_off(); //uart1_Init(115200); while(1) { ad_task(); shanshuo!shanshuo; // KeySettings(); // DisplayTime(); // displayTimeCunt(); // if(setn 0)//不在设置状态下读取相关数据 if(mode1) { for(i0;i2;i)OLED_ShowCN(i*16,4,i16,1);//测试显示中文心率 for(i0;i2;i)OLED_ShowCN(i*1648,4,i18,1);//测试显示中文血氧 // DisplayTemperature(); // GetSteps(); GetHeartRateSpO2(); } // UsartSendReceiveData(); DelayMs(10);time_10ms; //逻辑判断 if(mode0)//设置界面 第一行默认 电量和时间了以及 应该 {//下26 限27 上28 心16 率 17 血18 氧19 // OLED_ShowCN(016*1,1,65,1); // OLED_ShowChar(16*20,1,:,2,0); OLED_ShowCN(016*0,3,18,0);OLED_ShowCN(016*1,3,19,0);OLED_ShowCN(016*2,3,26,0);OLED_ShowCN(016*3,3,27,0);//血氧下限 OLED_ShowChar(016*4,3,:,2,0);OLED_ShowChar(016*48,3,xue_low/100,2,0);OLED_ShowChar(016*416,3,xue_low%100,2,0);//数值 OLED_ShowCN(016*0,5,16,0);OLED_ShowCN(016*1,5,17,0);OLED_ShowCN(016*2,5,28,0);OLED_ShowCN(016*3,5,27,0);//心率上限 OLED_ShowChar(016*4,5,:,2,0);OLED_ShowChar(016*48,5,heart_high/1000,2,0);OLED_ShowChar(016*416,5,heart_high/10%100,2,0);OLED_ShowChar(016*424,5,heart_high%100,2,0); } keyKEY_Scan(); if(key1){can_mode(can_mode1)%2;} if(key2){if(can_mode0){if(xue_low95)xue_low;}if(can_mode1){if(heart_high150)heart_highheart_high2;}} if(key3){if(can_mode0){if(xue_low80)xue_low--;}if(can_mode1){if(heart_high120)heart_highheart_high-2;}} if(key4){mode1;OLED_CLS();} key0; // hrAvg 心率 // spo2Avg 血氧 if(hrAvgheart_high)//心率高 {led_on();beep_on();DelayMs(200);led_off();beep_off();DelayMs(200);led_on();beep_on();DelayMs(200);led_off();beep_off();} else{led_off();beep_off();} if(spo2Avgxue_lowspo2Avg70)//血氧 {led_on();beep_on();DelayMs(200);led_off();beep_off();DelayMs(200);led_on();beep_on();DelayMs(200);led_off();beep_off();} else {led_off();beep_off();} } }5.1.2MAX30102程序#include max30102_read.h #include max30102.h #include algorithm.h #include myiic.h #define MAX_BRIGHTNESS 255 uint32_t aun_ir_buffer[150]; //infrared LED sensor data uint32_t aun_red_buffer[150]; //red LED sensor data int32_t n_ir_buffer_length; //data length int32_t n_spo2; //SPO2 value int8_t ch_spo2_valid; //indicator to show if the SPO2 calculation is valid int32_t n_heart_rate; //heart rate value int8_t ch_hr_valid; //indicator to show if the heart rate calculation is valid uint8_t uch_dummy; int32_t hr_buf[16]; int32_t hrSum; extern int32_t hrAvg; int32_t spo2_buf[16]; int32_t spo2Sum; extern int32_t spo2Avg; int32_t spo2BuffFilled; int32_t hrBuffFilled; int32_t hrValidCnt 0; int32_t spo2ValidCnt 0; int32_t hrThrowOutSamp 0; int32_t spo2ThrowOutSamp 0; int32_t spo2Timeout 0; int32_t hrTimeout 0; uint32_t un_min, un_max,un_prev_data; uint32_t un_brightness; //variables to calculate the on-board LED brightness that reflects the heartbeats void Init_MAX30102(void) { int32_t i; un_brightness 0; un_min 0x3FFFF; un_max 0; bsp_InitI2C();//IIC初始化 maxim_max30102_reset(); //resets the MAX30102 maxim_max30102_read_reg(REG_INTR_STATUS_1, uch_dummy); //Reads/clears the interrupt status register maxim_max30102_init(); //initialize the MAX30102 n_ir_buffer_length 150; //buffer length of 150 stores 3 seconds of samples running at 50sps //read the first 150 samples, and determine the signal range for(i 0; i n_ir_buffer_length; i) { //while(KEY0 1); //wait until the interrupt pin asserts //read from MAX30102 FIFO #if (MAX_VERSION VERSION_1_) maxim_max30102_read_fifo((aun_ir_bufferi), (aun_red_bufferi));//read from MAX30102 FIFO #elif (MAX_VERSION VERSION_2_) maxim_max30102_read_fifo((aun_red_bufferi), (aun_ir_bufferi));//read from MAX30102 FIFO #endif if(un_min aun_red_buffer[i]) un_min aun_red_buffer[i]; //update signal min if(un_max aun_red_buffer[i]) un_max aun_red_buffer[i]; //update signal max } un_prev_data aun_red_buffer[i]; //calculate heart rate and SpO2 after first 150 samples (first 3 seconds of samples) maxim_heart_rate_and_oxygen_saturation(aun_ir_buffer, n_ir_buffer_length, aun_red_buffer, n_spo2, ch_spo2_valid, n_heart_rate, ch_hr_valid); } void ReadHeartRateSpO2(void) { int32_t i; float f_temp; static u8 COUNT8; i 0; un_min 0x3FFFF; un_max 0; //dumping the first 50 sets of samples in the memory and shift the last 100 sets of samples to the top for(i 50; i 150; i) { aun_red_buffer[i - 50] aun_red_buffer[i]; aun_ir_buffer[i - 50] aun_ir_buffer[i]; //update the signal min and max if(un_min aun_red_buffer[i]) un_min aun_red_buffer[i]; if(un_max aun_red_buffer[i]) un_max aun_red_buffer[i]; } //take 50 sets of samples before calculating the heart rate. for(i 100; i 150; i) { un_prev_data aun_red_buffer[i - 1]; #if (MAX_VERSION VERSION_1_) maxim_max30102_read_fifo((aun_ir_bufferi), (aun_red_bufferi));//read from MAX30102 FIFO #elif (MAX_VERSION VERSION_2_) maxim_max30102_read_fifo((aun_red_bufferi), (aun_ir_bufferi));//read from MAX30102 FIFO #endif //calculate the brightness of the LED if(aun_red_buffer[i] un_prev_data) { f_temp aun_red_buffer[i] - un_prev_data; f_temp / (un_max - un_min); f_temp * MAX_BRIGHTNESS; f_temp un_brightness - f_temp; if(f_temp 0) un_brightness 0; else un_brightness (int)f_temp; } else { f_temp un_prev_data - aun_red_buffer[i]; f_temp / (un_max - un_min); f_temp * MAX_BRIGHTNESS; un_brightness (int)f_temp; if(un_brightness MAX_BRIGHTNESS) un_brightness MAX_BRIGHTNESS; } } maxim_heart_rate_and_oxygen_saturation(aun_ir_buffer, n_ir_buffer_length, aun_red_buffer, n_spo2, ch_spo2_valid, n_heart_rate, ch_hr_valid); if(COUNT 8) { COUNT 0; if ((ch_hr_valid 1) (n_heart_rate 150) (n_heart_rate 60)) { hrTimeout 0; // Throw out up to 1 out of every 5 valid samples if wacky if (hrValidCnt 4) { hrThrowOutSamp 1; hrValidCnt 0; for (i 12; i 16; i) { if (n_heart_rate hr_buf[i] 10) { hrThrowOutSamp 0; hrValidCnt 4; } } } else { hrValidCnt hrValidCnt 1; } if (hrThrowOutSamp 0) { // Shift New Sample into buffer for(i 0; i 15; i) { hr_buf[i] hr_buf[i 1]; } hr_buf[15] n_heart_rate; // Update buffer fill value if (hrBuffFilled 16) { hrBuffFilled hrBuffFilled 1; } // Take moving average hrSum 0; if (hrBuffFilled 2) { //hrAvg 0; } else if (hrBuffFilled 4) { for(i 14; i 16; i) { hrSum hrSum hr_buf[i]; } hrAvg hrSum 1; } else if (hrBuffFilled 8) { for(i 12; i 16; i) { hrSum hrSum hr_buf[i]; } hrAvg hrSum 2; } else if (hrBuffFilled 16) { for(i 8; i 16; i) { hrSum hrSum hr_buf[i]; } hrAvg hrSum 3; } else { for(i 0; i 16; i) { hrSum hrSum hr_buf[i]; } hrAvg hrSum 4; } } hrThrowOutSamp 0; } else { hrValidCnt 0; if (hrTimeout 8) { hrAvg 0; hrBuffFilled 0; } else { hrTimeout; } } if ((ch_spo2_valid 1) (n_spo2 80)) { spo2Timeout 0; // Throw out up to 1 out of every 5 valid samples if wacky if (spo2ValidCnt 4) { spo2ThrowOutSamp 1; spo2ValidCnt 0; for (i 12; i 16; i) { if (n_spo2 spo2_buf[i] - 10) { spo2ThrowOutSamp 0; spo2ValidCnt 4; } } } else { spo2ValidCnt spo2ValidCnt 1; } if (spo2ThrowOutSamp 0) { // Shift New Sample into buffer for(i 0; i 15; i) { spo2_buf[i] spo2_buf[i 1]; } spo2_buf[15] n_spo2; // Update buffer fill value if (spo2BuffFilled 16) { spo2BuffFilled spo2BuffFilled 1; } // Take moving average spo2Sum 0; if (spo2BuffFilled 2) { //spo2Avg 0; } else if (spo2BuffFilled 4) { for(i 14; i 16; i) { spo2Sum spo2Sum spo2_buf[i]; } spo2Avg spo2Sum 1; } else if (spo2BuffFilled 8) { for(i 12; i 16; i) { spo2Sum spo2Sum spo2_buf[i]; } spo2Avg spo2Sum 2; } else if (spo2BuffFilled 16) { for(i 8; i 16; i) { spo2Sum spo2Sum spo2_buf[i]; } spo2Avg spo2Sum 3; } else { for(i 0; i 16; i) { spo2Sum spo2Sum spo2_buf[i]; } spo2Avg spo2Sum 4; } } spo2ThrowOutSamp 0; } else { spo2ValidCnt 0; if (spo2Timeout 8) { spo2Avg 0; spo2BuffFilled 0; } else { spo2Timeout; } } } }5.1.3MAX30102IIC驱动#include max30102.h #include myiic.h #define max30102_WR_address 0xAE bool maxim_max30102_write_reg(uint8_t uch_addr, uint8_t uch_data) { /* 第1步发起I2C总线启动信号 */ i2c_Start(); /* 第2步发起控制字节高7bit是地址bit0是读写控制位0表示写1表示读 */ i2c_SendByte(max30102_WR_address | I2C_WR); /* 此处是写指令 */ /* 第3步发送ACK */ if (i2c_WaitAck() ! 0) { goto cmd_fail; /* EEPROM器件无应答 */ } /* 第4步发送字节地址 */ i2c_SendByte(uch_addr); if (i2c_WaitAck() ! 0) { goto cmd_fail; /* EEPROM器件无应答 */ } /* 第5步开始写入数据 */ i2c_SendByte(uch_data); /* 第6步发送ACK */ if (i2c_WaitAck() ! 0) { goto cmd_fail; /* EEPROM器件无应答 */ } /* 发送I2C总线停止信号 */ i2c_Stop(); return true; /* 执行成功 */ cmd_fail: /* 命令执行失败后切记发送停止信号避免影响I2C总线上其他设备 */ /* 发送I2C总线停止信号 */ i2c_Stop(); return false; } bool maxim_max30102_read_reg(uint8_t uch_addr, uint8_t *puch_data) /** * \brief Read a MAX30102 register * \par Details * This function reads a MAX30102 register * * \param[in] uch_addr - register address * \param[out] puch_data - pointer that stores the register data * * \retval true on success */ { /* 第1步发起I2C总线启动信号 */ i2c_Start(); /* 第2步发起控制字节高7bit是地址bit0是读写控制位0表示写1表示读 */ i2c_SendByte(max30102_WR_address | I2C_WR); /* 此处是写指令 */ /* 第3步发送ACK */ if (i2c_WaitAck() ! 0) { goto cmd_fail; /* EEPROM器件无应答 */ } /* 第4步发送字节地址 */ i2c_SendByte((uint8_t)uch_addr); if (i2c_WaitAck() ! 0) { goto cmd_fail; /* EEPROM器件无应答 */ } /* 第6步重新启动I2C总线。下面开始读取数据 */ i2c_Start(); /* 第7步发起控制字节高7bit是地址bit0是读写控制位0表示写1表示读 */ i2c_SendByte(max30102_WR_address | I2C_RD); /* 此处是读指令 */ /* 第8步发送ACK */ if (i2c_WaitAck() ! 0) { goto cmd_fail; /* EEPROM器件无应答 */ } /* 第9步读取数据 */ { *puch_data i2c_ReadByte(); /* 读1个字节 */ i2c_NAck(); /* 最后1个字节读完后CPU产生NACK信号(驱动SDA 1) */ } /* 发送I2C总线停止信号 */ i2c_Stop(); return true; /* 执行成功 返回data值 */ cmd_fail: /* 命令执行失败后切记发送停止信号避免影响I2C总线上其他设备 */ /* 发送I2C总线停止信号 */ i2c_Stop(); return false; } bool maxim_max30102_init(void) /** * \brief Initialize the MAX30102 * \par Details * This function initializes the MAX30102 * * \param None * * \retval true on success */ { if(!maxim_max30102_write_reg(REG_INTR_ENABLE_1, 0xc0)) // INTR setting return false; if(!maxim_max30102_write_reg(REG_INTR_ENABLE_2, 0x00)) return false; if(!maxim_max30102_write_reg(REG_FIFO_WR_PTR, 0x00)) //FIFO_WR_PTR[4:0] return false; if(!maxim_max30102_write_reg(REG_OVF_COUNTER, 0x00)) //OVF_COUNTER[4:0] return false; if(!maxim_max30102_write_reg(REG_FIFO_RD_PTR, 0x00)) //FIFO_RD_PTR[4:0] return false; if(!maxim_max30102_write_reg(REG_FIFO_CONFIG, 0x6f)) //sample avg 8, fifo rolloverfalse, fifo almost full 17 return false; if(!maxim_max30102_write_reg(REG_MODE_CONFIG, 0x03)) //0x02 for Red only, 0x03 for SpO2 mode 0x07 multimode LED return false; if(!maxim_max30102_write_reg(REG_SPO2_CONFIG, 0x2F)) // SPO2_ADC range 4096nA, SPO2 sample rate (400 Hz), LED pulseWidth (411uS) return false; if(!maxim_max30102_write_reg(REG_LED1_PA, 0x17)) //Choose value for ~ 4.5mA for LED1 return false; if(!maxim_max30102_write_reg(REG_LED2_PA, 0x17)) // Choose value for ~ 4.5mA for LED2 return false; if(!maxim_max30102_write_reg(REG_PILOT_PA, 0x7f)) // Choose value for ~ 25mA for Pilot LED return false; return true; } bool maxim_max30102_read_fifo(uint32_t *pun_red_led, uint32_t *pun_ir_led) /** * \brief Read a set of samples from the MAX30102 FIFO register * \par Details * This function reads a set of samples from the MAX30102 FIFO register * * \param[out] *pun_red_led - pointer that stores the red LED reading data * \param[out] *pun_ir_led - pointer that stores the IR LED reading data * * \retval true on success */ { uint32_t un_temp; uint8_t uch_temp; *pun_ir_led 0; *pun_red_led 0; maxim_max30102_read_reg(REG_INTR_STATUS_1, uch_temp); maxim_max30102_read_reg(REG_INTR_STATUS_2, uch_temp); /* 第1步发起I2C总线启动信号 */ i2c_Start(); /* 第2步发起控制字节高7bit是地址bit0是读写控制位0表示写1表示读 */ i2c_SendByte(max30102_WR_address | I2C_WR); /* 此处是写指令 */ /* 第3步发送ACK */ if (i2c_WaitAck() ! 0) { // printf(read fifo failed); goto cmd_fail; /* EEPROM器件无应答 */ } /* 第4步发送字节地址 */ i2c_SendByte((uint8_t)REG_FIFO_DATA); if (i2c_WaitAck() ! 0) { goto cmd_fail; /* EEPROM器件无应答 */ } /* 第6步重新启动I2C总线。下面开始读取数据 */ i2c_Start(); /* 第7步发起控制字节高7bit是地址bit0是读写控制位0表示写1表示读 */ i2c_SendByte(max30102_WR_address | I2C_RD); /* 此处是读指令 */ /* 第8步发送ACK */ if (i2c_WaitAck() ! 0) { goto cmd_fail; /* EEPROM器件无应答 */ } un_temp i2c_ReadByte(); i2c_Ack(); un_temp 16; *pun_red_led un_temp; un_temp i2c_ReadByte(); i2c_Ack(); un_temp 8; *pun_red_led un_temp; un_temp i2c_ReadByte(); i2c_Ack(); *pun_red_led un_temp; un_temp i2c_ReadByte(); i2c_Ack(); un_temp 16; *pun_ir_led un_temp; un_temp i2c_ReadByte(); i2c_Ack(); un_temp 8; *pun_ir_led un_temp; un_temp i2c_ReadByte(); i2c_Ack(); *pun_ir_led un_temp; *pun_red_led 0x03FFFF; //Mask MSB [23:18] *pun_ir_led 0x03FFFF; //Mask MSB [23:18] /* 发送I2C总线停止信号 */ i2c_Stop(); return true; cmd_fail: /* 命令执行失败后切记发送停止信号避免影响I2C总线上其他设备 */ /* 发送I2C总线停止信号 */ i2c_Stop(); return false; } bool maxim_max30102_reset() /** * \brief Reset the MAX30102 * \par Details * This function resets the MAX30102 * * \param None * * \retval true on success */ { if(!maxim_max30102_write_reg(REG_MODE_CONFIG, 0x40)) return false; else return true; }5.1.4MAX30102算法#include algorithm.h //uch_spo2_table is approximated as -45.060*ratioAverage* ratioAverage 30.354 *ratioAverage 94.845 ; const uint8_t uch_spo2_table[184] { 95, 95, 95, 96, 96, 96, 97, 97, 97, 97, 97, 98, 98, 98, 98, 98, 99, 99, 99, 99, 99, 99, 99, 99, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 99, 99, 99, 99, 99, 99, 99, 99, 98, 98, 98, 98, 98, 98, 97, 97, 97, 97, 96, 96, 96, 96, 95, 95, 95, 94, 94, 94, 93, 93, 93, 92, 92, 92, 91, 91, 90, 90, 89, 89, 89, 88, 88, 87, 87, 86, 86, 85, 85, 84, 84, 83, 82, 82, 81, 81, 80, 80, 79, 78, 78, 77, 76, 76, 75, 74, 74, 73, 72, 72, 71, 70, 69, 69, 68, 67, 66, 66, 65, 64, 63, 62, 62, 61, 60, 59, 58, 57, 56, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 31, 30, 29, 28, 27, 26, 25, 23, 22, 21, 20, 19, 17, 16, 15, 14, 12, 11, 10, 9, 7, 6, 5, 3, 2, 1 } ; void maxim_heart_rate_and_oxygen_saturation(uint32_t *pun_ir_buffer, int32_t n_ir_buffer_length, uint32_t *pun_red_buffer, int32_t *pn_spo2, int8_t *pch_spo2_valid, int32_t *pn_heart_rate, int8_t *pch_hr_valid) /** * \brief Calculate the heart rate and SpO2 level * \par Details * By detecting peaks of PPG cycle and corresponding AC/DC of red/infra-red signal, the an_ratio for the SPO2 is computed. * Since this algorithm is aiming for Arm M0/M3. formaula for SPO2 did not achieve the accuracy due to register overflow. * Thus, accurate SPO2 is precalculated and save longo uch_spo2_table[] per each an_ratio. * * \param[in] *pun_ir_buffer - IR sensor data buffer * \param[in] n_ir_buffer_length - IR sensor data buffer length * \param[in] *pun_red_buffer - Red sensor data buffer * \param[out] *pn_spo2 - Calculated SpO2 value * \param[out] *pch_spo2_valid - 1 if the calculated SpO2 value is valid * \param[out] *pn_heart_rate - Calculated heart rate value * \param[out] *pch_hr_valid - 1 if the calculated heart rate value is valid * * \retval None */ { uint32_t un_ir_mean ; int32_t k, n_i_ratio_count; int32_t i, n_exact_ir_valley_locs_count, n_middle_idx; int32_t n_th1, n_npks; int32_t an_ir_valley_locs[15] ; int32_t n_peak_interval_sum; int32_t n_y_ac, n_x_ac; int32_t n_spo2_calc; int32_t n_y_dc_max, n_x_dc_max; int32_t n_y_dc_max_idx, n_x_dc_max_idx; int32_t an_ratio[5], n_ratio_average; int32_t n_nume, n_denom ; // calculates DC mean and subtract DC from ir un_ir_mean 0; for (k 0 ; k n_ir_buffer_length ; k ) un_ir_mean pun_ir_buffer[k] ; un_ir_mean un_ir_mean / n_ir_buffer_length ; // remove DC and invert signal so that we can use peak detector as valley detector for (k 0 ; k n_ir_buffer_length ; k ) an_x[k] -1 * (pun_ir_buffer[k] - un_ir_mean) ; // 4 pt Moving Average for(k 0; k BUFFER_SIZE - MA4_SIZE; k) { an_x[k] ( an_x[k] an_x[k 1] an_x[k 2] an_x[k 3]) / (int)4; } // calculate threshold n_th1 0; for ( k 0 ; k BUFFER_SIZE ; k) { n_th1 an_x[k]; } n_th1 n_th1 / ( BUFFER_SIZE); if( n_th1 30) n_th1 30; // min allowed if( n_th1 60) n_th1 60; // max allowed for ( k 0 ; k 15; k) an_ir_valley_locs[k] 0; // since we flipped signal, we use peak detector as vSalley detector maxim_find_peaks( an_ir_valley_locs, n_npks, an_x, BUFFER_SIZE, n_th1, 4, 15 );//peak_height, peak_distance, max_num_peaks n_peak_interval_sum 0; if (n_npks 2) { for (k 1; k n_npks; k) n_peak_interval_sum (an_ir_valley_locs[k] - an_ir_valley_locs[k - 1] ) ; n_peak_interval_sum n_peak_interval_sum / (n_npks - 1); *pn_heart_rate (int32_t)( (40 * 60) / n_peak_interval_sum ); *pch_hr_valid 1; } else { *pn_heart_rate -999; // unable to calculate because # of peaks are too small *pch_hr_valid 0; } // load raw value again for SPO2 calculation : RED(y) and IR(X) for (k 0 ; k n_ir_buffer_length ; k ) { an_x[k] pun_ir_buffer[k] ; an_y[k] pun_red_buffer[k] ; } // find precise min near an_ir_valley_locs n_exact_ir_valley_locs_count n_npks; //using exact_ir_valley_locs , find ir-red DC andir-red AC for SPO2 calibration an_ratio //finding AC/DC maximum of raw n_ratio_average 0; n_i_ratio_count 0; for(k 0; k 5; k) an_ratio[k] 0; for (k 0; k n_exact_ir_valley_locs_count; k) { if (an_ir_valley_locs[k] BUFFER_SIZE ) { *pn_spo2 -999 ; // do not use SPO2 since valley loc is out of range *pch_spo2_valid 0; return; } } // find max between two valley locations // and use an_ratio betwen AC compoent of Ir Red and DC compoent of Ir Red for SPO2 for (k 0; k n_exact_ir_valley_locs_count - 1; k) { n_y_dc_max -16777216 ; n_x_dc_max -16777216; if (an_ir_valley_locs[k 1] - an_ir_valley_locs[k] 3) { for (i an_ir_valley_locs[k]; i an_ir_valley_locs[k 1]; i) { if (an_x[i] n_x_dc_max) { n_x_dc_max an_x[i]; n_x_dc_max_idx i; } if (an_y[i] n_y_dc_max) { n_y_dc_max an_y[i]; n_y_dc_max_idx i; } } n_y_ac (an_y[an_ir_valley_locs[k 1]] - an_y[an_ir_valley_locs[k] ] ) * (n_y_dc_max_idx - an_ir_valley_locs[k]); //red n_y_ac an_y[an_ir_valley_locs[k]] n_y_ac / (an_ir_valley_locs[k 1] - an_ir_valley_locs[k]) ; n_y_ac an_y[n_y_dc_max_idx] - n_y_ac; // subracting linear DC compoenents from raw n_x_ac (an_x[an_ir_valley_locs[k 1]] - an_x[an_ir_valley_locs[k] ] ) * (n_x_dc_max_idx - an_ir_valley_locs[k]); // ir n_x_ac an_x[an_ir_valley_locs[k]] n_x_ac / (an_ir_valley_locs[k 1] - an_ir_valley_locs[k]); n_x_ac an_x[n_y_dc_max_idx] - n_x_ac; // subracting linear DC compoenents from raw n_nume ( n_y_ac * n_x_dc_max) 7 ; //prepare X100 to preserve floating value n_denom ( n_x_ac * n_y_dc_max) 7; if (n_denom 0 n_i_ratio_count 5 n_nume ! 0) { an_ratio[n_i_ratio_count] (n_nume * 100) / n_denom ; //formular is ( n_y_ac *n_x_dc_max) / ( n_x_ac *n_y_dc_max) ; n_i_ratio_count; } } } // choose median value since PPG signal may varies from beat to beat maxim_sort_ascend(an_ratio, n_i_ratio_count); n_middle_idx n_i_ratio_count / 2; if (n_middle_idx 1) n_ratio_average ( an_ratio[n_middle_idx - 1] an_ratio[n_middle_idx]) / 2; // use median else n_ratio_average an_ratio[n_middle_idx ]; if( n_ratio_average 2 n_ratio_average 184) { n_spo2_calc uch_spo2_table[n_ratio_average] ; *pn_spo2 n_spo2_calc ; *pch_spo2_valid 1;// float_SPO2 -45.060*n_ratio_average* n_ratio_average/10000 30.354 *n_ratio_average/100 94.845 ; // for comparison with table } else { *pn_spo2 -999 ; // do not use SPO2 since signal an_ratio is out of range *pch_spo2_valid 0; } } void maxim_find_peaks( int32_t *pn_locs, int32_t *n_npks, int32_t *pn_x, int32_t n_size, int32_t n_min_height, int32_t n_min_distance, int32_t n_max_num ) /** * \brief Find peaks * \par Details * Find at most MAX_NUM peaks above MIN_HEIGHT separated by at least MIN_DISTANCE * * \retval None */ { maxim_peaks_above_min_height( pn_locs, n_npks, pn_x, n_size, n_min_height ); maxim_remove_close_peaks( pn_locs, n_npks, pn_x, n_min_distance ); *n_npks min( *n_npks, n_max_num ); } void maxim_peaks_above_min_height( int32_t *pn_locs, int32_t *n_npks, int32_t *pn_x, int32_t n_size, int32_t n_min_height ) /** * \brief Find peaks above n_min_height * \par Details * Find all peaks above MIN_HEIGHT * * \retval None */ { int32_t i 1, riseFound 0, holdOff1 0, holdOff2 0, holdOffThresh 4; *n_npks 0; while (i n_size - 1) { if (holdOff2 0) { if (pn_x[i] n_min_height pn_x[i] pn_x[i - 1]) // find left edge of potential peaks { riseFound 1; } if (riseFound 1) { if ((pn_x[i] n_min_height) (holdOff1 holdOffThresh)) // if false edge { riseFound 0; holdOff1 0; } else { if (holdOff1 holdOffThresh) { if ((pn_x[i] n_min_height) (pn_x[i - 1] n_min_height)) { if ((*n_npks) 15 ) { pn_locs[(*n_npks)] i; // peak is right edge } holdOff1 0; riseFound 0; holdOff2 8; } } else { holdOff1 holdOff1 1; } } } } else { holdOff2 holdOff2 - 1; } i; } } void maxim_remove_close_peaks(int32_t *pn_locs, int32_t *pn_npks, int32_t *pn_x, int32_t n_min_distance) /** * \brief Remove peaks * \par Details * Remove peaks separated by less than MIN_DISTANCE * * \retval None */ { int32_t i, j, n_old_npks, n_dist; /* Order peaks from large to small */ maxim_sort_indices_descend( pn_x, pn_locs, *pn_npks ); for ( i -1; i *pn_npks; i ) { n_old_npks *pn_npks; *pn_npks i 1; for ( j i 1; j n_old_npks; j ) { n_dist pn_locs[j] - ( i -1 ? -1 : pn_locs[i] ); // lag-zero peak of autocorr is at index -1 if ( n_dist n_min_distance || n_dist -n_min_distance ) pn_locs[(*pn_npks)] pn_locs[j]; } } // Resort indices int32_to ascending order maxim_sort_ascend( pn_locs, *pn_npks ); } void maxim_sort_ascend(int32_t *pn_x, int32_t n_size) /** * \brief Sort array * \par Details * Sort array in ascending order (insertion sort algorithm) * * \retval None */ { int32_t i, j, n_temp; for (i 1; i n_size; i) { n_temp pn_x[i]; for (j i; j 0 n_temp pn_x[j - 1]; j--) pn_x[j] pn_x[j - 1]; pn_x[j] n_temp; } } void maxim_sort_indices_descend( int32_t *pn_x, int32_t *pn_indx, int32_t n_size) /** * \brief Sort indices * \par Details * Sort indices according to descending order (insertion sort algorithm) * * \retval None */ { int32_t i, j, n_temp; for (i 1; i n_size; i) { n_temp pn_indx[i]; for (j i; j 0 pn_x[n_temp] pn_x[pn_indx[j - 1]]; j--) pn_indx[j] pn_indx[j - 1]; pn_indx[j] n_temp; } }六、工程主要内容工程的具体内容包括但不限于原理图、程序代码、流程图等等。完整工程、原理图、APP 源码我已经开源到 Gitee需要的同学可以评论区留言或者私信007--心率血氧体温监测系统(基础版)