一句简单的开头
模块简介
MAX30102是一个集成的脉搏血氧仪和心率监测仪生物传感器的模块。
它集成了一个红光LED和一个红外光LED、光电检测器、光器件,以及带环境光抑制的低噪声电子电路。
MAX30102采用一个1.8V电源和一个独立的5.0V用于内部LED的电源,应用于可穿戴设备进行心率和血氧采集检测,佩戴于手指点耳垂和手腕处。
标准的I2C兼容的通信接口可以将采集到的数值传输给Arduino、KL25Z、STM32、STC51等单片机进行心率和血氧计算。
此外,该芯片还可以通过软件关断模块,待机电流接近为零,实现电源始终维持供电状态。
主要参数:
产品名称 | MAX30102 心率模块 |
LED峰值波长器 | 660nm/880nm |
LED供电电压 | 3.3 ~ 5V |
检测信号类型 | 光反射信号(PPG) |
输出信号接口 | I2C接口 |
通信接口电压 | 1.8 ~ 3.3V ~ 5V(可选) |
产品尺寸:

工作原理
光电容积法的基本原理是利用人体组织在血管搏动时造成透光率不同来进行脉搏和血氧饱和度测量的。光源一般采用对动脉血中氧合血红蛋白(Hb02)和血红蛋自(Hb)有选择性的特定波长的发光二极管(一般选用660nm附近的红光和900nm附近的红外光)。当光束透过人体外周血管,由于动脉搏动充血容积变化导致这束光的透过光率发生改变,此时由光电变换器接收人体组织反射的光线,转变为电信号并将其放大和输出。由于脉搏是随心脏的搏动而周期性变化的信号,动脉血管容积也周期性变化,因此光电变换器的电信号变化周期就是脉搏率。同时根据血氧饱和度的定义,计算如下:
透光率转化为电信号:动脉搏动充血容积变化导致这束光的透光率发送改变,此时由光电变换接收经人体组织反射光线,转变为电信号并将其放大输出。

原理图及引脚说明


VIN | 电源输入 1.6V-5.5V |
3位焊盘 | 选择总线的上拉电平,取决于引脚主控电压可选1.8v或者3.3v |
SDA | IIC-SDA |
SCL | IIC-SCL |
GND | 地 |
INT | INT 低电平有效中断(漏极开路)MAX30102 的中断引脚 |
IRD | IR_DRV IR LED阴极和LED驱动器连接点 一般不接 |
RD | R_DRV 红色LED阴极和LED驱动器连接点 一般不接 |
芯片内部有3.3V-5.0V的LED电源和1.8V的逻辑电源,所以模块带有两路稳压电路,将5V电源分别转化为3.3V和1.8V;由于LED驱动电源的供电范围为3.3V-5.0V,3.3V稳压电路可省去。
由于MAX30102的逻辑电路的IIC通信电平为1.8V,这与我们常用的STM32单片机的引脚电平不匹配。
这里有个解决方法,因为MAX30102的SDA、SCL、INT引脚为开漏,如图

所以可以将模块上的R1、R2、R3电阻去掉,对于51单片机来说,在SDA、SCL、INT引脚上分别加一个4.7-10k电阻上拉至5V;对于STM32单片机,只需要将相应的控制引脚配置为上拉模式即可。
STM32软件驱动
官方驱动
IIC通信代码
char ch_i2c_data;
ch_i2c_data=uch_addr;
if(i2c.write(I2C_WRITE_ADDR, &ch_i2c_data, 1, true)!=0)
return false;
if(i2c.read(I2C_READ_ADDR, &ch_i2c_data, 1, false)==0)
{
*puch_data=(uint8_t) ch_i2c_data;
return true;
}
else
return false;
}
bool maxim_max30102_init()
/
- 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,0x0f)) //sample avg = 1, fifo rollover=false, 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,0x27)) // SPO2_ADC range = 4096nA, SPO2 sample rate (100 Hz), LED pulseWidth (400uS)
return false;
if(!maxim_max30102_write_reg(REG_LED1_PA,0x24)) //Choose value for ~ 7mA for LED1
return false;
if(!maxim_max30102_write_reg(REG_LED2_PA,0x24)) // Choose value for ~ 7mA 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;
unsigned char uch_temp;
*pun_red_led=0;
*pun_ir_led=0;
char ach_i2c_data[6];
//read and clear status register
maxim_max30102_read_reg(REG_INTR_STATUS_1, &uch_temp);
maxim_max30102_read_reg(REG_INTR_STATUS_2, &uch_temp);
ach_i2c_data[0]=REG_FIFO_DATA;
if(i2c.write(I2C_WRITE_ADDR, ach_i2c_data, 1, true)!=0)
return false;
if(i2c.read(I2C_READ_ADDR, ach_i2c_data, 6, false)!=0)
{
return false;
}
un_temp=(unsigned char) ach_i2c_data[0];
un_temp<<=16;
*pun_red_led+=un_temp;
un_temp=(unsigned char) ach_i2c_data[1];
un_temp<<=8;
*pun_red_led+=un_temp;
un_temp=(unsigned char) ach_i2c_data[2];
*pun_red_led+=un_temp;
un_temp=(unsigned char) ach_i2c_data[3];
un_temp<<=16;
*pun_ir_led+=un_temp;
un_temp=(unsigned char) ach_i2c_data[4];
un_temp<<=8;
*pun_ir_led+=un_temp;
un_temp=(unsigned char) ach_i2c_data[5];
*pun_ir_led+=un_temp;
*pun_red_led&=0x03FFFF; //Mask MSB [23:18]
*pun_ir_led&=0x03FFFF; //Mask MSB [23:18]
return true;
}
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;
}
#include "algorithm.h"
#include "mbed.h"
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 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 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 ,un_only_once ;
int32_t k ,n_i_ratio_count;
int32_t i, s, m, n_exact_ir_valley_locs_count ,n_middle_idx;
int32_t n_th1, n_npks,n_c_min;
int32_t an_ir_valley_locs[15] ;
int32_t an_exact_ir_valley_locs[15] ;
int32_t an_dx_peak_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 ;
// remove DC of ir signal
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 ;
for (k=0 ; k<n_ir_buffer_length ; k++ ) an_x[k] = pun_ir_buffer[k] - un_ir_mean ;
// 4 pt Moving Average
for(k=0; k< BUFFER_SIZE-MA4_SIZE; k++){
n_denom= ( an_x[k]+an_x[k+1]+ an_x[k+2]+ an_x[k+3]);
an_x[k]= n_denom/(int32_t)4;
}
// get difference of smoothed IR signal
for( k=0; k<BUFFER_SIZE-MA4_SIZE-1; k++)
an_dx[k]= (an_x[k+1]- an_x[k]);
// 2-pt Moving Average to an_dx
for(k=0; k< BUFFER_SIZE-MA4_SIZE-2; k++){
an_dx[k] = ( an_dx[k]+an_dx[k+1])/2 ;
}
// hamming window
// flip wave form so that we can detect valley with peak detector
for ( i=0 ; i<BUFFER_SIZE-HAMMING_SIZE-MA4_SIZE-2 ;i++){
s= 0;
for( k=i; k<i+ HAMMING_SIZE ;k++){
s -= an_dx[k] *auw_hamm[k-i] ;
}
an_dx[i]= s/ (int32_t)1146; // divide by sum of auw_hamm
}
n_th1=0; // threshold calculation
for ( k=0 ; k<BUFFER_SIZE-HAMMING_SIZE ;k++){
n_th1 += ((an_dx[k]>0)? an_dx[k] : ((int32_t)0-an_dx[k])) ;
}
n_th1= n_th1/ ( BUFFER_SIZE-HAMMING_SIZE);
// peak location is acutally index for sharpest location of raw signal since we flipped the signal
maxim_find_peaks( an_dx_peak_locs, &n_npks, an_dx, BUFFER_SIZE-HAMMING_SIZE, n_th1, 8, 5 );//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_dx_peak_locs[k]-an_dx_peak_locs[k -1]);
n_peak_interval_sum=n_peak_interval_sum/(n_npks-1);
*pn_heart_rate=(int32_t)(6000/n_peak_interval_sum);// beats per minutes
*pch_hr_valid = 1;
}
else {
*pn_heart_rate = -999;
*pch_hr_valid = 0;
}
for ( k=0 ; k<n_npks ;k++)
an_ir_valley_locs[k]=an_dx_peak_locs[k]+HAMMING_SIZE/2;
// raw value : RED(=y) and IR(=X)
// we need to assess DC and AC value of ir and red PPG.
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 =0;
for(k=0 ; k<n_npks ;k++){
un_only_once =1;
m=an_ir_valley_locs[k];
n_c_min= 16777216;//2^24;
if (m+5 < BUFFER_SIZE-HAMMING_SIZE && m-5 >0){
for(i= m-5;i<m+5; i++)
if (an_x[i]<n_c_min){
if (un_only_once >0){
un_only_once =0;
}
n_c_min= an_x[i] ;
an_exact_ir_valley_locs[k]=i;
}
if (un_only_once ==0)
n_exact_ir_valley_locs_count ++ ;
}
}
if (n_exact_ir_valley_locs_count <2 ){
*pn_spo2 = -999 ; // do not use SPO2 since signal ratio is out of range
*pch_spo2_valid = 0;
return;
}
// 4 pt MA
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])/(int32_t)4;
an_y[k]=( an_y[k]+an_y[k+1]+ an_y[k+2]+ an_y[k+3])/(int32_t)4;
}
//using an_exact_ir_valley_locs , find ir-red DC andir-red AC for SPO2 calibration ratio
//finding AC/DC maximum of raw ir * red between two valley locations
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_exact_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 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_exact_ir_valley_locs[k+1]-an_exact_ir_valley_locs[k] >10){
for (i=an_exact_ir_valley_locs[k]; i< an_exact_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_exact_ir_valley_locs[k+1]] - an_y[an_exact_ir_valley_locs[k] ] )*(n_y_dc_max_idx -an_exact_ir_valley_locs[k]); //red
n_y_ac= an_y[an_exact_ir_valley_locs[k]] + n_y_ac/ (an_exact_ir_valley_locs[k+1] - an_exact_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_exact_ir_valley_locs[k+1]] - an_x[an_exact_ir_valley_locs[k] ] )*(n_x_dc_max_idx -an_exact_ir_valley_locs[k]); // ir
n_x_ac= an_x[an_exact_ir_valley_locs[k]] + n_x_ac/ (an_exact_ir_valley_locs[k+1] - an_exact_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_nume100)/n_denom ; //formular is ( n_y_ac n_x_dc_max) / ( n_x_ac *n_y_dc_max) ;
n_i_ratio_count++;
}
}
}
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.060n_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 ratio is out of range
*pch_spo2_valid = 0;
}
}
void maxim_find_peaks(int32_t pn_locs, int32_t pn_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, pn_npks, pn_x, n_size, n_min_height );
maxim_remove_close_peaks( pn_locs, pn_npks, pn_x, n_min_distance );
pn_npks = min( pn_npks, n_max_num );
}
void maxim_peaks_above_min_height(int32_t pn_locs, int32_t pn_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, n_width;
*pn_npks = 0;
while (i < n_size-1){
if (pn_x[i] > n_min_height && pn_x[i] > pn_x[i-1]){ // find left edge of potential peaks
n_width = 1;
while (i+n_width < n_size && pn_x[i] == pn_x[i+n_width]) // find flat peaks
n_width++;
if (pn_x[i] > pn_x[i+n_width] && (*pn_npks) < 15 ){ // find right edge of peaks
pn_locs[(*pn_npks)++] = i;
// for flat peaks, peak location is left edge
i += n_width+1;
}
else
i += n_width;
}
else
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 longo 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;
}
}
#include "stm32f103c8t6.h"
#include "mbed.h"
#include "algorithm.h"
#include "MAX30102.h"
#define MAX_BRIGHTNESS 255
uint32_t aun_ir_buffer[500]; //IR LED sensor data
int32_t n_ir_buffer_length; //data length
uint32_t aun_red_buffer[500]; //Red LED sensor data
int32_t n_sp02; //SPO2 value
int8_t ch_spo2_valid; //indicator to show if the SP02 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;
Serial pc(SERIAL_TX, SERIAL_RX); //initializes the serial port, TX-PA2, RX-PA3
PwmOut pwmled(PB_3); //initializes the pwm output PB3 that connects to the LED
DigitalIn INT(PB_7); //pin PB7 connects to the interrupt output pin of the MAX30102
DigitalOut led(PC_13); //PC13 connects to the on board user LED
// the setup routine runs once when you press reset:
int main() {
uint32_t un_min, un_max, un_prev_data; //variables to calculate the on-board LED brightness that reflects the heartbeats
int i;
int32_t n_brightness;
float f_temp;
maxim_max30102_reset(); //resets the MAX30102
// initialize serial communication at 115200 bits per second:
pc.baud(115200);
pc.format(8,SerialBase::None,1);
wait(1);
//read and clear status register
maxim_max30102_read_reg(0,&uch_dummy);
//wait until the user presses a key
// while(pc.readable()==0)
// {
// pc.printf("x1B[2J"); //clear terminal program screen
// pc.printf("Press any key to start conversionnr");
// wait(1);
// }
// uch_dummy=getchar();
maxim_max30102_init(); //initializes the MAX30102
n_brightness=0;
un_min=0x3FFFF;
un_max=0;
n_ir_buffer_length=500; //buffer length of 100 stores 5 seconds of samples running at 100sps
//read the first 500 samples, and determine the signal range
for(i=0;i<n_ir_buffer_length;i++)
{
while(INT.read()==1); //wait until the interrupt pin asserts
maxim_max30102_read_fifo((aun_red_buffer+i), (aun_ir_buffer+i)); //read from MAX30102 FIFO
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
pc.printf("red=");
pc.printf("%i", aun_red_buffer[i]);
pc.printf(", ir=");
pc.printf("%inr", aun_ir_buffer[i]);
}
un_prev_data=aun_red_buffer[i];
//calculate heart rate and SpO2 after first 500 samples (first 5 seconds of samples)
maxim_heart_rate_and_oxygen_saturation(aun_ir_buffer, n_ir_buffer_length, aun_red_buffer, &n_sp02, &ch_spo2_valid, &n_heart_rate, &ch_hr_valid);
//Continuously taking samples from MAX30102. Heart rate and SpO2 are calculated every 1 second
while(1)
{
i=0;
un_min=0x3FFFF;
un_max=0;
//dumping the first 100 sets of samples in the memory and shift the last 400 sets of samples to the top
for(i=100;i<500;i++)
{
aun_red_buffer[i-100]=aun_red_buffer[i];
aun_ir_buffer[i-100]=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 100 sets of samples before calculating the heart rate.
for(i=400;i<500;i++)
{
un_prev_data=aun_red_buffer[i-1];
while(INT.read()==1);
maxim_max30102_read_fifo((aun_red_buffer+i), (aun_ir_buffer+i));
if(aun_red_buffer[i]>un_prev_data)//just to determine the brightness of LED according to the deviation of adjacent two AD data
{
f_temp=aun_red_buffer[i]-un_prev_data;
f_temp/=(un_max-un_min);
f_temp*=MAX_BRIGHTNESS;
n_brightness-=(int)f_temp;
if(n_brightness<0)
n_brightness=0;
}
else
{
f_temp=un_prev_data-aun_red_buffer[i];
f_temp/=(un_max-un_min);
f_temp*=MAX_BRIGHTNESS;
n_brightness+=(int)f_temp;
if(n_brightness>MAX_BRIGHTNESS)
n_brightness=MAX_BRIGHTNESS;
}
pwmled.write(1-(float)n_brightness/256);//pwm control led brightness
if(n_brightness<120)
led=1;
else
led=0;
//send samples and calculation result to terminal program through UART
pc.printf("red=");
pc.printf("%i", aun_red_buffer[i]);
pc.printf(", ir=");
pc.printf("%i", aun_ir_buffer[i]);
pc.printf(", HR=%i, ", n_heart_rate);
pc.printf("HRvalid=%i, ", ch_hr_valid);
pc.printf("SpO2=%i, ", n_sp02);
pc.printf("SPO2Valid=%inr", ch_spo2_valid);
}
maxim_heart_rate_and_oxygen_saturation(aun_ir_buffer, n_ir_buffer_length, aun_red_buffer, &n_sp02, &ch_spo2_valid, &n_heart_rate, &ch_hr_valid);
}
}
#include "stm32f10x.h"
#include "usart.h"
#include "delay.h"
#include "max30102.h"
#include "myiic.h"
#include "algorithm.h"
#include "oled_iic.h"
#define MAX_BRIGHTNESS 255
u32 aun_ir_buffer[500];//IR LED sensor data
int32_t n_ir_buffer_length; //数据长度
u32 aun_red_buffer[500]; //Red LED sensor data
int32_t n_sp02; //SPO2 value
int8_t ch_spo2_valid; //indicator to show if the SP02 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
int main(void)
{
u8 t = 0; // 初始化一个计数变量t,用于计数,初始值为0
u32 un_min, un_max, un_prev_data; // 初始化三个32位无符号整数,用于存储信号的最小值、最大值和上一个数据
int32_t n_brightness; // 初始化一个32位有符号整数,用于表示亮度调整
u16 i; // 初始化一个16位无符号整数i,用于循环计数
u8 temp[6]; // 初始化一个长度为6的无符号字符数组,用于存储传感器采样的数据
float f_temp; // 初始化一个单精度浮点数,用于存储临时的浮点型数据
u8 dis_hr = 0, dis_spo2 = 0; // 初始化两个8位无符号整数,用于表示心率和血氧饱和度的显示值
delay_init(); // 调用延时函数的初始化,初始化延时功能
HZ = GB16_NUM(); // 获取GB16字体的数量
delay_ms(50); // 进行短暂的延时,等待系统初始化
OLED_Init(); // 初始化OLED显示屏
delay_ms(50); // 进行短暂的延时,等待OLED初始化完成
OLED_Clear(); // 清除OLED屏幕上的内容
delay_ms(50); // 进行短暂的延时,等待清屏完成
OLED_ShowCH(16,2,"心率:000");
OLED_ShowCH(16,4,"血氧:000%");
uart_init(115200);
MX_GPIO_Init();
max30102_init();
un_min = 0x3FFFF; // 初始化信号最小值为一个较大的初始值
un_max = 0; // 初始化信号最大值为0
n_ir_buffer_length = 500; // 缓冲区长度为500,存储5秒运行时的100sps采样数据
// 读取前500个样本,确定信号范围
for(i = 0; i < n_ir_buffer_length; i++)
{
while(MAX30102_INT == 1); // 等待直到中断引脚触发中断
//读取心率血氧传感器(max30102)的FIFO(First In, First Out)数据,然后解析这些数据并将其存储在相应的缓冲区中。
//调用max30102_FIFO_ReadBytes函数从max30102的FIFO寄存器中读取6个字节的数据,存储在名为temp的数组中。
max30102_FIFO_ReadBytes(REG_FIFO_DATA, temp);
//将temp数组的前三个字节组合成一个32位的长整型,用于存储红光传感器的数据。这个数据的组合方式是:
//将temp[0]的低两位(temp[0] & 0x03)左移16位,然后与temp[1]左移8位的结果以及temp[2]的结果进行按位或运算。
aun_red_buffer[i] = (long)((long)((long)temp[0] & 0x03) << 16) | (long)temp[1] << 8 | (long)temp[2];
//将temp数组的后三个字节组合成一个32位的长整型,用于存储红外光传感器的数据。这个数据的组合方式与红光传感器类似。
aun_ir_buffer[i] = (long)((long)((long)temp[3] & 0x03) << 16) | (long)temp[4] << 8 | (long)temp[5];
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]; // 更新信号最大值
}
un_prev_data = aun_red_buffer[i]; // 记录最后一个样本值
// 在前500个样本(5秒的样本)之后,计算心率和血氧饱和度
maxim_heart_rate_and_oxygen_saturation(aun_ir_buffer, n_ir_buffer_length, aun_red_buffer, &n_sp02, &ch_spo2_valid, &n_heart_rate, &ch_hr_valid);
while(1)
{
i = 0;
un_min = 0x3FFFF;
un_max = 0;
// 将内存中的前100组样本丢弃,将最后400组样本移动到数组的前面
for(i = 100; i < 500; i++)
{
aun_red_buffer[i - 100] = aun_red_buffer[i];
aun_ir_buffer[i - 100] = aun_ir_buffer[i];
// 更新信号的最小值和最大值
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];
}
// 取得计算心率之前的100组样本
for(i = 400; i < 500; i++)
{
un_prev_data = aun_red_buffer[i - 1];
while(MAX30102_INT == 1);
max30102_FIFO_ReadBytes(REG_FIFO_DATA, temp);
aun_red_buffer[i] = (long)((long)((long)temp[0] & 0x03) << 16) | (long)temp[1] << 8 | (long)temp[2];
aun_ir_buffer[i] = (long)((long)((long)temp[3] & 0x03) << 16) | (long)temp[4] << 8 | (long)temp[5];
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;
n_brightness -= (int)f_temp;
if(n_brightness < 0)
n_brightness = 0;
}
else
{
f_temp = un_prev_data - aun_red_buffer[i];
f_temp /= (un_max - un_min);
f_temp *= MAX_BRIGHTNESS;
n_brightness += (int)f_temp;
if(n_brightness > MAX_BRIGHTNESS)
n_brightness = MAX_BRIGHTNESS;
}
// 将样本和计算结果通过UART发送到终端程序
if((ch_hr_valid == 1)) // ch_hr_valid == 1 && ch_spo2_valid ==1 && n_heart_rate<120 && n_sp02<101
{
if(n_heart_rate < 120)
dis_hr = n_heart_rate;
else
dis_hr = 0;
dis_spo2 = n_sp02;
}
else
{
dis_hr = 0;
dis_spo2 = 0;
}
t++;
if(t > 10)
{
t = 0;
printf("HR=%i,SpO2=%irn ", dis_hr, dis_spo2);
}
OLED_ShowNum(56, 2, dis_hr, 3, 1);
OLED_ShowNum(56, 4, dis_spo2, 3, 1);
}
// 调用函数计算心率和血氧饱和度,并通过OLED显示
maxim_heart_rate_and_oxygen_saturation(aun_ir_buffer, n_ir_buffer_length, aun_red_buffer, &n_sp02, &ch_spo2_valid, &n_heart_rate, &ch_hr_valid);
delay_ms(200);
}
}
#ifndef __MAX30102_H
#define __MAX30102_H
#include "sys.h"
//
#define MAX30102_INT PBin(9)
#define I2C_WR 0 / 写控制bit /
#define I2C_RD 1 / 读控制bit /
#define max30102_WR_address 0xAE
#define I2C_WRITE_ADDR 0xAE
#define I2C_READ_ADDR 0xAF
//register addresses
#define REG_INTR_STATUS_1 0x00
#define REG_INTR_STATUS_2 0x01
#define REG_INTR_ENABLE_1 0x02
#define REG_INTR_ENABLE_2 0x03
#define REG_FIFO_WR_PTR 0x04
#define REG_OVF_COUNTER 0x05
#define REG_FIFO_RD_PTR 0x06
#define REG_FIFO_DATA 0x07
#define REG_FIFO_CONFIG 0x08
#define REG_MODE_CONFIG 0x09
#define REG_SPO2_CONFIG 0x0A
#define REG_LED1_PA 0x0C
#define REG_LED2_PA 0x0D
#define REG_PILOT_PA 0x10
#define REG_MULTI_LED_CTRL1 0x11
#define REG_MULTI_LED_CTRL2 0x12
#define REG_TEMP_INTR 0x1F
#define REG_TEMP_FRAC 0x20
#define REG_TEMP_CONFIG 0x21
#define REG_PROX_INT_THRESH 0x30
#define REG_REV_ID 0xFE
#define REG_PART_ID 0xFF
void MX_GPIO_Init(void);
void max30102_init(void);
void max30102_reset(void);
u8 max30102_Bus_Write(u8 Register_Address, u8 Word_Data);
u8 max30102_Bus_Read(u8 Register_Address);
void max30102_FIFO_ReadWords(u8 Register_Address,u16 Word_Data[][2],u8 count);
void max30102_FIFO_ReadBytes(u8 Register_Address,u8* Data);
void maxim_max30102_write_reg(uint8_t uch_addr, uint8_t uch_data);
void maxim_max30102_read_reg(uint8_t uch_addr, uint8_t *puch_data);
void maxim_max30102_read_fifo(uint32_t pun_red_led, uint32_t pun_ir_led);
#endif
#include "max30102.h"
#include "myiic.h"
#include "delay.h"
// 初始化GPIO配置
void MX_GPIO_Init()
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);//GPIOB
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7|GPIO_Pin_8;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_ResetBits(GPIOB, GPIO_Pin_7|GPIO_Pin_8);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
// 通过I2C总线写入数据
u8 max30102_Bus_Write(u8 Register_Address, u8 Word_Data)
{
// 采用串行EEPROM随即读取指令序列,连续读取若干字节
// 第1步:发起I2C总线启动信号
iic_Start();
// 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读
iic_Send_Byte(max30102_WR_address | I2C_WR); // 此处是写指令
// 第3步:发送ACK
if (iic_Wait_Ack() != 0)
{
goto cmd_fail; // EEPROM器件无应答
}
// 第4步:发送字节地址
iic_Send_Byte(Register_Address);
if (iic_Wait_Ack() != 0)
{
goto cmd_fail; // EEPROM器件无应答
}
// 第5步:开始写入数据
iic_Send_Byte(Word_Data);
// 第6步:发送ACK
if (iic_Wait_Ack() != 0)
{
goto cmd_fail; // EEPROM器件无应答
}
// 发送I2C总线停止信号
iic_Stop();
return 1; // 执行成功
cmd_fail: // 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备
// 发送I2C总线停止信号
iic_Stop();
return 0;
}
// 通过I2C总线读取数据
u8 max30102_Bus_Read(u8 Register_Address)
{
u8 data;
// 第1步:发起I2C总线启动信号
iic_Start();
// 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读
iic_Send_Byte(max30102_WR_address | I2C_WR); // 此处是写指令
// 第3步:发送ACK
if (iic_Wait_Ack() != 0)
{
goto cmd_fail; // EEPROM器件无应答
}
// 第4步:发送字节地址
iic_Send_Byte((uint8_t)Register_Address);
if (iic_Wait_Ack() != 0)
{
goto cmd_fail; // EEPROM器件无应答
}
// 第6步:重新启动I2C总线。下面开始读取数据
iic_Start();
// 第7步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读
iic_Send_Byte(max30102_WR_address | I2C_RD); // 此处是读指令
// 第8步:发送ACK
if (iic_Wait_Ack() != 0)
{
goto cmd_fail; // EEPROM器件无应答
}
// 第9步:读取数据
{
data = iic_Read_Byte(0); // 读1个字节
iic_NAck(); // 最后1个字节读完后,CPU产生NACK信号(驱动SDA = 1)
}
// 发送I2C总线停止信号
iic_Stop();
return data; // 执行成功 返回data值
cmd_fail: // 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备
// 发送I2C总线停止信号
iic_Stop();
return 0;
}
// 通过I2C总线读取多个字节的数据
void max30102_FIFO_ReadWords(u8 Register_Address, u16 Word_Data[][2], u8 count)
{
u8 i = 0;
u8 no = count;
u8 data1, data2;
// 第1步:发起I2C总线启动信号
iic_Start();
// 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读
iic_Send_Byte(max30102_WR_address | I2C_WR); // 此处是写指令
// 第3步:发送ACK
if (iic_Wait_Ack() != 0)
{
goto cmd_fail; // EEPROM器件无应答
}
// 第4步:发送字节地址
iic_Send_Byte((uint8_t)Register_Address);
if (iic_Wait_Ack() != 0)
{
goto cmd_fail; // EEPROM器件无应答
}
// 第6步:重新启动I2C总线。下面开始读取数据
iic_Start();
// 第7步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读
iic_Send_Byte(max30102_WR_address | I2C_RD); // 此处是读指令
// 第8步:发送ACK
if (iic_Wait_Ack() != 0)
{
goto cmd_fail; // EEPROM器件无应答
}
// 第9步:读取数据
while (no)
{
data1 = iic_Read_Byte(0);
iic_Ack();
data2 = iic_Read_Byte(0);
iic_Ack();
Word_Data[i][0] = (((u16)data1 << 8) | data2); //
data1 = iic_Read_Byte(0);
iic_Ack();
data2 = iic_Read_Byte(0);
if (1 == no)
iic_NAck(); // 最后1个字节读完后,CPU产生NACK信号(驱动SDA = 1)
else
iic_Ack();
Word_Data[i][1] = (((u16)data1 << 8) | data2);
no--;
i++;
}
// 发送I2C总线停止信号
iic_Stop();
cmd_fail: // 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备
// 发送I2C总线停止信号
iic_Stop();
}
// 通过I2C总线读取字节数据
void max30102_FIFO_ReadBytes(u8 Register_Address, u8* Data)
{
max30102_Bus_Read(REG_INTR_STATUS_1);
max30102_Bus_Read(REG_INTR_STATUS_2);
// 第1步:发起I2C总线启动信号
iic_Start();
// 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读
iic_Send_Byte(max30102_WR_address | I2C_WR); // 此处是写指令
// 第3步:发送ACK
if (iic_Wait_Ack() != 0)
{
goto cmd_fail; // EEPROM器件无应答
}
// 第4步:发送字节地址
iic_Send_Byte((uint8_t)Register_Address);
if (iic_Wait_Ack() != 0)
{
goto cmd_fail; // EEPROM器件无应答
}
// 第6步:重新启动I2C总线。下面开始读取数据
iic_Start();
// 第7步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读
iic_Send_Byte(max30102_WR_address | I2C_RD); // 此处是读指令
// 第8步:发送ACK
if (iic_Wait_Ack() != 0)
{
goto cmd_fail; // EEPROM器件无应答
}
// 第9步:读取数据
Data[0] = iic_Read_Byte(1);
Data[1] = iic_Read_Byte(1);
Data[2] = iic_Read_Byte(1);
Data[3] = iic_Read_Byte(1);
Data[4] = iic_Read_Byte(1);
Data[5] = iic_Read_Byte(0);
// 最后1个字节读完后,CPU产生NACK信号(驱动SDA = 1)
// 发送I2C总线停止信号
iic_Stop();
cmd_fail: // 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备
// 发送I2C总线停止信号
iic_Stop();
}
// MAX30102传感器初始化配置
void max30102_init(void)
{
iic_Init(); // 初始化I2C总线
max30102_reset(); // 复位MAX30102传感器
// 下面是一些配置参数的设置
// max30102_Bus_Write(REG_INTR_ENABLE_1, 0xc0); // INTR setting
max30102_Bus_Write(REG_INTR_ENABLE_1, 0xc0); // 使能中断,开启PPG_RDY_EN
max30102_Bus_Write(REG_INTR_ENABLE_2, 0x00); // 关闭其他中断
// MAX30102配置参数设置
max30102_Bus_Write(REG_INTR_ENABLE_1, 0xc0); // INTR setting
max30102_Bus_Write(REG_INTR_ENABLE_2, 0x00);
max30102_Bus_Write(REG_FIFO_WR_PTR, 0x00); // 设置FIFO写指针,位[4:0]
max30102_Bus_Write(REG_OVF_COUNTER, 0x00); // 设置溢出计数器,位[4:0]
max30102_Bus_Write(REG_FIFO_RD_PTR, 0x00); // 设置FIFO读指针,位[4:0]
max30102_Bus_Write(REG_FIFO_CONFIG, 0x0f); // 配置FIFO,设置样本平均值为1,禁止FIFO循环,FIFO几乎满时触发中断
max30102_Bus_Write(REG_MODE_CONFIG, 0x03); // 配置工作模式,0x02表示仅红光模式,0x03表示SpO2模式,0x07表示多模式LED
max30102_Bus_Write(REG_SPO2_CONFIG, 0x27); // 配置SpO2,设置ADC范围为4096nA,SpO2采样率为100 Hz,LED脉冲宽度为400uS
max30102_Bus_Write(REG_LED1_PA, 0x24); // 设置LED1功率,选择值约为7mA
max30102_Bus_Write(REG_LED2_PA, 0x24); // 设置LED2功率,选择值约为7mA
max30102_Bus_Write(REG_PILOT_PA, 0x7f); // 设置Pilot LED功率,选择值约为25mA
}
// 下面是对其他函数的注释,与max30102_init函数相似
void max30102_reset(void)
{
max30102_Bus_Write(REG_MODE_CONFIG, 0x40);
max30102_Bus_Write(REG_MODE_CONFIG, 0x40);
}
void maxim_max30102_write_reg(uint8_t uch_addr, uint8_t uch_data)
{
iic_Write_One_Byte(I2C_WRITE_ADDR, uch_addr, uch_data);
}
void maxim_max30102_read_reg(uint8_t uch_addr, uint8_t *puch_data)
{
iic_Read_One_Byte(I2C_WRITE_ADDR, uch_addr, puch_data);
}
void maxim_max30102_read_fifo(uint32_t pun_red_led, uint32_t pun_ir_led)
{
uint32_t un_temp;
unsigned char uch_temp;
char ach_i2c_data[6];
*pun_red_led = 0;
*pun_ir_led = 0;
// 读取和清除状态寄存器
maxim_max30102_read_reg(REG_INTR_STATUS_1, &uch_temp);
maxim_max30102_read_reg(REG_INTR_STATUS_2, &uch_temp);
// 读取FIFO数据
iic_ReadBytes(I2C_WRITE_ADDR, REG_FIFO_DATA, (u8 *)ach_i2c_data, 6);
// 解析FIFO数据
un_temp = (unsigned char)ach_i2c_data[0]; // 读取FIFO数据的第一个字节
un_temp <<= 16; // 左移16位,将数据放入对应的高位
*pun_red_led += un_temp; // 累加到红光LED的数据中
un_temp = (unsigned char)ach_i2c_data[1]; // 读取FIFO数据的第二个字节
un_temp <<= 8; // 左移8位,将数据放入对应的高位
*pun_red_led += un_temp; // 累加到红光LED的数据中
un_temp = (unsigned char)ach_i2c_data[2]; // 读取FIFO数据的第三个字节
*pun_red_led += un_temp; // 累加到红光LED的数据中
un_temp = (unsigned char)ach_i2c_data[3]; // 读取FIFO数据的第四个字节
un_temp <<= 16; // 左移16位,将数据放入对应的高位
*pun_ir_led += un_temp; // 累加到红外LED的数据中
un_temp = (unsigned char)ach_i2c_data[4]; // 读取FIFO数据的第五个字节
un_temp <<= 8; // 左移8位,将数据放入对应的高位
*pun_ir_led += un_temp; // 累加到红外LED的数据中
un_temp = (unsigned char)ach_i2c_data[5]; // 读取FIFO数据的第六个字节
*pun_ir_led += un_temp; // 累加到红外LED的数据中
*pun_red_led &= 0x03FFFF; // 对红光LED的数据进行按位与操作,屏蔽掉高位 [23:18]
*pun_ir_led &= 0x03FFFF; // 对红外LED的数据进行按位与操作,屏蔽掉高位 [23:18]
}
//Copyright (C) 2015 Maxim Integrated Products, Inc., All Rights Reserved.
#ifndef ALGORITHM_H_
#define ALGORITHM_H_
#include "sys.h"
#define true 1
#define false 0
#define FS 100
#define BUFFER_SIZE (FS* 5)
#define HR_FIFO_SIZE 7
#define MA4_SIZE 4 // DO NOT CHANGE
#define HAMMING_SIZE 5// DO NOT CHANGE
#define min(x,y) ((x) < (y) ? (x) : (y))
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);
void maxim_find_peaks( int32_t pn_locs, int32_t pn_npks, int32_t *pn_x, int32_t n_size, int32_t n_min_height, int32_t n_min_distance, int32_t n_max_num );
void maxim_peaks_above_min_height( int32_t pn_locs, int32_t pn_npks, int32_t *pn_x, int32_t n_size, int32_t n_min_height );
void maxim_remove_close_peaks( int32_t pn_locs, int32_t pn_npks, int32_t *pn_x, int32_t n_min_distance );
void maxim_sort_ascend( int32_t *pn_x, int32_t n_size );
void maxim_sort_indices_descend( int32_t pn_x, int32_t pn_indx, int32_t n_size);
#endif
#include "algorithm.h"
//汉明窗口,函数的目的是在时域内对信号进行平滑处理,以减小信号两端的幅度,防止频谱泄漏。
//计算公式:w(n)=0.54-0.46cos(2 pi n/N-1),其中 N 是窗口的长度
const uint16_t auw_hamm[31]={ 41, 276, 512, 276, 41 }; //Hamm= long16(512* hamming(5)');
//uch_spo2_table is computed as -45.060ratioAverage ratioAverage + 30.354 *ratioAverage + 94.845 ;
//ratioAverage 是一个用于计算血氧饱和度的比值。这个比值与红外光和红光的交流和直流分量的比值有关
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 } ;
static int32_t an_dx[ BUFFER_SIZE-MA4_SIZE]; // delta
static int32_t an_x[ BUFFER_SIZE]; //ir
static int32_t an_y[ BUFFER_SIZE]; //red
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
- 通过检测PPG周期的峰值和相应的红/红外信号的AC/DC,计算出SPO2的比值。
- 因为这个算法的目标是Arm M0/M3。由于寄存器溢出,SPO2的公式没有达到精度。
- 因此,准确的SPO2是预先计算出来的,并且每个比率都节省了很长的uch_spo2_table[]。
- param[in] *pun_ir_buffer -红外传感器数据缓冲区
- param[in] n_ir_buffer_length -红外传感器数据缓冲区长度
- param[in] *pun_red_buffer -红光传感器数据缓冲区
- param[out] *pn_spo2 -计算的SpO2值
- param[out] *pch_spo2_valid -如果计算的SpO2值有效,则为1
- param[out] *pn_heart_rate -计算心率值
- param[out] *pch_hr_valid - 1如果计算的心率值是有效的
- 存储计算结果的指针pn_spo2、pch_spo2_valid、pn_heart_rate和pch_hr_valid。
- retval None
- 实现步骤:
主要实现过程包括:
(1)去除IR信号的直流成分:
计算IR信号的平均值,然后从原始IR信号中减去平均值,得到去除直流成分的信号。
(2)平滑信号:
对去除直流成分的IR信号进行4点移动平均。
(3)计算信号差分:
计算平滑后的IR信号的差分。
(4)使用汉明窗口翻转波形:
对差分信号使用汉明窗口,以便在后续步骤中检测波谷。
(5)使用峰值检测找到波谷:
使用峰值检测算法找到波谷的位置,并保存在an_ir_valley_locs数组中。
(6)计算心率:
根据波谷的位置计算心率,单位为每分钟心跳数(BPM)。
(7)计算IR和红光信号的AC/DC比值:
在两个相邻波谷之间,找到IR和红光信号的AC(交流)和DC(直流)成分,并计算AC/DC比值。
在每个心跳周期内,计算红外和红光信号的AC分量和DC分量。
AC分量反映了血液的脉动变化,而DC分量反映了总体的血液吸收
(8)使用比值计算血氧饱和度:
使用IR和红光信号的AC/DC比值,查表计算血氧饱和度。结果存储在pn_spo2中。
红外和红光的AC/DC比值与血液的含氧量相关,因为含氧血液和脱氧血液对红外和红光的吸收比例不同
使用预先计算好的SPO2校准表,将红外和红光的AC/DC比值映射到实际的SPO2值。
*/
{
uint32_t un_ir_mean, un_only_once; // IR信号的均值和标志位,用于去除直流(DC)成分
int32_t k, n_i_ratio_count; // 循环变量k,AC/DC比值计数器
int32_t i, s, m, n_exact_ir_valley_locs_count, n_middle_idx;
// 循环变量i,临时变量s,计数器m,精确IR谷点数目,中间索引
int32_t n_th1, n_npks, n_c_min; // 阈值n_th1,峰值计数n_npks,最小值n_c_min
int32_t an_ir_valley_locs[15]; // IR信号谷点的数组
int32_t an_exact_ir_valley_locs[15]; // 精确IR信号谷点的数组
int32_t an_dx_peak_locs[15]; // 微分信号峰点的数组
int32_t n_peak_interval_sum; // 峰值间隔总和
int32_t n_y_ac, n_x_ac; // 红光和红外光的AC(交流)分量
int32_t n_spo2_calc; // 计算得到的SPO2值
int32_t n_y_dc_max, n_x_dc_max; // 红光和红外光的DC(直流)最大值
int32_t n_y_dc_max_idx, n_x_dc_max_idx; // 红光和红外光的DC最大值索引
int32_t an_ratio[5], n_ratio_average; // AC/DC比值数组和比值平均值
int32_t n_nume, n_denom; // 分子和分母,用于计算AC/DC比值
// 去除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;
for (k = 0; k < n_ir_buffer_length; k++) an_x[k] = pun_ir_buffer[k] - un_ir_mean;
// 4点移动平均
for (k = 0; k < BUFFER_SIZE - MA4_SIZE; k++)
{
// 计算4个点的和
n_denom = (an_x[k] + an_x[k + 1] + an_x[k + 2] + an_x[k + 3]);
// 计算移动平均值并替换原始值
an_x[k] = n_denom / (int32_t)4;
}
// 计算信号差分,计算信号an_x的差分,即相邻元素之间的差值。
//通过对原始信号进行差分运算,可以获取信号的变化率,有助于后续的信号处理和特征提取。
for (k = 0; k < BUFFER_SIZE - MA4_SIZE - 1; k++)
an_dx[k] = (an_x[k + 1] - an_x[k]);
// 2点移动平均
for (k = 0; k < BUFFER_SIZE - MA4_SIZE - 2; k++)
{
an_dx[k] = (an_dx[k] + an_dx[k + 1]) / 2;
}
// 汉明窗口
//汉明窗口是一种窗口函数,通常用于在信号处理中对信号进行平滑处理,以减少频谱泄漏。
//在这里,通过对差分信号应用汉明窗口,可以对信号进行加权,突出信号中的特定频率成分。
//这个加权后的信号被用于后续的信号处理步骤,以提取相关的特征或信息。
for (i = 0; i < BUFFER_SIZE - HAMMING_SIZE - MA4_SIZE - 2; i++)
{
s = 0;
// 应用汉明窗口对信号进行加权
for (k = i; k < i + HAMMING_SIZE; k++)
{
// 汉明窗口的权重数组为 auw_hamm
// 将差分信号 an_dx 与汉明窗口加权,并求和
s -= an_dx[k] * auw_hamm[k - i];
}
// 将加权和除以汉明窗口权重之和(1146),归一化
an_dx[i] = s / (int32_t)1146;
}
// 阈值计算,计算差分信号数组 an_dx 的平均阈值 n_th1
n_th1 = 0; // 初始化阈值
// 遍历差分信号数组 an_dx,计算阈值
for (k = 0; k < BUFFER_SIZE - HAMMING_SIZE; k++)
{
// 阈值为正差分信号之和,如果差分信号为负值,则取绝对值累加
n_th1 += ((an_dx[k] > 0) ? an_dx[k] : ((int32_t)0 - an_dx[k]));
}
// 将阈值除以数组长度,得到平均阈值
n_th1 = n_th1 / (BUFFER_SIZE - HAMMING_SIZE);
// 使用峰值检测找到波谷,计算心率
/*
思路:
maxim_find_peaks 函数找到差分信号数组 an_dx 中的峰值位置,并将这些位置存储在 an_dx_peak_locs 数组中
然后,通过计算峰值之间的平均间隔,利用心率计算公式计算心率值。
最后,根据检测到的峰值位置,计算波谷位置,并将波谷位置存储在 an_ir_valley_locs 数组中。
*/
maxim_find_peaks(an_dx_peak_locs, &n_npks, an_dx, BUFFER_SIZE - HAMMING_SIZE, n_th1, 8, 5);
n_peak_interval_sum = 0;
// 如果检测到至少两个峰值
if (n_npks >= 2)
{
// 计算峰值之间的平均间隔
for (k = 1; k < n_npks; k++)
n_peak_interval_sum += (an_dx_peak_locs[k] - an_dx_peak_locs[k - 1]);
// 计算心率(每分钟心跳数)
n_peak_interval_sum = n_peak_interval_sum / (n_npks - 1);
*pn_heart_rate = (int32_t)(6000 / n_peak_interval_sum);
// 设置心率有效标志为1
*pch_hr_valid = 1;
}
else
{
// 如果未检测到足够的峰值,设置心率为无效值-999
*pn_heart_rate = -999;
// 设置心率有效标志为0
*pch_hr_valid = 0;
}
// 将峰值位置加上 HAMMING_SIZE / 2 作为波谷位置,为后面寻找准确的波谷做准备
for (k = 0; k < n_npks; k++)
an_ir_valley_locs[k] = an_dx_peak_locs[k] + HAMMING_SIZE / 2;
// 计算AC/DC比值,从原始的红外和红光信号中提取出用于计算的实际波谷位置
/*
首先,将原始的红外信号存储在 an_x 数组中,红光信号存储在 an_y 数组中。
然后,遍历波谷位置数组,寻找每个波谷的实际位置。通过在波谷位置的前后寻找最小值,确定波谷的确切位置,
并将这些位置存储在 an_exact_ir_valley_locs 数组中。
*/
for (k = 0; k < n_ir_buffer_length; k++)
{
an_x[k] = pun_ir_buffer[k]; // 将红外信号存储在 an_x 数组中
an_y[k] = pun_red_buffer[k]; // 将红光信号存储在 an_y 数组中
}
n_exact_ir_valley_locs_count = 0;
// 遍历波谷位置数组,找到每个波谷的实际位置
for (k = 0; k < n_npks; k++)
{
un_only_once = 1;
m = an_ir_valley_locs[k];
n_c_min = 16777216; // 设置一个较大的初始值,2^24
// 在波谷位置的前后寻找最小值,以确定波谷的实际位置
if (m + 5 < BUFFER_SIZE - HAMMING_SIZE && m - 5 > 0)
//检查实际红外信号谷值位置 m 前后是否有足够的空间,以防数组越界。
{
for (i = m - 5; i < m + 5; i++)
//循环遍历从当前谷值位置向前后各取5个点的范围。
{
if (an_x[i] < n_c_min)
{
if (un_only_once > 0)
{
//第一次找到最小值,将其设为0以标记已经找到最小值。
un_only_once = 0;
}
n_c_min = an_x[i];
an_exact_ir_valley_locs[k] = i;
}
}
// 如果找到了最小值,增加实际波谷位置的计数
if (un_only_once == 0)
n_exact_ir_valley_locs_count++;
}
}
// 波谷是信号的极小值,用于确定每个心跳周期的起始点。如果确切波谷位置的数量小于2,说明信号比值超出范围,设置SPO2为无效值-999
if (n_exact_ir_valley_locs_count < 2)
{
*pn_spo2 = -999;
*pch_spo2_valid = 0;
return;
}
// 4点移动平均,平滑信号并降低高频噪声的影响
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]) / (int32_t)4;
an_y[k] = (an_y[k] + an_y[k + 1] + an_y[k + 2] + an_y[k + 3]) / (int32_t)4;
}
// 使用an_exact_ir_valley_locs,找到IR-RED DC和IR-RED AC进行SPO2校准比率
//使用先前找到的红外信号的谷位置(an_exact_ir_valley_locs),来计算IR-RED DC和IR-RED AC的比率。用于SPO2(血氧饱和度)的校准。
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_exact_ir_valley_locs[k] > BUFFER_SIZE)
{
*pn_spo2 = -999; // 因为谷位置超出范围,不是正确的SPO2
*pch_spo2_valid = 0;
return;
}
}
/*
计算红光和红外信号的交流分量的乘积,并将计算得到的AC/DC比值保存到数组中。
其中,通过找到两个谷值之间的最大值来估算直流分量,然后计算交流分量,并最终得到AC/DC比值。
整个过程旨在对信号进行预处理,为后续的SPO2(血氧饱和度)计算提供必要的信息。
*/
for (k = 0; k < n_exact_ir_valley_locs_count - 1; k++)
{
n_y_dc_max = -16777216; // 初始化红光信号的最大直流分量为一个较小的负数
n_x_dc_max = -16777216; // 初始化红外信号的最大直流分量为一个较小的负数
// 判断两个谷值之间的距离是否大于10个样本点,确保两谷之间有足够的数据进行计算
if (an_exact_ir_valley_locs[k + 1] - an_exact_ir_valley_locs[k] > 10)
{
// 遍历两个谷值之间的数据
for (i = an_exact_ir_valley_locs[k]; i < an_exact_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;
}
}
// 计算红光的交流分量
//计算红光信号在两个相邻谷值之间的交流成分,使用两个谷值之间的光强差值乘以相邻两个峰谷之间的距离。
//an_y: 这是存储红光信号光强数据的数组
//an_exact_ir_valley_locs: 这是存储实际红外信号波谷位置的数组
//n_y_dc_max_idx,红光信号的最大峰值位置的索引
n_y_ac = (an_y[an_exact_ir_valley_locs[k + 1]] - an_y[an_exact_ir_valley_locs[k]]) * (n_y_dc_max_idx - an_exact_ir_valley_locs[k]);
//将得到的红光的交流分量加到红光信号在前一个谷值位置的基线值上
n_y_ac = an_y[an_exact_ir_valley_locs[k]] + n_y_ac / (an_exact_ir_valley_locs[k + 1] - an_exact_ir_valley_locs[k]);
// 消除红光信号的直流分量
n_y_ac = an_y[n_y_dc_max_idx] - n_y_ac;
//写成abs(n_y_ac - an_y[n_y_dc_max_idx])是否可以?
// 计算红外光的交流分量
n_x_ac = (an_x[an_exact_ir_valley_locs[k + 1]] - an_x[an_exact_ir_valley_locs[k]]) * (n_x_dc_max_idx - an_exact_ir_valley_locs[k]);
n_x_ac = an_x[an_exact_ir_valley_locs[k]] + n_x_ac / (an_exact_ir_valley_locs[k + 1] - an_exact_ir_valley_locs[k]);
// 消除红外信号的直流分量
n_x_ac = an_x[an_exact_ir_valley_locs[k + 1]] - n_x_ac;
// 将红光信号的AC分量与红外信号最大峰值位置的直流分量相乘,作为计算SPO2的分子
n_nume = (n_y_ac * n_x_dc_max) >> 7;
// 将红外信号的AC分量与红光信号最大峰值位置的直流分量相乘,作为计算SPO2的分母
n_denom = (n_x_ac * n_y_dc_max) >> 7;
// 判断分母是否大于0,避免除零错误,并判断是否满足条件,满足则进行下一步计算
if (n_denom > 0 && n_i_ratio_count < 5 && n_nume != 0)
{
// 计算AC/DC比值并保存到数组中
an_ratio[n_i_ratio_count] = (n_nume * 20) / n_denom;
n_i_ratio_count++;
}
}
}
/*
对排序后的AC/DC比值数组进行处理,计算得到中值平均(或直接取中值),并通过判断得到的AC/DC比值
是否在有效范围内,确定最终的SPO2值。如果AC/DC比值有效,通过查表获取对应的SPO2值,否则将SPO2
值设为无效。在实际应用中,通过对AC/DC比值的处理,结合预先计算的SPO2表格,可以更准确地估算血氧饱和度。
*/
// 将存储AC/DC比值的数组按升序排列
maxim_sort_ascend(an_ratio, n_i_ratio_count);
// 计算中间值的索引
n_middle_idx = n_i_ratio_count / 2;
// 如果中间索引大于1,使用中值平均计算AC/DC比值
if (n_middle_idx > 1)
n_ratio_average = (an_ratio[n_middle_idx - 1] + an_ratio[n_middle_idx]) / 2;
else
n_ratio_average = an_ratio[n_middle_idx];
// 判断计算得到的AC/DC比值是否在有效范围内
if (n_ratio_average > 2 && n_ratio_average < 184)
{
// 通过查表获取对应的SPO2值
n_spo2_calc = uch_spo2_table[n_ratio_average];
*pn_spo2 = n_spo2_calc;
*pch_spo2_valid = 1; // 设置SPO2有效标志
// 可选:用以下公式计算浮点数SPO2,与查表结果进行比较
// float_SPO2 = -45.060 n_ratio_average n_ratio_average / 10000 + 30.354 * n_ratio_average / 100 + 94.845;
}
else
{
// 如果计算得到的AC/DC比值不在有效范围内,设置SPO2为无效值
*pn_spo2 = -999;
*pch_spo2_valid = 0;
}
}
void maxim_find_peaks(int32_t pn_locs, int32_t pn_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, pn_npks, pn_x, n_size, n_min_height );
maxim_remove_close_peaks( pn_locs, pn_npks, pn_x, n_min_distance );
pn_npks = min( pn_npks, n_max_num );
}
void maxim_peaks_above_min_height(int32_t pn_locs, int32_t pn_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, n_width;
*pn_npks = 0;
while (i < n_size-1){
if (pn_x[i] > n_min_height && pn_x[i] > pn_x[i-1]){ // find left edge of potential peaks
n_width = 1;
while (i+n_width < n_size && pn_x[i] == pn_x[i+n_width]) // find flat peaks
n_width++;
if (pn_x[i] > pn_x[i+n_width] && (*pn_npks) < 15 ){ // find right edge of peaks
pn_locs[(*pn_npks)++] = i;
// for flat peaks, peak location is left edge
i += n_width+1;
}
else
i += n_width;
}
else
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 longo 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;
}
}
- 作者:L_Z_J
- 链接:https://www.mcoi.top/article/Post-MAX30102
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。









