第4章 电机控制相关的STM32外设(4.3)
4.3 高级定时器
由于STM32的高级定时器包含了通用定时器的所有功能,所以我们跳过通用定时器部分,直接来学习高级定时器,大家只要学会了高级定时器的使用,通用定时器也就迎刃而解了。
STM32G474有3个高级定时器(TIM1、TIM8以及TIM20)。这些定时器彼此完全独立,不共享任何资源。本小节我们将通过7个实验来学习高级定时器的各个功能,分别是高级定时器中断实验、高级定时器PWM输出实验、高级定时器互补输出带死区刹车实验、高级定时器输入捕获实验、高级定时器PWM输入实验、高级定时器输出比较实验和高级定时器输出指定个数PWM实验。
本小节分为如下几部分:
4.3.1 高级定时器简介
4.3.2 高级定时器中断实验
4.3.3 高级定时器PWM输出实验
4.3.4 高级定时器互补输出带死区刹车控制实验
4.3.5 高级定时器输入捕获实验
4.3.6 高级定时器PWM输入实验
4.3.7 高级定时器输出比较模式实验
4.3.8 高级定时器输出指定个数PWM实验
4.3.1 高级定时器简介
STM32G474的定时器有13个之多,其基本特性不尽相同,为了更好地区别各个定时器的特性,我们列了一个表格,如下所示:

表4.3.1.1 定时器基本特性表
注:该表参考数据手册 《STM32G474VET6.pdf》的3.24小节(第35页)
由上表知道:该STM32芯片的TIM2和TIM5计数器是32位的,其他的都是16位的。通用定时器和高级定时器其实也就是在基本定时器的基础上,添加了一些其他功能,如:输入捕获、输出比较、输出PWM和单脉冲模式以及互补输出等,其特性有一些的差异,但是基本原理都一样。
高分辨率定时器(HRTIM)它由7个定时器组成。1个主站和6个从站,总共12个高分辨率输出。它们可以用于生成具有高精度定时的数字信号,如PWM或相移脉冲等。
对于本章我们主要学习高级定时器的功能。
高级定时器的框图
下面先来学习高级定时器框图,通过学习高级定时器框图会有一个很好的整体掌握,同时对之后的编程也会有一个清晰的思路。

图4.3.1.1高级定时器框图
如上图,高级定时器的框图比基本定时器的框图复杂许多,为了方便介绍,我们将其分成七个部分讲解:
① 时钟源
高级定时器时钟可由下列的时钟源提供:
1)内部时钟 (tim_ker_ck);
2)外部时钟模式 1:外部输入引脚 (tim_tix_in[15:0]),x=1,2(即只能来源于通道一或者通道2);
3)外部时钟模式 2:外部触发输入 (tim_etr[15:1]);
4)内部触发输入 (tim_itr[15:0]):使用一个定时器作为另一定时器的预分频器;
高级定时器时钟源的设置方法如下表所示:

表4.3.1.2 高级定时器时钟源设置方法
② 控制器
控制器包括:从模式控制器、编码器接口和触发控制器(TRGO)。从模式控制器可以控制计数器复位、启动、递增/递减、计数。编码器接口针对编码器计数。触发控制器用来提供触发信号给别的外设,比如为其他定时器提供时钟或者为DAC/ADC的触发转换提供信号。
③ 时基单元
高级定时器时基单元包括:计数器寄存器(TIMx_CNT)、预分频器寄存器(TIMx_PSC)、自动重载寄存器(TIMx_ARR)、重复计数器寄存器(TIMx_RCR)。其中,前3个寄存器的内容和基本定时器是基本一样的,大家可以参考基本定时器的介绍。
下面我们来介绍一些不同的点:
1,高级定时器的计数模式有三种:递增计数模式、递减计数模式和中心对齐模式。
递增计数模式在讲解基本定时器的时候已经讲过了,那么对应到递减计数模式就很好理解了。就是来了一个计数脉冲,计数器就减1,直到计数器寄存器的值减到0,减到0时定时器溢出,由于是递减计数,故而称为定时器下溢,定时器溢出就会伴随着更新事件的发生。然后计数器又从自动重载寄存器影子寄存器的值开始继续递减计数,如此循环。最后是中心对齐模式,字面上不太好理解。该模式下,计数器先从0开始递增计数,直到计数器的值等于自动重载寄存器影子寄存器的值减1时,定时器上溢,同时生成更新事件,然后从自动重载寄存器影子寄存器的值开始递减计算,直到计数值等于1时,定时器下溢,同时生成更新事件,然后又从0开始递增计数,依此循环。每次定时器上溢或下溢都会生成更新事件。计数器的计数模式的设置请参考TIMx_CR1寄存器的位CMS和位DIR。
下面通过一张图给大家展示定时器工作在不同计数模式下,更新事件发生的情况。

图4.3.1.2 更新事件发生条件
上图中,纵轴表示计数器的计数值,横轴表示时间,ARR表示自动重载寄存器的值,小红点就是更新事件发生的时间点。举个例子,递增计数模式下,当计数值等于ARR时,计数器的值被复位为0,定时器溢出,并伴随着更新事件的发生,后面继续递增计数。递减计数模式和中心对齐模式请参考前面的描述。
上表的描述属于硬件更新事件发生条件,我们还可以通过UG位产生软件更新事件。
关于影子寄存器和定时器溢出时间计算公式等内容可以参考基本定时器的相关内容。
2,重复计数器寄存器(TIMx_RCR)是高级定时器特有的。
在G4系列中,高级定时器TIM1、TIM8和TIM20都有重复计数器。下面来介绍一下重复计数器有什么作用?在学习基本定时器和通用定时器的时候,我们知道定时器发生上溢或者下溢时,会直接生成更新事件。但是有重复计数器的定时器并不完全是这样的,定时器每次发生上溢或下溢时,重复计数器的值会减一,当重复计数器的值为0时,再发生一次上溢或者下溢才会生成定时器更新事件。如果我们设置重复计数器寄存器RCR的值为N,那么更新事件将在定时器发生N+1次上溢或下溢时发生。
这里需要注意的是重复计数器寄存器是具有影子寄存器的,所以RCR寄存器只是起缓冲的作用。RCR寄存器的值会在更新事件发生时,被转移至其影子寄存器中,从而真正生效。
重复计数器的特性,在控制生成PWM信号时很有用,后面会有相应的实验。
④ 输入捕获
输入捕获包括:4个输入捕获通道、输入滤波和边沿检测以及预分频器等部分。IO端口通过复用功能与这些通道相连。配置好IO端口的复用功能后,将需要测量的信号输入到相应的IO端口,输入捕获部分可以对输入的信号的上升沿,下降沿或者双边沿进行捕获,常见的测量有:测量输入信号的脉冲宽度、测量 PWM 输入信号的频率和占空比等。
下面简单说一下测量高电平脉冲宽度的工作原理,方便大家的理解:一般先要设置输入捕获的边沿检测极性,如:我们设置上升沿检测,那么当检测到上升沿时,定时器会把计数器CNT的值锁存到相应的捕获/比较寄存器TIMx_CCRy里,y=1~4。然后我们再设置边沿检测为下降沿检测,当检测到下降沿时,定时器会把计数器CNT的值再次锁存到相应的捕获/比较寄存器TIMx_CCRy里。最后,我们将前后两次锁存的CNT的值相减,就可以算出高电平脉冲期间内计数器的计数个数,再根据定时器的计数频率就可以计算出这个高电平脉冲的时间。如果要测量的高电平脉宽时间长度超过定时器的溢出时间周期,就会发生溢出,这时候我们还需要做定时器溢出的额外处理。低电平脉冲捕获同理。
⑤ 输入捕获和输出比较公用部分
该部分需要结合第④部分或者第⑥部分共同完成相应功能。
⑥ 输出比较
输出比较包括:4个输出比较通道、4个互补通道、死区发生器以及输出控制器,用于输出比较模式或PWM输出模式。
高级定时器输出比较部分和通用定时器相比,多了带死区控制的互补输出功能。图4.3.1.1第⑥部分的TIMx_CH1N、TIMx_CH2N、 TIMx_CH3N和TIMx_CH4N分别是定时器通道1、通道2、通道3和通道4的互补输出通道。 DTG是死区发生器,死区时间由DTG[7:0]位来配置。如果不使用互补通道和死区时间控制,那么高级定时器TIM1和TIM8和通用定时器的输出比较部分使用方法基本一样,只是要注意MOE位得置1定时器才能输出。
如果使用互补通道,那么就有一定的区别了,具体我们在高级定时器互补输出带死区控制实验小节再来介绍。
⑦ 断路功能
断路功能也称刹车功能,一般用于电机控制的刹车。G4系列有2个断路通道,断路源可以是刹车输入引脚(TIMx_BKIN),也可以是一个时钟失败事件。时钟失败事件由复位时钟控制器中的时钟安全系统产生。 系统复位后,断路功能默认被禁止,M OE位为低。
使能断路功能的方法:将TIMx_BDTR的位BKE置1。断路输入引脚TIMx_BKIN的输入有效电平可通过TIMx_BDTR寄存器的位BKP设置。
使能刹车功能后:由TIMx_BDTR的MOE、OSSI、OSSR位,TIMx_CR2的OISx、OISxN位,TIMx_CCER的CCxE、CCxNE位控制OCx和OCxN输出状态。无论何时,OCx和OCxN输出都不能同时处在有效电平。
当发生断路输入后,会怎么样?
1,MOE位被异步地清零,OCx和OCxN为无效、空闲或复位状态(由OSSI位选择)。
2,OCx和OCxN的状态:由相关控制位状态决定,当使用互补输出时:根据情况自动控制输出电平,参考《STM32G4xx参考手册_V7(英文版).pdf》手册第1218页的表268 具有断路功能的互补通道Ocx和OcxN的控制位。
3,BIF位置1,如果使能了BIE位,还会产生刹车中断;如果使能了TDE位,会产生DMA请求。
4,如果AOE位置1,在下一个更新事件UEV时,MOE位被自动置1。
高级定时器框图部分就简单介绍到这里,下面通过实际的实验来学习高级定时器。
4 .3.2 高级定时器中断实验
本小节我们来学习使用高级定时器中断,以定时器8中断为例,首先来了解相关的寄存器。
4.3.2.1 TIM1/TIM8/TIM20寄存器
下面介绍TIM1/TIM8/TIM20这些高级定时器中使用到的几个重要的寄存器,其他更多关于定时器的资料可以参考《STM32G4xx参考手册_V7(英文版).pdf》的第28章。
l 控制寄存器 1(TIMx_CR1)
TIM1/TIM8/TIM20的控制寄存器1描述如图4.3.2.1.1所示:

图4.3.2.1.1 TIMx_CR1寄存器
上图中,我们只列出了本章需要用到的一些位,其中:位7(APRE)用于控制自动重载寄存器的影子寄存器是否有效,在基本定时器的时候已经讲过,请回顾。
DIR位,用于控制定时器的计数方向,我们需要向上计数模式,所以设置DIR=0。
CEN位,用于使能计数器的工作,必须要设置该位为1,才可以开始计数。
l 从模式控制寄存器(TIMx_SMCR)
TIM1/TIM8/TIM20的从模式控制寄存器描述如图4.3.2.1.2所示:

图4.3.2.1.2 TIMx_SMCR寄存器
该寄存器需要结合bit16的SMS[3]以及SMS[2:0]位,用于从模式选择,其实就是选择计数器输入时钟的来源。比如高级定时器中断实验我们设置SMS[3:0]=0000,禁止从模式,这样PSC预分频器的时钟就直接来自内部时钟(CK_INT),按照我们例程sys_stm32_clock_init函数的配置,频率为170Mhz。这里我们多数情况是默认状态即选择来自于内部时钟。
l TIMx DMA/中断使能寄存器(TIMx_DIER)
TIM1/TIM8/TIM20的DMA/中断使能寄存器描述如图4.3.2.1.3所示:

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

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

图4.3.2.1.5 TIMx_CNT寄存器
TIM1/TIM8/TIM20/TIM20的计数寄存器是16位的,计数模式可以是递增计数模式、递减计数模式和中心对齐计数模式。其他定时器和基本定时器一样,可以直接写该寄存器设置计数的初始值,也可以读取该寄存器获取计数器值。
l 预分频寄存器(TIMx_PSC)
TIM1/TIM8/TIM20的预分频寄存器描述如图4.3.2.1.6所示:

图4.3.2.1.6 TIMx_PSC寄存器
所有定时器的预分频寄存器都是16位的,即写入该寄存器的数值范围是0到65535,表示1到65536分频。比如我们要17000分频,就往该寄存器写入16999。
l 自动重载寄存器(TIMx_ARR)
TIM1/TIM8/TIM20的自动重载寄存器描述如图4.3.2.1.7所示:

图4.3.2.1.7 TIMx_ARR寄存器
在G4系列中,TIM2和TIM5的自动重载寄存器是32位的,其他定时器的自动重载寄存器是一个16位的,该寄存器可以由APRE位设置是否进行缓冲。计数器的值会和自动重载寄存器的影子寄存器进行比较,当两者相等,定时器就会溢出,从而发生更新事件,如果打开了更新中断,还会进入中断里面。
4.3.2.2 硬件设计
1. 例程功能
LED0用来指示程序正在运行,200ms翻转一次。LED1在定时器中断中翻转,500ms进入中断一次。
2. 硬件资源
1)LED灯
LED0 – PE0
LED1 – PE1
2)定时器1
3. 原理图
定时器属于STM32G474的内部资源,只需要软件设置好即可正常工作。我们通过LED1来指示STM32G474的定时器进入中断的频率,LED0则指示程序的运行状态。
4.3.2.3 程序设计
本实验的相关HAL库驱动以及实验配置步骤请参考基本定时器相关内容,基本一样。不同点是基本定时器只能是递增计数模式,高级定时器可以递增计数模式、递减计数模式和中心对齐模式。
4.3.2.3.1程序流程图
下面看看本实验的程序流程图,main函数中并没有对LED1的操作,我们把对LED1的操作放到定时器的中断中进行处理:

图4.3.2.3.1.1 高级定时器中断实验程序流程图
4.3.2.3.2 程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。高级定时器驱动源码包括两个文件:atim.c和atim.h。
首先看atim.h头文件的几个宏定义:
/* 高级定时器定义 */
#define ATIM_TIMX_INT TIM1
#define ATIM_TIMX_INT_IRQn TIM1_UP_TIM16_IRQn
#define ATIM_TIMX_INT_IRQHandler TIM1_UP_TIM16_IRQHandler
#define ATIM_TIMX_INT_CLK_ENABLE() do{ __HAL_RCC_TIM1_CLK_ENABLE(); }while(0) /* TIM1 时钟使能 */
通过修改这4个宏定义,可以支持TIM1~TIM8任意一个定时器。
下面再来看一下atim.c文件的代码,主要包括两个函数,先来看看高级定时器的初始化函数,其定义如下:
/**
* @brief 高级定时器TIMX定时中断初始化函数
* @note
* 高级定时器的时钟来自APB2, 而PCLK2 = 170Mhz, 我们设置PPRE2不分频, 因此
* 高级定时器时钟 = 170Mhz
* 定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.
* Ft=定时器工作频率,单位:Mhz
*
* @param arr: 自动重装值
* @param psc: 时钟预分频数
* @retval 无
*/
void atim_timx_int_init(uint16_t arr, uint16_t psc)
{
ATIM_TIMX_INT_CLK_ENABLE(); /* 使能TIMx时钟 */
g_timx_handle.Instance = ATIM_TIMX_INT; /* 通用定时器x */
g_timx_handle.Init.Prescaler = psc; /* 预分频系数 */
g_timx_handle.Init.CounterMode = TIM_COUNTERMODE_UP; /* 递增计数模式 */
g_timx_handle.Init.Period = arr; /* 自动装载值 */
HAL_TIM_Base_Init(&g_timx_handle);
HAL_NVIC_SetPriority(ATIM_TIMX_INT_IRQn, 1, 3);
HAL_NVIC_EnableIRQ(ATIM_TIMX_INT_IRQn); /* 开启ITMx中断 */
/* 使能定时器x和定时器x更新中断 */
HAL_TIM_Base_Start_IT(&g_timx_handle);
}
这里配置的参数和基本定时器中断实验的是一样的,只是这里没有使用到HAL库的HAL_TIM_Base_MspInit函数来存放NVIC和使能时钟的代码,而是全部存放到atim_timx_int_init函数里。在一个项目中,用到多个定时器时,建议大家使用这种方式来处理代码,这样方便代码的管理。
下面再来看看定时器中断服务函数,其定义如下:
/**
* @brief 定时器中断服务函数
* @param 无
* @retval 无
*/
void ATIM_TIMX_INT_IRQHandler(void)
{
/* 以下代码没有使用定时器HAL库共用处理函数来处理,而是直接通过判断中断标志位的方式 */
if(__HAL_TIM_GET_FLAG(&g_timx_handle, TIM_FLAG_UPDATE) != RESET)
{
LED1_TOGGLE();
/* 清除定时器溢出中断标志位 */
__HAL_TIM_CLEAR_IT(&g_timx_handle, TIM_IT_UPDATE);
}
}
可以看到,这里我们没有使用HAL库的定时器公共处理函数来处理中断部分的代码,而是通过自行判断中断标志位的方式来处理。只不过获取标志位的方式还是使用HAL库的函数宏__HAL_TIM_GET_FLAG(),大家也可以直接使用寄存器的方式来操作。
通过__HAL_TIM_GET_FLAG()获取中断标志位并判断是否了中断,然后处理中断程序,最后通过__HAL_TIM_CLEAR_IT()将中断标志位清零,这样就完成了一次对中断的处理。这样的方式来处理中断,也是大家学习HAL库需要掌握的。在一个项目中,用到多个定时器相关中断时,建议大家使用这种方式来处理代码,这样方便代码的管理 。
4.3.2.4 下载验证
下载代码后,可以看到LED0不停闪烁(每400ms一个周期),而LED1也是不停的闪烁,但是闪烁时间较LED0慢(每1s一个周期)。
4.3.3 高级定时器PWM输出实验
本小节我们来学习使用高级定时器的PWM输出模式。
脉冲宽度调制(PWM),是英文“Pulse Width Modulation”的缩写,简称脉宽调制,是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术。我们可以让定时器产生PWM,在计数器频率固定时,PWM频率或者周期由自动重载寄存器(TIMx_ARR)的值决定,其占空比由捕获/比较寄存器(TIMx_CCRx)的值决定。PWM产生原理示意图如下图所示:

图4.3.3.1 PWM原理示意图
上图中,定时器工作在递增计数模式,纵轴是计数器的计数值CNT,横轴表示时。当CNT
定时器产生PWM的方式有许多种,下面我们以边沿对齐模式(即递增计数模式/递减计数模式)为例,PWM模式1或者PWM模式2产生PWM的示意图,如下图所示:

图4.3.3.2生成PWM图
STM32G474的定时器除了TIM6和TIM7,其他的定时器都可以用来产生PWM输出。其中高级定时器TIM1、TIM8和TIM20可以同时产生多达8路的PWM输出。而通用定时器也能同时产生多达4路的PWM输出!本实验我们以使用TIM1的CH1产生一路PWM输出为例进行学习。
4.3.3.1 TIM1/TIM8/TIM20寄存器
要使用STM32G474的高级定时器输出PWM,除了需要上一小节介绍的寄存器外,我们还会用到另外5个寄存器,包括:捕获/比较模式寄存器(TIMx_CCMR1/2)、捕获/比较使能寄存器(TIMx_CCER)、捕获/比较寄存器(TIMx_CCR1~4)、重复计数器寄存器(TIMx_RCR)、断路和死区寄存器(TIMx_BDTR)。上一小节介绍过的寄存器,这里就不再赘述,大家有需要的话可以回顾高级定时器中断章节,接下来我们介绍一下另外的5个寄存器。
l 捕获/比较模式寄存器1/2(TIMx_CCMR1/2)
TIM1/TIM8/TIM20的捕获/比较模式寄存器(TIMx_CCMR1/2),该寄存器一般有2个:TIMx _CCMR1和TIMx _CCMR2。TIMx_CCMR1控制CH1和CH2,而TIMx_CCMR2控制CH3和CH4。TIMx_CCMR1寄存器描述如图4.3.3.1.1所示:

图4.3.3.1.1 TIMx_CCMR1寄存器
该寄存器的有些位在不同模式下,功能不一样,我们前面已经说过。比如我们要让TIM1的CH1输出PWM波为例,该寄存器的模式设置位OC1M[2:0]就是对应着通道1的模式设置,此部分由3位组成,总共可以配置成8种模式,我们使用的是PWM模式,所以这3位必须设置为110或者111,分别对应PWM模式1和PWM模式2。这两种PWM模式的区别就是输出有效电平的极性相反,这里我们设置为PWM模式2。位3 OC1PE是输出比较通道1的预装使能,该位需要置1,另外CC1S[1:0]用于设置通道1的方向(输入/输出)默认设置为0,就是设置通道作为输出使用。
l 捕获/比较使能寄存器(TIMx_CCER)
TIM1/TIM8/TIM20的捕获/比较使能寄存器,该寄存器控制着各个输入输出通道的开关。TIMx_CCER寄存器描述如图4.3.3.1.2所示:

图4.3.3.1.2 TIMx_CCER寄存器
该寄存器比较简单,要让TIM1的CH1输出PWM波,这里我们要使能CC1E位,该位是通道1输入/输出使能位,要想PWM从IO口输出,这个位必须设置为1。CC1P位是设置通道1的输出极性,我们设置0,即OC1高电平有效。
l 捕获/比较寄存器1/2/3/4(TIMx_CCR1/2/3/4)
捕获/比较寄存器(TIMx_CCR1/2/3/4),该寄存器总共有4个,对应4个通道CH1~CH4。我们使用的是通道1,所以来看看TIMx_CCR1寄存器描述如图4.3.3.1.3所示:

图4.3.3.1.3 TIMx_CCR1寄存器
在输出模式下,捕获/比较寄存器影子寄存器的值与CNT的值比较,根据比较结果产生相应动作,利用这点,我们通过修改这个寄存器的值,就可以控制PWM的占空比了。
l 重复计数器寄存器(TIMx_RCR)
重复计数器寄存器仅高级定时器拥有,此寄存器用于设置重复计数器值,因为它具有影子寄存器,所以它本身只是起缓冲作用。当更新事件发生时,该寄存器的值会转移到其影子寄存器中,从而真正起作用。TIMx_ RCR寄存器描述如图4.3.3.1.4所示:

图4.3.3.1.4 TIMx_ RCR寄存器
该寄存器的REP[15:0]的取值范围:0~65535。因为这个寄存器只是起缓冲作用,如果大家对该寄存器写入值后,想要立即生效,可以通过对UG位写1,产生软件更新事件。
注意:如果是通用定时器,则没有重复计数器寄存器。
l TIM1/TIM8/TIM20断路和死区寄存器(TIMx_BDTR)
高级定时器TIM1/TIM8/TIM20的通道用作输出时,还必须配置断路和死区寄存器(TIMx_BDTR)的位MOE,该寄存器各位描述如图4.3.3.1.5所示:

图4.3.3.1.5 TIMx_BDTR寄存器
该寄存器,我们只需要关注位15(MOE),要想高级定时器的PWM正常输出,则必须设置MOE位为1,否则不会有输出。注意:如果是不支持互补输出的定时器,则没有断路和死区寄存器,比如TIM2~TIM5。
4.3.3.2 硬件设计
1. 例程功能
通过TIM1_CH1(由PA8复用)输出PWM,从而实现输出脉冲到该IO上。然后使用示波器查看PA8输出的PWM波形,LED0闪烁提示系统正在运行。
2. 硬件资源
1)LED灯:
LED0 – PE0
2)定时器1,使用TIM1通道1,由PA8复用。
3)示波器,使用示波器连接开发板的PA8和GND引脚。
3. 原理图
定时器属于STM32G474的内部资源,只需要软件设置好即可正常工作。我们通过示波器查看PA8输出波形情况,LED0指示程序是否正常运行。
4.3.3.3 程序设计
4.3.3.3.1 定时器的HAL库驱动
定时器在HAL库中的驱动代码在前面介绍基本定时器已经介绍了部分,这里我们再介绍几个本实验用到的函数。
1. HAL_TIM_PWM_Init函数
定时器的PWM输出模式初始化函数,其声明如下:
HAL_StatusTypeDef HAL_TIM_PWM_Init(TIM_HandleTypeDef *htim);
l 函数描述:
用于初始化定时器的PWM输出模式。
l 函数形参:
形参1是TIM_HandleTypeDef结构体类型指针变量,基本定时器的时候已经介绍。
l 函数返回值:
HAL_StatusTypeDef枚举类型的值。
l 注意事项:
该函数实现的功能以及使用方法和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);
l 函数描述:
该函数用于设置定时器的PWM通道。
l 函数形参:
形参1是TIM_HandleTypeDef结构体类型指针变量,用于配置定时器基本参数。
形参2是TIM_OC_InitTypeDef结构体类型指针变量,用于配置定时器的输出比较参数。
重点了解一下TIM_OC_InitTypeDef结构体指针类型,其定义如下:
typedef struct
{
uint32_t OCMode; /* 输出比较模式选择,寄存器的时候说过了,共8种模式 */
uint32_t Pulse; /* 设置比较值 */
uint32_t OCPolarity; /* 设置输出比较极性 */
uint32_t OCNPolarity; /* 设置互补输出比较极性 */
uint32_t OCFastMode; /* 使能或失能输出比较快速模式 */
uint32_t OCIdleState; /* 选择空闲状态下的非工作状态(OC1 输出) */
uint32_t OCNIdleState; /* 设置空闲状态下的非工作状态(OC1N 输出) */
} TIM_OC_InitTypeDef;
我们重点关注前三个结构体成员。成员变量OCMode用来设置模式,这里我们设置为PWM模式2。成员变量Pulse用来设置捕获比较值。成员变量TIM_OCPolarity用来设置输出极性。其他成员TIM_OutputNState,TIM_OCNPolarity,TIM_OCIdleState和TIM_OCNIdleState后面用到再介绍。
形参3是定时器通道,范围:TIM_CHANNEL_1到TIM_CHANNEL_4。这里我们使用的是定时器1的通道1,所以取值为TIM_CHANNEL_1即可。
l 函数返回值:
HAL_StatusTypeDef枚举类型的值。
3. HAL_TIM_PWM_Start函数
定时器的PWM输出启动函数,其声明如下:
HAL_StatusTypeDef HAL_TIM_PWM_Start(TIM_HandleTypeDef *htim,uint32_t Channel);
l 函数描述:
用于使能通道输出和启动计数器,即启动PWM输出。
l 函数形参:
形参1是TIM_HandleTypeDef结构体类型指针变量。
形参2是定时器通道,范围:TIM_CHANNEL_1到TIM_CHANNEL_4。
l 函数返回值:
HAL_StatusTypeDef枚举类型的值。
l 注意事项:
对于单独使能定时器的方法,在上一章定时器实验我们已经讲解。实际上,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);
l 函数描述:
用于配置定时器时钟源。
l 函数形参:
形参1是TIM_HandleTypeDef结构体类型指针变量。
形参2是TIM_ClockConfigTypeDef结构体类型指针变量,用于配置定时器时钟源参数。
TIM_ClockConfigTypeDef定义如下:
typedef struct
{
uint32_t ClockSource; /* 时钟源 */
uint32_t ClockPolarity; /* 时钟极性 */
uint32_t ClockPrescaler; /* 定时器预分频器 */
uint32_t ClockFilter; /* 时钟过滤器 */
} TIM_ClockConfigTypeDef;
l 函数返回值:
HAL_StatusTypeDef枚举类型的值。
l 注意事项:
该函数主要配置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);
后面的定时器初始化凡是用到内部时钟我们都没有去初始化,系统默认即可。
定时器PWM输出模式配置步骤
1)开启TIMx和通道输出的GPIO时钟,配置该IO口的复用功能输出
首先开启TIMx的时钟,然后配置GPIO为复用功能输出。本实验我们默认用到定时器8通道1,对应IO是PA8,它们的时钟开启方法如下:
__HAL_RCC_TIM1_CLK_ENABLE(); /* 使能定时器1 */ __HAL_RCC_GPIOA_CLK_ENABLE(); /* 开启GPIOA时钟 */
IO口复用功能是通过函数HAL_GPIO_Init来配置的。
2)初始化TIMx,设置TIMx的ARR和PSC等参数
使用定时器的PWM输出功能时,通过HAL_TIM_PWM_Init函数初始化定时器ARR和PSC等参数。
注意:该函数会调用:HAL_TIM_PWM_MspInit函数,我们可以通过后者存放定时器和GPIO时钟使能、GPIO初始化、中断使能以及优先级设置等代码。
3)设置TIMx_CHy的PWM模式,输出比较极性,比较值等参数
在HAL库中,通过HAL_TIM_PWM_ConfigChannel函数来设置定时器为PWM1模式或者PWM2模式,根据需求设置输出比较极性,设置比较值(控制占空比)等。
4)使能TIMx,使能TIMx的CHy输出
在HAL库中,通过调用HAL_TIM_PWM_Start函数来使能TIMx的某个通道输出PWM。
5)修改TIM2_CCR1来控制占空比
在经过以上设置之后,PWM其实已经开始输出了,只是其占空比和频率都是固定的,而我们通过修改比较值来控制PWM的输出占空比。HAL库中提供一个修改占空比的宏定义:
__HAL_TIM_SET_COMPARE (__HANDLE__, __CHANNEL__, __COMPARE__);
__HANDLE__是TIM_HandleTypeDef结构体类型指针变量,__CHANNEL__对应PWM的输出通道,__COMPARE__则是要写到捕获/比较寄存器(TIMx_CCR1/2/3/4)的值。实际上该宏定义最终还是往对应的捕获/比较寄存器写入比较值来控制PWM波的占空比,如下解析:
比如我们要修改定时器1通道1的输出比较值(控制占空比),寄存器操作方法:
TIM1->CCR1 = ledrpwmval; /* ledrpwmval是比较值,并且动态变化的, 所以我们要周期性调用这条语句,已达到及时修改PWM的占空比*/
__HAL_TIM_SET_COMPARE这个宏定义函数最终也是调用这个寄存器操作的,所以说我们使用HAL库的函数其实就是间接操作寄存器的。
4.3.3.3.2 程序流程图

图4.3.3.3.2.1 高级定时器PWM输出实验程序流程图
4.3.3.3.3 程序解析
这里我们这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。高级定时器驱动源码包括两个文件:atim.c和atim.h。
首先看atim.h头文件的几个宏定义:
/* TIMX PWM输出定义 */
#define ATIM_TIMX_PWM_CHY_GPIO_PORT GPIOA
#define ATIM_TIMX_PWM_CHY_GPIO_PIN GPIO_PIN_8
#define ATIM_TIMX_PWM_CHY_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0) /* PA口时钟使能 */
/* TIMX 引脚复用设置
* 因为PA8默认并不是TIM1的功能脚, 必须开启复用, PA8才能用作TIM1的PWM输出引脚.
*/
#define ATIM_TIMX_PWM_CHY_GPIO_AF GPIO_AF6_TIM1
#define ATIM_TIMX_PWM TIM1
#define ATIM_TIMX_PWM_CHY TIM_CHANNEL_1
#define ATIM_TIMX_PWM_CHY_CLK_ENABLE() do{ __HAL_RCC_TIM1_CLK_ENABLE(); }while(0) /* TIM1 时钟使能 */
可以把上面的宏定义分成两部分,第一部分是定时器1输出通道1对应的IO口的宏定义。第二部分则是定时器1输出通道1的相应宏定义。
下面看atim.c的程序,首先是高级定时器PWM输出初始化函数:
/**
* @brief 高级定时器TIMX 通道Y PWM输出 初始化函数(使用PWM模式1)
* @note
* 高级定时器的时钟来自APB2,而PCLK2 = 170Mhz,我们设置PPRE2不分频,因此
* 高级定时器时钟 = 170Mhz
* 定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.
* Ft=定时器工作频率,单位:Mhz
*
* @param arr: 自动重装值
* @param psc: 时钟预分频数
* @retval 无
*/
void atim_timx_pwm_chy_init(uint16_t arr, uint16_t psc)
{
g_timx_pwm_chy_handle.Instance = ATIM_TIMX_PWM; /* 定时器x */
g_timx_pwm_chy_handle.Init.Prescaler = psc; /* 定时器分频 */
g_timx_pwm_chy_handle.Init.CounterMode = TIM_COUNTERMODE_UP;/* 向上计数 */
g_timx_pwm_chy_handle.Init.Period = arr; /* 自动重装载值 */
g_timx_pwm_chy_handle.Init.RepetitionCounter = 0; /* 不重复计数*/
HAL_TIM_PWM_Init(&g_timx_pwm_chy_handle); /* 初始化PWM */
g_timx_oc_pwm_chy.OCMode = TIM_OCMODE_PWM2; /* 模式选择PWM2 */
/* 设置比较值,此值用来确定占空比 ,默认比较值为自动重装载值的一半,即占空比为50%*/
g_timx_oc_pwm_chy.Pulse = ( arr + 1 ) / 2;
g_timx_oc_pwm_chy.OCPolarity = TIM_OCPOLARITY_HIGH;/* 输出比较极性为高 */
HAL_TIM_PWM_ConfigChannel(&g_timx_pwm_chy_handle, & g_timx_oc_pwm_chy, ATIM_TIMX_PWM_CHY); /* 配置TIMx通道y */
HAL_TIM_PWM_Start(&g_timx_pwm_chy_handle,
ATIM_TIMX_PWM_CHY); /* 开启对应PWM通道 */
}
HAL_TIM_PWM_Init初始化TIM1并设置TIM1的ARR和PSC等参数,其次通过调用函数HAL_TIM_PWM_ConfigChannel设置TIM1_CH1的PWM模式以及比较值等参数,最后通过调用函数HAL_TIM_PWM_Start来使能TIM1以及使能PWM通道TIM1_CH1输出。
本实验我们使用PWM的MSP初始化回调函数HAL_TIM_PWM_MspInit来存放时钟、GPIO的初始化代码,其定义如下:
/**
* @brief 定时器底层驱动,时钟使能,引脚配置
此函数会被HAL_TIM_PWM_Init()调用
* @param htim:定时器句柄
* @retval 无
*/
void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim)
{
if (htim->Instance == ATIM_TIMX_PWM)
{
GPIO_InitTypeDef gpio_init_struct;
ATIM_TIMX_PWM_CHY_CLK_ENABLE(); /* 开启IO时钟 */
ATIM_TIMX_PWM_CHY_GPIO_CLK_ENABLE(); /* 开启定时器X时钟 */
gpio_init_struct.Pin = ATIM_TIMX_PWM_CHY_GPIO_PIN;/* 通道y的GPIO口 */
gpio_init_struct.Mode = GPIO_MODE_AF_PP; /* 复用推挽输出 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速 */
gpio_init_struct.Alternate = ATIM_TIMX_PWM_CHY_GPIO_AF; /* 端口复用 */
HAL_GPIO_Init(ATIM_TIMX_PWM_CHY_GPIO_PORT, &gpio_init_struct);
}
}
该函数首先判断定时器寄存器基地址,符合条件后,开启对应的GPIO时钟和定时器时钟,并且初始化GPIO。上面是使用HAL库标准的做法,我们亦可把HAL_TIM_PWM_MspInit函数里面的代码直接放到gtim_timx_pwm_chy_init函数里。这样做的好处是当一个项目中用到多个定时器时,代码的移植性、可读性好,方便管理。
在main.c里面编写如下代码:
int main(void)
{
uint8_t t = 0;
uint8_t dir = 1;
uint16_t ledrpwmval = 0;
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(85, 2, 2, 4, 8); /* 设置时钟,170Mhz */
delay_init(170); /* 延时初始化 */
led_init(); /* 初始化LED */
atim_timx_pwm_chy_init(500 - 1, 170 - 1); /* 2kHz脉冲输出 */
while (1)
{
if (dir) ledrpwmval++; /* dir==1 ledrpwmval递增 */
else ledrpwmval--; /* dir==0 ledrpwmval递减 */
if (ledrpwmval > 300)dir = 0; /* ledrpwmval到达300后,方向为递减 */
if (ledrpwmval == 0)dir = 1; /* ledrpwmval递减到0后,方向改为递增 */
/* 修改比较值控制占空比 */
__HAL_TIM_SET_COMPARE(&g_timx_pwm_chy_handle, ATIM_TIMX_PWM_CHY,
ledrpwmval);
t++;
if (t > 50)
{
t = 0;
LED0_TOGGLE(); /* 控制LED0闪烁, 提示程序运行状态 */
}
delay_ms(10);
}
}
本小节开头我们就说PWM波频率由自动重载寄存器(TIMx_ARR)的值决定,其占空比则由捕获/比较寄存器(TIMx_CCRx)的值决定。下面结合实际看看具体怎么计算:
定时器1的时钟为1倍APB2,即频率为170MHZ,而调用atim_timx_pwm_chy_init初始化函数之后,就相当于写入预分频寄存器的值为169,写入自动重载寄存器的值为499。由定时器溢出公式得:
Tout= ((arr+1)*(psc+1))/Tclk= ((499+1)*(169+1))/170000000=0.0005s
再由频率是周期的倒数关系得到PWM的频率为2000Hz。
占空比怎么计算的呢?结合图4.3.3.1,我们分两种情况分析,输出比较极性为低和输出比较极性为高,它们的情况正好相反。因为在main函数中的比较值是动态变化的,不利于我们计算占空比,我们假设比较值固定为200,在本实验中可以调用如下语句得到。
__HAL_TIM_SET_COMPARE(&g_timx_pwm_chy_handle, GTIM_TIMX_PWM_CHY, 200);
我们在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示波器进行验证,效果图如图4.3.3.3.3.1所示:

图4.3.3.3.3.1 验证效果图
这里把输出比较极性低和输出比较极性高的PWM波形都显示出来了。本实验默认设置PWM模式2、输出比较极性高,当CCR1寄存器的值设置为200时,对应的PWM波形如上图黄色的波形图。如果把输出比较极性设置为低,对应的波形图就是绿色的波形图了。
大家感兴趣也可以自行用示波器进行验证。
4.3.3.4下载验证
下载代码后,我们需要使用示波器连接开发板的PA8和GND引脚,在示波器上就可以看到频率为2kHz,占空比不断变化的PWM波形,效果图如图4.3.3.4.1所示:

图4.3.3.4.1 验证效果图
4.3.4 高级定时器互补输出带死区刹车控制实验
本小节我们来学习使用高级定时器的互补输出带死区控制功能。对于刚接触这个知识的朋友可能会问:什么是互补输出?还带死区控制?What?下面给大家简单说一下。

图4.3.4.1 互补输出
上图中,CH1输出黄色的PWM,它的互补通道CH1N输出绿色的PWM。通过对比,可以知道这两个PWM刚好是反过来的,CH1的PWM为高电平期间,CH1N的PWM则是低电平,反之亦然,这就是互补输出。
下面来看一下什么是带死区控制的互补输出:

图4.3.4.2 带死区控制的互补输出
上图中,CH1输出的PWM和CH1N输出的PWM在高低电平转换间,插入了一段时间才实现互补输出。这段时间称为死区时间,可以通过TIMx_BDTR寄存器的DTG[7:0]位配置控制死区时间的长度,后面会详细讲解如何配置死区时间。上图中,箭头指出的两段死区时间的长度是一样的,因为都是由同一个死区发生器产生。
理解了互补输出和带死区控制的互补输出,下面来看一下带死区控制的互补输出有什么用?带死区控制的互补输出经常被用于控制电机的H桥中,下面给大家画了一个H桥的简图:

图4.3.4.3 H桥简图
图4.3.4.3是H桥的简图,实际控制电机正反转的H桥会根据复杂些,而且更多的是使用MOS管,这里只是为了解释带死区控制的互补输出在H桥中的控制逻辑原理,大家理解原理就行。上图的H桥搭建全部使用的是NPN,并且导通逻辑都是基极为高电平时导通。如果Q1和Q4三极管导通,那么电机的电流方向是从左到右(假设电机正转);如果Q2和Q3三极管导通,那么电机的电流方向是从右到左(假设电机反转)。上述就是H桥控制电机正反转的逻辑原理。但是同一侧的三极管是不可以同时导通的,否则会短路,比如:Q1和Q2同时导通或者Q3和Q4同时导通,这都是不可取的。
下面大家想一下图4.3.4.1的OC1(CH1)和OC1N(CH1N)输出的PWM输入到图4.3.4.3的H桥中,会怎样?按理来说应该是OC1N输出高电平的时候,OC1输出就是低电平,刚好Q2和Q3导通,电机的电流方向是从右到左(假设电机反转);反之,OC1输出高电平的时候,OC1N输出就是低电平,刚好Q1和Q4导通,电机的电流方向是从左到右(假设电机正转),这似乎已经完美解决电机正反转问题了。实际上,元器件是有延迟特性的,比如:控制信号从OC1传导至电机,是要经过一定的时间的,复杂的H桥电路更是如此。由于元器件特性,就会导致直接使用互补输出信号驱动H桥时存在短路现象。为了避免这种情况,于是就有了带死区控制的互补输出来驱动H桥电路。如图4.3.4.2的死区时间就是为了解决元器件延迟特性的。用户必须根据与输出相连接的器件及其特性来调整死区时间。
死区时间计算
下面来看一下定时器的死区时间是怎么计算并设置的?死区时间是由TIMx_CR1寄存器的CKD[1:0]位和TIMx_BDTR寄存器的DTGF[7:0]位来设置,如下图所示:

图4.3.4.4 CKD[1:0]和DTG[7:0]位
死区时间计算分三步走:
第一步:通过CKD[1:0]位确定tDTS。根据CKD[1:0]位的描述,可以得到下面的式子:

CKD[1:0]:CKD[1:0]位设置的值。
Tclk:定时器的时钟源频率(单位为MHz)。
假设定时器时钟源频率是170MHz,我们设置CKD[1:0]位的值为2,代入上面的式子可得:

通过上式可得tDTS约等于23.53ns,本实验例程中我们也是这样设置的。
第二步:根据DTG[7:5]选择计算公式。
第三步:代入选择的公式计算。
下面给大家举个例子,假设定时器时钟源频率是170MHz,我们设置CKD[1:0]位的值为2,DTG[7:0]位的值为250。从上面的例子知道CKD[1:0]位的值为2,得到的tDTS=23.53ns。下面来看一下DTG[7:0]位的值为250,应该选择DTG[7:0]位描述中哪条公式?250的二进制数为11111010,即DTG[7:5]为111,所以选择第四条公式:DT=(32+ DTG[4:0]) * t dtg,其中t dtg = 16 * tDTS。可以看到手册上的式子符号大小写乱乱的,这里大小写不敏感。由手册的公式可以得到DT = (32+ DTG[4:0]) * 16 * tDTS = (32+ 26) * 16 * 23.53ns =21,835.84ns = 21.8us,即死区时间为21.8us。死区时间计算方法就给大家介绍到这里。
关于互补输出和死区插入的更多内容请看《STM32G4xx参考手册_V7(英文版).pdf》的28.3.17小节,下面我们介绍相关的寄存器。
4.3.4.1 TIM1/TIM8/TIM20寄存器
高级定时器互补输出带死区控制除了用到定时器的时基单元:计数器寄存器(TIMx_CNT)、预分频器寄存器(TIMx_PSC)、自动重载寄存器(TIMx_ARR)之外。主要还用到以下这些寄存器:
l 控制寄存器1(TIMx_CR1)
TIM1/TIM8/TIM20的控制寄存器1描述如图4.3.4.1.1所示:

图4.3.4.1.1 TIMx_CR1寄存器
上图中我们只列出了本实验需要用的一些位,其中:位7(APRE)用于控制自动重载寄存器是否进行缓冲,在基本定时器的时候已经讲过,请回顾。本实验中,我们把该位置1。
CKD[1:0]位指示定时器时钟(CK_INT)频率与死区发生器以及数字滤波器(ETR、TIx)所使用的死区及采样时钟(tDTS)之间的分频比。我们设置CKD[1:0]位为10,结合高级定时器时钟源频率等于APB2总线时钟频率,即170MHz,可以得到tDTS=23.53ns。
CEN位,用于使能计数器的工作,必须要设置该位为1,才可以开始计数。
l 捕获/比较模式寄存器1/2(TIMx_CCMR1/2)
TIM1/TIM8/TIM20的捕获/比较模式寄存器(TIMx_CCMR1/2),该寄存器一般有2个:TIMx_CCMR1和TIMx _CCMR2。TIMx_CCMR1控制CH1和CH2,而TIMx_CCMR2控制CH3和CH4。TIMx_CCMR1寄存器描述如图4.3.4.1.2所示:

图4.3.4.1.2 TIMx_CCMR1寄存器
该寄存器的有些位在不同模式下,功能不一样,我们现在用到输出比较模式。关于该寄存器的详细说明,请参考《STM32G4xx参考手册_V7(英文版).pdf》第1183页。
本实验我们用到了定时器1输出比较的通道1,所以我们需要配置TIM1_CCMR1模式设置位OC1M[2:0],我们使用的是PWM模式1,所以这3位必须设置为110。
l 捕获/比较使能寄存器(TIMx_CCER)
TIM1/TIM8/TIM20的捕获/比较使能寄存器,该寄存器控制着各个输入输出通道的开关和极性。TIMx_CCER寄存器描述如图4.3.4.1.3所示:

图4.3.4.1.3 TIMx_CCER寄存器
该寄存器比较简单,要让TIM1的通道1输出,我们需要把对应的捕获/比较1输出使能位CC1E置1。因为本实验中,我们需要实现互补输出,所以还需要把CC1NE位置1,使能互补通道输出。CC1P和CC1NP分别是通道1输出和通道1互补输出的极性设置位。这里我们把CC1P和CC1NP位都置1,即输出极性为低,就可以得到互补的PWM。
l 捕获/比较寄存器1/2/3/4(TIMx_CCR1/2/3/4)
捕获/比较寄存器(TIMx_CCR1/2/3/4),该寄存器总共有4个,对应4个通道CH1~CH4。我们使用的是通道1,所以来看看TIMx_CCR1寄存器描述如图4.3.4.1.4所示:

图4.3.4.1.4 TIMx_CCR1寄存器
该寄存器20位有效位,本实验中可以通过改变该寄存器的值来改变PWM的占空比。
l 断路和死区寄存器(TIMx_BDTR)
TIM1/TIM8/TIM20断路和死区寄存器,该寄存器各位描述如图4.3.4.1.5所示:

图4.3.4.1.5 TIMx_BDTR寄存器
该寄存器控制定时器的断路和死区控制的功能。我们先看断路控制,用到断路输入功能(断路输入引脚为PB12),位BKE置1即可。
位BKP选择断路输入信号有效电平。本实验中,我们选择低电平有效,即BKP为0。
位AOE是自动输出使能位,如果使能AOE位,那么在我们输入刹车信号后再断开了刹车信号,互补的PWM会自动恢复输出,如果失能AOE位,那么在输入刹车信号后再断开了刹车信号,互补的PWM就不会恢复输出,而是一直保持刹车信号输入时的状态。为了方便观察,我们使能该位,即置1。
位MOE是使能主输出,想要高级定时器的通道正常输出,则必须设置MOE位为1。
最后是DTG[7:0]位,用于设置死区时间,前面已经教过大家怎么设置了。这里以我们例程的设置为例,CKD[1:0] 设置为10,定时器时钟源频率是170MHz,所以tDTS = 23.53ns。
本例程的DTG[7:0]位的值设置为十进制100,即二进制数0110 0100。DTG[7:5]=011,符合第一条式子:DT=DTG[7:0] * t dtg,其中 t dtg = tDTS。DT是死区时间,可以得到DT = 100*23.53 ns = 2.35us。到后面下载验证小节,我们通过示波器验证一下这个死区时间计算的理论值和实际值是否一样。
4.3.4.2 硬件设计
1. 例程功能
1、利用TIM1_CH1(PA8)输出70%占空比的PWM波,它的互补输出通道(PB13)则是输出30%占空比的PWM波。
2、刹车功能,当给刹车输入引脚(PB12)输入低电平时,进行刹车,即PA8和PB13停止输出PWM波。
3、LED0闪烁指示程序运行。
2. 硬件资源
1)LED灯
LED0 – PE0
2)定时器1
TIM1正常输出通道 PA8
TIM1互补输出通道 PB13
TIM1刹车输入 PB12
3. 原理图
定时器属于STM32G474的内部资源,只需要软件设置好即可正常工作。通过示波器可以观察PA8和PB13引脚PWM输出的情况。还可以通过给PB12引脚接入低电平进行刹车。
4.3.4.3 程序设计
4.3.4.3.1 定时器的HAL库驱动
定时器在HAL库中的驱动代码在前面已经介绍了部分,这里我们再介绍几个本实验用到的函数。
1. HAL_TIMEx_ConfigBreakDeadTime函数
定时器的断路和死区时间配置初始化函数,其声明如下:
HAL_StatusTypeDef HAL_TIMEx_ConfigBreakDeadTime(TIM_HandleTypeDef *htim, TIM_BreakDeadTimeConfigTypeDef *sBreakDeadTimeConfig);
l 函数描述:
用于初始化定时器的断路(即刹车)和死区时间。
l 函数形参:
形参1是TIM_HandleTypeDef结构体类型指针变量,基本定时器的时候已经介绍。
形参2是TIM_BreakDeadTimeConfigTypeDef结构体类型指针变量,用于配置断路和死区参数,其定义如下:
typedef struct
{
uint32_t OffStateRunMode; /* 运行模式下的关闭状态选择 */
uint32_t OffStateIDLEMode; /* 空闲模式下的关闭状态选择 */
uint32_t LockLevel; /* 寄存器锁定配置 */
uint32_t DeadTime; /* 死区时间设置 */
uint32_t BreakState; /* 断路(即刹车)输入使能控制 */
uint32_t BreakPolarity; /* 断路输入极性 */
uint32_t BreakFilter; /* 断路输入滤波器 */
uint32_t AutomaticOutput; /* 自动恢复输出使能控制 */
} TIM_BreakDeadTimeConfigTypeDef;
l 函数返回值:
HAL_StatusTypeDef枚举类型的值。
2. HAL_TIMEx_PWMN_Start函数
定时器的互补输出启动函数。其声明如下:
HAL_StatusTypeDef HAL_TIMEx_PWMN_Start(TIM_HandleTypeDef *htim, uint32_t Channel);
l 函数描述:
该函数用于启动定时器的互补输出。
l 函数形参:
形参1是TIM_HandleTypeDef结构体类型指针变量,用于配置定时器基本参数。
形参2是定时器通道,范围:TIM_CHANNEL_1到TIM_CHANNEL_4。
l 函数返回值:
HAL_StatusTypeDef枚举类型的值。
定时器互补输出带死区控制配置步骤
1)开启TIMx和通道输出以及刹车输入的GPIO时钟,配置该IO口的复用功能输出
首先开启TIMx的时钟,然后配置GPIO为复用功能输出。本实验我们默认用到定时器1通道1,对应IO是PA8,互补输出通道引脚是PB13,刹车输入引脚是PB12,它们的时钟开启方法如下:
__HAL_RCC_TIM1_CLK_ENABLE(); /* 使能定时器1 */ __HAL_RCC_GPIOA_CLK_ENABLE(); /* 开启GPIOA时钟 */ __HAL_RCC_GPIOB_CLK_ENABLE(); /* 开启GPIOB时钟 */
IO口复用功能是通过函数HAL_GPIO_Init来配置的。
2)初始化TIMx,设置TIMx的ARR和PSC等参数
这里我们要使用定时器的PWM模式功能,所以调用的是HAL_TIM_PWM_Init函数来初始化定时器ARR和PSC等参数。注意:本实验要使用该函数配置TIMx_CR1寄存器的CKD[1:0]位,从而确定t DTS,方便后续设置死区时间。
注意:该函数会调用HAL_TIM_PWM_MspInit函数,但是为了方便移植代码,提高不同实验代码的独立性,我们就直接在atim_timx_cplm_pwm_init函数中,使能定时器时钟和GPIO时钟,初始化通道对应IO引脚等。
3)设置定时器为PWM模式,输出比较极性,互补输出极性等参数
通过HAL_TIM_PWM_ConfigChannel函数来设置定时器为PWM1模式,根据需求设置OCy输出极性和OCyN互补输出极性等。
4)设置死区参数。
通过HAL_TIMEx_ConfigBreakDeadTime函数来设置死区参数,比如:设置死区时间、运行模式的关闭输出状态、空闲模式的关闭输出状态、刹车输入有效信号极性和是否允许刹车后自动恢复输出等。
5)启动Ocy输出以及OCyN互补输出
通过HAL_TIM_PWM_Start函数启动OCy输出,通过HAL_TIMEx_PWMN_Start函数启动启动OCyN互补输出。
4.3.4.3.2 程序流程图

图4.3.4.3.2.1 高级定时器互补输出带死区控制实验
4.3.4.3.3 程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。高级定时器驱动源码包括两个文件:atim.c和atim.h。
首先看atim.h头文件的几个宏定义:
/* TIMX 互补输出定义 */
/* 主输出通道引脚 */
#define ATIM_TIMX_CPLM_CHY_GPIO_PORT GPIOA
#define ATIM_TIMX_CPLM_CHY_GPIO_PIN GPIO_PIN_8
#define ATIM_TIMX_CPLM_CHY_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0) /* PA口时钟使能 */
/* 互补输出通道引脚 */
#define ATIM_TIMX_CPLM_CHYN_GPIO_PORT GPIOB
#define ATIM_TIMX_CPLM_CHYN_GPIO_PIN GPIO_PIN_13
#define ATIM_TIMX_CPLM_CHYN_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0) /* PB口时钟使能 */
/* 刹车输入引脚 */
#define ATIM_TIMX_CPLM_BKIN_GPIO_PORT GPIOB
#define ATIM_TIMX_CPLM_BKIN_GPIO_PIN GPIO_PIN_12
#define ATIM_TIMX_CPLM_BKIN_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0) /* PB口时钟使能 */
/* TIMX 引脚复用设置
* 因为PA8、PB13、PB12默认并不是TIM8的功能脚,
* 必须开启复用, PA8、PB13、PB12才能用作TIM1的互补输出及刹车引脚。
*/
#define ATIM_TIMX_CPLM_CHY_GPIO_AF GPIO_AF6_TIM1
#define ATIM_TIMX_CPLM TIM1
#define ATIM_TIMX_CPLM_CHY TIM_CHANNEL_1
#define ATIM_TIMX_CPLM_CHY_CCRY ATIM_TIMX_CPLM->CCR1
#define ATIM_TIMX_CPLM_CLK_ENABLE() do{ __HAL_RCC_TIM1_CLK_ENABLE(); }while(0) /* TIM1 时钟使能 */
可以把上面的宏定义分成两部分,第一部分包括是定时器1输出、互补输出和刹车输入通道对应的IO口的宏定义,第二部分则是定时器1的相应宏定义。注意:因为PA8、PB13、PB12默认并不是TIM1的功能脚, 必须开启复用, PA8、PB13、PB12才能用作TIM1的互补输出及刹车引脚。
下面来看atim.c文件的程序,首先是高级定时器互补输出初始化函数,其定义如下:
/**
* @brief 高级定时器TIMX 互补输出 初始化函数(使用PWM模式1)
* @note
* 配置高级定时器TIMX 互补输出, 一路OCy 一路OCyN, 并且可以设置死区时间
*
* 高级定时器的时钟来自APB2, 而PCLK2=170Mhz,我们设置PPRE2不分频, 因此
* 高级定时器时钟 = 170Mhz
* 定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.
* Ft=定时器工作频率, 单位 : Mhz
*
* @param arr: 自动重装值。
* @param psc: 预分频系数
* @retval 无
*/
void atim_timx_cplm_pwm_init(uint16_t arr, uint16_t psc)
{
GPIO_InitTypeDef gpio_init_struct = {0};
TIM_OC_InitTypeDef tim_oc_cplm_pwm = {0};
ATIM_TIMX_CPLM_CLK_ENABLE(); /* 开启定时器X时钟 */
ATIM_TIMX_CPLM_CHY_GPIO_CLK_ENABLE(); /* 开启主输出通道的GPIO时钟 */
ATIM_TIMX_CPLM_CHYN_GPIO_CLK_ENABLE(); /* 开启互补通道的GPIO时钟 */
ATIM_TIMX_CPLM_BKIN_GPIO_CLK_ENABLE(); /* 开启刹车输入的GPIO时钟 */
gpio_init_struct.Pin = ATIM_TIMX_CPLM_CHY_GPIO_PIN; /* 主输出通道的IO */
gpio_init_struct.Mode = GPIO_MODE_AF_PP; /* 复用推挽输出 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速 */
gpio_init_struct.Alternate = ATIM_TIMX_CPLM_CHY_GPIO_AF; /* IO复用 */
HAL_GPIO_Init(ATIM_TIMX_CPLM_CHY_GPIO_PORT, &gpio_init_struct);
gpio_init_struct.Pin = ATIM_TIMX_CPLM_CHYN_GPIO_PIN; /* 互补通道的IO */
HAL_GPIO_Init(ATIM_TIMX_CPLM_CHYN_GPIO_PORT, &gpio_init_struct);
gpio_init_struct.Pin = ATIM_TIMX_CPLM_BKIN_GPIO_PIN; /* 刹车输入的IO */
HAL_GPIO_Init(ATIM_TIMX_CPLM_BKIN_GPIO_PORT, &gpio_init_struct);
/* 设置定时器 */
g_timx_cplm_pwm_handle.Instance = ATIM_TIMX_CPLM; /* 定时器x */
g_timx_cplm_pwm_handle.Init.Prescaler = psc; /* 预分频系数 */
g_timx_cplm_pwm_handle.Init.CounterMode = TIM_COUNTERMODE_UP; /* 递增计数*/
g_timx_cplm_pwm_handle.Init.Period = arr; /* 自动重装载值 */
/* CKD[1:0] = 10, tDTS = 4 * tCK_INT = Ft / 4 = 42Mhz */
g_timx_cplm_pwm_handle.Init.ClockDivision = TIM_CLOCKDIVISION_DIV4;
g_timx_cplm_pwm_handle.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; /* 使能影子寄存器TIMx_ARR */
HAL_TIM_PWM_Init(&g_timx_cplm_pwm_handle) ;
/* 设置PWM输出 */
tim_oc_cplm_pwm.OCMode = TIM_OCMODE_PWM1; /* PWM模式1 */
tim_oc_cplm_pwm.OCPolarity = TIM_OCPOLARITY_LOW; /* OCy 低电平有效 */
tim_oc_cplm_pwm.OCNPolarity = TIM_OCNPOLARITY_LOW; /* OCyN 低电平有效 */
HAL_TIM_PWM_ConfigChannel(&g_timx_cplm_pwm_handle, &tim_oc_cplm_pwm, ATIM_TIMX_CPLM_CHY);
/* 设置死区参数,开启死区中断 */
g_sbreak_dead_time_config.LockLevel = TIM_LOCKLEVEL_OFF;/* 不用寄存器锁功能*/
g_sbreak_dead_time_config.BreakState = TIM_BREAK_ENABLE;/* 使能刹车输入 */
/* 刹车输入有效信号极性为低 */
g_sbreak_dead_time_config.BreakPolarity = TIM_BREAKPOLARITY_LOW;
/* 使能AOE位,允许刹车结束后自动恢复输出 */
g_sbreak_dead_time_config.AutomaticOutput = TIM_AUTOMATICOUTPUT_ENABLE;
HAL_TIMEx_ConfigBreakDeadTime(&g_timx_cplm_pwm_handle, &g_sbreak_dead_time_config);
/* OCy 输出使能 */
HAL_TIM_PWM_Start(&g_timx_cplm_pwm_handle, ATIM_TIMX_CPLM_CHY);
/* OCyN 输出使能 */
HAL_TIMEx_PWMN_Start(&g_timx_cplm_pwm_handle, ATIM_TIMX_CPLM_CHY);
}
在atim_timx_cplm_pwm_init函数中,没有使用HAL库的MSP回调,而是把相关的初始化都写到该函数里面。
第一部分,使能定时器和相关通道对应的GPIO时钟,以及初始化相关IO引脚。
第二部分,通过HAL_TIM_PWM_Init函数初始化定时器的ARR和PSC等参数。
第三部分,通过HAL_TIM_PWM_ConfigChannel函数设置PWM模式1、输出极性等。
第四部分,通过HAL_TIMEx_ConfigBreakDeadTime函数配置断路功能。
最后一定记得要调用HAL_TIM_PWM_Start函数和HAL_TIMEx_PWMN_Start函数启动通道输出和互补通道输出。
为了方便,我们还定义了设置输出比较值和死区时间的函数,其定义如下:
/**
* @brief 定时器TIMX 设置输出比较值 & 死区时间
* @param ccr: 输出比较值
* @param dtg: 死区时间
* @arg dtg[7:5]=0xx时, 死区时间 = dtg[7:0] * tDTS
* @arg dtg[7:5]=10x时, 死区时间 = (64 + dtg[6:0]) * 2 * tDTS
* @arg dtg[7:5]=110时, 死区时间 = (32 + dtg[5:0]) * 8 * tDTS
* @arg dtg[7:5]=111时, 死区时间 = (32 + dtg[5:0]) * 16 * tDTS
* @note tDTS = 1 / (Ft / CKD[1:0]) = 1 / 42M = 23.8ns
* @retval 无
*/
void atim_timx_cplm_pwm_set(uint16_t ccr, uint8_t dtg)
{
g_sbreak_dead_time_config.DeadTime = dtg; /* 死区时间设置 */
HAL_TIMEx_ConfigBreakDeadTime(&g_timx_cplm_pwm_handle,
&g_sbreak_dead_time_config); /* 重设死区时间 */
__HAL_TIM_MOE_ENABLE(&g_timx_cplm_pwm_handle); /* MOE=1,使能主输出 */
ATIM_TIMX_CPLM_CHY_CCRY = ccr; /* 设置比较寄存器 */
}
通过重新调用HAL_TIMEx_ConfigBreakDeadTime函数设置死区时间,注意这里的g_sbreak_dead_time_config是全局结构体变量,在atim_timx_cplm_pwm_init函数已经初始化其他结构体成员了,这里只是对DeadTime成员(死区时间)配置。死区时间的计算方法前面已经讲解过,这里只要把要设置的DTG[7:0]值,通过dtg形参赋值给DeadTime结构体成员就行。另外一个形参是ccr,用于设置捕获/比较寄存器的值,即控制PWM的占空比。
在main.c里面编写如下代码:
int main(void)
{
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(85, 2, 2, 4, 8); /* 设置时钟,170Mhz */
delay_init(170); /* 延时初始化 */
led_init(); /* 初始化LED */
atim_timx_cplm_pwm_init(1000 - 1, 170 - 1);/* 1Mhz的计数频率 1Khz的周期 */
atim_timx_cplm_pwm_set(300, 100); /* 占空比:7:3死区时间100*tDTS */
while (1)
{
LED0_TOGGLE(); /* LED0(红灯) 翻转 */
delay_ms(200);
}
}
先看atim_timx_cplm_pwm_init(1000 - 1, 170 - 1)这个语句,这两个形参分别设置自动重载寄存器的值为999,以及定时器预分频器寄存器的值为169。先看预分频系数,我们设置为170分频,定时器1的时钟源频率等于APB2总线时钟频率,即170MHz,可以得到计数器的计数频率是1MHz,即每1us计数一次。再到自动重载寄存器的值为999决定的是PWM的频率(周期),可以得到PWM的周期为(999+1)*1us = 1000us = 1ms。边沿对齐模式下,使用PWM模式1或者PWM模式2,得到的PWM周期是定时器溢出时间。这里的1ms,也可以直接通过定时器溢出时间计算公式Tout= ((arr+1)*(psc+1))/Tclk得到。
调用atim_timx_cplm_pwm_set(300, 100) 这个语句,相当于设置捕获/比较寄存器的值为300,DTG[7:0]的值为100。通过计算可以得到PWM的占空比为70%,死区时间为2.35us。根据PWM生成原理分析,再结合图4.3.3.2产生PWM示意图,以及我们在atim_timx_cplm_pwm_init函数配置PWM模式1、OCy输出极性为低,占空比的计算很简单,可以由(1000-300)/1000得到。关于死区时间的计算方法,前面已经讲解过,这里以DTG[7:0]的值为100为例,再来讲解一遍计算过程。我们例程配置CKD[1:0]位的值为2,可以得到tDTS = 23.53ns。基于这个前提,通过改变DTG[7:0]的值,可以得到不同的死区时间。这里我们配置DT
G[7:0]的值为100,即二进制数0110 0100,符合第一种情况dtg[7:5]=0xx时,死区时间DT = DTG [7:0] * tDTS。可以得到死区时间DT = 100*23.53 ns = 2.35us。
下面我们下载到开发板验证一下。
4.3.4.4 下载验证
下载代码后,可以看到LED0在闪烁,说明程序已经正常在跑了。 我们需要借助DS100示波器观察PA8正常输出和PB13互补输出PWM的情况,示波器显示截图如图4.3.4.4.1所示:

图4.3.4.4.1 PA8正常输出和PB13互补输出PWM的情况
图4.3.4.4.1中的由上到下分别是PA8输出70%占空比的PWM波和PB13互补输出30%占空比的PWM波。互补输出的PWM波的正脉宽减去正常的PWM的负脉宽的值除以2就是死区时间,也可以是正常的PWM的正脉宽减去互补输出的PWM波的负脉宽的值除以2。我们使用第一种方法得到:死区时间 =(702 – 698)/2 us=2us。与我们理论到的的值2.35us基本一样,这样的误差是正常的。
要是不相信,我们再举个例子,我们把调用的函数改为atim_timx_cplm_pwm_set(300, 250),即配置DTG[7:0]的值为250,这个例子的计算过程在本实验前面死区时间计算的内容讲过,这里就不再赘述。经过计算得到死区时间DT = 21.8 us。修改好代码后,编译下载到板子,示波器显示截图如下图所示:

图4.3.4.4.2 修改程序后PA8正常输出和PB13互补输出PWM的情况
由图4.3.4.4.2可得到,死区时间 =(722 - 678)/2 us= 22us。与我们理论到的的值21.8us是差不多的,误差在正常范围。由此证明我们的死区时间设置是没有问题。
刹车功能验证:当给刹车输入引脚(PB12)接入低电平(这里直接用杜邦线连接PB12到GND)时,就会进行刹车,即PA8和PB13停止输出PWM波,如图4.3.4.4.3所示:

图4.3.4.4.3 刹车后的输出情况
因为我们使能了AOE位(即把该位置1),如果刹车输入为无效极性时,MOE位在发生下一个更新事件时自动置1,恢复运行模式(即继续输出PWM)。因此当停止给PB12接入高电平(拔掉之前连接的杜邦线),PWM会自动恢复输出。

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

图4.3.5.1.1 TIMx_CCMR1寄存器
该寄存器在输入模式和输出模式下,功能是不一样,所以需要看准不同模式的描述,请打开手册查看。TIMx_CCMR1寄存器对应于通道1和通道2的设置,CCMR2寄存器对应通道3和通道4。如:TIMx_CCMR1寄存器位[7:0]用于捕获/比较通道1的控制,而位[15:8]则用于捕获/比较通道2的控制。我们用到定时器1通道1输入,所以需要配置TIMx_CCMR1的位[7:0]。
其中CC1S[1:0],这两个位用于CCR1的通道配置,这里我们设置IC1S[1:0]=01,也就是配置IC1映射在TI1上。
输入捕获1预分频器IC1PSC[1:0],这个比较好理解。我们是1次边沿就触发1次捕获,所以选择00就行了。
输入捕获1滤波器IC1F[3:0],这个用来设置输入采样频率和数字滤波器长度。其中,fCK_INT是定时器时钟源频率,按照例程的配置为170Mhz,而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。
l 捕获/比较使能寄存器(TIMx_CCER)
TIM1/TIM8/TIM20的捕获/比较使能寄存器,该寄存器控制着各个输入输出通道的开关和极性。TIMx_CCER寄存器描述如图4.3.5.1.2所示:

图4.3.5.1.2 TIMx_CCER寄存器
我们要用到这个寄存器的最低2位,CC1E和CC1P位。要使能输入捕获,必须设置CC1E=1,而CC1P则根据自己的需要来配置。我们这里是保留默认设置值0,即高电平触发捕获。
l TIMx DMA/中断使能寄存器(TIMx_DIER)
TIM1/TIM8/TIM20/TIM20的DMA/中断使能寄存器描述如图4.3.5.1.3所示:

图4.3.5.1.3 TIMx_DIER寄存器
我们需要用到中断来处理捕获数据,所以必须开启通道1的捕获比较中断,即CC1IE设置为1。同时我们还需要在定时器溢出中断中累计定时器溢出的次数,所以还需要使能定时器的更新中断,即UIE置1。
控制寄存器:TIMx_CR1,我们只用到了它的最低位,也就是用来使能定时器的。
最后再来看看捕获/比较寄存器1:TIMx_CCR1,该寄存器用来存储捕获发生时,TIMx_CNT的值,我们从TIMx_CCR1就可以读出通道1捕获发生时刻的TIMx_CNT值,通过两次捕获(一次上升沿捕获,一次下降沿捕获)的差值,就可以计算出高电平脉冲的宽度(注意:对于脉宽太长的情况,还要计算定时器溢出的次数)。
4.3.5.2 硬件设计
1. 例程功能
1、本实验我们基于前面的高级定时器PWM输出实验来实现,使用TIM1_CH1(PA8)输出PWM波形,然后使用TIM8_CH1(PC6)来做输入捕获,捕获PA8上的高电平脉宽,并将脉宽时间通过串口打印出来,这里能测试的最长时间为:4194303 us。
2、LED0闪烁指示程序运行。
2. 硬件资源
1) LED灯:
LED0 – PE0
2) 定时器1,使用TIM1通道1,将PA8复用到TIM1_CH1。
3) 定时器8,使用TIM8通道1,将PC6复用到TIM8_CH1。
3. 原理图
定时器属于STM32G474的内部资源,只需要软件设置好即可正常工作。我们借助TIM1_CH1输出PWM波形作为输入脉冲源并通过串口上位机来监测定时器输入捕获的情况。
4.3.5.3 程序设计
4.3.5.3.1 定时器的HAL库驱动
定时器在HAL库中的驱动代码在前面已经介绍了部分,这里我们再介绍几个本实验用到的函数。
1. HAL_TIM_IC_Init函数
定时器的输入捕获模式初始化函数,其声明如下:
HAL_StatusTypeDef HAL_TIM_IC_Init(TIM_HandleTypeDef *htim);
l 函数描述:
用于初始化定时器的输入捕获模式。
l 函数形参:
形参1是TIM_HandleTypeDef结构体类型指针变量,基本定时器的时候已经介绍。
l 函数返回值:
HAL_StatusTypeDef枚举类型的值。
l 注意事项:
与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);
l 函数描述:
该函数用于设置定时器的输入捕获通道。
l 函数形参:
形参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;
该结构体成员我们现在介绍一下。成员变量ICPolarity用来设置输入信号的有效捕获极性,取值范围为:TIM_ICPOLARITY_RISING(上升沿捕获),TIM_ICPOLARITY_FALLING(下降沿捕获)和TIM_ICPOLARITY_BOTHEDGE(双边沿捕获)。成员变量ICSelection用来设置映射关系,我们配置IC1直接映射在TI1上,选择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_4。
l 函数返回值:
HAL_StatusTypeDef枚举类型的值。
3. HAL_TIM_IC_Start_IT函数
启动定时器输入捕获模式函数,其声明如下:
HAL_StatusTypeDef HAL_TIM_IC_Start_IT(TIM_HandleTypeDef *htim, uint32_t Channel);
l 函数描述:
用于启动定时器的输入捕获模式,且开启输入捕获中断。
l 函数形参:
形参1是TIM_HandleTypeDef结构体类型指针变量。
形参2是定时器通道,范围:TIM_CHANNEL_1到TIM_CHANNEL_4。
l 函数返回值:
HAL_StatusTypeDef枚举类型的值。
l 注意事项:
如果我们不需要开启输入捕获中断,只是开启输入捕获功能,HAL库函数为:
HAL_StatusTypeDef HAL_TIM_IC_Start(TIM_HandleTypeDef *htim, uint32_t Channel);
定时器输入捕获模式配置步骤
1)开启TIMx和输入通道的GPIO时钟,配置该IO口的复用功能输入
首先开启TIMx的时钟,然后配置GPIO为复用功能输入。本实验我们默认用到定时器8通道1,对应IO是PC6,它们的时钟开启方法如下:
__HAL_RCC_TIM8_CLK_ENABLE(); /* 使能定时器8 */ __HAL_RCC_GPIOC_CLK_ENABLE(); /* 开启GPIOC时钟 */
IO口复用功能是通过函数HAL_GPIO_Init来配置的。
2)初始化TIMx,设置TIMx的ARR和PSC等参数
使用定时器的输入捕获功能时,我们调用的是HAL_TIM_IC_Init函数来初始化定时器ARR和PSC等参数。
注意:该函数会调用HAL_TIM_IC_MspInit函数,我们可以通过后者存放定时器和GPIO时钟使能、GPIO初始化、中断使能以及优先级设置等代码。
3)设置TIMx_CHy的输入捕获模式,开启输入捕获
在HAL库中,定时器的输入捕获模式是通过HAL_TIM_IC_ConfigChannel函数来设置定时器某个通道为输入捕获通道,包括映射关系,输入滤波和输入分频等。
4)使能定时器更新中断,开启捕获功能以及捕获中断,配置定时器中断优先级
通过__HAL_TIM_ENABLE_IT函数使能定时器更新中断。
通过HAL_TIM_IC_Start_IT函数使能定时器并开启捕获功能以及捕获中断。
通过HAL_NVIC_EnableIRQ函数使能定时器中断。
通过HAL_NVIC_SetPriority函数设置中断优先级。
因为我们要捕获的是高电平信号的脉宽,所以,第一次捕获是上升沿,第二次捕获时下降沿,必须在捕获上升沿之后,设置捕获边沿为下降沿,同时,如果脉宽比较长,那么定时器就会溢出,对溢出必须做处理,否则结果就不准了。
5)编写中断服务函数
定时器8做输入捕获实验,需要用到两个中断服务函数:TIM8_UP_IRQHandler和TIM8_CC_IRQHandler,前者是更新中断服务函数,我们用来记录溢出次数,后者是捕获中断服务函数,用来处理捕获的事件。HAL库为了使用方便,提供了一个定时器中断通用处理函数HAL_TIM_IRQHandler,该函数会调用一些定时器相关的回调函数,用于给用户处理定时器中断到了之后,需要处理的程序。本实验我们除了用到更新(溢出)中断回调函数HAL_TIM_PeriodElapsedCallback之外,还要用到捕获中断回调函数HAL_TIM_IC_CaptureCallback。详见本实验例程源码。
4.3.5.3.2 程序流程图

图4.3.5.3.2.1 高级定时器输入捕获实验程序流程图
4.3.5.3.3 程序解析
本实验需要利用TIM1_CH1来输出PWM作为输入捕获的脉冲源,关于PWM输出的部分我们这里不再赘述,大家可以回顾4.3.3章节的内容。
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。高级定时器驱动源码包括两个文件:atim.c和atim.h。
首先看atim.h头文件的几个宏定义:
/********************** 高级定时器 PWM输出实验相关宏定义 ***********************/
/* TIMX PWM输出定义 */
#define ATIM_TIMX_PWM_CHY_GPIO_PORT GPIOA
#define ATIM_TIMX_PWM_CHY_GPIO_PIN GPIO_PIN_8
#define ATIM_TIMX_PWM_CHY_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0) /* PA口时钟使能 */
/* TIMX 引脚复用设置
* 因为PA8默认并不是TIM1的功能脚, 必须开启复用, PA8才能用作TIM1的PWM输出引脚
*/
#define ATIM_TIMX_PWM_CHY_GPIO_AF GPIO_AF6_TIM1
#define ATIM_TIMX_PWM TIM1
#define ATIM_TIMX_PWM_CHY TIM_CHANNEL_1
#define ATIM_TIMX_PWM_CHY_CLK_ENABLE() do{ __HAL_RCC_TIM1_CLK_ENABLE(); }while(0) /* TIM1 时钟使能 */
/******************* 第二部分 高级定时器 输入捕获实验相关宏定义 *******************/
/* TIMX 输入捕获定义
* 这里的输入捕获使用定时器TIM1_CH1
*/
#define ATIM_TIMX_CAP_CHY_GPIO_PORT GPIOC
#define ATIM_TIMX_CAP_CHY_GPIO_PIN GPIO_PIN_6
#define ATIM_TIMX_CAP_CHY_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0) /* PC口时钟使能 */
/* TIMX 引脚复用设置
* 因为PC6默认并不是TIM8的功能脚, 必须开启复用, PC6才能用作TIM8的输入捕获引脚
*/
#define ATIM_TIMX_CAP_CHY_GPIO_AF GPIO_AG4_TIM8
#define ATIM_TIMX_CAP TIM8
#define ATIM_TIMX_CAP_IRQn TIM8_CC_IRQn
#define ATIM_TIMX_CAP_IRQHandler TIM8_CC_IRQHandler
#define ATIM_TIMX_INT_IRQn TIM8_UP_IRQn
#define ATIM_TIMX_INT_IRQHandler TIM8_UP_IRQHandler
#define ATIM_TIMX_CAP_CHY TIM_CHANNEL_1
#define ATIM_TIMX_CAP_CHY_CLK_ENABLE() do{ __HAL_RCC_TIM8_CLK_ENABLE(); }while(0) /* TIM8 时钟使能 */
可以把上面的宏定义分成两部分,第一部分是定时器1输出通道1对应的IO口的宏定义,第二部分则是定时器8输入通道1的相应宏定义。
下面来看atim.c的程序,我们只看输入捕获的部分,首先是高级定时器输入捕获初始化函数。
/**
* @brief 高级定时器TIMX 通道Y 输入捕获 初始化函数
* @note
* 高级定时器的时钟来自APB2, 而PCLK2=170Mhz,我们设置PPRE2不分频, 因此
* 高级定时器时钟 = 170Mhz
* 定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.
* Ft=定时器工作频率,单位:Mhz
*
* @param arr: 自动重装值
* @param psc: 时钟预分频数
* @retval 无
*/
void atim_timx_cap_chy_init(uint32_t arr, uint16_t psc)
{
TIM_IC_InitTypeDef timx_ic_cap_chy = {0};
g_timx_cap_chy_handle.Instance = ATIM_TIMX_CAP; /* 定时器x */
g_timx_cap_chy_handle.Init.Prescaler = psc; /* 定时器分频 */
g_timx_cap_chy_handle.Init.CounterMode = TIM_COUNTERMODE_UP; /* 递增计数 */
g_timx_cap_chy_handle.Init.Period = arr; /* 自动重装载值 */
HAL_TIM_IC_Init(&g_timx_cap_chy_handle);
timx_ic_cap_chy.ICPolarity = TIM_ICPOLARITY_RISING; /* 上升沿捕获 */
timx_ic_cap_chy.ICSelection = TIM_ICSELECTION_DIRECTTI;/* 映射到TI1上 */
timx_ic_cap_chy.ICPrescaler = TIM_ICPSC_DIV1; /* 不分频 */
timx_ic_cap_chy.ICFilter = 0; /* 不滤波 */
HAL_TIM_IC_ConfigChannel(&g_timx_cap_chy_handle, &timx_ic_cap_chy, ATIM_TIMX_CAP_CHY); /* 配置TIM1通道1 */
/* 使能更新中断 */
__HAL_TIM_ENABLE_IT(&g_timx_cap_chy_handle, TIM_IT_UPDATE);
/* 开启捕获TIM1的通道1 */
HAL_TIM_IC_Start_IT(&g_timx_cap_chy_handle, ATIM_TIMX_CAP_CHY);
}
HAL_TIM_IC_Init初始化定时器的基础工作参数,如:ARR和PSC等,第二部分则是调用HAL_TIM_IC_ConfigChannel函数配置输入捕获通道映射关系,滤波和分频等。最后是使能更新中断和使能通道输入以及定时器捕获中断。通道对应的IO、时钟开启和NVIC的初始化都在HAL_TIM_IC_MspInit函数里编写,其定义如下:
/**
* @brief 高级定时器输入捕获初始化接口
HAL库调用的接口,用于配置不同的输入捕获
* @param htim:定时器句柄
* @note 此函数会被HAL_TIM_IC_Init()调用
* @retval 无
*/
void HAL_TIM_IC_MspInit(TIM_HandleTypeDef *htim)
{
if (htim->Instance == ATIM_TIMX_CAP) /* 输入通道捕获 */
{
GPIO_InitTypeDef gpio_init_struct;
ATIM_TIMX_CAP_CHY_CLK_ENABLE(); /* 使能TIMx时钟 */
ATIM_TIMX_CAP_CHY_GPIO_CLK_ENABLE(); /* 开启IO的时钟 */
gpio_init_struct.Pin = ATIM_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_HIGH; /* 高速 */
gpio_init_struct.Alternate = ATIM_TIMX_CAP_CHY_GPIO_AF; /* 端口复用 */
HAL_GPIO_Init(ATIM_TIMX_CAP_CHY_GPIO_PORT, &gpio_init_struct);
HAL_NVIC_SetPriority(ATIM_TIMX_INT_IRQn, 2, 3);
HAL_NVIC_EnableIRQ(ATIM_TIMX_INT_IRQn);
HAL_NVIC_SetPriority(ATIM_TIMX_CAP_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(ATIM_TIMX_CAP_IRQn);
}
}
该函数调用HAL_GPIO_Init函数初始化定时器输入通道对应的IO,并且开启GPIO时钟,使能定时器。其中要注意IO口复用功能的选择一定要选对了。最后配置中断抢占优先级和响应优先级,以及打开定时器中断。
通过上面的两个函数输入捕获的初始化就完成了,下面先来介绍两个变量:
/* 输入捕获状态(g_timxchy_cap_sta) * [7] :0,没有成功的捕获;1,成功捕获到一次. * [6] :0,还没捕获到高电平;1,已经捕获到高电平了. * [5:0]:捕获高电平后溢出的次数,最多溢出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 ; /* 输入捕获值 */
这两个变量用于辅助实现高电平捕获。其中g_timxchy_cap_sta,是用来记录捕获状态, (这个变量,我们把它当成一个寄存器那样来使用)。对其各位赋予状态含义,描述如下表所示:

表4.3.5.3.3.1 g_timxchy_cap_sta各位描述
变量g_timxchy_cap_sta的位[5:0]是用于记录捕获高电平定时器溢出次数,总共6位,所以最多可以记录溢出的次数为2的6次方减一次,即63次。
变量g_timxchy_cap_val,则用来记录捕获到下降沿的时候,TIM1_CNT寄存器的值。
下面开始看中断服务函数的逻辑程序,HAL_TIM_IRQHandler函数会调用下面两个回调函数,我们的逻辑代码就是放在回调函数里,函数定义如下:
/**
* @brief 定时器输入捕获中断处理回调函数
* @param htim:定时器句柄指针
* @note 该函数在HAL_TIM_IRQHandler中会被调用
* @retval 无
*/
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
if ((g_timxchy_cap_sta & 0X80) == 0) /* 还没成功捕获 */
{
if (g_timxchy_cap_sta & 0X40) /* 捕获到一个下降沿 */
{
g_timxchy_cap_sta |= 0X80; /* 标记成功捕获到一次高电平脉宽 */
g_timxchy_cap_val = HAL_TIM_ReadCapturedValue(&g_timx_cap_chy_handler,
ATIM_TIMX_CAP_CHY); /* 获取当前的捕获值 */
TIM_RESET_CAPTUREPOLARITY(&g_timx_cap_chy_handler,
ATIM_TIMX_CAP_CHY); /* 一定要先清除原来的设置 */
TIM_SET_CAPTUREPOLARITY(&g_timx_cap_chy_handler,ATIM_TIMX_CAP_CHY,
TIM_ICPOLARITY_RISING); /* 配置TIMx通道1上升沿捕获 */
}
else /* 还未开始,第一次捕获上升沿 */
{
g_timxchy_cap_sta = 0; /* 清空 */
g_timxchy_cap_val = 0;
g_timxchy_cap_sta |= 0X40; /* 标记捕获到了上升沿 */
__HAL_TIM_DISABLE(&g_timx_cap_chy_handler); /* 关闭定时器x */
__HAL_TIM_SET_COUNTER(&g_timx_cap_chy_handler,0); /* 计数器清零 */
TIM_RESET_CAPTUREPOLARITY(&g_timx_cap_chy_handler,
ATIM_TIMX_CAP_CHY); /* 一定要先清除原来的设置!!*/
TIM_SET_CAPTUREPOLARITY(&g_timx_cap_chy_handler,ATIM_TIMX_CAP_CHY,
TIM_ICPOLARITY_FALLING); /* 定时器x通道1设置为下降沿捕获 */
__HAL_TIM_ENABLE(&g_timx_cap_chy_handler); /* 使能定时器x */
}
}
}
/**
* @brief 定时器更新中断回调函数
* @param htim:定时器句柄指针
* @note 此函数会被定时器中断函数共同调用的
* @retval 无
*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == ATIM_TIMX_CAP)
{
if ((g_timxchy_cap_sta & 0X80) == 0) /* 还没成功捕获 */
{
if (g_timxchy_cap_sta & 0X40) /* 已经捕获到高电平了 */
{
if ((g_timxchy_cap_sta & 0X3F) == 0X3F)/* 高电平太长了 */
{
TIM_RESET_CAPTUREPOLARITY(&g_timx_cap_chy_handle,
ATIM_TIMX_CAP_CHY); /* 先清除原来的设置 */
/* 配置TIMx通道1上升沿捕获 */
TIM_SET_CAPTUREPOLARITY(&g_timx_cap_chy_handle,
ATIM_TIMX_CAP_CHY, TIM_ICPOLARITY_RISING);
g_timxchy_cap_sta |= 0X80; /* 标记成功捕获了一次 */
g_timxchy_cap_val = 0XFFFF;
}
else /* 累计定时器溢出次数 */
{
g_timxchy_cap_sta++;
}
}
}
}
}
现在我们来介绍一下,捕获高电平脉宽的思路:首先,设置TIM5_CH1捕获上升沿,然后等待上升沿中断到来,当捕获到上升沿中断,此时如果g_timxchy_cap_sta的第6位为0,则表示还没有捕获到新的上升沿,就先把g_timxchy_cap_sta、g_timxchy_cap_val和TIM5_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里面编写如下代码:
int main(void)
{
uint32_t temp = 0;
uint8_t t = 0;
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(85, 2, 2, 4, 8); /* 设置时钟,170Mhz */
delay_init(170); /* 延时初始化 */
usart_init(115200); /* 串口初始化为115200 */
led_init(); /* 初始化LED */
/* 10Khz的计数频率,500ms的周期,占空比为50%,也就是高电平时间=250ms=250000us */
atim_timx_pwm_chy_init(5000 - 1, 17000 - 1);
atim_timx_cap_chy_init(0xFFFF - 1, 170 - 1); /* 以1Mhz的频率计数 捕获 */
while (1)
{
if (g_timxchy_cap_sta & 0X80) /* 成功捕获到了一次高电平 */
{
temp = g_timxchy_cap_sta & 0X3F;
temp *= 0xFFFF; /* 溢出时间总和 */
temp += g_timxchy_cap_val; /* 得到总的高电平时间 */
printf("HIGH:%d us\r\n", temp); /* 打印总的高点平时间 */
g_timxchy_cap_sta = 0; /* 开启下一次捕获 */
}
t++;
if (t > 20) /* 200ms进入一次 */
{
t = 0;
LED0_TOGGLE(); /* LED0闪烁 ,提示程序运行 */
}
delay_ms(10);
}
}
首先看到atim_timx_pwm_chy_init(5000 - 1, 17000 - 1)这个语句,这里初始化了定时器1的PWM输出,PWM的周期为500ms,占空比为50%,也就是一个周期内高电平时间 = 250ms = 250000us。
再看到atim_timx_cap_chy_init(0xFFFF-1, 170 - 1)这个语句,这两个形参分别设置自动重载寄存器的值为65535,以及定时器预分频系数为169。定时器1是16位的计数器,这里的自动重载寄存器的值只设置为65535最大值,我们只用到16位计数器的计数值。预分频系数,我们设置为170分频,定时器8的时钟等于的APB2,即170MHZ,可以得到计数器的计数频率是1MHZ,即1us计数一次,所以我们的捕获时间精度是1us。这里可以知道定时器的溢出时间是65536us。
While(1)无限循环通过判断g_timxchy_cap_sta的第7位,来获知有没有成功捕获到一次高电平,如果成功捕获,先计算总的高电平时间,再通过串口传输到电脑。
4.3.5.4 下载验证
下载代码后,可以看到LED0在闪烁,说明程序已经正常在跑了,我们再打开串口调试助手,选择对应的串口端口,我这边是COM3,然后把PA8通过杜邦线接到PC6,就可以看到串口打印TIM1_CH1( PA8 )输出的PWM脉冲的高电平持续时间,如图4.3.5.4.1所示:
4.3.6 高级定时器PWM输入实验
本小节我们来学习使用高级定时器PWM输入模式,此模式是输入捕获模式的一个特例。PWM输入模式经常被应用于测量PWM脉宽和频率。PWM输入模式在《STM32G4xx参考手册_V7(英文版).pdf》手册1108页有详细的文字描述。下面我们结合这些文字,配合高级定时器框图给大家介绍PWM输入的工作原理:

图4.3.6.1 PWM输入模式工作原理示意图
① 确定定时器时钟源。本实验中我们使用内部时钟(tim_ker_ck),G4系列高级定时器挂载在APB2总线上,按照sys_stm32_clock_init函数的配置,定时器时钟频率等于APB2总线时钟频率,即170MHz。计数器的计数频率确定了测量的精度。
② 确定PWM输入的通道。PWM输入模式下测量PWM,PWM信号输入只能从通道1(CH1)或者通道2(CH2)输入。
③ 确定IC1和IC2的捕获边沿。这里以通道1(CH1)输入PWM为例,一般我们习惯设置IC1捕获边沿为上升沿捕获,IC2捕获边沿为下降沿捕获。
④ 选择触发输入信号(TRGI)。这里也是以通道1(CH1)输入PWM为例,那么我们就应该选择TI1FP1为触发输入信号。如果是通道2(CH2)输入PWM,那就选择TI2FP2为触发输入信号。可以看到这里并没有对应通道3(CH3)或者通道4(CH4)的触发输入信号,所以我们只选择通道1或者通道2作为PWM输入的通道。
⑤ 从模式选择:复位模式。复位模式的作用是:在出现所选触发输入 (TRGI) 上升沿时,重新初始化计数器并生成一个寄存器更新事件。
⑥ 读取一个PWM周期内计数器的计数个数,以及高电平期间的计数个数,再结合计数器的计数周期(即计一个数的时间),最终通过计算得到输入的PWM周期和占空比等参数。以通道1(CH1)输入PWM,设置IC1捕获边沿为上升沿捕获,IC2捕获边沿为下降沿捕获为例,那么CCR1寄存器的值+1就是PWM周期内计数器的计数个数,CCR2寄存器的值+1就是PWM高电平期间计数器的计数个数。通过这两个值就可以计算出PWM的周期或者占空比等参数。
再举个例子,以通道1(CH1)输入PWM,设置IC1捕获边沿为下降沿捕获,IC2捕获边沿为上升沿捕获为例,那么CCR1寄存器的值+1依然是PWM周期内计数器的计数个数,但是CCR2寄存器的值+1就是PWM低电平期间计数器的计数个数。通过这两个得到的参数依然可以计算出PWM的其它参数。这个大家了解一下就可以了,一般我们使用第六介绍的例子。
通过上面的描述,如果大家还不理解,下面我们结合PWM输入模式时序来分析一下。PWM输入模式时序图如图22.5.2所示:

图4.3.6.2 PWM输入模式时序图
图4.3.6.2是以通道1(CH1)输入PWM,设置IC1捕获边沿为上升沿捕获,IC2捕获边沿为下降沿捕获为例的PWM输入模式时序图。
从时序图可以看出,计数器的计数模式是递增计数模式。从左边开始看,当TI1来了上升沿时,计数器的值被复位为0(原因是从模式选择为复位模式),IC1和IC2都发生捕获事件。然后计数器的值计数到2的时候,IC2发生了下降沿捕获,捕获事件会导致这时候的计数器的值被锁存到CCR2寄存器中,该值+1就是高电平期间计数器的计数个数。最后计数器的值计数到4的时候,IC1发生了上升沿捕获,捕获事件会导致这时候的计数器的值被锁存到CCR1寄存器中,该值+1就是PWM周期内计数器的计数个数。
假设计数器的计数频率是170MHz,那我们就可以计算出这个PWM的周期、频率和占空比等参数了。下面就以这个为例给大家计算一下。由计数器的计数频率为170MHz,可以得到计数器计一个数的时间是5.88ns(即测量的精度是5.88ns)。知道了测量精度,再来计算PWM的周期,PWM周期 =(4+1)*(1/170000000) = 29.41ns,那么PWM的频率就是34MHz。占空比 = (2+1)/(4+1) =3/5(即占空比为60%)。
4.3.6.1 TIM1/TIM8/TIM20寄存器
高级定时器PWM输入模式实验除了用到定时器的时基单元:计数器寄存器(TIMx_CNT)、预分频器寄存器(TIMx_PSC)、自动重载寄存器(TIMx_ARR) 之外。主要还用到以下这些寄存器:
l 从模式控制寄存器(TIMx_SMCR)
TIM1/TIM8/TIM20的从模式控制寄存器描述如图4.3.6.1.1所示:

图4.3.6.1.1 TIMx_SMCR寄存器
该寄存器的SMS[2:0]位需要和SMS[3]结合查看,用于从模式选择。比如在本实验中我们需要用到复位模式,所以设置SMS[3:0]=0100。TS[2:0]需要和TS[4:3]结合查看,该位是触发选择,我们设置为滤波后的定时器输入1 (TI1FP1),即TS[4:0]为00101。
l 捕获/比较模式寄存器1/2(TIMx_CCMR1/2)
TIM1/TIM8/TIM20的捕获/比较模式寄存器(TIMx_CCMR1/2),该寄存器一般有2个:TIMx_CCMR1和TIMx _CCMR2。TIMx_CCMR1控制CH1和CH2,而TIMx_CCMR2控制CH3和CH4。TIMx_CCMR1寄存器描述如图4.3.6.1.2所示:

图4.3.6.1.2 TIMx_CCMR1寄存器
该寄存器的有些位在不同模式下,功能不一样,我们现在用到输入捕获模式。关于该寄存器的详细说明,请参考《STM32G4xx参考手册_V7(英文版).pdf》第1183页,28.6.7节。
本实验我们通过定时器1通道1输入PWM信号,所以IC1和IC2都映射到TI1上。配置CC1S[1:0]=01、CC2S [1:0]=10,其他位不用设置,默认为0即可。
l 捕获/比较使能寄存器(TIMx_CCER)
TIM1/TIM8/TIM20的捕获/比较使能寄存器,该寄存器控制着各个输入输出通道的开关和极性。TIMx_CCER寄存器描述如图4.3.6.1.3所示:

图4.3.6.1.3 TIMx_CCER寄存器
IC1捕获上升沿,所以CC1P位置0,即捕获发生在IC1的上升沿。IC2捕获下降沿,所以CC2P位置1,即捕获发生在IC1的下降沿。设置好捕获边沿后,还需要使能这两个通道捕获,即CC1E和CC2E位置1。
l 捕获/比较寄存器1/2/3/4(TIMx_CCR1/2/3/4)
捕获/比较寄存器(TIMx_CCR1/2/3/4),该寄存器总共有4个,对应4个通道CH1~CH4。我们使用的是通道1,所以来看看TIMx_CCR1寄存器描述如图4.3.6.1.4所示:

图4.3.6.1.4 TIMx_CCR1寄存器
本实验中,CCR1寄存器用于获取PWM周期内计数器的计数个数。CCR2寄存器用于获取PWM高电平期间计数器的计数个数。
l DMA/中断使能寄存器(TIMx_DIER)
DMA/中断使能寄存器描述如图4.3.6.1.5所示:

图4.3.6.1.5 TIMx_DIER寄存器
该寄存器位0(UIE)用于使能或者禁止更新中断,因为本实验我们用到更新中断,所以该位需要置1。位1(CC1IE)用于使能或者禁止捕获/比较1中断,我们用到捕获中断,所以该位需要置1。
4.3.6.2 硬件设计
1. 例程功能
首先通过TIM1_CH1(PA8)输出PWM波。然后把PA8输出的PWM波用杜邦线接入PC6(定时器8通道1),最后通过串口打印PWM波的脉宽和频率等信息。LED0闪烁来提示程序正在运行。
2. 硬件资源
1)LED灯
LED0 – PE0
2)定时器1通道1(PA8)输出PWM波
定时器8通道1(PC6)输入PWM波
3. 原理图
定时器属于STM32G474的内部资源,只需要软件设置好即可正常工作。我们把PA8引脚输出的PWM波用杜邦线接入PC6引脚,然后通过电脑串口上位机软件观察打印出来的信息。
4.3.6.3 程序设计
定时器PWM输入模式实验用到的HAL库中的驱动代码在前面实验都有介绍过了。 我们在程序解析再详细讲解应用到的函数,下面介绍一下高级定时器PWM输入模式的配置步骤。
高级定时器PWM输入模式配置步骤
1)开启TIMx和输入通道的GPIO时钟,配置该IO口的复用功能输入
首先开启TIMx的时钟,然后配置GPIO为复用功能输入。本实验我们默认用到定时器8通道1,对应IO是PC6,它们的时钟开启方法如下:
__HAL_RCC_TIM8_CLK_ENABLE(); /* 使能定时器8 */ __HAL_RCC_GPIOC_CLK_ENABLE(); /* 开启GPIOC时钟 */
IO口复用功能是通过函数HAL_GPIO_Init来配置的。
2)初始化TIMx,设置TIMx的ARR和PSC等参数
使用定时器的输入捕获功能时,我们调用的是HAL_TIM_IC_Init函数来初始化定时器ARR和PSC等参数。
注意:该函数会调用HAL_TIM_IC_MspInit函数,但是为不跟前面的实验共用该回调函数,提高独立性,我们就直接在atim_timx_pwmin_chy_init函数中,使能定时器时钟和GPIO时钟,初始化通道对应IO引脚等。
3)从模式配置,IT1触发更新
通过HAL_TIM_SlaveConfigSynchronization函数,配置从模式:复位模式、定时器输入触发源、边缘检测、是否滤波等。
4)设置IC1捕获相关参数。
通过HAL_TIM_IC_ConfigChannel函数来设置定时器捕获通道1的工作方式,包括边缘检测极性、映射关系,输入滤波和输入分频等。
5)设置IC2捕获相关参数。
通过HAL_TIM_IC_ConfigChannel函数来设置定时器捕获通道2的工作方式,包括边缘检测极性、映射关系,输入滤波和输入分频等。
6)使能定时器更新中断,开启捕获功能,配置定时器中断优先级
通过__HAL_TIM_ENABLE_IT函数使能定时器更新中断。
通过HAL_TIM_IC_Start_IT函数使能定时器并开启通道1和通道2的捕获功能,使能捕获中断。
通HAL_NVIC_EnableIRQ函数使能定时器中断。
通过HAL_NVIC_SetPriority函数设置中断优先级。
7)编写中断服务函数
TIM1和TIM8有独立的输入捕获中断服务函数,分别是TIM1_CC_IRQHandler和TIM8_CC_IRQHandler,其他定时器则没有,所以如果是TIM1和TIM8可以直接使用输入捕获中断服务函数来处理输入捕获中断。在使用TIM8的时候,如果要考虑定时器8溢出,可以重定义更新中断服务函数TIM8_UP_IRQHandler。如果使用HAL库的中断回调机制,可以在相关中断服务函数中直接调用定时器中断公共处理函数HAL_TIM_IRQHandler,然后我们直接重定义相关的中断回调函数来编写中断程序即可。
本实验我们不考虑溢出的情况,来实现一个简单的PWM输入功能,只需要用到捕获中断回调函数HAL_TIM_IC_CaptureCallback,大家如果需要更加宽频的PWM输入功能,可以加上溢出中断的处理逻辑。
4.3.6.3.1 程序流程图

图4.3.6.3.1.1 高级定时器PWM输入模式实验程序流程图
4.3.6.3.2 程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。高级定时器驱动源码包括两个文件:atim.c和atim.h。
首先看atim.h头文件的几个宏定义:
/********************* 高级定时器 PWM输出实验相关宏定义 *********************/
/* TIMX PWM输出定义 */
#define ATIM_TIMX_PWM_CHY_GPIO_PORT GPIOA
#define ATIM_TIMX_PWM_CHY_GPIO_PIN GPIO_PIN_8
#define ATIM_TIMX_PWM_CHY_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0) /* PA口时钟使能 */
/* TIMX 引脚复用设置
* 因为PA8默认并不是TIM1的功能脚, 必须开启复用, PA8才能用作TIM1的PWM输出引脚.
*/
#define ATIM_TIMX_PWM_CHY_GPIO_AF GPIO_AF6_TIM1
#define ATIM_TIMX_PWM TIM1
#define ATIM_TIMX_PWM_CHY TIM_CHANNEL_1
#define ATIM_TIMX_PWM_CHY_CLK_ENABLE() do{ __HAL_RCC_TIM1_CLK_ENABLE(); }while(0) /* TIM1 时钟使能 */
/****************** 第二部分 高级定时器 PWM输入实验相关宏定义 *****************/
/* TIMX 输入捕获定义
* 这里的输入捕获使用定时器TIM1_CH1
*/
#define ATIM_TIMX_PWMIN_CHY_GPIO_PORT GPIOC
#define ATIM_TIMX_PWMIN_CHY_GPIO_PIN GPIO_PIN_6
#define ATIM_TIMX_PWMIN_CHY_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0) /* PC口时钟使能 */
/* TIMX 引脚复用设置
* 因为PC6默认并不是TIM8的功能脚, 必须开启复用, PC6才能用作TIM8的输入捕获引脚
*/
#define ATIM_TIMX_PWMIN_CHY_GPIO_AF GPIO_AG4_TIM8
#define ATIM_TIMX_PWMIN TIM8
#define ATIM_TIMX_PWMIN_CC_IRQn TIM8_CC_IRQn
#define ATIM_TIMX_PWMIN_CC_IRQHandler TIM8_CC_IRQHandler
#define ATIM_TIMX_PWMIN_CH1 TIM_CHANNEL_1
#define ATIM_TIMX_PWMIN_CH2 TIM_CHANNEL_2
#define ATIM_TIMX_PWMIN_CHY_CLK_ENABLE() do{ __HAL_RCC_TIM8_CLK_ENABLE(); }while(0) /* TIM8 时钟使能 */
可以把上面的宏定义分成两部分,第一部分包括是定时器1通道1对应的IO口的宏定义,第二部分则是定时器2的相应宏定义以及TIM1/ TIM8独有的捕获中断服务函数。
下面看atim.c的程序,关于定时器1的PWM输出部分这里就不再赘述,大家有需要的的话可以回顾4.3.3章节。首先看高级定时器PWM输入模式初始化函数,其定义如下:
/**
* @brief 高级定时器TIMX 通道Y PWM输入模式 初始化函数
* @note
* 高级定时器的时钟来自APB2, 而PCLK2=170MHz, 我们设置PPRE2不分频,因此
* 高级定时器时钟 = 170MHz
* 定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.
* Ft=定时器工作频率,单位:Mhz
*
* @param 无
* @retval 无
*/
void atim_timx_pwmin_chy_init(void)
{
GPIO_InitTypeDef gpio_init_struct = {0};
TIM_SlaveConfigTypeDef slave_config = {0};
TIM_IC_InitTypeDef tim_ic_pwmin_chy = {0};
ATIM_TIMX_PWMIN_CHY_CLK_ENABLE(); /* 使能TIM1时钟 */
ATIM_TIMX_PWMIN_CHY_GPIO_CLK_ENABLE(); /* 开启捕获IO的时钟*/
gpio_init_struct.Pin = ATIM_TIMX_PWMIN_CHY_GPIO_PIN; /* PWM输入IO */
gpio_init_struct.Mode = GPIO_MODE_AF_PP; /* 复用推挽输出 */
gpio_init_struct.Pull = GPIO_PULLDOWN; /* 下拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速 */
gpio_init_struct.Alternate = ATIM_TIMX_PWMIN_CHY_GPIO_AF; /* 端口复用 */
HAL_GPIO_Init(ATIM_TIMX_PWMIN_CHY_GPIO_PORT, &gpio_init_struct);
g_timx_pwmin_chy_handle.Instance = ATIM_TIMX_PWMIN; /* 定时器1 */
g_timx_pwmin_chy_handle.Init.Prescaler = 170 - 1; /* 定时器预分频系数 */
g_timx_pwmin_chy_handle.Init.CounterMode = TIM_COUNTERMODE_UP;/* 递增计数*/
g_timx_pwmin_chy_handle.Init.Period = 65535 ; /* 自动重装载值 */
HAL_TIM_IC_Init(&g_timx_pwmin_chy_handle); /* 初始化定时器 */
/* 从模式配置,IT1触发更新 */
slave_config.SlaveMode = TIM_SLAVEMODE_RESET; /* 从模式:复位模式 */
slave_config.InputTrigger = TIM_TS_TI1FP1; /* 触发源:TI1FP1 */
/* 一开始捕获到上升沿复位计数值 */
slave_config.TriggerPolarity = TIM_INPUTCHANNELPOLARITY_RISING;
slave_config.TriggerFilter = 0; /* 不滤波 */
HAL_TIM_SlaveConfigSynchro(&g_timx_pwmin_chy_handle, &slave_config);
/* IC1捕获:上升沿触发TI1FP1 */
tim_ic_pwmin_chy.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING;
tim_ic_pwmin_chy.ICSelection = TIM_ICSELECTION_DIRECTTI; /* IC1映射到TI1*/
tim_ic_pwmin_chy.ICPrescaler = TIM_ICPSC_DIV1; /* 不分频 */
tim_ic_pwmin_chy.ICFilter = 0; /* 不滤波 */
HAL_TIM_IC_ConfigChannel(&g_timx_pwmin_chy_handle, &tim_ic_pwmin_chy, ATIM_TIMX_PWMIN_CH1); /* 配置TIM1通道1 */
/* IC2捕获:下降沿触发TI1FP2 */
tim_ic_pwmin_chy.ICPolarity = TIM_INPUTCHANNELPOLARITY_FALLING;
tim_ic_pwmin_chy.ICSelection = TIM_ICSELECTION_INDIRECTTI; /*IC2映射到TI1*/
HAL_TIM_IC_ConfigChannel(&g_timx_pwmin_chy_handle, &tim_ic_pwmin_chy, ATIM_TIMX_PWMIN_CH2); /* 配置TIM1通道2 */
/* 开启TIM1的捕获通道1 */
HAL_TIM_IC_Start_IT(&g_timx_pwmin_chy_handle, ATIM_TIMX_PWMIN_CH1);
/* 开启TIM1的捕获通道2 */
HAL_TIM_IC_Start_IT(&g_timx_pwmin_chy_handle, ATIM_TIMX_PWMIN_CH2);
/* TIM1/TIM8/TIM20有独立的输入捕获中断服务函数 */
if ( ATIM_TIMX_PWMIN == TIM1 || ATIM_TIMX_PWMIN == TIM8)
{
HAL_NVIC_SetPriority(ATIM_TIMX_PWMIN_CC_IRQn, 1, 3); /*设置中断优先级*/
HAL_NVIC_EnableIRQ(ATIM_TIMX_PWMIN_CC_IRQn); /* 开启TIMx中断 */
}
}
在atim_timx_pwmin_chy_init函数中,没有使用HAL库的MSP回调,而是把相关的初始化都写到该函数里面。
第一部分,使能定时器和相关通道对应的GPIO时钟,以及初始化相关IO引脚。
第二部分,通过HAL_TIM_IC_Init函数初始化定时器的ARR和PSC等参数。
第三部分,通过HAL_TIM_SlaveConfigSynchronization函数配置从模式,复位模式等。
第四部分,通过HAL_TIM_IC_ConfigChannel函数分别配置IC1和IC2。
第五部分,配置NVIC,使能定时器中断,配置抢占优先级和响应优先级。
最后,通过调用HAL_TIM_IC_Start_IT函数使能捕获中断,并且使能定时器。
注意:我们在atim_timx_pwmin_chy_init函数中直接设置了定时器1的预分频系数和重装载值,它的频率 = 170M / 170 = 1M Hz,周期为1us,重装载值为65535,定时器1的溢出时间为65536us。当计数溢出的时候,我们不做处理,也就是说,按照这个配置,只能检测PWM周期不超过65536us的信号。如果大家需要测量更长周期的PWM波形,可以自行修改代码,对计数溢出进行相应的处理。
下面要介绍的就是中断服务函数,及对应的回调函数。我们这里只用到ATIM_TIMX_PWMIN_CC_IRQHandler这个函数,即高级定时器独有的输入捕获中断服务函数。在中断服务函数我们调用HAL库的中断处理函数HAL_TIM_IRQHandler来处理,然后我们的逻辑程序就放在定时器输入捕获中断处理回调函数HAL_TIM_IC_CaptureCallback里。这两个函数定义如下:
/**
* @brief 定时器TIMX 输入捕获 中断服务函数
* @note 仅TIM1/TIM8/TIM20有这个函数,其他普通定时器没有这个中断服务函数!
* @param 无
* @retval 无
*/
void ATIM_TIMX_PWMIN_CC_IRQHandler(void)
{
HAL_TIM_IRQHandler(&g_timx_pwmin_chy_handle); /* 定时器共用处理函数 */
}
/**
* @brief 定时器输入捕获中断处理回调函数
* @param htim:定时器句柄指针
* @note 该函数在HAL_TIM_IRQHandler中会被调用,PWM输入属于输入捕获中的特例
* @retval 无
*/
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == ATIM_TIMX_PWMIN)
{
if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) /* 通道1捕获到上升沿 */
{
__HAL_TIM_SET_COUNTER(&g_timx_pwmin_chy_handle,0);
g_timxchy_pwmin_cval = HAL_TIM_ReadCapturedValue(htim,
ATIM_TIMX_PWMIN_CH1)+1; /* 记录整个周期的计数值 */
g_timxchy_pwmin_sta = 1; /* 标记捕获成功 */
}
else if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2)/* 通道2捕获到下降沿*/
{
g_timxchy_pwmin_hval =HAL_TIM_ReadCapturedValue(htim,
ATIM_TIMX_PWMIN_CH2)+1; /* 记录高电平时间的计数值 */
}
}
}
在输入捕获中断处理回调函数中,我们先判断通道1是否捕获到了上升沿,如果捕获到了,就把计数器清零,通过调用HAL_TIM_ReadCapturedValue函数获取整个周期对应的计数值,然后标记捕获成功,如果捕获到通道2的下降沿,就通过调用HAL_TIM_ReadCapturedValue函数获取高电平时间对应的计数值。
下面介绍一下待测试的PWM怎么得到。因为在高级定时器PWM输出实验中我们已经编写了PWM输出的程序,所以这里直接使用高级定时器的PWM输出实验的代码进行初始化,从而让TIM1_CH1(PA8)输出PWM波。然后我们用杜邦线把PA8和PC6连接起来。这样PA8输出的PWM就可以输入到PC6(定时器8 通道1)进行测量。
在main.c里面编写如下代码:
int main(void)
{
uint8_t t = 0;
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(85, 2, 2, 4, 8); /* 设置时钟,170Mhz */
delay_init(170); /* 延时初始化 */
usart_init(115200); /* 串口初始化为115200 */
led_init(); /* 初始化LED */
/* 10K Hz的计数频率,50ms的周期,即20HZ */
atim_timx_pwm_chy_init(500 - 1, 17000 - 1);
/* 初始化PWM输入,以1Mhz的频率计数,最大捕获周期 2^16 us */
atim_timx_pwmin_chy_init(0xFFFF - 1, 170 - 1);
while (1)
{
t++;
if (t % 20 ==0)
{
LED0_TOGGLE();
}
if (g_timxchy_pwmin_sta == 1) /* 成功捕获 */
{
g_timxchy_pwmin_sta = 0; /* 清除成功捕获标志 */
printf("\r\n");
printf("高电平时间:%d us\r\n",g_timxchy_pwmin_hval);
printf("PWM周期:%d us\r\n",g_timxchy_pwmin_cval );
printf("PWM频率:%.2fHz\r\n",(float)1000000/g_timxchy_pwmin_cval);
}
delay_ms(10);
}
}
先看atim_timx_pwm_chy_init(500 - 1, 17000 - 1)这个语句,这两个形参分别设置自动重载寄存器的值为499,以及定时器预分频系数为16999。先看预分频系数,我们设置为17000分频,定时器8的时钟等于APB2,即170MHz,可以得到计数器的计数频率是10k HZ,即100us计数一次。再到自动重载寄存器的值为499决定的是PWM波的频率(周期),可以得到PWM的周期为500*100us = 50000us, 频率为20Hz。然后我们在定时器8的初始化时已经设置比较值为arr的一半,即PWM的占空比为50%,所以一个周期的高电平时间为25000us。下载验证的时候验证一下捕获到的与输出的是否一致。
atim_timx_pwmin_chy_init这个语句,就初始化PWM输入捕获。然后在无限循环中每10ms判断g_timxchy_pwmin_sta标志变量,是否捕获到数据,捕获到就打印和计数相关信息。
下面我们下载到开发板验证一下。
4.3.6.4 下载验证
下载代码后,可以看到LED0在闪烁,说明程序已经正常在跑了,我们再打开串口调试助手,选择对应的串口端口,我这边是COM3。然后用杜邦线把PA8引脚连接到PC6引脚,就可以看到串口助手不断打印PWM波的信息,如图4.3.6.4.1所示:

图4.3.6.4.1 打印PWM波的信息
可以看到打印出来的PWM波信息为:周期是50000us,频率是20Hz,占空比是50%,和我们的预想结果一样。
大家可以通过atim_timx_pwm_chy_init函数来设置PWM波的周期和频率,通过g_atimx_oc_pwm_chy_handle.Pulse这个结构体成员设置占空比。
注意:本实验没有对计数溢出做处理,所以可测量的PWM波范围有一定局限性(最大周期65536us),大家如果需要更加宽频的PWM检测功能,则可以加上计数溢出的处理。
4.3.7 高级定时器输出比较模式实验
本小节我们来学习使用高级定时器输出比较模式下的翻转功能,通过定时器的3个通道分别输出3个50%占空比、不同相位的PWM。
输出比较模式下翻转功能作用是:当计数器的值等于捕获/比较寄存器影子寄存器的值时,OC1REF 发生翻转,进而控制通道输出(OCx)翻转。通过翻转功能实现输出PWM的具体原理如下:PWM频率由自动重载寄存器(TIMx_ARR)的值决定,在这个过程中,只要自动重载寄存器的值不变,那么PWM占空比就固定为50%。我们可以通过捕获/比较寄存器(TIMx_CCRx)的值改变PWM的相位。生成PWM的原理如图4.3.7.1所示:

图4.3.7.1 输出比较模式翻转功能输出PWM波原理示意图
本实验就是根据图4.3.7.1的原理来设计的,具体实验是:我们设置固定的ARR值为999,那么PWM占空比固定为50%,通过改变3个通道的捕获/比较寄存器(TIMx_CCRx)的值使得每个通道输出的PWM的相位都不一样,注意捕获/比较寄存器的值设置范围是:0 ~ ARR。比如:TIMx_CCR1=250-1,TIMx_CCR2=500-1,TIMx_CCR3=750-1,那么可以得到通道1~通道3输出的PWM的相位分别是:25%、50%、75%。翻转功能输出的PWM周期,这里用T表示,其计算公式如下:
T = 2*(arr+1)*((psc+1)/ Tclk)
其中:
T:翻转功能输出的PWM周期(单位为s)。
Tclk:定时器的时钟源频率(单位为MHz)。
arr:自动重装寄存器(TIMx_ARR)的值。
psc:预分频器寄存器(TIMx_PSC)的值。
4.3.7.1 TIM1/TIM8/TIM20寄存器
高级定时器输出比较模式除了用到定时器的时基单元:计数器寄存器(TIMx_CNT)、预分频器寄存器(TIMx_PSC)、自动重载寄存器(TIMx_ARR) 之外。主要还用到以下这些寄存器:
l 控制寄存器1(TIMx_CR1)
TIM1/TIM8/TIM20的控制寄存器1描述如图4.3.7.1.1所示:

图4.3.7.1.1 TIMx_CR1寄存器
上图中我们只列出了本实验需要用的一些位,其中:位7(APRE)用于控制自动重载寄存器是否具有缓冲作用,在基本定时器的时候已经讲过,请回顾。本实验中,我们把该位置1。
位4(DIR)用于配置计数器的计数方向,本实验默认置0即可。
位CEN位,用于使能计数器的工作,必须要设置该位为1,才可以开始计数。
其它位保持复位值即可。
l 捕获/比较模式寄存器1/2(TIMx_CCMR1/2)
TIM1/TIM8/TIM20的捕获/比较模式寄存器(TIMx_CCMR1/2),该寄存器一般有2个:TIMx_CCMR1和TIMx _CCMR2。TIMx_CCMR1控制CH1和CH2,而TIMx_CCMR2控制CH3和CH4。TIMx_CCMR1寄存器描述如图4.3.7.1.2所示:

图4.3.7.1.2 TIMx_CCMR1寄存器
该寄存器的有些位在不同模式下,功能不一样,我们现在用到输出比较模式。关于该寄存器的详细说明,请参考《STM32G4xx参考手册_V7(英文版).pdf》第1186页,28.6.7节。
本实验我们用到了定时器8输出比较的3个通道,所以我们需要配置TIM1_CCMR1和TIM1_CCMR2。以TIM1_CCMR1寄存器为例,模式设置位OC1M[2:0]就是对应着通道1的模式设置,此部分由3位组成,总共可以配置成8种模式,我们使用的是翻转功能,所以这3位必须设置为011。通道2也是如此,将位OC2M[2:0]设置为011。通道3就要设置TIM1_CCMR2寄存器的位OC3M[2:0]。除此之外,我们还要设置输出比较的预装载使能位,如通道1对应输出比较的预装载使能位OC1PE置1,其他通道也要把相应位置1。
l 捕获/比较使能寄存器(TIMx_CCER)
TIM1/TIM8/TIM20的捕获/比较使能寄存器,该寄存器控制着各个输入输出通道的开关和极性。TIMx_CCER寄存器描述如图4.3.7.1.3所示:

图4.3.7.1.3 TIMx_CCER寄存器
该寄存器比较简单,要让TIM1的3个通道都输出,我们需要把对应的捕获/比较1输出使能位置1。通道1到通道3的使能位分别是:CC1E、CC2E、CC3E,我们把这3个位置置1,使能通道输出。
l 捕获/比较寄存器1/2/3/4(TIMx_CCR1/2/3/4)
捕获/比较寄存器(TIMx_CCR1/2/3/4),该寄存器总共有4个,对应4个通道CH1~CH4。本实验需要用到1~3通道,以通道1对应的TIMx_ CCR1寄存器为例,其描述如图4.3.7.1.4所示:

图4.3.7.1.4 TIMx_CCR1寄存器
这里,我们通过改变TIMx_CCR1/2/3寄存器的值来改变3个通道输出的PWM的相位。
l TIM1/TIM8/TIM20断路和死区寄存器(TIMx_BDTR)
本实验用的是高级定时器,我们还需要配置:断路和死区寄存器(TIMx_BDTR),该寄存器各位描述如图4.3.7.1.5所示:

图4.3.7.1.5 TIMx_BDTR寄存器
该寄存器,我们只需要关注位15(MOE),要想高级定时器的通道正常输出,则必须设置MOE位为1,否则不会有输出。
4.3.7.2 硬件设计
1. 例程功能
使用输出比较模式的翻转功能,通过定时器8的3路通道输出占空比固定为50%、相位分别是25%、50%和75%的PWM。
2. 硬件资源
1)LED灯
LED0 – PE0
2)PC6复用为TIM8_CH1
PC7复用为TIM8_CH2
PC8复用为TIM8_CH3
3. 原理图
定时器属于STM32G474的内部资源,只需要软件设置好即可正常工作。我们需要通过示波器观察PC6、PC7和PC8引脚PWM输出的情况。
4.3.7.3 程序设计
4.3.7.3.1 定时器的HAL库驱动
定时器在HAL库中的驱动代码在前面已经介绍了部分,请回顾,这里我们再介绍几个本实验用到的函数。
1. HAL_TIM_OC_Init函数
定时器的输出比较模式初始化函数,其声明如下:
HAL_StatusTypeDef HAL_TIM_OC_Init(TIM_HandleTypeDef *htim);
l 函数描述:
用于初始化定时器的输出比较模式。
l 函数形参:
形参1是TIM_HandleTypeDef结构体类型指针变量,基本定时器的时候已经介绍。
l 函数返回值:
HAL_StatusTypeDef枚举类型的值。
2. HAL_TIM_OC_ConfigChannel函数
定时器的输出比较通道设置初始化函数。其声明如下:
HAL_StatusTypeDef HAL_TIM_OC_ConfigChannel(TIM_HandleTypeDef *htim, TIM_OC_InitTypeDef *sConfig, uint32_t Channel);
l 函数描述:
该函数用于初始化定时器的输出比较通道。
l 函数形参:
形参1 是TIM_HandleTypeDef结构体类型指针变量,用于配置定时器基本参数。
形参2是TIM_OC_InitTypeDef结构体类型指针变量,用于配置定时器的输出比较参数。在高级定时器PWM输出实验已经介绍过TIM_OC_InitTypeDef结构体指针类型。
形参3 是定时器通道,范围:TIM_CHANNEL_1到TIM_CHANNEL_4。
l 函数返回值:
HAL_StatusTypeDef枚举类型的值。
3. HAL_TIM_OC_Start函数
定时器的输出比较启动函数,其声明如下:
HAL_StatusTypeDef HAL_TIM_OC_Start(TIM_HandleTypeDef *htim, uint32_t Channel);
l 函数描述:
用于启动定时器的输出比较模式。
l 函数形参:
形参1是TIM_HandleTypeDef结构体类型指针变量。
形参2是定时器通道,范围:TIM_CHANNEL_1到TIM_CHANNEL_4。
l 函数返回值:
HAL_StatusTypeDef枚举类型的值。
l 注意事项:
HAL库也同样提供了单独使能定时器的输出通道函数,函数为:
void TIM_CCxChannelCmd(TIM_TypeDef *TIMx, uint32_t Channel, uint32_t ChannelState);
HAL_TIM_OC_Start函数内部也调用了该函数。
定时器输出比较模式配置步骤
1)开启TIMx和通道输出的GPIO时钟,配置该IO口的复用功能输出
首先开启TIMx的时钟,然后配置GPIO为复用功能输出。本实验我们默认用到定时器8通道1、2、3,对应IO是PC6\PC7\PC8,它们的时钟开启方法如下:
__HAL_RCC_TIM8_CLK_ENABLE(); /* 使能定时器8 */ __HAL_RCC_GPIOC_CLK_ENABLE(); /* 开启GPIOC时钟 */
IO口复用功能是通过函数HAL_GPIO_Init来配置的。
2)初始化TIMx,设置TIMx的ARR和PSC等参数
使用定时器的输出比较模式时,我们调用的是HAL_TIM_OC_Init函数来初始化定时器ARR和PSC等参数。
注意:该函数会调用HAL_TIM_OC_MspInit函数,我们可以通过后者存放定时器和GPIO时钟使能、GPIO初始化、中断使能以及优先级设置等代码。
3)设置定时器为输出比较模式,输出比较极性,输出比较值、翻转功能等参数
在HAL库中,通过HAL_TIM_OC_ConfigChannel函数来设置定时器为输出比较模式,根据需求设置输出比较的极性,设置输出比较值、翻转功能等。
最后我们通过__HAL_TIM_ENABLE_OCxPRELOAD函数使能通道的预装载。
4)开启定时器并输出PWM
通过HAL_TIM_OC_Start函数使能定时器并开启输出。
4.3.7.3.2 程序流程图

图4.3.7.3.2.1 高级定时器输出比较模式实验程序流程图
4.3.7.3.3 程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。高级定时器驱动源码包括两个文件:atim.c和atim.h。
首先看atim.h头文件的几个宏定义:
/********************* 高级定时器 输出比较实验相关宏定义 **********************/
/* TIMX 输出比较定义 */
#define ATIM_TIMX_COMP_CH1_GPIO_PORT GPIOC
#define ATIM_TIMX_COMP_CH1_GPIO_PIN GPIO_PIN_6
#define ATIM_TIMX_COMP_CH1_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0) /* PC口时钟使能 */
#define ATIM_TIMX_COMP_CH2_GPIO_PORT GPIOC
#define ATIM_TIMX_COMP_CH2_GPIO_PIN GPIO_PIN_7
#define ATIM_TIMX_COMP_CH2_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0) /* PC口时钟使能 */
#define ATIM_TIMX_COMP_CH3_GPIO_PORT GPIOC
#define ATIM_TIMX_COMP_CH3_GPIO_PIN GPIO_PIN_8
#define ATIM_TIMX_COMP_CH3_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0) /* PC口时钟使能 */
/* TIMX 引脚复用设置
* 因为PC6、PC7、PC8默认并不是TIM8的功能脚, 必须开启复用, 才能用作TIM8的PWM输出引脚
*/
#define ATIM_TIMX_COMP_GPIO_AF GPIO_AG4_TIM8
#define ATIM_TIMX_COMP TIM8
#define ATIM_TIMX_COMP_CH1_CCRX ATIM_TIMX_COMP->CCR1
#define ATIM_TIMX_COMP_CH2_CCRX ATIM_TIMX_COMP->CCR2
#define ATIM_TIMX_COMP_CH3_CCRX ATIM_TIMX_COMP->CCR3
#define ATIM_TIMX_COMP_CLK_ENABLE() do{ __HAL_RCC_TIM8_CLK_ENABLE(); }while(0) /* TIM8 时钟使能 */
可以把上面的宏定义分成两部分,第一部分是定时器8输出通道1~通道3对应的IO口的宏定义。第二部分则是定时器8的相应宏定义。
下面来看到atim.c文件的程序,首先是高级定时器输出比较模式初始化函数,其定义如下:
/**
* @brief 高级定时器TIMX 输出比较模式 初始化函数(使用输出比较模式)
* @note
* 配置高级定时器TIMX 3路输出比较模式PWM输出,实现50%占空比,不同相位控制
* 注意,本例程输出比较模式,每2个计数周期才能完成一个PWM输出,因此输出频率减半
* 另外,我们还可以开启中断在中断里面修改CCRx,从而实现不同频率/不同相位的控制
* 但是我们不推荐这么使用,因为这可能导致非常频繁的中断,从而占用大量CPU资源
*
* 高级定时器的时钟来自APB2,而PCLK2 = 170MHz,我们设置PPRE2不分频,因此
* 高级定时器时钟 = 170MHz
* 定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us
* Ft=定时器工作频率,单位:Mhz
*
* @param arr: 自动重装值。
* @param psc: 时钟预分频数
* @retval 无
*/
void atim_timx_comp_pwm_init(uint16_t arr, uint16_t psc)
{
TIM_OC_InitTypeDef sConfigOC = {0};
g_timx_comp_pwm_handle.Instance = ATIM_TIMX_COMP; /* 定时器x */
g_timx_comp_pwm_handle.Init.Prescaler = psc; /* 定时器分频 */
g_timx_comp_pwm_handle.Init.CounterMode = TIM_COUNTERMODE_UP;/* 向上计数 */
g_timx_comp_pwm_handle.Init.Period = arr; /* 自动重装载值 */
g_timx_comp_pwm_handle.Init.AutoReloadPreload =
TIM_AUTORELOAD_PRELOAD_ENABLE; /* 使能影子寄存器TIMx_ARR */
HAL_TIM_OC_Init(&g_timx_comp_pwm_handle); /* 输出比较模式初始化 */
sConfigOC.OCMode = TIM_OCMODE_TOGGLE; /* 比较输出模式 */
sConfigOC.Pulse = 250 - 1; /* 设置输出比较寄存器的值 */
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH /* 输出比较极性为高 */
HAL_TIM_OC_ConfigChannel(&g_timx_comp_pwm_handle, &sConfigOC,
TIM_CHANNEL_1); /* 初始化定时器输出比较通道1 */
/* 通道1 预装载使能 */
__HAL_TIM_ENABLE_OCxPRELOAD(&g_timx_comp_pwm_handle, TIM_CHANNEL_1);
sConfigOC.Pulse = 500 - 1;
HAL_TIM_OC_ConfigChannel(&g_timx_comp_pwm_handle, &sConfigOC,
TIM_CHANNEL_2); /* 初始化定时器输出比较通道2*/
/* 通道2 预装载使能 */
__HAL_TIM_ENABLE_OCxPRELOAD(&g_timx_comp_pwm_handle, TIM_CHANNEL_2);
sConfigOC.Pulse = 750 - 1;
sConfigOC.OCIdleState = TIM_OCIDLESTATE_RESET;
HAL_TIM_OC_ConfigChannel(&g_timx_comp_pwm_handle, &sConfigOC,
TIM_CHANNEL_3); /* 初始化定时器输出比较通道3 */
/* 通道3 预装载使能 */
__HAL_TIM_ENABLE_OCxPRELOAD(&g_timx_comp_pwm_handle, TIM_CHANNEL_3);
/* 开启通道x输出 */
HAL_TIM_OC_Start(&g_timx_comp_pwm_handle,TIM_CHANNEL_1);
HAL_TIM_OC_Start(&g_timx_comp_pwm_handle,TIM_CHANNEL_2);
HAL_TIM_OC_Start(&g_timx_comp_pwm_handle,TIM_CHANNEL_3);
}
在atim_timx_comp_pwm_init 函数中,首先调用HAL_TIM_OC_Init函数初始化定时器的ARR和PSC等参数。然后通过调用函数HAL_TIM_OC_ConfigChannel设置通道1~通道3的工作参数,包括:输出比较模式功能、输出比较寄存器的值,输出极性等。接着调用__HAL_TIM_ENABLE_OCxPRELOAD函数宏使能CCR1/2/3寄存器的预装载。最后通过调用函数HAL_TIM_OC_Start来使能TIM1通道1~通道3输出。
HAL_TIM_OC_Init函数会调用HAL_TIM_OC_MspInit回调函数,我们把使能定时器和通道对应的IO时钟、IO初始化的代码存放到该函数里,其定义如下:
/**
* @brief 定时器底层驱动,时钟使能,引脚配置
此函数会被HAL_TIM_OC_Init()调用
* @param htim:定时器句柄
* @retval 无
*/
void HAL_TIM_OC_MspInit(TIM_HandleTypeDef *htim)
{
if (htim->Instance == ATIM_TIMX_COMP)
{
GPIO_InitTypeDef gpio_init_struct;
ATIM_TIMX_COMP_CLK_ENABLE(); /* 使能定时器x时钟 */
ATIM_TIMX_COMP_CH1_GPIO_CLK_ENABLE(); /* 使能通道1的IO时钟 */
ATIM_TIMX_COMP_CH2_GPIO_CLK_ENABLE(); /* 使能通道2的IO时钟 */
ATIM_TIMX_COMP_CH3_GPIO_CLK_ENABLE(); /* 使能通道3的IO时钟 */
/* 初始化通道1的IO */
gpio_init_struct.Pin = ATIM_TIMX_COMP_CH1_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_AF_PP;
gpio_init_struct.Pull = GPIO_NOPULL;
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
gpio_init_struct.Alternate = ATIM_TIMX_COMP_GPIO_AF;
HAL_GPIO_Init(ATIM_TIMX_COMP_CH1_GPIO_PORT, &gpio_init_struct);
/* 初始化通道2的IO */
gpio_init_struct.Pin = ATIM_TIMX_COMP_CH2_GPIO_PIN;
HAL_GPIO_Init(ATIM_TIMX_COMP_CH2_GPIO_PORT, &gpio_init_struct);
/* 初始化通道3的IO */
gpio_init_struct.Pin = ATIM_TIMX_COMP_CH3_GPIO_PIN;
HAL_GPIO_Init(ATIM_TIMX_COMP_CH3_GPIO_PORT, &gpio_init_struct);
}
}
该函数主要是使能定时器和通道对应的IO时钟,初始化IO口。
在main.c里面编写如下代码:
int main(void)
{
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(85, 2, 2, 4, 8); /* 设置时钟,170Mhz */
delay_init(170); /* 延时初始化 */
led_init(); /* 初始化LED */
atim_timx_comp_pwm_init(1000 - 1, 170 - 1); /* 1Mhz的计数频率,计数1000次 */
ATIM_TIMX_COMP_CH1_CCRX = 250 - 1; /* 通道1 相位25% */
ATIM_TIMX_COMP_CH2_CCRX = 500 - 1; /* 通道2 相位50% */
ATIM_TIMX_COMP_CH3_CCRX = 750 - 1; /* 通道3 相位75% */
while (1)
{
LED0_TOGGLE(); /* LED0(红灯) 翻转 */
delay_ms(200);
}
}
本小节开头我们讲解了输出比较模式翻转功能如何产生PWM波,下面结合程序一起计算出PWM波的周期,频率等参数。
定时器8时钟源的时钟频率等于APB2总线时钟频率,即170MHz,而调用atim_timx_comp_pwm_init(1000 - 1, 170 - 1)初始化函数之后,就相当于写入预分频寄存器的值为169,写入自动重载寄存器的值为999。将这些参数代入本小节介绍的翻转功能输出的PWM周期计算公式,可得:
T = 2*(arr+1)*((psc+1)/ Tclk) = 2*(999+1)*((169+1)/ 170000000) = 0.002s
由上述式子得到PWM周期为2ms,频率为500Hz。ARR值为固定为1000,所以占空比则固定为50%。定时器8通道1~通道3输出的PWM波的相位分别是:25%、50%、75%。
4.3.7.4 下载验证
下载代码后,可以看到LED0在闪烁,说明程序已经正常在跑了。 我们需要借助示波器观察PC6、PC7和PC8引脚PWM输出的情况,如图4.3.7.4.1所示:

图4.3.7.4.1 相位为25%、50%、75%的PWM波
图4.3.7.4.1中,由上到下分别是引脚PC6、PC7和PC8输出的PWM波,即分别对应的是TIM8_CH1、TIM8_CH2和TIM8_CH3输出的相位为25%、50%和75%的PWM波。大家可以把其中一个通道的捕获/比较寄存器的值设置为0,那么就可以得到PWM初相位的波形,即相位为0%。
4.3.8 高级定时器输出指定个数PWM实验
要实现定时器输出指定个数PWM,只需要掌握下面几点内容:
第一,如果大家还不清楚定时器是如何输出PWM的,请回顾通用定时器PWM输出实验的内容,这部分的知识是一样的。但是需要注意的是:我们需要把MOE位置1,这样高级定时器的通道才能输出。
第二,要清楚重复计数器特性,设置重复计数器寄存器RCR的值为N,那么更新事件将在定时器发生N+1次上溢或下溢时发生。换句话来说就是,想要指定输出N个PWM,只需要把N-1写入RCR寄存器。因为在边沿对齐模式下,定时器溢出周期对应着PWM周期,我们只要在更新事件发生时,停止输出PWM就行。
第三,为了保证定时器输出指定个数的PWM后,定时器马上停止继续输出,我们使能更新中断,并在定时器中断里关闭计数器。
原理部分我们就讲到这里,下面直接开始寄存器的介绍。
4.3.8.1 TIM1/TIM8/TIM20寄存器
下面介绍TIM1/TIM8/TIM20这些高级定时器中使用到的几个重要的寄存器,其他更多关于定时器的资料可以参考《STM32G4xx参考手册_V7(英文版).pdf》的第28章。
l 控制寄存器 1(TIMx_CR1)
TIM1/TIM8/TIM20的控制寄存器1描述如图4.3.8.1.1所示:

图4.3.8.1.1 TIMx_CR1寄存器
上图中我们只列出了本章需要用的一些位,其中:位7(APRE)用于控制自动重载寄存器是否具有缓冲作用,在基本定时器的时候已经讲过,请回顾。在本实验中我们把该位要置1,这样就算改变ARR寄存器的值,该值也不会马上生效,而是等待之前设置的PWM完整输出后(发生更新事件)才生效。位4(DIR)用于配置计数器的计数方向,这里我们默认置0。位0(CEN),用于使能计数器的工作,必须要设置该位为1,才可以开始计数。
l 捕获/比较模式寄存器1(TIMx_CCMR1)
TIM1/TIM8/TIM20的捕获/比较模式寄存器(TIMx_CCMR1/2),该寄存器一般有2个:TIMx _CCMR1和TIMx _CCMR2。TIMx_CCMR1控制CH1和CH2,而TIMx_CCMR2控制CH3和CH4。TIMx_CCMR1寄存器描述如图4.3.8.1.2所示:

图4.3.8.1.2 TIMx_CCMR1寄存器
该寄存器的有些位在不同模式下,功能不一样,我们前面已经说过。比如我们要让TIM1的CH1输出PWM波为例,该寄存器的模式设置位OC1M[2:0]就是对应着通道1的模式设置,此部分由3位组成,总共可以配置成8种模式,我们使用的是PWM模式,所以这3位必须设置为110或者111,分别对应PWM模式1和PWM模式2。这两种PWM模式的区别就是输出有效电平的极性相反,这里我们设置为PWM模式1。位3 OC1PE是输出比较通道1的预装使能,该位需要置1,另外CC1S[1:0]用于设置通道1的方向(输入/输出)默认设置为0,就是设置通道作为输出使用。
l 捕获/比较使能寄存器(TIMx_CCER)
TIM1/TIM8/TIM20的捕获/比较使能寄存器,该寄存器控制着各个输入输出通道的开关。TIMx_CCER寄存器描述如图4.3.8.1.3所示:

图4.3.8.1.3 TIMx_CCER寄存器
该寄存器比较简单,要让TIM1的CH1输出PWM波,这里我们要使能CC1E位,该位是通道1输入/输出使能位,要想PWM从IO口输出,这个位必须设置为1。CC1P位是设置通道1的输出极性,我们设置0,即OC1高电平有效。
l 事件产生寄存器(TIMx_EGR)
TIM1/TIM8/TIM20的事件产生寄存器,该寄存器主要是用户用软件更新各类事件和某些寄存器位的操作寄存器。TIMx_EGR寄存器描述如图4.3.8.1.4所示:

图4.3.8.1.4 TIMx_EGR寄存器
UG位是更新事件的控制位,作用和定时器溢出时产生的更新事件一样,区别是这里是通过软件产生的,而定时器溢出是硬件自己完成的。只有开启了更新中断,这两种方式都可以产更新中断。本实验用到该位去产生软件更新器事件,在需要的时候把UG位置1即可,会由硬件自动清零。
l 重复计数器寄存器(TIMx_RCR)
重复计数器寄存器用于设置重复计数器值,因为它具有影子寄存器,所以它本身只是起缓冲作用。当更新事件发生时,该寄存器的值会转移到其影子寄存器中,从而真正起作用。TIMx_RCR寄存器描述如图4.3.8.1.5所示:

图4.3.8.1.5 TIMx_ RCR寄存器
该寄存器的REP[15:0]的取值范围:0~65535。因为这个寄存器只是起缓冲作用,如果大家对该寄存器写入值后,想要立即生效,可以通过对UG位写1,产生软件更新事件。
l 捕获/比较寄存器1/2/3/4(TIMx_CCR1/2/3/4)
捕获/比较寄存器(TIMx_CCRx),该寄存器总共有4个,对应TIM1/TIM8/TIM20的4个通道CH1-CH4。我们使用的是TIM8的通道1,所以来看看TIMx_CCR1寄存器描述,如图4.3.8.1.6所示:

图4.3.8.1.6 TIMx_CCR1寄存器
在输出模式下,捕获/比较寄存器影子寄存器的值与CNT的值比较,根据比较结果产生相应动作,利用这点,我们通过修改这个寄存器的值,就可以控制PWM的占空比了。
l TIM1/TIM8/TIM20断路和死区寄存器(TIMx_BDTR)
如果是通用定时器,则配置以上说的寄存器就够了,但是如果是高级定时器的通道用作输出时,还必须配置断路和死区寄存器(TIMx_BDTR)的位MOE,该寄存器各位描述如图4.3.8.1.7所示:

图4.3.8.1.7 TIMx_BDTR寄存器
本实验,我们只需要关注该寄存器的位15(MOE),要想高级定时器的PWM正常输出,则必须设置MOE位为1,否则不会有输出。
4.3.8.2 硬件设计
1. 例程功能
将TIM8_CH1(由PC6复用)输出PWM,然后利用示波器查看PWM波形。上电默认输出5个PWM波,之后按一下按键KEY0,就输出5个PWM波。LED0闪烁指示程序运行状态。
2. 硬件资源
1)LED灯:LED0 – PE0
2)独立按键
KEY0 – PE12
3)定时器8,使用TIM8通道1,将TIM8_CH1输出到PC6。
3. 原理图
定时器属于STM32G474的内部资源,只需要软件设置好即可正常工作。我们通过示波器来检测STM32G474的定时器的PWM输出情况,同时还用按键KEY0进行控制。
4.3.8.3 程序设计
本实验用到的HAL库驱动请回顾高级定时器PWM输出实验的介绍。下面介绍一下定时器输出指定个数PWM的配置步骤。
定时器输出指定个数PWM配置步骤
1)开启TIMx和通道输出的GPIO时钟,配置该IO口的复用功能输出
首先开启TIMx的时钟,然后配置GPIO为复用功能输出。本实验我们默认用到定时器8通道1,对应IO是PC6,它们的时钟开启方法如下:
__HAL_RCC_TIM8_CLK_ENABLE(); /* 使能定时器8 */ __HAL_RCC_GPIOC_CLK_ENABLE(); /* 开启GPIOC时钟 */
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模式,根据需求设置输出比较的极性,设置比较值(控制占空比)等。
本实验我们设置TIM8的通道1为PWM1模式,输出比较极性为高。捕获/比较寄存器的值(即比较值)设置为自动重装载值的一半,即PWM占空比为50%。
4)使能定时器更新中断,开启定时器并输出PWM,配置定时器中断优先级
通过__HAL_TIM_ENABLE_IT函数使能定时器更新中断。
通过HAL_TIM_PWM_Start函数使能定时器并开启输出PWM。
通过HAL_NVIC_EnableIRQ函数使能定时器中断。
通过HAL_NVIC_SetPriority函数设置中断优先级。
5)编写中断服务函数
定时器8更新中断服务函数为:TIM8_UP_IRQHandler,当发生更新中断的时候,程序就会执行该中断服务函数。HAL库提供了一个定时器中断公共处理函数HAL_TIM_IRQHandler,该函数会根据中断类型调用相关的中断回调函数。用户根据自己的需要重定义这些中断回调函数来处理中断程序。本实验我们不使用HAL库的中断回调机制,而是把中断程序写在定时器中断服务函数里。详见本章例程源码。
4.3.8.3.1 程序流程图

图4.3.8.3.1.1 高级定时器输出指定个数PWM实验程序流程图
4.3.8.3.2 程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。高级定时器驱动源码包括两个文件:atim.c和atim.h。
首先看atim.h头文件的几个宏定义:
/********************** 高级定时器 输出比较实验相关宏定义 **********************/
/* TIMX 输出比较定义 */
#define ATIM_TIMX_NPWM_CHY_GPIO_PORT GPIOC
#define ATIM_TIMX_NPWM_CHY_GPIO_PIN GPIO_PIN_6
#define ATIM_TIMX_NPWM_CHY_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0) /* PC口时钟使能 */
/* TIMX 引脚复用设置
* 因为PC6默认并不是TIM8的功能脚, 必须开启复用, 才能用作TIM8的PWM输出引脚
*/
#define ATIM_TIMX_NPWM_CHY_GPIO_AF GPIO_AG4_TIM8
#define ATIM_TIMX_NPWM TIM8
#define ATIM_TIMX_NPWM_IRQn TIM8_UP_IRQn
#define ATIM_TIMX_NPWM_IRQHandler TIM8_UP_IRQHandler
#define ATIM_TIMX_NPWM_CHY TIM_CHANNEL_1
#define ATIM_TIMX_NPWM_CHY_CLK_ENABLE() do{ __HAL_RCC_TIM8_CLK_ENABLE(); }while(0) /* TIM8 时钟使能 */
可以把上面的宏定义分成两部分,第一部分是定时器8输入通道1对应的IO口的宏定义,第二部分则是定时器8输入通道1的相应宏定义。
下面看atim.c的程序,首先是输出指定个数PWM初始化函数,其定义如下:
/**
* @brief 高级定时器TIMX 通道Y 输出指定个数PWM 初始化函数
* @note
* 高级定时器的时钟来自APB2,而PCLK2 = 170MHz,我们设置PPRE2不分频,因此
* 高级定时器时钟 = 170MHz
* 定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.
* Ft=定时器工作频率,单位:Mhz
*
* @param arr: 自动重装值
* @param psc: 时钟预分频数
* @retval 无
*/
void atim_timx_npwm_chy_init(uint16_t arr, uint16_t psc)
{
GPIO_InitTypeDef gpio_init_struct;
ATIM_TIMX_NPWM_CHY_GPIO_CLK_ENABLE(); /* IO时钟使能 */
ATIM_TIMX_NPWM_CHY_CLK_ENABLE(); /* TIMx时钟使能 */
gpio_init_struct.Pin = ATIM_TIMX_NPWM_CHY_GPIO_PIN; /* 通道y的GPIO */
gpio_init_struct.Mode = GPIO_MODE_AF_PP; /* 复用推挽输出 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速 */
gpio_init_struct.Alternate = ATIM_TIMX_NPWM_CHY_GPIO_AF; /* 端口复用 */
HAL_GPIO_Init(ATIM_TIMX_NPWM_CHY_GPIO_PORT, &gpio_init_struct);
g_timx_npwm_chy_handle.Instance = ATIM_TIMX_NPWM; /* 定时器x */
g_timx_npwm_chy_handle.Init.Prescaler = psc; /* 定时器分频 */
g_timx_npwm_chy_handle.Init.CounterMode = TIM_COUNTERMODE_UP; /* 递增计数*/
g_timx_npwm_chy_handle.Init.Period = arr; /* 自动重装载值 */
g_timx_npwm_chy_handle.Init.AutoReloadPreload =
TIM_AUTORELOAD_PRELOAD_ENABLE;
HAL_TIM_PWM_Init(&g_timx_npwm_chy_handle); /* 初始化PWM */
timx_oc_npwm_chy.OCMode = TIM_OCMODE_PWM1; /* 模式选择PWM1 */
/* 比较值为重装载值的50%,即占空比为50% */
timx_oc_npwm_chy.Pulse = ( arr + 1 )/2;
timx_oc_npwm_chy.OCPolarity = TIM_OCPOLARITY_HIGH; /* 输出比较极性为高 */
HAL_TIM_PWM_ConfigChannel(&g_timx_npwm_chy_handle, &timx_oc_npwm_chy,
ATIM_TIMX_NPWM_CHY); /* 配置TIMx通道y */
/* 开启对应PWM通道 */
HAL_TIM_PWM_Start(&g_timx_npwm_chy_handle, ATIM_TIMX_NPWM_CHY);
/* 开启更新中断 */
__HAL_TIM_ENABLE_IT(&g_timx_npwm_chy_handle, TIM_IT_UPDATE);
HAL_NVIC_SetPriority(ATIM_TIMX_NPWM_IRQn, 1, 3);
HAL_NVIC_EnableIRQ(ATIM_TIMX_NPWM_IRQn); /* 开启ITMx中断 */
}
atim_timx_npwm_chy_init函数包含了输出通道对应IO的初始代码、NVIC、使能时钟、定时器基础工作参数和输出模式配置的所有代码。下面来看看该函数的代码内容。
第一部分:使能定时器和GPIO的时钟。
第二部分:调用HAL_TIM_PWM_Init函数初始化定时器基础参数,如:ARR和PSC等。
第三部分:定时器输出通道对应的IO的初始化。
第四部分:调用HAL_TIM_PWM_ConfigChannel设置PWM模式以及比较值等参数。
第五部分:NVIC的初始化,配置抢占优先级、响应优先级和开启NVIC定时器中断。
最后是使能更新中断和使能通道输出。
为了方便代码的管理和移植性等,这里就没有使用HAL_TIM_PWM_MspInit这个函数来存放使能时钟、GPIO、NVIC相关的代码,而是全部存放在atim_timx_npwm_chy_init函数中。
下面我们看设置PWM个数的函数,其定义如下:
/* g_npwm_remain表示当前还剩下多少个脉冲要发送
* 每次最多发送256个脉冲
*/
static uint32_t g_npwm_remain = 0;
/**
* @brief 高级定时器TIMX NPWM设置PWM个数
* @param npwm: PWM的个数, 范围:1~2^32-1
* @retval 无
*/
void atim_timx_npwm_chy_set(uint32_t npwm)
{
if (npwm == 0)
{
return;
}
g_npwm_remain = npwm; /* 保存脉冲个数 */
/* 产生一次更新事件,在中断里面处理脉冲输出 */
HAL_TIM_GenerateEvent(&a_timx_pwm_chy_handle, TIM_EVENTSOURCE_UPDATE);
__HAL_TIM_ENABLE(&a_timx_pwm_chy_handle); /* 使能定时器TIMX */
}
我们要输出多少个周期的PWM就用这个函数来设置。该函数作用是把我们设置输出的PWM个数的值赋值给静态全局变量g_npwm_remain,该变量会在更新中断服务函数回调函数中发挥作用。最后对TIMx_EGR寄存器UG位写1,产生一次更新事件,并使能定时器。
下面来介绍定时器中断服务函数,其定义如下:
/**
* @brief 高级定时器更新中断服务函数
* @param 无
* @retval 无
*/
void ATIM_TIMX_NPWM_IRQHandler(void)
{
uint16_t npwm = 0;
/* 以下代码没有使用定时器HAL库共用处理函数来处理,而是直接通过判断中断标志位的方式 */
if(__HAL_TIM_GET_FLAG(&g_timx_npwm_chy_handle, TIM_FLAG_UPDATE) != RESET)
{
if (g_npwm_remain >= 256) /* 还有大于256个脉冲需要发送 */
{
g_npwm_remain = g_npwm_remain - 256;
npwm = 256;
}
else if (g_npwm_remain % 256) /* 还有位数(不到256)个脉冲要发送 */
{
npwm = g_npwm_remain % 256;
g_npwm_remain = 0; /* 没有脉冲了 */
}
if (npwm) /* 有脉冲要发送 */
{
/* 设置RCR值为npwm-1, 即npwm个脉冲 */
ATIM_TIMX_NPWM->RCR = npwm - 1;
/* 产生一次更新事件,在中断里面处理脉冲输出 */
HAL_TIM_GenerateEvent(&g_timx_npwm_chy_handle,
TIM_EVENTSOURCE_UPDATE);
__HAL_TIM_ENABLE(&g_timx_npwm_chy_handle); /* 使能定时器TIMX */
}
else
{
/* 关闭定时器TIMX,使用__HAL_TIM_DISABLE需要失能通道输出,所以不用 */
ATIM_TIMX_NPWM->CR1 &= ~(1 << 0);
}
/* 清除定时器溢出中断标志位 */
__HAL_TIM_CLEAR_IT(&g_timx_npwm_chy_handle, TIM_IT_UPDATE);
}
}
这里我们没有使用HAL库的中断回调机制,而是想寄存器操作一样,直接通过判断中断标志位处理中断。通过__HAL_TIM_GET_FLAG函数宏判断是否发生更新中断,然后进行更新中断的代码处理,最后通过__HAL_TIM_CLEAR_IT函数宏清除更新中断标志位。
因为重复计数器寄存器 (TIM8_RCR)是8位有效的,所以在定时器中断服务函数中首先对全局变量g_npwm_remain(即我们要输出的PWM个数)进行判断,是否大于256,如果大于256,那就得分次写入重复计数器寄存器。写入重复计数寄存器后,需要产生软件更新事件把RCR寄存器的值更新到RCR影子寄存器中,最后一定不要忘记清除定时器更新中断标志位。
在main函数里面编写如下代码:
int main(void)
{
uint8_t key = 0;
uint8_t t = 0;
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(85, 2, 2, 4, 8); /* 设置时钟,170Mhz */
delay_init(170); /* 延时初始化 */
usart_init(115200); /* 串口初始化为115200 */
led_init(); /* 初始化LED */
key_init(); /* 初始化按键 */
atim_timx_npwm_chy_init(5000 - 1, 17000 - 1); /* 2Hz的PWM频率 */
atim_timx_npwm_chy_set(5); /* 输出5个PWM波 */
while (1)
{
key = key_scan(0);
if (key == KEY0_PRES) /* KEY0按下 */
{
atim_timx_npwm_chy_set(5); /* 输出5个PWM波*/
}
t++;
if (t > 50)
{
t = 0;
LED0_TOGGLE(); /* 控制LED0闪烁 */
}
delay_ms(10);
}
}
先看atim_timx_npwm_chy_init(5000 - 1, 17000 - 1);这个语句,这两个形参分别设置自动重载寄存器的值为4999,以及定时器预分频系数为16999。按照sys_stm32_clock_init函数的配置,定时器8的时钟频率等于APB2总线时钟频率,即170MHz,可以得到计数器的计数频率是10KHz。自动重载寄存器的值决定的是PWM周期或频率,计数器计5000个数所用的时间是PWM的周期。在边沿对齐模式下,定时器的溢出周期等于PWM的周期。根据定时器溢出时间计算公式,可得:
Tout= ((arr+1)*(psc+1))/Tclk= ((4999+1)*(16999+1))/170000000=0.5s
再由频率是周期的倒数关系得到PWM的频率为2Hz。
我们在atim_timx_npwm_chy_init这个函数的里面已经配置了比较值为ARR的一半,即PWM的占空比为50%。
初始化完定时器8的PWM输出之后,就输出5个PWM波,然后在while循环检测按键是否按下,如果按键0按下,则输出5个PWM波。
4.3.8.4 下载验证
下载代码后,可以看到LED0不停闪烁,代表程序正在运行。下面我们使用正点原子DS100手持数字示波器,把PC6引脚波形截获,具体如下:

图4.3.8.4.1 PC6引脚波形图
图4.3.8.4.1 中,PWM脉冲数为5个,频率是2HZ,占空比50%,请大家自行测量。