感谢EEPW推出的“ST NUCLEO-WBA55CG无线MCU开发大作战”活动,之前分享了屏幕与温湿度传感器数据的链接:
【STM32WBA55CG开发板】便携式健康仪-TFT屏幕驱动【过程贴】 https://forum.eepw.com.cn/thread/388989/1
【STM32WBA55CG开发板】便携式健康仪-DHT11温湿度传感器驱动【过程贴】 https://forum.eepw.com.cn/thread/389137/1
本次来分享驱动血氧传感器MAX30102来获取心跳与血氧数据的过程贴。
MAX30102是一个集成的脉搏血氧饱和度和心率监测模块。它包括内部LED、光电探测器、光学元件和具有环境光抑制功能的低噪声电子设备。传感器内部通过一个1.8V电源和一个单独的5.0V电源为内部LED供电。利用标准I2C兼容接口进行通信,同时MAX30102传感器的封装上加了玻璃盖,极大地增强了芯片的抗干扰能力,使其测量效果可以达到更优。另外本模块可以通过软件断电,待机电流也可忽略不计,从而使电源导轨始终保持通电状态。
工作原理
MAX30102或任何光学脉搏血氧和心率传感器由一对高强度LED(红色 和红外LED,均为不同波长)和一个光电探测器组成。这些LED的波长分别为660nm和880nm。MAX30102的工作原理是将两种光都照射到手指或耳垂上并使用光电探测器测量反射光量。这种通过光检测脉冲的方法称为光电体积描记图。MAX30102的工作可分为两部分:心率测量和脉搏血氧饱和度(测量血液中的氧含量)。
心率测量:动脉血中的氧合血红蛋白(HbO2)具有吸收红外光的特性。血液越红(血红蛋白越高),吸收的红外光越多。当血液随着每次心跳泵入手指时,反射光的量会发生变化,从而在光电探测器的输出端产生变化的波形。当继续照射光并获取光电探测器读数时,很快就会开始获得心跳 (HR) 脉搏读数。
脉搏血氧测量:是基于血液中的血红蛋白对光的吸收特性。氧合血红蛋白吸收的红外光比红光多,而脱氧血红蛋白吸收的红光比红外光多。因此,血氧仪中的红光和红外线LED交替发出光线,光电二极管接收没有被吸收的光信号,并将其转化为电信号放大和输出。光电二极管接收到的红光和红外光的比值用于计算血液中含氧的百分比。根据动脉血流的脉动特性,可计算出脉搏速率和强度。
传感器是IIC接口,在硬件连接方面,配置如下:
参考网上的驱动分享,本次实验的驱动如下:
/** * ************************************************************************ * * @file MAX30102.c * @author zxr * @brief * * ************************************************************************ * @copyright Copyright (c) 2024 zxr * ************************************************************************ */ #include "MAX30102.h" #include "main.h" uint16_t fifo_red; //定义FIFO中的红光数据 uint16_t fifo_ir; //定义FIFO中的红外光数据 /** * ************************************************************************ * @brief 向MAX30102寄存器写入一个值 * * @param[in] addr 寄存器地址 * @param[in] data 传输数据 * * @return * ************************************************************************ */ uint8_t max30102_write_reg(uint8_t addr, uint8_t data) { HAL_I2C_Mem_Write(&hi2c1, MAX30102_Device_address, addr, 1, &data,1,HAL_MAX_DELAY); return 1; } /** * ************************************************************************ * @brief 读取MAX30102寄存器的一个值 * * @param[in] addr 寄存器地址 * * @return * ************************************************************************ */ uint8_t max30102_read_reg(uint8_t addr ) { uint8_t data=0; HAL_I2C_Mem_Read(&hi2c1, MAX30102_Device_address, addr, 1, &data, 1, HAL_MAX_DELAY); return data; } /** * ************************************************************************ * @brief MAX30102传感器复位 * * * @return * ************************************************************************ */ uint8_t Max30102_reset(void) { if(max30102_write_reg(REG_MODE_CONFIG, 0x40)) return 1; else return 0; } /** * ************************************************************************ * @brief MAX30102传感器模式配置 * * * ************************************************************************ */ void MAX30102_Config(void) { max30102_write_reg(REG_INTR_ENABLE_1,0xc0); //INTR setting max30102_write_reg(REG_INTR_ENABLE_2,0x00);// max30102_write_reg(REG_FIFO_WR_PTR,0x00);//FIFO_WR_PTR[4:0] max30102_write_reg(REG_OVF_COUNTER,0x00);//OVF_COUNTER[4:0] max30102_write_reg(REG_FIFO_RD_PTR,0x00);//FIFO_RD_PTR[4:0] max30102_write_reg(REG_FIFO_CONFIG,0x0f);//sample avg = 1, fifo rollover=false, fifo almost full = 17 max30102_write_reg(REG_MODE_CONFIG,0x03);//0x02 for Red only, 0x03 for SpO2 mode 0x07 multimode LED max30102_write_reg(REG_SPO2_CONFIG,0x27); // SPO2_ADC range = 4096nA, SPO2 sample rate (50 Hz), LED pulseWidth (400uS) max30102_write_reg(REG_LED1_PA,0x32);//Choose value for ~ 10mA for LED1 max30102_write_reg(REG_LED2_PA,0x32);// Choose value for ~ 10mA for LED2 max30102_write_reg(REG_PILOT_PA,0x7f);// Choose value for ~ 25mA for Pilot LED } /** * ************************************************************************ * @brief 读取FIFO寄存器的数据 * * * ************************************************************************ */ void max30102_read_fifo(void) { uint16_t un_temp; fifo_red=0; fifo_ir=0; uint8_t ach_i2c_data[6]; //read and clear status register max30102_read_reg(REG_INTR_STATUS_1); max30102_read_reg(REG_INTR_STATUS_2); ach_i2c_data[0]=REG_FIFO_DATA; HAL_I2C_Mem_Read(&hi2c1,MAX30102_Device_address,REG_FIFO_DATA,1,ach_i2c_data,6,HAL_MAX_DELAY); un_temp=ach_i2c_data[0]; un_temp<<=14; fifo_red+=un_temp; un_temp=ach_i2c_data[1]; un_temp<<=6; fifo_red+=un_temp; un_temp=ach_i2c_data[2]; un_temp>>=2; fifo_red+=un_temp; un_temp=ach_i2c_data[3]; un_temp<<=14; fifo_ir+=un_temp; un_temp=ach_i2c_data[4]; un_temp<<=6; fifo_ir+=un_temp; un_temp=ach_i2c_data[5]; un_temp>>=2; fifo_ir+=un_temp; if(fifo_ir<=10000) { fifo_ir=0; } if(fifo_red<=10000) { fifo_red=0; } }
血氧心率的计算:
/** * ************************************************************************ * @brief 更新血氧数据 * @note 从 MAX30102 的 FIFO 中读取红光和红外数据,并将它们存储到两个复数数组s1和s2中, * 这些数据随后可以用于进行傅里叶变换等后续处理 * * ************************************************************************ */ void blood_data_update(void) { //标志位被使能时 读取FIFO g_fft_index=0; while(g_fft_index < FFT_N) { while(max30102_read_reg(REG_INTR_STATUS_1)&0x40 ) { //读取FIFO max30102_read_fifo(); //read from MAX30102 FIFO2 //将数据写入fft输入并清除输出 if(g_fft_index < FFT_N) { //将数据写入fft输入并清除输出 s1[g_fft_index].real = fifo_red; s1[g_fft_index].imag= 0; s2[g_fft_index].real = fifo_ir; s2[g_fft_index].imag= 0; g_fft_index++; } } } } /** * ************************************************************************ * @brief 血液信息转换 * * * ************************************************************************ */ void blood_data_translate(void) { float n_denom; uint16_t i; //直流滤波 float dc_red =0; float dc_ir =0; float ac_red =0; float ac_ir =0; for (i=0 ; i<FFT_N ; i++ ) { dc_red += s1[i].real ; dc_ir += s2[i].real ; } dc_red =dc_red/FFT_N ; dc_ir =dc_ir/FFT_N ; for (i=0 ; i<FFT_N ; i++ ) { s1[i].real = s1[i].real - dc_red ; s2[i].real = s2[i].real - dc_ir ; } //移动平均滤波 for(i = 1;i < FFT_N-1;i++) { n_denom= ( s1[i-1].real + 2*s1[i].real + s1[i+1].real); s1[i].real= n_denom/4.00; n_denom= ( s2[i-1].real + 2*s2[i].real + s2[i+1].real); s2[i].real= n_denom/4.00; } //八点平均滤波 for(i = 0;i < FFT_N-8;i++) { n_denom= ( s1[i].real+s1[i+1].real+ s1[i+2].real+ s1[i+3].real+ s1[i+4].real+ s1[i+5].real+ s1[i+6].real+ s1[i+7].real); s1[i].real= n_denom/8.00; n_denom= ( s2[i].real+s2[i+1].real+ s2[i+2].real+ s2[i+3].real+ s2[i+4].real+ s2[i+5].real+ s2[i+6].real+ s2[i+7].real); s2[i].real= n_denom/8.00; } //开始变换显示 g_fft_index = 0; //快速傅里叶变换 FFT(s1); FFT(s2); for(i = 0;i < FFT_N;i++) { s1[i].real=sqrtf(s1[i].real*s1[i].real+s1[i].imag*s1[i].imag); s1[i].real=sqrtf(s2[i].real*s2[i].real+s2[i].imag*s2[i].imag); } //计算交流分量 for (i=1 ; i<FFT_N ; i++ ) { ac_red += s1[i].real ; ac_ir += s2[i].real ; } for(i = 0;i < 50;i++) { if(s1[i].real<=10) break; } //读取峰值点的横坐标 结果的物理意义为 int s1_max_index = find_max_num_index(s1, 60); int s2_max_index = find_max_num_index(s2, 60); //检查HbO2和Hb的变化频率是否一致 if(i>=45) { //心率计算 uint16_t Heart_Rate = 60.00 * SAMPLES_PER_SECOND * s1_max_index / FFT_N; heart = Heart_Rate; //血氧含量计算 float R = (ac_ir*dc_red)/(ac_red*dc_ir); float sp02_num = -45.060*R*R+ 30.354 *R + 94.845; SpO2 = sp02_num; //状态正常 } else //数据发生异常 { heart = 0; SpO2 = 0; } //结束变换显示 } /** * ************************************************************************ * @brief 心率血氧循环函数 * * * ************************************************************************ */ void blood_Loop(void) { //血液信息获取 blood_data_update(); //血液信息转换 blood_data_translate(); SpO2 = (SpO2 > 99.99) ? 99.99:SpO2; //printf("心率:%3d/min, 血氧:%2d%%\n\r", heart, (int)SpO2); }
实物:
未进行测量时,心率,血氧数据为0.
测量成功后,屏幕显示测量结果: