ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SmartKnob 力反馈旋钮:用软件定义止动与虚拟凹轮,10 分钟调出你要的阻尼手感

SmartKnob 力反馈旋钮:用软件定义止动与虚拟凹轮,10 分钟调出你要的阻尼手感

SmartKnob 力反馈旋钮:用软件定义止动与虚拟凹轮,10 分钟调出你要的阻尼手感

【免费下载链接】smartknobHaptic input knob with software-defined endstops and virtual detents项目地址: https://gitcode.com/gh_mirrors/smar/smartknob

想要"咔哒咔哒"的段落手感,却只能靠齿轮硬凑?旋钮拧到边界那声闷响,能不能变成一段越拧越软的阻尼墙?

SmartKnob 是款力反馈旋钮:它把"止动"和"凹轮咔哒感"从机械结构里搬进代码,用无刷云台电机配磁编码器做闭环扭矩控制,让止动边界和虚拟凹轮全部由软件定义——手感从此是可调参数,不是焊死的零件。

花十分钟,你能做到:

  • 看懂虚拟凹轮是怎么靠"角度差"算出来的
  • 自己调position_width_radians、detent_strength_unit把阻尼拧到舒服
  • 用snap_point解决"切换不干脆"或"位置乱跳"
  • 对照排查嗡嗡声、空转卡顿、边界漂移这几类典型毛病

它凭什么不同:软件定义止动 vs 机械齿轮的三段差距

传统旋钮的手感被齿轮结构锁死:齿距是固定的,想改"多密一档"就得换齿轮,想改"边界软硬"几乎无解。SmartKnob 把这三件事都交给固件。

  • 时间线/倍速场景:普通旋钮只能在固定齿距上停;这里可以只让 1x、2x、4x 各有一个吸点,中间完全顺滑,非等距凹轮直接写进配置。
  • 边界手感:从"啪"地撞死,改成随角度渐弱的"弹簧墙",止动强度是个独立旋钮(endstop_strength_unit),和凹轮强度分开调。
  • 手感随场景变:同一颗旋钮,切个配置就能在"精密微调"和"快速扫档"之间切换,不用动任何硬件。⚡

系统全景:电机控制任务与状态机是怎么分工的

固件是几个 FreeRTOS 任务,各司其职。核心在 motor_task.cpp,它持有 SimpleFOC(FOC 磁场定向控制库)的BLDCMotor实例,负责扭矩环和凹轮状态机。

  • MotorTask:读编码器 → 算凹轮位 → 出扭矩,是力反馈的心脏。
  • InterfaceTask(interface_task.cpp):走 USB 串口,支持明文与 protobuf 两种协议,负责下发配置、上送状态。
  • DisplayTask:驱动旋钮内那块圆形 LCD,显示当前档位。
  • Configuration(configuration.cpp):用 FFat 把电机/应变校准值存盘,断电不丢。
  • 传感器:默认推荐 MT6701(SSI 带 CRC),也兼容 TLV493D、MAQ430。

每圈主循环大致这样走:

  1. 传感器刷新轴角,落到motor.shaft_angle;
  2. 算当前轴角与"最近凹轮中心"的偏差;
  3. 偏差越过snap_point→ 整数位进/退一格,凹轮中心平移一个position_width_radians;
  4. 把偏差喂进 PID 扭矩环(边界处P换成止动强度);
  5. FOC 把扭矩指令翻译成三相 PWM 去驱动电机;
  6. 每 5ms 把最新状态推给显示和主机侧。

体验的来源:虚拟凹轮与扭矩闭环的代码级拆解

先搭好扭矩闭环,后面所有"手感"才有落脚点。电机一初始化就切到扭矩控制模式,PID 参数直接来自 mad2804.h 的宏:

motor.controller = MotionControlType::torque; motor.voltage_limit = FOC_VOLTAGE_LIMIT; motor.linkSensor(&encoder); motor.PID_velocity.P = FOC_PID_P; // 1 motor.PID_velocity.I = FOC_PID_I; // 0 motor.PID_velocity.D = FOC_PID_D; // 0.148 motor.PID_velocity.output_ramp = FOC_PID_OUTPUT_RAMP; // 5000 motor.PID_velocity.limit = FOC_PID_LIMIT; // 3

这里借用 SimpleFOC 的"速度环 PID 变量"来跑扭矩,好处是用 SimpleFOC Studio 就能串口实时改参。P 负责把偏差变回中心,D 抑制高速时的过冲,output_ramp限住扭矩跳变速度,limit封顶最大输出。

虚拟凹轮的"吸附",本质是维护一个"凹轮中心"并和它比距离,偏差一旦越线就平移中心、整数位走一格:

// 轴当前角度 与 最近凹轮中心 的偏差 float angle_to_detent_center = motor.shaft_angle - current_detent_center; float snap_point_radians = config.position_width_radians * config.snap_point; if (angle_to_detent_center > snap_point_radians && current_position > config.min_position) { current_detent_center += config.position_width_radians; // 凹轮中心前移一格 current_position--; } else if (angle_to_detent_center < -snap_point_radians && current_position < config.max_position) { current_detent_center -= config.position_width_radians; current_position++; }

snap_point是"越过多少比例才算切档",它同时给了迟滞:要越过半个档宽才换挡,松手时又被拉回中心,这就是"咔哒"的来源。真实代码在此基础上还叠加了snap_point_bias,用来做"非对称"凹轮。

真正让不同档宽都不吵的,是 D 项随凹轮宽度自适应——细档给大 D、粗档给小 D,分三段线性插值:

const float derivative_lower_strength = config.detent_strength_unit * 0.08; const float derivative_upper_strength = config.detent_strength_unit * 0.02; const float derivative_position_width_lower = radians(3); const float derivative_position_width_upper = radians(8); const float raw = derivative_lower_strength + (derivative_upper_strength - derivative_lower_strength) / (derivative_position_width_upper - derivative_position_width_lower) * (config.position_width_radians - derivative_position_width_lower); motor.PID_velocity.D = config.detent_positions_count > 0 ? 0 : CLAMP(raw, min(derivative_lower_strength, derivative_upper_strength), max(derivative_lower_strength, derivative_upper_strength));

意思是:3° 的细档拿到 0.08×强度 的 D,8° 的粗档只拿 0.02×强度,中间线性过渡。D 太大粗档会放大传感器噪声变成噪声,D 太小细档又"咔"不出来,这段逻辑就是在两头之间自动找平衡;一旦启用了自定义凹轮位(detent_positions),就干脆把 D 归零,避免多重"咔哒"打架。

最后是个顺手的小技巧——触觉点击,用一段"推一下再拉回来"的扭矩脉冲模拟按压:

case CommandType::HAPTIC: { float strength = command.data.haptic.press ? 5 : 1.5; // 按下更强 motor.move(strength); for (uint8_t i = 0; i < 3; i++) { motor.loopFOC(); delay(1); } motor.move(-strength); for (uint8_t i = 0; i < 3; i++) { motor.loopFOC(); delay(1); } motor.move(0); break; }

按下比松开强(5 vs 1.5),两三次loopFOC的短爆发就是一次"哒"。它独立于凹轮逻辑,适合在触发某个事件时给主机侧一个明确的触觉确认。

调优路线:position_width_radians 与 detent_strength_unit 起步取值表

手感的旋钮字段都来自 smartknob.proto 里的SmartKnobConfig,下面是建议的起手值:

字段它管什么建议起手值
position_width_radians相邻凹轮间的角度宽度(弧度)精细 3°–5°,快切 10°–15°
detent_strength_unit凹轮吸力强度,0 表示关0.5–1.0,超过 1 易不稳
endstop_strength_unit边界止动扭矩,0 关0.5–1.0,约凹轮的 1.5–2×
snap_point越线才换挡的滞后阈值0.5–1.1,固件强制 ≥0.5
snap_point_bias非对称凹轮偏置(松手易回中)默认 0
detent_positions最多 5 个"磁性"凹轮位想要非等距吸点时填

调优路径(由稳到细):

  1. 先校准电机:跑一次自动校准,拿到方向、极对数、电角度零位,写进持久化配置;不校准后面全白搭。
  2. 定档宽:设position_width_radians,先按"一格里想放多少有效位"来估。
  3. 给吸力:上detent_strength_unit,边转边加,直到松手能稳稳停在档上。
  4. 磨切换:用snap_point找"既干脆又不乱跳"的点,偏大更稳、偏小更灵敏。
  5. 场景化收尾:给不同应用存不同配置,需要回中感就加snap_point_bias。🔧

想玩更野的?detent_positions让你只给指定档位加吸点,做出对数刻度、音量"重点位"这类非线性手感;配合snap_point_bias还能做"按住松手会滑回原点"的回归键。

排错手册:嗡嗡声、空转卡顿与边界漂移对照排查

1. 现象:凹轮强度调很低时,空转也有"咔哒"卡顿原因:电机 cogging(齿槽效应,不通电也想停在某些角度)太大,盖过了软件凹轮。 解决:换低 cogging 的 32mm 空心轴无刷云台电机;cogging 严重时低强度/细档会直接被机械"咬合"干扰。

2. 现象:旋转时发出嗡嗡/蜂鸣声原因:D 项放大了磁编码器的噪声,或滤波不够。 解决:调大FOC_LPF(mad2804.h 中默认 0.0075)平滑输出;必要时降低 D;优先用带 CRC 的 MT6701(SSI)降低数据毛刺。

3. 现象:位置在边界或某档附近来回乱跳原因:snap_point太小,迟滞不足,抖动就能触发换挡。 解决:固件已强制snap_point ≥ 0.5,把它往上调(0.7–1.1)加迟滞;确认detent_strength_unit没有超过 1。

4. 现象:松手后旋钮不回凹轮中心、慢慢漂走原因:未校准或校准数据不准,"凹轮中心"跟真实位置对不上。 解决:重跑电机校准;注意主循环在高速(>60 rad/s)时会主动撤掉扭矩防止失控,所以"越转越飘"往往不是控制环的锅,而是零位/方向没对准。

延伸与获取:固件入口、协议文件与示例代码位置

  • 任务与启动顺序看 firmware/src/main.cpp:Display、Motor、Interface 三个任务在这里串起来。
  • 力反馈核心逻辑在 firmware/src/motor_task.cpp,FOC 参数在 firmware/src/motors/。
  • 主机↔旋钮协议字段(min/max_position、detent_positions等)定义在 proto/smartknob.proto。
  • 想动手连一台:Python 示例 software/python/simple_example.py 直接改配置发下去;Web/Node 侧在 software/js/packages/ 里有 core、webserial、示例 CLI。
  • 拉完整工程:git clone https://gitcode.com/gh_mirrors/smar/smartknob。

把"手感"当成一组可下发、可回读、可随场景切换的参数,旋钮就不再是被动元件,而是一块可以重写的输入介质。🎯 下一站,或许就是把这套力反馈接到你的时间线、播放器或合成器里——让它替你的手指"记住"哪里该停。

【免费下载链接】smartknobHaptic input knob with software-defined endstops and virtual detents项目地址: https://gitcode.com/gh_mirrors/smar/smartknob

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进