基于Wio Terminal的无线空中鼠标:IMU传感器融合与蓝牙HID实战
1. 项目缘起:当开发板不再满足于“开发”
作为一名嵌入式开发者和硬件爱好者,我手头总有几个“吃灰”的开发板。Wio Terminal 就是其中之一,它集成了屏幕、按键、传感器和无线模块,功能强大,但完成几个示例项目后,它就静静地躺在角落了。直到有一天,我在调试一台没有外接鼠标的迷你主机,或者更常见的是,在沙发上用电视盒子浏览网页时,那种对“无线空中鼠标”的渴望变得无比强烈。
市面上当然有现成的演示空中鼠标或PPT翻页笔的方案,但要么功能单一,要么价格不菲。看着手边的 Wio Terminal,一个想法冒了出来:它自带三轴加速度计和陀螺仪(IMU)、多个按键、甚至还有蓝牙,这不就是一个完美的“鼠标”硬件原型吗?为什么不能把它变成一个完全可定制、可编程的无线鼠标呢?这个想法不仅仅是为了“废物利用”,更是对嵌入式设备功能边界的一次有趣探索。它涉及到传感器数据采集、姿态解算、数据滤波、HID(人机接口设备)协议模拟以及无线通信,是一个综合性极强的实战项目。
于是,我决定动手,将 Wio Terminal 变身为一个功能齐全的无线鼠标。这个过程不仅让我对IMU数据处理和HID协议有了更深的理解,还踩了不少坑,积累了一套从数据“毛刺”到平滑光标移动的完整解决方案。下面,我就把这次改造的详细过程、核心原理和避坑经验分享出来。
2. 硬件核心:Wio Terminal 的“鼠标”潜质解剖
要把 Wio Terminal 变成鼠标,我们首先得清楚它身上有哪些“零件”可供我们驱使。这不是简单的软件模拟,而是硬件能力的重新定义。
2.1 传感器部分:姿态感知的基石
Wio Terminal 板载了 LIS3DHTR 三轴加速度计和 ICM-42607 六轴IMU(包含三轴陀螺仪和三轴加速度计)。这是我们实现空中鼠标或姿态控制鼠标的核心。
- 加速度计:测量的是物体在三个轴向上受到的“力”(包括重力)。当设备静止时,加速度计主要测量的是重力加速度在各个轴上的分量,我们可以借此计算出设备相对于水平面的倾斜角度(俯仰和横滚)。但用它来直接计算位移或速度会非常不准确,因为它对任何微小的震动都非常敏感,积分后会产生巨大的漂移。
- 陀螺仪:测量的是物体绕三个轴旋转的角速度。它对旋转运动非常敏感,可以通过积分角速度来得到角度变化。但陀螺仪也存在漂移问题,长时间积分后,即使设备静止,计算出的角度也会慢慢偏离真实值。
所以,单独使用任一种传感器都无法获得稳定、可靠的姿态数据。这正是我们项目中需要解决的首要算法问题。
2.2 输入设备:超越普通鼠标的交互维度
一个标准的鼠标有左键、右键、滚轮。Wio Terminal 在这方面可谓“豪华”:
- 物理按键:正面有三个可编程按键,侧面还有两个。这足以映射左键、右键、中键,甚至还能多出两个自定义键(如前进、后退)。
- 五向摇杆:这简直是一个“摇杆+按键”的复合体。除了可以模拟鼠标移动(通过摇杆方向),其按下动作本身就可以作为一个独立的按键使用(例如映射为DPI切换)。
- 光传感器与红外发射器:虽然不直接用于鼠标功能,但可以开发一些智能交互,比如检测到用户拿起设备时自动唤醒鼠标模式,或者用作简单的遥控器。
- 屏幕:这是最大的亮点。我们可以用它来实时显示鼠标电量、连接状态、当前DPI(灵敏度)、甚至是一个小小的光标预览。这是任何商业鼠标都无法提供的可视化反馈。
2.3 无线连接:摆脱线缆的束缚
Wio Terminal 集成了 Realtek RTL8720DN 芯片,同时支持 WiFi 和蓝牙 5.0。对于鼠标应用,蓝牙是更理想的选择,因为它功耗相对较低,且与电脑、手机、平板等设备的兼容性极好。我们需要让 Wio Terminal 通过蓝牙模拟成一个标准的 HID 鼠标设备,这样无需安装任何驱动,主机就能即连即用。
2.4 供电与续航
Wio Terminal 自带锂离子电池充电管理电路和一块 200mAh 的电池。对于持续运行的鼠标来说,这块电池偏小。一个重要的改造点或使用建议就是外接一块更大容量的电池(通过其 Grove 接口或直接焊接),并优化代码以进入低功耗模式(例如,通过加速度计检测静止状态,自动进入睡眠)。
3. 核心算法:从传感器数据到稳定光标移动
这是整个项目的技术心脏。如何把原始的、充满噪声的 IMU 数据,转换成稳定、跟手的光标移动指令?直接读取原始数据并映射是行不通的,你会得到一个疯狂跳动、完全无法控制的光标。
3.1 数据采集与预处理
首先,我们需要以较高的频率(例如100Hz)读取加速度计和陀螺仪的原始数据。这些数据通常带有高频噪声和固定的零点漂移。第一步是进行简单的软件滤波,比如一个移动平均滤波器,来平滑数据。
// 示例:简单的移动平均滤波(伪代码) #define FILTER_SIZE 5 float accelXBuffer[FILTER_SIZE]; int bufferIndex = 0; float applyLowPassFilter(float newValue, float *buffer) { buffer[bufferIndex] = newValue; bufferIndex = (bufferIndex + 1) % FILTER_SIZE; float sum = 0; for(int i=0; i<FILTER_SIZE; i++) { sum += buffer[i]; } return sum / FILTER_SIZE; }3.2 姿态解算:互补滤波器的实战应用
如前所述,加速度计在静态时测角度准,但动态时噪声大;陀螺仪测动态旋转准,但会漂移。互补滤波器(Complementary Filter)是一种巧妙且计算量小的融合算法,它利用高频特性好的陀螺仪和低频特性好的加速度计,取长补短。
其核心思想可以理解为:用陀螺仪积分得到的角度作为主体,但用加速度计计算出的角度不断去“纠正”陀螺仪的长期漂移。公式通常如下:
当前估计角度 = α * (上一刻估计角度 + 陀螺仪角速度 * Δt) + (1 - α) * 加速度计角度
其中,α是一个介于0和1之间的滤波系数(通常取0.98左右)。Δt是采样时间间隔。
(上一刻估计角度 + 陀螺仪角速度 * Δt)是陀螺仪的积分结果,响应快,但会漂移。加速度计角度是通过atan2(accelY, accelZ)等公式从加速度数据中计算出来的(假设一个轴朝下),在静态时非常准确,但动态时噪声大。α接近1,表示更信任陀螺仪(高频);(1-α)很小,表示只用一点点加速度计的数据去修正低频漂移。
在我的实现中,我分别对俯仰角(Pitch)和横滚角(Roll)应用了互补滤波器。这样,我就得到了一个相对稳定、响应及时的设备姿态角度。
3.3 从姿态到光标位移:映射策略
得到稳定的俯仰和横滚角后,如何映射到屏幕上的光标移动?
- 定义零位:让 Wio Terminal 保持一个你感觉舒适的握持姿势,按下某个校准键(比如摇杆按下),将此时的俯仰角和横滚角记录为
pitch_zero和roll_zero。 - 计算角度差:在后续操作中,实时计算当前角度与零位的差值:
delta_pitch = current_pitch - pitch_zero,delta_roll = current_roll - roll_zero。 - 映射为移动速度:这是关键。不要直接将角度差映射为光标位置!那会使得光标移动非常生硬且难以控制。正确的方法是,将角度差映射为光标的移动速度。
- 当设备倾斜角度很小时,光标缓慢移动;倾斜角度越大,光标移动速度成比例增加(可以设置一个非线性曲线,如指数关系,让微调更精准,大范围移动更快)。
- 这模拟了真实鼠标的“速度控制”模式,而不是“位置控制”模式,体验上自然得多。
- 生成HID报告:将计算出的 X 轴速度(通常对应
delta_roll)和 Y 轴速度(对应-delta_pitch,注意屏幕Y轴方向与俯仰角方向通常相反)填充到蓝牙 HID 鼠标报告数据结构中,并发送给主机。
4. 软件实现:蓝牙HID与功能集成
有了算法核心,我们需要用代码将其实现,并让电脑识别为鼠标。
4.1 开发环境与库选择
我使用 Arduino IDE 进行开发,因为它对 Wio Terminal 的支持非常完善。关键库包括:
Seeed_Arduino_LIS3DHTR和Seeed_Arduino_ICM42607:用于读取IMU数据。Seeed_Arduino_rpcUnified/Seeed_Arduino_rpcBLE或Adafruit_BluefruitSPI:用于实现蓝牙 HID 功能。Wio Terminal 的官方库正在向 RPC 框架迁移,但 Adafruit 的库在 HID 描述符定制上可能更灵活。我最终选择了基于 RPC 框架的 BLE HID 方案。
4.2 模拟蓝牙HID设备
要让电脑识别,Wio Terminal 需要广播自己是一个 HID 设备,并提供正确的 HID 描述符。描述符定义了设备类型(鼠标)、报告格式(包含哪些数据:按钮状态、X位移、Y位移、滚轮等)。
// 这是一个简化的HID鼠标报告描述符示例,它定义了报告包含:1字节按钮,2字节X位移,2字节Y位移,1字节滚轮。 static const uint8_t hidReportDescriptor[] = { 0x05, 0x01, // Usage Page (Generic Desktop) 0x09, 0x02, // Usage (Mouse) 0xA1, 0x01, // Collection (Application) 0x09, 0x01, // Usage (Pointer) 0xA1, 0x00, // Collection (Physical) // 按钮 8位 0x05, 0x09, // Usage Page (Button) 0x19, 0x01, // Usage Minimum (Button 1) 0x29, 0x08, // Usage Maximum (Button 8) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x01, // Logical Maximum (1) 0x95, 0x08, // Report Count (8) 0x75, 0x01, // Report Size (1) 0x81, 0x02, // Input (Data,Var,Abs) // 填充 8位 0x95, 0x01, // Report Count (1) 0x75, 0x08, // Report Size (8) 0x81, 0x03, // Input (Const,Var,Abs) // X轴 16位 0x05, 0x01, // Usage Page (Generic Desktop) 0x09, 0x30, // Usage (X) 0x16, 0x01, 0x80, // Logical Minimum (-32767) 0x26, 0xFF, 0x7F, // Logical Maximum (32767) 0x75, 0x10, // Report Size (16) 0x95, 0x01, // Report Count (1) 0x81, 0x06, // Input (Data,Var,Rel) // Rel代表相对位移 // Y轴 16位 0x09, 0x31, // Usage (Y) 0x81, 0x06, // Input (Data,Var,Rel) // 滚轮 8位 0x09, 0x38, // Usage (Wheel) 0x15, 0x81, // Logical Minimum (-127) 0x25, 0x7F, // Logical Maximum (127) 0x75, 0x08, // Report Size (8) 0x95, 0x01, // Report Count (1) 0x81, 0x06, // Input (Data,Var,Rel) 0xC0, // End Collection 0xC0 // End Collection };在 Arduino 代码中,我们需要初始化 BLE,设置设备名称为“Wio Mouse”,并添加 HID 服务。然后,在主循环中,不断执行传感器读取、数据处理,最后将生成的 X/Y 速度值和按钮状态打包成一个 HID 报告,通过BLEHID().mouseReport()类似的函数发送出去。
4.3 按键与屏幕功能集成
- 按键映射:在
loop()函数中检测各个按键和摇杆的按下状态,将其转换为对应的鼠标按钮位(MOUSE_LEFT,MOUSE_RIGHT,MOUSE_MIDDLE)并填入 HID 报告。 - 摇杆:除了按下作为中键,其 X/Y 方向读数可以作为一种备用或精确模式下的光标控制方式(直接映射为相对位移)。
- 屏幕显示:使用 TFT 库,可以创建一个简单的界面,显示电池图标(通过读取模拟引脚电压估算)、蓝牙连接状态、当前灵敏度档位等。这大大提升了产品的完成度和可交互性。
5. 调优与避坑:从“能用”到“好用”
把基础功能跑通只是第一步,让这个自制鼠标达到“好用”甚至“跟手”的程度,需要大量的调优和问题解决。
5.1 光标漂移与抖动处理
这是最常见的问题。即使使用了互补滤波器,在完全静止时,光标可能仍会缓慢移动或轻微抖动。
- 死区设置:在角度差
delta_pitch和delta_roll的绝对值小于一个很小的阈值(如0.5度)时,直接将其视为0,不产生任何光标移动。这能有效消除手部微小颤抖带来的影响。 - 速度曲线优化:不要使用简单的线性映射。尝试使用指数或自定义的 S 型曲线进行映射。在小角度区间,映射出的速度增长很慢,便于精细操作;在大角度区间,速度增长加快,便于快速移动光标。这类似于游戏鼠标的灵敏度调节。
- 传感器校准:上电后,让设备在水平桌面静止几秒钟,自动采集这段时间内加速度计和陀螺仪的平均值作为零偏校准值,并在后续计算中减去。这能消除硬件本身的微小偏差。
5.2 蓝牙连接稳定与功耗
- 连接间隔:BLE 连接参数中的“连接间隔”决定了设备与主机通信的频率。间隔太短(如7.5ms)功耗高但响应快;间隔太长(如100ms)会有明显延迟。对于鼠标,20-30ms是一个不错的平衡点。需要在代码中尝试请求主机调整连接参数。
- 功耗优化:这是让电池续航更久的关键。实现一个“睡眠-唤醒”机制:当加速度计连续一段时间检测到的运动幅度非常小(设备静止)时,让 MCU 进入深度睡眠模式,并关闭蓝牙广播。当检测到较大运动时(用户拿起设备),通过中断唤醒 MCU,重新初始化蓝牙并快速连接。Wio Terminal 的 IMU 本身支持运动中断功能,可以很好地利用这一点。
5.3 握持体验与零位校准
空中鼠标的握持姿势因人而异。固定的零位不适合所有人。
- 动态零位:我实现了一个长按某个按键(如侧面按键)进入“零位校准模式”的功能。此时屏幕提示“请保持舒适握姿并按下摇杆”,用户调整好姿势后按下摇杆,新的零位就被记录到EEPROM中,下次开机自动加载。
- 多种控制模式:除了主要的“姿态控制模式”,还可以通过按键切换为“摇杆控制模式”(适合精细作图)或“混合模式”(摇杆控制移动,姿态控制用于快速大范围移动)。通过屏幕菜单可以方便地切换。
5.4 应对不同操作系统的主机
一个隐藏的坑是,不同操作系统(Windows, macOS, Linux)对蓝牙 HID 报告的处理可能有细微差别。例如,某些系统下滚轮速度的缩放比例不同。我的解决方案是在代码中增加一个“操作系统选择”菜单(通过屏幕设置),针对不同系统应用不同的速度乘数或报告格式微调,确保体验一致。
6. 功能扩展与进阶玩法
当基础鼠标功能稳定后,Wio Terminal 的潜力才真正开始展现。
6.1 多媒体与快捷键控制
多余的按键可以轻松映射为键盘媒体键,如播放/暂停、音量加减、下一曲等。这需要 Wio Terminal 同时模拟 HID 键盘设备(复合设备)。在 HID 描述符中同时包含鼠标和键盘的描述符即可实现。这样,它就成了一个集鼠标、演示笔、媒体控制器于一身的设备。
6.2 手势识别
利用 IMU 数据序列,可以识别简单的手势。例如:
- 快速翻腕:识别为“返回桌面”或“切换应用”的快捷键(模拟 Win+D 或 Cmd+Tab)。
- 画圈:识别为调节系统音量。
- 双击机身:通过检测特定的加速度脉冲模式,实现鼠标“双击”或执行自定义命令。
这需要引入简单的手势识别算法(如动态时间规整 DTW 或更简单的阈值+时序判断),虽然会增加复杂度,但可玩性极高。
6.3 与Home Assistant等智能家居联动
Wio Terminal 支持 WiFi。我们可以让它连接家庭网络,在检测到特定手势或按下特定组合键时,通过 MQTT 或 REST API 向 Home Assistant 发送指令,从而控制灯光、空调等智能设备。这时,它就从一个人机交互设备,升级为了一个可穿戴的智能家居遥控器。
6.4 开源与社区改进
我将这个项目的核心代码和配置文件开源在了 GitHub 上。社区的力量是巨大的,已经有朋友贡献了基于更复杂滤波算法(如卡尔曼滤波)的版本,提供了更好的姿态稳定性;也有人增加了对 USB HID 模式的支持(通过 USB-C 线直连电脑,作为有线鼠标使用,零延迟)。这种可扩展性,正是开源硬件项目的魅力所在。
将 Wio Terminal 改造成鼠标,远不止是一个简单的“模拟输入设备”项目。它贯穿了传感器融合、实时滤波、无线通信、协议模拟、低功耗设计和用户体验优化等多个嵌入式开发的核心领域。过程中遇到的每一个问题——光标抖动、连接不稳定、功耗过高——都迫使你去深入理解底层原理,而不是停留在调用API的层面。最终得到的,不仅是一个独一无二的、完全按自己习惯定制的外设,更是一套解决复杂系统问题的实战经验。如果你手头也有闲置的开发板,不妨尝试一下这个项目,它带给你的收获,肯定会超过一个鼠标本身的价值。
