目錄
定時器基本定時功能實現
CubeMX設置
手動書寫代碼部分
定時器啟動
實現溢出回調函數
HAL_Delay介紹
HAL_Delay實現原理
HAL_Delay的優點
HAL_Delay的缺點
利用滴答定時器(SysTick)實現微秒級延時
PWM
?PWM介紹
通用定時器中的重要寄存器
PWM中的捕獲比較通道
什么是定時器通道?
定時器通道如何工作?
為什么需要定時器通道?
PWM實現呼吸燈
CubeMX配置
關于TIMx_PSC和TIMx_ARR的計算
幾種不同方式實現設定PWM的周期
如何選擇不同PSC與ARR的組合?
代碼編寫
定時器基本定時功能實現
CubeMX設置
注意,這里只是對定時器配置實現一個很簡單的定時功能。
時鐘來源
定時器中斷使能?
定時器預分頻值,計數模式,計數周期
手動書寫代碼部分
定時器啟動
下面主要介紹一下關于定時器的簡單運用
關于下面圖片中的初始化函數我們可以不用過于在意,因為在我們上面CUBEMX中已經配置過了,而且定時器的初始化相比于外部中斷要復雜的多,所以就不推薦手動書寫代碼配置了,主要掌握CubeMX中的定時器初始化配置方式就行。
在main.c中讓定時器開始運行
盡管我們在CubeMX或類似的配置工具中配置了定時器,并生成了代碼,但生成代碼僅僅是設置了定時器的參數(如時鐘源、預分頻器、自動重載值、中斷使能等)。這些配置通常在 MX_TIMx_Init() 或類似的初始化函數中完成。
然而,這些初始化函數并不會自動啟動定時器或啟用中斷,需要我們調用相應的啟動函數來手動對定時器開啟。這是一種設計哲學,記住就好,也就是多了一行開啟時鐘代碼。
啟動定時器函數需要一個參數,接受一個TIM_HandleTypeDef類型的地址,
這個參數在我們的TIM2_Init函數中有,是在tim.c中定義的,是一個全局變量。
由于我們的main.c文件中包含了tim.h頭文件,并且tim.h中對于這個變量進行了extern聲明,所以在main中可以直接使用htim2了
htim2 用于表示特定定時器實例(在這里是 TIM2) 的一個句柄 (Handle) 結構體變量,包含了管理特定定時器所需的所有配置信息和狀態,我們對定時器進行配置的時候主要就是對這個變量進行配置。
- 它是全局的:通常定義在文件作用域,可以在程序的任何地方(只要包含了定義它的文件或聲明它的頭文件)訪問和修改它。
? - 它是訪問點:所有針對 TIM2 定時器的 HAL 庫函數(如 HAL_TIM_Base_Start_IT(&htim2)、HAL_TIM_PeriodElapsedCallback(&htim) 等)都需要通過這個 htim2 句柄來知道它們正在操作的是哪個定時器。
實現溢出回調函數
由于我們使用了CubeMX配置了定時器,所以關于定時器的TIM6_DAC_IRQHander()不需要我們來聲明并實現,這里的邏輯和中斷處理那塊的邏輯差不多,不過定時器這里有很多回調函數,對于簡單的定時功能,我們需要實現的是溢出回調函數——HAL_TIM_PeriodElapsedCallback()。
通過HAL_TIM_PeriodElapsedCallback()可以實現周期性任務:
- LED 閃爍: 每隔 500ms 翻轉一次 LED 的狀態。
- 數據采樣: 每隔 10ms 讀取一次傳感器數據。
- 任務調度: 以固定的頻率觸發一個任務的執行。
關于HAL_TIM_PeriodElapsedCallback這個弱定義函數可以放main.c函數中進行實現。
補充一下main.c文件中函數聲明和實現的寫法
????????聲明在 main() 函數之前: 在 main.c 中,確實經常看到用戶編寫的函數(在 main() 函數之前進行聲明(原型聲明)。
????????實現通常在 main() 函數之后: 而這些函數的實現(函數體)則經常放在 main() 函數的后面,通常是在 /* USER CODE BEGIN 4 */ 和 /* USER CODE END 4 */ 這樣的用戶代碼區域內。
其他模塊的文件一般我們的選擇是:在.h中進行函數的聲明,在.c文件中進行函數的實現。?
HAL_Delay介紹
HAL_Delay實現原理
HAL_Delay() 的實現方式主要基于 SysTick 定時器 和一個全局的滴答計數變量 (uwTick)。
如果是使用CubeMX進行的配置,那么默認會在main()中調用HAL_Init(),里面會自動幫我們啟動滴答定時器(SysTick)。
SysTick 是 Cortex-M 系列處理器(包括 STM32)內置的一個 24 位倒計時定時器。它直接集成在 CPU 核心內部。
工作方式:
- 我們會配置 SysTick 的重裝載值(Reload Value)。
- 一旦啟用,SysTick 計數器會從這個重裝載值開始遞減,SysTick 計數器一般直接使用系統的主時鐘頻率HCLK。
- 當計數器遞減到 0 時,它會產生一個 SysTick 中斷,然后自動重新裝載并再次開始遞減。所以?每過 1ms,SysTick 計數器遞減到 0,觸發一次?SysTick 中斷。
- 執行 ISR: 處理器響應 SysTick 中斷,執行相應的 ISR。
- 調用 HAL_IncTick(): 在 SysTick 的 ISR 內部,HAL_IncTick() 被執行。
- 更新 uwTick: HAL_IncTick() 將全局變量 uwTick 加 1。
- HAL_GetTick() 函數的實現非常直接,它只是簡單地返回當前 uwTick 變量的值:
HAL_Delay的偽代碼如下:
void HAL_Delay(uint32_t Delay) {uint32_t tickstart = HAL_GetTick(); // 獲取當前系統滴答值uint32_t wait = tickstart + Delay; // 計算目標滴答值// 等待直到達到目標滴答值,并處理uwTick可能溢出的情況while((HAL_GetTick() < wait) && ((HAL_GetTick() - tickstart) < Delay)) {// 空循環,CPU在此處忙等待}
}
HAL_Delay的優點
1. 使用簡單,易于上手
HAL_Delay()
的接口非常直觀:你只需要傳入一個你想要延時的毫秒數,函數就會阻塞相應的時長。對于嵌入式編程的初學者來說,這是最容易理解和使用的延時方式,能夠快速實現一些簡單的功能
2. 無需額外配置
一旦 HAL 庫和 SysTick 定時器被初始化(這通常在項目啟動時自動完成),HAL_Delay()
就可以直接調用,無需進行額外的定時器配置,也不需要編寫中斷服務程序。這大大簡化了開發流程,尤其是在快速原型開發或對延時精度要求不高的場景下。
HAL_Delay的缺點
1. 阻塞式操作 (Blocking):
這是最主要的缺點。當調用 HAL_Delay() 時,CPU 會進入一個忙等待循環,不執行任何其他有用的任務,直到延時結束。
2. 不適用于精確的微秒級延時:
HAL_Delay() 的精度是毫秒級,因為它依賴于 1ms 的 SysTick 中斷。對于需要微秒(us)甚至納秒(ns)級別的精確延時,HAL_Delay() 無法滿足要求。
3.不適用于中斷服務程序 (ISR) 中使用:
嚴重問題: 絕對不能在中斷服務程序 (ISR) 中直接調用 HAL_Delay()。
HAL_Delay() 依賴于 SysTick 中斷來更新 uwTick 變量。如果 SysTick 中斷的優先級低于(數值上大于)當前執行的 ISR,那么 SysTick 中斷將無法搶占當前 ISR 并執行,導致 uwTick 無法更新。這樣一來,HAL_Delay() 就會陷入無限循環,使系統徹底崩潰。
HAL_Delay()中的SysTick定時器默認的優先級是最低的,所以在ISR中調用HAL_Delay不可能調用成功,會持續阻塞在這里,除非我們手動調整SysTick定時器優先級(讓其變得更高),但是這也是非常非常非常不推薦的!!!
4. 與 RTOS 的兼容性問題:
如果您的項目使用了實時操作系統 (RTOS),如 FreeRTOS,直接使用 HAL_Delay() 是不推薦的。
RTOS 有自己的任務調度機制,它提供的延時函數(例如 FreeRTOS 的 osDelay() 或 vTaskDelay())會在任務延時期間將當前任務掛起,并允許調度器切換到其他任務執行,從而充分利用 CPU 資源。
利用滴答定時器(SysTick)實現微秒級延時
上面說到了使用滴答定時器(SysTick)實現的延遲函數HAL_Delay,只能實現ms級別延時,對于更精確的微秒級別是不支持的,下面我們自己來使用SysTick實現微秒級延時。
下面實現的delay_us 函數是一個典型的忙等待(busy-waiting) 實現,也就是基于查詢方式實現的,沒有用到中斷。
void delay_us(uint32_t nus){uint32_t ticks;uint32_t told, tnow, tcnt = 0;uint32_t reload = SysTick->LOAD + 1; //計數個數為重裝載值加1ticks=nus*(SystemCoreClock/1000000); //nus 微秒總共需要的 SysTick 節拍數told= SysTick->VAL; //初始計數器值while(1){tnow=SysTick->VAL;if(tnow!= told){if(tnow<told) tcnt += told- tnow; //SysTick遞減的計數器else tcnt += reload- tnow + told;told= tnow;if(tcnt>=ticks) break; //延時時間已到,退出}}}
這段代碼通過不斷讀取 SysTick 的值,并巧妙地處理了 SysTick 遞減計數和溢出(繞回)的特性,來精確地累加流逝的節拍數。當累加的節拍數達到預設的目標值時,就完成了微秒級的延時。
PWM
下面這個視頻是對pwm比較專業一點的介紹
【STM32】輸出比較模式講解以及STM32CUBEMX+MDK代碼實現_嗶哩嗶哩_bilibili
?下面這篇文章是對pwm比較通俗一點的介紹,更易理解
?PWM原理 PWM頻率與占空比詳解-CSDN博客
?PWM介紹
PWM,全稱是脈沖寬度調制,它通過數字方式來模擬出模擬信號的效果,簡單來說,PWM的原理就是通過快速開關一個數字信號(比如電源),并且控制它在一個周期內“開”的時間長短來達到目的。
脈沖寬度通常就是指在一個完整的 PWM 周期內,信號處于高電平(ON 狀態)的持續時間長度。
下面是一些關鍵點:
-
PWM周期(一個PWM完整波需要的時間)(Period):這是指一個完整的PWM波形所需的時間,也就是說,信號從“開”到“關”再回到“開”的總時間。
-
占空比(Duty Cycle):這是PWM的核心。它表示在一個周期內,信號處于“開”狀態的時間所占的比例。占空比越高,信號“開”的時間就越長。
-
PWM頻率(Frequency):這是指每秒鐘有多少個PWM周期。頻率越高,信號切換得越快,看起來就越平滑,越像一個真正的模擬信號。PWM 的頻率是由 ARR 和 PSC 共同決定的
PWM是如何工作的?
想象一下你有一個燈泡,你想控制它的亮度。
如果你一直給燈泡供電(100%占空比),它就會全亮。
如果你完全不給燈泡供電(0%占空比),它就會熄滅。
如果你以很快的速度,比如每秒鐘開關1000次,每次只讓燈泡亮一半的時間(50%占空比),那么因為你的眼睛無法分辨這么快的開關,燈泡看起來就會是半亮的狀態。
這就是PWM的工作原理。通過調整“開”的時間比例(占空比),我們就可以控制燈泡的亮度、電機的轉速、音頻信號的音量等等。
通用定時器中的重要寄存器
預分頻器寄存器 (TIMx_PSC)
這個寄存器用于設置定時器時鐘的預分頻值,從而設置了定時器的時鐘頻率。
PSC (Prescaler Value):定時器時鐘源會通過這個預分頻器進行分頻,從而得到計數器實際使用的時鐘頻率。計算方式:計數器時鐘頻率 = 定時器時鐘源頻率 / (PSC + 1)。
自動重載寄存器 (TIMx_ARR)
這個寄存器定義了計數器達到多少時會溢出并重新開始計數(或改變計數方向)。
?
- ARR (Auto-Reload Value):當計數器達到 ARR 的值時,會發生更新事件。
? - 計算方式:PWM 的頻率是由 ARR 和 PSC 共同決定的。PWM 頻率 = 計數器時鐘頻率 / (ARR + 1) = 定時器時鐘源頻率 / ((PSC + 1) * (ARR + 1))。
? - 作用:設置 PWM 信號的周期和頻率。ARR 值越大,PWM周期越長,頻率越低。
捕獲/比較寄存器 (TIMx_CCRx)
每個 PWM 通道都有一個對應的 CCRx 寄存器(如 TIMx_CCR1, TIMx_CCR2 等)。
CCRx (Capture/Compare Register Value):這個寄存器存儲的值與計數器 CNT 的值進行比較,從而決定 PWM 信號的占空比。
- 計算方式:占空比 = CCRx / (ARR + 1)。
? - 例如,如果 ARR = 999,CCRx = 500,那么占空比就是 500 / 1000 = 50%。
? - 作用:設置 PWM 信號的脈沖寬度,進而控制占空比。
當然通用定時器中還有一些其他寄存器,很多這些寄存器由CubeMX幫我們自動設置好了,所以不需要很關注。
PWM中的捕獲比較通道
什么是定時器通道?
你可以把一個微控制器里的定時器想象成一個多功能的廚房定時器總機。這個總機本身可以計時(例如,設定每秒滴答一次)。而通道就是這個總機上獨立的定時器插口或功能模塊。
每個通道都可以獨立地配置來完成特定的任務,例如:
-
捕獲輸入(Input Capture): 測量外部信號的脈沖寬度、頻率或邊沿之間的時間間隔。
-
比較輸出(Output Compare): 在定時器計數到預設值時,改變輸出引腳的狀態(高/低電平),用于產生PWM波形、延時輸出脈沖等。
-
PWM 生成(PWM Generation): 最常見的用途之一,生成可調占空比的脈沖寬度調制信號,用于電機調速、LED調光等。
-
單脈沖模式(One-Pulse Mode): 在事件發生后產生一個固定寬度的脈沖。
一個定時器通常會有2個、4個或更多個通道,這意味著這個定時器可以同時處理2個、4個或更多個上述的獨立任務。
定時器通道如何工作?
每個定時器通道內部都有一組專門的寄存器來配置它的行為,其中最重要的就是比較/捕獲寄存器 (Capture/Compare Register, CCR)。
-
作為輸出(Output Compare/PWM): 當定時器的內部計數器(通常是TIMx_CNT寄存器)的值與某個通道的CCR寄存器的值相等時,定時器就會觸發該通道預設的動作(例如,翻轉輸出電平、生成PWM脈沖)。你可以為每個通道設置不同的CCR值,從而產生不同的輸出波形或在不同時間點觸發事件。一旦配置好,通道會根據定時器計數器的值自動在引腳上生成PWM波形,無需CPU干預。
-
作為輸入(Input Capture): 當外部引腳上的信號(例如,上升沿或下降沿)發生變化時,定時器會將當前內部計數器(TIMx_CNT)的值“捕獲”到對應通道的CCR寄存器中。通過讀取不同邊沿捕獲到的CCR值,就可以計算出脈沖寬度、周期等。
為什么需要定時器通道?
-
多任務并行: 如果一個應用需要同時生成兩個不同頻率或占空比的PWM波形,或者同時測量兩個不同信號的頻率,那么使用一個多通道定時器會比使用兩個獨立的單功能定時器更高效、更節省資源。
-
資源優化: 微控制器內部的硬件定時器是有限的寶貴資源。多通道設計允許單個定時器模塊完成多種定時/計數相關的任務,從而節省了片上定時器模塊的數量。
-
靈活性: 每個通道都可以獨立配置其工作模式,極大地增加了定時器模塊的靈活性,使其能夠適應各種復雜的應用需求。
-
硬件實現: 定時器通道通常通過硬件邏輯實現,這意味著一旦配置完成,它們就能自動、精確地工作,無需CPU干預,從而減輕了CPU的負擔,提高了系統的實時性。
PWM實現呼吸燈
CubeMX配置
PB10 引腳配置成 TIM2_CH3(定時器2的通道3) 的操作,正是屬于 STM32 微控制器中的 輸出復用功能模式。
復用功能 (Alternate Function, AF): 一個GPIO引腳除了其通用IO功能外,還可以“復用”為某個片內外設(如定時器、SPI、I2C、USART等)的專用功能引腳。
這樣配置之后的效果:
引腳功能特化: PB10 不再是簡單的 GPIO,它變成了一個由硬件定時器控制的專用引腳。
- 作為 PWM 輸出,?PB10 引腳將輸出一個脈沖寬度調制(PWM)波形。
?- 作為 輸出比較,當 TIM2 的計數器值與 TIM2_CH3 的比較值(CCR3)相等時,PB10 引腳的電平狀態會按照預設的模式發生變化(例如,翻轉、置高、置低)。
?- 作為 輸入捕獲,PB10 引腳將作為一個輸入引腳。當外部信號在這個引腳上發生預設的邊沿(上升沿、下降沿或雙邊沿)時,TIM2 定時器的當前計數值會被立即“捕獲”并存儲到 TIM2_CH3 對應的捕獲/比較寄存器(CCR3)中。
我想要讓LED1變成呼吸燈,在我的電路板上,LED1對應的是PB10,然后右鍵查看對應的多路復用模式中對應的正好是TIM2_CH3,我們選擇定時器2的通道3,然后我們需要去配置?TIM2
?
具體的參數配置,主要是要計算分頻系數(TIMx_PSC)和自動重載寄存器 (TIMx_ARR)
?通過設置這兩個寄存器,就實現了設置PWM的周期和頻率
這里我們的主時鐘頻率為100Mhz,假如我想要讓輸入TIM2的時鐘頻率變為100Khz,讓PWM的周期變成20ms,那么此時TIMx_PSC和TIMx_ARR計算方式如下:
TIMx_PSC =?100Mhz/100Khz - 1 = 999;
由于TIM2的時鐘頻率為100Khz,所以一個節拍對應的為1/100000=0.00001s=0.01ms,
20ms/0.01ms=2000,所以TIMx_ARR=2000-1=1999
關于TIMx_PSC和TIMx_ARR的計算
幾種不同方式實現設定PWM的周期
在上面的例子中,要達到 PWM 周期為 20ms 這個目標,TIMx_PSC (分頻系數) 和 TIMx_ARR (自動重載寄存器) 的設置并非只有一種固定組合。它們是相互關聯的,我們可以通過調整其中一個,來相應地調整另一個,以達到相同的周期。
-
定時器時鐘頻率:這是輸入到特定定時器(例如 TIM2)的時鐘頻率,通常是主時鐘頻率經過 APB 分頻器后得到的。在我們的例子中,假設主時鐘是 100MHz。
? -
TIMx_PSC
(Prescaler Value):分頻系數。它決定了定時器計數器實際的計數頻率。計數器每經過(TIMx_PSC + 1)
個定時器時鐘周期,才遞增/遞減一次。
? -
TIMx_ARR
(Auto-Reload Register):自動重載值。它決定了定時器計數器的上限。計數器從 0 數到TIMx_ARR
(或從TIMx_ARR
數到 0),表示一個完整的計數周期,共(TIMx_ARR + 1)
個節拍。
方法一:我們之前的方法 (分頻后的時鐘頻率為 100 KHz)
-
選擇分頻系數
TIMx_PSC = 999
-
此時
(PSC + 1) = 1000
-
分頻后的定時器計數頻率 = 100?MHz/1000=100?KHz
-
-
計算
TIMx_ARR
:-
?(TIMx_ARR + 1) \times 1000 = 2,000,000
-
?(TIMx_ARR + 1) = 2,000,000 / 1000 = 2000
-
TIMx_ARR=1999
-
這種組合是:
PSC = 999
,ARR = 1999
。?
-
方法二:讓分頻后的時鐘頻率為 1 MHz
-
選擇分頻系數
TIMx_PSC = 99
-
此時
(PSC + 1) = 100
-
分頻后的定時器計數頻率 = 100?MHz/100=1?MHz
-
-
計算
TIMx_ARR
:-
(TIMx_ARR + 1) \times 100 = 2,000,000
-
(TIMx_ARR + 1) = 2,000,000 / 100 = 20000
-
TIMx_ARR=19999
-
這種組合是:
PSC = 99
,ARR = 19999
。?
-
方法三:讓分頻后的時鐘頻率為 50 KHz
-
選擇分頻系數
TIMx_PSC = 1999
-
此時
(PSC + 1) = 2000
-
分頻后的定時器計數頻率 = 100?MHz/2000=50?KHz
-
-
計算
TIMx_ARR
:-
(TIMx_ARR + 1) \times 2000 = 2,000,000
-
?(TIMx_ARR + 1) = 2,000,000 / 2000 = 1000
-
TIMx_ARR=999
-
這種組合是:
PSC = 1999
,ARR = 999
。
-
如何選擇不同PSC與ARR的組合?
雖然有多種組合可以達到相同的 PWM 周期,但在實際應用中,選擇哪種組合通常取決于以下因素:
-
占空比精度 (Duty Cycle Resolution):
-
ARR
值越大,表示在一個 PWM 周期內有更多的計數節拍。 -
這意味著您可以更精細地調整占空比。例如,如果
ARR = 19999
,您可以將占空比設置為(0/20000)
到(19999/20000)
之間的任何值,有 20000 個可能的占空比級別。 -
如果
ARR = 999
,您只有 1000 個占空比級別。 -
因此,通常會選擇較大的
ARR
值以獲得更高的占空比精度,前提是ARR
不超過寄存器的最大值(如 16位定時器ARR
最大為 65535,32位定時器更大)。
-
-
PSC
和ARR
的寄存器大小限制:-
大多數通用定時器的
PSC
和ARR
寄存器是 16 位的,這意味著它們的值不能超過 65535。 -
有些高級定時器或較新的微控制器可能有 32 位的定時器。在選擇
PSC
和ARR
時,需要確保它們不超過對應寄存器的最大值。 -
在我們的例子中,
2,000,000
這個乘積超出了 16 位定時器的單個寄存器范圍,所以必須進行分頻,即PSC
和ARR
都不能為 0(除非定時器頻率非常低)。
-
-
計算方便性/可讀性:
-
有時會選擇整數倍的分頻,使得計數頻率成為一個“整”的 KHz 或 MHz 值,方便理解和計算。
-
綜上所述,我們通常會選擇較大的 ARR
值以獲得更高的占空比精度,但是同時也要注意PSC 和 ARR 設置的值不能超過寄存器大小限制!
代碼編寫
由于CubeMX依舊已經幫我們做了很多工作,所以這里我們需要修改的很少。
呼吸燈的效果是通過 PWM (脈沖寬度調制) 來實現的。我們通過周期性地改變 PWM 波形的占空比(即高電平持續時間與整個周期的比值),來控制 LED 的亮度。
- 當占空比從 0% 逐漸增加到 100% 時,LED 會從滅逐漸變亮;
- 當占空比從 100% 逐漸減小到 0% 時,LED 會從亮逐漸變滅。
這個過程循環往復,就形成了“呼吸”的效果。
下面是啟動 PWM 輸出的關鍵函數。它告訴定時器 TIM2 的通道3 開始生成 PWM 波形。請務必在進入 while(1) 循環之前調用它。
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_3);
?這個函數會啟動定時器(如果它尚未運行)并使其 PWM 輸出開始在指定的通道引腳上生成波形。
關于調節占空比其實就是在調節比較捕獲寄存器的值:
- 計算方式:占空比 = CCRx / (ARR + 1)。
- 例如,如果 ARR = 999,CCRx = 500,那么占空比就是 500 / 1000 = 50%。
設置比較捕獲寄存器TIMx_CCRx的值:
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_3, duty);
這是一個 HAL 庫的宏(本質上是直接操作寄存器),用于設置指定定時器通道的比較值(CCR 寄存器)。
duty_cycle 的值直接決定了 PWM 的占空比。當 duty_cycle 接近 0 時,LED 滅;接近 PWM_MAX_DUTY 時,LED 最亮。
/* USER CODE BEGIN 2 */HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_3); /* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */uint32_t duty;while (1){/* USER CODE END WHILE */for(duty=0; duty<2000; duty += 20){__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_3, duty);HAL_Delay(20);}for(duty=2000; duty>0; duty-=20){__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_3, duty);HAL_Delay(20);}/* USER CODE BEGIN 3 */}
?
推薦好文:
STM32定時器詳解:原理、配置與應用實戰-CSDN博客
STM32 定時器TIM-CSDN博客