使用 STM32 测量频率和占空比的几种方法

发布者:自在逍遥最新更新时间:2018-08-04 来源: eefocus关键字:STM32  测量频率  占空比 手机看文章 扫描二维码
随时随地手机看文章

使用平台:官方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的参考手册,在定时器当中有这样一种模式:


简而言之,理论上,通过这种模式,可以用硬件直接测量出频率和占空比。当时我们发现这一模式时欢欣鼓舞,以为可以一步解决这一问题,代码如下:

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

                {

                        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因为多次修改的缘故,与名称本身含义有所区别,示意如下:



该方法尤其是在中低频(<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滤波……

所以,当使用别人的程序之前,请一定仔细查看电路图。




这样,最高频率能够达到约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还在处理中断的时候这一次周期就结束了,使得最终测量到的频率为真实频率的整数倍左右。示意图如下:




因此,高频时仍然推荐以下方法。


思路四:使用外部时钟计数器


这种方法是我这几天回答问题时推荐的方法。思路是配置两个定时器,定时器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%的时间)!原理也很简单,大学概率课上都说过这个概率基本原理:

当采样数n趋于无穷时,事件A的概率即趋近于统计的频率。所以,当采样数越大,则采样到的高电平占样本总数的频率即趋近于概率——占空比!



因此,基本思路即是等间隔(速度无所谓,但必须是保证等概率采样)采样,并将这些数据存入一个数组,反复刷新。这样,可以在任意时间对数组中数据进行统计,获得占空比数据。


以下是代码,使用了三通道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

                                        {

                                                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当中。


我认为,我的这篇文章,应当是很全面了。当然,限于水平,存在着未完善和不正确的地方,也欢迎指正。


关键字:STM32  测量频率  占空比 引用地址:使用 STM32 测量频率和占空比的几种方法

上一篇:STM32之timer1产生PWM(互补通道)
下一篇:STM32F051 触摸按键功能

推荐阅读最新更新时间:2024-03-16 16:10

10天学会STM32的学习心得总结
01 前言 有读者问,如何系统地入门学习stm32呢? 假如你会使用8051 , 会写C语言,那么STM32本身并不需要刻意的学习。 我们要考虑的是, 我可以快速用STM32实现什么?为什么使用STM32而不是8051? 是因为51的频率太低,无法满足计算需求? 是51的管脚太少,无法满足众多外设的IO? 是51的功耗太大,电池挺不住? 是51的功能太弱,而你要使用SPI、I2C、ADC、DMA? 是51的内存太小而你要存储的东西太多? 当你需要使用STM32某些功能,而51实现不了的时候, 那STM32自然不需要学习,你会直接去寻找STM32某方面的使用方法。 比如要用spi协议的网卡、要使用串口通信、要使用
[单片机]
STM32驱动ST7920的12864液晶(串行方式)
/****************************************************************************************** * 文件名称 :12864.c * 版权 : * 模块名称 :st7920驱动的12864液晶的串行工作方式驱动程序 * cpu : stm32f103rct6 主频:72M * 作者 : * 创建日期 :2009-10-15 * 功能概要 : *----------------------------------------修改历史------------------------------------------
[单片机]
STM32系列MCU开发环境的搭建
导读: 选择某款MCU的学习一般从其开发环境的搭建开始,即安装支持该处理器的编译、调试软件。其中最为关键的就是编译器的选择与安装,编译器的具体工作原理和作用可以参见本公众号“嵌入式ARM篇”合集文章《01_编译过程简介及为什么需要交叉编译器》。支持STM32系列MCU的编译软件有很多种,开发编译环境也各不相同,其中Keil uVision5(以下简称Keil5)编译软件以其简单易用的特点,应用最为广泛。 为了便于MCU的快速推广,开发商积极推出了各自的底层应用函数库,便于工程师的快速开发与应用。STM32系列MCU目前主要有两种函数库,一种是标准库,一种是HAL库(以下简称硬件库)。在使用标准库开发不同系列的MCU时,其中的功
[单片机]
<font color='red'>STM32</font>系列MCU开发环境的搭建
网站虚拟主机+安信可A6C GPRS模块实现对stm32的远程升级
利用http协议使用普通的网站虚拟主机+安信可A6C GPRS模块实现对stm32的远程升级 步骤: 1.生成bin文件并将bin文件放到虚拟主机目录内 2.做一个php的页面分块读取该bin文件,以asc字符echo出来,并标记好头尾长度和校验。 3.GPRS协议栈连接主机路径,收到内容,将内容解析,校验,无误后保存到备份flash空间 4.全部保存完成后,做个标识在参数flash区,等待重新开机 5.做个bootloader,开机后检测是否有保存好的新版本号,如果有则覆盖旧版本,否则直接进入入口向量。 由于GPRS传输http协议的内容属于短连接,AT指令的情况多而复杂,很多需要判断处理,有较高的工作强度,这里的内容暂
[单片机]
一种24V直流电机驱动器设计
 电动机分为交流电机和直流电机两大类。长期以来,直流电机以其良好的线性特性、优异的控制性能、较强的过载能力成为大多数变速运动控制和闭环位置伺服控制系统的最佳选择,一直处在调速领域主导地位。传统的直流电机调速方法很多,如调压调速、弱磁调速等,它们存在着调速响应慢、精度差、调速装置复杂等缺点。随着全控式电力电子器件技术的发展, 以大功率晶体管作为开关器件的直流脉宽调制(PWM)调速系统已成为直流调速系统的主要发展方向。   为配套24V直流电机,设计了一种直流无刷电机驱动器。采用美国Microchip公司的PIC16F690单片机作为控制器, MOSFET为驱动元件, 配以相应的控制软件构成控制系统。实践表明,整个系统的精度、
[电源管理]
一种24V直流电机驱动器设计
STM32驱动0.96 OLED I2C显示程序
0.96 OLED的主控芯片是SSD1306,用STM32驱动OLED显示的程序如处图所示 #include system.h #include SysTick.h #include OLED_I2C.h int main() { u8 i; extern const unsigned char BMP1 ; SysTick_Init(72); I2C_Configuration(); OLED_Init(); while(1) { OLED_CLS(); for(i=0;i 5
[单片机]
<font color='red'>STM32</font>驱动0.96 OLED I2C显示程序
STM32开发板学习笔记--PWM
使用TIM2在PA0口控制发光二极管的亮度,用接在PA3和PA8脚的按键控制PWM宽度 在网上找了一些例程,对我的帮助很大,弄了一上午,当看到发光二极管的亮度随着按键改变时,这种喜悦感是做其他事情感受不到的,这就是我玩单片机的乐趣所在! 不说废话了,先初始化I/O void GPIO_Configuration(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 ; //PWM输出 GPIO_InitStructure.GPIO_Mode = GPIO
[单片机]
STM32实现命令行
一、前言 工作中的开发环境都是基于linux命令行交互,作为命令行的重度使用者,玩单片机也要使用命令行工具,百度了一些命令行工具,有几个不错的开源 cmd 交互工具,主要看了 finsh和 nr_micro_shell 两个开源项目。Finsh 功能上限较多,但资源占用比较高,nr 有基本功能,且占用 MCU 资源较少,Github 给出的对比列表如下: Nr 和 Finsh 编译资源对比: 原始工程 添加nr_micro_shell增加量 添加finsh增加量 ROM 63660 +3832 +26908 RAM 4696 +1104 +1304 综合衡量了一下,移植 nr 作为后续项目的一个命令行交互。本篇文章分
[单片机]
在<font color='red'>STM32</font>实现命令行
小广播
添点儿料...
无论热点新闻、行业分析、技术干货……
设计资源 培训 开发板 精华推荐

最新单片机文章
何立民专栏 单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

换一换 更多 相关热搜器件
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2024 EEWORLD.com.cn, Inc. All rights reserved