用STM32定时器测量信号频率——测频法和测周法[原创cnblogs.com/helesheng]
一、测频法(又称“计频法”)原理
二、测周法(又称“计时法”)原理
三、两种测量方法的选择——以STM32为例
四、用STM32定时器实现测频法(计频法)的思路和源码
1、外部时钟源模式1
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);
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的时基单元和中断
//!!!!!初始化定时器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被配置为对外部时钟进行计数的计数器,当标准时间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 }
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 }
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清零,这两个事件间的脉冲数被漏记,从而提高了计数精度。
2、外部时钟源模式2
TIM_TIxExternalClockConfig(TIM2,TIM_TIxExternalCLK1Source_TI2,TIM_ICPolarity_Rising,0);
TIM_SelectInputTrigger(TIM2,TIM_TS_ETRF);
五、用STM32定时器实现测周法(计时法)的思路和源码
根据图2所示的测周法原理,需要在被测信号周期(Tx2)内对STM32内部的最高频率72MHz(为获得最高测量精度和分辨率)的时钟进行计数。最简单的方式将被测信号作为外部中断源,并在外部中断服务程序中读取定时器中的计数值,但这样做会使中断入口时间也计算在Tx2以内。因此实现测周法的最佳方案,是使用STM32通用定时器或高级定时器的捕获功能(Input Capture)。
STM32的输入捕获电路框图如图4所示,它能在定时器的某个通道TIMx_CHy发生指定脉冲边沿的时刻及时地将此时的计数器计数值锁存在“捕获/比较寄存器”中,从而有效地避免了上面提到的方法中进入中断时延造成的计时误差。
1、第一种方法:每次捕获引发定时器中断读取
1 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //使能通用定时器TIM2时钟 2 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //使能GPIOA时钟
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的时间基单元
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);
1 TIM_ITConfig(TIM2,TIM_IT_Update|TIM_IT_CC2,ENABLE);//允许更新中断 ,允许CC2IE捕获中断
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寄存器
1 TIM_Cmd(TIM2, ENABLE); //使能TIM2
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 }
2、第二种方法:捕获引发DMA,再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
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
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传输完成中断
1 TIM_DMACmd(TIM2,TIM_DMA_CC2,ENABLE);
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 }
更多关于STM32定时器的使用方法,欢迎大家购买我的新书《基于STM32的嵌入式系统原理及应用》(科学出版社出版 ISBN:9787030697974)或我的B站账号“何乐生0”