驗(yàn):狀態(tài)機(jī)與PD控制)
簡(jiǎn)介圍繞STM32智能小車的黑線循跡與紅外避障綜合實(shí)驗(yàn)這套工程源碼面向剛接觸嵌入式控制、希望從電機(jī)驅(qū)動(dòng)與傳感器采集入手理解小車運(yùn)動(dòng)邏輯的學(xué)習(xí)者。代碼以定時(shí)器配置電機(jī)為核心封裝前進(jìn)、剎車、左轉(zhuǎn)、左旋轉(zhuǎn)、右轉(zhuǎn)、右旋轉(zhuǎn)、后退等基礎(chǔ)運(yùn)動(dòng)函數(shù)注釋較為詳細(xì)便于把巡線判斷與避障策略拆成可讀、可改的模塊適合課程實(shí)驗(yàn)與入門小車項(xiàng)目對(duì)照練習(xí)。壓縮包共192個(gè)文件約6.49MB主體是34個(gè)c源文件與35個(gè)h頭文件覆蓋外設(shè)驅(qū)動(dòng)和工程組織同時(shí)保留Keil工程配置、編譯中間文件以及hex燒錄文件拿到后可直接對(duì)照目錄梳理定時(shí)器、GPIO與中斷相關(guān)配置。已有1187人學(xué)習(xí)說(shuō)明其在基礎(chǔ)巡線避障實(shí)驗(yàn)中有一定參考價(jià)值讀者可據(jù)此快速定位運(yùn)動(dòng)控制與傳感器處理的代碼入口并在此基礎(chǔ)上調(diào)整速度、轉(zhuǎn)向時(shí)長(zhǎng)與避障閾值。1. 從一塊循跡底板說(shuō)起STM32 智能小車綜合實(shí)驗(yàn)到底要解決什么很多人拿到 STM32 智能小車套件后做的第一件事是讓電機(jī)轉(zhuǎn)起來(lái)結(jié)果輪子一轉(zhuǎn)黑線循跡和紅外避障就全亂了——車要么在原地畫龍要么一頭頂?shù)綁ι?。這套 ZYSTM32-A1 綜合實(shí)驗(yàn)的價(jià)值不在于把兩個(gè)實(shí)驗(yàn)拼在同一個(gè)工程里而在于讓黑線循跡和紅外避障共用一塊主控、一套主循環(huán)、一份狀態(tài)機(jī)并且在兩種工況切換時(shí)不打架。它適合剛點(diǎn)完 LED、想完整做第一個(gè)嵌入式小車項(xiàng)目的人課程設(shè)計(jì)、畢設(shè)前期驗(yàn)證、入門競(jìng)賽練手都能直接用。讀完你應(yīng)該拿到一套能跑、能調(diào)、能講清楚“為什么這么寫”的方案而不是一張只會(huì)照抄的接線圖。2. 需求拆解與硬件選型三種工況、五路紅外和一套電機(jī)驅(qū)動(dòng)動(dòng)手畫原理圖之前先要把“這個(gè)小車要干什么”寫死。需求邊界不清后面選型就會(huì)一路糾結(jié)傳感器到底買三路還是五路驅(qū)動(dòng)到底用 L298N 還是 TB6612主控用 51 夠不夠。這一章按功能邊界、器件選型、引腳分配的順序把這些定下來(lái)。2.1 功能邊界循跡、避障、協(xié)同三種工況各自的驗(yàn)收口徑我把這套小車的功能切成三個(gè)工況每個(gè)工況都要有能測(cè)的驗(yàn)收口徑不然答辯時(shí)說(shuō)不清“做完了”是什么狀態(tài)。第一種是純循跡工況場(chǎng)地是一條 1.52 cm 寬的黑色電工膠帶直道加彎道最小曲率半徑按 30 cm 設(shè)計(jì)。驗(yàn)收口徑是從起點(diǎn)到終點(diǎn)約 5 m 全程不脫線單圈時(shí)間穩(wěn)定在 ±1 s 以內(nèi)。之所以把線寬限定在 2 cm 以內(nèi)是因?yàn)槲迓芳t外傳感器的探頭間距通常在 1015 mm線太寬會(huì)讓相鄰兩路同時(shí)壓線偏差計(jì)算的平均值會(huì)變鈍彎道就跟不上。第二種是純避障工況車前 1530 cm 出現(xiàn)障礙物時(shí)小車要在 0.3 s 內(nèi)做出反應(yīng)單側(cè)有障礙就向另一側(cè)讓正前方堵死就先后退再轉(zhuǎn)向。這個(gè)距離不是隨口定的紅外避障模塊的有效檢測(cè)距離通常在 380 cm 之間可調(diào)調(diào)太遠(yuǎn)會(huì)在彎道被場(chǎng)地邊沿誤觸發(fā)調(diào)太近則留給轉(zhuǎn)向的時(shí)間不夠。第三種是協(xié)同工況也是這套“綜合實(shí)驗(yàn)”真正的難點(diǎn)小車正在循跡前方突然出現(xiàn)障礙物要先切到避障動(dòng)作繞開之后還得自己找回黑線繼續(xù)跑。很多人的工程在單工況測(cè)試時(shí)都正常一到協(xié)同就露餡——避障結(jié)束后車在原地轉(zhuǎn)圈找線或者剛回到線上又被同一個(gè)障礙物觸發(fā)第二次。提示需求邊界一次就寫清楚“不做什么”比寫“做什么”更重要。這套方案不做攝像頭視覺、不做 SLAM 建圖、不做電機(jī)編碼器閉環(huán)調(diào)速純粹用開關(guān)量傳感器加 PWM 差速實(shí)現(xiàn)。邊界定了后面所有取舍都有依據(jù)。2.2 主控與傳感器選型為什么是 STM32F103 加五路 TCRT5000主控這塊市面上常見三條路8 位機(jī)、STM32F103 這類 Cortex-M3、以及跑 Linux 的板子。選 8 位機(jī)的理由是便宜、資料老但它的問題在于定時(shí)器和硬件 PWM 通道太少兩路電機(jī) PWM 加上一路 1 ms 系統(tǒng)節(jié)拍基本就把資源吃滿了再想加串口調(diào)試都緊張中斷響應(yīng)也慢。STM32F103C8T6 這類芯片剛好卡在甜點(diǎn)上72 MHz 主頻、多個(gè)通用定時(shí)器、通道足夠跑兩路互補(bǔ) PWM 加一路 1 ms 節(jié)拍GPIO 數(shù)量也夠接五路循跡加兩路避障。選它的真正理由不是“性能強(qiáng)”而是“外設(shè)資源剛好夠而且不用搶”。循跡傳感器選五路 TCRT5000 反射式模塊而不是攝像頭。攝像頭能拿到更豐富的路面信息但要做圖像二值化、邊緣提取幀率還受限于串口傳輸對(duì)一個(gè)小車項(xiàng)目來(lái)說(shuō)投入產(chǎn)出比不劃算。五路紅外的信息量是 5 bit雖然粗但實(shí)時(shí)性極好——GPIO 一讀就有結(jié)果控制周期可以壓到 10 ms。代價(jià)是它對(duì)地面反光敏感淺色瓷磚上和在深色地毯上同一個(gè)模塊的輸出可能完全相反這一點(diǎn)必須靠電位器現(xiàn)場(chǎng)整定。避障用的也是紅外反射式模塊檢測(cè)距離可調(diào)。這里有個(gè)容易被忽略的差別循跡模塊是貼地近距離檢測(cè)避障模塊是朝前十幾厘米檢測(cè)兩者雖然都是紅外反射原理但發(fā)射功率和比較器閾值完全不同不能混用同一型號(hào)。至于電機(jī)驅(qū)動(dòng)L298N 和 TB6612 都行區(qū)別在壓降和效率對(duì)比項(xiàng)L298NTB6612FNG導(dǎo)通壓降約 1.42 V電池電壓低時(shí)明顯掉速約 0.5 V低壓差效率與發(fā)熱發(fā)熱大長(zhǎng)時(shí)間跑需要散熱片發(fā)熱小可長(zhǎng)時(shí)間連續(xù)工作邏輯電平5 V 邏輯3.3 V 主控需注意電平匹配兼容 3.3 V 邏輯PWM 頻率上限建議 10 kHz 以內(nèi)可上到 20 kHz 以上適合場(chǎng)景教學(xué)演示、短時(shí)跑動(dòng)長(zhǎng)時(shí)間連續(xù)跑、電池供電我一般建議這套實(shí)驗(yàn)用 TB6612不是因?yàn)橘F而是因?yàn)殡姵貜?8.4 V 掉到 7 V 的時(shí)候L298N 會(huì)先掉 2 V電機(jī)實(shí)際只拿到 5 V速度肉眼可見地變慢而循跡的 PD 參數(shù)是按某個(gè)速度整定的速度一變參數(shù)就全部作廢——這種“跑兩圈就變慢”的問題排查起來(lái)非常磨人。2.3 引腳與外設(shè)資源分配表選型定完之后先把引腳表排出來(lái)避免寫到一半發(fā)現(xiàn)某個(gè)引腳被復(fù)用沖突。下面這張表是這套方案實(shí)際用的分配可以直接照著接線功能引腳模式/外設(shè)備注循跡 最左PB0GPIO 輸入上拉權(quán)重 -4循跡 左PB1GPIO 輸入上拉權(quán)重 -2循跡 中PB2GPIO 輸入上拉權(quán)重 0循跡 右PB3GPIO 輸入上拉默認(rèn) JTDO需關(guān) JTAG循跡 最右PB4GPIO 輸入上拉默認(rèn) NJTRST同上避障 前左PB5GPIO 輸入上拉檢測(cè)到障礙為低避障 前右PB6GPIO 輸入上拉檢測(cè)到障礙為低左電機(jī) PWMPA6TIM3_CH110 kHz左電機(jī) IN1/IN2PA4 / PA5GPIO 推挽輸出方向控制右電機(jī) PWMPA7TIM3_CH210 kHz右電機(jī) IN3/IN4PB12 / PB13GPIO 推挽輸出方向控制調(diào)試串口PA9 / PA10USART1115200打印狀態(tài)1 ms 節(jié)拍TIM2定時(shí)中斷系統(tǒng)時(shí)基啟動(dòng)按鍵PA0GPIO 輸入按下開始跑注意PB3 和 PB4 上電默認(rèn)被 JTAG 占用不關(guān)掉的話讀回來(lái)永遠(yuǎn)是固定值表現(xiàn)為“這兩路傳感器失靈”。初始化里加一句GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);只保留 SWD問題就沒了。這個(gè)坑幾乎每個(gè)第一次用 F103 的人都會(huì)踩一次。3. 總體架構(gòu)設(shè)計(jì)三層結(jié)構(gòu)、兩個(gè)節(jié)拍和一個(gè)狀態(tài)機(jī)引腳定完接下來(lái)是軟件架構(gòu)。小車項(xiàng)目最容易寫成“一個(gè)大 while 循環(huán)里塞滿 if-else”能跑但改一處崩三處。這套方案分成三層加一個(gè)狀態(tài)機(jī)好處是每層都能單獨(dú)測(cè)傳感器可以只打印不驅(qū)動(dòng)電機(jī)電機(jī)可以只給定值不看傳感器狀態(tài)機(jī)可以拿串口喂假數(shù)據(jù)驗(yàn)證切換邏輯。3.1 感知層、決策層、執(zhí)行層的職責(zé)劃分感知層的職責(zé)只有一個(gè)把物理量翻譯成干凈的整數(shù)。五路循跡讀出來(lái)是 5 個(gè) bool經(jīng)過(guò)權(quán)重加權(quán)平均變成 -4 到 4 的偏差值兩路避障讀出來(lái)是兩個(gè) bool但要做 3 次采樣的多數(shù)表決濾掉瞬時(shí)的抖動(dòng)。感知層絕對(duì)不做判斷也不調(diào)用電機(jī)函數(shù)——這條紀(jì)律很重要否則后面想換傳感器就得動(dòng)半個(gè)工程。決策層是整個(gè)方案的大腦包含兩件事控制律和狀態(tài)機(jī)??刂坡奢斎肫睢⑤敵鲛D(zhuǎn)向量狀態(tài)機(jī)輸入避障標(biāo)志和丟線標(biāo)志輸出當(dāng)前應(yīng)該執(zhí)行哪種動(dòng)作。把這兩件事分開寫是因?yàn)樗鼈兊恼{(diào)試方法完全不同控制律看的是曲線和參數(shù)狀態(tài)機(jī)看的是狀態(tài)跳轉(zhuǎn)表。執(zhí)行層負(fù)責(zé)把決策結(jié)果翻譯成電機(jī)能懂的東西給定 -999 到 999 的帶符號(hào)占空比映射成 TIM 比較寄存器的值和兩個(gè)方向引腳的高低電平。執(zhí)行層還要做限幅因?yàn)椴钏贂r(shí)base turn很容易超過(guò)量程。三層的調(diào)用頻率也不一樣。感知層每 10 ms 采一次決策層每 20 ms 算一次執(zhí)行層是跟隨決策層立即輸出的。這個(gè)頻率差不是隨便定的10 ms 采樣配合 0.3 m/s 的車速每 3 mm 走一次采樣五路探頭的間距在 10 mm 量級(jí)采樣間隔遠(yuǎn)小于探頭間距不會(huì)漏掉線。3.2 定時(shí)器節(jié)拍1 ms 基準(zhǔn)怎么切出 10 ms 與 20 ms 任務(wù)裸機(jī)項(xiàng)目里最省事的調(diào)度方式是“1 ms 中斷打拍子主循環(huán)看拍子做事”。TIM2 配置成 1 ms 中斷中斷里只做一件事g_tick。中斷函數(shù)里千萬(wàn)不要讀傳感器、不要算 PID、更不要延時(shí)——中斷里做的事情越多抖動(dòng)越大PWM 輸出的節(jié)拍就越不穩(wěn)。/* TIM272MHz / 72 1MHzARR 999 → 1ms 中斷一次 */ volatile uint32_t g_tick 0; void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); g_tick; /* 中斷里只做計(jì)數(shù)其他全交給主循環(huán) */ } } int main(void) { uint32_t t_sample 0, t_ctrl 0; board_init(); /* 時(shí)鐘、GPIO、PWM、串口、TIM2 */ while (1) { if (g_tick - t_sample 10) { /* 10ms采一次傳感器 */ t_sample g_tick; sensor_scan(); /* 更新 g_err、g_ob_l、g_ob_r */ } if (g_tick - t_ctrl 20) { /* 20ms跑一次決策 */ t_ctrl g_tick; car_step(); /* 狀態(tài)機(jī) 控制律 輸出 */ } debug_report(); /* 空閑時(shí)打印不影響節(jié)拍 */ } }這里用g_tick - t_sample 10而不是g_tick % 10 0是為了防止某次任務(wù)執(zhí)行時(shí)間過(guò)長(zhǎng)導(dǎo)致漏拍——減法寫法即使錯(cuò)過(guò)一個(gè)節(jié)拍下一次也會(huì)立刻補(bǔ)上而取模寫法會(huì)整整丟掉一輪。參數(shù)上10 ms 和 20 ms 是最常用的組合如果場(chǎng)地彎道特別急可以把決策周期壓到 10 ms代價(jià)是串口打印會(huì)擠占時(shí)間調(diào)試信息要相應(yīng)減量。3.3 狀態(tài)機(jī)設(shè)計(jì)循跡態(tài)、避障態(tài)、搜索態(tài)、停車態(tài)怎么切狀態(tài)機(jī)是這個(gè)方案里最值得寫進(jìn)答辯的部分。四個(gè)狀態(tài)跳轉(zhuǎn)條件寫死誰(shuí)也別搶誰(shuí)的活當(dāng)前狀態(tài)跳轉(zhuǎn)條件下一狀態(tài)動(dòng)作循跡左右避障同時(shí)觸發(fā)后退雙輪反轉(zhuǎn) 300 ms循跡僅左側(cè)觸發(fā)右讓右轉(zhuǎn)差速持續(xù) 400 ms循跡僅右側(cè)觸發(fā)左讓左轉(zhuǎn)差速持續(xù) 400 ms循跡偏差值 丟線標(biāo)志搜索沿上次偏差方向原地找線后退障礙消失且計(jì)時(shí)結(jié)束循跡恢復(fù) PD 循跡左讓 / 右讓計(jì)時(shí)結(jié)束且障礙消失循跡恢復(fù) PD 循跡搜索重新檢測(cè)到黑線循跡清空 PD 的歷史誤差任意按鍵長(zhǎng)按 2 s停車PWM 歸零兩個(gè)設(shè)計(jì)細(xì)節(jié)決定了它能不能穩(wěn)定跑。第一是退出條件必須是“計(jì)時(shí)結(jié)束且障礙消失”只寫“障礙消失”會(huì)在障礙物邊沿反復(fù)橫跳車頭就在那里來(lái)回抖。第二是從避障或搜索回到循跡時(shí)必須清空 PD 的last_err否則上一狀態(tài)殘留的歷史誤差會(huì)讓微分項(xiàng)突然打出一個(gè)大輸出車會(huì)猛地甩一下。4. 關(guān)鍵模塊實(shí)現(xiàn)偏差計(jì)算、比例微分循跡與避障優(yōu)先級(jí)架構(gòu)講完落到代碼。這一章四段代碼對(duì)應(yīng)四個(gè)模塊每段都能單獨(dú)燒進(jìn)去驗(yàn)證先只打印偏差確認(rèn)五路讀數(shù)對(duì)得上位置再接控制律最后接電機(jī)。跳過(guò)任何一步后面調(diào)參數(shù)都是盲調(diào)。4.1 五路紅外采樣與加權(quán)偏差計(jì)算/* 極性統(tǒng)一TCRT5000 模塊 DO 壓黑線時(shí)輸出低電平取反成 1 檢測(cè)到黑線 */ #define S_LL (!GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0)) #define S_L (!GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1)) #define S_M (!GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_2)) #define S_R (!GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_3)) #define S_RR (!GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_4)) #define LINE_LOST 99 static const int16_t kWeight[5] { -4, -2, 0, 2, 4 }; /* 位置權(quán)重單位半格 */ /* 返回 -4 ~ 4 的偏差負(fù)數(shù)表示線偏左返回 LINE_LOST 表示五路全白 */ int16_t line_read_error(void) { uint8_t s[5]; int16_t sum 0, cnt 0; s[0] S_LL; s[1] S_L; s[2] S_M; s[3] S_R; s[4] S_RR; for (uint8_t i 0; i 5; i) { if (s[i]) { sum kWeight[i]; cnt; } } if (cnt 0) return LINE_LOST; return sum / cnt; /* 取平均抑制多路同時(shí)壓線造成的偏差跳變 */ }第一段代碼的核心是“極性收斂”和“取平均”兩個(gè)動(dòng)作。極性收斂指的是不管模塊輸出高有效還是低有效都在宏定義這一層統(tǒng)一成“1 檢測(cè)到黑線”這樣后面所有邏輯只認(rèn)一種約定換一批模塊只要改五個(gè)宏。取平均是為了處理一種常見情況車正好壓在兩條線的交匯處或者線寬超過(guò)探頭間距兩三路同時(shí)為 1此時(shí)直接把權(quán)重相加會(huì)得到 -6 這種超出量程的值除法一平均就回到 -2量程始終鎖在 -4 到 4 之內(nèi)。丟線返回 99 是個(gè)約定值比返回 0 好——返回 0 會(huì)被當(dāng)成“線在正中間”車會(huì)保持直行沖出場(chǎng)地返回 99 就能在狀態(tài)機(jī)里單獨(dú)分支處理。4.2 循跡控制律為什么用 PD 而不是純比例typedef struct { float kp; float kd; int16_t last_err; int16_t out_limit; } pd_t; static pd_t g_pd { 12.0f, 30.0f, 0, 320 }; /* 初始參數(shù)需按實(shí)車整定 */ /* 輸入偏差輸出轉(zhuǎn)向量帶符號(hào)已限幅 */ int16_t track_output(int16_t err) { int16_t diff; float out; diff err - g_pd.last_err; g_pd.last_err err; out g_pd.kp * err g_pd.kd * diff; if (out g_pd.out_limit) out g_pd.out_limit; if (out -g_pd.out_limit) out -g_pd.out_limit; return (int16_t)out; } /* 差速執(zhí)行base 是基礎(chǔ)速度turn 是轉(zhuǎn)向修正 */ void track_run(int16_t err) { int16_t base 250; /* 0 ~ 999 */ int16_t turn track_output(err); motor_set(MOTOR_L, base turn); motor_set(MOTOR_R, base - turn); }只用比例項(xiàng)P的問題是明顯的偏差一出現(xiàn)就立刻修正修正過(guò)頭就反向超調(diào)車在直道上左右小幅擺動(dòng)也就是俗稱的“畫龍”。微分項(xiàng)D看的是偏差的變化量偏差快速變化時(shí)提前給一個(gè)反向的抑制量等效于給系統(tǒng)加了阻尼。這套參數(shù)里kp 12、kd 30是初始值實(shí)際整定順序是先把kd設(shè)為 0kp從 5 開始往上加加到車在直道上剛好開始小幅擺動(dòng)然后把kp退回一檔開始加kd加到擺動(dòng)消失為止。out_limit設(shè)成 320 而不是 999是因?yàn)樗鷅ase 250配合——如果限幅太寬一次修正就能讓一側(cè)輪子反轉(zhuǎn)車會(huì)原地打轉(zhuǎn)。經(jīng)驗(yàn)上把out_limit控制在base的 1.2 倍左右即單側(cè)輪子最多減到接近 0 但不會(huì)反轉(zhuǎn)。4.3 避障優(yōu)先級(jí)與轉(zhuǎn)向策略#define OB_L (!GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_5)) /* 檢測(cè)到障礙為 1 */ #define OB_R (!GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_6)) typedef enum { ST_TRACK, ST_BACK, ST_AVOID_L, ST_AVOID_R, ST_SEARCH, ST_STOP } state_t; static state_t g_state ST_TRACK; static uint16_t g_state_tick 0; /* 狀態(tài)內(nèi)計(jì)時(shí)單位 20ms */ /* 每 20ms 調(diào)用一次返回新狀態(tài) */ state_t car_sm_step(uint8_t ob_l, uint8_t ob_r, int16_t err) { g_state_tick; switch (g_state) { case ST_TRACK: if (ob_l ob_r) { g_state_tick 0; return ST_BACK; } if (ob_l) { g_state_tick 0; return ST_AVOID_R; } if (ob_r) { g_state_tick 0; return ST_AVOID_L; } if (err LINE_LOST){ g_state_tick 0; return ST_SEARCH; } return ST_TRACK; case ST_BACK: /* 后退 15 拍 300ms */ if (g_state_tick 15 !ob_l !ob_r) return ST_TRACK; return ST_BACK; case ST_AVOID_R: /* 右讓 20 拍 400ms */ if (g_state_tick 20 !ob_l) { g_pd.last_err 0; return ST_TRACK; } return ST_AVOID_R; case ST_AVOID_L: if (g_state_tick 20 !ob_r) { g_pd.last_err 0; return ST_TRACK; } return ST_AVOID_L; case ST_SEARCH: /* 最多找 1.5s找不到就停 */ if (err ! LINE_LOST) { g_pd.last_err 0; return ST_TRACK; } if (g_state_tick 75) return ST_STOP; return ST_SEARCH; default: return ST_STOP; } }這段狀態(tài)機(jī)里有三個(gè)刻意寫進(jìn)去的保護(hù)。第一每個(gè)避障狀態(tài)的退出都要求“計(jì)時(shí)到 障礙消失”兩個(gè)條件同時(shí)成立缺一個(gè)都會(huì)在障礙物邊緣反復(fù)切換。第二ST_AVOID_R的退出只判斷!ob_l不判斷!ob_r因?yàn)橄蛴易尩臅r(shí)候右側(cè)傳感器可能會(huì)掃到新的障礙如果兩個(gè)都要求消失車會(huì)卡死在讓行狀態(tài)。第三搜索狀態(tài)有超時(shí)上限1.5 s 還找不到線就停車避免車在原地?zé)o限轉(zhuǎn)圈燒電機(jī)——這個(gè)上限比“一直找下去”更實(shí)用至少在測(cè)試時(shí)你能看出它停在了哪里。4.4 PWM 調(diào)速與電機(jī)方向控制#define MOTOR_L 0 #define MOTOR_R 1 void motor_pwm_init(void) { GPIO_InitTypeDef gpio; TIM_TimeBaseInitTypeDef tb; TIM_OCInitTypeDef oc; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); gpio.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; /* TIM3_CH1 / CH2 */ gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); /* 72MHz / 72 1MHz 計(jì)數(shù)ARR 99 → PWM 頻率 10kHz */ tb.TIM_Prescaler 71; tb.TIM_Period 99; tb.TIM_CounterMode TIM_CounterMode_Up; tb.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM3, tb); oc.TIM_OCMode TIM_OCMode_PWM1; oc.TIM_OutputState TIM_OutputState_Enable; oc.TIM_OCPolarity TIM_OCPolarity_High; oc.TIM_Pulse 0; TIM_OC1Init(TIM3, oc); TIM_OC2Init(TIM3, oc); TIM_Cmd(TIM3, ENABLE); } /* duty: -999 ~ 999負(fù)數(shù)為反轉(zhuǎn) */ void motor_set(uint8_t ch, int16_t duty) { if (duty 999) duty 999; if (duty -999) duty -999; if (ch MOTOR_L) { GPIO_WriteBit(GPIOA, GPIO_Pin_4, duty 0 ? Bit_RESET : Bit_SET); GPIO_WriteBit(GPIOA, GPIO_Pin_5, duty 0 ? Bit_SET : Bit_RESET); TIM_SetCompare1(TIM3, (uint16_t)(duty 0 ? duty : -duty)); } else { GPIO_WriteBit(GPIOB, GPIO_Pin_12, duty 0 ? Bit_RESET : Bit_SET); GPIO_WriteBit(GPIOB, GPIO_Pin_13, duty 0 ? Bit_SET : Bit_RESET); TIM_SetCompare2(TIM3, (uint16_t)(duty 0 ? duty : -duty)); } }PWM 頻率取 10 kHz 有兩個(gè)理由一是高于人耳可聽范圍的下沿電機(jī)不會(huì)發(fā)出尖銳的嘯叫聲二是 TB6612 這類 MOS 驅(qū)動(dòng)在 10 kHz 時(shí)的開關(guān)損耗可以接受長(zhǎng)時(shí)間跑不會(huì)燙手。如果換成 L298N頻率不建議超過(guò) 10 kHz否則三極管來(lái)不及完全導(dǎo)通效率會(huì)更低。motor_set里把“方向引腳”和“占空比”這兩件事綁在一個(gè)函數(shù)里是為了避免出現(xiàn)“改了占空比忘了改方向”導(dǎo)致電機(jī)反著轉(zhuǎn)的問題。傳入負(fù)占空比時(shí)方向引腳翻轉(zhuǎn)、比較寄存器取絕對(duì)值邏輯上單調(diào)接線反了也只需要在調(diào)用處加個(gè)負(fù)號(hào)。5. 避坑與排查這套小車上最容易翻車的五個(gè)點(diǎn)前面四章講的是“應(yīng)該怎么寫”這一章講的是“實(shí)際會(huì)怎么壞”。這五個(gè)問題在我自己搭和幫別人調(diào)的過(guò)程中反復(fù)出現(xiàn)每一個(gè)都有明確的現(xiàn)象、成因和解法照著對(duì)號(hào)入座能省掉大量時(shí)間。5.1 一上電就往一邊猛沖或者讀出來(lái)的傳感器永遠(yuǎn)不變現(xiàn)象車通電后兩個(gè)輪子朝相反方向轉(zhuǎn)或者串口打印出來(lái)的五路循跡值永遠(yuǎn)是1 1 1 1 1。原因前者是電機(jī)方向引腳的電平組合和預(yù)期相反兩個(gè)輪子一個(gè)正轉(zhuǎn)一個(gè)反轉(zhuǎn)后者幾乎都是 PB3、PB4 被 JTAG 占用。F103 復(fù)位后 PB3 默認(rèn)是 JTDO、PB4 默認(rèn)是 NJTRST配置成普通 GPIO 輸入也沒用讀回來(lái)的值固定。解決方向問題不要靠改接線在motor_set里把對(duì)應(yīng)通道的 IN1/IN2 互換即可接線保持整齊便于排查。JTAG 問題在 GPIO 初始化之前加一句GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);只保留 SWD 下載口燒錄不受影響。5.2 直道跑得很穩(wěn)一進(jìn)彎道就沖出去現(xiàn)象場(chǎng)地直道上小車走得筆直進(jìn)入半徑 30 cm 的彎道后車頭貼著外側(cè)沖出去糾正不回來(lái)。原因三種可能按概率排序是控制周期太長(zhǎng)、kp偏小、差速飽和限幅太窄??刂浦芷?20 ms 時(shí)車速 0.4 m/s 每拍走 8 mm彎道上的偏差累積得很快等算出來(lái)已經(jīng)晚了kp太小則修正力度不夠out_limit太小則想修正也修不動(dòng)。解決先把決策周期壓到 10 ms 試一次如果明顯改善就是周期問題。否則把out_limit從 320 放到 400再看彎道表現(xiàn)最后才動(dòng)kp。順序很重要——先放限幅再調(diào)增益不然你會(huì)以為kp不夠一路加大結(jié)果直道上開始畫龍。5.3 避障模塊在深色地面上一直誤報(bào)現(xiàn)象場(chǎng)地?fù)Q到深色地膠或者黑色桌面上避障模塊不管前面有沒有東西都輸出“有障礙”小車一直在后退。原因紅外反射式模塊靠接收反射光強(qiáng)判斷距離深色表面吸光接收端收到的光很弱比較器就判定為“沒有反射 → 有障礙”。這不是模塊壞了是原理決定的。解決一是現(xiàn)場(chǎng)重新調(diào)電位器把閾值調(diào)到實(shí)際能用的位置二是給模塊加個(gè)遮光筒用黑色熱縮管剪一段套在探頭外面擋住環(huán)境光干擾三是軟件上做多數(shù)表決連續(xù) 3 次采樣有 2 次以上為 1 才算真障礙。第三條最省事代價(jià)是響應(yīng)延遲一個(gè)采樣周期10 ms 完全能接受。5.4 電機(jī)一轉(zhuǎn)單片機(jī)就復(fù)位現(xiàn)象空載時(shí)串口打印正常一旦給電機(jī)加占空比單片機(jī)隨機(jī)復(fù)位串口輸出亂碼或者程序跑飛。原因電機(jī)啟動(dòng)瞬間電流是堵轉(zhuǎn)電流的量級(jí)電池內(nèi)阻上產(chǎn)生壓降主控的供電被拉低到復(fù)位閾值以下。另外電機(jī)是感性負(fù)載換向時(shí)會(huì)產(chǎn)生反向尖峰通過(guò)電源和地線串回主控。解決硬件上電機(jī)電源和主控電源分開走線只在電池端單點(diǎn)共地電機(jī)兩端并 0.1 μF 電容加一個(gè)續(xù)流二極管主控電源入口加 100 μF 電解電容和 0.1 μF 陶瓷電容。軟件上啟動(dòng)時(shí)讓占空比斜坡上升每 20 ms 加 50而不是直接從 0 跳到 250沖擊電流能小一半。這套小車上電就復(fù)位的問題九成是這兩條之一沒做到。5.5 避障結(jié)束回不了線在原地轉(zhuǎn)圈現(xiàn)象小車成功繞開障礙物但之后一直在原地左右轉(zhuǎn)找不到黑線最后超時(shí)停車。原因繞障動(dòng)作把車帶離黑線太遠(yuǎn)五路探頭全部落在白區(qū)返回LINE_LOST而搜索邏輯只是原地轉(zhuǎn)轉(zhuǎn)向方向和線的實(shí)際位置相反。另外g_pd.last_err沒有清零微分項(xiàng)一上來(lái)就是一個(gè)大值。解決搜索時(shí)不要原地轉(zhuǎn)改成“沿著繞障的反方向小半徑畫弧”也就是右讓之后向左找左讓之后向右找這個(gè)方向和繞開時(shí)車頭偏轉(zhuǎn)的方向相反命中率最高。同時(shí)把搜索速度降到基礎(chǔ)的 60%慢一點(diǎn)更容易被探頭掃到?;氐骄€之后第一件事是清空l(shuí)ast_err讓 PD 從零開始。6. 驗(yàn)收與答辯用一套可復(fù)現(xiàn)的測(cè)試把車“跑通”講清楚功能做完不等于項(xiàng)目做完驗(yàn)收測(cè)試要能重復(fù)、能記錄、能拿給不懂的人看。我一般按下面這張表跑一遍每項(xiàng)測(cè)三次取最差的那次因?yàn)榇疝q現(xiàn)場(chǎng)發(fā)揮失常是常態(tài)按最差成績(jī)準(zhǔn)備才穩(wěn)。序號(hào)測(cè)試項(xiàng)測(cè)試方法合格線1直道循跡3 m 直線膠帶全速通過(guò)3 次全部不脫線2彎道循跡R30 cm 連續(xù)彎跑 5 圈脫線次數(shù) ≤ 13單側(cè)避障車前左側(cè) 20 cm 放障礙物0.3 s 內(nèi)開始右讓繞過(guò)后 2 s 內(nèi)回到線上4正前避障車前正中 20 cm 放障礙物先后退再轉(zhuǎn)向不撞不卡死5協(xié)同彎道中段放障礙物完成繞障并繼續(xù)循跡全程不超時(shí)停車6掉電重啟連續(xù)上電 20 次20 次均正常啟動(dòng)無(wú)復(fù)位異常測(cè)試時(shí)把串口打印打開每 100 ms 輸出一行err / ob_l / ob_r / state / duty跑完之后把日志導(dǎo)出來(lái)畫一條偏差隨時(shí)間的曲線。這張曲線在答辯時(shí)比任何口頭描述都有說(shuō)服力——評(píng)委一眼能看出來(lái)你的系統(tǒng)在彎道上是“提前修正”還是“撞上去才修”這兩種的控制水平完全不同。答辯講述我建議按“工況 → 取舍 → 一次翻車”的順序走不要從技術(shù)棧開始講。先說(shuō)清楚這套車分了哪三種工況、每種工況的驗(yàn)收口徑是什么這能證明你想過(guò)需求再說(shuō)你做了什么取舍比如為什么用五路紅外不用攝像頭、為什么用 TB6612 不用 L298N、為什么控制周期是 20 ms 而不是 5 ms每個(gè)取舍都要落到具體的數(shù)字上最后講一次你踩過(guò)的坑和怎么定位的比如“車一進(jìn)彎就沖出去我一開始以為是 kp 不夠加了兩輪之后直道開始畫龍才回頭去查控制周期發(fā)現(xiàn) 20 ms 下每拍走 8 mm改成 10 ms 就好了”。這個(gè)故事里包含了現(xiàn)象、錯(cuò)誤假設(shè)、糾正過(guò)程比背概念更能說(shuō)明你真的動(dòng)手做過(guò)。從那以后我每次做這種帶傳感器的車類項(xiàng)目都會(huì)在寫控制邏輯之前先把“只打印不驅(qū)動(dòng)”的版本跑通讓串口把五路原始值、兩路避障值、偏差值一起打出來(lái)人工推著車沿場(chǎng)地走一遍確認(rèn)每一路的跳變位置和物理位置對(duì)得上。這一步花不了二十分鐘但能擋掉后面八成莫名其妙的“算法不收斂”——畢竟傳感器讀錯(cuò)了再好的 PD 參數(shù)也是白搭。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取