运动语义框架
QMI8658A 只是一个来源。框架对外暴露的是语义——"敲了一下"、"拿起来了"——而不是某个寄存器的某一位。本页说明契约、仲裁规则、QMI8658A 绑定细节,以及新增来源的做法。
两个平面
一条运动输入同时携带两个平面,二者都是一等公民:
| 平面 | 内容 | 产生方 |
|---|---|---|
| 数据面 | MotionSample:原始计数、加速度 mG、角速度 dps×10、roll/pitch/yaw | 驱动 |
| 语义面 | MotionEvents:Tap / Still / TiltEnter / Shake / Lift … | QMI8658 引擎(Still)与 core 识别器(其余) |
不做"输出语义 / 输出原始数据"的二选一。Android 的 SensorEvent 与 Zephyr 的 sensor_trigger 都是两条流并存:上层要原始数据做记录、标定、滤波,语义则省掉每个产品重写一遍手势阈值。MotionBatch 把两者装在同一次投递里,消费方按需取用。
语义从哪来
- 硬件引擎:芯片自带判定器,功耗与延迟最优,参数在寄存器里。QMI8658A 如今只使能 No-Motion,
Still是出场时唯一的硬件语义。 - core 识别器:
MotionRecognizer,只看数据面,阈值全在 core,换传感器不改变业务含义。敲击、倾斜、摇动、拿起/放下、姿态全在这里——包括曾经尝试放到硬件上的Tap(见下方死路复盘)。
两者在 MotionInput::poll 里汇合,一起仲裁后再下发。
仲裁在平面内进行:软件 Tap 与软件 Shake 同属 Action family,会收敛成一个。
MotionEvent::Tap 与 GestureEvent::Tap 则分属两个平面的不同事件,不合并。前者是加速度计测到的机械敲击(可能来自外壳、被敲的桌面,甚至设备放在振动面上),后者是手指落在玻璃上,带位置和按压时长。同一个物理动作可以同时产生两者,二者都保留。
契约
MotionSource::sample(now_ms) -> Result<Option<MotionReading>, MotionError>
MotionReading { sample, hardware } 是「一次成功轮询」的结果。
Ok(None) 表示这一拍没有新帧。在 20 ms 节拍下属正常情况,不得当故障上报。
识别器的语义在 MotionInput 里叠加,MotionSource 实现不需要知道识别器的存在。
MotionEvent 的互斥按三个 family 划分:
| Family | 成员 | 优先级最高者 |
|---|---|---|
State | Still Moving Activity Step | Step |
Pose | TiltEnter TiltExit Portrait Landscape | TiltEnter / TiltExit |
Action | Tap Shake Lift Place | Shake |
同 family 的两个候选描述同一个物理动作,仲裁只留一个。跨 family 的事件允许共存(「在动」且「已倾斜」是两个同时成立的事实)。
优先级让更具体的描述胜出:摇动包含了它自己引发的倾斜抖动,拿起包含了随之而来的姿态变化。
family 冷却 300 ms,抑制同族的重复抖动。
Tap(40)低于 Shake(100)与 Lift(80),所以一次既敲又拿的动作只报后者。
MotionEvents 容量为 8,必须能装下仲裁前的并集——1 个硬件引擎加 5 个 core 分类器(tap / tilt / posture / shake / lift-place)共 6 个,仲裁后才收敛到每族至多一个。
能力声明
MotionCapabilities 是位集,声明而非开关。数据面无条件下发,所以某个来源不具备某语义时只是永不抛出,而不是关掉整条流。
能力分两层,避免把软件算出来的东西记到硬件头上:
QMI8658_CAPABILITIES— 驱动自己算的:TELEMETRYSTILL。RECOGNIZER_CAPABILITIES— core 从任意数据面推出来的:TAPTILTSHAKELIFT_PLACEPOSTURE。MOTION_CAPABILITIES— 两者并集,即该板对外宣传的能力。
板层通过 HasMotion::motion_capabilities() 声明它自己的并集,app 启动时读一次并打进日志([MOTION] declared capabilities 0x…),现场排查时能直接看出这一版固件宣称能报什么。没有加速度计的板返回 MotionCapabilities::EMPTY。
参数速查
台架标定对着下面这两张表走。参数分两层:core 识别器(纯软件阈值)与 QMI8658A 引擎(寄存器里的硬件阈值),细节分别在下面两节。
横版面板把这一页排成两块文本加一个仪表盘:第一块是各轴读数,第二块是估计量与语义计数,第三块是姿态仪的圆盘。一块放得下十八行,而各轴读数一个人就占十五行,所以估计量那四行只能挪到第二块,而不是再加第四块。
ATTITUDE 第一块:各轴读数
| 标签 | 字段 | 单位 | 意义 | 对应参数 |
|---|---|---|---|---|
A0–A2 | raw_accel | LSB | 加速度原始计数(±4 g,8192 LSB/g) | — |
M0–M2 | accel_mg | mG | 加速度,去重力前 | — |
G0–G2 | raw_gyro | LSB | 陀螺仪原始计数(±1024 °/s,64 LSB/°/s) | — |
D0–D2 | gyro_dps_x10 | 0.1 °/s | 角速度 | — |
R0–R2 | tilt_deg_x10 | 0.1° | roll / pitch / yaw(yaw 无磁力计会漂) | TILT_ENTER_DEG_X10 / TILT_EXIT_DEG_X10 |
ATTITUDE 第二块
前四行是估计量,不是原始轴值:它们是识别器判了什么,不是传感器测了什么。
| 标签 | 字段 | 单位 | 意义 | 对应参数 |
|---|---|---|---|---|
ST | status | 位图 | STATUS1(0x2F) 原值 | — |
LR | gravity_deviation_mg | mG | |测量模长 − 1 g|(静止判据的量) | STILL_GRAVITY_DEV_MG |
SR | shake_residual_mg | mG | 摇动估计残差(矢量长度) | SHAKE_ON_MG |
TR | tap_residual_mg | mG | tap 平方和(多轴线性加速度平方和)的平方根 | TAP_PEAK_MAG_MG2 |
这四行是标定专用:原始轴值里这些量按定义就是 0(静止时模长恰落在 1 g 壳上),阈值只能在设备上读。其下十四行是每个语义的累计计数。
| 标签 | 语义 | 触发条件 | 本板 |
|---|---|---|---|
TAP | 单击 | 识别器上报 Tap{count},count=1 | 计数 |
2T | 双击 | 同上,count=2 | 计数 |
3T | 三连击 | 同上,count=3(封顶) | 计数 |
STL | 静止 | No-Motion 引擎(三轴 AND,上升沿) | 计数 |
MOV | 移动 | 未宣告 | N/A |
ACT | 活动 | 未宣告 | N/A |
STP | 计步 | 未宣告 | N/A |
TIN | 倾斜进入 | 15° 带符号 | 计数 |
TOX | 倾斜退出 | 10° 迟滞 | 计数 |
SHK | 摇动 | 残差 > 2000 mG 且 800 ms 内 3 次上穿 | 计数 |
LFT | 拿起 | 模长离开 [850, 1150] mG 带(对 1 g 偏离 > 150 mG)持续 150 ms | 计数 |
PLC | 放下 | 模长回到 [850, 1150] mG 带持续 150 ms | 计数 |
PRT | 竖屏 | X/Y 水平分量中较大者 ≥ 300 mG,且 Y 为主 | 计数 |
LND | 横屏 | 同上但 X 为主 | 计数 |
N/A 与 0 的区别是刻意的:0 意味着阈值没触发(要调),N/A 意味着这块板根本不上报(不用调)。计数为 u16 饱和累计、每 boot 归零、无清零入口。
识别器阈值
全部在 recognizer.rs,不在驱动里。
| 语义 | 判据 | 参数 |
|---|---|---|
TiltEnter / TiltExit | 15° 进入 / 10° 退出,带符号迟滞 | — |
Tap | 残差越过峰阈且上一拍在安静阈下,峰须在预算内跌回并待满 | 见下 |
Shake | 残差矢量长度 > 2000 mG,800 ms 内 3 次上穿 | 见下 |
Lift / Place | 测量模长偏离 1 g 超过 150 mG,持续 150 ms | 见下 |
Portrait / Landscape | 两轴水平分量都低于 300 mG 时无有意义姿态,两个都不报 | — |
带符号迟滞是必需的:单阈值会在边界每拍抖出一对进出。
Tap
去重力用逐轴 300 ms 移动平均(TAP_BASELINE_TAU_MS),但基线只在静止相位 blend。
手势(Peak/Quiet/Between)期间基线冻结。否则连续几下敲击会被基线吸收,过冲把「安静读数」抬高,饿死后面打击的 80 ms 峰值预算:探针实测三连击能把残差地板推到约 155 mG,而安静阈正卡在这个量级。
判据取三轴线性加速度平方和 Σlin² 直取,不做能量平滑——要量的是残差自己在峰预算内跌回安静阈的速度。
瞬时平方越过 250 mG 峰阈(TAP_PEAK_MAG_MG2 = 62_500 mG²)且上一拍低于 150 mG 安静阈(TAP_QUIET_MG2 = 22_500 mG²),才算打击上升沿。
这样一条上升沿筛掉两类假阳性。敲击是从安静一步跨到峰阈;转动的残差逐拍爬过两条阈值,永远凑不出这一跳。上一拍不在安静下的升也进不来——缓变的拿起顶到 320 mG、转动持续越线,都是。
进入峰阶段后是衰减预算。残差必须在 80 ms(TAP_PEAK_WINDOW_MS)内跌回安静阈之下,峰才成立。
预算耗尽还挂在高位就是按压或拿起(拿起在高位撑约 1 s,任何 80 ms 窗口都满足不了)、或转动,整段丢弃。成立的峰还要在安静阈下完整待满 80 ms(TAP_QUIET_WINDOW_MS)才算一记,一次敲击的余振不算第二记。
第一记生效瞬间记下锚点,之后 500 ms(TAP_DOUBLE_WINDOW_MS)内再来的打击都滚进同一次 tap,再次走峰→安静→生效,计数 1/2/3 封顶。窗结束时按 count 上报一次 Tap,单击因此天然滞后 500 ms 才定论。
最后的收尾判据是落定预算(TAP_SETTLED_MOTION_MS = 200 ms):自第一记起算,信号在安静阈之上的累计时长超过 200 ms,说明手势没有收住。
敲击后的余振会安静待在阈下,转动则持续越过又返回,所以当场丢弃不报。转动的启动瞬变在局部上与敲击同形——硬件引擎恰恰锁死在那类瞬变上——只有「整个手势窗是否收在安静阈下」能分开二者。
阈值在 mG² 域,TR 公布其平方根(峰阈约 250 mG)。
已上机标定(探针打印逐拍残差流,50 Hz):静止噪声 9–38 mG,250 的峰阈留约七倍余量;一次较用力敲击是 1–2 个样本的 364–4866 mG 瞬变(±4 g 量程剪顶),普通日常轻敲也能跨峰阈。
拿起-停顿-放回的缓慢上升在 50–350 mG 持续约 1 s,抬手一步最大约 320 mG。它能跨峰阈,但持续高位撑不过 80 ms 衰减预算。
偶发桌面耦合单样本杂散 300–988 mG 与真实敲击同形,判为敲击(可接受)。
event.rs 把 Tap 归入 Action family(priority 40),摇动(100)与拿起(80)同族可压过它。
Shake
判据是重复次数而不是持续时长。
敲桌与摇动的区别不在单次幅度而在有没有第二次:一次冲量过线只有一次上穿,一次摇动每个反转各有一次。
不能用「超阈值累计时长、跌破则等量衰减」的漏积分。正弦在一个周期里高于自身峰值附近阈值的时间占比最多只有一半(实测本配置下 5 Hz 约 10%),等量衰减后每周期净增量恒为负,任何振荡都永远攒不够确认窗。
阈值取自 W3C《Accelerometer》的参照实现(shakeThreshold = 25 m/s² ≈ 2.5 g)。该规范是加速度计规范,不是同套里的《Device Orientation and Motion》。
有两处下修。识别器每 20 ms 才看一次信号,贴着峰值的阈值会整拍漏掉;而 3 g 的摇动在 5 Hz 上只剩约 2.5 g 残差,2.5 g 恰在信号顶端。2.0 g 是能让每个反转留下 2–3 个超阈样本的值。
W3C 的例子是 60 Hz、只看单轴 sensor.x、单拍阈值;矢量长度、需 3 次过阈、限 800 ms 都是本产品的设计而非规范要求。尚未上机标定。
Lift / Place
测量模长偏离 1 g 超过 150 mG 视为静止被打断,持续 150 ms 后相信。判据在平方域比较(850² < x²+y²+z² < 1150²),纯整数无开方。
用模长方向是不背估计误差的:旧判据减去的是自身重力估计,估计漂多少判据就跟着漂多少,桌面可能离自己估计 100 mG。原始模长只受加速度计本底噪声影响。
150 mG 取自 ST 官方 FSM pick-up 示例(LSM6DSO:上桌判 0.9 g、拿起逃逸 1.1 g,即拿起只需对重力偏出 100 mG)。台架静置的模长抖动只有 8–22 mG,150 mG 留了约七倍裕量。
更宽的 350 mG 带在台架上只有甩起来(大幅加速)才越线,普通拿起被漏掉。
纯转动(翻转、拿起而不加力)不改变模长,仍判静止。这是 ST 模长逃逸法共有的盲区,不是本实现独有的。检测的是进出静止的跳变,并不声称能分辨桌面与手掌。
Portrait / Landscape
两轴水平分量都低于 300 mG 视为平躺,此时无有意义姿态,两个都不报。
去重力估计
去重力用一阶低通,写成时间常数而非每拍系数,所以轮询频率变化时拐角不动。Shake 与 Tap 各有一个估计,时间常数不同,互不干扰。
| 估计 | 时间常数 | 20 ms 轮询下的拐角 | 谁在用 |
|---|---|---|---|
| 摇动估计 | 70 ms | ≈ 2.0 Hz | Shake |
| Tap 基线 | 300 ms | ≈ 0.5 Hz | Tap |
首个有效样本定义当前朝向而不是被拿去和朝向相减——否则开机瞬间正在被拿起的设备会凭空多出一个冲量。Lift/Place 不做去重力,判据是原始模长(见上表),不依赖估计。
Shake 用残差的矢量长度,不是三轴求和。
三轴加起来等于整流,多轴振荡整流之后在反转之间回不到零,会一直挂在阈值之上,根本没有上穿可数。矢量长度在反转处归零,才有干净的边沿。
调参时 SHAKE_ON_MG 与 LFT/PLC 的 150 mG 带宽互不相干,可分别标定。
传感器 ODR 是 896.8 Hz(6DOF 模式由陀螺仪决定,见下),core 每 20 ms(50 Hz)轮询一次,也就是每 18 帧只取 1 帧。
5–10 Hz 的摇动在 50 Hz 下每振荡仍有 5–10 个样本,所以数反转数得上来。
但反转的宽度是按角度算的。一次上穿要能被采到,阈值必须低到让该段相位跨度超过一拍——这也是 SHAKE_ON_MG 从 2.5 g 下修到 2.0 g 的原因之一。
代价是 40 ms 轮询与 20 ms 轮询会在不同时刻报出同一次摇动(粗轮询可能整个漏掉一个波峰、多等一个周期凑够次数)。这是采样决定的,不是时间基准的问题。
敲击判据同样跑在这条 20 ms 平面上,与 ODR 无关。896.8 Hz 的高 ODR 只服务于 No-Motion 引擎的样本计窗与陀螺 yaw 诊断(见下)。
yaw 是积分量,无磁力计会无界漂移,仅作诊断显示,任何语义与业务判断都不得读它。
改用模长与去重力残差判据之后,没有任何语义再来自陀螺仪。它仍留在 MotionSample 里做遥测与屏幕上的 G 显示,驱动也照常读取,但 SHAKE、TAP 与 LIFT_PLACE 都只看加速度。
之所以仍使能陀螺仪,一半是遥测价值,一半是 6DOF 模式的 ODR 由陀螺仪决定:关掉它会落到加速度计单轴那一列(1000 Hz 而非 896.8 Hz),把上面 No-Motion 的窗口换算挪动。
QMI8658A 绑定
S3 板未接 INT1/INT2,所以全部靠轮询。由此产生三条硬约束:
-
CTRL8 bit7 必须置 1。它把 CTRL9 命令的完成信号从 INT1 改道到
STATUSINT.bit7,没有它就只能盲等。改道后CTRL8 = 0x84(No-Motion + 握手选择)。 -
参数配置期间 CTRL7 必须为 0。数据手册要求
aEN = gEN = 0,因此顺序是:写 CTRL1/2/3/5 →CTRL7 = 0→ 两次 CAL 写入与 CTRL9 命令(Motion 引擎的两半) →CTRL8 = 0x84→CTRL7 = 0x03。先装参数再开引擎,否则引擎会带着复位默认值跑。 -
握手必须有界,但按墙钟计。
await_cmd_done最多 1000 次、每次间隔 1 ms,失败返回EngineConfig,不能挂住输入循环——那上面还挂着触摸与按键扫描。用墙钟而不是紧凑轮询,是因为
CmdDone是粘滞标志:紧凑轮询会在器件还没算完时就放弃,而这里一次误判超时会让整个运动平面永久失效。手册没给
CmdDone的最坏时间,所以预算取自唯一公布了该项的独立实现(RIOT 用 1000 ms 命令超时)。这里放宽是零成本的,因为它只在init()里跑一次开机流程,正常路径第一次轮询就返回。
CTRL5 显式写 0x00,即 aLPF_EN 与 gLPF_EN 全关。手册表 22 中 bit4 是 gLPF_EN、bit0 是 aLPF_EN,aLPF_MODE(2:1) 为 00 即 2.66% of ODR。
加速度计低通关,保留 896.8 Hz 全带宽。曾怀疑 24 Hz aLPF 是磕击瞬变衰减的主因,上机对照(同手势、关/开 aLPF 各测一轮)证明静止噪声与敲击读数纹丝不变——衰减来自结构与耦合,不来自这条低通。关掉它只是清掉嫌疑、给敲击瞬变留全带宽,没有任何可测的代价。
陀螺仪低通保持关(bit4=0):没有语义读陀螺通路找峰。
ODR 与窗口换算:CTRL2 = 0x13、CTRL3 = 0x53,ODR 索引 0b0011。
手册表 22 的加速度计列读作 1000 Hz,但 6DOF 列是 896.8 Hz,而注 13 说明两个传感器都使能时实际生效的是 6DOF 那一列。
ODR 已经不再为敲击服务——敲击判据在 core 的 20 ms 平面上跑、与 ODR 无关——但 896.8 Hz 仍稳超 No-Motion 引擎的需求,且保持陀螺 yaw 诊断全程率。
No-Motion 引擎按样本计窗、产品按毫秒思考。窗口在代码里以毫秒表述、再换算成样本数(odr_samples),并钉在寄存器值上——改一次 ODR 就会静默缩放所有窗口。当前 80 ms → 72 样本。
每个引擎的参数分两半,由 CAL4_H 高半字节选择(0x01 第一半,0x02 第二半),Motion 引擎两次 CTRL9 命令。写入用一次从 CAL1_L 起的 8 字节突发(CAL1_L、1_H、2_L、2_H、3_L、3_H、4_L、4_H)。两半装的东西并不连续,且 16 位参数横跨一对寄存器,所以逐字节按表 34(Motion)填:
| 寄存器 | Motion 第一半 | Motion 第二半 |
|---|---|---|
| CAL1_L | AnyMotionXThr | AnyMotionWindow |
| CAL1_H | AnyMotionYThr | NoMotionWindow |
| CAL2_L | AnyMotionZThr | SigMotionWait[7:0] |
| CAL2_H | NoMotionXThr | SigMotionWait[15:8] |
| CAL3_L | NoMotionYThr | SigMotionConfirm[7:0] |
| CAL3_H | NoMotionZThr | SigMotionConfirm[15:8] |
| CAL4_L | MOTION_MODE_CTRL | NA |
| CAL4_H | 0x01 | 0x02 |
这类交错填法很容易把参数填错位而不自知,所以 init_pushes_both_parameter_sets_of_the_no_motion_engine 直接钉死全部 16 个字节,对着表逐行核。
状态映射只剩一路:STATUS1.bit6 → Still。
这是电平不是事件。No-Motion 引擎只要仍认为设备静止,每一拍都置位,直接上抛会把一次落定变成帧率级的 Still 连发(台架观察到 STL 无上限自增)。驱动只在 0→1 上升沿上报一次,位保持则静默,回落再置位是一次新的上报。
Tap 位(bit1)被明确忽略、不解析。它可能仍被硅片引擎的锁死瞬变置位,但驱动不读其结果寄存器、更不把它当事件——a_tap_bit_is_no_longer_decoded 钉住的是「tap 位既不产生事件、也不转化为额外 I²C 读」。
握手靠 STATUSINT.bit7,命令后须回写 CTRL9 = 0x00 ACK。
复位必须握手,不能盲等。7.4 说复位最长 15 ms,而 5.9/7.4 都要求复位后立即读 0x4D:0x80 表示复位成功(power-on 与 soft reset 都会置位),且该位会被后续操作覆盖,所以读完就得走。
驱动改为轮询 0x4D(最多 100 次 × 200 µs = 20 ms)而不是固定延时,失败返回 ResetIncomplete 且不写任何配置寄存器。
手册对复位字节自相矛盾:表 27 说写 0xB0,7.4 正文说写 0x0B,互为比特反转。这里取表 27 的 0xB0,而 0x4D 握手正好把这个歧义变成可检出的结果——字节错了就不会置位,直接报 ResetIncomplete,而不是静默配置失败。
STATUS1 位清理语义,手册只写了一个。§12.4 的原句是 "Reading STATUS1 by the host will clear the WoM bit",说的是 bit2 的唤醒标志,不是 No-Motion(bit6)或 Tap(bit1)——位是否自清属于未定义行为。
驱动不猜,只在上升沿上报、把真实 STATUS1 随帧带上,让现场用 ST 配左列标定行验证一次物理动作对应几次上报。
台架读数(bit6 静置期持续置位)与「电平、自清」一致,所以边沿判据 1:1 还原动作次数。
注意别去读 STATUSINT 判数据就绪:syncSmpl=0 下它的 bit0 是 INT2 电平、bit1 是 INT1 电平,只有 bit7 的 CmdDone 有意义。数据就绪必须读 STATUS0(bit0=aDA,bit1=gDA)。
配置失败后驱动不自愈:init 是唯一的配置入口,轮询在未初始化时返回 MotionError::NotReady。S3 板把运动当作可选传感器,init 失败只打一条日志并继续启动,设备照常工作、只是没有运动平面;NotReady 经限流后不会刷屏。
本产品只开 No-Motion;Tap 引擎故意不使能(见下),CTRL8 的 Any-Motion / Significant-Motion / Pedometer 位保持为 0,那些语义归 core 识别器。
NoMotionX/Y/ZThr 步长 0.03125 g(当前 0x08 = 0.25 g 斜率),NoMotionWindow 以样本计(80 ms → 72)。
MOTION_MODE_CTRL = 0xF7 中 bit0-2 使能 AnyMotion 三轴、bit4-6 使能 NoMotion 三轴,bit7 决定引擎把已使能轴 AND 还是 OR。
取 0xF7(三轴全开 + AND)是「静止」的严格读法:三轴都必须在整个窗口内低于各自的斜率阈值。
0x07 的 OR 逻辑只要有一轴落定就报静止,设备正在转动时另一轴稳住就能误报——这正是 STL 早先偏多的一个来源。
AnyMotionX/Y/ZThr = 0x00 是占位值而非选定的阈值。真实的 0 会让引擎对任何偏差都触发,而这正是它保持未使能、并由测试钉住的原因。
敲击不走硅片引擎
QMI8658A 的 tap 引擎在本产品的轮询方案下不可达,敲击判据由 core 的 20 ms 流承担(见上文「识别器阈值」的 Tap),驱动只保留 No-Motion、不声明 TAP 能力。
ODR = 896.8 Hz 不是为敲击保留的,而是给 No-Motion 引擎的样本计窗与陀螺 yaw 诊断用。
排查过程、寄存器读数与两次被推翻的假设见运动实现记录。
节拍与 I²C 预算
MOTION_SCAN_MS = 20。所有来源共用 app 的 5 ms 基础 tick,即每 4 拍一次:快到不漏掉敲击和拿起,慢到给共享 I²C 留出触摸与按键。顺序轮询下单次 poll 约 0.4 ms(STATUS1 1 字节 + STATUS0 1 字节 + 12 字节数据),20 ms 有充足余量。读取无忙等。
发射做增量抑制:加速度变化 > 20 mG 或角速度 > 5 dps 才下发。但识别器每拍都看到数据,静帧不会因此漏判。
诊断显示
右列是每个语义的累计计数表(标签与判据见参数速查)。
TAP/2T/3T 由 Tap{count} 的 count=1/2/3 分别触发,都计(count 封顶 3,第三记之后的手势即三连击)。当 count=3 也取得到之后,3T 不再像芯片时代那样恒 0。TIN/TOX 把四个方向的进入/退出合并计数。
本板未宣告的能力显示 N/A(当前 S3 宣告 0x3CB,不含 STP MOV ACT)——RECOGNIZER_CAPABILITIES 与驱动能力都没启用它们,不是阈值问题。
计数是 u16 饱和累计、每 boot 归零、没有清零入口(与触摸计数器语义一致)。选 u16 而非触摸侧的 u8 是因为摇动在 300 ms 不应期下约 3 次/秒,255 不到一分半就溢出,不够标定。
一拍里仲裁后可能同时带着多个跨 family 的事件(摇动和它引发的倾斜同批抵达),这些全部计数而非只留优先级最高的一个,否则倾斜会从统计里消失。
新增来源
- 在
iot-bsp-esp写一个只依赖embedded_hal的驱动,实现MotionSource。 - 返回
MotionReading,硬件引擎的判定填进hardware,不要在驱动里跑 core 阈值。 - 声明该驱动自身的能力并集;core 派生的那部分不重复声明。
- 板层
take_motion挂上MotionInput::new(driver)。
业务映射是另一层。InputEvent::Motion 目前映射为 BusinessIntent::Invalid——框架已就位,把语义接到具体产品意图是业务决策,不在驱动层做。
多 IMU 时按来源独立采样、独立健康检查、独立识别;每个来源一份 MotionCapabilities。50 Hz 语义总线不适合飞控/平衡车那类需要高率惯性流的场景,那是另一条路。
测试
moon run iot:test # core:契约、识别器、仲裁、状态机
moon run iot:test-bsp # 驱动:寄存器序列、握手、状态映射
test-bsp 依赖 Cargo.toml 里对 esp-hal 的 target 隔离(cfg(target_os = "none"))。QMI8658A 与 PCA9557 源码只依赖 embedded_hal,因此把这层 ESP 依赖挡在裸机目标之外,就能在工作机上直接跑驱动单测,不必为芯片 HAL 付出构建代价,也不需要新建 crate。
驱动测试覆盖:使能顺序(引擎必须在全部 CTRL9 命令之后才开)、两次参数集写入内容、握手超时不挂死、tap 位不再解码、引擎事件在数据面未更新时仍上报、身份不匹配、量程换算、yaw 环绕与步长上界、能力归属。