目录
6、TIM(Timer)定时器
Delay函数
前面一直用的delay函数
Delay.h
#ifndef __DELAY_H
#define __DELAY_H
void Delay_us(uint32_t us);
void Delay_ms(uint32_t ms);
void Delay_s(uint32_t s);
#endif
Delay.c
#include "stm32f10x.h"
/**
* @brief 微秒级延时
* @param xus 延时时长,范围:0~233015
* @retval 无
*/
void Delay_us(uint32_t xus)
{
SysTick->LOAD = 72 * xus; //设置定时器重装值
SysTick->VAL = 0x00; //清空当前计数值
SysTick->CTRL = 0x00000005; //设置时钟源为HCLK,启动定时器
while(!(SysTick->CTRL & 0x00010000)); //等待计数到0
SysTick->CTRL = 0x00000004; //关闭定时器
}
/**
* @brief 毫秒级延时
* @param xms 延时时长,范围:0~4294967295
* @retval 无
*/
void Delay_ms(uint32_t xms)
{
while(xms--)
{
Delay_us(1000);
}
}
/**
* @brief 秒级延时
* @param xs 延时时长,范围:0~4294967295
* @retval 无
*/
void Delay_s(uint32_t xs)
{
while(xs--)
{
Delay_ms(1000);
}
}
STM32中功能最强大、结构最复杂的外设——定时器。
定时器共四个部分,分为八个小节笔记。本小节为第一部分第一节。
在第一部分,是定时器的基本定时的功能:定时中断功能、内外时钟源选择
在第二部分,是定时器的输出比较功能,最常见的用途是产生PWM波形,用于驱动电机等设备
在第三部分,是定时器的输入捕获功能和主从触发模式,来实现测量方波频率
在第四部分,是定时器的编码器接口,能够更加方便读取正交编码器的输出波形,编码电机测速
实验一:定时中断的功能,定时器使用内部时钟定了一个一秒的时间,每隔一秒申请一下中断,然后在中断函数里执行number++,最后在OLED上显示number。
实验二:定时器外部时钟,这个程序使用了外部时钟来驱动定时器,我们可以在定时器指定的外部引脚上输入一个方波信号,来提供定时器计数的时钟。现在这里我暂时用这个对射式红外传感器来手动模拟一个外部时钟。我们用挡光片依次遮挡、移开、遮挡、移开、提供一个方波,可以看到OLED上下面这个CNT就是定时器中计数器的值,每遮挡、移开一次计数器加一,然后计数器寄到九后自动清零,同时申请中断,执行number++。
使用定时器的外部时钟可以提供一个更加精确的时钟来计时,或者也可以把外部时钟当做一个计数器,用来统计引脚上电平翻转的次数,毕竟定时器本质上就是一个计数器。
TIM简介
TIM(Timer)定时器,可定时触发中断
定时器本质上就是一个计数器,当这个计数器的输入是一个准确可靠的基准时钟的时候,那他在对这个基准时钟进行计数的过程,实际上就是计时的过程。
定时器可以对输入的时钟进行计数(在stm32中定时器的基准时钟一般是主频72MHz,如果对72MHz记72个数,那就是1MHz也就是1us的时间(72MHz就是1秒记72M个数,可以理解为对72个数计数1M次,记72个数的频率就是1MHz,用时1us)),如果记72000个数,那就是1KHz也就是1ms的时间,并在计数值达到设定值时触发中断。
stm32的定时器拥有16位(2的16次方是65536)的计数器(计数器就是用来执行计数定时的寄存器,每来一个时钟,计数器加1)、16位预分频器(可以对计数器的时钟进行分频,让计数更加灵活)、16位自动重装寄存器(是计数的目标值,计多少个时钟申请中断),这几个寄存器构成了定时器最核心的部分,我们把这一块电路称为时机单元,在72MHz计数时钟下可以实现最大59.65s的定时(也就是预分频器设置最大,自动重装也设置最大)。
为什么在72MHz计数时钟下可以实现最大59.65s的定时?
72M/65536/65536,得到的是中断频率,然后取倒数,就是59.65秒多,大家可以自己算一下。
详细解释:在定时器中,预分频器和计数器都是16位的,所以它们的最大值是65535,而不是65536。预分频器的最大值决定了计数时钟的频率,而计数器的最大值决定了定时器的最大计数周期。因此,如果预分频器和计数器的最大值都设置为65535,那么定时器的最大时间就是72MHz/65536/65536,得到的是中断频率,倒数就是中断时间。【最大值是65536,但计数是从0~65535】
这就是最大的定时时间,应该说还是挺长的了。如果你嫌这个还不够长,STM32的定时器还支持级联的模式,也就是一个定时器的输出当做另一个定时器的输入,这样加一起最大的定时时间就是59.65秒,再乘两次6536,这个时间大概是8000多年。如果还嫌短,那就再级联一个定时器,定时时间还会再延长6536x6536倍,这个可见指数爆炸的威力,小小的STM32利用3个定时器几年就能实现丈量宇宙年龄的能力。
定时器不仅具备基本的定时中断功能,而且还包含内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等多种功能。
定时器根据复杂度和应用场景分为了高级定时器、通用定时器(最常用)、基本定时器三种类型。
定时器类型
类型 | 编号 | 总线 | 功能 |
---|---|---|---|
高级定时器 | TIM1、TIM8 | APB2 | 拥有通用定时器全部功能,并额外具有重复计数器、死区生成、互补输出、刹车输入等功能 |
通用定时器 | TIM2、TIM3、TIM4、TIM5 | APB1 | 拥有基本定时器全部功能,并额外具有内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等功能 |
基本定时器 | TIM6、TIM7 | APB1 | 拥有定时中断、主模式触发DAC的功能 |
除了TIM1-8,在库函数中还出现了TIM9、10、11等(这些一般都用不到,知道就好)
三种定时器所连的总线是不一样的,其中高级定时器连接的是性能更高的APB2总线,通用定时器和基本定时器连接的是APB1中线,这个在RCC开启时钟的时候要注意一下。
最后看一下功能:
先看基本定时器,它的功能最少只有基本的定时中断功能和一个主模式触发DAC的功能,所以基本定时器还可以和DAC联合使用,这就是基本定时器,还是比较简单的。
然后是通用定时器,这就复杂起来了,它拥有基本定时器全部功能,同时它还额外具有内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等功能,这些功能就是我们本课程重点讲的。
最后就是高级定时器了,这就更复杂了,它拥有通用定时器全部功能,同时它还具有重复计数器、死区生成、互补输出、刹车输入等功能,这些功能主要是为了三相无刷电机的驱动设计的,我们本课程暂时不会涉及到。
STM32F103C8T6定时器资源:TIM1、TIM2、TIM3、TIM4,也就是一个高级定时器和三个通用定时器,没有基本定时器;不同的型号,定时器的数量是不同的。你在操作这个外设之前,一定要查一下它是不是有这个外设,别操作到了不存在的外设,那样是不会起作用的。
接下来,我们就依次来看一下高级定时器、通用定时器和基本定时器的结构图,看一下这三种定时器是怎么样来工作的,设计这些结构都能完成哪些任务。
基本定时器
理解时基单元的工作流程(定时器产生中断的全部流程)、主模式触发DAC的功能,如下内容:
下面这三个构成了最基本的计数计时电路,所以这一块电路就叫做时基单元
时基单元:预分配器(PSC)、自动重装载寄存器(ARR)、计数器(CNT)
时基单元之前连接的就是基准计数时钟的输入,最终来到了这个位置。由于基本定时器只能选择内部时钟,所以你可以直接认为这根线直接连到了输入端的这里,也就是内部时钟CK_INT,内部时钟的来源是RCC_TIMxCLK,这里的频率值一般都是系统的主频72MHz,所以通向时基单元的计数基准频率就是72MHz。
进入时基单元首先是预分频器(PSC),它可以对72MHz的计数时钟进行预分频(比如,预分频器写0就是不分频输出72MHz,这时候输出频率等于输入频率等于72MHz,写1是进行二分频输出36MHz,写2是三分频输出24MHz …,所以预分频的值和实际的分频系数相差1,即,实际分频系数=预分频器的值+1),预分频器是16位的,最大值可以写65535,也就是最大65536分频。
然后是计数器,对预分频后的计数时钟进行计数,计数时钟每来一个上升沿,计数器的值加1,这个计数器的值也是16位的,值可以从0一直加到65535,如果再加的话,计数器就会回到0重新开始。所以计数器的值在计时过程中会不断地自增运行,当自增运行到目标值时,产生中断,那就完成了定时的任务,所以还需一个存储目标值的寄存器,那就是自动重装载寄存器了。
自动重装寄存器也是16位的,它存的是我们写入的计数目标,在运行的过程中,计数值不断自增,自动重装载是固定的目标,当计数值等于自动重装值时,也就是计时时间到了,那它就会产生一个中断信号,并且清零计数器,计数器自动开始下一次的计数计时。在这里图上画的一个向上的折线箭头就代表这里会产生中断信号,像这种计数值等于自动重装值产生的中断,叫做“更新中断”,这个更新中断之后就会通向NVIC,我们再配置好NVIC的定时器通道,那定时器的更新中断就能够得到CPU的响应。这里向下的箭头代表的是会产生一个事件,这里对应的事件就叫做“更新事件”,更新事件不会触发中断,但可以触发内部其他电路的工作。
总结定时器产生中断的全部流程:从基准时钟到预分频器再到计数器,计数器计数自增,同时不断地与自动重装寄存器进行比较,值相等时,即计时时间到,这时就会产生一个更新中断和更新事件,CPU响应更新中断,就完成了我们定时中断的任务了。
下图红圈,是一个向上的折线箭头,就代表这里会产生中断信号,像这种计数值等于自动重装值产生的中断,叫做“更新中断”。
下图红圈,是一个向下的折线箭头,代表的是产生一个事件,这里对应的事件就叫做“更新事件”,更新事件不会触发中断,但可以触发内部其它电路的工作。
主模式触发DAC的功能
下面,简单介绍一下(后续讲),主模式触发DAC的功能,STM32定时器的一大特色就是主从触发模式(主从触发模式能让内部的硬件在不受程序的控制下实现自动运行),如果能把主从触发模式掌握好,那在某些情景下将会极大地减轻CPU的负担。
主模式触发DAC的作用就是,在我们使用DAC的时候,可能会用DAC输出一段波形,那就需要每隔一段时间来触发一次DAC,让它输出下一个电压点。如果用正常的思路来实现的话,就是先设置一个定时器产生中断,每隔一段时间在中断程序中调用代码手动触发一次DAC转换,然后DAC输出,这样会使主程序处于频繁被中断的状态,这会影响主程序的运行和其他中断的响应,所以定时器就设计了一个主模式,使用这个主模式可以把定时器的更新事件映射到触发输出TRGO(Trigger Out)的位置,然后TRGO直接接到DAC的触发转换引脚上,这样,定时器的更新就不需要再通过中断来触发DAC转换了,仅需要把更新事件通过主模式映射到TRGO,然后TRGO就会直接区触发DAC,整个过程不需要软件的参与,实现了硬件自动化,这就是主模式的作用,当然除了主模式外,还有更多硬件自动化的设计(后续讲)。
这个可编程定时器的主要部分是一个带有自动重装载的16位累加计数器,计数器的时钟通过一个预分频器得到。
软件可以读写计数器、自动重装载寄存器和预分频寄存器,即使计数器运行时也可以操作。时基单元包含:
预分频寄存器(TIMx_PSC)
预分频器
预分频可以以系数介于1至65536之间的任意数值对计数器时钟分频,就是对输入的基淮频率提前进行一个分频的操作。它是通过一个16位寄存器(TIMx-PSC)的计数实现分频。因为TIMx-PSC控制寄存器具有缓冲,可以在运行过程中改变它的数值,新的预分频数值将在下一个更新事件时起作用。
假设这个寄存器写0,就是不分频,或者说是1分频,这时候输出频率=输入频率=72MHz;如果预分频器写1,那就是2分频,输出频率=输入频率/2=36MHz,所以预分频器的值和实际的分频系数相差了1,即实际分频系数=预分频器的值+1。
时序图讲解32:34
注意:实际的设置计数器使能信号CNT_EN相对于CEN滞后一个时钟周期。
计数器寄存器(TIMx_CNT)
计数器由预分频输出CK_CNT驱动,设置TIMx_CR1寄存器中的计数器使能位(CEN)使能计数器计数。这个计数器可以对预分频后的计数时钟进行计数,计数时钟每来一个上升滑,计数器的值就加1,由于这个计数器也是16位的,所以里面的值可以从0一直加到65535,如果再加的话,计数器就会回到0重新开始。所以计数器的值在计时过程中会不断地自增运行,当自增运行到目标值时,产生中断,那就完成了定时的任务,所以现在还需要一个存储目标值的寄存器,那就是自动重装寄存器了。
通用定时器
通用定时器结构就瞬间复杂了很多。
1.通用定时器与基本定时器异同
首先,中间最核心的部分,还是时基单元,这部分结构和工作流程和基本定时器是一样的,不过对于通用定时器而言,计数器的计数模式就不止向上计数一种了(向上自增),通用定时器和高级定时器支持向上计数模式、向下计数模式和中央对齐模式。(基本定时器仅支持向上计数模式)。最常用的还是向上计数模式。
-
向下计数模式就是从重装值开始,向下自减,减到0之后,回到重装值同时申请中断,然后继续下一轮,依次循环
-
中央对齐模式就是从0开始,先向上自增,计到重装值,申请中断,然后再向下自减,减到0,再申请中断,然后继续下一轮,依次循环
2.内外时钟源选择和主从触发模式
-
如下,是内外时钟源选择和主从触发模式的结构。
-
内外时钟源选择:对于基本定时器,定时只能选择内部时钟,也就是系统频率72MHz;对于通用定时器,时钟源可以选择内部时钟或者外部时钟。
外部时钟的选择有如下四种:
第一个外部时钟就是来自TIMx_ETR引脚上的外部时钟,TIMx_ETR(External)引脚的位置可以参考引脚定义表,中关于默认复用功能和重定义功能的定义,如下图所示。可以看到这里有TIM2_CH1_ETR,意思就是这个TIM2的CH1和ETR都复用在了引脚PA0上。下面还有其他定时器的引脚CH2、CH3、CH4和其他定时器的一些引脚也可以在表中找到。
这里可以在TIM2的ETR引脚也就是PA0上接一个外部方波时钟,然后配置一下内部的极性选择、边沿检测和预分频器电路,再配置一下输入滤波电路,这些电路可以对外部时钟进行一定的整形(因为是外部时钟,所以难免会有毛刺,这些电路就可以对输入的波形进行滤波),同时也可以选择一下极性和预分频器,最后滤波后的信号,兵分两路,上面一路ETRF进入触发控制器,紧跟着就可以选择作为时基单元的时钟了,在stm32中,这一路也叫做‘外部时钟模式2’(如图中红线);另一路与其他信号通过数据选择器输出TRGI(Trigger In,触发输入),从名字上来看,它主要是作为触发输入来使用的,这个触发输入可以触发定时器的从模式。关于触发输入和从模式的内容之后再涉及,本节主要把这个触发输入当作外部时钟来使用的情况,你暂且可以把这个TRGI当做外部时钟的输入来看,当这个TRGI当作外部时钟来使用时,这一路就称为外部时钟模式1(如图中黄线所示)。
那通过这一路的外部时钟都有哪些呢,往左看,第一个就是ETR引脚的信号,这里ETR引脚既可以通过上面这一路来当做时钟,又可以通过下面这一路来当做时钟,两种情况对于时钟输入而言是等价的,只不过是下面这一路输入会占用触发输入的通道而已。
- 第二个外部时钟可以是来自其他定时器的信号ITR
主模式的输出TRGO可以通向其他定时器,实际上通向的就是ITR引脚,通过这一路就可以实现定时器级联的功能。如上黄线所示,ITR0到ITR3分别来自其他4个定时器的TRGO输出,具体的连接方式如下表所示,这就是ITR和定时器的连接关系,实现定时器级联功能。例如,可以先初始化TIM3,然后使用主模式把它的更新事件映射到TRGO上,接着再初始化TIM2,选择ITR2对应的就是TIM3的TRGO,然后后面再选择时钟为外部时钟模式1,这样TIM3的更新事件就可以驱动TIM2的时基单元,也就是实现了定时器的级联。
-
第三个外部时钟可来自TIMx_CH1的TI1_ED,CH1引脚的边沿,即从CH1引脚连接的输入捕获模块获得时钟,ED意为Edge边沿,意为通过这一路的时钟,上升沿和下降沿均有效。
-
第四个外部时钟可来自TIMx_CH1的TI1FP1和来自TIMx_CH2的TI2FP2
总结一下,外部时钟模式1的输入可以是ETR引脚、其他定时器、CH1引脚的边沿、CH1引脚和CH2引脚,还是比较复杂的,一般情况下外部时钟通过ETR引脚就可以了;
下面设置这么复杂的输入,不仅仅是为了扩大时钟树的范围,更多的还是为了某些特殊应用场景而设计的,比如未来定时器的级联而设计的这一部分,下面这一部分我们之后讲输入捕获和测频率时还会继续讲的。
对于时钟输入而言,最常用的还是内部的72兆赫兹的时钟,如果要使用外部时钟,首选ETR引脚外部时钟模式2的输入,这一路最简单最直接。
编码器模式
最后这里还有一块没有讲到,这个是定时器的一个编码器接口(红框下方),可以读取正交编码器的输出波形,这个我们后续课程也会再讲。
主模式输出
这部分电路可以把内部的一些事件映射到这个TRGO引脚上,比如我们刚才讲基本定时器分析的,将更新事件映射到TRGO,用于触发DAC。这里也是一样,它可以把定时器内部的一些事件映射到这里来,用于触发其它定时器、DAC或者ADC,可见这个触发输出的范围是比基本定时器更广一些的。
输出比较电路
通用定时器结构图的右下角即为定时器的输出比较功能的结构,如下图所示。有四个输出通道,分别对应CH1到CH4的引脚,可以用来输出PWM波形,驱动电机。
输入捕获电路
通用定时器的左下角即为输入捕获电路的结构图,它同输出比较功能一样有四个通道,对应CH1到CH4引脚。可以用于测量输入方波的频率。因为输入捕获和输出比较不能同时使用,故中间的捕获/比较寄存器是输入捕获和输出比较电路共用的,CH1到CH4的引脚也是共用的。
那有关输入捕获和输出比较这部分电路,在之后具体分析。
高级定时器
高级定时器的大部分结构和通用定时器相同,只在部分作了功能拓展。相比于通用定时器,拓展了框图右边红圈的内容。
1.重复次数计数器
在申请中断的的地方,增加了一个重复次数计数器,它的作用是:可以实现每隔几个计数周期,才发生一次更新事件和中断。原来的结构是每个计数周期完成后就都会发生更新,现在这个计数器实现每隔几个周期再更新一次,相当于对输出的更新信号又作了一次分频。(对于高级定时器,我们之前计算的最大定时时间59秒多,在这里就还需要再乘一个65536,也就是提升了很多的定时时间).
下面部分,是高级定时器对输出比较模块的升级了,暂时了解即可
2.死区生成电路与三相无刷电机
图中的DTG和DTG寄存器组成死区生成电路,右侧的引脚TIMx_CH1/CH2/CH3由原来的每路一个变成了两个互补的输出引脚(TIMx_CH1/CH2/CH3和TIMx_CH1N/CH2N/CH3N),可以输出一对互补的PWM波。这些电路是为了驱动三相无刷电机设计的。在四轴飞行器、电动车后轮、电钻中都可以发现三相无刷电机。三相无刷电机的驱动电路需要三个桥臂,每个桥臂需要2个大功率开关管来控制,总共需要6个大功率开关管控制。所以输出的PWM引脚的前三路就变为了互补的输出引脚,而第四路TIMx_CH4没有变化。三相电机只需要三路。
为了防止互补输出的PWM驱动桥臂时,在开关切换的瞬间,由于器件的不理想,造成短暂的直通现象,故前面添加了死区生成电路。在开关切换的瞬间,产生一定时长的死区,让桥臂的上下管全部关断,防止出现直通现象。
3.刹车输入
刹车输入的主要作用是给电机驱动提供安全保障。如果外部引脚BKIN(Break In)产生了刹车信号,或者内部时钟失效,产生了故障,控制电路就会自动切断电机的输出,防止意外的发生。
定时中断基本结构
定时中断的基本结构如下图所示:
首先中间最重要的还是PSC(Prescaler)预分频器、CNT (Counter)计数器、ARR (AutoReloadRegister)自动重装器这三个寄存器构成的时基单元。下面是运行控制,就是控制寄存器的一些位,比如启动停止、向上或向下计数等等,我们操作这些寄存器就能控制时基单元的运行了。
左边是为时基单元提供时钟的部分,这里可以选择RCC提供的内部时钟,也可以选择ETR引脚提供的外部时钟模式2。在本小节示例程序里,第一个定时器定时中断就是用的内部时钟这一路,第二个定时器外部时钟就是用的外部时钟模式2这一路。当然还可以选择这里的触发输入当做外部时钟,即外部时钟模式1,对应的有ETR外部时钟、ITRx其他定时器、Tlx输入捕获通道,这些就是定时器的所有可选的时钟源了。最后这里,还有个编码器模式,这一般是编码器独用的模式,普通的时钟用不到这个。
接下来右边这里,就是计时时间到,产生更新中断后的信号去向。如果是高级定时器的话还会多一个重复计数器。那这里中断信号会先在状态寄存器里置一个中断标志位,这个标志位会通过中断输出控制,到NVIC申请中断。
为什么会有一个中断输出控制呢?
因为这个定时器模块有很多地方都要申请中断。比如上面这个图不仅更新要申请中断,这里触发信号也会申请中断,还有下面的输入捕获和输出比较匹配时也会申请。所以这些中断都要经过中断输出控制,如果需要这个中断,那就允许,如果不需要,那就禁止。简单来说,这个中断输出控制就是一个中断输出的允许位,如果需要某个中断,就记得允许一下。
时基单元运行时序举例
STM32中,关于时序运行的内容很多,具体请见手册的详细讨论,这里仅举一些时基单元的例子作简要分析。
1.预分频器时序分析
图中描述了当预分频器的分频系数从1变为2时,计数器的时序图。第一行是CK_PSC是预分频器的输入时钟,选内部时钟的话一般是72MHz,这个时钟在不断运行;下面的CNT_EN是计数器使能,高电平计数器正常运行,低电平计数器停止,再下面是CK_CNT是计数器时钟,既是预分频器的时钟输出也是计数器的时钟输入(前面时基框图里有)。开始时,计数器未使能,计数器时钟不运行;然后使能后,前半段,当计数器使能信号CNT_EN变为高电平后的下一个CK_PSC的高电平,定时器时钟CK_CNT接收CK_PSC。且此时预分频器的分频系数为1,PSC = 0,预分频器完成一分频,计数器时钟等于预分频前的时钟,即,CK_PSC = CK_CNT;后半段,预分频系数变为2,计数器时钟变为预分频前时钟的一半。
在计数器时钟的驱动下,下面的计数器寄存器也跟随时钟的上升沿不断自增;当计数器寄存器的值依次递增达到0xFC后立即跳变为0x00,说明重装载寄存器ARR设计的目标计数值就是0xFC,当计数值记到和重装值相等,并且下一个时钟来临时,计数值才清零,此时电路产生一个更新事件脉冲信号UEV,并产生中断信号,计数值清0。这就是一个计数周期的工作流程。
2.缓冲(影子)寄存器
然后是最下面的三行时序,描述的是预分频寄存器的一种缓冲机制,也就是这个预分频寄存器实际上是有两个:一个是倒数第三行的预分频控制寄存器,供我们读写用的,并不直接决定分频系数;另一个是倒数第二行的预分频缓冲寄存器(影子寄存器),才是真正起作用的寄存器。
比如我们在某个时刻把预分频寄存器由0改成了1,如果在此时立刻改变时钟的分频系数,那么就会导致这里在一个计数周期内,前半部分和后半部分的频率不一样,这里计数计到一半,计数频率突然就会改变了,这虽然一般并不会有什么问题,但是s t m32 的定时器比较严谨,设计了这个缓冲寄存器,这样当我在计数记到一半的时候,改变了分频值,这个变化并不会立刻生效,而是会等到本次计数周期结束时产生的更新事件,预分频计数器的值才会被传递到缓冲寄存器里面去,才会生效,所以在这里看到,即使我在计数中途改变了预分频值,计数频率仍然会保持为原来的频率,直到本轮技术完成,在下一轮计数时改变后的分频值才会起作用。
在定时器结构图中,有些寄存器的画法采用了方框下加阴影的方式,就说明该寄存器不是只有一个寄存器,而是有两个寄存器来形成缓冲机制,下图中包括预分频器、自动重装载寄存器、捕获比较寄存器都是的。实际上,真正使时序电路状态发生更改的都是影子寄存器。
最后 ,预分频器内部实际上也是靠计数来分频的,当预分频值为零时,计数器就一直为零,直接输出原频率,当预分频值为1时,计数器就0 1 0 1 0 1 0 1,这样计数,再回到零的时候输出一个脉冲,这样输出频率就是输入频率的二分频。预分频器的值和时间的分频系数之间有一个数的偏移。
计数器计数频率:CK_CNT = CK_PSC / (PSC + 1)。
3.计数器时序分析
1.计数器工作时序图如下。
内部分频因子为2,就是分频系数为2。第一行是内部时钟72MHz,第二行是时钟使能,高电平启动,第三行是计数器时钟,因为分频系数为2,所以这个频率是上面CK_INT除2,然后计数器在这个时钟每个上升沿自增,当增到0036时发生溢出,之后再来一个上升沿,计数器清零,产生一个更新事件脉冲,另外还会置一个更新中断标志位UIF,标志位UIF只要置1就会去申请中断,然后中断响应后,需要在中断程序中手动清零,以上就是计数器的工作流程。
计数器溢出频率:CK_CNT_OV = CK_CNT / (ARR + 1) = CK_PSC/ (PSC + 1) / (ARR + 1)
CK_PSC=72 MHz
计数器溢出时间:1/计数器溢出频率,这就是我们计算定时时间(溢出时间)的式子。
- 计数器无预装时序图(缓冲机制失效 设置APRE = 0)
那刚才说了,预分频器为了防止计数中途更改数值造成错误,设计了缓冲寄存器,这个计数器也少不了这样的设计,并且这个缓冲计算器是用还是不用,是可以自己设置的。
计数器无预装时序就是没有缓冲寄存器的情况。
在计数器正在进行自增计数,突然更改了自动加载寄存器,就是自动重装载寄存器ARR,由FF改成了36,即计数值的目标值就由FF变成了36,所以计数器寄存器计到36之后,就直接更新,开始下一轮计数。
3.计数器有预装时序(缓冲机制有效 APRE = 1)
计数器有预装时序就是有缓冲寄存器的情况。
通过设置ARPE位,就可以选择是否使用预装功能。
在有预装的情况下,在计数中途,若突然将自动加载寄存器计数目标由F5改成了36,下面影子寄存器才是真正起作用的,它还是F5,所以现在计数的目标还是计到F5,产生更新事件,同时,要更改的36才被传递到影子寄存器,在下一个计数周期这个更改的36才有效(类似10086,本月更改,下月生效),所以引入影子寄存器的目的实际上是为了同步,就是让值的变化和更新事件同步发生,防止在运行途中更改造成错误。
在上面这个例子中,若不用影子寄存器的话,更改TIMx_ARR寄存器的值有一种不严谨情况:当F5改到36立即生效,但此时计数器已经到了F1,已经超过36了,F1只能增加,但它的目标值却是36比F1小,此时计数器寄存器的值只能递增,故该寄存器会一直递增到最大值0xFFFF之后回到0x0000,再依次递增,再加到36,才能产生更新。这里就可以看出,如果不使用缓冲机制,可能会给电路时序的工作造成一些问题。
当然如果你不介意这样的问题的话,那就不用管这些细节了,毕竟STM32设计出来要考虑到各种各样的情况。
4.RCC时钟树简介
为什么需要时钟?
参考:超清晰STM32时钟树动画讲解门电路的竞争冒险:
光速传播很快,但是逻辑门内部运算涉及MOS管的充放电过程需要一定时间,这样内部电平时序有些混乱
周期性驱动内部边沿触发器(类似心脏) ,此时等电路稳定了再进行电平比较
RCC时钟树:在STM32中用来产生和配置时钟,并且把配置好的时钟发送到各个外设的系统。 时钟是所有外设运行的基础,所以时钟是最先配置的东西。在执行主程序之前还会执行一个SystemInit函数,这个函数的作用就是配置RCC时钟树。这个结构看上去挺复杂的,配置起来还是比较麻烦的,不过好在ST公司已经帮我们写好了配置这个时钟数的SystemInit函数。
RCC时钟树可以分为左右两部分:时钟产生电路(左)和时钟分配电路(右)。中间的SYSCLK就是系统时钟72MHz。
1.时钟产生电路
在时钟产生电路,有四个振荡源,分别是内部的8MHz高速RC振荡器、外部的4-16MHz高速石英晶体振荡器(也就是晶振,一般都外接8MHz)、外部的32.768kHz低速晶振振荡器(一般给RTC提供时钟)、内部的40kHz低速RC振荡器(给看门狗WDG提供时钟)。上面的两个高速晶振是用来提供系统时钟的,AHB\APB2\APB1的时钟都是来源于这两个高速晶振。内部和外部都有一个8MHz的晶振,都是可以用的,只不过外部的石英振荡器比内部的RC振荡器更加稳定,所以一般都用外部晶振。如果系统非常简单,且不需要过于精确的时钟,就可以使用内部的RC振荡器,这样可以省下外部的晶振电路。
在SystemInit函数中ST是这样来配置时钟的:首先会启动内部的8MHz高速RC振荡器产生时钟,选择该时钟为系统时钟,暂时以8MHz的内部时钟运行;然后再启动外部时钟,配置外部时钟信号流经如下图所示的电路:
外部晶振信号进入PLLMUL锁相环进行倍频,8MHz倍频9倍,得到72MHz,待锁相环输出稳定后,选择锁相环输出为系统时钟。这样就把系统时钟从8MHz切换为了72MHz,以上就是ST配置的过程,大家可以自己分析一下SystemInit函数。
这样分析之后,可以解决实际应用的一个问题:那就是如果外部晶振出问题,可能会出现程序时钟慢大概10倍的现象。如果外部时钟的硬件电路有问题(外部晶振引脚焊接短路或连接错误等),系统的时钟就无法切换到72MHz,会保持内部的8MHz运行。8M相比于72M大概就慢了10倍。
图中的CSS称为时钟安全系统,它同样负责切换时钟。CSS可以监测外部时钟的运行状态,一旦外部时钟失效,它就会自动把外部时钟切换为内部时钟,从而保证程序可以正常运行,防止程序卡死造成事故。另外在高级定时器的刹车输入功能中,也有CSS,一旦CSS检测到外部时钟失效,通过或门就会立刻反应到输出控制器,让输出控制的电机立刻停止,防止意外。(即切断输出控制引脚,切断电机输出,防止发生意外。)
2.时钟分配电路
首先系统时钟72MHz进入AHB总线,在AHB总线上有一个预分频器,在SystemInit函数配置的默认分频系数为1,所以AHB总线的时钟自然是72MHz。
之后信号进入APB1总线,APB1上同样有预分频器,这里SystemInit默认配置的分频系数为2,输出为36MHz,所以APB1总线的时钟为36MHz,但是前面定时器说过,所有定时器的时钟都是72MHz,这是为什么?通用定时器和基本定时器是接在APB1上的,但是APB1(APB2同理)连接定时器还有如图所示的以下结构:
通用定时器和基本定时器通过图中APB1下方的支路与APB1连接。由于APB1的预分频系数默认为2,则输出到定时器的时钟频率×2。APB2的预分频器的分频系数默认配置为1,其他流程与APB1同理。所以基本定时器,通用定时器,高级定时器的内部基准时钟都是72MHz,这样设计为我们使用定时器带来了方便,不用考虑不同定时器时钟不同的问题了(前提是不乱修改ST提供的SystemInit函数中的默认配置)。
在这些时钟输出这里,都有一个与门进行输出控制,控制位写的是外部时钟使能,这就是我们在程序中写RCC_APB2/1PeriphClockCmd作用的地方,外设时钟控制作用的地方,打开时钟就是在这个位置写1,让左边的时钟能够通过与门输出给外设。
那关于时钟数的内容,就讲到这里,剩下的还有一些给ADC、SDIO等这些提供时钟的电路,大家就自己看看了。
参考手册
继续深入学习,可查阅参考手册
示例程序(定时器定时中断&定时器外部时钟)
知识点get:
前面定时器框图里出现好几个滤波器,滤波器工作原理:可以滤掉信号的抖动干扰。在一个固定的时钟频率f下进行采样,如果连续N个采样点都为相同的电平,那就代表输入信号稳定了,就把这个采样值输出出去;如果这N个采样值不全都相同,那就说明信号有抖动,这时就保持上一次的输出,或者直接输出低电平也行,这样就能保证输出信号在一定程度上的滤波;这里的采样频率f和采样点数N都是滤波器的参数,频率越低,采样点数越多,滤波效果越好,不过相应的信号延迟就越大;采样频率f由内部时钟直接而来,也可以是由内部时钟加一个时钟分频而来,这个分频多少是由参数ClockDivision决定的,这个参数其实跟时基单元关系并不大,它的可选值可以选择1分频(也就是不分频),2分频和4分频,我们随便配置一个就好了,后面代码里使用1分频,也就是不分频 。
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_ETRClockMode2Config(TIM2,TIM_ExtTRGPSC_OFF,TIM_ExtTRGPolarity_NonInverted,0x00);
定时器初始化步骤如下:
第一步,RCC开启时钟,定时器的基准时钟和整个外设的工作时钟就都打开了
第二步,选择时基单元的时钟源,对于定时中断就选择内部时钟源
第三步,配置时基单元,包括预分频器、自动重装载器、计数模式等,这些参数用一个结构体就可配置好了
第四步,配置中断输出控制,允许更新中断输出到NVIC
第五步,配置NVIC,在NVIC中打开定时器中断通道并分配一个优先级
第六步,运行控制,使能一下计数器,要不然计数器是不会运行的。当定时器使能后,计数器就开始计数了,当计数器更新时,触发中断
最后再写一个定时器中断函数,这样这个中断函数每隔一段时间就能自动执行一次了。
第一步,RCC开启时钟,这个基本上每个代码都是第一步。在这里打开时钟后,定时器的基准时钟和整个外设的工作时钟就都会同时打开了
第二步,选择时基单元的时钟源。对于定时中断,我们就选择内部时钟源
void TIM_InternalClockConfig(TIM_TypeDef* TIMx);选择内部时钟
void TIM_ITRxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource);选择ITR其他定时器的时钟,参数TIMx选择要配置的定时器,参数TIM_InputTriggerSource选择要接入哪个其他的定时器
void TIM_TIxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_TIxExternalCLKSource, uint16_t TIM_ICPolarity, uint16_t ICFilter);选择TIx捕获通道的时钟,参数TIM_TIxExternalCLKSource选择TIx具体的某个引脚,还有两个参数是输入的极性和滤波器。
void TIM_ETRClockMode1Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity,uint16_t ExtTRGFilter);选择ETR通过外部时钟模式1输入的时钟,参数TIM_ExtTRGPrescaler外部触发预分频器,这里可以对ETR外部时钟再提前做一个分频,还有两个参数是输入的极性和滤波器。
void TIM_ETRClockMode2Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter);选择ETR通过外部时钟模式2输入的时钟。
void TIM_ETRConfig(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity,uint16_t ExtTRGFilter);这个不是用来选择时钟的,单独用来配置ETR引脚的预分频器、极性、滤波器这些参数
注:没选择时钟,会默认内部时钟
然后最后一个函数,TIM_ETRConfig,这个不是用来选择时钟的,就是单独用来配置ETR引脚的预分频器、极性、滤波器这些参数的
涉及函数如下:
void TIM_InternalClockConfig(TIM_TypeDef* TIMx)
作用:配置TIMx内部时钟
参数说明:
第三步,配置时基单元。包括这里的预分频器、自动重装器、计数模式等等,这些参数用一个结构体就可以配置好了。
涉及函数如下:
void TIM_TimeBaseInit(TIM_TypeDef* TIMx, TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct)
作用:根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx时基单元外设。
参数说明:
18:49~20:17
如何确定时间参数讲解
假设定时1s,也就是定时频率为1Hz,那我们就可以PSC给一个7200,ARR给一个10000,然后两个参数都再减一个1,因为预分频器和计数器都有1个数的偏差,所以这里要再减个1。然后注意这个PSC和ARR的取值都要在0~65535之间,不要超范围了
第四步,配置中断输出控制,允许更新中断输出到NVIC(开启更新中断到NVIC的通路)
涉及函数如下:
void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState)
作用:使能/失能TIM中断输出信号。
参数说明:
注:TIM_IT_Update 更新中断
在STM32库里还提及其它中断源
第五步,配置NVIC,在NMC中打开定时器中断的通道,并分配一个优先级。这部分在上节我们也用过,流程基本是一样的
涉及函数:
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup)
void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct)
第六步,就是运行控制了。整个模块配置完成后,我们还需要使能一下计数器。要不然计数器是不会运行的。当定时器使能后,计数器就会开始计数了,当计数器更新时,触发中断。
涉及函数如下:
void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState)
作用:启用或禁用指定的TIM外设。
参数说明:
这样初始化基本上就OK了,接下来,我们再看几个函数,因为在初始化结构体里有很多关键的参数,比如自动重装值和预分频值等等,这些参数可能会在初始化之后还需要更改,如果为了改某个参数还要再调用一次初始化函数,那太麻烦了。所所以这里有一些单独的函数,可以方便地更改这些关键参数。
比如这里的TIM_PrescalerConfig(TIM_TypeDef* TIMx, uint16_t Prescaler, uint16_t TIM_PSCReloadMode),就是用来单独写预分频值的,看一下参数,Prescaler,就是要写入的预分频值;后面还有个参数,PSCReloadMode,写入的模式。我们上一小节说了,预分频器有一个缓冲器,写入的值是在更新事件发生后才有效的,所以这里有个写入的模式,可以选择是听从安排,在更新事件生效,或者是,在写入后,手动产生一个更新事件,让这个值立刻生效。
TIM_CounterModeConfig(TIM_TypeDef* TIMx, uint16_t TIM_CounterMode);,用来改变计数器的计数模式,参数CounterMode,选择新的计数器模式。
TIM_ARRPreloadConfig(TIM_TypeDef* TIMx, FunctionalState NewState);,自动重装器预装功能配置,前面讲过,直接配置使能或失能就可以。
TIM_SetCounter(TIM_TypeDef* TIMx, uint16_t Counter);,给计数器写入一个值。如果你想手动给一个计数值,就可以用这个函数
TIM_SetAutoreload(TIM_TypeDef* TIMx, uint16_t Autoreload);给自动重装器写入一个值,如果你想手动给一个自动重装值,就可以用这个函数
uint16_t TIM_GetCounter(TIM_TypeDef* TIMx);获取当前计数器的值,如果你想看当前计数器计到哪里了,就可以调用一下这个函数,返回值就是当前的计数器的值
uint16_t TIM_GetPrescaler(TIM_TypeDef* TIMx);获取当前的预分频器的值
最后我们再写一个定时器的中断函数。这样这个中断函数每隔一段时间就能自动执行一次了。
本次实验要完成的现象是:定义一个 uint16_t 的 Num 变量,使其每秒+1。
Timer.c
#include "stm32f10x.h" // Device header
/*
定时器初始化
对应定时中断结构图
第一步,RCC开启时钟,定时器的基准时钟和整个外设的工作时钟就都打开了
第二步,选择时基单元的时钟源,对于定时中断就选择内部时钟源
第三步,配置时基单元,包括预分频器、自动重装载器、计数模式等,参数用结构体配置
第四步,配置输出中断控制,允许更新中断输出到NVIC
第五步,配置NVIC,在NVIC中打开定时器中断通道并分配一个优先级
第六步,运行控制,使能计数器,当定时器使能后,计数器就开始计数了,当计数器更新时,触发中断
最后再写一个中断函数,中断函数每隔一段时间就能自动执行一次
*/
void Timer_Init(void) //定时中断初始化代码
{
//初始化tim2,也就是通用定时器
//使用APB1的开启时钟函数,TIM2是APB1总线的外设
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
//选择时基单元的时钟,选择内部时钟;若不调用这个函数,系统上电也是默认是内部时钟
TIM_InternalClockConfig(TIM2);
//配置时基单元
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //指定时钟分频,1分频也就是不分频
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式,向上计数
/*定时1s也就是定时频率为1Hz,定时频率=72M/ (PSC + 1) / (ARR + 1) = 1s =1Hz,
那就可以PSC给7200,ARR给10000(1MHz等于10^6Hz),然后两个参数再减1
在这里预分频是对72M进行7200分频,得到的就是10k的计数频率,
在10k的频率下,计10000个数,就是1s的时间*/
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只有在更新事件时才会起作用
为了让写入的值立刻起作用,故在函数的最后手动生成了一个更新事件
但是更新事件和更新中断是同时发生的,更新中断会置更新中断标志位,手动生成一个更新事件,就相当于在初始化时立刻进入更新函数执行一次
在开启中断之前手动清除一次更新中断标志位,就可以避免刚初始化完成就进入中断函数的问题(避免出现上电后OLED从1开始计数)*/
TIM_ClearFlag(TIM2,TIM_FLAG_Update);
//使能中断到NVIC的通路
TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE); //开启更新中断到NVIC的通道
//NVIC中断
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//NVIC优先级分组
NVIC_InitTypeDef NVIC_InitTyStructure;
NVIC_InitTyStructure.NVIC_IRQChannel = TIM2_IRQn;// 定时器2在NVIC里的通道
NVIC_InitTyStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitTyStructure.NVIC_IRQChannelPreemptionPriority = 2;//抢占优先级
NVIC_InitTyStructure.NVIC_IRQChannelSubPriority = 1;//响应优先级
NVIC_Init(&NVIC_InitTyStructure);
//启动定时器
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 "Delay.h"
#include "OLED.h"
#include "Timer.h"
uint16_t Num;
int main(void)
{
OLED_Init(); //初始化OLED
Timer_Init(); //初始化定时器
OLED_ShowString(1,1,"Num:");
while(1)
{
OLED_ShowNum(1,5,Num,5);
OLED_ShowNum(2,5,TIM_GetCounter(TIM2),5);//CNT计数器值的变化情况(变化范围是ARR从0一直到自动重装值(10000-1))
}
}
//定时器2中断函数放在使用中断的main.c文件中;在startup文件中
void TIM2_IRQHandler(void) //当定时器产生更新中断时,这个函数就会自动被执行
{
//检查中断标志位
if(TIM_GetITStatus(TIM2,TIM_IT_Update) == SET)
{
//执行相应的用户代码
Num ++;//定时器每秒自动加一个Num全局变量
TIM_ClearITPendingBit(TIM2,TIM_IT_Update);//清除标志位
}
}
定时器外部时钟选择实验
-
可以在引脚定义图里找TIMx的ETR引脚是哪个
-
在上一个定时中断实例程序基础上进行更改;基本任务仍然是定时中断,时钟部分就不使用内部时钟了
对射式红外传感器,DO数字输出接到PA0引脚,这个PA0引脚就是TIM2的ETR引脚,我们就在这个引脚输入一个外部时钟。
本次实验要完成的现象是:用光敏传感器手动模拟一个外部时钟,定义一个 uint16_t 的 Num 变量,当外部时钟触发10次(预分频之后的脉冲)后Num + 1。器件连接图和程序源码如下所示:
提示:
这里手册里推荐配置是浮空输入,但是我一般不太喜欢浮空输入,因为一旦悬空,电平就会跳个没完,所以我准备给上拉输入,这也是可以的。
那什么时候需要用浮空输入呢?就是如果你外部的输入信号功率很小,内部的这个上拉电阻可能会影响到这个输入信号,这时就可以用一下浮空输入,防止影响外部输入的电平。
在6-1的基础上更改,尤其注意在第二步更改时基单元的时钟源,通过ETR引脚的外部时钟模式2配置。
void TIM_ETRClockMode2Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter)
作用:配置TIMx外部时钟模式2
参数说明:
Timer.c
#include "stm32f10x.h" // Device header
/*
定时器初始化
对应定时中断结构图
第一步,RCC开启时钟,定时器的基准时钟和整个外设的工作时钟就都打开了
第二步,选择时基单元的时钟源,对于定时中断就选择内部时钟源
第三步,配置时基单元,包括预分频器、自动重装载器、计数模式等,参数用结构体配置
第四步,配置输出中断控制,允许更新中断输出到NVIC
第五步,配置NVIC,在NVIC中打开定时器中断通道并分配一个优先级
第六步,运行控制,使能计数器,当定时器使能后,计数器就开始计数了,当计数器更新时,触发中断
最后再写一个中断函数,中断函数每隔一段时间就能自动执行一次
*/
void Timer_Init(void) //定时中断初始化代码
{
//初始化tim2,也就是通用定时器
//使用APB1的开启时钟函数,TIM2是APB1总线的外设
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
//外部模式2需要用到gpio,进行GPIOA的时钟配置
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0