简介:面向嵌入式开发与健康监测场景的STM32完整工程,基于STM32微控制器实时采集体温、心率,并利用加速度计/陀螺仪(MPU6050)实现老人跌倒检测,数据可通过串口、蓝牙或Wi-Fi上报,适用于居家看护、养老院监护、远程医疗预警等场景。压缩包共230个文件,约29.37MB,主要包含C/H源码、Keil工程配置、编译生成的AXF/HEX固件,以及传感器驱动、LCD显示和定时器相关代码,可帮助学习者快速理解多传感器融合与嵌入式数据上报流程。已有300人学习下载。资源内除了完整工程代码,还提供心率PPG/ECG处理思路、跌倒判断逻辑、通信协议设计等关键模块,适合作为课程设计、毕业设计或产品原型参考,便于在此基础上进行二次开发和功能扩展。
1. 从一颗传感器到一整套看护方案:这个项目到底在做什么
先说结论:这不是一个"实验室里跑通Demo"就完事的项目,而是一套可以部署到养老院、独居老人家庭或康复病房的嵌入式健康监测终端。核心功能三件事——实时测量体温、采集心率数据、识别老人跌倒动作,全部跑在一块STM32上,不上云也能本地独立工作。
我为什么强调"本地独立工作"?因为市面上很多所谓健康监测设备,一旦断网就成了板砖,传感器数据全走云端处理。而STM32方案的优势恰恰在于:所有信号采集、滤波、特征判断都在MCU内部完成,只有最终报警信息才需要对外传输。这对于网络条件不稳定的养老场景,反而更可靠。
整个系统的信号流大致是这样:体温传感器和心率传感器通过I2C或单总线把原始数据送进STM32,六轴加速度计(跌倒检测的核心器件)通过SPI或I2C持续输出三轴加速度和角速度。STM32内部跑三个并行任务——温度采集任务、心率滤波任务、跌倒识别算法——最后把状态汇总到屏幕和蜂鸣器。
这个项目适合谁来搞?
- 正在学STM32但想做点"真正有社会价值"的项目的学生
- 想给家里老人做一套低成本的监测设备、又不想买市面成品的人
- 嵌入式开发者想练手传感器融合和状态机设计
如果你只会点灯、点数码管,这个项目正好把你往前推一大步:它涉及I2C、SPI、定时器中断、串口通信、ADC采集、低功耗设计——STM32的大部分核心外设,你都能碰到。
2. 传感器选型逻辑:为什么是这三颗芯片,而不是别的
选传感器是整条开发链路里最容易被新手低估的一环。很多人上来就买模块,看见引脚能接就焊,结果数据的噪声大得没法看,又找不到问题出在哪。我直接把常用方案摆个表格,说说我踩过的取舍。
| 功能模块 | 可用传感器 | 接口方式 | 我的选择 | 选择理由 |
|---|---|---|---|---|
| 体温测量 | DS18B20、MLX90614、NTC热敏电阻 | 单总线 / I2C / ADC | MLX90614 | 非接触测量,更卫生,适合老人日常佩戴 |
| 心率采集 | MAX30102、PulseSensor、BH1790GLC | I2C / ADC | MAX30102 | 集成红光+红外双LED,能算血氧,前期扩展空间大 |
| 跌倒检测 | MPU6050、MPU9250、LSM6DS3 | I2C / SPI | MPU6050 | 资料最多,六轴数据足够,价格便宜 |
先说体温。DS18B20是很多教程的首选,便宜、单总线、不用校准。但它有个致命问题——属于接触式测量,放在身上会被环境温度慢慢"掰弯",测出来的数据其实是"皮肤加热了传感器"的结果。MLX90614是红外测温,对准手腕动脉区就能读,不接触皮肤,误差在±0.2°C左右,而且它的SMBus协议和I2C几乎兼容,STM32标准库直接能驱动。
心率方面我不推荐模拟输出的PulseSensor。因为它输出的是模拟电压,要用ADC采集,而ADC的噪声很容易干扰心率信号的峰值识别。MAX30102直接输出数字信号,内部自带滤波逻辑,可配置采样率,I2C接口读取寄存器值就行。而且它带绿光LED,绿光对血液吸收率比红光高,穿透浅表层血管的能力更适合手腕佩戴式测量。
重点说MPU6050。这颗六轴芯片(三轴加速度+三轴陀螺仪)简直是为跌倒检测量身定做的。加速度计能感知重力方向变化,陀螺仪能感知身体旋转角速度。摔倒的过程在物理意义上非常明确:先是自由落体般失重,加速度数值骤降,然后猛烈撞击地面,加速度瞬间飙升,最后人躺倒在地,身体姿态从竖直变为水平。
我还考虑过MPU9250,多了一个磁力计,看起来更高级。但对跌倒检测来说,磁力计除了增加代码量毫无用处——检测靠的是加速度和角速度,不是方向和航向。多花钱买用不上的功能,不如把省下的钱花在传感器的滤波电路上。
3. 硬件电路与PCB设计避坑实录
传感器的模数转换、信号调理这些电路如果没弄好,后端代码怎么写都救不回来。我直接说我踩过最疼的两个坑。
3.1 MLX90614的供电纹波问题
第一次焊完板子,MLX90614读出来的温度在33°C到41°C之间乱跳,以为是I2C时序问题,折腾了两天驱动代码。最后用示波器一看,电源线上有200mV的纹波,导致MLX90614内部ADC的参考电压也跟着抖。
解决办法很简单:在MLX90614的VCC和GND之间加一个10μF的钽电容和一个0.1μF的陶瓷电容并联。如果还想更稳,就把传感器单独用一颗LDO(我用的AMS1117-3.3)供电,别跟电机、蜂鸣器共用电源平面。实测共用电源时数据波动±0.5°C,独立供电后稳定在±0.1°C。
3.2 MPU6050的中断引脚与时钟同步
MPU6050有一个INT引脚,数据准备好后会拉高通知MCU读取。很多教程直接把这个引脚悬空,主循环轮询读取,虽然功能能实现,但时序上容易读错。因为轮询读取时,可能刚好读到新旧数据各一半的状态,表现为数据跳变。
我建议把INT引脚接到STM32的外部中断引脚(比如PA1),配置为上升沿触发。这样一来,每次数据更新时MPU6050会主动"敲门",MCU在中断里读取,保证读到的是一帧完整的数据。这个做法需要把I2C配置为主模式,速率设置在400kHz以下,否则MPU6050容易通信超时。
还有一个细节:MPU6050的I2C地址是0x68,但如果AD0引脚接高电平就变成0x69。很多人在这翻车,明明代码没错,就是找不到设备。直接把AD0接地或者悬空,固定用0x68地址。
3.3 蜂鸣器和继电器的干扰隔离
这个项目的上电自检逻辑里,蜂鸣器会响一声。第一次整体调试时,蜂鸣器一响,屏幕上的心率数值直接跳空一拍。查了半天,原来是蜂鸣器是感性负载,开关瞬间产生反向电动势,顺着电源线和地线干扰了MAX30102的I2C通信。
解决手段是两层:蜂鸣器并联一个1N4148二极管(续流二极管),方向反接,吸收反向电动势;电源入口加一个磁珠和10μF电容构成的π形滤波器。从那以后,蜂鸣器再怎么响,传感器数据都纹丝不动。这个经验直接套用到你的项目里,能省一个礼拜的调试时间。
4. 定时器分时复用:一个定时器干三件事的调度策略
STM32的定时器资源看着不少,但真做项目时就发现不够用:温度采集要周期刷新、心率传感器要定时读取和滤波、跌倒检测需要高频(100Hz)读取加速度数据、显示屏要刷新、蜂鸣器要控制。如果每件事都占一个定时器,F103C8T6这种主流芯片立马就不够用了。
我的做法是一个TIM2搞定所有周期任务,用"时基+任务标记"的方式做时间片轮转调度。这个思路在裸机开发里非常实用,不依赖RTOS,代码量反而更小。
具体操作:TIM2配置为1ms中断(72MHz主频下,预分频72-1,自动重载1000-1),每进一次中断执行tick++。然后在主循环里判断全局的tick变量:
extern volatile uint32_t tick; #define TEMP_PERIOD 1000 // 温度采集周期:1秒 #define HEART_PERIOD 50 // 心率传感器读取:20Hz #define FALL_PERIOD 5 // 跌倒检测:200Hz读加速度 uint8_t temp_flag = 0; uint8_t heart_flag = 0; uint8_t fall_flag = 0; void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update)) { tick++; if (tick % TEMP_PERIOD == 0) temp_flag = 1; if (tick % HEART_PERIOD == 0) heart_flag = 1; if (tick % FALL_PERIOD == 0) fall_flag = 1; TIM_ClearITPendingBit(TIM2, TIM_IT_Update); } } int main(void) { while (1) { if (temp_flag) { temp_flag = 0; read_temperature_task(); } if (heart_flag) { heart_flag = 0; read_heart_rate_task(); } if (fall_flag) { fall_flag = 0; read_fall_task(); } // 其余时间跑显示刷新和按键扫描 } }这里有个关键点:主循环的任务必须短平快,不能阻塞。例如读取温度时I2C通信等待时间较长,那就分两次读——先发出读取命令,下次轮询时再取结果。如果主循环卡在某个传感器的等待循环里几百毫秒,其他任务的定时就全乱套了。
我在实际测试中发现,用这个分时复用调度后,跌倒检测的采样率能稳定在200Hz,心率传感器读取稳定在20Hz,温度刷新1秒一次,三件事互不干扰,CPU占用率还不到30%。
5. 跌倒检测核心算法:差分阈值+姿态判定的实战经验
跌倒检测是整个项目算法含量最高的部分,也是最容易"看起来能做、实际根本不准"的部分。光看加速度的绝对值是远远不够的——老人弯腰捡东西、快速坐下、被人搀扶时的加速度变化,跟跌倒看起来非常像。
我的做法是两段式判定,先看运动强度,再看姿态变化。具体拆成三个判据:
**判据一:合加速度突变。**用公式A = sqrt(Ax² + Ay² + Az²)算出合加速度。正常情况下合加速度接近1g(重力加速度),跌倒时先失重(A远小于1g),再撞击(A远大于1g)。判定条件设定为:先检测到A小于0.4g持续至少50ms,随后1秒内A超过2.5g的峰值。这个阈值是我拿20组模拟数据调出来的,不同身高体重的人差异不大,比较普适。
**判据二:姿态角翻转。**跌倒后人体大概率从竖直变成水平。用加速度计分量计算倾斜角,pitch = atan2(Ay, sqrt(Ax² + Az²)),当角度从竖直(接近90°)快速变化到水平(小于30°)时,判定为一次姿态翻转。这里有个细节——必须用低通滤波后的角度。MPU6050原始数据小抖动很大,直接算角度会出现突然翻转的假象。我用滑动平均窗口(窗口长度16,约80ms)把角度曲线磨平后再判断。
**判据三:静止确认。**跌倒在地后,如果只是瞬间的冲击,人可能马上就坐起来了,这种情况不需要报警。所以算法最后一步是"倒地后3秒内,合加速度一直稳定在1g附近(排除继续剧烈运动)",才确认是"跌倒后未起身"状态。
三个判据按顺序执行,形成一个简单的状态机:正常 → 疑似失重 → 撞击确认 → 姿态翻转 → 静止躺倒 → 触发报警。任意一步超时未满足,状态机自动复位回"正常"。
这套算法实测下来,对"走路滑倒""从椅子跌坐""床上滚落"三种场景都能正确识别,误报率大约百分之几,主要是快速弯腰捡东西后没立刻直起身的情况。这个可以通过提高姿态翻转的阈值角度(从30°降到20°)来规避,但也可能漏掉一些"跌坐后身体仍然前倾"的场景——阈值要按实际使用环境调整,没有一个放之四海而皆准的参数。
6. 心率数据为什么不可信:信号质量的判断与滤波处理
如果你直接把MAX30102的原始数据画出来,那基本是一条疯狂抖动的曲线。要从中算出准确的心率,得做三件事:滑动平均滤波、峰值检测、还有最关键的信噪比判断。
MAX30102的原始数据是红外光透过毛细血管后的吸收量,心脏泵血时血量变化会在波形上形成一个小峰。这个峰非常微弱,很容易被手指移动、环境光干扰盖掉。我用的处理流程是这样的:
首先做滑动平均滤波。窗口长度取多少很关键——窗口太短滤不掉高频噪声,窗口太长会把心率信号的细节也磨没。按照心率范围60-150次/分,主频大约1-2.5Hz,采样率如果是20Hz,那一个波形周期大约有8-20个采样点。滑动平均窗口取5,能在保留波形峰谷特征的前提下滤掉大部分高频抖动。
然后是峰值检测。我用的方法是"求导+阈值":计算相邻采样点的差值,当差值的符号从正变负、且当前值超过一个动态阈值时,认为是一个波峰。动态阈值怎么定?我是每3秒更新一次,取这3秒内振幅最大值的60%作为这轮检测的阈值。这样即便手轻微晃动导致振幅变化,峰谷检测都能自适应调整。
但还有一个更大的坑——很多情况下,波形根本没有规律。比如老人手凉、末梢血液循环差的时候,MAX30102的红外波形几乎是平的,随机噪声都比你血管搏动的信号大。如果这时还硬算心率,算出来的数完全是瞎猜的。所以一定要加一个信号质量判断模块:
uint8_t check_signal_quality(uint16_t *buffer, uint16_t len) { uint16_t max_val = 0, min_val = 0xFFFF; for (uint16_t i = 0; i < len; i++) { if (buffer[i] > max_val) max_val = buffer[i]; if (buffer[i] < min_val) min_val = buffer[i]; } uint16_t amplitude = max_val - min_val; // 振幅低于300个ADC码值,认为信号不可信 if (amplitude < 300) return 0; return 1; }这个振幅阈值300是我调出来的经验值——正常佩戴时MAX30102的红外ADC数值在几万范围内,红光通道波形峰谷差在500-1500之间。如果你发现连300都过不了,大概率是佩戴太松或手指冰凉,此时不再计算心率,直接在屏幕上提示"请重新佩戴",而不是显示一个荒谬的数字。宁可不显示,也不显示假数据——这一点在医疗健康类项目里比什么都重要。
7. 一个让我排查了两天的Bug:I2C通信被中断抢占
这个项目的整体调试还算顺利,唯一让我头疼了两天的问题是:心率数据和温度数据偶尔会出现"瞬间跳变",甚至直接读到0xFF这种错误字节。因为两个传感器的数据我都放在中断里读取,理论上不该出这种事,但复现时发现,只要蜂鸣器报警(跌倒状态触发),I2C读取就立刻乱套。
排查过程是这样的:先怀疑是I2C时序被拉长导致超时,于是把I2C时钟从400kHz降到100kHz——问题依旧。然后怀疑是外部中断频繁触发导致I2C通信被打断,于是把外部中断优先级降到了最低——依然乱。最后才发现,问题出在MPU6050的INT引脚接线上:这个引脚和蜂鸣器控制引脚在PCB布局上挨得太近,蜂鸣器的2000Hz方波信号通过PCB走线间的寄生电容耦合到了INT线上,产生了大量假中断。
解决起来很直接:把MPU6050的INT引脚在软件上配置为内部上拉,同时把控制蜂鸣器的GPIO从默认的推挽输出改成开漏输出(外加外部上拉电阻)。改了之后,假中断完全消失,I2C读取错误率直接降到0。
这个教训的通用价值是:多传感器系统的"传感器间干扰",不一定来自电源,也可能来自GPIO之间的电磁耦合。调试时遇到诡异现象,先怀疑物理层,再怀疑协议层和代码层。顺序反过来,很可能白白浪费几天时间。
8. 数据可视化与后续扩展:屏幕显示、低功耗和无线传输的思路
系统做出来之后,显示层我用了0.96寸OLED(SSD1306),I2C接口,一页显示体温和心率,翻页显示姿态状态。这块屏幕在STM32平台上有现成的库,移植成本极低。但如果你想做更复杂的界面,比如波形显示,OLED就不够用了,可以考虑换TFT-LCD或LVGL加持的屏幕。
功耗方面,整机在连续工作模式下电流大约80mA,其中屏幕占了20mA,传感器占了30mA,MCU自身30mA。如果做成便携设备(比如老人随身佩戴),这个功耗是不合格的。低功耗优化的基本思路是:让传感器间歇工作,比如每5分钟测一次体温和心率,每次测10秒后进入待机;摔倒检测模块保持高频工作(毕竟这个不能停);MCU用停机模式,靠RTC定时唤醒。用这个方案,一块1000mAh的锂电池可以撑一周左右。
无线传输方面,如果要把数据传给手机或后台,我建议用ESP8266或ESP32做WiFi模块,STM32通过串口把传感器数据发给它,走MQTT协议上云。这里有个要注意的问题:STM32和ESP8266串口通信时,波特率不要设太高,115200在长距离传输下容易丢字节,9600太慢传不了高频率的数据波形,57600是我实测比较稳定的折中值。
这套系统如果继续往下做,可以加GPS定位(跌倒时带上位置信息)、加ECG心电模块(做更全面的心脏健康评估)、加语音播报(跌倒时自动呼叫老人或家属)。从项目完成度来说,先把手上的三个模块做扎实、数据做稳定,比贪多求全更有价值——这也是我个人做嵌入式项目最深的体会。
本文还有配套的精品资源,点击获取