用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。而较大的N1和被测频率fc1就意味着测量时间Tc1将变得很长,即每次得到测量结果的时间很长,时效性变低。因此当被测信号频率较低时,合理的测试方法是下面介绍的测周法(又称“计时法”)。

二、测周法(又称“计时法”)原理

测周法的测量对象是频率较低的被测信号,因此颠倒了图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的时基单元和中断

//!!!!!初始化定时器2,用于对外部被测脉冲进行计数!!!!!//
void Timer2_Init(void)//(u16 arr,u16 psc)
{
  TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //时钟使能

    TIM_TimeBaseStructure.TIM_Period = 65535;//设置在下一个更新事件装入活动的自动重装载寄存器周期的值     计数到5000
    TIM_TimeBaseStructure.TIM_Prescaler =0;
    //设置用来作为TIMx时钟频率除数的预分频值  10Khz的计数频率  
    TIM_TimeBaseStructure.TIM_ClockDivision = 0; //设置时钟分割:TDTS = Tck_tim
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  //TIM向上计数模式
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位

    TIM_ITConfig(  //使能或者失能指定的TIM中断
        TIM2, //TIM2
        TIM_IT_Update,    //TIM 中断源
        ENABLE  //使能
        );
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;  //TIM2更新中断
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;  //先占优先级0级
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;  //从优先级3级
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能
    NVIC_Init(&NVIC_InitStructure);  //根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器
 
    TIM_TIxExternalClockConfig(TIM2,TIM_TIxExternalCLK1Source_TI2,TIM_ICPolarity_Falling,0);
    //选择外部输入作为TIM2的时钟
    TIM_SetCounter(TIM2,0);//设置TIM2计数初值为0
    TIM_Cmd(TIM2, ENABLE);  //使能TIMx外设
}
配置用于对外部脉冲计数的TIM2

上面的代码中TIM2被配置为对外部时钟进行计数的计数器,当标准时间Tc1到到达后,TIM2计数的值就是测频法计数得到的数值。但为了对付计数器TIM2在标准时间Tc1完成之前就发生溢出从而造成计数值错误的情况,上面的代码还使用了TIM2中断。

4)计数器TIM2溢出中断服务程序

TMR2中断服务程序要做的事,是在TIM2计数器溢出后对溢出次数进行计数(溢出计数由软件完成,计数器为全局变量top_watch)。

1 unsigned  short top_watch=0;//用于在顶层对外部脉冲计数器溢出次数进行计数
2 void TIM2_IRQHandler(void)   //TIM2中断,产生标准时间
3 {
4     if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) //检查指定的TIM中断发生与否:TIM 中断源 
5         {
6             TIM_ClearITPendingBit(TIM2, TIM_IT_Update  );  //清除TIMx的中断待处理位:TIM 中断源 
7             top_watch++;
8         }
9 }
计数器TIM2中断服务程序

5)标准时长定时器TIM3的中断服务程序

最后我们只需要在标准时长Tc1到达时——也就是TIM3定时器发生中断时,读取Tc1内的脉冲数计算即可(通过函数TIM_GetCounter(TIM2);)。

 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的中断服务程序
Tc1在这里是10ms(即1/100秒),因此计数脉冲数乘以100就可以得到被测脉冲的频率。当然TMR2存在溢出的可能,若当前读取计数器中的值为cnt而上一次标准时长结束TMR3中断时计数器的计数的值为last_cnt,则两次TMR3中断之间的脉冲数为:top_watch*65536+(cnt - last_cnt)。计算时若cnt< last_cnt,结果可能为负数,我使用了下面的代码:
1           if(cnt >= last_cnt)//如果发生过溢出,当前计数结果就有可能比上次的计数结果还小
2                 pul_num = (unsigned int)(top_watch<<16)+ (unsigned int)(cnt - last_cnt);
3             else
4                 pul_num = (unsigned int)((top_watch-1)<<16) + (unsigned int)(65536 + cnt - last_cnt);

其中,移位操作是为了提高乘法计算的效率。

这里没有在中断服务程序中对计数器TIM2清零,而是任由TIM2自由计数,反而采用当前计数值和上次计数值求差的办法。这种方法看似繁琐,但保证了TIM2能够不间断的连续计数,而不会使从发生TIM3定时中断,到进入TIM3中服务程序对TIM2清零,这两个事件间的脉冲数被漏记,从而提高了计数精度。

另外,代码使用数组 frq[10]连续存取10次的测量得到的频率。如果需要,随后可以通过平均或其他方法来提高频率测量精度,当然也可以只使用一次测量的结果。
 

 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时钟
使能定时器和GPIO时钟
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寄存器
配置向量中断控制器NVIC
6)使能定时器
1 TIM_Cmd(TIM2, ENABLE);  //使能TIM2
使能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
配置DMA控制器
其中,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
使能DMA1CH7、TIM2和DMA1中断
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传输完成中断 
配置向量中断控制器并使能DMA中断
7)配置由TIM2的捕捉事件触发DMA传输

1 TIM_DMACmd(TIM2,TIM_DMA_CC2,ENABLE);
由TIM2的捕捉事件触发DMA传输
这个操作最容易忘记,其作用是让定时器捕捉触发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中断服务程序
由于DMA控制器只能传输16位的捕获结果,因此,如果两次捕获之间发生了溢出,DMA中断服务程序中是不可能直接知道的。所以前面关于这种方法的缺点中提到过:这种方法只能测量被测脉冲的周期小于定时器的溢出周期都的脉冲频率。当后一次捕获结果减去前一次捕获结果为一个复负数时,意味着两次捕获之间发生了一次定时器溢出,因此需要在计时结果中加上0xFFFF,再来计算对应频率。

 更多关于STM32定时器的使用方法,欢迎大家购买我的新书《基于STM32的嵌入式系统原理及应用》(科学出版社出版 ISBN:9787030697974)或我的B站账号“何乐生0

 

 

posted @ 2020-12-09 10:28  helesheng  阅读(29638)  评论(9编辑  收藏  举报