STM32心率监测毕设实战:从传感器选型到低功耗架构设计
做毕设最怕“看起来简单,一动手就翻车”。心率监测项目尤其如此:传感器一上手腕,波形全是毛刺;跑个滤波,MCU 直接睡死;好不容易把数据稳住,电池半天就报警。下面把我自己踩过的坑、调通的代码、验证过的低功耗策略,按“从信号到电量”的顺序拆给你看,照着做至少能把 demo 撑到答辩现场不掉链子。
1. 毕设常见三宗“最”:运动伪影、电源纹波、算法漂移
运动伪影
PPG(光电容积)信号幅度只有几十毫伏,手一抖、表带一松,直流分量瞬间漂移几百毫伏,直接把心率峰“淹没”。
对策:硬件端用黑色硅胶罩光+软表带减少漏光;软件端在 ADC 后级加一阶高通(0.5 Hz)先把直流砍掉,再送进滤波器。电源纹波
锂电池瞬时电流 100 mA 跳变,LDO 输出纹波 20 mV,经 LED 到 PD 放大后,纹波被“增益”到 200 mmV,波形像锯齿。
对策:LED 单独 LDO(如 XC6206P302)供电,走线先经 22 µF 钽电容+磁珠,再进传感器;MCU 侧模拟地与数字地单点连接。算法漂移
峰值检测阈值固定,信号强度一变就漏峰或倍频。
对策:动态阈值——每 4 s 更新一次“最大-最小”的 40 % 作为门限,再配 250 ms 不应期,基本能把 60–180 bpm 锁死。
2. 主流光学传感器怎么选:MAX30102 vs AFE4404
| 指标 | MAX30102 | AFE4404 |
|---|---|---|
| 接口 | I2C 1 MHz | SPI 15 MHz |
| LED | 2 路(绿/红) | 4 路(可编程) |
| 分辨率 | 18 bit | 22 bit |
| 采样电流 | 0.8 mA@100 Hz | 1.5 mA@100 Hz |
| 价格 | 15 元 | 45 元 |
| 封装 | 5 mm×3.3 mm | 6 mm×6 mm |
结论:
- 只做心率、成本敏感、板子面积小——MAX30102 足够;
- 想顺带做血氧、需要 660 nm/880 nm 双波长——直接上 AFE4404,省得二次换板。
3. STM32 HAL 的 I2C 驱动:Clean Code 模板
下面代码基于 STM32L432 + MAX30102,HAL 库版本 1.11。把“读/写/中断”三层拆开,方便以后替换成 SPI 传感器。
/* max30102.h */ typedef struct { I2C_HandleTypeDef *hi2c; uint8_t dev_addr; uint8_t int_flag; } max30102_t; /* max30102.c */ static HAL_StatusTypeDef max30102_write_reg(max30102_t *s, uint8_t reg, uint8_t val) { uint8_t buf[2] = {reg, val}; return HAL_I2C_Master_Transmit(s->hi2c, s->dev_addr, buf, 2, HAL_MAX_DELAY); } static HAL_StatusTypeDef max30102_read_fifo(max30102_t *s, uint8_t reg, uint8_t *buf, uint16_t len) { return HAL_I2C_Mem_Read(s->hi2c, s->dev_addr, reg, I2C_MEMADD_SIZE_8BIT, buf, len, HAL_MAX_DELAY); } /* 初始化:LED 7 mA、采样 100 Hz、LED 脉 100 µs */ void max30102_init(max30102_t *s) { max30102_write_reg(s, REG_LED_PULSE_AMP, 0x3F); // 7 mA max30102_write_reg(s, REG_SPO2_CONFIG, 0x47); // 100 Hz max30102_write_reg(s, REG_LED_CONFIG, 0x03); // 红+IR 开 }要点:
- 所有寄存器地址用宏,别手写“0x0F”;
- 读写函数返回 HAL_StatusTypeDef,上层任务可判断重试;
- 100 Hz 采样率下,I2C 1 MHz 读 6 byte 耗时 0.3 ms,留给滤波绰绰有余。
4. 数字滤波:滑动平均 + 低通,两行代码搞定
- 滑动平均(去毛刺)
窗口 8 点,移位寄存器实现,省 RAM。
#define MA_SIZE 8 static uint32_t ma_buf[MA_SIZE]; static uint8_t ma_idx = 0; uint32_t moving_average(uint32_t new_sample) { ma_buf[ma_idx++] = new_sample; ma_idx &= (MA_SIZE - 1); // 位运算取模 uint32_t sum = 0; for (uint8_t i = 0; i < MA_SIZE; i++) sum += ma_buf[i]; return sum / MA_SIZE; }- 一阶低通(去高频)
截止频率 8 Hz,采样率 100 Hz,系数 α = 0.47。
static int32_t lpf_prev = 0; int32_t low_pass(int32_t in) { int32_t out = lpf_prev + 47 * (in - lpf_prev) / 100; lpf_prev = out; return out; }实测:跑 3 分钟跑步机,原始波形峰峰值 800 mV,滤波后 120 mV,心率误差 ±2 bpm。
5. FreeRTOS 任务拆分与低功耗模式
任务划分思路:把“耗电大户”与“实时采样”拆开,利用 STM32L4 的 Stop 0/1 模式,LED 关掉后整机 6 µA。
任务列表
- Task_Sample:100 Hz 硬件定时器触发,单次 0.3 ms,结束后立即
vTaskSuspend(NULL); - Task_Filter:每 20 ms 被 Task_Sample 唤醒,跑滑动平均+低通;
- Task_HeartRate:每 250 ms 算一次峰值,更新全局心率;
- Task_LowPower:无任务运行时,调用
HAL_PWREx_EnterSTOP0(),RTC 1 kHz 唤醒。
- Task_Sample:100 Hz 硬件定时器触发,单次 0.3 ms,结束后立即
低功耗配置代码片段
void vApplicationIdleHook(void) { __disable_irq(); // 关中断 if (eTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED) { HAL_PWREx_EnterSTOP0(); // 2 µs 唤醒 } __enable_irq(); }注意:
- STOP0 唤醒后,PLL 需要重新使能,在
HAL_RCC_MCOConfig()里把系统时钟恢复到 80 MHz; - 进入低功耗前,先把 I2C 外设
__clock disable,否则漏电 200 µA。
6. 生产级避坑指南:PCB、焊接、复位
PCB 布局
- LED 与 PD 下方禁止走数字线,实在要过,走内层且包地;
- 传感器底部焊盘开窗做“光窗”,开窗区周围 0.3 mm 不要铺铜,减少反射。
电气隔离
- 锂电池 4.2 V 直接给 LED 驱动,MCU 侧 3.3 V,如果共地且 LDO 压差大,热插拔 USB 会触发 MCU 掉电复位;
- 解决:LED 侧串 1 Ω + 100 nF RC,再经 Ω 磁珠到模拟地。
焊接
- MAX30102 为 5×3 mm DFN,底部中央有裸焊盘,必须刷锡膏过回流焊,否则 I2C 地址都读不到;
- 手焊党:热风 350 °C,先四周定位,再集中吹裸焊盘,见锡亮立即撤风。
7. 下一步还能玩什么?
血氧检测:把 MAX30102 的红外通道打开,用 660 nm/880 nm 双波长比值法,R 值查表即可得 SpO2,误差 2 %。
蓝牙传输:STM32L432 自带 UART,外接 BC417 透传模块,心率数据 1 byte/s,跑 115200 波特,功耗增加不到 3 mA。
云端同步:用 ESP32-C3 做协处理器,MQTT 上阿里云,毕业设计秒变“物联网医疗”。
把上面的 I2C 驱动、滤波、FreeRTOS 模板打包成一个最小可运行工程,推到 GitHub,再写一行 README:“Star 过 50 放血氧算法”。别小看这句话,我当年就是靠它把仓库刷到 Trending,答辩老师当场给了优秀。代码已经给你,接下来就看你把它戴在哪只手腕上了。