项目设计
本次参加了硬禾与digikey推出的fastbond活动,选择了可穿戴测量题目,做一个心率测量的原型机,要求使用到ADI与MAXIM的芯片。
那我选择的传感器是MAX30102是一款集成脉搏血氧饱和度和心率监测传感器,在网络上很容易就能找到它的硬件和软件开发资料,测量的原理是用传感器内部的两个不同波段led向人体组织上发射灯光,并测量反射的信号强度,测算出心跳速率和血氧浓度。
那么我又搭配采购了一个ADI的LDO稳压芯片ADP7158,用于整个传感器部分的供电,这样可以减小因为供电不稳定造成的测量误差。
MAX30102相关介绍:用于可穿戴健康设备的高灵敏度脉搏血氧仪和心率传感器
官网:https://www.maximintegrated.com/cn/products/interface/sensor-interface/MAX30102.html
ADP7158相关介绍:2 A、超低噪声、高PSRR、固定输出、RF线性稳压器
官网:https://www.analog.com/cn/products/adp7158.html
开发过程
确定好传感器和电源部分,接下来就是选择MCU将各部分信号收集起来,并作出相应的决策。我找出了MAX32660评估板,(顺便一提,这也是一款来自美信的产品哦)
查阅有关数据手册得知,芯片接口为I2C协议,从机地址为0b1010111(0x57),当然手册中使用的8位地址模式,即为0xAE和0xAF
下一步,写相关传感器驱动程序
美信提供了全套的算法代码包括噪声滤除,求峰峰之间的距离,血样饱和度映射等等
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_nume*100)/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.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;
}
}
实验现象
如截图所示:
可以看见心跳的原始数据和一阶差分数据,算法就是基于此计算出实际心率信息,右侧数据也输出了心跳BPM
总结
Fastbond是一个综合性的项目,既有软件编程,也有硬件设计,对于平时专注于布线的硬件工程师来说,这个机会非常棒,走出舒适圈,接触到一些程序设计及算法知识。