天天看點

使用 STM32 測量頻率和占空比的幾種方法

以前在大學時寫的教程文章,主要是把自己當時參賽的方法拿出來做了個總結。

想當年天天水論壇好為人師,現在已經全面轉向計算機視覺方向了,頗為感慨。不過,自己的理性選擇,個中得失早就意料之中。塞翁失馬,焉知非福?

原文連結:http://www.openedv.com/forum.php?mod=viewthread&tid=82594&extra=

【教程】使用STM32測量頻率和占空比的幾種方法(申請置酷!)

這幾天在論壇上面解答了好幾個詢問 STM32 測量頻率的貼子,覺得這種需求還是存在的(示波器、電機控制等等)。而簡單搜尋了一下論壇,這方面的貼子有但是不全。正好今年參加比賽做過這方面的題目(最後是一等獎嘿嘿),是以把我們當時嘗試過的各種方案都列出來,友善以後大家使用,也是作為一個長期在論壇的潛水黨對論壇的回報。

PS: 由于我們當時的題目除了測量頻率之外,更麻煩的是測量占空比。而這兩個的測量方法聯系比較緊密,是以也一并把測量占空比的方法寫出來。因為時間有限,是以并不能把所有思路都一一測試,隻是寫在下面作為參考,敬請諒解。

使用平台:官方 STM32F429DISCOVERY 開發闆, 180MHz 的主頻,定時器頻率 90MHz 。

相關題目: ( 1 )測量脈沖信号頻率 f_O ,頻率範圍為 10Hz ~ 2MHz ,測量誤差的絕對值不大于 0.1% 。( 15 分) ( 2 )測量脈沖信号占空比 D ,測量範圍為 10 %~ 90 %,測量誤差的絕對值不大于 2% 。( 15 分)

思路一:外部中斷

思路:這種方法是很容易想到的,而且對幾乎所有 MCU 都适用(連 51 都可以)。方法也很簡單,聲明一個計數變量 TIM_cnt ,每次一個上升沿 / 下降沿就進入一次中斷,對 TIM_cnt++ ,然後定時統計即可。如果需要占空比,那麼就另外用一個定時器統計上升沿、下降沿之間的時間即可。

缺點:缺陷顯而易見,當頻率提高,将會頻繁進入中斷,占用大量時間。而當頻率超過 100kHz 時,中斷程式時間甚至将超過脈沖周期,産生巨大誤差。同時更重要的是,想要測量的占空比由于受到中斷程式影響,誤差将越來越大。

總結 : 我們當時第一時間就把這個方案 PASS 了,沒有相關代碼(這個代碼也很簡單)。不過,該方法在頻率較低( 10K 以下)時,可以拿來測量頻率。在頻率更低的情況下,可以拿來測占空比。

思路二: PWM 輸入模式

思路:翻遍 ST 的參考手冊,在定時器當中有這樣一種模式: 總結 : 我們當時第一時間就把這個方案 PASS 了,沒有相關代碼(這個代碼也很簡單)。不過,該方法在頻率較低( 10K 以下)時,可以拿來測量頻率。在頻率更低的情況下,可以拿來測占空比。

思路二: PWM 輸入模式

思路:翻遍 ST 的參考手冊,在定時器當中有這樣一種模式:

使用 STM32 測量頻率和占空比的幾種方法
使用 STM32 測量頻率和占空比的幾種方法
使用 STM32 測量頻率和占空比的幾種方法
使用 STM32 測量頻率和占空比的幾種方法

簡而言之,理論上,通過這種模式,可以用硬體直接測量出頻率和占空比。當時我們發現這一模式時歡欣鼓舞,以為可以一步解決這一問題,代碼如下:

void Tim2_PWMIC_Init(void)
{
        GPIO_InitTypeDef GPIO_InitStructure;
  NVIC_InitTypeDef NVIC_InitStructure;
        TIM_ICInitTypeDef  TIM_ICInitStructure;
  /* TIM4 clock enable */
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);
 
  /* GPIOB clock enable */
  RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
   
  /* TIM4 chennel2 configuration : PB.07 */
  GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_7;
  GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
  GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
  GPIO_InitStructure.GPIO_PuPd  = GPIO_PuPd_UP ;
  GPIO_Init(GPIOB, &GPIO_InitStructure);
   
  /* Connect TIM pin to AF2 */
  GPIO_PinAFConfig(GPIOB, GPIO_PinSource7, GPIO_AF_TIM4);
         
  /* Enable the TIM4 global Interrupt */
  NVIC_InitStructure.NVIC_IRQChannel = TIM4_IRQn;
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  NVIC_Init(&NVIC_InitStructure);
         
        TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;
  TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
  TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
  TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
  TIM_ICInitStructure.TIM_ICFilter = 0x0;
 
  TIM_PWMIConfig(TIM4, &TIM_ICInitStructure);
 
  /* Select the TIM4 Input Trigger: TI2FP2 */
  TIM_SelectInputTrigger(TIM4, TIM_TS_TI2FP2);
 
  /* Select the slave Mode: Reset Mode */
  TIM_SelectSlaveMode(TIM4, TIM_SlaveMode_Reset);
  TIM_SelectMasterSlaveMode(TIM4,TIM_MasterSlaveMode_Enable);
 
  /* TIM enable counter */
  TIM_Cmd(TIM4, ENABLE);
 
  /* Enable the CC2 Interrupt Request */
   TIM_ITConfig(TIM4, TIM_IT_CC2, ENABLE);
 
}
//中斷程式:
void TIM4_IRQHandler(void)
{
  /* Clear TIM4 Capture compare interrupt pending bit */
  TIM_ClearITPendingBit(TIM4, TIM_IT_CC1|TIM_IT_CC2);
 
  /* Get the Input Capture value */
  IC2Value = TIM_GetCapture2(TIM4);//周期
 
  if (IC2Value != 0)
  {
                highval[filter_cnt]=TIM_GetCapture1(TIM4);//高電平周期
                waveval[filter_cnt]=IC2Value;
                filter_cnt++;
                if(filter_cnt>=FILTER_NUM)
                        filter_cnt=0;
  }
  else
  {
    DutyCycle = 0;
    Frequency = 0;
  }
}
//主循環:
  while (1)
  {
                uint32_t highsum=0,wavesum=0,dutysum=0,freqsum=0;
                LCD_Clear(0);
                for(i=0;i<FILTER_NUM;i++)
                {
                        highsum+=highval[i];
                        wavesum+=waveval;
                }
  [/i]              delay_ms(1);
                DutyCycle=highsum*1000/wavesum;
                Frequency=(SystemCoreClock/2*1000/wavesum);
                freq=Frequency*2.2118-47.05;//線性補償
                sprintf(str,"DUTY:%3d\nFREQ:%.3f KHZ\n",DutyCycle,freq/1000);
                 
                LCD_ShowString(0,200,str);
                delay_ms(100);
  }
           

但是,經過測量之後發現這種方法測試資料不穩定也不精确,資料不停跳動,且和實際值相差很大。 ST 的這些功能經常有這種問題,比如定時器的編碼器模式,在 0 點處頻繁正負跳變時有可能會卡死。這些方法雖然省事,穩定性卻不是很好。

經過線性補償可以一定程度上減少誤差(參數在不同情況下不同):

freq=Frequency*2.2118-47.05;

這種方法無法實作要求。是以在這裡我并不推薦這種方法。如果有誰能夠有較好的程式,也歡迎發出來。

思路三:輸入捕獲

思路:一般來說,對STM32有一定了解的壇友們在測量頻率的問題上往往都會想到利用輸入捕獲。首先設定為上升沿觸發,當進入中斷之後(rising)記錄與上次中斷(rising_last)之間的間隔(周期,其倒數就是頻率)。再設定為下降沿,進入中斷之後與上升沿時刻之差即為高電平時間(falling-rising_last),高電平時間除周期即為占空比

程式如下,注意由于為了減少程式複雜性使用了32位定時器5(計數周期如果是1us時可以計數4294s,否則如果是16位隻能計數65ms),如果需要在F1上使用則需要自行處理:

//定時器5通道1輸入捕獲配置
//arr:自動重裝值(TIM2,TIM5是32位的!!)
//psc:時鐘預分頻數
void TIM5_CH1_Cap_Init(u32 arr,u16 psc)
{
        GPIO_InitTypeDef GPIO_InitStructure;
        TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
        NVIC_InitTypeDef NVIC_InitStructure;
 
         
        RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM5,ENABLE);          //TIM5時鐘使能    
        RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);         //使能PORTA時鐘        
         
        GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; //GPIOA0
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;//複用功能
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;        //速度100MHz
        GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽複用輸出
        GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_DOWN; //下拉
        GPIO_Init(GPIOA,&GPIO_InitStructure); //初始化PA0
 
        GPIO_PinAFConfig(GPIOA,GPIO_PinSource0,GPIO_AF_TIM5); //PA0複用位定時器5
   
           
        TIM_TimeBaseStructure.TIM_Prescaler=psc;  //定時器分頻
        TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up; //向上計數模式
        TIM_TimeBaseStructure.TIM_Period=arr;   //自動重裝載值
        TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1; 
         
        TIM_TimeBaseInit(TIM5,&TIM_TimeBaseStructure);
         
 
        //初始化TIM5輸入捕獲參數
        TIM5_ICInitStructure.TIM_Channel = TIM_Channel_1; //CC1S=01         選擇輸入端 IC1映射到TI1上
  TIM5_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;        //上升沿捕獲
  TIM5_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; //映射到TI1上
  TIM5_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;         //配置輸入分頻,不分頻 
  TIM5_ICInitStructure.TIM_ICFilter = 0x00;//IC1F=0000 配置輸入濾波器 不濾波
  TIM_ICInit(TIM5, &TIM5_ICInitStructure);
                 
        TIM_ITConfig(TIM5,TIM_IT_Update|TIM_IT_CC1,ENABLE);//允許更新中斷 ,允許CC1IE捕獲中斷        
         
  TIM_Cmd(TIM5,ENABLE );         //使能定時器5
 
  
  NVIC_InitStructure.NVIC_IRQChannel = TIM5_IRQn;
        NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2;//搶占優先級
        NVIC_InitStructure.NVIC_IRQChannelSubPriority =0;                //子優先級
        NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;                        //IRQ通道使能
        NVIC_Init(&NVIC_InitStructure);        //根據指定的參數初始化VIC寄存器、
         
         
}
//捕獲狀态(對于32位定時器來說,1us計數器加1,溢出時間:4294秒)
//定時器5中斷服務程式         
void TIM5_IRQHandler(void)
{                     
                         
        if(TIM_GetITStatus(TIM5, TIM_IT_CC1) != RESET)//捕獲1發生捕獲事件
        {
                if(edge==RESET)//上升沿
                {
                        rising=TIM5->CCR1-rising_last;
                        rising_last=TIM5->CCR1;
                        TIM_OC1PolarityConfig(TIM5,TIM_ICPolarity_Falling); //CC1P=0 設定為上升沿捕獲
                        edge=SET;
                }
                else
                {
                        falling=TIM5->CCR1-rising_last;
                        TIM_OC1PolarityConfig(TIM5,TIM_ICPolarity_Rising); //CC1P=0 設定為上升沿捕獲
                        edge=RESET;
                }
        }
        TIM_ClearITPendingBit(TIM5, TIM_IT_CC1|TIM_IT_Update); //清除中斷标志位
}
主程式:
  while (1)
  {
                uint32_t highsum=0,wavesum=0,dutysum=0,freqsum=0;
                LCD_Clear(0);
 
                delay_ms(1);
 
                sprintf(str,"rise:%3d\nfall:%d\nfall-rise:%d",rising,falling,falling-rising);
                LCD_ShowString(0,100,str);
                sprintf(str,"Freq:%.2f Hz\nDuty:%.3f\n",90000000.0/rising,(float)falling/(float)rising);//頻率、占空比
                LCD_ShowString(0,200,str);
                delay_ms(100);
  }
           

注意的是,中斷程式當中的變量rising,last因為多次修改的緣故,與名稱本身含義有所差別,示意如下:

使用 STM32 測量頻率和占空比的幾種方法

該方法尤其是在中低頻( <100kHz)之下精度不錯。 缺點:稍有經驗的朋友們應該都能看出來,該方法仍然會帶來極高的中斷頻率。在高頻之下,首先是 CPU時間被完全占用,此外,更重要的是,中斷程式時間過長往往導緻會錯過一次或多次中斷信号,表現就是測量值在實際值、實際值× 2、實際值× 3等之間跳動。實測中,最高頻率可以測到約 400kHz。 總結:該方法在低頻率( <100kHz)下有着很好的精度,在考慮到其它程式的情況下,建議在 10kHz之下使用該方法。同時,可以參考以下的改程序式減少 CPU負載。 改進: 前述問題,限制頻率提高的主要因素是過長的中斷時間(一般應用情景之下,還有其它程式部分的限制)。是以進行以下改進: 1.           使用 2個通道,一個隻測量上升沿,另一個隻測量下降沿。這樣可以減少切換觸發邊沿的延遲,缺點是多用了一個 IO口。 2.           使用寄存器,簡化程式 最終程式如下:

/TIM2_CH1->PA5
//TIM2_CH2->PB3
void TIM2_CH1_Cap_Init(u32 arr,u16 psc)
{
        GPIO_InitTypeDef GPIO_InitStructure;
        TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
        NVIC_InitTypeDef NVIC_InitStructure;
        TIM_ICInitTypeDef  TIM_ICInitStructure;
         
        TIM_DeInit(TIM2);
        RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);          //TIM2時鐘使能    
        RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA|RCC_AHB1Periph_GPIOB, ENABLE);         //使能PORTA時鐘        
         
        GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; //GPIOA0
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;//複用功能
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_25MHz;        //速度100MHz
        GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽複用輸出
        GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_DOWN; //下拉
        GPIO_Init(GPIOA,&GPIO_InitStructure); //初始化PA0
         
        GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3; //GPIOA0
        GPIO_Init(GPIOB,&GPIO_InitStructure); //初始化PA0
         
        GPIO_PinAFConfig(GPIOA,GPIO_PinSource5,GPIO_AF_TIM2); //PA0複用位定時器5
        GPIO_PinAFConfig(GPIOB,GPIO_PinSource3,GPIO_AF_TIM2); //PA0複用位定時器5
         
        TIM_TimeBaseStructure.TIM_Prescaler=psc;  //定時器分頻
        TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up; //向上計數模式
        TIM_TimeBaseStructure.TIM_Period=arr;   //自動重裝載值
        TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1; 
         
        TIM_TimeBaseInit(TIM2,&TIM_TimeBaseStructure);
        //初始化TIM2輸入捕獲參數
        TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; //CC1S=01         選擇輸入端 IC1映射到TI1上
  TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;        //上升沿捕獲
  TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; //映射到TI1上
  TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;         //配置輸入分頻,不分頻 
  TIM_ICInitStructure.TIM_ICFilter = 0x00;//IC1F=0000 配置輸入濾波器 不濾波
  TIM_ICInit(TIM2, &TIM_ICInitStructure);
         
        TIM_ICInitStructure.TIM_Channel = TIM_Channel_2; //CC1S=01         選擇輸入端 IC1映射到TI1上
        TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Falling;        //上升沿捕獲
         
        TIM_ICInit(TIM2, &TIM_ICInitStructure);
                 
        TIM_ITConfig(TIM2,TIM_IT_Update|TIM_IT_CC1|TIM_IT_CC2,ENABLE);//允許更新中斷 ,允許CC1IE捕獲中斷        
//        TIM2_CH1_Cap_DMAInit();
  TIM_Cmd(TIM2,ENABLE );         //使能定時器5
  
  NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
        NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0;//搶占優先級3
        NVIC_InitStructure.NVIC_IRQChannelSubPriority =0;                //子優先級3
        NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;                        //IRQ通道使能
        NVIC_Init(&NVIC_InitStructure);        //根據指定的參數初始化VIC寄存器、
         
}
//定時器2中斷服務程式(對于32位定時器來說,1us計數器加1,溢出時間:4294秒)
void TIM2_IRQHandler(void)
{                     
        if(TIM2->SR&TIM_FLAG_CC1)//TIM_GetITStatus(TIM2, TIM_IT_CC1) != RESET)//捕獲1發生捕獲事件
        {
                rising=TIM2->CCR1-rising_last;
                rising_last=TIM2->CCR1;
                return;
        }
        if(TIM2->SR&TIM_FLAG_CC2)//TIM_GetITStatus(TIM2, TIM_IT_CC2) != RESET)
        {
                falling=TIM2->CCR2-rising_last;
                return;
        }
        TIM2->SR=0;
}
           

之是以改用 TIM2 是因為 TIM5 的 CH1(PA0) 還是按鍵輸入引腳。本來想來這應當也沒什麼,按鍵不按下不就是開路嘛。但是後來發現官方開發闆上還有一個 RC 濾波…… 是以,當使用别人的程式之前,請一定仔細檢視電路圖。

使用 STM32 測量頻率和占空比的幾種方法

這樣,最高頻率能夠達到約1.1MHz,是一個不小的進步。但是,其根本問題——中斷太頻繁——仍然存在。

解決思路也是存在的。本質上,我們實際上隻需要讀取CCR1和CCR2寄存器。而在記憶體複制過程中,面對大資料量的轉移時,我們會想到什麼?顯然,我們很容易想到——利用DMA。是以,我們使用輸入捕獲事件觸發DMA來搬運寄存器而非觸發中斷即可,然後将這些資料存放在一個數組當中并循環重新整理。這樣,我們可以随時來檢視資料并計算出頻率。

這一方法我曾經嘗試過,沒有調出來,因為,有一個更好的方法存在。但是理論上這是沒有問題的,以供參考我列出如下。

【注意:這段程式無法工作,僅供參考!!!】

//TIM2_CH1->DMA1_CHANNEL3_STREAM5
u32        val[FILTER_NUM]={0};
void        TIM2_CH1_Cap_DMAInit(void)
{
        NVIC_InitTypeDef NVIC_InitStructure;
        DMA_InitTypeDef  DMA_InitStructure;
         
        RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1,ENABLE);//DMA1時鐘使能 
         
  DMA_DeInit(DMA1_Stream5);
         
        while (DMA_GetCmdStatus(DMA1_Stream5) != DISABLE){}//等待DMA可配置 
         
  /* 配置 DMA Stream */
  DMA_InitStructure.DMA_Channel = DMA_Channel_3;  //通道選擇
  DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(TIM5->CCR1);//DMA外設位址
  DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)val;//DMA 存儲器0位址
  DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;//存儲器到外設模式
  DMA_InitStructure.DMA_BufferSize = FILTER_NUM;//資料傳輸量 
  DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外設非增量模式
  DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//存儲器增量模式
  DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;//外設資料長度:8位
  DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;//存儲器資料長度:8位
  DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;// 使用普通模式 
  DMA_InitStructure.DMA_Priority = DMA_Priority_High;//中等優先級
  DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;         
  DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;
  DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;//存儲器突發單次傳輸
  DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;//外設突發單次傳輸
  DMA_Init(DMA1_Stream5, &DMA_InitStructure);//初始化DMA Stream
 
        TIM_DMAConfig(TIM5,TIM_DMABase_CCR1,TIM_DMABurstLength_16Bytes);
        TIM_DMACmd(TIM5,TIM_DMA_CC1,ENABLE);
        //如果需要DMA中斷則如下面所示
                NVIC_InitStructure.NVIC_IRQChannel = DMA1_Stream5_IRQn;                                                                        //使能TIM中斷
                NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02;                //搶占優先級
                NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02;                                                //子優先級
                NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;                                                                        //使能中斷
        NVIC_Init(&NVIC_InitStructure); 
        DMA_ITConfig(DMA1_Stream5,DMA_IT_TC,ENABLE);        
        //開啟DMA傳輸 
        DMA_Cmd(DMA1_Stream5, ENABLE);  
}
void        DMA1_Stream5_IRQHandler(void)
{
        DMA_ClearITPendingBit(DMA1_Stream5,DMA_IT_TCIF5);
}
           

@xkwy 大神在回複中提出了幾個改進意見,列出如下: 1. 可以設定僅有通道 2 進行下降沿捕獲并觸發中斷,而通道 1 捕獲上升沿不觸發中斷。在中斷函數當中,一次讀取 CCR1 和 CCR2 。這樣可以節省大量時間。 2. 可以先進行一次測量,根據測量值改變預分頻值 PSC ,進而提高精度 3. 間隔采樣。例如每 100ms 采樣 10ms. 這樣的改進應當能夠将最高采樣頻率增加到 2M. 但是頻率的進一步提高仍然不可能。因為這時的主要沖突是中斷函數時間過長,導緻 CPU 還在進行中斷的時候這一次周期就結束了,使得最終測量到的頻率為真實頻率的整數倍左右。示意圖如下:

使用 STM32 測量頻率和占空比的幾種方法

是以,高頻時仍然推薦以下方法。

思路四:使用外部時鐘計數器

這種方法是我這幾天回答問題時推薦的方法。思路是配置兩個定時器,定時器a設定為外部時鐘計數器模式,定時器b設定為定時器(比如50ms溢出一次,也可以用軟體定時器),然後定時器b中斷函數中統計定時器a在這段時間内的增量,簡單計算即可。

代碼:

//TIM7->100ms
//TIM2_CH2->PB3
void TIM_Cnt_Init(void)
{
        GPIO_InitTypeDef GPIO_InitStructure;
        TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
        NVIC_InitTypeDef NVIC_InitStructure;
         
        TIM_DeInit(TIM2);
        TIM_DeInit(TIM7);
         
        RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2|RCC_APB1Periph_TIM7,ENABLE);          //TIM2時鐘使能    
        RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);         //使能PORTA時鐘        
//IO        
        GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3; //GPIOA0
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;//複用功能
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_25MHz;        //速度100MHz
        GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽複用輸出
        GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; //下拉
        GPIO_Init(GPIOB,&GPIO_InitStructure); //初始化PA0
         
        GPIO_PinAFConfig(GPIOB,GPIO_PinSource3,GPIO_AF_TIM2); //PA0複用位定時器5
//TIM2配置
        TIM_TimeBaseStructure.TIM_Prescaler=0;  //定時器分頻
        TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up; //向上計數模式
        TIM_TimeBaseStructure.TIM_Period=0xFFFFFFFF;   //自動重裝載值
        TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1; 
        TIM_TimeBaseInit(TIM2,&TIM_TimeBaseStructure);
         
        TIM_TIxExternalClockConfig(TIM2,TIM_TIxExternalCLK1Source_TI2,TIM_ICPolarity_Rising,0);//外部時鐘源
//TIM7        100ms        
        TIM_TimeBaseStructure.TIM_Prescaler=18000-1;  //定時器分頻
        TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up; //向上計數模式
        TIM_TimeBaseStructure.TIM_Period=1000-1;   //自動重裝載值
        TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1; 
        TIM_TimeBaseInit(TIM7,&TIM_TimeBaseStructure);
//中斷
  NVIC_InitStructure.NVIC_IRQChannel = TIM7_IRQn;
        NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0;//搶占優先級3
        NVIC_InitStructure.NVIC_IRQChannelSubPriority =0;                //子優先級3
        NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;                        //IRQ通道使能
        NVIC_Init(&NVIC_InitStructure);        //根據指定的參數初始化VIC寄存器、
        TIM_ITConfig(TIM7,TIM_IT_Update,ENABLE);//允許更新中斷 ,允許CC1IE捕獲中斷        
 
        TIM_Cmd(TIM7,ENABLE );         //使能定時器5
  TIM_Cmd(TIM2,ENABLE );         //使能定時器5        
}
u32 TIM7_LastCnt;
//頻率為TIM_ExtCntFreq 
void TIM7_IRQHandler(void)
{
        char        str[32];
        TIM_ExtCntFreq=(TIM2->CNT-TIM7_LastCnt)*(1/SAMPLE_PERIOD);// SAMPLE_PERIOD為采樣周期0.1s
        sprintf(str,"%3.3f",TIM_ExtCntFreq/1000.0);//必須加這一句,莫明其妙
        TIM7_LastCnt=TIM2->CNT;
        TIM_ClearITPendingBit(TIM7,TIM_IT_Update);
}
           

缺點:

1. 無法測量占空比,高頻的占空比測量方法見下文。

2. 在頻率較低的情況下,測量精度不如思路 3 (因為測量周期為 100ms ,此時如果脈沖周期是 200ms ……)。

3. 輸入幅值必須超過 3V 。如果不夠或者超出,需要加入前置放大器。

總結:這種方法精度很高,實測在 2MHz 之下誤差為 30Hz 也就是 0.0015% (由中斷服務程式引發,可以使用線性補償修正),在 25MHz 之下也是誤差 30Hz 左右(沒法達到更高的原因是波形發生器的最大輸出頻率是 25MHz^_^ )。同時,從根本上解決了中斷頻率過高的問題。而由于低頻的問題,建議:在低頻時,或者加大采樣間隔(更改 TIM7 的周期),或者采用思路 3 的輸入捕獲。

此外,還有一個莫名其妙的問題就是,中斷當中如果不加入 sprintf(str,"%3.3f",TIM_ExtCntFreq/1000.0) 這一句, TIM_ExtCntFreq 就始終為 0 。我猜測是優化的問題,但是加入 volatile 也沒有用,時間不夠就沒有理睬了。

思路五: ADC 采樣測量(機率測量法)

一般的高端示波器,測量頻率即是這種方法。簡而言之,高速采樣一系列資料,然後通過頻譜分析(例如快速傅裡葉變換 FFT ),獲得頻率。 F4 有着 FPU 和 DSP 指令,計算速度上可以接受。但是 ADC 的采樣頻率遠遠達不到。官方手冊上聲明,在三通道交替采樣 +DMA 之下,最高可以達到 8.4M 的采樣率。然而,根據香農采樣定理,采樣頻率至少要達到信号的 2 倍。 2M 信号和 8.4M 的采樣率,即使能夠計算,誤差也無法接受。是以, ADC 采樣是無法測量頻率特别是高頻頻率的。

但是,無法測量頻率,卻可以測量占空比,乃至超調量和上升時間(信号從 10% 幅值上升到 90% 的時間)!原理也很簡單,大學機率課上都說過這個機率基本原理:

使用 STM32 測量頻率和占空比的幾種方法

當采樣數n趨于無窮時,事件A的機率即趨近于統計的頻率。是以,當采樣數越大,則采樣到的高電平占樣本總數的頻率即趨近于機率——占空比!

使用 STM32 測量頻率和占空比的幾種方法

是以,基本思路即是等間隔(速度無所謂,但必須是保證等機率采樣)采樣,并将這些資料存入一個數組,反複重新整理。這樣,可以在任意時間對數組中資料進行統計,獲得占空比資料。

以下是代碼,使用了三通道 8 位 ADC+DMA 。理論上,采用查詢法也是可以的。

//ADC1-CH13-PC3
//DMA2-CH0-STREAM0
  #define ADCx                     ADC1
  #define ADC_CHANNEL              ADC_Channel_13
  #define ADCx_CLK                 RCC_APB2Periph_ADC1
  #define ADCx_CHANNEL_GPIO_CLK    RCC_AHB1Periph_GPIOC
  #define GPIO_PIN                 GPIO_Pin_3
  #define GPIO_PORT                GPIOC
  #define DMA_CHANNELx             DMA_Channel_0
  #define DMA_STREAMx              DMA2_Stream0
  #define ADCx_DR_ADDRESS          ((uint32_t)&(ADCx->DR))//((uint32_t)0x4001224C)
void        ADC_DMAInit(void)
{
  ADC_InitTypeDef       ADC_InitStructure;
  ADC_CommonInitTypeDef ADC_CommonInitStructure;
  DMA_InitTypeDef       DMA_InitStructure;
  GPIO_InitTypeDef      GPIO_InitStructure;
 
  /* Enable ADCx, DMA and GPIO clocks ****************************************/
  RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);
  RCC_AHB1PeriphClockCmd(ADCx_CHANNEL_GPIO_CLK, ENABLE);  
  RCC_APB2PeriphClockCmd(ADCx_CLK, ENABLE);
   
 
  /* DMA2 Stream0 channel2 configuration **************************************/
  DMA_InitStructure.DMA_Channel = DMA_CHANNELx;  
  DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)ADCx_DR_ADDRESS;
  DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)&(ADC_DATAPOOL[0]);
  DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;
  DMA_InitStructure.DMA_BufferSize = ADC_POOLSIZE;
  DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
  DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
  DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
  DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
  DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
  DMA_InitStructure.DMA_Priority = DMA_Priority_High;
  DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;         
  DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;
  DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
  DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
  DMA_Init(DMA_STREAMx, &DMA_InitStructure);
  DMA_Cmd(DMA_STREAMx, ENABLE);
 
  /* Configure ADC3 Channel7 pin as analog input ******************************/
  GPIO_InitStructure.GPIO_Pin = GPIO_PIN;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AN;
  GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL ;
  GPIO_Init(GPIO_PORT, &GPIO_InitStructure);
 
  /* ADC Common Init **********************************************************/
  ADC_CommonInitStructure.ADC_Mode = ADC_Mode_Independent;
  ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div2;
  ADC_CommonInitStructure.ADC_DMAAccessMode = ADC_DMAAccessMode_Disabled;
  ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_20Cycles;
  ADC_CommonInit(&ADC_CommonInitStructure);
 
  /* ADC3 Init ****************************************************************/
  ADC_InitStructure.ADC_Resolution = ADC_Resolution_8b;
  ADC_InitStructure.ADC_ScanConvMode = DISABLE;
  ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
  ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_None;
  ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T1_CC1;
  ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
  ADC_InitStructure.ADC_NbrOfConversion = 1;
  ADC_Init(ADCx, &ADC_InitStructure);
 
  /* ADC3 regular channel7 configuration **************************************/
  ADC_RegularChannelConfig(ADCx, ADC_CHANNEL, 1, ADC_SampleTime_480Cycles);
 
 /* Enable DMA request after last transfer (Single-ADC mode) */
  ADC_DMARequestAfterLastTransferCmd(ADCx, ENABLE);
 
  /* Enable ADC3 DMA */
  ADC_DMACmd(ADCx, ENABLE);
 
  /* Enable ADC3 */
  ADC_Cmd(ADCx, ENABLE);
}
主程式:
                                        for(j=0;j<ADC_POOLSIZE;j++)
                                        {
                                                if(ADC_DATAPOOL[j]>0x01)
                                                        posicnt++;
                                        }
                                        duty=100*posicnt/(float)(ADC_POOLSIZE)+0.1f;//線性補償
           

缺點:

1. 精度低:實測 2MHz 下誤差約 1.3% ,低頻時無法統計(比如,頻率 10Hz ,而 ADC 采樣時間 50ms 。這時如果采樣時間中剛好全是高電平,占空比為 1 ……)。

2. 記憶體占用大:資料池大小為 65536 ,占用了 64KB 記憶體 。

3. 有響應延遲:測量出來的是“平均占空比”而非“瞬時占空比”。由于我測試時使用的是波形發生器,輸出波形相當穩定( 1W+ 的價格畢竟是有它的道理的……),實際應用當中一般不能夠達到這樣的水準,勢必帶來響應延遲(準确說應該是采樣系統積分慣性越大)。

4. 幅值過低( 0.3V )無法測量,過高則超過 ADC 允許最大值。是以必須視情況使用不同的前置放大器。

實際上使用時如何取舍,就需要看實際情況了。畢竟,這隻是低成本下的解決方案而已。

綜上,對這幾種方法做一個總結:

外部中斷:編寫容易,通用性強。缺點是中斷進入頻繁,誤差大。

PWM 輸入:全硬體完成, CPU 負載小,編寫容易。缺點是不穩定,誤差大。

輸入捕獲:可達到約 400kHz 。低頻精度高, 10Hz 可達到 0.01% 以下, 400kHz 也有 3% 。缺點是中斷頻繁,無法測量高頻,幅值必須在 3.3~5V 之間。

外部時鐘計數器(首選):可達到非常高的頻率(理論上應當是 90MHz )和非常低的誤差( 2MHz 下為 0.0015% 且可線性補償)。缺點是低頻精度較低,同樣幅值必須在 3.3~5V 之間。

ADC 采樣頻率測量法:難以測量頻率,高頻下對占空比、上升時間有可以接受的測量精度( 2MHz 下約 1.3% ),低頻下無法測量。幅值 0.3~3.3V ,加入前置放大則幅值随意。

ADC 采樣頻譜分析:高端示波器專用, STM32 棄療。

我采用的方法是:首先 ADC 測量幅值并據此改變前置放大器放大倍數,調整幅值為 3.3V ,同時測量得到參考占空比。而後使用外部時鐘計數器測量得到頻率,如果較高( >10000 )則确認為頻率資料,同時 ADC 測量占空比确認為占空比資料。否則再使用輸入捕獲方法測量得到頻率、占空比資料。

對于各個方法存在的線性誤差,使用了線性補償來提高精度。一般情況下,使用存儲在 ROM 中的資料作為參數,當需要校正時,采用如下校正思路:

波形發生器生成一些預設參數波形(例如 10Hz , 10% ; 100K , 50% ; 2M , 90% ……),在不同區間内多次測量得到資料,随後以原始資料為 x ,真實資料為 y ,去除異常資料之後,做 y=f(x) 的線性回歸,并取相關系數最高的作為新的參數,同時存儲在 ROM 當中。

我認為,我的這篇文章,應當是很全面了。當然,限于水準,存在着未完善和不正确的地方,也歡迎指正。

繼續閱讀