尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

DIY智能骑行台:基于Arduino+霍尔传感器的BLE-FTMS改装

DIY智能骑行台:基于Arduino+霍尔传感器的BLE-FTMS改装 简介一套基于Arduino与双ESP32的BLE室内自行车健身机开源方案面向嵌入式开发者、骑行爱好者以及体育器材DIY玩家旨在用较低成本模拟专业骑行台与功率计的核心功能。方案拆分为测量端与模拟端一个ESP32采集曲柄力量与踏频另一个ESP32接收数据、模拟训练器并转发同时控制电阻螺丝与12864屏幕用户可在ERG模式调节阻力、在模拟模式切换档位或从SD卡加载预设阻力曲线。资源共30个文件、压缩包约1.24MB包含2份Arduino固件代码、1个Python配置器、1份说明文档以及Eagle电路设计、Fusion 360三维模型、3D打印STL与DXF加工图纸覆盖电子、结构、控制与配置工具可直接复刻或二次开发。配套文档还介绍了“健身机器服务”规范便于理解BLE FTMS广播数据格式目前已有758人学习下载适合希望深入室内骑行设备通信协议并快速搭建软硬件原型的开发者。 把一台踩起来咯吱作响的老式室内骑行台变成能被 Zwift、运动 App 和手机直接识别的“智能健身机”这种事情听起来像改装达人才能干但用 Arduino 加一个霍尔传感器就能做到。我最近刚把这个项目从想法跑到能稳定连接 Zwift 出数据核心就是围绕 ble-ftms 这套 BLE 标准做开发。这篇文章是整个实现过程的完整复盘包括 BLE 协议里那些容易把人绕晕的概念、硬件怎么选、转速怎么算、数据包怎么组以及测试过程中我踩过的几个大坑给准备自己动手做室内骑行数据方案的朋友做个参考。如果你手头有一台没有蓝牙数据输出的老款骑行台或者只是享受把代码和机械件拼在一起的乐趣这套方案能让你用很少的硬件成本换到完整的室内训练数据链路。整篇不讨论把数据接到大屏软件之外的商业玩法只讲从传感器到 GATT 数据上报这条最务实的主线。1. 项目概述给骑行台加“嘴”让它说话1.1 骑行台加装BLE到底解决了什么室内骑行训练最大的痛点不是汗流浃背而是练完了你不知道自己刚才到底踩出了什么数据。速度、踏频、距离、功率这些是衡量训练效果的基本指标但很多老款室内自行车上的机械码表只能显示一个大概的时速根本没有办法把数据给手机 App 或者 Zwift 这类虚拟骑行软件用。加装 BLE 模块之后骑行台的数据就可以变成标准化信号。手机上的极核 BLE 助手、Zwift、Peloton、甚至自己写的调试工具都能把它当成一台真正的智能健身器械来用。你不需要为了一个踏频功能去换一台几千块的智能骑行台只需要在原有机械结构上加一个传感器再加一块几块钱的 Arduino 兼容开发板。另外还有个很实际的应用场景很多骑友在室内训练时喜欢用手机上的训练计划或者边骑边在投屏上看实时数据。没有蓝牙数据接口这些 App 就等于瞎了。BLE-FTMS 方案能让你把一台普通阻力骑行台变成“标准协议设备”几乎所有支持 Fitness Machine Service 的客户端都可以直接识别。1.2 这套方案的选型逻辑先说主控。项目标题里写的是 Arduino但我的建议是尽量选带 BLE 模块的开发板最省事的是 ESP32。ESP32 的 Arduino 支持已经非常成熟自带双模蓝牙模块功耗也能接受价格在十几块钱以内性能跑一个小小的 FTMS 外设绰绰有余。相比之下Arduino Uno 或 Nano 本身不带蓝牙需要外挂串口透传模块这会额外引入波特率配置、AT 指令、模块配对等一堆问题不推荐新手走这条路。再说传感器。室内骑行台的转动件一般就两个地方可以测飞轮和曲柄。用霍尔传感器加磁铁的方式最简单不接触、无磨损成本不到五块钱。A3144 霍尔开关模块直接输出数字电平接一个 GPIO 就能检测磁铁经过的信号。关于为什么用 FTMS 标准而不用自定义 BLE 协议后面会专门展开。简单说自定义协议意味着手机端和骑行软件端都得跟着造轮子数据格式每次都要对齐而 FTMS 是蓝牙技术联盟定义好的健身器械服务UUID 固定、字段顺序固定客户端天然支持你只需要做“数据上报”这一件事。2. BLE-FTMS协议核心把数据翻译成App听得懂的语言2.1 BLE基础概念广播、连接、MTU、绑定做这个项目之前我先把 BLE 的几个基础概念理清了一遍如果不搞清楚后面调试起来会很茫然。BLE 和经典蓝牙 BR/EDR 的区别简单概括就是经典蓝牙像打电话建立连接后持续占线适合音频这类需要大吞吐的场景BLE 像发微信消息连接后通过不定期的事件交换数据功耗低特别适合传感器这类低频短数据的传输。FTMS 走的就是 BLE 这条路线。BLE 外设的工作流程大体是先广播然后等待中心设备扫描到并发起连接连接建立后再通过 GATT 服务交换数据。广播阶段有几个概念容易混比如“广播类型”。如果你只是想让手机在扫描列表里看到设备用普通可连接广播就行如果想让 App 自动识别设备类型可以在广播数据里加入服务 UUID。很多骑行软件就是靠广播包里的 0x1826 这个 Fitness Machine Service UUID 来确定设备类型的这个细节在第三章代码部分展开讲。MTU 是另一个容易踩坑的概念。BLE 默认情况下单包数据最多 23 字节其中包含 3 字节蓝牙协议头留给应用层的只有 20 字节。如果你的数据包里字段很多超过 20 字节就需要 MTU 协商。MTU 协商是由连接发起方手机 App主动请求的Arduino 作为外设只能接受请求不能主动发起。我的代码里把数据字段数量控制在 20 字节以内就是为了规避这个坑实际测试下来也确实最稳定。绑定Bond在调试时也可能出现。部分 App 连接设备后要求执行配对绑定这时候如果你在 Arduino 端没有处理配对回调连接就会卡在等待配对状态。后面我会专门讲这个问题的处理办法。2.2 FTMS服务结构和Indoor Bike Data数据格式FTMS 是 BLE GATT 服务里专门为健身器械定义的一类服务服务 UUID 是 0x1826。在这个服务下面不同的特征值负责不同功能对我们做室内自行车来说最重要的特征值就是 Indoor Bike DataUUID 是 0x2AD2。这个特征值的数据是周期性上报的上报格式有严格规定。最常用的字段顺序如下字段位数单位说明Flags2字节-用每一位标记后面哪些字段存在Instantaneous Speed2字节km/h速度值实际数值乘以100存储Instantaneous Cadence2字节rpm曲柄当前踏频Total Distance4字节m累计距离Instantaneous Power2字节W当前功率Elapsed Time2字节秒设备启动后的累计时间Flags 的前面几位决定后面字段是否出现在数据包里。比如我们要上报速度和踏频Flags 就置位第0位和第1位数据包里就按顺序放速度、踏频两个字段。这样设计的好处是省流量不是必要的字段完全可以不填充客户端也会根据 Flags 自动解析。如果你的骑行台有磁阻或风阻结构还可以通过功率模型估算瞬时功率然后把第3位置位上报功率字段。不过这个模型依赖阻力标定做精准了比较复杂MVP 阶段可以先不上报功率字段等基础链路稳定后再加。2.3 为什么选择标准FTMS而不是自定义协议在做这个项目时我考虑过直接把传感器数据通过自定义 BLE Characteristic 上报手机端再写个 App 接收当时觉得这样自由度更高。后来发现纯粹是给自己找麻烦。自定义协议的问题在于每一个使用环节都需要适配。你用 Zwift 测试Zwift 不认识你的自定义 UUID根本不会把你的设备识别成自行车你想换一个骑行 App另一个 App 也不认识所有逻辑又得重写。而 FTMS 是现成的“通用语言”只要你的服务 UUID 是 0x1826Indoor Bike Data 特征的数据格式符合规范主流虚拟健身软件就能秒认设备。而且 FTMS 的特征结构是固定的开发时反而省心。你不需要自己设计“第几个字节是什么数据”这样的协议照着官方文档顺序打包就行。调试的时候随便找一个支持 FTMS 的调试工具就能验证数据是否正确。用标准协议相当于让全球开发者一起帮你抽象好了兼容层。3. 硬件选型与Arduino环境搭建3.1 主控、传感器与供电方案硬件清单非常简单我把它们列全ESP32 开发板一块我用的是 WROOM-32 经典款任何 30 引脚的 ESP32 板都行A3144 霍尔开关模块一个直径 3mm 到 5mm 的钕磁铁几颗杜邦线若干移动电源或者 USB 电源适配器骑行台附近需要供电霍尔传感器的安装位置很关键。我是把磁铁用强力胶固定在飞轮边缘霍尔模块固定在骑行台支架上让磁铁每次转到霍尔元件正面时能给一个低电平脉冲。安装时要注意磁铁与霍尔元件的间距一般控制在 5mm 到 10mm 之间会比较稳定太远信号丢太近会碰到。测踏频的话可以把磁铁固定在曲柄臂上霍尔模块固定在车架上。这个位置测出的脉冲频率就是踏频本身rpm后续计算会简单很多。我的代码同时兼容这两种接法区别只在计算环节下面在核心实现部分说明。供电方面ESP32 的电流峰值接近 500mA普通 USB 口完全能带起来。室内骑行台一般固定在室内直接插一个 USB 充电头就能长期供电。如果想做到完全无线可以用一块 18650 锂电池接一个 AMS1117-3.3 稳压板给 ESP32 供电但要注意电池容量我实测一颗 2000mAh 的电池能用七八个小时。3.2 Arduino IDE配置与库目录管理的坑软件环境方面我用的还是老牌的 Arduino IDE版本 2.x 和 1.8.x 都行。安装 ESP32 开发板支持有两种方法一种是在“开发板管理器”里搜索 esp32 by Espressif另一种是离线安装包。离线安装包的好处是可以绕过国内下载慢的问题直接解压到 Arduino 的 hardware 目录。库的管理有个细节值得说如果你用 WindowsArduino 默认的库目录在“文档/Arduino/libraries”下面所有第三方库都装在这里。如果你像我一样喜欢把工程目录放到别的盘可以在“文件 - 首选项”里修改“项目文件位置”但库目录和项目文件位置是两个独立概念别弄混。想让库目录也换位置可以通过设置环境变量或者修改 Arduino15 相关的配置文件来实现但对普通用户来说最稳妥的方案是保持默认库目录项目目录随意改就好了。ESP32 的 BLE 功能不需要额外安装第三方库官方在 esp32 core 里自带了 BLEDevice 库直接用即可。如果你用的是 Arduino Uno 加外挂 BLE 模块那就要根据模块型号找对应库和 AT 指令集了复杂度会明显升高。最后是我实际遇到的一个小坑Arduino IDE 2.x 在首次编译 ESP32 工程时会从服务器拉取 esp32 工具链下载时间可能特别长。解决办法是提前下载好 esp32 离线包手动安装或者在等它下载的时候耐心多试几次中间一旦出现“Json invalid”多数是网络问题重试即可别乱改配置。4. 核心实现转速采集到FTMS数据上报4.1 霍尔传感器转速采集与计算代码主要分三部分转速数据采集、BLE 服务创建、数据打包上报。我先说数据采集。霍尔传感器接在 ESP32 的 GPIO4 上用中断方式检测磁铁经过的信号。磁铁每扫过一次霍尔元件就触发一次中断我在中断服务函数里记录相邻两次脉冲的时间差用来计算瞬时转速。const int hallPin 4; volatile uint32_t lastHallMicros 0; volatile uint32_t hallIntervalMicros 0; void IRAM_ATTR hallISR() { uint32_t now micros(); uint32_t delta now - lastHallMicros; if (delta 2000 delta 2000000) { hallIntervalMicros delta; } lastHallMicros now; } void setup() { pinMode(hallPin, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(hallPin), hallISR, FALLING); }代码里加了两个时间判断间隔太短小于2毫秒的脉冲丢弃这是为了滤掉霍尔模块在磁铁刚靠近时可能产生的接触抖动间隔太长大于2秒的脉冲也丢弃防止静止状态下上一次的数据一直占用。主循环里计算转速时把相邻脉冲间隔换算成 RPM。如果只有一个磁铁一分钟内的转数就是每分钟脉冲数float intervalSec hallIntervalMicros / 1000000.0; float flywheelRpm 60.0 / intervalSec;如果飞轮上装了多个磁铁要除以磁铁数量。这个 RPM 是飞轮转数。如果传感器装在曲柄上这个值就是踏频 Cadence 本身。我代码里用了一个宏来判断传感器安装位置#define SENSOR_ON_FLYWHEEL 1 #define MAGNET_COUNT 24.2 构建GATT服务与特征BLE 部分直接用 ESP32 的 BLEDevice 库创建服务。服务 UUID 填 0x1826特征 UUID 填 0x2AD2属性设置为可通知NOTIFY这样客户端通过订阅通知就能持续收到数据。BLEServer *pServer BLEDevice::createServer(); BLEService *pFtmsService pServer-createService(BLEUUID(0x1826)); BLECharacteristic *pIndoorBikeData pFtmsService-createCharacteristic( BLEUUID(0x2AD2), BLECharacteristic::PROPERTY_NOTIFY );还需要注册一个特征回调在客户端订阅/退订通知的时候记录状态。很多新手漏掉这个回调也没报错但调试进度判断会麻烦一些。class BikeDataCallbacks : public BLECharacteristicCallbacks { void onSubscribe(BLECharacteristic *pCharacteristic, uint16_t cccd) override { if (cccd 0x0001) { Serial.println(client subscribed); } } }; pIndoorBikeData-setCallbacks(new BikeDataCallbacks());广播配置是容易忽略的重点。为了让 Zwift 和手机 App 自动识别设备广播包里必须加入 0x1826 这个服务 UUID。这样扫码的时候客户端会直接看到这是一个“Fitness Machine”设备而不是一堆未知设备里的某一个。BLEAdvertising *pAdv pServer-getAdvertising(); pAdv-addServiceUUID(BLEUUID(0x1826)); pAdv-start();4.3 数据打包与通知上报每次上报数据时先组装 FTMS 标准数据包再用 notify 推送给客户端。因为我只上报速度和踏频两个字段数据包只有 6 字节完全在默认 20 字节的 MTU 范围内不需要处理 MTU 协商。const uint16_t FLAG_SPEED 0x0001; const uint16_t FLAG_CADENCE 0x0002; void sendIndoorBikeData(float speedKmh, float cadenceRpm) { uint8_t data[6]; int idx 0; uint16_t flags FLAG_SPEED | FLAG_CADENCE; data[idx] flags 0xFF; data[idx] (flags 8) 0xFF; uint16_t speedVal (uint16_t)(speedKmh * 100.0f); data[idx] speedVal 0xFF; data[idx] (speedVal 8) 0xFF; uint16_t cadenceVal (uint16_t)cadenceRpm; data[idx] cadenceVal 0xFF; data[idx] (cadenceVal 8) 0xFF; pIndoorBikeData-setValue(data, idx); pIndoorBikeData-notify(); }上报频率我设置在每 500 毫秒一包。这个频率对传感器数据来说足够平滑也不会给 BLE 连接造成压力。如果上报太快部分手机 App 反而会因为收包太多出现卡顿或者延迟。把飞轮转速换算成速度的时候我用的是飞轮周长乘以 RPM 再除以时间系数这个换算依赖于飞轮的实测尺寸。我车上的飞轮周长大约是 2.1 米换算逻辑如下float flywheelCircumference 2.10f; // 单位米 float speedKmh flywheelRpm * flywheelCircumference * 60.0f / 1000.0f;如果传感器装在曲柄上则不需要这一步换算踏频 RPM 直接就填到 Cadence 字段里。4.4 控制点与状态处理进阶FTMS 服务里还有一个 Fitness Machine Control Point 特征UUID 是 0x2AD9用来接收客户端下发的开始/停止、设定阻力等控制指令。这个特征需要支持 WRITE 属性并且要按协议规定返回状态码。MVP 阶段可以先不实现控制点因为大部分骑行软件在“仅读取数据”模式下不会强求控制点。但是如果后续你想让 Zwift 控制阻力、模拟坡度就必须实现。控制点的消息结构较复杂包括 Op Code、参数、操作码状态等建议在基础数据链路稳定后再单独加避免一次引入太多变量出了问题不好定位。5. 调试实录搜不到、断连、数据跳变怎么查5.1 App/Bike软件搜不到设备这是遇到最多的问题。先检查广播是否真的在跑最简单的方法是用手机上的 BLE 调试助手扫描看有没有出现自定义名称的设备。如果扫描列表里根本没有大概率是广播没有启动或者开发板没有正确运行到广播那行代码。如果已经能看到设备名但 Zwift 不识别那就是广播包里没有服务 UUID 0x1826App 不知道这是健身器械。我见过很多人的代码里服务是加了但广播配置里忘了加 UDP结果只能手动去设备列表里找通用蓝牙设备而 Zwift 这类软件只认 FTMS 服务所以直接过滤掉了。另外手机系统权限也可能导致扫描不到设备。Android 12 以上版本对蓝牙权限要求很严需要在应用设置里打开“附近设备”权限iOS 则需要在系统设置里允许对应 App 使用蓝牙。这个不属于设备端问题但很常被忽略。5.2 MTU不足导致数据截断或无法订阅如果数据包超过 20 字节客户端又没有发起 MTU 协商就会出现数据被截断或者订阅通知失败的情况。我的经验是刚开始做原型时尽量避免把全部字段都放进数据包先只上报速度和踏频等链路稳定后再研究扩展字段。如果你一定要上报距离、功率、时间等完整字段那就要在代码里监听 MTU 变化事件。ESP32 的 BLEDevice 库提供了 setMTU(int mtu) 接口但注意这是外设端对被动的 MTU 请求的响应上限设置真正的协商还得看客户端。连接按钮旁边通常会有日志显示当前 MTU调试助手类的工具一般都有这个展示可以用来确认协商结果。5.3 数据跳变与频率问题霍尔传感器数据跳变的原因第一是中断抖动没有滤干净。我的代码里用相邻两次脉冲间隔最小值做过滤即小于 2ms 的间隔直接忽略能解决大部分脉冲抖动问题。第二是磁铁安装方向不对。霍尔元件有感应面磁铁的北极或南极靠近时输出极性不同。如果磁铁装反了信号脉冲可能不稳定表现为 RPM 数值偶尔突然飙到几千。第三是上报频率和设备处理速度不匹配。如果主循环里做了太多串口打印或者延迟操作会导致中断数据没有被及时读取上报给手机的数据就忽高忽低。串口打印是调试时必要的但记得在最终版本里关掉至少降低打印频率。5.4 绑定与配对异常如果你的调试 App 连接后一直处于“正在绑定”状态问题出在配对处理。ESP32 的 BLEDevice 库需要你主动注册安全回调才能正确处理配对和绑定请求。一个简单的处理方式是只接受 No Bond 模式不存储配对信息BLEDevice::setSecurityCallbacks(new MySecurityCallback());如果 App 强制要求 Bond而你这边没响应连接就会卡住。大部分健身类 App 用的是 Just Works 配对不需要输入 PIN 码但如果遇到强制绑定最直接的排查办法是换一个 BLE 调试助手试试先排除 App 本身行为导致的兼容性问题。我目前的代码里把绑定回调设为始终接受并返回 No BondZwift、Decathlon 的室内骑行 App、极核助手都试过连接都很稳定。最后再分享一个我在实际项目中的体会室内自行车改装 BLE 最容易让人卡住的地方不是硬件接线也不是代码语法而是对 FTMS 数据格式的理解偏差。Flags 每一位代表什么、数据按什么顺序排这些细节一旦对齐了剩下的工作就是按部就班的组装。做完成之后我用 Zwift 连续骑了一小时速度、踏频数据全程没有断流那一刻确实体会到了动手做一个“标准化外设”的乐趣。这个项目后续还可以往功率估算、自动阻力控制、独立供电等方向扩展如果你也正在捣鼓同类的改装可以先按照第五部分的问题清单排查一遍大概率能找到你卡住的点。本文还有配套的精品资源点击获取
返回列表