1. 项目概述从传统测量到智能穿戴的转变几年前我在反复徒步一条山间小径时脑子里总盘旋着一个问题这段路的实际坡度到底是多少怎么测才最准手机上的测斜仪App精度不错把手机侧立在路面上就能读数在特别陡峭的路段玩一下还挺有趣。但地图软件给出的坡度数据是用总爬升高度除以行进距离算出来的平均值它抹平了沿途所有细微的起伏变化。人走在路上总会感觉脚下的坡比实际测出来的要陡。这种感知与现实的偏差激发了我动手做一个更贴合实际、能连续记录每一步路面倾角的设备。倾斜仪这东西历史悠久从古老的水准泡机械式发展到如今能轻松与微控制器结合的电子传感器。基础款的传感器很便宜但想要从原始数据里解算出有意义的姿态角得费不少功夫做数学运算。这次我选择了一款“自带大脑”的传感器——BNO055。它内部集成了传感器融合算法能把加速度计、陀螺仪、磁力计的数据处理好直接输出稳定的欧拉角俯仰、横滚、偏航或四元数大大减轻了主控芯片的运算负担。这让整个设备可以做得非常小巧甚至能绑在鞋子上成为一个可穿戴的测量平台。鞋子为什么是个理想的测量平台因为它为你行走的坡面提供了一个明确的、垂直于坡面的参考长度。想象一下自行车是两个轮子的接触点提供基线汽车则更长。但无论哪种方式测量的前提都是找到一个可靠的“零位基准点”。我做的这个设备非常轻巧可充电并通过蓝牙与iPhone上的一个App通信实时记录倾斜角度和GPS位置。数据既可以实时查看也能导出为CSV文件扔进Google Earth做三维可视化或者做进一步分析。收集这些数据可能看起来有点“数据控”但对于步态分析、越野路线设计哪怕微小的坡度变化都会影响行走或骑行的难易度来说自动化的坡度数据采集或许能提供一些有价值的参考。2. 核心硬件选型与设计思路这个项目的硬件核心非常精简一个传感器、一块电池、一个微控制器再加上必要的开关和按钮。但每个元件的选择都直接关系到最终设备的精度、续航、体积和可靠性。2.1 传感器为什么是BNO055市面上姿态传感器很多从简单的MPU6050六轴到更复杂的十轴模块。我最终选择了Adafruit的BNO055 9-DOF绝对方向IMU融合 breakout板。它的价格约34美元比基础传感器高但绝对物有所值。核心优势在于其内置的Fusion Hub融合处理器。普通的IMU如MPU6050只能给你原始的加速度和角速度数据。要得到稳定的姿态角你必须在主控芯片上实现互补滤波或卡尔曼滤波算法。这不仅占用宝贵的CPU资源和时间算法调参本身也是个技术活容易受到振动、磁干扰的影响而产生漂移或抖动。BNO055则不同。它内部有一颗独立的ARM Cortex-M0微处理器专门运行Bosch Sensortec的传感器融合算法。你只需要通过I2C读取它计算好的欧拉角或四元数即可。这意味着高精度与稳定性算法在芯片层面优化直接输出低噪声、已校准的姿态数据极大简化了上层应用。降低主控负担ESP32-C3无需进行复杂的滤波运算可以更专注于数据通信、逻辑控制和功耗管理。内置校准传感器支持完整的系统校准加速度计、陀螺仪、磁力计并通过状态寄存器反馈校准状态确保数据可靠。对于这个倾斜仪项目我们主要关心俯仰角Pitch即设备前后倾斜的角度。BNO055直接提供这个值省去了大量底层数据处理工作。注意BNO055有几种工作模式。在这个项目中我们应将其设置为“NDOF_FMC_OFF”模式。这是九自由度融合模式但关闭了快速磁力计校准。因为在鞋子上磁力计极易受到地磁场畸变如鞋钉、周围金属干扰反而引入误差。我们主要依赖加速度计和陀螺仪来感知俯仰角关闭磁力计融合能避免不必要的跳变。2.2 微控制器ESP32-C3的性价比之选主控我选择了Seeed Studio的XIAO ESP32-C3。这个小板子约5美元堪称宝藏RISC-V内核低功耗相比经典的ESP32C3系列功耗控制更优秀对于电池供电设备至关重要。集成蓝牙5.0与Wi-Fi本项目主要使用蓝牙与iPhone通信其蓝牙堆栈稳定连接距离也足够。板载充电管理直接支持单节锂聚合物电池充电省去了外接充电芯片的麻烦。极致小巧尺寸大约只有拇指指甲盖大小为设备小型化提供了可能。价格低廉在拥有上述功能的前提下价格非常有竞争力。它的GPIO数量对于本项目也绰绰有余连接BNO055的I2CSDA, SCL、连接零位按钮的一个数字输入、以及为蓝牙天线预留的射频引脚。2.3 供电与结构设计供电部分选用了一块300mAh的锂聚合物电池。经过实测在每5秒通过蓝牙发送一次数据的工作周期下续航可以轻松超过4小时满足一次长距离徒步的需求。充电通过ESP32-C3板载的Type-C接口完成约1小时可充满。结构设计外壳通过3D打印PLA材料实现。设计上考虑了以下几点防水防尘主体采用上下盖结合USB充电口有独立的旋拧式密封盖。上下盖接合处可以涂抹少量硅脂或放置O型圈来增强密封性。虽然PLA本身不防水但合理的结构设计能应对一般的雨雪和泥水溅射。传感器固定BNO055模块通过M2螺丝牢固固定在下壳内确保其与鞋底的相对位置稳定避免因晃动产生测量基准变化。天线布置ESP32-C3的板载天线性能在封闭壳体内会衰减。我额外使用了一小片胶粘式PCB天线将其贴在内壳顶部远离金属螺丝和电池以优化蓝牙信号强度。人体工学整体造型流线型通过扎带固定在鞋面时不会剐蹭到另一只脚或植被。打印颜色建议与鞋色相近降低突兀感。3. 电路连接与组装实操要点整个电路的连接非常简单遵循“电源先行信号后走”的原则。下图是接线示意图的详细解读---------------------------- | ESP32-C3 (XIAO) | | | BAT ------ VBAT | BAT- ------ GND | | | | [On/Off Switch] | | (串联在BAT线上) | | | | D4 (GPIO4) ------ SDA | | D5 (GPIO5) ------ SCL | | 3V3 ------ VIN | | GND ------ GND | | | | D10(GPIO10)------[Button]--- GND | | ---------------------------- | | (I2C总线) v ------------------- | BNO055 Sensor | | (Adafruit) | -------------------接线步骤与要点电源主线将电池的正极BAT先接到防水拨动开关的一端开关的另一端接到ESP32-C3的VBAT引脚。电池负极BAT-直接接GND。务必确保开关断开状态下焊接防止短路。I2C连接这是数据通道。将BNO055的VIN接ESP32-C3的3V3输出GND接GND。SDA和SCL分别接ESP32-C3的D4(GPIO4)和D5(GPIO5)。I2C总线需要上拉电阻但幸运的是Adafruit的BNO055 breakout板和ESP32-C3的这两个IO口内部通常都有可启用或已配置的上拉电阻。为了稳定我建议在代码中显式启用内部上拉pinMode(4, INPUT_PULLUP); pinMode(5, INPUT_PULLUP);零位按钮使用一个常开型轻触开关。一端接ESP32-C3的D10(GPIO10)另一端接GND。在代码中将该引脚设置为INPUT_PULLUP模式。这样未按下时引脚读高电平按下时接地变为低电平。组装顺序 a.预埋热熔螺母在3D打印的下壳的四个固定孔内使用烙铁加热并嵌入M2规格的热熔螺母用于固定BNO055模块。 b.固定核心部件用M2*4mm螺丝将BNO055模块锁紧在下壳。将ESP32-C3板子用少量热熔胶固定在其卡槽内。热熔胶足以应对徒步的振动且需要维修时也易于清除。 c.焊接与理线按照示意图焊接所有导线。线长留出适当余量但不宜过长将多余线材 neatly 塞入外壳周围的走线槽内避免挤压传感器或天线区域。 d.固定开关与按钮将防水开关和零位按钮分别用超级胶水如401胶水粘牢在壳体预留孔位中。注意胶水不要渗入开关或按钮的活动部件。 e.粘贴天线撕掉胶粘式天线的背胶将其平整地贴在内壳顶部中央区域。 f.合盖与密封在上下壳接合面均匀涂抹一层薄薄的环氧树脂胶或专用的塑料粘合胶然后对齐压紧。用橡皮筋固定直至胶水固化。最后拧上USB密封盖。实操心得在合盖前务必进行一次完整的通电测试用USB线连接电脑上传一个简单的测试程序如读取BNO055 ID并打印角度确认传感器工作正常、蓝牙可被发现、按钮响应灵敏。一旦封胶再修改就麻烦了。4. 固件编程算法逻辑与蓝牙通信解析固件是设备的大脑负责读取传感器数据、执行倾角判断算法、管理蓝牙通信和响应按钮事件。我们使用Arduino IDE进行开发需要安装Adafruit BNO055库和ArduinoBLE库。4.1 程序框架与初始化#include Wire.h #include Adafruit_BNO055.h #include ArduinoBLE.h #include utility/imumaths.h Adafruit_BNO055 bno Adafruit_BNO055(55, 0x28); // I2C地址0x28 BLEService inclineService(180D); // 使用标准心率服务UUID进行“伪装” BLEFloatCharacteristic inclineChar(2A37, BLERead | BLENotify); // 心率测量特征用于发送浮点数 const int zeroButtonPin 10; const long sampleInterval 100; // 读取传感器间隔(ms) const long broadcastInterval 5000; // 蓝牙广播间隔(ms) float zeroOffset 0.0; float currentIncline 0.0; float smoothedIncline 0.0; unsigned long lastSampleTime 0; unsigned long lastBroadcastTime 0; bool footStable false; unsigned long stableStartTime 0; const float stableAccelThreshold 0.2; // 判定脚部静止的加速度阈值(m/s^2) const unsigned long stableTimeThreshold 100; // 静止持续时间阈值(ms) void setup() { Serial.begin(115200); pinMode(zeroButtonPin, INPUT_PULLUP); // 初始化BNO055设置为NDOF_FMC_OFF模式 if(!bno.begin()) { Serial.println(BNO055 init failed!); while(1); } bno.setMode(Adafruit_BNO055::OPERATION_MODE_NDOF_FMC_OFF); delay(1000); bno.setExtCrystalUse(true); // 使用外部晶振更精准 // 初始化蓝牙 if (!BLE.begin()) { Serial.println(BLE init failed!); while (1); } BLE.setLocalName(RovingInclinometer); BLE.setAdvertisedService(inclineService); inclineService.addCharacteristic(inclineChar); BLE.addService(inclineService); inclineChar.writeValue(0.0); // 初始值 BLE.advertise(); getZero(); // 开机后等待10秒设置初始零位 }关键点解析蓝牙服务伪装我们使用了标准的心率服务0x180D和心率测量特征0x2A37。这是因为像Strava这类运动App普遍支持从蓝牙心率带读取数据。我们将倾角数据“伪装”成心率值一个浮点数进行广播这样就能被更多通用运动App记录增加了数据应用的灵活性。传感器模式OPERATION_MODE_NDOF_FMC_OFF是关键它启用了九轴融合但关闭了快速磁力计校准更适合动态且磁环境复杂的场景。零位校准getZero()函数在启动时被调用为测量建立基准。4.2 核心算法如何判断“有效”的倾角这是本项目的核心逻辑。我们不是简单地持续读取俯仰角因为走路时鞋子一直在动。我们需要捕捉脚掌完全落地、承重静止的那一瞬间的倾角这才是路面的真实坡度。void loop() { unsigned long currentMillis millis(); // 1. 定期读取传感器数据 if (currentMillis - lastSampleTime sampleInterval) { lastSampleTime currentMillis; // 获取线性加速度已去除重力影响 imu::Vector3 linearAccel bno.getVector(Adafruit_BNO055::VECTOR_LINEARACCEL); // 获取俯仰角Pitch imu::Vector3 euler bno.getVector(Adafruit_BNO055::VECTOR_EULER); float pitchAngle euler.y(); // BNO055的y轴对应Pitch float accelZ linearAccel.z(); // Z轴线性加速度垂直于传感器平面 // 2. 状态机判断脚部是否处于稳定状态 if (abs(accelZ) stableAccelThreshold) { // 加速度小于阈值可能处于稳定期 if (!footStable) { // 刚刚进入稳定状态记录开始时间 stableStartTime currentMillis; footStable true; } else { // 持续稳定中 if (currentMillis - stableStartTime stableTimeThreshold) { // 稳定时间超过阈值认为脚已踩实记录此刻的角度 currentIncline pitchAngle - zeroOffset; // 可选在此处加入简单的卡尔曼滤波或移动平均进一步平滑单次读数 } } } else { // 加速度超过阈值脚在移动重置稳定状态 footStable false; } // 3. 对最终输出的倾角进行移动平均滤波针对多次稳定读数 smoothedIncline smooth(currentIncline); } // 4. 定期通过蓝牙广播数据 if (currentMillis - lastBroadcastTime broadcastInterval) { lastBroadcastTime currentMillis; inclineChar.writeValue(smoothedIncline); // 广播平滑后的倾角 } // 5. 检查零位按钮 if (digitalRead(zeroButtonPin) LOW) { delay(50); // 简单消抖 if (digitalRead(zeroButtonPin) LOW) { getZero(); } } // 处理蓝牙连接事件 BLE.poll(); } float smooth(float input) { // 一个简单的移动平均滤波器 static float filterBuffer[5] {0}; static byte index 0; static float sum 0; sum - filterBuffer[index]; filterBuffer[index] input; sum filterBuffer[index]; index (index 1) % 5; return sum / 5.0; } void getZero() { // 零位校准函数 Serial.println(Zeroing... Keep device stable for 10s.); delay(10000); // 等待10秒让用户将设备放在基准平面上 imu::Vector3 euler bno.getVector(Adafruit_BNO055::VECTOR_EULER); zeroOffset euler.y(); // 将当前俯仰角设为基准零位 Serial.print(Zero offset set to: ); Serial.println(zeroOffset); }算法逻辑拆解获取数据读取去除重力后的线性加速度和原始的俯仰角。稳定状态检测这是一个简单的状态机。我们关注Z轴垂直于鞋底平面的线性加速度。当脚在空中摆动或触地瞬间加速度变化剧烈。当脚掌完全落地、承重静止时Z轴加速度理论上应接近0忽略微小的身体晃动。我们设定一个阈值如0.2 m/s²和一段稳定时间如100ms。只有当加速度低于阈值并保持超过100ms时才判定为“有效稳定状态”。角度捕获在判定为有效稳定状态的时刻记录此时的俯仰角并减去之前校准的zeroOffset得到相对于水平基准面的倾角。数据平滑单次读数可能有噪声。我们对连续几次捕获到的有效倾角进行移动平均滤波得到更平滑的输出值smoothedIncline。定时广播每5秒将平滑后的倾角值通过蓝牙特征值广播出去。注意事项stableAccelThreshold和stableTimeThreshold是两个关键参数。阈值设得太小或时间设得太长可能会漏掉一些快速的脚步设得太大或太短则容易在脚部未完全稳时误触发。需要根据实际步行节奏进行微调。对于慢走当前参数比较合适对于跑步则需要调整。5. 移动端数据采集与可视化实践设备端准备好后我们需要一个“记录仪”。我选择了iPhone上的BLExAR这款App。它是一款强大的通用蓝牙数据调试器可以扫描、连接蓝牙设备并图形化显示特征值数据还支持将数据记录为CSV文件。5.1 使用BLExAR进行数据记录设备连接打开BLExAR扫描蓝牙设备。你应该能看到名为“RovingInclinometer”的设备。点击连接。服务与特征值连接后App会列出设备的所有服务。找到“Heart Rate”0x180D服务点击进入。你会看到“Heart Rate Measurement”0x2A37特征值。启用通知与绘图点击这个特征值旁边的“Notify”或“Subscribe”按钮。一旦启用设备端每5秒发送的数据就会被App接收。你可以点击“Graph”标签页实时看到倾角数据随时间变化的曲线。开始记录点击屏幕上的“Record”或“Log Data”按钮不同版本可能名称不同App就会开始将接收到的时间戳和数值记录到内存中。导出数据记录结束后通常可以在App的“Logs”或“Data”页面找到保存的记录并支持通过邮件将CSV文件发送给自己。CSV文件格式通常如下Timestamp, Value 2023-10-27 14:05:23.450, 2.5 2023-10-27 14:05:28.452, 3.1 2023-10-27 14:05:33.455, 1.8 ...Value列就是我们广播的倾角度数。5.2 进阶玩法与运动生态整合BLExAR很好用但如果你想将坡度数据直接整合进像Strava这样的专业运动记录App呢这就是前面“伪装”成心率服务的妙用。原理Strava可以从连接的蓝牙心率传感器读取心率数据。我们的设备广播的“心率”特征值实际上是我们处理后的倾角数据单位是度但被编码成一个浮点数。在Strava中连接在Strava开始记录一项户外步行或跑步活动前进入传感器设置搜索蓝牙传感器。它应该能发现我们的设备并识别为“心率监测器”。配对连接。记录活动开始记录活动并行走。Strava会像记录心率一样每秒或根据其采样率读取一个“心率”值。这个值就是你的实时路面倾角。数据后处理活动结束后Strava会显示一个心率曲线图——实际上那是你的坡度曲线你可以通过Strava的Web端导出原始数据GPX或TCX格式其中就包含了这个“心率”数据流。用文本编辑器或脚本将其提取出来并做好单位转换可能需要乘以一个系数因为Strava可能期望心率是整数范围即可得到同步了位置和时间的坡度数据。实操心得使用这种“伪装”方法时需要注意数据范围。Strava可能将异常高的“心率”值视为错误而过滤掉。建议将倾角数据映射到一个合理的“心率”范围例如坡度-20°到20°映射到60-180 BPM。这需要在设备固件中做一次线性变换。5.3 在Google Earth中进行三维可视化这是最直观的数据呈现方式之一。准备数据你需要两份数据一是从BLExAR导出的带时间戳的倾角CSV文件二是从iPhone健康App或其他GPS记录软件甚至可以是Strava导出的GPX文件导出的、时间同步的轨迹文件GPX格式。数据融合使用Python配合pandas库或Excel根据时间戳将倾角数据与GPS轨迹中的每个航点进行匹配、合并。生成KML文件KML是Google Earth使用的标记语言。你可以编写一个简单的脚本读取融合后的数据为每个航点创建一个Placemark并根据其倾角值来设置图标的颜色或标签。例如用绿色表示下坡负角度红色表示上坡正角度颜色深浅代表坡度大小。加载与浏览在Google Earth Pro中打开生成的KML文件。你的徒步路线就会以彩色线条的形式叠加在三维地形上。点击路线上的点还能弹出气泡显示该点的精确坡度、海拔和时间。这种可视化不仅能让你回顾整条路线的坡度分布还能非常清晰地看到在等高线密集处陡坡坡度值的变化对于路线规划和分析极具价值。6. 测试、校准与常见问题排查任何测量设备验证其精度和可靠性都是必不可少的环节。6.1 静态精度测试我使用3D打印了几个已知角度的斜面测试块例如15°30°45°。将设备牢固地放置在测试块表面通过串口监视器或BLExAR读取其输出的倾角值。在多次测试中BNO055的表现非常出色静态测量误差基本在±1°以内这完全满足徒步坡度分析的需求。校准的重要性BNO055虽然出厂已校准但在首次使用或经历剧烈温度变化、冲击后进行完整的系统校准能获得最佳性能。校准方法通常包括将设备在多个静止姿态六个面各放置几秒以及缓慢地绕三个轴各旋转数圈。Adafruit的库提供了检查校准状态的函数bno.getCalibration()它会返回系统、陀螺仪、加速度计、磁力计的校准状态0-3。对于我们的模式应确保系统校准状态sys_cal达到3完全校准。6.2 动态行走测试与误差分析我在自家一条长约1/3英里的车道上反复行走测试。数据通过BLExAR记录并导出分析。数据波动实测数据显示在平坦路面上读数存在约±1°的微小波动。这主要来源于几个方面算法延迟脚部从触地到被算法判定为“稳定”有微小延迟此时身体重心可能已有细微变化。落地姿态差异每一步鞋底与地面的接触角度并非绝对一致。传感器噪声尽管经过滤波传感器本身和电路仍有本底噪声。身体晃动行走时上半身的自然摆动会通过脚踝传递微小的力矩。与参考值对比同时使用iPhone的水平仪App在几个特征点进行手动测量对比。设备读数与手动测量值的差异也在±1°范围内验证了其动态测量的有效性。6.3 常见问题与解决方案速查表在实际制作和使用中你可能会遇到以下问题问题现象可能原因排查与解决步骤蓝牙无法连接或搜索不到1. 设备未通电或开关未开。2. 蓝牙天线接触不良或屏蔽严重。3. 固件中蓝牙未成功初始化或设备名设置错误。4. 手机蓝牙未开启或距离过远。1. 检查电池电量测量VBAT引脚电压。2. 检查天线是否粘贴牢固是否被金属部件遮挡。可尝试暂时移除外壳测试。3. 通过串口监视器查看启动日志确认BLE.begin()成功并打印本地设备名。4. 将手机靠近设备1米内重试。倾角读数始终为0或固定值1. BNO055初始化失败或通信中断。2. I2C引脚接错或上拉电阻未启用。3. 算法中的稳定状态判定条件过于苛刻从未触发。1. 检查串口输出确认bno.begin()返回true。尝试读取传感器温度或芯片ID进行基础通信测试。2. 用万用表检查SDA/SCL线电压应在3.3V左右按下拉时应能拉低。确认代码中启用了内部上拉。3. 调大stableAccelThreshold如改为0.5或减小stableTimeThreshold如改为50ms并通过串口打印accelZ和footStable状态来调试。读数漂移严重随时间变化1. 传感器未校准或校准状态差。2. 磁力计干扰如果在相关模式下。3. 设备温度变化大影响传感器零偏。1. 运行完整的传感器校准程序并通过bno.getCalibration()确认所有状态为3。2. 确保使用NDOF_FMC_OFF模式避免磁力计影响。3. 让设备在工作温度下预热几分钟后再进行零位校准。按钮校准无反应1. 按钮接线错误或虚焊。2. GPIO引脚模式设置错误应为INPUT_PULLUP。3. 消抖逻辑过于简单或延时太长错过了检测。1. 用万用表通断档检查按钮按下时对应GPIO是否与GND连通。2. 检查代码中pinMode(zeroButtonPin, INPUT_PULLUP)。3. 简化按钮检测逻辑或增加串口打印确认按键事件能被触发。电池续航远低于4小时1. 蓝牙广播间隔太短。2. 传感器采样率过高。3. 存在异常大电流消耗如短路、LED未断开。4. 电池老化或容量虚标。1. 尝试将broadcastInterval增加到10秒或更长。2. 将sampleInterval增加到200ms。3. 断开所有外设仅连接核心部件测量静态工作电流应低于10mA。检查开关LED是否被误连接点亮。4. 更换一块已知容量的新电池测试。6.4 提升测量精度的技巧预热与校准在开始正式测量前将设备通电并执行几个大幅度的俯仰和横滚动作持续约1-2分钟帮助传感器内部的算法收敛。然后将其放置在已知水平面如室内的地板、桌面按下零位按钮进行校准。固定方式使用扎带将设备紧密地固定在鞋面上确保其与鞋底平行且在整个行走过程中不会发生相对滑动或旋转。可以考虑设计一个带有防滑垫的底座。步行节奏算法依赖于检测脚部静止的瞬间。保持平稳、节奏一致的步态能获得更连续、更可靠的数据。快跑或跳跃式的步伐会导致算法难以捕捉稳定状态。数据后处理原始数据不可避免会有噪声和野值。在电脑端进行后处理时可以应用更复杂的滤波算法如中值滤波、低通滤波来平滑曲线或根据GPS速度信息剔除明显异常的数据点如速度很快时却捕捉到“稳定”倾角。这个便携式倾斜仪项目从构思到实现打通了传感器数据采集、嵌入式处理、无线通信和移动端应用的全链路。它不仅仅是一个测量工具更是一个如何将现代廉价的电子模块组合起来解决一个具体、有趣问题的范例。你可以基于此框架进行扩展比如增加气压计测量海拔变化进行交叉验证或者将数据通过Wi-Fi直接上传到云端服务器。希望这个详细的分享能给你带来启发和动手的乐趣。