定时器的组成包括四部分:时钟选择单元,时基单元 ,输入捕获 ,输出比较。
一,时钟选择单元:
内部时钟(CK_INT)
外部时钟模式1:外部引脚输入
外部时钟模式2:外部触发输入ETR
内部触发输入(ITRX):定时器级联
二,时基单元组成
CNT计数器 :向上、 向下、中央对齐 三种计数方
PSC预分频寄存器
ARR自动重装载寄存器
三,输入捕获单元
在滤波电路处进行高频滤波,即一些高频波在滤波电路被滤掉
四,输出比较单元
此处应注意的是输出通道和输入通道是同一条,应进行设置!
定时器编程步骤:
16位向上、向下、向上/向下自动装载计数器
16位可编程(可实时修改)预分频器,计数器时钟频率的分频系数为1~65536之间
4个独立通道(TIMx_CH1~4),这些通道可以用来作为:输入捕获、输出比较、PWM生成(边缘或中间对齐模式)或单脉冲模式输出
可使用外部信号(TIMx_ETR)控制定时器和定时器互连(可以用一个定时器控制另一个定时器)的同步电路
产生相应事件的中断/DMA
A.更新:计数器向上溢出/向下溢出,计数器初始化(通过软件或者内部/外部触发)
B.触发事件:(计数器启动、停止、初始化或由内部/外部触发计数)
C.输入捕获
D.输出比较
E.针对定位的增量(正交)编码器和霍尔传感器
F.触发输入作为外部时钟或者按周期的电流管理
首先是控制寄存器 1(TIMx_CR1),该寄存器的各位描述如图 13.1.1 所示
在本实验中,我们只用到了 TIMx_CR1 的最低位,也就是计数器使能位,该位必须置 1, 才能让定时器开始计数。接下来介绍第二个与我们这章密切相关的寄存器:DMA/中断使能寄 存器(TIMx_DIER)。该寄存器是一个 16 位的寄存器,其各位描述如图 13.1.2 所示:
这里我们同样仅关心它的第 0 位,该位是更新中断允许位,本章用到的是定时器的更新中 断,所以该位要设置为 1,来允许由于更新事件所产生的中断。 接下来我们看第三个与我们这章有关的寄存器:预分频寄存器(TIMx_PSC)。该寄存器用 设置对时钟进行分频,然后提供给计数器,作为计数器的时钟。该寄存器的各位描述如图 13.1.3 所示:
这里,定时器的时钟来源有 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)的内容传送到影子寄存器。 自动重装载寄存器的各位描述如图 13.1.4 所示:
最后,我们要介绍的寄存器是:状态寄存器(TIMx_SR)。该寄存器用来标记当前与定时 器相关的各种事件/中断是否发生。该寄存器的各位描述如图 13.1.5 所示:
只要对以上几个寄存器进行简单的设置,我们就可以使用通用定时器了,并且可以产生中 断。 这一章,我们将使用定时器产生中断,然后在中断服务函数里面翻转 DS1 上的电平,来指 示定时器中断的产生。接下来我们以通用定时器 TIM3 为实例,来说明要经过哪些步骤,才能 达到这个要求,并产生中断。这里我们就对每个步骤通过库函数的实现方式来描述。首先要提 到 的 是 , 定 时 器 相 关 的 库 函 数 主 要 集 中 在 HAL 库 文件 stm32f4xx_hal_tim.h 和 stm32f4xx_hal_tim.c 文件中。定时器配置步骤如下
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。参数 Period 是设置自动重载计数周期值。参数 ClockDivision 是用来 设置时钟分频因子,也就是定时器时钟频率 CK_INT 与数字滤波器所使用的采样时钟之间的分 频比。参数 RepetitionCounter 用来设置重复计数器寄存器的值,用在高级定时器中。 第三个参数 Channel 用来设置活跃通道。前面我们讲解过,每个定时器最多有四个通道可 以用来做输出比较,输入捕获等功能之用。这里的 Channel 就是用来设置活跃通道的,取值范 围为:HAL_TIM_ACTIVE_CHANNEL_1~ HAL_TIM_ACTIVE_CHANNEL_4。 第四个 hdma 是定时器的 DMA 功能时用到,为了简单起见,我们暂时不讲解太复杂。 第五个参数Lock和State,是状态过程标识符,是HAL库用来记录和标志定时器处理过程。 定时器初始化范例如下
TIM_HandleTypeDef TIM3_Handler; //定时器句柄
TIM3_Handler.Instance=TIM3; //通用定时器 3
TIM3_Handler.Init.Prescaler=8999; //分频系数
TIM3_Handler.Init.CounterMode=TIM_COUNTERMODE_UP; //向上计数器
TIM3_Handler.Init.Period=4999; //自动装载值
TIM3_Handler.Init.ClockDivision=TIM_CLOCKDIVISION_DIV1;//时钟分频因子
HAL_TIM_Base_Init(&TIM3_Handler);
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);//触发中断
对于这些回调函数的使用方法我们在后面用到的时候会给大家详细讲解。 通过以上几个步骤,我们就可以达到我们的目的了,使用通用定时器的更新中断,来控制 DS1 的亮灭。
我配置按键为外部中断输入源,所以把引脚PA4-PA7配置为外部中断。然后设置为上拉输入,下降沿触发。
在NVIC中使能外部中断,并分配优先级。
static void MX_GPIO_Init(void)
{
/*Configure GPIO pins : PA4 PA5 PA6 PA7 */
GPIO_InitStruct.Pin = GPIO_PIN_4|GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/* EXTI interrupt init*/
HAL_NVIC_SetPriority(EXTI4_IRQn, 14, 0);
HAL_NVIC_EnableIRQ(EXTI4_IRQn);
HAL_NVIC_SetPriority(EXTI9_5_IRQn, 15, 0);
HAL_NVIC_EnableIRQ(EXTI9_5_IRQn);
}
对普通函数的调用:
调用程序发出对普通函数的调用后,程序执行立即转向被调用函数执行,直到被调用函数执行完毕后,再返回调用程序继续执行。从发出调用的程序的角度看,这个过程为“调用–>等待被调用函数执行完毕–>继续执行”
对回调函数调用:
调用程序发出对回调函数的调用后,不等函数执行完毕,立即返回并继续执行。这样,调用程序执和被调用函数同时在执行。当被调函数执行完毕后,被调函数会反过来调用某个事先指定函数,以通知调用程序:函数调用结束。这个过程称为回调(Callback),这正是回调函数名称的由来。
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
//添加回调后的程序逻辑
if (htim->Instance == htim2.Instance) //判断是否定时器2
{
}
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
//添加回调后的程序逻辑
if(huart->Instance == USART1) //判断是否串口1
{
}
}
回调函数的原理是使用函数指针实现类似“软中断”的概念。
比如在上层的两个函数A和B,把自己的函数指针传给了C,C通过调用A和B的函数指针达到“当做了什么,通知上层来调用A或者B”的目的。
从更底层的角度上,代码之间都是在一段程序里面或者可以理解为一致代码段的跳转。
这是我自己的一些学习的一些东西,有很多纰漏,望见谅。