第10章 STM32F4通用定时器简介
第十章 STM32F4通用定时器简介
1. 通用定时器简介
STM32F4 的通用定时器包含一个 16 位或 32 位自动重载计数器(CNT),该计数器由可编程预分频器(PSC) 驱动。 STM32F4 的通用定时器可以被用于:测量输入信号的脉冲长度(输入捕获)或者产生输出波形(输出比较和 PWM)等。 使用定时器预分频器和 RCC 时钟控制器预分频器,脉冲长度和波形周期可以在几个微秒到几个毫秒间调整。 STM32F4 的每个通用定时器都是完全独立的,没有互相共享的任何资源。
STM32 的通用 TIMx (TIM2~TIM5 和 TIM9~TIM14)定时器功能包括:
-
16 位/32 位(仅 TIM2 和 TIM5)向上、向下、向上/向下自动装载计数器(TIMx_CNT),注意: TIM9~TIM14 只支持向上(递增)计数方式。
-
16 位可编程(可以实时修改)预分频器(TIMx_PSC),计数器时钟频率的分频系数为 1~ 65535 之间的任意数值。
-
4 个独立通道(TIMx_CH1~4, TIM9~TIM14 最多 2 个通道),这些通道可以用来作为:
-
输入捕获
-
输出比较
-
PWM生成(边缘或中间对齐模式),注意:TIM9-TIM14不支持中间对齐模式
-
单脉冲模式输出
-
可使用外部信号(TIMx_ETR)控制定时器和定时器互连(可以用 1 个定时器控制另外一个定时器)的同步电路。
-
如下事件发生时产生中断/DMA(TIM9~TIM14 不支持 DMA):
-
更新:计数器向上溢出/向下溢出,计数器初始化(通过软件或者内部/外部触发)
-
触发事件(计数器启动、停止、初始化或者由内部/外部触发计数)
-
输入捕获
-
输出比较
-
支持针对定位的增量(正交)编码器和霍尔传感器电路(TIM9~TIM14 不支持)
-
触发输入作为外部时钟或者按周期的电流管理(TIM9~TIM14 不支持)
2. 定时器常用寄存器
2.1 控制寄存器TIMx_CR1
- CEN (Counter Enable):
- 位位置:0
- 功能:启用定时器计数器。当该位设置为1时,定时器开始计数。
- UDIS (Update Disable):
- 位位置:1
- 功能:禁止更新事件产生。当该位为1时,计数器更新事件被禁用。
- URS (Update Request Source):
- 位位置:2
- 功能:更新请求源配置。当该位为1时,仅当计数器达到最大值时才会产生更新事件。
- OPM (One Pulse Mode):
- 位位置:3
- 功能:单脉冲模式。当该位为1时,定时器在接收到触发事件后只会计数一次,然后自动停止。
- DIR (Direction):
- 位位置:4
- 功能:计数方向选择。0表示向上计数,1表示向下计数。
- CMS (Center-Aligned Mode Selection):
- 位位置:5-6
- 功能:选择中心对齐模式。当设置为特定值时,可以实现不同的对齐模式。
- ARPE (Auto-Reload Preload Enable):
- 位位置:7
- 功能:自动重载预装使能。当该位为1时,计数器的自动重载值在更新事件发生时会被预加载。
- CKD (Clock Division):
- 位位置:8-9
- 功能:时钟分频选择,影响计数器的计数速度。
在使用TIMx_CR1寄存器时,通常需要先配置其它相关寄存器(如预分频器和自动重载值),然后再配置CR1寄存器以启动定时器。以下是一个简单的代码示例:
TIM_HandleTypeDef htimx;
void MX_TIMx_Init(void) {
// 初始化定时器
htimx.Instance = TIMx;
htimx.Init.Prescaler = 8399; // 预分频
htimx.Init.CounterMode = TIM_COUNTERMODE_UP; // 向上计数
htimx.Init.Period = 9999; // 自动重载值
htimx.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_Base_Init(&htimx);
// 配置控制寄存器
TIMx->CR1 |= TIM_CR1_CEN; // 启动计数器
}
2.2 DMA/中断使能寄存器(TIMx_DIER)
- UIE (Update Interrupt Enable):
- 位位置:0
- 功能:使能计数器更新中断。当计数器溢出或达到自动重载值时,会产生更新中断。
- CC1IE (Capture/Compare 1 Interrupt Enable):
- 位位置:1
- 功能:使能通道1的捕获/比较中断。当通道1触发捕获或比较事件时,会产生中断。
- CC2IE (Capture/Compare 2 Interrupt Enable):
- 位位置:2
- 功能:使能通道2的捕获/比较中断。
- CC3IE (Capture/Compare 3 Interrupt Enable):
- 位位置:3
- 功能:使能通道3的捕获/比较中断。
- CC4IE (Capture/Compare 4 Interrupt Enable):
- 位位置:4
- 功能:使能通道4的捕获/比较中断。
- TIE (Trigger Interrupt Enable):
- 位位置:6
- 功能:使能触发中断。当定时器接收到触发信号时,会产生中断。
- COMIE (COM Interrupt Enable):
- 位位置:7
- 功能:使能通用中断。当比较模式产生中断时会触发。
- UDE (Update DMA Request Enable):
- 位位置:8
- 功能:使能更新事件的DMA请求。
- CC1DE (Capture/Compare 1 DMA Request Enable):
- 位位置:9
- 功能:使能通道1的DMA请求。
- CC2DE (Capture/Compare 2 DMA Request Enable):
- 位位置:10
- 功能:使能通道2的DMA请求。
- CC3DE (Capture/Compare 3 DMA Request Enable):
- 位位置:11
- 功能:使能通道3的DMA请求。
- CC4DE (Capture/Compare 4 DMA Request Enable):
- 位位置:12
- 功能:使能通道4的DMA请求。
在配置TIMx_DIER寄存器时,通常需要根据应用需求选择需要使能的中断和DMA请求。以下是一个简单的代码示例,展示如何使能更新中断和通道1的捕获中断:
void MX_TIMx_Init(void) {
// 初始化定时器
htimx.Instance = TIMx;
htimx.Init.Prescaler = 8399; // 预分频
htimx.Init.CounterMode = TIM_COUNTERMODE_UP; // 向上计数
htimx.Init.Period = 9999; // 自动重载值
HAL_TIM_Base_Init(&htimx);
// 配置中断使能寄存器
TIMx->DIER |= (TIM_DIER_UIE | TIM_DIER_CC1IE); // 使能更新中断和通道1中断
// 启动定时器
HAL_TIM_Base_Start(&htimx);
}
2.3 预分频寄存器(TIMx_PSC)
该寄存器用设置对时钟进行分频,然后提供给计数器,作为计数器的时钟。
这里,定时器的时钟来源有四个:
-
内部时钟(CK_INT)
-
外部时钟模式 1:外部输入脚(TIx)
-
外部时钟模式 2:外部触发输入(ETR),仅适用于 TIM2、 TIM3、 TIM4
-
内部触发输入(ITRx):使用 A 定时器作为 B 定时器的预分频器(A 为 B 提供时钟)。
这些时钟,具体选择哪个可以通过 TIMx_SMCR 寄存器的相关位来设置。这里的 CK_INT时钟是从 APB1 倍频的来的,除非 APB1 的时钟分频数设置为 1(一般都不会是 1),否则通用定时器 TIMx 的时钟是 APB1 时钟的 2 倍,当 APB1 的时钟不分频的时候,通用定时器 TIMx的时钟就等于 APB1 的时钟。这里还要注意的就是高级定时器以及 TIM9~TIM11 的时钟不是来自 APB1,而是来自 APB2 的。
使用示例
假设我们希望使用 84 MHz 的时钟来生成 1 秒的延迟。
- 计算预分频值:
- 输入时钟:84 MHz
- 目标计数频率:1 Hz
- 预分频值(PSC)计算:
- 配置定时器:
- 在初始化代码中,设置 TIMx_PSC 为 83,999,999。
以下是一个简单的示例,展示如何在 STM32 中配置 TIMx_PSC:
#include "stm32f4xx_hal.h"
void Timer_Config(void)
{
TIM_HandleTypeDef htim2;
// 启用 TIM2 时钟
__HAL_RCC_TIM2_CLK_ENABLE();
// 配置定时器基本参数
htim2.Instance = TIM2;
htim2.Init.Prescaler = 83999; // 84MHz / (83999 + 1) = 1kHz
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 1000 - 1; // 1s = 1000ms
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
// 初始化定时器
HAL_TIM_Base_Init(&htim2);
// 启动定时器
HAL_TIM_Base_Start(&htim2);
}
int main(void)
{
HAL_Init();
Timer_Config();
while (1)
{
// 主循环
if (__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE) != RESET)
{
__HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE);
// 每秒执行的代码
}
}
}
2.4 计数寄存器(TIMx_CNT)
该寄存器是定时器的计数器,该寄存器存储了当前定时器的计数值。
- 计数方向:
- 定时器可以配置为向上计数(从0到设定的自动重载值)或向下计数(从自动重载值降到0)。这一点通过定时器的配置寄存器中的相关位来设置。
- 计数值范围:
- 计数值的范围通常取决于定时器的位宽。例如,对于16位定时器,计数范围为0到65535;对于32位定时器,计数范围为0到4294967295。
- 读写操作:
- 可以通过读取TIMx_CNT寄存器来获得当前计数值,也可以通过写入该寄存器来重置计数器,或者设定一个新的起始计数值。
以下是一个简单的代码示例,展示如何读取和设置TIMx_CNT寄存器的值:
#include "stm32f4xx_hal.h"
void MX_TIMx_Init(void) {
// 初始化定时器结构体
TIM_HandleTypeDef htimx;
htimx.Instance = TIMx;
htimx.Init.Prescaler = 8399; // 预分频
htimx.Init.CounterMode = TIM_COUNTERMODE_UP; // 向上计数
htimx.Init.Period = 9999; // 自动重载值
HAL_TIM_Base_Init(&htimx);
// 启动定时器
HAL_TIM_Base_Start(&htimx);
}
void SomeFunction(void) {
// 读取当前计数值
uint32_t currentCount = TIMx->CNT;
// 打印当前计数值(假设有打印函数)
printf("Current Timer Count: %lu\n", currentCount);
// 重置计数器
TIMx->CNT = 0; // 将计数器重置为0
}
2.5 自动重装载寄存器(TIMx_ARR)
接着我们介绍自动重装载寄存器(TIMx_ARR),该寄存器在物理上实际对应着 2 个寄存器。一个是程序员可以直接操作的,另外一个是程序员看不到的,这个看不到的寄存器在《STM32F4xx 中文参考手册》里面被叫做影子寄存器。事实上真正起作用的是影子寄存器。
根据 TIMx_CR1 寄存器中 APRE 位的设置: APRE=0 时,预装载寄存器的内容可以随时传送到影子寄存器,此时 2 者是连通的;而 APRE=1 时,在每一次更新事件(UEV)时,才把预装载寄存器(ARR) 的内容传送到影子寄存器。
- 功能:
- TIMx_ARR定义了定时器的周期。当计数器(TIMx_CNT)达到此值时,会触发更新事件(Update Event),并根据配置可以产生中断或其他动作。
- 计数模式:
- 计数模式可以设置为向上计数、向下计数或中心对称计数,这些模式将影响计数过程中如何处理ARR值。
- 配置:
- 可以通过定时器的初始化函数来设置ARR的值。通常在配置定时器时一起设置。
- 范围限制:
- 对于16位定时器,ARR的有效值范围为0到65535;对于32位定时器,范围为0到4294967295。
以下是一个简单的代码示例,展示如何设置和读取TIMx_ARR寄存器的值:
#include "stm32f4xx_hal.h"
void MX_TIMx_Init(void) {
// 初始化定时器结构体
TIM_HandleTypeDef htimx;
htimx.Instance = TIMx; // 定时器实例
htimx.Init.Prescaler = 8399; // 预分频
htimx.Init.CounterMode = TIM_COUNTERMODE_UP; // 向上计数
htimx.Init.Period = 9999; // 设置自动重载值
HAL_TIM_Base_Init(&htimx);
// 启动定时器
HAL_TIM_Base_Start(&htimx);
}
void SomeFunction(void)
{
// 获取当前自动重载寄存器的值
uint32_t autoReloadValue = TIMx->ARR;
// 打印自动重载值(假设有打印函数)
printf("Auto Reload Value: %lu\n", autoReloadValue);
// 修改自动重载值
TIMx->ARR = 4999; // 将自动重载值修改为4999
}
2.6 状态寄存器(TIMx_SR)
该寄存器用来标记当前与定时器相关的各种事件/中断是否发生。
- 更新标志(UIF - Update Interrupt Flag):
- 功能:指示定时器溢出或更新事件。
- 操作:当计数器达到预设值并发生溢出时,该标志被置位(设为1)。用户可以通过软件清除该标志。
- 用途:常用于定时任务的调度。
- 捕获标志(CCxIF - Capture/Compare Interrupt Flag):
- 功能:用于指示输入捕获或输出比较事件。
- 操作:在发生输入捕获或输出比较时,相应的CCxIF标志位会被置位。
- 用途:可用于精确测量信号的时间或者生成精确的定时输出。
- 比较溢出标志(CCxOF - Capture/Compare Overcapture Flag):
- 功能:指示在捕获模式下,新的捕获事件发生时,之前的捕获数据未能被读取而导致覆盖。
- 操作:当新的捕获事件发生而未读取之前的事件时,该标志会被置位。
- 用途:用于避免数据丢失,程序员需要及时读取捕获数据以清除该标志。
以下是一个简单的STM32定时器使用的示例代码:
// 假设TIM2为定时器
void TIM2_IRQHandler(void) {
// 检查更新标志
if (TIM2->SR & TIM_SR_UIF) {
// 清除标志
TIM2->SR &= ~TIM_SR_UIF; // 或者可以通过执行相关操作来清除
// 执行定时器溢出处理
handle_timer_overflow();
}
// 检查捕获标志
if (TIM2->SR & TIM_SR_CC1IF) {
// 处理捕获事件
uint32_t captured_value = TIM2->CCR1;
// 清除捕获标志
TIM2->SR &= ~TIM_SR_CC1IF;
handle_capture_event(captured_value);
}
}
3. 定时器基本配置步骤
这一章,我们将使用定时器产生中断,然后在中断服务函数里面翻转 DS1 上的电平,来指示定时器中断的产生。接下来我们以通用定时器 TIM3 为实例,来说明要经过哪些步骤,才能达到这个要求,并产生中断。 这里我们就对每个步骤通过库函数的实现方式来描述。
3.1 TIM3时钟使能
HAL 中定时器使能是通过宏定义标识符来实现对相关寄存器操作的,方法如下:
__HAL_RCC_TIM3_CLK_ENABLE(); // 使能 TIM3 时钟
3.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.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 指定的定时器
3.4 TIM3中断优先级设置
在定时器中断使能之后,因为要产生中断,必不可少的要设置 NVIC 相关寄存器,设置中断优先级。之前多次讲解到中断优先级的设置,这里就不重复讲解。
和串口等其他外设一样, HAL 库为定时器初始化定义了回调函数 HAL_TIM_Base_MspInit。一般情况下,与 MCU 有关的时钟使能,以及中断优先级配置我们都会放在该回调函数内部。函数声明如下:
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim);
对于回调函数,这里我们就不做过多讲解,大家只需要重写这个函数即可。
3.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 的亮灭。
2024.10.2 第一次修订,后期不再维护