控制:角度采樣與PID調(diào)參實(shí)踐)
簡(jiǎn)介《自由擺的控制系統(tǒng)》是一份面向電子設(shè)計(jì)競(jìng)賽、嵌入式控制課程設(shè)計(jì)及自動(dòng)控制學(xué)習(xí)者的完整方案文檔圍繞擺桿擺動(dòng)過(guò)程中保持末端平板穩(wěn)定這一問(wèn)題展開(kāi)可作為動(dòng)態(tài)角度檢測(cè)與電機(jī)隨動(dòng)控制的參考范例。文檔共1個(gè)docx文件壓縮包約36KB篇幅精煉但內(nèi)容完整便于直接查閱與二次整理。內(nèi)容以STM32F103VET6為主控涵蓋TFT液晶顯示、WDD35D4導(dǎo)電塑料角位移傳感器實(shí)時(shí)測(cè)角、A/D采樣反饋、四宏細(xì)分驅(qū)動(dòng)器驅(qū)動(dòng)步進(jìn)電機(jī)等關(guān)鍵環(huán)節(jié)并給出擺架框架、檢測(cè)系統(tǒng)、控制驅(qū)動(dòng)系統(tǒng)三部分結(jié)構(gòu)。文中還對(duì)控制器、角度檢測(cè)、電機(jī)與電源模塊進(jìn)行方案比較說(shuō)明選型依據(jù)并涉及硬幣相對(duì)靜止、激光筆打靶等基本要求與發(fā)揮部分適合讀者借鑒硬件搭建、控制思路和方案論證寫法。目前已有308人學(xué)習(xí)下載。1. 自由擺不是“擺著玩”這套平板控制系統(tǒng)到底解決什么問(wèn)題擺桿一松手末端平板跟著來(lái)回?cái)[板上硬幣不能滾、激光筆還得穩(wěn)穩(wěn)打到靶心——自由擺的控制系統(tǒng)就是為這個(gè)場(chǎng)景做的。它用 STM32F103VET6 當(dāng)控制核心WDD35D4 導(dǎo)電塑料角位移傳感器實(shí)時(shí)測(cè)擺桿和平板角度TFT 液晶做狀態(tài)顯示步進(jìn)電機(jī)經(jīng)四宏細(xì)分驅(qū)動(dòng)器帶動(dòng)平板轉(zhuǎn)角。適合正在做嵌入式課程設(shè)計(jì)、控制類學(xué)科競(jìng)賽或者想拿一套完整“傳感器采樣—角度解算—電機(jī)執(zhí)行—閉環(huán)調(diào)參”鏈路練手的人。硬件不算貴但機(jī)械安裝、模擬采樣和步進(jìn)丟步這幾個(gè)環(huán)節(jié)每個(gè)都能讓人調(diào)一晚上。2. 方案與硬件架構(gòu)STM32F103VET6 為核心的三大模塊怎么搭這套系統(tǒng)的骨架拆開(kāi)看很樸素一個(gè)擺架負(fù)責(zé)讓平板自由擺動(dòng)一組傳感器負(fù)責(zé)把角度變成電壓一塊 STM32 負(fù)責(zé)算驅(qū)動(dòng)器加步進(jìn)電機(jī)負(fù)責(zé)把平板拉回目標(biāo)角度。真正花時(shí)間的是選型和電氣連接因?yàn)樽杂蓴[是一個(gè)動(dòng)態(tài)系統(tǒng)傳感器測(cè)的是實(shí)時(shí)角度電機(jī)執(zhí)行又有延遲任何一環(huán)選錯(cuò)后面調(diào) PID 都是白費(fèi)勁。2.1 控制器和電源為什么不用 5112V 電池怎么分壓控制器選型上51 單片機(jī)不是不能跑而是跑不動(dòng)這套東西。自由擺控制需要同時(shí)做兩路 ADC 采樣、中值濾波、角度換算、PID 運(yùn)算、步進(jìn)脈沖狀態(tài)機(jī)和 TFT 刷新51 的指令周期和片內(nèi) RAM 很快就見(jiàn)底稍微把控制周期壓到 5ms 就會(huì)顧此失彼。STM32F103VET6 是 Cortex-M3 內(nèi)核72MHz 主頻512KB Flash、64KB RAM片內(nèi) ADC 多通道用庫(kù)函數(shù)開(kāi)發(fā)也省事這也是項(xiàng)目正文最終選它的原因。這里有個(gè)容易混的點(diǎn)項(xiàng)目正文寫的是“8 通道 10 位 ADC”實(shí)際 STM32F103 的 ADC 是 12 位初始化時(shí)按 12 位右對(duì)齊配滿量程用 4095如果手里的例程是 10 位版本換算系數(shù)要改成 1023兩套代碼混用角度會(huì)直接差 4 倍。電源部分原文給的是 12V 電池經(jīng) L7805 和 LM2576 轉(zhuǎn)換后給系統(tǒng)供電。L7805 是線性穩(wěn)壓輸出 5V紋波小但壓差大時(shí)發(fā)熱明顯LM2576 是開(kāi)關(guān)降壓效率高適合把 12V 先降到一個(gè)中間電壓。常見(jiàn)接法是12V 電池直接進(jìn)步進(jìn)驅(qū)動(dòng)器的主電源同時(shí)經(jīng) LM2576 降到 6.5V7V再進(jìn) L7805 穩(wěn)到 5V5V 給 TFT、傳感器和電機(jī)驅(qū)動(dòng)器邏輯3.3V 由板載 LDO 給 STM32 和 ADC 參考。模擬部分最好單獨(dú)一路 LC 濾波不要和電機(jī)驅(qū)動(dòng)邏輯共用一根細(xì)地線否則 ADC 讀數(shù)會(huì)跟著脈沖頻率跳。供電軌來(lái)源供給對(duì)象注意點(diǎn)12V電池步進(jìn)驅(qū)動(dòng)器主電源并 470μF 電解電容抑制啟停跌落6.57VLM2576 降壓L7805 輸入開(kāi)關(guān)頻率噪聲要加 LC 濾波5VL7805 線性穩(wěn)壓TFT、WDD35D4、驅(qū)動(dòng)器邏輯加散熱片電流余量留 30%3.3V板載 LDOSTM32、ADC 參考模擬地和數(shù)字地單點(diǎn)連接引腳分配我一般先落一個(gè)頭文件后面 ADC、定時(shí)器、TFT 都按這個(gè)表來(lái)改板子只改這里。下面這段只放關(guān)鍵引腳完整工程里還會(huì)把按鍵、串口和限位開(kāi)關(guān)加進(jìn)去。/* board_pinmap.h —— 自由擺平板控制板引腳分配 */ #ifndef BOARD_PINMAP_H #define BOARD_PINMAP_H #include stm32f1xx_hal.h /* WDD35D4 兩路模擬輸出一路測(cè)擺桿一路測(cè)平板 */ #define SENSOR_PLATE_PORT GPIOA #define SENSOR_PLATE_PIN GPIO_PIN_0 /* ADC1_IN0平板傾角 */ #define SENSOR_ARM_PORT GPIOA #define SENSOR_ARM_PIN GPIO_PIN_1 /* ADC1_IN1擺桿角度 */ /* 步進(jìn)驅(qū)動(dòng)器PUL 走定時(shí)器通道DIR/ENA 普通 GPIO */ #define STEP_PUL_PORT GPIOA #define STEP_PUL_PIN GPIO_PIN_2 /* TIM2_CH3 復(fù)用推挽 */ #define STEP_DIR_PORT GPIOB #define STEP_DIR_PIN GPIO_PIN_0 #define STEP_ENA_PORT GPIOB #define STEP_ENA_PIN GPIO_PIN_1 /* TFT 液晶SPI1 手動(dòng)片選刷新時(shí)不影響控制節(jié)拍 */ #define TFT_CS_PORT GPIOA #define TFT_CS_PIN GPIO_PIN_4 #define TFT_DC_PORT GPIOA #define TFT_DC_PIN GPIO_PIN_5 #define TFT_RST_PORT GPIOA #define TFT_RST_PIN GPIO_PIN_6 #endif這段頭文件的作用是把“哪個(gè)腳接什么”固定下來(lái)。ADC 通道和 GPIO 復(fù)用關(guān)系必須和 CubeMX 里的配置一致比如 PA0 做 ADC1_IN0 時(shí)不能再配成普通輸出PUL 腳要設(shè)成復(fù)用推挽否則定時(shí)器波形出不來(lái)。實(shí)際改板時(shí)最容易翻車的是 DIR 和 ENA 接反上電后平板會(huì)往一個(gè)方向頂死后面避坑章會(huì)細(xì)說(shuō)。2.2 角度傳感器和電機(jī)WDD35D4 與步進(jìn)電機(jī)的組合邏輯測(cè)角方案原文對(duì)比了三種普通電位器、ADXL345 加速度傳感器、WDD35D4 導(dǎo)電塑料角位移傳感器。普通電位器便宜但線性度差來(lái)回磨損后阻值跳變用在自由擺這種需要連續(xù)動(dòng)態(tài)測(cè)量的場(chǎng)合角度噪聲會(huì)直接灌進(jìn) PID。ADXL345 是加速度原理靜態(tài)測(cè)傾角很好用但自由擺擺動(dòng)時(shí)傳感器同時(shí)感受到重力和擺動(dòng)加速度解算出來(lái)的角度在動(dòng)態(tài)段會(huì)偏越擺得快越不準(zhǔn)。WDD35D4 是電阻分壓式角位移傳感器轉(zhuǎn)軸帶動(dòng)電刷在導(dǎo)電塑料膜上滑動(dòng)輸出與角度成比例的電壓屬于絕對(duì)位置傳感器斷電再上電不會(huì)丟零位精度也夠用。它的代價(jià)是電刷有機(jī)械磨損安裝時(shí)要注意同軸度不能讓轉(zhuǎn)軸受側(cè)向力。電機(jī)選的是步進(jìn)電機(jī)而不是伺服。伺服閉環(huán)性能好、高速制動(dòng)強(qiáng)但價(jià)格高配套驅(qū)動(dòng)器也貴步進(jìn)電機(jī)開(kāi)環(huán)控制給脈沖就走16 細(xì)分后每步 1.8°/160.1125°對(duì)于平板轉(zhuǎn)角補(bǔ)償已經(jīng)夠細(xì)。四宏細(xì)分驅(qū)動(dòng)器的作用是把單片機(jī)發(fā)出的脈沖放大成電機(jī)繞組電流同時(shí)做細(xì)分減弱低頻振動(dòng)。這里要算清楚脈沖當(dāng)量如果電機(jī)軸和平板軸直接連接、減速比 1:1那么平板轉(zhuǎn) 1° 需要 1/0.1125≈8.89 個(gè)脈沖如果中間加了一級(jí) 2:1 減速脈沖數(shù)還要乘 2。這個(gè)數(shù)后面 PID 輸出換算和限位保護(hù)都要用不能憑感覺(jué)寫。提示步進(jìn)電機(jī)的保持力矩和驅(qū)動(dòng)器電流撥碼要匹配。電流設(shè)小了帶不動(dòng)平板設(shè)大了電機(jī)發(fā)熱、低速嘯叫常見(jiàn)做法是先按電機(jī)額定電流的 70% 設(shè)再用手輕推平板測(cè)試保持力。2.3 機(jī)械與電氣連接擺架、傳感器、驅(qū)動(dòng)器怎么裝機(jī)械上分三塊擺架框架負(fù)責(zé)支撐擺桿末端裝電機(jī)平板固定在電機(jī)軸上傳感器同軸安裝在平板轉(zhuǎn)軸或擺桿轉(zhuǎn)軸處測(cè)的是實(shí)際轉(zhuǎn)角不是電機(jī)走了多少步。這個(gè)“同軸”很關(guān)鍵傳感器如果通過(guò)齒輪間接測(cè)齒輪間隙會(huì)讓讀數(shù)滯后PID 會(huì)越調(diào)越振。擺桿軸承要順暢不能有卡滯否則自由擺周期會(huì)變控制模型對(duì)不上。平板本身盡量輕硬幣放在板面中心附近激光筆固定在平板邊緣盡量讓激光筆軸線和平板法線平行。電氣連接上WDD35D4 一般三根線供電、地、滑動(dòng)端輸出。STM32 的 ADC 輸入不能超過(guò) 3.3V而傳感器常見(jiàn) 5V 供電、輸出 05V所以中間必須分壓。常見(jiàn)做法是用 10kΩ 和 20kΩ 電阻分壓5V 輸出降到 3.33V 左右再進(jìn) ADC分壓比在代碼里要乘回去。步進(jìn)驅(qū)動(dòng)器一側(cè)PUL 接脈沖DIR 接方向ENA 接使能另外還有共陽(yáng)/共陰接法按驅(qū)動(dòng)器手冊(cè)來(lái)。單片機(jī)和驅(qū)動(dòng)器必須共地否則脈沖電平?jīng)]有參考電機(jī)走位會(huì)隨機(jī)。連接項(xiàng)單片機(jī)側(cè)外設(shè)側(cè)說(shuō)明平板角度PA0 / ADC1_IN0WDD35D4 輸出經(jīng)分壓分壓比 1.5代碼要乘回?cái)[桿角度PA1 / ADC1_IN1第二路傳感器與平板路分開(kāi)標(biāo)定步進(jìn)脈沖PA2 / TIM2_CH3驅(qū)動(dòng)器 PUL脈沖上升沿有效步進(jìn)方向PB0驅(qū)動(dòng)器 DIR高低電平對(duì)應(yīng)正反轉(zhuǎn)步進(jìn)使能PB1驅(qū)動(dòng)器 ENA低電平使能常見(jiàn)TFTSPI1 PA4/5/6液晶模塊片選手動(dòng)控制3. 角度采集鏈路WDD35D4 從模擬電壓到角度值的完整實(shí)現(xiàn)自由擺控制里角度是唯一的反饋量采樣鏈路不穩(wěn)后面 PID 就是在噪聲上做微分電機(jī)一定會(huì)抖。這一章按“接線—采樣—濾波—標(biāo)定—異常檢測(cè)”的順序拆代碼可以直接往工程里搬但參數(shù)要按自己傳感器和機(jī)械結(jié)構(gòu)改。3.1 接線、ADC 配置與采樣節(jié)拍WDD35D4 的輸出是模擬電壓STM32 用 ADC1 的規(guī)則通道掃描兩路平板角度和擺桿角度。采樣節(jié)拍不要隨手用HAL_Delay輪詢那樣控制周期會(huì)被 TFT 刷新和串口打印拖亂。常見(jiàn)做法是用定時(shí)器 TRGO 觸發(fā) ADC1kHz 采樣DMA 把結(jié)果搬到緩沖區(qū)控制任務(wù)只讀最新值。ADC 采樣時(shí)間設(shè)長(zhǎng)一點(diǎn)比如 71.5 個(gè)周期因?yàn)閭鞲衅鬏敵鲎杩辜由戏謮弘娮韬蟛凰愕筒蓸訒r(shí)間太短采樣保持電容充不滿讀數(shù)會(huì)偏低。/* 兩路 ADC DMA 定時(shí)器觸發(fā)1kHz 采樣 */ static void sensor_adc_init(void) { ADC_ChannelConfTypeDef ch {0}; hadc1.Instance ADC1; hadc1.Init.ScanConvMode ADC_SCAN_ENABLE; /* 掃描兩路 */ hadc1.Init.ContinuousConvMode DISABLE; /* 由定時(shí)器觸發(fā) */ hadc1.Init.DiscontinuousConvMode DISABLE; hadc1.Init.ExternalTrigConv ADC_EXTERNALTRIGCONV_T2_TRGO; hadc1.Init.DataAlign ADC_DATAALIGN_RIGHT; /* 右對(duì)齊12 位 */ hadc1.Init.NbrOfConversion 2; HAL_ADC_Init(hadc1); ch.Channel ADC_CHANNEL_0; /* 平板角度 */ ch.Rank ADC_REGULAR_RANK_1; ch.SamplingTime ADC_SAMPLETIME_71CYCLES_5; HAL_ADC_ConfigChannel(hadc1, ch); ch.Channel ADC_CHANNEL_1; /* 擺桿角度 */ ch.Rank ADC_REGULAR_RANK_2; HAL_ADC_ConfigChannel(hadc1, ch); HAL_ADCEx_Calibration_Start(hadc1); /* 上電先校準(zhǔn) */ HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_dma_buf, 2); }這段初始化的關(guān)鍵參數(shù)是ExternalTrigConv和SamplingTime。TRGO 來(lái)自 TIM2周期設(shè) 1msADC 每 1ms 自動(dòng)采一輪DMA 把兩個(gè)通道的結(jié)果寫到adc_dma_buf[0]和[1]。NbrOfConversion2表示一次觸發(fā)掃兩個(gè)通道順序按 Rank 來(lái)別把平板和擺桿接反。如果沿用 10 位配置DataAlign不變但換算滿量程要改成 1023HAL_ADCEx_Calibration_Start仍然要調(diào)校準(zhǔn)能明顯減小零漂。3.2 中值濾波加一階低通把機(jī)械抖動(dòng)壓下去自由擺擺動(dòng)時(shí)擺桿和軸承的機(jī)械振動(dòng)會(huì)疊加到傳感器上ADC 原始值畫(huà)出來(lái)是一條毛刺很重的曲線。直接用這個(gè)值做 PID微分項(xiàng)會(huì)把毛刺放大成電機(jī)抖動(dòng)。我一般先用 9 點(diǎn)滑動(dòng)中值濾波去掉偶發(fā)尖峰再串一個(gè)一階低通壓高頻。中值濾波窗口不要太大9 個(gè)點(diǎn)已經(jīng)有 4 個(gè)采樣周期的滯后再大控制會(huì)發(fā)鈍低通系數(shù)取 0.20.3太小響應(yīng)慢太大濾波等于沒(méi)做。#define FILTER_WIN 9 static uint16_t win[2][FILTER_WIN]; static uint8_t win_idx; /* 9 點(diǎn)中值濾波窗口小插入排序足夠快 */ static uint16_t median9(const uint16_t *src) { uint16_t t[FILTER_WIN]; for (int i 0; i FILTER_WIN; i) t[i] src[i]; for (int i 1; i FILTER_WIN; i) { uint16_t key t[i]; int j i - 1; while (j 0 t[j] key) { t[j 1] t[j]; j--; } t[j 1] key; } return t[FILTER_WIN / 2]; } /* 一階低通alpha 越大越跟手越小越平滑 */ static float lowpass(float x, float y, float alpha) { return alpha * x (1.0f - alpha) * y; }median9每次對(duì) 9 個(gè)歷史值排序取中間那個(gè)計(jì)算量很小放在 1kHz 采樣中斷里也不會(huì)拖垮 CPU。lowpass的y是上一次輸出x是本次中值結(jié)果alpha0.25時(shí)截止頻率大概在幾十赫茲正好壓住機(jī)械振動(dòng)又不影響自由擺的低頻運(yùn)動(dòng)。兩個(gè)通道各用一套窗口和狀態(tài)不要共用一個(gè)win_idx否則平板和擺桿的數(shù)據(jù)會(huì)串。3.3 零點(diǎn)、量程與非線性修正角度標(biāo)定怎么做傳感器裝上去以后輸出電壓和角度不是天然對(duì)應(yīng)的。常見(jiàn)做法是兩點(diǎn)標(biāo)定把平板放在水平位置記下電壓v0再把平板轉(zhuǎn)到 90° 位置記下v90。中間按線性換算公式是angle (v - v0) * 90 / (v90 - v0) offset。offset是安裝零偏如果傳感器轉(zhuǎn)軸和平板法線沒(méi)對(duì)準(zhǔn)水平時(shí)讀數(shù)不是 0就用這個(gè)參數(shù)補(bǔ)。WDD35D4 的導(dǎo)電塑料膜兩端有無(wú)效區(qū)標(biāo)定范圍不要貼著 0° 和 360° 端點(diǎn)盡量在 -45°45° 或 0°90° 的工作段內(nèi)取兩點(diǎn)線性度更好。typedef struct { float v0; /* 0° 對(duì)應(yīng)的 ADC 電壓?jiǎn)挝?V */ float v90; /* 90° 對(duì)應(yīng)的 ADC 電壓?jiǎn)挝?V */ float offset; /* 安裝零偏單位 度 */ float div; /* 分壓比5V 經(jīng) 10k/20k 分壓為 1.5 */ } AngleCalib; static float adc_to_voltage(uint16_t raw, float div) { /* 12 位 ADC參考 3.3V10 位版本把 4095 改成 1023 */ return raw * 3.3f / 4095.0f * div; } static float voltage_to_angle(float v, const AngleCalib *c) { float k 90.0f / (c-v90 - c-v0); return (v - c-v0) * k c-offset; }div是分壓比5V 經(jīng) 10k 和 20k 分壓后進(jìn) ADC實(shí)際電壓是 ADC 讀數(shù)的 1.5 倍所以要在換算時(shí)乘回去。v0和v90不要用理論值必須現(xiàn)場(chǎng)拿萬(wàn)用表或串口打印實(shí)際 ADC 值記錄。標(biāo)定完把兩組數(shù)寫進(jìn) Flash 或頭文件下次上電直接加載。平板和擺桿兩路要分別標(biāo)定因?yàn)閮蓚€(gè)傳感器的安裝角度和分壓電阻誤差不一樣共用一組參數(shù)會(huì)引入幾度的固定偏差。3.4 采樣異常檢測(cè)斷線、超量程和參考電壓跌落模擬鏈路最常見(jiàn)的故障是傳感器斷線、分壓電阻虛焊、ADC 輸入過(guò)壓。代碼里要加一層保護(hù)連續(xù)多次讀到 0 或滿量程就判定傳感器異常電機(jī)停止輸出TFT 上顯示故障碼。另外如果 3.3V 參考被電機(jī)啟停拉低所有角度會(huì)同時(shí)往下掉這種不是傳感器壞而是電源問(wèn)題要靠硬件加電容和軟件限幅一起處理。我一般會(huì)在控制任務(wù)里做軟限幅角度超過(guò)機(jī)械行程 ±5° 就強(qiáng)制回中避免 PID 積分飽和后平板頂?shù)较尬弧?. 執(zhí)行機(jī)構(gòu)與控制閉環(huán)細(xì)分驅(qū)動(dòng)器加步進(jìn)電機(jī)怎么把平板拉回目標(biāo)角執(zhí)行鏈路分兩層底層是定時(shí)器產(chǎn)生脈沖、驅(qū)動(dòng)器放大電流、步進(jìn)電機(jī)走步上層是 PID 根據(jù)角度誤差算出“這一拍該走多少脈沖”。自由擺是動(dòng)態(tài)系統(tǒng)平板的目標(biāo)角不一定是固定 0°基礎(chǔ)要求是保持水平發(fā)揮部分要讓硬幣相對(duì)靜止目標(biāo)角得跟著擺桿狀態(tài)變。這一章把脈沖當(dāng)量、脈沖生成、PID 和主循環(huán)串起來(lái)。4.1 驅(qū)動(dòng)器撥碼與細(xì)分16 細(xì)分下的脈沖當(dāng)量四宏細(xì)分驅(qū)動(dòng)器的細(xì)分由撥碼開(kāi)關(guān)設(shè)定常見(jiàn)有 1、2、4、8、16、32 幾檔。項(xiàng)目里用 16 細(xì)分步進(jìn)電機(jī)步距角 1.8°那么每個(gè)脈沖對(duì)應(yīng) 1.8/160.1125°。平板直接裝在電機(jī)軸上減速比 1:1轉(zhuǎn) 1° 需要約 8.89 個(gè)脈沖如果平板和電機(jī)之間加了同步帶減速比 2:1就要 17.78 個(gè)脈沖。驅(qū)動(dòng)器電流撥碼按電機(jī)額定電流設(shè)42 步進(jìn)電機(jī)常見(jiàn) 1.5A2A設(shè)太大電機(jī)會(huì)燙設(shè)太小帶載丟步。細(xì)分檔位每脈沖角度轉(zhuǎn) 1° 所需脈沖適用場(chǎng)景11.8°0.56只做粗略定位振動(dòng)大40.45°2.22低速輕載80.225°4.44一般控制160.1125°8.89本項(xiàng)目兼顧平順和速度320.05625°17.78高精度但脈沖頻率要求高撥碼設(shè)置完最好用記號(hào)筆在驅(qū)動(dòng)器上寫一下調(diào)試時(shí)換過(guò)撥碼又忘了脈沖當(dāng)量對(duì)不上PID 輸出會(huì)整體差一倍。電機(jī)線序也要確認(rèn)四線步進(jìn)電機(jī)兩組線圈接錯(cuò)電機(jī)會(huì)原地抖動(dòng)不走這時(shí)候先別懷疑程序拿萬(wàn)用表量線圈通斷。4.2 定時(shí)器脈沖與加減速別讓電機(jī)啟動(dòng)就丟步步進(jìn)電機(jī)的脈沖不能一上來(lái)就給很高頻率啟動(dòng)頻率超過(guò)電機(jī)的牽入頻率就會(huì)丟步表現(xiàn)為“程序算走了 100 步平板只動(dòng)了 80 步”。常見(jiàn)做法是用定時(shí)器中斷翻轉(zhuǎn) PUL 腳周期由period_us控制上層根據(jù)剩余脈沖數(shù)和當(dāng)前速度做梯形加減速加速段周期從 5ms 逐步減到 0.2ms勻速段保持減速段再拉長(zhǎng)。下面這個(gè)狀態(tài)機(jī)放在 1ms 定時(shí)器里跑非阻塞不占死循環(huán)。typedef struct { int32_t remain; /* 剩余脈沖數(shù)正負(fù)表示方向 */ uint32_t period_us; /* 當(dāng)前脈沖周期越小越快 */ uint32_t last_tick; uint8_t level; /* PUL 電平上升沿驅(qū)動(dòng)器采一次 */ } Stepper; static void stepper_task(Stepper *s, uint32_t now_us) { if (s-remain 0) { HAL_GPIO_WritePin(STEP_PUL_PORT, STEP_PUL_PIN, GPIO_PIN_RESET); return; } /* 方向腳在脈沖前建立避免第一個(gè)脈沖方向錯(cuò) */ HAL_GPIO_WritePin(STEP_DIR_PORT, STEP_DIR_PIN, s-remain 0 ? GPIO_PIN_SET : GPIO_PIN_RESET); if (now_us - s-last_tick s-period_us) { s-last_tick now_us; s-level !s-level; HAL_GPIO_WritePin(STEP_PUL_PORT, STEP_PUL_PIN, s-level ? GPIO_PIN_SET : GPIO_PIN_RESET); if (!s-level) { /* 下降沿計(jì)一個(gè)完整脈沖 */ s-remain (s-remain 0) ? -1 : 1; } } }period_us是半周期實(shí)際脈沖頻率是1/(2*period_us)。比如period_us500對(duì)應(yīng) 1kHz電機(jī)轉(zhuǎn)速大概 1kHz/16/2000.3125 轉(zhuǎn)/秒。remain帶符號(hào)正數(shù)正轉(zhuǎn)負(fù)數(shù)反轉(zhuǎn)走到 0 自動(dòng)停不會(huì)一直發(fā)脈沖。加速時(shí)上層每 10ms 把period_us減一點(diǎn)減速時(shí)反過(guò)來(lái)加別在stepper_task里做浮點(diǎn)運(yùn)算1ms 中斷里越簡(jiǎn)單越穩(wěn)。4.3 控制算法位置式 PID、死區(qū)和目標(biāo)角切換基礎(chǔ)要求是平板保持水平目標(biāo)角target0°發(fā)揮部分要讓硬幣在擺動(dòng)中相對(duì)靜止目標(biāo)角不能固定為 0°得根據(jù)擺桿角度和角速度給一個(gè)補(bǔ)償角。硬幣隨平板擺動(dòng)時(shí)需要水平方向的加速度平板傾斜可以讓重力沿板面的分量參與提供這個(gè)加速度同時(shí)改變支持力。實(shí)際調(diào)試時(shí)先用水平模式把 PID 調(diào)穩(wěn)再切到硬幣模式目標(biāo)角用target k1*arm_deg k2*arm_rate試出來(lái)k1、k2靠實(shí)驗(yàn)湊別指望一次算準(zhǔn)。typedef struct { float kp, ki, kd; float target; /* 目標(biāo)角水平模式為 0 */ float integral; float last_err; float out_limit; /* 輸出限幅單位脈沖/控制周期 */ float deadband; /* 死區(qū)度 */ } PID; static float pid_calc(PID *p, float measure, float dt) { float err p-target - measure; /* 小誤差進(jìn)死區(qū)避免電機(jī)來(lái)回抖 */ if (fabsf(err) p-deadband) { p-integral * 0.95f; /* 緩慢泄放積分防止退出死區(qū)時(shí)突跳 */ return 0.0f; } float deriv (err - p-last_err) / dt; p-integral err * dt; p-last_err err; float out p-kp * err p-ki * p-integral p-kd * deriv; /* 輸出限幅 抗積分飽和 */ if (out p-out_limit) { out p-out_limit; p-integral - err * dt; } else if (out -p-out_limit) { out -p-out_limit; p-integral - err * dt; } return out; }dt是控制周期主循環(huán)里固定 0.005s不要用HAL_GetTick的毫秒值直接除時(shí)間戳抖動(dòng)會(huì)讓微分項(xiàng)爆掉。out_limit按最大脈沖頻率折算比如每 5ms 最多走 20 個(gè)脈沖就設(shè) 20。deadband一般取 0.2°0.5°太小電機(jī)在目標(biāo)附近來(lái)回走太大硬幣會(huì)滾。PID 輸出是“本周期應(yīng)該走的脈沖數(shù)”正負(fù)號(hào)直接給stepper_move不用再單獨(dú)算方向。4.4 主循環(huán)與 TFT 顯示5ms 控制節(jié)拍怎么排主循環(huán)不能把 TFT 刷新、串口打印和 PID 混在一起跑SPI 刷屏一次可能幾毫秒控制周期立刻被拉長(zhǎng)。常見(jiàn)做法是定時(shí)器給 5ms 節(jié)拍控制任務(wù)只做采樣讀取、PID、電機(jī)更新TFT 每 50ms 刷一次串口波形每 20ms 發(fā)一幀。這樣控制節(jié)拍穩(wěn)定屏幕看起來(lái)也不閃。void control_task(void) { uint32_t now HAL_GetTick(); if (now - last_control 5) return; /* 5ms 控制周期 */ last_control now; float plate_deg get_plate_angle(); /* 已濾波、已標(biāo)定 */ float arm_deg get_arm_angle(); /* 水平模式 target0硬幣模式在這里改 pid_plate.target */ float pulses pid_calc(pid_plate, plate_deg, 0.005f); /* 脈沖數(shù)換算成速度絕對(duì)值給頻率符號(hào)給方向 */ stepper_set_speed(stepper, fabsf(pulses) * 180.0f); stepper_move(stepper, (int32_t)pulses); static uint8_t disp_cnt; if (disp_cnt 10) { /* 50ms 刷一次屏 */ disp_cnt 0; tft_show_status(plate_deg, arm_deg, pulses); } }stepper_set_speed里的 180 是經(jīng)驗(yàn)系數(shù)把“每周期脈沖數(shù)”映射成脈沖頻率實(shí)際要根據(jù)電機(jī)和機(jī)械負(fù)載調(diào)。stepper_move把脈沖數(shù)累加到remain底層狀態(tài)機(jī)自己走完。TFT 顯示建議至少放四個(gè)量平板角度、擺桿角度、PID 輸出、當(dāng)前模式調(diào)參時(shí)一眼能看出是傳感器跳還是電機(jī)沒(méi)跟上。按鍵調(diào)kp/ki/kd比反復(fù)燒錄快得多哪怕只做三個(gè)按鍵加一個(gè)確認(rèn)鍵調(diào)試效率也完全不一樣。5. 避坑與排查自由擺平板控制最容易翻車的 5 個(gè)點(diǎn)這套系統(tǒng)硬件不復(fù)雜但機(jī)電耦合強(qiáng)很多問(wèn)題看起來(lái)像程序 bug實(shí)際是接線、電源或機(jī)械安裝。下面五條是按我調(diào)試時(shí)踩坑頻率排的每條按“現(xiàn)象—原因—解決”寫照著排查能少走不少?gòu)澛贰?.1 上電后平板往一個(gè)方向猛轉(zhuǎn)限位也擋不住現(xiàn)象程序下載完一上電電機(jī)就朝一個(gè)方向連續(xù)轉(zhuǎn)角度讀數(shù)變化但 PID 像沒(méi)起作用平板頂?shù)綑C(jī)械限位后電機(jī)堵轉(zhuǎn)嘯叫。原因最常見(jiàn)是 DIR 方向腳和驅(qū)動(dòng)器實(shí)際邏輯反了PID 輸出正方向電機(jī)卻往負(fù)方向走誤差越大輸出越大形成正反饋。其次是 ENA 使能腳電平配錯(cuò)驅(qū)動(dòng)器一直使能單片機(jī)復(fù)位期間 GPIO 處于浮空驅(qū)動(dòng)器把干擾當(dāng)成脈沖。還有一種是單片機(jī)地和驅(qū)動(dòng)器地沒(méi)共脈沖電平?jīng)]有參考走位隨機(jī)。解決先拔掉 PUL 線只給 DIR 和 ENA手動(dòng)改方向腳電平確認(rèn)平板正反轉(zhuǎn)和代碼符號(hào)一致再把 ENA 默認(rèn)拉高按驅(qū)動(dòng)器手冊(cè)確認(rèn)使能電平復(fù)位期間電機(jī)保持釋放最后用萬(wàn)用表量單片機(jī)和驅(qū)動(dòng)器地之間的電壓必須接近 0V。機(jī)械限位只能當(dāng)最后一道保護(hù)不能靠它停機(jī)。5.2 角度讀數(shù)靜止時(shí)也跳動(dòng)起來(lái)跳得更厲害現(xiàn)象平板不動(dòng)TFT 上角度在小數(shù)點(diǎn)后一位來(lái)回跳擺桿一擺角度曲線毛刺更大PID 輸出跟著抖電機(jī)發(fā)出細(xì)碎響聲。原因WDD35D4 如果是 5V 供電輸出直接進(jìn) STM32 ADC 會(huì)超過(guò) 3.3V內(nèi)部鉗位二極管導(dǎo)通讀數(shù)被削頂分壓電阻虛焊或阻值不匹配也會(huì)讓讀數(shù)漂。另一個(gè)常見(jiàn)原因是傳感器供電和電機(jī)驅(qū)動(dòng)邏輯共用 5V電機(jī)啟停時(shí)電源跌落ADC 參考跟著動(dòng)。采樣時(shí)間設(shè)得太短采樣保持電容充不滿也會(huì)讓讀數(shù)偏低且不穩(wěn)。解決先量傳感器輸出到 ADC 引腳的電壓確認(rèn)不超過(guò) 3.3V超過(guò)就老老實(shí)實(shí)加分壓10k20k 是最省事的組合傳感器供電單獨(dú)走 LC 濾波地和 ADC 地單點(diǎn)連接ADC 采樣時(shí)間加到 71.5 周期以上軟件上加中值加低通但別用低通掩蓋硬件問(wèn)題。分壓比記得在代碼里乘回去否則角度整體偏小。5.3 硬幣在擺動(dòng)中還是滾平板看起來(lái)“水平”但就是不穩(wěn)現(xiàn)象靜態(tài)調(diào)水平很準(zhǔn)一擺起來(lái)硬幣就往一側(cè)滾加快擺動(dòng)直接掉板激光筆在靶面上畫(huà)圈打不中中心。原因基礎(chǔ)模式目標(biāo)角固定 0°但擺動(dòng)時(shí)平板有水平加速度硬幣需要摩擦力提供加速度靠水平板面硬扛摩擦不夠就滾??刂浦芷谔L(zhǎng)也會(huì)導(dǎo)致補(bǔ)償滯后5ms 和 20ms 完全是兩個(gè)效果。機(jī)械上平板和電機(jī)軸連接有間隙PID 算完了電機(jī)也走了平板實(shí)際角度還差一點(diǎn)傳感器讀數(shù)滯后。解決先確認(rèn)控制周期真的是 5ms用示波器翻 PUL 腳看脈沖間隔別只看代碼發(fā)揮部分切到目標(biāo)角補(bǔ)償模式用擺桿角度和角速度算target從很小的系數(shù)開(kāi)始試機(jī)械間隙用頂絲或漲緊套消除傳感器盡量和負(fù)載同軸安裝硬幣測(cè)試前先把平板靜態(tài)調(diào)平再用手緩慢擺擺桿觀察角度曲線和電機(jī)響應(yīng)是否同相。5.4 步進(jìn)電機(jī)丟步、嘯叫走 100 步實(shí)際只轉(zhuǎn)了 80 步現(xiàn)象空載轉(zhuǎn)得很順裝上平板就丟步低速時(shí)電機(jī)嘯叫高速時(shí)直接堵轉(zhuǎn)PID 輸出一直加角度就是回不來(lái)。原因驅(qū)動(dòng)器電流撥碼設(shè)小了帶不動(dòng)負(fù)載細(xì)分設(shè)太高脈沖頻率上去了但電機(jī)響應(yīng)跟不上加速曲線太陡啟動(dòng)頻率超過(guò)牽入頻率電源功率不夠12V 在電機(jī)啟停時(shí)跌到 9V 以下驅(qū)動(dòng)器欠壓保護(hù)。解決按電機(jī)額定電流的 70% 起步調(diào)電流摸電機(jī)外殼溫度燙手就降一檔細(xì)分從 8 或 16 開(kāi)始別一上來(lái)就 32加減速用梯形曲線啟動(dòng)頻率先設(shè) 200Hz穩(wěn)定后再往上提12V 電源并 470μF 以上電解電容條件允許換功率更大的電池或電源。丟步是開(kāi)環(huán)步進(jìn)的固有風(fēng)險(xiǎn)關(guān)鍵位置加一個(gè)光電限位做回零校準(zhǔn)比反復(fù)調(diào) PID 管用。5.5 激光筆打靶總是偏換了角度也不對(duì)現(xiàn)象平板水平時(shí)激光點(diǎn)偏靶心幾厘米擺動(dòng)后軌跡是橢圓調(diào) PID 只能讓橢圓變小中心始終偏。原因激光筆安裝軸線和平板法線不平行平板轉(zhuǎn)角度時(shí)激光點(diǎn)畫(huà)圓而不是走直線傳感器零位和機(jī)械水平?jīng)]對(duì)齊軟件顯示 0° 時(shí)平板實(shí)際是 1°2°靶面距離變了但補(bǔ)償系數(shù)沒(méi)改角度和光點(diǎn)位移比例對(duì)不上。解決先把平板用水平儀調(diào)平再調(diào)激光筆支架讓光點(diǎn)在近處和遠(yuǎn)處靶面上重合說(shuō)明射線和平板法線平行軟件零位用按鍵或串口命令現(xiàn)場(chǎng)清零不要用理論電壓靶距變化后重新標(biāo)定angle-光點(diǎn)位移的比例。激光筆供電單獨(dú)走線和電機(jī)線分開(kāi)避免 PWM 干擾造成光點(diǎn)閃爍。6. 二次開(kāi)發(fā)把這套控制板改成你自己的實(shí)驗(yàn)平臺(tái)這套自由擺平板控制的代碼結(jié)構(gòu)不復(fù)雜很適合當(dāng)二次開(kāi)發(fā)底座。想換傳感器可以把 WDD35D4 換成增量式編碼器接定時(shí)器編碼器模式角度閉環(huán)從模擬采樣變成計(jì)數(shù)抗干擾更好也可以加一片六軸 IMU做互補(bǔ)濾波把擺桿角速度也納入前饋。想研究控制算法把位置式 PID 換成“前饋 PD”前饋?lái)?xiàng)用擺桿角加速度估算能明顯減小擺動(dòng)過(guò)程中的相位滯后。改造方向改動(dòng)點(diǎn)驗(yàn)證方法編碼器閉環(huán)定時(shí)器編碼器模式替換 ADC 角度手動(dòng)轉(zhuǎn)軸看計(jì)數(shù)是否連續(xù)IMU 融合I2C/SPI 讀六軸互補(bǔ)濾波靜態(tài)傾角對(duì)比水平儀前饋補(bǔ)償擺桿角速度/角加速度參與目標(biāo)角正弦擺動(dòng)下看硬幣是否滑動(dòng)上位機(jī)波形串口按 CSV 發(fā)角度、PID 輸出上位機(jī)畫(huà)曲線看相位差無(wú)線調(diào)參串口透?jìng)髂K替換有線調(diào) Kp 時(shí)看階躍響應(yīng)驗(yàn)證不要一上來(lái)就放硬幣。先做階躍響應(yīng)平板目標(biāo)角從 0° 跳到 5°用串口把角度和 PID 輸出打成 CSV看超調(diào)、調(diào)節(jié)時(shí)間和穩(wěn)態(tài)誤差再做正弦跟隨手動(dòng)讓擺桿按固定周期擺記錄平板角度和擺桿角度看兩條曲線的相位差和幅值比。硬幣測(cè)試放在最后而且先從慢擺開(kāi)始逐步加快每次只改一個(gè)參數(shù)。串口輸出我一般用下面這種最笨的格式上位機(jī)直接按逗號(hào)拆不需要額外協(xié)議。/* 20ms 發(fā)一幀波特率 115200上位機(jī)按 CSV 畫(huà)圖 */ void telemetry_task(void) { static uint32_t last; if (HAL_GetTick() - last 20) return; last HAL_GetTick(); printf(%.2f,%.2f,%.2f,%.2f\r\n, get_plate_angle(), /* 平板角度 */ get_arm_angle(), /* 擺桿角度 */ pid_plate.target, /* 當(dāng)前目標(biāo)角 */ pid_plate.integral); /* 積分項(xiàng)看是否飽和 */ }printf重定向到串口時(shí)注意別放在 5ms 控制任務(wù)里放 20ms 任務(wù)或者主循環(huán)低優(yōu)先級(jí)位置否則串口阻塞會(huì)把控制節(jié)拍拉長(zhǎng)。integral打出來(lái)很有用積分一直往上頂說(shuō)明輸出限幅了或者機(jī)械卡住這時(shí)候調(diào) Kp 沒(méi)用得先查執(zhí)行機(jī)構(gòu)。從那以后我每次改完控制參數(shù)都強(qiáng)制先跑一遍階躍加正弦再放本文還有配套的精品資源點(diǎn)擊獲取