第二十四章通用定时器实验
本章我们主要来学习通用定时器,STM32MP157有10个通用定时器(TIM2~TIM5,TIM12~ TIM17)。我们将通过四个实验来学习通用定时器的几个功能,分别是通用定时器中断实验、通用定时器PWM输出实验、通用定时器输入捕获实验和通用定时器脉冲计数实验。
本章分为如下几个小节:
24.1、通用定时器简介;
24.2、通用定时器中断实验;
24.3、通用定时器PWM输出实验;
24.4、通用定时器输入捕获实验;
24.5、通用定时器脉冲计数实验;
24.1 通用定时器简介
24.1.1 STM32MP157的通用定时器
1. 定时器资源
STM32MP157的通用定时器有10个之多,其基本特性也是不尽相同,为了更好的区别各个定时器的特性,我们列了一个表格,如下所示:
定时器 类型 | 定时器 | 计数 器位数 | 计数 模式 | 预分频 系数 (整数) | 产生 DMA 请求 | 捕获/比较 通道 | 互补 输出 | 最大接 口时钟 (MHz) | 最大定 时器 (MHz) |
基本定 时器 | TIM6 TIM7 | 16 | 递增 | 1~65535 | 可以 | 0 | 无 | 104.5 | 209 |
通用定 时器 | TIM2 TIM5 | 32 | 递增、递 减、中心对齐 | 1~65535 | 可以 | 4 | 无 | 104.5 | 209 |
TIM3 TIM4 | 16 | 递增、递 减、中心对齐 | 1~65535 | 可以 | 4 | 无 | 104.5 | 209 | |
TIM12 | 16 | 递增 | 1~65535 | 不可以 | 2 | 无 | 104.5 | 209 | |
TIM13 TIM14 | 16 | 递增 | 1~65535 | 不可以 | 1 | 无 | 104.5 | 209 | |
TIM15 | 16 | 递增 | 1~65535 | 可以 | 2 | 有 | 104.5 | 209 | |
TIM16 TIM17 | 16 | 递增 | 1~65535 | 可以 | 1 | 有 | 104.5 | 209 | |
高级定 时器 | TIM1 TIM8 | 16 | 递增、递 减、中心对齐 | 1~65535 | 可以 | 4 | 有 | 104.5 | 209 |
表24.1.1. 1定时器基本特性表
由上表知道:除了TIM2和TIM5是32位的计数器,其他定时器是16位的。通用定时器和高级定时器是在基本定时器的基础上,添加了一些额外功能,基本定时器有的功能通用定时器都有,而且还增加了递减计数、PWM生成、输入捕获、输出比较等功能。高级定时器又包含了通用定时器的所有功能,此外还增加带可编程死区的互补输出、重复计数器、断路输入等功能。以上定时器中,通用定时器数量较多,并且其特性也有一定的差异,但是基本原理一样。
2. 通用定时器框图
下面我们以TIM2/TIM3/TIM4/TIM5的框图为例来学习通用定时器框图,其他通用定时器的框图会有差异,因为内容比较多,大家学习了这里的框图再看ST官方的手册其他的定时器框图就会比较容易理解。通过学习通用定时器框图会有一个很好的整体掌握,同时对之后的编程也会有一个清晰的思路。
图24.1.1. 1通用定时器框图
如上图,通用定时器的框图比基本定时器的框图复杂许多,为了方便介绍,我们将其分成六个部分讲解:
- ①时钟源通用定时器时钟可由下列的时钟源提供:
1)内部时钟 (CK_INT)
2)外部时钟模式 1:外部输入引脚 (TIx),x=1,2,3,4
3)外部时钟模式 2:外部触发输入 (ETR)
4)内部触发输入 (ITRx):使用一个定时器作为另一定时器的预分频器
内部时钟 (CK_INT)
这里的内部时钟 (CK_INT)实际来自APB1,定时器TIM2~TIM7和定时器TIM12~TIM14挂在APB1总线上,定时器TIM1、TIM8和TIM15~TIM17挂在APB2上。TIM2/TIM3/TIM4/TIM5定时器的时钟源是APB1经过一个倍频器才接到这些定时器的(即时钟不是直接来自APB1),当APB1的预分频系数为1时,此倍频器倍频值为1,定时器的时钟频率等于APB1的频率;当 APB1的预分频系数为其它数值时,此倍频器倍频值为2,定时器的时钟频率等于APB1的频率2倍。这个情况跟基本定时器的一样,请回顾基本定时器的这部分内容,最后得到TIM2/TIM3/TIM4/TIM5定时器的时钟频率为2倍的APB1,即209MHZ。如下图,可以在STM32CubeMX上动态配置时钟:
图24.1.1. 2定时器时钟
外部时钟模式 1
图24.1.1. 3外部时钟连接示例
根据上图,我们做简单的描述:
时钟信号
如果时钟源选择的是外部时钟模式 1,即时钟信号来自外部输入引脚TI1~TI4中的某一个(即定时器的通道TIMx_CH1~ TIMx_CH4),属于触发输入TRGI。
滤波器和边沿检测
外部输入引脚输入的信号经过滤波器处理以后,再经过边沿检测器输出上升沿或者下降沿有效信号。滤波器的功能,简单来说就是多次检测视为一次有效,也就是连续进行N次采样检测,如果采样检测的结果都是高电平,则说明这是一个有效的电平信号,这样便可以过滤掉那干扰信号。
触发源选择
触发输入源有很多,可以来自内部触发ITRx(x等于0~4)、边沿检测器TI1F_ED、滤波后的定时器输入1(TI1FP1)、滤波后的定时器输入2(TI2FP2)、外部触发输入(ETRF)中的某一个。其中ITRx可由内部其他定时器产生信号,即使用一个定时器作为另一个定时器的预分频器,提供触发信号的定时器工作于主模式,接受触发信号的定时器工作于从模式。
上图中,采用外部时钟模1时,如果时钟信号来自外部输入引脚TI2,通过配置TIMx_SMCR 寄存器的TS[4:0]= 00101可以配置触发信号来自滤波后的定时器输入 1 (TI1FP1),也可以配置TIMx_SMCR寄存器的TS[4:0]= 00100选择TI1边沿检测器(TI1F_ED)为触发源。
从模式选择
对于外部时钟模式1,触发信号接到TRGI引脚给外部时钟模式1以后,还需要配置TIMx_SMCR寄存器的SMS[2:0]位= 0111来配置从模式为外部时钟模式1。
外部时钟模式 2
图24.1.1. 4外部时钟模式
根据上图,我们做简单的描述:
时钟信号输入
使用外部是种模式2时,时钟信号来自ETR引脚,ETR引脚可以为定时器提供外部时钟信号,例如PA0可以复用为TIM2_ETR/TIM2_CH1,如果配置PA0复用为TIM2_ETR的话,那么PA0引脚作为外部时钟输入引脚,例如可以让别的引脚模拟输出脉冲或者PWM波形,然后用杜邦线将此模拟输出脉冲引脚连接到PA0,给PA0提供时钟脉冲,或者将外部要采集的脉冲接入到PA0(注意IO口耐压范围)也是可以的。
图24.1.1. 5引脚可以复用为ETR
如果要选择ETR作为时钟源,需要配置TIMx_AF1 寄存器中的 ETRSEL[3:0] 位来选择正确的 ETR 源(如上面的TIM2_ETR),并通过配置TIMx_SMCR寄存器来设置预分频器、上升沿或者下降沿检测以及使能外部时钟模式 2,还需要配置TIMx_CR1 寄存器的CEN=1 来使能计数器。具体配置步骤可以查阅参考手册。
根据以上外部时钟模式1和外部时钟模式2的框图,对比两者:
外部时钟模式1的时钟信号来自定时器通道TIMx_CH1~ TIMx_CH4,经过滤波、边沿检测和极性选择后,以触发信号TRGI的形式进入到从模式选择器,作为定时器的时钟源,如下图中的1路线。外部时钟模式2的时钟信号来自特定的ETR引脚,此信号经过极性选择、分频和滤波后(分频和滤波不是必需的,可以根据外来信号频率的高低以及信号干扰信号程度来决定),不经过从模式选择器,像内部时钟(CK_INT)一样直接进入到了计数器,为计数器提供时钟,如图中的路线2。内部时钟(CK_INT)是下图中的路线3。
图24.1.1. 6通用定时器框图(部分)
关于两种模式的时钟输入引脚:
外部时钟模式1的是来自定时器通道TIMx_CH1~ TIMx_CH4,而外部时钟模式2则来自特定的ETR引脚;外部时钟模式1的时钟信号具有触发的特点,定时器工作于外部时钟模式1从模式,触发信号可以产生触发事件,从而产生中断或者DMA请求;外部时钟模式2来自ETR引脚,只是一个时钟信号,不具备触发的功能,定时器可以工作在主模式,也可以工作在从模式(复位、发、门控等)。
例如对TIM2的通道1可以配置这两种模式:
图24.1.1. 7配置
内部触发输入(ITRx)
定时器连接来自其它定时器的触发输出,即使用一个定时器作为另一定时器的预分频器。发送触发输出信号的定时器工作于主模式,接收触发信号的定时器工作于从模式。主模式定时器可以对从模式定时器的计数器执行复位、启动、停止操作或为其提供时钟。这种模式也就是定时器的级联,如下图:
图24.1.1. 8主/从模式示意图
接下来我们继续分析通用定时器框图部分的内容。
- ②控制器控制器包括:从模式控制器、编码器接口和触发控制器(TRGO)。从模式控制器可以控制计数器复位、启动、递增/递减、计数。编码器接口针对编码器计数,我们没用到。触发控制器用来提供触发信号给别的外设,比如为其它定时器提供时钟或者为DAC/ADC的触发转换提供信号。
- ③时基单元时基单元包括:计数器寄存器(TIMx_CNT)、预分频器寄存器(TIMx_PSC)、自动重载寄存器(TIMx_ARR)。这里注意的一点是这里的计数器模式有三种:递增、递减和中心对齐,并且TIM2和TIM5是32位的。
递增计数模式在基本定时器已经讲过,递减计算模式就很好理解,就是来一个脉冲计数器减1,直到计数器值减到0,然后计数器又从自动重载寄存器ARR的值开始继续递减计数,并生成定时器下益事件。
而中心对齐计数模式字面上不好理解,该模式下,计数器从0开始递增计数,直到计数值等于自动重载寄存器ARR的值减1后,生成定时器上溢事件,然后从自动重载寄存器ARR的值开始递减计算,直到计数值等于1,并生成定时器下益事件。然后又从0开始计数,一直循环。每次发生计数器上溢和下溢事件都会生成更新事件。 - ④输入捕获输入捕获包括:4个输入捕获通道(TIMx_CH1~ TIMx_CH4)、输入滤波和边沿检测和预分频器等部分,用于输入捕获功能,如:测量输入信号的脉冲宽度、测量 PWM 输入信号的频率和占空比等。
下面简单说一下输入捕获的工作原理:一般先设置输入捕获为上升沿检测,并记录发生上升沿时计数器寄存器(TIMx_CNT)的值。然后设置输入捕获为下降沿检测,当检测到下降沿到来时,记录此时计数器寄存器(TIMx_CNT)的值。最后,用后面记录的值减去前面记录的值,就得到此次高电平的脉冲宽度,再根据定时器的计数频率就可以计算出这个高电平脉冲的时间。低电平脉冲捕获同理。 - ⑤输入捕获和输出比较公用部分这部分包括:4个捕获比较寄存器,后面寄存器内容再详细分析。
- ⑥输出比较
输出比较包括:4个输出比较通道和相应的输出控制器组成,用于输出比较模式或PWM输出模式。
通用定时器定时器比较多,有10个,我们这里就主要以TIM2/TIM3/TIM4/TIM5为例子进行实验讲解,其它没讲到的定时器也是类似的,具体可以查询参考手册来了解。下面分别通过四个实验来详细学习通用定时器的功能。
24.2 通用定时器中断实验
本实验配置好的实验工程已经放到了开发板光盘中,路径为:开发板光盘A-基础资料\1、程序源码\3、M4裸机驱动例程\库V1.2\实验13-1 通用定时器-中断实验。
24.2.1 TIM2/TIM3/TIM4/TIM5寄存器
下面介绍TIM2/TIM3/TIM4/TIM5的几个与定时器中断相关且重要的寄存器,其他的通用定时器的寄存器会有一些差异,请大家自行对比《STM32H7xx参考手册》第39~41章,具体如下:
1. 控制寄存器 1(TIMx_CR1)
TIM2/TIM3/TIM4/TIM5的控制寄存器1描述如下图所示:
图24.2.1. 1寄存器
上图中我们只列出了本章需要用的一些位,其中:
位0(CEN),用于计数器使能,将该位置1表示禁止计数器,将改位置0表示使能计数器;
位4(DIR),用于控制定时器的计数方向,我们需要向上计数模式,所以设置DIR=0;
位6和位5(CMS[1:0]位),用于控制中心对齐模式,本章我们使用边沿对齐模式,所以设置为00即可;
位7(ARPE)用于控制自动重载预装载使能,0表示TIMx_ARR 寄存器不进行缓冲,1表示TIMx_ARR 寄存器进行缓冲,在基本定时器章节介绍影子寄存器时我们有讲解过。
2. 从模式控制寄存器(TIMx_SMCR)
TIM2/TIM3/TIM4/TIM5的从模式控制寄存器描述如下图所示:
图24.2.1. 2寄存器
该寄存器的SMS[3:0]位,用于从模式选择,其实就是选择计数器输入时钟的来源。比如通用定时器中断实验我们设置SMS[3:0]=0000,禁止从模式,这样PSC预分频器的时钟就直接来自内部时钟(CK_INT),频率一般为209Mhz(APB1频率的2倍)。而通用定时器中断实验我们设置SMS[3:0]=0111,外部时钟模式1,这样就可以检测按键的脉冲当做计数器时钟。
3. TIMx DMA/中断使能寄存器 (TIMx_DIER)
TIM2/TIM3/TIM4/TIM5的DMA/中断使能寄存器描述如图21.2.1.3所示:
图24.2.1. 3寄存器
该寄存器涉及触发DMA请求、捕获/比较中断以及更新中断使能,本章实验只用到后面两个。位0(UIE)是更新中断允许位,通用定时器中断实验需要用到定时器的更新中断,所以该位要设置为1来允许由于更新事件所产生的中断。而位1到位4是捕获/比较中断使能位,分别对应四个输入输出通道,将其置1表示使能中断,清0表示禁止中断。
4. 状态寄存器(TIMx_SR)
图24.2.1. 4寄存器
该寄存器都是一些中断标志位,CC1OF~CC4OF对应捕获/比较重复捕获标志,以第9位为例,如果CC1IF位为1,表示TIMx_CCR1寄存器中已捕获到计数器值且CC1IF 标志已置1;
位6属于触发中断标志,0表示未发生触发事件,1表示触发中断挂起;
位0(UIF)表示更新中断标志,当定时器中断来到该位会由硬件置1,标志中断到来,我们需要在中断服务函数里面把该位清零。
5. 计数器寄存器(TIMx_CNT)
图24.2.1. 5寄存器
因为定时器2和定时器5的计数器是32位的,所以当用到这两个定时器的时候,TIMx_CNT寄存器的32位都是用做计数器寄存器,其他定时器的就跟基本定时器一样,只用到低16位。
6. 预分频寄存器(TIMx_PSC)
图24.2.1. 6寄存器
所有定时器的预分频寄存器都是16位的,即写入该寄存器的数值范围是0到65535,表示1到65536分频。比如我们要20900分频,就往该寄存器写入20899。
7. 自动重载寄存器(TIMx_ARR)
图24.2.1. 7寄存器
该寄存器是用于存放与计数器寄存器比较的值,ARR[15:0]为自动重载值的低 16 位,ARR[31:16]为自动重载值的高 16 位,定时器2和定时器5的计数器是32位的,那该寄存器也是32位,其他定时器的就跟基本定时器一样,只用到低16位。
24.2.2 HAL库的API函数
本章节实验重要的API函数和上一章基本定时器介绍的内容差不多,这里就不再重复介绍此部分内容。
24.2.3 硬件设计
1. 例程功能
LED0用来指示程序运行,500ms为一个周期。LED1用于定时器中断取反,指示定时器中断状态,1000ms为一个周期。
2. 硬件资源
1)LED灯
LED0 | LED1 | 总线 |
PI0 | PF3 | AHB4 |
表24.2.3. 1硬件资源
2)定时器3
3. 原理图
定时器属于STM32MP157的内部资源,只需要软件设置好即可正常工作。我们通过LED1来指示STM32MP157的定时器进入中断情况。
24.2.4 程序设计
1. 程序流程图
下面看看本实验的程序流程图:
图24.2.4. 1通用定时器中断实验程序流程图
2. 新建文件
在工程存放目录Drivers\BSP\TIMER下新建gtim.c和gtim.h文件,并将gtim.c文件关联到工程中:
图24.2.4.2 新建文件
3. 添加用户代码
(1)gtim.h文件
gtim.h文件代码如下:
#ifndef __GTIM_H
#define __GTIM_H
#include "./SYSTEM/sys/sys.h"
#define GTIM_TIMX_INT TIM3 /* 通用定时器3定义 */
#define GTIM_TIMX_INT_IRQn TIM3_IRQn /* TIM3中断号 */
#define GTIM_TIMX_INT_IRQHandler TIM3_IRQHandler/* TIM3中断服务函数 */
/* TIM3 时钟使能 */
#define GTIM_TIMX_INT_CLK_ENABLE() do{ __HAL_RCC_TIM3_CLK_ENABLE(); }while(0)
/* 通用定时器 定时中断初始化函数 */
void gtim_timx_int_init(uint16_t arr, uint16_t psc);
#endif
(2)gtim.c文件
gtim.c文件代码如下:
1 #include "./BSP/TIMER/gtim.h"
2 #include "./BSP/LED/led.h"
3
4 TIM_HandleTypeDef g_timx_handle; /* 定时器x句柄 */
5 /**
6 * @brief 通用定时器TIMX定时中断初始化函数
7 * @note
8 通用定时器的时钟来自APB1,当APB1DIV≥2分频的时候
9通用定时器的时钟为APB1时钟的2倍, 而APB1为104.5M, 所以定时器时钟 = 209Mhz
10定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.
11定时器工作频率,单位:Mhz
12 *
13 * @param自动重装值。
14 * @param时钟预分频数
15 * @retval无
16 */
17 void gtim_timx_int_init(uint16_t arr, uint16_t psc)
18 {
19 GTIM_TIMX_INT_CLK_ENABLE(); /* 使能TIMx时钟 */
20
21 g_timx_handle.Instance = GTIM_TIMX_INT; /* 通用定时器x */
22 g_timx_handle.Init.Prescaler = psc; /* 分频值psc */
23 g_timx_handle.Init.CounterMode = TIM_COUNTERMODE_UP; /* 向上计数器 */
24 g_timx_handle.Init.Period = arr; /* 自动装载值 */
25 g_timx_handle.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;/* 时钟分频因子 */
26 HAL_TIM_Base_Init(&g_timx_handle);
27 /* 设置中断优先级,抢占优先级1,子优先级3 */
28 HAL_NVIC_SetPriority(GTIM_TIMX_INT_IRQn, 1, 3);
29 HAL_NVIC_EnableIRQ(GTIM_TIMX_INT_IRQn); /* 开启ITMx中断 */
30
31 HAL_TIM_Base_Start_IT(&g_timx_handle); /* 使能定时器x和定时器x更新中断 */
32 }
33 /**
34 * @brief定时器中断服务函数
35 * @param无
36 * @retval无
37 */
38 void GTIM_TIMX_INT_IRQHandler(void)
39 {
40 HAL_TIM_IRQHandler(&g_timx_handle);
41 }
42 /**
43 * @brief定时器更新中断回调函数
44 * @param定时器句柄指针
45 * @retval无
46 */
47 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
48 {
49 if (htim == (&g_timx_handle)) /* 判断是否是TIM3 */
50 {
51 LED1_TOGGLE(); /* LED1翻转 */
52 }
53 }
gtim.c文件代码和上一章节基本定时器实验的代码差不多:
第17~32行,gtim_timx_int_init函数主要用于初始化定时器,其中:
第19行,使能通用定时器3时钟;
第21~25行,配置定时器3的参数,包括:分频值为psc(分频值我们可以在调用函数的时候指定,是比较方便的)、计数模式为向上计数、自动装载值为arr(自动装载值也是可以自己设置的)、时钟分频因为为1,即不分频;
第26行,根据以上的设置的参数初始化定时器3;
第28和29行,设置通用定时器3的抢占优先级为1,子优先级为3,并使能定时器;
第31行,更新定时器中断和使能定时器;
第38~41行,定时器中断服务函数GTIM_TIMX_INT_IRQHandler通过调用定时器中断请求函数HAL_TIM_IRQHandler来完成定时器中断功能;
第47~53行,在定时器更新中断回调函数中实现LED1翻转,进入中断请求函数以后,会调用HAL_TIM_PeriodElapsedCallback函数。即每当计数器溢出时都调用此回调函数,此函数中实现LED1翻转,每进入一次中断,LED1就翻转一次,所以我们会看到LED1在闪烁。
24.2.5 编译和测试
保存修改,再进行编译,编译不报错后,进入Debug模式验证,实验现象和上一章节的基本定时器的一样,即运行后,LED0和LED1同时点亮,LED0以500ms周期闪烁,LED1以1s周期闪烁。
24.3 通用定时器PWM输出实验
本实验配置好的实验工程已经放到了开发板光盘中,路径为:开发板光盘A-基础资料\1、程序源码\3、M4裸机驱动例程\库V1.2\实验13-2 通用定时器-PWM输出实验。
24.3.1 定时器的PWM输出模式
1. 脉冲宽度调制
本小节我们来学习如何使用通用定时器的PWM输出模式。
脉冲宽度调制(PWM),是英文“Pulse Width Modulation”的缩写,简称脉宽调制,是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术。
PWM通过对一系列脉冲的宽度进行调制后,等效输出一系列幅值相等的波形,它把模拟信号转化为数字电路所需要的编码。PWM 常用来做电机控制、LCD 背光亮度调节、开关电源等,在测量、通信、及功率控制等许多领域中广泛应用。如下图,一模拟信号(正弦波)经过PWM后输出一系列幅值相等的脉冲(我们称为PWM波),使用这些脉冲来代替正弦波或所需要的波形,可以通过调节这些脉冲的占空比等效控制输出电压的大小,例如,在PWM频率一定的条件下,比如占空比为100%时,输出电压为5V,占空比为0时,输出电压为0V,当我们需要输出2.5V的电压时,只要将占空比调节为50%就能实现。
图24.3.1. 1输出
注:
PWM占空比就是一个脉冲周期内高电平在整个周期占的比例
如果芯片内部自带PWM输出功能模块,可以直接配置PWM实现PWM输出。如果芯片内部没有PWM功能模块,我们也可以通过定时器来控制一个IO口按照一定的时间间隔输出一高一低的电平以模拟PWM波形,例如模拟上图的波形b)。本章节我们就是使用定时器的一个通道输出PWM波形。
2. PWM输出模式
PWM有两种输出模式,分别为PWM模式1和PWM模式2。可以理解PWM模式1是与PWM模式2互补的波,PWM模式1为高电平时,PWM模式2为低电平,反之亦然。PWM模式和计数器计数模式对通道输出的PWM波电平有影响:在PWM模式1下,当计数器向上计数时,如果CNT< CCRx时,通道为有效电平,否则为无效电平;当计数器向下计数时,如果CNT>CCRx时,通道为无效电平,否则为有效电平。
PWM模式 | 计数器计数方向 | 电平情况 |
PWM模式1 | 向上计数 | 如果CNT< CCRx时,通道为有效电平;否则为无效电平 |
向下计数 | 如果CNT>CCRx时,通道为无效电平;否则为有效电平 | |
PWM模式2 | 向上计数 | 如果CNT< CCRx时,通道为无效电平;否则为有效电平 |
向下计数 | 如果CNT>CCRx时,通道为有效电平;否则为无效电平 |
表24.3.1. 1输出模式
例如一个外设是低电平有效才会运行,采用递增计数时:当CNT< CCRx时,此外设运行;当CNT> CCRx时,此外设不运行。
3. PWM 生成(边沿对齐模式)
PWM波频率由自动重载寄存器(TIMx_ARR)的值决定,其占空比则由捕获/比较寄存器(TIMx_CCRx)的值决定。它们生成PWM的原理如图下图所示:
图24.3.1. 2原理示意图
上图就是一个简单的PWM原理示意图。图中,我们假定定时器工作在边沿对齐,向上计数PWM模式,且当CNT<CCRx时,输出0,当CNT>=CCRx时输出1。那么就可以得到如上的PWM示意图:当CNT值小于CCRx的时候,IO输出低电平(0),当CNT值大于等于CCRx的时候,IO输出高电平(1),当CNT达到ARR值的时候,重新归零,然后重新向上计数,依次循环。改变CCRx的值,就可以改变PWM输出的占空比,改变ARR的值,就可以改变PWM输出的频率,这就是PWM输出的原理。
4. PWM 生成(中心对齐模式)
通用定时器(包括后面我们后面会学到的高级定时器)除了支持单向的向上或向下计数模式外,还支持中心对齐计数模式,即一个计数周期内分别由向上计数和向下计数两个过程组成,中心对齐模式用来输出对称波形,比如正弦波,可以基于中心对齐模式来实现PWM输出比较功能。中心对齐模式由TIMx_CR1寄存器的CMS[1:0]位来配置,当CMS[1:0]配置:
00:边沿对齐模式。计数器根据方向位 (DIR) 递增计数或递减计数;
01:中心对齐模式 1。计数器交替进行递增计数和递减计数,输出比较中断标志位,只在计数器递减计数时被置1;
10:中心对齐模式 2。计数器交替进行递增计数和递减计数,输出比较中断标志位,只在计数器增计数时被置1;
11:中心对齐模式 3。计数器交替进行递增计数和递减计数,输出比较中断标志位,只在计数器增计数时和递减计数时均被置1。
以上列出有3种中心对齐模式,我们以PWM模式1和中心对齐模式 3为例,PWM波形图如下:
图24.3.1. 3中心对齐模式
如上,对于中心对齐模式3,计数器在向上计数或者向下计数时,输出比较中断标志位被置1,当CNT<CCRx时,PWM是高电平1,当CNT>CCRx时,PWM是低电平。
STM32MP157的定时器除了基本定时器TIM6和TIM7,其他的定时器都可以用来产生PWM输出。其中高级定时器TIM1和TIM8可以同时产生多达7路的PWM输出。而通用定时器也能同时产生多达4路的PWM输出!本实验我们以使用TIM5的CH4通道产生一路PWM输出来控制LED0为例进行学习,如下图是STM32MP157数据手册的部分截图,我们的开发板上PI0接的LED0,PI0可以复用为TIM5_CH4,通过配置TIM5_CH4产生PWM波形可以控制LED0工作。
图24.3.1. 4可以复用为TIM5_CH4
24.3.2 TIM2/TIM3/TIM4/TIM5寄存器
要使STM32H750的通用定时器TIMx产生PWM输出,除了上一小节介绍的寄存器外,我们还会用到3个寄存器,来控制PWM。这三个寄存器分别是:捕获/比较模式寄存器(TIMx_CCMR1/2)、捕获/比较使能寄存器(TIMx_CCER)、捕获/比较寄存器(TIMx_CCR1~4)。接下来我们简单介绍一下这三个寄存器。
1. 捕获/比较模式寄存器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_CCMR2寄存器描述如下图所示:
图24.3.2. 1寄存器
该寄存器的有些位在不同模式下,功能不一样,我们现在只用到输出比较,输入捕获后面的实验再讲解。关于该寄存器的详细说明,请参考《STM32MP157参考手册》第41.4.10小节。 比如我们要让TIM5的CH4输出PWM波为例进行介绍:
OC4M[3:0] 是输出比较4模式设置位,对应着通道4的输出比较4模式设置,此部分由4位组成。总共可以配置成14种模式,我们使用的是PWM模式,所以这4位必须设置为0110或者0111,分别对应PWM模式1和PWM模式2。这两种PWM模式的区别就是输出有效电平的极性相反;
OC4PE是输出比较通道4的预装使能,该位需要置1;
CC4S[1:0]用于设置通道1的方向(输入/输出)默认设置为0,就是设置通道作为输出使用。
2. 捕获/比较使能寄存器(TIMx_ CCER)
TIM2/TIM3/TIM4/TIM5的捕获/比较使能寄存器,该寄存器控制着各个输入输出通道的开关和极性。TIMx_CCER寄存器描述如下图所示:
图24.3.2. 2寄存器
该寄存器比较简单,要让TIM5的CH4输出PWM波,这里我们要使能CC4E位,该位是输入/捕获通道4(ch4)的使能位,要想PWM从IO口输出,这个位必须设置为1。CC4P位是设置通道4的输出极性,我们默认设置0,即OC4 高电平有效。
3. 捕获/比较寄存器1/2/3/4(TIMx_ CCR1/2/3/4)
捕获/比较寄存器(TIMx_ CCR1/2/3/4),该寄存器总共有4个,对应4个通道CH1~CH4。我们使用的是通道4,所以来看看TIMx_ CCR4寄存器描述如下图所示:
图24.3.2. 3寄存器
此寄存器是捕获/比较寄存器 4 的预装载值,在输出模式下,该寄存器的值与CNT的值比较,根据比较结果产生相应动作,利用这点,我们通过修改这个寄存器的值,就可以控制PWM的输出脉宽了。注意,对于TIM2和TIM5来说,该寄存器是32位有效的,对其他定时器来说,则是16位有效位。
4. TIM1/TIM8断路和死区寄存器(TIMx_ BDTR)
如果是通用定时器,则配置以上说的寄存器就够了,但是如果是高级定时器,则还需要配置:断路和死区寄存器(TIMx_BDTR),该寄存器各位描述如图21.3.1.4所示:
图24.3.2. 4寄存器
该寄存器,我们只需要关注位15(MOE),主输出使能位,要想高级定时器的PWM正常输出,则必须设置MOE位为1,否则不会有输出。注意:通用定时器不需要配置这个。该寄存器的其他位我们这里就不详细介绍了,讲到高级定时器的时候再介绍其它位。
24.3.3 定时器的HAL库驱动
定时器在HAL库中的驱动代码在前面介绍基本定时器已经介绍了部分,这里我们再介绍几个和本实验用到的函数。
1. HAL_TIM_PWM_Init函数
定时器的PWM输出模式初始化函数,其声明如下:
HAL_StatusTypeDef HAL_TIM_PWM_Init(TIM_HandleTypeDef *htim);
- 函数描述:用于初始化定时器的PWM输出模式。
- 函数形参:形参1是TIM_HandleTypeDef结构体类型指针变量,基本定时器的时候已经介绍。
- 函数返回值:HAL_StatusTypeDef枚举类型的值。HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(串口忙碌)、HAL_TIMEOUT(超时)
- 注意事项:该函数实现的功能以及使用方法和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通道设置初始化函数。其声明如下:
HAL_StatusTypeDef HAL_TIM_PWM_ConfigChannel(TIM_HandleTypeDef *htim,
TIM_OC_InitTypeDef *sConfig,
uint32_t Channel)
- 函数描述:该函数用于设置定时器的PWM通道。
- 函数形参:形参1是TIM_HandleTypeDef结构体类型指针变量,用于配置定时器基本参数。形参2是TIM_OC_InitTypeDef结构体类型指针变量,用于配置定时器的输出比较参数。重点了解一下TIM_OC_InitTypeDef结构体指针类型,其定义如下:
typedef struct
{
uint32_t OCMode; /* 输出比较模式选择,寄存器的时候说过了,共7种模式 */
uint32_t Pulse; /* 设置比较值,默认比较值为自动重装载值的一半,即占空比为50% */
uint32_t OCPolarity; /* 设置输出比较极性 */
uint32_t OCNPolarity; /* 设置互补输出比较极性 */
uint32_t OCFastMode; /* 使能或失能输出比较快速模式 */
uint32_t OCIdleState; /* 选择空闲状态下的非工作状态(OC1 输出) */
uint32_t OCNIdleState; /* 设置空闲状态下的非工作状态(OC1N 输出) */
} TIM_OC_InitTypeDef;
该结构体成员我们重点关注前三个。成员变量OCMode用来设置模式,这里我们设置为PWM模式1。成员变量Pulse用来设置捕获比较值。成员变量TIM_OCPolarity用来设置输出极性是高还是低。其他的参数TIM_OutputNState,TIM_OCNPolarity,TIM_OCIdleState和TIM_OCNIdleState是高级定时器才用到的。
形参3是定时器通道,范围:TIM_CHANNEL_1到TIM_CHANNEL_4。这里我们使用的是定时器5的通道4,所以取值为TIM_CHANNEL_4即可。- 函数返回值:HAL_StatusTypeDef枚举类型的值。HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(串口忙碌)、HAL_TIMEOUT(超时)
3. HAL_TIM_PWM_Start函数
定时器的PWM输出启动函数,其声明如下:
HAL_StatusTypeDef HAL_TIM_PWM_Start(TIM_HandleTypeDef *htim,uint32_t Channel)
- 函数描述:用于启动定时器的PWM输出模式。
- 函数形参:形参1是TIM_HandleTypeDef结构体类型指针变量。
形参2是定时器通道,范围:TIM_CHANNEL_1到TIM_CHANNEL_4。 - 函数返回值:HAL_StatusTypeDef枚举类型的值。HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(串口忙碌)、HAL_TIMEOUT(超时)
- 注意事项:对于单独使能定时器的方法,在上一章定时器实验我们已经讲解。实际上,HAL库也同样提供了单独使能定时器的输出通道函数,函数为:
void TIM_CCxChannelCmd(TIM_TypeDef *TIMx,uint32_t Channel,uint32_t ChannelState);
HAL_TIM_PWM_Start函数内部也调用了该函数。
4. HAL_TIM_ConfigClockSource函数
配置定时器时钟源函数,其声明如下:
HAL_StatusTypeDef HAL_TIM_ConfigClockSource(TIM_HandleTypeDef *htim, \ TIM_ClockConfigTypeDef *sClockSourceConfig)
- 函数描述:用于配置定时器时钟源。
- 函数形参:形参1是TIM_HandleTypeDef结构体类型指针变量。形参2是TIM_ClockConfigTypeDef结构体类型指针变量,用于配置定时器时钟源参数。
TIM_ClockConfigTypeDef定义如下:
typedef struct
{
uint32_t ClockSource; /* 时钟源 */
uint32_t ClockPolarity; /* 时钟极性 */
uint32_t ClockPrescaler; /* 定时器预分频器 */
uint32_t ClockFilter; /* 时钟过滤器 */
} TIM_ClockConfigTypeDef;
- 函数返回值:HAL_StatusTypeDef枚举类型的值。HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(串口忙碌)、HAL_TIMEOUT(超时)
- 注意事项:该函数主要操作TIMx_SMCR寄存器,系统默认定时器的时钟源就是内部时钟,所以一般定时器要使用内部时钟,我们就不对定时器的时钟源就行初始化,默认即可。这里只是让大家知道有这个函数可以设定时器的时钟源。比如用HAL_TIM_ConfigClockSource初始化选择内部时钟,方法如下:
TIM_HandleTypeDef timx_handle; /* 定时器x句柄 */
TIM_ClockConfigTypeDef sClockSourceConfig = {0};
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;/* 选择内部时钟 */
HAL_TIM_ConfigClockSource(&timx_handle,&sClockSourceConfig);
其他的时钟源请大家参考TIMx_SMCR寄存器和HAL库。后面的定时器初始化凡是用到内部时钟我们都没有去初始化,系统默认即可。
5. 修改占空比
前面我们说过,通过修改比较值TIMx_CCRx则可以控制通道的输出占空比。继而控制LED0的亮度(本实验是使用TIM5,则可以通过控制TIM5_CCR2来控制CH4的输出占空比)。HAL库中提供一个修改占空比的宏定义:
#define __HAL_TIM_SET_COMPARE(__HANDLE__, __CHANNEL__, __COMPARE__) \
(((__CHANNEL__) == TIM_CHANNEL_1) ? ((__HANDLE__)->Instance->CCR1 = (__COMPARE__)) :\
((__CHANNEL__) == TIM_CHANNEL_2) ? ((__HANDLE__)->Instance->CCR2 = (__COMPARE__)) :\
((__CHANNEL__) == TIM_CHANNEL_3) ? ((__HANDLE__)->Instance->CCR3 = (__COMPARE__)) :\
((__CHANNEL__) == TIM_CHANNEL_4) ? ((__HANDLE__)->Instance->CCR4 = (__COMPARE__)) :\
((__CHANNEL__) == TIM_CHANNEL_5) ? ((__HANDLE__)->Instance->CCR5 = (__COMPARE__)) :\
((__HANDLE__)->Instance->CCR6 = (__COMPARE__)))
__HANDLE__是TIM_HandleTypeDef结构体类型指针变量,__CHANNEL__对应PWM的输出通道,__COMPARE__则是要写到捕获/比较寄存器(TIMx_ CCR1/2/3/4)的值。实际上该宏定义最终还是往对应的捕获/比较寄存器写入比较值来控制PWM波的占空比。如下解析:
比如我们要修改定时器5通道4的输出比较值(控制占空比),寄存器操作方法:
TIM5->CCR2 = ledrpwmval; /* ledrpwmval是比较值,并且动态变化的,
所以我们要周期性调用这条语句,已达到及时修改PWM的占空比 */
__HAL_TIM_SET_COMPARE (__HANDLE__, __CHANNEL__, __COMPARE__)这个宏定义函数最终也是调用这个寄存器操作的,所以说我们使用HAL库的函数其实就是间接操作寄存器的。
24.3.4 硬件设计
1. 例程功能
通过PWM控制LED0由暗变到亮,然后又从亮变到暗,每个过程大概持续时间大概为3秒钟左右。
2. 硬件资源
1)LED灯
LED0 | 总线 |
PI0 | AHB4 |
表24.3.4. 1硬件资源
2)定时器5输出通道4(TIM5_CH4)
从核心板原理图看出,PI0和TIM5_CH4存在复用关系,我们在STM32CubeMX上把PI0配置为TIM5_CH4即可,然后程序控制TIM5_CH4输出PWM波形,从而控制LED0。
图24.3.4. 1引脚部分原理图
从《STM32MP157A&D数据手册》中也可以查阅PI0的复用关系:
图24.3.4. 2数据手册部分截图
3. 原理图
定时器属于STM32MP157的内部资源,只需要软件设置好即可正常工作。
24.3.5 程序设计
PI0引脚上接的是LED0,如果PI0上输出低电平,则LED0点亮,我们称LED0电平为低电平有效,如果PI0输出的是高电平,则LED0熄灭。PI0引脚可以复用为TIM5_CH4,即定时器5的PWM通道4,通过通道输出PWM高低电平来控制LED0点亮和熄灭,通过控制PWM的占空比来控制LED0的有效电平。本节实验配置步骤为:
1)通道选择:配置PI0引脚复用为TIM5_CH4;
2)时基配置:配置TIM5的预分频器PSC、自动重载TIMx_ARR值、计数模式、PWM模式、TIM5时钟源选择以及比较值等参数;
3)时钟树配置;
4)生成初始化代码;
5)添加用户代码,实现动态改变占空比,从而控制LED0灯的亮灭,可以实现类似呼吸灯的效果。
1. 程序流程图
图24.3.5. 1通用定时器中断实验程序流程图
2. 添加用户代码
(1)gtim.h文件代码
#ifndef __GTIM_H
#define __GTIM_H
#include "./SYSTEM/sys/sys.h"
#define GTIM_TIMX_PWM_CHY_GPIO_PORT GPIOI /* GPIOI端口 */
#define GTIM_TIMX_PWM_CHY_GPIO_PIN GPIO_PIN_0 /* PI0引脚 */
#define GTIM_TIMX_PWM_CHY_GPIO_AF GPIO_AF2_TIM5 /* AF功能选择为TIM5 */
/* 使能GPIOI时钟 */
#define GTIM_TIMX_PWM_CHY_GPIO_CLK_ENABLE() do{__HAL_RCC_GPIOI_CLK_ENABLE();}while(0)
#define GTIM_TIMX_PWM TIM5 /* TIM5 */
#define GTIM_TIMX_PWM_CHY TIM_CHANNEL_4 /* 通道Y, 1<= Y <=4 */
#define GTIM_TIMX_PWM_CHY_CCRX TIM5->CCR4 /* 通道Y的输出比较寄存器 */
/* 使能TIM5时钟 */
#define GTIM_TIMX_PWM_CHY_CLK_ENABLE() do{__HAL_RCC_TIM5_CLK_ENABLE();}while(0)
/* 通用定时器 定时中断初始化函数 */
void gtim_timx_int_init(uint16_t arr, uint16_t psc);
/* 通用定时器 PWM初始化函数 */
void gtim_timx_pwm_chy_init(uint16_t arr, uint16_t psc);
/* 通用定时器 PWM-GPIO初始化函数 */
void gtim_timx_comp_pwm_gpio_init(void);
#endif
可以把上面的宏定义分成两部分,第一部分是定时器5输出通道4对应的IO口的宏定义。第二部分则是定时器5输出通道4的相应宏定义。
这里的宏定义是定时器5通道4输出PWM波形,定时器5通道4刚好和PI0引脚复用,所以当该引脚输出PWM波形后,LED0的亮度会发生变化。
(2)gtim.c文件代码
1 TIM_HandleTypeDef g_timx_pwm_chy_handle; /* 定时器x句柄 */
2 TIM_OC_InitTypeDef g_timx_oc_pwm_chy_handle; /* 定时器输出句柄 */
3
4 /**
5 * @brief通用定时器TIMX 通道Y PWM输出 初始化函数(使用PWM模式1)
6 * @note
7通用定时器的时钟来自APB1,当APB1DIV≥2分频的时候
8通用定时器的时钟为APB1时钟的2倍, 而APB1为104.5M, 所以定时器时钟 = 209Mhz
9 定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.
10 定时器工作频率,单位:Mhz
11 *
12 * @param自动重装值。
13 * @param时钟预分频数
14 * @retval无
15 */
16 void gtim_timx_pwm_chy_init(uint16_t arr,uint16_t psc)
17 {
18 gtim_timx_comp_pwm_gpio_init();
19 GTIM_TIMX_PWM_CHY_CLK_ENABLE();
20
21 g_timx_pwm_chy_handle.Instance = GTIM_TIMX_PWM; /* 定时器x */
22 g_timx_pwm_chy_handle.Init.Prescaler = psc; /* 定时器分频 */
23 /* 向上计数模式 */
24 g_timx_pwm_chy_handle.Init.CounterMode = TIM_COUNTERMODE_UP;
25 g_timx_pwm_chy_handle.Init.Period = arr; /* 自动重装载值 */
26 /* 时钟分频因子 */
27 g_timx_pwm_chy_handle.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
28 HAL_TIM_PWM_Init(&g_timx_pwm_chy_handle); /* 初始化PWM */
29
30 g_timx_oc_pwm_chy_handle.OCMode = TIM_OCMODE_PWM1; /* 模式选择PWM1 */
31 /* 设置比较值,此值用来确定占空比,即默认占空比为50% */
32 g_timx_oc_pwm_chy_handle.Pulse = arr/2;
33 /* 输出比较极性为低 */
34 g_timx_oc_pwm_chy_handle.OCPolarity = TIM_OCPOLARITY_LOW;
35 /* 配置TIMx通道y */
36 HAL_TIM_PWM_ConfigChannel(&g_timx_pwm_chy_handle, \ &g_timx_oc_pwm_chy_handle, GTIM_TIMX_PWM_CHY);
37 /* 开启PWM通道1 */
38 HAL_TIM_PWM_Start(&g_timx_pwm_chy_handle, GTIM_TIMX_PWM_CHY);
39 }
40 /**
41 * @brief通用定时器PWM输出通道引脚初始化函数
42 * @param定时器句柄
43 * @note此函数会被gtim_timx_pwm_chy_init()调用
44 * @retval无
45 */
46 void gtim_timx_comp_pwm_gpio_init(void)
47 {
48 GPIO_InitTypeDef gpio_init_struct;
49 GTIM_TIMX_PWM_CHY_GPIO_CLK_ENABLE(); /* 开启通道y的CPIO时钟 */
50
51 gpio_init_struct.Pin = GTIM_TIMX_PWM_CHY_GPIO_PIN;/* 通道y的CPIO口 */
52 gpio_init_struct.Mode = GPIO_MODE_AF_PP; /* 复用推挽输出 */
53 gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
54 gpio_init_struct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; /* 高速 */
55 /* 定时器x通道y的CPIO口复用 */
56 gpio_init_struct.Alternate = GTIM_TIMX_PWM_CHY_GPIO_AF;
57 /* 初始化GPIO */
58 HAL_GPIO_Init(GTIM_TIMX_PWM_CHY_GPIO_PORT, &gpio_init_struct);
59 }
我们分析一下以上代码:
第16~39行,gtim_timx_pwm_chy_init函数用于初始化通用定时器5通道4,我们看看该函数做了哪些工作:
第18和19行,使能GPIOI时钟、使能定时器5 时钟;
第21行,指定定时器5;
第22行,设置定时器5的分频值为可配置的参数psc;
第24行,配置定时器5的计数模式为向上计数模式;
第25行,配置定时器5 的自动重载值为可配置的参数arr;
第27行,配置定时器预分频值为1,那么定时器5的时钟频率就是209MHz;
第28行,根据以上配置IDE参数来初始化TIM5;
第30行,配置定时器5位PWM模式1,另外还有PWM模式2,可以理解PWM mode l是与PWM mode 2模式互补的波,PWM模式1为高电平时PWM模式2为低电平,反之亦然。
第32行,Pulse是占空比值,即TIM5_CCR4的值,也就是有效电平的值,可以配置在0-arr之间,例如配置0。这里配置arr/2,即占空比为50%。在后面的实验中,我们会对TIMx_CCR4寄存器写入新的值来改变占空比,从而控制LED逐渐点亮和熄灭。
第34行,配置输出极性,这里我们选择Low(LED0是低电平有效)。以上配置中要注意的是输出极性和PWM模式,其中TIM5_CNT为TIM5的计数寄存器,用于计数器计数,TIM5_CCR4为TIM5的比较寄存器,其值由用户设置,如上面设置为arr/2。
这里注意的是,板子上的LED0是低电平有效,如果输出极性选择为Low:
在PWM模式1下当向上计数时,如果TIM5_CNT<TIM5_CCR4时,通道4为有效电平,否则为无效电平;在向下计数时,如果TIM5_CNT>TIM5_CCR4时通道4为无效电平,否则为有效电平。
如果在PWM模式2下,在向上计数时,如果TIM5_CNT<TIM5_CCR4时通道4为无效电平,否则为有效电平;在向下计数时,如果TIM5_CNT>TIM5_CCR4时通道4为有效电平,否则为无效电平。
例如配置为PWM模式2、向上计数、比较值始终为600、自动重装载值为500、输出极性为低,如果这么配置的话,计数器最大值只能为500,永远小于600,即TIM5_CNT<TIM5_CCR4,通道4电平为无效值,所以LED0灯永远不会亮。
第36行,根据以上参数配置定时器5的通道4;
第38行,开启定时器5的通道1。
第46~59行,初始化定时器5通道4的引脚(即PI0),我们看看该函数:
第49行,使能GPIO时钟;
第51~54行,配置定时器5通道4的引脚为推挽输出、上拉、高速模式;
第56行,配置PI0复用为TIM5通道4;
第58行,初始化GPIO。
(3)main.c文件代码
1 #include "./SYSTEM/sys/sys.h"
2 #include "./SYSTEM/delay/delay.h"
3 #include "./SYSTEM/usart/usart.h"
4 #include "./BSP/LED/led.h"
5 #include "./BSP/BEEP/beep.h"
6 #include "./BSP/KEY/key.h"
7 #include "./BSP/TIMER/gtim.h"
8 /**
9 * @brief主函数
10 * @param无
11 * @retval无
12 */
13 int main(void)
14 {
15 uint16_t ledrpwmval = 0;
16 uint8_t dir = 1;
17 HAL_Init(); /* 初始化HAL库 */
18 /* 初始化M4内核时钟,209M */
19 if(IS_ENGINEERING_BOOT_MODE())
20 {
21 sys_stm32_clock_init(34, 2, 2, 17, 6826);
22 }
23 delay_init(209); /* 延时初始化 */
24 led_init(); /* 初始化LED */
25 /* 209M/209=1M的计数频率,自动重装载为500,那么PWM频率为1M/500=2kHZ */
26 gtim_timx_pwm_chy_init(500 - 1, 209 - 1);
27 while(1)
28 {
29 delay_ms(10);
30 if (dir)ledrpwmval++; /* dir==1 ledrpwmval递增 */
31 else ledrpwmval--; /* dir==0 ledrpwmval递减 */
32
33 if (ledrpwmval > 300)dir = 0; /* ledrpwmval到达300后,方向为递减 */
34 if (ledrpwmval == 0)dir = 1; /* ledrpwmval递减到0后,方向改为递增 */
35 GTIM_TIMX_PWM_CHY_CCRX = ledrpwmval;/* 修改比较值,修改占空比 */
36 }
37 }
第30~34行,当dir=1的时候,ledrpwmval递增,当dir=0的时候,ledrpwmval递减。ledrpwmval一开始配置为0,当其从0递增到300的时候,dir等于0,然后ledrpwmval就又递减。这几行代码就是控制ledrpwmval从0递增到300以后,再从300递减到0。
第35行,将不断变化的ledrpwmval值写入TIM5的TIMx_CCR4寄存器中实现动态修改占空比,占空比变化以后,LED0灯就会先逐渐变亮,再逐渐变暗,如此反复。
第35行的代码也可以直接操作寄存器来替代:
(TIM5->CCR4)=ledrpwmval;
本小节开头我们就说过PWM波频率由自动重载寄存器(TIMx_ARR)的值决定,其占空比则由捕获/比较寄存器(TIMx_CCRx)的值决定,实验中LED0是低电平有效的,且设置为向上计数模式。下面我们取比较值ledrpwmval为100(也就是TIM5_CCR4的值)时的情况,计算看看占空比。
如果输出极性(也叫比较极性)为低,当计数器TIM5_CNT的值小于100时,通道4为有效电平,LED0亮;当计数器的值大于100时,通道4为无效电平,LED0灭:
如果输出极性为高,当计数器的值小于100时,通道4为无效电平,LED0灭;当计数器的值大于100时,通道4为有效电平,LED0亮:
可以看到输出比较极性为低和输出比较极性为高的占空比正好反过来。感兴趣大家可以用示波器进行验证。
24.3.6 编译和测试
下载代码后,我们将看LED0不停的由暗变到亮,然后又从亮变到暗。每个过程持续时间大概为3秒钟左右(0.005s*300*2=3s)。
本实验也可以使用中断的方式来做,直接参考上一章节 通用定时器中断实验章节 的来做,在回调函数中编写配置TIM5_CCR4变化的值即可实现,大家可以尝试。
24.4 通用定时器输入捕获实验
本实验配置好的实验工程已经放到了开发板光盘中,路径为:开发板光盘A-基础资料\1、程序源码\3、M4裸机驱动例程\库V1.2\实验13-3 通用定时器-输入捕获实验。
24.4.1 输入捕获原理
1. 输入捕获简介
本小节我们来学习如何使用通用定时器的输入捕获模式。输入捕获就是指对TIMx_CHy通道上的输入信号的上升沿、下降沿或者双边沿进行捕获/检测,在边沿信号发生跳变(比如上升沿/下降沿)的时候捕获到这些信号,将计数器的计数值TIMx_CNT保存到对应通道的捕获/比较寄存器中(TIMx_CCRy)完成一次捕获。在捕获模式中还可以配置捕获时是否触发中断/DMA等以完成捕获的一些响应。下面我们以一个周期的脉冲为例说明:
图24.4.1. 1输入捕获示意图
计数器以一定频率工作,根据计数器的工作频率可以计算出计数器每计数一次(一个节拍)所使用的时间t。如上图,当捕获到一个上升沿,记录计数器的计数值为CNT1,接着捕获到一个下降沿,记录计数器的计数值为CNT2。两次计数器的差值(CNT2-CNT1)就是两次捕获之间计数器计数了多少个节拍,t*(CNT2-CNT1)就是此高电平脉冲的宽度。同理,要测量低电平脉冲的宽度,可以是t*(CNT3-CNT2)。此脉冲的周期是,t*(CNT3-CNT1),频率是1/(t*(CNT3-CNT1))。如果捕获的脉宽的宽度超过了捕获定时器的周期,那就会发生溢出,必须对溢出做处理,否则测试的数据不准确。我们用一个简图来详细说明输入捕获的原理,如下图所示:
图24.4.1. 2输入捕获脉宽测量原理
图中ARR是自动重载寄存器(TIMx_ARR)的值,CCRx是捕获时计数器(TIMx_CNT)的值。
上图就是输入捕获测量高电平脉宽的原理,假定定时器工作在向上计数模式,图中t1~t2的时间,就是我们需要测量的高电平时间。测量方法如下:首先设置定时器通道x为上升沿捕获,这样,t1时刻,就会捕获到当前的CNT值,然后立即清零CNT,并设置通道x为下降沿捕获,这样到t2时刻,又会发生捕获事件,得到此时的CNT值,记为CCRx2。这样,根据定时器的计数频率,我们就可以算出t1~t2的时间,从而得到高电平脉宽。
在t1~t2之间,可能产生N次定时器溢出,这就要求我们对定时器溢出做处理,防止高电平太长,导致数据不准确。t1~t2之间,CNT计数的次数等于:N*ARR+CCRx2,有了这个计数次数,再乘以CNT的计数周期,即可得到t2-t1的时间长度,即高电平持续时间。输入捕获的原理,我们就介绍到这。
2. 输入捕获框图
图24.4.1. 3输入捕获框图
输入通道
如上图,需要测试的信号从定时器通道TIMx_CH1~TIMx_CH4中输入,例如从TIMx_CH1通道输入的信号T1x。
捕获通道
输入的信号经过滤波采样去除掉干扰信号后,生成一个信号TIxF,TIxF再经过带有极性选择功能的边沿检测器生成一个信号 (TIxFPx),该信号可用作从模式控制器的触发输入,然后从从模式控制器输出ICx,我们称ICx为捕获通道。
捕获比较模块
从捕获通道ICx出来的信号先进行预分频后输出 ICxPS信号,此信号最终进入到捕获比较模块中,捕获比较模块由一个预装载寄存器和一个影子寄存器组成,可通过读写操作访问预装载寄存器实现捕获/比较处理。在捕获模式下,捕获实际发生在影子寄存器中,然后将影子寄存器的内容复制到预装载寄存器中。在比较模式下,预装载寄存器的内容将复制到影子寄存器中,然后将影子寄存器的内容与计数器进行比较。
在输入捕获模式下,当相应的ICx信号第一次检测到跳变沿后,将使用捕获/比较寄存器 (TIMx_CCRx)来锁存计数器的值,发生捕获事件时,硬件会将TIMx_SR 寄存器相应的捕获/比较中断标志CCXIF标志位置1,从而触发中断(如果已经使能了中断),可以通过软件将CCxIF清零或者通过读取TIMx_CCRx中的值也可以将CCxIF清零。前面是第一次捕获,如果发生第二次或者多次捕获(重复捕获),如果CCxIF 标志未被清零,这样 CCxOF 重复捕获标志会被置 1,CCxOF只能通过软件清零。
要处理重复捕获,建议在读出重复捕获标志之前读取数据,这样可避免在读取重复捕获标志之后与读取数据之前可能出现的重复捕获信息丢失。
输出
输出通道也是TIMx_CH1~TIMx_CH4中的某个,通过将TIMx_CCER寄存器CCxE位置1,使在相应输出引脚上输出OCx信号。
24.4.2 TIM2/TIM3/TIM4/TIM5寄存器
本章实验会使用通用定时器5的通道1,通用定时器输入捕获实验需要用到的寄存器有:TIMx_ARR、TIMx_PSC、TIMx_CCMR1、TIMx_CCER、TIMx_DIER、TIMx_CR1、TIMx_CCR1这些寄存器在前面的章节都有提到,在这里只需针对性的介绍。
1. 捕获/比较模式寄存器1/2(TIMx_CCMR1/2)
该寄存器我们在PWM输出实验时讲解了他作为输出功能的配置,现在重点学习输入捕获模式的配置,因为本实验我们用到定时器5通道1输入,所以我们要看TIMx_CCMR1寄存器。相同的寄存器可用于输入捕获模式或输出比较模式。
图24.4.2. 1《STM32MP157参考手册》部分截图
其描述如下图所示:
图24.4.2. 2寄存器
TIMx_CCMR1的低16位用于配置通道1和通道2,低八位[7:0]用于捕获/比较通道1的控制,而高八位[15:8]则用于捕获/比较通道2的控制,因为TIMx还有CCMR2这个寄存器,所以可以知道CCMR2是用来控制通道3和通道4。我们用到定时器5通道1输入,重点关注TIMx_CCMR1的[7:0]位(其[8:15]位配置类似)。
- CC1S[1:0] 这两个位用于捕获/比较1选择(CC1S),该位字段定义通道的方向(输入/输出)以及使用的输入,将该位配置:
00:将CC1通道配置为输出(CC1表示捕获比较通道1);
01:将CC1通道配置为输入,IC1映射到TI1;
10:将CC1通道配置为输入,将IC1映射到TI2;
11:将CC1通道配置为输入,将IC1映射到TRC。仅当通过TIMx_SMCR寄存器的TS位选择了内部触发输入时,该模式才有效。
注:仅当通道为OFF(TIMx_CCER中的CC1E = 0)时,CC1S位才可写。
本章节实验是输入捕获实验,所以将设置IC1S[1:0]=01,也就是配置IC1映射在TI1上,即CCR1对应TIMx_CH1 - IC1PSC[1:0] 配置输入捕获1预分频器,用于CC1输入(IC1)的预分频器的比率,将该位配置:
00:没有预分频器,每次在捕获输入上检测到边沿时都进行捕获
01:每2个事件捕获一次
10:每4个事件捕获一次
11:每8个事件捕获一次
本章节实验,我们是来1次边沿就触发1次捕获,所以选择00。 - IC1F [3:0]
输入捕获 1 滤波器,可定义 TI1 输入的采样频率和适用于 TI1 的数字滤波器带宽。数字滤波器由事件计数器组成,每N个连续事件才视为一个有效输出边沿:
0000:无滤波器,按 f DTS频率进行采样;
0001:f SAMPLING =f CK_INT,N=2 ;
0010:f SAMPLING =f CK_INT,N=4 ;
......
1110:f SAMPLING =f DTS /32,N=6
1111:f SAMPLING =f DTS /32,N=8
其中,f SAMPLING是采样频率,fCK_INT是定时器的输入频率,这里为209Mhz,而fDTS则是根据TIMx_CR1的CKD[1:0]的设置来确定的,如果CKD[1:0]设置为00,那么fDTS=fCK_INT。N值就是滤波长度。
举个简单的例子:假设IC1F[3:0]=0011,并设置IC1映射到通道1上,且为上升沿触发,那么在捕获到上升沿的时候,再以fCK_INT的频率,连续采样到8次通道1的电平,如果都是高电平,则说明却是一个有效的触发,就会触发输入捕获中断(如果开启了的话)。这样可以滤除那些高电平脉宽低于8个采样周期的脉冲信号,从而达到滤波的效果。
本章节,我们不做滤波处理,所以设置IC1F[3:0]=0000,只要采集到上升沿,就触发捕获。
2. 捕获/比较使能寄存器(TIMx_ CCER)
TIM2/TIM3/TIM4/TIM5的捕获/比较使能寄存器,该寄存器控制着各个输入输出通道的开关和极性。TIMx_CCER寄存器描述下图所示:
图24.4.2. 3寄存器
此寄存器每3位控制一个通道,我们使用的是定时器1,只介绍通道1相关部分。
CC1E:是捕获/比较1输出使能,当CC1通道配置为输入时,此位为0表示禁止捕获,此位为1表示使能捕获;
CC1P:用于配置捕获/比较1输出极性,当CC1通道配置为输入时:
00:未反相/上升沿触发;
01:反相/下降沿触发;
10:保留,不使用此配置;
11:未反相/上升沿和下降沿均触发。
CC1NP:用于配置捕获/比较1互补输出极性,当CC1通道配置为输入时,该位与 CC1P 配合使用可定义TI1FP1/TI2FP1极性。
我们要用到这个寄存器的最低2位,CC1E和CC1P位。要使能输入捕获,必须设置CC1E=1,而CC1P则根据自己的需要来配置。我们这里是保留默认设置值0,即高电平触发捕获。
3. DMA/中断使能寄存器(TIMx_DIER)
接下来我们再看看DMA/中断使能寄存器:TIMx_DIER,该寄存器的各位描述如下图:
图24.4.2. 4寄存器
UIE位表示更新中断使能。0:禁止更新中断;1:使能更新中断。
CC1IE位用于捕获/比较1中断使能。0:禁止CC1中断;1:使能CC1中断。
CC2IE位用于捕获/比较2中断使能。0:禁止CC2中断;1:使能CC2中断。
本小节,我们需要用到中断来处理捕获数据,所以必须开启通道1的捕获比较中断,即CC1IE设置为1。同时我们还需要在定时器溢出中断中累计定时器溢出的次数,所以还需要使能定时器的更新中断,即UIE置1。
4. 控制寄存器(TIMx_CR1)
图24.4.2. 5寄存器
TIMx_CR1我们只用到了它的最低位,也就是用来使能定时器。将第0位置1,表示使能计数器,清零表示关闭计时器。
5. 捕获/比较寄存器1(TIMx_CCR1)
图24.4.2. 6寄存器
最后再来看看捕获/比较寄存器1:TIMx_CCR1,该寄存器用来存储通道1捕获发生时,TIMx_CNT的值,我们从TIMx_CCR1就可以读出通道1捕获发生时刻的TIMx_CNT值,通过两次捕获(一次上升沿捕获,一次下降沿捕获)的差值,就可以计算出高电平脉冲的宽度(注意,对于脉宽太长的情况,还要计算定时器溢出的次数)。
24.4.3 定时器的HAL库驱动
1. HAL_TIM_IC_Init函数
定时器的输入捕获模式初始化函数,其声明如下:
HAL_StatusTypeDef HAL_TIM_IC_Init(TIM_HandleTypeDef *htim)
- 函数描述:用于初始化定时器的输入捕获模式。
- 函数形参:形参1是TIM_HandleTypeDef结构体类型指针变量,基本定时器的时候已经介绍此结构体。
- 函数返回值:HAL_StatusTypeDef枚举类型的值。HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(串口忙碌)、HAL_TIMEOUT(超时)。
- 注意事项:与PWM输出实验一样,当使用定时器做输入捕获功能时,在HAL库中并不使用定时器初始化函数HAL_TIM_Base_Init来实现,而是使用输入捕获特定的定时器初始化函数HAL_TIM_IC_Init。该函数内部还会调用输入捕获初始化回调函数HAL_TIM_IC_MspInit来初始化输入通道对应的GPIO(复用),以及输入捕获相关的配置。
2. HAL_TIM_IC_ConfigChannel函数
定时器的输入捕获通道设置初始化函数。其声明如下:
HAL_StatusTypeDef HAL_TIM_IC_ConfigChannel(TIM_HandleTypeDef *htim,
TIM_IC_InitTypeDef *sConfig, uint32_t Channel);
- 函数描述:该函数用于设置定时器的输入捕获通道。
- 函数形参:形参1是TIM_HandleTypeDef结构体类型指针变量,用于配置定时器基本参数,我们前面也介绍过此结构体,这里就不再重复介绍了。形参2是TIM_IC_InitTypeDef结构体类型指针变量,用于配置定时器的输入捕获参数。重点了解一下TIM_IC_InitTypeDef结构体指针类型,其定义如下:
typedef struct
{
uint32_t ICPolarity; /* 输入捕获触发方式选择,比如上升、下降和双边沿捕获 */
uint32_t ICSelection; /* 输入捕获选择,用于设置映射关系 */
uint32_t ICPrescaler; /* 输入捕获分频系数 */
uint32_t ICFilter; /* 输入捕获滤波器设置 */
} TIM_IC_InitTypeDef;
- 该结构体成员我们主要设置前三个成员变量。成员变量ICSelection用来设置映射关系,我们配置IC1直接映射在TI1上,所以此成员就选择TIM_ICSELECTION_DIRECTTI。其它映射关系还可以选择以下这些宏:
/* 选择TIM输入1、2、3或4分别连接到IC1,IC2,IC3或IC4 */
#define TIM_ICSELECTION_DIRECTTI TIM_CCMR1_CC1S_0
/* 选择TIM输入1、2、3或4分别连接到IC2,IC1,IC4或IC3 */
#define TIM_ICSELECTION_INDIRECTTI TIM_CCMR1_CC1S_1
/* 选择TIM输入1、2、3或4连接到TRC */
#define TIM_ICSELECTION_TRC TIM_CCMR1_CC1S
成员变量ICPrescaler用来设置输入捕获分频系数,可以设置为:
#define TIM_ICPSC_DIV1 0x00000000U /* 不分频 */
#define TIM_ICPSC_DIV2 TIM_CCMR1_IC1PSC_0 /* 2分频 */
#define TIM_ICPSC_DIV4 TIM_CCMR1_IC1PSC_1 /* 4分频 */
#define TIM_ICPSC_DIV8 TIM_CCMR1_IC1PSC /* 8分频 */
本实验需要设置为不分频,所以选值为TIM_ICPSC_DIV1。
成员变量ICFilter用来设置滤波器长度,这里我们不使用滤波器,所以设置为0。
形参3是定时器通道,范围:TIM_CHANNEL_1到TIM_CHANNEL_6,比如定时器5只有4个通道,那就选择范围只有TIM_CHANNEL_1到TIM_CHANNEL_4,就具体情况选择。- 函数返回值:HAL_StatusTypeDef枚举类型的值。HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(串口忙碌)、HAL_TIMEOUT(超时)。
3. HAL_TIM_IC_Start_IT函数
启动定时器输入捕获模式函数,其声明如下:
HAL_StatusTypeDef HAL_TIM_IC_Start_IT(TIM_HandleTypeDef *htim,uint32_t Channel);
- 函数描述:用于启动定时器的输入捕获模式,且开启输入捕获中断。
- 函数形参:形参1是TIM_HandleTypeDef结构体类型指针变量。
形参2是定时器通道,范围:TIM_CHANNEL_1到TIM_CHANNEL_4。 - 函数返回值:HAL_StatusTypeDef枚举类型的值。HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(串口忙碌)、HAL_TIMEOUT(超时)。
- 注意事项:如果我们不需要开启输入捕获中断,只是开启输入捕获功能,可以使用HAL库函数:
HAL_StatusTypeDef HAL_TIM_IC_Start(TIM_HandleTypeDef *htim,uint32_t Channel);
4. HAL_TIM_IC_Start函数
使能输入捕获功能,使能以后,输入捕获的功能才可以打开。
HAL_StatusTypeDef HAL_TIM_IC_Start(TIM_HandleTypeDef *htim, uint32_t Channel);
函数参数和返回值上面的HAL_TIM_IC_Start_IT函数一样。
- 函数描述: 用于启动定时器的输入捕获模式,此函数没有开启中断。
- 函数形参: 形参1是TIM_HandleTypeDef结构体类型指针变量。
形参2是定时器通道,范围:TIM_CHANNEL_1到TIM_CHANNEL_6。 - 函数返回值:HAL_StatusTypeDef枚举类型的值。HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(串口忙碌)、HAL_TIMEOUT(超时)。
5. HAL_TIM_ReadCapturedValue函数
uint32_t HAL_TIM_ReadCapturedValue(TIM_HandleTypeDef *htim, uint32_t Channel)
- 函数描述: 用于获取定时器对应通道当前的捕获值,也就是TIMx_CCRx的值。
- 函数形参: 形参1是TIM_HandleTypeDef结构体类型指针变量。
形参2是定时器通道,范围:TIM_CHANNEL_1到TIM_CHANNEL_4。 - 函数返回值:HAL_StatusTypeDef枚举类型的值。HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(串口忙碌)、HAL_TIMEOUT(超时)。
24.4.4 硬件设计
1. 例程功能
使用TIM5_CH1来做输入捕获,捕获PA0上的高电平脉宽,并将脉宽时间通过串口打印出来,然后通过按WKUP按键,模拟输入高电平,这里能测试的最长时间为:4194303 us。同时LED0闪烁指示程序在运行。
2. 硬件资源
1)LED0灯UART4和WKUP按键(通过按键给PA0输入高脉冲)
LED0 | WKUP | UART4_TX | UART4_RX |
PI0 | PA0 | PG11 | PB2 |
表24.4.4. 1硬件资源
2)定时器5输出通道1(TIM5_CH1)
定时器属于STM32MP157的内部资源,只需要软件设置好即可正常工作。
3. 原理图
从核心板原理图看出,PA0上接了WKUP按键,此按键高电平有效(按键按下,对应的IO口即为高电平)。PA0还可以复用为TIM5_CH1。程序上通过配置PA0复用为TIM5_CH1,然后按下WKUP按键后,PA0就输入一个高电平脉冲,调用HAL库的函数,实现串口上位机监测定时器输入捕获的情况。
图24.4.4. 1引脚部分原理图
从《STM32MP157A&D数据手册》中也可以查阅PI0的复用关系:
图24.4.4. 2《STM32MP157A&D数据手册》部分截图
24.4.5 程序设计
1. 程序流程图
图24.4.5. 1通用定时器中断实验程序流程图
2. 添加用户代码
(1)gtim.h文件代码
在gtim.h文件中添加如下代码:
/* 定义引脚PA0 */
#define GTIM_TIMX_CAP_CHY_GPIO_PORT GPIOA
#define GTIM_TIMX_CAP_CHY_GPIO_PIN GPIO_PIN_0
#define GTIM_TIMX_CAP_CHY_GPIO_AF GPIO_AF2_TIM5 /* AF功能选择 */
/* GPIOA时钟使能 */
#define GTIM_TIMX_CAP_CHY_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)
#define GTIM_TIMX_CAP TIM5
#define GTIM_TIMX_CAP_IRQn TIM5_IRQn
#define GTIM_TIMX_CAP_IRQHandler TIM5_IRQHandler
#define GTIM_TIMX_CAP_CHY TIM_CHANNEL_1 /* 通道Y, 1<= Y <=4 */
#define GTIM_TIMX_CAP_CHY_CCRX TIM5->CCR1 /* 通道Y的输出比较寄存器 */
/* TIMX 时钟使能 */
#define GTIM_TIMX_CAP_CHY_CLK_ENABLE() do{ __HAL_RCC_TIM5_CLK_ENABLE(); }while(0)
可以把上面的宏定义分成两部分,第一部分是定时器5输入通道1对应的IO口的宏定义,第二部分则是定时器5输入通道1的相应宏定义。
(2)gtim.c文件代码
gtim.h头文件就添加了这部分的程序,下面看gtim.c的程序,首先是通用定时器输入捕获初始化函数。
TIM_HandleTypeDef g_timx_cap_chy_handler; /* 定时器x句柄 */
TIM_IC_InitTypeDef g_timx_ic_cap_chy_handler; /* 定时器x输入捕获结构体定义 */
/**
* @brief通用定时器TIMX 通道Y 输入捕获 初始化函数
* @note
通用定时器的时钟来自APB1,当APB1DIV≥2分频的时候
通用定时器的时钟为APB1时钟的2倍, 而APB1为104.5M, 所以定时器时钟 = 209Mhz
定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.
定时器工作频率,单位:Mhz
*
* @param自动重装值
* @param时钟预分频数
* @retval无
*/
void gtim_timx_cap_chy_init(uint16_t arr, uint16_t psc)
{
g_timx_cap_chy_handler.Instance = GTIM_TIMX_CAP; /* 定时器5 */
g_timx_cap_chy_handler.Init.Prescaler = psc; /* 定时器分频 */
/* 向上计数模式*/
g_timx_cap_chy_handler.Init.CounterMode = TIM_COUNTERMODE_UP;
g_timx_cap_chy_handler.Init.Period = arr; /* 自动重装载值 */
/* 1分频
g_timx_cap_chy_handler.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_IC_Init(&g_timx_cap_chy_handler); /* 初始化定时器 */
}
/**
* @brief通用定时器输入捕获引脚初始化函数
* @param定时器句柄
* @note此函数会被HAL_TIM_IC_Init()调用
* @retval无
*/
void HAL_TIM_IC_MspInit(TIM_HandleTypeDef *htim)
{
if (htim->Instance == GTIM_TIMX_CAP) /* 输入通道捕获 */
{
GPIO_InitTypeDef gpio_init_struct;
GTIM_TIMX_CAP_CHY_CLK_ENABLE(); /* 使能TIMx时钟 */
GTIM_TIMX_CAP_CHY_GPIO_CLK_ENABLE(); /* 开启捕获IO的时钟 */
gpio_init_struct.Pin = GTIM_TIMX_CAP_CHY_GPIO_PIN; /* 输入捕获的GPIO口 */
gpio_init_struct.Mode = GPIO_MODE_AF_PP; /* 复用推挽输出 */
gpio_init_struct.Pull = GPIO_PULLDOWN; /* 下拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; /* 高速 */
/* 复用为捕获TIM5的通道1 */
gpio_init_struct.Alternate = GTIM_TIMX_CAP_CHY_GPIO_AF;
HAL_GPIO_Init(GTIM_TIMX_CAP_CHY_GPIO_PORT, &gpio_init_struct);
/*上升沿捕获*/
g_timx_ic_cap_chy_handler.ICPolarity= TIM_ICPOLARITY_RISING;
/* 映射到TI1上 */
g_timx_ic_cap_chy_handler.ICSelection = TIM_ICSELECTION_DIRECTTI;
g_timx_ic_cap_chy_handler.ICPrescaler = TIM_ICPSC_DIV1; /* 配置输入分频 */
g_timx_ic_cap_chy_handler.ICFilter = 0; /* 配置输入滤波器,不滤波 */
HAL_TIM_IC_ConfigChannel(&g_timx_cap_chy_handler, \ &g_timx_ic_cap_chy_handler, GTIM_TIMX_CAP_CHY); /* 配置TIM5通道1 */
HAL_TIM_IC_Start_IT(&g_timx_cap_chy_handler, GTIM_TIMX_CAP_CHY); /*启动*/
__HAL_TIM_ENABLE_IT(&g_timx_cap_chy_handler, TIM_IT_UPDATE); /* 使能中断 */
HAL_NVIC_SetPriority(GTIM_TIMX_CAP_IRQn, 1, 3); /* 抢占1,子优先级3 */
HAL_NVIC_EnableIRQ(GTIM_TIMX_CAP_IRQn); /* 开启ITMx中断 */
}
}
以上代码中,gtim_timx_cap_chy_init函数配置了TIM5的分频值为可配置的参数psc、计数模式为向上计数、自动装载值为可配置的arr、时钟分频值为1、然后调用HAL_TIM_IC_Init来初始化定时器的ARR和PSC等参数。HAL_TIM_IC_Init函数会调用HAL_TIM_IC_MspInit函数,然后其他部分的初始化都在它的MSP回调函数HAL_TIM_IC_MspInit里处理。
HAL_TIM_IC_MspInit函数调用HAL_GPIO_Init函数初始化定时器输入通道的GPIO口,并且开启GPIO时钟,使能定时器。其中要注意IO口复用功能的选择一定要选对了。第二部分则是调用HAL_TIM_IC_ConfigChannel函数配置映射关系,滤波和分频等。最后就是使能捕获通道和定时器中断,以及中断抢占优先级和响应优先级的设置。
通过上面的两个函数输入捕获的初始化就完成了,下面先来介绍两个变量。
/* 输入捕获状态(g_timxchy_cap_sta)
没有成功的捕获;1,成功捕获到一次.
还没捕获到高电平;1,已经捕获到高电平了.
捕获高电平后溢出的次数,最多溢出63次,所以最长捕获值 = 63*65536 + 65535 = 4194303
注意:为了通用,我们默认ARR和CCRy都是16位寄存器,对于32位的定时器(如:TIM5),
* 也只按16位使用
按1us的计数频率,最长溢出时间为:4194303 us, 约4.19秒
* (说明一下:正常32位定时器来说,1us计数器加1,溢出时间:4294秒)
*/
uint8_t g_timxchy_cap_sta = 0; /* 输入捕获状态 */
uint16_t g_timxchy_cap_val =0 ; /* 输入捕获值 */
上述代码定义的这两个变量用于辅助实现高电平捕获,注意使用extern来申明变量,表示在其它文件中也可以使用此变量。变量capture_state用于表示输入捕获的状态,这里将其定位为8位,我们把它当成一个8位的寄存器来用,对其各位赋予状态含义,描述如下表所示:
capture_state | ||
bit7 | bit6 | bit5~0 |
捕获完成标志 | 捕获到高/低电平标志 | 捕获高电平后定时器溢出的次数 |
0:未捕获完成 1: 捕获完成 | 0:未捕获到低电平 1: 捕获到低电平 | 记录捕获高电平后溢出的次数 |
表24.4.5. 1 capture_state各位描述
变量capture_state的位[5:0]是用于记录捕获高电平定时器溢出次数,总共6位,所以最多可以记录溢出的次数为(26-1)次,即63次,所以最长捕获值= 63*65536+65535 = 4194303。前面配置定时器1MHz的计数频率,所以每计数一个节拍就用时1us,所以最长溢出时间为4194303us,约4.19秒。
注意:为了通用,我们默认ARR和CCRy都是16位寄存器,对于32位的定时器(如:TIM5),也只按16位使用。
第6位用于标志是否有捕获到高/低电平。第7位用于表示是否已经捕获完成,0表示未捕获完成,1表示捕获完成。一个高电平脉冲是有一个上升沿和一个下降沿组成。
capture_val用于记录捕获到下降沿的时候,TIM5_CNT寄存器的值,脉冲捕获成功以后,需要通过capture_val的值计算出脉冲的宽度。
下面开始看中断服务函数的逻辑程序:
/**
* @brief定时器中断服务函数
* @param无
* @retval无
*/
void GTIM_TIMX_CAP_IRQHandler(void)
{
HAL_TIM_IRQHandler(&g_timx_cap_chy_handler); /* 定时器共用处理函数 */
}
如果计数器计数时,当TIMx_CNT 的值和TIMx_ARR相等时计数器发生溢出,产生一个溢出中断(更新中断)。如果定时器配置为输入捕获,极性为上升沿,当来一个上升沿脉冲时产生一个捕获中断。GTIM_TIMX_CAP_IRQHandler函数调用定时器中断请求函数HAL_TIM_IRQHandler。
在中断请求函数HAL_TIM_IRQHandler中,通过调用TIM5更新中断回调函数HAL_TIM_PeriodElapsedCallback来处理溢出中断,通过调用TIM5输入捕获中断处理回调函数HAL_TIM_IC_CaptureCallback来处理捕获中断。在HAL库中,这两个回调函数都是弱定义、无实际内容的函数,需要用户自定义以实现中断处理功能。
下面我们添加TIM5更新中断回调函数和TIM5输入捕获中断处理回调函数,函数定义如下:
1 /**
2 * @brief定时器输入捕获中断处理回调函数
3 * @param定时器句柄指针
4 * @note该函数在HAL_TIM_IRQHandler中会被调用
5 * @retval无
6 */
7 void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
8 {
9 if ((g_timxchy_cap_sta & 0X80) == 0) /* 还未成功捕获 */
10 {
11 if (g_timxchy_cap_sta & 0X40) /* 捕获到一个下降沿 */
12 {
13 g_timxchy_cap_sta |= 0X80; /* 标记成功捕获到一次高电平脉宽 */
14 /* 获取当前的捕获值 */
15 g_timxchy_cap_val = \ HAL_TIM_ReadCapturedValue(&g_timx_cap_chy_handler, GTIM_TIMX_CAP_CHY);
16 /* 一定要先清除原来的设置 */
17 TIM_RESET_CAPTUREPOLARITY(&g_timx_cap_chy_handler, \ GTIM_TIMX_CAP_CHY);
18
19 /* 配置TIM5通道1上升沿捕获 */
20 TIM_SET_CAPTUREPOLARITY(&g_timx_cap_chy_handler, \ GTIM_TIMX_CAP_CHY, TIM_ICPOLARITY_RISING);
21 }
22 else /* 还未开始,第一次捕获上升沿 */
23 {
24 g_timxchy_cap_sta = 0; /* 清空 */
25 g_timxchy_cap_val = 0;
26 g_timxchy_cap_sta |= 0X40; /* 标记捕获到了上升沿 */
27 __HAL_TIM_DISABLE(&g_timx_cap_chy_handler); /* 关闭定时器5 */
28 __HAL_TIM_SET_COUNTER(&g_timx_cap_chy_handler,0);
29 /* 一定要先清除原来的设置!! */
30 TIM_RESET_CAPTUREPOLARITY(&g_timx_cap_chy_handler, \ GTIM_TIMX_CAP_CHY);
31 /* 定时器5通道1设置为下降沿捕获 */
32 TIM_SET_CAPTUREPOLARITY(&g_timx_cap_chy_handler, \ GTIM_TIMX_CAP_CHY, TIM_ICPOLARITY_FALLING);
33 __HAL_TIM_ENABLE(&g_timx_cap_chy_handler); /* 使能定时器5 */
34 }
35 }
36 }
第9行,如果8位的g_timxchy_cap_sta和0X80位与等于0的话,表示此时还没有捕获成功(位与后等于0则表示第7位为0,表示捕获未完成);
第11行,在没有捕获成功时,如果这时候g_timxchy_cap_sta和0X40位相与等于1的话,表示此时第6位为1,即捕获到了下降沿;
第13行,捕获到低电平以后,则将g_timxchy_cap_sta的第7位置1,标记此时完成一次捕获;
第15行,捕获成功后,先获取当前的捕获值,也就是获取此时TIMx_CCR1寄存器的值,因为发生捕获时,计数器TIMx_CNT的值会被锁存到捕获寄存器TIMx_CCR1中。这里,HAL_TIM_ReadCapturedValue函数返回的是此时TIMx_CCR1的值,把此值赋值给我们前面定义的变量g_timxchy_cap_val;
第17行,调用TIM_RESET_CAPTUREPOLARITY宏清除清除极性,即清除原来设置的下降沿捕获,下一行代码会重新配置为上升沿捕获,即捕获完下降沿,下次要捕获上升沿;
第20行,配置TIM5的通道1为上升沿捕获;
以上第9~21行是捕获下降沿的逻辑代码,下面第22~34行的是捕获上升沿的逻辑代码,因为一个脉冲是由一个上升沿和一个下降沿组成的。一开始capture_state和capture_val的值都是0,而且程序初始化时配置极性为上升沿,所以程序运行以后首先执行第22行之后的代码。
第22行,如果g_timxchy_cap_sta和0X40位与后不等于1的话,即第6位为0,表示捕获到上升沿;
第24~26行,捕获到上升沿时,先清空g_timxchy_cap_sta和g_timxchy_cap_val,并将g_timxchy_cap_sta的第6位置1,表示已经捕获到高电平了,不过此时并没有完成捕获,因为一次完整的捕获是先捕获到到高电平再捕获到低电平才组成一个脉冲,所以g_timxchy_cap_sta的第7位被清零,表示未完成捕获。
第27~28行,调用__HAL_TIM_DISABLE来关闭TIM5,即关闭计数器,并调用__HAL_TIM_SET_COUNTER来设置计数器值为0。这里为什么要关闭定时器还要设置计数器计数值为0呢?我们以一个图来说明:
一个高电平脉冲的宽度是上升沿到下降沿之间这段时间的值,通过捕获寄存器的值可以知道捕获到这次高电平脉冲时计数器计数了多少个节拍,则可以计算此高电平脉冲的宽度。如下图,捕获到上升沿时计数器的值为0,捕获到下降沿时计数器的值为g_timxchy_cap_val,假设计数器的频率为f,则这段高电平脉冲的时间为g_timxchy_cap_val /f。
图24.4.5.2高电平脉冲捕获示意图(没有溢出的情况下)
第30和32行,清除原来的设置,再设置TIM5为下降沿捕获,为下一个捕获做准备。
第33行,再次开启定时器。
以上就是输入捕获中断处理回调函数,通过来回切换捕获边沿的极性以及关闭和开启定时器来实现。在捕获的过程中,如果脉冲的时间比较长,计数器可能会发生溢出,我们来看看溢出中断回调函数,如下:
1 /**
2 * @brief定时器更新中断回调函数
3 * @param定时器句柄指针
4 * @note此函数会被定时器中断函数共同调用的
5 * @retval无
6 */
7 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
8 {
9 if (htim == (&g_timx_cap_chy_handler))
10 {
11 if ((g_timxchy_cap_sta & 0X80) == 0) /* 还未成功捕获 */
12 {
13 if (g_timxchy_cap_sta & 0X40) /* 已经捕获到高电平了 */
14 {
15 if ((g_timxchy_cap_sta & 0X3F) == 0X3F) /* 高电平太长了 */
16 {
17 /* 获取当前的捕获值 */
18 g_timxchy_cap_val = HAL_TIM_ReadCapturedValue \ (&g_timx_cap_chy_handler, TIM_CHANNEL_1);
19 /* 一定要先清除原来的设置 */
20 TIM_RESET_CAPTUREPOLARITY(&g_timx_cap_chy_handler, \ GTIM_TIMX_CAP_CHY);
21 /* 定时器5通道1设置为下降沿捕获 */
22 TIM_SET_CAPTUREPOLARITY(&g_timx_cap_chy_handler, \ GTIM_TIMX_CAP_CHY, TIM_ICPOLARITY_RISING);
23 g_timxchy_cap_sta |= 0X80; /* 标记成功捕获了一次 */
24 g_timxchy_cap_val = 0XFFFF;
25 }
26 else
27 {
28 g_timxchy_cap_sta++;
29 }
30 }
31 }
32 }
33 }
第9行,如果判断是TIM5,则执行第9行以后的语句;
第11行,如果g_timxchy_cap_sta的第7位是0,表示还未成功捕获到高电平脉冲;
第13行,如果g_timxchy_cap_sta的第6位为1,表示捕获到了下降沿;
第15行,g_timxchy_cap_sta的第0~5位表示捕获到低电平后定时器的溢出次数,如果g_timxchy_cap_sta的第0~5位都是1,表示已经达到g_timxchy_cap_sta最大记录的溢出次数了;
第18行,HAL_TIM_ReadCapturedValue返回的是当前的捕获值,先将此时的捕获值保存在g_timxchy_cap_val中;
第20~22行,清除原来的设置,并设置TIM5的通道1为上升沿捕获,为下一个捕获做准备;
第23行,将g_timxchy_cap_sta的第7位置1,捕获到低电平脉冲时,表示已经完成了一次捕获;
第24行,16位的g_timxchy_cap_val用于保存捕获值,如果脉冲太长了,将g_timxchy_cap_val设置为最大值0XFFFF。
第28行,如果g_timxchy_cap_sta的第0~5位还没达到最大值,表示g_timxchy_cap_sta还能继续记录溢出次数,每次捕获到一次低电平,则g_timxchy_cap_sta自加1;
(3)main.c文件代码
1 #include "./SYSTEM/sys/sys.h"
2 #include "./SYSTEM/delay/delay.h"
3 #include "./SYSTEM/usart/usart.h"
4 #include "./BSP/LED/led.h"
5 #include "./BSP/BEEP/beep.h"
6 #include "./BSP/KEY/key.h"
7 #include "./BSP/TIMER/gtim.h"
8
9 extern uint8_t g_timxchy_cap_sta; /* 输入捕获状态 */
10 extern uint16_t g_timxchy_cap_val; /* 输入捕获值 */
11
12 /**
13 * @brief主函数
14 * @param无
15 * @retval无
16 */
17 int main(void)
18 {
19 unsigned int i = 0; /* 定义变量i,用于指示LED0灯什么时候亮 */
20 uint32_t temp = 0; /* 定义变量temp,用于计算溢出时间 */
21
22 HAL_Init(); /* 初始化HAL库 */
23 /* 初始化M4内核时钟,209M */
24 if(IS_ENGINEERING_BOOT_MODE())
25 {
26 sys_stm32_clock_init(34, 2, 2, 17, 6826);
27 }
28 LED1(0); /* 关闭 LED1 */
29 usart_init(115200); /* 串口初始化为115200 */
30 delay_init(209); /* 延时初始化 */
31 led_init(); /* 初始化LED */
32 key_init(); /* 初始化按键 */
33 gtim_timx_cap_chy_init(0XFFFF, 209 - 1); /* 以1Mhz的频率计数 捕获 */
34
35 while(1)
36 {
37 if (g_timxchy_cap_sta & 0X80) /* 成功捕获到高电平脉冲 */
38 {
39 temp = g_timxchy_cap_sta&0X3F;
40 temp *= 65536; /* 溢出时间总和 */
41 temp += g_timxchy_cap_val; /* 得到总的高电平时间 */
42 //printf("HIGH:%lld us\r\n",temp); /* 打印总的高电平时间 */
43 printf("HIGH:%d us\r\n", temp);
44 g_timxchy_cap_sta = 0; /* 开启下一次捕获 */
45 }
46 i++;
47 if (i > 20) /* 200ms进入一次 */
48 {
49 i = 0;
50 LED0_TOGGLE();
51 }
52 delay_ms(10);
53 }
54 }
第37行,表示如果capture_state的第7位为1,即成功捕获到高电平脉冲;
第39行,使用变量temp获取此时溢出的次数;
第40行,计算溢出时间总和,计数器每次计数65536个节拍后溢出一次;
第41行,temp +=表示计数器溢出节拍数+捕获值等于计数器总的节拍数。前面TIM5初始化部分,配置计数器每个节拍计时1us,所以总共计时temp us;
第43行,打印总的高电平时间;
第44行,将capture_state清0,因为如果capture_state的第7位一直是1的话,就不会进行二次捕获,所以在main.c文件中处理完捕获数据后,要将capture_state清0,从而开启第二次捕获;
第47~52行,当t自加到20时,LED0翻转;
24.4.6 编译和测试
保存修改,编译测试无报错后进行仿真,按下按键WK_UP后,串口终端显示如下,打印的数据中可以看到出现一个10us,这种就是按键按下时发生的抖动引起的,这就是为什么在按键实验中进行了消抖处理,防止因为按键抖动对数据产生干扰。定时器5是32位的计数器,本实验我们为了通用性只配置了16位,如果想使用定时器的32位,可以把自动重载寄存器的值设置为(0XFFFFFFFF-1),main.c文件中temp *= 65536改为temp*=0XFFFFFFFF即可。
图24.4.6.1运行结果
24.5 通用定时器脉冲计数实验(外部时钟模式1)
本实验配置好的实验工程已经放到了开发板光盘中,路径为:开发板光盘A-基础资料\1、程序源码\3、M4裸机驱动例程\库V1.2\实验13-4 通用定时器-脉冲计数实验。
在17.1小节介绍定时器框图时我们有对外部时钟模式1和外部时钟模式2进行过讲解,本小节实验我们使用定时器的外部时钟模式1的方式来对外部输入的脉冲进行计数,当然,在外部时钟模式1下,定时器也是工作在从模式,也可以说为使用从模式进行脉冲计数。
在17.1小节我们知道定时器的时钟来源有四种, 前面的三个通用定时器实验使用的时钟源都是来自内部时钟 (CK_INT),本小节的实验我们将使用外部时钟模式 1:外部输入引脚 (TIx)作为定时器的时钟源。
本章节使用TIM2的通道1作为外部时钟输入引脚,此引脚上接的是按键WK_UP,此按键是高电平有效,按下以后,会给PA0提供一个高电平脉冲作为定时器的计数器时钟,每按下一次按键产生一次高电平脉冲,计数器加一,这时定时器就工作在从模式。
关于定时器的主从模式该怎么理解?定时器都可以通过外部信号触发而启动计数,还可以通过另外一个定时器的某种TRGO信号(包括复位,使能,更新,比较脉冲等TRGO信号)触发启动计数。像这样通过一个定时器触发另一个定时器,发出触发信号的定时器工作于主模式,接受触发信号而启动的定时器工作于从模式。
下面开始讲解本实验用到的寄存器配置情况。
24.5.1 TIM2/TIM3/TIM4/TIM5寄存器
通用定时器脉冲计数实验需要用到的寄存器有:TIMx_ARR、TIMx_PSC、TIMx_CCMR1、TIMx_CCER、TIMx_DIER、TIMx_CR1、TIMx_EGR这些寄存器在前面的章节都有介绍到了,在本小节我们只需针对性的介绍。
1. 捕获/比较模式寄存器1/2(TIMx_CCMR1/2)
该寄存器我们在PWM输出实验时讲解了他作为输出功能的配置,在输入捕获实验学习了输入捕获模式的配置,本小节我们的外部信号也同样要作为输入信号给定时器作为时钟源,所以我们要看输入捕获模式定时器对应功能。WK_UP按键(PA0)对应着定时器2的通道1。接下来我们开始配置TIMx_CCMR1寄存器,其描述如下图所示:
图24.5.1. 1 TIMx_CCMR1寄存器
因为我们要让PA0引脚输入的脉冲到定时器2的通道1,所以应该配置TIM2_CCMR1寄存器,低八位[7:0]用于捕获/比较通道1的控制,其中
CC1S[1:0],这两个位用于CCR1的通道配置,这里我们设置IC1S[1:0]=01,也就是配置IC1映射在TI1上,即CCR1对应TIMx_CH1。
IC1PSC[1:0]输入捕获1预分频器,我们是1次高电平脉冲就触发1次计数,所以不用分频选择00即可。
IC1F[3:0]输入捕获1滤波器,这个用来设置输入采样频率和数字滤波器长度,关于滤波长度的介绍请看上一个实验。这里,我们不做滤波处理,所以设置IC1F[3:0]=0000,只要采集到上升沿,就触发捕获。
2. 捕获/比较使能寄存器(TIMx_ CCER)
TIM2/TIM3/TIM4/TIM5的捕获/比较使能寄存器,该寄存器控制着各个输入输出通道的开关和极性。TIMx_CCER寄存器描述如下图所示:
图24.5.1. 2 TIMx_CCER寄存器
我们要用到这个寄存器的最低2位,CC1E和CC1P位。要使能输入捕获,必须设置CC1E=1,而CC1P则根据自己的需要来配置。我们这里是保留默认设置值0,即高电平触发捕获。
可以看到上面两个寄存器配置和输入捕获实验的配置是一样的。两个实验配置最大的区别就是本实验是在从模式下工作的,下面主要看看从模式控制寄存器的配置。
3. 从模式控制寄存器(TIMx_ SMCR)
TIM2/TIM3/TIM4/TIM5的从模式控制寄存器,该寄存器用于配置从模式,以及定时器的触发源相关的设置。TIMx_SMCR寄存器描述如下图所示:
图24.5.1. 3 TIMx_SMCR寄存器
因为我们要让外部引脚脉冲信号作为定时器的时钟源,所以位[2:0]和位16组合的SMS[0:3],我们设置的值是0111,即外部时钟模式1。位[6:4]和位[21:20]组合的TS[0:5]是触发选择设置,TIMx_CH1对应TI1FP1,TIMx_CH2则对应TI2FP2,我们是定时器通道1,所以需要配置的值为00101。ETF[3:0]和ETPS[1:0]分别是外部触发滤波器和外部触发预分频器,我们没有用到。
4. DMA/中断使能寄存器(TIMx_DIER)
接下来我们再看看DMA/中断使能寄存器:TIMx_DIER,该寄存器的各位描述如下图。本小节,我们需要在定时器溢出中断中累计定时器溢出的次数,所以需要使能定时器的更新中断,即UIE置1。
图24.5.1. 4寄存器
控制寄存器:TIMx_CR1,我们只用到了它的最低位CEN,将该位置1则使能定时器。
24.5.2 定时器的HAL库驱动
定时器在HAL库中的驱动代码在前面已经介绍了部分,这里我们针对定时器从模式介绍HAL_TIM_SlaveConfigSynchronization函数。
1. HAL_TIM_SlaveConfigSynchronization函数
该函数是HAL_TIM_SlaveConfigSynchro函数的宏定义,真正的函数定义是后者,其定义如下:
HAL_StatusTypeDef HAL_TIM_SlaveConfigSynchro(TIM_HandleTypeDef *htim,
TIM_SlaveConfigTypeDef *sSlaveConfig);
- 函数描述: 该函数用于配置定时器的从模式。
- 函数形参:形参1是TIM_HandleTypeDef结构体类型指针变量,用于配置定时器基本参数。形参2是TIM_SlaveConfigTypeDef结构体类型指针变量,用于配置定时器的从模式。重点了解一下TIM_SlaveConfigTypeDef结构体指针类型,其定义如下:
typedef struct
{
uint32_t SlaveMode; /* 从模式选择 */
uint32_t InputTrigger; /* 输入触发源选择 */
uint32_t TriggerPolarity; /* 输入触发极性 */
uint32_t TriggerPrescaler; /* 输入触发预分频 */
uint32_t TriggerFilter; /* 输入滤波器设置 */
} TIM_SlaveConfigTypeDef;
- 函数返回值:
HAL_StatusTypeDef枚举类型的值。HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(串口忙碌)、HAL_TIMEOUT(超时)
2. 几个重要的宏
实验中我们会用到一下几个宏定义:
__HAL_TIM_ENABLE_IT /* 使能句柄指定的定时器更新中断 */
__HAL_TIM_SET_COUNTER /* 在运行时设置定时器计数器寄存器的值 */
__HAL_TIM_ENABLE /* 启用定时器 */
__HAL_TIM_DISABLE /* 关闭定时器,同时也将通道输出或者输入关闭
__HAL_TIM_GET_COUNTER /* 在运行时获取定时器计数器寄存器值 */
3.函数
HAL_TIM_IC_Start_IT函数我们在前面的17.4小节也讲解过,主要用于启动定时器的输入捕获模式,且开启输入捕获中断。
4. 回调函数HAL_TIM_PeriodElapsedCallback
更新中断回调函数HAL_TIM_PeriodElapsedCallback函数我们在上一章节实验使用过,实验中我们需要手动编写此回调函数来处理溢出中断。
24.5.3 硬件设计
1. 例程功能
用TIM2_CH1做输入捕获,捕获PA0上的高电平脉冲,可以通过按WK_UP按键,输入高电平脉冲,调用HAL库的API将脉冲进行计数,然后通过串口UART4打印出来,通过按KEY0重设当前计数,LED0闪烁,提示程序运行。
2. 硬件资源
1)LED0灯UART4和WKUP按键(通过按键给PA0输入高脉冲)
LED0 | WKUP | UART4_TX | UART4_RX | KEY0 |
PI0 | PA0 | PG11 | PB2 | PG3 |
表24.5.3. 1硬件资源
2)定时器2输出通道1(TIM2_CH1)
定时器属于STM32MP157的内部资源,只需要软件设置好即可正常工作。
3. 原理图
从核心板原理图看出,PA0上接了WKUP按键,此按键高电平有效(按键按下,对应的IO口即为高电平)。PA0还可以复用为TIM2_CH1,程序上通过配置PA0复用为TIM2_CH1,然后按下WKUP按键后,PA0就输入一个高电平脉冲给定时器。
图24.5.3. 1引脚部分原理图
从《STM32MP157A&D数据手册》中也可以查阅PA0的复用关系:
图24.5.3. 2《STM32MP157A&D数据手册》部分截图
24.5.4 程序设计
1. 程序流程图
图24.5.4. 1通用定时器中断实验程序流程图
2. 添加用户代码
(1)gtim.h文件代码
/* TIMX 输入计数定义
* 这里的输入计数使用定时器TIM2_CH1,捕获WK_UP按键的输入
* 默认是针对TIM2~TIM5, TIM12~TIM17. 只有CH1和CH2通道可以用做输入计数, CH3/CH4不支持!
* 注意: 通过修改这9个宏定义,可以支持TIM1~TIM17任意一个定时器,CH1/CH2对应IO口做输入计数
* 特别要注意:默认用的PA0,设置的是下拉输入!如果改其他IO,对应的上下拉方式/AF功能等也得改!
*/
#define GTIM_TIMX_CNT_CHY_GPIO_PORT GPIOA
#define GTIM_TIMX_CNT_CHY_GPIO_PIN GPIO_PIN_0
#define GTIM_TIMX_CNT_CHY_GPIO_AF GPIO_AF1_TIM2 /* AF功能选择 */
#define GTIM_TIMX_CNT_CHY_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0) /* PA口时钟使能 */
#define GTIM_TIMX_CNT TIM2
#define GTIM_TIMX_CNT_IRQn TIM2_IRQn
#define GTIM_TIMX_CNT_IRQHandler TIM2_IRQHandler
#define GTIM_TIMX_CNT_CHY TIM_CHANNEL_1 /* 通道Y, 1<= Y <=2 */
/* TIMX 时钟使能 */
#define GTIM_TIMX_CNT_CHY_CLK_ENABLE() do{__HAL_RCC_TIM2_CLK_ENABLE();}while(0)
可以把上面的宏定义分成两部分,第一部分是定时器2输入通道1对应的IO口的宏定义,第二部分则是定时器2输入通道1的相应宏定义。其中我们需要注意的点是:只有CH1和CH2通道可以用做输入计数,CH3/CH4不支持!
(2)gtim.c文件代码
gtim.h头文件就添加了这部分的程序,下面看gtim.c的程序,首先是通用定时器脉冲计数初始化函数。
TIM_HandleTypeDef g_timx_cnt_chy_handler; /* 定时器x句柄 */
/* 记录定时器计数器的溢出次数, 方便计算总脉冲个数 */
uint32_t g_timxchy_cnt_ofcnt = 0 ;
/**
* @brief通用定时器TIMX 通道Y 脉冲计数 初始化函数
* @note
本函数选择通用定时器的时钟选择: 外部时钟源模式1(SMS[2:0] = 111)
这样CNT的计数时钟源就来自 TIMX_CH1/CH2, 可以实现外部脉冲计数(脉冲接入CH1/CH2)
*
时钟分频数 = psc, 一般设置为0, 表示每一个时钟都会计数一次, 以提高精度.
通过读取CNT和溢出次数, 经过简单计算, 可以得到当前的计数值, 从而实现脉冲计数
*
* @param自动重装值
* @retval无
*/
void gtim_timx_cnt_chy_init(uint16_t psc)
{
g_timx_cnt_chy_handler.Instance = GTIM_TIMX_CNT; /* 定时器x */
g_timx_cnt_chy_handler.Init.Prescaler = psc; /* 定时器分频 */
/*向上计数模式 */
g_timx_cnt_chy_handler.Init.CounterMode = TIM_COUNTERMODE_UP;
g_timx_cnt_chy_handler.Init.Period = 65535; /* 自动重装载值 */
/* 1分频 */
g_timx_cnt_chy_handler.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_IC_Init(&g_timx_cnt_chy_handler);
}
/**
* @brief通用定时器输入捕获初始化接口
库调用的接口,用于配置不同的输入捕获
* @param定时器句柄
* @note此函数会被HAL_TIM_IC_Init()调用
* @retval无
*/
void HAL_TIM_IC_MspInit(TIM_HandleTypeDef *htim)
{
if (htim->Instance == GTIM_TIMX_CNT) /*输入计数捕获*/
{
GPIO_InitTypeDef gpio_init_struct;
TIM_SlaveConfigTypeDef tim_slave_config_handle = {0};
TIM_IC_InitTypeDef timx_ic_cnt_chy_handler = {0};
GTIM_TIMX_CNT_CHY_CLK_ENABLE(); /* 使能TIMx时钟 */
GTIM_TIMX_CNT_CHY_GPIO_CLK_ENABLE(); /* 开启GPIOA时钟 */
gpio_init_struct.Pin = GTIM_TIMX_CNT_CHY_GPIO_PIN;/* 输入捕获的GPIO口 */
gpio_init_struct.Mode = GPIO_MODE_AF_PP; /* 复用推挽输出 */
gpio_init_struct.Pull = GPIO_PULLDOWN; /* 下拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; /* 高速 */
/* 复用为捕获通道 */
gpio_init_struct.Alternate = GTIM_TIMX_CNT_CHY_GPIO_AF;
HAL_GPIO_Init(GTIM_TIMX_CNT_CHY_GPIO_PORT, &gpio_init_struct);
/* 从模式:外部触发模式1 */
tim_slave_config_handle.SlaveMode = TIM_SLAVEMODE_EXTERNAL1;
/* 输入触发:选择 TI1FP1(TIMX_CH1) 作为输入源 */
tim_slave_config_handle.InputTrigger = TIM_TS_TI1FP1;
/* 触发极性:上升沿 */
tim_slave_config_handle.TriggerPolarity = TIM_TRIGGERPOLARITY_RISING;
/* 触发预分频:无 */
tim_slave_config_handle.TriggerPrescaler = TIM_TRIGGERPRESCALER_DIV1;
tim_slave_config_handle.TriggerFilter = 0x0; /* 滤波:本例中不需要任何滤波 */
HAL_TIM_SlaveConfigSynchro(&g_timx_cnt_chy_handler,&tim_slave_config_handle);
/* 配置输入捕获模式 */
/* 上升沿捕获 */
timx_ic_cnt_chy_handler.ICPolarity = TIM_ICPOLARITY_RISING;
/* 映射到TI1上 */
timx_ic_cnt_chy_handler.ICSelection = TIM_ICSELECTION_DIRECTTI;
timx_ic_cnt_chy_handler.ICPrescaler = TIM_ICPSC_DIV1;/*配置输入不分频*/
timx_ic_cnt_chy_handler.ICFilter = 0; /* 配置输入滤波器,不滤波 */
/* 配置TIMx通道y */
HAL_TIM_IC_ConfigChannel(&g_timx_cnt_chy_handler, \ &timx_ic_cnt_chy_handler, GTIM_TIMX_CNT_CHY);
/* 开始捕获TIMx的通道y */
HAL_TIM_IC_Start(&g_timx_cnt_chy_handler, GTIM_TIMX_CNT_CHY);
/*使能更新中断*/
__HAL_TIM_ENABLE_IT(&g_timx_cnt_chy_handler,TIM_IT_UPDATE);
/* 设置中断优先级,抢占优先级1,子优先级3 */
HAL_NVIC_SetPriority(GTIM_TIMX_CNT_IRQn,1,3);
HAL_NVIC_EnableIRQ(GTIM_TIMX_CNT_IRQn); /* 开启ITMx中断 */
}
}
上一章节的实验代码也用到了HAL_TIM_IC_MspInit函数,关于该函数我们只列出与本实验相关的初始化程序。HAL_TIM_IC_MspInit函数是HAL_TIM_IC_Init函数的MSP回调函数,我们通过TIM_HandleTypeDef类型结构体变量的Instance成员去区分要初始化哪个外设,Instance成员定义的就是该外设寄存器的基地址相关信息。
gtim_timx_cnt_chy_init函数完成了TIM2通道1的初始化,HAL_TIM_IC_Init函数会调用HAL_TIM_IC_MspInit函数。HAL_TIM_IC_MspInit函数的第一部分调用HAL_GPIO_Init函数初始化定时器输入通道的GPIO口,并且开启GPIO时钟,使能定时器。其中要注意IO口复用功能的选择一定要选对了。第二部分则是调用HAL_TIM_SlaveConfigSynchro函数配置定时器的从模式,TIM_SlaveConfigTypeDef结构体的内容。第三部分则是调用HAL_TIM_IC_ConfigChannel函数配置映射关系,滤波和分频等。最后就是是使能捕获通道和定时器更新中断,以及中断抢占优先级和响应优先级的设置。
下面我们看定时器的中断服务函数的回调函数:
/**
* @brief通用定时器TIMX 脉冲计数 更新中断服务函数
* @param无
* @retval无
*/
void GTIM_TIMX_CNT_IRQHandler(void)
{
HAL_TIM_IRQHandler(&g_timx_cnt_chy_handler); /* 定时器共用处理函数 */
}
/**
* @brief定时器更新中断回调函数
* @param定时器句柄指针
* @note此函数会被定时器中断函数共同调用的
* @retval无
*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim == (&g_timx_cnt_chy_handler))
{
g_timxchy_cnt_ofcnt++; /* 累计溢出次数 */
}
}
中断请求函数HAL_TIM_IRQHandler会调用更新中断回调函数HAL_TIM_PeriodElapsedCallback,该函数中让g_timxchy_cnt_ofcnt全局变量自加。我们在初始化定时器的时候,设置自动重装载值是65535,而且是向上计数的,所以定时器需要计数65536次才会溢出,即每当定时器溢出时,g_timxchy_cnt_ofcnt自加1。
再来介绍两个自定义的功能函数,第一个是获取当前计数值函数,其定义如下:
1 /**
2 * @brief通用定时器TIMX 通道Y 获取当前计数值
3 * @param无
4 * @retval当前计数值
5 */
6 uint32_t gtim_timx_cnt_chy_get_count(void)
7 {
8 uint32_t count = 0;
9 count = g_timxchy_cnt_ofcnt * 65536; /* 计算溢出次数对应的计数值 */
10 /* 加上当前CNT的值 */
11 count += __HAL_TIM_GET_COUNTER(&g_timx_cnt_chy_handler);
12 return count;
13 }
14 /**
15 * @brief通用定时器TIMX 通道Y 重启计数器
16 * @param无
17 * @retval当前计数值
18 */
19 void gtim_timx_cnt_chy_restart(void)
20 {
21 __HAL_TIM_DISABLE(&g_timx_cnt_chy_handler); /* 关闭定时器TIMX */
22 g_timxchy_cnt_ofcnt = 0; /* 累加器清零 */
23 __HAL_TIM_SET_COUNTER(&g_timx_cnt_chy_handler, 0); /* 计数器清零 */
24 __HAL_TIM_ENABLE(&g_timx_cnt_chy_handler); /* 使能定时器TIMX */
25 }
以上两个函数比较简单,我们简单分析一下这两个函数。
gtim_timx_cnt_chy_restart函数用于重启计数器,按下KEY0按键后就会调用此函数实现计数器清零:
第21行,先使用宏__HAL_TIM_DISABLE将TIMx_CR1的第0位CEN清零,即关闭计数器,计数器关闭后,定时器就不工作了;
第22行,将g_timxchy_cnt_ofcnt先清零,只要关闭计数器,该变量就应该清零;
第23行,将TIM2的计数器清零;
第24行,重新使能TIM2;
gtim_timx_cnt_chy_get_count用于计算计数器的值,通过此值可以知道计数器记录的高脉冲次数。程序运行后,先按下KEY0清零计数器,然后再按下WK_UP按键给PA0提供高电平脉冲,每按下一次WK_UP就给PA0一次高电平脉冲:
第8行,定义32位的局部变量count为0;
第9行,count用于获取总溢出的计数器值;
第11行,count用于获取总的计数器计数值,也就是溢出的值加上没有溢出的捕获值;
第12行,函数返回值是计数器的计数值。
(3)修改main.c文件
main.c文件代码如下:
1 #include "./SYSTEM/sys/sys.h"
2 #include "./SYSTEM/delay/delay.h"
3 #include "./SYSTEM/usart/usart.h"
4 #include "./BSP/LED/led.h"
5 #include "./BSP/BEEP/beep.h"
6 #include "./BSP/KEY/key.h"
7 #include "./BSP/TIMER/gtim.h"
8
9 /**
10 * @brief主函数
11 * @param无
12 * @retval无
13 */
14 int main(void)
15 {
16 uint32_t curcnt = 0;
17 uint32_t oldcnt = 0;
18 uint8_t key = 0;
19 uint8_t t = 0;
20
21 HAL_Init(); /* 初始化HAL库 */
22 /* 初始化M4内核时钟,209M */
23 if(IS_ENGINEERING_BOOT_MODE())
24 {
25 sys_stm32_clock_init(34, 2, 2, 17, 6826);
26 }
27 usart_init(115200); /* 串口初始化为115200 */
28 delay_init(209); /* 延时初始化 */
29 led_init(); /* 初始化LED */
30 key_init(); /* 初始化按键 */
31 gtim_timx_cnt_chy_init(0); /* 定时器计数初始化, 不分频 */
32 gtim_timx_cnt_chy_restart(); /* 重启计数 */
33
34 while(1)
35 {
36 key = key_scan(0); /* 扫描按键 */
37 if (key == KEY0_PRES) /* KEY0按键按下,重启计数 */
38 {
39 gtim_timx_cnt_chy_restart(); /* 重新启动计数 */
40 }
41 curcnt = gtim_timx_cnt_chy_get_count(); /* 获取计数值 */
42 if (oldcnt != curcnt)
43 {
44 oldcnt = curcnt;
45 printf("CNT:%d\r\n", oldcnt); /* 打印脉冲个数 */
46 }
47 t++;
48 if (t > 20) /* 200ms进入一次 */
49 {
50 t = 0;
51 LED0_TOGGLE(); /* LED0闪烁 ,提示程序运行 */
52 }
53 delay_ms(10);
54 }
55 }
第16~19行,定义几个变量,用于while循环中计算脉冲;
第32行,进入主函数之前先清除计数器;
第36~40行,执行按键扫描,如果是KEY0按下,则将计数器清零;
第41行,gtim_get_count返回的是计数器的计数值,curcnt记录此值;
第42~46行,打印脉冲个数;
第47~53,LED0以每隔200ms的时间闪烁,指示程序正在运行;
24.5.5 编译和测试
保存修改,编译工程无报错后,进入仿真模式进行测试。按下WK_UP按键后,串口打印“CNT:1”,按下KEY0后,串口打印“CNT:0”。
24.5.5.1运行结果