当前位置:首页 > 消费电子 > 消费电子
[导读]手环心率不准,八成不是传感器烂,是固件把原始PPG直接拿去数峰。手腕绿光反射信号里混着运动伪影、环境光、基线漂移、工频噪声,原始计数一跳心率就飙到120。 正确链路是:定时采样→去基线→带通/滑动滤波→信号质量判断→峰值检测算IBI→运动段切自相关兜底。


手环心率不准,八成不是传感器烂,是固件把原始PPG直接拿去数峰。手腕绿光反射信号里混着运动伪影、环境光、基线漂移、工频噪声,原始计数一跳心率就飙到120。 正确链路是:定时采样→去基线→带通/滑动滤波→信号质量判断→峰值检测算IBI→运动段切自相关兜底。

一、采样:别全程100Hz硬采

MAX30102类传感器绿光通道够用,静止时25~50Hz、运动时才拉到100Hz。采样进环形缓冲,DMA搬运别在主循环读I2C。

#define PPG_FS 50

#define BUF_LEN 256

static int32_t raw_buf[BUF_LEN];

static uint16_t wi;


void ppg_dma_cb(int32_t v){

   raw_buf[wi] = v;

   wi = (wi+1)%BUF_LEN;

}

LED驱动电流按肤色/佩戴自动调增益,别固定满流,不然强光下ADC饱和整段废掉。

二、去基线漂移

PPG基线漂移来自呼吸、佩戴压力变化,频率<0.5Hz,和脉搏频带叠在一起。 手环MCU别上高阶高通,直接用“长窗滑动平均”扣基线:

#define BASE_WIN 32   /* 约0.6s@50Hz */

static int32_t base_sum;

static int32_t base_buf[BASE_WIN];

static uint8_t bi;


int32_t ppg_remove_baseline(int32_t v){

   base_sum -= base_buf[bi];

   base_buf[bi] = v;

   base_sum += v;

   bi = (bi+1)%BASE_WIN;

   int32_t base = base_sum/BASE_WIN;

   return v - base;          /* AC分量 */

}

长窗只跟慢漂移、不吞脉搏波;窗口别太短,否则基线跟着峰一起动。

三、轻量带通:滑动平均+差分

完整Butterworth在M0上没必要,两段滑动平均就能压高频毛刺:

#define LP_WIN 4

static int32_t lp_buf[LP_WIN];

static uint8_t li;


int32_t ppg_lowpass(int32_t ac){

   lp_buf[li]=ac; li=(li+1)%LP_WIN;

   int32_t s=0; for(int i=0;i<LP_WIN;i++) s+=lp_buf[i];

   return s/LP_WIN;

}

想要再干净点,接一个二阶biquad带通0.5~3Hz(对应30~180bpm),系数用工具算好写常量,别运行时算。

四、信号质量:先判能不能算

运动伪影一来,峰值检测必炸。先用加速度计或PPG高频能量估“可信度”:

uint8_t ppg_signal_quality(int32_t ac_arr[], int n){

   int32_t mx=-1e9, mn=1e9;

   for(int i=0;i<n;i++){

       if(ac_arr[i]>mx)mx=ac_arr[i];

       if(ac_arr[i]<mn)mn=ac_arr[i];

   }

   int32_t amp = mx-mn;

   if(amp < MIN_AMP) return 0;     /* 信号太弱 */

   return 1;

}

振幅过小、加速度突变超过阈值,这段直接标低置信,不更新HR或只给自相关估。

五、峰值检测算心率

核心是用“动态阈值+脉宽校验”找收缩峰,峰间间隔IBI算bpm:

static int32_t thr;

static uint8_t rising;

static uint32_t last_peak;


uint16_t ppg_peak_hr(int32_t ac, uint32_t tms){

   static int32_t ema=0;

   ema = ema + (ac-ema)*3/16;          /* 基线幅值跟踪 */

   thr = ema + (ABS(ac-ema)>>2) + 8;   /* 动态阈值 */


   if(!rising && ac>thr){

       rising=1;

   }else if(rising && ac<thr){

       rising=0;

       uint32_t ibi = tms-last_peak;

       last_peak = tms;

       if(ibi>200 && ibi<1500){        /* 40~300bpm合理窗 */

           return 60000/ibi;

       }

   }

   return 0;   /* 0表示本次无有效峰 */

}

要点:

阈值跟EMA走,别写死常数

脉宽/IBI限窗过滤抖动尖峰

连续多个IBI取中位数,别每峰刷新显示

返回值0时不覆盖上一次HR

六、运动段切自相关

跑步、摆手时峰值法误检严重,自相关更稳:

uint16_t ppg_autocorr_hr(int32_t buf[], int n, uint32_t fs){

   int lag_best=0; int32_t corr_best=0;

   for(int lag=fs/4; lag<fs*2; lag++){   /* 30~240bpm */

       int64_t c=0;

       for(int i=0;i+lag<n;i++) c+=(int64_t)buf[i]*buf[i+lag];

       if(c>corr_best){ corr_best=c; lag_best=lag; }

   }

   return lag_best? 60*fs/lag_best : 0;

}

静止走峰值法,运动走自相关,输出再做滑动中位滤波,跳变能从±30bpm压到±8bpm。

七、落坑清单

原始raw直接数峰→基线一漂全错

阈值写死常数→佩戴松紧一变就漏

高阶IIR在M0上爆精度→用biquad常量系数

运动段还信峰值→心率乱跳

每拍刷新显示→数字抖动

LED常亮100Hz→耗电极高

不判信号质量→弱灌注也硬出数

IBI不限窗→偶发尖峰算出300bpm

加速度不辅助→跑步必翻车

八、落地顺序

DMA采绿光通道,25~50Hz

长窗扣基线得AC

滑动平均/轻量biquad去高频

振幅+ACC判信号质量

静止:动态阈值峰值检测

运动:自相关估频

IBI中位滤波输出HR

低灌注/强运动标“测量中”

手环心率精准度不在算法多高级,在“脏数据别硬算”。去基线保住波形、动态阈值抗佩戴变化、运动段切自相关,这三件事做对,静态误差±3bpm、走路±5bpm都拿得到;剩下的是光学结构遮光和皮肤接触——那是硬件下限,固件救不了。量产固件别追求每次心跳都出数,宁可间隔2秒给一次稳定值,也别数字狂跳显得“灵敏”。



本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
关闭