OpenEdv-开源电子网

 找回密码
 立即注册
正点原子全套STM32/Linux/FPGA开发资料,上千讲STM32视频教程免费下载...
查看: 101|回复: 0

《STM32H7R7开发指南 V1.1 》第二十章 通用定时器实验

[复制链接]

1302

主题

1318

帖子

2

精华

超级版主

Rank: 8Rank: 8

积分
5575
金钱
5575
注册时间
2019-5-8
在线时间
1481 小时
发表于 前天 10:15 | 显示全部楼层 |阅读模式
第二十章 通用定时器实验

1)实验平台:正点原子STM32H7R7开发板

2)章节摘自【正点原子】STM32H7R7开发指南 V1.1

3)购买链接: https://detail.tmall.com/item.htm?id=820823382459

4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/stm32/zdyz_stm32h7rx.html

5)正点原子官方B站:https://space.bilibili.com/394620890

6)正点原子STM32开发板技术交流群:756580169

2.jpg

3.png

本章我们主要来学习通用定时器,上一章我们知道STM32H7R7有11个通用定时器(TIM2~TIM5,TIM9,TIM12~TIM17)。我们将通过四个实验来学习通用定时器的各个功能,分别是通用定时器中断实验、通用定时器PWM输出实验、通用定时器输入捕获实验和通用定时器脉冲计数实验。
本章分为如下几个小节:
20.1 通用定时器简介
20.2 通用定时器中断实验
20.3 通用定时器PWM输出实验
20.4 通用定时器输入捕获实验
20.5 通用定时器脉冲计数实验


20.1 通用定时器简介
STM32H7R7的通用定时器有11个之多,其基本特性也是不尽相同,为了更好的区别各个定时器的特性,我们列了一个表格,如下所示:

1.png
表20.1.1 定时器基本特性表

注:该表摘自《STM32H7R7L8H6H.pdf》,原表在该文档的3.33小节(第58页)。
由上表知道:通用定时器和高级定时器其实也就是在基本定时器的基础上,添加了一些其他功能,如:输入捕获、输出比较、输出PWM和单脉冲模式等。而通用定时器数量较多,其特性也有一些的差异,但是基本原理都一样。
通用定时器框图
下面我们以TIM2/TIM3/TIM4/TIM5的框图为例来学习通用定时器框图,其他通用定时器的框图会有差异,因为内容比较多,大家学习了这里的框图再看ST官方手册其他的定时器框图就会比较容易理解。通过学习通用定时器框图会有一个很好的整体掌握,同时对之后的编程也会有一个清晰的思路。


第二十章 通用定时器实验988.png
图20.1.1 通用定时器框图

如上图,通用定时器的框图比基本定时器的框图复杂许多,为了方便介绍,我们将其分成8个部分讲解:
①时钟源
通用定时器时钟可以选择下面四类时钟源之一:
1)内部时钟(tim_ker_ck)
2)外部时钟模式1:外部输入引脚(tim_ti1或tim_ti2),即只能来自于通道1或者通道2
3)外部时钟模式2:外部触发输入(tim_etr_in)
4)内部触发输入(tim_itr):使用一个定时器作为另一定时器的预分频器
通用定时器时钟源的设置方法如下表所示:


2.png
表20.1.2 通用定时器时钟源设置方法

内部时钟(tim_ker_ck)
STM32H7R7系列的TIM2/TIM3/TIM4/TIM5/ TIM6/TIM7/ TIM12/ TIM13/ TIM14都是挂载在APB1总线上,这些定时器的内部时钟(tim_ker_ck)实际上来自于APB1总线提供的时钟。但是这些定时器时钟不是由APB1总线直接提供,而是要先经过一个倍频器。在HAL库版本例程源码的sys.c文件中,系统时钟初始化函数sys_stm32_clock_init已经设置APB1总线时钟频率为150MHz,PPRE1预分频器的预分频系数为2,所以这些定时器时钟源频率为300MHz。因为在TIMPRE位默认设置为0的情况下,当APB1的PPRE1预分频器的预分频系数≥2分频时,挂载在APB1总线上的定时器时钟频率是该总线时钟频率的两倍。这个和基本定时器一样,可回顾基本定时器这部分内容。
APB2总线上挂载的通用定时器TIM9/TIM15/TIM16/TIM17,以及高级定时器TIM1,它们的情况是上面的描述是一样的,不同点是:定时器挂载的总线变成了APB2,需要看的预分频器变成了PPRE2。在系统时钟初始化函数sys_stm32_clock_init已经设置APB2总线时钟频率为150MHz,PPRE2预分频器的预分频系数为2,所以上述的定时器时钟源频率为300MHz。
外部时钟模式1(tim_ti1 or tim_ti2)
外部时钟模式1这类时钟源,顾名思义时钟信号来自芯片外部。时钟源进入定时器的流程如下:外部时钟源信号→IO→TIMx_CH1(或者TIMx_CH2),这里需要注意的是:外部时钟模式1下,时钟源信号只能从CH1或者CH2输入到定时器,CH3和CH4都是不可以的。从IO到TIMx_CH1(或者TIMx_CH2),就需要我们配置IO的复用功能,才能使IO和定时器通道相连通。
时钟源信号来到定时器CH1或CH2后,需要经过什么“关卡”才能到达计数器作为计数的时钟频率的,下面通过外部时钟模式1框图给大家解答。


第二十章 通用定时器实验2324.png
图20.1.2 外部时钟模式1框图

图20.1.2中是以CH2(通道2)为例的,时钟源信号到达CH2后,那么这里我们把这个时钟源信号用tim_ti2表示,因为它只是个信号,来到定时器内部,那我们就按定时器内部的信号来命名,所谓入乡随俗。
tim_ti2首先经过一个滤波器,由ICF[3:0]位来设置滤波方式,也可以设置不使用滤波器。
接着经过边沿检测器,由CC2P位来设置检测的边沿,可以上升沿或者下降沿检测。
然后经过触发输入选择器,由TS[4:0]位来选择tim_trgi(触发输入信号)的来源。可以看到图20.1.2中框出了tim_ti1_ed、tim_ti1fp1和tim_ti2fp2三个触发输入信号(tim_trgi)。tim_ti1_ed表示来自于CH1,并且没有经过边沿检测器过滤的信号,所以它是CH1的双边沿信号,即上升沿或者下降沿都是有效的。tim_ti1fp1表示来自CH1并经过边沿检测器后的信号,可以是上升沿或者下降沿。tim_ti2fp2表示来自CH2并经过边沿检测器后的信号,可以是上升沿或者下降沿。这里以CH2为例,那只能选择tim_ti2fp2。如果是CH1为例,那就可以选择tim_ti1_ed或者tim_ti1fp1。
最后经过从模式选择器,由ECE位和SMS[2:0]位来选择定时器的时钟源。这里我们介绍的是外部时钟模式1,所以ECE位置0,SMS[2:0] = 111即可。tim_psc_ck需要经过定时器的预分频器分频后,最终就能到达计数器进行计数了。
外部时钟模式2(tim_etr_in)
外部时钟模式2,顾名思义时钟信号来自芯片外部。时钟源进入定时器的流程如下:外部时钟源信号→IO→TIMx_ETR。从IO到TIMx_ETR,就需要我们配置IO的复用功能,才能使IO和定时器相连通。
时钟源信号来到定时器TIMx_ETR后,需要经过什么“关卡”才能到达计数器作为计数的时钟频率的,下面通过外部时钟模式2框图给大家解答。


第二十章 通用定时器实验3163.png
图20.1.3 外部时钟模式2框图

图20.1.3中,首先配置TIMx_AF1寄存器中的ETRSEL[3:0]位选择正确的ETR源(内部或外部,这里我们选择外部IO作为时钟源,所以ETRSEL[3:0]位设置为0000即可)。注意:不是所有通用定时器都有这个寄存器的,这个需要查看参考手册。因为外部时钟模式2的时钟信号是来自IO外部的,ETRSEL[3:0]位默认就是0000,所以我们可以直接不用设置这个寄存器。
接着经过外部触发极性选择器,由ETP位来设置上升沿有效还是下降沿有效,选择下降沿有效的话,信号会经过反相器。
然后经过外部触发预分频器,由ETPS[1:0]位来设置预分频系数,系数范围:1、2、4、8。
紧接着经过滤波器,由ETF[3:0]位来设置滤波方式,也可以设置不使用滤波器。fDTS由TIMx_CR1寄存器的CKD位域设置。
最后经过从模式选择器,由ECE位和SMS[2:0]位来选择定时器的时钟源。这里我们介绍的是外部时钟模式2,直接把ECE位置1即可。CK_PSC需要经过定时器的预分频器分频后,最终就能到达计数器进行计数了。
内部触发输入(tim_itr)
内部触发输入是使用一个定时器作为另一个定时器的预分频器,即实现定时器的级联。该功能的工作示意图如图20.1.4所示。


第二十章 通用定时器实验3718.png
图20.1.4 TIM_mstr作为TIM_slv的预分频器框图

上图中,TIM_mstr作为TIM_slv的预分频器,需要完成的配置步骤如下:
1,TIM_mstr的CR2寄存器的MMS[3:0]位设置为0010,即TIM_mstr的主模式选择为更新(选择更新事件作为触发输出(TRGO))。
2,TIM_slv的SMCR寄存器的TS[4:0]位设置为00010,即使用tim_itr2作为内部触发。TS[4:0]位用于配置触发选择,除了tim_itr2,还有其它的选择,详细描述如下图所示:


第二十章 通用定时器实验3968.png
图20.1.5触发选择

上图中的触发选择中,我们在讲解外部时钟模式1的时候说过tim_ti1_ed、tim_ti1fp1和tim_ti2fp2,以及外部时钟模式2讲的tim_etr_in,它们都是属于外部的,其余的都是内部触发了。那么这内部触发都代表什么意思呢?大家打开《STM32H7Rx参考手册_V6(英文版).pdf》的1775页,就可以找下面这个表。

第二十章 通用定时器实验4150.png
表20.1.3 TIMx内部触发连接

在步骤2中,TS[4:0]位设置为00010,使用tim_itr2作为内部触发,这个tim_itr2什么意思?由表20.1.3可以知道,当从模式定时器为TIM2时,tim_itr2表示主模式定时器就是TIM3。这里只是TIM2~5的内部触发连接情况,其它定时器请查看参考手册的相应章节。
3,TIM2_SMCR寄存器的SMS[3:0]位设置为0111,即从模式控制器选择外部时钟模式1。
4,TIM3和TIM2的CEN位都要置1,即启动计数器。
定时器的时钟源这部分内容是非常重要的,因为这计数器工作的基础。虽然定时器有四类时钟源之多,但是我们最常用的还是内部时钟。
②控制器
控制器包括:从模式控制器、编码器接口和触发控制器(TRGO)。从模式控制器可以控制计数器复位、启动、递增/递减、计数。编码器接口针对编码器计数。触发控制器用来提供触发信号给别的外设,比如为其它定时器提供时钟或者为DAC/ADC的触发转换提供信号。
③时基单元
时基单元包括:计数器寄存器(TIMx_CNT)、预分频器寄存器(TIMx_PSC)、自动重载寄存器(TIMx_ARR)。这部分内容和基本定时器基本一样的,大家可以参考基本定时器的介绍。
不同点是:通用定时器的计数模式有三种:递增计数模式、递减计数模式和中心对齐模式;TIM2 和TIM5的计数器是32位的。递增计数模式在讲解基本定时器的时候已经讲过了,那么对应到递减计数模式就很好理解了。就是来了一个计数脉冲,计数器就减1,直到计数器寄存器的值减到0,减到0时定时器溢出,由于是递减计数,故而称为定时器下溢,定时器溢出就会伴随着更新事件的发生。然后计数器又从自动重载寄存器影子寄存器的值开始继续递减计数,如此循环。最后是中心对齐模式,字面上不太好理解。该模式下,计数器先从0开始递增计数,直到计数器的值等于自动重载寄存器影子寄存器的值减1时,定时器上溢,同时生成更新事件,然后从自动重载寄存器影子寄存器的值开始递减计算,直到计数值等于1时,定时器下溢,同时生成更新事件,然后又从0开始递增计数,依此循环。每次定时器上溢或下溢都会生成更新事件。计数器的计数模式的设置请参考TIMx_CR1寄存器的位CMS和位DIR。
下面通过一张图给大家展示定时器工作在不同计数模式下,更新事件发生的情况。


第二十章 通用定时器实验5133.png
图20.1.6 更新事件发生条件

上图中,纵轴表示计数器的计数值,横轴表示时间,ARR表示自动重载寄存器的值,小红点就是更新事件发生的时间点。举个例子,递增计数模式下,当计数值等于ARR时,计数器的值被复位为0,定时器溢出,并伴随着更新事件的发生,后面继续递增计数。递减计数模式和中心对齐模式请参考前面的描述。
上表的描述属于硬件更新事件发生条件,我们还可以通过UG位产生软件更新事件。
关于影子寄存器和定时器溢出时间计算公式等内容可以参考基本定时器的相关内容。
④输入捕获
图20.1.1中的第④部分是输入捕获,一般应用是要和第⑤部分一起完成测量功能。TIMx_CH1~ TIMx_CH4表示定时器的4个通道,这4个通道都是可以独立工作的。IO端口通过复用功能与这些通道相连。配置好IO端口的复用功能后,将需要测量的信号输入到相应的IO端口,输入捕获部分可以对输入的信号的上升沿,下降沿或者双边沿进行捕获,常见的测量有:测量输入信号的脉冲宽度、测量 PWM 输入信号的频率和占空比等。后续有相应的实验。
下面简单说一下测量高电平脉冲宽度的工作原理,方便大家的理解:一般先要设置输入捕获的边沿检测极性,如:我们设置上升沿检测,那么当检测到上升沿时,定时器会把计数器CNT的值锁存到相应的捕获/比较寄存器TIMx_CCRy里,y=1~4。然后我们再设置边沿检测为下降沿检测,当检测到下降沿时,定时器会把计数器CNT的值再次锁存到相应的捕获/比较寄存器TIMx_CCRy里。最后,我们将前后两次锁存的CNT的值相减,就可以算出高电平脉冲期间内计数器的计数个数,再根据定时器的计数频率就可以计算出这个高电平脉冲的时间。如果要测量的高电平脉宽时间长度超过定时器的溢出时间周期,就会发生溢出,这时候我们还需要做定时器溢出的额外处理。低电平脉冲捕获同理。
上面的描述是第④部分输入捕获整体上的一个应用情况,下面我们来看第④部分的细节。当需要测量的信号进入通道后,需要经过哪些“关卡”?我们用图20.1.7给大家讲解。


第二十章 通用定时器实验5987.png
图20.1.7 通道1输入阶段

图20.1.7是图20.1.1第④部分通道1的“放大版”,这里是以通道1输入捕获为例进行介绍,其他通道同理。
待测量信号到达TIMx_CH1后,那么这里我们把这个待测量信号用tim_ti1表示,原因在讲解外部时钟模式1的时候说过,所谓“入乡随俗”。
tim_ti1首先经过一个滤波器,由ICF[3:0]位来设置滤波方式,也可以设置不使用滤波器。fDTS由TIMx_CR1寄存器的CKD位设置。
接着经过边沿检测器,由CC1P位来设置检测的边沿,可以上升沿或者下降沿检测。CC1NP是配置互补通道的边沿检测的,在高级定时器才有,通用定时器没有。
然后经过输入捕获映射选择器,由CC1S[1:0]位来选择把tim_ic1映射到tim_ti1_fp1、tim_ti2_fp1还是tim_trc。这里我们的待测量信号从通道1进来,所以选择IC1映射到tim_ti1_fp1上即可。
紧接着经过输入捕获1预分频器,由ICPS[1:0]位来设置预分频系数,范围:1、2、4、8。
最后需要把CC1E位置1,使能输入捕获,IC1PS就是分频后的捕获信号。这个信号将会到达图20.1.1的第⑤部分。
下面我们接着看图20.1.1的第⑤部分的“放大版”,如下图所示:


第二十章 通用定时器实验6530.png
图20.1.8 捕获/比较通道1主电路(输入捕获功能部分)

图20.1.8中,灰色阴影部分是输出比较功能部分,讲到第⑥部分输出比较的时候再介绍。左边没有阴影部分就是输入捕获功能部分了。
首先看到捕获/比较预装载寄存器,我们以通道1为例,那么它就是CCR1寄存器,通道2、通道3、通道4就分别对应CCR2、CCR3、CCR4。在图20.1.1中就可以看到CCR1~4是有影子寄存器的,所以这里就可以看到图20.1.8中有捕获/比较影子寄存器,该寄存器不可直接访问。
图20.1.8左下角的CC1G位可以产生软件捕获事件,那么硬件捕获事件如何产生的?这里我们还是以通道1输入为例,CC1S[1:0] = 01,即IC1映射到TI1上;CC1E位置1,使能输入捕获;比如不滤波、不分频,ICF[3:0] = 00,ICPS[1:0] = 00;比如检测上升沿,CC1P位置0;接着就是等待测量信号的上升沿到来。当上升沿到来时,IC1PS信号就会触发输入捕获事件发生,计数器的值就会被锁存到捕获/比较影子寄存器里。当CCR1寄存器没有被进行读操作的时候,捕获/比较影子寄存器里的值就会锁存到CCR1寄存器中,那么程序员就可以读取CCR1寄存器,得到计数器的计数值。检测下降沿同理。
⑤输入捕获和输出比较公用部分
该部分需要结合第④部分或者第⑥部分共同完成相应功能。
⑥输出比较
图20.1.1.1中的第⑥部分是输出比较,一般应用是要和第⑤部分一起完成定时器输出功能。TIMx_CH1~ TIMx_CH4表示定时器的4个通道,这4个通道都是可以独立工作的。IO端口通过复用功能与这些通道相连。
下面我们按照输出信号产生过程顺序给大家介绍定时器如何实现输出功能的?首先看到第⑤部分的“放大版”图,如下图所示:


第二十章 通用定时器实验7279.png
图20.1.9 捕获/比较通道1主电路(输出比较功能部分)

图20.1.9中,灰色阴影部分是输入捕获功能部分,前面已经讲过。这里我们看到右边没有阴影部分就是输出比较功能部分了。下面以通道1输出比较功能为例给大家介绍定时器如何实现输出功能的。
首先程序员写CCR1寄存器,即写入比较值。这个比较值需要转移到对应的捕获/比较影子寄存器后才会真正生效。什么条件下才能转移?图20.1.9中可以看到compare_transfer旁边的与门,需要满足三个条件:CCR1不在写入操作期间、CC1S[1:0] = 0配置为输出、OC1PE位置0(或者OC1PE位置1,并且需要发生更新事件,这个更新事件可以软件产生或者硬件产生)。
当CCR1寄存器的值转移到其影子寄存器后,新的值就会和计数器的值进行比较,它们的比较结果将会通过第⑥部分影响定时器的输出。
下面来看看第⑥部分通道1的“放大版”,如下图所示:


第二十章 通用定时器实验7679.png
图20.1.10 通道1输出阶段

上图中,可以看到输出模式控制器,由OC1M[3:0]位配置输出比较模式,该位的描述请参考《STM32H7Rx参考手册_V6(英文版).pdf》相关定时器章节的TIMx_CCMR1寄存器。
tim_oc1ref是输出参考信号,高电平有效,为高电平时称之为有效电平,为低电平时称之为无效电平。它的高低电平受到三个方面的影响:OC1M[3:0]位配置的输出比较模式、第⑤部分比较器的比较结果、还有就是OC1CE位配置的tim_strf信号。tim_strf信号可以将tim_oc1ref电平强制清零,该信号来自IO外部。
一般来说,当计数器的值和捕获/比较寄存器的值相等时,输出参考信号tim_oc1ref的极性就会根据我们选择的输出比较模式而改变。如果开启了比较中断,还会发生比较中断。
CC1P位用于选择通道输出极性。
CC1E位置1使能通道输出。
tim_oc1信号就会从TIMx_CH1输出到IO端口,再到IO外部。
下面分别通过四个实验来学习通用定时器的功能。
⑦ocxref清除信号
图20.1.1.1中的第⑦部分是tim_ocxref清零信号,这些输入可以用于清除tim_ocxref信号,通常用于硬件逐周期脉宽控制。
⑧定时器中断信号和DMA请求信号
图20.1.1.1中的第⑧部分是定时器中断信号和DMA请求信号,了解即可。


20.2 通用定时器中断实验

20.2.1 TIM2/TIM3/TIM4/TIM5寄存器
下面介绍几个与定时器中断相关且重要的寄存器,其他的通用定时器的寄存器会有一些差异,请大家自行对比《STM32H7Rx参考手册_V6(英文版).pdf》相关章节。下面先来看TIMx_CR1寄存器的一些位描述,具体如下:
控制寄存器 1(TIMx_CR1)
TIM2/TIM3/TIM4/TIM5的控制寄存器1描述如图20.2.1.1所示:


第二十章 通用定时器实验8479.png
图20.2.1.1 TIMx_CR1寄存器

上图中我们只列出了本实验需要用的一些位,其中:位7(APRE)用于控制自动重载寄存器是否进行缓冲,如果ARPE位置1,ARR起缓冲作用,即只有在更新事件发生时才会把ARR的值写入其影子寄存器里;如果ARPE位置0,那么修改自动重载寄存器的值时,该值会马上被写入其影子寄存器中,从而立即生效。
CMS[1:0]位,用于设置边沿对齐模式还是中心对齐模式,本实验我们使用边沿对齐模式,所以设置为00即可。
DIR位,用于控制定时器的计数方向,我们使用递增计数模式,所以设置DIR位为0。
CEN位,用于使能计数器的工作,必须要设置该位为1,计数器才会开始计数。
从模式控制寄存器(TIMx_SMCR)
TIM2/TIM3/TIM4/TIM5的从模式控制寄存器描述如图20.2.1.2所示:


第二十章 通用定时器实验8846.png
图20.2.1.2 TIMx_SMCR寄存器

该寄存器的SMS[3:0]位,用于从模式选择,其实就是选择计数器输入时钟的来源。比如通用定时器中断实验我们设置SMS[3:0]=0000,禁止从模式,这样PSC预分频器的时钟就直接来自内部时钟,按照我们例程sys_stm32_clock_init函数的配置,频率为300MHz(APB1总线时钟频率的2倍)。
TIMx DMA/中断使能寄存器 (TIMx_DIER)
TIM2/TIM3/TIM4/TIM5的DMA/中断使能寄存器描述如图20.2.1.3所示:


第二十章 通用定时器实验9103.png
图20.2.1.3 TIMx_DIER寄存器

该寄存器用于使能/失能触发DMA请求、捕获/比较中断以及更新中断。本实验只用到更新中断,所以把位0(UIE)置1即可。
状态寄存器(TIMx_SR)        
TIM2/TIM3/TIM4/TIM5的状态寄存器描述如图20.2.1.4所示:


第二十章 通用定时器实验9245.png
图20.2.1.4 TIMx_SR寄存器

该寄存器都是一些中断标志位,比如更新中断标志位、捕获/比较中断标志位等。在通用定时器中断实验我们用到更新中断标志位,当定时器更新中断到来后,位0(UIF)会由硬件置1,我们需要在中断服务函数里面把该位清零。
计数器寄存器(TIMx_CNT)
TIM2/TIM3/TIM4/TIM5的计数器寄存器描述如图20.2.1.5所示:


第二十章 通用定时器实验9431.png
图20.2.1.5 TIMx_CNT寄存器

当用到16位定时器的时候,TIMx_CNT寄存器的32位都是用做计数器寄存器,其他定时器的就跟基本定时器一样,低16位有效。可以直接写该寄存器设置计数的初始值,也可以读取该寄存器获取计数器值。
预分频寄存器(TIMx_PSC)
定时器的预分频寄存器都是16位的,即写入该寄存器的数值范围是0到65535,表示1到65536分频。比如我们要30000分频,就往该寄存器写入29999。该寄存器比较简单,这里就不专门列出讲解了。
自动重载寄存器(TIMx_ARR)
TIM2/TIM3/TIM4/TIM5的自动重载寄存器描述如图20.2.1.6所示:


第二十章 通用定时器实验9729.png
图20.2.1.6 TIMx_ARR寄存器

该寄存器可以由APRE位设置是否进行缓冲。计数器的值会和自动重装寄存器影子寄存器进行比较,当两者相等,定时器就会溢出,从而发生更新事件,如果打开了更新中断,还会发生更新中断。

20.2.2 硬件设计

1. 例程功能
LED1用来指示程序正在运行,200ms翻转一次。LED0在定时器中断中翻转,500ms进入中断一次。

2. 硬件资源
1)LED灯
            LED0:LED0 – PD14
            LED1:LED1 – PC0
2)定时器4

3. 原理图
定时器属于STM32H7R7的内部资源,只需要软件设置好即可正常工作。我们通过LED0来指示STM32H7R7的定时器进入中断的频率,LED1则指示程序的运行状态。


20.2.3 程序设计
本实验的相关HAL库驱动以及实验配置步骤请参考基本定时器相关内容,基本一样。不同点是基本定时器只能是递增计数模式,通用定时器可以递增计数模式、递减计数模式和中心对齐模式。

20.2.3.1 程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。通用定时器驱动源码包括两个文件:gtim.c和gtim.h。
首先看gtim.h头文件的几个宏定义:

  1. #define GTIM_TIMX                       TIM4
  2. #define GTIM_TIMX_IRQn                  TIM4_IRQn
  3. #define GTIM_TIMX_IRQHandler            TIM4_IRQHandler
  4. #define GTIM_TIMX_CLK_ENABLE()          do{ __HAL_RCC_TIM4_CLK_ENABLE(); }while(0)  /* TIM4 时钟使能 */
复制代码
通过修改这4个宏定义,可以支持TIM1~TIM17任意一个定时器。
下面再来看一下gtim.c文件的代码,主要包括两个函数,先来看看通用定时器的初始化函数,其定义如下:

  1. /**
  2. * @brief   初始化通用定时器中断
  3. * [url=home.php?mod=space&uid=271674]@param[/url]   arr: 自动重装载值
  4. * @param   psc: 预分频系数
  5. * @retval  无
  6. */
  7. void gtim_timx_int_init(uint16_t arr, uint16_t psc)
  8. {
  9.     /* 初始化TIM */
  10.     g_gtimx_handle.Instance = GTIM_TIMX;
  11.     g_gtimx_handle.Init.Prescaler = psc;                       /* 预分频系数 */
  12.     g_gtimx_handle.Init.CounterMode = TIM_COUNTERMODE_UP;      /* 计数模式 */
  13.     g_gtimx_handle.Init.Period = arr;                          /* 重装载值 */
  14.     g_gtimx_handle.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;/* 时钟分频 */
  15. g_gtimx_handle.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
  16. /* 自动重装载预加载模式 */
  17.     HAL_TIM_Base_Init(&g_gtimx_handle);
  18.    
  19.     /* 开启TIM中断模式计数 */
  20.     HAL_TIM_Base_Start_IT(&g_gtimx_handle);
  21. }
复制代码
这里配置的参数和基本定时器中断实验的是一样的,HAL_TIM_Base_Init函数初始化之后,会调用HAL库的HAL_TIM_Base_MspInit函数,下面,再看到HAL_TIM_Base_MspInit函数,其定义如下:
  1. /**
  2. * @brief   HAL库基本定时器初始化MSP函数
  3. * @param   无
  4. * @retval  无
  5. */
  6. void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim)
  7. {
  8.     if (htim->Instance == GTIM_TIMX)
  9.     {
  10.         /* 使能通用定时器时钟 */
  11.         GTIM_TIMX_CLK_ENABLE();
  12.         
  13.         /* 配置中断优先级并使能中断 */
  14.         HAL_NVIC_SetPriority(GTIM_TIMX_IRQn, 0, 0);
  15.         HAL_NVIC_EnableIRQ(GTIM_TIMX_IRQn);
  16.     }
  17. }
复制代码
HAL_TIM_Base_MspInit函数用于存放GPIO、NVIC和时钟相关的代码,这里首先判断定时器的寄存器基地址,满足条件后,首先设置使能定时器的时钟,然后设置定时器中断的抢占优先级为0,响应优先级为0,最后开启定时器中断。这里没有用到IO引脚,所以不用初始化GPIO。
接着是定时器中断服务函数的定义,这里用的是宏名,其定义如下:

  1. /**
  2. * @brief   计数通用定时器中断服务函数
  3. * @param   无
  4. * @retval  无
  5. */
  6. void GTIM_TIMX_IRQHandler(void)
  7. {
  8.     HAL_TIM_IRQHandler(&g_gtimx_handle);
  9. }
复制代码
这个函数实际上调用HAL库的定时器中断公共处理函数HAL_TIM_IRQHandler。HAL库的中断公共处理函数,会根据中断标志位调用各个中断回调函数,中断要处理的逻辑代码就写到这个回调函数中。比如这里我们使用到的是超时中断,定义的超时中断回调函数如下:
  1. /**
  2. * @brief   HAL库定时器超时中断回调函数
  3. * @param   无
  4. * @retval  无
  5. */
  6. void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
  7. {
  8.     if (htim->Instance == GTIM_TIMX)
  9.     {
  10.         LED0_TOGGLE();
  11.     }
  12. }
复制代码
超时中断回调函数是所有定时器公用的,所以我们就需要在超时中断回调函数中对定时器寄存器基地址进行判断,只有符合对应定时器发生的超时中断,才能进行相应的处理,从而避免多个定时器同时使用到超时中断,导致超时中断代码的逻辑混乱。这里我们使用定时器4的超时中断,所以进入超时中断回调函数后,先判断是不是定时器4的寄存器基地址,当然这里使用宏的形式,GTIM_TIMX的原型就是TIM4,执行的逻辑代码是翻转LED0。

20.2.4 下载验证
下载代码后,可以看到LED1不停闪烁(每400ms一个周期),而LED0也是不停的闪烁,但是闪烁时间较LED1慢(每1s一个周期)。

20.3 通用定时器PWM输出实验
本小节我们来学习使用通用定时器的PWM输出模式。
脉冲宽度调制(PWM),是英文“Pulse Width Modulation”的缩写,简称脉宽调制,是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术。我们可以让定时器产生PWM,在计数器频率固定时,PWM频率或者周期由自动重载寄存器(TIMx_ARR)的值决定,其占空比由捕获/比较寄存器(TIMx_CCRx)的值决定。PWM产生原理示意图如下图所示:


第二十章 通用定时器实验12929.png
图20.3.1 PWM生成示意图

上图中,定时器工作在递增计数模式,纵轴是计数器的计数值CNT,横轴表示时。当CNT<CCRx时,IO输出低电平(逻辑0);当CNT>=CCRx时,IO输出高电平(逻辑1);当CNT=ARR时,定时器溢出,CNT的值被清零,然后继续递增,依次循环。在这个循环中,改变CCRx的值,就可以改变PWM的占空比,改变ARR的值,就可以改变PWM的频率,这就是PWM输出的原理。
定时器产生PWM的方式有许多种,下面我们以边沿对齐模式(即递增计数模式/递减计数模式)为例,PWM模式1或者PWM模式2产生PWM的示意图,如下图所示:


第二十章 通用定时器实验13237.png
图20.3.2 产生PWM示意图

STM32H7R7的定时器除了TIM6和TIM7外,其他的定时器都可以产生PWM输出。其中高级定时器TIM1可以同时产生多达7路的PWM输出。而通用定时器也能同时产生多达4路的PWM输出!本实验我们以TIM4的CH3产生一路PWM输出为例进行学习。

20.3.1 TIM2/TIM3/TIM4/TIM5寄存器
要使STM32H7R7的通用定时器TIMx产生PWM输出,除了上一小节介绍的寄存器外,我们还会用到另外3个寄存器,来控制PWM输出。这三个寄存器分别是:捕获/比较模式寄存器(TIMx_CCMR1/2)、捕获/比较使能寄存器(TIMx_CCER)、捕获/比较寄存器(TIMx_CCR1~4)。接下来我们简单介绍一下这三个寄存器。
捕获/比较模式寄存器1/2(TIMx_CCMR1/2)
TIM2/TIM3/TIM4/TIM5的捕获/比较模式寄存器(TIMx_CCMR1/2),该寄存器一般有2个:TIMx _CCMR1和TIMx _CCMR2。TIMx_CCMR1控制CH1和CH2,而TIMx_CCMR2控制CH3和CH4。TIMx_CCMR1寄存器描述如图20.3.1.1所示:


第二十章 通用定时器实验13754.png
图20.3.1.1 TIMx_CCMR1寄存器

第二十章 通用定时器实验13780.png
图20.3.1.2 TIMx_CCMR2寄存器

该寄存器的有些位在不同模式下,功能不一样,我们现在用到输出比较,先看输出比较部分,输入捕获在后面的实验再讲解。关于该寄存器的详细说明,请参考《STM32H7Rx参考手册_V6(英文版).pdf》第1859页,42.5.7节。比如我们要让TIM4的CH3输出PWM波,TIMx_CCMR2寄存器的模式设置位OC3M[3:0]就是对应着通道3的模式设置。总共可以配置14种模式,我们使用的是PWM模式1或者PWM模式2,所以位OC3M[8:0]设置为0110或者0111。这两种PWM模式的区别就是输出有效电平的极性相反。位 OC1PE控制输出比较通道1的预装载使能,实际就是控制CCR1寄存器是否进行缓冲。因为CCR1寄存器也是有影子寄存器的,影子寄存器才是真正起作用的寄存器。CC3S[1:0]用于设置通道1的方向(输入/输出)默认设置为0,就是设置通道作为输出使用。
捕获/比较使能寄存器(TIMx_ CCER)
TIM2/TIM3/TIM4/TIM5的捕获/比较使能寄存器描述如图20.3.1.3所示:


第二十章 通用定时器实验14261.png
图20.3.1.3 TIMx_CCER寄存器

该寄存器比较简单,要让TIM4的CH3输出PWM波,这里我们要使能CC3E位,该位是通道3输入/输出使能位,要想PWM从IO口输出,这个位必须设置为1。CC3P位是设置通道3的输出极性,我们默认设置0。
捕获/比较寄存器1/2/3/4(TIMx_ CCR1/2/3/4)
捕获/比较寄存器(TIMx_ CCR1/2/3/4),该寄存器总共有4个,分别对应4个通道CH1~CH4。我们使用的是通道3,所以来看看TIMx_ CCR3寄存器描述,如图20.3.1.4所示:


第二十章 通用定时器实验14520.png
图20.3.1.4 TIMx_ CCR3寄存器

在输出模式下,捕获/比较寄存器影子寄存器的值与CNT的值比较,根据比较结果产生相应动作,利用这点,我们通过修改这个寄存器的值,就可以控制PWM的占空比了。

20.3.2 硬件设计

1. 例程功能
使用TIM4通道3(由PD14复用)输出PWM, PD14引脚连接了LED0,从而实现PWM输出控制LED0亮度。

2. 硬件资源
1)LED灯   
        LED0: LED0 – PD14
        LED1: LED1 – PC0
2)定时器4输出通道3(对应PD14)

3. 原理图
定时器属于STM32H7R7的内部资源,只需要软件设置好即可正常工作。我们通过LED0来间接指示定时器的PWM输出情况。


20.3.3 程序设计

20.3.3.1 定时器的HAL库驱动
定时器在HAL库中的驱动代码在前面介绍基本定时器已经介绍了部分,这里我们再介绍几个本实验用到的函数。
1. HAL_TIM_PWM_Init函数
定时器PWM输出基础工作参数初始化函数,其声明如下:

  1. HAL_StatusTypeDef HAL_TIM_PWM_Init(TIM_HandleTypeDef *htim);
复制代码
函数描述:
用于初始化定时器的基础工作参数,即初始化TIM_HandleTypeDef结构体成员。
函数形参:
形参1是TIM_HandleTypeDef结构体类型指针变量,基本定时器的时候已经介绍。
函数返回值:HAL_StatusTypeDef枚举类型的值。
注意事项:
该函数实现的功能以及使用方法和HAL_TIM_Base_Init类似,作用都是初始化定时器的ARR和PSC等参数。为什么HAL库要提供这个函数而不直接让我们使用HAL_TIM_Base_Init函数呢?这是因为HAL库为定时器的针对PWM输出定义了单独的MSP回调函数HAL_TIM_PWM_MspInit,所以当我们调用HAL_TIM_PWM_Init进行PWM初始化之后,该函数内部会调用MSP回调函数HAL_TIM_PWM_MspInit。当我们使用HAL_TIM_Base_Init初始化定时器参数的时候,它内部调用的回调函数是HAL_TIM_Base_MspInit,这里大家注意区分。
2. HAL_TIM_PWM_ConfigChannel函数
定时器PWM模式通道配置函数。其声明如下:

  1. HAL_StatusTypeDef HAL_TIM_PWM_ConfigChannel(TIM_HandleTypeDef *htim,
  2. TIM_OC_InitTypeDef *sConfig, uint32_t Channel);
复制代码
函数描述:
该函数用于设置定时器的PWM输出模式及通道等参数。
函数形参:
形参1是TIM_HandleTypeDef结构体类型指针变量,用于配置定时器基本参数。
形参2是TIM_OC_InitTypeDef结构体类型指针变量,用于配置定时器输出比较参数。
下面重点来了解一下TIM_OC_InitTypeDef结构体指针类型,其定义如下:

  1. typedef struct
  2. {
  3.   uint32_t OCMode;                 /* 输出比较模式选择,寄存器的时候说过了,共14种模式 */
  4.   uint32_t Pulse;                  /* 设置比较值*/
  5.   uint32_t OCPolarity;           /* 设置输出比较极性 */
  6.   uint32_t OCNPolarity;           /* 设置互补输出比较极性 */
  7.   uint32_t OCFastMode;            /* 使能或失能输出比较快速模式 */
  8.   uint32_t OCIdleState;           /* 选择空闲状态下的非工作状态(OC1 输出) */
  9.   uint32_t OCNIdleState;         /* 设置空闲状态下的非工作状态(OC1N 输出) */
  10. } TIM_OC_InitTypeDef;  
复制代码
我们重点关注前三个结构体成员。成员变量OCMode用来设置模式,这里我们设置为PWM模式1。成员变量Pulse用来设置捕获比较值。成员变量TIM_OCPolarity用来设置输出极性。其他成员TIM_OCNPolarity,TIM_OCIdleState和TIM_OCNIdleState后面用到再介绍。
形参3是定时器通道,范围:TIM_CHANNEL_1~6,比如定时器4只有4个通道,那选择范围就只有TIM_CHANNEL_1~4,所以要根据具体情况选择。
函数返回值:HAL_StatusTypeDef枚举类型的值。
3. HAL_TIM_PWM_Start函数
定时器PWM输出启动函数,其声明如下:

  1. HAL_StatusTypeDef HAL_TIM_PWM_Start(TIM_HandleTypeDef *htim, uint32_t Channel);
复制代码
函数描述:
用于使能通道输出和启动计数器,即启动PWM输出。
函数形参:
形参1是TIM_HandleTypeDef结构体类型指针变量。
形参2是定时器通道,范围:TIM_CHANNEL_1到TIM_CHANNEL_6。
函数返回值:HAL_StatusTypeDef枚举类型的值。
注意事项:
HAL库提供了单独使能定时器输出通道的函数,其声明如下:

  1. void TIM_CCxChannelCmd(TIM_TypeDef *TIMx, uint32_t Channel,
  2. uint32_t ChannelState);
复制代码
HAL_TIM_PWM_Start函数内部也是调用了该函数。
4. HAL_TIM_ConfigClockSource函数
配置定时器时钟源函数,其声明如下:

  1. HAL_StatusTypeDef HAL_TIM_ConfigClockSource(TIM_HandleTypeDef *htim,
  2. TIM_ClockConfigTypeDef *sClockSourceConfig);
复制代码
函数描述:
用于配置定时器时钟源。
函数形参:
形参1是TIM_HandleTypeDef结构体类型指针变量。
形参2是TIM_ClockConfigTypeDef结构体类型指针变量,用于配置定时器时钟源参数。
TIM_ClockConfigTypeDef定义如下:

  1. typedef struct
  2. {
  3.   uint32_t ClockSource;                     /* 时钟源 */
  4.   uint32_t ClockPolarity;                   /* 时钟极性 */
  5.   uint32_t ClockPrescaler;                  /* 定时器预分频器 */
  6.   uint32_t ClockFilter;                            /* 时钟过滤器 */
  7. } TIM_ClockConfigTypeDef;
复制代码
函数返回值:HAL_StatusTypeDef枚举类型的值。
注意事项:
该函数主要配置TIMx_SMCR寄存器。默认情况下,定时器的时钟源是内部时钟。本实验就是使用内部时钟的,所以我们不用对时钟源就行初始化,默认即可。这里只是让大家知道有这个函数可以设定时器的时钟源。比如用HAL_TIM_ConfigClockSource初始化选择内部时钟,方法如下:

  1. TIM_HandleTypeDef                timx_handle;                 /* 定时器x句柄 */
  2. TIM_ClockConfigTypeDef sClockSourceConfig = {0};
  3. sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;    /* 选择内部时钟 */
  4. HAL_TIM_ConfigClockSource(&timx_handle, &sClockSourceConfig);
复制代码
后面的定时器初始化凡是用到内部时钟我们都没有去进行初始化,系统默认即可。
定时器PWM输出模式配置步骤
1)开启TIMx和相应通道输出的GPIO时钟,配置该IO口的复用功能输出
首先开启TIMx的时钟,然后配置GPIO为复用功能输出。本实验我们默认用到定时器4通道3,对应IO是PD14,它们的时钟开启方法如下:

  1. __HAL_RCC_TIM4_CLK_ENABLE();                    /* 使能定时器4 */
  2. __HAL_RCC_GPIOD_CLK_ENABLE();                   /* 开启GPIOD时钟 */
复制代码
IO口复用功能是通过函数HAL_GPIO_Init来配置的。
2)初始化TIMx,设置TIMx的ARR和PSC等参数
使用定时器的PWM输出功能时,通过HAL_TIM_PWM_Init函数初始化定时器ARR和PSC等参数。
注意:该函数会调用:HAL_TIM_PWM_MspInit函数,我们可以通过后者存放定时器和GPIO时钟使能、GPIO初始化、中断使能以及优先级设置等代码。
3)设置定时器为PWM模式,输出比较极性,比较值等参数
在HAL库中,通过HAL_TIM_PWM_ConfigChannel函数来设置定时器为PWM1模式或者PWM2模式,根据需求设置输出比较的极性,设置比较值(控制占空比)等。
4)使能TIMx,使能TIMx的CHy输出
在HAL库中,通过调用 HAL_TIM_PWM_Start函数来使能TIMx的某个通道输出PWM。
5)修改TIM4_CCR3来控制占空比
在经过以上设置之后,PWM其实已经开始输出了,只是其占空比和频率都是固定的,而我们可以通过修改比较值来控制PWM的输出占空比。HAL库中提供一个修改占空比的宏定义:

  1. __HAL_TIM_SET_COMPARE (__HANDLE__, __CHANNEL__, __COMPARE__)
复制代码
__HANDLE__是TIM_HandleTypeDef结构体类型指针变量,__CHANNEL__对应PWM的输出通道,__COMPARE__则是要写到捕获/比较寄存器(TIMx_ CCR1/2/3/4)的值。实际上该宏定义最终还是往对应的捕获/比较寄存器写入比较值来控制PWM波的占空比。如下解析:
比如我们要修改定时器4通道3的输出比较值(控制占空比),寄存器操作方法:

  1. TIM4->CCR3 = counter; /* counter是比较值,并且动态变化的,
  2. 所以我们要周期性调用这条语句,已达到及时修改PWM的占空比 */
复制代码
__HAL_TIM_SET_COMPARE这个宏定义函数最终也是调用这个寄存器操作的,所以说我们使用HAL库的函数其实就是间接操作寄存器的。

20.3.3.2 程序解析
这里我们只讲解核心代码,详细的源码请大家参考本实验对应源码。通用定时器驱动源码包括两个文件:gtim.c和gtim.h。
首先看gtim.h头文件的几个宏定义:

  1. /* 通用定时器定义 */
  2. #define GTIM_TIMX                  TIM4
  3. #define GTIM_TIMX_CLK_ENABLE()     do{__HAL_RCC_TIM4_CLK_ENABLE(); } while (0)
  4. #define GTIM_TIMX_CHY              TIM_CHANNEL_3
  5. #define GTIM_TIMX_CHY_GPIO_PORT     GPIOD
  6. #define GTIM_TIMX_CHY_GPIO_PIN      GPIO_PIN_14
  7. #define GTIM_TIMX_CHY_GPIO_AF       GPIO_AF2_TIM4
  8. #define GTIM_TIMX_CHY_GPIO_CLK_ENABLE()
  9. do{__HAL_RCC_GPIOD_CLK_ENABLE();}while(0)
复制代码
可以把上面的宏定义分成两部分,第一部分是定时器4输出通道3的相应宏定义。第二部分则是定时器4输出通道3对应的IO口的宏定义,这里的宏定义是定时器4通道3输出PWM控制LED0的相关宏定义。
gtim.h头文件就添加了这部分的程序,下面看gtim.c的程序,首先是通用定时器PWM输出初始化函数。

  1. /**
  2. * @brief   初始化通用定时器PWM
  3. * @param   arr: 自动重装载值
  4. * @param   psc: 预分频系数
  5. * @retval  无
  6. */
  7. void gtim_timx_pwm_chy_init(uint16_t arr, uint16_t psc)
  8. {
  9.     TIM_OC_InitTypeDef timx_oc_pwm_struct = {0};
  10.    
  11.     /* 初始化TIM PWM */
  12.     g_gtimx_handle.Instance = GTIM_TIMX;
  13.     g_gtimx_handle.Init.Prescaler = psc;                        /* 预分频系数 */
  14.     g_gtimx_handle.Init.CounterMode = TIM_COUNTERMODE_UP;       /* 计数模式 */
  15.     g_gtimx_handle.Init.Period = arr;                           /* 重装载值 */
  16.     g_gtimx_handle.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; /* 时钟分频 */
  17. g_gtimx_handle.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
  18. /* 自动重装载预加载模式 */
  19.     HAL_TIM_PWM_Init(&g_gtimx_handle);                  /* 配置TIM PWM通道 */
  20.     timx_oc_pwm_struct.OCMode = TIM_OCMODE_PWM1;        /* PWM模式 */
  21.     timx_oc_pwm_struct.Pulse = (arr + 1) >> 1;          /* 占空比 */
  22.     timx_oc_pwm_struct.OCPolarity = TIM_OCPOLARITY_LOW; /* 有效电平 */
  23. HAL_TIM_PWM_ConfigChannel(&g_gtimx_handle, &timx_oc_pwm_struct,
  24. GTIM_TIMX_CHY);
  25.    
  26.     /* 开启TIM PWM输出 */
  27.     HAL_TIM_PWM_Start(&g_gtimx_handle, GTIM_TIMX_CHY);
  28. }
复制代码
HAL_TIM_PWM_Init初始化TIM4并设置TIM4的ARR和PSC等参数,其次通过调用函数HAL_TIM_PWM_ConfigChannel设置TIM4_CH3的PWM模式以及比较值等参数,最后通过调用函数HAL_TIM_PWM_Start来使能TIM4以及使能PWM通道TIM4_CH3输出。
本实验我们使用PWM的MSP初始化回调函数HAL_TIM_PWM_MspInit来存放时钟、GPIO的初始化代码,其定义如下:

  1. /**
  2. * @brief   HAL库定时器PWM初始化MSP函数
  3. * @param   TIM句柄指针
  4. * @retval  无
  5. */
  6. void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim)
  7. {
  8.     GPIO_InitTypeDef gpio_init_struct = {0};
  9.    
  10.     if (htim->Instance == GTIM_TIMX)
  11.     {
  12.         /* 使能时钟 */
  13.         GTIM_TIMX_CHY_GPIO_CLK_ENABLE();
  14.         GTIM_TIMX_CLK_ENABLE();
  15.         
  16.         /* 初始化TIM PWM输出引脚 */
  17.         gpio_init_struct.Pin = GTIM_TIMX_CHY_GPIO_PIN;
  18.         gpio_init_struct.Mode = GPIO_MODE_AF_PP;
  19.         gpio_init_struct.Pull = GPIO_PULLUP;
  20.         gpio_init_struct.Speed = GPIO_SPEED_FREQ_LOW;
  21.         gpio_init_struct.Alternate = GTIM_TIMX_CHY_GPIO_AF;
  22.         HAL_GPIO_Init(GTIM_TIMX_CHY_GPIO_PORT, &gpio_init_struct);
  23.     }
  24. }
复制代码
该函数首先判断定时器寄存器基地址,符合条件后,开启对应的GPIO时钟和定时器时钟,并且初始化GPIO。上面是使用HAL库标准的做法,我们亦可把HAL_TIM_PWM_MspInit函数里面的代码直接放到gtim_timx_pwm_chy_init函数里。这样做的好处是当一个项目中用到多个定时器时,代码的移植性、可读性好,方便管理。
在main.c里面编写如下代码:

  1. int main(void)
  2. {
  3.     uint16_t compare = 0;
  4.     int8_t dir = 1;
  5.     uint8_t counter = 0;
  6.    
  7.     sys_mpu_config();                           /* 配置MPU */
  8.     sys_cache_enable();                         /* 使能Cache */
  9.     HAL_Init();                                 /* 初始化HAL库 */
  10.     sys_stm32_clock_init(300, 6, 2);            /* 配置时钟,600MHz */
  11.     delay_init(600);                            /* 初始化延时 */
  12.     usart_init(115200);                         /* 初始化串口 */
  13.     led_init();                                 /* 初始化LED */
  14. gtim_timx_pwm_chy_init(500 - 1, 300 - 1);   
  15. /* 初始化通用定时器PWM输出,PWM频率为2KHz */
  16.    
  17.     while (1)
  18.     {
  19.         if (compare == 300)
  20.         {
  21.             dir = -1;
  22.         }
  23.         else if (compare == 0)
  24.         {
  25.             dir = 1;
  26.         }
  27.         compare += dir;
  28.         __HAL_TIM_SET_COMPARE(&g_gtimx_handle, GTIM_TIMX_CHY, compare);
  29.         
  30.         if (counter++ == 20)
  31.         {
  32.             counter = 0;
  33.             LED1_TOGGLE();
  34.         }
  35.         
  36.         delay_ms(10);
  37.     }
  38. }
复制代码
本小节开头我们就说PWM频率由自动重载寄存器(TIMx_ARR)的值决定,其占空比则由捕获/比较寄存器(TIMx_CCRx)的值决定。下面结合实际看看具体怎么计算:
定时器4的时钟源频率为2倍APB1总线时钟频率,即频率为300MHz,而调用gtim_timx_pwm_chy_init初始化函数之后,就相当于写入预分频寄存器的值为299,写入自动重载寄存器的值为499。基本定时器讲的定时器溢出公式由公式得:
Tout= ((arr+1)*(psc+1))/Tclk= ((499+1)*(299+1))/300000000=0.0005s
再由频率是周期的倒数关系得到PWM的频率为2000Hz。
占空比怎么计算的呢?结合图20.3.1,我们分两种情况分析,输出比较极性为低和输出比较极性为高,它们的情况正好相反。因为在main函数中的比较值是动态变化的,不利于我们计算占空比,我们假设比较值固定为200,在本实验中可以调用如下语句得到。

  1. __HAL_TIM_SET_COMPARE(&g_gtimx_handle, GTIM_TIMX_CHY, compare);
复制代码
因为LED0是低电平有效,所以我们在gtim_timx_pwm_chy_init函数中设置了输出比较极性为低,那么当比较值固定为200时,占空比 = (arr+1) – CCR1 / (arr+1) = 500-200/500=60%。其中arr是写入自动重载寄存器(TIMx_ARR)的值,CCR1就是写入捕获/比较寄存器1(TIMx_CCR1)的值。这里我们还需要提醒一下,占空比是指在一个周期内,高电平时间相对于总时间所占的比例。
另外一种情况:设置了输出比较极性为高,那么当比较值固定为200时,占空比 = CCR1 / (arr+1) = 200/500=40%。可以看到输出比较极性为低和输出比较极性为高的占空比正好反过来。
在这里,我们使用DS100示波器进行验证,效果图如图20.3.3.3.1所示:


第二十章 通用定时器实验23670.png
图20.3.3.3.1 验证效果图

这里把输出比较极性低和输出比较极性高的PWM波形都显示出来了。本实验默认设置PWM模式1、输出比较极性低,当CCR1寄存器的值设置为200时,对应的PWM波形如上图黄色的波形图。如果把输出比较极性设置为高,对应的波形图就是绿色的波形图了。
大家感兴趣也可以自行用示波器进行验证。


20.3.4 下载验证
下载代码后,定时器4通道3输出PWM信号到PD14口。可以看到LED1闪烁,LED0不停的由暗变到亮,然后又从亮变到暗。

20.4 通用定时器输入捕获实验
本小节我们来学习使用通用定时器的输入捕获模式。
输入捕获模式可以用来测量脉冲宽度或者测量频率。我们以测量脉宽为例,用一个简图来说明输入捕获脉宽测量原理,如图20.4.1所示:


第二十章 通用定时器实验24010.png
图20.4.1 输入捕获脉宽测量原理

图20.4.1中,t1到t2的时间段,就是我们需要测量的高电平时间。测量方法如下:假如定时器工作在递增计数模式,首先设置定时器通道x为上升沿捕获,这样在t1时刻上升沿到来时,就会发生捕获事件。这里我们还会打开捕获中断,所以捕获事件发生就意味着捕获中断也会发生。在捕获中断里将计数器值清零,并设置通道x为下降沿捕获,这样t2时刻下降沿到来时,就会发生捕获事件和捕获中断。捕获事件发生时,计数器的值会被锁存到捕获/比较寄存器中(比如通道1对应的是CCR1寄存器)。那么在捕获中断里,我们读取捕获/比较寄存器就可以获取到高电平脉冲时间内,计数器计数的个数,从而可以算出高电平脉冲的时间。这里是假设定时器没有溢出为前提的。
实际上,t1到t2时间段,定时器可能会产生N次溢出,这就需要我们对定时器溢出做相应的处理,防止高电平太长,导致测量出错。在t1到t2时间段,假设定时器溢出N次,那么高电平脉冲时间内,计数器计数的个数计算方法为:N*(ARR+1)+ CCRx2,CCRx2表示t2时间点,捕获/比较寄存器的值。经过计算得到高电平脉宽时间内计数器计数个数后,用这个个数乘以计数器的计数周期,就可得到高电平持续的时间。就是输入捕获测量高电平脉宽时间的整个过程。
STM32H7R7的定时器除了TIM6和TIM7,其他定时器都有输入捕获功能。输入捕获,简单的说就是通过检测TIMx_Chy输入的信号边沿,在信号边沿发生跳变(比如上升沿/下降沿)时,会发生捕获事件,将当前定时器的值(TIMx_CNT)锁存到对应通道的捕获/比较寄存器(TIMx_CCRy)里,完成一次捕获。同时还可以配置捕获事件发生时是否触发捕获中断/DMA。
另外还要考虑测量的过程中是否可能发生定时器溢出,如果可能溢出,还要做溢出处理。


20.4.1 TIM2/TIM3/TIM4/TIM5寄存器
通用定时器输入捕获实验需要用到的寄存器有:TIMx_ARR、TIMx_PSC、TIMx_CCMR1、TIMx_CCER、TIMx_DIER、TIMx_CR1、TIMx_CCR1这些寄存器在前面的章节都有提到,在这里只需针对性的介绍。
捕获/比较模式寄存器1/2(TIMx_CCMR1/2)
该寄存器我们在PWM输出实验时讲解了他作为输出功能的配置,现在重点学习输入捕获模式的配置。因为本实验我们用到定时器2通道1输入,所以我们要看TIMx_CCMR1寄存器,其描述如图20.4.1.1所示:


第二十章 通用定时器实验25059.png
图20.4.1.1 TIMx_CCMR1寄存器

该寄存器在输入模式和输出模式下,功能是不一样,所以需要看准不同模式的描述,请打开手册查看。TIMx_CCMR1寄存器对应于通道1和通道2的设置,CCMR2寄存器对应通道3和通道4。如:TIMx_CCMR1寄存器位[7:0]和位16用于捕获/比较通道1的控制,而位[15:8]和位24则用于捕获/比较通道2的控制。我们用到定时器2通道1输入,所以需要配置TIMx_CCMR1的位[7:0]和位16。
其中CC1S[1:0],这两个位用于CCR1的通道配置,这里我们设置IC1S[1:0]=01,也就是配置IC1映射在TI1上。
输入捕获1预分频器IC1PSC[1:0],这个比较好理解。我们是1次边沿就触发1次捕获,所以选择00就行了。
输入捕获1滤波器IC1F[3:0],这个用来设置输入采样频率和数字滤波器长度。其中,fCK_INT是定时器时钟源频率,按照例程的配置为300Mhz,而fDTS则是根据TIMx_CR1的CKD[1:0]的设置来确定的,如果CKD[1:0]设置为00,那么fDTS=fCK_INT。N值采样次数,举个简单的例子:假设IC1F[3:0]=0011,并设置IC1映射到TI1上。表示以fCK_INT为采样频率,当连续8次都是采样到TI1为高电平或者低电平,滤波器才输出一个有效输出边沿。当8次采样中有高有低,那就保持原来的输出,这样可以滤除高频干扰信号,从而达到滤波的效果。这里,我们不做滤波处理,所以设置IC1F[3:0]=0000。
捕获/比较使能寄存器(TIMx_ CCER)
TIM2/TIM3/TIM4/TIM5的捕获/比较使能寄存器,该寄存器控制着各个输入输出通道的开关和极性。TIMx_CCER寄存器描述如图20.4.1.2所示:


第二十章 通用定时器实验25823.png
图20.4.1.2 TIMx_CCER寄存器

我们要用到这个寄存器的最低2位,CC1E和CC1P位。要使能输入捕获,必须设置CC1E=1,而CC1P则根据自己的需要来配置。我们这里是保留默认设置值0,即高电平触发捕获。
接下来我们再看看DMA/中断使能寄存器:TIMx_DIER,该寄存器的各位描述见图20.2.1.3(在20.2.1小节)。本小节,我们需要用到中断来处理捕获数据,所以必须开启通道1的捕获比较中断,即CC1IE设置为1。同时我们还需要在定时器溢出中断中累计定时器溢出的次数,所以还需要使能定时器的更新中断,即UIE置1。
控制寄存器:TIMx_CR1,我们只用到了它的最低位,也就是用来使能定时器的。
最后再来看看捕获/比较寄存器1:TIMx_CCR1,该寄存器用来存储发生捕获事件时,TIMx_CNT的值,我们从TIMx_CCR1就可以读出通道1捕获事件发生时刻的TIMx_CNT值,通过两次捕获(一次上升沿捕获,一次下降沿捕获)的差值,就可以计算出高电平脉冲的宽度(注意,对于高电平脉宽太长的情况,还要计算定时器溢出的次数)。


20.4.2 硬件设计

1. 例程功能
1、使用TIM2_CH1来做输入捕获,捕获PA0上的高电平脉宽,并将脉宽时间通过串口打印出来,通过按WK_UP按键,模拟输入高电平,例程中能测试的最长高电平脉宽时间为:4194303 us。
2、LED1闪烁指示程序运行。

2. 硬件资源
1)LED灯
            LED : LED1 – PC0
2)独立按键:WK_UP - PA0
3)定时器2,使用TIM2通道1,将PA0复用为TIM2_CH1。

3. 原理图
定时器属于STM32H7R7的内部资源,只需要软件设置好即可正常工作。我们借助WK_UP做输入脉冲源并通过串口上位机来监测定时器输入捕获的情况。


20.4.3 程序设计

20.4.3.1 定时器的HAL库驱动
定时器在HAL库中的驱动代码在前面已经介绍了部分,这里我们再介绍几个本实验用到的函数。

1. HAL_TIM_IC_Init函数
定时器的输入捕获模式初始化函数,其声明如下:

  1. HAL_StatusTypeDef HAL_TIM_IC_Init(TIM_HandleTypeDef *htim);
复制代码
函数描述:
用于初始化定时器的输入捕获模式。
函数形参:
形参1是TIM_HandleTypeDef结构体类型指针变量,介绍基本定时器的时候已经介绍。
函数返回值:
HAL_StatusTypeDef枚举类型的值。
注意事项:
与PWM输出实验一样,当使用定时器做输入捕获功能时,在HAL库中并不使用定时器初始化函数HAL_TIM_Base_Init来实现,而是使用输入捕获特定的定时器初始化函数HAL_TIM_IC_Init。该函数内部还会调用输入捕获初始化回调函数HAL_TIM_IC_MspInit来初始化输入通道对应的GPIO(复用),以及输入捕获相关的配置。

2. HAL_TIM_IC_ConfigChannel函数
定时器的输入捕获通道设置初始化函数。其声明如下:

  1. HAL_StatusTypeDef HAL_TIM_IC_ConfigChannel(TIM_HandleTypeDef *htim,
  2. TIM_IC_InitTypeDef *sConfig, uint32_t Channel);
复制代码
函数描述:
该函数用于设置定时器的输入捕获通道。
函数形参:
形参1是TIM_HandleTypeDef结构体类型指针变量,用于配置定时器基本参数。
形参2是TIM_IC_InitTypeDef结构体类型指针变量,用于配置定时器的输入捕获参数。
重点了解一下TIM_IC_InitTypeDef结构体指针类型,其定义如下:

  1. typedef struct
  2. {
  3.   uint32_t ICPolarity;                 /* 输入捕获触发方式选择,比如上升、下降和双边沿捕获 */
  4.   uint32_t ICSelection;         /* 输入捕获选择,用于设置映射关系 */
  5.   uint32_t ICPrescaler;         /* 输入捕获分频系数 */
  6.   uint32_t ICFilter;                 /* 输入捕获滤波器设置 */
  7. } TIM_IC_InitTypeDef;
复制代码
该结构体成员我们现在介绍一下。成员变量ICPolarity用来设置输入信号的有效捕获极性,取值范围为:TIM_ICPOLARITY_RISING(上升沿捕获),TIM_ICPOLARITY_FALLING(下降沿捕获)和TIM_ICPOLARITY_BOTHEDGE(双边沿捕获)。成员变量ICSelection用来设置映射关系,我们配置IC1直接映射在tim_ti1_fp1上,选择TIM_ICSELECTION_DIRECTTI(另外还有两个输入通道TIM_ICSELECTION_INDIRECTTI 和TIM_ICSELECTION_TRC)。成员变量ICPrescaler用来设置输入捕获分频系数,可以设置为TIM_ICPSC_DIV1(不分频),TIM_ICPSC_DIV2(2分频),TIM_ICPSC_DIV4(4分频)以及TIM_ICPSC_DIV8(8分频),本实验需要设置为不分频,所以选值为TIM_ICPSC_DIV1。成员变量ICFilter用来设置滤波器长度,这里我们不使用滤波器,所以设置为0。
形参3是定时器通道,范围:TIM_CHANNEL_1到TIM_CHANNEL_6,比如定时器5只有4个通道,那选择范围只有TIM_CHANNEL_1到TIM_CHANNEL_4,就具体情况选择。
函数返回值:
HAL_StatusTypeDef枚举类型的值。

3. HAL_TIM_IC_Start_IT函数
启动定时器输入捕获模式函数,其声明如下:

  1. HAL_StatusTypeDef HAL_TIM_IC_Start_IT(TIM_HandleTypeDef *htim,
  2.                                         uint32_t Channel);
复制代码
函数描述:
用于启动定时器的输入捕获模式,且开启输入捕获中断。
函数形参:
形参1是TIM_HandleTypeDef结构体类型指针变量。
形参2是定时器通道,范围:TIM_CHANNEL_1到TIM_CHANNEL_6。
函数返回值:
HAL_StatusTypeDef枚举类型的值。
注意事项:
如果我们不需要开启输入捕获中断,只是开启输入捕获功能,HAL库函数为:

  1. HAL_StatusTypeDef HAL_TIM_IC_Start(TIM_HandleTypeDef *htim, uint32_t Channel);
复制代码
定时器输入捕获模式配置步骤
1)开启TIMx和相应输入通道的GPIO时钟,配置该IO口的复用功能输入
首先开启TIMx的时钟,然后配置GPIO为复用功能输出。本实验我们默认用到定时器2通道1,对应IO是PA0,它们的时钟开启方法如下:

  1. __HAL_RCC_TIM2_CLK_ENABLE();                    /* 使能定时器2 */
  2. __HAL_RCC_GPIOA_CLK_ENABLE();                   /* 开启GPIOA时钟 */
复制代码
IO口复用功能是通过函数HAL_GPIO_Init来配置的。
2)初始化TIMx,设置TIMx的ARR和PSC等参数
使用定时器的输入捕获功能时,我们调用的是HAL_TIM_IC_Init函数来初始化定时器ARR和PSC等参数。
注意:该函数会调用:HAL_TIM_IC_MspInit函数,我们可以通过后者存放定时器和GPIO时钟使能、GPIO初始化、中断使能以及优先级设置等代码。
3)设置定时器为输入捕获模式,开启输入捕获
在HAL库中,定时器的输入捕获模式是通过HAL_TIM_IC_ConfigChannel函数来设置定时器某个通道为输入捕获通道,包括映射关系,输入滤波和输入分频等。
4)使能定时器更新中断,开启捕获功能以及捕获中断,配置定时器中断优先级
通过__HAL_TIM_ENABLE_IT函数使能定时器更新中断。
通过HAL_TIM_IC_Start_IT函数使能定时器并开启捕获功能以及捕获中断。
通过HAL_NVIC_EnableIRQ函数使能定时器中断。
通过HAL_NVIC_SetPriority函数设置中断优先级。
因为我们要捕获的是高电平信号的脉宽,所以,第一次捕获是上升沿,第二次捕获是下降沿,必须在捕获上升沿之后,设置捕获边沿为下降沿,同时,如果脉宽比较长,那么定时器就会溢出,对溢出必须做处理,否则结果就不准了。
5)编写中断服务函数
定时器中断服务函数为:TIMx_IRQHandler等,当发生中断的时候,程序就会执行中断服务函数。HAL库提供了一个定时器中断公共处理函数HAL_TIM_IRQHandler,该函数会根据中断类型调用相关的中断回调函数。用户根据自己的需要重定义这些中断回调函数来处理中断程序。本实验我们除了用到更新(溢出)中断回调函数HAL_TIM_PeriodElapsedCallback之外,还要用到捕获中断回调函数HAL_TIM_IC_CaptureCallback。详见本实验例程源码。

20.4.3.2 程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。通用定时器驱动源码包括两个文件:gtim.c和gtim.h。
首先看gtim.h头文件的几个宏定义:

  1. #define GTIM_TIMX                       TIM2
  2. #define GTIM_TIMX_IRQn                  TIM2_IRQn
  3. #define GTIM_TIMX_IRQHandler            TIM2_IRQHandler
  4. #define GTIM_TIMX_CLK_ENABLE()          do { __HAL_RCC_TIM2_CLK_ENABLE(); } while (0)
  5. #define GTIM_TIMX_CHY                   TIM_CHANNEL_1
  6. #define GTIM_TIMX_CHY_GPIO_PORT         GPIOA
  7. #define GTIM_TIMX_CHY_GPIO_PIN          GPIO_PIN_0
  8. #define GTIM_TIMX_CHY_GPIO_AF           GPIO_AF1_TIM2
  9. #define GTIM_TIMX_CHY_GPIO_CLK_ENABLE() do { __HAL_RCC_GPIOA_CLK_ENABLE(); } while (0)
复制代码
可以把上面的宏定义分成两部分,第一部分是定时器2输入通道1对应的IO口的宏定义,第二部分则是定时器2输入通道1的相应宏定义。
gtim.h头文件就添加了这部分的程序,下面看gtim.c的程序,首先是通用定时器输入捕获初始化函数,其定义如下:

  1. /**
  2. * @brief   初始化通用定时器输入捕获
  3. * @param   arr: 自动重装载值
  4. * @param   psc: 预分频系数
  5. * @retval  无
  6. */
  7. void gtim_timx_cap_chy_init(uint16_t arr, uint16_t psc)
  8. {
  9.     TIM_IC_InitTypeDef timx_ic_cap_struct = {0};
  10.    
  11.     /* 初始化TIM输入捕获 */
  12.     g_gtimx_handle.Instance = GTIM_TIMX;
  13.     g_gtimx_handle.Init.Prescaler = psc;                       /* 预分频系数 */
  14.     g_gtimx_handle.Init.CounterMode = TIM_COUNTERMODE_UP;      /* 计数模式 */
  15.     g_gtimx_handle.Init.Period = arr;                          /* 重装载值 */
  16.     g_gtimx_handle.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;/* 时钟分频 */
  17. g_gtimx_handle.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
  18. /* 自动重装载预加载模式 */
  19.     HAL_TIM_IC_Init(&g_gtimx_handle);
  20.    
  21.     /* 配置TIM输入捕获通道 */
  22.     timx_ic_cap_struct.ICPolarity = TIM_ICPOLARITY_RISING;     /* 极性 */
  23.     timx_ic_cap_struct.ICSelection = TIM_ICSELECTION_DIRECTTI; /* 输入选择 */
  24.     timx_ic_cap_struct.ICPrescaler = TIM_ICPSC_DIV1;           /* 输入分频 */
  25.     timx_ic_cap_struct.ICFilter = 0;                           /* 输入滤波 */
  26. HAL_TIM_IC_ConfigChannel(&g_gtimx_handle, &timx_ic_cap_struct,
  27. GTIM_TIMX_CHY);
  28.    
  29.     /* 使能TIM更新中断 */
  30.     __HAL_TIM_ENABLE_IT(&g_gtimx_handle, TIM_IT_UPDATE);
  31.    
  32.     /* 开启TIM中断模式输入捕获 */
  33.     HAL_TIM_IC_Start_IT(&g_gtimx_handle, GTIM_TIMX_CHY);
  34. }
复制代码
HAL_TIM_IC_Init初始化定时器的基础工作参数,如:ARR和PSC等,第二部分则是调用HAL_TIM_IC_ConfigChannel函数配置输入捕获通道映射关系,滤波和分频等。最后是使能更新中断和使能通道输入以及定时器捕获中断。通道对应的IO、时钟开启和NVIC的初始化都在HAL_TIM_IC_MspInit函数里编写,其定义如下:
  1. /**
  2. * @brief   HAL库定时器输出捕获初始化MSP函数
  3. * @param   无
  4. * @retval  无
  5. */
  6. void HAL_TIM_IC_MspInit(TIM_HandleTypeDef *htim)
  7. {
  8.     GPIO_InitTypeDef gpio_init_struct;
  9.    
  10.     /* 输入捕获定时器 */
  11.     if (htim->Instance == GTIM_TIMX)
  12.     {
  13.         /* 使能相关时钟 */
  14.         GTIM_TIMX_CHY_GPIO_CLK_ENABLE();
  15.         GTIM_TIMX_CLK_ENABLE();
  16.         
  17.         /* 初始化输入捕获引脚 */
  18.         gpio_init_struct.Pin = GTIM_TIMX_CAP_CHY_GPIO_PIN;
  19.         gpio_init_struct.Mode = GPIO_MODE_AF_PP;
  20.         gpio_init_struct.Pull = GPIO_PULLDOWN;
  21.         gpio_init_struct.Speed = GPIO_SPEED_FREQ_LOW;
  22.         gpio_init_struct.Alternate = GTIM_TIMX_CAP_CHY_GPIO_AF;
  23.         HAL_GPIO_Init(GTIM_TIMX_CAP_CHY_GPIO_PORT, &gpio_init_struct);
  24.         
  25.         /* 配置中断优先级并使能中断 */
  26.         HAL_NVIC_SetPriority(GTIM_TIMX_CAP_IRQn, 0, 0);
  27.         HAL_NVIC_EnableIRQ(GTIM_TIMX_CAP_IRQn);
  28.     }
  29. }
复制代码
该函数调用HAL_GPIO_Init函数初始化定时器输入通道对应的IO,并且开启GPIO时钟,使能定时器。其中要注意IO口复用功能的选择一定要选对了。最后配置中断抢占优先级和响应优先级,以及打开定时器中断。
通过上面的两个函数输入捕获的初始化就完成了,下面先来介绍两个变量。

  1. /* 输入捕获定时器相关变量 */
  2. uint8_t g_timx_chy_cap_sta = 0;     /* [7]: 捕获完成 [6]: 捕获到高电平 [5:0]: 捕获计数值溢出计数器 */
  3. uint16_t g_timx_chy_cap_val = 0;    /* 捕获的计数值 */
复制代码
这两个变量用于辅助实现高电平捕获。其中g_timxchy_cap_sta,是用来记录捕获状态, (这个变量,我们把它当成一个寄存器那样来使用)。对其各位赋予状态含义,描述如下表所示:

3.png
表20.4.3.3.1 g_timxchy_cap_sta各位描述

变量g_timxchy_cap_sta的位[5:0]是用于记录捕获高电平定时器溢出次数,总共6位,所以最多可以记录溢出的次数为2的6次方减一次,即63次。
变量g_timxchy_cap_val,则用来记录捕获到下降沿的时候,TIM2_CNT寄存器的值。
下面开始看中断服务函数的逻辑程序,HAL_TIM_IRQHandler函数会调用下面两个回调函数,我们的逻辑代码就是放在回调函数里,函数定义如下:

  1. /**
  2. * @brief   HAL库TIM超时中断回调函数
  3. * @param   TIM句柄指针
  4. * @retval  无
  5. */
  6. void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
  7. {
  8.     if (htim->Instance == GTIM_TIMX)
  9.     {
  10.         if ((g_timx_chy_cap_sta & 0x80) == 0)               /* 还未捕获完成 */
  11.         {
  12.             if ((g_timx_chy_cap_sta & 0x40) != 0)           /* 已经捕获到高电平 */
  13.             {
  14.                 if ((g_timx_chy_cap_sta & 0x3F) == 0x3F)
  15. /* 捕获计数值溢出计数器已满 */
  16.                 {
  17.                     /* 重新使能捕获并强制标记为捕获完成 */
  18.                     TIM_RESET_CAPTUREPOLARITY(&g_gtimx_handle, GTIM_TIMX_CHY);
  19.                     TIM_SET_CAPTUREPOLARITY(&g_gtimx_handle, GTIM_TIMX_CHY,
  20. TIM_ICPOLARITY_RISING);
  21.                     g_timx_chy_cap_sta |= 0x80;
  22.                     g_timx_chy_cap_val = 0xFFFF;
  23.                 }
  24.                 else
  25.                 {
  26.                     /* 更新捕获计数值溢出计数器 */
  27.                     g_timx_chy_cap_sta++;
  28.                 }
  29.             }
  30.         }
  31.     }
  32. }

  33. /**
  34. * @brief   HAL库TIM输入捕获中断回调函数
  35. * @param   TIM句柄指针
  36. * @retval  无
  37. */
  38. void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
  39. {
  40.     if (htim->Instance == GTIM_TIMX)
  41.     {
  42.         if ((g_timx_chy_cap_sta & 0x80) == 0)       /* 捕获未完成 */
  43.         {
  44.             if ((g_timx_chy_cap_sta & 0x40) != 0)   /* 已捕获到上升沿 */
  45.             {
  46.                 /* 标记为捕获完成并重新开启输入捕获 */
  47.                 g_timx_chy_cap_sta |= 0x80;
  48.                 g_timx_chy_cap_val = HAL_TIM_ReadCapturedValue(&g_gtimx_handle,
  49. GTIM_TIMX_CHY);
  50.                 TIM_RESET_CAPTUREPOLARITY(&g_gtimx_handle, GTIM_TIMX_CHY);
  51.                 TIM_SET_CAPTUREPOLARITY(&g_gtimx_handle, GTIM_TIMX_CHY,
  52. TIM_ICPOLARITY_RISING);
  53.             }
  54.             else                                    /* 未捕获到上升沿 */
  55.             {
  56.                 /* 从捕获到第一个上升沿开始计数 */
  57.                 g_timx_chy_cap_sta = 0;
  58.                 g_timx_chy_cap_val = 0;
  59.                 g_timx_chy_cap_sta |= 0x40;
  60.                 __HAL_TIM_DISABLE(&g_gtimx_handle);
  61.                 __HAL_TIM_SET_COUNTER(&g_gtimx_handle, 0);
  62.                 TIM_RESET_CAPTUREPOLARITY(&g_gtimx_handle, GTIM_TIMX_CHY);
  63.                 TIM_SET_CAPTUREPOLARITY(&g_gtimx_handle, GTIM_TIMX_CHY,
  64. TIM_ICPOLARITY_FALLING);
  65.                 __HAL_TIM_ENABLE(&g_gtimx_handle);
  66.             }
  67.         }
  68.     }
  69. }
复制代码
现在我们来介绍一下,捕获高电平脉宽的思路:首先,设置TIM2_CH1捕获上升沿,然后等待上升沿中断到来,当捕获到上升沿中断,此时如果g_timxchy_cap_sta的第6位为0,则表示还没有捕获到新的上升沿,就先把g_timxchy_cap_sta、g_timxchy_cap_val和TIM2_CNT寄存器等清零,然后再设置g_timxchy_cap_sta的第6位为1,标记捕获到高电平,最后设置为下降沿捕获,等待下降沿到来。如果等待下降沿到来期间,定时器发生了溢出,就用g_timxchy_cap_sta变量对溢出次数进行计数,当最大溢出次数来到的时候,就强制标记捕获完成,并配置定时器通道上升沿捕获。当下降沿到来的时候,先设置g_timxchy_cap_sta的第7位为1,标记成功捕获一次高电平,然后读取此时的定时器值到g_timxchy_cap_val里面,最后设置为上升沿捕获,回到初始状态。
这样,我们就完成一次高电平捕获了,只要g_timxchy_cap_sta的第7位一直为1,那么就不会进行第二次捕获,我们在main函数处理完捕获数据后,将g_timxchy_cap_sta置零,就可以开启第二次捕获。
在main.c里面编写如下代码:

  1. int main(void)
  2. {
  3.     uint32_t total = 0;
  4.     uint8_t t = 0;
  5.    
  6.     sys_mpu_config();                           /* 配置MPU */
  7.     sys_cache_enable();                         /* 使能Cache */
  8.     HAL_Init();                                 /* 初始化HAL库 */
  9.     sys_stm32_clock_init(300, 6, 2);            /* 配置时钟,600MHz */
  10.     delay_init(600);                            /* 初始化延时 */
  11.     usart_init(115200);                         /* 初始化串口 */
  12.     led_init();                                 /* 初始化LED */
  13. gtim_timx_cap_chy_init(0xFFFF, 300 - 1);   
  14. /* 初始化通用定时器输入捕获,捕获频率为1MHz */
  15.    
  16.     while (1)
  17.     {
  18.         if ((g_timx_chy_cap_sta & 0x80) != 0)   /* 捕获完成 */
  19.         {
  20.             total = g_timx_chy_cap_sta & 0x3F;  /* 计算总捕获到的计数值 */
  21.             total *= 0xFFFF;
  22.             total += g_timx_chy_cap_val;
  23.             printf("High: %dus\r\n", total);
  24.             g_timx_chy_cap_sta = 0;             /* 开启下一次输入捕获 */
  25.         }
  26.         
  27.         if (++t == 20)
  28.         {
  29.             t = 0;
  30.             LED1_TOGGLE();
  31.         }
  32.         
  33.         delay_ms(10);
  34.     }
  35. }
复制代码
先看gtim_timx_cap_chy_init(0XFFFF, 300 - 1)这个语句,这两个形参分别设置自动重载寄存器的值为65535,以及预分频器寄存器的值为299。定时器2是32位的计数器,为了通用性,我们只使用16位,所以计数器设置为65535。预分频系数,我们设置为300分频,定时器2的时钟频率是2倍的APB1总线时钟频率,即300MHz,可以得到计数器的计数频率是1MHz,即1us计数一次,所以我们的捕获时间精度是1us。这里可以知道定时器的溢出时间是65536us。
while(1)无限循环通过判断g_timxchy_cap_sta的第7位,来获知有没有成功捕获到一次高电平,如果成功捕获,先计算总的高电平时间,再通过串口传输到电脑。

20.4.4 下载验证
下载代码后,可以看到LED1在闪烁,说明程序已经正常在跑了,我们再打开串口调试助手,选择对应的串口端口,我这边是COM14,然后按KEY_UP按键,可以看到串口打印的高电平持续时间,如图20.4.4.1所示:


第二十章 通用定时器实验37675.png
图20.4.4.1 打印捕获到的高电平时间

20.5 通用定时器脉冲计数实验
前面我们介绍了通用定时器的四类时钟源,本小节我们来学习使用通用定时器的外部时钟模式1这类时钟源。
前面的三个通用定时器实验的时钟源都是来自内部时钟 (tim_ker_ck),本实验我们将使用外部时钟模式 1:外部输入引脚 (tim_ti1或tim_ti2)作为定时器的时钟源。关于这个外部输入引脚(tim_ti1或tim_ti2),我们使用WK_UP按键按下产生的高电平脉冲作为定时器的计数器时钟,每按下一次按键产生一次高电平脉冲,计数器加一。
下面通过框图给大家展示本实验用到定时器内部哪些资源,如下图所示:


第二十章 通用定时器实验37974.png
图20.5.1 脉冲计数实验原理

前面介绍过,外部时钟模式1的外部输入引脚只能是通道1或者通道2对应的IO,通道3或者通道4是不可以的。以通道1输入为例,外部时钟源信号通过通道1输入后,接下来我们用tim_ti1表示该信号。tim_ti1分别要经过滤波器、边沿检测器后,来到tim_ti1fp1,被触发输入选择器选择为触发源,接着来到从模式控制器。从模式选择为外部时钟模式1,这时候外部时钟源信号就会到达时基单元的预分频器,后面就是经过分频后就作为计数器的计数时钟了。这个过程的描述,大家亦可参考前面介绍外部时钟模式1的描述。因为前面已经介绍过,这里只是简单讲一下。
如果大家想时钟源信号的上升沿和下降沿,计数器都计数,可以选择tim_ti1f_ed作为触发输入选择器的触发源。
假设计数器工作在递增计数模式,那么每来一个选择的边沿,计数器就加一。最后,外部时钟源信号的边沿计数个数会保存计数器寄存器中,我们只需要直接读取CNT的值即可。这里是没有考虑定时器溢出的情况,如果定时器溢出还需要对溢出进行处理。比如开启更新中断,定时器溢出后,在更新中断里,对溢出次数进行记录,然后用溢出次数乘以溢出一次计数的个数,再加上CNT现在的值,就可以得到总的计数个数了。在例程中,我们也是这样处理的。
下面开始讲解本实验用到的寄存器配置情况。


20.5.1 TIM2/TIM3/TIM4/TIM5寄存器
通用定时器脉冲计数实验需要用到的寄存器有:TIMx_ARR、TIMx_PSC、TIMx_CCMR1、TIMx_CCER、TIMx_DIER、TIMx_CR1、TIMx_EGR这些寄存器在前面的章节都有提到,在这里只需针对性的介绍。
捕获/比较模式寄存器1/2(TIMx_CCMR1/2)
该寄存器我们在PWM输出实验时讲解了他作为输出功能的配置,在输入捕获实验学习了输入捕获模式的配置,本小节我们的外部信号也同样要作为输入信号给定时器作为时钟源,所以我们要看输入捕获模式定时器对应功能。WK_UP按键(PA0)对应着定时器2的通道1,接下来我们开始配置TIMx_CCMR1寄存器,其描述如图20.5.1.1所示:


第二十章 通用定时器实验38880.png
图20.5.1.1 TIMx_CCMR1寄存器

用到定时器2的通道1,所以要配置TIM2_CCMR1寄存器的位[7:0],其中CC1S[1:0],这两个位用于CCR1的通道配置,这里我们设置IC1S[1:0]=01,也就是配置IC1映射在tim_ti1上,即CCR1对应TIMx_CH1。
输入捕获1预分频器IC1PSC[1:0],我们是1次高电平脉冲就触发1次计数,所以不用分频选择00即可。
输入捕获1滤波器IC1F[3:0],这个用来设置输入采样频率和数字滤波器长度,关于滤波长度的介绍请看上一个实验。这里,我们不做滤波处理,所以设置IC1F[3:0]=0000,只要采集到上升沿,就触发捕获。
捕获/比较使能寄存器(TIMx_ CCER)
TIM2/TIM3/TIM4/TIM5的捕获/比较使能寄存器,该寄存器控制着各个输入输出通道的开关和极性。TIMx_CCER寄存器描述如图20.5.1.2所示:


第二十章 通用定时器实验39288.png
图20.5.1.2 TIMx_CCER寄存器

我们要用到这个寄存器的最低2位,CC1E和CC1P位。要使能输入捕获,必须设置CC1E=1,而CC1P则根据自己的需要来配置。我们这里是保留默认设置值0,即高电平触发捕获。
从模式控制寄存器(TIMx_ SMCR)
TIM2/TIM3/TIM4/TIM5的从模式控制寄存器,该寄存器用于配置从模式,以及定时器的触发源相关的设置。TIMx_SMCR寄存器描述如图20.5.1.3所示:


第二十章 通用定时器实验39505.png
图20.5.1.3 TIMx_SMCR寄存器

因为我们要让外部引脚脉冲信号作为定时器的时钟源,所以位SMS[2:0]和位16组合的SMS[0:3],我们设置的值是0111,即外部时钟模式1。位[6:4] 和位[21:20]组合的TS[0:5]是触发选择设置,TIMx_CH1对应tim_ti1fp1,TIMx_CH2则对应tim_ti2fp2,我们是定时器通道1,所以需要配置的值为00101。ETF[3:0]和ETPS[1:0]分别是外部触发滤波器和外部触发预分频器,我们没有用到。
接下来我们再看看DMA/中断使能寄存器:TIMx_DIER,该寄存器的各位描述见图20.2.1.3(在20.2.1小节)。本实验,我们需要用到定时器更新中断,在中断服务函数中累加定时器溢出的次数,所以需要使能定时器的更新中断,即UIE置1。
控制寄存器1:TIMx_CR1,我们只用到了它的最低位,也就是用来使能定时器的。


20.5.2 硬件设计

1. 例程功能
使用TIM2_CH1做输入捕获,我们将捕获PA0上的高电平脉宽,并对脉宽进行计数,通过串口打印出来。大家可以通过按WK_UP按键,输入高电平脉冲,通过按KEY0重设当前计数。LED1闪烁,提示程序运行。

2. 硬件资源
1)LED灯
            LED : LED1 – PC0
2)独立按键:
            KEY0    – PE9
            WK_UP – PC13
3)串口1:
            USART1_TX  – PB14
            USART1_RX  – PB15
4)定时器2,使用TIM2通道1,PA0复用为TIM2_CH1。

3. 原理图
定时器属于STM32H7R7的内部资源,只需要软件设置好即可正常工作。我们借助WK_UP做输入脉冲源,捕获PA0上的高电平脉宽,然后对脉宽进行计数并通过串口上位机打印出来。还可以通过按KEY0重设当前计数。


20.5.3 程序设计

20.5.3.1 定时器的HAL库驱动
定时器在HAL库中的驱动代码在前面已经介绍了部分,这里我们针对定时器从模式介绍HAL_TIM_SlaveConfigSynchronization函数。
1. HAL_TIM_SlaveConfigSynchronization函数
该函数是HAL_TIM_SlaveConfigSynchro函数的宏定义,真正的函数定义是后者,其定义如下:

  1. HAL_StatusTypeDef HAL_TIM_SlaveConfigSynchro(TIM_HandleTypeDef *htim,
  2. TIM_SlaveConfigTypeDef *sSlaveConfig);
复制代码
函数描述:
该函数用于配置定时器的从模式选择、输入触发源等参数。
函数形参:
形参1是TIM_HandleTypeDef结构体类型指针变量,用于配置定时器基本参数。
形参2是TIM_SlaveConfigTypeDef结构体类型指针变量,用于配置定时器的从模式。
重点了解一下TIM_SlaveConfigTypeDef结构体指针类型,其定义如下:

  1. typedef struct
  2. {
  3.   uint32_t SlaveMode;                       /* 从模式选择 */
  4.   uint32_t InputTrigger;               /* 输入触发源选择 */
  5.   uint32_t TriggerPolarity;           /* 输入触发极性 */
  6.   uint32_t TriggerPrescaler;          /* 输入触发预分频 */
  7.   uint32_t TriggerFilter;                   /* 输入滤波器设置 */
  8. } TIM_SlaveConfigTypeDef;
复制代码
函数返回值:
HAL_StatusTypeDef枚举类型的值。
定时器从模式脉冲计数配置步骤
1)开启TIMx和输入通道的GPIO时钟,配置该IO口的复用功能输入
首先开启TIMx的时钟,然后配置GPIO为复用功能输出。本实验我们默认用到定时器2通道1,对应IO是PA0,它们的时钟开启方法如下:

  1. __HAL_RCC_TIM2_CLK_ENABLE();                    /* 使能定时器2 */
  2. __HAL_RCC_GPIOA_CLK_ENABLE();                   /* 开启GPIOA时钟 */
复制代码
IO口复用功能是通过函数HAL_GPIO_Init来配置的。
2)初始化TIMx,设置TIMx的ARR和PSC等参数
使用定时器的输入捕获功能时,我们调用的是HAL_TIM_IC_Init函数来初始化定时器ARR和PSC等参数。
注意:该函数会调用:HAL_TIM_IC_MspInit函数,我们可以通过后者存放定时器和GPIO时钟使能、GPIO初始化、中断使能和优先级设置等代码。
3)选择从模式:外部触发模式1
TIMx_SMCR寄存器控制着定时器的从模式,包括模式选择,触发源选择、极性和输入预分频等。HAL库是通过HAL_TIM_SlaveConfigSynchro函数来初始化定时器从模式配置参数的。
4)设置定时器为输入捕获模式,开启输入捕获
在HAL库中,定时器的输入捕获模式是通过HAL_TIM_IC_ConfigChannel函数来设置定时器某个通道为输入捕获通道,包括映射关系,输入滤波和输入分频等。
5)使能定时器更新中断,开启捕获功能,配置定时器中断优先级
通过__HAL_TIM_ENABLE_IT函数使能定时器更新中断。
通过HAL_TIM_IC_Start函数使能定时器并开启捕获功能。
通过HAL_NVIC_EnableIRQ函数使能定时器中断。
通过HAL_NVIC_SetPriority函数设置中断优先级。
6)编写中断服务函数
定时器中断服务函数为:TIMx_IRQHandler等,当发生中断的时候,程序就会执行中断服务函数。HAL库提供了一个定时器中断公共处理函数HAL_TIM_IRQHandler,该函数会根据中断类型调用相关的中断回调函数。用户根据自己的需要重定义这些中断回调函数来处理中断程序。本实验我们不使用HAL库的中断回调机制,而是把中断程序写在定时器中断服务函数里。详见本实验例程源码。


20.5.3.2 程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。通用定时器驱动源码包括两个文件:gtim.c和gtim.h。
首先看gtim.h头文件的几个宏定义:

  1. #define GTIM_TIMX                       TIM2
  2. #define GTIM_TIMX_IRQn                  TIM2_IRQn
  3. #define GTIM_TIMX_IRQHandler            TIM2_IRQHandler
  4. #define GTIM_TIMX_CLK_ENABLE()          do { __HAL_RCC_TIM2_CLK_ENABLE(); } while (0)
  5. #define GTIM_TIMX_CHY                   TIM_CHANNEL_1
  6. #define GTIM_TIMX_CHY_GPIO_PORT         GPIOA
  7. #define GTIM_TIMX_CHY_GPIO_PIN          GPIO_PIN_0
  8. #define GTIM_TIMX_CHY_GPIO_AF           GPIO_AF1_TIM2
  9. #define GTIM_TIMX_CHY_GPIO_CLK_ENABLE() do { __HAL_RCC_GPIOA_CLK_ENABLE(); } while (0)
复制代码
可以把上面的宏定义分成两部分,第一部分是定时器2输入通道1对应的IO口的宏定义,第二部分则是定时器2输入通道1的相应宏定义。需要注意的点是:只有CH1和CH2通道可以用做输入计数,CH3/CH4不支持!
gtim.h头文件就添加了这部分的程序,下面看gtim.c的程序,首先是通用定时器脉冲计数初始化函数,其定义如下:

  1. /**
  2. * @brief   初始化通用定时器脉冲计数
  3. * @param   psc: 预分频系数
  4. * @retval  无
  5. */
  6. void gtim_timx_cnt_chy_init(uint16_t psc)
  7. {
  8.     TIM_SlaveConfigTypeDef tim_slave_config_struct = {0};
  9.    
  10.     /* 初始化TIM输入捕获 */
  11.     g_gtimx_handle.Instance = GTIM_TIMX;
  12.     g_gtimx_handle.Init.Prescaler = psc;                        /* 预分频系数 */
  13.     g_gtimx_handle.Init.CounterMode = TIM_COUNTERMODE_UP;       /* 计数模式 */
  14.     g_gtimx_handle.Init.Period = 0xFFFF;                        /* 重装载值 */
  15.     g_gtimx_handle.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; /* 时钟分频 */
  16. g_gtimx_handle.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
  17. /* 自动重装载预加载模式 */
  18.     HAL_TIM_IC_Init(&g_gtimx_handle);
  19.    
  20.     /* 配置TIM从模式 */
  21.     tim_slave_config_struct.SlaveMode = TIM_SLAVEMODE_EXTERNAL1;  /* 从模式 */
  22.     tim_slave_config_struct.InputTrigger = TIM_TS_TI1FP1;         /* 触发源 */
  23. tim_slave_config_struct.TriggerPolarity = TIM_TRIGGERPOLARITY_RISING;   
  24. /* 触发极性 */
  25. tim_slave_config_struct.TriggerPrescaler = TIM_TRIGGERPRESCALER_DIV1;   
  26. /* 触发分频 */
  27.     tim_slave_config_struct.TriggerFilter = 0;                    /* 触发滤波 */
  28.     HAL_TIM_SlaveConfigSynchro(&g_gtimx_handle, &tim_slave_config_struct);
  29.    
  30.     /* 使能TIM更新中断 */
  31.     __HAL_TIM_ENABLE_IT(&g_gtimx_handle, TIM_IT_UPDATE);
  32.    
  33.     /* 开启TIM输入捕获 */
  34.     HAL_TIM_IC_Start(&g_gtimx_handle, GTIM_TIMX_CHY);
  35. }
复制代码
gtim_timx_cnt_chy_init函数主要包含了定时器基础工作参数和从模式配置的所有代码。下面来看看该函数的代码内容。
第一部分调用HAL_TIM_IC_Init函数初始化定时器的基础工作参数,如:ARR和PSC等。
第二部分调用HAL_TIM_SlaveConfigSynchro函数配置从模式选择、输入捕获通道映射关系、捕获边沿和滤波等。
最后是使能更新中断和使能通道输入。
初始化完成之后,会调用HAL_TIM_IC_MspInit函数进行IO的初始化和NVIC的初始化,配置过程如下:

  1. /**
  2. * @brief   HAL库TIM输入捕获初始化MSP函数
  3. * @param   TIM句柄指针
  4. * @retval  无
  5. */
  6. void HAL_TIM_IC_MspInit(TIM_HandleTypeDef *htim)
  7. {
  8.     GPIO_InitTypeDef gpio_init_struct;
  9.    
  10.     if (htim->Instance == GTIM_TIMX)
  11.     {
  12.         /* 使能时钟 */
  13.         GTIM_TIMX_CHY_GPIO_CLK_ENABLE();
  14.         GTIM_TIMX_CLK_ENABLE();
  15.         
  16.         /* 初始化输入捕获引脚 */
  17.         gpio_init_struct.Pin = GTIM_TIMX_CHY_GPIO_PIN;
  18.         gpio_init_struct.Mode = GPIO_MODE_AF_PP;
  19.         gpio_init_struct.Pull = GPIO_PULLDOWN;
  20.         gpio_init_struct.Speed = GPIO_SPEED_FREQ_LOW;
  21.         gpio_init_struct.Alternate = GTIM_TIMX_CHY_GPIO_AF;
  22.         HAL_GPIO_Init(GTIM_TIMX_CHY_GPIO_PORT, &gpio_init_struct);
  23.         
  24.         /* 配置中断优先级并使能中断 */
  25.         HAL_NVIC_SetPriority(GTIM_TIMX_IRQn, 0, 0);
  26.         HAL_NVIC_EnableIRQ(GTIM_TIMX_IRQn);
  27.     }
  28. }
复制代码
我们在通用定时器输入捕获实验使用了HAL_TIM_IC_MspInit函数,所以我们要先判断基地址,开启符合条件的定时器的功能,可以看到,我们先使能IO和时钟,然后配置对应的GPIO口引脚,最后配置抢占优先级、响应优先级和开启NVIC定时器中断。
下面我们看定时器中断服务函数,和上面一样,我们直接调用HAL库的定时器中断公共处理函数HAL_TIM_IRQHandler处理。

  1. /**
  2. * @brief   TIM中断服务函数
  3. * @param   无
  4. * @retval  无
  5. */
  6. void GTIM_TIMX_IRQHandler(void)
  7. {
  8.     HAL_TIM_IRQHandler(&g_gtimx_handle);
  9. }
复制代码
在基本定时器中断实验中,我们知道中断逻辑程序的逻辑代码是放在更新中断回调函数里面的,这是HAL库回调机制标准的做法。因为我们在通用定时器输入捕获实验中使用过HAL_TIM_PeriodElapsedCallback更新中断回调函数,所以首先也要先判断寄存器基地址,符合条件后,我们让g_timx_chy_cnt_ofcnt变量++,累计定时器溢出次数。这样就完成一次对更新中断的处理。
  1. /**
  2. * @brief   HAL库TIM超时中断回调函数
  3. * @param   TIM句柄指针
  4. * @retval  无
  5. */
  6. void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
  7. {
  8.     if (htim->Instance == GTIM_TIMX)
  9.     {
  10.         g_timx_chy_cnt_ofcnt++;
  11.     }
  12. }
复制代码
再来介绍两个自定义的功能函数,第一个是获取当前计数值函数,其定义如下:
  1. /**
  2. * @brief   获取通用定时器脉冲计数值
  3. * @param   无
  4. * @retval  脉冲计数值
  5. */
  6. uint32_t gtim_timx_cnt_chy_get_count(void)
  7. {
  8.     uint32_t total;
  9.    
  10.     /* 计算总脉冲计数值 */
  11.     total = g_timx_chy_cnt_ofcnt * 0xFFFF;
  12.     total += __HAL_TIM_GET_COUNTER(&g_gtimx_handle);
  13.    
  14.     return total;
  15. }
复制代码
该函数先是计算定时器溢出次数对应的计数个数,因为定时器每溢出一次的计数个数是65536,所以这里用g_timx_chy_cnt_ofcnt乘以65536(0XFFFF),就可以得到溢出计数的个数,前提是ARR寄存器的值得设置为65535。然后再加上定时器计数器当前的值,计数器当前的值通过调用__HAL_TIM_GET_COUNTER函数宏可以获取。函数返回值是脉冲计数的总个数。
第二个自定义功能函数是重启计数器函数,其定义如下:

  1. /**
  2. * @brief   重启通用定时器脉冲计数
  3. * @param   无
  4. * @retval  无
  5. */
  6. void gtim_timx_cnt_chy_restart(void)
  7. {
  8.     __HAL_TIM_DISABLE(&g_gtimx_handle);
  9.     g_timx_chy_cnt_ofcnt = 0;
  10.     __HAL_TIM_SET_COUNTER(&g_gtimx_handle, 0);
  11.     __HAL_TIM_ENABLE(&g_gtimx_handle);
  12. }
复制代码
该函数先关闭定时器,然后做清零操作,包括:记录溢出次数全局变量g_timx_chy_cnt_ofcnt和定时器计数器的值,最后使能定时器重新计数。
通用定时器脉冲计数实验的整体驱动和逻辑程序还算比较容易理解,下面看看main.c里面编写的代码:

  1. int main(void)
  2. {
  3.     uint8_t key;
  4.     uint32_t count;
  5.     uint32_t count_prev = 0;
  6.     uint8_t t = 0;
  7.    
  8.     sys_mpu_config();                           /* 配置MPU */
  9.     sys_cache_enable();                         /* 使能Cache */
  10.     HAL_Init();                                 /* 初始化HAL库 */
  11.     sys_stm32_clock_init(300, 6, 2);            /* 配置时钟,600MHz */
  12.     delay_init(600);                            /* 初始化延时 */
  13.     usart_init(115200);                         /* 初始化串口 */
  14.     led_init();                                 /* 初始化LED */
  15.     key_init();                                 /* 初始化按键 */
  16.     gtim_timx_cnt_chy_init(0);                  /* 初始化通用定时器脉冲计数 */
  17.     gtim_timx_cnt_chy_restart();                /* 重启通用定时器脉冲计数 */
  18.    
  19.     while (1)
  20.     {
  21.         key = key_scan(0);
  22.         if (key == KEY0_PRES)
  23.         {
  24.             gtim_timx_cnt_chy_restart();        /* 重启通用定时器脉冲计数 */
  25.         }
  26.         
  27.         count = gtim_timx_cnt_chy_get_count();  /* 获取通用定时器脉冲计数值 */
  28.         if (count_prev != count)                /* 脉冲计数值有更新 */
  29.         {
  30.             count_prev = count;
  31.             printf("Cnt: %d\r\n", count);
  32.         }
  33.         
  34.         if (++t == 20)
  35.         {
  36.             t = 0;
  37.             LED1_TOGGLE();
  38.         }
  39.         
  40.         delay_ms(10);
  41.     }
  42. }
复制代码
调用定时器初始化函数gtim_timx_cnt_chy_init(0),形参是0,表示设置预分频器寄存器的值为0,即表示不分频。如果形参设置为1,就是2分频,这种情况,要按按键WK_UP两次才会计数一次,大家不妨试试。该函数内部已经设置自动重载寄存器的值为65535,所以在不分频的情况下,定时器发生一次更新中断,表示脉冲计数了65536次。

20.5.4 下载验证
下载代码后,可以看到LED1在闪烁,说明程序已经正常在跑了,我们再打开串口调试助手,然后每按KEY_UP按键一次,就可以看到串口打印的高电平脉冲次数。如果按KEY0按键,就会重设当前计数,从0开始计数,如图20.5.4.1所示:

第二十章 通用定时器实验48469.png
图20.5.4.1 打印高电平脉冲次数
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则



关闭

原子哥极力推荐上一条 /1 下一条

正点原子公众号

如发现本坛存在违规或侵权内容, 请点击这里发送邮件举报 (或致电020-38271790)。请提供侵权说明和联系方式。我们将及时审核依法处理,感谢配合。

QQ|手机版|OpenEdv-开源电子网 ( 粤ICP备12000418号-1 )

GMT+8, 2026-4-13 01:48

Powered by OpenEdv-开源电子网

© 2001-2030 OpenEdv-开源电子网

快速回复 返回顶部 返回列表