PID調(diào)節(jié)實(shí)戰(zhàn):從電感采集到舵機(jī)打角閉環(huán))
簡(jiǎn)介這份資源面向參加恩智浦智能車(chē)競(jìng)賽及學(xué)習(xí)嵌入式控制的學(xué)生與工程師聚焦基于STM32的四輪電磁循跡小車(chē)PID調(diào)節(jié)方案幫助解決電感采集、舵機(jī)與電機(jī)協(xié)同控制中的參數(shù)整定難題。壓縮包共192個(gè)文件約5.76MB以C源碼與頭文件為主體涵蓋電感信號(hào)采集、MPU6050姿態(tài)解算、LCD顯示、定時(shí)器與Flash驅(qū)動(dòng)等模塊并附帶Keil工程文件、編譯中間產(chǎn)物及可燒錄的hex固件便于直接編譯驗(yàn)證與二次開(kāi)發(fā)。目前已有4689人學(xué)習(xí)下載熱度較高。資源完整呈現(xiàn)了從傳感器數(shù)據(jù)讀取到PID輸出控制舵機(jī)角度與電機(jī)轉(zhuǎn)速的實(shí)現(xiàn)鏈路讀者可據(jù)此理解比例、積分、微分三項(xiàng)對(duì)循跡穩(wěn)定性的影響并參考工程結(jié)構(gòu)快速搭建調(diào)試環(huán)境是備賽與課程實(shí)踐中較實(shí)用的參考案例。1. 電磁循跡小車(chē)PID調(diào)節(jié)從電感采集到舵機(jī)打角的閉環(huán)實(shí)戰(zhàn)很多人第一次接觸電磁循跡都會(huì)卡在同一個(gè)地方電感值讀回來(lái)了濾波也做了歸一化也算了可小車(chē)一上賽道要么畫(huà)龍要么在直道突然甩頭。問(wèn)題往往不在算法本身而在 PID 參數(shù)和采集鏈路沒(méi)有對(duì)齊。這份資源圍繞恩智浦智能車(chē)競(jìng)賽里最常見(jiàn)的電磁組方案展開(kāi)核心是 STM32 主控 工字電感采集 舵機(jī)轉(zhuǎn)向 雙電機(jī)差速把「電感采集 → 偏差計(jì)算 → PID 輸出 → 舵機(jī)打角」這條閉環(huán)鏈路完整跑通。適合正在調(diào)車(chē)、準(zhǔn)備校賽、或者想把電磁循跡方案移植到自己板子上的開(kāi)發(fā)者尤其是那些 PID 調(diào)了半天還是抖的選手。2. 硬件鏈路與工程結(jié)構(gòu)先看清信號(hào)從哪來(lái)、到哪去2.1 電磁循跡的物理鏈路電磁循跡的本質(zhì)是賽道中心通電導(dǎo)線產(chǎn)生交變磁場(chǎng)車(chē)頭布置的工字電感感應(yīng)出微弱交流信號(hào)經(jīng)過(guò)選頻、放大、檢波后變成直流電壓再送進(jìn) STM32 的 ADC。常見(jiàn)做法是左右各一個(gè)水平電感中間一個(gè)豎直電感三個(gè)通道一起判斷車(chē)體相對(duì)導(dǎo)線的橫向偏移和方向。這里有個(gè)容易被忽略的點(diǎn)電感采集的不是「位置」而是「磁場(chǎng)強(qiáng)度」。水平電感離導(dǎo)線越近感應(yīng)電壓越大豎直電感在導(dǎo)線正上方時(shí)感應(yīng)最強(qiáng)偏離后迅速衰減。所以偏差計(jì)算不是簡(jiǎn)單做差而是要先歸一化再根據(jù)賽道寬度和電感布局做加權(quán)。工程里一般會(huì)這樣組織采集鏈路// 電感采集與預(yù)處理常見(jiàn)于 ADC 中斷或定時(shí)任務(wù)中 #define INDUCTOR_NUM 3 #define FILTER_DEPTH 8 static uint16_t adc_raw[INDUCTOR_NUM]; static uint32_t filter_buf[INDUCTOR_NUM][FILTER_DEPTH]; static uint8_t filter_idx 0; // 滑動(dòng)平均濾波抑制電磁信號(hào)的高頻抖動(dòng) void inductor_sample(void) { for (int i 0; i INDUCTOR_NUM; i) { adc_raw[i] adc_read_channel(i); // 讀取原始 ADC filter_buf[i][filter_idx] adc_raw[i]; } filter_idx (filter_idx 1) % FILTER_DEPTH; } uint16_t inductor_get(uint8_t ch) { uint32_t sum 0; for (int i 0; i FILTER_DEPTH; i) { sum filter_buf[ch][i]; } return (uint16_t)(sum / FILTER_DEPTH); // 返回濾波后的值 }這段代碼的關(guān)鍵在于FILTER_DEPTH決定平滑程度太大響應(yīng)慢太小抖動(dòng)大。電磁組常用 8 到 16 之間的滑動(dòng)窗口。adc_read_channel需要根據(jù)實(shí)際板子的 ADC 通道映射來(lái)寫(xiě)注意采樣時(shí)間要夠長(zhǎng)否則高阻信號(hào)源會(huì)采不準(zhǔn)。2.2 工程目錄與模塊劃分拿到這份資源后先別急著編譯。打開(kāi)目錄看一眼結(jié)構(gòu)能省掉后面很多找文件的麻煩。典型的電磁循跡工程會(huì)按下面這種方式組織目錄/文件作用Core/Src/main.c主循環(huán)、初始化、任務(wù)調(diào)度Core/Src/adc.cADC 多通道采集配置Core/Src/tim.cPWM 輸出、編碼器接口、定時(shí)中斷Hardware/inductor.c電感濾波、歸一化、偏差計(jì)算Hardware/motor.c電機(jī) PWM 與方向控制Hardware/servo.c舵機(jī)打角控制Control/pid.cPID 結(jié)構(gòu)體與計(jì)算函數(shù)Control/control.c循跡主控制邏輯這種劃分的好處是采集、執(zhí)行、控制三層分開(kāi)調(diào) PID 的時(shí)候只動(dòng)Control目錄不會(huì)把底層驅(qū)動(dòng)攪亂。如果你用的是 CubeMX 生成的工程Core目錄基本是自動(dòng)生成的Hardware和Control是自己加的移植時(shí)把這兩個(gè)目錄拷過(guò)去再改一下引腳定義就能用。2.3 偏差計(jì)算歸一化比濾波更重要很多人濾波做得很漂亮偏差還是跳原因是沒(méi)做歸一化。電感值會(huì)隨電池電壓、賽道高度、環(huán)境溫度變化直接拿原始值做差今天調(diào)好的參數(shù)明天就廢了。常見(jiàn)做法是先把三個(gè)電感值歸一化到 0 到 1000 之間// 歸一化把電感值映射到統(tǒng)一量綱消除供電和環(huán)境漂移 #define NORM_MAX 1000 static uint16_t norm[INDUCTOR_NUM]; void inductor_normalize(void) { uint16_t max_val 0, min_val 0xFFFF; for (int i 0; i INDUCTOR_NUM; i) { uint16_t v inductor_get(i); if (v max_val) max_val v; if (v min_val) min_val v; } if (max_val - min_val 50) { // 防止除零和全暗場(chǎng)景 for (int i 0; i INDUCTOR_NUM; i) norm[i] 0; return; } for (int i 0; i INDUCTOR_NUM; i) { norm[i] (uint32_t)(inductor_get(i) - min_val) * NORM_MAX / (max_val - min_val); } }歸一化之后再用左右水平電感的差值算橫向偏差用豎直電感判斷是否在十字或環(huán)島區(qū)域。偏差公式常見(jiàn)的是// 偏差計(jì)算左電感減右電感正負(fù)代表偏移方向 float error (float)norm[LEFT] - (float)norm[RIGHT];如果車(chē)頭電感布局不對(duì)稱(chēng)還要加一個(gè)加權(quán)系數(shù)。這個(gè)系數(shù)不是拍腦袋定的而是把車(chē)放在賽道中心讀幾次左右電感值取比值作為基準(zhǔn)。3. PID 調(diào)節(jié)實(shí)戰(zhàn)從參數(shù)整定到舵機(jī)打角3.1 位置式 PID 的結(jié)構(gòu)與實(shí)現(xiàn)電磁循跡里最常用的是位置式 PID輸出直接對(duì)應(yīng)舵機(jī)打角。積分項(xiàng)用來(lái)消除靜態(tài)偏差微分項(xiàng)用來(lái)抑制過(guò)沖。工程里一般會(huì)把 PID 參數(shù)做成結(jié)構(gòu)體方便在線調(diào)// pid.h typedef struct { float kp; float ki; float kd; float integral; float prev_error; float integral_limit; // 積分限幅防止飽和 float output_limit; // 輸出限幅保護(hù)舵機(jī) } PID_Handle; float pid_calc(PID_Handle *pid, float error, float dt);// pid.c float pid_calc(PID_Handle *pid, float error, float dt) { float derivative; pid-integral error * dt; if (pid-integral pid-integral_limit) pid-integral pid-integral_limit; else if (pid-integral -pid-integral_limit) pid-integral -pid-integral_limit; derivative (error - pid-prev_error) / dt; pid-prev_error error; float output pid-kp * error pid-ki * pid-integral pid-kd * derivative; if (output pid-output_limit) output pid-output_limit; else if (output -pid-output_limit) output -pid-output_limit; return output; }dt是控制周期電磁組常用 5ms 到 10ms。integral_limit一般取輸出限幅的 1/3 到 1/2太大積分會(huì)拖慢響應(yīng)太小消不掉靜差。output_limit對(duì)應(yīng)舵機(jī)最大打角比如 ±300具體看舵機(jī)行程和機(jī)械結(jié)構(gòu)。3.2 參數(shù)整定的順序與血淚經(jīng)驗(yàn)調(diào) PID 最忌諱三個(gè)參數(shù)一起動(dòng)。我一般按這個(gè)順序來(lái)先只開(kāi)kpki和kd置零。從小往大加直到小車(chē)開(kāi)始等幅振蕩記下這個(gè)臨界值然后取它的 60% 到 70% 作為初始kp。這一步?jīng)Q定小車(chē)能不能大致跟著線走。然后加kd。微分項(xiàng)是抑制振蕩的關(guān)鍵電磁組里kd往往比kp還大。加kd的時(shí)候觀察直道末端和入彎瞬間如果車(chē)頭開(kāi)始「提前打角」說(shuō)明微分起作用了如果出現(xiàn)高頻抖動(dòng)說(shuō)明kd太大把電感噪聲放大了。最后加ki而且要給得很小。電磁循跡對(duì)靜態(tài)偏差不敏感ki主要是消除彎道里的持續(xù)偏移。很多翻車(chē)案例都是ki給大了直道上積分飽和入彎直接甩出去。提示每次改完參數(shù)至少跑三圈再判斷。單圈偶然性太大電池電壓變化、賽道光線都會(huì)影響手感。3.3 舵機(jī)與電機(jī)的協(xié)同舵機(jī)負(fù)責(zé)轉(zhuǎn)向電機(jī)負(fù)責(zé)速度兩者必須配合。常見(jiàn)做法是舵機(jī) PID 輸出打角電機(jī)用編碼器做速度閉環(huán)直道加速、彎道減速。如果只調(diào)舵機(jī)不管速度入彎速度太高再好的 PID 也拉不回來(lái)。速度環(huán)一般用增量式 PI// 增量式 PI用于電機(jī)速度閉環(huán) typedef struct { float kp; float ki; float prev_error; float output; } SpeedPI; float speed_pi_calc(SpeedPI *pi, float target, float actual) { float error target - actual; pi-output pi-kp * (error - pi-prev_error) pi-ki * error; pi-prev_error error; if (pi-output 1000) pi-output 1000; if (pi-output -1000) pi-output -1000; return pi-output; }速度目標(biāo)值可以根據(jù)舵機(jī)打角動(dòng)態(tài)調(diào)整打角越大目標(biāo)速度越低。這樣入彎自動(dòng)減速出彎自動(dòng)加速比固定速度穩(wěn)得多。4. 避坑與排查調(diào)車(chē)現(xiàn)場(chǎng)最常見(jiàn)的五個(gè)問(wèn)題4.1 電感值跳變車(chē)在直道畫(huà)龍現(xiàn)象是直道上舵機(jī)左右小幅擺動(dòng)車(chē)頭走 S 形。原因通常是電感采集沒(méi)有濾波或者濾波窗口太小ADC 噪聲直接進(jìn)了 PID。解決方法是加大滑動(dòng)平均窗口或者在 ADC 后加一級(jí)一階低通濾波。另外檢查電感運(yùn)放電路的供電是否干凈電機(jī) PWM 干擾會(huì)串進(jìn)模擬通道。4.2 入彎反應(yīng)慢車(chē)沖出去現(xiàn)象是直道正常一到彎道就來(lái)不及打角。原因多半是kd太小或者控制周期太長(zhǎng)。電磁組建議控制周期不超過(guò) 10ms最好 5ms。如果kd已經(jīng)很大還是慢檢查偏差計(jì)算是不是用了濾波后的滯后值歸一化窗口太長(zhǎng)也會(huì)拖響應(yīng)。4.3 舵機(jī)抖動(dòng)發(fā)出嗡嗡聲現(xiàn)象是舵機(jī)在原地高頻抖動(dòng)車(chē)不動(dòng)或者走得很別扭。原因是 PID 輸出變化太快或者kd把噪聲放大了。解決方法是給 PID 輸出加一階低通或者降低kd同時(shí)檢查舵機(jī)供電是否獨(dú)立電機(jī)電流拉低電壓會(huì)導(dǎo)致舵機(jī)復(fù)位。4.4 電池電壓下降后參數(shù)失效現(xiàn)象是剛充滿(mǎn)電跑得好跑幾圈后開(kāi)始抖或者反應(yīng)遲鈍。原因是電感歸一化沒(méi)有做或者速度環(huán)目標(biāo)值沒(méi)有隨電壓補(bǔ)償。解決方法是確保歸一化在每次控制周期都執(zhí)行速度環(huán)可以加一個(gè)電壓前饋電池電壓低時(shí)適當(dāng)降低目標(biāo)速度。4.5 十字和環(huán)島誤判現(xiàn)象是直道過(guò)十字時(shí)舵機(jī)突然打角或者環(huán)島進(jìn)不去。原因是只靠水平電感算偏差沒(méi)有用豎直電感做區(qū)域判斷。常見(jiàn)做法是豎直電感值超過(guò)閾值時(shí)進(jìn)入特殊區(qū)域處理十字區(qū)域強(qiáng)制直行環(huán)島區(qū)域按固定方向打角直到出環(huán)。注意特殊區(qū)域的閾值要在賽道上實(shí)測(cè)不同賽道高度和導(dǎo)線電流都會(huì)影響別直接抄別人的數(shù)。5. 二次開(kāi)發(fā)與移植把這套方案改成你自己的車(chē)5.1 換主控怎么移植這套工程的核心邏輯在Hardware和Control兩個(gè)目錄和 STM32 的耦合主要在 ADC 讀取、PWM 輸出和定時(shí)器中斷。換到別的 MCU 時(shí)只需要重寫(xiě)這三個(gè)接口接口作用移植要點(diǎn)adc_read_channel讀電感 ADC改成目標(biāo)平臺(tái)的 ADC 驅(qū)動(dòng)pwm_set_duty設(shè)電機(jī)/舵機(jī) PWM注意頻率和分辨率get_dt獲取控制周期用定時(shí)器或系統(tǒng)滴答PID 和歸一化邏輯是純 C直接拷過(guò)去就能用。我一般會(huì)先把這三個(gè)接口用空函數(shù)占位編譯通過(guò)后再逐個(gè)填實(shí)現(xiàn)這樣能快速定位是驅(qū)動(dòng)問(wèn)題還是算法問(wèn)題。5.2 改電感布局后的參數(shù)重調(diào)如果你把三個(gè)電感改成五個(gè)或者把水平電感換成斜置偏差公式和 PID 參數(shù)都要重調(diào)。偏差公式可以改成加權(quán)和// 五電感布局的偏差計(jì)算示例 float error 0; error norm[FAR_LEFT] * (-2.0f); error norm[LEFT] * (-1.0f); error norm[RIGHT] * ( 1.0f); error norm[FAR_RIGHT] * ( 2.0f); // 中間豎直電感不參與偏差只做區(qū)域判斷權(quán)重系數(shù)根據(jù)電感間距和賽道寬度定間距越大外側(cè)權(quán)重越高。改完布局后kp和kd都要重新從小往大調(diào)不能沿用舊參數(shù)。5.3 加遙測(cè)把黑匣子打開(kāi)調(diào)車(chē)最痛苦的是不知道 PID 中間量。我習(xí)慣在工程里加一個(gè)簡(jiǎn)單的遙測(cè)通過(guò)串口把偏差、PID 輸出、電感值發(fā)到上位機(jī)。不用很復(fù)雜一個(gè) 10ms 的定時(shí)任務(wù)把幾個(gè) float 打包發(fā)出去就行// 簡(jiǎn)易遙測(cè)每 10ms 發(fā)一幀 void telemetry_send(void) { uint8_t buf[16]; int16_t err (int16_t)(pid_steer.prev_error * 100); int16_t out (int16_t)(servo_output); int16_t ind (int16_t)(norm[LEFT] - norm[RIGHT]); memcpy(buf[0], err, 2); memcpy(buf[2], out, 2); memcpy(buf[4], ind, 2); uart_send(buf, 6); }上位機(jī)用串口示波器一畫(huà)PID 哪一項(xiàng)在抖、哪個(gè)電感先響應(yīng)一目了然。這比盯著車(chē)猜快十倍。5.4 一個(gè)我踩過(guò)的坑最早調(diào)車(chē)的時(shí)候我總覺(jué)得 PID 參數(shù)是「調(diào)」出來(lái)的后來(lái)才發(fā)現(xiàn)是「測(cè)」出來(lái)的。有一次車(chē)在直道怎么都調(diào)不穩(wěn)換了三組參數(shù)都沒(méi)用最后用遙測(cè)一看左電感在電機(jī)啟動(dòng)瞬間會(huì)掉 30% 的值原因是電感和電機(jī)共用了一路電源電機(jī)電流把運(yùn)放供電拉低了。把電感供電單獨(dú)加一級(jí) LDO 之后同樣的 PID 參數(shù)直接就跑穩(wěn)了。從那以后我每次調(diào)車(chē)都強(qiáng)制走一遍先看遙測(cè)確認(rèn)采集鏈路干凈再動(dòng) PID。參數(shù)不是玄學(xué)是采集質(zhì)量的鏡子。希望這份資源能幫你少走點(diǎn)彎路把車(chē)先跑起來(lái)再談?wù){(diào)得快不快。本文還有配套的精品資源點(diǎn)擊獲取