定时器中断和PWM输出实验
所以阿…笔记的重要性哇!之前看过的内容在做后面的内容涉及到了发现没有笔记,看的时间太久远,竟然全部忘记了,真是个悲伤的故事:(
那就重来吧。由于这两个实验都跟TIM定时器关联性很大,就合在一起写了。
Part1 定时器中断实验
三种定时器的区别
这部分主要用到通用定时器
STM32L4 通用定时器简介
STM32L475 的通用 TIMx (TIM2~TIM5 和 TIM15~TIM17)定时器功能包括:
(1) 16 位(TIM3,TIM4)/32 位(TIM2,TIM5)向上、向下、向上/向下自动装载计数器,注意: TIM15~TIM17 只支持向上(递增)计数方式。
(2)16 位可编程(可以实时修改)预分频器,计数器时钟频率的分频系数为 1~65535 之间的任 意数值。
(3)4 个独立通道(TIMx_CH1~ 4,其中 TIM15 最多 2 个通道,TIM16 和 TIM17 最多 1 个 通道),这些通道可以用来作为: A.输入捕获 B.输出比较 C.PWM 生成(边缘或中间对齐模式) ,注意:TIM12~TIM17 不支持中间对齐模式 D.单脉冲模式输出
(4)可使用外部信号控制定时器和定时器互连的同步电路。
(5)如下事件发生时产生中断/DMA: A.更新:计数器向上溢出/向下溢出,计数器初始化(通过软件或者内部/外部触发) B.触发事件(计数器启动、停止、初始化或者由内部/外部触发计数) C.输入捕获 D.输出比较
黑色加粗部分是实验主要会涉及到的功能。
以下将涉及这几个功能的寄存器进行着重介绍
- 控制寄存器1(TIMx_CR1)
在实验中只用到了TIMx_CR1的最低位,也就是计数器使能位,该位必须置1才能让定时器开始计 数。
-
DMA/中断使能寄存器(TIM_DIER)
这里我们同样仅关心它的第 0 位,该位是更新中断允许位,本章用到的是定时器的更新中 断,所以该位要设置为 1,来允许由于更新事件所产生的中断。
-
预分频寄存器(TIMx_PSC)
该寄存器用 设置对时钟进行分频,然后提供给计数器,作为计数器的时钟。
定时器的时钟来源有 4 个:
1)内部时钟(CK_INT)
2)外部时钟模式 1:外部输入脚(TIx)
3)外部时钟模式 2:外部触发输入(ETR),仅适用于 TIM2、TIM3、TIM4
4)内部触发输入(ITRx):使用 A 定时器作为 B 定时器的预分频器(A 为 B 提供时钟)。
这些时钟,具体选择哪个可以通过 TIMx_SMCR 寄存器的相关位来设置。这里的 CK_INT 时钟是从 APB1 倍频的来的,除非 APB1 的时钟分频数设置为 1(一般都不会是 1),否则通用 定时器 TIMx 的时钟是 APB1 时钟的 2 倍,当 APB1 的时钟不分频的时候,通用定时器 TIMx 的时钟就等于 APB1 的时钟。这里还要注意的就是高级定时器以及 TIM9~TIM11 的时钟不是来 自 APB1,而是来自 APB2 的。
-
TIMx_CNT寄存器
该寄存器是定时器的计数器,该寄存器存储了当前 定时器的计数值。
-
自动重装载寄存器(TIMx_ARR)
该寄存器在物理上实际对应着 2 个寄存器。 一个是程序员可以直接操作的,另外一个是程序员看不到的,这个看不到的寄存器在《STM32F4xx 中文参考手册》里面被叫做影子寄存器。事实上真正起作用的是影子寄存器。根据 TIMx_CR1 寄存器中 APRE 位的设置:
APRE=0时,预装载寄存器的内容可以随时传送到影子寄存器,此时 2者是连通的;而
APRE=1 时,在每一次更新事件(UEV)时,才把预装载寄存器(ARR)的内容传送到影子寄存器。
- 状态寄存器(TIMx_SR)
该寄存器用来标记当前与定时 器相关的各种事件/中断是否发生。
配置步骤
1)TIM3 时钟使能。
HAL 中定时器使能是通过宏定义标识符来实现对相关寄存器操作的,方法如下:
__HAL_RCC_TIM3_CLK_ENABLE(); //使能 TIM3 时钟
2)初始化定时器参数,设置自动重装值,分频系数,计数方式等。 在HAL库中,定时器的初始化参数是通过定时器初始化函数HAL_TIM_Base_Init实现的:
HAL_StatusTypeDef HAL_TIM_Base_Init(TIM_HandleTypeDef *htim);
该函数只有一个入口参数,就是 TIM_HandleTypeDef 类型结构体指针,结构体类型为下面
我们看看这个结构体的定义:
typedef struct {
TIM_TypeDef *Instance;
TIM_Base_InitTypeDef Init;
HAL_TIM_ActiveChannel Channel;
DMA_HandleTypeDef *hdma[7];
HAL_LockTypeDef Lock;
__IO HAL_TIM_StateTypeDef State;
}TIM_HandleTypeDef;
第一个参数 Instance 是寄存器基地址。和串口,看门狗等外设一样,一般外设的初始化结 构体定义的第一个成员变量都是寄存器基地址。
这在HAL中都定义好了,比如要初始化串口1, 那么 Instance 的值设置为 TIM1 即可。
第二个参数 Init 为真正的初始化结构体 TIM_Base_InitTypeDef 类型。该结构体定义如下:
typedef struct {
uint32_t Prescaler; //预分频系数
uint32_t CounterMode; //计数方式
uint32_t Period; //自动装载值 ARR
uint32_t ClockDivision; //时钟分频因子
uint32_t RepetitionCounter;
} TIM_Base_InitTypeDef;
该初始化结构体中,
-
参数 Prescaler 是用来设置分频系数的,刚才上面有讲解。
-
参数 CounterMode 是用来设置计数方式,可以设置为向上计数,向下计数方式还有中央对齐计数方 式,比较常用的是向上计数模式 TIM_CounterMode_Up 和向下计数模式 TIM_CounterMode_Down。
计数模式
通用定时器可以向上计数、向下计数、向上向下双向计数模式。
①向上计数模式:计数器从0计数到自动加载值(TIMx_ARR),然后重新从0开始计数并且产生一个计数器溢出事件。
②向下计数模式:计数器从自动装入的值(TIMx_ARR)开始向下计数到0,然后从自动装入的值重新开始,并产生一个计数器向下溢出事件。
③中央对齐模式(向上/向下计数):计数器从0开始计数到自动装入的值-1,产生一个计数器溢出事件,然后向下计数到1并且产生一个计数器溢出事件;然后再从0开始重新计数。
-
参数 Period 是设置自动重载计数周期值。
-
参数 ClockDivision 是用来 设置时钟分频因子,也就是定时器时钟频率 CK_INT 与数字滤波器所使用的采样时钟之间的分 频比。
-
参数 RepetitionCounter 用来设置重复计数器寄存器的值,用在高级定时器中。
-
第三个参数 Channel 用来设置活跃通道。前面我们讲解过,每个定时器最多有四个通道可 以用来做输出比较,输入捕获等功能之用。这里的 Channel 就是用来设置活跃通道的,取值范围为:HAL_TIM_ACTIVE_CHANNEL_1~ HAL_TIM_ACTIVE_CHANNEL_4。
-
第四个 hdma 是定时器的 DMA 功能时用到,为了简单起见,我们暂时不讲解太复杂。
-
第五个参数Lock和State,是状态过程标识符,是HAL库用来记录和标志定时器处理过程。
3)使能定时器更新中断,使能定时器
HAL 库中,使能定时器更新中断和使能定时器两个操作可以在函数 HAL_TIM_Base_Start_IT()中一次完成的,该函数声明如下:
HAL_StatusTypeDef HAL_TIM_Base_Start_IT(TIM_HandleTypeDef *htim);
该函数非常好理解,只有一个入口参数。调用该定时器之后,会首先调用__HAL_TIM_ENABLE_IT 宏定义使能更新中断,然后调用宏定义__HAL_TIM_ENABLE 使能 相应的定时器。这里我们分别列出单独使能/关闭定时器中断和使能/关闭定时器方法:
__HAL_TIM_ENABLE_IT(htim, TIM_IT_UPDATE);//使能句柄指定的定时器更新中断
__HAL_TIM_DISABLE_IT (htim, TIM_IT_UPDATE);//关闭句柄指定的定时器更新中断
__HAL_TIM_ENABLE(htim);//使能句柄 htim 指定的定时器
__HAL_TIM_DISABLE(htim);//关闭句柄 htim 指定的定时
4)TIM3 中断优先级设置
在定时器中断使能之后,因为要产生中断,必不可少的要设置 NVIC 相关寄存器,设置中 断优先级。之前多次讲解到中断优先级的设置,这里就不重复讲解。 和串口等其他外设一样,HAL库为定时器初始化定义了回调函数HAL_TIM_Base_MspInit。 一般情况下,与 MCU 有关的时钟使能,以及中断优先级配置我们都会放在该回调函数内部。 函数声明如下:
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim);
5)编写中断服务函数。
在最后,还是要编写定时器中断服务函数,通过该函数来处理定时器产生的相关中断。通常情况下,在中断产生后,通过状态寄存器的值来判断此次产生的中断属于什么类型。然后执行相关的操作,我们这里使用的是更新(溢出)中断,所以在状态寄存器 SR 的最低位。在处 理完中断之后应该向 TIM3_SR 的最低位写 0,来清除该中断标志。 跟串口一样,对于定时器中断,HAL 库同样为我们封装了处理过程。这里我们以定时器 3 的更新中断为例来讲解。
首先,中断服务函数是不变的,定时器 3 的中断服务函数为:
TIM3_IRQHandler();
一般情况下我们是在中断服务函数内部编写中断控制逻辑。但是 HAL 库为我们定义了新的定时器中断共用处理函数 HAL_TIM_IRQHandler,在每个定时器的中断服务函数内部,我们 会调用该函数。该函数声明如下:
void HAL_TIM_IRQHandler(TIM_HandleTypeDef *htim);
而函数 HAL_TIM_IRQHandler 内部,会对相应的中断标志位进行详细判断,判断确定中断来源后,会自动清掉该中断标志位,同时调用不同类型中断的回调函数。所以我们的中断控制 逻辑只用编写在中断回调函数中,并且中断回调函数中不需要清中断标志位。 比如定时器更新中断回调函数为:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim);
跟串口中断回调函数一样,我们只需要重写该函数即可。对于其他类型中断,HAL 库同样 提供了几个不同的回调函数,这里我们列出常用的几个回调函数:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim);//更新中断
void HAL_TIM_OC_DelayElapsedCallback(TIM_HandleTypeDef *htim);//输出比较
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim);//输入捕获
void HAL_TIM_TriggerCallback(TIM_HandleTypeDef *htim);//触发中断
Tout(溢出时间)=(ARR+1)(PSC+1)/Tclk
其中: Tclk:TIM3 的输入时钟频率(单位为 Mhz)。 Tout:TIM3 溢出时间(单位为 us)。
Part2 PWM输出实验
PWM介绍
脉冲宽度调制(PWM),是英文“Pulse Width Modulation”的缩写,简称脉宽调制,是利用 微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术。简单一点,就是对脉冲宽 度的控制,PWM 原理如图所示:
图中,我们假定定时器工作在向上计数 PWM 模式,且当 CNT<CCRx 时,输出 0,当 CNT>=CCRx 时输出 1。那么就可以得到如上的 PWM 示意图:
当 CNT 值小于 CCRx 的时候,IO 输出低电平(0),当 CNT 值大于等于 CCRx 的时候, IO 输出高电平(1),当 CNT 达到 ARR 值的时候,重新归零,然后重新向上计数,依次循环。 改变 CCRx 的值,就可以改变 PWM 输出的占空比,改变 ARR 的值,就可以改变 PWM 输出的 频率,这就是 PWM 输出的原理。
配置步骤
定时器输出 PWM 需要经过以下步骤:
1)开启 TIM2 和 GPIO 时钟,配置 PA0 和 PA1 选择复用功能 AF1(TIM2)输出。
2)初始化 TIM2,设置 TIM2 的 ARR 和 PSC 等参数。
3)设置 TIM2_CH1 和 TIM2_CH2 的 PWM 模式,输出比较极性,比较值等参数。
4)使能 TIM2,使能 TIM2 的 CH1 和 CH2 输出。
5)修改 TIM2_CCR1 和 TIM2_CCR2 来控制占空比。
用到的寄存器
除了上一章介绍的寄存器外,我们 还会用到 3 个寄存器,来控制 PWM 的。这三个寄存器分别是:捕获/比较模式寄存器 (TIMx_CCMR1/2)、捕获 /比较使能寄存(TIMx_CCER)、捕获 /比较寄存器(TIMx_CCR1~4)。 接下来我们简单介绍一下这三个寄存器。
捕获/比较模式寄存器 (TIMx_CCMR1/2)
该寄存器一般有 2 个:TIMx _CCMR1和 TIMx _CCMR2。TIMx_CCMR1 控制 CH1 和 2,而 TIMx_CCMR2 控制 CH3 和 4。以下我们 将以 TIM3 为例进行介绍。TIM3_CCMR2 寄存器各位描述如图所示:
该寄存器的有些位在不同模式下,功能不一样,所以在图中,我们把寄存器分了 2 层,上面一层对应输出而下面的则对应输入。这里我们需要说明的是模式设置位 OC4M,此部分由 3 位组成。总共可以配置成 7 种模式,我们使用的是 PWM 模式,所以这 3 位必须设置为 110/111。 这两种 PWM 模式的区别就是输出电平的极性相反。另外 CC4S 用于设置通道的方向(输入/输 出)默认设置为 0,就是设置通道作为输出使用。
** TIM3 的捕获/比较使能寄存器(TIM3_CCER)**
该寄存器比较简单,我们这里只用到了 CC4E 位,该位是输入/捕获 4 输出使能位,要想 PWM 从 IO 口输出,这个位必须设置为 1,所以我们需要设置该位为 1。
捕获/比较寄存器(TIMx_CCR1~4)
在输出模式下,该寄存器的值与 CNT 的值比较,根据比较结果产生相应动作。利用这点, 我们通过修改这个寄存器的值,就可以控制 PWM 的输出脉宽了。
配置涉及函数
1)开启 TIM2 和 GPIO 时钟,配置 PA0 和 PA1 选择复用功能 AF1(TIM2)输出。
HAL库使能 TIM2 时钟和 GPIO 时钟方法是:
__HAL_RCC_TIM2_CLK_ENABLE(); //使能定时器 2
__HAL_RCC_GPIOA_CLK_ENABLE(); //开启 GPIOA 时钟
接下来便是要配置 PA1 复用映射为 TIM2 的 PWM 输出引脚。关于 IO 口复用映射,在串口通信实验中有详细讲解,主要是通过函数 HAL_GPIO_Init 来实现的:
GPIO_Initure.Pin = GPIO_PIN_0 | GPIO_PIN_1; //PA0.1
GPIO_Initure.Mode = GPIO_MODE_AF_PP; //复用推挽输出
GPIO_Initure.Pull = GPIO_PULLUP; //上拉
GPIO_Initure.Speed = GPIO_SPEED_HIGH; //高速
GPIO_Initure.Alternate = GPIO_AF1_TIM2; //PA1复用为TIM2_CH1.CH2
HAL_GPIO_Init(GPIOA, &GPIO_Initure);
在 IO 口初始化配置中,我们只需要将成员变量 Mode 配置为复用推挽输出,同时成员变量 Alternate 配置为 GPIO_AF1_TIM2,即可实现 PA1 映射为定时器 1 通道 2 的 PWM 输出引脚。
2)初始化 TIM3,设置 TIM3 的 ARR 和 PSC 等参数。
初始化定时器的 ARR 和 PSC 等参数是通过函数 HAL_TIM_Base_Init 来 实现的,但是这里大家要注意,对于我们使用定时器的 PWM 输出功能时,HAL 库为我们提供 了一个独立的定时器初始化函数 HAL_TIM_PWM_Init,该函数声明为:
HAL_StatusTypeDef HAL_TIM_PWM_Init(TIM_HandleTypeDef *htim);
该函数实现的功能以及使用方法和 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,这里大家注意 区分。 所以大家一定要注意,使用 HAL_TIM_PWM_Init 初始化定时器时,回调函数为: HAL_TIM_PWM_MspInit,该函数声明为:
void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim);
一般情况下,上面步骤 1 的时钟使能和 IO 口初始化映射都编写在回调函数内部。
3)设置 TIM3_CH4 的 PWM 模式,输出比较极性,比较值等参数。
在 HAL 库中,PWM 通道设置是通过函数 HAL_TIM_PWM_ConfigChannel 来设置的:
HAL_StatusTypeDef HAL_TIM_PWM_ConfigChannel(
TIM_HandleTypeDef *htim, TIM_OC_InitTypeDef* sConfig, uint32_t Channel);
第一个参数 htim 是定时器初始化句柄,也就是 TIM_HandleTypeDef 结构体指针类型,这和 HAL_TIM_PWM_Init 函数调用时候参数保存一致即可。 第二个参数sConfig是TIM_OC_InitTypeDef结构体指针类型,这也是该函数最重要的参数。 该参数用来设置 PWM 输出模式,极性,比较值等重要参数。首先我们来看看结构体定义:
typedef struct {
uint32_t OCMode; //PWM 模式
uint32_t Pulse; //捕获比较值
uint32_t OCPolarity; //极性
uint32_t OCNPolarity;
uint32_t OCFastMode; //快速模式
uint32_t OCIdleState;
uint32_t OCNIdleState; } TIM_OC_InitTypeDef;
该结构体成员我们重点关注前三个。成员变量 OCMode 用来设置模式,也就是我们前面讲解的 7 种模式,这里我们设置为 PWM 模式 1。成员变量 Pulse 用来设置捕获比较值。成员变量 TIM_OCPolarity 用来设置输出极性是高还是低。其他的参数 TIM_OutputNState , TIM_OCNPolarity,TIM_OCIdleState 和 TIM_OCNIdleState 是高级定时器才用到的。 第三个参数 Channel 用来选择定时器的通道,取值范围为 TIM_CHANNEL_1~ TIM_CHANNEL_4。
例如我们要初始化定时器 2 的通道 4 为 PWM 模式 1,输出极性为低,那么实例代码为:
TIM2_CHnHandler.OCMode = TIM_OCMODE_PWM1; //模式选择PWM1
TIM2_CHnHandler.Pulse = arr / 2; //设置比较值,此值用来确定占空比,默认比较值为自动重装载值的一半,即占空比为50%
TIM2_CHnHandler.OCPolarity = TIM_OCPOLARITY_HIGH; //输出比较极性为低
HAL_TIM_PWM_ConfigChannel(&TIM2_Handler, &TIM2_CHnHandler, TIM_CHANNEL_1); //配置TIM2通道1
4)使能 TIM2,使能 TIM2 的 CH1.2 输出
在完成以上设置了之后,我们需要使能 TIM2 并且使能 TIM2_CH1 输出。在 HAL 库中, 函数 HAL_TIM_PWM_Start 可以用来实现这两个功能,函数声明如下:
HAL_StatusTypeDef HAL_TIM_PWM_Start(TIM_HandleTypeDef *htim, uint32_t Channel);
该函数第二个入口参数 Channel 是用来设置要使能输出的通道号。 对于单独使能定时器的方法,在上一章定时器实验我们已经讲解。实际上,HAL 库也同样提供了单独使能定时器的输出通道函数,函数为:
void TIM_CCxChannelCmd(TIM_TypeDef* TIMx, uint32_t Channel, uint32_t ChannelState);
5)修改 TIM3_CCR4 来控制占空比。
最后,在经过以上设置之后,PWM 其实已经开始输出了,只是其占空比和频率都是固定 的,而我们通过修改比较值TIM2_CCR1则可以控制CH1的输出占空比。HAL 库中并没有提供独立的修改占空比函数,这里我们可以编写这样一个函数如下:
void TIM_SetTIM2Compare1(u32 compare)
{
TIM2_CHnHandler.Pulse = compare;
//TIM2->CCR1 = compare;
}
程序怎么通过控制PWM占空比来控制电机速度的?
通过复用STM32L475的引脚PA0和PA1
通过查阅STM32L475的原理图,我们可以看到motorA和motorB分别连接到PA0和PA1上。
我们通过引脚复用:
GPIO_Initure.Mode = GPIO_MODE_AF_PP; //复用推挽输出
GPIO_Initure.Pull = GPIO_PULLUP; //上拉
GPIO_Initure.Speed = GPIO_SPEED_HIGH; //高速
GPIO_Initure.Alternate = GPIO_AF1_TIM2; //PA1复用为TIM2_CH1.CH2
将CH1和motorA,CH2和motor的输出值联系起来。
tips:复用推挽输出和普通推挽输出在输出时均使用2个mos管,其输出电路是相同的(P-mos和N-mos)。区别在于控制输出的信号来源:(普通)推挽输出控制MOS管的信号来源于输出寄存器,而复用推挽输出的控制信号来自于单片机的内置外设控制器。此时我们的控制信号就来源于CH1和CH2,我们通过控制通道的占空比来控制输入的电压大小从而控制motor的转速。
电压调节: 在PWM波频率一定的条件下,通过改变其du占空比的大小,来实现电压的值调节。
比如占空比为100%时,输出全电压,占空比为0时,输出电压为0。比如总的输出是30V,那么输出2-22V对应的占空比为2/30 --22/30,即占空比约为7%–73%。
在设计开关电源时,PWM的频率是一定的,计算输出电压时,占空比是50%,也就是一个周期内开关管一半时间导通,一半时间截止。如果设计输出2-20V,那取中间值11V来设计。