
簡介本資源是一套基于STM32F10x系列芯片的PID閉環控制實戰工程面向嵌入式初學者與智能車競賽備賽者聚焦編碼器測速、多路PWM驅動與實時PID調速三大核心能力訓練。項目完整實現TIM1/TIM8雙組8通道PWM輸出、TIM7定時中斷測速、TIM2–TIM5四路編碼器接口采集、以及2.3寸TFT屏實時顯示脈沖增量與累計值可直接用于尋跡小車、碼盤測速等典型運動控制場景。壓縮包共86個文件37個.h頭文件定義外設與算法接口34個.c源文件實現PID控制器、編碼器讀取、LCD驅動及系統配置另含hex固件、Keil工程配置文件及調試腳本總大小330KB結構清晰、模塊解耦便于理解底層時序邏輯與控制流程。目前已有1923人學習下載提供可編譯運行的完整Keil工程含詳細注釋與分層函數設計是掌握STM32定時器高級用法與閉環控制落地的優質實踐范例。1. 用 STM32 的 TIM4 捕獲碼盤編碼器脈沖實現閉環尋跡小車的 PID 控制不是調參玄學而是可復現的位置式 PID 工程落地你手頭有一塊 STM32F103C8T6或類似主頻 72MHz 的 Cortex-M3 芯片接了兩個帶 AB 相輸出的旋轉編碼器俗稱“碼盤”想讓小車沿著黑線穩定行走——但發現電機轉速忽快忽慢、過沖嚴重、甚至原地抖動。這不是硬件壞了而是你還沒把編碼器信號真正“翻譯”成位置反饋更沒把 PID 控制器嵌入到 TIM4 的更新中斷里跑起來。本篇不講 PID 公式推導只聚焦如何用 STM32 標準外設庫或 HAL配置 TIM4 為編碼器接口模式讀取真實位移值怎么把位置式 PID 算法寫進中斷服務函數避免浮點運算拖慢響應為什么 PB、TI、TD 這三個參數必須按物理量級拆解比如 TI 不是隨便填 100而是對應電機機械時間常數的 25 倍以及最關鍵的——如何用串口實時打印編碼器計數值和 PID 輸出驗證閉環是否真正建立。適合剛做完 GPIO 點燈、正準備啃運動控制的嵌入式開發者也適合需要快速復現畢業設計中“尋跡小車”模塊的工程師。2. 配置 TIM4 為編碼器接口模式用硬件自動計數避開軟件定時采樣失真2.1 為什么必須用 TIM4而非 TIM2/TIM3做編碼器接口STM32F103 系列中只有 TIM2、TIM3、TIM4 支持編碼器接口模式Encoder Interface Mode但 TIM2 是 32 位定時器資源緊張時易與 ADC/DMA 沖突TIM3 常被用于 PWM 輸出驅動電機而 TIM4 是 16 位通用定時器專為低優先級外設預留且其通道 1CH1、通道 2CH2恰好映射到 PA11TIM4_CH1和 PA12TIM4_CH2——這兩引腳在多數最小系統板上未被占用物理布線最方便。更重要的是TIM4 更新中斷UPDATE IRQ默認優先級較低不會搶占電機 PWM 更新通常放在 TIM3保證控制周期穩定。若強行用 TIM2 接編碼器可能因高優先級中斷頻繁搶占導致 PID 計算延遲表現為小車軌跡發飄。提示確認你的開發板上 PA11/PA12 是否已焊接跳線帽或未被其他功能復用。可用萬用表測通斷或臨時改用 PB6/PB7TIM4_CH1/CH2 的重映射引腳但需開啟 AFIO 時鐘并調用GPIO_PinRemapConfig(GPIO_PartialRemap_TIM4, ENABLE)。2.2 編碼器硬件接線與電氣匹配要點AB 相增量式編碼器輸出為開漏Open-Drain或推挽Push-Pull信號常見電壓為 5V 或 3.3V。STM32F103 的 IO 口耐壓為 5V 容限僅部分引腳但PA11/PA12 不屬于 5V 容限引腳。若編碼器輸出 5V必須加電平轉換方案一推薦用 74LVC245 或 TXB0104 做雙向電平轉換成本約 2 元抗干擾強方案二應急在 PA11/PA12 上各串一個 1kΩ 限流電阻 并聯 3.3V 鉗位二極管如 BAV99但長期使用可能損傷 IO方案三最簡選用 3.3V 供電的磁編碼器如 AS5047P直接連接省去電平轉換。接線順序嚴格遵循編碼器 A 相 → PA11 (TIM4_CH1) 編碼器 B 相 → PA12 (TIM4_CH2) 編碼器 GND → STM32 GND共地 編碼器 VCC → 3.3V若用 3.3V 編碼器注意A/B 相接反會導致計數方向相反小車會“倒著走”。若發現小車前進時編碼器值遞減只需交換 A/B 相連線無需改代碼。2.3 TIM4 編碼器模式初始化代碼標準庫版// 初始化 TIM4 編碼器接口使用標準外設庫 void TIM4_Encoder_Init(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; GPIO_InitTypeDef GPIO_InitStructure; // 1. 開啟相關時鐘 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); // 2. 配置 PA11/PA12 為復用推挽輸出實際為輸入但需配置為復用功能 GPIO_InitStructure.GPIO_Pin GPIO_Pin_11 | GPIO_Pin_12; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; // 浮空輸入由內部上拉/下拉決定 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // 3. 配置 TIM4 為編碼器模式TI1FP1 和 TI2FP2 作為編碼器輸入 TIM_TimeBaseStructure.TIM_Period 0xFFFF; // 自動重裝載值16位最大 TIM_TimeBaseStructure.TIM_Prescaler 0; // 分頻系數為0即不分頻 TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; // 此處無效編碼器模式由從模式控制 TIM_TimeBaseInit(TIM4, TIM_TimeBaseStructure); // 4. 配置輸入捕獲通道關鍵設置濾波和極性 TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_BothEdge; // AB相邊沿都觸發計數 TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter 0x0F; // 采樣頻率為 fCK_INT / 16濾除高頻噪聲典型值0x0F TIM_ICInit(TIM4, TIM_ICInitStructure); // 5. 啟用編碼器模式從模式編碼器模式 TIM_EncoderInterfaceConfig(TIM4, TIM_EncoderMode_TI12, TIM_ICPolarity_BothEdge, TIM_ICPolarity_BothEdge); // 6. 清零計數器并使能 TIM_SetCounter(TIM4, 0); TIM_Cmd(TIM4, ENABLE); }參數說明TIM_ICFilter 0x0F表示對輸入信號進行 4 次采樣2^416再判斷有效邊沿。適用于電機轉速 ≤ 1000 RPM 的場景若小車高速運行2000 RPM可降至0x078 次采樣以降低延遲TIM_EncoderMode_TI12選擇 TI1CH1和 TI2CH2共同構成正交解碼支持四倍頻計數即 A/B 每個周期產生 4 個計數脈沖TIM_SetCounter(TIM4, 0)必須在TIM_Cmd()前執行否則首次讀取TIM_GetCounter(TIM4)可能返回隨機值TIM_ICPolarity_BothEdge允許 A/B 相任意邊沿觸發這是實現四倍頻的關鍵不能設為Rising或Falling單邊沿。2.4 驗證編碼器是否正常工作用串口打印原始計數值在main()中添加如下測試邏輯uint16_t last_count 0; while(1) { uint16_t current_count TIM_GetCounter(TIM4); if (current_count ! last_count) { printf(ENC: %d\r\n, (int16_t)current_count); // 強制轉為有符號顯示負值 last_count current_count; } Delay_ms(100); // 避免刷屏 }用手勻速轉動編碼器軸觀察串口輸出若數值穩定遞增 → A/B 相序正確硬件連接無誤若數值跳變劇烈如 0→100→50→200→ 編碼器電源不穩或濾波參數過大需檢查TIM_ICFilter若數值始終為 0 → 檢查 PA11/PA12 是否懸空應接編碼器輸出或TIM_Cmd()是否被遺漏若數值遞減 → A/B 相接反交換連線即可。3. 實現位置式 PID 控制器嵌入 TIM4 更新中斷避免浮點瓶頸3.1 為什么必須用位置式 PID而非增量式做尋跡尋跡小車的核心控制目標是位置跟蹤讓車體中心始終對準黑線中心。這意味著控制器輸出PWM 占空比必須與當前位置偏差呈線性關系。位置式 PID 輸出公式為u(k) Kp * e(k) Ki * Σe(i) Kd * [e(k) - e(k-1)]其中e(k) setpoint - encoder_position。而增量式 PID 輸出的是本次與上次的差值 Δu(k)需累加才能得到最終 PWM 值——這在中斷中易因變量競爭導致累加錯誤且無法直接限制輸出上下限需額外做飽和處理。位置式結構清晰、易于限幅、調試直觀是尋跡類位置控制的工業首選。提示“PID 中 PB TI TD” 是工程界對比例帶PB、積分時間TI、微分時間TD的簡稱對應Kp 100/PB、Ki Kp/TI、Kd Kp*TD。但 STM32 資源有限我們直接使用Kp/Ki/Kd整數倍縮放避免除法。3.2 PID 參數物理意義與初值設定方法參數物理含義尋跡小車典型初值基于 1000 線碼盤 1:30 減速比調參方向Kp比例增益決定響應速度0.81.5單位PWM 占空比 / 編碼器計數過大 → 振蕩過小 → 響應遲鈍Ki積分增益消除靜態誤差0.0050.02單位PWM 占空比 / (編碼器計數·ms)過大 → 積分飽和小車緩慢爬行過小 → 黑線偏移后無法回正Kd微分增益抑制超調0.30.8單位PWM 占空比 / (編碼器計數/ms)過大 → 對噪聲敏感電機嘶叫過小 → 過沖明顯注意上述數值基于 1000 線碼盤每轉 4000 個計數 30:1 減速電機。若用 600 線碼盤Kp需乘以 1000/600≈1.67若電機減速比為 10:1則Kp需除以 3。參數必須隨機械結構縮放不能照搬。3.3 位置式 PID 實現固定點運算無浮點// PID 結構體全局變量避免中斷中 malloc typedef struct { int32_t setpoint; // 目標位置編碼器計數值 int32_t current_pos; // 當前位置從 TIM4 讀取 int32_t error; // 當前誤差 int32_t last_error; // 上次誤差 int32_t integral; // 積分項帶防飽和 int32_t output; // PID 輸出PWM 占空比 int32_t Kp; // 比例系數放大 1000 倍如 1.2 → 1200 int32_t Ki; // 積分系數放大 1000000 倍如 0.01 → 10000 int32_t Kd; // 微分系數放大 1000 倍如 0.5 → 500 int32_t out_max; // 輸出上限如 1000 → 對應 100% 占空比 int32_t out_min; // 輸出下限如 -1000 } PID_TypeDef; PID_TypeDef pid { .setpoint 0, .Kp 1200, // Kp 1.2 .Ki 10000, // Ki 0.01 .Kd 500, // Kd 0.5 .out_max 1000, .out_min -1000 }; // TIM4 更新中斷服務函數執行 PID 計算 void TIM4_IRQHandler(void) { if (TIM_GetITStatus(TIM4, TIM_IT_Update) ! RESET) { // 1. 讀取當前編碼器位置關鍵在中斷中讀保證原子性 pid.current_pos (int16_t)TIM_GetCounter(TIM4); // 2. 計算誤差 pid.error pid.setpoint - pid.current_pos; // 3. 積分項帶防飽和當輸出已達限幅停止積分 if ((pid.output pid.out_min pid.output pid.out_max) || (pid.output pid.out_max pid.error 0) || (pid.output pid.out_min pid.error 0)) { pid.integral pid.error; } // 限制積分項范圍防止 windup if (pid.integral 1000000) pid.integral 1000000; if (pid.integral -1000000) pid.integral -1000000; // 4. 微分項用后向差分避免微分沖擊 int32_t derivative pid.error - pid.last_error; pid.last_error pid.error; // 5. 位置式 PID 計算全部為整數運算 pid.output (pid.Kp * pid.error) / 1000 (pid.Ki * pid.integral) / 1000000 (pid.Kd * derivative) / 1000; // 6. 輸出限幅 if (pid.output pid.out_max) pid.output pid.out_max; if (pid.output pid.out_min) pid.output pid.out_min; // 7. 將 PID 輸出映射到 PWM假設 TIM3_CH1 輸出左輪 PWM // 此處調用你已配置好的 PWM 設置函數如 TIM_SetCompare1(TIM3, abs(pid.output)); // 注意pid.output 為負值時驅動右輪正值驅動左輪實現差速轉向 TIM_ClearITPendingBit(TIM4, TIM_IT_Update); } }關鍵邏輯說明pid.current_pos (int16_t)TIM_GetCounter(TIM4)強制轉為int16_t是因為 TIM4 是 16 位計數器高位溢出后自動歸零int16_t可正確表示 -32768~32767 的相對位移積分防飽和邏輯僅當輸出未達限幅或雖達限幅但誤差方向與限幅方向一致時才允許積分累加微分項用derivative pid.error - pid.last_error而非(current_pos - last_pos)因位置微分噪聲極大而誤差微分更平滑所有系數均放大整數倍Kp*1000運算后統一除以放大倍數避免浮點指令Cortex-M3 無硬件 FPU浮點運算耗時 20 個周期abs(pid.output)用于驅動單側電機實際差速尋跡需將pid.output作為左右輪 PWM 差值例如left_pwm base_pwm pid.outputright_pwm base_pwm - pid.output。3.4 在主循環中設置目標位置與調試輸出int main(void) { // 初始化系統時鐘、GPIO、TIM4、USART 等 SystemInit(); USART1_Init(); // 初始化串口1 TIM4_Encoder_Init(); TIM3_PWM_Init(); // 初始化 TIM3 輸出 PWM // 開啟 TIM4 更新中斷優先級設為 2低于 TIM3 PWM 更新 NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel TIM4_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 2; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); while(1) { // 動態設置目標位置例如黑線中心對應編碼器值 0 // 實際尋跡中setpoint 由攝像頭/紅外傳感器計算得出此處簡化為固定值 pid.setpoint 0; // 實時打印調試信息每 200ms 一次避免中斷中打印 static uint32_t last_print 0; if (SysTick_GetTime() - last_print 200) { printf(POS:%d ERR:%d OUT:%d\r\n, pid.current_pos, pid.error, pid.output); last_print SysTick_GetTime(); } } }串口輸出示例POS:12 ERR:-12 OUT:-15 POS:8 ERR:-8 OUT:-10 POS:2 ERR:-2 OUT:-2 POS:0 ERR:0 OUT:0若ERR快速收斂至 0 且OUT穩定在 0 附近說明閉環已建立若OUT持續震蕩需調小Kp或增大Ki。4. 尋跡小車實戰從編碼器讀數到差速轉向的完整信號鏈4.1 編碼器數據到轉向指令的映射關系尋跡小車不直接控制“位置”而是通過左右輪速差實現轉向。核心映射邏輯為黑線偏左 → 小車需右轉 → 左輪減速 / 右輪加速 → 編碼器左側計數值 右側 → 左側誤差 e_left 0 → PID 輸出為正 → 左輪 PWM 減小右輪 PWM 增大因此需雙編碼器分別采集左右輪脈沖。常見做法左輪編碼器接 TIM2CH1/CH2→TIM_GetCounter(TIM2)右輪編碼器接 TIM4CH1/CH2→TIM_GetCounter(TIM4)設定目標兩輪計數值差值diff left_count - right_count應趨近于 0直行若diff 0左輪轉得快說明車身右偏需增大右輪 PWM反之亦然。4.2 雙編碼器同步讀取與抗干擾處理TIM2 和 TIM4 的更新中斷不同步直接讀取可能導致diff計算錯誤。解決方案硬件同步將 TIM2 和 TIM4 的TRGOTrigger Output連在一起用同一事件觸發兩者更新軟件同步在主循環中用__disable_irq()關閉全局中斷依次讀取兩個定時器計數再恢復中斷int32_t get_wheel_diff(void) { int32_t left, right; __disable_irq(); left (int16_t)TIM_GetCounter(TIM2); right (int16_t)TIM_GetCounter(TIM4); __enable_irq(); return left - right; }注意__disable_irq()會關閉所有中斷時間必須極短1μs否則影響 PID 實時性。若 TIM2/TIM4 頻率相同如都設為 1kHz 更新也可用 DMA 雙緩沖一次性讀取兩個寄存器。4.3 差速轉向的 PWM 輸出實現假設 TIM3_CH1 控制左輪TIM3_CH2 控制右輪基礎 PWM 占空比為base_pwm 500對應 50% 占空比則// 在 TIM4 更新中斷中計算完 pid.output 后 int32_t left_pwm base_pwm - pid.output; // 輸出為正 → 左輪減速 int32_t right_pwm base_pwm pid.output; // 輸出為正 → 右輪加速 // 限幅處理 if (left_pwm 1000) left_pwm 1000; if (left_pwm 0) left_pwm 0; if (right_pwm 1000) right_pwm 1000; if (right_pwm 0) right_pwm 0; // 設置 PWM 占空比假設 TIM3 已配置為 PWM 模式 TIM_SetCompare1(TIM3, left_pwm); TIM_SetCompare2(TIM3, right_pwm);此時pid.output的物理意義變為左右輪 PWM 占空比的差值。當小車沿直線行駛時pid.output ≈ 0左右輪速度一致當黑線右偏diff 0pid.output 0左輪減速、右輪加速車身向右修正。4.4 實際調參步驟與典型現象對照表現象可能原因調參操作驗證方法小車完全不動Kp過小或setpoint設錯將Kp從 1200 逐步增至 2000觀察OUT是否跳變串口打印OUT值手動轉動輪子看是否響應小車原地左右擺動Kp過大或Kd過小先將Kp減半600再微調Kd從 500 增至 800觀察OUT是否高頻震蕩若震蕩頻率與電機機械共振接近需降Kd小車緩慢偏離黑線Ki過小或積分限幅太嚴將Ki從 10000 增至 20000同時擴大integral限幅范圍ERR長期不為 0且integral值持續增長小車過沖后停不穩Kd過大或編碼器噪聲未濾除將Kd從 500 降至 200同時TIM_ICFilter從 0x0F 增至 0x07OUT值出現尖峰毛刺對應電機“咔噠”聲5. 進階技巧用串口指令動態修改 PID 參數免燒錄驗證調參效果5.1 實現串口參數熱更新發送 KP1500 即刻生效在USART1_IRQHandler中解析 ASCII 指令避免每次改參都重新編譯下載char rx_buffer[20]; uint8_t rx_index 0; void USART1_IRQHandler(void) { uint8_t res; if(USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { res USART_ReceiveData(USART1); if(res ! \r res ! \n rx_index 19) { rx_buffer[rx_index] res; } else if(res \r || res \n) { rx_buffer[rx_index] \0; parse_pid_command(rx_buffer); rx_index 0; } } } void parse_pid_command(char* cmd) { if(strncmp(cmd, KP, 3) 0) { pid.Kp atoi(cmd3); printf(Kp updated to %d\r\n, pid.Kp); } else if(strncmp(cmd, KI, 3) 0) { pid.Ki atoi(cmd3); printf(Ki updated to %d\r\n, pid.Ki); } else if(strncmp(cmd, KD, 3) 0) { pid.Kd atoi(cmd3); printf(Kd updated to %d\r\n, pid.Kd); } else if(strncmp(cmd, SP, 3) 0) { pid.setpoint atoi(cmd3); printf(Setpoint updated to %d\r\n, pid.setpoint); } }操作流程小車靜止打開串口助手如 XCOM發送KP1800→ 觀察小車對擾動的響應速度是否提升發送KD300→ 觀察過沖是否減小發送SP50→ 模擬黑線右偏 50 個計數看小車能否修正回中心。5.2 用 Excel 繪制實時曲線抓取串口數據做 PID 性能分析將串口輸出重定向到文件XCOM 支持日志保存格式為POS:12 ERR:-12 OUT:-15 POS:8 ERR:-8 OUT:-10 ...用 Python 腳本提取ERR和OUT列生成 CSVimport re with open(pid_log.txt) as f: lines f.readlines() data [] for line in lines: match re.search(rERR:(-?\d) OUT:(-?\d), line) if match: data.append(f{match.group(1)},{match.group(2)}\n) with open(pid_data.csv, w) as f: f.writelines(data)導入 Excel作散點圖橫軸ERR縱軸OUT。理想位置式 PID 應呈近似直線斜率≈Kp若出現明顯彎曲說明積分/微分作用過強需調整Ki/Kd。5.3 碼盤分辨率對控制精度的影響量化計算1000 線碼盤經四倍頻后每轉 4000 個計數。若車輪直徑 60mm周長 ≈ 188.4mm則單個計數對應位移 188.4 / 4000 ≈ 0.0471 mm若要求定位精度 ±1mm則允許誤差范圍 ≈ ±21 個計數因此Kp至少需使|OUT| ≥ 1當|ERR| 21即Kp ≥ 1/21 ≈ 0.048未放大前→ 對應代碼中Kp ≥ 48放大 1000 倍此計算證明碼盤線數不是越高越好。600 線碼盤2400 計數/轉對應 0.0785mm/計數已滿足多數尋跡需求盲目追求 2500 線碼盤反而增加信號噪聲需更強濾波拖慢響應。本文還有配套的精品資源點擊獲取