本帖最后由 正点原子运营 于 2023-7-17 12:17 编辑
第二十二章 高级定时器实验
1)实验平台:正点原子探索者STM32F407开发板
2) 章节摘自【正点原子】STM32F407开发指南 V1.1
3)购买链接:https://detail.tmall.com/item.htm?id=609294673401
4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/stm32/zdyz_stm32f407_explorerV3.html
5)正点原子官方B站:https://space.bilibili.com/394620890
6)STM32技术交流QQ群:151941872
22.4 高级定时器互补输出带死区控制实验本小节我们来学习使用高级定时器的互补输出带死区控制功能。对于刚接触这个知识的朋友可能会问:什么是互补输出?还带死区控制?What?下面给大家简单说一下。 上图中,CH1输出黄色的PWM,它的互补通道CH1N输出绿色的PWM。通过对比,可以知道这两个PWM刚好是反过来的,CH1的PWM为高电平期间,CH1N的PWM则是低电平,反之亦然,这就是互补输出。 下面来看一下什么是带死区控制的互补输出? 上图中,CH1输出的PWM和CH1N输出的PWM在高低电平转换间,插入了一段时间才实现互补输出。这段时间称为死区时间,可以通过TIMx_BDTR寄存器的DTG[7:0]位配置控制死区时间的长度,后面会详细讲解如何配置死区时间。上图中,箭头指出的两段死区时间的长度是一样的,因为都是由同一个死区发生器产生。
理解了互补输出和带死区控制的互补输出,下面来看一下带死区控制的互补输出有什么用?带死区控制的互补输出经常被用于控制电机的H桥中,下面给大家画了一个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)。 假设定时器时钟源频率是168MHz,我们设置CKD[1:0]位的值为2,代入上面的式子可得: 通过上式可得tDTS约等于23.81ns,本实验例程中我们也是这样设置的。 第二步:根据DTG[7:5]选择计算公式。 第三步:代入选择的公式计算。
下面给大家举个例子,假设定时器时钟源频率是168MHz,我们设置CKD[1:0]位的值为2,DTG[7:0]位的值为250。从上面的例子知道CKD[1:0]位的值为2,得到的tDTS=23.81ns。下面来看一下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.81ns =22095.68ns = 22.01us,即死区时间为22.01us。死区时间计算方法就给大家介绍到这里。
关于互补输出和死区插入的更多内容请看《STM32F4xx参考手册_V4(中文版).pdf》的14.3.11小节,下面我们介绍相关的寄存器。
22.4.1 TIM1/TIM8寄存器高级定时器互补输出带死区控制除了用到定时器的时基单元:计数器寄存器(TIMx_CNT)、预分频器寄存器(TIMx_PSC)、自动重载寄存器(TIMx_ARR)之外。主要还用到以下这些寄存器: l 控制寄存器1(TIMx_CR1) TIM1/TIM8的控制寄存器1描述如图22.4.1.1所示: 上图中我们只列出了本实验需要用的一些位,其中:位7(APRE)用于控制自动重载寄存器是否进行缓冲,在基本定时器的时候已经讲过,请回顾。本实验中,我们把该位置1。
CKD[1:0]位指示定时器时钟(CK_INT)频率与死区发生器以及数字滤波器(ETR、TIx)所使用的死区及采样时钟(tDTS)之间的分频比。我们设置CKD[1:0]位为00,结合高级定时器时钟源频率等于APB2总线时钟频率,即168MHz,可以得到tDTS=4 * 1 /168us = 23.81ns。
CEN位,用于使能计数器的工作,必须要设置该位为1,才可以开始计数。
l 捕获/比较模式寄存器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所示: 该寄存器的有些位在不同模式下,功能不一样,我们现在用到输出比较模式。关于该寄存器的详细说明,请参考《STM32F4xx参考手册_V4(中文版).pdf》第375页。
本实验我们用到了定时器1输出比较的通道1,所以我们需要配置TIM1_CCMR1模式设置位OC1M[2:0],我们使用的是PWM模式1,所以这3位必须设置为110。
l 捕获/比较使能寄存器(TIMx_CCER) TIM1/TIM8的捕获/比较使能寄存器,该寄存器控制着各个输入输出通道的开关和极性。TIMx_CCER寄存器描述如图22.4.1.3所示: 该寄存器比较简单,要让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寄存器描述如图22.4.1.4所示: 对于TIM1和TIM8来说,该寄存器16位有效位,本实验中可以通过改变该寄存器的值来改变PWM的占空比。
l 断路和死区寄存器(TIMx_BDTR) TIM1/TIM8断路和死区寄存器,该寄存器各位描述如图22.4.1.5所示: 该寄存器控制定时器的断路和死区控制的功能。我们先看断路控制,用到断路输入功能(断路输入引脚为PE15),位BKE置1即可。
位BKP选择断路输入信号有效电平。本实验中,我们选择高电平有效,即BKP置1。
位AOE是自动输出使能位,如果使能AOE位,那么在我们输入刹车信号后再断开了刹车信号,互补的PWM会自动恢复输出,如果失能AOE位,那么在输入刹车信号后再断开了刹车信号,互补的PWM就不会恢复输出,而是一直保持刹车信号输入时的状态。为了方便观察,我们使能该位,即置1。
位MOE是使能主输出,想要高级定时器的通道正常输出,则必须设置MOE位为1。
最后是DTG[7:0]位,用于设置死区时间,前面已经教过大家怎么设置了。这里以我们例程的设置为例,CKD[1:0] 设置为10,定时器时钟源频率是168MHz,所以tDTS = 23.81ns。
本例程的DTG[7:0]位的值设置为十进制100,即二进制数0110 0100。DTG[7:5]=011,符合第一条式子:DT=DTG[7:0] * t dtg,其中 t dtg = tDTS。DT是死区时间,可以得到DT = 100*23.81 ns = 2.38us。到后面下载验证小节,我们通过示波器验证一下这个死区时间计算的理论值和实际值是否一样。
22.4.2 硬件设计1. 例程功能 1,利用TIM1_CH1(PE9)输出70%占空比的PWM波,它的互补输出通道(PE8)则是输出30%占空比的PWM波。 2,刹车功能,当给刹车输入引脚(PE15)输入高电平时,进行刹车,即PE8和PE9停止输出PWM波。 3,LED0闪烁指示程序运行。
2. 硬件资源 1)LED灯 LED0 – PF9 2)定时器1 TIM1正常输出通道 PE9 TIM1互补输出通道 PE8 TIM1刹车输入 PE15
3. 原理图 定时器属于STM32F407的内部资源,只需要软件设置好即可正常工作。我们需要通过示波器观察PE8和PE9引脚PWM输出的情况。还可以通过给PE15引脚接入高电平进行刹车。
22.4.3 程序设计22.4.3.1 定时器的HAL库驱动 定时器在HAL库中的驱动代码在前面已经介绍了部分,这里我们再介绍几个本实验用到的函数。
1.HAL_TIMEx_ConfigBreakDeadTime函数 定时器的断路和死区时间配置初始化函数,其声明如下: - HAL_StatusTypeDefHAL_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是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_CHY_GPIO_AF,
- * 如果我们使用默认的复用功能输出, 则不用设置重映射, 是可以不需要该函数的! 根据具体需要来实现.
- */
- #define ATIM_TIMXCPLM_CHY_GPIO_AF GPIO_AF1_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的相应宏定义。注意:因为PE8/PE9/PE15, 默认并不是TIM1的复用功能脚, 必须开启完全重映射,具体请参考《STM32F4xx参考手册_V4(中文版).pdf》第191页,GPIOx_AFRL/H寄存器的描述。
下面来看atim.c文件的程序,首先是高级定时器互补输出初始化函数,其定义如下: - /**
- * @brief 高级定时器TIMX 互补输出 初始化函数(使用PWM模式1)
- * @note
- * 配置高级定时器TIMX 互补输出, 一路OCy 一路OCyN, 并且可以设置死区时间
- *
- * 高级定时器的时钟来自APB2, 而PCLK2 = 168Mhz, 我们设置PPRE2不分频, 因此
- * 高级定时器时钟 = 168Mhz
- * 定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.
- * Ft=定时器工作频率,单位:Mhz
- * @param arr: 自动重装值。
- * @param psc: 时钟预分频数
- * @retval 无
- */
- voidatim_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_PULLDOWN;
- 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 = 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);
- tim_oc_cplm_pwm.OCMode = TIM_OCMODE_PWM1; /* PWM模式1 */
- tim_oc_cplm_pwm.OCPolarity = TIM_OCPOLARITY_HIGH; /* OCy 高电平有效 */
- tim_oc_cplm_pwm.OCNPolarity = TIM_OCNPOLARITY_HIGH; /* OCyN 高电平有效 */
- tim_oc_cplm_pwm.OCIdleState =TIM_OCIDLESTATE_SET; /* 当MOE=0,OCx=0 */
- tim_oc_cplm_pwm.OCNIdleState =TIM_OCNIDLESTATE_SET; /* 当MOE=0,OCxN=0 */
- 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 / 42M= 23.8ns
- * @retval 无
- */
- voidatim_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(336, 8, 2, 7); /* 设置时钟,168Mhz */
- delay_init(168); /* 延时初始化 */
- usart_init(115200); /* 串口初始化为115200 */
- led_init(); /* 初始化LED */
- atim_timx_cplm_pwm_init(1000 - 1, 168 - 1); /* 1Mhz的计数频率 1Khz的周期. */
- atim_timx_cplm_pwm_set(300, 100); /* 占空比:7:3,死区时间100 * tDTS */
- while (1)
- {
- delay_ms(10);
- t++;
- if (t >= 50)
- {
- LED0_TOGGLE(); /* LED0(RED)闪烁 */
- t = 0;
- }
- }
- }
复制代码先看atim_timx_cplm_pwm_init(1000- 1, 168 - 1)这个语句,这两个形参分别设置自动重载寄存器的值为999,以及定时器预分频器寄存器的值为167。先看预分频系数,我们设置为168分频,定时器1的时钟源频率等于APB2总线时钟频率,即168MHz,可以得到计数器的计数频率是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.38us。根据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 = 23.81ns。基于这个前提,通过改变DTG[7:0]的值,可以得到不同的死区时间。这里我们配置DTG[7:0]的值为100,即二进制数0110 0100,符合第一种情况dtg[7:5]=0xx时,死区时间DT = DTG [7:0] * tDTS。可以得到死区时间DT = 100*23.81 ns = 2.38us。
下面我们下载到开发板子验证一下。
22.4.4 下载验证下载代码后,可以看到LED0在闪烁,说明程序已经正常在跑了。我们需要借助示波器观察PE9正常输出和PE8互补输出PWM的情况,示波器显示截图如图22.4.4.1所示: 图22.4.4.1 PE8正常输出和PE9互补输出PWM的情况 图22.4.4.1中的由上到下分别是PE8互补输出70%占空比的PWM波和PE9输出30%占空比的PWM波。正常的PWM波的负脉宽减去互补输出的PWM的正脉宽的值除以2就是死区时间,也可以是正常的PWM的正脉宽减去互补输出的PWM波的负脉宽的值除以2。我们使用第一种方法得到:死区时间 =(702 – 698)/2 us=2us。与我们理论到的的值2.38us基本一样,这样的误差是正常的。
要是不相信,我们再举个例子,我们把调用的函数改为atim_timx_cplm_pwm_set(300,250),即配置DTG[7:0]的值为250,这个例子的计算过程在本实验前面死区时间计算的内容讲过,这里就不再赘述。经过计算得到死区时间DT =22.09us。修改好后,示波器显示截图如下图所示: 图22.4.4.2 修改程序后PE9正常输出和PE8互补输出PWM的情况 由图22.4.4.2可得到,死区时间 =(722– 678)/2 us= 22us。与我们理论到的的值22.09us也是差不多的,误差在正常范围。由此证明我们的死区时间设置是没有问题。
刹车功能验证:当给刹车输入引脚(PE15)接入高电平(这里直接用杜邦线连接PE15到3.3V)时,就会进行刹车,即PE9和PE8停止输出PWM波,如图22.4.4.3所示。
刹车功能验证:当给刹车输入引脚(PE15)接入高电平(这里直接用杜邦线把PE15连接到3.3V)时,就会进行刹车,MOE位被硬件清零。由《STM32F4xx参考手册_V4(中文版).pdf》第382页 表73可以知道刹车信号输入后,如果存在时钟,经过死区后OCx=OISx 且 OCxN=OISxN。在atim_timx_cplm_pwm_init函数中,我们设置当MOE=0时,OCx=0、OCxN=0,即PE9和PE8都是输出低电平。下面通过示波器来验证一下,如图22.4.4.3所示: 从上图可以看到PE9和PE8输出的都是低电平,符合我们预期的设置。
另外因为我们使能了AOE位(即把该位置1),如果刹车输入为无效极性时,MOE位在发生下一个更新事件时自动置1,恢复运行模式(即继续输出PWM)。因此当停止给PE15接入低电平(拔掉之前连接的杜邦线),PWM会自动恢复输出。
22.5 高级定时器PWM输入模式实验本小节我们来学习使用高级定时器PWM输入模式,此模式是输入捕获模式的一个特例。PWM输入模式经常被应用于测量PWM脉宽和频率。PWM输入模式在《STM32F4xx参考手册_V4(中文版).pdf》手册347页有详细的文字描述。下面我们结合这些文字,配合高级定时器框图给大家介绍PWM输入的工作原理。 第一,确定定时器时钟源。本实验中我们使用内部时钟(CK_INT),F4系列高级定时器挂载在APB2总线上,按照sys_stm32_clock_init函数的配置,定时器时钟频率为2倍的APB2总线时钟频率,即168MHz。计数器的计数频率确定了测量的精度。
第二,确定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是以通道1(CH1)输入PWM,设置IC1捕获边沿为上升沿捕获,IC2捕获边沿为下降沿捕获为例的PWM输入模式时序图。
从时序图可以看出,计数器的计数模式是递增计数模式。从左边开始看,当TI1来了上升沿时,计数器的值被复位为0(原因是从模式选择为复位模式),IC1和IC2都发生捕获事件。然后计数器的值计数到2的时候,IC2发生了下降沿捕获,捕获事件会导致这时候的计数器的值被锁存到CCR2寄存器中,该值+1就是高电平期间计数器的计数个数。最后计数器的值计数到4的时候,IC1发生了上升沿捕获,捕获事件会导致这时候的计数器的值被锁存到CCR1寄存器中,该值+1就是PWM周期内计数器的计数个数。
假设计数器的计数频率是168MHz,那我们就可以计算出这个PWM的周期、频率和占空比等参数了。下面就以这个为例给大家计算一下。由计数器的计数频率为168MHz,可以得到计数器计一个数的时间是5.95ns(即测量的精度是5.95ns)。知道了测量精度,再来计算PWM的周期,PWM周期 =(4+1)*(1/168000000) = 29.75ns,那么PWM的频率就是33.6MHz。占空比 = (2+1)/(4+1) =3/5(即占空比为60%)。
22.5.1 TIM1/TIM8寄存器高级定时器PWM输入模式实验除了用到定时器的时基单元:计数器寄存器(TIMx_CNT)、预分频器寄存器(TIMx_PSC)、自动重载寄存器(TIMx_ARR)之外。主要还用到以下这些寄存器: l 从模式控制寄存器(TIMx_SMCR) TIM1/TIM8的从模式控制寄存器描述如图22.5.1.1所示: 该寄存器的SMS[2:0]位,用于从模式选择。比如在本实验中我们需要用到复位模式,所以设置SMS[2:0]=100。TS[2:0]位是触发选择,我们设置为滤波后的定时器输入1(TI1FP1),即TS[2:0]为101。
l 捕获/比较模式寄存器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所示: 该寄存器的有些位在不同模式下,功能不一样,我们现在用到输入捕获模式。关于该寄存器的详细说明,请参考《STM32F4xx参考手册_V4(中文版).pdf》第375页,14.4.7节。
本实验我们通过定时器1通道1输入PWM信号,所以IC1和IC2都映射到TI1上。配置CC1S[1:0]=01、CC2S [1:0]=10,其他位不用设置,默认为0即可。
l 捕获/比较使能寄存器(TIMx_CCER) TIM1/TIM8的捕获/比较使能寄存器,该寄存器控制着各个输入输出通道的开关和极性。TIMx_CCER寄存器描述如图22.5.1.3所示: 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寄存器描述如图22.5.1.4所示: 本实验中,CCR1寄存器用于获取PWM周期内计数器的计数个数。CCR2寄存器用于获取PWM高电平期间计数器的计数个数。
l DMA/中断使能寄存器(TIMx_DIER) DMA/中断使能寄存器描述如图22.5.1.5所示: 该寄存器位0(UIE)用于使能或者禁止更新中断,因为本实验我们用到更新中断,所以该位需要置1。位1(CC1IE)用于使能或者禁止捕获/比较1中断,我们用到捕获中断,所以该位需要置1。
22.5.2 硬件设计1. 例程功能 首先通过TIM14_CH1(PF9)输出PWM波。然后把PF9输出的PWM波用杜邦线接入PC6(定时器8通道1),最后通过串口打印PWM波的脉宽和频率等信息。通过LED1闪烁来提示程序正在运行。
2. 硬件资源 1)LED灯 LED0 – PF9 LED1 –PF10 2)定时器14通道4(PF9)输出PWM波 定时器8通道1(PC6)输入PWM波
3. 原理图 定时器属于STM32F407的内部资源,只需要软件设置好即可正常工作。我们把PF9引脚输出的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_TIM10_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_AF GPIO_AF3_TIM8
- #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_TIM13_IRQn
- #define ATIM_TIMX_PWMIN_IRQHandler TIM8_UP_TIM13_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 = 168Mhz, 我们设置PPRE2不分频, 因此
- * 高级定时器时钟 = 168Mhz
- * 定时器溢出时间计算方法: Tout = ((arr +1) * (psc + 1)) / Ft us.
- * Ft=定时器工作频率,单位:Mhz
- *
- * @param 无
- * @retval 无
- */
- voidatim_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();
- 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 ;
- 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; /* 定时器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 无
- */
- voidatim_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 无
- */
- voidATIM_TIMX_PWMIN_IRQHandler(void)
- {
- atim_timx_pwmin_chy_process();
- }
- /**
- * @brief 定时器TIMX 输入捕获 中断服务函数
- * @note 仅TIM1/TIM8有这个函数,其他普通定时器没有这个中断服务函数!
- * @param 无
- * @retval 无
- */
- voidATIM_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 voidatim_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_TIM13_IRQHandler中断服务函数,本实验同样使用到该函数,编译会报错,这里的做法是屏蔽实验10-1的相关代码。具体请看atim.c文件源码。atim.c文件的程序就介绍到这。
下面介绍一下待测试的PWM怎么得到。因为在实验9-2通用定时器PWM输出实验我们已经编写了PWM波输出的程序,所以这里直接使用通用定时器的PWM输出实验的代码进行初始化,从而让TIM14_CH1(PF9)输出PWM波。然后我们用杜邦线把PF9和PC6连接起来。这样PF9输出的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(336, 8, 2, 7); /* 设置时钟,168Mhz */
- delay_init(168); /* 延时初始化 */
- usart_init(115200); /* 串口初始化为115200 */
- led_init(); /* 初始化LED */
- gtim_timx_pwm_chy_init(10 - 1, 168 - 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("\r\n"); /* 输出空,另起一行 */
- printf("PWMPSC :%d\r\n",g_timxchy_pwmin_psc); /* 打印分频系数 */
- printf("PWMHight:%d\r\n", g_timxchy_pwmin_hval);/*打印高电平脉宽*/
- printf("PWMCycle:%d\r\n", 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("PWMHight time:%.3fus\r\n", ht); /* 打印高电平脉宽长度 */
- printf("PWMCycle time:%.3fus\r\n", ct); /* 打印周期时间长度 */
- printf("PWMFrequency :%.3fHz\r\n", f); /* 打印频率 */
- atim_timx_pwmin_chy_restart(); /* 重启PWM输入检测 */
- }
- LED0_TOGGLE(); /* LED0(RED)闪烁 */
- t = 0;
- }
- }
- }
复制代码先看gtim_timx_pwm_chy_init(10- 1, 168 - 1)这个语句,这两个形参分别设置自动重载寄存器的值为9,以及定时器预分频寄存器的值为167。先看预分频系数,我们设置为168分频,定时器1的时钟频率等于APB2总线时钟频率,即168MHz,可以得到计数器的计数频率是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在闪烁,说明程序已经正常在跑了,我们再打开串口调试助手,选择对应的串口端口。然后用杜邦线把PF9引脚连接到PC6引脚,就可以看到串口助手不断打印PWM波的信息,如图22.5.4.1所示: 可以看到打印出来的PWM波信息为:周期是10us,频率是100KHz,占空比是80%,和我们的预想结果一样。
大家可以通过gtim_timx_pwm_chy_init函数的形参设置其他参数的PWM波,以及GTIM_TIMX_PWM_CHY_CCRX设置占空比。这里的测试的PWM波有一定的范围,不是全范围的PWM都可以进行准确的测试,大家可以进行验证。
|