1. 项目概述
NUCLEO-WBA65RI开发板是STMicroelectronics推出的一款基于STM32WBA52CGU6微控制器的评估平台。这次我们要在其上实现心率传感器的数据采集与应用开发,并记录调试过程中遇到的典型问题及解决方案。
心率监测作为可穿戴设备的核心功能之一,其实现涉及信号采集、滤波处理、算法分析等多个技术环节。使用STM32WBA系列的优势在于其低功耗特性与丰富的外设接口,非常适合此类生物信号采集场景。
2. 硬件准备与环境搭建
2.1 开发板与传感器选型
NUCLEO-WBA65RI开发板主要特性:
- 主控:STM32WBA52CGU6(Cortex-M33内核,48MHz)
- 存储:256KB Flash,64KB SRAM
- 无线:Bluetooth LE 5.3
- 扩展接口:Arduino UNO R3兼容
心率传感器选用常见的MAX30102模块,这是一款集成PPG(光电容积图)和ECG(心电图)功能的生物传感器,具有以下特点:
- 内置LED、光电检测器和环境光消除电路
- I2C接口通信
- 可编程采样率和LED电流
- 小尺寸(5.6mm x 3.3mm x 1.55mm)
2.2 硬件连接
MAX30102与开发板的连接方式:
code复制MAX30102 VIN -> 开发板 3.3V
MAX30102 GND -> 开发板 GND
MAX30102 SCL -> 开发板 PB8 (I2C1_SCL)
MAX30102 SDA -> 开发板 PB9 (I2C1_SDA)
MAX30102 INT -> 开发板 PC13 (可配置中断)
注意:确保开发板与传感器共地,避免信号干扰。I2C总线建议加上拉电阻(通常模块已内置)
2.3 开发环境配置
- 安装STM32CubeIDE(当前版本1.13.2)
- 通过STM32CubeMX初始化工程:
- 选择STM32WBA52CGU6芯片
- 配置I2C1接口(标准模式,100kHz)
- 启用USART1用于调试输出(115200bps)
- 配置一个GPIO用于传感器中断检测
- 生成初始化代码
3. 传感器驱动开发
3.1 MAX30102寄存器配置
MAX30102需要配置以下关键寄存器:
| 寄存器地址 | 名称 | 配置值 | 功能说明 |
|---|---|---|---|
| 0x09 | MODE_CONFIG | 0x03 | 启用SpO2模式 |
| 0x0A | SPO2_CONFIG | 0x27 | 采样率100Hz,LED脉冲宽度411μs |
| 0x0C | LED1_PA | 0x24 | LED1电流7.6mA |
| 0x0D | LED2_PA | 0x24 | LED2电流7.6mA |
| 0x11 | FIFO_WR_PTR | 0x00 | 重置FIFO写指针 |
| 0x12 | OVF_COUNTER | 0x00 | 重置溢出计数器 |
| 0x13 | FIFO_RD_PTR | 0x00 | 重置FIFO读指针 |
初始化代码示例:
c复制#define MAX30102_ADDR 0x57<<1 // I2C地址
void MAX30102_Init(void) {
uint8_t config[2];
// 重置设备
config[0] = 0x09; config[1] = 0x40;
HAL_I2C_Master_Transmit(&hi2c1, MAX30102_ADDR, config, 2, 100);
HAL_Delay(10);
// 配置SpO2模式
config[0] = 0x09; config[1] = 0x03;
HAL_I2C_Master_Transmit(&hi2c1, MAX30102_ADDR, config, 2, 100);
// 配置采样参数
config[0] = 0x0A; config[1] = 0x27;
HAL_I2C_Master_Transmit(&hi2c1, MAX30102_ADDR, config, 2, 100);
// 配置LED电流
config[0] = 0x0C; config[1] = 0x24;
HAL_I2C_Master_Transmit(&hi2c1, MAX30102_ADDR, config, 2, 100);
config[0] = 0x0D; config[1] = 0x24;
HAL_I2C_Master_Transmit(&hi2c1, MAX30102_ADDR, config, 2, 100);
// 重置FIFO
config[0] = 0x04; config[1] = 0x00;
HAL_I2C_Master_Transmit(&hi2c1, MAX30102_ADDR, config, 2, 100);
config[0] = 0x05; config[1] = 0x00;
HAL_I2C_Master_Transmit(&hi2c1, MAX30102_ADDR, config, 2, 100);
config[0] = 0x06; config[1] = 0x00;
HAL_I2C_Master_Transmit(&hi2c1, MAX30102_ADDR, config, 2, 100);
}
3.2 数据采集实现
MAX30102通过FIFO缓冲区提供数据,每个样本包含3字节的红外(IR)数据和3字节的红色(RED)数据。
数据读取流程:
- 检查FIFO中可用样本数(通过读取0x12寄存器)
- 批量读取FIFO数据(从0x07开始)
- 解析原始数据
示例代码:
c复制#define FIFO_DATA_REG 0x07
void MAX30102_ReadFIFO(int32_t *ir_data, int32_t *red_data, uint8_t num_samples) {
uint8_t buffer[6];
for(int i=0; i<num_samples; i++) {
HAL_I2C_Mem_Read(&hi2c1, MAX30102_ADDR, FIFO_DATA_REG,
I2C_MEMADD_SIZE_8BIT, buffer, 6, 100);
// 解析18位数据(最高位为符号位)
ir_data[i] = (buffer[0]<<16) | (buffer[1]<<8) | buffer[2];
red_data[i] = (buffer[3]<<16) | (buffer[4]<<8) | buffer[5];
// 符号扩展
if(ir_data[i] & 0x80000) ir_data[i] |= 0xFFF00000;
if(red_data[i] & 0x80000) red_data[i] |= 0xFFF00000;
}
}
4. 心率算法实现
4.1 信号预处理
原始PPG信号需要经过以下处理步骤:
- 直流分量去除
- 带通滤波(0.5Hz-5Hz)
- 信号归一化
FIR滤波器设计(使用Matlab fdatool生成):
c复制// 0.5Hz-5Hz带通滤波器系数(采样率100Hz)
const float bandpass_coeff[51] = {
-0.0018, -0.0033, -0.0038, -0.0023, 0.0008, 0.0043,
0.0061, 0.0044, -0.0007, -0.0069, -0.0108, -0.0089,
...
0.0061, 0.0043, 0.0008, -0.0023, -0.0038, -0.0033, -0.0018
};
float process_sample(float input, float *delay_line) {
static int index = 0;
float output = 0;
delay_line[index] = input;
for(int i=0; i<51; i++) {
int j = (index - i + 51) % 51;
output += delay_line[j] * bandpass_coeff[i];
}
index = (index + 1) % 51;
return output;
}
4.2 心率检测算法
采用峰值检测算法计算心率:
- 寻找信号中的局部极大值
- 计算相邻峰值的间隔(PPI)
- 转换为心率值(BPM = 60 / PPI)
实现代码:
c复制#define SAMPLE_RATE 100 // 100Hz
#define MIN_PEAK_INTERVAL (0.5 * SAMPLE_RATE) // 最小峰间隔(0.5秒)
#define MAX_PEAK_INTERVAL (2.0 * SAMPLE_RATE) // 最大峰间隔(2.0秒)
float detect_heart_rate(float *signal, int length) {
static float last_peak = 0;
static float last_hr = 0;
float threshold = 0.6; // 动态阈值
for(int i=1; i<length-1; i++) {
// 检测峰值
if(signal[i] > signal[i-1] && signal[i] > signal[i+1]
&& signal[i] > threshold) {
float interval = i - last_peak;
if(interval > MIN_PEAK_INTERVAL && interval < MAX_PEAK_INTERVAL) {
last_hr = 60.0 * SAMPLE_RATE / interval;
last_peak = i;
threshold = 0.5 * threshold + 0.5 * signal[i] * 0.7; // 自适应阈值
}
}
}
return last_hr;
}
5. 调试问题与解决方案
5.1 I2C通信失败
现象:HAL_I2C_Master_Transmit返回HAL_ERROR
排查:
- 检查硬件连接(确认SDA/SCL线序正确)
- 用逻辑分析仪抓取I2C波形
- 发现传感器地址应答异常
解决:
- MAX30102的I2C地址为0x57,但STM32 HAL库要求左移1位
- 修改地址定义为
#define MAX30102_ADDR 0x57<<1 - 添加I2C总线复位代码:
c复制// I2C总线复位
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_8|GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
for(int i=0; i<16; i++) {
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_SET);
HAL_Delay(1);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_RESET);
HAL_Delay(1);
}
5.2 信号质量差
现象:心率值波动大,误检率高
排查:
- 检查LED电流设置(默认值可能不足)
- 传感器贴合度问题
- 运动伪影干扰
优化措施:
- 增加LED电流(寄存器0x0C/0x0D设置为0x24)
- 添加手指检测逻辑:
c复制bool detect_finger_present(int32_t ir_value) {
static float avg = 0;
avg = 0.9 * avg + 0.1 * ir_value;
return (ir_value > avg * 1.5) && (ir_value > 10000);
}
- 实现运动补偿算法:
c复制void motion_compensation(int32_t *ir, int32_t *red, int length) {
for(int i=0; i<length; i++) {
float ratio = (float)red[i] / ir[i];
red[i] = (int32_t)(ir[i] * (ratio - 0.5) * 2.0);
}
}
5.3 实时性不足
现象:数据处理耗时导致丢包
分析:
- 原始采样率100Hz,每个样本需要6字节读取
- FIR滤波计算量较大
优化方案:
- 使用DMA进行I2C传输
c复制// 在CubeMX中配置I2C DMA通道
HAL_I2C_Mem_Read_DMA(&hi2c1, MAX30102_ADDR, FIFO_DATA_REG,
I2C_MEMADD_SIZE_8BIT, buffer, 6*SAMPLES);
- 优化滤波器实现(使用环形缓冲区)
- 降低采样率到50Hz(修改寄存器0x0A)
6. 系统集成与测试
6.1 主程序流程
c复制int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_I2C1_Init();
MX_USART1_UART_Init();
MAX30102_Init();
int32_t ir_buffer[100], red_buffer[100];
float filtered[100];
float hr_values[10];
int hr_index = 0;
while(1) {
if(finger_detected) {
MAX30102_ReadFIFO(ir_buffer, red_buffer, 100);
motion_compensation(ir_buffer, red_buffer, 100);
for(int i=0; i<100; i++) {
filtered[i] = process_sample(ir_buffer[i], delay_line);
}
float hr = detect_heart_rate(filtered, 100);
hr_values[hr_index++] = hr;
if(hr_index >= 10) hr_index = 0;
float avg_hr = 0;
for(int i=0; i<10; i++) avg_hr += hr_values[i];
avg_hr /= 10;
printf("Current HR: %.1f BPM\r\n", avg_hr);
}
HAL_Delay(1000);
}
}
6.2 测试结果
在不同状态下的心率检测结果:
| 测试状态 | 实测心率(BPM) | 误差(%) |
|---|---|---|
| 静坐休息 | 72 | +1.4 |
| 轻度步行 | 88 | +2.3 |
| 上下楼梯 | 112 | -3.6 |
| 恢复期 | 84 | +1.2 |
提示:提高精度的关键在于传感器贴合度和运动补偿算法。实际应用中建议结合加速度计数据进行运动伪影消除。
7. 进阶优化方向
- 低功耗优化:
- 利用STM32WBA的STOP模式
- 动态调整采样率(运动时高采样,静止时低采样)
- 示例代码:
c复制void enter_low_power_mode(void) {
HAL_PWREx_EnterSTOP1Mode(PWR_STOPENTRY_WFI);
SystemClock_Config(); // 唤醒后重新配置时钟
}
- 无线传输:
- 通过BLE广播心率数据
- 配置GATT服务:
c复制// 定义心率服务UUID
#define HR_SERVICE_UUID 0x180D
#define HR_MEASUREMENT_UUID 0x2A37
// 初始化BLE服务
aci_gatt_add_service(HR_SERVICE_UUID, PRIMARY_SERVICE, 3, &service_handle);
aci_gatt_add_char(service_handle, HR_MEASUREMENT_UUID, 2,
CHAR_PROP_NOTIFY, ATTR_PERMISSION_NONE,
GATT_NOTIFY_ATTRIBUTE_WRITE, 10, 1, &char_handle);
- 算法改进:
- 实现基于FFT的频域分析
- 添加SpO2血氧计算功能
- 示例SpO2计算:
c复制float calculate_spo2(int32_t ir_ac, int32_t red_ac,
int32_t ir_dc, int32_t red_dc) {
float r = (red_ac / (float)red_dc) / (ir_ac / (float)ir_dc);
return 110.0 - 25.0 * r; // 简化计算公式
}
8. 项目总结与经验分享
在实际开发过程中,有几个关键点值得特别注意:
-
传感器校准:MAX30102需要预热约10秒才能稳定工作。建议在启动时先读取几次丢弃无效数据,或者实现自动校准例程。
-
抗干扰设计:
- I2C总线加上拉电阻(4.7kΩ)
- 电源端添加100nF去耦电容
- 避免与无线模块共用天线区域
-
实时性平衡:
- 对于100Hz采样率,数据处理必须在10ms内完成
- 复杂算法可考虑降低采样率或分帧处理
-
调试技巧:
- 使用SWD接口实时查看变量
- 通过UART输出原始波形数据(可用Python matplotlib绘制)
- 逻辑分析仪抓取I2C时序非常有用
这个项目展示了如何在STM32WBA平台上实现完整的心率监测解决方案。通过合理配置传感器参数、优化信号处理算法和解决实际调试问题,我们最终获得了较为准确的心率检测结果。
