在现代人机交互设备中,空中飞鼠(Air Mouse)允许用户摆脱桌面的物理限制,通过在空中挥动设备来控制屏幕上的光标。它通常被应用于幻灯片演示、智能电视遥控以及 VR 交互中。
本文将深入解析一套在嵌入式设备上运行的空中飞鼠算法的完整实现。该实现硬件上基于 BMI270(陀螺仪 + 加速度计),在算法上巧妙融合了 Mahony 姿态解算滤波器 和 1 Euro 自适应光标滤波器。
整体架构:从物理运动到像素位移
这套空中飞鼠系统的核心思想是:角速度积分计算姿态 加速度计修正重力方向 姿态欧拉角映射为屏幕坐标 动态滤波与交互防抖。
当用户同时长按设备的侧边按键(Side 1 + Side 2)2秒时,系统会挂起底部的 PAW3395 光学追踪传感器,唤醒 BMI270 IMU 模块,正式进入空中飞鼠模式。整个数据流分为两个核心模块:attitude.c(姿态解算) 和 pointer.c(光标映射)。
第一部分:姿态解算(Attitude Estimation)
要获取鼠标在空中的指向,我们需要计算设备的 3D 姿态。这里使用了经典的 Mahony 互补滤波算法。
1. 基础数学:四元数积分
系统使用四元数 来表示设备在空间中的旋转。 陀螺仪测量得到三轴角速度 。根据刚体运动学,四元数的微分方程为:
在代码实现中,离散化的四元数积分更新公式表达如下:
依此类推计算 。更新后,必须对四元数进行归一化以保证其表示有效旋转。
2. Mahony 加速度计重力修正
陀螺仪积分不可避免地会产生累积误差(积分漂移)。为了消除漂移,我们利用加速度计测量的重力向量作为绝对参考点。 Mahony 滤波的核心在于叉乘误差反馈。
首先,根据当前四元数推算出理论上的重力向量 。因为在导航系中重力垂直向下,将其通过逆向旋转映射到机体系,提取出一半的重力向量参数:
const float half_vx = a->q1 * a->q3 - a->q0 * a->q2;
const float half_vy = a->q0 * a->q1 + a->q2 * a->q3;
const float half_vz = a->q0 * a->q0 - 0.5f + a->q3 * a->q3;
(注:代码为了计算效率,提取了 )
然后,将加速度计测得的向量进行归一化,得到实际的重力向量 。 计算理论重力与实际重力的误差,这里使用向量外积(叉乘):。
const float half_ex = ny * half_vz - nz * half_vy;
// ...计算半误差向量
这个误差向量 代表了当前姿态估计与实际重力方向的偏差角度。
最后,使用比例增益 mahony_kp(在此系统中默认为 1.0f)将这个误差反馈补偿到陀螺仪的角速度读取中:
3. ZARU(零偏更新):对抗温漂的终极武器
尽管有加速度计修正,但加速度计只能修正 Roll(横滚)和 Pitch(俯仰),对绕重力轴的 Yaw(偏航)无能为力。如果陀螺仪存在静态零偏(Zero Bias),指针会在屏幕上发生横向匀速漂移。
为此,系统引入了 ZARU (Zero Angular Rate Update) 算法。 ZARU 通过统计学特征判断设备是否处于绝对静止状态:
-
陀螺仪角速度的平方和
zaru_gyro_dps(默认 0.5 dps)。 -
加速度计的模长在
zaru_accel_tol_g(默认 0.08g)范围内。
当检测到静止(still == true)且持续超过确认帧数(zaru_confirm = 100帧)时,系统会将此时陀螺仪的微小输出认定为静态漂移,将其累加求均值(或通过时间常数 zaru_tau_s 进行动态逼近),并从后续的测量中扣除(gx - a->bias_x 等)。
第二部分:光标映射与 One Euro 滤波
在 pointer.c 中,系统需要把欧拉角(Yaw, Pitch)转换为屏幕上的 DX 和 DY(相对坐标)。
1. 坐标转换基础
系统设定了灵敏度参数(例如 36 count/度)。欧拉角的增量乘以灵敏度即为像素偏移。
float x = yaw_deg - p->neutral_yaw;
float y = pitch_deg - p->neutral_pitch;
为了避免由于取整造成的亚像素精度丢失,系统维护了 pend_x 和 pend_y,只输出整数部分 ox / oy,小数部分留到下一帧累加。
2. 核心挑战:生理震颤与 One Euro 滤波
人手悬空持握设备时,必然产生高频的生理震颤(Jitter)。如果直接进行线性映射,屏幕上的光标会像筛糠一样抖动。 如果使用简单的低通滤波(LPF),虽然能滤除抖动,但在用户快速挥动设备时会产生无法忍受的“拖拽感”和延迟(Lag)。
这套代码使用了 HCI(人机交互)领域极其著名的 1 Euro Filter (一欧元滤波器)。 1 Euro 滤波器本质上是一个一阶低通滤波器,其核心公式为:
平滑系数 取决于时间步长 和截止频率 :
1 Euro 的魔法在于自适应的截止频率 :
其中 是当前手部运动的导数(速度)。
-
手不动或慢速移动时:速度很小,(代码中设为 3.0 Hz)。由于截止频率极低,抖动被完美滤除,光标非常稳定。
-
手快速移动时:速度很大, 迅速增大。截止频率变高,滤波作用减弱,从而实现零延迟的跟随响应。
第三部分:极致的交互体验细节
除了纯粹的数学算法,要让空中飞鼠“好用”,pointer.c 中还包含了非常精妙的交互控制逻辑。
- 按键防抖冻结 (Click Guard)
当你按下悬空鼠标的左键或右键时,肌肉发力必然会导致设备产生不自觉的偏转。系统侦测到鼠标按键按下(Press Edge)时,会调用
pointer_freeze()强制将状态置为frozen = true,停止坐标输出。 何时解冻?
-
运动解锁:如果按住期间转动角度超过
unlock_deg(1.0度),说明用户在刻意拖拽(Drag),强制解锁。 -
时间解锁:松开按键后,经过
settle_ms(35毫秒)的延时缓冲,等待机械振动消失后解锁。
- 空中离合 (Clutch)
普通鼠标移动到鼠标垫边缘时,我们会把它“抬起来”移回中心。空中飞鼠怎么“抬”?
系统设定中键(Middle Button)为离合器(Clutch)。按下中键时,光标被完全冻结;松手时,触发
pointer_rebase,将当前的欧拉角设定为新的中心零点(Neutral Pose)。
结语
一个看似简单的空中飞鼠,其背后是深厚的信号处理与 HCI 功底。从硬件传感器的调度切换、对抗传感器物理特性的 Mahony 与 ZARU 算法,再到针对人体工学优化的 1 Euro 滤波与防抖状态机。这些优美的 C 语言数学实现,最终呈现给了用户一个如丝般顺滑的魔法指针。