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。
每圈主循环大致这样走:
- 传感器刷新轴角,落到
motor.shaft_angle; - 算当前轴角与"最近凹轮中心"的偏差;
- 偏差越过
snap_point→ 整数位进/退一格,凹轮中心平移一个position_width_radians; - 把偏差喂进 PID 扭矩环(边界处
P换成止动强度); - FOC 把扭矩指令翻译成三相 PWM 去驱动电机;
- 每 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 个"磁性"凹轮位 | 想要非等距吸点时填 |
调优路径(由稳到细):
- 先校准电机:跑一次自动校准,拿到方向、极对数、电角度零位,写进持久化配置;不校准后面全白搭。
- 定档宽:设
position_width_radians,先按"一格里想放多少有效位"来估。 - 给吸力:上
detent_strength_unit,边转边加,直到松手能稳稳停在档上。 - 磨切换:用
snap_point找"既干脆又不乱跳"的点,偏大更稳、偏小更灵敏。 - 场景化收尾:给不同应用存不同配置,需要回中感就加
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),仅供参考