先把结论放前面:用一块指甲盖大小的XIAO nRF54LM20A Sense,配合板载的六轴惯性传感器,加上一个经过反复调参检测的步数算法,再用一条低功耗蓝牙链路把数据同步到手机,我做出来了一只能戴在手腕上、连续跑好几天不乱报数的计步器。这篇文章是这整个项目从选型、搭建到踩坑、调优的完整记录,写给那些想低成本做可穿戴设备、又不想被智能手表生态绑住手脚的朋友。
我估摸着不少人和我有同款冲动:看了手环上那个步数数据,总觉得“这东西我也能做一个”。真下手才发现,一个靠谱的计步器远不是“读加速度计然后数峰值”这么简单,尤其当它要从桌面上一个裸板变成“戴在手腕上晃一整天”的真实穿戴设备,算法、功耗、结构、调试都要重新审视。XIAO nRF54LM20A Sense正好卡在“够小、够省电、够灵敏”这个点上,让整个项目变得异常顺手。
1. 为什么这块“指甲盖板”会是腕戴计步器的合理答案
1.1 项目需求先拆清楚:计步器到底要看哪几项指标
很多朋友做一个硬件项目时习惯先开开发板选型,我则习惯先把需求拆成可以量化的指标,再回头挑硬件。腕戴式计步器乍一听很简单,但把它拆开之后,至少有以下四条硬指标:
- 体积与重量:设备必须能固定在手腕上,体积不能比一元硬币大太多,否则佩戴感会很差,戴一会儿就想摘掉。
- 功耗:用户不可能天天充电。计步器要能做到“数完步数放口袋/抽屉里,下个月还能用”,而不是“早晨充满电,下午就剩一半”。
- 数据准确度:步数存在±10%误差通常可以接受,但不能“坐个公交车滴滴滴给你记三千步”,这是体验崩塌的起点。
- 数据可读性:裸板可以不依赖手机,但我个人更愿意通过BLE把数据同步到手机App,能查看历史曲线、导出数据,这也方便调试。
把这些指标放在面前,再回头选板子的时候,很多常见选择就不成立了。ESP32系列虽然计算能力强、生态庞大,但它的电流底子太厚——Wi-Fi开启状态下轻松到几百毫安,就算只用BLE,也很难把手持设备做到“月抛”续航。而传统的低功耗单片机确实功耗低,但要么集成度低,要么传感器还得外挂,自己折腾PCB布局时间成本直线上升。
1.2 一颗SoC解决两种核心芯片:MCU、射频、甚至传感器
XIAO nRF54LM20A Sense的核心SoC来自Nordic的nRF54L20系列。与传统组合“MCU+BH蓝牙芯片”或“MCU+独立传感器”相比,它在一块非常小的模组上把以下东西全部集成在了一起:
- 一颗带浮点单元的低功耗处理器,足够跑常规的DSP运算,比如滤波器、峰值检测;
- 收发性能不错的低功耗蓝牙射频,满足手机等设备的数据同步;
- 板载IMU(惯性测量单元),意味着传感器这部分不用再外挂,省掉大量连线;
- 板载天线和必要的匹配电路,不用手动设计射频前端;
- 丰富可用的GPIO/ADC/I2C/SPI/UART接口,后续扩展屏幕、按钮等外设很从容。
Sense版本的命名区别就在这里:它在普通版本基础上加上了惯性传感器,正好命中可穿戴项目的核心需求。我从打开包装到点亮IMU,全程没有碰过烙铁,没有飞线,没有翻Datasheet查引脚定义,这种体验对初期原型验证实在太重要了。
1.3 和ESP32系列、nRF52840的对比:选型不能只看算力
我把市面上几类常见平台对比过,放到一起看会更直观:
| 比对项 | XIAO nRF54LM20A Sense | XIAO nRF52840 (Sense) | ESP32系列开发板 |
|---|---|---|---|
| 板载IMU | 有,开箱即用 | 部分版本有 | 通常没有,需外挂 |
| BLE支持 | 原生支持 | 原生支持 | 支持,但功耗偏高 |
| 工作电流(典型BLE场景) | 低,适合穿戴类 | 较低 | 相对偏高 |
| 算力水平 | 足以跑滤波/计步 | 足以跑基础计步 | 更高,适合复杂算法或Wi-Fi场景 |
| 模块体积 | 非常小 | 小 | 通常偏大 |
| 项目类型匹配 | 电池供电的穿戴/传感器节点 | 穿戴/传感器节点 | 需要Wi-Fi或更高算力的原型 |
实际测试下来,算力并不是计步器的瓶颈,功耗也不是可以拍脑袋决定的。最关键的其实是集成度——传感器已经在板上了,在项目初期能省掉至少两三天走线、调试的时间。比起“算力强”,我更在意“从零起步到第一个数据点”的时间成本,这块板子在这点上优势非常明显。
2. 计步算法的“物理课”:读懂加速度波形才能写好判断逻辑
2.1 走路在加速度传感器上是什么样子
先把绕不开的基础讲清楚。三轴加速度计输出的数据是三轴的重力与加速度分量组合。当你手拿着手机或手表自然走路时,垂直方向(通常是其中一个轴)的加速度会呈现非常有节奏的起伏:脚落地时出现明显冲击峰,迈步腾空时出现一个相对的波谷。这种波形像人的心率信号一样,有周期性,频率基本落在1到3赫兹之间,也就是每分钟约60到180步。
所以计步算法的本质,不是“聪明地数数”,而是从一段加速度时间序列中识别出周期性信号,并把它和“突发动作”区分开。这个区分度决定了算法能不能扛住干扰。
2.2 从峰值检测到步频滤波:一套稳定的判断链路
市面常见的计步算法可以归纳为下面这条链路,我照着实现过,亲测稳定:
- 预处理:对三轴加速度取模,得到一个与姿态关系较小的总加速度值。然后在时间轴上做滑动平均或低通滤波,去掉高频抖动。
- 峰值检测:在滤波后的波形中找到局部极大值。判断一个峰值不是“本轮最大的数”,而是它比左右邻域都大,且高于当前设定的阈值。
- 阈值自适应:固定阈值在早期原型里能用,但用户走路快慢不同、步伐轻重不同,固定阈值一换人就得重新调。好一点的实现是用最近若干个峰的幅度和间隔动态更新阈值。
- 步频约束:相邻两个峰之间的时间间隔,要落在合理步频区间内(比如300ms到1000ms)。太短的间隔通常不是走路,而是抖动;太长的间隔说明可能只是偶然的摆动。
- 恢复期:检测到一次有效步后,进入短时间锁定期,避免同一个波形被重复计数。
这五步听着简单,真正实现起来最容易翻车的地方在第二步:峰值检测算法的“领域大小”和“锁定期长度”是天然矛盾体。窗口太小,晃动干扰容易产生大量伪峰;窗口太大,快走时两个真实步峰可能被合并成一步。我实际调参下来,采样周期设为20ms一档,滑动窗口开到50个点左右,初始效果比较平衡。
2.3 手腕佩戴为什么比手拿手机更容易误计
同样是基于加速度计,手机放口袋和手表戴手腕,算法难度完全不同。手机在口袋里基本不动,只有走路时才产生明显加速度振动,信号干净。而手腕上,哪怕你不走路,只是在打字、挥手、提东西,加速度计都会有大幅度输出。
我实测过一套裸算法,直接把手机计步思路搬到手表上,结果我做杯咖啡的时间,设备理直气壮给我记了四十多步。原因很简单:手腕运动自由度大,动作频谱和走路高度重叠。
所以腕戴计步器的算法,不能只看“有没有周期性”。我会在代码里额外加两个判断:
- 姿态稳定度判断:走路时,肢体运动虽然强烈,但其水平面的姿态变化通常比随意甩手更规律。通过对一段窗口内的姿态角方差做评估,把“高随机摆动”场景降权。
- 低频周期一致性:走路的峰值间隔相对稳定,而甩手、敲击的间隔随机性很高。可以用间隔的变异系数做一个阈值过滤。
这一层属于经验向优化,不是教科书标准答案,但在我实测中能把误计率压到一个相当可接受的范围。
2.4 我最终采用的滑动窗口+自适应阈值实现(含代码)
我把测试过的框架简化成下面这个可直接跑的Arduino风格示例。它读取板载IMU的原始三轴数据,计算合加速度,做滑动滤波和峰值检测,符合条件就输出一次步数。
#include <Arduino.h> // 伪代码,根据你的实际IMU驱动调整读取接口 float ax, ay, az; float filtered = 0.0f; float lastValue = 0.0f; float threshold = 1.2f; // 动态阈值初始值 unsigned long lastStepTime = 0; const unsigned long minInterval = 300; // 最快步频限制 / ms const unsigned long maxInterval = 1000; // 最慢步频限制 / ms const int sampleWindow = 30; // 滑动窗口点数 float history[30]; int historyIndex = 0; void setup() { Serial.begin(115200); // 初始化IMU并读取初始数据 } void loop() { // readIMU(ax, ay, az); float accMagnitude = sqrt(ax * ax + ay * ay + az * az); // 简单一阶低通滤波 filtered = 0.8f * filtered + 0.2f * accMagnitude; // 更新滑动窗口并求局部均值/方差(示意) history[historyIndex % sampleWindow] = filtered; historyIndex++; // 峰值检测:当前值大于前后邻居,且超过当前阈值 if (filtered > lastValue && filtered > threshold) { unsigned long now = millis(); unsigned long interval = now - lastStepTime; if (interval > minInterval && interval < maxInterval) { stepCounter++; lastStepTime = now; Serial.print("step: "); Serial.println(stepCounter); } // 动态阈值:跟随最近一个峰值的幅度缓慢调整 threshold = 0.6f * threshold + 0.4f * filtered; } lastValue = filtered; delay(20); }这个版本非常“原型”,但结构上已经包含滤波、峰值、锁定时长、动态阈值四个核心元素。真实产品级算法会把姿态、方差判断加进来,而这个原型足够在串口监视器上看到正确反应,也足够让你对比不同场景下的误差。
3. 从烧录到串口出数:环境搭建与第一个可用版本
3.1 认识一下XIAO nRF54LM20A Sense的板载资源
拿到板子第一件事就是对着PCB端详一番。外形和多数XIAO系列差不多,左右两排邮票孔,正面一颗主芯片,一面是传感器区域。这个尺寸做成腕戴模块几乎不需要考虑“怎么塞进表壳”的问题,我后来用的3D打印腕带几乎是贴合的。
IDE之前,建议先读一下官方Wiki确认板载引脚映射。以XIAO系列习惯来说,I2C接口通常映射到固定引脚,SPI/UART也有对应定义。这块Sense板因为多了传感器,会有几个电源域或控制引脚需要注意,看Wiki清单比对着丝印硬猜可靠得多。
3.2 用Zephyr还是Arduino?我建议的入场方式
这块板的官方支持主要分两条路:Zephyr和Arduino。我个人的建议分情况:
- 只想快速验证方案、追求几天内跑通:选Arduino。示例丰富、生态简单,串口监视器就能完成绝大多数调试。我的第一版计步器原型就是用Arduino方式完成的。
- 目标要成为长期产品,要有复杂任务调度、深入功耗管理:直接进Zephyr。nRF系列在Zephyr下的BLE协议栈非常成熟,还能用设备树配置引脚和传感器,后期扩展蓝牙Service更专业。
我先用Arduino跑通,再在Zephyr下复刻了一遍。两者在传感器读取上差别不大,但BLE服务定义和电源管理部分,Zephyr的灵活度明显更高。如果你并不急于产品化,Arduino足够成为学习路径的起点。
3.3 三步跑通IMU数据读取并打印
这个阶段目标不是计步,而是确认传感器数据流正常。按三步走:
- 安装开发板包:在Arduino开发环境里添加Seeed开发板库并选择对应的nRF54L20型号。完成后先烧录一个最常规的LED闪烁示例,确认烧录链路线、串口识别都正常。
- 运行IMU示例:官方提供的IMU读取示例一般会循环打印三轴或六轴原始数据。打开串口监视器,晃一晃板子,确认数值随姿态变化而变化。
- 写一个小的波形查看辅助工具:如果条件允许,用一个简单的Python脚本读串口数据并画成实时曲线,能直观看到加速度波形,这对后面调算法太有帮助了。
没有波形可视化去调计步阈值,基本等价于闭着眼睛调色,我强烈建议至少用Arduino串口绘图器或Python matplotlib画个实时图。
3.4 把算法集成到主循环:实时步数输出
数据读取打通后,把上一章的算法代码粘进去,主循环里就会开始输出步数。我测试时做了一个很傻但是很有效的验证:把板子绑在手腕上,走了一段固定距离,同时用手机计步器对照。
第一轮测试结果通常“惨不忍睹”,但这非常正常。不要立刻去怀疑硬件,先把阈值、滤波系数、锁定期这三个变量分别拉开看影响。我的调试习惯是一次只动一个参数。比如固定阈值从1.1调到1.5,看误计是变好了还是变差了;再独立调锁定期长短。这样做虽然慢,却能让你真正理解算法里每个参数的物理意义。
4. 从“能数步数”到“能戴一天”:BLE上报与电源侧的真正挑战
4.1 把步数通过BLE发到手机:Service/Characteristic的常规组织
计步器作为一个传感器节点,最自然的BLE组织方式是用一个自定义Service承载步数数据。通常我会建:
- 一个步数计数Characteristic,从设备主动Notify到手机;
- 一个电池电量Characteristic,供手机端读取剩余电量;
- 可选一个控制Characteristic,用手机调整灵敏度等参数。
在Arduino或Zephyr里,BLE服务注册的代码框架都比较固定。跑通之后,手机端用nRF Connect或厂商提供的App直接扫描、连接、订阅服务,就能看到实时步数更新了。这里有个容易被忽略的体验点:不要让握把连接状态频繁变化。很多低功耗设备为了省电会把广播间隔拉得很长,导致手机端连接后响应速度慢。我的做法是区分“长期连接”和“仅同步时连接”两种场景,日常只做定时广播,用户打开App时才建立连接。
4.2 功耗从哪里来,就给哪里动刀:实测电流构成
计步器的功耗大头通常不在MCU运算,而在传感器常开采样、BLE广播和对外的GPIO漏电流。我把这几点逐项开关,用电流表测了不同状态下的静态电流,得到基本结论:
| 场景 | 测得电流量级 | 说明 |
|---|---|---|
| 全速运行、传感器高频采样、BLE持续连接 | mA级 | 原型阶段默认状态 |
| 传感器1Hz低采样、系统空转 | µA到低mA级之间 | 适合长时间待机 |
| 系统睡眠、仅定时唤醒采样 | µA级别量级 | 理想穿戴设备常态 |
实际优化顺序建议是:
- 降低传感器采样频率。计步采样20ms够用,但如果你同时在做姿态识别,可以考虑只在运动剧烈时提频,安静时降到1Hz甚至休眠。
- 延长BLE广播间隔或只在需要时连接。毕竟计步器不是聊天工具,手机不需要秒级知道你的步数。
- 关闭板载LED等指示,很多开发板在跑示例时会默认点灯,这可是实打实的毫安级消耗。
- 检查GPIO浮空输入,浮空引脚在低功耗模式下可能产生额外电流,把这些引脚全部显式配置成下拉或禁用。
这一套优化做下来,实验板从“接充电宝”变成“一颗纽扣电池用很久”是完全可行的。手头没有精密电源分析仪的话,用万用表串到电池回路里读待机电流也够判断量级。
4.3 电池、开关与穿戴结构的细节
硬件层面我踩过一个很典型的坑:直接用锂电池给板子的3.3V引脚供电,结果在电机振动、天线发射等瞬时大电流场景下,电压会跌落,进而导致IMU读数和BLE通信出现随机异常。
正确做法是选合适的电池和电源路径。常见的板载稳压能把纽扣电池锂电池的电压稳到3.3V,但要注意压差和最大电流能力。如果走纽扣电池,务必把峰值电流约束好,BLE广播瞬间的电流需求不小,电池内阻太大容易被拉低电压。另外加一个物理滑动开关比依赖软件休眠更符合真实穿戴设备的直觉,毕竟没人希望设备藏在手腕上时还偷偷耗电。
4.4 低功耗配置下,BLE断连问题为何反而更隐蔽
低功耗优化和BLE连接质量是一对矛盾。广播间隔从20ms拉到200ms甚至500ms后,手机端扫描难度明显增加,连接稳定性也变差。我遇到过的典型“灵异事件”是:设备明明在正常跑算法,手机上就是刷不出数据,一看日志才发现广播参数被自动调整了,手机扫描窗口没对齐。
如果遇到这种问题,第一反应不要改算法,先用手机不同位置、不同姿势测几组广播参数。必要时在设备端保留两个模式:调试模式用密集广播,正式模式用省电广播。系统具备功耗优化的意识是好事,但不要在一开始就把所有广播间隔都调大,否则你可能花一整天找“丢包”问题,结果只是参数没匹配上。
5. 实测中真正让我头疼的几个“非功能性问题”
5.1 误计漂移:挥手、敲键盘、坐公交都算步数
第一版算法在正常走路时表现不错,但一进入办公室就原形毕露。敲键盘的腕部抖动频率高、幅度小,峰值检测很容易命中。挥手这个动作幅度大、间隔规律,几乎就是故意来骗峰值检测的。
我把误计场景分为两类处理:
- 幅度大但频率不稳定:比如挥手、甩手。用步频间隔的变异系数过滤,随机性太大的信号不计数。
- 幅度小但频率高:比如打字。用绝对阈值下限过滤,低于某个加速度幅度的振动不计数。
这两个过滤条件很简单,但放在统一判断链里,误计立减。更激进的做法是引入姿态识别,但这个阶段先别过度设计。
5.2 不同人戴法不同:算法参数不能是一套死参数
同一套参数,我戴着测试稳定,换我同事戴一下试试,误差立刻升高。原因在于:
- 有人走路摆臂幅度大,有人几乎不动;
- 有人习惯戴紧腕带,有人喜欢松垮佩戴;
- 有人脚掌落地轻,有人步伐冲击大。
解决这个问题最朴素的做法是预留“灵敏度档位”。我在设备端保留一个参数接口,用户可以在App或物理按键里切换“日常/走路/跑步”模式。每种模式对应一组阈值、锁定期和滤波系数。实测下来,与其让算法盲目自适应,不如让用户花一秒钟选一个模式,效果往往更稳定。
5.3 电压跌落带来的IMU异常读数
这个问题不是一天遇到的。某次我装上新电池后测试,发现设备时不时会猛地报出一串奇怪的步数值。排查半天,原因不在算法,而在电池老化,瞬时内阻变大导致跌落,IMU内部逻辑进入异常状态,随机输出超范围数据。
从那以后,我在代码里给IMU数据加了合法性校验:如果某轴读数超过合理范围或所有轴同时为0,就忽略该帧并重新初始化传感器。这是一个很小的保护逻辑,但它能省去很多排查时间。
5.4 BLE连接优先级与调试效率的取舍
为了省电,我把广播间隔调得比较长。结果每次手机连接设备都很“随缘”,有时等十秒都扫不到。后来我加了一个物理按键,按下之后进入“强制可发现模式”:广播间隔临时缩短到20ms,持续30秒后再恢复省电参数。这个设计既不影响日常续航,又大幅提高了调试体验。
这个思路可以推广到很多穿戴设备上:不要让产品在性能和可调试性之间二选一,而是通过显式状态切换,让两种模式都接近最优。
6. 从计步器到更多可穿戴玩法:以XIAO生态为例
6.1 给计步器加一块微型显示屏
刚做完纯BLE版时,我发现单靠手机看数据还是差点意思,尤其是抬手想看步数时还要解锁手机。后来在项目中加入了一块很小的OLED屏,I2C接口与IMU共用总线,显示当前步数和电池电量。
这个扩展几乎没有改动算法,只是把步数刷新到显示屏上。但这一步之后,设备的完整个性直接从“开发板”变成“小品消费品”。如果屏幕前面有一块半透外壳,显示方案还能进一步简化。
6.2 从计步到行为识别:姿态、跌倒与运动区分
计步只是可穿戴设备里的一个小功能。算法层面加一个姿态解算后,就能获得更多行为信息:抬手、躺下、跌倒、跑步、骑行。XIAO nRF54LM20A Sense板载IMU的数据能力足以支撑这些行为识别,关键是算法层面的工程量和采样参数的取舍。
我建议不要一上来就做“全功能运动识别”,先把计步做稳,再加入“跑步/走路/静止”三态分类,之后才考虑跌倒检测这类风险较高的场景。
6.3 XIAO生态里的其他可穿戴灵感
最近XIAO社区里有一个很火爆的方向是智能眼镜、头戴显示和微型PCB整合。很多人把ESP32-S3版本的XIAO用作眼镜端的数据处理与显示驱动,配合分体式屏幕与传感器,做成信息提醒眼镜。这和腕戴计步器在项目逻辑上是完全一致的:尽量小的算力平台,尽量低的功耗,尽量固定的佩戴位置,把传感器和交互集成在一起。
回到腕带场景,我下一步计划是把计步数据和GPS模块联合起来做成一个运动轨迹记录器。这会引入新的功耗问题,也会让算法更有挑战。如果你在做一个类似的小型可穿戴设备,我建议先把传感器、BLE、电源这三位一体跑稳,后续所有扩展都会轻松很多。
如果让我重新做这一版,我会把重点放在两件事上:一是在代码里增加一个干净的配置层,把灵敏度、模式、采样频率全部通过BLE可读写配置,而不是烧一次程序改一次参数;二是在硬件结构上把电池、板子、腕带做成一体的可换模块,方便日常清洗和维护。计步器做到这一步已经超越了“玩具”阶段,它是你熟悉低功耗可穿戴开发全流程的一个极佳载体。