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