基于STM32和MPU6050的跌倒检测系统设计与实现
简介:本资源是一套面向嵌入式初学者与物联网实践者的老年人防跌倒报警系统完整工程,基于STM32F103系列微控制器与MPU6050六轴姿态传感器实现跌倒状态实时识别与声光报警。项目聚焦居家养老安全场景,融合I2C通信、传感器数据滤波、姿态解算与阈值判断等典型嵌入式开发技能,适合课程设计、毕业设计及智能硬件入门实战。压缩包共89个文件,含42个头文件(.h)定义外设驱动与算法接口、38个源文件(.c)覆盖主控逻辑、OLED显示、MPU6050驱动、DMP初始化及定时器中断处理等核心模块,另有hex可执行文件、Keil工程配置(.uvprojx)、调试脚本(keilkill.bat)及固件库(STM32F10x_FWLib),总大小仅407KB,结构清晰、模块解耦度高。已有919人学习下载,提供即编译即运行的完整软硬件协同方案,包含从传感器采集、姿态角计算到跌倒触发蜂鸣报警的全流程实现,是掌握STM32+惯性传感应用的高实用性参考工程。 摔倒是老年人群体里最要命的安全隐患之一,而且往往不是摔那一下本身有多严重,真正致命的是摔倒后没人及时发现,错过了最佳救助时间。我这两年一直在做智能穿戴相关的项目,这套基于STM32和MPU6050的跌倒检测方案,前前后后改了四个版本,从最早的“能触发报警”一路做到“误报率能接受、家人敢用”的程度。这篇文章就把从硬件搭建、姿态解算到跌倒判断算法的完整过程拆开讲,如果你正准备做健康监护类项目,或者手里正好有这两颗芯片想练手,这篇内容可以直接照着做。
这套方案的核心思路不复杂:用MPU6050六轴传感器采集人体运动数据,STM32负责读取和运算,通过合加速度突变加姿态角度变化来判断是否发生跌倒,一旦确认就驱动蜂鸣器报警。但“能跑”和“能用”之间隔着大量细节,比如阈值怎么定、状态机怎么切、误报怎么压、数据怎么滤波,这些我会结合我的实际踩坑经历一条条说清楚。
1. 方案设计与硬件选型思路
1.1 为什么选STM32加MPU6050这个组合
先说传感器。跌倒检测领域有人用单加速度计、有人用加速度加气压计、有人直接上毫米波雷达,但MPU6050至今仍是性价比最高、资料最全、最适合入门的选择。它内部集成了三轴加速度计和三轴陀螺仪,能同时输出六个维度的原始数据,加速度计负责感知重力方向和线性加速度变化,陀螺仪负责感知旋转角速度,两者结合正好能覆盖“跌倒”这个动作的两个关键特征——身体发生剧烈加速、身体姿态发生明显翻转。
STM32作为主控就更不用多说,Cortex-M3内核的F103系列在淘宝上十几块钱就能买到最小系统板,性能完全够用,官方HAL库把I2C、串口、定时器这些外设封装得比较完善,开发效率远高于纯寄存器操作。而且STM32的生态积累非常厚,网上随便一搜就是大把现成的MPU6050驱动,遇到问题基本都能找到解决方案。
1.2 检测逻辑的初步建模
在动手写代码之前,我建议你先想清楚一个问题:跌倒到底怎么定义?
我自己的建模思路是这样的。正常站立或行走时,人体合加速度保持在1g附近(g为重力加速度,约9.8m/s²),身体倾角在0到30度之间变化。跌倒过程可以拆成三个阶段:先是身体失重下坠,加速度短暂降到1g以下,然后身体与地面或障碍物碰撞,出现一个明显的高加速度冲击峰值,幅度往往超过2.5g甚至3g,最后人体躺倒,身体倾角接近90度并保持相对静止。
所以检测算法要抓的就是这三个特征:失重、冲击、静止倾斜。只抓冲击峰值容易误报,比如老人猛一下坐进沙发里也会出现峰值,但坐完姿态还是接近垂直的;只抓倾角也不行,因为老人躺床上休息时倾角同样很大。三者结合,误报率才能压下来。
1.3 硬件清单与接线方式
这是我最终定型的一版硬件配置:
- STM32F103C8T6最小系统板
- MPU6050模块(带稳压和上拉电阻的常见GY-521模块即可)
- 有源蜂鸣器模块(低电平触发)
- 0.96寸OLED屏幕(I2C接口,调试和显示状态用)
- 一个按键(用于解除报警)
- 3.7V锂电池加一个AMS1117-3.3降压模块
接线如下,注意I2C总线的上拉电阻在GY-521模块上已经集成,不需要额外加。
| 模块 | 引脚 | STM32引脚 |
|---|---|---|
| MPU6050 | VCC | 3.3V |
| MPU6050 | GND | GND |
| MPU6050 | SCL | PB6(I2C1_SCL) |
| MPU6050 | SDA | PB7(I2C1_SDA) |
| MPU6050 | AD0 | GND(I2C地址0x68) |
| OLED | VCC/GND/SCL/SDA | 与MPU6050并联到同一I2C总线 |
| 蜂鸣器 | I/O | PA0 |
| 按键 | 一端接PA1,另一端接GND | PA1 |
AD0接地时I2C器件地址是0x68,接高电平是0x69,这个后面驱动代码里会用到,别搞错。
2. 工程搭建与MPU6050数据读取
2.1 基于HAL库快速搭建STM32工程
现在的开发方式我强烈建议直接用STM32CubeMX生成HAL库工程,不要再用标准库手写寄存器了。CubeMX里选好STM32F103C8T6芯片,配置RCC时把HSE设为Crystal/Ceramic Resonator,SYS里Debug选Serial Wire保留调试口,I2C1设为标准模式100kHz,USART1打开用于打印调试信息。
GPIO方面要把PA0和PA1配置为GPIO_Output和GPIO_Input,注意PA1会用作按键读取,需要设置为上拉输入,这样按键按下接地时读到的是低电平。工程生成后在main.c里把I2C1句柄和串口句柄的声明找出来,后面驱动代码要用。
CubeMX生成工程不是终点,有几个细节得注意。I2C速率不要一上来就拉到400kHz,MPU6050虽然支持高速模式,但飞线连接时信号完整性没保障,100kHz最稳。另外在SystemClock_Config里确认系统时钟确实跑到了72MHz,很多莫名其妙的问题其实都源自时钟配置不对。
2.2 MPU6050寄存器配置与初始化流程
MPU6050的驱动本质上就是通过I2C读写一组寄存器,核心就那么几个。初始化顺序我踩过坑之后固定成了这样:
先读WHO_AM_I寄存器确认设备在线,地址是0x75,读到的值应为0x68。如果读出来是全0或者0xFF,先别急着查代码,八成是接线问题。确认设备在线后,向PWR_MGMT_1(0x6B)写入0x00,唤醒芯片并选择内部时钟源。然后配置SMPLRT_DIV(0x19)为0x07,设置采样率为1kHz除以(1+7),即125Hz。接着配置CONFIG(0x1A)写入0x04,把DLPF低通滤波器设为21Hz带宽,可以有效滤掉高频抖动和电源噪声。
量程配置是接下来的重点。GYRO_CONFIG(0x1B)写入0x08,把陀螺仪量程设为±500°/s,ACCEL_CONFIG(0x1C)写入0x00,加速度计量程设为±2g。这里选±2g是有讲究的,跌倒冲击峰值虽然可能到3g以上,但±2g量程下加速度计的分辨率最高达16384 LSB/g,测量精度最好。稍后检测算法里我会用2.5g作为冲击阈值,±2g量程下读到的原始值会饱和在32767,正好对应2g,不会影响判断。
初始化的完整代码如下:
#define MPU6050_ADDR (0x68 << 1) #define MPU6050_WHO_AM_I 0x75 #define MPU6050_PWR_MGMT_1 0x6B #define MPU6050_SMPLRT_DIV 0x19 #define MPU6050_CONFIG 0x1A #define MPU6050_GYRO_CONFIG 0x1B #define MPU6050_ACCEL_CONFIG 0x1C HAL_StatusTypeDef MPU6050_WriteReg(uint8_t reg, uint8_t data) { return HAL_I2C_Mem_Write(&hi2c1, MPU6050_ADDR, reg, I2C_MEMADD_SIZE_8BIT, &data, 1, 100); } HAL_StatusTypeDef MPU6050_ReadRegs(uint8_t reg, uint8_t *buf, uint8_t len) { return HAL_I2C_Mem_Read(&hi2c1, MPU6050_ADDR, reg, I2C_MEMADD_SIZE_8BIT, buf, len, 100); } uint8_t MPU6050_Init(void) { uint8_t id = 0; MPU6050_ReadRegs(MPU6050_WHO_AM_I, &id, 1); if (id != 0x68) return 0; // 设备异常 MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x00); MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x07); MPU6050_WriteReg(MPU6050_CONFIG, 0x04); MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x08); MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x00); return 1; }MPU6050_ReadRegs这个函数后面读取六轴原始数据时也会复用,从0x3B寄存器开始连续读14个字节,一次性就能拿到加速度和陀螺仪的全部数据。
2.3 原始数据读取与单位换算
MPU6050的输出寄存器是16位有符号数,高字节在前。加速度数据从0x3B到0x40共6个字节,陀螺仪从0x43到0x48共6个字节。读取后需要做单位换算:
- 加速度原始值除以16384,得到单位为g的加速度
- 陀螺仪原始值除以65.5(±500°/s量程下的灵敏度),得到单位为度/秒的角速度
代码实现如下:
typedef struct { float ax, ay, az; // 单位g float gx, gy, gz; // 单位°/s } IMU_Data_t; IMU_Data_t imu; void MPU6050_ReadData(void) { uint8_t buf[14]; MPU6050_ReadRegs(0x3B, buf, 14); int16_t ax_raw = (buf[0] << 8) | buf[1]; int16_t ay_raw = (buf[2] << 8) | buf[3]; int16_t az_raw = (buf[4] << 8) | buf[5]; int16_t gx_raw = (buf[8] << 8) | buf[9]; int16_t gy_raw = (buf[10] << 8) | buf[11]; int16_t gz_raw = (buf[12] << 8) | buf[13]; imu.ax = ax_raw / 16384.0f; imu.ay = ay_raw / 16384.0f; imu.az = az_raw / 16384.0f; imu.gx = gx_raw / 65.5f; imu.gy = gy_raw / 65.5f; imu.gz = gz_raw / 65.5f; }这里有个细节:连着0x3B到0x48一起读14个字节,比单独读加速度和陀螺仪两次更高效。I2C每发起一次传输都有起始停止条件,频繁切换容易出问题,一次读完是更稳妥的做法。
3. 姿态解算:从原始数据到可信角度
3.1 为什么不能直接拿原始数据判断跌倒
刚开始做这个项目时我特别天真,直接把合加速度超过阈值就触发报警,结果实验现场惨不忍睹——走路颠一下报警,弯腰捡东西报警,甚至用力跺了一下脚也报警。原因在于加速度计测量的是“比力”,它分不清重力分量和运动加速度分量。老人正常走路时脚落地会产生明显的冲击加速度,这个分量的方向和大小都在快速变化,直接拿合加速度做阈值判断,本质上是在拿一个非常嘈杂的信号做判断,误报是必然的。
所以正确做法是把传感器数据分成两条处理线:一条算合加速度抓冲击事件,另一条算姿态角度判断身体是否躺倒。姿态角不能直接用加速度计的原始值算,需要一个解算过程把六轴数据融合起来。
3.2 加速度计与陀螺仪的互补关系
姿态解算的经典方案有互补滤波、Mahony算法和EKF,在STM32F103这种主频不高的芯片上,一阶互补滤波是性价比最高的选择。
原理说穿了很简单。加速度计在静止时能准确测出重力方向,由此可以算出roll和pitch角,但动态下噪声很大,稍微一晃角度值就剧烈跳动。陀螺仪积分得到的角度在短时间内非常平滑准确,但积分会随时间漂移,时间越长误差越大。互补滤波的思想就是相信陀螺仪的短期变化,用加速度计长期校正漂移,两者各取所长。
从加速度计算姿态角的公式:
float acc_roll = atan2f(imu.ay, imu.az) * 180.0f / PI; float acc_pitch = atan2f(-imu.ax, sqrtf(imu.ay * imu.ay + imu.az * imu.az)) * 180.0f / PI;atan2f函数能自动处理象限问题,比atan好用得多,这算是我写姿态解算踩坑后的一个心得。
3.3 一阶互补滤波的代码实现
互补滤波的公式长这样:
float roll, pitch; float dt = 0.008f; // 125Hz采样率对应周期 void Attitude_Update(void) { float acc_roll = atan2f(imu.ay, imu.az) * 180.0f / PI; float acc_pitch = atan2f(-imu.ax, sqrtf(imu.ay * imu.ay + imu.az * imu.az)) * 180.0f / PI; roll = 0.98f * (roll + imu.gx * dt) + 0.02f * acc_roll; pitch = 0.98f * (pitch + imu.gy * dt) + 0.02f * acc_pitch; }这里的两个系数0.98和0.02是关键。0.98是陀螺仪权重,0.02是加速度计权重,两者相加等于1。权重怎么理解?可以把它看成一份信任分配——你更信任短期积分的结果(陀螺仪),但也留了一小部分信任给长期无漂移的参考(加速度计)。系数0.02意味着一旦加速度计算出的角度和积分结果出现偏差,大约每50个采样周期会拉回来一点,既不会让动态响应变迟钝,又能有效抑制陀螺仪零漂。
这个dt必须和实际采样周期严格对应。我在125Hz采样率下应该用0.008秒,但最开始偷懒写了个固定0.01秒,结果姿态角一直缓慢朝一个方向偏,找了很久才发现是采样率没对上。建议在定时器中断里推进数据读取和解算,确保采样周期精确。
4. 跌倒检测算法:状态机设计与阈值整定
4.1 合加速度的计算与物理含义
有了滤波后的姿态角,接下来要回到合加速度。合加速度的计算很简单:
float magnitude = sqrtf(imu.ax * imu.ax + imu.ay * imu.ay + imu.az * imu.az);静止时合加速度约等于1g,运动时会在1g上下波动。跌倒时身体撞击地面,合加速度会在极短时间内超过2.5g甚至3g,这个特征非常明显。但要强调一点,合加速度对方向不敏感,它只反映加速度大小的变化,无法区分“身体砸向地面”和“快速抬手”这类动作——所以它只能作为触发条件,不能作为唯一的判据。
4.2 三阶段状态机设计
我把检测过程设计成一个三状态状态机,每个状态对应跌倒过程的一个阶段,这样比单纯两个阈值判断要可靠得多。
typedef enum { STATE_NORMAL = 0, STATE_IMPACT, STATE_TILT_CHECK } Fall_State_t; Fall_State_t fall_state = STATE_NORMAL; uint32_t impact_time = 0;STATE_NORMAL是待机状态,程序持续监控合加速度。一旦合加速度超过冲击阈值,状态切换到STATE_IMPACT,同时记录时间戳。进入STATE_IMPACT后,系统在1.5秒内检查两个条件:一是合加速度是否回落到正常范围(1.3g以下),说明身体已经停止运动;二是滤波后的倾角是否超过50度,说明身体处于接近水平的状态。两个条件同时满足,判定为跌倒确认,触发报警。如果1.5秒内条件不满足,状态机复位回STATE_NORMAL,等待下一次触发。
状态机代码:
void Fall_Detect_Update(void) { float mag = sqrtf(imu.ax * imu.ax + imu.ay * imu.ay + imu.az * imu.az); float tilt = fabsf(roll) > fabsf(pitch) ? fabsf(roll) : fabsf(pitch); switch (fall_state) { case STATE_NORMAL: if (mag > 2.5f) { fall_state = STATE_IMPACT; impact_time = HAL_GetTick(); } break; case STATE_IMPACT: if (HAL_GetTick() - impact_time > 1500) { fall_state = STATE_NORMAL; // 超时,回到待机 } else if (mag < 1.3f && tilt > 50.0f) { fall_state = STATE_NORMAL; Fall_Alarm_Trigger(); // 确认跌倒,触发报警 } break; } }这个状态机的精髓在于把“瞬间的冲击”和“持续的姿态变化”组合在一起。单独看任何一个条件都可能误报,但组合起来后,必须同时满足“撞了一下”和“躺下了”两个条件才算数。我实测下来误报率能从单纯阈值判断的八成以上降到了两成以下。
4.3 阈值整定的实验方法与参考值
阈值不能拍脑袋定,我用串口把合加速度和倾角以一定的格式打印出来,让动作测试人员做一组动作,记录不同动作下的数值范围,再确定阈值。
我整理了一份参考数据:
| 动作 | 合加速度峰值 | 最大倾角 | 状态机结果 |
|---|---|---|---|
| 正常站立 | 约1g | 小于10度 | 不报警 |
| 正常行走 | 1.2g到2.0g | 小于30度 | 不报警 |
| 快速坐下 | 2.0g到2.4g | 约15度 | 不报警(倾角不够) |
| 弯腰捡东西 | 1.5g到1.8g | 约60度 | 不报警(冲击不够) |
| 仰面跌倒 | 2.8g到3.5g | 大于80度 | 报警 |
| 侧面跌倒 | 2.5g到3.0g | 大于80度 | 报警 |
这几个数值是我在状态机配合下测出来的。冲击阈值设在2.5g,倾角阈值设在50度,超时窗口设在1.5秒。如果你的佩戴位置或者测试人群有差异,这些值需要重新标定,不要直接抄。
4.4 报警执行与误报抑制机制
报警触发后,蜂鸣器以1Hz频率鸣叫,同时OLED屏幕显示“FALL DETECTED”和当前的倾角数据。如果老人在摔倒后还能活动,按下按键可以解除报警。如果1分钟内没人解除,报警强度提升,蜂鸣器切换为持续鸣叫,并在扩展方案中通过GSM或WiFi模块把报警信息发送给家属手机。
这里有个容易被忽略的细节:报警触发后不要立刻恢复检测。老人摔倒后往往需要一段时间才能站起来或者等待救援,如果传感器一直检测到“躺着”的状态,就会反复触发报警,造成干扰。我加了一个30秒的报警锁定期,报警触发后30秒内忽略新的事件,30秒后自动恢复到待机状态。
5. 整机调试与常见问题排查
5.1 数据漂移与随机跳变问题
调试中遇到最多的问题就是MPU6050数据漂移。表现为传感器静止不动,但角度一直在缓慢变化,或者读数突然跳到一个离谱的值。
漂移分两种。一种是陀螺仪零漂,静止时陀螺仪输出不是一个固定的0,而是一个很小的非零值,积分后角度会缓缓朝一个方向偏。解决方法是上电后先静止采集100组陀螺仪数据取平均,作为零偏值,之后每次读取都减去这个零偏。这是必须做的,不做的话互补滤波的长期稳定性无从谈起。
另一种是尖峰跳变,某一次读数突然异常高,原因通常是I2C时序不稳定或者电源纹波干扰。排查先从供电入手,用示波器看3.3V电源轨,如果纹波超过100mV,就需要在电源引脚旁边加一个10uF和0.1uF的电容。改善I2C线缆布局也很重要,杜邦线越短越好,尽量用屏蔽线或者干脆把传感器通过PCB排针直连。
5.2 误报漏报的参数调节手法
误报率高的时候先不要急着改大冲击阈值,容易把真报警也滤掉了。正确思路是:先用串口记录误报场景的数据,比如老人弯腰捡东西时合加速度到底到了多少、倾角到了多少,再去对应调节。我碰到过一种典型的误报场景是老人坐到较矮的椅子上,冲击峰值到了2.4g,倾角因为身体微微后仰达到了40多度,两个值都接近阈值但没触顶。这时候把倾角阈值从50度提高到55度,误报就消失了,同时不影响真实的跌倒检测,因为真跌倒是铁定会超过80度的。
漏报的情况则完全不同。如果合加速度一直达不到2.5g的阈值,通常是因为传感器佩戴位置太松,或者身体撞到柔软物体缓冲掉了冲击。除了提醒佩戴者固定好设备外,我建议把状态机的超时窗口从1.5秒放宽到2秒,这样能抓住更多缓冲时间较长的跌倒类型。
5.3 I2C通信卡死与复位恢复
HAL库的I2C在从机无响应时可能会卡在HAL_I2C_Mem_Read里不返回,表现是整个系统像死机了一样。这个问题在飞线环境下尤其常见。我的处理方法是给I2C通信加上超时和重试机制,主循环里每次通信前先用HAL_I2C_IsDeviceReady探测设备,如果连续5次失败就认为传感器掉线,重新调用初始化函数。实测下来这个办法能解决九成以上的通信卡死问题。
补充一个和本文主题无关但STM32开发必踩的坑:如果你同时在用ST-Link调试,配置GPIO时别把SWDIO和SWCLK这两个引脚复用到其他功能上,否则调试器会直接失联,处理起来非常头疼。我在CubeMX中把SYS的Debug设为Serial Wire就是为了保留调试口。
5.4 低功耗与续航优化思路
做可穿戴设备就绕不开功耗问题。我这版方案连续运行电流在50mA左右,用500mAh锂电池大约能撑10个小时,作为原型验证够用了,但离真正的产品还有距离。
省电可以从三方面入手。第一,采样率不用全程拉满,正常状态下用50Hz足够,当检测到合加速度超过1.8g时再动态提升到200Hz,把高采样率留给最需要的高动态阶段。第二,传感器休眠,MPU6050支持低功耗模式,在判断老人处于静止状态超过5分钟后,可以让主控进入STOP模式,仅保留RTC定时唤醒,每秒钟醒来一次采样几张数据,确认没有异常再继续休眠。第三,OLED不要常亮,只在报警和调试时点亮,这个屏幕在满亮度时能吃掉将近30mA电流,省下来非常可观。
6. 模拟测试与真实场景的差距
写完代码后,我搭建了一套简单的跌落测试平台来做模拟验证。方法是在地面上铺一块20厘米厚的海绵垫,测试者将一个内置了这套系统的腰包绑在腰部,然后从站立姿势向海绵垫倒下,重复仰面倒、侧面倒、趴着倒三种姿势各10次;再用正常走路、跑步、坐下、弯腰捡东西四个日常动作各做20次,统计报警准确度。
测试下来仰面和侧面跌倒的检出率达到100%,俯面倒因为姿态角接近90度但冲击峰值稍低,有一两次没触发,后来把冲击阈值从2.5g降到2.3g后问题解决。日常动作的误报率大约在15%,主要集中在快速坐下的动作上,我针对坐姿做了倾角上限的判断,最终把误报率压到了10%以内。对于一套采用M3内核加六轴传感器的原型方案来说,这个指标可以接受,但距离真正的商用产品还有不小的距离,商用方案还需要加入气压计数据来辅助判断高度变化。
这套系统后续可以扩展的方向很多。最实用的是加一个ESP8266模块,通过MQTT协议把报警信息推到微信或钉钉,家人无论在哪里都能第一时间收到通知;也可以在报警触发后自动拨打预设电话,这需要外接一个SIM800C模块。数据采集方面可以用SD卡模块记录老人的活动轨迹和姿态变化趋势,长期积累后还能分析出平衡能力的衰退趋势,提前发现跌倒风险。如果你也想做类似的项目,我的建议是先把姿态解算和状态机跑通,再一步步加功能,不要一开始就把系统搞得太复杂,单片机的调试复杂度是随着功能数量指数级上升的。
本文还有配套的精品资源,点击获取
