深入浅出地理解STM32中的定时器工作原理
一、如何实现延时
延时在嵌入式应用中是常用操作,嵌入式应用中的延时通常可以用以下3种方式实现:
1 纯硬件电路
在早期仪器仪表中,经常使用模拟或数字电路来实现定时/计数功能。 例如,模拟电
路中555集成芯片,辅以少量的电阻和电容,即可实现一个定时器。
由千硬件电路结构固定,由其产生的延时时间无法改变。
因此,使用纯硬件电路实现的延时主要应用于无微控制器的简单应用系统或是有特殊要求的应用系统中。
目前,嵌入式系统中使用纯硬件方式来实现延时已经越来越少了。
2 纯软件编程
微控制器基于一定的时钟条件运行,因此,可以根据代码执行所需的时钟周期来完成延时操作。
通常,在基千微控制器的嵌入式系统中,对千短时延时,可以通过执行一定数量的空指令来实现,空指令每执行一次需要一个周期;对于长时延时,可以通过循环结构来实现。
例如,可以使用循环语句for (nCount = 0xFFFFFF; nCount != 0; nCount--)
实现LED点亮和熄灭时间的延迟。
延时的纯软件方式实现起来非常简单,但 具有以下缺点:
(1) 对于不同的微控制器, 每条指令的执行时间不同,很难做到精确延时。
例如,在上面讲到的LED闪烁应用案例 中,如果要使LED点亮和熄灭的时间精确到各为500ms,
对应软件实现的循环语句中决定延时时间的变量 nCount 的具体取值很难由计算准确得出。
(2) 延时过程中 CPU 始终被占用, CPU利用率不高
虽然纯软件定时/计数方式有以上缺点,但由千其简单方便、易于实现等优点,在当今的嵌入式应用中,尤其在短延时和不 精确延时中,被频繁地使用。 例如,高速ADC的转换时间可能只需要几个时钟周期,这种情况下,使用软件延时反而效率更高。
3 可编程定时/计数器
当前的微控制器往往内置一个或多个定时/计数器,以代替CPU 计数,克服纯硬件和纯软件方式的缺点,并结合它们各自的优点实现延时。
并且,这些定时/计数器都是用户可编程的, 其时钟源、预分频系数、工作模式和启动/停止等参数均可由软件配置。
由此可见, 微控制器内置的定时/计数器具有通用性强、用户可编程、可重复利用、不占用CPU、成本低等特点,是目前使用最多的一种定时/计数的实现方式。
二、可编程定时/计数器有哪些功能?
可编程定时/计数器(简称定时器)是当代微控制器标配的片上外设和功能模块。
它不仅可以实现延时,而且还完成其他功能:
(1) 如果时钟源来自内部系统时钟,那么可编程定时/计数器可以实现精确的定时。此时的定时器工作于普通模式、比较输出或PWM输出模式,通常用千延时 、输出指定波形、驱动电机等应用中。
(2) 如果时钟源来自外部输入信号,那么可编程定时/计数器可以完成对外部信号的计数。 此时的定时器工作于输入捕获模式,通常用千测戴输入信号的频率和占空比、测量外部事件的发生次数和时间间隔等应用中。
在嵌入式系统应用中,使用定时器可以完成以下功能:
(3) 在多任务的分时系统中用作中断来实现任务的切换。
(4) 周期性执行某个任务,如每隔固定时间完成一次AD采集。
(5) 延时一定时间执行某个任务,如交通灯信号变化。
(6) 显示实时时间,如万年历。
(7) 产生不同频率的波形,如MP3播放器。
(8) 产生不同脉宽的波形,如驱动伺服电机。
(9) 测拭脉冲的个数,如测县转速。
(10) 测量脉冲的宽度,如测最频率。
三、STM32F103 的定时器有哪些
1 基本定时器 TIM6 和 TIM7
1.1 概述
STM32Fl03 基本定时器 TIM6 和 TIM7 只具备最基本的定时功能,即累计时钟脉冲数超过预定值时,产生定时器溢出事件。 如果使能了中断或者 DAC 操作,则将产生中断或者 DAC 操作。
TIM6 和 TIM7 还可以作为通用定时器提供时间基准。 特别地,它们可以为数模转换器 DAC 提供时钟。 实际上,在 STM32Fl03 微控制器内部 TIM6 和 TIM7 直接连接到 DAC 并通过触发输出直接驱动 DAC。
DAC也是一种片内外设功能电路,其功能就是将数字信号转换为模拟信号,以输出特定的波形。
正常来说,我们如果要用DAC输出特定的波形,那么就要每隔一段时间就触发DAC,让其输出下一个电压点。这样的话,可以设置一个定时器,每隔一段时间,触发中断,启动中断程序,程序中触发DAC转换,然后DAC输出。
这样做,可以,但是,主程序处于被频繁中断的状态,这会影响主程序的运行,以及其他中断的响应。
在参考手册中,对于该功能的描述如下:
原话是:they are also specifically used to drive the digital-to-analog converter (DAC). In fact, the timers are internally connected to the DAC and are also able to drive it through their trigger outputs (触发输出)。
它们还专门用于驱动数模转换器(DAC)。事实上,定时器内部连接到DAC,也能够通过其 触发输出 驱动DAC。
再看下面这个图:
这里定时器产生的更新事件,可以映射到TRGO的位置,TRGO就是 trigger outputs (触发输出)。TRGO直接接到DAC的触发转换引脚上。
1.2 内部结构
STM32Fl03基本定时器 TIM6 和 TIM7 的内部结构较为简单,由触发控制器、一个16位预分频器、一个带自动重装载寄存器的16位计数器等构成。其中,由可编程的16位预分频器驱动的具有自动重装载功能的16位计数器TIMx_CNT是STM32Fl03基本定时器的核心。
基本定时器,基本由四部分组成:
(1)定时器时钟源
下面画圈 的部分,就是 定时器时钟源在时钟树上的位置。
从STM32Fl03基本定时器TIM6和TIM7的内部结构可以看出,基本定时器TIM6 和 TIM7只有一种时钟源一一内部时钟CK_INT。对于STM32Fl03 所有的定时器 ,内部时钟CK_INT都来自 RCC(Reset and Clock Control,复位和时钟控制)的TIMxCLK。
TIMxCLK根据APBl的预分频系数分为两种情况:
(1)若APBl预分频系数等于1,TIMxCLK等于APBl时钟频率PCLK1。
(2)若 APBl 预分频系数不等千1,TIMxCLK 等千 APBl 时钟频率PCLK1X2。
通常情况下,STM32Fl03 上电复位后,APB1 的预分频系数为 2, APB1 时钟频率PCLKl 为 36MHz。
因此 如上所述,基本定时器 TIM6 和 TIM7 的时钟 TIMxCLK,是APBl 时钟频率PCLKl 的 2 倍,即 72MHz
(2)计数器时钟
小知识点: 什么是预分频器?
A prescaler is an electronic counting circuit used to reduce a high frequency electrical signal to a lower frequency by integer division.
预分频器是一种电子计数电路,用于通过整数除法将高频电信号降低到较低频率。
(3)计数器
所谓向上计数,就是从0开始,不断增大。计数器CNT是16位 的寄存器,那么计数的范围就是
0000
H
到
F
F
F
F
H
0000H 到 FFFFH
0000H到FFFFH,
F
F
F
F
H
=
15
×
1
6
3
+
15
×
1
6
2
+
15
×
16
+
15
=
61440
+
3840
+
240
+
15
=
61440
+
4080
+
15
=
61440
+
4095
=
65535
FFFFH=15 \times16^{3}+15 \times16^{2}+15 \times16+15=61440+3840+240+15=61440+4080+15=61440+4095=65535
FFFFH=15×163+15×162+15×16+15=61440+3840+240+15=61440+4080+15=61440+4095=65535,所以CNT的最大计数值是
65535
65535
65535
(4)自动重装载计数器
1.3 工作过程
STM32F103 基本定时器中的 16位计数器 TIMx_CNT 只能工作在向上计数模式,自动重装载寄存器中保存的是定时器的溢出值。
基本定时器工作时,脉冲计数器 TIMx_CNT 从 0 开 始,在时钟 CK_CNT触发下不断累加计数。
当脉冲计数器 TIMx_CNT的计数值等千自动重装载寄存器 TIMx_ARR ( Auto Reload Register) 中保存的预设值时, 产生溢出事件,可以触发中断或 DAC请求。 然后,脉冲计数器TIMx_CNT的计数值被清零,重新开始向上计数。
延时时间
=
(
A
R
R
+
1
)
×
(
P
S
C
+
1
)
/
T
I
M
x
C
L
K
延时时间=(ARR+1)\times(PSC+1)/TIMxCLK
延时时间=(ARR+1)×(PSC+1)/TIMxCLK
1.4 库函数文件
在定时器头文件 tim.h
中,声明了四个定时器初始化的函数,基本定时器只用其中的一个,
void TIM_TimeBaseInit(TIM_TypeDef* TIMx,
TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);
在tim.c
文件,找到函数
这个函数要传入两个参数变量,一个是指定是哪个定时器,第二个是结构体变量TIM_TimeBaseInitStruct
,是用TIM_TimeBaseInitTypeDef
结构体定义的,TIM_TimeBaseInitTypeDef
的定义如下(在 tim.h
中):
typedef struct
{
uint16_t TIM_Prescaler; // 设置 预分频器的值,可以是0000H到FFFFH
uint16_t TIM_CounterMode; // 设置 计数方式,基本定时器只能是向上计数方式
uint16_t TIM_Period; // 定时器周期,其实就是自动重载寄存器的值,可以是0~65535
uint16_t TIM_ClockDivision; // 时钟分频,基本定时器没有这个功能,不用设置
uint8_t TIM_RepetitionCounter; // 重复计数器,基本定时器没有这个功能,不用设置
} TIM_TimeBaseInitTypeDef;
1.5
2 通用定时器 TIM2 ~ TIM5
2.0 总体结构
通用定时器的结构如下所示:
下面按模块分别讲解:
2.1 时钟源选择模块
(1)内部时钟源
红色框内的是内部时钟源,内部时钟 CK_INT 即来自于芯片内部,等于 72M,一般情况下,我们都是使用内部时钟。
当从模式控制寄存器 TIMx_SMCR 的 SMS 位等于 000 时,则使用内部时钟。
(2)外部时钟模式 1
除了内部时钟,还可以选择外部时钟,一个外部时钟,就是来自TIMx_ETR引脚上的时钟,TIMx_ETR引脚的位置,可以通过引脚定义表查看
① 时钟信号输入引脚
当使用外部时钟模式 1 的时候,时钟信号来自于定时器的输入通道,总共有 4 个,分别为 TI1/2/3/4,即 TIMx_CH1/2/3/4。
具体使用哪一路信号,由 TIM_CCMRx 的位CCxS[1:0]配置,其中 CCMR1 控制 TI1/2,CCMR2 控制 TI3/4。
② 滤波器
如果来自外部的时钟信号的频率过高或者混杂有高频干扰信号的话,我们就需要使用滤波器对信号重新采样,来达到降频或者去除高频干扰的目的,具体的由 TIMx_CCMRx的位 ICxF[3:0]配置。
③ 边沿检测
边沿检测的信号来自于滤波器的输出,在成为触发信号之前,需要进行边沿检测,决定是上升沿有效还是下降沿有效,具体的由 TIMx_CCER 的位 CCxP 和 CCxNP 配置。
④ 触发选择
当使用外部时钟模式 1 时,触发源有两个,一个是滤波后的定时器输入 1(TI1FP1)和滤波后的定时器输入 2(TI2FP2),具体的由 TIMxSMCR 的位 TS[2:0]配置。
⑤ 从模式选择
选定了触发源信号后,最后我们需把信号连接到 TRGI 引脚,让触发信号成为外部时钟模式 1 的输入,最终等于 CK_PSC,然后驱动计数器 CNT 计数。具体的配置TIMx_SMCR 的位 SMS[2:0]为 000 即可选择外部时钟模式 1。
(3)外部时钟模式 2
①:时钟信号输入引脚
当使用外部时钟模式 2 的时候,时钟信号来自于定时器的特定输入通道 TIMx_ETR,只有 1 个。
②:外部触发极性
来自 ETR 引脚输入的信号可以选择为上升沿或者下降沿有效,具体的由 TIMx_SMCR 的位 ETP 配置。
③:外部触发预分频器
由于 ETRP 的信号的频率不能超过 TIMx_CLK(72M)的 1/4,当触发信号的频率很高的情况下,就必须使用分频器来降频,具体的由 TIMx_SMCR 的位 ETPS[1:0]配置。
④:滤波器
如果 ETRP 的信号的频率过高或者混杂有高频干扰信号的话,我们就需要使用滤波器对 ETRP 信号重新采样,来达到降频或者去除高频干扰的目的。具体的由 TIMx_SMCR 的 位 ETF[3:0]配置,其中的 fDTS是由内部时钟 CK_INT 分频得到,具体的由 TIMx_CR1 的位CKD[1:0]配置。
⑤:从模式选择
经过滤波器滤波的信号连接到 ETRF 引脚后,触发信号成为外部时钟模式 2 的输入,最终等于 CK_PSC,然后驱动计数器 CNT 计数。具体的配置 TIMx_SMCR 的位 ECE 为 1即可选择外部时钟模式 2。
⑥:使能计数器
经过上面的 5 个步骤之后,最后我们只需使能计数器开始计数,外部时钟模式 2 的配置就算完成。使能计数器由 TIMx_CR1 的位 CEN 配置。
(4)内部触发输入(ITRx)
2.1 计数方式
首先,通用定时器,计数器的计数方式被扩展成了三种方式:向上计数、向下计数、双向计数。具体区别,截图如下:
(1)向上计数
(2)向下计数
(3)双向计数
4 定时中断的实现
4.1 工作流程
(1)第 1 步
RCC开启时钟,这样,定时器的基准时钟和整个外设的工作时钟就同时打开了。
(2)第 2 步
选择时基单元的时钟源,对于定时中断,这里我就选择内部时钟源。
(3)第 3 步
配置时基单元,包括预分频器、自动重装载寄存器、计数器的计数方式。
(4)第 4 步
配置输出中断控制,允许更新中断输出到 NVIC。
(5)第 5 步
配置 NVIC,在NVIC中打开定时器中断的通道,并分配一个优先级。
(6)第 6 步
运行控制
整个模块配置完成后,还需要使能一下计数器
使能之后,计数器就开始计数了,当计数器更新时,触发中断,执行中断函数。
4.2 实验准备
4.3 keil中实现
在keil中,对于建立 定时器设置的 C 文件 和 h 文件:
/********************* Timer.c ********************/
#include "stm32f10x.h" // Device header
extern uint16_t Num;
void Timer_Init(void)
{
/* 第一步:打开定时器时钟 */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
/* 第二步:选择时基单元的时钟 */
TIM_InternalClockConfig(TIM2); // 这里选择的是内部时钟
// 这样,TIM2的时基单元就
// 是由内部时钟来驱动了
// 实际上,定时器上电后,
// 默认的就是用内部时钟驱动时基单元
/* 第三步:配置时基单元 */
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; // TIM_ClockDivision用于指定时钟分频,这里TIM_CKD_DIV1 代表1分频,也就是不分频
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; // TIM_CounterMode用于设置计数方式,TIM_CounterMode_Up代表向上计数
TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1; // 这里设置的是ARR的值,也就是自动重装载寄存器(Auot-reload register)的值,可以是 0~65535
TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1; // 这里是预分频器的值,可以是0~65535
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; // 设置重复计数器的值,高级定时器时会用到,这里不用,设置为0即可
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
/* 第四步:使能更新中断 */
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
/* 第五步:配置NVIC */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
/* 第六步:启动定时器 */
TIM_Cmd(TIM2, ENABLE);
/** 写到这里,定时中断的初始化工作就已经完成了 **/
}
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
{
Num++;
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
/********************* Timer.h ********************/
#ifndef __TIMER_H
#define __TIMER_H
void Timer_Init(void);
void TIM2_IRQHandler(void);
#endif
再看一下下面关于延时时间计算公式:
延时时间
=
(
A
R
R
+
1
)
×
(
P
S
C
+
1
)
/
T
I
M
x
C
L
K
延时时间=(ARR+1)\times(PSC+1)/TIMxCLK
延时时间=(ARR+1)×(PSC+1)/TIMxCLK
TIMxCLK一般就是
72
M
H
Z
72 MHZ
72MHZ,也就是
72
×
1
0
6
H
Z
72\times10^{6}HZ
72×106HZ,这里我们向延时1秒种,那么凑数就可以了,
A
R
R
ARR
ARR可以是
10000
−
1
10000 - 1
10000−1,
P
r
e
s
c
a
l
e
r
Prescaler
Prescaler设置为
7200
−
1
7200-1
7200−1,那么带入公式之后,得到
(
10000
−
1
+
1
)
×
(
7200
−
1
+
1
)
/
(
72
×
1
0
6
)
=
(
1
0
4
×
72
×
1
0
2
)
/
(
72
×
1
0
6
)
=
1
秒
(10000 - 1+1)\times(7200-1+1)/(72\times10^{6})=(10^{4}\times72\times10^{2})/(72\times10^{6})=1 秒
(10000−1+1)×(7200−1+1)/(72×106)=(104×72×102)/(72×106)=1秒
4.3 实验效果
定时中断现象
5 定时中断-外部时钟源
定时器中断-外部时钟
#include "stm32f10x.h" // Device header
extern uint16_t Num;
void Timer_Init(void)
{
/* 第一步:打开时钟 */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
/* 第二步:选择时基单元的时钟 */
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;
TIM_TimeBaseInitStructure.TIM_Prescaler = 1 - 1;
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
/* 第四步:使能更新中断 */
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
/* 第五步:配置NVIC */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
/* 第六步:启动定时器 */
TIM_Cmd(TIM2, ENABLE);
/** 写到这里,定时中断的初始化工作就已经完成了 **/
}
uint16_t Timer_GetCounter(void)
{
return TIM_GetCounter(TIM2);
}
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
{
Num++;
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
6 PWM 实现呼吸灯
6.1 什么是 PWM
PWM = Pulse Width Modulation = 脉冲宽度调制
在具有惯性的系统中,可以通过对一系列脉冲的宽度进行调制,来等效地获得所需要地模拟参量,常应用于电机控制速度等领域。
PWM 参数有哪些:
(1) 频率
1
/
T
s
1/T_{s}
1/Ts
(2) 占空比
T
O
N
/
T
s
T_{ON}/T_{s}
TON/Ts
(3) 分辨率,占空比变化步距
按理说,LED灯只有亮和灭两种状态。怎么控制它的亮度呢?
就用PWM,利用PWM,让LED灯不断地点亮、熄灭,当亮灭的频率足够大时,LED灯就不闪烁了,呈现一个中等亮度。
当我们调整点亮和熄灭时间的比例时,就能让LED灯呈现不同的亮度。
这个思想,就跟电机调速一样,给电机通电,断电,通电,断电,那么电机的速度就维持在一个中等速度,通过调节通电和断电时间的比例,就可以调节电机的速度。
定时器的输出比较功能:
(1) OC(Output Compare)= 输出比较
(2) 输出比较可以通过比较计数器 CNT 和 “捕获/比较寄存器(Capture/Compare register, CCR)”值的关系,来对输出电平进行置1、置0或翻转的操作,用于输出一定频率和占空比的PWM波形。(注意:PWM = Pulse Width Modulation = 脉冲宽度调制)
(3) 每个高级定时器和通用定时器都拥有4个输出比较通道。
(4) 高级定时器的前3个通道额外拥有死区生成和互补输出的功能。
这里再说明一点:
要知道,高级定时器和通用定时器,具有输入捕获和输出比较,这两个功能,CCR这个寄存器,是具有两重功能的,在输入捕获中,它就是捕获寄存器,在输出比较中,它就是输出比较器,所以,综上,我们就叫它“捕获/比较寄存器”。就是这么简单。
(1)高级定时器的输出比较电路
要理解这个电路的作用,要结合它的外接电路,一般是这样的:
黄色的是MOS管,
① 上管导通,下管断开,输出的就是高电平;
② 上管断开,下管导通,输出就是低电平;
③ 上管和下管都断开,输出呈现高阻态;
④ 不能同时导通,会烧掉的。
这就是推挽电路的工作流程,
有两个这样的推挽电路,就构成了H桥电路,就能控制电机的正反转了。
有三个这个推挽电路,就能够驱动三相无刷电机了。
OC1和OC1N的输出电平必须是相反的,也就是互补的。
but,这里会存一个安全性问题,比如说,现在是上管导通,下管断开,下一个状态是上管断开,下管导通。这个状态之间的转换,是瞬间立刻,干干净净地完成吗?不能,极有可能出现这种情况:
上管还没完全断开,下管就导通,两种MOS管都处于导通状态,啪!!! 烧了!!!
ST公司为了避免这种情况,设计了死区电路,
上管开始关闭时,延迟一小段时间,再导通下管!哈哈哈,没有复杂的理论推导,就是这么直接!!!