STM32系统时钟简介

系统定时器

SysTick系统定时器属于CM3的内核外设,而不是片上外设,即只要是使用CM3核的芯片都有这个功能。有关寄存器的定义和部分库函数都在core_CM3.h中定义和实现。
定时器是用来计时的,与传统的软件模拟(while或for实现)计时相比,定时器在计时精度上有着明显的优势,并且还不占用CPU的资源,可以让CPU去处理别的事情。定时的原理:向重装载数值寄存器中写入需要定时的数值,然后配置系统定时器的控制及状态寄存器,开始倒数计数。当当前数值寄存器中的值减到0时,会根据控制及状态寄存器的配置来决定是否产生中断、以及把控制及状态寄存器中的状态位置1(M3的状态标志位是第16bit,不同的核状态标志位可能不一样)。要是使能了中断,则CPU执行相应的中断服务函数响应中断。而同时计数器把重装载数值寄存器中的值重新装载到当前数值寄存器中,然后开始下一轮的计数,以此循环往复。
从上面可以知道,定时器其实就是做减法计数操作。那么多长时间减一次(时钟)、计数到0的时候是否要产生中断、以及是否使能定时器?这些问题咱们可以配置相应的寄存器来实现。

  • STK_CTRL:第16bit指示定时器中的数值是否减到0,要是到0则置1(读出1后会自动清零),bit2用来选择定时器的时钟,bit1用来配置是否产生中断。bit0则是开启定时器的使能开关
  • 在这里插入图片描述
  • STK_LOAD,24bit的重装载数值寄存器。从这里可以知道系统定时器的最大计数值是224
  • 在这里插入图片描述
  • STK_VAL,当前数值寄存器,读取这个寄存器既可获取到当前计数的值。
  • 在这里插入图片描述
    所以咱们只需要使用上述的寄存器就可以使用系统定时器。系统定时器一般用于操作系统,用于产生时基,维持系统的心跳。由于系统定时器使用的是处理器的时钟,所以处理器进入低功耗模式后,系统定时器也会停止工作。

而对于STM32的编程则更倾向使用库函数来配置系统定时器。系统定时器配置库函数为SysTick_Config(),其中参数是重装载数值寄存器的值。默认使用系统时钟,要是有需要可以自己修改函数中的配置。源码如下

/* ##################################    SysTick function  ############################################ */

#if (!defined (__Vendor_SysTickConfig)) || (__Vendor_SysTickConfig == 0)

/**
 * @brief  Initialize and start the SysTick counter and its interrupt.
 *
 * @param   ticks   number of ticks between two interrupts
 * @return  1 = failed, 0 = successful
 *
 * Initialise the system tick timer and its interrupt and start the
 * system tick timer / counter in free running mode to generate 
 * periodical interrupts.
 */
static __INLINE uint32_t SysTick_Config(uint32_t ticks)
{ 
  if (ticks > SysTick_LOAD_RELOAD_Msk)  return (1);            /* Reload value impossible */
                                                               
  SysTick->LOAD  = (ticks & SysTick_LOAD_RELOAD_Msk) - 1;      /* set reload register */
  NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1);  /* set Priority for Cortex-M0 System Interrupts */
  SysTick->VAL   = 0;                                          /* Load the SysTick Counter Value */
  SysTick->CTRL  = SysTick_CTRL_CLKSOURCE_Msk | 
                   SysTick_CTRL_TICKINT_Msk   | 
                   SysTick_CTRL_ENABLE_Msk;                    /* Enable SysTick IRQ and SysTick Timer */
  return (0);                                                  /* Function successful */
}

#endif

其中NVIC_SetPriority ()设置系统定时器中断的优先级。通过对IRQn的判断用来区分是系统异常还是外部中断。其中系统异常的优先级由内核外设SCB的寄存器SHPRx控制,而外部中断的优先级由内核外设NVIC中的IPx寄存器控制。

static __INLINE void NVIC_SetPriority(IRQn_Type IRQn, uint32_t priority)
{
  if(IRQn < 0) {
    SCB->SHP[((uint32_t)(IRQn) & 0xF)-4] = ((priority << (8 - __NVIC_PRIO_BITS)) & 0xff); } /* set Priority for Cortex-M3 System Interrupts */
  else {
    NVIC->IP[(uint32_t)(IRQn)] = ((priority << (8 - __NVIC_PRIO_BITS)) & 0xff);    }        /* set Priority for device specific Interrupts  */
}

这里讲到内核外设优先级和之前讲述的外设中断优先级很相似,优先级的范围是0~15,数值越低优先级越高。要是同时设置了内核中断优先级以及外部中断优先级,则优先级的比较是:把外部中断优先级转换为4bit的二进制数,然后和内核中断比较。

这里讲解下如何使用定时器实现自己想要的时间。假如定时器使用的时钟是CLKAHB,那么一次计数操作的时间为T=1/CLKAHB,所以计数器的时间为t = VALUELOAD*T= VALUELOAD/CLKAHB。VALUELOAD = CLKAHB *t,在库中定义了SystemCoreClock为72M,所以可以使用这个宏很方便的来算出响应时间的重装载数值寄存器中的值。

配置好了系统定时器,接下来就是需要把中断服务函数的函数体实现。在启动文件中已近声明为 void SysTick_Handler(void),在这个函数里面实现自己想要的功能。

要是只是想用定时器来实现简单的来计时,比如实现延时功能。那么可以用简单的方法来实现。一直轮询控制及状态寄存器的16bit的值,值为1代表此次计数结束。读取1后会自动清为0。代码就是while ( !((SysTick->CTRL)&(1<<16)) );

所以系统定时器的编程还是很简单:

  • 调用SysTick_Config()函数,配置中断优先级、重装载寄存器的值以及配置控制与状态寄存器的值、清除当前数值寄存器的值。
  • 接着就是实现中断服务函数SysTick_Handler(),在这里实现自己想要的功能。

外设定时器

在STM32F1系列中,除了互联型的产品,共有8个定时器,分为基本定时器、通用定时器、高级定时器。TIM6和TIM7是一个16位且只能向上计数的定时器,只能定时没有外部IO。通用定时器TIM2/3/4/5是一个16位且可以向上/下计数的定时器,可以定时也可用于输出比较。高级定时器TIM1/8是一个16位且可以向上/下计数的定时器,可以定时也可用于输出比较和输入捕获。
无论是何种定时器,其最基本的功能都是定时。要定时必须要有时基,时基有自动重装载寄存器、计数器、计数器时钟组成。计数器中数值计数的频率由计数器时钟决定(一个时钟计数一次),计数到多少或从多少开始计数则由重装载寄存器中的值决定的,而计数器中的值达到了要求后会产生中断同时开始下一轮的计数

基本定时器TIM6/7

基本定时器和系统定时器很类似,但在计数方式和系统定时器有些差别,系统定时器是向下计数而通用定时器是向上计数。TIM6/TIM7可以作为通用定时器提供时间基准外,特别地可以为数模转换器提供时钟。实际上,它们在芯片内部直接连接到DAC并通过触发输出直接驱动DAC。具有特性:
● 16位自动重装载累加计数器
● 16位可编程(可实时修改)预分频器,用于对输入的时钟按系数为1~65536之间的任意数值分频
● 触发DAC的同步电路
● 在更新事件(计数器溢出)时产生中断/DMA请求
在这里插入图片描述

  • 时钟源TIMxCLK,即内部时钟CK_INT,是经APB1预分频器后分频提供。APB1预分频系数为1,则使用APB1时钟。否则翻倍使用,库函数中对APB1预分频的系统是2,因此调用库函数时钟源是72M
  • 计数器时钟分频器,定时器时钟源经过PSC预分频器之后,得到CK_INT用来驱动计数器计数。PSC是一个16位的预分频器,对时钟TIMxCLK进行1~65536之间的任何一个数进行分频。
  • 计数器CNT:计数器CNT是一个16位的计数器,只能往上计数,最大数值为65535。当计数达到自动重装载寄存器的时候产生更新事件,并清零从头开始计数。
  • 自动重装载寄存器,自动重装载寄存器是一个16位的寄存器。计数器计数到这个值的时候,产生中断,计数器从0开始继续计数。
    结构框图就如上面这么简单,下面对框图中的细节描述下,要是只想使用基本定时器的功能,可以不看这些细节
    自动重装载寄存器和预分频寄存器都有影子寄存器,每次都写实际上是通过读写预加载寄存器并没有直接操作影子寄存器。在计数器产生事件更新时则会把预加载寄存器中的值传送到相应的影子寄存器中。其中预分频寄存器是否使用影子寄存器由TIMx_CR1寄存器中的ARPE字段决定,而计数器时钟预分频器一直使用影子寄存器,即写入预分频中的值要在紧接着的下一个更新事件触发时才会生效。
    ARPE = 0时(不使用影子寄存器,即TIMx_ARR寄存器没有缓冲,写入的值立即生效)
    在这里插入图片描述
    ARPE = 1时(TIMx_ARR寄存器有缓冲,写入自动重装载寄存器中的值在下一个更新事件时才会开始使用)
    在这里插入图片描述

时钟源

只有在CEN位(TIMx_CR1寄存器中)置1后内部时钟才会向预分频器提供时钟
在这里插入图片描述

预分频器

下图是预分频系数从1变到2的时序图,预分频器是通过一个16位寄存器的计数来实现分频的(即数值达到预分频器中的值时产生一个脉冲)。预分频器具有缓冲,可以在运行过程中改变它的值,新的预分频值将在下一个更新事件时起作用。
在这里插入图片描述
更新事件可以通过计数器溢出产生,也可以通过软件设置TIMx_EGR寄存器的UG位来实现(软件设置,硬件自动清除)。当TIMx_CR1寄存器的URS位为’0’,则每当计数器达到溢出值时,硬件发出更新事件,也可以通过软件触发更新事件;而置为“1”时,则更新事件只能由计数器计数溢出硬件触发。如果设置了UDIS位则会禁止产生更新事件,这样不会产生更新事件,所以分频系数也是不会变的,但是计数器能正常工作,计数器和预分频器依然会在应产生更新事件时重新从0开始计数(但预分频系数不变)。如果此时TIMx_CR1寄存器中的URS(选择更新请求),设置UG位可以产生一次更新事件UEV,但不设置UIF标志。
note:设置TIMx_EGR寄存器的UG位时重新初始化定时器的计数器并产生对寄存器的更新。注意:预分频器也被清除(但预分频系数不变)

计数模式

即使在计数器运行时,软件也可以读写计数器、自动重装载寄存器和预分频寄存器。自动重装载寄存器是预加载的, 每次读写自动重装载寄存器时,实际上是通过读写预加载寄存器实现。可以配置自动重装载预加载使能位,来实现写入预加载寄存器的内容是立即还是在每次事件更新时传送到它的影子寄存器。在计数器溢出时,硬件发出更新事件
假如要修改定时器的时间,则需要修改自动重装载寄存器的值。修改自动重装载的值后也是在更新事件产生时更新自动重装载影子寄存器(前提是ARPE使能)。如下图:
在这里插入图片描述

基本定时器编程

上述就是通用定时器的工作原理。在库函数中定义如下结构体:

/** 
  * @brief  TIM Time Base Init structure definition
  * @note   This structure is used with all TIMx except for TIM6 and TIM7.    
  */

typedef struct
{
  uint16_t TIM_Prescaler;         /*!< Specifies the prescaler value used to divide the TIM clock.
                                       This parameter can be a number between 0x0000 and 0xFFFF */

  uint16_t TIM_CounterMode;       /*!< Specifies the counter mode.
                                       This parameter can be a value of @ref TIM_Counter_Mode */

  uint16_t TIM_Period;            /*!< Specifies the period value to be loaded into the active
                                       Auto-Reload Register at the next update event.
                                       This parameter must be a number between 0x0000 and 0xFFFF.  */ 

  uint16_t TIM_ClockDivision;     /*!< Specifies the clock division.
                                      This parameter can be a value of @ref TIM_Clock_Division_CKD */

  uint8_t TIM_RepetitionCounter;  /*!< Specifies the repetition counter value. Each time the RCR downcounter
                                       reaches zero, an update event is generated and counting restarts
                                       from the RCR value (N).
                                       This means in PWM mode that (N+1) corresponds to:
                                          - the number of PWM periods in edge-aligned mode
                                          - the number of half PWM period in center-aligned mode
                                       This parameter must be a number between 0x00 and 0xFF. 
                                       @note This parameter is valid only for TIM1 and TIM8. */
} TIM_TimeBaseInitTypeDef;    

其中通用寄存器能使用上的是TIM_Prescaler和TIM_Period,即定时器预分频器设置系数和自动重装载的值。

在对基本定时器的编程流程为:

  • 开启定时器的时钟,没有时钟一些都是免谈。基本定时器TIM6/7都是挂载在APB1总线上。
  • 初始化时基结构体TIM_TimeBaseInitTypeDef,调用函数TIM_TimeBaseInit()进行初始化。
  • 使能基本定时器的中断,调用TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState)。基本定时器 TIM6 and TIM7 can only generate an update interrupt.
  • 开启定时器void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState)
  • 要是使能中断则需要实现相应中断服务函数和NVIC的配置。如TIM6_IRQHandler()和TIM7_IRQHandler。
    由于外部中断很多时候共用同一个中断通道,所以在中断服务函数中要区分是什么中断类型在基本定时器中可以通过API: ITStatus TIM_GetITStatus(TIM_TypeDef* TIMx, uint16_t TIM_IT)来获取是什么中断触发了。而通过调用void TIM_ClearITPendingBit(TIM_TypeDef* TIMx, uint16_t TIM_IT);来清除已经处理后的中断。
    库函数的具体实现这里就不贴出来了,对于想了解的同学,可以自己去库文件里面查阅源码。
发布了35 篇原创文章 · 获赞 1 · 访问量 1870

猜你喜欢

转载自blog.csdn.net/lzj_linux188/article/details/103762038