【嵌入式学习-STM32F103-TIM-定时中断和外部时钟】

定时器四部分讲解内容,本文是第一部分

1、定时器基本定时,定一个时间,然后让定时器每隔一段时间产生一个中断,来实现每隔一个固定时间执行一段程序的目的,比如要做一个时钟、秒表或者使用一些程序算法

2、定时器输出比较的功能,输出比较这个模块最常见的用途是产生PWM波形,用于驱动电机等设备,使用stm32的PWM波形来驱动舵机和直流电机的例子

3、定时器输入捕获的功能,学习使用输入捕获这个模块来测量方波频率的例子

4、定时器的编码器接口,使用这个编码器接口,能够更加方便地读取正交编码器的输出波形,在编码电机测速中,应用广泛

TIM简介

72M/65536/65536得到中断频率,取倒数就是59.65s
级联,一个定时器的输出,当作另外一个定时器的输入,最大时间达到59.65s * 65536 * 65536

在这里插入图片描述

高级定时器连接的是性能更高的APB2总线,通用定时器和基本定时器连接的是APB1总线,在RCC开启时钟时注意。

高级定时器具有重复计数器、死区生成、互补输出、刹车输入等功能主要是为了三相无刷电机的驱动设计的。

高级到低级向下兼容

C8T6只有1个高级定时器和3个通用定时器

在这里插入图片描述

基本定时器

时基单元:预分频器、计数器、自动重装载寄存器

在这里插入图片描述
预分频器之前,连接的就是基准计数时钟的输入,由于基本定时器只能选择内部时钟,所以可以认为这根线直接到输入端这里,也就是内部时钟CK_INT,内部时钟来源是RCC_TIMxCLK,频率值一般都是系统的主频72MHZ。
在这里插入图片描述
如果预分频器写1,那就是2分频,输出频率=输入频率/2=36MHZ
如果预分频器写2,那就是3分频,输出频率=输入频率/3=24MHZ

预分频器的值和实际的分频系数相差1,即实际分频系数=预分频器的值+1,这个预分频器的值是16位,所以最大值可以写65535,也就是65536分频。预分频器就是对输入的基准频率提前进行一个分频的操作。

计数时钟每来一个上升沿,计数器的值就加1,计数器是16位,所以里面的值可以从0一直加到65535.如果再加的话,计数器就会回到0重新开始。所以计数器的值在计时过程中会不断自增运行,当自增运行到目标值时,产生中断,那就完成了定时的任务。

自动重装载寄存器:存储目标值的寄存器。在运行的过程中,计数值不断自增,自动重装值是固定的目标,当计数值等于自动重装值时,也就是计时时间到了。那他会产生一个中断信号,并且清零计数器。计数器自动开始下一次的计数计时。

在这里插入图片描述

UI折线箭头代表这里会产生中断信号,像这种计数值等于自动重装值产生的中断,称为更新中断,这个更新中断之后,就会通往NVIC,我们再配置好NVIC的定时器通道,那么定时器的更新中断就能够得到CPU的响应。

U向下箭头表示更新事件,更新事件不会触发中断,但可以触发内部其他电路的工作。

主从触发

它能让内部的硬件在不受程序的控制下实现自动运行。主模式触发DAC有啥用?
在我们使用DAC时,可能会用DAC输出一段波形,那就需要每隔一段时间来触发一次DAC,让它输出下一个电压点。正常思路,先设置一个定时器产生中断,每隔一段时间在中断程序中调用代码手动触发一次DAC转换,然后DAC输出,缺点:这样会使主程序处于频繁被中断的状态,这回影响主程序的运行和其他中断的响应。

在这里插入图片描述
所以定时器就设计了一个主模式,使用主模式可以把这个定时器的更新事件映射到这个触发输出TRGO(trigger out)的位置,然后TRGO直接接到DAC的触发转换引脚上。这样,定时器的更新就不需要再通过触发中断来触发DAC转换。无需软件参与,实现硬件自动化

在这里插入图片描述

通用定时器

通用定时器和高级定时器支持3种计数模式,向上计数、向下计数和中央对齐这三种模式。

在这里插入图片描述
内外时钟源选择和主从触发模式结构

在这里插入图片描述

对于基本定时器而言,定时只能选择内部时钟,也就是系统频率72MHZ,
对于通用定时器而言,定时还可以选择外部时钟,第一个外部时钟来自TIMx_ETR引脚的外部时钟。

在这里插入图片描述
TIMx_ETR是一个定时器的输入端口,可以用来接收外部的时钟信号。在其引脚接上一个外部方波时钟。然后配置一下内部的极性选择、边沿检测和预分频器电路,再配置输入滤波电路,这些电路可以对外部时钟进行一定的整型,因为外部引脚时钟,难免有毛刺,这些电路可对输入的波形进行滤波。

在这里插入图片描述

滤波后的信号兵分两路
上面一路ETRF进入触发控制器,可以选择作为时基单元的时钟。如果要在ETR外部引脚提供时钟或者相对ETR时钟进行计数,把这个定时器当作计数器来用,可配置以下一路的电路,stm32中称为==“外部时钟模式2”==

在这里插入图片描述

除了ETRF可以提供时钟,TRGI还可以提供时钟,主要用作触发输入来使用的。
触发输入作为外部时钟来使用,称为“外部时钟模式1”
通过TRGI有5通路

在这里插入图片描述

TRGO可以通向其他定时器,而通向其他定时器的时候,就可以接到其他定时器的ITR引脚上,通过这一路就可以实现定时器级联

在这里插入图片描述

总结

外部时钟模式1的输入可以是ETR引脚,其他定时器,CH1引脚的边沿、CH1引脚和CH2引脚。

在这里插入图片描述

一般情况下,外部时钟通过ETR引脚即可
对于时钟输入而言,最常用的还是内部的72MHZ的时钟,如果要使用外部时钟,首选ETR引脚外部时钟模式2的输入。,这一路最简单最直接

在这里插入图片描述
定时器的编码器接口,可以读取正交编码器的输出波形

在这里插入图片描述
定时器的主模式输出,可以把内部的一些事件映射到这个TRGO引脚上,比如基本定时器分析中,将更新事件映射到TRGO,用于触发其他定时器、DAC或ADC。

在这里插入图片描述

右边:输出比较电路,总共有4个通道,可以用于输出PWM波形,驱动电机

左边:输入捕获电路,总共四个通道,可以用于测量输入方波的频率、占空比

中间寄存器:捕获/比较寄存器,是输入捕获和输出比较电路公用的,因为输入捕获和输出比较不能同时使用,因此寄存器机器引脚是公用的。

在这里插入图片描述

通用定时器与高级定时器的区别

在这里插入图片描述
第一个:申请中断的地方,增加了一个重复次数计数器,有了这个计数器之后,就可以实现每隔几个计数周期,才发生一次更新事件和更新中断。原来的结构是每个计数周期完成后都会发生更新。相当于对输出的更新信号又做了一次分频。

在这里插入图片描述
以下是高级定时器对输出比较模块的升级

DTG是死区生成电路,右边的输出引脚由原来的一个变为两个互补的输出,可以输出一对互补的PWM波。为了驱动三相无刷电机

刹车输入:给电机驱动提供安全保障,如果外部引脚BKIN产生了刹车信号,或者内部时钟失效,产生了故障,那么控制电路就会自动切断电机的输出,防止以外的发生。

在这里插入图片描述

定时中断基本结构图

在这里插入图片描述

定时中断和内外时钟源选择

运行控制:就是控制寄存器的一些位,比如启动停止、向上或向下计数等,操作这些寄存器,就能控制时基单元的运行。

左边:为时基单元提供时钟的部分.

第一个定时中断使用RCC的内部时钟

第二个定时器外部时钟就是用外部时钟模式2

右边:计时时间到,产生更新中断后信号的去向(如果高级定时器,还会有一个重复计数器)

在这里插入图片描述

中断信号会先在状态寄存器里置一个中断标志位,这个标志位会通过中断输出控制,到NVIC申请中断。中断输出控制就是一个中断输出的允许位,如果需要某个中断,就记得允许一下。

在这里插入图片描述

时序

预分频器时序

缓冲寄存器(影子寄存器)真正起作用的寄存器,比如我们在某一时刻,把预分频寄存器由0改为1,如果在此时立刻改变时钟的分频系数,

那么就会导致这里,在一个计数周期内,前半部分和后半部分的频率不一样。这里计数计到一半,计数频率突然改变,而缓冲寄存器,当计数计到一半的时候改变了分频值,这个变化并不会立刻生效,而是会等到本次计数周期结束时产生更新事件,预分频寄存器的值才会被传递到缓冲寄存器里面,才会生效。

在这里插入图片描述

因此,可以看到,即使在计数中途改变了预分频值,技术频率仍然会保持原来的频率,直到本轮计数完成,在下一轮计数时,改变后的分频值才会起作用。

在这里插入图片描述

计数器时序

内部时钟分频因子为2,就是分频系数为2,第一行是内部时钟72MHZ,第二行是时钟使能,高电平启动,第三行是计数器时钟,因为分频系数为2,所以这个频率是内部时钟除2,然后计数器在计数器时钟每个上升沿自增,当增加到0036的时候,发生溢出,那再来一个上升沿,计数器清零,计数器溢出,产生一个更新事件脉冲,另外还会置一个更新中断标志位UIF,该标志位置1,就回去申请中断,然后中断响应后,需要在中断程序中手动清零。

在这里插入图片描述
引入影子寄存器的目的实际上是为了同步,就是让值的变化和更新事件同步发生,防止在运行中途更改造成错误。

在这里插入图片描述
下图,在计数的中途,突然把F5改为36,影子寄存器是真正起作用的。它在自动加载寄存器改变后,还是F5,所以现在计数的目标还是计算到F5,产生更新事件,同时要更改的36才被传递到影子寄存器里,在下一个计数周期36才会有效。
在这里插入图片描述

RCC时钟树

RCC时钟树是stm32用来产生和配置时钟,并且把配置好的时钟发送到各个外设的系统,时钟是所有外设运行的基础,所以时钟是最先需要配置的东西。

在这里插入图片描述
在这里插入图片描述
中间:SYSCLK是系统时钟72MHZ。在时钟产生电路,有四个震荡源,分别是内部的8MHZ高速RC振荡器,外部的4-16MHZ高速石英晶体振荡器,即晶振,一般是8MHZ,外部的32.768KHZ低速晶振,一般是给RTC提供时钟的,最后是内部的40KHZ低速RC振荡器,这个可以给看门狗提供时钟。

高速晶振是用来提供系统时钟,如提供给AHB APB2 APB1总线

外部的石英振荡器比内部的RC振荡器更加稳定,因此一般用外部晶振,如果系统很简单,不需要精确的时钟,可选择内部时钟

ST配置流程

在SystemInit函数里,ST配置时钟,首先它会启动内部时钟,选择内部8MHZ为系统时钟,暂时以内部8MHZ的时钟运行,然后再启动外部时钟,进入PLL锁相环进行倍频,8MHZ倍频9倍,得到72MHZ,等到锁相环输出稳定后,选择锁相环输出为系统时钟,这样就把系统时钟由8MHZ切换到72MHZ

CSS:安全保障

在这里插入图片描述
无论高级、通用还是基本定时器,它们的内部基准时钟都是72MHZ
在这里插入图片描述
控制位“外部时钟使能”–>RCC_APB2/1PeriphClockCmd

在这里插入图片描述
打开时钟,就是在这个位置写1,让左边的时钟能够通过与门输出给外设。
在这里插入图片描述

代码部分

程序现象

1、使用定时中断的功能,定时器使用内部时钟定了1秒的时间,每隔1秒申请一下中断,然后在中断函数里执行Num++,最后在OLED上显示Num
在这里插入图片描述

2、定时器外部时钟,使用外部时钟驱动定时器,在定时器指定的外部引脚上,输入一个方波信号,来提供定时器计数的时钟,目前用对射式红外传感器来手动模拟一个外部时钟,用挡光片一次遮挡、移开、遮挡、移开提供一个方波。OLED上的CNT就是定时器中计数器的值,每遮挡移开一次,计数器加1,然后计数器计到9后自动清零。同时申请中断,执行Num++。
在这里插入图片描述

定时中断

接线图

在这里插入图片描述

步骤

第一步:RCC开启时钟,定时器的基准时钟和整个外设的工作时钟就都会同时打开

第二步: 选择时基单元的时钟源,对于定时中断,我们选择内部时钟源

第三步:配置时基单元,包括预分频器、自动重装器、计数模式等等

第四步:配置输出中断控制,允许更新中断输出到NVIC

第五步:配置NVIC,在NVIC中打开定时器中断的通道,并分配一个优先级

第六步:运行控制

第七步:使能计数器

最后:当计数器更新时,触发中断,写一个定时器中断函数

在这里插入图片描述

在这里插入图片描述
定时1s,即定时频率为1HZ

相当于1 = 72MHz/(PSC + 1) /(ARR + 1),PSC=7200,ARR=10000

	TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1;                        //ARR的值
	TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1;                     //PSC的值,对72M进行7200分频,得到10K的计数频率,在10K的频率下,计算10000个数,就是1s的时间。

在当前工程搜索中断源
在这里插入图片描述
启动文件找定时器2的中断服务函数

在这里插入图片描述

Timer.c

#include "stm32f10x.h"
#include "Timer.h"

//如果你想跨文件使用变量,可以在使用变量的那个文件的上面,用extern声明一下要用的变量
extern uint16_t Num;

//初始化定时器
void Timer_Init(void)
{
	//开启RCC内部时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
	
	//选择时基单元的时钟(定时器上电后默认就是使用内部时钟)
	TIM_InternalClockConfig(TIM2);
	
	//配置时基单元
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
	TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1;                        //ARR的值
	TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1;                     //PSC的值
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;             //重复计数器的值
	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
	
	//本质:在调用中断前,中断状态寄存器不能有标志位
	//避免刚一上电就立刻进入中断,在TimeBaseInit的后面和中断的前面(手动清除中断标志位)
	TIM_ClearFlag(TIM2, TIM_FLAG_Update);
	
	//使能更新中断,开启了更新中断到NVIC的通路
	TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);  //TIM_IT_Update Update更新中断
	
	//NVIC配置
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn ;  //定时器2在NVIC的通道
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	//最后NVIC_Init
	NVIC_Init(&NVIC_InitStructure);
	
	//启动定时器,此时定时器开始工作,当产生更新时,就会触发中断
	TIM_Cmd(TIM2, ENABLE);
}

//当定时器产生更新中断时,这个函数就会自动被执行
void TIM2_IRQHandler(void)
{
	//检查中断标志位,TIM_FLAG_Update为想看的中断标志位
	if(TIM_GetFlagStatus(TIM2, TIM_FLAG_Update)== SET)
	{
		//清除相应的标志位
		TIM_ClearITPendingBit(TIM2,TIM_FLAG_Update);
		Num++;
	}
	
}

Timer.h

#ifndef __TIMER_H
#define __TIMER_H

void Timer_Init(void);

#endif /*__TIMER_H*/

main.c

在main函数里,中断函数每秒自动把Num++,然后在主循环里显示。

预分频值和自动重装值对中断频率的影响

如果把自动重装值改为1000,那么由原来计10000个数变成了1000个数

在这里插入图片描述

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"
//定义全局变量Num,Num要在中断函数里执行++
uint16_t Num;

int main(void)
{
	OLED_Init();
	//初始化定时器,此时定时器开始工作
	Timer_Init();
	
	OLED_ShowChar(1, 1, 'A');
	OLED_ShowString(1, 3, "HelloWorld!");

	
	while (1)
	{
		OLED_ShowNum(1,5,Num,5);
	}
}

外部时钟

接线图

对射式红外传感器,DO数字输出接到PA0引脚,这个PA0引脚就是TIM2的ETR引脚,我们在这个引脚输入一个外部时钟
在这里插入图片描述

滤波器:以一个采样频率f采样N个点,如果N个点都一样,才会输出有效。
在这里插入图片描述
引脚需要用到GPIO,在配置时基单元之前还要先配置GPIO

在这里插入图片描述
上拉电阻防止跳动
基本任务还是定时中断,但我们不选用内部时钟,我们使用ETR外部时钟模式2
TIM_ETRClockModelConfig通过ETR引脚的外部时钟模式2配置

什么时候需要用到浮空输入呢?
如果外部的输入信号功率很小,内部的上拉电阻可能会影响到这个输入信号,这时就可以用浮空输入,放置影响外部输入的电平。

Timer.c

#include "stm32f10x.h"
#include "Timer.h"

//如果你想跨文件使用变量,可以在使用变量的那个文件的上面,用extern声明一下要用的变量
extern uint16_t Num;

//初始化定时器
void Timer_Init(void)
{
	//开启RCC内部时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
	
	/* 初始化GPIO*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStruct;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	
	GPIO_Init(GPIOA,&GPIO_InitStruct);
	//引脚要用到GPIO,因此在配置外部时钟模式2前要先配置GPIO
	//选择时基单元的时钟,不使用内部时钟源,通过ETR引脚的外部时钟模式2配置,不需要分频,不用滤波器
	TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted, 0X00);
	
	//配置时基单元
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
	TIM_TimeBaseInitStructure.TIM_Period = 10 - 1;                        //ARR的值
	TIM_TimeBaseInitStructure.TIM_Prescaler = 1 - 1;                     //PSC的值
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;             //重复计数器的值
	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
	
	//本质:在调用中断前,中断状态寄存器不能有标志位
	//避免刚一上电就立刻进入中断,在TimeBaseInit的后面和中断的前面(手动清除中断标志位)
	TIM_ClearFlag(TIM2, TIM_FLAG_Update);
	
	//使能更新中断,开启了更新中断到NVIC的通路
	TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);  //TIM_IT_Update Update更新中断
	
	//NVIC配置
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn ;  //定时器2在NVIC的通道
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	//最后NVIC_Init
	NVIC_Init(&NVIC_InitStructure);
	
	//启动定时器,此时定时器开始工作,当产生更新时,就会触发中断
	TIM_Cmd(TIM2, ENABLE);
}
//实时查看CNT的计数器的值
uint16_t Timer_GetCounter(void)
{
	return TIM_GetCounter(TIM2);
}


//当定时器产生更新中断时,这个函数就会自动被执行
void TIM2_IRQHandler(void)
{
	//检查中断标志位,TIM_FLAG_Update为想看的中断标志位
	if(TIM_GetFlagStatus(TIM2, TIM_FLAG_Update)== SET)
	{
		//清除相应的标志位
		TIM_ClearITPendingBit(TIM2,TIM_FLAG_Update);
		Num++;
	}
	
}

Timer.h

#ifndef __TIMER_H
#define __TIMER_H

void Timer_Init(void);
uint16_t Timer_GetCounter(void);  //获取计数值

#endif /*__TIMER_H*/

main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"

uint16_t Num;

int main(void)
{
	OLED_Init();
	//初始化定时器,此时定时器开始工作
	Timer_Init();
	
	OLED_ShowString(1, 1, "Num:");
	OLED_ShowString(2, 1, "CNT:");

	
	while (1)
	{
		OLED_ShowNum(1,5,Num,5);
		OLED_ShowNum(1,5,Timer_GetCounter(),5);
	}
}
  • 13
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值