器人怕癢?FSR觸覺感知+ROS 2交互原型實(shí)戰(zhàn))
做“機(jī)器人被撓腳心”這個(gè)項(xiàng)目起因其實(shí)挺簡單我想驗(yàn)證一個(gè)小型機(jī)器人對“突發(fā)觸摸”能做出多自然的反應(yīng)。這個(gè)項(xiàng)目在我自己整理的《FM及機(jī)器人系列》里屬于TK觸覺互動(dòng)測試專題后來做著做著發(fā)現(xiàn)它已經(jīng)不只是一個(gè)整蠱玩具而是一套完整的“觸覺感知 運(yùn)動(dòng)反饋 狀態(tài)管理”機(jī)器人交互原型。很多做陪伴機(jī)器人、展覽機(jī)器人、教育機(jī)器人的人最后都會(huì)遇到同一個(gè)問題機(jī)器人太“木”了摸它沒反應(yīng)或者反應(yīng)延遲明顯體驗(yàn)非常差。而“撓腳心”恰好是觸發(fā)快速自然反應(yīng)的極佳測試場景我做完這套之后很多做ROS開發(fā)的同事也拿它當(dāng)入門練手項(xiàng)目。這個(gè)項(xiàng)目最適合兩類人一類是剛開始學(xué)機(jī)器人控制、ROS系統(tǒng)想找一個(gè)有趣又不枯燥的載體來練習(xí)的人另一類是已經(jīng)被課程里的正運(yùn)動(dòng)學(xué)、逆運(yùn)動(dòng)學(xué)搞得有點(diǎn)疲憊的初學(xué)者想換個(gè)更有成就感的角度理解傳感器和執(zhí)行器如何協(xié)同工作。我會(huì)從硬件選型、傳感器原理、狀態(tài)機(jī)設(shè)計(jì)、ROS 2集成到完整實(shí)操和常見問題把整個(gè)項(xiàng)目展開來講盡量讓你能直接“抄作業(yè)”。1. 整體設(shè)計(jì)思路先想清楚“怕癢”這個(gè)動(dòng)作需要什么1.1 為什么選擇“撓腳心”作為交互場景傳統(tǒng)機(jī)器人給人的印象是“被動(dòng)執(zhí)行”你按按鈕它才動(dòng)你發(fā)指令它才做。但這種模式離“人機(jī)自然交互”非常遠(yuǎn)。這幾年我接觸過的服務(wù)機(jī)器人、導(dǎo)覽機(jī)器人項(xiàng)目客戶幾乎都會(huì)提一個(gè)需求能不能讓機(jī)器人更有“人情味”其中最容易出效果的一招就是讓它對自己的身體被觸碰產(chǎn)生反應(yīng)。微軟的小冰、優(yōu)必選的Walker等產(chǎn)品也都做過“怕癢”“怕摸”這類演示目的就是拉近機(jī)器人和用戶的距離?!皳夏_心”是觸發(fā)人類自然反應(yīng)最強(qiáng)的動(dòng)作之一換到機(jī)器人身上它天然具備三個(gè)測試價(jià)值。首先腳底是機(jī)器人身體上相對隱蔽、區(qū)域明確的部位傳感器安裝不會(huì)破壞整體外觀。其次觸碰腳底的力度差異很大從輕輕拂過到用力按壓能很好地測試傳感器量程和閾值設(shè)計(jì)。最關(guān)鍵的一點(diǎn)是“縮腳”“抖動(dòng)”“發(fā)出聲音”這一整套反應(yīng)在觀感上充滿喜劇色彩非常適合在課堂、展會(huì)、活動(dòng)上做演示容易調(diào)動(dòng)氣氛也讓學(xué)習(xí)者更有動(dòng)力把項(xiàng)目調(diào)好。從本質(zhì)上看這個(gè)項(xiàng)目要解決的三個(gè)技術(shù)問題很清晰腳底受到觸碰時(shí)傳感器如何快速準(zhǔn)確感知感知到信號后機(jī)器人如何選擇并執(zhí)行“反應(yīng)動(dòng)作”整個(gè)過程的延遲要控制在多短才能讓交互顯得自然。這三個(gè)問題分別對應(yīng)硬件層、控制層和交互層恰好是機(jī)器人開發(fā)的三個(gè)基本切面。1.2 整體方案選型分層設(shè)計(jì)避免一鍋燴我在設(shè)計(jì)這個(gè)項(xiàng)目時(shí)沒有選擇“一塊板子搞定所有事”的做法而是把系統(tǒng)分成了感知層、控制層、表現(xiàn)層三層。感知層用壓敏電阻FSR檢測腳底壓力控制層用一塊ESP32作為主控負(fù)責(zé)采集信號、運(yùn)行狀態(tài)機(jī)、輸出控制指令表現(xiàn)層由舵機(jī)、振動(dòng)馬達(dá)和音頻模塊組成分別實(shí)現(xiàn)“縮腳”“顫抖”“笑/求饒”三種反饋。之所以分層是因?yàn)檫@個(gè)項(xiàng)目后續(xù)有很強(qiáng)的擴(kuò)展性。如果你只想做一個(gè)整蠱玩具用一塊Arduino Nano就能完成全部工作但如果你想把項(xiàng)目接入ROS 2或者把“撓癢反應(yīng)”做成整個(gè)機(jī)器人交互系統(tǒng)的一個(gè)子系統(tǒng)就必須把感知、決策、動(dòng)作分開。我自己剛開始也是圖省事把代碼全寫在一個(gè)loop里后來加了ROS接口之后不得不重寫了一遍教訓(xùn)很深刻。硬件選型上我最終選用了ESP32而非Arduino Uno原因是ESP32的ADC精度更高、支持Wi-Fi/藍(lán)牙而且雙核處理器可以一個(gè)核跑傳感器采樣、另一個(gè)核跑狀態(tài)機(jī)。不過要注意ESP32的ADC有非線性問題后面我會(huì)專門講怎么校準(zhǔn)。執(zhí)行器方面腳部縮回動(dòng)作用了SG90舵機(jī)但經(jīng)過幾次測試后我強(qiáng)烈建議換成MG90S或MG996R因?yàn)镾G90在連續(xù)抖動(dòng)時(shí)很容易發(fā)熱甚至燒毀。振動(dòng)馬達(dá)用的是常見的手機(jī)扁平馬達(dá)通過一個(gè)S8050三極管驅(qū)動(dòng)音頻部分用了DFPlayer Mini播放MP3音效比直接用蜂鳴器逼真得多。2. 觸覺感知是怎么實(shí)現(xiàn)的FSR傳感器原理與安裝細(xì)節(jié)2.1 FSR壓敏電阻的工作原理FSRForce Sensing Resistor壓敏電阻是這個(gè)項(xiàng)目感知層的主角。它的工作原理很簡單當(dāng)壓力施加在傳感器有效區(qū)域上時(shí)內(nèi)部導(dǎo)電粒子和電極之間的接觸面積增加導(dǎo)致電阻值下降。沒有壓力時(shí)它的電阻通常在1MΩ以上隨著壓力增大可以降到幾kΩ甚至幾百Ω。要讓ESP32讀取這個(gè)電阻變化不能直接把FSR接到ADC引腳因?yàn)锳DC讀的是電壓而不是電阻。標(biāo)準(zhǔn)做法是接一個(gè)分壓電路把FSR和一個(gè)固定電阻串聯(lián)中間抽頭接到ADC輸入。這里有個(gè)非常容易踩的坑很多人以為固定電阻的阻值選得越大越好其實(shí)不是。固定電阻阻值決定了電路的靈敏度范圍選得太大輕輕一碰ADC就到滿量程后面沒法區(qū)分“輕觸”和“重壓”選得太小重壓時(shí)電壓變化不明顯分辨率又不夠。我最終選的固定電阻是10kΩ這個(gè)值在大多數(shù)場景下能兼顧。舉個(gè)例子我用的FSR型號是Interlink FSR402它在輕觸時(shí)阻值約為20kΩ重壓時(shí)約2kΩ。用10kΩ固定電阻做分壓時(shí)輕觸狀態(tài)ADC口電壓大概是3.3 * 10 / (20 10) 1.1V重壓狀態(tài)大概是3.3 * 10 / (2 10) 2.75V整個(gè)區(qū)間接近1.65V非常利于分辨率分配。如果你用的是其他型號FSR可以先用萬用表測一下輕觸和重壓下的實(shí)際阻值再根據(jù)公式R_fixed sqrt(R_light * R_heavy)估算最佳固定電阻這個(gè)公式來自分壓電路的最大靈敏度條件我實(shí)測下來比較可靠。2.2 腳底結(jié)構(gòu)改造與傳感器固定FSR傳感器雖然叫“壓敏電阻”但它對安裝方式非常挑剔。最理想的狀態(tài)是FSR放置在一個(gè)平整堅(jiān)硬的平面上上方覆蓋一層薄而柔軟的壓力傳遞層。如果直接貼在軟海綿上FSR會(huì)因?yàn)榛膹澢a(chǎn)生非線性輸出如果上方壓著一塊硬塑料壓力又會(huì)集中在某一點(diǎn)導(dǎo)致大面積區(qū)域沒有參與感應(yīng)。我的做法是先3D打印一個(gè)腳掌形狀的硬底殼底部留一個(gè)和FSR有效區(qū)域尺寸一致的淺槽把FSR用雙面膠粘在槽里再在最上面貼一層1mm厚的硅膠腳墊。FSR的引腳線則沿著腿側(cè)布線用螺旋纏繞管保護(hù)起來一直延伸到主控板。這里有個(gè)細(xì)節(jié)需要特別強(qiáng)調(diào)FSR的引腳和柔性基材連接處非常脆弱劇烈彎折幾次就會(huì)內(nèi)部斷路而表現(xiàn)癥狀不是完全沒信號而是信號忽大忽小極難排查。我后來把所有靠近關(guān)節(jié)的線都換了硅膠軟線并在線材進(jìn)入腳掌的位置打了熱熔膠固定這個(gè)問題才徹底解決。如果你沒有3D打印機(jī)也可以用EVA泡棉板手工挖槽代替但一定要注意讓FSR背面貼到的位置是平整的不要在傳感器正下方設(shè)置凸起結(jié)構(gòu)否則長時(shí)間受壓之后FSR的阻值會(huì)發(fā)生永久性漂移。我拆了一個(gè)客戶寄來維修的機(jī)器人就是因?yàn)槟_底用了一個(gè)凸點(diǎn)防滑墊半年之后FSR輸出的“無壓力”電壓從0.8V變成了1.6V觸發(fā)閾值完全錯(cuò)亂。2.3 執(zhí)行機(jī)構(gòu)的配合讓“害怕”有層次傳感器解決的是“被摸到”的問題而動(dòng)作執(zhí)行解決的是“怎么反應(yīng)”的問題。這個(gè)項(xiàng)目里我用了三種執(zhí)行器分別對應(yīng)三種不同的反應(yīng)等級。舵機(jī)控制腳掌縮起模擬“躲開”振動(dòng)馬達(dá)裝在身體骨架上模擬“發(fā)抖”DFPlayer音頻模塊負(fù)責(zé)播放聲音可以是笑聲也可以是“不要撓了”的配音。這三種執(zhí)行器的配合順序決定了交互效果是否真實(shí)。我一開始的做法是檢測到觸摸三個(gè)執(zhí)行器同時(shí)啟動(dòng)結(jié)果機(jī)器人顯得很“神經(jīng)質(zhì)”沒有任何情緒遞進(jìn)。后來參考了動(dòng)物行為學(xué)里“驚嚇反射”的概念調(diào)整成有層次的反應(yīng)路徑輕觸時(shí)只縮一下腳停頓0.2秒持續(xù)觸摸時(shí)腳掌連續(xù)抖動(dòng)同時(shí)開啟振動(dòng)馬達(dá)壓力繼續(xù)增加或者累計(jì)觸摸時(shí)間超過2秒才播放聲音并讓舵機(jī)進(jìn)行大幅度擺動(dòng)模擬“掙扎”。這樣的層次感讓整個(gè)交互變得可信很多也讓我意識到硬件能力其實(shí)早就夠了真正決定“像不像”的是軟件里的狀態(tài)設(shè)計(jì)和動(dòng)作編排。3. 軟件核心狀態(tài)機(jī)驅(qū)動(dòng)“被撓”反應(yīng)3.1 信號采集與濾波防止誤觸發(fā)FSR信號采集的第一步是讀取ADC值并做平滑處理。很多人拿到傳感器后直接在loop里面寫一句analogRead()然后拿去和閾值比這種方式在靜態(tài)壓力下沒有問題但用手指撓腳心時(shí)壓力是快速波動(dòng)的單次讀取的值可能一下沖到閾值以上、一下跌回閾值以下導(dǎo)致機(jī)器人短時(shí)間內(nèi)瘋狂觸發(fā)看起來就像抽風(fēng)。我實(shí)際使用的方案是滑動(dòng)平均濾波每次讀入一個(gè)值和最近5次的值取平均。這個(gè)濾波窗口長度是經(jīng)過測試的窗口太短比如3次濾不掉高頻抖動(dòng)窗口太長比如10次以上反應(yīng)延遲會(huì)明顯增大觸摸響應(yīng)變得遲鈍。5次窗口在ESP32的ADC采樣率下大約帶來15ms的延遲人幾乎感知不到。代碼實(shí)現(xiàn)上我放在一個(gè)定時(shí)器中斷里執(zhí)行采樣而不是主循環(huán)里。ESP32的ADC讀取過程中如果被Wi-Fi任務(wù)打斷會(huì)返回異常值所以采樣代碼做了異常剔除——連續(xù)兩次ADC跳變超過1000單位就認(rèn)為這次采樣無效丟棄重讀。這個(gè)處理后面幫我排除了至少一半的“幽靈觸發(fā)”問題。3.2 四狀態(tài)狀態(tài)機(jī)空閑、警覺、掙扎、恢復(fù)整個(gè)反應(yīng)邏輯我用一個(gè)簡單的狀態(tài)機(jī)來管理四個(gè)狀態(tài)分別是IDLE空閑、TOUCHED警覺、STRUGGLE掙扎、RECOVER恢復(fù)。設(shè)計(jì)思路是這樣的IDLE狀態(tài)下系統(tǒng)正常檢測壓力壓力超過閾值的時(shí)刻記錄為觸摸起始時(shí)間進(jìn)入TOUCHED。TOUCHED狀態(tài)下機(jī)器人執(zhí)行小幅動(dòng)作比如腳掌縮回一個(gè)小角度但不會(huì)持續(xù)反饋同時(shí)累積一個(gè)“癢感值”。如果觸摸在1秒內(nèi)結(jié)束系統(tǒng)回到IDLE如果觸摸持續(xù)癢感值累加超過設(shè)定值后進(jìn)入STRUGGLE。STRUGGLE狀態(tài)下才是完整的“掙扎”表現(xiàn)——連續(xù)抖動(dòng)、發(fā)聲、大幅擺動(dòng)。當(dāng)壓力消失后進(jìn)入RECOVER狀態(tài)這個(gè)狀態(tài)有點(diǎn)像一個(gè)“冷卻時(shí)間”5秒內(nèi)不響應(yīng)任何觸摸避免剛安靜下來又被立刻戳一下導(dǎo)致系統(tǒng)處于永動(dòng)狀態(tài)。冷卻結(jié)束后回到IDLE。這里有一個(gè)非常關(guān)鍵但又容易被忽略的細(xì)節(jié)狀態(tài)切換不能太“陡”。比如從IDLE直接進(jìn)入STRUGGLE如果動(dòng)作突然從靜止跳到最大幅度舵機(jī)在速度很快的情況下會(huì)產(chǎn)生巨大沖擊電流造成電壓跌落甚至讓ESP32重啟。我在狀態(tài)切換時(shí)加入了動(dòng)作淡入機(jī)制也就是每個(gè)動(dòng)作指令都帶一個(gè)加速度限制舵機(jī)速度從當(dāng)前值按步長逼近目標(biāo)值而不是直接跳到目標(biāo)角度。這個(gè)機(jī)制讓整個(gè)機(jī)器人的動(dòng)作看起來“肉肉的”反而比機(jī)械式的瞬間到位更有生物感。3.3 壓力和動(dòng)作的映射關(guān)系為了讓“越撓越癢”的感受傳遞出來我把FSR的壓力值映射成了三個(gè)檔位。輕壓電壓0.8V~1.5V對應(yīng)“縮腳”動(dòng)作中壓1.5V~2.2V對應(yīng)“連續(xù)抖動(dòng)振動(dòng)馬達(dá)”重壓2.2V以上對應(yīng)“大幅掙扎播放音效”。這個(gè)映射不是寫死閾值而是做一個(gè)分段線性映射函數(shù)讓輸出動(dòng)作幅度和輸入壓力成正比。映射的實(shí)現(xiàn)需要一個(gè)校準(zhǔn)過程。我建議先把ADC原始值通過串口打印出來用不同力度多次按壓記錄三個(gè)區(qū)間的典型值。由于FSR在長期使用后會(huì)有輕微漂移比較穩(wěn)妥的做法是在系統(tǒng)啟動(dòng)時(shí)做一個(gè)短暫的“無壓力基準(zhǔn)采樣”把這個(gè)基準(zhǔn)值作為零點(diǎn)之后所有判斷都基于相對壓力而非絕對電壓。這個(gè)功能代碼量不大但能大大提升長期使用的穩(wěn)定性。我后面做過一個(gè)實(shí)驗(yàn)用同一套設(shè)備連續(xù)運(yùn)行72小時(shí)套了基準(zhǔn)校準(zhǔn)后觸發(fā)閾值幾乎沒變而沒套校準(zhǔn)的觸發(fā)電平漂了將近0.3V已經(jīng)足夠引起誤觸發(fā)了。4. 接入ROS 2把玩具變成機(jī)器人開發(fā)練手工程4.1 為什么非要接ROS 2先聲明一下如果你只是想做一個(gè)整蠱機(jī)器人或者桌面擺件完全不需要上ROS 2用我上面說的Arduino單機(jī)方案就夠了。但如果你是想認(rèn)真走機(jī)器人開發(fā)路線ROS 2幾乎是繞不過去的門檻而“能產(chǎn)生直觀交互反饋”的項(xiàng)目是最好的學(xué)習(xí)載體。我在帶新人學(xué)ROS 2的時(shí)候發(fā)現(xiàn)讓人對著吃灰的說明書去學(xué)話題通信效率極低但如果讓我摸一下機(jī)器人、機(jī)器人就掙扎一下新人會(huì)追著問“這個(gè)信號到底是怎么從傳感器傳到執(zhí)行器的”這時(shí)候講話題、講節(jié)點(diǎn)接受度完全不一樣。從系統(tǒng)結(jié)構(gòu)上看ROS 2很適合這個(gè)項(xiàng)目的原因在于“模塊化”。傳感器驅(qū)動(dòng)、狀態(tài)判斷、動(dòng)作執(zhí)行可以是三個(gè)獨(dú)立節(jié)點(diǎn)你甚至可以只在傳感器節(jié)點(diǎn)上做仿真用ros2 topic pub手動(dòng)發(fā)消息就能看見動(dòng)作執(zhí)行的效果。這樣即使硬件還沒到貨代碼部分已經(jīng)可以并行開發(fā)了對團(tuán)隊(duì)協(xié)作也非常友好。4.2 節(jié)點(diǎn)劃分與話題定義在這個(gè)項(xiàng)目里我劃分了三個(gè)節(jié)點(diǎn)。第一個(gè)是sensor_node負(fù)責(zé)讀取ESP32通過串口發(fā)來的壓力值并把float數(shù)據(jù)發(fā)布到 /touch_strength 話題上。選擇這個(gè)做法的原因是ESP32作為單片機(jī)負(fù)責(zé)實(shí)時(shí)采樣ROS 2作為上層負(fù)責(zé)邏輯兩者邊界非常清晰。第二個(gè)是react_node訂閱 /touch_strength運(yùn)行上文提到的狀態(tài)機(jī)計(jì)算當(dāng)前應(yīng)當(dāng)執(zhí)行的反應(yīng)等級并把結(jié)果發(fā)布到 /react_command 話題。第三個(gè)是action_node訂閱 /react_command把抽象的“掙扎”指令翻譯成具體的串口指令比如“舵機(jī)轉(zhuǎn)到120度振動(dòng)馬達(dá)開循環(huán)3次”發(fā)給ESP32執(zhí)行。話題類型上/touch_strength 用的是標(biāo)準(zhǔn)類型 std_msgs/msg/Float32而 /react_command 我自定義了一個(gè)消息包含兩個(gè)字段float32 strength 和 string reaction_type。用自定義消息而不是直接用String是為了將來擴(kuò)展方便——比如以后要加“表情屏控制”“語音回復(fù)”直接在同一個(gè)消息里加字段就行不用新增話題。這個(gè)架構(gòu)的好處是任何一個(gè)節(jié)點(diǎn)都可以單獨(dú)替換或測試。有一次我的ESP32硬件壞了我沒有等新的板子到直接寫了一個(gè)模擬傳感器節(jié)點(diǎn)按正弦波規(guī)律發(fā)布壓力值整個(gè)交互邏輯照常跑通。這在傳統(tǒng)的“一坨代碼”式開發(fā)里是不可能做到的。4.3 參數(shù)配置與launch啟動(dòng)在ROS 2里我把所有閾值參數(shù)都做成了參數(shù)而不是寫死在代碼里。比如 /react_node 下的 touch_threshold、struggle_threshold、recover_time都可以在launch文件里傳值這樣調(diào)整機(jī)器人“性格”的時(shí)候不需要重新編譯。我還做了一個(gè)很實(shí)用的功能通過參數(shù)控制 “敏感度模式”把閾值整體除以一個(gè)系數(shù)。展覽演示時(shí)設(shè)為1.5變得很敏感觀眾輕輕碰一下就有反饋家里調(diào)試時(shí)設(shè)為0.8減少誤觸發(fā)。launch文件本身也很簡單用Python的launch格式把三個(gè)節(jié)點(diǎn)串起來順手啟動(dòng)一個(gè)串口權(quán)限設(shè)置命令。其實(shí)把這套流程跑通之后你再去理解ROS 2里那些更復(fù)雜的navigation、moveit配置思路都是相通的無非就是“節(jié)點(diǎn)話題參數(shù)”的組合變化。5. 完整實(shí)操從接線到跑通的每一步5.1 物料清單與接線方式先列一個(gè)可以直接照著買的物料清單都是非常常見的東西物料型號/規(guī)格數(shù)量用途主控板ESP32 DevKitC V41采集與底層控制壓敏電阻Interlink FSR4022左右腳底檢測固定電阻10kΩ 1%2FSR分壓舵機(jī)MG90S金屬齒輪2腳掌抬起/晃動(dòng)振動(dòng)馬達(dá)手機(jī)扁平馬達(dá)3V1顫抖體感三極管S80501驅(qū)動(dòng)振動(dòng)馬達(dá)音頻模塊DFPlayer Mini1音效播放揚(yáng)聲器3W 4Ω1輸出聲音電源5V 3A DC-DC模塊1穩(wěn)定供電3D打印件腳掌殼、腿部支架1套結(jié)構(gòu)支撐硅膠腳墊1mm厚2壓力傳導(dǎo)與防滑接線方面FSR部分按之前說的分壓電路連接兩個(gè)FSR分別接ESP32的GPIO34和GPIO35。需要特別提醒的是ESP32的ADC引腳并不是所有GPIO都能用的有些引腳內(nèi)部已經(jīng)連接到flash電路讀取時(shí)會(huì)有很大噪聲。我自己踩過坑的包括GPIO36?不36和39是純輸入ADC通道沒有問題但像GPIO2、GPIO15之類的被占用引腳就不要碰了。舵機(jī)的信號線接GPIO12和GPIO13這兩路是硬件PWM通道穩(wěn)定度好于軟件PWM。振動(dòng)馬達(dá)通過三極管接到GPIO14音頻模塊的RX、TX分別接GPIO16和GPIO17。供電是這個(gè)項(xiàng)目里最容易出問題的部分。ESP32的USB供電在舵機(jī)瞬間啟動(dòng)時(shí)會(huì)產(chǎn)生嚴(yán)重壓降表現(xiàn)為機(jī)器人一抖就重啟。我的解決方案是舵機(jī)和主控分開供電舵機(jī)直接用5V 3A的DC-DC模塊供電主控板也從同一個(gè)電源取電但通過一個(gè)大容量電解電容1000μF在電源入口做緩沖。這樣即使舵機(jī)瞬間拉走1.5A電流主控電壓波動(dòng)也能控制在0.2V以內(nèi)。5.2 下位機(jī)代碼與上位機(jī)代碼下位機(jī)ESP32代碼我分成三個(gè)文件main.ino負(fù)責(zé)初始化和主循環(huán)sensor.cpp負(fù)責(zé)FSR采樣和濾波action.cpp負(fù)責(zé)執(zhí)行舵機(jī)、馬達(dá)、音頻的具體動(dòng)作。主循環(huán)的邏輯很簡潔每10ms讀取一次壓力值將值通過串口發(fā)送同時(shí)檢查是否收到來自ROS 2端的控制指令收到就執(zhí)行對應(yīng)動(dòng)作。由于采樣放在定時(shí)器中斷里主循環(huán)里的串口接收不會(huì)阻塞傳感數(shù)據(jù)的更新。示例里最關(guān)鍵的是定時(shí)器采樣部分hw_timer_t *sampleTimer NULL; volatile float filteredPressure 0; volatile bool sampleReady false; void IRAM_ATTR onSampleTimer() { int raw analogRead(FSR_PIN); // 簡單的異常剔除跳變過大時(shí)忽略本次值 static int lastRaw 0; if (abs(raw - lastRaw) 1000) { lastRaw raw; return; } lastRaw raw; // 5點(diǎn)滑動(dòng)平均 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環(huán)境里定時(shí)器回調(diào)函數(shù)必須用IRAM_ATTR修飾否則在Wi-Fi棧啟用時(shí)會(huì)觸發(fā)異常重啟。這是ESP32的一個(gè)“經(jīng)典坑”很多新手都會(huì)在這里卡一晚上。上位機(jī)ROS 2部分sensor_node的核心是串口讀取并轉(zhuǎn)換數(shù)據(jù)類型。Python里用pyserial讀串口解析形如P:1.234\n的協(xié)議幀然后發(fā)布到 /touch_strength。整個(gè)循環(huán)用 rclpy.spin_once() 配合小延時(shí)避免串口緩沖區(qū) overflow。5.3 靈敏度標(biāo)定的具體操作標(biāo)定這一步我建議按照以下步驟來不要跳步。先打開串口監(jiān)視器在無壓力的狀態(tài)下記錄10秒取平均作為基準(zhǔn)零點(diǎn)。然后用手指以你感覺“很輕”的力度觸碰FSR記錄這一瞬間的電平值記為“輕觸值”再用正常力度按一下記為“中壓值”再用最大力氣按壓記為“重壓值”。把這三個(gè)值填到react_node的params.yaml里作為壓力分檔的錨點(diǎn)。實(shí)際操作中有一個(gè)很容易被忽略的變量溫度。FSR的阻值會(huì)隨溫度漂移尤其是放在腳底這種不透氣的結(jié)構(gòu)里機(jī)器人內(nèi)部溫度升高后基線會(huì)變。所以我把“啟動(dòng)時(shí)自動(dòng)校準(zhǔn)零點(diǎn)”做成了默認(rèn)行為每次上電都會(huì)重新讀取基線而不是用代碼里寫死的0.8V。這個(gè)改動(dòng)小但長期運(yùn)行穩(wěn)定性的提升非常明顯。5.4 動(dòng)作參數(shù)的調(diào)試心得動(dòng)作表現(xiàn)的調(diào)試本質(zhì)上是在玩一組參數(shù)舵機(jī)目標(biāo)角度、轉(zhuǎn)動(dòng)速度、停頓時(shí)間、循環(huán)次數(shù)。我的參考經(jīng)驗(yàn)是輕觸反應(yīng)舵機(jī)轉(zhuǎn)20度、速度250、停頓300ms掙扎反應(yīng)舵機(jī)轉(zhuǎn)45度、速度600、循環(huán)4次每次間隔80ms。如果發(fā)現(xiàn)動(dòng)作看起來“假”最常見的原因其實(shí)是舵機(jī)速度太快了快到看起來像被電擊而不是在發(fā)抖。真人的顫抖頻率大概在5~10Hz如果想讓機(jī)器人看起來更接近生物抖動(dòng)間隔應(yīng)該在100ms~200ms之間而不是越快越像。這個(gè)經(jīng)驗(yàn)是我調(diào)了很多版才總結(jié)出來的你可以直接拿去用。6. 常見問題與排查技巧實(shí)錄6.1 傳感器不觸發(fā)或者一直觸發(fā)先上排查清單按概率排序現(xiàn)象大概率原因解決辦法完全沒有信號FSR引腳扯斷測FSR兩端電阻量不出變化就是斷了信號一直滿量程FSR被硬物壓住/分壓電阻接錯(cuò)檢查安裝層重新焊接固定電阻信號不穩(wěn)定線材內(nèi)部虛斷換硅膠軟線接頭處打膠固定上電時(shí)誤觸發(fā)一次上電瞬間ADC未穩(wěn)定在sampleTimer回調(diào)里加“啟動(dòng)前200ms丟棄”邏輯使用一段時(shí)間后誤觸發(fā)FSR零點(diǎn)漂移啟用啟動(dòng)自動(dòng)校準(zhǔn)重新標(biāo)定有一個(gè)排查小技巧把FSR的兩個(gè)引腳不通過分壓電路直接接到萬用表電阻檔用手按壓看阻值變化。如果阻值變化正常幾千Ω到幾十kΩ那問題在電路或者軟件如果阻值不變問題在傳感器本身。這樣能快速把問題定位到硬件層。6.2 舵機(jī)抖動(dòng)會(huì)導(dǎo)致整機(jī)重啟這個(gè)現(xiàn)象基本都是供電問題。舵機(jī)標(biāo)稱工作電流是堵轉(zhuǎn)電流MG90S堵轉(zhuǎn)電流能到1.2A左右而ESP32模塊的穩(wěn)壓芯片根本頂不住這種瞬時(shí)抽載。解決方案有三個(gè)層次第一舵機(jī)電源單獨(dú)走DC-DC模塊不經(jīng)過主板的3.3V穩(wěn)壓器第二電源輸入端并聯(lián)一個(gè)大電容增強(qiáng)瞬態(tài)響應(yīng)第三軟件上給舵機(jī)指令加“加速/減速”限制不要讓舵機(jī)以最大速度瞬間轉(zhuǎn)向。還有一點(diǎn)很多人不知道舵機(jī)信號線在上電瞬間會(huì)短暫輸出高電平導(dǎo)致舵機(jī)沖到最大角度如果此時(shí)機(jī)器人擺在不穩(wěn)定的架子上會(huì)摔下來。解決辦法是在舵機(jī)信號線和地之間加一個(gè)10kΩ下拉電阻讓上電瞬間信號保持低電平直到主控程序把PWM信號穩(wěn)定輸出。6.3 交互延遲大手感“肉”手感“肉”的原因主要有兩個(gè)一是主循環(huán)里用了大量的delay()阻塞代碼傳感器采樣被拖慢了二是ROS 2端的狀態(tài)機(jī)循環(huán)周期太長比如用默認(rèn)的0.5s定時(shí)器來做狀態(tài)切換那觸摸到反應(yīng)之間會(huì)有明顯卡頓。我的做法是下位機(jī)用硬件定時(shí)器做毫秒級采樣狀態(tài)機(jī)跑在ROS 2節(jié)點(diǎn)里但用50ms周期循環(huán)而不是默認(rèn)的500ms。如果你只跑單機(jī)版不接ROS 2那么整個(gè)狀態(tài)機(jī)也放在定時(shí)器回調(diào)里執(zhí)行主循環(huán)只負(fù)責(zé)串口輸出和處理外部指令。實(shí)測下來從手指碰到FSR到舵機(jī)開始轉(zhuǎn)動(dòng)的延遲能控制在60ms以內(nèi)這個(gè)數(shù)值已經(jīng)低于人類對“即時(shí)反饋”的感知閾值交互體驗(yàn)非常流暢。6.4 舵機(jī)轉(zhuǎn)動(dòng)時(shí)音頻播放卡頓音頻卡頓的問題根源在DFPlayer Mini和舵機(jī)共用電源時(shí)舵機(jī)的干擾導(dǎo)致音頻解碼芯片供電不穩(wěn)。DFPlayer Mini對電源噪聲非常敏感如果聽到卡頓、破音多半是電源不干凈。解決辦法是給DFPlayer Mini單獨(dú)加一個(gè)100μF電容和1mH電感組成的LC濾波或者干脆用一個(gè)獨(dú)立的5V供電給它。此外音頻文件本身要轉(zhuǎn)成不超過320kbps的MP3采樣率不要用44.1kHz用22.05kHz就夠了這樣解碼負(fù)擔(dān)更小抗干擾能力也更強(qiáng)。在我實(shí)際做的幾個(gè)版本里還有一個(gè)容易忽略的聲音問題播放音效和舵機(jī)動(dòng)作同時(shí)啟動(dòng)時(shí)音效的起點(diǎn)會(huì)和動(dòng)作的起點(diǎn)有些微偏差耳朵可能察覺不到但在視頻回放里會(huì)被放大。最自然的做法是讓音效稍微提前100ms啟動(dòng)因?yàn)槿硕鷮Α跋扔新曇艉笥袆?dòng)作”比“先有動(dòng)作后有聲音”的容忍度高得多這個(gè)細(xì)節(jié)能讓整體觀感真實(shí)不少。最后分享一點(diǎn)我的個(gè)人體會(huì)做這個(gè)項(xiàng)目最大的收獲不是“讓機(jī)器人怕癢”這個(gè)玩笑本身而是把一個(gè)看起來很無厘頭的場景硬生生拆成了傳感、控制、狀態(tài)管理、系統(tǒng)集成這四塊正好覆蓋了機(jī)器人開發(fā)的主要環(huán)節(jié)。我前后給三個(gè)朋友做過類似的項(xiàng)目每次都能在對外的演示中迅速吸引圍觀甚至有人看到一半就問能不能直接把代碼拿去給學(xué)生上課用。如果你也想做我的建議是不要一上來就追求完美先讓機(jī)器人有最基礎(chǔ)的反應(yīng)——碰到腳底就縮一下這一版跑通了再逐步加入聲音、抖動(dòng)、狀態(tài)機(jī)最后再接ROS 2。這樣每走一步都能看到成果學(xué)習(xí)動(dòng)力會(huì)保持得很好。如果你在調(diào)試中遇到了上面沒提到的問題順著“硬件—感知—電源—軟件”這四個(gè)方向排查絕大多數(shù)難點(diǎn)都能快速定位。希望這個(gè)項(xiàng)目也能讓你在機(jī)器人開發(fā)這條路上找到一個(gè)既好玩又有收獲的切入點(diǎn)。