爸妈年纪大了,最怕的就是一个人在家时磕着碰着没人知道。前阵子我做养老相关的硬件方案验证,最常被问到的一个问题是:能不能做一个老人随身带的小手环,万一摔倒了能自动通知家人,家人手机上还能看到位置。这个需求听起来复杂,但拆开技术点,就是标题里那几样东西的叠加——基于STM32单片机的手环跌倒检测系统,带GSM短信报警、GPS北斗定位、APP地图显示,再加一个按键控制的高亮灯手电筒。整套方案并不神秘:STM32做主控,加速度传感器判断跌倒动作,GSM模块把报警短信发出去,GPS/北斗模块把坐标塞进短信里,手机端APP把坐标落到地图上,按键和LED负责手电筒照明。不管你是做毕业设计、课程设计,还是想给家里老人做一款守护设备原型,这条链路都有参考价值。下面我从选型到代码,从短信指令到坐标纠偏,按实际能落地的思路完整讲一遍。
1. 功能拆解与硬件选型:先别急着焊板子
1.1 从标题里拆出真正的功能模块
标题虽然很长,但信息密度很高。我把它拆成四个核心任务:
| 功能 | 实现模块 | 核心要点 |
|---|---|---|
| 跌倒检测 | 三轴/六轴加速度传感器 | 采集加速度,识别摔倒冲击与姿态变化 |
| 短信报警 | GSM模块 | AT指令控制发送报警短信 |
| 定位 | GPS/北斗双模定位模块 | NMEA协议解析经纬度、时间、定位状态 |
| 地图显示 | 手机APP | 接收位置并在地图SDK上展示 |
| 手电筒 | 高亮LED + 按键 + MOS驱动 | 按键控制大功率LED通断、亮度 |
看清楚模块清单后再谈选型,否则很容易买一堆用不上的器件。实际项目中,手电筒看起来很简单,但它的硬件驱动和按键逻辑,反而和跌倒检测一样需要仔细设计。
1.2 主控为什么选STM32
这套系统的外设需求不算复杂:一路I2C读加速度传感器,两路UART分别接GSM模块和定位模块,几个GPIO接按键和LED,最多再来一个UART接调试串口。随便找一颗满足这些外设的单片机都能做,但STM32F103C8T6几乎成了这类项目的默认答案。
原因很现实:一是便宜,十几块钱的芯片足够跑完整套逻辑;二是资料多,无论是寄存器操作还是HAL库,踩坑经验在论坛里一抓一大把;三是功耗和性能平衡,72MHz的主频做合加速度计算、状态判读绰绰有余,20KB RAM在合理裁剪代码时也够用。
有人会问,用Arduino不是更快?但GSM模块和定位模块的串口通信、超时处理、缓冲区管理这些逻辑,Arduino的跑马灯式开发风格容易把流程写乱,而且最终要落到低功耗、看门狗这些可靠设计时,STM32的开发自由度明显更高。
1.3 传感器、通讯模块与供电的选型逻辑
加速度传感器我建议使用MPU6050,虽然是六轴(加速度计+陀螺仪),但跌倒检测用加速度计就够,陀螺仪的角速度数据可以作为辅助判据,对识别姿态变化很有帮助。如果追求省电和简单,ADXL345单加速度计也完全可以。MPU6050走I2C,一根SDA一根SCL就能搞定,接线干净。
GSM模块要选串口电平兼容的型号。我常用的模块支持2.8V左右的高电平串口,和STM32的3.3V电平可以直接对接,不需要电平转换芯片。GSM模块整机供电是另一个大坑,发射时峰值电流可能飙到2A,绝对不能从单片机稳压输出上硬拖,后面我会专门讲供电。
定位模块建议选支持GPS和北斗双模的型号,单独GPS模块在楼宇密集区的定位速度和稳定性差不少。手里有一块串口输出NMEA语句的双模模块,9600波特率,冷启动大概几十秒,室外空旷环境表现很稳。
供电部分则是一节3.7V锂电池加充放电管理,再用一颗低压差稳压芯片给STM32和传感器供3.3V。GSM模块可以从电池端取电,因为它的工作电压范围通常是3.4V到4.4V,刚好贴合锂电池电压区间。
2. 跌倒识别这一关:加速度数据如何变成报警信号
2.1 先从物理量说起
很多人第一次接触跌倒检测,觉得只要“加速度突然变大”就触发报警。但真正落地时你会发现,这个判断太粗糙了。要理解跌倒识别的算法,得先明白加速度传感器到底在测什么。
静止站立时,三个轴的合成加速度约为1g(约9.8米每平方秒)。注意,如果把传感器固定在手上或者腰间,静止时重力加速度会分摊到多个轴上,不一定全部落在Z轴。跌倒过程大致是:身体先失重(人往下掉),合加速度短暂下降;随后撞到地面,出现明显的冲击峰值,可能超过2.5g甚至3g;最后人躺平,姿态和站立时完全不同,而且短时间内不再大幅运动。
所以,跌倒识别不是看单一数值,而是看整个时间窗口内的“合加速度曲线 + 姿态变化 + 后续静止状态”。这三个特征缺一不可,少了任何一个,误报率都会高到无法使用。
2.2 跌倒的三段式特征
我把常用判断特征整理成表,这组参数可以作为初版算法的基础:
| 阶段 | 特征 | 参考判断条件 |
|---|---|---|
| 失重阶段 | 合加速度回落 | 低于0.8g,持续若干毫秒 |
| 冲击阶段 | 合加速度尖峰 | 超过2.5g~3.0g |
| 姿态阶段 | 传感器姿态趋于水平 | 某水平轴加速度接近1g |
| 静止阶段 | 短时间内运动幅度小 | 几秒内合加速度稳定在1g附近 |
单看冲击峰值很容易把“拍桌子”、“猛捶沙发”也当成跌倒。加上姿态判断后,剧烈撞击但人没有躺下的动作会被过滤掉;再加上后静止判断,又可以把“跳跃后落地但马上恢复站立”这样的场景排除。
2.3 算法流程与防误报设计
实际操作中,我倾向用一段滑动窗口循环处理数据,而不是依赖中断。每50毫秒采集一次加速度,计算合加速度,然后用一个状态机来控制流程:
- 正常状态:持续采样,发现合加速度超过阈值,进入“疑似撞击”状态;
- 疑似撞击:等待几百毫秒,检查加速度是否回落,同时观察姿态是否改变;
- 疑似倒地:继续观察数秒,如果合加速度稳定在1g上下并且姿态接近水平,则判定跌倒;
- 报警确认:触发GSM短信,同时进入冷却时间,防止重复误报。
防误报还有一招很实用:在APP或者短信里留一条退路,例如报警短信发出后,允许老人通过按键取消报警,或者家人收到短信后先电话确认。这个设计在真实场景里非常重要,因为算法永远无法做到百分百准确。
2.4 代码级实现思路
下面是一个简化版的C语言思路,我用定时器中断保证采样周期稳定:
void ReadAccel(uint16_t *ax, uint16_t *ay, uint16_t *az); float vector_magnitude = sqrt(ax*ax + ay*ay + az*az) / ACCEL_SCALE; if (vector_magnitude > IMPACT_THRESHOLD) { impact_flag = 1; impact_time = HAL_GetTick(); } else if (impact_flag) { // 检查冲击后是否进入低运动状态 if (vector_magnitude < STATIC_THRESHOLD) { static_counter++; if (static_counter > STATIC_SAMPLES) { // 判定位姿是否接近水平 if (IsHorizontal()) { SendAlertSMS(); impact_flag = 0; EnableCooldown(COOLDOWN_5MIN); } } } }代码本身不难,难的是确定那几个阈值。我个人的经验是,先把阈值放低(比如2.2g),然后带着板子做各种日常动作实测,记录误报情况,再逐步调高到误报和漏报都能接受的区间。没有捷径,就是反复试。
3. 报警链路打通:GSM短信与定位模块的串口协作
3.1 GSM模块的AT指令入门
GSM模块和STM32之间就是串口通信,核心是AT指令。模块上电后先要等它搜网,这个时间从几秒到几十秒不等。判断模块状态最常用的指令是:
AT模块返回OK,说明串口通信正常。接着查询网络注册状态:
AT+CREG?返回+CREG: 0,1表示已经注册上网络。发送短信的流程是先进入短信发送模式:
AT+CMGS=13800001234然后输入短信内容,最后发一个十六进制的结束符0x1A(对应ASCII的CTRL+Z)。在STM32里用串口发送时,结束符要用char end = 0x1A;这种方式发送,不能用字符串直接拼。
还有个容易踩的细节:AT指令的换行符。很多模块要求以\r\n结尾,少发一个回车,指令就石沉大海。调试时先用一个串口工具,手动把整个流程跑通,再写进单片机程序。
3.2 GPS/北斗模块的NMEA数据怎么解析
定位模块上电后,会以9600波特率不断输出NMEA格式的文本行。常用的语句有$GNGGA(位置信息)和$GNRMC(推荐最小定位信息)。
看一条典型的GGA语句理解字段含义:
$GNGGA,093856.00,3101.23456,N,12123.45678,E,1,08,1.2,12.5,M,0.0,M,,*5F字段从左到右依次是:UTC时间(上午9点38分56秒)、纬度(31度1.23456分,北纬)、经度(121度23.45678分,东经)、定位状态(1表示有效定位)、卫星数、水平精度因子、海拔高度等。
单片机解析时不要用sscanf图省事,因为NMEA语句的字段长度不固定,而且数据是异步到达的,容易遇到半行数据。正确做法是:以$开头到\r\n结束,攒一整行后再按逗号拆字段。解析出经纬度后,还要把“度分格式”换算成“度”格式:
float raw_degree = atof(buff); float degree = (int)(raw_degree / 100) + (raw_degree - (int)(raw_degree / 100) * 100) / 60;3.3 短信内容设计与人机交互
报警短信的接收人是家人,内容必须让人一眼看懂并知道怎么处理。我的设计是:
[跌倒报警] 平台时间 14:23:10 位置:31.0173N,121.2334E 点此查看地图:http://xxxx?lat=31.0173&lon=121.2334如果已经连到服务器,可以把坐标转换成地图链接,家人点开直接看位置。没条件部署服务器时,纯短信也能用,收信人把经纬度输入地图搜索框一样能定位,只是操作麻烦些。
短信长度也要考虑,一条普通短信最多约70个汉字或140个ASCII字符,链接和坐标要控制字数,太长会拆成多条,反而影响阅读。实际测试中,我最终把地址链接做了短链处理,保持短信只占一条。
3.4 定位失败与重复报警的兜底方案
定位模块不是任何时候都能定位成功,室内基本无信号,冷启动也可能搜不到星。此时强行把空坐标发出去,家人只会收到一条无意义短信。我在程序里的兜底逻辑是:
- 跌倒判定成立后,立即尝试读取定位数据;
- 如果1分钟内定位失败,先发送“老人跌倒但暂未定位到坐标”的短信;
- 之后每过一段时间重试,一旦定位成功,再补发带坐标的位置短信;
- 整个报警流程进入冷却期(例如5分钟),期间不再重复报警,避免GSM短信轰炸。
家人在场景里最需要的是“有人摔了”这个事实,坐标可以稍后补。这个优先级顺序比一开始就死等定位更实用。
4. 按键手电筒:小功能里藏着的硬件与软件细节
4.1 为什么LED不能直接接IO口
高亮灯手电筒听起来简单,但直接拿STM32的GPIO驱动大功率LED是行不通的。普通GPIO输出电流只有几毫安到二十毫安,而一颗1W白光LED需要350mA左右电流。真让GPIO硬扛,芯片发热不说,很快就烧引脚。
正确做法是用一颗N沟道MOS管做低边开关。MOS管的栅极接STM32的IO口,漏极接LED负极,LED正极通过限流电阻接电池正极,源极接地。IO输出高电平,MOS管导通,LED点亮;IO输出低电平,MOS管截止,LED熄灭。
如果电池是3.7V锂电池,LED的典型压降约3.0到3.3V,可以直接串一颗限流电阻凑到目标电流。以350mA为例,电阻值约(3.7 - 3.2) / 0.35 ≈ 1.4欧姆,选2欧姆左右、功率1W以上的电阻比较稳妥。要注意的是锂电池电压会从4.2V降到3.4V,电流会跟着变,所以这种限流方案精度一般。想要亮度恒定,可以换成恒流驱动芯片,但对一个手电筒功能来说,限流电阻加PWM调节完全够用。
4.2 按键消抖与单击/长按状态机
按键硬件上需要加一个上拉电阻,软件里最基础的是消抖。我的习惯是用定时器每10毫秒扫描一次按键电平,连续读到多次稳定电平后才认为按键状态有效,而不是用延时消抖。因为大电流LED开启瞬间电池电压会有波动,用阻塞延时容易把主流程卡住。
按键功能可以做成这样:
- 单击:开关手电筒;
- 长按:触发求助短信(可以和跌倒报警走同一条短信链路)。
为了区分单击和长按,我用一个简单状态机:记录按键按下的时间戳,松开时判断时间差。小于300毫秒是单击,大于1秒是长按。要注意的是,单击开关灯之后,还要等一个“按键释放确认”周期,防止按下后松开的瞬间又触发一次。
4.3 手电筒场景的功耗与使用体验
手电筒的耗电大头是LED,350mA的电流对一块几百毫安时的小锂电池来说撑不了多久。老人夜间起床使用,不需要全程满亮度。我建议加一个PWM亮度调节:短按第一下开灯是低亮度,第二下变高亮度,第三下关灯。这样既省电,又不至于半夜被强光刺眼。
另外,电池电压检测也建议做。当电压低于3.5V时,手电筒自动禁止高亮度模式,只允许低亮度照明。否则电池电压被拉低到GSM模块的最低工作电压以下,会导致整个系统不稳定,反而影响报警主功能。
5. APP地图显示:定位数据从短信到屏幕的落地之旅
5.1 数据链路怎么选
很多人看到标题里“APP地图设计”时,默认以为手环直接蓝牙连APP显示位置。但在GSM短信方案下,手环和手机之间并没有持续连接。实际做起来有三种路线:
| 方案 | 数据流向 | 适合场景 |
|---|---|---|
| 位置短信 + 地图链接 | 手环 -> 家人手机短信,点链接看地图 | 最简单,不用做APP |
| 手环+蓝牙模块 | 手环 -> 手机APP,近距离直接显示 | 适合调试演示,距离受限 |
| 手环+服务器+APP | 手环通过GPRS/4G上传,APP从服务器拉取 | 功能完整,适合长期产品 |
如果要以“APP地图设计”作为项目的重点,我建议做第三种,但考虑到GSM模块本身支持GPRS数据传输,可以让手环把定位数据通过TCP发送到服务器,APP再从服务器拉取。这样家人不在身边时,也能远程看到老人的实时位置和历史轨迹。
开发APP时不需要从零写地图引擎。接入国内主流地图开放平台的SDK,引入一个地图控件,添加标记点、轨迹线,几分钟就能搭出界面。前期调试时,甚至可以先做一个只读位置的小工具,验证数据链路通了再美化界面。
5.2 必须处理的坐标偏移问题
这是地图开发里最容易忽略的坑。定位模块输出的是WGS84坐标系(GPS卫星系统使用的国际标准坐标),而国内地图开放平台出于坐标加密要求,使用的通常是GCJ02坐标系(俗称“火星坐标系”)。直接把WGS84坐标传给地图SDK,你会发现位置整体偏移几百米,这是坐标系不匹配导致的。
解决办法是在服务器端或者APP端做一次坐标转换。网上有公开的转换算法,核心思路是把经纬度先换算成平面坐标,再做非线性纠偏。IOS直传这种方式看上去别人的代码能调通,但转换公式写错一个常数,地图上的位置就离谱到隔壁街道。我的建议是,先拿一个已知地点的GPS原始坐标和地图显示坐标对比,验证转换逻辑正确后再大批量处理数据。
5.3 APP端功能设计与刷新策略
APP的界面不需要复杂,核心要素就四块:
- 地图视图:显示老人手环的最后已知位置;
- 状态卡片:显示老人“正常/疑似跌倒”状态、最后定位时间;
- 报警提示:收到跌倒报警时,地图自动定位到报警位置并弹出提醒;
- 历史轨迹:按时间回放老人的活动轨迹,方便家人掌握日常活动习惯。
刷新策略上要克制。老人手环不是高频运动追踪器,位置上传频率设为每5分钟一次就够。实时性要求高的跌倒报警走的是短信,定位数据是辅助确认位置的,频繁刷新只会白白消耗流量和电池。
6. 实测排障记录:这些坑每一届做类似项目的人都会遇到
6.1 供电与串口通信:第一道坎
我刚开始把GSM模块和STM32用同一个3.3V稳压电源供电,结果只要模块一发射短信,单片机就复位。原因很简单:GSM发射瞬间电流太大,把电压瞬间拉低,STM32供电跌到复位阈值以下。解决方案是让GSM模块直接从电池取电,同时给它的电源输入端并联一个大容量的电解电容(比如470微法),吸收瞬态压降。单片机和其他传感器则通过稳压芯片供电,两路电只在电池端交汇,系统立刻稳定下来。
串口通信的坑也集中在前几次调试。模块波特率设成9600,单片机代码里却配成115200,出来的自然是乱码。还有公地问题,GSM模块、定位模块、STM32必须共地,否则UART电平参考点不一致,数据错乱是必然的。
6.2 定位测试与天线摆放
定位模块的实测结果和芯片参数是两回事。我把手环样机放在室内靠窗位置,冷启动定位用了接近两分钟;拿到楼顶空旷处,最快一次只要三十多秒。陶瓷天线对手环外壳的金属件特别敏感,手表表带、金属卡扣贴在天线附近,卫星数量明显下降。
建议所有项目成员拿到样机后,先固定一个标准测试位:室外空旷、手环腕带朝上、天线区域无遮挡。这样测出来的定位时间才有可比性,不会因为测试环境不一致得出互相矛盾的结论。
6.3 防误报参数的实际调参记录
跌倒检测的阈值调参过程最能体现“纸上谈兵”和“动手实测”的区别。我最初把冲击阈值设为2.5g,自动判定逻辑也写好了,结果实测时拍桌子、快速甩手、从椅子上跳起来都触发了报警,误报率高得没法用。把阈值调到3.0g,再加上“冲击后必须姿态接近水平”和“数秒内保持静止”两个条件,误报才明显减少。
有意思的是,后来在模拟老人缓慢倒地的场景时,发现冲击峰值只有2.2g左右。阈值定高了会漏报,定低了误报,最终我针对不同佩戴位置做了一组对照试验,同时引入了陀螺仪的角速度变化作为辅助判据才平衡下来。做完这轮实测,我深刻体会到一件事:算法参数一定要跟着真实使用场景调,不能只看数据手册。
实际的佩戴位置也很关键。手环如果戴在手腕上,跌倒时手臂的挥动可能导致加速度特征不明显;戴在腰部则更贴近人体重心变化。业内很多方案会选择推荐佩戴在腰腹部,但老人通常更习惯戴手腕,这会直接影响算法的判断逻辑。我在最终方案里做了一个简单的补偿:在APP里可以选择佩戴位置,不同位置对应不同阈值组。
做完整套系统,我最大的体会是,这类项目真正难的不是某一个单独模块,而是让传感器、通讯模块、电源管理和APP这一堆东西在有限体积、有限功耗和有限成本下稳定协同。给后来人的建议很实在:先分模块单独调试,每个模块用串口工具确认无问题后再整合;不要嫌麻烦,合体之后出了问题,排查成本成倍上升。后续想做产品级方案的话,可以考虑把GSM换成4G Cat.1模块,主控换成低功耗系列,再用实时操作系统管理多任务,整个系统的可靠性和续航都会再上一个台阶。