STM32学习笔记(11)——定时器初步应用

Posted Mount256

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了STM32学习笔记(11)——定时器初步应用相关的知识,希望对你有一定的参考价值。

前排提示一下:这些代码都是本人跟着野火的教程视频写的(与野火的例程会有出入),编程思路则由本人编写。

之前我们已经详细过了一遍高级定时器的功能框图,现在来简要说一下其他定时器。STM32 有三种定时器,定时器分类如下:

在这里插入图片描述

其中,高级定时器(定时、输出比较、输入捕获、互补输出) 的功能最齐全,其次是通用定时器(定时、输出比较、输入捕获),最后是基本定时器 (定时,只能向上计数)

一、基本定时器——LED循环亮灭

【实现功能】通过基本定时器(TIM6、TIM7),使 LED 每隔 1000ms 反转一次工作模式,即实现循环亮灭的效果。

【基本思路】要产生 1000ms 的时间间隔,首先要想办法产生 1ms 的时间。时钟源的频率为 72MHz,想要得到 1MHz 的定时器频率,就必须使分频因子 PSC = 72 - 1,这样定时器频率就为 72 / (PSC+1) = 1MHz,定时器周期为 1/ 1 0 6 10^6 106 秒(即1us),那么只要重复1000次这样的周期就是 1ms,所以 Period(ARR) = 1000 - 1。因此,1ms 时间后将会产生一次更新事件。

通过更新事件产生一次中断,进而运行中断服务函数,那么我们只需在函数体内记录触发的次数即可,触发次数达1000次即说明过去了 1000 * 1ms = 1000ms = 1s 的时间。因此我们定义一个全局变量uint16_t time,来记录触发次数,达到 1000 次后,LED反转,同时变量清零。

【编程要点】

  • 初始化时基结构体,并使能定时器;
  • 初始化定时器的中断标志位,开启定时器中断
  • 配置中断优先级NVIC;
  • 编写中断服务函数;
  • 编写主函数。

以下为部分程序(使用基本定时器TIM6):

1. basic_tim.h

#ifndef  __BASIC_TIM_H
#define  __BASIC_TIM_H

#include "stm32f10x.h"
#include "stm32f10x_tim.h"
#include "misc.h"

/****** 基本定时器TIM6、7 ******/
#define BASIC_TIMx				TIM6
#define BASIC_TIMx_CLK			RCC_APB1Periph_TIM6
#define BASIC_TIM_Period    	(1000 - 1)  //自动重装溢出值,必须减1,因为在内部计算时会自动加1
#define BASIC_TIM_Prescaler  	(72 - 1) 	//预分频值,必须减1,因为在内部计算时会自动加1
#define BASIC_TIMx_IRQn  		TIM6_IRQn
#define BASIC_TIMx_IRQHandler   TIM6_IRQHandler

void BASIC_TIM_Config(void);
void BASIC_TIM_NVIC_Config(void);

#endif	 /* __BASIC_TIM_H */

2. basic_tim.c

#include "basic_tim.h"

void BASIC_TIM_Config(void)
{
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
	
	/* 开启APB1时钟 */
	RCC_APB1PeriphClockCmd(BASIC_TIMx_CLK, ENABLE);
	/* 设置预分频 */
	TIM_TimeBaseInitStructure.TIM_Prescaler = BASIC_TIM_Prescaler;
	/* 设置计数模式 */
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
	/* 设置自动重装溢出值 */
	TIM_TimeBaseInitStructure.TIM_Period = BASIC_TIM_Period;
	/* 设置时钟分频因子 */
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
	/* 设置重复计数值 */
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
	
	/* 初始化基本定时器 */
	TIM_TimeBaseInit(BASIC_TIMx, &TIM_TimeBaseInitStructure);
	/* 清除中断标志位 */
	TIM_ClearFlag(BASIC_TIMx, TIM_FLAG_Update);
	/* 开启中断 */
	TIM_ITConfig(BASIC_TIMx, TIM_IT_Update, ENABLE);
	/* 使能定时器 */
	TIM_Cmd(BASIC_TIMx, ENABLE);
}

void BASIC_TIM_NVIC_Config(void)
{
	NVIC_InitTypeDef NVIC_InitStructure;
	
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);
	NVIC_InitStructure.NVIC_IRQChannel = BASIC_TIMx_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStructure);
}

3. stm32f10x_it.c

#include "stm32f10x_it.h" 
#include "basic_tim.h"

extern volatile uint16_t time;

/* 用户添加了基本定时器中断服务函数 */
void BASIC_TIMx_IRQHandler(void)
{
	if(TIM_GetITStatus(BASIC_TIMx, TIM_IT_Update) != RESET) // 如果产生了一次更新中断事件
	{
		time++; // 已过去一次定时器周期(1ms),time加1
		TIM_ClearITPendingBit(BASIC_TIMx, TIM_IT_Update); // 清除更新事件的中断标志位,为下一次中断做准备
	}
}

4. main.c

#include "stm32f10x.h"
#include "led.h"
#include "basic_tim.h"

volatile uint16_t time = 0;

int main(void)
{	
	LED_Init();
	BASIC_TIM_NVIC_Config();
	BASIC_TIM_Config();
	
	while(1)
	{
		if(time == 1000) // 满1000次定时器周期,说明过去了1s时间,LED反转
		{
			time = 0; // 为下一次定时器周期计数做准备
			LED0 = !LED0;
		}
	}
}

二、通用定时器——测量脉宽

【实现功能】用户按下按键并松手,通用定时器(TIM2、TIM3、TIM4、TIM5)会测量这段按下去的时间是多少,并通过串口通信将测量结果显示在上位机屏幕上。

【基本思路】通过查看电路原理图(下图)可知,按键WK_UP正好与端口 PA0 中的TIM5_CH1相连,因此我们使用通用定时器 TIM5,并使能 PA0 这个端口。我们实现了这样一个功能:只要按下按键WK_UP,TIM5的通道1即能捕获输入信号。

在这里插入图片描述

按下按键,产生一段高电平;松开按键,产生低电平。这样连续的的按下又松开,就产生了方波信号。因此,我们要捕获的信号实际上是一段方波信号,我们要测量的脉宽实际上是一段高电平信号的延续时间,理解这一点很重要。

如何测量这样一段高电平信号的时间呢?我们先用朴素的语言理一下基本的思路:

  1. 先等待上升沿到来,一旦上升沿到来,意味着高电平信号来了,定时器就会立刻去捕获,这样就产生了一次边沿触发中断事件,此时我们可以在中断服务函数中命令定时器开始计数了。
  2. 由于我们按下按键的时长一般会比计数器的溢出时间要长,因此计数器会发生溢出,这样就产生了一次更新中断事件。需要注意的是,一次按下可能会产生不止一次溢出,即不止产生一次更新中断事件。
  3. 这时若松开按键,高电平瞬变为低电平,定时器就会触发下降沿中断事件,计数器此时可能未发生溢出,这时取出计数器的值 CCR 的值,通过计算,这样一次脉宽测量便结束了。

溢出一次所需的时间应设置多长呢?时钟源的频率为 72MHz,我们一般想要得到 1MHz 的定时器频率,就必须使分频因子 PSC = 72 - 1,这样定时器频率就为 72 / (PSC+1) = 1MHz,定时器周期为 1/ 1 0 6 10^6 106 秒(1us)。由于我们是测量脉宽的,所以计数器的重装载值 Period 直接设置成最大值即可,因为 Period(寄存器ARR)的最大值为 65535,所以我们设置 ARR = 65535,即65535 * 1 / 1 0 6 10^6 106 s 时间后将会产生一次更新事件。

我们用一张图来重新说明这里的原理:

在这里插入图片描述

如图所示,1处表示捕获到了上升沿,产生了边沿触发中断事件,2、3、4、5是更新中断事件,6处是捕获到了下降沿,一次测量结束。需要注意,1 ~ 2、2 ~ 3、3 ~ 4、4 ~ 5是总共4次从0到溢出的计数过程,所间隔的时间都是 65535 * 1 / 1 0 6 10^6 106 s。5~6是从0开始计数,还未溢出就捕获了下降沿,这时计数器计数到了 a (a < 65535,寄存器CCR的值为a)。所以,对于这张图,最终的脉宽时长为 (4 * 65535 * 1 / 1 0 6 10^6 106) + (a * 1 / 1 0 6 10^6 106) = (4 * 65535 + a) * (1 / 1 0 6 10^6 106) s

结论:设溢出的次数为 x,最后一次计数器的值(CCR)为 a,则脉宽的测量公式为:(x * 65535 + a) * (1 / 1 0 6 10^6 106) s,即(x * 65535 + a) / (72MHz/(PSC+1)) s 但在实际编程中,/运算的结果只是整数部分,因此可用%运算算出小数部分。

【编程要点】

  • 使能GPIO:PA0;
  • 初始化时基单元和输入捕获通道;
  • 配置NVIC中断;
  • 初始化定时器的中断标志位,开启定时器中断(共两个:更新事件和捕获事件)
  • 编写中断服务函数的第一个内容:更新事件产生中断,溢出次数 x 加 1(即记录过去了多少次定时器周期);
  • 编写中断服务函数的第二个内容:边沿触发产生中断,捕获到上升沿时开始计数,此时要将捕获方式改为下降沿捕获;捕获到下降沿时记录 CCR 的值,此时要将捕获方式改为上升沿捕获,为下一次捕获做准备;
  • 编写主函数,用上面的公式计算测量结果,通过 USART 输出测量结果(记得在魔法棒里勾选使用 MicroLib)。

以下为部分代码,添加了本人注释。注意,为了记录上升沿、下降沿是否已经捕获过,以及CCR的值和溢出次数,我们自定义了一个结构体(其实也可以不弄结构体,定义成结构体的好处是可以使得相关变量的联系更加紧密):

1. general_tim.h

#ifndef __GENERAL_TIM_H
#define __GENERAL_TIM_H

#include "stm32f10x.h"

#define GENERAL_TIM  				TIM5
#define GENERAL_TIM_CLK             RCC_APB1Periph_TIM5
#define GENERAL_TIM_PERIOD			0xFFFF  // = 65535
#define GENERAL_TIM_PSC				(72 - 1)
#define GENERAL_TIM_RECNT			0

#define GENERAL_TIM_CH1_PORT_CLK	RCC_APB2Periph_GPIOA
#define GENERAL_TIM_CH1_PORT		GPIOA
#define GENERAL_TIM_CH1_PIN			GPIO_Pin_0
#define GENERAL_TIM_CHANNEL_x		TIM_Channel_1

#define GENERAL_TIM_IRQn			TIM5_IRQn

void GENERAL_TIM_Init(void);

// 定时器输入捕获用户自定义变量结构体声明
typedef struct
{   
	uint8_t   Capture_FinishFlag;   // 捕获结束标志位
	uint8_t   Capture_StartFlag;    // 捕获开始标志位
	uint16_t  Capture_CcrValue;     // 捕获寄存器的值
	uint16_t  Capture_Period;       // 自动重装载寄存器更新标志(溢出次数) 
}TIM_ICUserValueTypeDef;

extern TIM_ICUserValueTypeDef TIM_ICUserValueStructure;

#endif /* __GENERAL_TIM_H */

2. general_tim.c

#include "general_tim.h"

TIM_ICUserValueTypeDef TIM_ICUserValueStructure = {0,0,0,0};

/* GPIO初始化,WK_UP 和 TIM5_CH1 对应的是PA0 */
static void GENERAL_TIM_GPIO_Config(void)
{
	GPIO_InitTypeDef GPIO_InitStructure;
	
	RCC_APB2PeriphClockCmd(GENERAL_TIM_CH1_PORT_CLK, ENABLE);
	GPIO_InitStructure.GPIO_Mode 	= GPIO_Mode_IN_FLOATING;
	GPIO_InitStructure.GPIO_Speed 	= GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Pin 	= GENERAL_TIM_CH1_PIN;
	GPIO_Init(GENERAL_TIM_CH1_PORT, &GPIO_InitStructure);
}

/* 时基单元和输入捕获通道初始化 */
static void GENERAL_TIM_MODE_Config(void)
{
	TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
	TIM_ICInitTypeDef		TIM_ICInitStructure;
	
	RCC_APB1PeriphClockCmd(GENERAL_TIM_CLK, ENABLE);
	
	/* 时基单元结构体初始化 */
	TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
	TIM_TimeBaseStructure.TIM_CounterMode 	= TIM_CounterMode_Up;
	TIM_TimeBaseStructure.TIM_Period 		= GENERAL_TIM_PERIOD;
	TIM_TimeBaseStructure.TIM_Prescaler 	= GENERAL_TIM_PSC;
	TIM_TimeBaseStructure.TIM_RepetitionCounter = 0;
	TIM_TimeBaseInit(GENERAL_TIM, &TIM_TimeBaseStructure);
	
	/* 输入捕获通道结构体初始化 */
	TIM_ICInitStructure.TIM_Channel 	= GENERAL_TIM_CHANNEL_x;
	TIM_ICInitStructure.TIM_ICFilter 	= 0; // 滤波系数,本次实验用不到
	TIM_ICInitStructure.TIM_ICPolarity 	= TIM_ICPolarity_Rising;
	TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
	TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
	TIM_ICInit(GENERAL_TIM, &TIM_ICInitStructure);
	
	//清除更新和捕获中断标志位
	TIM_ClearFlag(GENERAL_TIM, TIM_FLAG_Update | TIM_FLAG_CC1);	
	//开启更新和捕获中断  
	TIM_ITConfig(GENERAL_TIM, TIM_IT_Update | TIM_IT_CC1, ENABLE);
	//使能计数器
	TIM_Cmd(GENERAL_TIM, ENABLE);
}

/* NVIC中断初始化 */
static void GENERAL_TIM_NVIC_Config(void)
{
	NVIC_InitTypeDef NVIC_InitStructure;
	
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);
	NVIC_InitStructure.NVIC_IRQChannel 						= GENERAL_TIM_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 	= 0;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority 			= 3;
	NVIC_InitStructure.NVIC_IRQChannelCmd 					= ENABLE;
	NVIC_Init(&NVIC_InitStructure);
}

void GENERAL_TIM_Init(void)
{
	GENERAL_TIM_GPIO_Config();
	GENERAL_TIM_MODE_Config();
	GENERAL_TIM_NVIC_Config();
}

3. stm32f10x_it.c

#include "stm32f10x_it.h" 
#include "general_tim.h"


// 原理:捕获了边沿信号或产生更新事件即会触发中断,跳转至该中断服务函数
// 至于边沿触发是上升沿触发中断还是下降沿触发中断要看用户的配置
// 按键按下去的时候,触发上升沿捕获中断,同时开始计数,并且将触发中断模式设置为下降沿触发
// 每一次计数产生溢出,溢出次数加1
// 在按键松开的时候,即会触发下降沿捕获中断,此时一次脉冲测量完成,并且将触发中断模式重新设置为上升沿触发
void TIM5_IRQHandler(void)
{
	/******* 1. 判断是否为更新事件触发了中断 *******/
	// 如果被捕获的输入信号时间大于定时器的最大计数范围(65535)时,定时器就会溢出,产生更新中断,说明已经过去了一个定时器周期
	// 一般来说,我们按下按键的时长相对于单片机来说比较长,因此肯定会出现溢出
	// 每次溢出使得判断为真,下面的代码就会运行一次
	if ( TIM_GetITStatus(GENERAL_TIM, TIM_IT_Update) != RESET )               
	{	
		TIM_ICUserValueStructure.Capture_Period++; // 溢出次数(周期)加1	
		TIM_ClearITPendingBit(GENERAL_TIM, TIM_IT_Update); // 清除更新中断标志 		
	}
	
	/******* 2. 判断是上升沿还是下降沿捕获触发了中断 *******/
	// 用户每一次按键按下又松开的期间,下面的代码只会运行两次,自己想想这是为什么(其实上面已经解释得很清楚了)
	if( TIM_GetITStatus(GENERAL_TIM, TIM_IT_CC1) != RESET ) 
	{
		/* 如果发生第一次捕获(即捕获上升沿) */
		if( TIM_ICUserValueStructure.Capture_StartFlag == 0 )
		{
			TIM_SetCounter(GENERAL_TIM, 0); // 开始计数
			TIM_ICUserValueStructure.Capture_Period = 0; // 初始化记录周期的值
			TIM_ICUserValueStructure.Capture_CcrValue = 0; // 初始化记录捕获寄存器的值
			// 配置发生中断的模式为下降沿触发,那么下一次发生中断时即为捕获了下降沿,为待会儿的下降沿捕获做准备
			TIM_OC1PolarityConfig(GENERAL_TIM, TIM_ICPolarity_Falling); 
			TIM_ICUserValueStructure.Capture_StartFlag = 1; // 标记上升沿已经触发过了
			TIM_ICUserValueStructure.Capture_FinishFlag = 0;
		}
		/* 否则即为第二次捕获(即捕获下降沿) */
		else
		{
			TIM_ICUserValueStructure.Capture_CcrValue = TIM_GetCapture1(GENERAL_TIM); // 记录此时捕获寄存器CCR的值
			// 配置发生中断的模式为上升沿触发,那么下一次发生中断时即为捕获了上升沿,为下一次上升沿捕获做准备
			TIM_OC1PolarityConfig(GENERAL_TIM, TIM_ICPolarity_Rising); 
			TIM_ICUserValueStructure.Capture_StartFlag = 0; // 重置标志位,为下一次上升沿捕获做准备
			TIM_ICUserValueStructure.Capture_FinishFlag = 1;
		}
		TIM_ClearITPendingBit(GENERAL_TIM, TIM_IT_CC1); // 清除捕获中断标志
	}
}

4. main.c

#include "stm32f10x.h"
#include "usart.h"
#include "general_tim.h"

int main(void)
{	
	uint32_t time;
	uint32_t TIM_PscCLK = 72000000 / (GENERAL_TIM_PSC+1);
	
	USART_Config();
	GENERAL_TIM_Init();
	
	printf("开始测试!来!\\n");
	
	while(1)
	{
		if(TIM_ICUserValueStructure.Capture_FinishFlag == 1)
		{
			// 高电平时间的计数器的值 = 定时器周期 * 65535 + 多出来的(不够一个定时器周期的)时间(即捕获寄存器的值)
			// 单位为 us,因此需要除于一个分频因子得到单位为 s 的结果
			time = TIM_ICUserValueStructure.Capture_Period * (GENERAL_TIM_PERIOD+1) + 
			       (TIM_ICUserValueStructure.Capture_CcrValue+1);
			
			// 打印高电平脉宽时间
			printf ( "\\r\\n测得高电平脉宽时间:%d.%d s\\r\\n", time / TIM_PscCLK, time % TIM_PscCLK );
			
			TIM_ICUserValueStructure.Capture_FinishFlag = 0;			
		}		
	}
 }

以上是关于STM32学习笔记(11)——定时器初步应用的主要内容,如果未能解决你的问题,请参考以下文章

STM32学习笔记(14)——ADC初步应用

STM32学习及应用笔记一:SysTick定时器学习及应用

STM32学习笔记

硬件——STM32 , 软件框架

STM32学习笔记——通用定时器计数延时

STM32学习笔记(10)——高级定时器TIM