简介:这套基于STM32的人体健康监测装置项目,适合计算机、通信、自动化等相关专业学生作为毕业设计或课程设计参考。系统通过传感器检测体温与心率,并利用MPU6050判断老人是否摔倒,摔倒后可将告警信息发送给子女,整体方案完整。资源共230个文件,压缩包约29.34MB,以C语言源码(.c/.h)、编译中间文件(.o/.d/.crf)及Keil工程配置为主,同时提供烧录hex文件、说明文档与效果图片,工程组织清晰,便于直接打开进行二次修改。代码已经过调试测试,答辩评审达到98分,可直接运行学习;也可在此基础上扩展报警、云端上报等功能。目前已有109人学习,适合希望入手STM32综合项目或完善健康监测类毕业设计的读者下载使用。
1. 这个STM32健康监测项目的骨架与设计取舍
老人跌倒后如果长时间无人发现,后果往往比跌倒本身更严重。这个项目没有做云平台、没有App,却把最关键的检测链路做在了本地:STM32F103系列主控负责读取MPU6050的加速度和角速度,通过阈值判据识别摔倒动作;同时采集体温和心率,在本地屏幕上显示,通过出厂默认串口透传把告警发出去。它适合三类人:正在做嵌入式课程设计和毕业设计的学生,想快速验证跌倒检测算法但不想从零写传感器驱动的工程师,以及需要一个“能跑通但又不只是点灯”的STM32综合例程来练手的开发者。整个工程基于标准外设库(SPL),从stm32f10x_tim.c就能看出定时器用于采样节拍,inv_mpu.c是MPU6050的DMP驱动,lcd.c完成本地交互,Flash则用来存历史事件。下面按硬件、算法、通信、调试的顺序把这个工程拆开讲。
2. 硬件架构与模块选型:传感器链路与引脚分配
这个设计的主控选型是STM32F103系列,面向健康监测场景,它的ADC、I2C、USART和定时器资源够用,外围电路也不需要特别处理。传感器端,温度、心率和MPU6050来源不同,必须分组供电和隔离信号。
2.1 主控与传感器的选型理由
STM32F103C8T6是零死角覆盖一切中低端健康监测方案的芯片。它有两个I2C,一个用来挂MPU6050和数字温度传感器,另一个可以挂心率模块或预留给OLED屏。心率采集通常用光电容积脉搏波(PPG)方案,比如MAX30100/MAX30102,通过I2C读出红外光和红光原始值;如果工程资料里没有硬件I2C,也可以用GPIO软件模拟,项目里出现stm32f10x_tim.c说明定时器被用于控制采样周期和软件延时。
温度传感器建议用单总线的DS18B20而不是热敏电阻。原因在于单个引脚就能读温度,且精度在0.5摄氏度内,老人体温监测场景不需要医疗级绝对值,只要趋势不变态。要注意DS18B20每次转换时间是12bit精度下的750ms,如果和心率采样共用定时器中断,需要把温度转换放到主循环状态机里而不是中断里。
MPU6050则通过I2C读取三轴加速度和角速度。它内部自带DMP(数字运动处理器),可以通过inv_mpu.c直接输出四元数,省去大量解算工作。但跌倒检测主要依赖加速度幅值变化,DMP的四元数反而有延迟,所以这个项目直接用原始加速度数据算SVM(向量模)和姿态倾斜角,而非DMP输出。
2.2 硬件引脚映射表
工程源码里stm32f10x_gpio.c和stm32f10x_usart.c会定义具体端口,以典型F103C8T6核心板为例:
| 模块 | 信号 | STM32引脚 | 说明 |
|---|---|---|---|
| MPU6050 | SCL/SDA | PB10/PB11 | I2C2,外部4.7kΩ上拉 |
| MPU6050 | AD0 | GND | I2C地址为0x68 |
| DS18B20 | DQ | PA1 | 开漏输出,4.7kΩ上拉 |
| 心率模块 | SCL/SDA | PB6/PB7 | I2C1,用于MAX30100 |
| LCD1602/OLED | RS/EN/D4-D7 | PA2~PA8 | 4线模式节省引脚 |
| SIM800A/ESP8266 | TX/RX | PA9/PA10 | USART1,用于告警发送 |
| 调试串口 | TX/RX | PA2/PA3 | USART2,打印日志 |
这个映射表不是死的。如果用的是一块集成核心板和扩展板,注意检查PB10和PB11是否被板载其他器件占用。我一般建议把所有传感器供电分开:VCC统一5V,传感器板载3.3V稳压,但MPU6050和心率模块必须共用同一个地,不能浮地,否则数据会出现周期性尖刺。
2.3 供电与硬件连接调整
电池供电时,整机瞬时电流在GSM模块发短信时接近2A,普通锂电池不够撑,需要并联1000μF电解电容,并在GSM模块电源端加一个10Ω/1W电阻做软启动。如果用ESP8266替代GSM模块,电流尖峰小得多,但需要额外拉高EN引脚。
接线时优先保证重心低:MPU6050尽量焊接在PCB中心而不是板边,因为跌倒检测的加速度参考点是传感器自身,远离重心会导致旋转时引入额外向心加速度,动作幅度稍有变化就容易误判。如果这个项目只用一根杜邦线把MPU6050悬空,那检测到的震动会比你真实跌倒剧烈得多,调试时容易被误导。
心率模块的佩戴方式比接线更关键。MAX30100这类光学传感器需要在相对静止的皮肤表面工作,如果直接贴在衣服上,红外光的反射率几乎为零。正确做法是让被测者食指轻轻按压模块表面,避免压迫过度导致血流阻断。硬件说明里如果只提供了模块引脚,没有提到这一点,多半需要再加一段固定绑带,否则测试时怎么调阈值都白搭。
3. MPU6050跌倒检测算法与代码实现
跌倒检测是这套装置的核心判断逻辑。MPU6050输出的原始加速度范围在±2g、±4g、±8g、±16g间可选,原始量除以对应的灵敏度就能得到g值。项目里通常配置为±2g,也就是16384 LSB/g,分辨率足够检测人体跌倒动作。
3.1 数据读取与坐标归一化
inv_mpu.c里提供了运动中断和数据就绪中断的底层读取函数,更直接的方式是从寄存器手动读取。标准外设库的第一个坑是:MPU6050的寄存器地址是8位,I2C一次最多写32字节,读6字节加速度加6字节角速度必须分两次读。工程里如果沿用官方例程,一般会封装一个MPU6050_Read_Accel()函数。读取代码如下:
void MPU6050_Read_Accel(short* ax, short* ay, short* az) { uint8_t buf[6]; MPU6050_I2C_ReadRegs(MPU6050_ACCEL_XOUT_H, buf, 6); *ax = (int16_t)((buf[0] << 8) | buf[1]); *ay = (int16_t)((buf[2] << 8) | buf[3]); *az = (int16_t)((buf[4] << 8) | buf[5]); }逻辑说明:MPU6050_I2C_ReadRegs是从指定寄存器地址连续读取字节数组的函数。加速度输出寄存器地址MPU6050_ACCEL_XOUT_H为0x3B,依次排列X、Y、Z的高低位。因为加速度可能为负,所以用int16_t而不是uint16_t,否则负加速度会变成两万多的大数,整条判定逻辑全部失效。
参数说明:-2g到+2g量程时,敏感度为16384 LSB/g。静止平放时理论值应为ax=0, ay=0, az=16384(即1g)。如果你的板子不是水平的,az会略低,ax或ay会出现非零值,这是正常的,不影响跌倒判定,因为最终的判据是加速度模长而非单一轴。
3.2 跌倒判定:SVM冲击、姿态倾角、静置时长三级判定
跌倒过程最典型的物理特征是:先出现一个加速度突变(身体撞击地面),然后姿态发生明显翻转且持续一定时间保持静止。单纯的阈值比较会有两个问题:一是弯腰捡东西也会让姿态角变化,但不会产生撞击;二是被人快速推一下会产生撞击但很快站起来,不满足静置条件。
因此采用三级判据:
- 加速度向量模长SVM超过冲击阈值;
- 撞击后的若干秒内,躯干与水平面的倾斜角超过阈值;
- 该倾斜状态保持超过2秒。
#define IMPACT_THRESHOLD 2.6f // 单位g,大于此值认为发生剧烈撞击 #define TILT_THRESHOLD 40.0f // 单位度,躯干与垂直轴的夹角 #define KEEP_TILT_TICKS 20 // 20 * 100ms = 2s 静置时长 void Fall_Detect(float ax, float ay, float az, uint32_t tick_100ms) { static uint8_t impact_flag = 0; static uint8_t tilt_counter = 0; float svm = sqrtf(ax*ax + ay*ay + az*az); float pitch = atanf(ax / sqrtf(ay*ay + az*az)) * 57.2958f; float roll = atanf(ay / sqrtf(ax*ax + az*az)) * 57.2958f; float tilt_angle = fabsf(pitch) > fabsf(roll) ? fabsf(pitch) : fabsf(roll); if (svm > IMPACT_THRESHOLD) { impact_flag = 1; tilt_counter = 0; } if (impact_flag) { if (tilt_angle > TILT_THRESHOLD) { tilt_counter++; if (tilt_counter >= KEEP_TILT_TICKS) { Fall_Event_Trigger(); impact_flag = 0; tilt_counter = 0; } } else { impact_flag = 0; tilt_counter = 0; } } }逻辑说明:先算SVM冲击,再算俯仰角/横滚角中较大的那个作为倾斜角。impact_flag一旦置位,后续只是检测倾斜和静置,不再要求svm持续超过阈值。这很关键,因为撞击后人体处于减速或反弹状态,加速度可能瞬间接近0,如果你用“svm连续超阈值”做判断,永远等不来第二次触发。atanf需要调用数学库,在Keil中勾选MicroLIB,否则链接体积会增大且出现__aeabi_f2iz相关报错。
参数说明:IMPACT_THRESHOLD=2.6g是经验值。站立静止时SVM为1g,慢走时峰值约1.8g,快跑约2.3g,剧烈跌倒可超过3g。如果老人有帕金森或不自主抖动,建议提高到2.8g以上。TILT_THRESHOLD取40度而不是90度,是因为绝大多数跌倒场景身体不会完全平躺,儿童跌倒会翻滚,老人侧倒卡在家中扶手上可能只有45度。KEEP_TILT_TICKS按100ms一个调度周期算,2秒足够区分“跌倒后意识清醒自己站起来”和“真的倒下起不来”。
3.3 采样节拍与定时器配置
检测函数不能放在main的死循环里裸奔,因为DS18B20的750ms转换会卡住主循环,导致跌倒检测超时。工程中用stm32f10x_tim.c配置一个100ms的时基中断,每进一次中断把加速度换算成工程值,放入全局结构体,主循环里再跑Fall_Detect。
void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); MPU6050_Read_Accel(&ax_raw, &ay_raw, &az_raw); axg = ax_raw / 16384.0f; ayg = ay_raw / 16384.0f; azg = az_raw / 16384.0f; get_ds18b20_temp(Cmd_ConvertT); // 非阻塞发起温度转换 } }这段代码里,定时器中断只负责更新数据,温度转换只“发起”不“等待结果”。主循环里查询温度转换标志位的设计,能把传感器最慢的环节隔离开。注意MPU6050_Read_Accel用I2C读取需要耗时约2ms,中断里占这么久对系统节律不太友好。更稳妥的做法是:中断里置一个采样标志位,主循环检测到标志后再读传感器,配合外部中断引脚实现数据同步。
3.4 温度和心率数据采集要点
DS18B20单总线时序需要严格延时,代码放在stm32f10x_gpio.c中自己翻转GPIO。初始化分三步:复位脉冲、存在脉冲、写ROM指令。常见错误是:在for循环里用空转delay,编译器开优化后会把空循环优化掉,导致时序完全乱掉。建议用定时器或SysTick提供的delay_us函数,并加volatile修饰循环变量。
心率模块如果是MAX30100,I2C寄存器操作时不光要读FIFO数据,还要先配置采样率、LED电流和ADC范围。代码中一个典型配置是:
MAX30100_WriteReg(MAX30100_SPO2_CONFIG, 0x07); // 50Hz采样率,16bit ADC MAX30100_WriteReg(MAX30100_LED_CONFIG, 0xA0); // 红LED 11mA,IR LED 11mA MAX30100_WriteReg(MAX30100_MODE_CONFIG, 0x03); // 血氧+心率模式温度、心率、加速度这三类数据如果要在同一个时间线上对齐,需要给每个传感器打时间戳。项目中如果没有RTC芯片,就利用系统上电后的毫秒计时器作为时间戳,事件告警信息里带一个相对时间,例如“FALL at 233s”,远程端看到也能判断事件顺序。
4. LCD显示与GSM短信通知:事件状态机的工程化
跌倒检测不只是本地屏幕显示,项目要求把摔倒信息发送给子女。发送的载体可以是GSM模块(SIM800A)也可以是ESP8266转WiFi。无论哪种方式,核心是事件状态机的设计,避免一次跌倒触发几十条短信。
4.1 事件状态机定义
状态机分为正常、撞击待确认、已确认告警、告警已发送四个状态。每个状态都有超时处理,防止卡死。
| 状态 | 进入条件 | 动作 | 超时时间 | 超时后 |
|---|---|---|---|---|
| NORMAL | 上电或恢复 | 清标志位 | 无 | 无 |
| PRE_ALARM | SVM超阈 | 记录当前时间 | 3秒 | 回NORMAL |
| ALARM | 倾斜持续 | 点亮LED | 5秒 | 回NORMAL |
| SENT | 发送成功 | 记录Flash | 60秒 | 回NORMAL |
当检测函数触发Fall_Event_Trigger()时,状态机不等于立刻发短信,而是先置PRE_ALARM。如果紧接着3秒内倾斜保持,则置ALARM并发送;如果只是撞击但人马上起来了,3秒超时自动恢复。这个设计避免了“摔了一跤但拍下裤子站起来”时让人虚惊一场。
4.2 LCD显示刷新策略
LCD1602或OLED不能每100ms刷新一次,频率过高会导致字符闪烁和串口干扰。合理做法是每秒刷新一次页面,结合按键切换显示温度、心率、步数和状态。
typedef enum { PAGE_TEMP, PAGE_HR, PAGE_STATUS } DisplayPage; void LCD_Refresh_Task(uint32_t now_ms) { static uint32_t last_refresh = 0; char line_buf[17]; if (now_ms - last_refresh < 1000) return; last_refresh = now_ms; switch (current_page) { case PAGE_TEMP: snprintf(line_buf, sizeof(line_buf), "Temp: %.1fC", temp_value); LCD_Line1(line_buf); snprintf(line_buf, sizeof(line_buf), "HR: %d bpm", heart_rate); LCD_Line2(line_buf); break; case PAGE_STATUS: if (fall_alarm_flag) { LCD_Line1("** FALL **"); LCD_Line2("SENDING ALERT"); } else { LCD_Line1("MONITORING"); LCD_Line2("SAFE"); } break; } }逻辑说明:last_refresh实现简单非阻塞节流,Unix时间戳用毫秒差值做比较。snprintf限制缓冲区长度,防止温度值超过LCD显示宽度时内存越界。LCD1602的写时序中,使能信号高电平要保持至少40ns,如果用的是GPIO模拟并口,在EN=1后加一个delay_us(1)再拉低即可。
4.3 短信通知命令序列
SIM800A通过USART1与STM32连接,波特率115200。发短信的AT命令流程固定为:AT+CMGFA先发出去等待模块返回OK,接着发送AT+CMGS="手机号",再等一下返回>,最后发送正文和十六进制结束符0x1A。
void GSM_Send_Alert(const char* phone_num, const char* msg) { char cmd[64]; snprintf(cmd, sizeof(cmd), "AT+CMGS=\"%s\"\r\n", phone_num); UART1_SendString(cmd); while (!UART1_Receive_NoWait(">")) { ; } UART1_SendString(msg); UART1_SendByte(0x1A); // Ctrl+Z 结束符 UART1_Receive_Until("+CMGS:", 10000); }这里有个细节:GSM模块上电后需要约6秒搜索网络,不能在主控上电后立刻发AT命令,否则第一个AT会丢失。工程里一般用延时或查询模块返回寄存器的方式等待。发送失败时不能死等,要设置超时并重发两次,仍失败则把告警事件写入Flash,等待网络恢复后下次开机再补发。
4.4 用Flash保存告警历史
资源列表里出现了stm32f10x_flash.c,作用是掉电保存跌倒事件。STM32F103的内置Flash每页1KB,写入前需要先擦除,FLASH擦写寿命约1万次。告警频率不高,足够使用。写入时要注意:只能由高到低写0,不能把1写回0,所以每次追加事件都重新读出一整页、修改、擦除再写回。
typedef struct __attribute__((packed)) { uint32_t timestamp; float temp; uint16_t heart_rate; uint8_t type; // 0=跌倒, 1=心率异常, 2=温度异常 } HealthEvent_t; void Flash_SaveEvent(const HealthEvent_t* evt) { uint32_t addr = 0x0801F800; // 最后一页 FLASH_Unlock(); while (FLASH_GetFlagStatus(FLASH_FLAG_BSY) != RESET) { ; } FLASH_ErasePage(addr); for (int i = 0; i < sizeof(HealthEvent_t) / 2; i++) { uint16_t w = ((uint16_t*)evt)[i]; FLASH_ProgramHalfWord(addr + i * 2, w); } FLASH_Lock(); }这里的packed确保结构体内存布局按1字节对齐,否则在ARM默认4字节对齐下sizeof会有间隙,写入Flash后读出会错位。Flash地址选在片上代码尾部之前的空闲页,实际地址需要看工程里代码段大小,不能随便写0x0801F800,否则会覆盖代码。
5. 调试与校准:陀螺仪漂移、心率噪声与Keil避坑
很多使用者拿到这个源码跑起来发现两个现象:一是在桌子上轻轻敲一下板子就报跌倒,二是心率数值在 30 和 180 之间乱跳。这两个问题都不是主控坏了,而是数据没有被校准和滤波就把参数喂给了判定逻辑。
5.1 加速度零偏与陀螺仪漂移
MPU6050出厂有每颗芯片的零偏误差,平均值在±50mg。虽然加速度计不像陀螺仪那样持续积分,但零偏会影响静止时SVM的大小。如果某轴零偏是-40mg,静止SVM是1.0g加减0.04g,在阈值2.6g时不影响冲击判断,但会在跌倒后姿态倾角计算中引入3度到5度误差。早期调试建议先采集100组静止数据的均值作为补偿值。
陀螺仪的漂移主要影响姿态角长时间解算。本项目的倾斜角直接用加速度比算,不受陀螺仪漂移影响,但如果你在代码里融合了陀螺仪的积分,必须加入高通滤波或定期重置。使用互补滤波时,陀螺仪权重一般设0.98,加速度计权重0.02,权重比取决于你更信任哪个传感器。静止场景下加速度计更稳,动态场景下陀螺仪更稳,取0.98只是通用起步点。
5.2 心率信号的运动伪差
PPG心率传感器对运动极其敏感,手指轻轻一动就会产生与心跳频率接近的伪峰。常规思路是加巴特沃斯低通滤波,原理是用心率的频谱范围是0.8Hz到3.3Hz(48~198BPM),滤掉高频抖动。除了滤波,还必须在代码里加一个信号质量检查:发现红外值波动范围超过1秒内平均值的30%,直接丢弃该段数据,返回前一次有效心率。
int16_t IR_ADC(void) { static int16_t last_val = 0; static uint16_t skip_count = 0; uint16_t raw = Read_IR_ADC(); if (abs((int32_t)raw - last_val) > (last_val * 0.3f)) { skip_count++; return last_val; } skip_count = 0; last_val = raw; return raw; }这段代码是过滤毛刺,不能替代真正的数字滤波器。测试时,用示波器观察I2C读到的FFT,或者直接串口打印原始序列,在Excel里查看曲线。如果原始曲线里基线上下浮动幅度大于峰值的20%,说明信号源接触不良,此时怎么加滤波都没用。
5.3 Keil工程配置常见报错
下载资源里的Healthy.uvguix.15399这类文件是Keil的GUI布局配置,不是工程文件本身。真正需要打开的是.uvprojx或者旧版本的.uvproj。换电脑打开时常见的报错是找不到器件:需要双击右下角的魔术棒,在Device标签里切换到STM32F103C8,或者打开STM32F10x_StdPeriph_Driver的宏定义检查。
编译链接时报Error: L6218E: Undefined symbol inv_mpu_initialize,大多是没把inv_mpu.c和inv_motion_driver.c加入工程。标准外设库工程还容易撞上__disable_irq未定义,原因是启动文件选择的是带浮点中断保护的芯片版本,F103没有FPU,启动文件要选择startup_stm32f10x_hd.s而非带_fp后缀的文件。
调试时要在Keil里开启仿真下载,但使用ST-Link烧录时,如果调试器连接不稳定,最容易出现“No target connected”。新款ST-Link需要固件升级到V2.1以上,USB线要插主板的USB2.0接口而不是USB3.0转接器,很多接触不良问题来自劣质转接线,与代码无关。
5.4 复现工程时的验证清单
把样例工程烧录后不要直接调阈值,按下面的顺序逐项验证:
- 串口打印原始加速度值,板子水平静止时输出(接近0,接近0,16384),如果你读到az=8192,说明重置化已经动了,量程不是±2g;
- 平放板子上升高10cm自然掉落,SVM最大值应超过2.5g,如果不到,说明掉落的加速度被板子缓冲吸收;
- 把板子竖起来靠在墙上,倾斜角应显示约90度,如果显示45度,检查atanf参数有没有和g转换后的值一起算;
- 数据波动剧烈且伴有周期性的尖峰,检查I2C总线上拉电阻和杜邦线长度,超过20cm必加10k上拉。
这一套验证做完,再跑跌倒测试才可能有效果。跳过这步直接调阈值,只会不断改变阈值然后发现行为毫无逻辑。
6. 从毕设到产品:低功耗看门狗与远程上报的进阶技巧
项目跑通后,距离可用还有三个障碍:系统死机后无人重启、电池坚持不了一天、子女收不到消息也不知道设备是否还活着。工程上最常见的是加看门狗、降功耗、以及心跳包。
交给用户的产品板必须在主循环里喂狗,我一般配置独立看门狗(IWDG)超时4秒,在主循环每个周期末尾喂狗。如果某个传感器卡死导致主循环超过4秒,系统自动重启,重启后根据Flash告警标志决定是否补发短信。补发逻辑要限制次数,防止陷入无限重启发短信的循环。
功耗方面,最耗电的是GSM模块和心率传感器。GSM模块在空闲时进入睡眠模式有效电流可降到1mA以下,但需要串口发送AT+CSCLK=1且该引脚拉低。更实用的降低功耗技巧是:把MLX90614这类数字温度传感器的转换周期从1Hz降到0.2Hz,心率模块以100ms时间内完成一次采样后立刻关掉LED,MPU6050进入睡眠模式并靠运动中断唤醒。
void LOWPOWER_Mode_Enter(void) { GSM_SendCmd("AT+CSCLK=1"); MPU6050_Set_SleepMode(1); HAL_DBGMCU_EnableDBGStandbyMode(); // 低功耗调试兼容 __WFI(); // 等待唤醒 }这段代码示意性的写法是:主循环没有事件时进入STANDBY模式,心率与加速度的阈值变化通过外部中断唤醒STM32。注意在Keil在线调试时直接开WFI会丢失仿真连接,所以调试阶段要把__WFI()用宏封起来。低功耗调通后,整机平均电流能控制在50mA以内,用1500mAh锂电池才能支撑一天一夜。
远程上报不是必修项,但如果你想把告警发给子女手机却不想用GSM短信费,可以用ESP8266加一个MQTT协议。做法很简单:用USART3与ESP8266通信,发送AT+CIPSTART建立TCP连接,然后MQTT的CONNECT报文与PUBLISH报文用协议栈拼装。STM32上资源少,不用移植整个MQTT库,只需手工构建固定报文。心跳包每60秒发一次到服务器,服务器超过180秒没收到就标记设备离线,子女App能显示“设备已离线”,避免设备坏了还没人知道。
最后说一个实用性技巧:跌倒检测的最终测试不要只在开发板上测,把整个装置装在一个布袋里系到胸口,然后做前倒、侧倒、坐倒在软垫三个动作。前倒的SVM冲击最大,侧倒姿态角变化最明显,坐倒是复合动作。如果这三个动作都能正确触发告警并且日常走路、弯腰捡钥匙不误触,那阈值就基本调好了。
本文还有配套的精品资源,点击获取