当前位置:AIGC资讯 > 数据采集 > 正文

关于STM32F103输入捕获高精度采集频率信号的方法

前言

前段时间需要做一款频率采集设备,由于成本考虑,使用了APM32F103作为主控,APM32F103和STM32F103基本完全通用,有极个别BUG。不影响本次试验。客户要求的频率信号为11KHz到23KHz,精度要求在任何频率范围误差不能大于当前频率的万分之一以上(排除温度影响),采集速度要100次每秒以上。占空比可能会有变化。这种要求其实也只能选择STM32的输入捕获功能,我使用TIM2时钟的外部输入捕获,定时器采用36MHz时钟,时钟两分频。硬件输入分频设置为不分频,因为外部有硬件滤波器和信号处理电路,在此则不加输入滤波器。(时钟跑72M,遇到定时器中断少进或多进的BUG,所以改为36M)
(注:2023/08/09修改了部分程序和文案修改)

一、程序思路

其实采集频率的思路很简单,就是根据脉冲与脉冲直接的时间长短,得出周期时间。我采用的是全部上升沿捕获,而不是上升沿后改为下降沿。作用是防止占空比的变化影响采集精度。上升沿第一次进入中断后将TIM2计数器清空,将中间寄存器加一后退出,当中间寄存器大于等于10时,将TIM2中的计数值读取到采样寄存器中。当前采集到的就是80个输入脉冲周期的计数器值,根据要求,最小采样速度为100次每秒,11KHZ采集速度最快为137.5次每秒,也勉强满足要求。

二、程序

定时器初始化代码:

void TIM2_signal_input_Init(void)
{	 
	GPIO_InitTypeDef GPIO_InitStructure;
	TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
   	NVIC_InitTypeDef NVIC_InitStructure;

	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);	//使能TIM2时钟
 	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);  //使能GPIOA时钟
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; 
	GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_0;  
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	GPIO_ResetBits(GPIOA,GPIO_Pin_0);						 //PA0 下拉
	
	//定时器
	TIM_TimeBaseStructure.TIM_Prescaler =1; 	//预分频  
	TIM_TimeBaseStructure.TIM_Period = 0XFFFF; //自动重装值 
	TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //时钟分割
	TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  
	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); 
  
	//输入捕获
	TIM2_ICInitStructure.TIM_Channel = TIM_Channel_1;//该是那就映射到那
	TIM2_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;	 //配置输入分频,不分频 
    TIM2_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;	//上升沿捕获
    TIM2_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; //映射到TI1上
    TIM2_ICInitStructure.TIM_ICFilter = 0x00;//不滤波
    TIM_ICInit(TIM2, &TIM2_ICInitStructure);
	
	//中断
	NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;  //TIM2中断
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;  //从优先级0级
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;  //先占优先级2级
	NVIC_Init(&NVIC_InitStructure);  //根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器 
	TIM_ITConfig(TIM2,TIM_IT_Update|TIM_IT_CC1,ENABLE);//允许更新中断 ,允许CC1IE捕获中断	
    TIM_Cmd(TIM2,ENABLE ); 	//使能定时器2
}

中断代码:

//定时器2中断服务程序	 
void TIM2_IRQHandler(void)
{ 
 static unsigned char H_C=0;
	 
 	if((TIM2CH1_CAPTURE_STA&0X80)==0)//捕获成功后等待数据处理完成后,再次捕获	
	{	  
		if(TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
		{	    
			if((TIM2CH1_CAPTURE_STA&0X3F)==0X3F)  //频率太低了,不检测了
			{
				TIM2CH1_CAPTURE_STA|=0X80;
				TIM2CH1_CAPTURE_VAL=0XFFFF;
			}
			else TIM2CH1_CAPTURE_STA++;	 
		}
	if (TIM_GetITStatus(TIM2, TIM_IT_CC1) != RESET)//捕获1发生捕获事件
		{	
			if(TIM2CH1_CAPTURE_STA&0X40)		
			{//捕获10个周期了	
				TIM2CH1_CAPTURE_VAL=TIM_GetCapture1(TIM2);
				TIM2CH1_CAPTURE_STA|=0X80;		//出去处理数据,防止再次进入中断
			}else  								
			{//第一次捕获上升沿
			    if(H_C==0)
			    {
				  TIM2CH1_CAPTURE_STA=0;			//清空
				  TIM2CH1_CAPTURE_VAL=0;
				  TIM_SetCounter(TIM2,0);
				}
				H_C++;
				if(H_C==divider)  //divider :该变量表示将多少个频率周期整合在一起运算,以降低误差
				{//捕获(divider-1)个周期了
				   H_C=0;
				   TIM2CH1_CAPTURE_STA|=0X40;		
				}
			}		    
		}			     	    					   
 	}
 
    TIM_ClearITPendingBit(TIM2, TIM_IT_CC1|TIM_IT_Update); //清除中断标志位
}

转换为实际频率:

		 if(TIM2CH1_CAPTURE_STA&0X80)//成功捕获到八分频扭矩频率周期
		{
			temp=TIM2CH1_CAPTURE_STA&0X3F;
			temp*=65536;//溢出时间总和
			temp+=TIM2CH1_CAPTURE_VAL;//得到总的时间
			frequent_input=(36000000.0*divider)/temp; //根据当前的分频系数divider计算实际值
			frequent_input=frequent_input-(pow((frequent_input/100),2)*(0.00312/(divider)));//消除静差 
	        if(frequent_input>1000) divider=frequent_input/1000;     //根据当前频率大小修改下次采集的分频系数
	        else divider=1;	
			ui_cst=(unsigned int)frequent_input;
			
			//备注:上面的系数0.00312可根据实际误差修改,采集到的频率数据偏大,则将该数据增加,反之减小。
			//系数的目的为消除定时器在进入中断时,没有迅速读取CNT的数据,而多累加的值。
	        //可以自己加入数据输出函数
	        //
	        //
	        
		    TIM2CH1_CAPTURE_STA=0;			
		}

下载到板子

这是20KHZ,实际采样和输出对比。


这是10KHZ,实际采样和输出对比。

三、程序移植

因为我现在的程序在一个较为大型程序中,剔除的话有很多变量和函数封装。而且上面的这段程序有很大的优化空间。谨慎移植。很多读者想了解移植办法,那我就简单说下移植办法。这三个程序段则为整个数据处理的底层。仅仅是部分全局变量未加入其中。

void TIM2_signal_input_Init(void) 这个函数放在主函数的初始化中。裸机时放在main函数的主while循环前。
void TIM2_IRQHandler(void) 这个函数就是中断服务函数,没强制要求放在固定位置。
if(TIM2CH1_CAPTURE_STA&0X80) 开头的函数为数据处理函数,如果使用RTOS,则最好使用消息队列。如果是裸机跑的话,建议放在main函数的主while循环内,或者定时器的中断服务函数内。

总结

如果要求采样速度有降低,可以分频系数拉大,数据可以更精确。当前程序实际测试重复精度在十万分之五左右。可以看出程序和正点原子很像,感谢正点原子的教程与例程参考。

更新时间 2023-11-08