
STM32 閉環風扇PA0 EXTI 真測 RPM CH224K 快充協商供電附完整工程標簽STM32、STM32F103C8T6、EXTI、DS18B20、WS2812、Type-C、CH224K、嵌入式硬件倉庫https://github.com/kaka12331/stm32-closed-loop-fan這篇解決什么問題網上的「智能風扇」教程基本是同一套ADC 讀電位器 → 映射成占空比 → 寫 PWM。它有兩個說不出口的問題它是開環的。你寫下去 60% 占空比風扇到底轉了多少轉程序完全不知道。風扇卡住、堵轉、電壓跌了代碼一無所覺還在開開心心輸出 60%。它供不動真風扇。用 USB 5V 帶一個 12V 服務器風扇要么轉不起來要么一加速整個板子跟著復位。這個工程把這兩點都解決了PA0 用 EXTI 捕獲風扇自帶的 TACH 測速線算出真實 RPM供電側用Type-C CH224K 協商高壓直供風扇功率軌MCU 和傳感器走獨立的 5V→3.3V 邏輯軌。順帶記錄三個把我坑了很久的地方sprintf(%f)直接死機、DS18B20 在有中斷的系統里讀不出溫度、以及 OLED 全屏刷動畫慢得像幻燈片。硬件與引腳引腳功能PA0風扇測速 TACHEXTI上拉PA1電位器 ADC 調速PA2DS18B20 單總線PA7WS2812 數據線PA8風扇 PWM低邊開關PB8 / PB9OLED I2C SCL / SDAPB12 / PB15按鍵動畫模式 / 風扇模式供電為什么要上 CH224K普通做法是給風扇單獨接一個 12V 電源適配器桌面上就多一根線。這里換了個思路Type-C ──? CH224K 協商高壓 ──? VBUS 直供風扇功率軌 │ └──? TPS54260 ──? 5V ──? AMS1117-3.3 ──? MCU / 傳感器CH224K 是一顆受電端Sink快充協議芯片向充電器申請高于 5V 的檔位。談成之后 VBUS 上就是高壓直接喂給風扇的功率軌MCU 這一側再用 TPS54260 降到 5V、AMS1117 降到 3.3V。這么做的實際收益是強弱電分軌風扇啟動瞬間的浪涌全部在功率軌上消化不會打到 MCU 的 3.3V 上。開頭說的「一加速就復位」根因就是風扇和 MCU 共用一路弱電源。一根 Type-C 線同時供電和調速桌面上不用再多一個電源磚——這是這個項目最實用的部分。一、真閉環EXTI 數 TACH 脈沖算 RPM四線 PWM 風扇的第三根線是 TACH 測速輸出風扇每轉一圈輸出固定數量的脈沖絕大多數是 2 個。把它接到 PA0配成 EXTI 上升沿觸發中斷里只做一件事計數。externvolatileuint32_tFan_Pulse_Count;// EXTI 中斷里 然后在 700 ms 的慢速任務里結算__disable_irq();// 關中斷保護數據uint32_tcurrent_pulsesFan_Pulse_Count;Fan_Pulse_Count0;__enable_irq();// 開中斷real_rpm(uint16_t)(current_pulses*42.85);42.85 這個魔數是怎么來的別背推一遍RPM 脈沖數 ÷ 2每轉2個脈沖 ÷ 0.7秒 × 60秒 脈沖數 × 60 ÷ (2 × 0.7) 脈沖數 × 42.857所以如果你的風扇是每轉 1 個或 4 個脈沖或者你把窗口從 700 ms 改成別的值這個系數要跟著重算照抄會得到一個離譜的轉速。為什么讀計數值必須關中斷current_pulses Fan_Pulse_Count; Fan_Pulse_Count 0;這兩行之間如果來了一個 EXTI那一個脈沖就被清零吞掉了。單次誤差不大但它是系統性偏低——每次結算都可能丟轉速會一直比真實值小一點。關中斷的窗口只有兩條賦值語句對 EXTI 的影響可以忽略。OLED 上同時顯示目標占空比和真實 RPM這兩個數不一致的時候就是風扇出問題的時候——這是開環方案給不了的信息。二、DS18B20 讀不出溫度先看你有沒有關中斷DS18B20 是單總線器件時序按微秒算復位脈沖 480 μs、寫 0 的低電平 60~120 μs、讀時隙必須在 15 μs 內采樣。問題來了你的系統里有 EXTI風扇測速、有 TIM 中斷。**風扇轉起來以后TACH 中斷每秒來上百次。**只要有一個中斷插在讀時隙中間這一位就廢了整個字節錯位讀回來是亂碼或者 85.0DS18B20 的上電默認值。所以整段讀寫必須原子化__disable_irq();// 保護微秒級時序uint8_ttemp_statusDS18B20_ReadTemperature(temperature);DS18B20_TriggerConversion();// 順手觸發下一次轉換__enable_irq();這里還用了一個小技巧讀完立刻觸發下一次轉換。DS18B20 轉換 12 位精度要 750 ms如果「觸發—等待—讀取」寫在一起主循環就得干等 750 ms。改成「這次讀上次的結果順便觸發下次」等待時間被 700 ms 的任務周期自然吸收掉一次都不用等。三、sprintf(%f)直接死機溫度是浮點數最自然的寫法是sprintf(display_buf,%.2f C,temperature);// 跑到這里就飛了Keil 標準庫下printf系列對浮點的支持要額外的庫支持和明顯更大的棧。默認啟動文件里的棧大小常見是 0x400扛不住直接棧溢出進 HardFault。不用去改啟動文件把小數拆成兩個整數就行inttemp_int(int)temperature;// 整數部分inttemp_frac(int)((temperature-temp_int)*100);// 兩位小數if(temp_frac0)temp_frac-temp_frac;// 防止出現 25.-5sprintf(display_buf,%2d.%02d C,temp_int,temp_frac);那行if (temp_frac 0)別刪。溫度是 -3.25 ℃ 的時候整數部分取到 -3小數部分算出來是 -25拼出來就是-3.-25。同一個坑我在平衡車工程里是主動繞開的——那邊干脆自己寫了FormatFixed2()和ParseFloat()全程不碰 printf 的浮點。四、OLED 動畫把 I2C 通信量砍掉一半128×64 的屏用軟件 I2C 刷全屏一幀 1024 字節。原始寫法一秒鐘刷不了幾幀動畫像幻燈片。這個工程的圖源是 120×120 的方圖要縮放貼到 128×64 的屏上。OLED_ShowFrame_Magic()做了四處優化for(page0;page8;page){// 優化1光標直接定位到居中起點 X32跳過左邊的黑邊OLED_SetCursor(page,32);// 優化2只循環 64 次不畫多余的背景I2C 通信量直接減半for(col0;col64;col){oled_byte0;// 優化3用位移 代替除法src_x(col*15)3;for(bit0;bit8;bit){dst_y(page3)bit;src_y(dst_y*15)3;// 優化4用 0x07 代替 % 8 取余if(Image[src_y*15(src_x3)](0x80(src_x0x07))){oled_byte|(1bit);}}OLED_WriteData(oled_byte);}}真正起決定作用的是第 2 條。方圖貼到 128 寬的屏上兩側本來就是黑邊那 64 列純粹是在給屏幕發 0。只畫中間 64 列I2C 傳輸量直接從 1024 字節降到 512 字節——通信是瓶頸所以幀率大致翻倍。第 3、4 條3代替/8、0x07代替%8在 Cortex-M3 上收益小得多因為它有單周期硬件除法器。但這種寫法在循環體里是免費的寫了不虧。改完之后每 20 ms 刷一幀50 FPS。五、調度一個 20 ms 心跳帶兩條時間線沒上 RTOS也沒有滿屏的delay。主循環只有一個節拍while(1){/* 按鍵處理 */uint8_tkeyKey_GetNum();if(key15){current_mode1;mode_init_flag1;OLED_Clear();}elseif(key12){current_mode0;mode_init_flag1;OLED_Clear();}if(current_mode0){/* 動畫模式每個節拍刷一幀 50 FPS */}else{/* 風扇模式 */adc_valueADC_Get_Average_Value(ADC_Channel_1,10);target_speed(uint8_t)(((uint32_t)adc_value*100)/4095);Set_Fan_Speed(target_speed);/* WS2812 隨動低速純藍 → 高速純紅 */uint8_tred_val(target_speed*255)/100;uint8_tblue_val255-red_val;for(uint8_ti0;i4;i){WS2812_SetColor(i,red_val,0,blue_val);}WS2812_Refresh();if(fan_slow_tick35)/* 35 × 20 ms 700 ms */{fan_slow_tick0;/* 結算 RPM 讀溫度 */}}Delay_ms(20);/* 統一心跳 */anim_tick;fan_slow_tick;}快慢分離的理由很實際ADC 和 PWM 要跟手旋鈕一轉就得有反應20 ms 正好轉速需要一個足夠長的窗口才能積夠脈沖、算得準溫度傳感器本身也要 750 ms 才轉換完塞進 700 ms 的慢速任務剛好。mode_init_flag是配套的細節切模式時OLED_Clear()會把靜態標簽一起擦掉所以要用這個標志讓新模式在第一個節拍里把Tgt:/RPM:/Temp:這些不變的文字重畫一次之后每個周期只更新數字。否則你會看到標簽在閃。這套「一個心跳 多條計數時間線」是最省事的裸機調度。想更進一步做成事件驅動/分層架構可以看我另一篇告別幾千行的 main.cSTM32 裸機開發天花板面向對象與事件驅動架構關鍵參數速查參數值為什么主循環節拍20 msADC/PWM 跟手的下限慢任務周期700 ms35 拍夠積脈沖且 ≥ DS18B20 的 750 ms 轉換需求RPM 系數42.8560 ÷ (2 脈沖/轉 × 0.7 s)換風扇要重算ADC 平均次數10壓電位器抖動動畫幀率50 FPS20 ms/幀WS2812 數量4低速純藍 → 高速純紅線性漸變復現步驟Keil μVision / MDK-ARM 打開Project.uvprojx目標芯片STM32F103C8T6SWD 下載先別接風扇功率軌。只上 5V 邏輯確認 OLED 亮、電位器數值隨旋鈕變化接上風扇用手隔著東西短暫擋一下看 OLED 的 RPM 是否跟著掉——不掉就是 TACH 線沒接好或者沒配上拉確認你的風扇每轉幾個脈沖不是 2 就改42.85最后接 DS18B20。讀回來恒定 85.0 ℃ 說明時序被打斷了回頭檢查__disable_irq()有沒有包住整段讀寫這個工程還能改的地方坦白說現在的Delay_ms(20)是阻塞的——那 20 ms 里 CPU 在空轉。想更徹底可以把節拍換成一個 TIM 中斷置標志位主循環只查標志。對這個項目來說沒必要但如果你要在上面再加 Wi-Fi 或者更多傳感器這一步遲早要走。另外DS18B20_ReadTemperature期間關中斷那幾百微秒里 TACH 脈沖是會丟的。因為只發生在 700 ms 一次的慢任務里對轉速的影響遠小于 1%可以接受但如果你要把測速精度做到工業級得換成硬件定時器輸入捕獲來數脈沖。相關文章裸機調度想做得更規范面向對象與事件驅動架構電源部分的底層原理DC-DC 降壓電路的「高頻熱回路」與布板玄學倉庫地址https://github.com/kaka12331/stm32-closed-loop-fan標準外設庫等第三方代碼遵循其原協議。如果你基于這個工程做了改動歡迎在評論區交流。