这是定时器部分的第一个内容,主要讲解定时器基本定时功能,定一个时间,让定时器每隔一段时间产生一个中断,来实现每隔一段时间执行一段程序的目的,比如你要做个时钟、秒表,或者使用一些程序算法的时候,都需要用到定时中断这一个功能。
一、定时器基本功能介绍
-
定时器的基本功能:定时触发中断:
-
以做时钟秒表和使用程序算法为例,说明定时中断可实现每隔固定时间执行一段程序的目的。
-
展示第一部分的两个程序现象,包括定时器定时中断(使用内部时钟定一秒时间,每秒自动加一数字并在 OLED 上显示)和定时器外部时钟(用外部时钟驱动定时器,如用对射式红外传感器模拟外部时钟,计数器每遮挡一次加一,计到九自动清零并执行数字加一)。
-
-
定时器本质是计数器:
-
定时器的基本功能是对输入的时钟进行计数,并在计数值达到设定值时触发中断。这表明定时器的核心功能是基于计数实现的,当计数器输入准确可靠的基准时钟时,计数过程就是计时过程。
-
例如在 STM32 中,定时器的基准时钟一般是主频 72 兆赫兹。通过对这个基准时钟进行计数,可以实现精确的时间测量和定时控制。以 STM32 中定时器的主频 72 兆赫兹为例,进行具体的计数与时间计算。如对 72 兆赫兹计数 72 个数,就是一兆赫兹,也就是 1 微秒的时间;如果记 72000 个数,那就是 1000Hz,也就是 1ms 的时间。
-
-
定时器的其他功能
-
不仅具备基本的定时中断功能,而且还包含内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等多种功能
二、定时器结构及参数
-
核心部分组成:
-
定时器的核心部分由 16 位计数器、预分频器、自动重装寄存器构成的时基单元。这个时机单元是定时器实现定时功能的关键部分。
-
预分频器连接基准计数时钟的输入,可以对输入的时钟进行预分频,让计数更加灵活。这里计数器就是用来执行计数定时的一个寄存器,计数器对预分频后的时钟进行计数,每来一个时钟,计数器加一。自动重装寄存器就是存储计数的目标值,当计数值等于自动重装值时,产生更新中断和更新事件。
-
这些寄存器构成了定时器最核心的部分,我们把这一块电路称为时基单元。
-
-
定时时间:
-
在 72 兆赫兹技术时钟下,通过对时机单元中各个寄存器的设置,可以实现不同的定时时间。例如,如果预分频器和自动重装器都设置最大,二的 16 次方是 65536,定时器的最大定时时间就是 59.65 秒,接近一分钟。计算方法是 72 兆除以 65536 再除以 65536,得到中断频率,取倒数就是定时时间。
-
STM32 的定时器还支持级联模式,一个定时器的输出当做另一个定时器的输入,这样可以极大延长定时时间,大概为八千多年。如果三个定时器级联,最大定时时间大概是 34万亿年,可见指数爆炸的威力。
-
三、定时器的类型
一、基本定时器
-
功能特点:
-
功能最少,只有基本的定时中断功能和一个主模式触发 DAC 的功能。
-
可以和 DAC 联合使用,例如在使用 DAC 输出波形时,基本定时器的主模式触发 DAC 功能可将定时器的更新事件映射到触发输出引脚,直接触发 DAC 转换,实现硬件自动化,减轻 CPU 负担。
-
-
连接总线:
-
基本定时器连接的是 AB1 总线。
-
-
时钟选择:
-
只能选择内部时钟,即系统频率 72 兆赫兹。
-
-
结构图详解:(非常重要)
一、基本组成部分
-
预分频器、计数器和自动重装计算器:
-
这三个最重要的寄存器构成了最基本的计数计时电路,被称为时机单元。
-
预分频器连接基准计数时钟的输入,可以对输入的 72 兆赫兹内部时钟进行预分频,就是对输入的基准频率进行一个提前的分频的操作。预分频器的值和实际的分频系数相差一,即实际分频系数等于预分频器的值加一。预分频器是 16 位的,最大值可以写 65535,也就是 65536 分频。
-
计数器对预分频后的时钟进行计数,计数时钟每来一个上升沿,计数器的值就加一。计数器也是 16 位的,所以里面的值可以从零一直加到 65535。如果再加的话,计数器就会回到零,重新开始计数。
-
自动重装寄存器存储计数的目标值,也是 16 位的。在运行过程中,计数值不断自增,当计数值等于自动重装值时,会产生一个中断信号,并且清零计数器,计数器自动开始下一次的计数计时。
-
-
运行控制部分:
-
用于控制计数器的一些运行状态,比如启动、停止、向上或向下计数等。通过操作这些寄存器,可以控制时机单元的运行。
-
二、工作流程
-
时钟输入:
-
由于基本定时器只能选择内部时钟,所以基准计数时钟直接连到了输入端,内部时钟的来源是 RCC 的 TMx_CLK,一般为系统主频 72 兆赫兹。
-
-
预分频:
-
72 兆赫兹的时钟经过预分频器进行分频,根据预分频器的值确定输出频率。例如,预分频器写零,就是不分频,输出频率等于输入频率等于 72 兆赫兹;预分频器写一,就是二分频,输出频率等于输入频率除以二,等于 36 兆赫兹。
-
-
计数器计数:
-
预分频后的时钟作为计数器的计数时钟,计数器在计数时钟的上升沿自增。当计数值达到自动重装值时,产生中断信号和更新事件。
-
-
中断和事件处理:
-
产生的更新中断信号会通往 NVIC,在配置好 NVIC 的定时器通道后,定时器的更新中断就能够得到 CPU 的响应。同时,更新事件不会触发中断,但可以触发内部其他电路的工作。
-
三、主模式触发 DAC 功能
-
功能用途:
-
它能让内部的硬件在不受程序的控制下实现自动运行,在使用 DAC 输出波形时,可以利用基本定时器的主模式触发 DAC 的功能。通过将定时器的更新事件映射到触发输出引脚 TRGO,直接触发 DAC 转换,实现硬件自动化,减轻 CPU 负担。
-
-
实现方式:
-
如果用正常的思路实现,需要先设置一个定时器产生中断,每隔一段时间在中断程序中调用代码手动触发一次 DAC 转换,然后 DAC 输出。但这样会使主程序处于频繁被中断的状态,影响主程序的运行和其他中断的响应。
-
使用主模式,将定时器的更新事件映射到 TRGO,然后 TRGO 直接接到 DAC 的触发转换引脚上,这样定时器的更新就不需要再通过中断来触发 DAC 转换了,仅需要把更新事件通过主模式映射的 TRGO,然后 TRGO 就会直接去触发 DAC 的整个过程,不需要软件的参与,实现了硬件的自动化。
-
二、通用定时器
-
功能特点:
-
拥有基本定时器全部功能,同时还额外具有内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等功能。
-
计数器计数模式除了向上计数外,还支持向下计数模式和中央对齐模式。
-
-
连接总线:
-
通用定时器也连接 AB1 总线。
-
-
结构图详解(非常重要):
一、核心时基单元
-
结构与基本定时器相同:
-
通用定时器中间最核心的部分是由预分频器、计数器和自动重装计数器构成的时机单元,这与基本定时器的核心结构一致。
-
预分频器连接基准计数时钟的输入,可以对输入的时钟进行预分频,让计数更加灵活。计数器对预分频后的时钟进行计数,每来一个时钟,计数器加一。自动重装寄存器存储计数的目标值,当计数值等于自动重装值时,计数值清零,同时产生更新中断和更新事件。
-
二、计数模式
-
多种计数模式:
-
通用定时器的计数器计数模式不止向上计数一种。除了向上计数模式外,还支持向下计数模式和中央对齐模式。
-
向上计数模式是计数器从零开始向上自增,到达自动重装值后清零,同时申请中断,然后开始下一轮循环。这是最常用的计数模式,在很多应用场景中都能发挥重要作用。
-
向下计数模式是从自动重装值开始向下递减,减到零后回到自动重装值,同时申请中断,接着继续下一轮循环。这种模式在一些需要倒计时或者特定的控制逻辑中可能会用到。
-
中央对齐模式是从零开始先向上自增到自动重装值,申请中断,然后再向下自减减到零,再申请中断,之后继续下一轮循环。中央对齐模式相对复杂,适用于一些对计数精度和对称性要求较高的场合。
-
三、内外时钟源选择
-
内部时钟:
-
通用定时器时钟源可以选择内部的 72 兆赫兹时钟,和基本定时器一样,内部时钟的来源是 RCC 的 TMx_CLK,一般为系统主频 72 兆赫兹。内部时钟稳定可靠,使用方便,是最常用的时钟源之一。
-
-
外部时钟:
-
相比基本定时器只能选择内部时钟,通用定时器还可以选择外部时钟,这为定时器的应用提供了更多的灵活性。
-
外部时钟模式二:外部时钟来自 TMx_ETR 引脚上的外部时钟。可以在定时器指定的外部硬件上,如 TM2 的 ETR 引脚(PA0 引脚)上接一个外部方波时钟。通过配置内部的极性、选择边缘检测和预分频器电路、输入滤波电路等,可以对外部时钟进行整形,因为外部硬件的时钟难免会有些毛刺。滤波后的信号兵分两路,上面引入 ETIF 进入触发控制器,紧跟着就可以选择作为实际单元的时钟。如果想在 ETR 外部引脚提供时钟或者把定时器当做计数器来用,可以配置这一路的电路。
-
外部时钟模式一:触发输入 TRGI 可以作为外部时钟来使用。通过这一路的外部时钟有多种来源,包括 ETR 引脚的信号、ITR 信号(来自其他定时器的时钟信号,主模式的输出 TRGO 可以通向其他定时器,接到其他定时器的 ITR 引脚)、TIxFPx 信号(TI1FP1 连接到 CHE 引脚的时钟,TI2FP2 连接到 CH2IN 引脚的时钟等,其中 TIxFPx 后缀加 “d” 表示边缘,即通过这一路输入的时钟上升沿和下降沿均有效)。一般情况下,外部时钟首选 ETR 引脚,外部时钟模式二的输入最简单最直接。
-
四、主模式输出
-
触发范围更广:
-
右边的这部分电路可以把内部的一些事件映射到 TRGO 引脚上,用于触发其他定时器、DAC 或者 ADC。相比基本定时器,通用定时器的触发输出范围更广。例如,可以把定时器内部的更新事件映射到 TRGO,用于触发 DAC。这种主模式输出功能可以实现硬件的自动化,减轻 CPU 的负担。
-
五、输入捕获和输出比较电路
-
输入捕获电路:
-
左边是输入捕获电路,有四个通道,分别对应 CH1 到 CH4 的硬件。可以用于测量输入方波的频率等。输入捕获电路可以在特定的时刻捕获输入信号的状态,通过对捕获的时间点进行分析,可以计算出输入信号的频率、周期等参数。
-
具体工作过程是,当输入信号的特定边缘(上升沿或下降沿)到来时,定时器会记录当前计数器的值。通过比较不同边缘到来时的计数器值,可以计算出输入信号的时间间隔,从而得到频率等信息。
-
-
输出比较电路:
-
右边是输出比较电路,也有四个通道,对应 CH1 到 CH4 的硬件。可以用于输出 PWM 波形驱动电机。输出比较电路可以将计数器的值与预设的比较值进行比较,当满足特定条件时,输出特定的信号。
-
例如,在输出 PWM 波形时,可以通过设置自动重装寄存器的值来控制 PWM 波形的周期,通过设置比较值来控制 PWM 波形的占空比。这样可以实现对电机等设备的精确控制。
-
-
共用捕获比较寄存器:
-
中间的捕获比较寄存器是输入捕获和输出比较电路共用的,因为输入捕获和输出比较不能同时使用,所以这里的计算器和引脚是共用的。关于输入捕获和输出比较这部分电路,留到后面的文章分析,本节主要介绍定时中断和内外时钟源选择,暂时用不到这部分电路。
-
三、高级定时器
-
功能特点:
-
拥有通用定时器全部功能,同时还具有重复计数器、死区生成、互补输出、刹车输入等功能,这些功能主要是为了三相无刷电机的驱动设计。
-
增加的重复计数器可以实现每隔几个计数周期才发生更新事件和更新中断,提升了定时时间。
-
死区生成电路和互补的 PWM 输出引脚可输出一对互补的 PWM 波,用于驱动三相无刷电机,防止在开关切换瞬间由于器件不理想造成短暂的直通现象。
-
刹车输入功能为电机驱动提供安全保障,当外部引脚 BKIN 产生刹车信号或者内部时钟失效产生故障时,控制电路会自动切断电机的输出,防止意外发生。
-
-
连接总线:
-
高级定时器连接的是性能更高的 APB2 总线。
-
-
结构图详解:(非常重要)
一、整体结构与通用定时器对比
高级定时器在结构上与通用定时器有一定的相似性,但也存在一些关键的变化。左上大部分结构与通用定时器类似,主要改动在右边几个部分。
二、重复计数器
-
位置及作用:在申请中断的地方增加了一个重复次数计数器。这个计数器可以实现每隔几个计数周期才发生一次更新事件和更新中断。相当于对输出的更信号又做了一次分频。
-
工作原理:原来的结构是每个计数周期完成后都会发生更新,现在有了这个重复计数器,可以对输出的更新信号进行再次分频。例如,对于之前计算的高级定时器最大定时时间 59 秒多,有了这个计数器后,定时时间还会再乘一个 65536,进一步提升了定时时间。
三、输出比较模块升级
-
死区生成电路(DTG):
-
作用:为了防止互补输出的 PWM 驱动桥臂时,在开关切换的瞬间由于器件不理想造成短暂的直通现象。
-
工作方式:在开关切换的瞬间产生一定时长的死区,让桥臂的上下管全都关断,从而防止直通现象。
-
-
互补输出引脚:
-
高级定时器的输出引脚由原来的一个变为了两个互补的输出,可以输出一对互补的 PWM 波。这些电路是为了驱动三相无刷电机设计的。
-
因为三相无刷电机的驱动电路一般需要三个桥臂,每个桥臂两个大功率开关管来控制,总共需要六个大功率开关管的控制,所以这里的输出 PWM 引脚的前三路就变为了互补的输出,而第四路却没什么变化,因为三项电机只需要三路就行了。
-
四、刹车输入功能
-
目的:为电机驱动提供安全保障。
-
工作机制:如果外部引脚 BKIN 产生了刹车信号或者内部时钟失效产生了故障,那么控制电路就会自动切断电机的输出,防止意外的发生。
总结:
-
这三种定时器是由高级到低级向下兼容的,高级定时器拥有通用定时器的功能,通用定时器有基本定时器的功能。
-
STM32F103C8T6这款芯片它内部拥有定时器资源是TIM1、TIM2、TIM3、TIM4,也就是一个高级定时器,三个通用定时器,没有基本定时器
四、定时中断基本结构
一、整体结构概述
定时中断基本结构图是对定时器的关键部分进行了提取和展示,去除了其他无关内容,并添加了一些在原图中未体现的部分,以便着重分析定时中断和内外时钟源选择所涉及的结构。
二、关键组成部分
-
时机单元:
-
由 PSC 预分频器、CNT 计数器、ARR 自动重装计算器这三个寄存器构成。这是定时器的核心部分,负责实现计数计时功能。
-
PSC 预分频器:连接基准计数时钟的输入,可以对输入时钟进行预分频。例如,预分频器的值和实际的分频系数相差一,即实际分频系数等于预分频器的值加一。预分频器为 16 位,最大值可以写 65535,实现 65536 分频。
-
CNT 计数器:对预分频后的时钟进行计数,计数时钟每来一个上升沿,计数器的值就加一。计数器也是 16 位,计数值可以从零一直加到 65535,若再加则会回到零重新开始计数。
-
ARR 自动重装计算器:存储计数的目标值,也是 16 位。当计数值等于自动重装值时,会产生更新中断和更新事件,同时清零计数器,开始下一次计数计时。
-
-
运行控制部分:
-
用于控制计数器的一些运行状态,如启动、停止、向上或向下计数等。通过操作这些寄存器,可以控制时机单元的运行。
-
-
时钟选择部分:
-
左边是为实际单元提供时钟的部分,可以选择 RCC 提供的内部时钟,也可以选择 ETR 引脚提供的外部时钟模式二,还可以选择触发输入当做外部时钟(即外部时钟模式一)。
-
内部时钟:一般是系统主频 72 兆赫兹。
-
外部时钟模式二:外部时钟来自 TMx_ETR 引脚上的外部时钟。例如,可以在定时器指定的外部硬件上,如 TM2 的 ETR 引脚(PA0 引脚)上接一个外部方波时钟,通过配置内部的极性、选择边缘检测和预分频器电路、输入滤波电路等,可以对外部时钟进行整形。
-
外部时钟模式一:触发输入 TRGI 可以作为外部时钟来使用,对应的有 ETR 引脚时钟等多种来源。
-
-
编码器模式:
-
一般是编码器独用的模式,普通的时钟用不到这个。
-
三、中断信号走向
-
更新中断产生:
-
当计时时间到,即计数值等于自动重装值时,会产生更新中断。中断信号会先在状态寄存器里置一个中断标志位。
-
如果是高级定时器,这里还会多一个重复计数器,可以实现每隔几个计数周期才发生一次更新事件和更新中断。
-
-
中断申请流程:
-
中断标志位会通过中断输出控制到 NVIC 申请中断。因为定时器模块有很多地方都要产生中断,比如更新、触发信号、输入捕获和输出比较匹配时等都会申请中断,所以这些中断都要经过中断输出控制。中断输出控制可以看作是一个中断输出的允许位,如果需要某个中断,就将其允许,不需要则禁止。
-
五、时序图分析
预分频器时序
一、预分频器在定时器中的位置和作用
-
位置:预分频器位于定时器的实际单元中,与计数器和自动重装计算器一起构成了定时器最核心的部分。
-
作用:预分频器连接基准计数时钟的输入,可以对输入的时钟进行预分频,让计数更加灵活。例如,当选择内部时钟时,一般为系统主频 72 兆赫兹,经过预分频器后,输出给计数器的时钟频率会发生变化。
二、预分频时序图分析
-
时钟输入:
-
第一行是 CK_PSC(预分频器的输入时钟),如果选择内部时钟,一般是 72 兆赫兹。这个时钟在不断地运行,为预分频器提供输入信号。
-
下面的 CNT_EN(计数器使能)为高电平时,计数器正常运行;为低电平时,计数器停止。
-
-
预分频过程:
-
CK_CNT(计数器时钟)既是预分频器的时钟输出,也是计数器的时钟输入。开始时计数器未使能,计数器时钟不运行。
-
在前半段,预分频器系数为一,此时计数器的时钟等于预分频器前的始终,即未进行分频。
-
在后半段,预分频器系数变为二,计数器的时钟就变为预分频器输入时钟的一半。
-
-
计数器运行和更新事件:
-
在计数器时钟的驱动下,下面计数器寄存器也跟随始终的上升沿不断自增。在中间的某个位置(FC 之后),计数值变为零。可以推断出 ARR(自动重装值)就是 FC,当计数值记得和重装值相等,并且下一个时钟来临时,计数值才清零,同时下面这里产生一个更新事件。这就是一个计数周期的工作流程。
-
三、预分频器的缓冲机制
-
缓冲寄存器的作用:
-
预分频器实际上有两个,一个是供我们读写用的,它并不直接决定分频系数;另外还有一个缓冲寄存器(或影子寄存器),这个缓冲寄存器才是真正起作用的计算器。
-
例如,在某个时刻把预分频计算器由零改成了一,如果在此时立刻改变时钟的分频系数,那么就会导致在一个计数周期内,前半部分和后半部分的频率不一样。为了避免这种情况,STM32 的定时器设计了这个缓冲寄存器。
-
-
缓冲机制的工作原理:
-
当在计数记到一半的时候改变了预分频值,这个变化并不会立刻生效,而是会等到本次计数周期结束时产生的更新事件,预分频计数器的值才会被传递到缓冲寄存器里面去,才会生效。
-
所以即使在计数中途改变了预分频值,计数频率仍然会保持为原来的频率,直到本轮计数完成,在下一轮计数时改变后的分频值才会起作用。
-
-
预分频器内部计数分频:
-
预分频器内部实际上也是靠计数来分频的。当预分频值为零时,计数器就一直为零,直接输出将频率;当预分频值为一时,计数器就 01010101 这样计数,再回到零的时候输出一个脉冲,这样输出频率就是输入频率的二分频。预分频器的值和实际的分频系数之间有一个数的偏移,最下面有这样一个公式:计数器计数频率 CK_CNTG 等于 CK_PSC(预分频器输入频率) 除以 PSC(预分频值)加一。
-
计数器时序
一、计数器时序图整体结构
-
关键参数:
-
内部时钟分频因子为二,即分频系数为二。这个参数决定了计数器时钟的频率是内部时钟频率的一半。
-
第一行是内部始终 72 兆赫兹,这是 STM32 的内部时钟频率,在这个例子中作为计数器的输入时钟源。
-
-
各部分含义:
-
第二行是时钟使能,高电平表示启动计数器,低电平则计数器停止工作。
-
第三行是计数器时钟,由于分频系数为二,所以这个频率是内部时钟频率除以二。
-
计数器在这个时钟的每个上升沿自增,当计数值达到特定值时会产生溢出和更新事件。
-
二、计数器工作流程
-
计数过程:
-
计数器在计数器时钟的驱动下,每个上升沿计数值加一。例如,从初始值开始,随着时钟的不断上升沿,计数值逐渐增加。
-
当计数值增加到 0x36 的时候发生溢出。这是因为在这个例子中,计数器是 16 位的,当计数值达到最大值后再增加就会溢出。
-
-
溢出处理:
-
当计数器溢出时,即计数值达到最大值后再来一个上升沿,计数器清零。同时,计数器溢出会产生一个更新事件脉冲。
-
另外还会置一个更新中断标志位(UIF)。这个标志位只要被置位就会去申请中断,通知 CPU 有更新事件发生。
-
-
中断处理:
-
当更新中断标志位被置位后,会向 NVIC(Nested Vectored Interrupt Controller)申请中断。
-
中断响应后,需要在中断程序中手动清零更新中断标志位,以便下一次中断能够正常触发。
-
三、相关公式解释
-
计数器溢出频率公式:
-
计数器溢出频率 CK_CNT_OV 等于 CK_CNT 除以 ARR 加一。其中,CK_CNT 是计数器时钟频率,ARR 是自动重装值。如果想计算溢出时间只需要将结果取倒数即可。
-
这个公式的含义是,计数器溢出的频率取决于计数器时钟频率和自动重装值。当计数器时钟频率确定后,自动重装值越大,计数器溢出的频率就越低。
-
计数器无预装时序
一、更改时机与生效机制
在计数过程中可能会有需要更改自动重装寄存器值的情况。由于没有预装时序,更改自动重装寄存器的值不会立即生效。就像改变预分频值一样,这个变化要等到本次计数周期结束时产生的更新事件,自动重装寄存器的值才会被传递到实际起作用的寄存器中,从而生效。
二、对计数过程的影响
-
计数频率保持不变:在计数过程中更改自动重装寄存器的值,不会立即改变计数器的运行状态和频率。计数器会继续按照原来的自动重装值进行计数,直到本轮计数完成。
-
例如,在计数器正以特定频率进行计数时,如果此时更改自动重装寄存器的值,计数器不会立刻响应这个变化,仍然会按照原来的节奏进行计数。
-
-
新值在下一轮计数生效:在下一轮计数时,新的自动重装值才会起作用。
-
当本轮计数结束,产生更新事件后,新的自动重装值被传递到实际起作用的寄存器中。在接下来的下一轮计数中,计数器将根据新的自动重装值来确定何时产生更新事件。
-
计数器有预装时序
一、更改时机与生效机制
在计数过程中可能会有需要更改自动重装寄存器值的情况。由于有预装时序,更改自动重装寄存器的值不会立即生效。就像改变预分频值一样,这个变化要等到特定的时机才会生效。
在下图中,我们可以看到定时器的结构包括预分频器、计数器和自动重装寄存器等部分。这些部分共同构成了定时器的核心,决定了定时器的计数和定时功能。当我们在计数过程中更改自动重装寄存器的值时,这个变化不会立即影响计数器的运行。
二、对计数过程的影响
-
计数频率保持不变:在计数过程中更改自动重装寄存器的值,不会立即改变计数器的运行状态和频率。计数器会继续按照原来的自动重装值进行计数,直到特定的触发事件发生。
-
例如,在下图中,我们可以看到计数器在时钟的上升沿进行计数。如果在计数过程中更改自动重装寄存器的值,计数器不会立刻响应这个变化,仍然会按照原来的节奏进行计数。
-
-
新值在下一轮计数生效:当特定的触发事件发生时,新的自动重装值才会被传递到实际起作用的寄存器中,从而影响计数器的后续计数行为。
-
在下图中,这个特定的触发事件可能是当前计数周期结束或者下一个特定的中断发生。只有在这些时候,新的自动重装寄存器值才会生效,计数器才会按照新的目标值进行计数。
-
三、总结
由此可以看出引入这个影子寄存器的目的实际上是为了同步,就是让值的变化和更新事件的变化同步发生,防止运行途中更改发生错误。在这个例子也可以看出,如果在这里不使用影子寄存器的话,F5改到36立刻生效,但此时计数值已经到了F1,已经超过36了,F1只能增加,但它的目标却是36,比它还小,这样F1就只能一直加,一直加,一直加到FFFF,再回到0,再加到36,才能产生更新,这就会造成一些小问题
六、RCC时钟树
时钟树的简介:
这个时钟树,就是STM32中用来产生和配置时钟,并且把配置好的时钟发送到各个外设的系统,时钟是所有外设运行的基础,所以时钟也是最先需要配置的东西。前文,说过,程序中主函数之前还会执行一个Systemlnit函数,这个函数就是用来配置这个时钟树的。
结构图内容的详解:
-
该图中左侧均是时钟的产生电路,右侧都是时钟的分配电路,中间的这个 SYSCLK 就是系统时钟 72MHz。
-
时钟产生电路中的四个震荡源:
一、内部高速时钟(HSI)
- 默认频率及特点:
-
内部高速时钟由芯片内部的 RC 振荡器产生,默认频率一般为 8MHz。
-
相比外部高速时钟,HSI 具有启动速度快的优点,因为它不需要外部晶体振荡器的起振时间。
-
- 在时钟树中的作用:
-
可以作为 PLLSRC 的选择之一,与 HSE 一起为锁相环提供输入源。当外部高速时钟不可用时,HSI 可以作为备用时钟源,确保系统仍然能够正常运行。
-
在一些对时钟精度要求不高的应用场景中,HSI 可以直接作为系统时钟 SYSCLK 的输入源,为处理器内核和其他模块提供时钟。
-
二、外部高速时钟(HSE)
-
基本特征:
-
由 OSCI_N 和 OSC_OUT 表示的外部晶体振荡器,频率一般为 4 - 16MHz。
-
外部的4-16MHz高速石英晶体振荡器,也就是晶振,一般都是接8MHz
-
为系统提供较为准确的高频时钟源,可作为 PLLSRC(锁相环输入源)的选择之一。
-
-
与 PLL 的关系:
-
可以经过 PLLXTPRE 进行预分频处理后作为 PLLSRC。当选择 HSE 作为锁相环输入源时,PLL 可以进行倍频操作,例如 ×16,以产生更高频率的时钟输出 PLLCLK。
-
-
外设时钟使能控制:
-
外设时钟使能部分可以控制 HSE 是否为特定外设提供时钟,具有较高的灵活性和可配置性。
-
三、外部低速时钟(LSE)
-
频率及用途:
-
频率为 32.768Hz 的低速外部晶体振荡器。
-
主要为实时时钟(RTC)提供时钟源,通过 RTCSEL1:0 可以选择是否将其作为 RTC 的时钟输入,例如 ATCCLK。
-
-
在实时时钟中的重要性:
-
由于其较低的频率,非常适合用于对时钟精度要求不高但需要长时间稳定运行的实时时钟模块。确保在系统断电或低功耗模式下,实时时钟仍然能够准确地记录时间。
-
四、内部低速时钟(LSI)
-
产生方式:
-
内部 的40KHz低速RC 振荡器产生的低频时钟。
-
-
应用场景:
-
一般用于一些特定的低功耗场景或作为备用时钟源。例如,在看门狗(IWDOCLK)中可能会用到,当系统出现故障或异常时,看门狗可以使用 LSI 作为时钟源进行监测和复位操作。
-
在低功耗模式下,LSI 可以为一些关键的低功耗模块提供时钟,以降低系统的整体功耗。
-
总结:
上面这两个高速晶振,是用来提供系统时钟的,我们的 AHB、APB2、APB1 的时都是来源于这两个高速晶振,这里内部和外部都有一个8MHz的晶振,都是可以用的,只不过是外部的石英振荡器比内部的 RC 振荡器更加稳定,所以一般使用外部晶振,但是如果你系统很简单而且不需要那么精确的时钟,那也是可以使用内部RC振荡器的,这样就可以省下外部最振的电路了
-
CSS(Clock SecuritySystem)时钟安全系统
一、作用
负责切换时钟,它可以监测外部时钟的运行状态,一但外部时钟失效,它就会自动把外部时钟切换回内部时钟,保证系统时钟的运行,防止程序卡死造成事故
-
时钟分配电路:
1. 首先系统时钟 72MHz 进入 AHB 总线,AHB总线有个预分频器,在Systemlnit里配置的分配系数为1,那 AHB 的时钟就是 72MHz。然后进入APB1总线,这里配置的分配系数是2,所以 APB1 总线的时钟为 72MHz/2=36MHz。
补充:通用定时器和基本定时器是接在 APB1 上的,而 APB1 的时钟是 36MHZ, 按理说它们的时钟应该是 36MHz,但是我们在说定时器时一直都说的是所有定时器的时钟都是 72MHz的原因是,下面的线还有一条支路,写着如果APB1预分频系数=1,则频率不变,否则频率x2,然后再看右边,发现这一路是单独为定时器2~7开通的,又因为APB1的预分频系数给的是2,所以这的频率还需要再乘2,所以通向定时器2~7的时钟就又回到了72MHz。所以无论什么定时器他们内部的基准时钟都是72MHz。
2. 然后我们再看一下下面,APB2的时钟这里给的分频系数为1,所以APB2的时钟和AHB一样,都是72MHz。这里接在 APB2 上的高级定时器也单开了一路,上面写的也是如果APB2预分频系数=1,则频率不变,否则频率x2,但是这里APB2的预分频系数就是1,所以频率不变,定时器 1 和 8 的时钟就是 72MHz。
3. 那在这些时钟输出这里,都有一个与门进行输出控制,控制位写的是外部时钟使能,这就是我们在程序中写 RCCAPB2/1PeriphClockCmd 化乍用的地方,打开时钟,就是在这个位置写14让左边的时钟能够通过与门输出给外设。
SystemInit函数中ST配置时钟的方式:
一、启动内部时钟:选择内部8MHz为系统时钟,暂时以内部8MHz的时钟运行。
二、再启动外部时钟:配置外部时钟,进入PLL锁相环。
三、进入PLL锁相环,进行倍频操作:8MHz倍频9倍,得到72MHz,等到锁相环输出稳定后,选择锁相环输出为系统时钟,这样就把系统时钟由 8MHz 切换为了 72MHz
四、补充:如果外部晶振出问题了,你设置的时序可能会慢十倍。,这是因为如果外部晶振出问题了,系统时钟就无法切换到72MHz,那它就会以内部的8MHz运行,8M相比较72M,大概就慢了10倍。
七、定时中断代码详解
一、工程准备与模块创建
-
复制代码并改名:从 OLED 显示屏代码复制并改名为 “6-1 定时器定时中断”。
-
建立定时器模块:在 “system” 目录下创建定时器模块(因为其不涉及外部硬件),包括一个.c 文件(Timer.c)和一个.h 文件(Timer.h)。通过右键添加新文件的方式进行创建,方便代码的组织和管理。
补充:有关TIM的一些函数
(一)定时器初始化相关函数
-
TIM_TimeBaseInit
函数:-
功能:用于配置定时器的时基单元。
-
参数:
-
第一个参数选择某个定时器,如 “TIMx”。
-
第二个参数是结构体,包含时钟分频、计数器模式、周期、预分频器值等参数。
-
例如,“ClockDivision” 参数决定时钟分频,虽与实际单元关系不大,但影响定时器外部信号输入引脚的滤波器采样频率。选择一分频(不分频)时,对应的参数值可通过搜索获取并选择。
-
“CounterMode” 参数可选择计数器模式,如向上计数模式。
-
“Period” 和 “Prescaler” 参数决定定时时间,通过公式 “定时频率 = 72MHz / (Prescaler + 1) / (ARR + 1)” 计算,需注意预分频器和计数器都有一个数的偏差,设置的值要再减一,且取值要在 0 - 65535 之间。
-
-
-
TIM_InternalClockConfig
函数:-
功能:选择内部时钟作为定时器的时钟源。
-
参数:参数只有一个,调用时传入要配置的定时器,如 “TIM_InternalClockConfig (TIM2);” 表示选择定时器 2 的内部时钟源。
-
-
TIM_Cmd
函数:-
功能:使能计数器,有运行控制的功能。
-
参数:
-
第一个参数选择定时器。
-
第二个参数为新的状态,即使能(ENABLE)或禁止(DISABLE)计数器。例如 “TIM_Cmd (TIM2, ENABLE);” 表示使定时器 2 的计数器可以运行。
-
-
-
TIM_ITConfig
函数:-
功能:使能中断输出信号,有中断输出控制的功能。
-
参数:
-
第一个参数选择定时器。
-
第二个参数选择要配置哪个中断输出,如 “TIM_IT_Update” 表示更新中断。
-
第三个参数为新的状态,只能(ENABLE)还是势能(DISABLE),即开启或关闭相应中断输出。
-
-
-
NVIC_PriorityGroupConfig
和NVIC_InitStructure
相关函数:-
用于配置 NVIC,在 NVIC 中打开定时器中断的通道,并分配一个优先级。
-
(二)参数更改相关函数
-
TIM_PrescalerConfig
函数:-
功能:用来单独写预分频值。
-
参数:
-
“preScaler” 是要写入的预分频值。
-
“PSC_ReloadMode” 是写入的模式,可以选择是听从安排在更新事件生效,或者是在写入后手动产生一个更新事件让这个值立刻生效,但这是细节问题,影响不大。
-
-
-
TIM_CounterModeConfigure
函数:-
功能:用来改变计数器的计算模式。
-
参数:“CountMode” 选择新的计数器模式。
-
-
TIM_ARRPreloadConfig
函数:-
功能:自动重装器预装功能配置。用于选择计数器的预装功能,通过给参数 “使能(ENABLE)还是失能(DISABLE)” 来决定。
-
-
TIM_SetCounter
、TIM_SetAutoReload
、TIM_GetCounter
、TIM_GetPrescaler
函数:-
TIM_SetCounter
给计数器写入一个值。 -
TIM_SetAutoReload
给自动重装器写个值。 -
TIM_GetCounter
获取当前计数器的值。 -
TIM_GetPrescaler
获取当前的预分频器的值。
-
(三)时钟源选择相关函数
-
TIM_InternalClockConfig
、TIM_ITRxExternalClockConfig
、TIM_TIxExternalClockConfig
、TIM_ETRClockMode1Config
、TIM_ETRClockMode2Config
、TIM_ETRConfigure
函数:-
对应实际单元的时钟选择部分,可以选择 RCC 内部时钟、ETR 引脚外部时钟、ITRx 其他定时器时钟、TIx 捕获通道时钟等。
-
TIM_InternalClockConfig
选择内部时钟,参数只有一个选择的定时器。 -
TIM_ITRxExternalClockConfig
选择 ITRX 其他定时器的时钟,参数是选择要配置的定时器和选择要接入哪个其他定时器。 -
TIM_TIxExternalClockConfig
选择 TIx 捕获通道的时钟,参数是选择要配置的定时器和选择TIx具体的某个引脚,输入的机型和滤波器。对于外部引脚的波形,一般都会有极性选择和滤波器,这样更灵活一些。 -
TIM_ETRClockMode1Config
选择 ETR 通过外部时钟模式一输入的时钟,参数包括外部触发预分频器(可以对ETR的外部时钟提前做一个分频)、极性和滤波器。 -
TIM_ETRClockMode2Config
选择 ETR 通过外部时钟模式二输入的时钟,参数与外部时钟模式一的函数类似。 -
TIM_ETRConfigure
单独用来配置 ETR 引脚的预分频器、极性、滤波器等参数。
-
(四)标志位获取和清除相关函数
-
如
TIM_GetFlagStatus
、TIM_ClearFlag
等函数:-
用于获取标志位和清除标志位。
-
二、定时器初始化函数
-
总体步骤:
-
RCC 开启时钟:对于定时器初始化,第一步通常是开启时钟。对于定时器 2(TM2通用定时器),因为它是 APB1 总线的外设,所以使用 “RCC_APB1PeriphClockCmd” 函数开启时钟,参数为 “RCC_APB1Periph_TIM2, ENABLE”。
-
选择时基单元时钟源:选择内部时钟源作为定时中断的时钟源。可以使用 “TIM_InternalClockConfig” 函数来配置,参数为 “TIM2”。虽然定时器上电后默认使用内部时钟,但为了步骤完整还是写上这一步。
-
配置实际单元:
-
时钟分频:通过 “TIM_TimeBaseInit” 函数配置实际单元参数。其中 “ClockDivision” 参数决定时钟分频,这里随便配一个一分频(不分频),因为该参数与实际单元关系不大,主要影响定时器外部信号输入引脚的滤波器采样频率。
-
计数器模式:选择向上计数模式,通过 “TIM_TimeBaseInitStruct->TIM_CounterMode” 设置为向上计数模式。
-
周期和预分频器值:“Period” 和 “Prescaler” 参数决定定时时间。例如,想定一个一秒的时间,可以参考公式 “定时频率 = 72MHz / (Prescaler + 1) / (ARR + 1)”,这里可以设置 “Prescaler” 为 7200,“ARR” 为 10000,经过计算可得在 10kHz 的频率下记 10000 个数为一秒。同时注意,由于预分频器和计数器都有一个数的偏差,所以设置的值要再减一,且取值要在 0 - 65535 之间。这个 PSC 和 ARR 的取值不是唯一的,可以预分频给少点,自动重装给多点,这样就是以一个比较高的频率计比较多的数;也可以预分频给多点,自动重装给少点,这样就是以一个比较低的频率计比较少的数。在这里我们对72M进行7200分频得到的就是10K的计数频率,在10K的频率下计10000个数,就是1s的时间。
-
重复计数器的值:是高级定时器才有的功能,这里写0即可。
-
-
配置输出中断控制:使用 “TIM_ITConfig” 函数允许更新中断输出到 NVIC,参数为 “TIM2, TIM_IT_Update, ENABLE”。
-
配置 NVIC:
-
优先级分组:使用 “NVIC_PriorityGroupConfig” 函数选择分组二,即 “NVIC_PriorityGroup_2”。
-
中断通道设置:通过一系列操作设置定时器 2 在 NVIC 中的中断通道,包括定义结构体、设置抢占优先级和响应优先级等。例如,找到定时器 2 在 NVIC 里的通道 “TIM2_IRQn”,设置参数为通道号、优先级等。
-
-
运行控制:使用 “TIM_Cmd” 函数启动定时器,参数为 “TIM2, ENABLE”,使计数器开始计数,当计数器更新时触发中断。
-
-
用到的库函数及参数解释:
-
TIM_TimeBaseInit:用于配置定时器的实际单元,参数包括选择的定时器(如 “TIM2”)和一个结构体,结构体中包含时钟分频、计数器模式、周期、预分频器值等参数。
-
TIM_InternalClockConfig:选择内部时钟作为定时器的时钟源,参数为选择的定时器。
-
TIM_Cmd:使能计数器,参数包括选择的定时器和新的状态(ENABLE 表示使能计数器,DISABLE 表示禁止计数器)。
-
TIM_ITConfig:使能中断输出信号,参数包括选择的定时器、要配置的中断类型(如 “TIM_IT_Update” 表示更新中断)和新的状态(ENABLE 或 DISABLE)。
-
NVIC_PriorityGroupConfig:配置 NVIC 的优先级分组。
-
NVIC_InitStructure:设置 NVIC 中断通道的结构体,通过一系列操作设置中断通道的参数,如中断通道号、抢占优先级、响应优先级等。
-
/**
* 函 数:定时中断初始化
* 参 数:无
* 返 回 值:无
*/
void Timer_Init(void)
{
/*开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //开启TIM2的时钟
/*配置时钟源*/
TIM_InternalClockConfig(TIM2); //选择TIM2为内部时钟,若不调用此函数,TIM默认也为内部时钟
/*时基单元初始化*/
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; //重复计数器,高级定时器才会用到
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure); //将结构体变量交给TIM_TimeBaseInit,配置TIM2的时基单元
/*中断输出配置*/
TIM_ClearFlag(TIM2, TIM_FLAG_Update); //清除定时器更新标志位
//TIM_TimeBaseInit函数末尾,手动产生了更新事件
//若不清除此标志位,则开启中断后,会立刻进入一次中断
//如果不介意此问题,则不清除此标志位也可
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); //开启TIM2的更新中断
/*NVIC中断分组*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC为分组2
//即抢占优先级范围:0~3,响应优先级范围:0~3
//此分组配置在整个工程中仅需调用一次
//若有多个中断,可以把此代码放在main函数内,while循环之前
//若调用多次配置分组的代码,则后执行的配置会覆盖先执行的配置
/*NVIC配置*/
NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; //选择配置NVIC的TIM2线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; //指定NVIC线路的抢占优先级为2
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //指定NVIC线路的响应优先级为1
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
/*TIM使能*/
TIM_Cmd(TIM2, ENABLE); //使能TIM2,定时器开始运行
}
三、中断函数编写
-
找到中断函数:在启动文件中找到定时器的中断函数,例如定时器 2 的中断函数 “TIM2_IRQHandler”,复制到代码中。
-
检查中断标志位:在定义中断函数之后,检查中断标志位,使用 “TIM_GetITStatus” 函数获取中断标志位,参数包括选择的定时器(如 “TIM2”)和想看的中断标志位(如 “TIM_IT_Update” 表示更新中断标志位)。
-
执行用户代码和清除标志位:如果更新中断标志位等于 “SET”,执行相应的用户代码,例如对全局变量 “number” 进行加加操作。最后使用 “TIM_ClearITPendingBit” 函数清除中断标志位,参数与获取中断标志位的函数相同。
定时器中断函数,可以复制到使用它的地方
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
{
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
四、代码测试与问题解决
-
声明初始化函数并测试:将定时器初始化函数的第一行放在头文件里声明一下,然后在主函数中调用定时器初始化函数 “timer_init”,使定时器开始工作。定义一个全局变量 “Num”,想让定时器每秒自动对 “Num” 变量加一,在中断函数中执行加加操作。
-
跨文件使用变量的问题及解决方法:
-
方法一:extern 声明变量:如果直接在中断函数中对 “Num” 变量进行加加操作,会因为 “Num” 是跨文件的变量而报错。解决方法之一是在使用变量的文件上面用 “extern” 声明变量,例如 “extern int16_t Num;”,告诉编译器该变量在别的文件里定义了,编译器会去找并将其当做对该变量的引用,注意这个过程没有定义新的变量。
-
方法二:复制中断函数到主函数所在文件:另一种解决方法是直接把中断函数复制到主函数后面,这样它们就在一个文件里了,就不需要用 “extern” 声明了。可以将原来的中断函数注释掉,当成一个中断函数的模板,哪个文件想用就复制过去。
-
-
上电后计数问题的解决:发现上电后 “Num” 的值从一开始计数的问题,一上电Num就从1开始计数,原因是在 “TIM_TimeBaseInit” 函数最后手动生成了一个更新事件,导致更新中断立刻进入。解决方案是在 “TIM_TimeBaseInit” 函数后、开启中断前手动调用 “TIM_ClearFlag” 函数清除更新中断标志位,例如 “TIM_ClearFlag (TIM2, TIM_FLAG_Update);”,这样就能避免刚初始化完就进中断的问题,使上电后数字从零开始增加。
-
观察计数器值变化:可以在代码中添加显示计数器值的功能,例如 “OLED_ShowNumber (2, 5, TIM_GetCounter (TIM2), 5);”,编译下载后可以观察到计数器的值在飞速变化,变化范围是从零一直到自动重装值。通过改变自动重装值和预分频值,可以观察到对中断频率的影响。例如将自动重装值从 10000 改为 1000,对应的定时时间变为原来的十分之一,上面数字加加的速度也比原来快了十倍;将预分频值去掉一个零,也会使 Num ++的速度变为原来的十倍。这就是预分频值和自动重装值对中断频率的影响。
#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:"); //1行1列显示字符串Num:
while (1)
{
OLED_ShowNum(1, 5, Num, 5); //不断刷新显示Num变量
}
}
/**
* 函 数:TIM2中断函数
* 参 数:无
* 返 回 值:无
* 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
* 函数名为预留的指定名称,可以从启动文件复制
* 请确保函数名正确,不能有任何差异,否则中断函数将不能进入
*/
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET) //判断是否是TIM2的更新事件触发的中断
{
Num ++; //Num变量自增,用于测试定时中断
TIM_ClearITPendingBit(TIM2, TIM_IT_Update); //清除TIM2更新事件的中断标志位
//中断标志位必须清除
//否则中断将连续不断地触发,导致主程序卡死
}
}
八、定时器外部时钟代码详解
一、工程准备:
-
复制 “6-1 定时器定时中断” 工程,改名为 “6-2 定时器外部时钟”。
-
打开工程,准备在此基础上进行更改。
二、代码修改
-
时钟源选择:
-
删除内部时钟选择的代码行,不再使用内部时钟。
-
选择通过 ETR 引脚的外部时钟模式二配置定时器时钟,使用 “TM_ETRClockMode2Config” 函数。
-
查看函数参数,第一个参数给 TM2,然后转到函数定义看剩下的参数。
-
第二个参数是外部触发预分频器,由于不需要分频,选择第一个值(不分频)复制放到参数中。
-
第三个参数是外部触发的极性,有反向(低电平或下降沿有效)和不反向(高电平或上升沿有效)两种选择,根据需求选择不反向并放到参数中。
-
第四个参数是外部触发滤波器,值必须是 0x00 - 0x0F 之间的一个值,决定滤波器的采样频率 f 和采样点数 n(就是以一个采样频率 f 采样 N 个点,如果N个点都一样,才会有效输出)。这里暂时不用滤波器,所以写 0x00。
/*外部时钟配置*/ TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted, 0x0F); //选择外部时钟模式2,时钟从TIM_ETR引脚输入 //注意TIM2的ETR引脚固定为PA0,无法随意更改 //最后一个滤波器参数加到最大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 = 1 - 1; //预分频器,即PSC的值 TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器,高级定时器才会用到 TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure); //将结构体变量交给TIM_TimeBaseInit,配置TIM2的时基单元
-
-
GPIO 配置:
-
因为引脚要用到 GPIO,所以需要先配置 GPIO。
-
首先开启 GPIOA 的时钟,使用 “RCC_APB2PeriphClockCmd (RCC_APB2Periph_GPIOA, ENABLE);”。
-
接着配置 GPIO 结构体,“GPIO_InitStructure”。
-
列出结构体成员,设置 GPIO 模式为上拉输入,参考手册配置表,选择上拉输入模式是为了防止外部输入信号功率很小且悬空时电平跳个没完,内部上拉电阻可能会影响输入信号,所以用上拉输入防止影响外部输入的电平。设置 GPIO_Pin 为 GPIO_Pin_0,GPIO_Speed 为 GPIO_Speed_50MHz。
-
最后通过 “GPIO_Init (GPIOA, &GPIO_InitStructure);” 初始化 GPIO。
/*开启时钟*/ RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //开启TIM2的时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟 /*GPIO初始化*/ 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); //将PA0引脚初始化为上拉输入
-
-
参数调整:
-
修改预分频值和自动重装值,因为是手动模拟外部时钟,速度没那么快,所以预分频给一(不需要分频),自动重装值给十,从零加到九即可。
-
/**
* 函 数:定时中断初始化
* 参 数:无
* 返 回 值:无
* 注意事项:此函数配置为外部时钟,定时器相当于计数器
*/
void Timer_Init(void)
{
/*开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //开启TIM2的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
/*GPIO初始化*/
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); //将PA0引脚初始化为上拉输入
/*外部时钟配置*/
TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted, 0x0F);
//选择外部时钟模式2,时钟从TIM_ETR引脚输入
//注意TIM2的ETR引脚固定为PA0,无法随意更改
//最后一个滤波器参数加到最大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 = 1 - 1; //预分频器,即PSC的值
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器,高级定时器才会用到
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure); //将结构体变量交给TIM_TimeBaseInit,配置TIM2的时基单元
/*中断输出配置*/
TIM_ClearFlag(TIM2, TIM_FLAG_Update); //清除定时器更新标志位
//TIM_TimeBaseInit函数末尾,手动产生了更新事件
//若不清除此标志位,则开启中断后,会立刻进入一次中断
//如果不介意此问题,则不清除此标志位也可
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); //开启TIM2的更新中断
/*NVIC中断分组*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC为分组2
//即抢占优先级范围:0~3,响应优先级范围:0~3
//此分组配置在整个工程中仅需调用一次
//若有多个中断,可以把此代码放在main函数内,while循环之前
//若调用多次配置分组的代码,则后执行的配置会覆盖先执行的配置
/*NVIC配置*/
NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; //选择配置NVIC的TIM2线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; //指定NVIC线路的抢占优先级为2
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //指定NVIC线路的响应优先级为1
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
/*TIM使能*/
TIM_Cmd(TIM2, ENABLE); //使能TIM2,定时器开始运行
}
三、封装计数器值获取函数与主函数显示
-
封装函数:
-
封装获取计数器值的函数,在代码中写 “int16_t timer_get_counter (void)” 函数,函数内部直接返回 “TM_GetCounter (TM2)”,获取定时器 2 的计数器值。然后将这个函数放在头文件里声明一下。
-
-
主函数显示:
-
在主函数中显示计数器值和 “Num” 值。在二行一列显示 “cnt:”,在二行五列显示调用封装的计数器值获取函数 “timer_get_counter”,长度为五。这样可以实时看到计数器的值。
-
/**
* 函 数:返回定时器CNT的值
* 参 数:无
* 返 回 值:定时器CNT的值,范围:0~65535
*/
uint16_t Timer_GetCounter(void)
{
return TIM_GetCounter(TIM2); //返回定时器TIM2的CNT
}
四、代码测试与现象观察
-
编译下载测试:
-
编译下载代码后,可以看到 OLED 显示屏上上面显示 “Num” 值,下面显示计数器值 “cnt”。
-
-
现象观察:
-
用滑片遮挡传感器时,计数器值变化,因为现在实际单元没有预分频,所以每次遮挡传感器,计数器值都会加一。如果有预分频,就是遮挡几次才能加一次。
-
当计数器加到自动重装值(九)后自动清零,同时申请中断,“Num” 加加。
-
可以通过改变预分频器、极性等参数观察不同的现象,也可以尝试其他时钟输入方式。
-
#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:"); //1行1列显示字符串Num:
OLED_ShowString(2, 1, "CNT:"); //2行1列显示字符串CNT:
while (1)
{
OLED_ShowNum(1, 5, Num, 5); //不断刷新显示Num变量
OLED_ShowNum(2, 5, Timer_GetCounter(), 5); //不断刷新显示CNT的值
}
}
/**
* 函 数:TIM2中断函数
* 参 数:无
* 返 回 值:无
* 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
* 函数名为预留的指定名称,可以从启动文件复制
* 请确保函数名正确,不能有任何差异,否则中断函数将不能进入
*/
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET) //判断是否是TIM2的更新事件触发的中断
{
Num ++; //Num变量自增,用于测试定时中断
TIM_ClearITPendingBit(TIM2, TIM_IT_Update); //清除TIM2更新事件的中断标志位
//中断标志位必须清除
//否则中断将连续不断地触发,导致主程序卡死
}