工業測試與控制系統中,經常需要對未知信號的頻率進行測試。對於10MHz以下的信號,用單片機(MCU)定時器完成這項任務顯然是最常見和最佳的選擇。目前性價比最高的單片機STM32擁有功能強大且數量眾多的定時器,能夠輕松的勝任各種頻率信號的測試工作。但也正是由於STM32的定時器功能過於強大和完善,常見的技術書籍往往將篇幅專注於STM32定時器的定時、PWM和觸發DMA傳輸等常見功能,而對於測頻率所需的計數和捕捉等功能往往一筆帶過,更不會專門針對具體應用給出定時器的配置方法。本文分別介紹用STM32通用定時器,實現“測頻法”(又稱“計頻法”)和“測周法”(又稱“計時法”)這兩種最常見方法的代碼和步驟。[原創cnblogs.com/helesheng]
在開始正文前還要總結一下“測頻法”和“測周法”兩種測頻方法的設計思路。
一、測頻法(又稱“計頻法”)原理
測頻法的思路,是對
較長的一段“標准時間”內被測信號的脈沖數量進行計數。如下圖所示,給出“標准時間”的標准信號周期為Tc1,頻率為fc1;外部輸入的被測信號周期為Tx1,頻率為fx1。
圖1 測頻法測量原理
如果計數器測得標准時間內Tc1時間內,被測信號共出現了N1個脈沖,則被測信號的頻率為:
(1)
采用測頻法,造成測量誤差的唯一原因是:計數器只能進行整數計數,而在Tc1時間窗口內,卻不一定剛好有整數個被測信號周期。因此測頻法造成的最大測量誤差為±1個被測信號,參考上面的(1)式,若計數結果為N1,則頻率的最大可能值為fx1+=(N1+1)fc1;最小可能值為fx1-=(N1-1)fc1。相對頻率誤差為:
ef=±1/N1×100% (2)
由(2)可知,N1越大,相對頻率誤差越小。
而這和我們的直覺相符:當標准信號頻率遠低於被測信號頻率時,Tc1窗口內的被測信號脈沖很多(N1很大),測頻法得到的結果就越准確。也就是說,實際應用中,測頻法(計頻法)的“用武之地”是在被測信號頻率較高時。而當被測信號頻率較低時,為獲得足夠高的測量精度增大N1,就有可能導致測量時間加長,測試的實時性降低。
當
被測信號頻率較低時,合理的測試方法是測周法(又稱“計時法”)。
二、測周法(又稱“計時法”)原理
測周法的測量對象是頻率較低的被測信號,因此顛倒了圖1中標准信號和被測信號頻率之間的對比關系。圖2所示的測周法,使用比被測信號
高得多的標准信號,在被測信號周期Tx2內對標准信號進行計數。
圖2 測周法測量原理
如果Tx2內得到的計數值為N2,則被測信號周期為:
采用測周法,造成測量誤差的原因也是:計數器只能進行整數計數,而在Tx2時間窗口內,卻不一定剛好有整數個標准信號周期。因此測周法造成的最大測量誤差為±1個標准信號周期,參考上面的(3)式,若計數結果為N2,則測量周期的最大可能值為Tx2+=(N2+1)Tc2;最小可能值為Tx2+=(N2+1)Tc2。相對周期誤差為:
eT=±1/N2×100% (4)
由(4)可知,N2越大,相對周期誤差越小。
當標准信號頻率遠高於被測信號頻率時,Tx2窗口內的被測信號脈沖很多(N2很大),測周法得到的結果就越准確。也就是說,實際應用中,
測周法法(計數法)的“用武之地”是在被測信號頻率較高時。
三、兩種測量方法的選擇——以STM32為例
上面的理論分析得到的結論是:測頻法用在被測信號的頻率“較低”時,測周法用在被測信號的頻率“較高”時,那到底多少頻率可以稱為“較低”,多少頻率又算“較高”呢?——答案是由標准信號頻率fc1和fc2,以及被測頻率fx決定。以最常用的STM32F1系列@72MHz為例:假設被測頻率為1KHz左右,需要100ms刷新一次測量結果(即測量時間分辨率為100ms)。
如果使用測頻法,為提高精度,需要使標准信號fc1相對於被測信號(1KHz左右)盡可能的“低”,用定時器產生100ms的測量周期,再在該測量周期內對被測信號進行計數。對照圖1,在100ms測量周期Tx1內約有N1=100ms/(1/1KHz)=100個脈沖,代入公式(2),得到相對誤差為:ef=1×10
-2。
如果使用測周法,為提高精度,需要使標准信號fc2相對於被測信號(1KHz左右)盡可能的“高”,所以直接使用定時器最高工作時鍾72MHz作為fc2。對照圖2,在被測信號周期Tx2內約有N2=(1/1KHz)/(1/72MHz)=72_000個脈沖,代入公式(4),得到相對誤差為:eT=1.39×10
-5。
顯然,對於這個具體問題,測周法的測量精度遠遠高於測頻法的測量精度。
四、用STM32定時器實現測頻法(計頻法)的思路和源碼
根據圖1所示的測頻法原理,需要在標准時段(Tc1)內對外部輸入的被測信號脈沖進行計數。
因此,測頻法需要兩個STM32定時器,第一個工作在定時器模式——對STM32內部已知的系統時鍾進行計數,產生標准時間長度Tc1;第二個工作在計數器模式——對外部輸入的被測信號脈沖數N1進行計數。第一個定時器可以是系統定時器SysTick、通用定時器(TIM2、TIM3、TIM4、TIM5)、高級定時器(TIM1和TIM8)或基本定時器(TIM6和TIM7)中的任何一個。而第二個定時器則只能使用具有外部時鍾輸入管腳的通用定時器或高級定時器。
STM32的通用定時器和高級定時器都支持兩種外部時鍾源模式:外部時鍾源模式1,通過定時器
輸入通道1和2(TMRxCH1、TMRxCH2)獲得外部時鍾;外部時鍾源模式2,通過專用的
TMRxETR管腳獲得外部時鍾。圖3所示的是兩種模式下,定時器得到時鍾的通路,紅色箭頭為外部時鍾源模式1外部時鍾脈沖輸入通路,藍色為外部時鍾源模式2外部時鍾脈沖輸入通路。
圖3 STM32通用和高級定時器的兩種外部時鍾源模式
下面分別給出兩種模式實現測頻法(計頻法)的程序設計思路和部分源代碼。
1、外部時鍾源模式1
通過外部時鍾源模式1(TMRxCH1、TMRxCH2管腳輸入外部測頻脈沖)的代碼大致如下:
1)使能相關定時器和GPIO時鍾,配置GPIO
1 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); 2 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); 3 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //使能porta 4 //PA1-> TIM2_CH2外部時鍾輸入 5 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;//PA1 6 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; 7 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz; //10M時鍾速度 8 GPIO_Init(GPIOA, &GPIO_InitStructure);
2)配置用於產生標准時長的TIM3的時基單元和中斷
1 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; 2 NVIC_InitTypeDef NVIC_InitStructure; 3 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //時鍾使能 4 //!!!!!定時器3,用於產生標准時長的對外部脈沖計數的窗口,從而計算外部脈沖的頻率!!!!!// 5 TIM_TimeBaseStructure.TIM_Period = arr-1; //設置在下一個更新事件裝入活動的自動重裝載寄存器周期的值 計數到5000 6 TIM_TimeBaseStructure.TIM_Prescaler =(psc-1); //設置用來作為TIMx時鍾頻率除數的預分頻值 10Khz的計數頻率 7 TIM_TimeBaseStructure.TIM_ClockDivision = 0; //設置時鍾分割:TDTS = Tck_tim 8 TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上計數模式 9 TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); //根據TIM_TimeBaseInitStruct中指定的參數初始化TIMx的時間基數單位 10 TIM_ITConfig( //使能或者失能指定的TIM中斷 11 TIM3, //TIM3 12 TIM_IT_Update, //數值溢出更新中斷 13 ENABLE //使能 14 ); 15 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); 16 NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn; //TIM2更新中斷 17 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; //先占優先級0級 18 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2; //從優先級3級 19 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能 20 NVIC_Init(&NVIC_InitStructure); //根據NVIC_InitStruct中指定的參數初始化外設NVIC寄存器 21 TIM_SetCounter(TIM3,0); 22 TIM_Cmd(TIM3, ENABLE); //使能TIMx外設
3)配置用於對外部被測脈沖進行計數的TIM2的時基單元和中斷
1 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; 2 NVIC_InitTypeDef NVIC_InitStructure; 3 4 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //時鍾使能 5 6 TIM_TimeBaseStructure.TIM_Period = 65535;//arr-1; //設置在下一個更新事件裝入活動的自動重裝載寄存器周期的值 計數到5000 7 TIM_TimeBaseStructure.TIM_Prescaler =0;//(psc-1); //設置用來作為TIMx時鍾頻率除數的預分頻值 10Khz的計數頻率 8 TIM_TimeBaseStructure.TIM_ClockDivision = 0; //設置時鍾分割:TDTS = Tck_tim 9 TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上計數模式 10 TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); //根據TIM_TimeBaseInitStruct中指定的參數初始化TIMx的時間基數單位 11 12 TIM_ITConfig( //使能或者失能指定的TIM中斷 13 TIM2, //TIM2 14 TIM_IT_Update, //TIM 中斷源 15 ENABLE //使能 16 ); 17 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); 18 NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; //TIM2更新中斷 19 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; //先占優先級0級 20 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //從優先級3級 21 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能 22 NVIC_Init(&NVIC_InitStructure); //根據NVIC_InitStruct中指定的參數初始化外設NVIC寄存器 23 24 TIM_TIxExternalClockConfig(TIM2,TIM_TIxExternalCLK1Source_TI2,TIM_ICPolarity_Falling,0); 25 //選擇外部輸入作為TIM2的時鍾 26 TIM_SetCounter(TIM2,0);//設置TIM2計數初值為0 27 TIM_Cmd(TIM2, ENABLE); //使能TIMx外設
這里使用了全局變量top_watch,對外部脈沖計數器溢出次數進行清零,防止在標准時間長Tc1內發生計數溢出。
4)編寫標准時長定時器TIM3的中斷服務程序
1 unsigned char i=0; 2 unsigned int frq[10];//連續存取10次的測頻法得到的頻率 3 unsigned int pul_num;//標准時間內的脈沖數量 4 unsigned int cnt;//讀取當前計數值 5 unsigned int last_cnt=0;//上一次的計數值 6 void TIM3_IRQHandler(void) //TIM3中斷,達到定時時間 7 { 8 if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) //檢查指定的TIM中斷發生與否:TIM 中斷源 9 { 10 TIM_ClearITPendingBit(TIM3, TIM_IT_Update); //清除TIMx的中斷待處理位:TIM 中斷源 11 cnt = TIM_GetCounter(TIM2);//讀取定時器2對外部脈沖的計數結果 12 if(cnt >= last_cnt)//如果發生過溢出,當前計數結果就有可能比上次的計數結果還小 13 pul_num = (unsigned int)(top_watch<<16)+ (unsigned int)(cnt - last_cnt); 14 else 15 pul_num = (unsigned int)((top_watch-1)<<16) + (unsigned int)(65536 + cnt - last_cnt); 16 last_cnt = cnt;//將當前計數結果復制到上次的復制結果寄存,方便下次計算 17 frq[i] = pul_num * 100;//由於定時器3是1/100秒溢出一次,所以頻率時脈沖個數的100倍 18 i++; 19 if(i == 10) 20 i=0; 21 top_watch=0; 22 } 23 }
這里使用數組 frq[10]連續存取10次的測頻法得到的頻率。每次執行上面的中斷服務程序,意味着時間剛好過去了一個Tc1(在這里是10ms,即1/100秒),可以通過函數TIM_GetCounter(TIM2);讀取定時器2對外部被測脈沖的計數結果。但該方法的問題是:用於對外部被測脈沖進行計數的TIM2有可能在臨近的兩次Tc1中斷之間發生一次乃至多次溢出/更新,從而造成被測脈沖數計算錯誤。解決的辦法是:在考慮外部被測脈沖計數定時器TIM2計數值的同時,也考慮TIM2溢出的次數top_watch(如上所述,top_watch會在每次TIM2溢出時自動加1)。另外,當前計數值(cnt)和上一次TIM3定時中斷發生時的計數值(last_cnt)來計算標准時間Tc1內的外部被測脈沖。
注意:a、當Tc1內發生過TIM2計數溢出時,當前計數值cnt可能小於上一次的計數值last_cnt。
b、這里沒有在中斷服務程序中對計數器TIM2清零,而是任由TIM2自由計數,反而采用當前計數值和上次計數值求差的辦法。這種方法看似繁瑣,但保證了TIM2能夠不間斷的連續計數,而不會使從發生TIM3定時中斷,到進入TIM3中服務程序對TIM2清零,這兩個事件間的脈沖數被漏記,從而提高了計數精度。
2、外部時鍾源模式2
STM32的每個通用定時器和定時器,除了支持從輸入通道1和2(TMRxCH1、TMRxCH2)輸入外部時鍾外,還各自擁有一個單獨的外部時鍾輸入管腳TIMx_ETR。例如,TIM1_ETR在PA12,TIM2_ETR在PA0,TIM3_ETR在PD2,TIM4_ETR在PE0。我最初也對這種設計感到奇怪——既然已經支持從TMRxCH1、TMRxCH2輸入外部時鍾,為什么還要支持使用專用管腳輸入?隨着使用的深入才逐漸明白STM32設計者的初衷,是想支持用外部時鍾驅動的捕獲輸入和比較輸出。因為如果TMRxCH1、TMRxCH2管腳被占用為外部時鍾輸入,則定時器的捕獲輸入和比較輸出管腳就會變少。
外部時鍾源模式1和外部時鍾源模式2之間的關系既然是這樣的,那么用兩種外部時鍾源模式來實現測頻法(計頻法)測量信號頻率的思路和代碼就幾乎相同了。不同點只是在配置定時器時鍾源的一句:
TIM_TIxExternalClockConfig(TIM2,TIM_TIxExternalCLK1Source_TI2,TIM_ICPolarity_Rising,0);
要更改為:
TIM_SelectInputTrigger(TIM2,TIM_TS_ETRF);
如果外部時鍾模式仍然使用定時器2,由於PA0管腳既是TIM2的通道1管腳,又是TIM2的ETR管腳,那么連管腳的配置都可以省去。也就是說,代碼其他部分和前面采用外部時鍾源模式1進行測頻法(計頻法)完全相同。
五、用STM32定時器實現測周法(計時法)的思路和源碼
根據圖2所示的測周法原理,需要在被測信號周期(Tx2)內對STM32內部的最高頻率72MHz(為獲得最高測量精度和分辨率)的時鍾進行計數。最簡單的方式將被測信號作為外部中斷源,並在外部中斷服務程序中讀取定時器中的計數值,但這樣做會使中斷入口時間也計算在Tx2以內。因此實現測周法的最佳方案,是使用STM32通用定時器或高級定時器的捕獲功能(Input Capture)。

圖4 STM32輸入捕獲電路框圖
STM32的輸入捕獲電路框圖如圖4所示,它能在定時器的某個通道TIMx_CHy發生指定脈沖邊沿的時刻及時地將此時的計數器計數值鎖存在“捕獲/比較寄存器”中,從而有效地避免了上面提到的方法中進入中斷時延造成的計時誤差。
一般而言,讀取捕獲到的邊沿的辦法有兩種:其一,每次捕獲都引發中斷,並在中斷服務程序中逐一讀取“捕獲/比較寄存器”中鎖存的時刻。其二,配置DMA,等DMA控制器將連續捕獲的幾個“捕獲/比較寄存器”時間,都存儲到DMA目標內存后再一次性讀取。
上述兩種測周法的實現方法各有優缺點:
第一個種方法的缺點在於需要在每個被測脈沖后,都頻繁的響應中斷,提高了CPU的占用率,而且有可能由於中斷嵌套導致有些脈沖被漏測。而這種方法的優點在於每捕獲一個被測脈沖,都可以對定時器溢出的次數進行判斷,當兩個被測脈沖間可能發生2次或2次以上的定時器溢出中斷時,只有這種方法在定時器中斷服務程序中用軟件處理定時器的溢出次數,才有可能得到正確的測周結果。因此,本方法(即每次捕獲都引發中斷的方法),適合被測脈沖的周期可能大於定時器溢出周期的情況——此時,兩次捕獲之間可能發生多余一次的定時器溢出)。
第二種方法,由於在每次捕獲后只會引發DMA傳輸,並只在DMA目標緩沖區放滿后才需要引發中斷,以一次性處理捕獲的計數值。所以這種方法的優點是中斷次數不如第一種方法頻繁,CPU占用率低,不可能出現漏存儲捕獲值的情況。而缺點是每次DMA傳輸,只能傳輸16位的捕獲寄存器TIMx_CCRy,因此頂多只能處理發生過一次定時器溢出的情況——認為當前捕獲結果小於上一次捕獲結果時,就是在這中簽發生了一次定時器溢出。本方法(即捕獲都引發DMA的方法),適合被測脈沖的周期必須小於定時器溢出周期的情況——此時,兩次捕獲之間最多發生一次的定時器溢出,且溢出時第二次捕獲值一定小於第一次捕獲值。
1、第一種方法:每次捕獲引發定時器中斷讀取
通過定時器輸入捕獲實現測周法的代碼大致如下:
1)使能定時器和GPIO時鍾,配置GPIO
1 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //使能通用定時器TIM2時鍾 2 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //使能GPIOA時鍾
2)接下來還要將TIM2CH2的輸入管腳PA0配置成輸入模式,這里不再贅述。
1 TIM_TimeBaseStructure.TIM_Period = 65535; //設定計數器溢出值(自動重裝值) 2 TIM_TimeBaseStructure.TIM_Prescaler = 0; //預分頻器 3 TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //設置時鍾分割因子 4 TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //向上計數模式 5 TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); 6 //根據TIM_TimeBaseInitStruct中指定的參數初始化TIMx的時間基單元
這里將自動重裝值設置為16位計數器的最大值65535(0xFFFF),以增加計數器計數范圍,降低自動重裝次數。
3)配置輸入捕獲器
標准外設庫使用時輸入捕獲器初始化結構體來配置其參數,該結構體的聲明代碼為:
1 TIM_ICInitTypeDef TIM2_ICInitStructure; //定義輸入捕獲器初始化結構體 2 對初始化結構體TIM2_ICInitStructure中的參數賦值,例如如下代碼: 3 TIM2_ICInitStructure.TIM_Channel = TIM_Channel_2; //選擇輸入捕獲通道為TIM2_CH2 4 TIM2_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; //上升沿捕獲 5 TIM2_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; //映射到TI2上 6 TIM2_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; //配置輸入捕獲脈沖分頻,不分頻 7 TIM2_ICInitStructure.TIM_ICFilter = 0x00;//配置輸入濾波器 不濾波 8 TIM_ICInit(TIM2, &TIM2_ICInitStructure);
4)使能定時器中斷
1 TIM_ITConfig(TIM2,TIM_IT_Update|TIM_IT_CC2,ENABLE);//允許更新中斷 ,允許CC2IE捕獲中斷
其中,第二個參數代表觸發中斷事件,其可選參數已經在前面定時模式介紹過了,這里使用了更新中斷或輸入捕捉通道2中斷都會觸發中斷的方式。這意味着,在中斷服務程序中應檢測到底是定時器自動重裝更新引起的中斷還是捕獲引起的中斷,並采取相應的應對措施。
5)配置向量中斷控制器NVIC
1 NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; //TIM2中斷 2 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; //先占優先級2級 3 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //從優先級0級 4 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能 5 NVIC_Init(&NVIC_InitStructure); //根據NVIC_InitStruct中指定的參數初始化外設NVIC寄存器
6)使能定時器
1 TIM_Cmd(TIM2, ENABLE); //使能TIM2
7) 編寫定時器中斷服務程序(含寄出和捕獲的操作)
1 unsigned short i=0; 2 unsigned int pul_width[10];//脈沖周期 3 unsigned int pul_frq[10];//對應的脈沖頻率 4 unsigned short ov_num;//定時器溢出的次數,用於記錄之前溢出的次數 5 unsigned short last_cap_val=0,cur_cap_val;//當前捕獲到的數值和上一次捕獲到的數值 6 //定時器2中斷服務程序(可以能由捕獲或定時器溢出) 7 void TIM2_IRQHandler(void) 8 { 9 if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) 10 //為了增加測量頻率的動態范圍,定時器溢出次數也要計算,相當於增加了定時器的位數 11 ov_num++ ;//溢出次數加一 12 if (TIM_GetITStatus(TIM2, TIM_IT_CC2) != RESET)//捕獲2發生捕獲事件 13 { 14 cur_cap_val = TIM_GetCapture1(TIM2);//讀取當前捕獲發生時的定時器數值 15 if(cur_cap_val >= last_cap_val)//如果發生過溢出,當前捕獲結果就有可能比上次的捕獲結果還小 16 pul_width[i] = (unsigned int)(ov_num<<16)+ (unsigned int)(cur_cap_val - last_cap_val); 17 else 18 pul_width[i] = (unsigned int)((ov_num-1)<<16) + (unsigned int)(65536 + cur_cap_val - last_cap_val); 19 pul_frq[i] = 72000000 /(float)pul_width[i] + 0.5; 20 //折算為頻率,加0.5是為了防止強制類型轉換帶來的舍棄誤差 21 last_cap_val = cur_cap_val;/將當前捕獲結果復制到上次的捕獲結果寄存,方便下次計算 22 ov_num = 0; 23 i++; 24 if(i == 10) 25 i = 0; 26 } 27 TIM_ClearITPendingBit(TIM2, TIM_IT_CC2|TIM_IT_Update); //清除中斷標志位 28 }
上述中斷服務程序用於測量從TIM2_CH2輸入的最近10個脈沖信號的周期和頻率。方法是用定時器的輸入捕獲模式每個上升沿到來的時刻,從而得到兩個上升沿之間的時間間隔即脈沖周期(pul_width),並進而通過計算得到脈沖對應的頻率(pul_frq)。該方法最大的問題是:用於捕獲的TIM2有可能在臨近的兩個輸入脈沖的上升沿之間發生一次乃至多次溢出/更新,從而造成時間間隔計算錯誤。解決的辦法是:同時允許更新中斷和捕獲中斷,並在中斷服務程序中對中斷源進行判斷。如果是溢出/更新引發的中斷,則對全局變量ov_num加一。直至下一個輸入上升沿引發捕獲中斷,則可以通過ov_num的值,以及本次捕獲發生時的定時器數值cur_cap_val和上一捕獲發生時的定時器數值last_cap_val來計算兩次捕獲發生之間的時間間隔。注意,這里沒有在捕獲后對計數器清零,而是任由計數器自由計數,反而采用當前計數值和上次計數值求差的辦法。這種方法看似繁瑣,但保證了TIM2能夠不間斷的連續計時,而不會使從被捕獲的上升沿到進入中斷對TIM2清零,這兩個事件間的時間被漏記,提高了捕獲時間的精度。
2、第二種方法:捕獲引發DMA,再DMA存滿后統一讀取
本方法前3步(包括GPIO、定時器時基單元和捕獲功能的初始化)與第一種方法完全相同這里不再贅述,從第4步開始配置DMA控制器介紹。
4)配置DMA控制器
1 //定義DMA的傳輸源頭為TIM2的捕獲通道2,0x40000000為定時器2的基地址,0x38為捕獲通道2(CCR2)的偏移地址 2 #define TIM2_CCR2_Address 0x40000038 3 //捕獲結果存放的緩沖,由DMA向里頭存數據 4 unsigned short cap_time[10] ={0}; 5 6 DMA_InitTypeDef DMA_InitStructure;//定義DMA結構體 7 NVIC_InitTypeDef NVIC_InitStructure; 8 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);//使能DMA1時鍾 9 /* DMA1通道7配置 */ 10 DMA_DeInit(DMA1_Channel7); //根據默認設置初始化DMA1 11 DMA_InitStructure.DMA_PeripheralBaseAddr = TIM2_CCR2_Address;//外設地址 12 DMA_InitStructure.DMA_MemoryBaseAddr = (u32)&cap_time;//內存地址 13 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;//從外設到內存的傳輸 14 DMA_InitStructure.DMA_BufferSize =10;//數據長度 15 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外設地址寄存器不遞增 16 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//內存地址遞增 17 DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;//外設傳輸以半字為單位 18 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;//內存以半字為單位 19 DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;//循環模式 20 DMA_InitStructure.DMA_Priority = DMA_Priority_High;//4優先級之一的(高優先級) 21 DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;//非內存到內存 22 DMA_Init(DMA1_Channel7, &DMA_InitStructure);//根據以上參數初始化DMA_InitStructure
其中,TIM2_CCR2_Address指代的0x40000038是定時器2的捕獲通道2的地址,用於DMA傳輸。另外,請注意,TIM2_CH2所在的DMA通道是DMA控制器1的通道7,這是STM32官方要求固定使用的,不能隨意更改。
另外這里將DMA緩沖區的長度設為10,意味着DMA控制器會在將10次捕獲的結果存儲到緩沖區后,再觸發一次中斷。在中斷服務程序中可以一次處理10個捕獲值。
5)使能DMA1CH7、TIM2和DMA1中斷
1 DMA_ITConfig(DMA1_Channel7, DMA_IT_TC, ENABLE);//配置DMA1通道1傳輸完成中斷 2 DMA_Cmd(DMA1_Channel7, ENABLE);//定時器2通道2的DMA在控制器1的通道7 3 TIM_Cmd(TIM2, ENABLE); //使能TIM2
6)配置向量中斷控制器並使能DMA中斷
1 NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel7_IRQn; //TIM2_CH2的DMA通道是DM1_CH7 2 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; //先占優先級2級 3 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //從優先級0級 4 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能 5 NVIC_Init(&NVIC_InitStructure); //根據NVIC_InitStruct中指定的參數初始化外設NVIC寄存器 6 DMA_ITConfig(DMA1_Channel7, DMA_IT_TC, ENABLE);//配置DMA1通道1傳輸完成中斷
7)配置由TIM2的捕捉事件觸發DMA傳輸
1 TIM_DMACmd(TIM2,TIM_DMA_CC2,ENABLE);
這個操作最容易忘記,其作用是讓定時器捕捉觸發DMA數據傳輸。
8)編寫DMA中斷服務程序
1 unsigned short i=0; 2 int pul_width[9]={0};//脈沖周期 3 int pul_frq[9]={0};//對應的脈沖頻率 4 //DMA中斷 5 void DMA1_Channel7_IRQHandler(void) 6 { 7 if(DMA_GetITStatus(DMA1_IT_TC7))//判斷通道1是否傳輸完成 8 { 9 for(i=0;i<9;i++) 10 { 11 pul_width[i] = cap_time[i+1]-cap_time[i]; 12 if(pul_width[i]<0) 13 //如果下一個捕獲值小於上一個捕獲值,則說明發生了溢出 14 pul_width[i] = pul_width[i] + 65536; 15 pul_frq[i] = 72000000 / (float)pul_width[i] + 0.5; 16 //折算為頻率,加0.5是為了防止四舍五入帶來的系統誤差; 17 } 18 DMA_ClearITPendingBit(DMA1_IT_TC7); //清除通道1傳輸完成標志位 19 }
由於DMA控制器只能傳輸16位的捕獲結果,因此,如果兩次捕獲之間發生了溢出,DMA中斷服務程序中是不可能直接知道的。所以前面關於這種方法的缺點中提到過:這種方法只能測量被測脈沖的周期小於定時器的溢出周期都的脈沖頻率。當后一次捕獲結果減去前一次捕獲結果為一個復負數時,意味着兩次捕獲之間發生了一次定時器溢出,因此需要在計時結果中加上0xFFFF,再來計算對應頻率。
