第三幕:转盘联动——堵转检测与手摇跳转
转盘是这台播放器最有“物理感”的部分:播放时匀速转,捏住就暂停,暂停后手动转盘能调进度。这背后是 TurntableControl 结构体,它把 AS5600 的角度和电机指令串成一套交互逻辑。
三个工作模式
空闲(电机停)──sync_motor(true)──▶ 旋转(模拟唱片)
▲ │
└──────sync_motor(false)─────────┘
│ 堵转 400 ms
▼
打碟(手摇 seek)──松手 800 ms──▶ 恢复旋转sync_motor(playing) 是总开关:播放时写电角速度(33.3 RPM)和电压指令启动电机;暂停/停止时把电压归零,电机任务自动关 PWM、拉低使能。
堵转检测
每 50 ms 检查一次角度增量。期望增量 = 机械角速度 × 时间,实际增量低于期望的 20% 就累计堵转时间:
fn should_pause_for_stall(&mut self) -> bool {
// 启动后 800 ms 是宽限期,不检测(电机还在加速)
if self.spin_started_at.elapsed().as_millis() < TURNTABLE_START_GRACE_MS {
self.last_angle_mrad = Some(angle);
self.stall_ms = 0;
return false;
}
let actual_delta = wrap_signed_mrad(angle - last_angle).abs();
let expected_delta = RECORD_MECHANICAL_SPEED_MRAD_PER_SEC * elapsed_ms as i32 / 1000;
let threshold = expected_delta * STALL_THRESHOLD_PERCENT / 100;
if actual_delta < threshold {
self.stall_ms += elapsed_ms;
} else {
self.stall_ms = 0;
}
self.stall_ms >= STALL_PAUSE_MS // 400 ms
}连续 400 ms 增量不足 → 判定被人捏住 → 暂停播放、关电机,进入打碟模式。
角度差要经过 wrap_signed_mrad 处理:角度是 0~6283 mrad 循环的,直接相减在跨越 0 点时会算出接近一整圈的假增量。
手摇跳转
暂停后,手转的角度按比例换算成进度偏移:一整圈 = 30 秒。
fn poll_jog_seek(&mut self, progress: Option<PlaybackProgress>) -> Option<u32> {
let progress = progress?;
let angle = current_sensor_angle_mrad()?;
// 刚进入打碟模式先给 300 ms 稳定期,防止启动瞬间的抖动被当成转动
if self.jog_reset_at.elapsed().as_millis() < JOG_ARM_DELAY_MS {
self.jog_last_angle_mrad = Some(angle);
self.jog_accum_mrad = 0;
return None;
}
let delta = wrap_signed_mrad(angle - last_angle) * JOG_DIRECTION;
if delta.abs() >= JOG_ACTIVITY_MRAD {
self.last_jog_activity = Instant::now();
}
self.jog_accum_mrad += delta;
let mrad_per_second = FULL_TURN_MRAD / JOG_SECONDS_PER_TURN;
if self.jog_accum_mrad.abs() < mrad_per_second {
return None;
}
let seconds_delta = self.jog_accum_mrad / mrad_per_second;
self.jog_accum_mrad %= mrad_per_second;
let target = (progress.elapsed_seconds as i32 + seconds_delta)
.clamp(0, progress.total_seconds as i32) as u32;
Some(target)
}累积满“1 秒的毫弧度”才触发一次 seek,避免手抖产生的微小角度反复跳进度。转动方向由 JOG_DIRECTION 控制:正转快进、反转快退。
松手恢复
手转停止 800 ms 后自动恢复播放:
fn should_resume_after_jog(&mut self) -> bool {
self.jog_resume_pending
&& self.last_jog_activity.elapsed().as_millis() >= JOG_RESUME_DELAY_MS
}jog_resume_pending 在第一次触发 seek 时置位,之后只要还在转就持续刷新 last_jog_activity;停手后 800 ms 没动静,主循环调用 continue_play() 和 sync_motor(true),唱片继续转。
与任务模型的配合
整套逻辑不需要锁:AS5600 任务写原子变量,主循环读快照;电机任务读原子指令,主循环写指令。TurntableControl 只是主循环里的一个普通结构体,状态不跨任务共享,所以没有数据竞争,也没有 async 嵌套地狱。
注
堵转检测的阈值(20%、400 ms、800 ms 宽限)都是调出来的经验值。电机启动慢就放宽宽限期,手比较抖就把活动阈值调大,改常量重编译即可。
