嵌入式学习:STM32F103定时中断和外部时钟详解

TIM目录

  • 定时器四部分讲解内容,本文是第一部分
  • TIM简介
  • 基本定时器
  • 主从触发
  • 通用定时器
  • 总结
  • 通用定时器与高级定时器的区别
  • ==定时中断基本结构图==
  • 定时中断和内外时钟源选择
  • 时序
  • 预分频器时序
  • 计数器时序
  • RCC时钟树
  • ST配置流程
  • ==代码部分==
  • 程序现象
  • 定时中断
  • 接线图
  • 步骤
  • Timer.c
  • Timer.h
  • main.c
  • 外部时钟
  • 接线图
  • Timer.c
  • Timer.h
  • main.c
  • 定时器四部分讲解内容,本文是第一部分

    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);
    	}
    }
    
    物联沃分享整理
    物联沃-IOTWORD物联网 » 嵌入式学习:STM32F103定时中断和外部时钟详解

    发表评论