繼口罩、抗原、藥品之后,最近電子血氧儀的價格也開始水漲船高。從一個多月前的100多元,暴漲到了300多元。
那么,這類家用的電子血氧儀是如何工作的呢?測量數據到底準不準?今天就帶大家來分析一下。

血氧儀是一種監測脈搏、血氧飽和度等指標的醫療器械,常見的家用型血氧儀,主要有指夾式、腕表式等形式。
一般大家最關注的是血氧飽和度(oxygen saturation簡寫為SpO2),它是指在全部血容量中被結合O2容量占全部可結合的O2容量的百分比,是人體攜帶氧氣能力的重要參考值。人體正常的SpO2應該不小于95%,長期低于93%時需要就醫。
SpO2 一般由以下公式計算:

其中CHbO2是氧合血紅蛋白濃度,CHb是還原血紅蛋白濃度。
一方面,這兩種血紅蛋白對不同波長的光有不同的吸收度;另一方面,當動脈跳動時,動脈中的血液量會發生變化,可以區分出皮膚、肌肉、靜脈血等對光的吸收影響(這些組織對光的吸收可以認為固定不變)。因此,利用兩種不同波長的光,經透射或反射后,采集數據綜合處理,就能計算出血氧飽和度。
現在市面上最常見的,都是光電式的血氧儀,如下圖所示,有透射式和反射式兩種實現方法。
常見的指夾式血氧儀就是透射式,智能手環或手表就是反射式,原理是差不多的。

而LED光源的選擇,與血紅蛋白對不同光波長的吸收率有關,下圖是兩種血紅蛋白對不同波長的光的消光系數圖:

可以看到,兩種血紅蛋白對波長為660nm左右光的吸收差別最大,而對波長為800nm左右光的吸收基本相等。
從理論上說,使用660nm和800nm波長的光作為光源是最合適的,但由于在800nm左右時,二者的消光系數斜率相差較大,光波長偏差一點就會引起較大的吸收率變化,這對LED的制造工藝要求太高。所以,工程實現時,一般不用800nm波長的LED,而選擇波長為860nm~920nm的LED作為另一個光源,這個區間的消光系數斜率基本一樣,而且變化平緩。
至此,硬件部分的實現我們已經了解大概了,其實核心就是要使用兩個LED作為光源,一個660nm波長的紅外光,一個900nm左右波長的紅光。兩束光分別通過透射(或反射)皮膚后,到達光電接收管,再采集光電接收管的值。
那么,采集到兩個光源的值后,又該如何處理呢?這里由于有比較多的公式推導,我們直接略過,給出下面的公式:

這里的實現需要三步:
第一步,我們采集的兩個LED光源的值,需要分離出直流分量和交流分量,也就是:紅光的交流分量ACred、紅光的直流分量DCred、紅外光的交流分量ACired、紅外光的直流分量DCired;
第二步,用采集到的四個值,計算出R;
第三步,用R計算SpO2,這個計算公式中a、b、c是三個需要校準的參數。需要大量的試驗數據去擬合出來。
有了以上的理論基礎,我們可以自己動手DIY一個血氧儀。
Maxim公司有一款集成芯片,可以實現大部分的硬件功能,就是MAX30100、MAX30102系列芯片。MAX30100已停產,新設計中不推薦使用,MAX30102是新一代產品。
目前價格還沒有太離譜:

MAX30102集成了一個660nm紅光LED、880nm紅外光LED、光電檢測器,以及帶環境光抑制的低噪聲電子電路。芯片內部含18bit ADC采集電路。對外是I2C接口。基本上單芯片就能實現光源信號的采集。
要注意,MAX30102的輸出值,只是兩個LED光源的采集值。后續還需要軟件去實現交流、直流分離,R的求解、SpO2的求解。順帶也可以求解出脈搏數據。
使用max30102很簡單,用I2C接口訪問,初始化代碼如下:
max30102_Bus_Write(REG_INTR_ENABLE_1,0xc0); // INTR settingmax30102_Bus_Write(REG_INTR_ENABLE_2,0x00);max30102_Bus_Write(REG_FIFO_WR_PTR,0x00); //FIFO_WR_PTR[4:0]max30102_Bus_Write(REG_OVF_COUNTER,0x00); //OVF_COUNTER[4:0]max30102_Bus_Write(REG_FIFO_RD_PTR,0x00); //FIFO_RD_PTR[4:0]max30102_Bus_Write(REG_FIFO_CONFIG,0x0f); //sample avg = 1, fifo rollover=false, fifo almost full = 17max30102_Bus_Write(REG_MODE_CONFIG,0x03); //0x02 for Red only, 0x03 for SpO2 mode 0x07 multimode LEDmax30102_Bus_Write(REG_SPO2_CONFIG,0x27); // SPO2_ADC range = 4096nA, SPO2 sample rate (100 Hz), LED pulseWidth (400uS)max30102_Bus_Write(REG_LED1_PA,0x24); //Choose value for ~ 7mA for LED1max30102_Bus_Write(REG_LED2_PA,0x24); // Choose value for ~ 7mA for LED2max30102_Bus_Write(REG_PILOT_PA,0x7f); // Choose value for ~ 25mA for Pilot LED
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;//read and clear status registermaxim_max30102_read_reg(REG_INTR_STATUS_1, &uch_temp);maxim_max30102_read_reg(REG_INTR_STATUS_2, &uch_temp);IIC_ReadBytes(I2C_WRITE_ADDR,REG_FIFO_DATA,(u8 *)ach_i2c_data,6);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]}
采集值最好經過濾波,以減少噪聲的干擾。
之后,再分離出交流、直流分量,求出R和SpO2即可,核心是這個函數:
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){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 signalun_ir_mean =0;for (k=0 ; kun_ir_mean =un_ir_mean/n_ir_buffer_length ;for (k=0 ; k// 4 pt Moving Averagefor(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; kan_dx[k]= (an_x[k+1]- an_x[k]);// 2-pt Moving Average to an_dxfor(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 detectorfor ( i=0 ; is= 0;for( k=i; ks -= an_dx[k] *auw_hamm[k-i] ;}an_dx[i]= s/ (int32_t)1146; // divide by sum of auw_hamm????}n_th1=0; // threshold calculationfor ( k=0 ; kn_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 signalmaxim_find_peaks( an_dx_peak_locs, &n_npks, an_dx, BUFFER_SIZE-HAMMING_SIZE, n_th1, 8, 5 );//peak_height, peak_distance, max_num_peaksn_peak_interval_sum =0;if (n_npks>=2){for (k=1; kn_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????????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 ; kan_x[k] = pun_ir_buffer[k] ;an_y[k] = pun_red_buffer[k] ;}// find precise min near an_ir_valley_locsn_exact_ir_valley_locs_count =0;for(k=0 ; kun_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;iif (an_x[i]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 MAfor(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 locationsn_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 SPO2for (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]); //redn_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 rawn_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]); // irn_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 rawn_nume=( n_y_ac *n_x_dc_max)>>7 ; //prepare X100 to preserve floating valuen_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*20)/n_denom ; //formular is ( n_y_ac *n_x_dc_max) / ( n_x_ac *n_y_dc_max) ; ///*************************n_nume原來是*100************************//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 medianelsen_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 ratio is out of range*pch_spo2_valid = 0;}}


SpO2 = -45.060*R*R+ 30.354*R+ 94.845
SpO2 = -7.6*R*- 20.7*R+ 112.2,(0.5
SpO2 = -86.47*R*R+ 77.21*R+ 81.68,(0.4



END
→點關注,不迷路←