用STM32定時器測量信號頻率——測頻法和測周法[原創cnblogs.com/helesheng]

工業測試與控制系統中,經常需要對未知信號的頻率進行測試。對於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,則被測信號周期為:
                                                                       (3)
採用測周法,造成測量誤差的原因也是:計數器只能進行整數計數,而在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);

使能相關定時器和GPIO時鐘,配置GPIO

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外設

配置用於產生標準時長的TIM3

3)配置用於對外部被測脈衝進行計數的TIM2的時基單元和中斷

 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外設

配置用於對外部被測脈衝計數的TIM2

這裡使用了全局變量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 }

TIM3的中斷服務程序

這裡使用數組 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)。STM32的輸入捕獲電路框圖如圖4所示,它能在定時器的某個通道TIMx_CHy發生指定脈衝邊沿的時刻及時地將此時的計數器計數值鎖存在「捕獲/比較寄存器」中,從而有效地避免了上面提到的方法中進入中斷時延造成的計時誤差。

通過定時器輸入捕獲實現測周法的代碼大致如下: 
1)使能定時器和GPIO時鐘,配置GPIO

1 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM5, ENABLE); //使能通用定時器TIM5時鐘
2 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);  //使能GPIOA時鐘

配置GPIO

2)接下來還要將TIM5CH1的輸入管腳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(TIM5, &TIM_TimeBaseStructure);
6 //根據TIM_TimeBaseInitStruct中指定的參數初始化TIMx的時間基單元

時基單元配置

這裡將自動重裝值設置為16位計數器的最大值65535(0xFFFF),以增加計數器計數範圍,降低自動重裝次數。

 3)配置輸入捕獲器 

標準外設庫使用時輸入捕獲器初始化結構體來配置其參數,該結構體的聲明代碼為: 

1 TIM_ICInitTypeDef  TIM5_ICInitStructure;  //定義輸入捕獲器初始化結構體
2 對初始化結構體TIM5_ICInitStructure中的參數賦值,例如如下代碼:
3 TIM5_ICInitStructure.TIM_Channel = TIM_Channel_1; //選擇輸入捕獲通道為TIM5_CH1
4 TIM5_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;     //上升沿捕獲
5 TIM5_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; //映射到TI1上
6 TIM5_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; //配置輸入捕獲脈衝分頻,不分頻
7 TIM5_ICInitStructure.TIM_ICFilter = 0x00;//配置輸入濾波器 不濾波
8 TIM_ICInit(TIM5, &TIM5_ICInitStructure);

配置輸入捕獲器

4)使能定時器中斷 

1 TIM_ITConfig(TIM5,TIM_IT_Update|TIM_IT_CC1,ENABLE);//允許更新中斷 ,允許CC1IE捕獲中斷

使能定時器中斷

其中,第二個參數代表觸發中斷事件,其可選參數已經在前面定時模式介紹過了,這裡使用了更新中斷或輸入捕捉通道1中斷都會觸發中斷的方式。這意味着,在中斷服務程序中應檢測到底是定時器自動重裝更新引起的中斷還是捕獲引起的中斷,並採取相應的應對措施。

 5)配置向量中斷控制器NVIC 

1 NVIC_InitStructure.NVIC_IRQChannel = TIM5_IRQn;  //TIM5中斷
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寄存器

配置向量中斷控制器NVIC

6)使能定時器

1 TIM_Cmd(TIM5, ENABLE);  //使能TIM5

使能定時器

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 //定時器5中斷服務程序(可以能由捕獲或定時器溢出)    
 7 void TIM5_IRQHandler(void)
 8 {
 9   if (TIM_GetITStatus(TIM5, TIM_IT_Update) != RESET)
10 //為了增加測量頻率的動態範圍,定時器溢出次數也要計算,相當於增加了定時器的位數
11      ov_num++ ;//溢出次數加一
12   if (TIM_GetITStatus(TIM5, TIM_IT_CC1) != RESET)//捕獲1發生捕獲事件
13   {
14      cur_cap_val = TIM_GetCapture1(TIM5);//讀取當前捕獲發生時的定時器數值
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(TIM5, TIM_IT_CC1|TIM_IT_Update); //清除中斷標誌位
28 }

編寫定時器中斷服務程序(含寄出和捕獲的操作)

上述中斷服務程序用於測量從TIM5_CH1輸入的最近10個脈衝信號的周期和頻率。方法是用定時器的輸入捕獲模式每個上升沿到來的時刻,從而得到兩個上升沿之間的時間間隔即脈衝周期(pul_width),並進而通過計算得到脈衝對應的頻率(pul_frq)。該方法最大的問題是:用於捕獲的TIM5有可能在臨近的兩個輸入脈衝的上升沿之間發生一次乃至多次溢出/更新,從而造成時間間隔計算錯誤。解決的辦法是:同時允許更新中斷和捕獲中斷,並在中斷服務程序中對中斷源進行判斷。如果是溢出/更新引發的中斷,則對全局變量ov_num加一。直至下一個輸入上升沿引發捕獲中斷,則可以通過ov_num的值,以及本次捕獲發生時的定時器數值cur_cap_val和上一捕獲發生時的定時器數值last_cap_val來計算兩次捕獲發生之間的時間間隔。注意,這裡沒有在捕獲後對計數器清零,而是任由計數器自由計數,反而採用當前計數值和上次計數值求差的辦法。這種方法看似繁瑣,但保證了TIM5能夠不間斷的連續計時,而不會使從被捕獲的上升沿到進入中斷對TIM5清零,這兩個事件間的時間被漏記,提高了捕獲時間的精度。