您现在的位置是:首页 >其他 >【正点原子STM32连载】 第二十二章 高级定时器实验 摘自【正点原子】STM32F103 战舰开发指南V1.2网站首页其他

【正点原子STM32连载】 第二十二章 高级定时器实验 摘自【正点原子】STM32F103 战舰开发指南V1.2

正点原子 2024-07-14 00:01:02
简介【正点原子STM32连载】 第二十二章 高级定时器实验 摘自【正点原子】STM32F103 战舰开发指南V1.2

1)实验平台:正点原子stm32f103战舰开发板V4
2)平台购买地址:https://detail.tmall.com/item.htm?id=609294757420
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/thread-340252-1-1.html

第二十二章 高级定时器实验

本章我们主要来学习高级定时器, STM32F103有2个高级定时器(TIM1和TIM8)。我们将通过四个实验来学习高级定时器的各个功能,分别是高级定时器输出指定个数PWM实验、高级定时器输出比较模式实验、高级定时器互补输出带死区控制实验和高级定时器PWM输入模式实验。
本章分为如下几个小节:
22.1 高级定时器简介
22.2 高级定时器输出指定个数PWM实验
22.3 高级定时器输出比较模式实验
22.4 高级定时器互补输出带死区控制实验
22.5 高级定时器PWM输入模式实验

22.1 高级定时器简介

高级定时器的框图和通用定时器框图很类似,只是添加了其它的一些功能,如:重复计数器、带死区控制的互补输出通道、断路输入等。这些功能在高级定时器框图的位置如下:
在这里插入图片描述

图 22.1.1高级定时器框图
上图中,框出来三个部分,这是和通用定时器不同的地方,下面来分别介绍它们。
①重复计数器
在F1系列中,高级定时器TIM1和TIM8都有重复计数器。下面来介绍一下重复计数器有什么作用?在学习基本定时器和通用定时器的时候,我们知道定时器发生上溢或者下溢时,会直接生成更新事件。但是有重复计数器的定时器并不完全是这样的,定时器每次发生上溢或下溢时,重复计数器的值会减一,当重复计数器的值为0时,再发生一次上溢或者下溢才会生成定时器更新事件。如果我们设置重复计数器寄存器RCR的值为N,那么更新事件将在定时器发生N+1次上溢或下溢时发生。
这里需要注意的是重复计数器寄存器是具有影子寄存器的,所以RCR寄存器只是起缓冲的作用。RCR寄存器的值会在更新事件发生时,被转移至其影子寄存器中,从而真正生效。
重复计数器的特性,在控制生成PWM信号时很有用,后面会有相应的实验。
②输出比较
高级定时器输出比较部分和通用定时器相比,多了带死区控制的互补输出功能。图22.1.1第②部分的TIMx_CH1N、TIMx_CH2N和TIMx_CH3N分别是定时器通道1、通道2和通道3的互补输出通道,通道4是没有互补输出通道的。DTG是死区发生器,死区时间由DTG[7:0]位来配置。如果不使用互补通道和死区时间控制,那么高级定时器TIM1和TIM8和通用定时器的输出比较部分使用方法基本一样,只是要注意MOE位得置1定时器才能输出。
如果使用互补通道,那么就有一定的区别了,具体我们在高级定时器互补输出带死区控制实验小节再来介绍。
③断路功能
断路功能也称刹车功能,一般用于电机控制的刹车。F1系列有一个断路通道,断路源可以是刹车输入引脚(TIMx_BKIN),也可以是一个时钟失败事件。时钟失败事件由复位时钟控制器中的时钟安全系统产生。系统复位后,断路功能默认被禁止,MOE位为低。
使能断路功能的方法:将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的状态:由相关控制位状态决定,当使用互补输出时:根据情况自动控制输出电平,参考《STM32F10xxx参考手册_V10(中文版).pdf》手册第245页的表75带刹车功能的互补通道Ocx和OcxN的控制位。
3,BIF位置1,如果使能了BIE位,还会产生刹车中断;如果使能了TDE位,会产生DMA请求。
4,如果AOE位置 1,在下一个 更新事件UEV时,MOE位被自动置 1。
高级定时器框图部分就简单介绍到这里,下面通过实际的实验来学习高级定时器。
22.2 高级定时器输出指定个数PWM实验
要实现定时器输出指定个数PWM,只需要掌握下面几点内容:
第一,如果大家还不清楚定时器是如何输出PWM的,请回顾通用定时器PWM输出实验的内容,这部分的知识是一样的。但是需要注意的是:我们需要把MOE位置1,这样高级定时器的通道才能输出。
第二,要清楚重复计数器特性,设置重复计数器寄存器RCR的值为N,那么更新事件将在定时器发生N+1次上溢或下溢时发生。换句话来说就是,想要指定输出N个PWM,只需要把N-1写入RCR寄存器。因为在边沿对齐模式下,定时器溢出周期对应着PWM周期,我们只要在更新事件发生时,停止输出PWM就行。
第三,为了保证定时器输出指定个数的PWM后,定时器马上停止继续输出,我们使能更新中断,并在定时器中断里关闭计数器。
原理部分我们就讲到这里,下面直接开始寄存器的介绍。
22.2.1 TIM1/TIM8寄存器
下面介绍TIM1/TIM8这些高级定时器中使用到的几个重要的寄存器,其他更多关于定时器的资料可以参考《STM32F10xxx参考手册_V10(中文版).pdf》的第13章。
 控制寄存器 1(TIMx_CR1)
TIM1/TIM8的控制寄存器1描述如图22.2.1.1所示:
在这里插入图片描述

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

图22.2.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,就是设置通道作为输出使用。
 捕获/比较使能寄存器(TIMx_ CCER)
TIM1/TIM8的捕获/比较使能寄存器,该寄存器控制着各个输入输出通道的开关。TIMx_CCER寄存器描述如图22.2.1.3所示:
在这里插入图片描述

图22.2.1.3 TIMx_CCER寄存器
该寄存器比较简单,要让TIM1的CH1输出PWM波,这里我们要使能CC1E位,该位是通道1输入/输出使能位,要想PWM从IO口输出,这个位必须设置为1。CC1P位是设置通道1的输出极性,我们设置0,即OC1 高电平有效。
 事件产生寄存器(TIMx_ EGR)
TIM1/TIM8的事件产生寄存器,该寄存器作用是让用户用软件方式产生各类事件。TIMx_EGR寄存器描述如图22.2.1.4所示:
在这里插入图片描述

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

图22.2.1.5 TIMx_ RCR寄存器
该寄存器的REP[7:0]位是低8位有效,即最大值255。因为这个寄存器只是起缓冲作用,如果大家对该寄存器写入值后,想要立即生效,可以通过对UG位写1,产生软件更新事件。
 捕获/比较寄存器1/2/3/4(TIMx_CCR1/2/3/4)
捕获/比较寄存器(TIMx_CCR1/2/3/4),该寄存器总共有4个,对应4个通道CH1~CH4。我们使用的是通道1,所以来看看TIMx_CCR1寄存器的描述,如图22.2.1.6所示:
在这里插入图片描述

图22.2.1.6 TIMx_ CCR1寄存器
在输出模式下,捕获/比较寄存器影子寄存器的值与CNT的值比较,根据比较结果产生相应动作,利用这点,我们通过修改这个寄存器的值,就可以控制PWM的占空比了。
 断路和死区寄存器(TIMx_ BDTR)
高级定时器TIM1/8的通道用作输出时,还必须配置断路和死区寄存器(TIMx_BDTR)的位MOE,该寄存器各位描述如图22.3.1.7所示:
在这里插入图片描述

图22.3.1.7 TIMx_ BDTR寄存器
本实验,我们只需要关注该寄存器的位15(MOE),要想高级定时器的PWM正常输出,则必须设置MOE位为1,否则不会有输出。
22.2.2 硬件设计

  1. 例程功能
    通过TIM8_CH1(由PC6复用)输出PWM,然后为了指示PWM的输出情况,我们用杜邦线将PC6和PE5引脚的排针连接起来,从而实现PWM输出控制LED1(硬件已连接在PPE5引脚上)的亮灭。注意的点是:PE5要设置成浮空输入,避免引脚冲突,我们在main函数中设置好了,请看源码。上电默认输出5个PWM波,连接好杜邦线后可以看见LED1亮灭五次。之后按一下按键KEY0,就会输出5个PWM波控制LED1亮灭五次。LED0闪烁提示系统正在运行。
  2. 硬件资源
    1)LED灯:
    LED0 – PB5
    LED1 – PE5
    2)独立按键:
    KEY0 – PE4
    3)定时器8,使用TIM8通道1,由 PC6复用。用杜邦线将PC6和PE5引脚连接起来。
  3. 原理图
    定时器属于STM32F103的内部资源,只需要软件设置好即可正常工作。我们通过LED1来指示STM32F103的定时器的PWM输出情况,所以需要用一根杜邦线连接PC6和PE5,同时还用按键KEY0进行控制。
    22.2.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模式,使用杜邦线把PC6与PE5进行连接,因为我们的LED1(连接PE5)是低电平亮,而我们希望输出最后一个PWM波的时候,LED1就灭,所以我们设置输出比较极性为高。捕获/比较寄存器的值(即比较值)设置为自动重装载值的一半,即PWM占空比为50%。
    4)使能定时器更新中断,开启定时器并输出PWM,配置定时器中断优先级
    通过__HAL_TIM_ENABLE_IT函数使能定时器更新中断。
    通过HAL_TIM_PWM_Start函数使能定时器并开启输出PWM。
    通过HAL_NVIC_EnableIRQ函数使能定时器中断。
    通过HAL_NVIC_SetPriority函数设置中断优先级。
    5)编写中断服务函数
    定时器中断服务函数为:TIMx_IRQHandler等,当发生中断的时候,程序就会执行中断服务函数。HAL库提供了一个定时器中断公共处理函数HAL_TIM_IRQHandler,该函数会根据中断类型调用相关的中断回调函数。用户根据自己的需要重定义这些中断回调函数来处理中断程序。本实验我们不使用HAL库的中断回调机制,而是把中断程序写在定时器中断服务函数里。详见本章例程源码。
    22.2.3.1 程序流程图
    在这里插入图片描述

图22.2.3.2.1 高级定时器输出指定个数PWM实验程序流程图
22.2.3.2 程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。高级定时器驱动源码包括两个文件:atim.c和atim.h。本章节的四个实验源码都是存放在atim.c和atim.h中,源码中也有明确的注释。
首先看atim.h头文件的几个宏定义:
/* TIMX 输出指定个数PWM 定义

  • 这里输出的PWM通过PC6(TIM8_CH1)输出, 我们用杜邦线连接PC6和PE5, 然后在程序里面将PE5设* 置成浮空输入就可以 看到TIM8_CH1控制LED1(GREEN)的亮灭, 亮灭一次表示一个PWM波
  • 默认使用的是TIM8_CH1.
  • 注意: 通过修改这几个宏定义, 可以支持TIM1/TIM8定时器, 任意一个IO口输出指定个数的PWM
 */
#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口时钟使能 */
#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 /* 通道Y,  1<= Y <=4 */
#define ATIM_TIMX_NPWM_CHY_CCRX            	TIM8->CCR1/* 通道Y的输出比较寄存器 */
#define ATIM_TIMX_NPWM_CHY_CLK_ENABLE()  	do{ __HAL_RCC_TIM8_CLK_ENABLE(); 
                                                   }while(0) 

可以把上面的宏定义分成两部分,第一部分是定时器8输入通道1对应的IO口的宏定义,第二部分则是定时器8输入通道1的相应宏定义。
下面看atim.c的程序,首先是输出指定个数PWM初始化函数,其定义如下:

/**
 * @brief       高级定时器TIMX 通道Y 输出指定个数PWM 初始化函数
 * @note
 *              高级定时器的时钟来自APB2, 而PCLK2 = 72Mhz, 我们设置PPRE2不分频, 因此
 *              高级定时器时钟 = 72Mhz
 *              定时器溢出时间计算方法: 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;
    TIM_OC_InitTypeDef timx_oc_npwm_chy;   /* 定时器输出 */
    ATIM_TIMX_NPWM_CHY_GPIO_CLK_ENABLE(); /* TIMX 通道IO口时钟使能 */
    ATIM_TIMX_NPWM_CHY_CLK_ENABLE();       /* TIMX 时钟使能 */

    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; /*使能TIMx_ARR进行缓冲 */
    g_timx_npwm_chy_handle.Init.RepetitionCounter = 0; /* 重复计数器初始值 */
    HAL_TIM_PWM_Init(&g_timx_npwm_chy_handle);           /* 初始化PWM */

    gpio_init_struct.Pin = ATIM_TIMX_NPWM_CHY_GPIO_PIN;/* 通道y的CPIO口 */
    gpio_init_struct.Mode = GPIO_MODE_AF_PP;             /* 复用推完输出 */
    gpio_init_struct.Pull = GPIO_PULLUP;                  /* 上拉 */
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;      /* 高速 */
    HAL_GPIO_Init(ATIM_TIMX_NPWM_CHY_GPIO_PORT, &gpio_init_struct);

    timx_oc_npwm_chy.OCMode = TIM_OCMODE_PWM1; /* 模式选择PWM 1*/
    timx_oc_npwm_chy.Pulse = arr / 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 */
    /* 设置中断优先级,抢占优先级1,子优先级3 */
    HAL_NVIC_SetPriority(ATIM_TIMX_NPWM_IRQn, 1, 3);                
    HAL_NVIC_EnableIRQ(ATIM_TIMX_NPWM_IRQn);         /* 开启ITMx中断 */

    __HAL_TIM_ENABLE_IT(&g_timx_npwm_chy_handle, TIM_IT_UPDATE);/* 允许更新中断 */
    HAL_TIM_PWM_Start(&g_timx_npwm_chy_handle, ATIM_TIMX_NPWM_CHY);/* 使能输出 */
}

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相关的代码,而是全部存放在gtim_timx_npwm_chy_init函数中。
下面我们看设置PWM个数的函数,其定义如下:

/* g_npwm_remain表示当前还剩下多少个脉冲要发送 
 * 每次最多发送256个脉冲
 */
static uint32_t g_npwm_remain = 0;

/**
 * @brief       高级定时器TIMX NPWM设置PWM个数
 * @param       rcr: PWM的个数, 1~2^32次方个
 * @retval      无
 */
void atim_timx_npwm_chy_set(uint32_t npwm)
{
    if (npwm == 0)return ;
g_npwm_remain = npwm;                           /* 保存脉冲个数 */
/* 产生一次更新事件,在中断里面处理脉冲输出 */
    HAL_TIM_GenerateEvent(&g_timx_npwm_chy_handle, TIM_EVENTSOURCE_UPDATE); 
    __HAL_TIM_ENABLE(&g_timx_npwm_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)                        /* 有脉冲要发送 */
        { 
            ATIM_TIMX_NPWM->RCR = npwm - 1; /* 设置RCR值为npwm-1, 即npwm个脉冲 */
            HAL_TIM_GenerateEvent(&g_timx_npwm_chy_handle, 
TIM_EVENTSOURCE_UPDATE); /* 产生一次更新事件,以更新RCR寄存器 */
            __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;
GPIO_InitTypeDef gpio_init_struct;

    HAL_Init();                              /* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9);	/* 设置时钟, 72Mhz */
    delay_init(72);                        	/* 延时初始化 */
    usart_init(115200);                  	/* 串口初始化为115200 */
    led_init();                            	/* 初始化LED */
key_init();                            	/* 初始化按键 */

/* 将 LED1 引脚设置为输入模式, 避免和PC6冲突 */
    gpio_init_struct.Pin = LED1_GPIO_PIN;               	/* LED1 引脚 */
    gpio_init_struct.Mode = GPIO_MODE_INPUT;            	/* 设置输入状态 */
    gpio_init_struct.Pull = GPIO_PULLUP;                 	/* 上拉 */
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_MEDIUM;   	/* 中速 */
    HAL_GPIO_Init(LED1_GPIO_PORT, &gpio_init_struct);  	/* 初始化LED1引脚 */

    atim_timx_npwm_chy_init(5000 - 1, 7200 - 1);/*10Khz的计数频率,2hz的PWM频率*/
/* 设置PWM占空比,50%,这样可以控制每一个PWM周期,LED1(BLUE) 有一半时间是亮的,
一半时间是灭的,LED1亮灭一次,表示一个PWM波 */
ATIM_TIMX_NPWM_CHY_CCRX = 2500; 
atim_timx_npwm_chy_set(5);    /* 输出5个PWM波(控制LED1)闪烁5次) */

    while (1)
    {
        key = key_scan(0);
        if (key == KEY0_PRES)   /* KEY0按下 */
        {
            gtim_timx_npwm_chy_set(5);	/* 输出5个PWM波(控制LED1闪烁5次) */
        }
        t++;
        delay_ms(10);
        if (t > 50)                   		/* 控制LED1闪烁, 提示程序运行状态 */
        {
            t = 0;
            LED0_TOGGLE();
        }
    }
}

先看gtim_timx_npwm_chy_init(5000 - 1, 7200 - 1);这个语句,这两个形参分别设置自动重载寄存器的值为4999,以及预分频器寄存器的值为7199。按照sys_stm32_clock_init函数的配置,定时器8的时钟频率等于APB2总线时钟频率,即72MHz,可以得到计数器的计数频率是10KHz。自动重载寄存器的值决定的是PWM周期或频率(请回顾21.3小节的内容),计数器计5000个数所用的时间是PWM的周期。在边沿对齐模式下,定时器的溢出周期等于PWM的周期。根据定时器溢出时间计算公式,可得:
Tout= ((arr+1)(psc+1))/Tclk= ((4999+1)(7199+1))/72000000=0.5s
再由频率是周期的倒数关系得到PWM的频率为2Hz。
占空比则由捕获/比较寄存器(TIMx_CCRx)的值决定,这里就是由TIM8_CCR1寄存器决定。初始化定时器8时我们设置通道输出比较极性为高,GTIM_TIMX_NPWM_CHY_CCRX = 2500,就设置了占空比为50%。因为我们的LED灯是低电平点亮,所以正占空比期间LED灯熄灭,负占空比期间LED灯亮。
22.2.4 下载验证
首先用杜邦线连接好PE5和PC6引脚的排针。下载代码后,可以看到LED1亮灭五次,然后我们每按一下按键KEY0,LED1都会亮灭五次。
下面我们使用正点原子DS100手持数字示波器,把PC6引脚的波形截获,具体如下:
在这里插入图片描述

图22.2.4 PC6引脚波形图
由LED的原理图可以知道,PC6引脚输出低电平LED1亮、输出高电平LED1灭。图22.2.4中,从左往右看,可以知道,LED0一开始是熄灭的,然后经过5次亮灭,最后就是一直保持熄灭的状态。PWM频率是2Hz,占空比50%,请大家自行测量。
在这里插入图片描述

22.3 高级定时器输出比较模式实验
本小节我们来学习使用高级定时器输出比较模式下翻转功能,通过定时器4个通道分别输出4个50%占空比、不同相位的PWM。
输出比较模式下翻转功能作用是:当计数器的值等于捕获/比较寄存器影子寄存器的值时,OC1REF 发生翻转,进而控制通道输出(OCx)翻转。通过翻转功能实现输出PWM的具体原理如下:PWM频率由自动重载寄存器(TIMx_ARR)的值决定,在这个过程中,只要自动重载寄存器的值不变,那么PWM占空比就固定为50%。我们可以通过捕获/比较寄存器(TIMx_CCRx)的值改变PWM的相位。生成PWM的原理如图22.3.1所示:
在这里插入图片描述

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

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

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

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

图22.3.1.4 TIMx_ CCR1寄存器
这里,我们通过改变TIMx_ CCR1/2/3/4寄存器的值来改变4个通道输出的PWM的相位。
 TIM1/TIM8断路和死区寄存器(TIMx_ BDTR)
本实验用的是高级定时器,我们还需要配置:断路和死区寄存器(TIMx_BDTR),该寄存器各位描述如图22.3.1.5所示。
在这里插入图片描述

图22.3.1.5 TIMx_ BDTR寄存器
该寄存器,我们只需要关注位15(MOE),要想高级定时器的通道正常输出,则必须设置MOE位为1,否则不会有输出。
22.3.2 硬件设计

  1. 例程功能
    使用输出比较模式的翻转功能,通过定时器8的4路通道输出占空比固定为50%、相位分别是25%、50%、75%和100%的PWM。
  2. 硬件资源
    1)LED灯
    LED0 – PB5
    2)PC6复用为TIM8_CH1
    PC7复用为TIM8_CH2
    PC8复用为TIM8_CH3
    PC9复用为TIM8_CH4
  3. 原理图
    定时器属于STM32F103的内部资源,只需要软件设置好即可正常工作。我们需要通过示波器观察PC6、PC7、PC8和PC9引脚PWM输出的情况。
    22.3.3 程序设计
    22.3.3.1 定时器的HAL库驱动
    定时器在HAL库中的驱动代码在前面已经介绍了部分,请回顾,这里我们再介绍几个本实验用到的函数。
  4. HAL_TIM_OC_Init函数
    定时器的输出比较模式初始化函数,其声明如下:
    HAL_StatusTypeDef HAL_TIM_OC_Init(TIM_HandleTypeDef *htim);
    函数描述:
    用于初始化定时器的输出比较模式。
    函数形参:
    形参1是TIM_HandleTypeDef结构体类型指针变量,基本定时器的时候已经介绍。
    函数返回值:
    HAL_StatusTypeDef枚举类型的值。
  5. HAL_TIM_OC_ConfigChannel函数
    定时器的输出比较通道设置初始化函数。其声明如下:
    HAL_StatusTypeDef HAL_TIM_OC_ConfigChannel(TIM_HandleTypeDef *htim,
    TIM_OC_InitTypeDef *sConfig, uint32_t Channel);
    函数描述:
    该函数用于初始化定时器的输出比较通道。
    函数形参:
    形参1是TIM_HandleTypeDef结构体类型指针变量,用于配置定时器基本参数。
    形参2是TIM_OC_InitTypeDef结构体类型指针变量,用于配置定时器的输出比较参数。
    在通用定时器PWM输出实验已经介绍过TIM_OC_InitTypeDef结构体指针类型。
    形参3是定时器通道,范围:TIM_CHANNEL_1到TIM_CHANNEL_4。
    函数返回值:
    HAL_StatusTypeDef枚举类型的值。
  6. HAL_TIM_OC_Start函数
    定时器的输出比较启动函数,其声明如下:
    HAL_StatusTypeDef HAL_TIM_OC_Start(TIM_HandleTypeDef *htim, uint32_t Channel);
    函数描述:
    用于启动定时器的输出比较模式。
    函数形参:
    形参1是TIM_HandleTypeDef结构体类型指针变量。
    形参2是定时器通道,范围:TIM_CHANNEL_1到TIM_CHANNEL_4。
    函数返回值:
    HAL_StatusTypeDef枚举类型的值。
    注意事项:
    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、4,对应IO是PC6PC7PC8PC9,它们的时钟开启方法如下:
    __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函数使能定时器并开启输出。
    22.3.3.2 程序流程图
    在这里插入图片描述

图22.3.3.2.1 高级定时器输出比较模式实验程序流程图
22.3.3.3 程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。高级定时器驱动源码包括两个文件:atim.c和atim.h。
首先看atim.h头文件的几个宏定义:
/* TIMX 输出比较模式 定义

  • 这里通过TIM8的输出比较模式,控制PC6,PC7,PC8,PC9输出4路PWM,占空比50%,并且每一路PWM
  • 之间的相位差为25%,修改CCRx可以修改相位. 默认是针对TIM8
  • 注意: 通过修改这些宏定义,可以支持TIM1/TIM8任意一个定时器,任意一个IO口使用输出比较模式,
* 输出PWM
 */
#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口时钟使能 */

#define ATIM_TIMX_COMP_CH4_GPIO_PORT            GPIOC
#define ATIM_TIMX_COMP_CH4_GPIO_PIN             GPIO_PIN_9
#define ATIM_TIMX_COMP_CH3_GPIO_CLK_ENABLE()  
do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0)   /* PC口时钟使能 */

#define ATIM_TIMX_COMP             TIM8 
#define ATIM_TIMX_COMP_CH1_CCRX  ATIM_TIMX_COMP->CCR1  /* 通道1的输出比较寄存器 */
#define ATIM_TIMX_COMP_CH2_CCRX  ATIM_TIMX_COMP->CCR2  /* 通道2的输出比较寄存器 */
#define ATIM_TIMX_COMP_CH3_CCRX  ATIM_TIMX_COMP->CCR3  /* 通道3的输出比较寄存器 */
#define ATIM_TIMX_COMP_CH4_CCRX  ATIM_TIMX_COMP->CCR4  /* 通道4的输出比较寄存器 */
#define ATIM_TIMX_COMP_CLK_ENABLE()     
do{ __HAL_RCC_TIM8_CLK_ENABLE();}while(0)    /* TIM8 时钟使能 */ 

可以把上面的宏定义分成两部分,第一部分是定时器1输出通道1~通道4对应的IO口的宏定义。第二部分则是定时器8的相应宏定义。
下面来看到atim.c文件的程序,首先是高级定时器输出比较模式初始化函数,其定义如下:

/**
 * @brief       高级定时器TIMX 输出比较模式 初始化函数(使用输出比较模式)
 * @note
 *              配置高级定时器TIMX 4路输出比较模式PWM输出,实现50%占空比,不同相位控制
 *              注意,本例程输出比较模式,每2个计数周期才能完成一个PWM输出,因此输出频率减半
 *              另外,我们还可以开启中断在中断里面修改CCRx,从而实现不同频率/不同相位的控制
 *              但是我们不推荐这么使用,因为这可能导致非常频繁的中断,从而占用大量CPU资源
 *
 *              高级定时器的时钟来自APB2, 而PCLK2 = 72Mhz, 我们设置PPRE2不分频, 因此
 *              高级定时器时钟 = 72Mhz
 *              定时器溢出时间计算方法: 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 timx_oc_comp_pwm = {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);            /* 输出比较模式初始化 */

    timx_oc_comp_pwm.OCMode = TIM_OCMODE_TOGGLE;        /* 比较输出模式翻转功能 */
    timx_oc_comp_pwm.Pulse = 250 - 1;                     /* 设置输出比较寄存器的值 */
    timx_oc_comp_pwm.OCPolarity = TIM_OCPOLARITY_HIGH;/* 输出比较极性为高 */
HAL_TIM_OC_ConfigChannel(&g_timx_comp_pwm_handle, &timx_oc_comp_pwm, 
TIM_CHANNEL_1);   /* 初始化定时器的输出比较通道1 */
/* CCR1寄存器预装载使能 */
    __HAL_TIM_ENABLE_OCxPRELOAD(&g_timx_comp_pwm_handle, TIM_CHANNEL_1); 

    tim_oc_handle.Pulse = 500;
HAL_TIM_OC_ConfigChannel(&g_timx_comp_pwm_handle, &tim_oc_handle, 
TIM_CHANNEL_2); /* 初始化定时器的输出比较通道2 */
/* CCR2寄存器预装载使能 */
    __HAL_TIM_ENABLE_OCxPRELOAD(&g_timx_comp_pwm_handle, TIM_CHANNEL_2); 

    tim_oc_handle.Pulse = 750;
HAL_TIM_OC_ConfigChannel(&g_timx_comp_pwm_handle, &tim_oc_handle, 
TIM_CHANNEL_3); /* 初始化定时器的输出比较通道3 */
/* CCR3寄存器预装载使能 */
    __HAL_TIM_ENABLE_OCxPRELOAD(&g_timx_comp_pwm_handle, TIM_CHANNEL_3); 
    tim_oc_handle.Pulse = 1000;
HAL_TIM_OC_ConfigChannel(&g_timx_comp_pwm_handle, &tim_oc_handle, 
TIM_CHANNEL_4); /* 初始化定时器的输出比较通道4 */
/* CCR4寄存器预装载使能 */   
 __HAL_TIM_ENABLE_OCxPRELOAD(&g_timx_comp_pwm_handle, TIM_CHANNEL_4); 

    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);
    HAL_TIM_OC_Start(&g_timx_comp_pwm_handle, TIM_CHANNEL_4);
}

在atim_timx_comp_pwm_init 函数中,首先调用HAL_TIM_OC_Init函数初始化定时器的ARR和PSC等参数。然后通过调用函数HAL_TIM_OC_ConfigChannel设置通道1通道4的工作参数,包括:输出比较模式功能、输出比较寄存器的值,输出极性等。接着调用__HAL_TIM_ENABLE_OCxPRELOAD函数宏使能CCR1/2/3/4寄存器的预装载。最后通过调用函数HAL_TIM_OC_Start来使能TIM1通道1通道4输出。
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();

        ATIM_TIMX_COMP_CH1_GPIO_CLK_ENABLE();
        ATIM_TIMX_COMP_CH2_GPIO_CLK_ENABLE();
        ATIM_TIMX_COMP_CH3_GPIO_CLK_ENABLE();
        ATIM_TIMX_COMP_CH4_GPIO_CLK_ENABLE();

        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;
        HAL_GPIO_Init(ATIM_TIMX_COMP_CH1_GPIO_PORT, &gpio_init_struct);

        gpio_init_struct.Pin = ATIM_TIMX_COMP_CH2_GPIO_PIN;
        HAL_GPIO_Init(ATIM_TIMX_COMP_CH2_GPIO_PORT, &gpio_init_struct);

        gpio_init_struct.Pin = ATIM_TIMX_COMP_CH3_GPIO_PIN;
        HAL_GPIO_Init(ATIM_TIMX_COMP_CH3_GPIO_PORT, &gpio_init_struct);

        gpio_init_struct.Pin = ATIM_TIMX_COMP_CH4_GPIO_PIN;
        HAL_GPIO_Init(ATIM_TIMX_COMP_CH4_GPIO_PORT, &gpio_init_struct);
    }
}

该函数主要是使能定时器和通道对应的IO时钟,初始化IO口。
在main.c里面编写如下代码:

int main(void)
{
    uint8_t t = 0;
    HAL_Init();             						/* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9);			/* 设置时钟, 72Mhz */
    delay_init(72);                          		/* 延时初始化 */
    usart_init(115200);                     		/* 串口初始化为115200 */
    led_init();                              		/* 初始化LED */
    atim_timx_comp_pwm_init(1000 - 1, 72 - 1);	/* 1Mhz的计数频率 1Khz的周期. */
    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% */
    ATIM_TIMX_COMP_CH4_CCRX = 999 - 1; 			/* 通道4 相位99% */
    while (1)
    {
        delay_ms(10);
        t++;
        if (t >= 20)
        {
            LED0_TOGGLE(); /* LED0(RED)闪烁 */
            t = 0;
        }
    }
}

本小节开头我们讲解了输出比较模式翻转功能如何产生PWM波,下面结合程序一起计算出PWM波的周期,频率等参数。
定时器8时钟源的时钟频率等于APB2总线时钟频率,即72MHz,而调用atim_timx_comp_pwm_init(1000 - 1, 72 - 1)初始化函数之后,就相当于写入预分频寄存器的值为71,写入自动重载寄存器的值为999。将这些参数代入本小节介绍的翻转功能输出的PWM周期计算公式,可得:
T = 2*(arr+1)((psc+1)/ Tclk) = 2(999+1)*((71+1)/ 72000000) = 0.002s
由上述式子得到PWM周期为2ms,频率为500Hz。ARR值为固定为1000,所以占空比则固定为50%。定时器8通道1~通道4输出的PWM波的相位分别是:25%、50%、75%、100%。
22.3.4 下载验证
下载代码后,可以看到LED0在闪烁,说明程序已经正常在跑了。 我们需要借助示波器观察PC6、PC7、PC8和PC9引脚PWM输出的情况,如下图所示:
在这里插入图片描述

图22.3.4.1 相位为25%、50%、75%、100%的PWM波
图22.3.4.1中,由上到下分别是引脚PE9、PE11、PE13和PE14输出的PWM,即分别对应的是TIM8_CH1、TIM8_CH2、TIM8_CH3和TIM8_CH4输出的相位为25%、50%、75%和100%的PWM。大家可以把其中一个通道的捕获/比较寄存器的值设置为0,那么就可以得到PWM初相位的波形,即相位为0%。

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

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

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

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

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

其中:
CKD[1:0]:CKD[1:0]位设置的值。
Tclk:定时器的时钟源频率(单位为MHz)。
假设定时器时钟源频率是72MHz,我们设置CKD[1:0]位的值为2,代入上面的式子可得:
在这里插入图片描述

通过上式可得tDTS约等于55.56ns,本实验例程中我们也是这样设置的。
第二步:根据DTG[7:5]选择计算公式。
第三步:代入选择的公式计算。
下面给大家举个例子,假设定时器时钟源频率是72MHz,我们设置CKD[1:0]位的值为2,DTG[7:0]位的值为250。从上面的例子知道CKD[1:0]位的值为2,得到的tDTS=55.56ns。下面来看一下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 * 55.56ns = 51559.68ns = 51.56us,即死区时间为51.56us。死区时间计算方法就给大家介绍到这里。
关于互补输出和死区插入的更多内容请看《STM32F10xxx参考手册_V10(中文版).pdf》手册的13.3.11小节,下面我们介绍相关的寄存器。
22.4.1 TIM1/TIM8寄存器
高级定时器互补输出带死区控制除了用到定时器的时基单元:计数器寄存器(TIMx_CNT)、预分频器寄存器(TIMx_PSC)、自动重载寄存器(TIMx_ARR) 之外。主要还用到以下这些寄存器:
 控制寄存器 1(TIMx_CR1)
TIM1/TIM8的控制寄存器1描述如图22.4.1.1所示:
在这里插入图片描述

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

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

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

图22.4.1.4 TIMx_ CCR1寄存器
该寄存器16位有效位,本实验中可以通过改变该寄存器的值来改变PWM的占空比。
 断路和死区寄存器(TIMx_ BDTR)
TIM1/TIM8断路和死区寄存器,该寄存器各位描述如图22.4.1.5所示:
在这里插入图片描述

图22.4.1.5 TIMx_ BDTR寄存器
该寄存器控制定时器的断路和死区控制的功能。我们先看断路控制,用到断路输入功能(断路输入引脚为PE15),位BKE置1即可。
位BKP选择断路输入信号有效电平。本实验中,我们选择高电平有效,即BKP置1。
位AOE是自动输出使能位,如果使能AOE位,那么在我们输入刹车信号后再断开了刹车信号,互补的PWM会自动恢复输出,如果失能AOE位,那么在输入刹车信号后再断开了刹车信号,互补的PWM就不会恢复输出,而是一直保持刹车信号输入时的状态。为了方便观察,我们使能该位,即置1。
位MOE是使能主输出,想要高级定时器的通道正常输出,则必须设置MOE位为1。
最后是DTG[7:0]位,用于设置死区时间,前面已经教过大家怎么设置了。这里以我们例程的设置为例,CKD[1:0] 设置为10,定时器时钟源频率是72MHz,所以tDTS = 55.56ns。
本例程的DTG[7:0]位的值设置为十进制100,即二进制数0110 0100。DTG[7:5]=011,符合第一条式子:DT=DTG[7:0] * t dtg,其中 t dtg = tDTS。DT是死区时间,可以得到DT = 100*55.56 ns = 5. 56us。到后面下载验证小节,我们通过示波器验证一下这个死区时间计算的理论值和实际值是否一样。
22.4.2 硬件设计

  1. 例程功能
    1,利用TIM1_CH1(PE9)输出70%占空比的PWM波,它的互补输出通道(PE8)则是输出30%占空比的PWM波。
    2,刹车功能,当给刹车输入引脚(PE15)输入高电平时,进行刹车,即PE8和PE9停止输出PWM波。
    3,LED0闪烁指示程序运行。
  2. 硬件资源
    1)LED灯
    LED0 – PB5
    2)定时器1
    TIM1正常输出通道 PE9
    TIM1互补输出通道 PE8
    TIM1刹车输入 PE15
  3. 原理图
    定时器属于STM32F103的内部资源,只需要软件设置好即可正常工作。我们需要通过示波器观察PE8和PE9引脚PWM输出的情况。还可以通过给PE15引脚接入高电平进行刹车。
    22.4.3 程序设计
    22.4.3.1 定时器的HAL库驱动
    定时器在HAL库中的驱动代码在前面已经介绍了部分,这里我们再介绍几个本实验用到的函数。
  4. HAL_TIMEx_ConfigBreakDeadTime函数
    定时器的断路和死区时间配置初始化函数,其声明如下:
    HAL_StatusTypeDef HAL_TIMEx_ConfigBreakDeadTime(TIM_HandleTypeDef *htim,
    TIM_BreakDeadTimeConfigTypeDef *sBreakDeadTimeConfig);
    函数描述:
    用于初始化定时器的断路(即刹车)和死区时间。
    函数形参:
    形参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;

函数返回值:
HAL_StatusTypeDef枚举类型的值。
2. HAL_TIMEx_PWMN_Start函数
定时器的互补输出启动函数。其声明如下:
HAL_StatusTypeDef HAL_TIMEx_PWMN_Start(TIM_HandleTypeDef htim,
uint32_t Channel);
函数描述:
该函数用于启动定时器的互补输出。
函数形参:
形参1是TIM_HandleTypeDef结构体类型指针变量,用于配置定时器基本参数。
形参2是定时器通道,范围:TIM_CHANNEL_1到TIM_CHANNEL_4。
函数返回值:
HAL_StatusTypeDef枚举类型的值。
定时器互补输出带死区控制配置步骤
1)开启TIMx和通道输出以及刹车输入的GPIO时钟,配置该IO口的复用功能输出
首先开启TIMx的时钟,然后配置GPIO为复用功能输出。本实验我们默认用到定时器1通道1,对应IO是PE9,互补输出通道引脚是PE8,刹车输入引脚是PE15,它们的时钟开启方法如下:
__HAL_RCC_TIM1_CLK_ENABLE(); /
使能定时器1 /
__HAL_RCC_GPIOE_CLK_ENABLE(); /
开启GPIOE时钟 */
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互补输出。
22.4.3.2 程序流程图
在这里插入图片描述

图22.4.3.2.1 高级定时器互补输出带死区控制实验
22.4.3.3 程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。高级定时器驱动源码包括两个文件:atim.c和atim.h。
首先看atim.h头文件的几个宏定义:

/*****************************************************************************/
/* TIMX 互补输出模式 定义 
 * 这里设置互补输出相关硬件配置, CHY即正常输出, CHYN即互补输出
 * 修改CCRx可以修改占空比.
 * 默认是针对TIM1
 * 注意: 通过修改这些宏定义,可以支持TIM1/TIM8定时器, 任意一个IO口输出互补PWM(前提是必须有互补输出功能)
 */

/* 输出通道引脚 */
#define ATIM_TIMX_CPLM_CHY_GPIO_PORT            GPIOE
#define ATIM_TIMX_CPLM_CHY_GPIO_PIN             GPIO_PIN_9
#define ATIM_TIMX_CPLM_CHY_GPIO_CLK_ENABLE()    
do{ __HAL_RCC_GPIOE_CLK_ENABLE(); }while(0)   /* PE口时钟使能 */

/* 互补输出通道引脚 */
#define ATIM_TIMX_CPLM_CHYN_GPIO_PORT           GPIOE
#define ATIM_TIMX_CPLM_CHYN_GPIO_PIN            GPIO_PIN_8
#define ATIM_TIMX_CPLM_CHYN_GPIO_CLK_ENABLE()   
do{ __HAL_RCC_GPIOE_CLK_ENABLE(); }while(0)   /* PE口时钟使能 */

/* 刹车输入引脚 */
#define ATIM_TIMX_CPLM_BKIN_GPIO_PORT           GPIOE
#define ATIM_TIMX_CPLM_BKIN_GPIO_PIN            GPIO_PIN_15
#define ATIM_TIMX_CPLM_BKIN_GPIO_CLK_ENABLE()   
do{ __HAL_RCC_GPIOE_CLK_ENABLE(); }while(0)   /* PE口时钟使能 */

/* TIMX REMAP设置
 * 因为PE8/PE9/PE15, 默认并不是TIM1的复用功能脚, 必须开启完全重映射, 才可以将: 
TIM1_CH1->PE9; TIM1_CH1N->PE8; TIM1_BKIN->PE15;
 * 这样, PE8/PE9/PE15, 才能用作TIM1的CH1N/CH1/BKIN功能.
 * 所以必须实现ATIM_TIMX_CPLM_CHYN_GPIO_REMAP, 通过sys_gpio_remap_set函数设置重映射
 * 如果我们使用默认的复用功能输出, 则不用设置重映射, 是可以不需要该函数的! 根据具体需要来实现.
 */
#define ATIM_TIMX_CPLM_CHYN_GPIO_REMAP()  do{__HAL_RCC_AFIO_CLK_ENABLE();
                                                     __HAL_AFIO_REMAP_TIM1_ENABLE();
                                                  }while(0)  
/* 互补输出使用的定时器 */
#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的相应宏定义。注意:因为PE8/PE9/PE15, 默认并不是TIM1的复用功能脚, 必须开启完全重映射,具体请参考《STM32F10xxx参考手册_V10(中文版).pdf》第123页,AFIO_MAPR寄存器的描述。
下面来看atim.c文件的程序,首先是高级定时器互补输出初始化函数,其定义如下:

/**
 * @brief      高级定时器TIMX 互补输出 初始化函数(使用PWM模式1)
 * @note
 *              配置高级定时器TIMX 互补输出, 一路OCy 一路OCyN, 并且可以设置死区时间
 *
 *              高级定时器的时钟来自APB2, 而PCLK2 = 72Mhz, 我们设置PPRE2不分频, 因此
 *              高级定时器时钟 = 72Mhz
 *              定时器溢出时间计算方法: 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();              /* TIMx 时钟使能 */
    ATIM_TIMX_CPLM_CHY_GPIO_CLK_ENABLE();   /* 通道X对应IO口时钟使能 */
    ATIM_TIMX_CPLM_CHYN_GPIO_CLK_ENABLE();  /* 通道X互补通道对应IO口时钟使能 */
    ATIM_TIMX_CPLM_BKIN_GPIO_CLK_ENABLE();  /* 通道X刹车输入对应IO口时钟使能 */
  
    gpio_init_struct.Pin = ATIM_TIMX_CPLM_CHY_GPIO_PIN;
    gpio_init_struct.Mode = GPIO_MODE_AF_PP; 
    gpio_init_struct.Pull = GPIO_PULLUP;
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH ;
    HAL_GPIO_Init(ATIM_TIMX_CPLM_CHY_GPIO_PORT, &gpio_init_struct);

    gpio_init_struct.Pin = ATIM_TIMX_CPLM_CHYN_GPIO_PIN;
    HAL_GPIO_Init(ATIM_TIMX_CPLM_CHYN_GPIO_PORT, &gpio_init_struct);

    gpio_init_struct.Pin = ATIM_TIMX_CPLM_BKIN_GPIO_PIN;
    HAL_GPIO_Init(ATIM_TIMX_CPLM_BKIN_GPIO_PORT, &gpio_init_struct);
    
    ATIM_TIMX_CPLM_CHYN_GPIO_REMAP(); /* 重映射定时器IO */

    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 = 18Mhz */
    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);

    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 低电平有效 */
    tim_oc_cplm_pwm.OCIdleState = TIM_OCIDLESTATE_SET;   /* 当MOE=0,OCx=1 */
    tim_oc_cplm_pwm.OCNIdleState = TIM_OCNIDLESTATE_SET;/* 当MOE=0,OCxN=1 */
HAL_TIM_PWM_ConfigChannel(&g_timx_cplm_pwm_handle, &tim_oc_cplm_pwm, 
ATIM_TIMX_CPLM_CHY);
/* 设置死区参数,开启死区中断 */
/* 运行模式的关闭输出状态 */
g_sbreak_dead_time_config.OffStateRunMode = TIM_OSSR_DISABLE;    
/* 空闲模式的关闭输出状态 */
    g_sbreak_dead_time_config.OffStateIDLEMode = TIM_OSSI_DISABLE;   
    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_HIGH;       
/* 使能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 / 18M = 55.56ns
 * @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)
{
uint8_t t = 0;
    HAL_Init();                                 		/* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9);    		/* 设置时钟, 72Mhz */
    delay_init(72);                            		/* 延时初始化 */
    usart_init(115200);                       		/* 串口初始化为115200 */
    led_init();                                 		/* 初始化LED */
    atim_timx_cplm_pwm_init(1000 - 1, 72 - 1);	/* 1Mhz的计数频率 1Khz的周期. */
atim_timx_cplm_pwm_set(300, 100);          	/* 占空比:7:3,死区时间100 * tDTS */

    while (1)
    {
        delay_ms(10);
        t++;
        if (t >= 20)
        {
            LED0_TOGGLE(); 						/* LED0(RED)闪烁 */
            t = 0;
        }
    }
}

先看atim_timx_cplm_pwm_init(1000 - 1, 72 - 1)这个语句,这两个形参分别设置自动重载寄存器的值为999,以及定时器预分频器寄存器的值为71。先看预分频系数,我们设置为72分频,定时器1的时钟源频率等于APB2总线时钟频率,即72MHz,可以得到计数器的计数频率是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%,死区时间为5.56us。根据PWM生成原理分析,再结合图21.3.2产生PWM示意图,以及我们在atim_timx_cplm_pwm_init函数配置PWM模式1、OCy输出极性为低,占空比的计算很简单,可以由(1000-300)/1000得到。关于死区时间的计算方法,前面已经讲解过,这里以DTG[7:0]的值为100为例,再来讲解一遍计算过程。由前面讲解的内容知道,我们例程配置CKD[1:0]位的值为2,可以得到tDTS= 55.56ns。基于这个前提,通过改变DTG[7:0]的值,可以得到不同的死区时间。这里我们配置DTG[7:0]的值为100,即二进制数0110 0100,符合第一种情况dtg[7:5]=0xx时,死区时间DT = DTG [7:0] * tDTS。可以得到死区时间DT = 100*55.56 ns = 5.56us。
下面我们下载到开发板子验证一下。
22.4.4 下载验证
下载代码后,可以看到LED0在闪烁,说明程序已经正常在跑了。 我们需要借助示波器观察PE9正常输出和PE8互补输出PWM的情况,示波器显示截图如图22.4.4.1所示:
在这里插入图片描述

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

图22.4.4.2 修改程序后PE9正常输出和PE8互补输出PWM的情况
由图22.4.4.2可得到,死区时间 =(751– 648)/2 us= 51.5us。与我们理论到的的值51.56us也是差不多的,误差在正常范围。由此证明我们的死区时间设置是没有问题。
刹车功能验证:当给刹车输入引脚(PE15)接入高电平(这里直接用杜邦线连接PE15到3.3V)时,就会进行刹车,即PE9和PE8停止输出PWM波,如图22.4.4.3所示:
刹车功能验证:当给刹车输入引脚(PE15)接入高电平(这里直接用杜邦线把PE15连接到3.3V)时,就会进行刹车,MOE位被硬件清零。由《STM32F10xxx参考手册_V10(中文版).pdf》第245页 表75可以知道刹车信号输入后,如果存在时钟,经过死区后OCx=OISx 且 OCxN=OISxN。在atim_timx_cplm_pwm_init函数中,我们设置当MOE=0时,OCx=1、OCxN=1,即PE9和PE8都是输出高电平。下面通过示波器来验证一下,如图22.4.4.3所示:
在这里插入图片描述

图22.4.4.3 刹车后的输出情况
从上图可以看到PE9和PE8输出的都是高电平,符合我们预期的设置。
另外因为我们使能了AOE位(即把该位置1),如果刹车输入为无效极性时,MOE位在发生下一个更新事件时自动置1,恢复运行模式(即继续输出PWM)。因此当停止给PE15接入低电平(拔掉之前连接的杜邦线),PWM会自动恢复输出。

22.5 高级定时器PWM输入模式实验
本小节我们来学习使用高级定时器PWM输入模式,此模式是输入捕获模式的一个特例。PWM输入模式经常被应用于测量PWM脉宽和频率。PWM输入模式在《STM32F10xxx参考手册_V10(中文版).pdf》手册216页有详细的文字描述。下面我们结合这些文字,配合高级定时器框图给大家介绍PWM输入的工作原理。
在这里插入图片描述

图22.5.1 PWM输入模式工作原理示意图
第一,确定定时器时钟源。本实验中我们使用内部时钟(CK_INT),F1系列高级定时器挂载在APB2总线上,按照sys_stm32_clock_init函数的配置,定时器时钟频率等于APB2总线时钟频率,即72MHz。计数器的计数频率确定了测量的精度。
第二,确定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所示:
在这里插入图片描述

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

图22.5.1.1 TIMx_SMCR寄存器
该寄存器的SMS[2:0]位,用于从模式选择。比如在本实验中我们需要用到复位模式,所以设置SMS[2:0]=100。TS[2:0]位是触发选择,我们设置为滤波后的定时器输入1 (TI1FP1),即TS[2:0]为101。
 捕获/比较模式寄存器1/2(TIMx_CCMR1/2)
TIM1/TIM8的捕获/比较模式寄存器(TIMx_CCMR1/2),该寄存器一般有2个:TIMx_CCMR1和TIMx _CCMR2。TIMx_CCMR1控制CH1和CH2,而TIMx_CCMR2控制CH3和CH4。TIMx_CCMR1寄存器描述如图22.5.1.2所示:
在这里插入图片描述

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

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

图22.5.1.4 TIMx_ CCR1寄存器
本实验中,CCR1寄存器用于获取PWM周期内计数器的计数个数。CCR2寄存器用于获取PWM高电平期间计数器的计数个数。
 DMA/中断使能寄存器(TIMx_DIER)
DMA/中断使能寄存器描述如图22.5.1.5所示:
在这里插入图片描述

图22.5.1.5 TIMx_DIER寄存器
该寄存器位0(UIE)用于使能或者禁止更新中断,因为本实验我们用到更新中断,所以该位需要置1。位1(CC1IE)用于使能或者禁止捕获/比较1中断,我们用到捕获中断,所以该位需要置1。
22.5.2 硬件设计

  1. 例程功能
    首先通过TIM3_CH2(PB5)输出PWM波。然后把PB5输出的PWM波用杜邦线接入PC6(定时器8通道1),最后通过串口打印PWM波的脉宽和频率等信息。通过LED1闪烁来提示程序正在运行。
  2. 硬件资源
    1)LED灯
    LED0 – PB5
    2)定时器3通道2(PB5)输出PWM波
    定时器8通道1(PC6)输入PWM波
  3. 原理图
    定时器属于STM32F103的内部资源,只需要软件设置好即可正常工作。我们把PB5引脚输出的PWM波用杜邦线接入PC6引脚,然后通过电脑串口上位机软件观察打印出来的信息。
    22.5.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可以直接使用输入捕获中断服务函数来处理输入捕获中断。在使用TIM1的时候,如果要考虑定时器1溢出,可以重定义更新中断服务函数TIM1_UP_IRQHandler。如果使用HAL库的中断回调机制,可以在相关中断服务函数中直接调用定时器中断公共处理函数HAL_TIM_IRQHandler,然后我们直接重定义相关的中断回调函数来编写中断程序即可。本实验为了兼容性,我们自定义一个中断处理函数atim_timx_pwmin_chy_process,里面包含了捕获中断和更新中断的处理,具体看源码。
22.5.3.1 程序流程图
在这里插入图片描述

图22.5.3.1.1 高级定时器PWM输入模式实验程序流程图
22.5.3.2 程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。高级定时器驱动源码包括两个文件:atim.c和atim.h。
首先看atim.h头文件的几个宏定义:
/* TIMX PWM输入模式 定义

  • 这里的输入捕获使用定时器TIM8_CH1
  • 默认是针对TIM1/TIM8等高级定时器
  • 注意: 通过修改这几个宏定义,可以支持TIM1~TIM8任意一个定时器的通道1/通道2
 */
#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口时钟使能 */

#define ATIM_TIMX_PWMIN                      TIM8                       
#define ATIM_TIMX_PWMIN_IRQn                TIM8_UP_IRQn
#define ATIM_TIMX_PWMIN_IRQHandler         TIM8_UP_IRQHandler
#define ATIM_TIMX_PWMIN_CHY                 TIM_CHANNEL_1		/* 通道Y,  1<= Y <=2*/ 
#define ATIM_TIMX_PWMIN_CHY_CLK_ENABLE()
do{ __HAL_RCC_TIM8_CLK_ENABLE(); }while(0)		/* TIM8 时钟使能 */

/* TIM1 / TIM8 有独立的捕获中断服务函数,需要单独定义,对于TIM2~5等,则不需要以下定义 */
#define ATIM_TIMX_PWMIN_CC_IRQn                 TIM8_CC_IRQn
#define ATIM_TIMX_PWMIN_CC_IRQHandler          TIM8_CC_IRQHandler

可以把上面的宏定义分成三部分,第一部分包括是定时器8通道1对应的IO口的宏定义,第二部分则是定时器8的相应宏定义,另外针对TIM1/ TIM8有独立的捕获中断服务函数,需要单独定义。
下面看atim.c的程序,首先是高级定时器PWM输入模式初始化函数,其定义如下:

/**
 * @brief      定时器TIMX 通道Y PWM输入模式 初始化函数
 * @note
 *              高级定时器的时钟来自APB2, 而PCLK2 = 72Mhz, 我们设置PPRE2不分频, 因此
 *              高级定时器时钟 = 72Mhz
 *              定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.
 *              Ft=定时器工作频率,单位:Mhz
 *
 *              本函数初始化的时候: 使用psc=0, arr固定为65535. 得到采样时钟频率为72Mhz,
精度约13.8ns
 *
 * @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();
    ATIM_TIMX_PWMIN_CHY_GPIO_CLK_ENABLE();
    __HAL_RCC_AFIO_CLK_ENABLE();
    AFIO_REMAP_PARTIAL(AFIO_EVCR_PORT_PC,AFIO_EVCR_PIN_PX6);/*复用TIM8_CH1/PC6*/

    gpio_init_struct.Pin = ATIM_TIMX_PWMIN_CHY_GPIO_PIN;
    gpio_init_struct.Mode = GPIO_MODE_AF_PP; 
    gpio_init_struct.Pull = GPIO_PULLDOWN;
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH ;
    HAL_GPIO_Init(ATIM_TIMX_PWMIN_CHY_GPIO_PORT, &gpio_init_struct);

    g_timx_pwmin_chy_handle.Instance = ATIM_TIMX_PWMIN;      /* 定时器8 */
    g_timx_pwmin_chy_handle.Init.Prescaler = 0;                /* 定时器预分频系数 */
    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, 
TIM_CHANNEL_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,
TIM_CHANNEL_2);
    /* 设置中断优先级,抢占优先级1,子优先级3 */
    HAL_NVIC_SetPriority(ATIM_TIMX_PWMIN_IRQn, 1, 3);               
    HAL_NVIC_EnableIRQ( ATIM_TIMX_PWMIN_IRQn );                /* 开启TIMx中断 */
    
    /* TIM1/TIM8有独立的输入捕获中断服务函数 */
    if ( ATIM_TIMX_PWMIN == TIM1 || ATIM_TIMX_PWMIN == TIM8)
{
/* 设置中断优先级,抢占优先级1,子优先级3 */
        HAL_NVIC_SetPriority(ATIM_TIMX_PWMIN_CC_IRQn, 1, 3);        
        HAL_NVIC_EnableIRQ(ATIM_TIMX_PWMIN_CC_IRQn);           /* 开启TIMx中断 */
    }

    __HAL_TIM_ENABLE_IT(&g_timx_pwmin_chy_handle, TIM_IT_UPDATE);
    HAL_TIM_IC_Start_IT(&g_timx_pwmin_chy_handle, TIM_CHANNEL_1);
    HAL_TIM_IC_Start_IT(&g_timx_pwmin_chy_handle, TIM_CHANNEL_2);
}

在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函数和__HAL_TIM_ENABLE_IT函数宏使能捕获中断和更新中断,并且使能定时器。
为了方便,我们定义了重新启动捕获函数,其定义如下:

/**
 * @brief       定时器TIMX PWM输入模式 重新启动捕获
 * @param       无
 * @retval      无
 */
void atim_timx_pwmin_chy_restart(void)
{
    sys_intx_disable();                            /* 关闭中断 */

    g_timxchy_pwmin_sta = 0;                      /* 清零状态,重新开始检测 */
    g_timxchy_pwmin_psc = 0;                      /* 分频系数清零 */

/* 以最大的计数频率采集,以得到最好的精度 */
    __HAL_TIM_SET_PRESCALER(&g_timx_pwmin_chy_handle, 0);  
    __HAL_TIM_SET_COUNTER(&g_timx_pwmin_chy_handle, 0);       /* 计数器清零 */
    
    __HAL_TIM_ENABLE_IT(&g_timx_pwmin_chy_handle, TIM_IT_CC1);/* 使能捕获中断 */
    __HAL_TIM_ENABLE_IT(&g_timx_pwmin_chy_handle, TIM_IT_UPDATE);/*使能更新中断*/
    __HAL_TIM_ENABLE(&g_timx_pwmin_chy_handle);                /* 使能定时器TIMX */

ATIM_TIMX_PWMIN->SR = 0;            /* 清除所有中断标志位 */

    sys_intx_enable();                   /* 打开中断 */
}
该函数首先关闭所有中断,然后把一些状态标志位清零、设置定时器预分频系数、计数器值、使能相关中断、以及清除相关中断标志位,最后才允许被中断。

最后要介绍的是中断服务函数,在定时器1的输入捕获中断服务函数TIM1_CC_IRQHandler和更新中断服务函数TIM1_UP_IRQHandler里面都是直接调用atim_timx_pwmin_chy_process函数。输入捕获中断服务函数和更新中断服务函数都是用到宏定义的,这三个函数定义如下:

/**
 * @brief       定时器TIMX 更新/溢出 中断服务函数
 *   @note      TIM1/TIM8的这个函数仅用于更新/溢出中断服务,捕获在另外一个函数!
 *              其他普通定时器则更新/溢出/捕获,都在这个函数里面处理!
 * @param       无
 * @retval      无
 */
void ATIM_TIMX_PWMIN_IRQHandler(void)
{
    atim_timx_pwmin_chy_process();
}

/**
 * @brief       定时器TIMX 输入捕获 中断服务函数
 *   @note      仅TIM1/TIM8有这个函数,其他普通定时器没有这个中断服务函数!
 * @param       无
 * @retval      无
 */
void ATIM_TIMX_PWMIN_CC_IRQHandler(void)
{
    atim_timx_pwmin_chy_process();
}

/**
 * @brief       定时器TIMX 通道Y PWM输入模式 中断处理函数
 * @note
 * 因为TIM1/TIM8等有多个中断服务函数,而TIM2~5/TIM12/TIM15等普通定时器只有1个中断服务
 * 函数,为了更好的兼容,我们对中断处理统一放到atim_timx_pwin_chy_process函数里面进行处理
 *
 * @param       无
 * @retval      无
 */
static void atim_timx_pwmin_chy_process(void)
{
    static uint8_t sflag = 0;               /* 启动PWMIN输入检测标志 */

    if (g_timxchy_pwmin_sta)
    {
        g_timxchy_pwmin_psc = 0;
ATIM_TIMX_PWMIN->SR = 0;            /* 清除所有中断标志位 */
        __HAL_TIM_SET_COUNTER(&g_timx_pwmin_chy_handle, 0);      /* 计数器清零 */
        return ;
}

/* 如果发生了更新中断 */
    if (__HAL_TIM_GET_FLAG(&g_timx_pwmin_chy_handle, TIM_FLAG_UPDATE))
{ 
/* 清除更新中断标记 */
        __HAL_TIM_CLEAR_FLAG(&g_timx_pwmin_chy_handle, TIM_FLAG_UPDATE);     
/* 没有发生周期捕获中断,且捕获未完成 */
        if (__HAL_TIM_GET_FLAG(&g_timx_pwmin_chy_handle, TIM_FLAG_CC1) == 0)   
        {
            sflag = 0;
            if (g_timxchy_pwmin_psc == 0)   /* 从0 到 1 */
            {
                g_timxchy_pwmin_psc ++;
            }
            else
            {
                if (g_timxchy_pwmin_psc == 65535)     /* 已经最大了,可能是无输入状态 */
                {
                    g_timxchy_pwmin_psc = 0;            /* 重新恢复不分频 */
                }
                else if (g_timxchy_pwmin_psc > 32767)/* 不能倍增了 */
                {
                    g_timxchy_pwmin_psc = 65535;       /* 直接等于最大分频系数 */
                }
                else
                {
                    g_timxchy_pwmin_psc += g_timxchy_pwmin_psc; /* 倍增 */
                }
            }

            __HAL_TIM_SET_PRESCALER(&g_timx_pwmin_chy_handle, 
g_timxchy_pwmin_psc); /* 设置定时器预分频系数 */
            __HAL_TIM_SET_COUNTER(&g_timx_pwmin_chy_handle, 0);  /* 计数器清零 */
ATIM_TIMX_PWMIN->SR = 0;            /* 清除所有中断标志位 */
            return ;
        }
    }

    if (sflag == 0)   /* 第一次采集到捕获中断 */
{
/* 检测到了第一次周期捕获中断 */
        if (__HAL_TIM_GET_FLAG(&g_timx_pwmin_chy_handle, TIM_FLAG_CC1))   
        { 
            sflag = 1;               /* 标记第一次周期已经捕获, 第二次周期捕获可以开始了 */
        }
ATIM_TIMX_PWMIN->SR = 0;            /* 清除所有中断标志位 */
        return ;                     /* 完成此次操作 */
    }

    if (g_timxchy_pwmin_sta == 0)/* 还没有成功捕获 */
{
    /* 检测到了周期捕获中断 */
        if (__HAL_TIM_GET_FLAG(&g_timx_pwmin_chy_handle, TIM_FLAG_CC1))   
        {
            g_timxchy_pwmin_hval = HAL_TIM_ReadCapturedValue(
&g_timx_pwmin_chy_handle, TIM_CHANNEL_2) + 1; /* 高定平脉宽捕获值 */
            g_timxchy_pwmin_cval = HAL_TIM_ReadCapturedValue(
&g_timx_pwmin_chy_handle, TIM_CHANNEL_1) + 1; /* 周期捕获值 */

            /* 高电平脉宽必定小于周期长度 */
            if (g_timxchy_pwmin_hval < g_timxchy_pwmin_cval)
            {
                g_timxchy_pwmin_sta = 1;         /* 标记捕获成功 */

                g_timxchy_pwmin_psc = ATIM_TIMX_PWMIN->PSC;/* 获取PWM输入分频系数 */
                
                if (g_timxchy_pwmin_psc == 0)   /* 分频系数为0的时候, 修正读取数据 */
                {
                    g_timxchy_pwmin_hval++;      /* 修正系数为1, 加1 */
                    g_timxchy_pwmin_cval++;      /* 修正系数为1, 加1 */
                }

                sflag = 0;
                /* 每次捕获PWM输入成功后, 停止捕获,避免频繁中断影响系统正常代码运行 */
                ATIM_TIMX_PWMIN->CR1  &= ~(1 << 0);        /* 关闭定时器TIMX */
                /* 关闭通道1捕获中断 */
                __HAL_TIM_DISABLE_IT(&g_timx_pwmin_chy_handle, TIM_IT_CC1); 
                /* 关闭通道2捕获中断 */
                __HAL_TIM_DISABLE_IT(&g_timx_pwmin_chy_handle, TIM_IT_CC2); 
/* 关闭更新中断 */
                __HAL_TIM_DISABLE_IT(&g_timx_pwmin_chy_handle, TIM_IT_UPDATE); 
                ATIM_TIMX_PWMIN->SR = 0;            /* 清除所有中断标志位 */
            }
            else
            {
                atim_timx_pwmin_chy_restart();
            }
        }
    }
    ATIM_TIMX_PWMIN->SR = 0;            /* 清除所有中断标志位 */
}

atim_timx_pwmin_chy_process函数包含了捕获中断程序和更新中断程序的处理。如果发生了更新中断(即定时器溢出),证明超出定时器量程,这里会加大预分频系数,以得到更大的量程。量程变大了,那么测量的精度就会降低,所谓鱼和熊掌不可兼得。代码中的“if (sflag == 0) /* 第一次采集到捕获中断 /”这个程序段,表示第一次采集到捕获中断。这时候相当于第一次捕获到上升沿,我们只是把sflag标志位置1,然后清除所有中断标志位,等待下次的捕获中断发生。如果再次发生捕获中断,就会来到“if (g_timxchy_pwmin_sta == 0) / 还没有成功捕获 */”程序段。通过HAL_TIM_ReadCapturedValue函数获取CCR1和CCR2寄存器的值,把这个获取到的寄存器值+1才是对应的计数器计数个数。如果预分频系数为0的时候,还要把这两个寄存器的值再+1,这样计算的结果更准确。其它的代码细节请大家自行查看源码,有详细的注释。
注释代码:实验10-1 高级定时器输出指定个数PWM实验使用到TIM8_UP_IRQHandler中断服务函数,本实验同样使用到该函数,编译会报错,这里的做法是屏蔽实验10-1的相关代码。具体请看atim.c文件源码。atim.c文件的程序就介绍到这。
下面介绍一下待测试的PWM怎么得到。因为在实验9-2通用定时器PWM输出实验我们已经编写了PWM波输出的程序,所以这里直接使用通用定时器的PWM输出实验的代码进行初始化,从而让TIM3_CH2(PB5)输出PWM波。然后我们用杜邦线把PB5和PC6连接起来。这样PB5输出的PWM就可以输入到PC6(定时器8 通道1)进行测量。
在main.c里面编写如下代码:

int main(void)
{
    uint8_t t = 0;
    double ht, ct, f, tpsc;
   
    HAL_Init();                  					/* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9);			/* 设置时钟, 72Mhz */
    delay_init(72);                         		/* 延时初始化 */
    usart_init(115200);                          	/* 串口初始化为115200 */
    led_init();                                   	/* 初始化LED */
    gtim_timx_pwm_chy_init(10 - 1, 72 - 1); 	/* 1Mhz的计数频率, 100Khz PWM */
atim_timx_pwmin_chy_init();                	/* 初始化PWM输入捕获 */

    GTIM_TIMX_PWM_CHY_CCRX = 2;                	/* 低电平宽度2,高电平宽度8 */
    while (1)
    {
        delay_ms(10);
        t++;
        if (t >= 20)    /* 每200ms输出一次结果,并闪烁LED0,提示程序运行 */
        {
            if (g_timxchy_pwmin_sta)    /* 捕获了一次数据 */
            {
              printf("
");                                     	/* 输出空,另起一行 */
              printf("PWM PSC  :%d
", g_timxchy_pwmin_psc); /* 打印分频系数 */
              printf("PWM Hight:%d
", g_timxchy_pwmin_hval);/*打印高电平脉宽*/
              printf("PWM Cycle:%d
", g_timxchy_pwmin_cval);/* 打印周期 */
/* 得到PWM采样时钟周期时间 */
              tpsc = ((double)g_timxchy_pwmin_psc + 1)/72; 
              ht = g_timxchy_pwmin_hval * tpsc;      		/* 计算高电平时间 */
              ct = g_timxchy_pwmin_cval * tpsc;          	/* 计算周期长度 */
              f = (1 / ct) * 1000000;                      	/* 计算频率 */
              printf("PWM Hight time:%.3fus
", ht); 	/* 打印高电平脉宽长度 */
                printf("PWM Cycle time:%.3fus
", ct);	/* 打印周期时间长度 */
                printf("PWM Frequency :%.3fHz
", f); 	/* 打印频率 */ 
                atim_timx_pwmin_chy_restart(); 			/* 重启PWM输入检测 */
            }
            LED0_TOGGLE();  								/* LED0(RED)闪烁 */
            t = 0;
        }
    }
}

先看gtim_timx_pwm_chy_init(10 - 1, 72 - 1)这个语句,这两个形参分别设置自动重载寄存器的值为9,以及定时器预分频寄存器的值为71。先看预分频系数,我们设置为72分频,定时器1的时钟频率等于APB2总线时钟频率,即72MHz,可以得到计数器的计数频率是1MHz,即1us计数一次。再到自动重载寄存器的值为9决定的是PWM波的频率(周期),可以得到PWM的周期为10*1us = 10us。然后通过GTIM_TIMX_PWM_CHY_CCRX = 2这个语句设置占空比,低电平宽度2,总的周期宽度是10,所以高电平宽度8。即产生的PWM波周期为10us,频率为100KHz,占空比为80%。下载验证的时候验证一下捕获到的与输出的是否一致。
atim_timx_pwmin_chy_init这个语句,就初始化PWM输入捕获。然后在无限循环中每200ms判断是否g_timxchy_pwmin_sta标志变量,是否捕获到数据,捕获到就打印和计数相关信息。
下面我们下载到开发板验证一下。
22.5.4 下载验证
下载代码后,可以看到LED0在闪烁,说明程序已经正常在跑了,我们再打开串口调试助手,选择对应的串口端口。然后用杜邦线把PB5引脚连接到PC6引脚,就可以看到串口助手不断打印PWM波的信息,如图22.5.4.1所示:
在这里插入图片描述

图22.5.4.1 打印高电平脉冲次数
可以看到打印出来的PWM波信息为:周期是10us,频率是100KHz,占空比是80%,和我们的预想结果一样。
大家可以通过gtim_timx_pwm_chy_init函数的形参设置其他参数的PWM波,以及GTIM_TIMX_PWM_CHY_CCRX设置占空比。这里的测试的PWM波有一定的范围,不是全范围的PWM都可以进行准确的测试,大家可以进行验证。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。