目录
一、定时器的概述
在没有定时器的时候,我们想要延时往往都是写一个Delay函数,里面执行许多的空操作得到的。但是这样的方式不能保证定时的准确性和扩展性,同时在等待期间CPU是无法执行其他函数的。换句话说CPU在死等待。
而定时功能又是一个非常重要的工具,我们在使用各种外设的时候,往往需要定时器来进行时序确认、来其他操作的同步性等等。STM32也必然包含了这种功能。
定时器原理:
使用晶振分频后的时钟频率进行计数,通过硬件的方式,不断让计数器++,当计数器的值满足你预期的时候,说明时钟定时结束。此时计数器会对外触发一个中断信号。
由于晶振的频率十分精准,所以这样定时往往精度较高,且不依赖CPU可以在定时期间释放CPU的工作资源,进而提高效率。
二、时基单元
下图是手册中找到的基本定时器结构框图:这个我们也称为时基单元,他作为其他通用定时器、高级定时器的一部分使用
分析这个图可以知道基本定时器的运行流程:
(1)时钟信号从TIMxCLK线发送到定时器的控制器。
(2)控制器将时钟信号经过预分频后传递给计数器。
(3)每来一个时钟周期,计数器++。
(4)当计数器的值和自动重装寄存器的值相同的时候,对外触发中断信号,或者由触发控制器对外触发TRGO信号到ADC。(这里的中断信号和触发事件可以同时配置,互不影响)
(5)同时计数器的值清零,重新开始计数。
我们把时钟树和基本定时器的框图结合起来看看。
时钟树中有一句话是这样说的:如果APB1预分频系数为1,则频率不变,否则频率*2。这个我们该如何理解呢?
APB1接的是AHB总线的时钟,经过APB1预分频器分频得到APB1总线的时钟。本来呢如果预分频系数为1,则APB1的时钟频率应该是72MHz,单丝这幅图中其实隐藏了一个部分,他接在APB1预分频器之后,且在定时器接线处之后,他使得APB1其他外设的频率永远不能超过36MHz。此时定时器频率率仍然为72MHz。
当APB2的预分频系数>1的时候,定时器的时钟频率则为APB1总线频率的两倍。且此时APB1总线的频率也会<36MHz。
- 例如:
- 系统时钟 72MHz,APB1 预分频系数为 4,则 APB1 时钟为 18MHz,定时器时钟为 36MHz。
- 系统时钟 72MHz,APB1 预分频系数为 2,则 APB1 时钟为 36MHz,定时器时钟为 72MHz。
- 结论:APB1 上的定时器时钟可能为 72MHz 或 36MHz,具体取决于 APB1 的预分频系数。
- 不同STM32型号的默认APB1预分频系数可能不同。例如:
- STM32F103:默认APB1预分频系数 = 2(导致APB1总线频率 = 36MHz,当SYSCLK=72MHz时)。
- 其他型号:可能默认APB1预分频系数 = 1(导致APB1总线频率 = SYSCLK)。
我们可以尝试计算一下一个时基单元最长的定时时间:
基本定时器TIM6和TIM7各包含一个16位自动装载计数器,由各自的可编程预分频器驱动。
16为计数器,意味着最多可以从0计数到65536个数,而一个数的时间为一个时钟周期。
假设我们采取默认的分频方式,即APB1时钟频率为36MHz,APB1的定时器时钟频率为72MHz。计算出该情况下一个时基单元的最长定时长度不到一分钟。
当然,基本定时器由于不支持定时器级联功能,所以最长定时时间看起来不多,如果你使用通用定时器,一个定时器的输出可以作为下一个定时器的输入的话,则最长时间将变为42亿年多。
三、基本定时器的的时序
(1)预分频器时序
预分频可以以系数介于1至65536之间的任意数值对计数器时钟分频。它是通过一个16位寄存器(TIMx_PSC)的计数实现分频。因为TIMx_PSC控制寄存器具有缓冲,可以在运行过程中改变它
的数值,新的预分频数值将在下一个更新事件时起作用。
这里有一个概念,叫做预分频器控制寄存器(1)和预分频器缓冲器(2)。这个2号我们也称为影子寄存器。
当你通过代码修改预分频器的分频规则的时候其实是修改预分频器控制寄存器(1),此时预分频器缓冲器(2)不受任何影响。分频器仍旧按照之前的分频规则向计数器传递信号。当这一次计数器达到自动重装寄存器的值后,产生中断信号、更新事件的同时,预分频器控制寄存器(1)的值才会填入预分频器缓冲器(2)。
简而言之,真正起作用的是2号预分频器缓冲器。之所以STM32要这么设计是为了防止在一个定时周期内修改了分频规则导致该次定时时间的错误。
同理:自动重装寄存器也会有一个影子寄存器。作用和上述类似。
(2)计数器时序
计数器时序与上图类似,不再赘述。
四、基本定时器的使用
从上面的过程我们可以把配置流程抽象成下图所示:
不过由于基本定时器没有接入到GPIO引脚,所以仅能使用内部时钟源作为定时器使用、或用于主模式触发DAC功能。如果你想使用计数功能、或使用外部时钟源,则可以选择使用基本定时器或者高级定时器。
下面是配置函数示例:
#include "time6.h"
#include "stm32f10x.h"
#include <stdio.h>
#include "led.h"
void TIM6_Init(uint16_t psc, uint16_t arr)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
NVIC_InitTypeDef NVIC_InitStructure;
// 1. 开启 TIM6 时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE);
// 2. 配置 TIM6
TIM_TimeBaseStructure.TIM_Prescaler = psc; // 预分频系数
TIM_TimeBaseStructure.TIM_Period = arr; // 自动重装载值
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // 计数模式:向上计数
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 时钟分频
TIM_TimeBaseInit(TIM6, &TIM_TimeBaseStructure);
//3.清除中断挂起位,避免程序启动时触发意外中断
TIM_ClearITPendingBit(TIM6, TIM_IT_Update);
// 4. 使能 TIM6 更新中断
TIM_ITConfig(TIM6, TIM_IT_Update, ENABLE);
// 5. 配置 NVIC 中断优先级
NVIC_InitStructure.NVIC_IRQChannel = TIM6_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// 6. 启动 TIM6
TIM_Cmd(TIM6, ENABLE);
}
// TIM6 中断处理函数
void TIM6_IRQHandler(void)
{
if (TIM_GetITStatus(TIM6, TIM_IT_Update) != RESET) // 检查中断标志
{
TIM_ClearITPendingBit(TIM6, TIM_IT_Update); // 清除中断标志
//处理逻辑
}
}