做“机器人被挠脚心”这个项目起因其实挺简单我想验证一个小型机器人对“突发触摸”能做出多自然的反应。这个项目在我自己整理的《FM及机器人系列》里属于TK触觉互动测试专题后来做着做着发现它已经不只是一个整蛊玩具而是一套完整的“触觉感知 运动反馈 状态管理”机器人交互原型。很多做陪伴机器人、展览机器人、教育机器人的人最后都会遇到同一个问题机器人太“木”了摸它没反应或者反应延迟明显体验非常差。而“挠脚心”恰好是触发快速自然反应的极佳测试场景我做完这套之后很多做ROS开发的同事也拿它当入门练手项目。这个项目最适合两类人一类是刚开始学机器人控制、ROS系统想找一个有趣又不枯燥的载体来练习的人另一类是已经被课程里的正运动学、逆运动学搞得有点疲惫的初学者想换个更有成就感的角度理解传感器和执行器如何协同工作。我会从硬件选型、传感器原理、状态机设计、ROS 2集成到完整实操和常见问题把整个项目展开来讲尽量让你能直接“抄作业”。1. 整体设计思路先想清楚“怕痒”这个动作需要什么1.1 为什么选择“挠脚心”作为交互场景传统机器人给人的印象是“被动执行”你按按钮它才动你发指令它才做。但这种模式离“人机自然交互”非常远。这几年我接触过的服务机器人、导览机器人项目客户几乎都会提一个需求能不能让机器人更有“人情味”其中最容易出效果的一招就是让它对自己的身体被触碰产生反应。微软的小冰、优必选的Walker等产品也都做过“怕痒”“怕摸”这类演示目的就是拉近机器人和用户的距离。“挠脚心”是触发人类自然反应最强的动作之一换到机器人身上它天然具备三个测试价值。首先脚底是机器人身体上相对隐蔽、区域明确的部位传感器安装不会破坏整体外观。其次触碰脚底的力度差异很大从轻轻拂过到用力按压能很好地测试传感器量程和阈值设计。最关键的一点是“缩脚”“抖动”“发出声音”这一整套反应在观感上充满喜剧色彩非常适合在课堂、展会、活动上做演示容易调动气氛也让学习者更有动力把项目调好。从本质上看这个项目要解决的三个技术问题很清晰脚底受到触碰时传感器如何快速准确感知感知到信号后机器人如何选择并执行“反应动作”整个过程的延迟要控制在多短才能让交互显得自然。这三个问题分别对应硬件层、控制层和交互层恰好是机器人开发的三个基本切面。1.2 整体方案选型分层设计避免一锅烩我在设计这个项目时没有选择“一块板子搞定所有事”的做法而是把系统分成了感知层、控制层、表现层三层。感知层用压敏电阻FSR检测脚底压力控制层用一块ESP32作为主控负责采集信号、运行状态机、输出控制指令表现层由舵机、振动马达和音频模块组成分别实现“缩脚”“颤抖”“笑/求饶”三种反馈。之所以分层是因为这个项目后续有很强的扩展性。如果你只想做一个整蛊玩具用一块Arduino Nano就能完成全部工作但如果你想把项目接入ROS 2或者把“挠痒反应”做成整个机器人交互系统的一个子系统就必须把感知、决策、动作分开。我自己刚开始也是图省事把代码全写在一个loop里后来加了ROS接口之后不得不重写了一遍教训很深刻。硬件选型上我最终选用了ESP32而非Arduino Uno原因是ESP32的ADC精度更高、支持Wi-Fi/蓝牙而且双核处理器可以一个核跑传感器采样、另一个核跑状态机。不过要注意ESP32的ADC有非线性问题后面我会专门讲怎么校准。执行器方面脚部缩回动作用了SG90舵机但经过几次测试后我强烈建议换成MG90S或MG996R因为SG90在连续抖动时很容易发热甚至烧毁。振动马达用的是常见的手机扁平马达通过一个S8050三极管驱动音频部分用了DFPlayer Mini播放MP3音效比直接用蜂鸣器逼真得多。2. 触觉感知是怎么实现的FSR传感器原理与安装细节2.1 FSR压敏电阻的工作原理FSRForce Sensing Resistor压敏电阻是这个项目感知层的主角。它的工作原理很简单当压力施加在传感器有效区域上时内部导电粒子和电极之间的接触面积增加导致电阻值下降。没有压力时它的电阻通常在1MΩ以上随着压力增大可以降到几kΩ甚至几百Ω。要让ESP32读取这个电阻变化不能直接把FSR接到ADC引脚因为ADC读的是电压而不是电阻。标准做法是接一个分压电路把FSR和一个固定电阻串联中间抽头接到ADC输入。这里有个非常容易踩的坑很多人以为固定电阻的阻值选得越大越好其实不是。固定电阻阻值决定了电路的灵敏度范围选得太大轻轻一碰ADC就到满量程后面没法区分“轻触”和“重压”选得太小重压时电压变化不明显分辨率又不够。我最终选的固定电阻是10kΩ这个值在大多数场景下能兼顾。举个例子我用的FSR型号是Interlink FSR402它在轻触时阻值约为20kΩ重压时约2kΩ。用10kΩ固定电阻做分压时轻触状态ADC口电压大概是3.3 * 10 / (20 10) 1.1V重压状态大概是3.3 * 10 / (2 10) 2.75V整个区间接近1.65V非常利于分辨率分配。如果你用的是其他型号FSR可以先用万用表测一下轻触和重压下的实际阻值再根据公式R_fixed sqrt(R_light * R_heavy)估算最佳固定电阻这个公式来自分压电路的最大灵敏度条件我实测下来比较可靠。2.2 脚底结构改造与传感器固定FSR传感器虽然叫“压敏电阻”但它对安装方式非常挑剔。最理想的状态是FSR放置在一个平整坚硬的平面上上方覆盖一层薄而柔软的压力传递层。如果直接贴在软海绵上FSR会因为基材弯曲而产生非线性输出如果上方压着一块硬塑料压力又会集中在某一点导致大面积区域没有参与感应。我的做法是先3D打印一个脚掌形状的硬底壳底部留一个和FSR有效区域尺寸一致的浅槽把FSR用双面胶粘在槽里再在最上面贴一层1mm厚的硅胶脚垫。FSR的引脚线则沿着腿侧布线用螺旋缠绕管保护起来一直延伸到主控板。这里有个细节需要特别强调FSR的引脚和柔性基材连接处非常脆弱剧烈弯折几次就会内部断路而表现症状不是完全没信号而是信号忽大忽小极难排查。我后来把所有靠近关节的线都换了硅胶软线并在线材进入脚掌的位置打了热熔胶固定这个问题才彻底解决。如果你没有3D打印机也可以用EVA泡棉板手工挖槽代替但一定要注意让FSR背面贴到的位置是平整的不要在传感器正下方设置凸起结构否则长时间受压之后FSR的阻值会发生永久性漂移。我拆了一个客户寄来维修的机器人就是因为脚底用了一个凸点防滑垫半年之后FSR输出的“无压力”电压从0.8V变成了1.6V触发阈值完全错乱。2.3 执行机构的配合让“害怕”有层次传感器解决的是“被摸到”的问题而动作执行解决的是“怎么反应”的问题。这个项目里我用了三种执行器分别对应三种不同的反应等级。舵机控制脚掌缩起模拟“躲开”振动马达装在身体骨架上模拟“发抖”DFPlayer音频模块负责播放声音可以是笑声也可以是“不要挠了”的配音。这三种执行器的配合顺序决定了交互效果是否真实。我一开始的做法是检测到触摸三个执行器同时启动结果机器人显得很“神经质”没有任何情绪递进。后来参考了动物行为学里“惊吓反射”的概念调整成有层次的反应路径轻触时只缩一下脚停顿0.2秒持续触摸时脚掌连续抖动同时开启振动马达压力继续增加或者累计触摸时间超过2秒才播放声音并让舵机进行大幅度摆动模拟“挣扎”。这样的层次感让整个交互变得可信很多也让我意识到硬件能力其实早就够了真正决定“像不像”的是软件里的状态设计和动作编排。3. 软件核心状态机驱动“被挠”反应3.1 信号采集与滤波防止误触发FSR信号采集的第一步是读取ADC值并做平滑处理。很多人拿到传感器后直接在loop里面写一句analogRead()然后拿去和阈值比这种方式在静态压力下没有问题但用手指挠脚心时压力是快速波动的单次读取的值可能一下冲到阈值以上、一下跌回阈值以下导致机器人短时间内疯狂触发看起来就像抽风。我实际使用的方案是滑动平均滤波每次读入一个值和最近5次的值取平均。这个滤波窗口长度是经过测试的窗口太短比如3次滤不掉高频抖动窗口太长比如10次以上反应延迟会明显增大触摸响应变得迟钝。5次窗口在ESP32的ADC采样率下大约带来15ms的延迟人几乎感知不到。代码实现上我放在一个定时器中断里执行采样而不是主循环里。ESP32的ADC读取过程中如果被Wi-Fi任务打断会返回异常值所以采样代码做了异常剔除——连续两次ADC跳变超过1000单位就认为这次采样无效丢弃重读。这个处理后面帮我排除了至少一半的“幽灵触发”问题。3.2 四状态状态机空闲、警觉、挣扎、恢复整个反应逻辑我用一个简单的状态机来管理四个状态分别是IDLE空闲、TOUCHED警觉、STRUGGLE挣扎、RECOVER恢复。设计思路是这样的IDLE状态下系统正常检测压力压力超过阈值的时刻记录为触摸起始时间进入TOUCHED。TOUCHED状态下机器人执行小幅动作比如脚掌缩回一个小角度但不会持续反馈同时累积一个“痒感值”。如果触摸在1秒内结束系统回到IDLE如果触摸持续痒感值累加超过设定值后进入STRUGGLE。STRUGGLE状态下才是完整的“挣扎”表现——连续抖动、发声、大幅摆动。当压力消失后进入RECOVER状态这个状态有点像一个“冷却时间”5秒内不响应任何触摸避免刚安静下来又被立刻戳一下导致系统处于永动状态。冷却结束后回到IDLE。这里有一个非常关键但又容易被忽略的细节状态切换不能太“陡”。比如从IDLE直接进入STRUGGLE如果动作突然从静止跳到最大幅度舵机在速度很快的情况下会产生巨大冲击电流造成电压跌落甚至让ESP32重启。我在状态切换时加入了动作淡入机制也就是每个动作指令都带一个加速度限制舵机速度从当前值按步长逼近目标值而不是直接跳到目标角度。这个机制让整个机器人的动作看起来“肉肉的”反而比机械式的瞬间到位更有生物感。3.3 压力和动作的映射关系为了让“越挠越痒”的感受传递出来我把FSR的压力值映射成了三个档位。轻压电压0.8V~1.5V对应“缩脚”动作中压1.5V~2.2V对应“连续抖动振动马达”重压2.2V以上对应“大幅挣扎播放音效”。这个映射不是写死阈值而是做一个分段线性映射函数让输出动作幅度和输入压力成正比。映射的实现需要一个校准过程。我建议先把ADC原始值通过串口打印出来用不同力度多次按压记录三个区间的典型值。由于FSR在长期使用后会有轻微漂移比较稳妥的做法是在系统启动时做一个短暂的“无压力基准采样”把这个基准值作为零点之后所有判断都基于相对压力而非绝对电压。这个功能代码量不大但能大大提升长期使用的稳定性。我后面做过一个实验用同一套设备连续运行72小时套了基准校准后触发阈值几乎没变而没套校准的触发电平漂了将近0.3V已经足够引起误触发了。4. 接入ROS 2把玩具变成机器人开发练手工程4.1 为什么非要接ROS 2先声明一下如果你只是想做一个整蛊机器人或者桌面摆件完全不需要上ROS 2用我上面说的Arduino单机方案就够了。但如果你是想认真走机器人开发路线ROS 2几乎是绕不过去的门槛而“能产生直观交互反馈”的项目是最好的学习载体。我在带新人学ROS 2的时候发现让人对着吃灰的说明书去学话题通信效率极低但如果让我摸一下机器人、机器人就挣扎一下新人会追着问“这个信号到底是怎么从传感器传到执行器的”这时候讲话题、讲节点接受度完全不一样。从系统结构上看ROS 2很适合这个项目的原因在于“模块化”。传感器驱动、状态判断、动作执行可以是三个独立节点你甚至可以只在传感器节点上做仿真用ros2 topic pub手动发消息就能看见动作执行的效果。这样即使硬件还没到货代码部分已经可以并行开发了对团队协作也非常友好。4.2 节点划分与话题定义在这个项目里我划分了三个节点。第一个是sensor_node负责读取ESP32通过串口发来的压力值并把float数据发布到 /touch_strength 话题上。选择这个做法的原因是ESP32作为单片机负责实时采样ROS 2作为上层负责逻辑两者边界非常清晰。第二个是react_node订阅 /touch_strength运行上文提到的状态机计算当前应当执行的反应等级并把结果发布到 /react_command 话题。第三个是action_node订阅 /react_command把抽象的“挣扎”指令翻译成具体的串口指令比如“舵机转到120度振动马达开循环3次”发给ESP32执行。话题类型上/touch_strength 用的是标准类型 std_msgs/msg/Float32而 /react_command 我自定义了一个消息包含两个字段float32 strength 和 string reaction_type。用自定义消息而不是直接用String是为了将来扩展方便——比如以后要加“表情屏控制”“语音回复”直接在同一个消息里加字段就行不用新增话题。这个架构的好处是任何一个节点都可以单独替换或测试。有一次我的ESP32硬件坏了我没有等新的板子到直接写了一个模拟传感器节点按正弦波规律发布压力值整个交互逻辑照常跑通。这在传统的“一坨代码”式开发里是不可能做到的。4.3 参数配置与launch启动在ROS 2里我把所有阈值参数都做成了参数而不是写死在代码里。比如 /react_node 下的 touch_threshold、struggle_threshold、recover_time都可以在launch文件里传值这样调整机器人“性格”的时候不需要重新编译。我还做了一个很实用的功能通过参数控制 “敏感度模式”把阈值整体除以一个系数。展览演示时设为1.5变得很敏感观众轻轻碰一下就有反馈家里调试时设为0.8减少误触发。launch文件本身也很简单用Python的launch格式把三个节点串起来顺手启动一个串口权限设置命令。其实把这套流程跑通之后你再去理解ROS 2里那些更复杂的navigation、moveit配置思路都是相通的无非就是“节点话题参数”的组合变化。5. 完整实操从接线到跑通的每一步5.1 物料清单与接线方式先列一个可以直接照着买的物料清单都是非常常见的东西物料型号/规格数量用途主控板ESP32 DevKitC V41采集与底层控制压敏电阻Interlink FSR4022左右脚底检测固定电阻10kΩ 1%2FSR分压舵机MG90S金属齿轮2脚掌抬起/晃动振动马达手机扁平马达3V1颤抖体感三极管S80501驱动振动马达音频模块DFPlayer Mini1音效播放扬声器3W 4Ω1输出声音电源5V 3A DC-DC模块1稳定供电3D打印件脚掌壳、腿部支架1套结构支撑硅胶脚垫1mm厚2压力传导与防滑接线方面FSR部分按之前说的分压电路连接两个FSR分别接ESP32的GPIO34和GPIO35。需要特别提醒的是ESP32的ADC引脚并不是所有GPIO都能用的有些引脚内部已经连接到flash电路读取时会有很大噪声。我自己踩过坑的包括GPIO36?不36和39是纯输入ADC通道没有问题但像GPIO2、GPIO15之类的被占用引脚就不要碰了。舵机的信号线接GPIO12和GPIO13这两路是硬件PWM通道稳定度好于软件PWM。振动马达通过三极管接到GPIO14音频模块的RX、TX分别接GPIO16和GPIO17。供电是这个项目里最容易出问题的部分。ESP32的USB供电在舵机瞬间启动时会产生严重压降表现为机器人一抖就重启。我的解决方案是舵机和主控分开供电舵机直接用5V 3A的DC-DC模块供电主控板也从同一个电源取电但通过一个大容量电解电容1000μF在电源入口做缓冲。这样即使舵机瞬间拉走1.5A电流主控电压波动也能控制在0.2V以内。5.2 下位机代码与上位机代码下位机ESP32代码我分成三个文件main.ino负责初始化和主循环sensor.cpp负责FSR采样和滤波action.cpp负责执行舵机、马达、音频的具体动作。主循环的逻辑很简洁每10ms读取一次压力值将值通过串口发送同时检查是否收到来自ROS 2端的控制指令收到就执行对应动作。由于采样放在定时器中断里主循环里的串口接收不会阻塞传感数据的更新。示例里最关键的是定时器采样部分hw_timer_t *sampleTimer NULL; volatile float filteredPressure 0; volatile bool sampleReady false; void IRAM_ATTR onSampleTimer() { int raw analogRead(FSR_PIN); // 简单的异常剔除跳变过大时忽略本次值 static int lastRaw 0; if (abs(raw - lastRaw) 1000) { lastRaw raw; return; } lastRaw raw; // 5点滑动平均 static float buf[5] {0}; static int idx 0; buf[idx] raw; idx (idx 1) % 5; float sum 0; for (int i 0; i 5; i) sum buf[i]; filteredPressure sum / 5.0; sampleReady true; }需要注意在ESP32的Arduino环境里定时器回调函数必须用IRAM_ATTR修饰否则在Wi-Fi栈启用时会触发异常重启。这是ESP32的一个“经典坑”很多新手都会在这里卡一晚上。上位机ROS 2部分sensor_node的核心是串口读取并转换数据类型。Python里用pyserial读串口解析形如P:1.234\n的协议帧然后发布到 /touch_strength。整个循环用 rclpy.spin_once() 配合小延时避免串口缓冲区 overflow。5.3 灵敏度标定的具体操作标定这一步我建议按照以下步骤来不要跳步。先打开串口监视器在无压力的状态下记录10秒取平均作为基准零点。然后用手指以你感觉“很轻”的力度触碰FSR记录这一瞬间的电平值记为“轻触值”再用正常力度按一下记为“中压值”再用最大力气按压记为“重压值”。把这三个值填到react_node的params.yaml里作为压力分档的锚点。实际操作中有一个很容易被忽略的变量温度。FSR的阻值会随温度漂移尤其是放在脚底这种不透气的结构里机器人内部温度升高后基线会变。所以我把“启动时自动校准零点”做成了默认行为每次上电都会重新读取基线而不是用代码里写死的0.8V。这个改动小但长期运行稳定性的提升非常明显。5.4 动作参数的调试心得动作表现的调试本质上是在玩一组参数舵机目标角度、转动速度、停顿时间、循环次数。我的参考经验是轻触反应舵机转20度、速度250、停顿300ms挣扎反应舵机转45度、速度600、循环4次每次间隔80ms。如果发现动作看起来“假”最常见的原因其实是舵机速度太快了快到看起来像被电击而不是在发抖。真人的颤抖频率大概在5~10Hz如果想让机器人看起来更接近生物抖动间隔应该在100ms~200ms之间而不是越快越像。这个经验是我调了很多版才总结出来的你可以直接拿去用。6. 常见问题与排查技巧实录6.1 传感器不触发或者一直触发先上排查清单按概率排序现象大概率原因解决办法完全没有信号FSR引脚扯断测FSR两端电阻量不出变化就是断了信号一直满量程FSR被硬物压住/分压电阻接错检查安装层重新焊接固定电阻信号不稳定线材内部虚断换硅胶软线接头处打胶固定上电时误触发一次上电瞬间ADC未稳定在sampleTimer回调里加“启动前200ms丢弃”逻辑使用一段时间后误触发FSR零点漂移启用启动自动校准重新标定有一个排查小技巧把FSR的两个引脚不通过分压电路直接接到万用表电阻档用手按压看阻值变化。如果阻值变化正常几千Ω到几十kΩ那问题在电路或者软件如果阻值不变问题在传感器本身。这样能快速把问题定位到硬件层。6.2 舵机抖动会导致整机重启这个现象基本都是供电问题。舵机标称工作电流是堵转电流MG90S堵转电流能到1.2A左右而ESP32模块的稳压芯片根本顶不住这种瞬时抽载。解决方案有三个层次第一舵机电源单独走DC-DC模块不经过主板的3.3V稳压器第二电源输入端并联一个大电容增强瞬态响应第三软件上给舵机指令加“加速/减速”限制不要让舵机以最大速度瞬间转向。还有一点很多人不知道舵机信号线在上电瞬间会短暂输出高电平导致舵机冲到最大角度如果此时机器人摆在不稳定的架子上会摔下来。解决办法是在舵机信号线和地之间加一个10kΩ下拉电阻让上电瞬间信号保持低电平直到主控程序把PWM信号稳定输出。6.3 交互延迟大手感“肉”手感“肉”的原因主要有两个一是主循环里用了大量的delay()阻塞代码传感器采样被拖慢了二是ROS 2端的状态机循环周期太长比如用默认的0.5s定时器来做状态切换那触摸到反应之间会有明显卡顿。我的做法是下位机用硬件定时器做毫秒级采样状态机跑在ROS 2节点里但用50ms周期循环而不是默认的500ms。如果你只跑单机版不接ROS 2那么整个状态机也放在定时器回调里执行主循环只负责串口输出和处理外部指令。实测下来从手指碰到FSR到舵机开始转动的延迟能控制在60ms以内这个数值已经低于人类对“即时反馈”的感知阈值交互体验非常流畅。6.4 舵机转动时音频播放卡顿音频卡顿的问题根源在DFPlayer Mini和舵机共用电源时舵机的干扰导致音频解码芯片供电不稳。DFPlayer Mini对电源噪声非常敏感如果听到卡顿、破音多半是电源不干净。解决办法是给DFPlayer Mini单独加一个100μF电容和1mH电感组成的LC滤波或者干脆用一个独立的5V供电给它。此外音频文件本身要转成不超过320kbps的MP3采样率不要用44.1kHz用22.05kHz就够了这样解码负担更小抗干扰能力也更强。在我实际做的几个版本里还有一个容易忽略的声音问题播放音效和舵机动作同时启动时音效的起点会和动作的起点有些微偏差耳朵可能察觉不到但在视频回放里会被放大。最自然的做法是让音效稍微提前100ms启动因为人耳对“先有声音后有动作”比“先有动作后有声音”的容忍度高得多这个细节能让整体观感真实不少。最后分享一点我的个人体会做这个项目最大的收获不是“让机器人怕痒”这个玩笑本身而是把一个看起来很无厘头的场景硬生生拆成了传感、控制、状态管理、系统集成这四块正好覆盖了机器人开发的主要环节。我前后给三个朋友做过类似的项目每次都能在对外的演示中迅速吸引围观甚至有人看到一半就问能不能直接把代码拿去给学生上课用。如果你也想做我的建议是不要一上来就追求完美先让机器人有最基础的反应——碰到脚底就缩一下这一版跑通了再逐步加入声音、抖动、状态机最后再接ROS 2。这样每走一步都能看到成果学习动力会保持得很好。如果你在调试中遇到了上面没提到的问题顺着“硬件—感知—电源—软件”这四个方向排查绝大多数难点都能快速定位。希望这个项目也能让你在机器人开发这条路上找到一个既好玩又有收获的切入点。
阅读完成 · 觉得有帮助?