STM32F103详细频率测量过程

STM32F103频率测量


前言

本项目是做一个测量频率的仪器设备 ,初衷是为了测量正弦、三角、方波等信号的频率,由于设备限制,该文章仅限于方波频率的测量。本次测量频率的范围在5M以内,误差大约在0.005‰。


提示:以下是本篇文章正文内容

一、测频方法?

测频一般有外部中断测量频率、定时器测频(PWM频率捕获)、外部计数器等。

外部中断测频是使用最多最容易实现的,其测量原理也十分简单:

当外部检测到上升沿时触发中断,此时开启定时器,进行计时;当检测到第二个上升沿时结束计时,该时间便是一个周期的时间,由此可以得到频率。

外部中断测量频率PWM输入捕获可查看STM32F103不完全手册,在此不做介绍

外部中断测频的缺点十分明显:当频率过高时,MCU频繁进入中断,导致其测量精度下降。
可以如此理解:当第一个周期的方波来了,系统进入中断,此时刚准备开启定时器,第二个周期的方波也来了,此时就会错过该周期,直接到第三个周期,最后算出来的结果就是不正确的。
有人可能就说,既然算出来的是第一个周期到第三个周期,那你将算出来的T取一半不就行了??这就是本末倒置了,你事先并不知道会错过几个周期!!)。

本次重点说明外部计数器是如何实现的频率测量。依据测频原理:采用两个定时器,一个用作定时记为①,一个用作计数记为②。①用于频率相对较低的信号,进行精准定时,②用于频率相对较高的信号,用于得到定时期间测脉冲值。
测频原理
在被测信号上升沿来临时开始计时,同时开启计数(两者顺序无要求),下一次上升沿来临时先停止计数,再停止计时(此时的顺序必须先停止计数再停计时)。由此便可计算频率。

二、代码实现

1.定时器初始化

定时器使用的TIM2的外部脉冲计数

GPIO口初始化及定时器初始化:

void TIM2_Cap_Init(void)                                        //配置 TIM2_CH1_ETR 为外部脉冲计数
{    
    GPIO_InitTypeDef GPIO_InitStructure;
    TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);    
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);   
    GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_0;             
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD;           //PA0 输入  
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    GPIO_SetBits(GPIOA,GPIO_Pin_0);                       //PA0 下拉
    //初始化定时器2 TIM2   
    TIM_TimeBaseStructure.TIM_Period = 0xFFFF;              //设定计数器自动重装值 
    TIM_TimeBaseStructure.TIM_Prescaler =0;             //预分频器   
    TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //设置时钟分割:TDTS = Tck_tim
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  //TIM向上计数模式
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);         //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位
    TIM_ITRxExternalClockConfig(TIM2,TIM_TS_ETRF);          //配置外部触发,否则不会计数
    TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted, 0);
    TIM_SetCounter(TIM2, 0);        
    
    TIM_Cmd(TIM2,ENABLE );                                  //使能定时器
}

重点部分解释:TIM_ITRxExternalClockConfig(TIM2,TIM_TS_ETRF); //配置外部触发,否则不会计数介绍定时器
本次采用的3),此处若想继续了解,可联系我一起进行学习探讨,下图摘选的系统外部中断配置的解释。系统文件解释

2.计数器的配置

代码如下:

void TIM3_Int_Init(u16 arr,u16 psc)
{
    TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
	NVIC_InitTypeDef NVIC_InitStructure;

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

	TIM_TimeBaseStructure.TIM_Period = arr; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值	 计数到5000为500ms
	TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置用来作为TIMx时钟频率除数的预分频值  10Khz的计数频率  
	TIM_TimeBaseStructure.TIM_ClockDivision = 0; //设置时钟分割:TDTS = Tck_tim
	TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  //TIM向上计数模式
	TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位
 
	TIM_ITConfig(  //使能或者失能指定的TIM中断
		TIM3, //TIM2
		TIM_IT_Update ,
		ENABLE  //使能
		);
	NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;  //TIM3中断
	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_Cmd(TIM3, ENABLE);  //使能TIMx外设
							 
}

该处采用的定时器3进行配置。


3.中断服务函数

//定时器3中断服务程序	 
void TIM3_IRQHandler(void)
{ 
	
	if(TIM_GetITStatus(TIM3,TIM_IT_Update)!= RESET)      //检查TIM5更新中断发生与否
	{
		TIM2CH1_CAPTURE_VAL=TIM_GetCounter(TIM2); //读取单位时间内计数器计的CNT值
		TIM_Cmd(TIM2,DISABLE ); 	//失能定时器2
		TIM_Cmd(TIM3,DISABLE ); 	//失能定时器3
		TIM_SetCounter(TIM2, 0);
		flag++;//
		                  
		CNT+=TIM2CH1_CAPTURE_VAL+overflow*65535;
		overflow=0;
		//printf("%d \r\n",CNT);
	if(flag<200)
		{
		
		TIM_Cmd(TIM2,ENABLE ); 	//使能定时器2
		TIM_Cmd(TIM3,ENABLE ); 	//使能定时器3
		}
	else
		data_output();
	TIM_ClearITPendingBit(TIM3, TIM_IT_CC1|TIM_IT_Update); //清除中断标志位
	}
	 
}

需要继续说明的是:一定先关计数,再关计时,能够减小误差! 其中data_output函数是数据输出,我为了方便直接串口发送数据,若有其他需要可以在data_output函数中添加OLED或LCD显示。
CNT+=TIM2CH1_CAPTURE_VAL+overflow65535;* 进行解释一下:在一个计时周期内,最终的计数值=定时器内的计数值+溢出次数*65535。
在这里插入图片描述

由前面定时器的配置可以知道,每次定时为5ms,因此累加200次后的计数值,就是1s的计数值,那么此时的计数值就是所要测得的频率!

void TIM2_IRQHandler(void)
{ 
	
	if(TIM_GetITStatus(TIM2,TIM_IT_Update)!= RESET)      //检查TIM2更新中断发生与否
		overflow++;
		TIM_ClearITPendingBit(TIM2, TIM_IT_CC1|TIM_IT_Update); //清除中断标志位
	 
}

4.误差说明

void data_output()
{
	if(CNT<10000)
		printf("%d \r\n",CNT);
		else
		{
			cnt=CNT*(1+0.00019);
			printf("%d \r\n",cnt);
		}
		TIM_SetCounter(TIM2, 0);
		TIM_SetCounter(TIM3, 0);
		CNT=0;
		flag=0;
		TIM_Cmd(TIM2,ENABLE ); 	//使能定时器2
		TIM_Cmd(TIM3,ENABLE ); 	//使能定时器3
}

按照测量原理,在测量过程中理论误差在《电子测量原理》一书中有所阐述,也可联系我进行浅显讨论,本文仅针对实际结果进行简要分析。

测量过程中发现在高于50K开始出现明显偏差,将实际值和理论值进行列表分析后发现,两者之间存在一定的线性关系,因此在data_output()函数中进行了线性修正cnt=CNT*(1+0.00019)。

产生原因可能如下:①频率过高时系统频繁进入中断,导致1s累计的误差越来越多;②频率过高时,在开启定时和开启计数器之间,遗漏了部分周期,且频率越高遗漏越多,因此在低频时不易发现;③系统本身会有一定的计数误差,并且存在的±1误差在累计200次后,也会有一定影响。

三、测试数据及现象

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

四、总结

实验能够测量较高频率,实验结果能够接受,后期我会将工程文件上传至云盘,有何问题可联系我QQ:2745498610,希望大家能点关注,谢谢大家!

五、2023.7.23更新

鉴于大家的疑问我更新回复一下

有朋友在其中遇到问题和我进行探讨:

1、程序输出一直为0,其原因可能有①使用的管脚或芯片型号不一致,这个时候需要根据开发手册自己更改;②输入方波信号幅度低于1.8V时,此时单片机管脚会认为是低电平,此时就可认为一直是低电平输入,那输出自然就是0了,推荐输入信号高于2V

2、有人就说如果自己的信号就是很小该如何?有条件推荐在输入信号前加一个过零比较器;也可以换一种测频方式(外部触发),大家可以去比较一下各种测量方式的优劣

3、关于误差部分,大家可以发现,里面有一个修正子函数,其实当你的频率较高时就会出现一定的误差,很推荐大家去看一下《电子测量原理》这本书,
我简单再啰嗦一下:程序中定时1s计算里面的脉冲值,其中的1s又被分解了是5ms×200次,也就是说程序其实是计算5ms的脉冲值,然后连续计数200次,累计下的值输出。
就会出现一个问题:两次计数直接的开关定时器会不会影响计数值呢?前面程序也叙述到了,当我频率比较低时,两次开关定时器速度很快,几乎不会遗漏脉冲值,此时我可以认为测量值就是实际值了;当频率较高时,此时两次开关定时器的速度和脉冲速度相当了,此时便不能忽略漏掉的脉冲值。
这个问题解决方式就是对程序进行一个修正,每个频率段漏掉的脉冲值是近似一样的,因此通过比较测量值和标准信号值,建立里面的线性关系就可以了。

4、很希望大家不要直接拿着工程直接运行,最好自己思考一下原理,然后大家可以发现这个程序会存在一个问题,就是只能测量整数段频率,例如我想测量1.1Hz、或者110.5Hz的频率,或者0.2Hz频率,这个程序是无法完成的。大家可以在我测频的基础上继续提升一下,试着继续完善这个程序

5、大家还有不懂的,或者其他可以一起交流!Matlab、Python(信号处理部分和数据部分哈)。还有关于模拟信号(RF)、和高速数字信号处理(FPGA)都可以一起交流讨论!

  • 46
    点赞
  • 174
    收藏
    觉得还不错? 一键收藏
  • 36
    评论
下面是一个基于STM32F103的PWM频率测量的源码示例: #include "stm32f10x.h" #define TIM_PERIOD 0xFFFF uint16_t pwm_value = 0; uint32_t pwm_frequency = 0; void TIM2_IRQHandler() { if(TIM_GetITStatus(TIM2, TIM_IT_CC1) != RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_CC1); static uint16_t last_value = 0; uint16_t current_value = TIM_GetCapture1(TIM2); if(current_value > last_value) { pwm_value = current_value - last_value; } else { pwm_value = (TIM_PERIOD - last_value) + current_value; } last_value = current_value; pwm_frequency = SystemCoreClock / pwm_value; } } int main() { // 初始化GPIO和Timer2 GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_Init(GPIOA, &GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Prescaler = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period = TIM_PERIOD; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; 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_ICInit(TIM2, &TIM_ICInitStructure); TIM_ITConfig(TIM2, TIM_IT_CC1, ENABLE); TIM_Cmd(TIM2, ENABLE); NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); while(1) { // 循环等待中断处理 } }

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 36
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值