智能手环心率采集编程:实现PPG信号的噪声过滤与心率精准计算
手环心率不准,八成不是传感器烂,是固件把原始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秒给一次稳定值,也别数字狂跳显得“灵敏”。





