1. 从一个“不起眼的小玩意”说起:DIY jump sensor 到底能做什么
第一次听到“DIY jump sensor”这个词,很多人脑子里冒出来的画面可能是健身房里的专业弹跳测试仪,或者是运动员身上贴满传感器的高科技装备。其实完全不是那么回事。所谓 jump sensor,直译过来就是“跳跃传感器”,核心功能只有一个:检测物体是否离开地面,以及离开地面多久、跳了多高、跳了几次。而加上“DIY”三个字,意味着这东西不需要你花大几千去买商用设备,用几十块钱的常见元件加上一块单片机,就能自己攒出来。
我最早接触这个需求,是帮一个做青少年体能训练的朋友解决“摸高测试”的痛点。传统摸高要么用粉笔在墙上画,要么用电子摸高器,前者不卫生、后者贵且固定。后来我想,能不能做一个巴掌大的小盒子,往地上一放,人从上面跳过去或者站在上面起跳,它就能自动记录腾空时间和跳跃高度?这就是 DIY jump sensor 最朴素的应用场景。
它的适用人群其实比想象中广得多。体能教练可以用它做纵跳测试,评估运动员的爆发力变化;体育老师可以用它做课堂上的立定跳远辅助测量;创客爱好者拿它当入门级的传感器融合项目练手;甚至家长也能陪孩子玩“跳格子”游戏时统计次数。技术栈上,它横跨了嵌入式开发、传感器信号处理、机械结构设计三个领域,是一个典型的“小项目、大综合”的练手题材。
这篇文章我会从方案选型一路讲到代码实现和现场调试,把我在这个项目上踩过的坑、试过的错、最后跑通的方案完整拆开。不管你是刚摸单片机的学生,还是想找个周末项目练手的老手,都能直接抄作业。
2. 方案选型:为什么我最终放弃了红外和超声波
2.1 三种主流检测原理的横向对比
做 jump sensor,第一步不是写代码,而是想清楚“用什么感知跳跃”。市面上能用的原理无非三类:红外对射、超声波测距、惯性测量单元(IMU)。我最初图便宜,先试了红外对管,结果被现实狠狠教育了一顿。
红外对射的原理很简单:一端发射红外光,另一端接收,人跳起来挡住光路就触发一次。听起来很美好,但实际用起来问题一大堆。首先,红外对管的检测范围很窄,通常只有几厘米到十几厘米,人跳起来脚一偏就漏检。其次,环境光干扰严重,阳光里的红外成分会让接收端一直处于饱和状态,白天在室外基本没法用。我试过加遮光罩、调制解调,折腾了两天,稳定性依然堪忧。
超声波方案稍微好一点。HC-SR04 这类模块便宜又好买,测距范围 2cm 到 400cm,理论上把模块朝上安装,人跳起来时距离变化就能反映高度。但超声波有个致命伤:波束角太大。HC-SR04 的波束角大约 15 度,在 1 米高度上光斑直径就有 26 厘米,人跳起来时身体各部位反射回来的回波混在一起,测距值跳得厉害。而且超声波对软质衣物吸收严重,穿羽绒服跳和穿紧身衣跳,回波强度完全不一样。
最后我选了IMU 方案,具体来说是用一块六轴传感器(三轴加速度计 + 三轴陀螺仪),固定在人身上或者踏板上,通过加速度积分来判断腾空。这个方案的好处是:不受环境光影响、不受衣物材质影响、体积小、功耗低。缺点是算法复杂,零漂和积分误差需要处理。但恰恰是这一点,让这个项目有了“技术含量”,也让我学到了最多东西。
| 方案 | 成本 | 精度 | 环境适应性 | 算法复杂度 | 推荐指数 |
|---|---|---|---|---|---|
| 红外对射 | 极低 | 低 | 差 | 低 | 不推荐 |
| 超声波 | 低 | 中 | 中 | 中 | 特定场景可用 |
| IMU | 中 | 高 | 好 | 高 | 强烈推荐 |
2.2 IMU 方案的核心思路拆解
用 IMU 做跳跃检测,核心逻辑其实就一句话:当合加速度的模值显著小于重力加速度时,说明物体处于失重状态,即腾空。自由落体时加速度计读数为零(理想情况),实际跳跃时由于空气阻力和身体摆动,读数会在 0 到 1g 之间波动。所以判断条件可以写成:合加速度模值持续低于某个阈值(比如 0.5g)超过一定时间(比如 50ms),就认为是一次跳跃的开始;当合加速度恢复到 1g 附近并稳定一段时间,认为跳跃结束。
这个思路听起来简单,但实际写起来有一堆细节要处理。比如人走路时也会有短暂的失重感,但持续时间通常只有 20-30ms,所以阈值时间要卡在 50ms 以上。再比如落地瞬间会有冲击加速度,可能达到 3g 甚至 5g,这个冲击信号要用来辅助判断落地时刻,提高腾空时间计算的精度。
我最终选的主控是ESP32-C3,原因有三:一是它自带 Wi-Fi 和蓝牙,方便把数据实时传到手机上看波形;二是它的 ADC 和 I2C 外设够用,驱动 IMU 绰绰有余;三是价格便宜,某宝上十几块钱就能买到开发板。IMU 选的是MPU6050,经典款,资料多,虽然老但足够用。如果你手头有 ICM-20602 或者 BMI160,也可以直接替换,代码改一下寄存器地址就行。
3. 硬件搭建:从零把电路跑起来
3.1 物料清单与接线方案
先列一下我最终定版的物料清单,都是常见件,不存在买不到的问题:
- ESP32-C3 开发板 x1
- MPU6050 模块 x1
- 锂电池 3.7V 500mAh x1
- TP4056 充电模块 x1
- 轻触开关 x1
- 洞洞板 5x7cm x1
- 排针、排母、细导线若干
接线方面,MPU6050 和 ESP32-C3 之间走 I2C 协议,只需要四根线:VCC 接 3.3V,GND 接 GND,SCL 接 GPIO5,SDA 接 GPIO4。这里有个坑要注意:MPU6050 模块上的上拉电阻。很多廉价模块自带上拉,但阻值偏大(10k),在 400kHz 的 I2C 速率下波形上升沿会变缓,导致通信失败。我的做法是在 SCL 和 SDA 上各并联一个 4.7k 的电阻到 3.3V,问题立刻解决。
电源部分,TP4056 负责给锂电池充电,输出直接接到 ESP32-C3 的 5V 引脚(开发板上有 LDO 会降到 3.3V)。这里注意 TP4056 的输出电压是电池电压,满电 4.2V,欠压 3.0V,而 ESP32-C3 的 5V 引脚实际可以接受 3.3V 到 5.5V 的输入,所以直连没问题。但如果你用的是其他开发板,一定要查清楚输入电压范围,别把板子烧了。
提示:MPU6050 对电源纹波比较敏感,如果发现数据跳动大,可以在 VCC 和 GND 之间并一个 100nF 的陶瓷电容,紧贴模块引脚焊接,效果立竿见影。
3.2 机械结构:传感器到底装在哪里
这个问题我纠结了很久。装在人身上(比如绑在腰间)和装在踏板上,算法逻辑完全不同。绑在身上时,传感器跟着人体一起运动,加速度计读的是人体质心的加速度;装在踏板上时,传感器静止,只有人跳离踏板后踏板回弹的振动信号。前者能直接测腾空时间,后者只能测“人离开踏板”这个事件,精度差很多。
我最终选了绑在腰间的方案。具体做法是找一条弹性绑带,把洞洞板固定在腰带位置,传感器芯片朝外。为什么朝外?因为 MPU6050 的 Z 轴垂直于芯片表面,朝外安装时 Z 轴正好对应人体的前后方向,X 轴对应左右,Y 轴对应上下。这样在静止站立时,Y 轴读数约为 1g,X 和 Z 轴约为 0g,后续做姿态解算时坐标系最直观。
如果你要做踏板方案,也不是不行,但需要加一个压力传感器或者微动开关来辅助判断起跳时刻。我试过用 FSR402 薄膜压力传感器,响应速度够快,但一致性差,不同批次灵敏度不一样,最后放弃了。
3.3 焊接与组装注意事项
焊接顺序建议从矮到高:先焊排针,再焊电阻电容,最后焊模块。MPU6050 模块的引脚比较细,焊接时烙铁温度调到 320 度左右,停留时间不要超过 3 秒,否则容易把焊盘烫掉。我第一块板子就是因为烙铁温度太高,把 SDA 焊盘弄脱落了,只能飞线补救。
电池用双面胶固定在洞洞板背面,TP4056 模块用热缩管包一下防止短路。整个装置做出来大概一个火柴盒大小,重量不到 50 克,绑在腰上几乎感觉不到。开关串联在电池和 TP4056 之间,不用的时候关掉,500mAh 电池能撑大概 8 小时连续工作。
4. 固件开发:从读取原始数据到算出跳跃高度
4.1 开发环境搭建与基础读数
我用的开发环境是 Arduino IDE,ESP32-C3 的板级支持包装 2.0.11 版本,这个版本对 C3 的支持最稳定。MPU6050 驱动直接用现成的库,但我不建议用太复杂的库,因为很多库封装了 DMP(数字运动处理器),反而把原始数据挡住了。我自己写了一个精简的 I2C 读写函数,直接操作寄存器,代码量不到 100 行,但完全可控。
初始化流程是这样的:先复位 MPU6050,设置采样率为 1kHz,配置加速度计量程为 ±4g,陀螺仪量程为 ±500°/s,然后关闭中断,使能加速度计和陀螺仪。这里量程选择有讲究:±4g 的量程对应灵敏度 8192 LSB/g,分辨率足够,而且落地冲击时不容易饱和。如果你选 ±2g,冲击瞬间读数会溢出,导致数据截断,影响落地检测。
基础读数代码大概长这样:
void read_mpu(float *ax, float *ay, float *az) { uint8_t buf[6]; i2c_read(MPU_ADDR, ACCEL_XOUT_H, buf, 6); *ax = (int16_t)((buf[0] << 8) | buf[1]) / 8192.0; *ay = (int16_t)((buf[2] << 8) | buf[3]) / 8192.0; *az = (int16_t)((buf[4] << 8) | buf[5]) / 8192.0; }读出来的单位是 g,静止时合加速度模值应该在 1.0 附近。如果偏差超过 0.1,说明传感器需要校准。校准方法很简单:把传感器水平静止放置,采集 1000 个样本取平均,得到零偏值,后续读数减去零偏即可。
4.2 姿态解算:为什么需要陀螺仪
只用加速度计能不能判断跳跃?能,但精度有限。因为加速度计测量的是“比力”,包含重力分量和运动加速度。当人体前倾或后仰时,重力在 Y 轴上的分量会变化,导致合加速度模值偏离 1g,容易误判。加入陀螺仪后,可以通过互补滤波或者卡尔曼滤波把姿态角算出来,然后把重力分量从加速度里减掉,得到纯运动加速度。
我用的互补滤波,公式很简单:
angle = 0.98 * (angle + gyro * dt) + 0.02 * accel_angle;其中accel_angle是从加速度计算出的倾角,gyro是陀螺仪角速度。这个滤波器的物理意义是:高频信任陀螺仪,低频信任加速度计。实际跑下来,姿态角误差在 1 度以内,完全够用。
算出姿态后,把加速度从机体坐标系转到地理坐标系,得到垂直方向的运动加速度a_vertical。这个值在静止时应该接近 0,跳跃时先负后正(起跳时向下蹬地,加速度向上,但传感器感受到的是反作用力,所以先出现负值再出现正值)。
4.3 跳跃检测状态机设计
整个检测逻辑我用一个状态机来实现,状态转移图如下(文字描述):IDLE(空闲)→ TAKEOFF(起跳)→ AIRBORNE(腾空)→ LANDING(落地)→ IDLE。
IDLE 状态下,持续监测合加速度模值。当模值低于 0.5g 且持续时间超过 50ms,进入 TAKEOFF 状态,记录当前时间戳t_start。TAKEOFF 状态是一个过渡态,持续 100ms,用来确认不是噪声引起的误触发。如果 100ms 内模值恢复到 0.8g 以上,说明是误触发,回到 IDLE;否则进入 AIRBORNE。
AIRBORNE 状态下,持续监测模值。当模值超过 2.5g 且持续时间超过 20ms,认为落地冲击发生,记录时间戳t_end,进入 LANDING。LANDING 状态持续 200ms,等待信号稳定,然后计算腾空时间t_air = t_end - t_start,通过公式h = 0.5 * g * (t_air/2)^2计算跳跃高度,最后回到 IDLE。
这个状态机的参数都是我在实测中调出来的。比如 50ms 的失重阈值,是因为人走路时单脚离地时间大约 300ms,但失重感只持续 20-30ms,所以 50ms 能过滤掉走路。再比如 2.5g 的落地阈值,是因为正常落地冲击在 3g 左右,但如果你轻轻落地可能只有 1.5g,所以这个阈值要结合你的使用场景调整。
4.4 数据滤波与零漂处理
MPU6050 的零漂是个绕不开的问题。即使静止不动,加速度计读数也会有 ±0.02g 的波动,陀螺仪零漂更大,可能达到 ±2°/s。这些零漂积分后会变成累积误差,导致姿态角慢慢漂移。
我的处理方法是:每次进入 IDLE 状态时,重新校准零偏。具体做法是采集 200 个样本,计算均值和标准差,如果标准差小于 0.05g,认为处于静止状态,更新零偏值。这样每次跳跃前都会自动校准,漂移问题基本解决。
另外,原始数据还要过一个低通滤波器,截止频率设在 20Hz。因为人体跳跃的主要频率成分在 5Hz 以下,20Hz 以上的都是噪声。我用的是二阶巴特沃斯滤波器,差分方程如下:
y[n] = 0.2929*y[n-1] - 0.1716*y[n-2] + 0.0975*x[n] + 0.1950*x[n-1] + 0.0975*x[n-2];这个滤波器在 20Hz 处衰减 3dB,在 50Hz 处衰减 20dB,效果很好。注意采样率要稳定在 200Hz,我用的是定时器中断触发采样,避免loop()里delay()导致采样间隔不均匀。
5. 实测与调参:那些文档里不会写的经验
5.1 第一次实测:数据跳得没法看
板子焊好、代码烧进去之后,我兴冲冲地绑在腰上跳了几下,结果串口打印出来的数据简直没法看。腾空时间算出来 0.8 秒,跳跃高度 80 厘米,而我实际只跳了 30 厘米左右。问题出在哪?我把原始数据存下来用 Excel 画了个图,发现落地冲击信号被误判成了两次跳跃,导致腾空时间被拉长了。
原因有两个:一是落地后身体会有一个反弹,加速度计读数会再次出现短暂的低值,被状态机误认为是第二次起跳;二是我的落地阈值设得太低,2.5g 的冲击在硬地面上很容易达到,但在木地板上只有 1.8g,导致落地检测延迟。
解决办法:在 LANDING 状态后加一个500ms 的锁定时间,期间不响应任何新的跳跃检测。同时把落地阈值改成自适应:根据前一次跳跃的冲击峰值动态调整,公式是threshold = 0.7 * last_peak。这样无论在地板还是地垫上,都能可靠检测。
5.2 不同跳跃类型的识别
调好基础检测后,我发现一个问题:下蹲跳和连续跳的波形完全不一样。下蹲跳有一个明显的下蹲蓄力过程,加速度先正后负再正,而连续跳几乎没有蓄力,直接起跳。如果都用同一套参数,连续跳的腾空时间会算短。
我的做法是加一个简单的分类器:如果起跳前 300ms 内垂直加速度出现过大于 0.5g 的正值,判定为下蹲跳,使用较长的失重阈值(80ms);否则判定为连续跳,使用较短的阈值(40ms)。这个分类器准确率大概 85%,对于业余训练足够用了。
5.3 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 串口无数据 | 接线错误 | 万用表测 VCC 和 GND | 检查 I2C 地址,MPU6050 默认 0x68 |
| 数据全为零 | 传感器未初始化 | 读 WHO_AM_I 寄存器 | 检查复位时序,加 100ms 延时 |
| 跳跃高度偏大 | 落地检测延迟 | 打印原始波形 | 提高落地阈值,加锁定时间 |
| 跳跃高度偏小 | 起跳检测延迟 | 检查失重阈值 | 降低阈值到 0.4g,缩短确认时间 |
| 走路误触发 | 失重时间太短 | 记录走路时波形 | 失重阈值时间加到 60ms 以上 |
| 数据跳动大 | 电源纹波 | 示波器看 3.3V 轨 | 加 100nF 和 10uF 电容 |
| 姿态角漂移 | 零偏未校准 | 静止时看角度变化 | 每次 IDLE 时重新校准 |
| 蓝牙断连 | 功耗太高 | 测电流 | 降低发射功率,增加连接间隔 |
注意:MPU6050 的 I2C 地址由 AD0 引脚决定,接 GND 时是 0x68,接 VCC 时是 0x69。如果你买了多个模块,可以通过这个引脚区分。
5.4 标定与验证方法
做任何传感器项目,标定都是绕不开的。我的标定方法很土但很有效:找一个已知高度的台阶,比如 20 厘米,站在上面往下跳,记录传感器算出的高度,和实际高度对比。重复 20 次,算平均误差和标准差。如果平均误差小于 2 厘米,标准差小于 1 厘米,说明标定合格。
我还用高速摄像机(手机慢动作 240fps)拍了一段跳跃视频,逐帧数出腾空帧数,换算成时间,和传感器数据对比。结果发现传感器算出的腾空时间比视频少了大约 30ms,原因是起跳瞬间脚离地时传感器还没检测到失重。这个系统误差可以通过在起跳检测里减去 15ms 来补偿,但我觉得没必要,因为相对变化比绝对精度更重要。
6. 数据可视化与扩展玩法
6.1 用手机实时看波形
ESP32-C3 自带蓝牙,我写了一个简单的 BLE 服务,把加速度数据打包成 20 字节的特征值,手机端用 nRF Connect 就能看波形。如果你想做得更精致,可以用 MIT App Inventor 拖一个界面,显示实时曲线和跳跃次数。我试过用 Web Bluetooth API 在浏览器里画图,延迟大概 100ms,对于观察波形足够了。
6.2 数据记录与训练分析
每次跳跃后,我把腾空时间、跳跃高度、落地冲击力三个数据存到 Flash 里,最多存 1000 条。训练结束后通过串口导出 CSV,丢进 Excel 或者 Python 里做趋势分析。我帮朋友做的那个体能训练项目,就是每周测一次纵跳,看爆发力的变化曲线。如果连续两周下降超过 10%,就提示可能过度训练,需要调整强度。
6.3 从单传感器到多传感器融合
如果你想让精度再上一个台阶,可以加一个BMP280 气压计。气压计对高度变化敏感,虽然绝对精度差(±1 米),但相对精度高(±0.1 米)。把加速度积分得到的高度和气压计高度做卡尔曼滤波融合,能有效抑制积分漂移。我试过这个方案,腾空时间 0.5 秒的跳跃,高度误差从 3 厘米降到了 1.5 厘米。代价是代码复杂度翻倍,看你愿不愿意折腾。
另一个扩展方向是多节点同步。比如在篮球场上放四个传感器节点,通过 Wi-Fi 时间同步,可以算出运动员的起跳位置和移动轨迹。这个玩法就大了,适合做团队项目。
7. 我踩过的坑与最后分享几个实用技巧
第一个坑是电池供电时的噪声。用 USB 供电时数据很干净,一切换到锂电池,加速度计读数就多了 0.05g 的周期性波动。后来发现是 TP4056 的充电电路在电池供电时仍有微弱振荡,加一个 LC 滤波器(10uH 电感 + 100uF 电容)就解决了。如果你也遇到类似问题,先查电源,再查信号。
第二个坑是I2C 总线死锁。ESP32-C3 的 I2C 外设在某些情况下会卡死,表现为 SCL 被拉低。解决办法是在初始化时加一个总线恢复函数:把 SCL 配置成 GPIO 输出,手动发送 9 个时钟脉冲,然后重新初始化 I2C。这个函数我放在setup()里,每次上电都执行一遍,再也没死锁过。
第三个坑是固件升级后参数丢失。我习惯把阈值参数存在 NVS(非易失存储)里,但 ESP32-C3 的 NVS 在固件升级后有时会保留旧值,导致新代码用了旧参数。后来我在setup()里加了一个版本号检查,如果版本号不匹配就恢复默认值。这个技巧在开发阶段特别有用,省得每次手动擦除 Flash。
最后分享一个小技巧:用 LED 闪烁来指示状态。IDLE 时 LED 慢闪(1Hz),检测到跳跃时快闪(10Hz),落地后常亮 200ms。这样你不用看串口就知道传感器在工作,调试效率翻倍。我用的是板载的 WS2812,颜色还能区分不同状态,绿色空闲、蓝色腾空、红色落地,一目了然。
这个项目我从开始到稳定运行大概花了三个周末,其中一半时间在调算法,一半时间在焊板子。如果你也想做一个,我的建议是先用面包板把电路搭起来,确认数据能读、波形能看,再动手焊洞洞板。算法部分不要一上来就追求完美,先把最简单的阈值检测跑通,再慢慢加滤波和状态机。传感器项目就是这样,数据对了,一切都对了。