如何基于AFE49I30传感器的ECG/PPG字节数组计算心率(BPM)
基于AFE49I30传感器的PPG/ECG心率计算实现(Xamarin.Forms)
一、字节数组解析前置步骤
AFE49I30的蓝牙传输字节是按传感器输出格式封装的,首先需将字节数组转换为原始PPG/ECG采样值:
1. 确定采样数据的字节格式
AFE49I30的PPG和ECG输出一般为16位有符号整数(需匹配硬件配置的12/14/16位输出调整),字节顺序通常为小端模式。解析逻辑如下:
PPG/ECG采样值解析代码
// 解析字节数组为采样值列表 private List<int> ParseRawSamples(byte[] receivedBytes, int sampleSize = 2) { var samples = new List<int>(); // 按每个采样占sampleSize字节拆分 for (int i = 0; i < receivedBytes.Length; i += sampleSize) { if (i + sampleSize > receivedBytes.Length) break; // 小端转int16 short rawValue = BitConverter.ToInt16(receivedBytes, i); // 若为12/14位采样,需做位对齐(示例:12位输出右移4位) // int adjustedValue = rawValue >> (16 - 12); samples.Add(rawValue); } return samples; }
二、心率计算核心逻辑
1. ECG心率计算(R波检测法)
ECG心率依赖检测QRS波群中的R波峰值,步骤如下:
- 滤波:去除基线漂移(滑动平均)和高频噪声(低通滤波)
- R波检测:设置动态阈值(当前窗口最大值的70%左右),捕捉超过阈值的峰值点
- BPM计算:通过相邻R波的时间间隔(RR间期)转换为心率:
BPM = 60 / (RR间期秒数)
实现代码示例
// 全局变量:存储R波时间戳、滑动窗口数据 private List<long> _rWaveTimestamps = new List<long>(); private const int EcgWindowSize = 100; // 滑动窗口大小 private List<int> _ecgBuffer = new List<int>(); private void CalculateEcgBpm(List<int> ecgSamples, long currentTimestampMs) { _ecgBuffer.AddRange(ecgSamples); // 维持窗口大小 if (_ecgBuffer.Count > EcgWindowSize) _ecgBuffer.RemoveRange(0, _ecgBuffer.Count - EcgWindowSize); // 1. 滑动平均滤波去基线 var filteredEcg = ApplyMovingAverage(_ecgBuffer, 5); // 2. 检测R波峰值 int threshold = (int)(filteredEcg.Max() * 0.7); for (int i = 1; i < filteredEcg.Count - 1; i++) { if (filteredEcg[i] > threshold && filteredEcg[i] > filteredEcg[i-1] && filteredEcg[i] > filteredEcg[i+1]) { // 去重:避免连续帧重复检测同一R波 if (_rWaveTimestamps.Count == 0 || currentTimestampMs - _rWaveTimestamps.Last() > 300) { _rWaveTimestamps.Add(currentTimestampMs); // 仅保留最近5个R波数据 if (_rWaveTimestamps.Count > 5) _rWaveTimestamps.RemoveAt(0); } } } // 3. 计算BPM if (_rWaveTimestamps.Count >= 2) { long totalIntervalMs = 0; for (int i = 1; i < _rWaveTimestamps.Count; i++) { totalIntervalMs += _rWaveTimestamps[i] - _rWaveTimestamps[i-1]; } long averageIntervalMs = totalIntervalMs / (_rWaveTimestamps.Count - 1); int bpm = (int)(60000.0 / averageIntervalMs); // 更新UI显示 // HeartRateLabel.Text = $"{bpm} BPM"; } } // 滑动平均滤波 private List<int> ApplyMovingAverage(List<int> data, int windowSize) { var result = new List<int>(); for (int i = 0; i < data.Count; i++) { int sum = 0; int count = 0; for (int j = Math.Max(0, i - windowSize/2); j <= Math.Min(data.Count-1, i + windowSize/2); j++) { sum += data[j]; count++; } result.Add(sum / count); } return result; }
2. PPG心率计算(峰值检测法)
PPG是光电容积脉搏波,心率计算依赖检测脉搏波峰值:
- 滤波:去除运动噪声(自适应滤波或带通滤波)和直流分量
- 峰值检测:设置动态阈值(基于实时信号波动调整)
- BPM计算:通过相邻峰值的时间间隔转换为心率
实现代码示例
// 全局变量:存储PPG峰值时间戳、滑动窗口 private List<long> _ppgPeakTimestamps = new List<long>(); private const int PpgWindowSize = 150; private List<int> _ppgBuffer = new List<int>(); private void CalculatePpgBpm(List<int> ppgSamples, long currentTimestampMs) { _ppgBuffer.AddRange(ppgSamples); if (_ppgBuffer.Count > PpgWindowSize) _ppgBuffer.RemoveRange(0, _ppgBuffer.Count - PpgWindowSize); // 1. 带通滤波(去除基线和高频噪声) var filteredPpg = ApplyBandPassFilter(_ppgBuffer); // 2. 检测脉搏波峰值 int baseline = (int)filteredPpg.Average(); int peakThreshold = baseline + (int)(filteredPpg.Max() - baseline) * 0.6; for (int i = 1; i < filteredPpg.Count - 1; i++) { if (filteredPpg[i] > peakThreshold && filteredPpg[i] > filteredPpg[i-1] && filteredPpg[i] > filteredPpg[i+1]) { if (_ppgPeakTimestamps.Count == 0 || currentTimestampMs - _ppgPeakTimestamps.Last() > 400) { _ppgPeakTimestamps.Add(currentTimestampMs); if (_ppgPeakTimestamps.Count > 5) _ppgPeakTimestamps.RemoveAt(0); } } } // 3. 计算BPM if (_ppgPeakTimestamps.Count >= 2) { long totalIntervalMs = 0; for (int i = 1; i < _ppgPeakTimestamps.Count; i++) { totalIntervalMs += _ppgPeakTimestamps[i] - _ppgPeakTimestamps[i-1]; } long averageIntervalMs = totalIntervalMs / (_ppgPeakTimestamps.Count - 1); int bpm = (int)(60000.0 / averageIntervalMs); // 更新UI // PpgHeartRateLabel.Text = $"{bpm} BPM"; } } // 简单带通滤波(去除基线和高频) private List<int> ApplyBandPassFilter(List<int> data) { // 滑动平均去基线 var noBaseline = ApplyMovingAverage(data, 20); // 差分去高频 var filtered = new List<int>(); for (int i = 1; i < noBaseline.Count; i++) { filtered.Add(noBaseline[i] - noBaseline[i-1]); } return filtered; }
三、集成到现有代码中
修改你的GetPPGLiveData和GetECGLiveData方法,加入解析与计算逻辑:
修改后的PPG数据处理
PPGList.ValueUpdated += (o, args) => { var receivedBytes = args.Characteristic.Value; var currentTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); Xamarin.Essentials.MainThread.BeginInvokeOnMainThread(() => { var ppgSamples = ParseRawSamples(receivedBytes); CalculatePpgBpm(ppgSamples, currentTimestamp); }); };
修改后的ECG数据处理
ECGList.ValueUpdated += (o, args) => { var receivedBytes = args.Characteristic.Value; var currentTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); Xamarin.Essentials.MainThread.InvokeOnMainThreadAsync(() => { var ecgSamples = ParseRawSamples(receivedBytes); CalculateEcgBpm(ecgSamples, currentTimestamp); return Task.CompletedTask; }); };
四、优化建议
- 硬件配置对齐:确认AFE49I30的采样率、输出位宽,调整解析代码中的位对齐逻辑,避免采样值失真
- 滤波优化:ECG可替换为巴特沃斯低通滤波,PPG可结合加速度传感器数据做运动 artifact 检测
- 阈值自适应:根据实时信号波动动态调整峰值检测阈值,提升不同场景下的准确率
- 线程安全:全局采样缓存和时间戳列表需加锁,避免多线程读写冲突
- 异常处理:加入字节数组长度检查、采样值范围校验,避免计算时出现异常
内容的提问来源于stack exchange,提问作者Makwana Prahlad
相关产品推荐
相关产品推荐

