本次课程采用单片机型号为STM32F103C8T6。
课程链接:江科大自化协 STM32入门教程
往期笔记链接:
STM32学习笔记(一)丨建立工程丨GPIO 通用输入输出
STM32学习笔记(二)丨STM32程序调试丨OLED的使用
STM32学习笔记(三)丨中断系统丨EXTI外部中断
如果上一篇笔记的内容为史诗级副本,本篇文章的内容我愿称之为传说级副本(一)。
一、TIM 定时器
1.1 TIM 定时器简介
TIM(Timer)定时器,它的基本功能是 对输入的时钟进行计数,并在计数值达到定值时触发中断,即定时触发中断定时器就是一个计数器,当计数器的输入是一个准确可靠的基准时钟时,对基准时钟进行计数的过程就是计时的过程。 在STM32中,定时器的基准时钟一般都是72MHz。
定时器最核心的部分称为 时基单元,它由以下三个16位的寄存器组成:
- 计数器(Counter):用来计数定时的寄存器,每来一个时钟,计数器加1。
- 预分频器(Prescaler):可以对计数器的时钟进行分频,让时钟更加灵活。
- 自动重装寄存器(Auto-reload Register):计数的目标值,即计多少个时钟申请中断。
由于构成时基单元的寄存器都是16位的,故如果我们将预分频器的值设置为最大,自动重装值也设置为最大,设中断频率为
f
0
f_0
f0,在72MHz主频下的最大定时时间
t
m
a
x
t_{max}
tmax,则:
f
0
=
72
/
(
2
16
)
2
M
H
z
t
m
a
x
=
1
f
0
=
59.65
s
f_0=72/(2^{16})^2MHz\\ t_{max}=\frac 1 f_0=59.65s
f0=72/(216)2MHztmax=f10=59.65s
59.65s的定时时间已经是比较长的时间了。如果由更高的时间定时需求,STM32的定时器支持级联 ,即一个定时器的输入为另一个定时器的输出,这时的定时时间约为八千多年。如果再级联一个定时器,这时的定时时间将会达到34万亿年。可见,STM32定时器的定时时间选择是相当自由的。
STM32的定时器的常用功能有以下几种(列出的功能本课程都会涉及):
- 定时中断功能
- 内外时钟源选择
- 输入捕获
- 输出比较
- 编码器接口
- 主从触发模式
- …
STM32的定时器还根据应用场景和复杂度设计了高级定时器、通用定时器、基本定时器三种类型。本课程主要学习和使用通用定时器。
1.2 TIM 定时器类型及其工作原理简介
有关STM32的定时器类型,编号和功能简述如下表所示:
同一个芯片一般有很多个定时器,它们都用TIMx表示。上表中只列出了TIM1~TIM8,但是再库函数中还出现了TIM9、TIM10、TIM11等,这些都不常用。
不同的定时器连接的总线也不相同,高级定时器连接的是性能更高的APB2总线,而通用定时器和基本定时器连接的是APB1总线。在使用RCC开启定时器时钟的时候要注意库函数的调用。
定时器的功能是从高级向低级向下兼容的,高级定时器包含通用定时器的全部功能,通用定时器包含基本定时器的全部功能。高级定时器相较于通用定时器的额外功能,例如重复计数器,死区生成,互补输出,刹车输入等,这些都是为了三相无刷电机FOC的驱动设计的,本课程暂时不会涉及。
本课程使用的STM32F103C8T6芯片的定时器资源有:TIM1,TIM2,TIM3,TIM4,即拥有一个高级定时器,三个通用定时器,没有基本定时器。不同的芯片型号,定时器的数量和类型是不同的。在使用外设之前,一定要查询外设是否存在。
1.2.1 基本定时器工作原理及其结构
基本定时器拥有定时中断,主模式触发DAC(数模转换器)的功能。基本定时器结构框图如下图所示:
由上图可以看到,基本定时器拥有由预分频器PSC(Prescaler),计数器CNT(Counter),ARR(Auto-reload Register)自动重装寄存器组成的基本时基单元。预分频器之前连接的是 基准时钟的输入,由于基本定时器只能连接内部时钟,故可以直接认为预分频器的时钟输入CK_PSC就是连接到内部时钟CK_INT上的。内部时钟的来源是RCC_TIMxCLK,这里的频率一般都是系统的主频72MHz。所以基本定时器的基准时钟输入只能是72MHz。
PSC预分频器可以对基准时钟进行预分频。如果预分频器写0,就是不分频,或者说1分频,这时 输出频率 = 输入频率 = 72MHz;如果预分频器写1,就是2分频,输出频率 = 输入频率 / 2 = 36MHz;如果预分频器写2,就是3分频……依次类推。所以我们有如下的关系:
实际分频系数
=
预分配器写入的值
+
1
实际分频系数=预分配器写入的值+1
实际分频系数=预分配器写入的值+1
预分频器是16位的寄存器,故最大值可以写入65535(
2
16
2^{16}
216),也就是65536分频。
CNT计数器可以对预分频后的计数时钟CK_CNT进行计数。计数时钟CK_CNT每来一个上升沿,计数器的值就+1(只能递增)。计数器也是16位的,它的值可以是0~65535,如果再+1,计数器就会回到0重新开始计数。
ARR自动重装寄存器存入计数器的计数目标,它也是16位的寄存器。在计数器运行不断自增的过程中,自动重装置就是一个固定的目标。计数器不断与重装寄存器进行比较,当计数值 = 自动重装值时,就说明计时时间到,这时自动重装寄存器就会输出一个中断信号,并且清零计数器,计数器自动开始下一次的计数计时。
在图中的折线向上的箭头,就代表产生的一个中断信号。UI意为Update Interrupt,即更新中断,它是计数值等于自动重装值的中断。更新中断会通往NVIC,只要配置好NVIC的中断通道,那么定时器的更新中断就能得到CPU的响应了。
在图中折线向下的箭头,就代表产生的一个事件。这里的U意为Update,即更新事件。更新中断不会触发中断,但可触发其他内部电路的工作。图中可以看到,自动重装寄存器也可响应其他外设产生的事件。
主从触发模式
STM32的一大特色就是主从触发模式。它能让内部的硬件在不受程序的控制下实现自动运行,即实现硬件自动化。
这里我的理解是:主模式,即作为信号的主人的模式,电路输出的信号能触发其他处于从模式的电路的响应。 正常来讲,在一个时刻只能执行一段代码,完成一个操作,如果能合理使用主从触发,就可以实现”一心二用“,将会极大地减少CPU运行负担(这里仅做简单了解即可,后续还会讲到)。
以基本定时器主模式触发DAC为例:在我们使用DAC的时候,可能会用DAC输出一段波形,那就需要每隔一段时间来触发一次DAC,让它输出下一个电压点。如果按中断的思路,需要先设置一个定时器产生中断,每隔一段时间在中断程序中用代码手动触发一次DAC转换,然后DAC输出波形。这样操作没有错,但是这样会使主程序频繁处于被中断的状态,有可能会影响主程序的运行和其他中断的响应。所以定时器的主模式可以把定时器的更新事件U映射到触发输出TRGO(Trigger Out)的位置,再通过TRGO直接连接到DAC的触发转换引脚上。这样DAC转换就不需要定时器产生的中断来触发了,仅需要把更新事件U通过主模式映射到TRGO,TRGO自动触发DAC就可以实现。这里体现了主模式的作用:整个过程不需要软件的参与,实现了硬件的自动化。
1.2.2 通用定时器工作原理及其结构
通用定时器拥有基本定时器全部功能,并额外具有内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等功能。 通用定时器的结构框图如下如所示:
- 通用定时器与基本定时器的异同
通用定时器最基本的结构也是时基单元,与基本定时器相同。可以认为通用定时器就是在基本定时器的基础上扩展了许多功能得到的。它在时基单元模块与基本定时器的区别是:计数器的计数模式不止向上计数一种。除了与基本定时器相同的向上计数的模式外,它还拥有以下两种模式:
- 向下计数模式:计数从重装值开始,向下自减,到0之后回到重装值,同时产生更新中断和更新事件,依此循环。
- 中央对齐模式:计数从0开始,向上自增,达到重装值后产生更新中断和更新之间,然后向下自减,达到重装值之后产生更新中断和更新事件,以此循环。
- 内外时钟源选择功能
结构图的上部分,为内外时钟源选择和主从触发模式的工作结构:
首先来看内外时钟源选择的工作原理。基本定时器只能选择来自芯片内部的内部时钟CK_INT,也就是系统频率72MHz。而通用定时器不仅可以选择内部时钟,还可以选择外部时钟。具体有以下三种:
-
来自TIMx_ETR引脚上的外部时钟ETR
TIMx_ETR引脚的位置可以参考引脚定义表中关于默认复用功能和重定义功能的定义,如下图所示。可以看到TIM2的CH1和ETR都复用在了引脚PA0上。其他定时器的引脚也可以在表中找到。
我们在TIMx_ETR引脚上外接一个方波时钟,进入的外部时钟ETR通过内部的极性选择、边沿检测和预分频器电路,产生外部时钟脉冲ETRP(这里我猜测P就是Pulse,即脉冲,暂时没有找到相关的解释)。之后再通过输入滤波电路,之后的信号兵分两路,上面的信号ETRF进入触发控制器(与内部时钟流程类似),之后就可以作为时基单元的时钟输入了,在STM32中,这一路称为 外部时钟模式2(如图中红线所示);另一路与其他信号通过一个数据选择器输出TRGI(Trigger In),当这个TRGI当作外部时钟来使用时,这一路就称为 外部时钟模式1(如图中黄线所示)。后者从名字上看,它主要是作为触发输入来使用的,这个触发输入可以触发定时器的从模式。关于从模式的内容之后再涉及,本节主要考量把这个触发输入当作外部时钟来考虑的情况。 -
来自其他定时器的信号ITR
主模式的输出TRGO可以通向其他定时器,实际上通向的就是ITR引脚,通过这一路就可以实现定时器级联的功能。如上如黄线所示,ITR0到ITR3分别来自其他4个定时器的TRGO输出,具体的连接方式如下表所示: -
来自TIMx_CH1的TI1_ED,即从CH1引脚连接的输入捕获模块获得时钟,ED意为Edge,意为通过这一路的时钟,上升沿和下降沿均有效。
-
来自TIMx_CH1的TI1FP1和来自TIMx_CH2的TI2FP2
总结一下,外部时钟模式1的输入可以是ETR引脚、其他定时器、CH1引脚的边沿、CH1引脚和CH2引脚;外部时钟模式2的输入只能是ETR引脚。
- 编码器接口功能
图中的编码器接口,它可以读取正交编码器的输出模型,后续的课程也会讲到。
- 主从触发模式功能
图中的TRGO与基本定时器类似,它可以将定时器内部的一些事件映射到其他电路,从而完成其他电路的功能。
- 输出比较功能
通用定时器结构图的右下角即为定时器的输出比较功能的结构,如下图所示。有四个输出通道,分别对应CH1到CH4的引脚,可以用来输出PWM波形,驱动电机。
- 输入捕获电路
通用定时器的左下角即为输入捕获电路的结构图,它同输出比较功能一样有四个通道,对应CH1到CH4。可以用于测量输入方波的频率。因为输入捕获和输出比较不能同时使用,故中间的捕获/比较寄存器是输入捕获和输出比较电路共用的,CH1到CH4的引脚也是共用的。
1.2.3 高级定时器工作原理及其结构
高级定时器拥有通用定时器全部功能,并额外具有重复计数器、死区生成、互补输出、刹车输入等功能。高级定时器结构框图如下图所示:
高级定时器的大部分结构和通用定时器相同,只在部分作了功能拓展。
- 重复次数计数器
在申请中断的的信号输出处,增加了一个重复次数计数器,它的作用是:可以实现每隔几个计数周期,才发生一次更新事件和中断。相当于对输出的更新信号又作了一次分频。
- 死区生成电路与三相无刷电机
图中的DTG和DTG寄存器组成死区生成电路,右侧的引脚TIMx_CH1/CH2/CH3由原来的每路一个变成了两个互补的输出引脚(TIMx_CH1/CH2/CH3和TIMx_CH1N/CH2N/CH3N),可以输出一对互补的PWM波。这些电路是为了驱动三相无刷电机设计的。在四轴飞行器、电动车后轮、电钻中都可以发现三相无刷电机。三相无刷电机的驱动电路需要三个桥臂,每个桥臂需要2个大功率开关管来控制,总共需要6个大功率开关管控制。所以输出的PWM引脚的前三路就变为了互补的输出引脚,而第四路TIMx_CH4没有变化。
为了防止互补输出的PWM驱动桥臂时,在开关切换的瞬间,由于器件的不理想,造成短暂的直通现象,故添加了死区生成电路。在开关切换的瞬间,产生一定时长的死区,让桥臂的上下管全部关断,防止出现直通现象。
- 刹车输入
刹车输入的主要作用是给电机驱动提供安全保障。如果外部引脚BKIN(Break In)产生了刹车信号,或者内部时钟失效,产生了故障,控制电路就会自动切断电机的输出,防止意外的发生。
二、定时中断和内外时钟源选择
2.1 定时中断的基本结构
定时中断的基本结构如下图所示:
在定时器中最核心的部分是时基单元。图中的“运行控制”就是控制寄存器的一些位,用来启动或停止计数器,配置向上向下计数方式等,操作这些寄存器就能控制时基单元的运行了。关于时钟源选择的部分,在上文都有详细的叙述,这里不再赘述。
计时时间到后,产生的中断信号会先在状态寄存器中置一个中断标志位,这个标志位会通过中断输出控制,到NVIC申请中断。这个中断输出控制存在的原因是:定时器模块有很多地方都要申请中断,如果需要该中断,就允许输出;如果不需要这个中断,就禁止输出。简单来说,中断输出控制就是中断输出的允许位。
2.2 时基单元运行时序举例
STM32中,关于时序运行的内容很多,具体请见手册的详细讨论,这里仅举一些时基单元的例子作简要分析。
2.2.1 缓冲(影子)寄存器
STM32在设计之初,为了保证能适用于多种多样的情况,故对时序运行过程中突然手动更改寄存器对时序的影响作了严谨的设计。这里引入缓冲(影子)寄存器,主要目的就是同步,即可以让寄存器设定的某些目标值的变化和更新事件同时发生,防止在运行途中更改造成错误。在定时器结构图中,有些寄存器的画法采用了方框下加阴影的方式,就说明该寄存器不是只有一个寄存器,而是有两个寄存器来形成缓冲机制。实际上,真正使时序电路状态发生更改的都是影子寄存器。
2.2.2 预分频器时序分析
计数器计数频率:CK_CNT = CK_PSC / (PSC + 1)
上图描述了当预分频器的分频系数从1变为2时,计数器的时序图。当计数器使能信号CNT_EN变为高电平后的下一个CK_PSC的高电平,定时器时钟CK_CNT接收CK_PSC。且此时预分频器的分频系数为1,PSC = 0,预分频器完成一分频,CK_PSC = CK_CNT。
可以看到,当计数器寄存器的值依次递增达到0xFC后立即跳变为0x00,说明重装载寄存器ARR设计的目标计数值就是0xFC,此时电路产生一个更新事件脉冲信号UEV,并产生中断信号。
在更新事件信号之前在TIMx_PSC中写入新数值,将预分频器的分频系数从1改为2,但是由于缓冲寄存器的存在,CK_CNT不会立即变为CK_PSC / 2,而是在下一次更新中断产生的同时,由预分频缓冲器(影子寄存器)修改分频系数为2,PSC = 1。
由预分频计数器时序可以看到,预分频的分频功能实际上也是通过计数器来实现的。当分频系数变为2后,预分频计数器按0、1、0、1依次计数,每当预分频计数器回到0时,预分频器输出信号CN_CNT输出一个脉冲。
2.2.3 计数器时序分析
- 计数器工作时序图
计数器溢出频率:CK_CNT_OV = CK_CNT / (ARR + 1) = CK_PSC / (PSC + 1) / (ARR + 1)
- 计数器无预装时序图(缓冲机制失效 APRE = 0)
如图所示,当计数器没有预装(缓冲机制)时,更改TIMx_ARR寄存器的值还有一种情况:当更改自动加载寄存器的值时,计数器寄存器的值已经大于了更改后的值;但是此时计数器寄存器的值只能递增,故该寄存器会一直递增到最大值0xFFFF之后回到0x0000,再依次递增,直到计数器寄存器的值与ARR寄存器的值相同时申请中断。这里就可以看出,如果不使用缓冲机制,可能会给电路时序的工作造成一些问题。 - 计数器有预装时序(缓冲机制有效 APRE = 1)
2.2.4 RCC时钟树简介
RCC时钟树:在STM32中用来产生和配置时钟,并且把配置好的各个外设都发射到各个外设的系统。 时钟是所有外设运行的基础,所以时钟是最先配置的东西。在程序执行时,在执行主程序之前还会执行一个SystemInit
函数(详见:STM32学习笔记(一)丨建立工程丨GPIO 通用输入输出),这个函数的作用就是配置RCC时钟树。
RCC时钟树可以分为两部分:时钟产生电路和时钟分配电路。
- 时钟产生电路
在时钟产生电路,有四个振荡源,分别是内部的8MHz高速RC振荡器、外部的4-16MHz高速晶振振荡器(一般都外接8MHz)、外部的32.768kHz低速晶振振荡器(一般给RTC提供时钟)、内部的40kHz低速RC振荡器(给看门狗WDG提供时钟)。外部的石英振荡器比内部的RC振荡器更加稳定。如果系统非常简单,且不需要过于精确的时钟,就可以使用内部的RC振荡器,这样可以省下外部的晶振电路。
在SystemInit
函数中是这样来配置时钟的:首先会启动内部的8MHz高速RC振荡器产生时钟,选择该时钟为系统时钟,暂时以8MHz的内部时钟运行;然后再启动外部时钟,配置外部时钟信号流经如下图所示的电路:
外部晶振信号进入PLLMUL锁相环进行倍频,8MHz倍频9倍,得到72MHz,待锁相环输出稳定后,选择锁相环输出为系统时钟。这样就把系统时钟从8MHz切换为了72MHz。这样分析可以得出一个结论:如果外部晶振出问题,可能会出现程序时钟慢大概10倍的现象。如果外部时钟的硬件电路有问题(晶振短路或连接错误等),系统的时钟就无法切换到72MHz,会保持内部的8MHz运行。
图中的CSS称为时钟安全系统,它同样负责切换时钟。CSS可以检测时钟的运行状态,一旦外部时钟失效,它就会自动把外部时钟切换为内部时钟,从而保证程序可以正常运行,不会卡死造成事故。另外在高级定时器的刹车输入功能中,CSS同样负责检测当外部时钟失效时,立即切断输出控制引脚,切断电机输出,防止发生意外。- 时钟分配电路
时钟产生电路产生的之中信号SYSCLK(72MHz)首先进入AHB总线,在AHB总线上有一个预分频器,在SystemInit
函数配置的默认分频系数为1,所以AHB总线的时钟自然是72MHz。
之后信号进入APB1总线,APB1上同样有预分频器,这里SystemInit
默认配置的分频系数为2,输出为36MHz,所以APB1总线的时钟为36MHz。通用定时器和基本定时器是接在APB1上的,但是APB1(APB2同理)连接定时器还有如图所示的以下结构:
通用定时器和基本定时器通过图中APB1下方的支路与APB1连接。由于APB1的预分频系数默认为2,则输出到定时器的时钟频率×2。APB2的预分频器的分频系数默认配置为1,其他其他流程与APB1同理。所以基本定时器,通用定时器,高级定时器的内部基准时钟都是72MHz,这样设计为我们使用定时器带来了方便,不用考虑不同定时器时钟不同的问题了(前提是不乱修改SystemInit
函数中的默认配置)。
在时钟输出端口,都有一个与门进行时钟输出控制。控制端外部时钟使能就是程序中RCC_APB2/1PeriphClockCmd
函数作用的地方。
2.3 定时中断和时钟源选择相关库函数使用
定时器相关的库函数非常多,本节仅对将要使用的库函数和 亿些使用细节 进行说明(即使这样也还是很多)。
- 定时器初始化配置
- 时基单元配置函数
// 恢复定时器缺省配置
void TIM_DeInit(TIM_TypeDef* TIMx);
// 时基单元初始化
void TIM_TimeBaseInit(TIM_TypeDef* TIMx, TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);
// 把时基单元初始化函数所用的结构体变量赋一个默认值
void TIM_TimeBaseStructInit(TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);
// 使能计数器(对应定时中断结构图中的“运行控制”功能)
void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState);
// 使能中断输出信号(对应定时中断结构图中的“中断输出控制”功能)
void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState);
- 时基单元的时钟源选择配置函数
// 时基单元的时钟选择相关函数
// 选择内部时钟
TIM_InternalClockConfig(TIM_TypeDef* TIMx);
// 选择ITRx其他定时器的时钟,TIM_InputTriggerSource为选择要接入哪个定时器
void TIM_ITRxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource);
// 选择TIx捕获通道的时钟,TIM_InputTriggerSource为选择的引脚,TIM_ICPolarity为输入极性选择,ICFilter为滤波配置
void TIM_TIxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_TIxExternalCLKSource,
uint16_t TIM_ICPolarity, uint16_t ICFilter);
// 选择ETR外部时钟模式1输入的时钟,TIM_ExtTRGPrescaler为ETR外部时钟预分频器,TIM_ExtTRGPolarity为输入极性选择,ExtTRGFilter为滤波配置
void TIM_ETRClockMode1Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity,
uint16_t ExtTRGFilter);
// 选择ETR外部时钟模式1输入的时钟,参数与上一个函数完全相同,且对于ETR外部时钟输入而言,两个函数等效,如果不需要触发输入的功能,则两个函数可以互换
void TIM_ETRClockMode2Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler,
uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter);
// 单独配置ETR外部引脚的预分频器,极性,滤波参数
void TIM_ETRConfig(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity,
uint16_t ExtTRGFilter);
- 参数(PSC、ARR等)更改函数(在程序运行过程中修改)
// 预分频值设置,TIM_PSCReloadMode为是否应用输入缓冲功能配置
void TIM_PrescalerConfig(TIM_TypeDef* TIMx, uint16_t Prescaler, uint16_t TIM_PSCReloadMode);
// 改变计数器的计数模式
void TIM_CounterModeConfig(TIM_TypeDef* TIMx, uint16_t TIM_CounterMode);
// 自动重装寄存器预装功能配置(计数器有无预装功能)
void TIM_ARRPreloadConfig(TIM_TypeDef* TIMx, FunctionalState NewState);
// 手动给计数器写入一个值
void TIM_SetCounter(TIM_TypeDef* TIMx, uint16_t Counter);
// 手动给自动重装寄存器写入一个值
void TIM_SetAutoreload(TIM_TypeDef* TIMx, uint16_t Autoreload);
// 获取当前计数器的值
uint16_t TIM_GetCounter(TIM_TypeDef* TIMx);
// 获取当前的预分频器的值
uint16_t TIM_GetPrescaler(TIM_TypeDef* TIMx);
// 获取定时中断的标志位和清除标志位,使用方法与EXTI相同
FlagStatus TIM_GetFlagStatus(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);
void TIM_ClearFlag(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);
ITStatus TIM_GetITStatus(TIM_TypeDef* TIMx, uint16_t TIM_IT);
void TIM_ClearITPendingBit(TIM_TypeDef* TIMx, uint16_t TIM_IT);
- 使用定时器库函数的一些细节
-
选择内部时钟函数:定时器上电后默认选择内部时钟,如果要选择内部时钟,这一句可以省略。
TIM_InternalClockConfig(TIM_TypeDef* TIMx);
-
时基单元初始化函数
TIN_TimeBaseInit
:在配置结构体变量时,会遇到以下几个细节问题
TIM_TimeBaseInitStructure.TIM_ClockDivision
(采样)时钟分频频率选择
在定时器的外部信号输入引脚一般都有一个滤波器来消除信号的抖动干扰,它的工作原理是:在一个固定的时钟频率 f f f下进行采样,如果连续 N N N个采样点都是相同的电平,就代表输入信号稳定了,就将采样值输出到下一级电路;如果 N N N个采样点不全都相同,就说明信号有抖动,这时保持上一次的输出,或直接输出低电平。 这样就能保证输出信号在一定程度上的滤波。这里的采样频率 f f f和采样点数 N N N都是滤波器的参数,频率越低,采样点数越多,滤波效果就越好,不过相应的信号延迟就越大。
采样频率 f f f的来源可以是内部时钟直接提供,也可以是内部时钟加一个时钟分频而来。 分频是多少,就由参数TIM_ClockDivision
决定。可见TIM_ClockDivision
与时基单元的关系并不大,它的可选值可以选择1分频,2分频和4分频。- 在配置结构体变量时,并没有能直接操作计数器CNT的参数。如果需要,可以采用
SetCounter
和GetCounter
两个函数来操作计数器。TIM_TimeBaseInitStructure.TIM_RepetitionCounter
重复计数寄存器,通过这个参数可以设置重复计数寄存器。但是通用定时器中没有这一个寄存器,故可以直接设置为0。- 定时时间的计算
参考公式: 计数器溢出频率:CK_CNT_OV = CK_CNT / (ARR + 1) = CK_PSC / (PSC + 1) / (ARR + 1),注意PSC(TIM_Prescaler
)和ARR(TIM_Period
)的取值都要在0~65535之间。- 在
TIM_TimeBaseInit
函数的最后,会立刻生成一个更新事件,来重新装载预分频器和重复计数器的值。预分频器有缓冲寄存器,我们写入的PSC和ARR只有在更新事件时才会起作用。但是更新事件和更新中断是同时发生的,更新中断会置更新中断标志位,手动生成一个更新事件,就相当于在初始化时立刻进入更新函数执行一次,在开启中断之前手动清除一次更新中断标志位,就可以避免刚初始化完成就进入中断函数的问题。
- 外部时钟配置函数
TIM_ETRClockMode2Config
TIM_ExtTRGPrescaler
外部时钟预分频器:可以选择外部时钟分频关闭(1分频)、2分频、4分频、8分频。TIM_ExtTRGPolarity
外部触发的极性:TIM_ExtTRGPolarity_Inverted
为反向极性,即低电平和下降沿有效;TIM_ExtTRGPolarity_NonInverted
为不反向,即高电平和上升沿有效。ExtTRGFilter
外部输入滤波器:工作原理与内部时钟的滤波器相似,它的值可以是0x00到0x0F之间的一个值,其决定了采样的 f f f和 N N N,具体的对应关系在手册中有对应表:
- GPIO配置:因为是使用外部接口输入时钟,故在使用该函数之前还需要配置GPIO端口。对于定时器,手册中给的推荐配置是浮空输入。但是浮空输入会导致引脚的输入电平极易受干扰,所以输入信号的功率不小时一般选择上拉或下拉输入。当外部的输入信号功率很小,内部的上拉/下拉电阻(较大)可能会影响到这个输入信号,这时就需要用浮空输入,防止影响外部输入的电平。
2.4 定时器定时中断实例
本次实验要完成的现象是:定义一个 uint16_t 的 Num 变量,使其每秒+1。器件连接图和程序源码如下所示:
Timer.c
#include "stm32f10x.h" // Device header
// 用extern声明变量后,这里的Num就会成为main.c文件中Num的引用
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; // 重复计数寄存器,高级定时器才有的模块,这里配置为0
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
// 在TIM_TimeBaseInit函数的最后,会立刻生成一个更新事件,来重新装载预分频器和重复计数器的值
// 预分频器有缓冲寄存器,我们写入的PSC和ARR只有在更新事件时才会起作用
// 为了让写入的值立刻起作用,故在函数的最后手动生成了一个更新事件
// 但是更新事件和更新中断是同时发生的,更新中断会置更新中断标志位,手动生成一个更新事件,就相当于在初始化时立刻进入更新函数执行一次
// 在开启中断之前手动清除一次更新中断标志位,就可以避免刚初始化完成就进入中断函数的问题
TIM_ClearFlag(TIM2, TIM_FLAG_Update);
// 配置中断输出控制,允许中断输出到NVIC
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
// 配置NVIC,在NVIC中打开定时器中断的通道,并分配优先级
// NVIC通道优先级分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
// 初始化NVIC
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; // 配置NVIC中断通道
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);
}
}
main.c
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "Timer.h"
uint16_t Num;
int main()
{
OLED_Init();
Timer_Init();
OLED_ShowString(1, 1, "Num:"); // 显示一个字符串
while(1)
{
OLED_ShowNum(1, 5, Num, 5);
// OLED_ShowNum(2, 5, TIM_GetCounter(TIM2), 5);
}
}
2.5 定时器外部时钟选择
本次实验要完成的现象是:用光敏传感器手动模拟一个外部时钟,定义一个 uint16_t 的 Num 变量,当外部时钟触发10次(预分频之后的脉冲)后Num + 1。器件连接图和程序源码如下所示:
Timer.c
#include "stm32f10x.h" // Device header
// 用extern声明变量后,这里的Num就会成为main.c文件中Num的引用
extern uint16_t Num;
void Timer_Init(void)
{
// 用RCC外设时钟控制打开定时器的基准时钟和外设的工作时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
// RCC打开GPIO的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, 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(GPIOA, &GPIO_InitStructure);
// 选择时基单元的时钟源(使用ETR外部时钟)
TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted, 0x0F);
// 配置时基单元
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 = 2 - 1; // 预分频系数PSC(这里如果PSC设置为0,会连续触发中断,导致主程序不运行,是我的芯片Bug问题)
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; // 重复计数寄存器,高级定时器才有的模块,这里配置为0
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
// 在TIM_TimeBaseInit函数的最后,会立刻生成一个更新事件,来重新装载预分频器和重复计数器的值
// 预分频器有缓冲寄存器,我们写入的PSC和ARR只有在更新事件时才会起作用
// 为了让写入的值立刻起作用,故在函数的最后手动生成了一个更新事件
// 但是更新事件和更新中断是同时发生的,更新中断会置更新中断标志位,手动生成一个更新事件,就相当于在初始化时立刻进入更新函数执行一次
// 在开启中断之前手动清除一次更新中断标志位,就可以避免刚初始化完成就进入中断函数的问题
TIM_ClearFlag(TIM2, TIM_FLAG_Update);
// 配置中断输出控制,允许中断输出到NVIC
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
// 配置NVIC,在NVIC中打开定时器中断的通道,并分配优先级
// NVIC通道优先级分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
// 初始化NVIC
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; // 配置NVIC中断通道
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);
}
}
main.c
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "Timer.h"
uint16_t Num;
int main()
{
OLED_Init();
Timer_Init();
OLED_ShowString(1, 1, "Num:"); // 显示一个字符串
OLED_ShowString(2, 1, "CNT:");
while(1)
{
OLED_ShowNum(1, 5, Num, 5);
OLED_ShowNum(2, 5, Timer_GetCounter(), 5);
}
}
下一篇:STM32学习笔记(五)丨TIM定时器及其应用(输出比较丨PWM驱动呼吸灯、舵机、直流电机)文章来源:https://www.toymoban.com/news/detail-412470.html
原创笔记,码字不易,欢迎点赞,收藏~ 如有谬误敬请在评论区不吝告知,感激不尽!博主将持续更新有关嵌入式开发、机器学习方面的学习笔记~文章来源地址https://www.toymoban.com/news/detail-412470.html
到了这里,关于STM32学习笔记(四)丨TIM定时器及其应用(定时中断、内外时钟源选择)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!