
1. 項目概述PWM輸入捕獲在藍橋杯嵌入式賽題中的核心地位如果你正在準備藍橋杯嵌入式組的比賽那么“PWM輸入捕獲”這個功能點絕對是你繞不開、必須啃下的硬骨頭。它不像簡單的GPIO點燈那樣直觀也不像串口通信那樣有現成的輪子PWM輸入捕獲往往隱藏在賽題對“信號測量”、“電機測速”、“脈沖計數”等需求背后是區分選手水平的關鍵技術點之一。我參加過也指導過多次比賽發現很多同學在實現一路PWM測量時還能勉強應付但一旦題目要求同時測量兩路甚至更多路信號比如同時測量兩個電機的轉速程序就立刻變得混亂不堪要么測量不準要么互相干擾最終導致整個系統功能崩潰。這個項目標題“一路PWM輸入捕獲和兩路輸入捕獲驅動快速編寫及常見問題”精準地戳中了備賽過程中的兩大痛點效率和穩定性。“快速編寫”意味著我們需要一套清晰、模塊化的驅動框架避免每次比賽都從頭造輪子而“常見問題”則直指那些讓程序跑飛、數據跳變的隱蔽陷阱。本文將基于STM32平臺藍橋杯嵌入式競賽指定平臺深入拆解PWM輸入捕獲的原理分享一套經過實戰檢驗的、可同時穩定驅動一路標準PWM輸入和兩路通用輸入捕獲的驅動代碼框架并附上那些在官方手冊里找不到的“踩坑”實錄與解決方案。無論你是初次接觸定時器捕獲功能還是想優化自己的代碼結構這篇文章都能提供直接的、可“抄作業”的參考。2. 核心思路與方案選型為什么是定時器輸入捕獲模式在開始寫代碼之前我們必須搞清楚“輸入捕獲”到底是什么以及為什么它是測量PWM波形的首選方案。PWM脈沖寬度調制信號的核心參數是頻率和占空比。測量它的方法有很多比如外部中斷配合普通定時器計時或者使用輸入捕獲模式。注意對于藍橋杯賽題強烈建議一律使用定時器的輸入捕獲模式而不是外部中斷軟件計時。后者在測量高頻信號、多路信號時會大量占用CPU資源并極易因中斷嵌套和響應延遲導致測量誤差巨大屬于事倍功半的方案。輸入捕獲模式的精妙之處在于它將“檢測邊沿”和“記錄時刻”這兩件事全部交給了定時器硬件自動完成。其工作原理可以概括為當指定的輸入引腳上發生預設的邊沿事件如上升沿時定時器硬件會瞬間將當前計數器的值CNT鎖存到專用的捕獲/比較寄存器CCRx中并可以產生中斷。我們只需要在中斷服務函數中讀取兩次捕獲到的計數器值它們的差值就代表了兩個邊沿之間的時間間隔再乘以計數器的時鐘周期就能得到高電平或低電平的脈寬。對于一路標準PWM輸入捕獲STM32的定時器提供了一個“PWM輸入模式”這實際上是輸入捕獲模式的一個特殊應用。它利用了兩個捕獲通道通常是CH1和CH2一個捕獲上升沿一個捕獲下降沿并且自動將兩個通道關聯到同一個輸入引腳。這樣在一次捕獲周期內硬件自動完成了周期和占空比的測量非常高效。但它的限制是一個定時器只能測量一路PWM信號。當賽題要求同時測量兩路輸入信號可以是兩路PWM也可以是兩路頻率信號時PWM輸入模式就不夠用了。這時我們需要回歸到基礎的兩路獨立輸入捕獲方案。通常有兩種配置思路單定時器雙通道使用一個定時器的兩個捕獲通道如CH1和CH2分別捕獲兩路不同的輸入信號。這種方法硬件資源利用率高但兩路信號共享同一個計數器基準需要注意中斷邏輯和資源競爭。雙定時器使用兩個獨立的定時器各自負責一路信號的捕獲。這種方法邏輯清晰互不干擾但消耗的定時器資源較多。在藍橋杯競賽中CT117E開發板資源有限我們需要精打細算。我的實戰經驗是如果兩路信號需要非常精確的同步測量或者頻率較高優先考慮方案2雙定時器。如果資源緊張且兩路信號測量要求相對獨立方案1單定時器雙通道經過精心設計后也能穩定工作。下文將分別給出這兩種場景下的驅動實現。3. 驅動框架設計與模塊化思想直接寫一堆混雜在main.c里的中斷服務函數是備賽大忌。一個好的驅動框架應該達到以下目標接口清晰、配置靈活、與業務邏輯解耦。我們將驅動分為三個層次硬件抽象層HAL/LL庫配置利用STM32CubeMX生成初始化代碼或直接編寫寄存器/標準庫代碼完成GPIO、定時器、中斷的底層配置。驅動接口層封裝核心的捕獲邏輯提供諸如PWM_In_Start()、PWM_In_GetFreq()、PWM_In_GetDuty()等函數。這一層負責處理原始計數器值計算頻率和占空比并管理捕獲狀態機。應用層調用驅動接口層的函數獲取測量結果用于屏幕顯示、邏輯判斷或控制輸出。這里重點分享驅動接口層的設計。我們需要定義幾個關鍵的結構體和全局變量來管理捕獲狀態。// PWM輸入捕獲結構體 (用于標準PWM輸入模式一路) typedef struct { TIM_HandleTypeDef *htim; // 定時器句柄 uint32_t IC_Channel; // 輸入捕獲通道 volatile uint32_t period; // 周期值 (單位: 定時器計數 ticks) volatile uint32_t pulse; // 高電平脈寬值 (單位: 定時器計數 ticks) volatile float freq_hz; // 計算后的頻率 (Hz) volatile float duty_cycle; // 計算后的占空比 (%) volatile uint8_t is_updated; // 數據更新標志 } PWM_In_Capture_t; // 通用輸入捕獲結構體 (用于兩路獨立捕獲模式) typedef struct { TIM_HandleTypeDef *htim; uint32_t IC_Channel_Rising; // 上升沿捕獲通道 uint32_t IC_Channel_Falling; // 下降沿捕獲通道 (若只測頻率可只用一個) volatile uint32_t last_capture_rise; // 上一次上升沿捕獲值 volatile uint32_t last_capture_fall; // 上一次下降沿捕獲值 volatile uint32_t period_ticks; // 周期計數 volatile uint32_t pulse_ticks; // 脈寬計數 volatile uint8_t capture_stage; // 捕獲階段狀態機 (0:等待上升沿, 1:已捕獲上升沿等下降沿...) volatile uint8_t is_valid; // 本次測量是否有效 } Dual_Input_Capture_t;使用結構體的好處是當需要管理多路捕獲時比如一路PWM輸入兩路獨立捕獲你可以輕松定義多個實例例如PWM_In_Capture_t pwm1;和Dual_Input_Capture_t cap1, cap2;代碼結構非常清晰。4. 一路標準PWM輸入捕獲的快速實現我們以STM32的TIM2定時器通道1PA0引腳為例實現標準PWM輸入模式。使用STM32CubeMX配置最為快捷將PA0配置為TIM2_CH1。打開TIM2選擇“PWM Input Mode”。你會發現CH1和CH2會被自動配置。CH1被映射到TI1輸入用于觸發CH2被用于捕獲。配置預分頻器PSC和自動重載值ARR。這是關鍵ARR的值決定了你能測量的最大周期。例如定時器時鐘為72MHzPSC設為71則計數器時鐘為1MHz1us計數一次。若ARR設為65535則最大可測周期為65.535ms對應最小頻率約15.26Hz。你需要根據賽題信號的可能范圍來調整這兩個參數。開啟定時器更新中斷和捕獲/比較中斷。生成的初始化代碼后我們編寫中斷服務函數和驅動接口。核心邏輯在HAL_TIM_IC_CaptureCallback()這個回調函數中。// 全局PWM捕獲結構體實例 PWM_In_Capture_t g_pwm_in; void PWM_In_Init(TIM_HandleTypeDef *htim) { g_pwm_in.htim htim; g_pwm_in.is_updated 0; // 啟動輸入捕獲使用HAL庫通道2用于捕獲 HAL_TIM_IC_Start_IT(htim, TIM_CHANNEL_2); // 啟動從模式下的觸發輸入通道1 HAL_TIM_IC_Start(htim, TIM_CHANNEL_1); } // 定時器輸入捕獲中斷回調函數 void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim-Instance g_pwm_in.htim-Instance) { // 讀取捕獲值 uint32_t capture HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2); if (__HAL_TIM_GET_FLAG(htim, TIM_FLAG_CC2OF)) { // 發生了捕獲溢出說明兩次中斷之間CPU太忙沒來得及處理數據可能不可靠 __HAL_TIM_CLEAR_FLAG(htim, TIM_FLAG_CC2OF); // 可以在這里設置一個錯誤標志 } // 判斷是上升沿還是下降沿捕獲在PWM輸入模式下硬件自動設置。 // 通常我們直接利用兩次連續捕獲值來計算周期和脈寬。 // 更穩健的做法是結合定時器溢出次數來計算長周期。 static uint32_t last_capture 0; uint32_t diff 0; if (last_capture ! 0) { // 計算差值注意計數器溢出 if (capture last_capture) { diff capture - last_capture; } else { diff (htim-Instance-ARR 1) - last_capture capture; } // 判斷是周期還是脈寬在PWM輸入模式下硬件會將TI1的上升沿和下降沿分別路由到CH1和CH2。 // 但HAL庫的回調不區分邊沿。一個更清晰的方案是在CH1和CH2的中斷里分別處理。 // 這里提供一個簡化的、在CH2中斷中處理的計算思路假設信號穩定 // 我們連續記錄三次捕獲值差值1是脈寬差值2是周期-脈寬。 static uint32_t cap_buf[3] {0}; static uint8_t idx 0; cap_buf[idx] capture; if (idx 3) idx 0; // 當緩沖區有3個有效值時進行計算 if (cap_buf[0] ! 0 cap_buf[1] ! 0 cap_buf[2] ! 0) { uint32_t diff1 (cap_buf[1] cap_buf[0]) ? (cap_buf[1] - cap_buf[0]) : (htim-Instance-ARR 1 - cap_buf[0] cap_buf[1]); uint32_t diff2 (cap_buf[2] cap_buf[1]) ? (cap_buf[2] - cap_buf[1]) : (htim-Instance-ARR 1 - cap_buf[1] cap_buf[2]); // 通常較小的差值是脈寬高電平較大的是低電平寬度 g_pwm_in.pulse (diff1 diff2) ? diff1 : diff2; g_pwm_in.period diff1 diff2; // 一個完整周期 // 轉換為實際物理量 float timer_clk 72000000.0f / (htim-Instance-PSC 1); // 計算定時器計數頻率 g_pwm_in.freq_hz timer_clk / g_pwm_in.period; g_pwm_in.duty_cycle (g_pwm_in.pulse * 100.0f) / g_pwm_in.period; g_pwm_in.is_updated 1; // 設置更新標志 } } last_capture capture; } } // 應用層獲取數據的函數 uint8_t PWM_In_GetValues(float *freq, float *duty) { if (g_pwm_in.is_updated) { *freq g_pwm_in.freq_hz; *duty g_pwm_in.duty_cycle; g_pwm_in.is_updated 0; // 清除標志 return 1; // 成功獲取新數據 } return 0; // 數據未更新 }實操心得PWM輸入模式雖然方便但其中斷回調邏輯容易混淆。上述代碼中利用環形緩沖區計算周期和脈寬的方法比較穩健。更推薦的做法是直接使用寄存器級別操作在TIMx_IRQHandler中根據捕獲標志位如TIM_FLAG_CC1、TIM_FLAG_CC2來分別處理上升沿和下降沿這樣邏輯最清晰效率也最高。HAL庫的通用回調函數有時會掩蓋這些細節。5. 兩路獨立輸入捕獲的驅動實現當需要測量兩路獨立信號時我們采用單定時器雙通道的方案例如TIM3的CH1和CH2演示如何測量頻率。如果需要占空比則需要每個信號占用兩個捕獲通道。配置要點以TIM3_CH1-PA6 TIM3_CH2-PA7為例配置PA6、PA7為復用功能映射到TIM3_CH1和CH2。配置TIM3設置合適的PSC和ARR。將兩個通道都配置為輸入捕獲模式邊沿選擇為“上升沿”。開啟兩個通道的捕獲中斷以及定時器更新中斷。驅動層實現關鍵我們需要兩個獨立的結構體實例并在中斷中根據通道號來區分和處理。Dual_Input_Capture_t g_cap_ch1 g_cap_ch2; void Dual_Input_Capture_Init(TIM_HandleTypeDef *htim) { // 初始化結構體 g_cap_ch1.htim htim; g_cap_ch1.IC_Channel_Rising TIM_CHANNEL_1; g_cap_ch1.capture_stage 0; g_cap_ch1.is_valid 0; g_cap_ch2.htim htim; g_cap_ch2.IC_Channel_Rising TIM_CHANNEL_2; g_cap_ch2.capture_stage 0; g_cap_ch2.is_valid 0; // 啟動兩個通道的輸入捕獲中斷 HAL_TIM_IC_Start_IT(htim, TIM_CHANNEL_1); HAL_TIM_IC_Start_IT(htim, TIM_CHANNEL_2); // 啟動定時器 HAL_TIM_Base_Start(htim); } // 輸入捕獲中斷回調 void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM3) { // 判斷是哪個通道的中斷 if (__HAL_TIM_GET_FLAG(htim, TIM_FLAG_CC1) ! RESET) { __HAL_TIM_CLEAR_FLAG(htim, TIM_FLAG_CC1); _process_capture(g_cap_ch1, TIM_CHANNEL_1); } if (__HAL_TIM_GET_FLAG(htim, TIM_FLAG_CC2) ! RESET) { __HAL_TIM_CLEAR_FLAG(htim, TIM_FLAG_CC2); _process_capture(g_cap_ch2, TIM_CHANNEL_2); } } } // 定時器更新中斷溢出中斷回調用于處理長周期測量 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM3) { // 為每個通道的溢出計數加一如果結構體里有這個字段的話 // g_cap_ch1.overflow_count; // g_cap_ch2.overflow_count; } } // 內部處理函數 static void _process_capture(Dual_Input_Capture_t *cap, uint32_t channel) { uint32_t current_capture HAL_TIM_ReadCapturedValue(cap-htim, channel); switch (cap-capture_stage) { case 0: // 第一次捕獲到上升沿 cap-last_capture_rise current_capture; cap-capture_stage 1; break; case 1: // 第二次捕獲到上升沿計算周期 if (current_capture cap-last_capture_rise) { cap-period_ticks current_capture - cap-last_capture_rise; } else { // 發生了計數器溢出需要結合溢出次數計算這里簡化處理 cap-period_ticks (cap-htim-Instance-ARR 1) - cap-last_capture_rise current_capture; } cap-last_capture_rise current_capture; // 更新為新的上升沿 // 計算頻率 float timer_clk 72000000.0f / (cap-htim-Instance-PSC 1); cap-freq_hz timer_clk / cap-period_ticks; cap-is_valid 1; // 標記數據有效 // 狀態機可以保持在1等待下一個上升沿繼續計算周期 break; } } // 應用層獲取通道1頻率 uint8_t Dual_Input_GetFreq_CH1(float *freq) { if (g_cap_ch1.is_valid) { *freq g_cap_ch1.freq_hz; g_cap_ch1.is_valid 0; // 讀取后清除標志 return 1; } return 0; } // 同理實現GetFreq_CH2注意事項上述兩路捕獲代碼共享同一個定時器計數器。這意味著如果兩路信號頻率相差很大高頻信號的中斷可能會頻繁打斷低頻信號的處理但測量值本身捕獲的CNT值是硬件鎖存的所以頻率測量值仍然是準確的。主要影響是CPU中斷負荷。如果兩路信號頻率都很高需要考慮使用DMA進行捕獲或者使用兩個定時器來徹底隔離。6. 常見問題、調試技巧與避坑指南在實際編寫和調試過程中你會遇到各種各樣的問題。下面是我總結的“血淚”清單問題1測量值跳動非常大完全不準。可能原因A定時器時鐘和分頻設置錯誤。這是最常見的問題。務必確認HCLK、APB1/APB2的時鐘頻率以及定時器所在的總線。STM32中如果APBx預分頻系數不為1掛載在其上的定時器時鐘會倍頻。最穩妥的方法是在代碼中打印出SystemCoreClock以及htim-Instance-PSC的值手動計算計數頻率。可能原因BARR值設置過小。如果輸入信號的周期大于定時器計數器溢出一次的時間就會發生溢出。你需要確保ARR設置得足夠大使得(ARR1) / 計數器頻率 待測信號最大周期。或者你必須開啟定時器更新中斷在中斷中記錄溢出次數在計算時結合溢出次數和捕獲值。可能原因CGPIO引腳復用錯誤。檢查原理圖確認你使用的引腳確實支持該定時器的該通道。使用CubeMX檢查引腳配置沖突。問題2只能捕獲到第一次邊沿之后再也進不了中斷。可能原因中斷標志未清除。在中斷服務函數中讀取捕獲值后必須清除對應的捕獲/比較中斷標志位。使用HAL庫時HAL_TIM_IRQHandler會幫你清除但如果你像上面示例那樣直接操作標志位就必須手動清除TIM_FLAG_CCx。忘記清標志是導致中斷只觸發一次的經典錯誤。問題3兩路捕獲互相干擾一路有信號另一路就沒數據。可能原因中斷優先級沖突或處理時間過長。確保兩路捕獲中斷的優先級設置正確默認都是0。如果在一路捕獲的中斷處理函數中進行了非常耗時的操作如浮點計算、LCD刷新可能會導致另一路中斷被延遲響應甚至丟失。解決方案是中斷里只做最必要的事記錄捕獲值、設置標志將復雜的計算如換算為頻率、占空比放到主循環或低優先級任務中。問題4測量低頻信號如1Hz以下誤差大。原因與解決低頻信號周期長ARR必須設置得非常大這會降低計數分辨率。更嚴重的是計數器可能會溢出多次。必須開啟定時器更新中斷并維護一個“溢出計數器”。計算周期時周期 溢出次數 * (ARR1) 本次捕獲值 - 上次捕獲值。這是測量低頻信號的標配操作。問題5使用HAL庫的HAL_TIM_IC_CaptureCallback無法區分是哪一路通道的中斷。解決這正是HAL庫封裝帶來的一個小麻煩。推薦的方法是不完全依賴這個通用回調。你可以重寫定時器的全局中斷服務函數TIMx_IRQHandler在里面先調用HAL_TIM_IRQHandler(htimx)然后立即檢查具體的中斷標志位進行后續處理。或者像本文示例一樣在HAL_TIM_IC_CaptureCallback中再通過__HAL_TIM_GET_FLAG來主動判斷是哪個通道觸發的。調試技巧利用示波器這是最直接的調試工具。用示波器觀察輸入引腳的波形確認信號是否真的到達單片機其電壓、頻率、占空比是否在預期范圍內。軟件仿真在Keil或STM32CubeIDE的調試模式下使用邏輯分析儀功能Signal Trace可以模擬輸入PWM信號并觀察捕獲寄存器的變化非常適合驗證底層驅動邏輯。打印調試信息通過串口將關鍵的中間變量打印出來比如捕獲的原始計數器值、計算出的周期ticks等。這能幫你定位問題是發生在硬件捕獲階段還是軟件計算階段。7. 進階優化與資源管理策略當賽題功能越來越復雜系統資源定時器、中斷變得緊張時我們需要更精細的策略。策略一DMA定時器捕獲對于需要極高頻率、連續不斷采集脈沖的應用如高速編碼器可以使用DMA將捕獲寄存器的值自動搬運到內存數組中。CPU只需要在數組半滿或全滿時處理一批數據大大降低了中斷頻率。這在藍橋杯高級應用中可能會遇到。策略二輸入捕獲與輸出PWM共用定時器一個定時器有多個通道可以同時用于輸入捕獲和輸出PWM。例如用TIM2的CH1做PWM輸出控制電機用CH2做輸入捕獲測量另一路信號。關鍵點在于PWM輸出和輸入捕獲的時鐘源、預分頻、計數模式必須一致。通常配置為向上計數PWM輸出模式1/2。這樣做可以節省一個定時器資源。策略三低功耗下的捕獲考慮如果設備有低功耗要求在測量間歇性信號時可以讓定時器在無信號時停止由外部中斷喚醒并啟動定時器進行捕獲。這需要對定時器的從模式、觸發源有更深的理解。資源分配建議針對CT117E板TIM2/TIM3/TIM4通用定時器功能齊全PWM、輸入捕獲、編碼器接口等是主力。TIM1/TIM8高級定時器功能更強帶死區控制等適合復雜的電機控制但資源更寶貴。基本定時器TIM6/TIM7只能用于基本定時不能做輸入捕獲。 在規劃時優先把需要輸入捕獲的功能分配到通用定時器上并提前規劃好各通道的用途避免沖突。最后驅動代碼的穩定性和效率最終要靠大量的測試來驗證。建議你建立一個簡單的測試用例用另一個定時器產生已知頻率和占空比的PWM信號輸入到你的捕獲引腳然后驗證測量結果是否準確。通過反復的“產生-測量-比對”循環你能最快地定位和解決驅動中的問題。把這些模塊調試穩定后封裝成獨立的.c/.h文件它們就會成為你應對藍橋杯嵌入式賽題的可靠武器庫。