控制:從傳感器選型到PID調(diào)優(yōu))
簡介這是一份面向嵌入式初學者與電子設計競賽選手的STM32溫控風扇完整工程源碼圍繞溫度采集、PID運算與PWM調(diào)速展開可用于課程設計、畢業(yè)設計或智能散熱方案的二次開發(fā)。壓縮包共181個文件約3.73MB以44個c源文件與45個h頭文件為核心涵蓋ADC、TIM、I2C等外設驅(qū)動另有25個o、24個crf等編譯中間文件及uvprojx、uvoptx工程配置配合hex燒錄文件與map、lst映射清單方便直接編譯下載與調(diào)試。系統(tǒng)集成1602液晶顯示與1x4鍵盤加減調(diào)速支持溫度閾值設定、過熱保護與狀態(tài)指示完整呈現(xiàn)從傳感器采樣到電機驅(qū)動的控制鏈路。目前已有5801人學習下載適合希望掌握STM32外設配置、PID閉環(huán)控制與PWM調(diào)速實戰(zhàn)的讀者參考借鑒。1. 基于 STM32 的溫控風扇從“能轉(zhuǎn)”到“該轉(zhuǎn)多快”的那條分界線很多人第一次做溫控風扇代碼燒進去風扇轉(zhuǎn)了就以為成了。但真正上手才發(fā)現(xiàn)問題不在“轉(zhuǎn)不轉(zhuǎn)”而在“什么時候轉(zhuǎn)、轉(zhuǎn)多快、溫度降下來之后怎么收”?;?STM32 的溫控風扇本質(zhì)是一個閉環(huán)控制系統(tǒng)NTC 或數(shù)字溫度傳感器采集環(huán)境溫度MCU 按預設策略計算目標轉(zhuǎn)速再通過 PWM 驅(qū)動風扇功率級。它解決的是“溫度變了風量跟著變”這件事適合剛學完 GPIO 和定時器、想找一個能摸到完整信號鏈的嵌入式練手項目也適合需要給機箱、電源、小空間做低成本散熱的從業(yè)者。下面按“選型—采集—控制—驅(qū)動—避坑—調(diào)優(yōu)”的順序把這條鏈路拆開講清楚。2. 先定方案再畫板傳感器、風扇和驅(qū)動怎么選2.1 溫度采集NTC 分壓還是數(shù)字傳感器溫控風扇的第一顆扣子是溫度怎么進 MCU。常見做法有兩類NTC 熱敏電阻加分壓電阻進 ADC或者 DS18B20、LM75 這類數(shù)字傳感器走單總線或 I2C。NTC 方案便宜、電路簡單但它是非線性器件ADC 讀到的電壓要經(jīng)過 Steinhart-Hart 公式或查表才能換成溫度。好處是響應快、成本低適合批量產(chǎn)品壞處是每一顆 NTC 的 B 值有離散性不校準的話不同板子之間可能差 2 到 3 攝氏度。數(shù)字傳感器省去了標定環(huán)節(jié)直接讀寄存器就是攝氏度分辨率常見 0.0625 或 0.125 攝氏度但成本高一些而且 I2C 走線長了容易被風扇電機干擾。我一般會這樣分如果只是做一臺樣機、追求開發(fā)速度選 DS18B20 或 LM75如果要上量、對 BOM 敏感選 NTC 加 ADC但必須留一個校準工位。下面給一個 NTC 查表法的核心代碼假設用的是 10k NTC、B 值 3950、分壓電阻也是 10k、ADC 12 位、參考電壓 3.3V。// NTC 溫度查表10k25C, B3950, 上拉 10k 到 3.3V // adc_val: 0~4095對應 NTC 兩端電壓 float ntc_get_temp(uint16_t adc_val) { // 先算 NTC 當前阻值R_ntc R_pull * (adc_val / (4095 - adc_val)) float r_ntc 10000.0f * ((float)adc_val / (4095.0f - (float)adc_val)); // 查表溫度點每 5 度一個阻值由 B 值公式反推 // 這里只列幾個關鍵點實際項目建議用 1 度間隔的 256 點表 const float temp_table[] {-20, -10, 0, 10, 25, 40, 60, 80, 100}; const float res_table[] {97000, 55000, 32000, 20000, 10000, 5300, 2500, 1200, 600}; // 線性插值找到 r_ntc 落在哪兩個點之間 for (int i 0; i 8; i) { if (r_ntc res_table[i1] r_ntc res_table[i]) { float ratio (r_ntc - res_table[i1]) / (res_table[i] - res_table[i1]); return temp_table[i1] ratio * (temp_table[i] - temp_table[i1]); } } return -100.0f; // 超出范圍返回錯誤值 }這段代碼的邏輯是先把 ADC 值換算成 NTC 阻值再用查表加線性插值得到溫度。參數(shù)上10000.0f是分壓電阻4095.0f是 12 位 ADC 滿量程這兩個值必須和實際電路一致否則溫度會整體偏移。查表點越密插值誤差越小如果只做 5 度間隔中間段的誤差可能到 1 攝氏度左右。實際項目里我會把表做成 1 度間隔用腳本生成直接放在const數(shù)組里省 RAM。2.2 風扇選型兩線、三線還是四線風扇選錯后面控制策略全白搭。兩線風扇只有電源和地轉(zhuǎn)速靠調(diào)電壓或 PWM 斬波但低速時可能啟動不了而且沒有測速反饋。三線風扇多一根 FG 信號輸出與轉(zhuǎn)速成正比的方波可以用來測實際轉(zhuǎn)速但調(diào)速仍然靠外部 PWM。四線風扇是 PWM 調(diào)速加 FG 測速PWM 信號直接進風扇內(nèi)部驅(qū)動 IC外部只需要給一個 25kHz 左右的方波調(diào)速線性度最好低速啟動也穩(wěn)。如果做溫控風扇我優(yōu)先推薦四線風扇。原因很簡單PWM 頻率可以固定在 25kHz避開人耳可聞范圍不會聽到嘯叫FG 反饋能形成閉環(huán)代碼里可以判斷“我讓它轉(zhuǎn) 2000 轉(zhuǎn)它實際轉(zhuǎn)了多少”。三線風扇也能用但 PWM 斬波會干擾 FG 信號測速需要加濾波。兩線風扇只適合“溫度高了就全速轉(zhuǎn)”的粗放場景。2.3 驅(qū)動級MOSFET 低邊還是專用驅(qū)動風扇電流一般不大12V 風扇常見 0.1A 到 0.5A5V 風扇更小。低邊 NMOS 加續(xù)流二極管是最簡單的驅(qū)動方式PWM 直接打 MOS 管柵極。但要注意風扇是感性負載關斷瞬間會產(chǎn)生反電動勢必須加續(xù)流二極管否則 MOS 管容易被擊穿。如果風扇電流超過 1A或者需要防反接、過流保護就用專用低邊驅(qū)動芯片內(nèi)部集成續(xù)流和保護。下面是一個低邊驅(qū)動的 GPIO 和定時器配置片段以 STM32F103 為例用 TIM3 的通道 1 輸出 PWMPA6 作為輸出引腳。// STM32F103 TIM3_CH1 PWM 初始化PA6 輸出頻率 25kHz void fan_pwm_init(void) { // 1. 開時鐘GPIOA 和 TIM3 RCC-APB2ENR | RCC_APB2ENR_IOPAEN; RCC-APB1ENR | RCC_APB1ENR_TIM3EN; // 2. PA6 配成復用推挽輸出50MHz GPIOA-CRL ~(0xF 24); GPIOA-CRL | (0xB 24); // 復用推挽50MHz // 3. TIM3 時基72MHz / (711) 1MHz 計數(shù)頻率 TIM3-PSC 71; // 25kHz 周期1MHz / 25000 40所以 ARR 39 TIM3-ARR 39; // 4. PWM 模式 1CNT CCR1 時輸出高 TIM3-CCMR1 | (0x6 4); // OC1M 110 TIM3-CCMR1 | (1 3); // OC1PE 預裝載使能 TIM3-CCER | (1 0); // CC1E 輸出使能 // 5. 初始占空比 0風扇不轉(zhuǎn) TIM3-CCR1 0; // 6. 使能自動重裝載預裝載開定時器 TIM3-CR1 | (1 7); TIM3-CR1 | (1 0); } // 設置風扇轉(zhuǎn)速duty 0~100 void fan_set_speed(uint8_t duty) { if (duty 100) duty 100; // ARR 39所以 CCR duty * 40 / 100 TIM3-CCR1 (uint16_t)(duty * 40 / 100); }這段代碼的關鍵參數(shù)是PSC 71和ARR 39。STM32F103 的 TIM3 掛在 APB1 上時鐘是 72MHz預分頻 72 之后得到 1MHz 計數(shù)頻率再除以 40 就是 25kHz。CCR1決定占空比fan_set_speed(50)就是 50% 占空比。注意四線風扇的 PWM 信號是給風扇內(nèi)部驅(qū)動用的占空比和轉(zhuǎn)速大致成線性但不同型號有差異最好用 FG 反饋做一次標定。3. 控制策略落地從閾值開關到 PID 的代碼實現(xiàn)3.1 最簡單的閾值控制先跑通再優(yōu)化剛上手時別急著上 PID。先用閾值控制把整條鏈路跑通溫度低于 35 度停轉(zhuǎn)35 到 45 度之間線性映射到 30% 到 70% 占空比高于 45 度全速。這樣代碼簡單調(diào)試直觀能快速驗證傳感器和驅(qū)動是否正常。// 閾值加線性映射temp 單位攝氏度返回 0~100 占空比 uint8_t temp_to_duty(float temp) { if (temp 35.0f) { return 0; // 低于 35 度停轉(zhuǎn) } else if (temp 45.0f) { // 35~45 度映射到 30~70 return (uint8_t)(30.0f (temp - 35.0f) * 4.0f); } else { return 100; // 高于 45 度全速 } }這個函數(shù)的參數(shù)是 35、45、30、70 四個點。35 和 45 是溫度閾值30 和 70 是對應占空比。實際調(diào)試時你會發(fā)現(xiàn)風扇在閾值附近頻繁啟停聽起來像“喘氣”。解決辦法是加遲滯比如低于 33 度才停高于 37 度才啟動中間保持當前轉(zhuǎn)速。遲滯帶一般設 2 到 4 攝氏度具體看風扇慣性和環(huán)境熱容。3.2 加入 FG 測速把“實際轉(zhuǎn)速”讀回來四線風扇的 FG 信號是開漏輸出需要上拉電阻一般 10k 到 4.7k。FG 頻率和轉(zhuǎn)速的關系是轉(zhuǎn)速 FG 頻率 × 60 / 每轉(zhuǎn)脈沖數(shù)。常見風扇每轉(zhuǎn) 2 個脈沖所以 100Hz 對應 3000 轉(zhuǎn)。用定時器的輸入捕獲功能測 FG 周期就能算出實際轉(zhuǎn)速。// TIM2_CH1 輸入捕獲測 FG 周期PA0 接 FG 信號 volatile uint32_t fg_period 0; // 單位微秒 volatile uint8_t fg_ready 0; void fg_capture_init(void) { RCC-APB2ENR | RCC_APB2ENR_IOPAEN; RCC-APB1ENR | RCC_APB1ENR_TIM2EN; // PA0 浮空輸入 GPIOA-CRL ~(0xF 0); GPIOA-CRL | (0x4 0); // TIM2 時基72MHz / 72 1MHz1 計數(shù) 1 微秒 TIM2-PSC 71; TIM2-ARR 0xFFFF; // CH1 輸入捕獲上升沿 TIM2-CCMR1 | (0x1 0); // CC1S 01IC1 映射到 TI1 TIM2-CCER | (1 0); // CC1E 使能 TIM2-CCER ~(1 1); // 上升沿捕獲 // 開捕獲中斷 TIM2-DIER | (1 1); NVIC_EnableIRQ(TIM2_IRQn); TIM2-CR1 | (1 0); } // 中斷里讀捕獲值算周期 void TIM2_IRQHandler(void) { static uint32_t last_capture 0; if (TIM2-SR (1 1)) { uint32_t now TIM2-CCR1; if (now last_capture) { fg_period now - last_capture; } else { fg_period (0xFFFF - last_capture) now; } last_capture now; fg_ready 1; TIM2-SR ~(1 1); } } // 換算轉(zhuǎn)速每轉(zhuǎn) 2 脈沖 uint32_t fan_get_rpm(void) { if (fg_period 0) return 0; // 轉(zhuǎn)速 60秒 / (周期微秒 * 2脈沖) * 1e6 return 30000000UL / fg_period; }這段代碼用輸入捕獲測 FG 周期fg_period單位是微秒fan_get_rpm按每轉(zhuǎn) 2 脈沖換算。參數(shù)上PSC 71保證 1 微秒分辨率ARR 0xFFFF防止溢出太頻繁。實際調(diào)試時如果轉(zhuǎn)速讀數(shù)為 0先檢查 FG 上拉電阻是否接好再用示波器看 PA0 有沒有方波。如果讀數(shù)跳動大可以在中斷里做多次平均或者用定時器每 100ms 讀一次捕獲值。3.3 PID 控制讓轉(zhuǎn)速平滑跟隨溫度閾值控制夠用但如果你想讓轉(zhuǎn)速隨溫度平滑變化PID 是繞不開的。溫控風扇的 PID 比較特殊被控量是溫度執(zhí)行器是風扇轉(zhuǎn)速但溫度變化慢風扇響應快所以積分時間要長微分項可以很弱甚至不用。// 增量式 PID輸入目標溫度和當前溫度輸出占空比增量 typedef struct { float kp; float ki; float kd; float last_err; float prev_err; float out; } pid_t; float pid_calc(pid_t *pid, float target, float actual) { float err target - actual; float delta; // 增量式Δu Kp*(e - e1) Ki*e Kd*(e - 2*e1 e2) delta pid-kp * (err - pid-last_err) pid-ki * err pid-kd * (err - 2.0f * pid-last_err pid-prev_err); pid-prev_err pid-last_err; pid-last_err err; pid-out delta; // 限幅 0~100 if (pid-out 100.0f) pid-out 100.0f; if (pid-out 0.0f) pid-out 0.0f; return pid-out; }參數(shù)整定上我一般先設kp 2.0、ki 0.05、kd 0然后觀察溫度階躍響應。如果溫度到目標值后還在緩慢上升說明積分不夠加大ki如果轉(zhuǎn)速來回振蕩說明kp太大先減kp再加ki。溫控風扇的采樣周期建議 500ms 到 1s太快了溫度沒變化PID 會積分飽和太慢了響應遲鈍。注意PID 輸出要限幅否則積分項會累積到很大溫度降下來后風扇還在全速轉(zhuǎn)這就是“積分飽和”血淚經(jīng)驗。4. 避坑與排查溫控風扇最容易翻車的 5 個點4.1 風扇不轉(zhuǎn)或轉(zhuǎn)速上不去現(xiàn)象PWM 占空比給到 100%風扇還是不轉(zhuǎn)或者轉(zhuǎn)得很慢。原因通常是驅(qū)動級供電不足或 MOS 管沒完全導通。先量風扇兩端電壓如果只有 5V 而風扇是 12V 的那肯定轉(zhuǎn)不起來。如果電壓正常但電流很小檢查 MOS 管柵極電壓是否達到閾值STM32 的 GPIO 是 3.3V有些 MOS 管需要 4.5V 以上才能完全導通這時候要加一級三極管或?qū)S抿?qū)動。解決換低閾值 MOS 管或者用驅(qū)動芯片。4.2 溫度讀數(shù)跳變嚴重現(xiàn)象ADC 讀到的溫度在 2 到 3 攝氏度范圍內(nèi)隨機跳。原因有兩個一是 NTC 走線太長引入了電源噪聲二是 ADC 采樣時間太短采樣保持電容沒充夠。解決在 NTC 兩端并一個 100nF 電容ADC 采樣時間設到 239.5 周期軟件里做 8 次平均。如果還跳檢查參考電壓是否穩(wěn)定VDDA 和 VSSA 之間要加 100nF 加 10uF 去耦。4.3 PWM 頻率選錯導致風扇嘯叫現(xiàn)象風扇轉(zhuǎn)起來有刺耳的“滋滋”聲。原因是 PWM 頻率落在人耳可聞范圍通常是 1kHz 到 5kHz。解決把 PWM 頻率提到 25kHz 以上超出人耳范圍。但注意頻率太高會導致 MOS 管開關損耗增加一般 25kHz 到 30kHz 是平衡點。四線風扇直接給 25kHz 方波內(nèi)部驅(qū)動會處理兩線風扇用低邊斬波時頻率也要避開可聞段。4.4 閾值附近頻繁啟?,F(xiàn)象溫度在閾值附近時風扇一會兒轉(zhuǎn)一會兒停像喘氣。原因是閾值控制沒有遲滯溫度微小波動就觸發(fā)狀態(tài)切換。解決加遲滯帶比如啟動閾值 37 度停止閾值 33 度中間保持當前狀態(tài)。如果用了 PID檢查積分項是否太小導致輸出在 0 附近抖動。另外溫度采樣加一階低通濾波temp_filtered 0.9 * temp_filtered 0.1 * temp_raw能明顯改善。4.5 FG 測速讀數(shù)為 0 或翻倍現(xiàn)象風扇在轉(zhuǎn)但fan_get_rpm返回 0或者讀數(shù)比實際高一倍。原因FG 上拉電阻沒接或者每轉(zhuǎn)脈沖數(shù)設錯。常見風扇每轉(zhuǎn) 2 脈沖但有些是 1 脈沖或 4 脈沖。解決先用示波器看 FG 引腳有沒有方波沒有就加上拉有方波但讀數(shù)不對改fan_get_rpm里的除數(shù)。另外輸入捕獲中斷里要清標志位否則會反復進中斷讀數(shù)亂跳。5. 進階調(diào)優(yōu)用串口繪圖和分段 PID 把溫度壓到 ±1 度把基礎功能跑通之后真正拉開差距的是調(diào)優(yōu)。我習慣用串口把溫度和轉(zhuǎn)速實時打出來上位機用簡單曲線看趨勢。下面是一個 100ms 定時上報的格式每行包含時間、溫度、目標轉(zhuǎn)速、實際轉(zhuǎn)速方便直接丟進繪圖工具。// 每 100ms 上報一次格式t,temp,target_rpm,actual_rpm void report_task(void) { static uint32_t tick 0; float temp ntc_get_temp(adc_read()); uint32_t target duty_to_rpm(g_fan_duty); // 根據(jù)占空比估算目標轉(zhuǎn)速 uint32_t actual fan_get_rpm(); printf(%lu,%.2f,%lu,%lu\n, tick, temp, target, actual); }參數(shù)上tick每 100ms 加一temp保留兩位小數(shù)target和actual都是整數(shù)。把這段輸出接到串口繪圖工具你能直觀看到溫度到目標值后有沒有超調(diào)、轉(zhuǎn)速有沒有振蕩。如果超調(diào)大先把kp減半如果穩(wěn)定時間太長把ki加倍。注意串口打印本身會占用時間如果波特率低于 115200打印一行可能超過 1ms影響控制周期所以上報任務要放在低優(yōu)先級或者用 DMA 發(fā)送。分段 PID 是另一個實用技巧。溫度離目標遠時用大kp快速響應溫度接近目標時換小kp和小ki避免超調(diào)。實現(xiàn)上就是根據(jù)誤差絕對值切換兩組參數(shù)// 分段 PID誤差大于 5 度用激進參數(shù)小于 5 度用保守參數(shù) float pid_calc_segmented(pid_t *pid, float target, float actual) { float err target - actual; if (err 5.0f || err -5.0f) { pid-kp 4.0f; pid-ki 0.10f; pid-kd 0.0f; } else { pid-kp 1.5f; pid-ki 0.03f; pid-kd 0.0f; } return pid_calc(pid, target, actual); }這套參數(shù)不是萬能但能給你一個起點。我自己的習慣是先讓風扇在 40 度目標下跑 10 分鐘看溫度曲線能不能壓在 ±1 度以內(nèi)如果壓不住先檢查傳感器是不是被風扇氣流直吹導致讀數(shù)偏低。傳感器位置比 PID 參數(shù)重要得多這是踩過坑才明白的。希望幫到你。本文還有配套的精品資源點擊獲取