STM32通用定时器原理详解及实现代码指南
STM32通用定时器学习
写于 2024/8/16 下午
文章目录
1. 通用定时器简介
TIM2/TIM3 /TIM4 /TIM5为F1系列的通用定时器。
2. 基本框图
可以看到,在通用定时器内一共由6部分组成,分别为
- 时钟源
- 控制器
- 时基单元
- 输入捕获
- 输入捕获和输出比较公用部分
- 输出比较
通用定时器包含基本定时器的所有功能
2.1 时钟源
通用定时器时钟可以选择下面四类时钟源之一:
2.1.1 内部时钟(CK_INT)
相似的,跟基本定时器一样,通用定时器TIM2/TIM3 /TIM4 /TIM5和基本定时器TIM6/TIM7都挂载在APB1总线上,虽然APB1的频率最高为36Mhz,但经过倍频后最大可以达到72Mhz
2.1.2 外部时钟模式 1(TI1、TI2)
外部时钟模式1的输入信号来自定时器的通道1与通道2,通道3与通道4不能作为外部时钟模式的输入信号。时钟源进入定时器的流程如下:外部时钟源信号→IO→TIMx_CH1(或者 TIMx_CH2)。从 IO到 TIMx_CH1(或者 TIMx_CH2),就需要我们配置 IO 的复用功能,才能使 IO 和定时器通道相连通。
信号从TIMx_CH2进入后,通过滤波器整形,由 **ICF[3:0]**位来设置滤波方式
,然后抵达边沿检测器。边沿检测器可以选择上边沿检测或者下边沿检测,使用TIMx_CCER中的CC2P位来设置。
然后经过触发输入选择器,由 TIMx_SMCR的**TS[2:0]**位来选择 TRGI(触发输入信号)的来源。可以看到图 21.1.2 中框出了 TI1F_ED、TI1FP1 和 TI2FP2 三个触发输入信号(TRGI)。TI1F_ED 表示来自于 CH1,并且没有经过边沿检测器过滤的信号,所以它是 CH1 的双边沿信号,即上升沿或者下降沿都是有效的。TI1FP1 表示来自 CH1 并经过边沿检测器后的信号,可以是上升沿或者下降沿。TI2FP2 表示来自 CH2 并经过边沿检测器后的信号,可以是上升沿或者下降沿。
最后经过从模式选择器,由TIMx_SMCR的 ECE 位和 **SMS[2:0]**位来选择定时器的时钟源。这里我们介绍的是外部时钟模式 1,所以 ECE 位置 0,SMS[2:0] = 111 即可。CK_PSC 需要经过定时器的预分频器分频后,最终就能到达计数器进行计数了。
2.1.3 外部时钟模式2(ETR)
外部时钟模式 2,时钟源进入定时器的流程如下:外部时钟源信号→IO→TIMx_ETR。从 IO 到 TIMx_ETR,就需要我们配置 IO 的复用功能,才能使IO 和定时器相连通。
定时器时钟信号首先从 ETR 引脚进来。接着经过外部触发极性选择器,由 ETP 位来设置上升沿有效还是下降沿有效,选择下降沿有效的话,信号会经过反相器。
然后经过外部触发预分频器,由 **ETPS[1:0]**位来设置预分频系数,系数范围:1、2、4、8。
紧接着经过滤波器器,由 **ETF[3:0]**位来设置滤波方式,也可以设置不使用滤波器。fDTS 由TIMx_CR1 寄存器的 CKD 位设置。
最后经过从模式选择器,由 ECE 位和 **SMS[2:0]**位来选择定时器的时钟源。这里我们介绍的是外部时钟模式 2,直接把 ECE 位置 1 即可。CK_PSC 需要经过定时器的预分频器分频后,最终就能到达计数器进行计数了。
2.1.4 内部触发输入(ITRx)
内部触发输入是使用一个定时器作为另一个定时器的预分频器,即实现定时器的级联。下面以 TIM1 作为 TIM2 的预分频器为例。
上图中,TIM1 作为 TIM2 的预分频器,需要完成的配置步骤如下:
1,TIM1_CR2 寄存器的 MMS[2:0]位设置为 010,即 TIM1 的主模式选择为更新(选择更新事件作为触发输(TRGO))。
2,TIM2_SMCR 寄存器的 TS[2:0]位设置为 000,即使用 ITR0 作为内部触发。TS[2:0]位用于配置触发选择,除了 ITR0,还有其他的选择,详细描述如下图所示:
上图中的触发选择中,我们在讲解外部时钟模式 1 的时候说过 TI1F_ED、TI1FP1 和 TI2FP2,以及外部时钟模式 2 讲的 ETRF,它们都是属于外部的,其余的都是内部触发了。
在步骤 2 中,TS[2:0]位设置为 000,使用 ITR0作为内部触发,这个 ITR0 什么意思?由表21.1.3 可以知道,当从模式定时器为 TIM2 时,ITR0 表示主模式定时器就是 TIM1。这里只是TIM2~5 的内部触发连接情况,其他定时器请查看参考手册的相应章节。
3,TIM2_SMCR 寄存器的 SMS[2:0]位设置为 111,即从模式控制器选择外部时钟模式 1。
4,TIM1 和 TIM2 的 CEN 位都要置 1,即启动计数器。
2.2 控制器
控制器包括:从模式控制器、编码器接口和触发控制器(TRGO)。从模式控制器可以控制计数器复位、启动、递增/递减、计数。编码器接口针对编码器计数。触发控制器用来提供触发信号给别的外设,比如为其它定时器提供时钟或者为 DAC/ADC 的触发转换提供信号。
2.3 时基单元
时基单元包括:计数器寄存器(TIMx_CNT)、预分频器寄存器(TIMx_PSC)、自动重载寄存器(TIMx_ARR)。这部分内容和基本定时器基本一样的,不同点在于通用定时器的计数模式有三种:递增计数模式、递减计数模式和中心对齐模式。
2.4 输入捕获
这里是以通道 1 输入捕获为例进行介绍,其他通道同理。待测量信号到达 TIMx_CH1 后,那么这里我们把这个待测量信号用 TI1 表示
TI1 首先经过一个滤波器,由 ICF[3:0]位来设置滤波方式,也可以设置不使用滤波器。fDTS由 TIMx_CR1 寄存器的 CKD 位设置。
接着经过边沿检测器,由 CC1P 位来设置检测的边沿,可以上升沿或者下降沿检测。CC1NP是配置互补通道的边沿检测的,在高级定时器才有,通用定时器没有。
然后经过输入捕获映射选择器,由 CC1S[1:0]位来选择把 IC1 映射到 TI1、TI2 还是 TRC。这里我们的待测量信号从通道 1 进来,所以选择 IC1 映射到 TI1 上即可。
紧接着经过输入捕获 1 预分频器,由 ICPS[1:0]位来设置预分频系数,范围:1、2、4、8。
最后需要把 CC1E 位置 1,使能输入捕获,IC1PS 就是分频后的捕获信号。这个信号将会到达输入捕获和输出比较公用部分
2.4.1 输入捕获测量脉冲宽度
通用定时器输入捕获实验配置步骤
- 配置定时器基础工作参数:
HAL_TIM_IC_Init()
- 定时器输入捕获MSP初始化:
HAL_TIM_IC_MspInit()
配置NVIC、CLOCK、GPIO等 - 配置输入通道映射、捕获边沿等:
HAL_TIM_IC_ConfigChannel()
- 设置优先级,使能中断:
HAL_NVIC_SetPriority()
、HAL_NVIC_EnableIRQ()
- 使能定时器更新中断:
__HAL_TIM_ENABLE_IT()
- 使能捕获、捕获中断及计数器:
HAL_TIM_IC_Start_IT()
- 编写中断服务函数:
TIMx_IRQHandler()
HAL_TIM_IRQHandler()
- 编写更新中断和捕获回调函数:
HAL_TIM_PeriodElapsedCallback()
HAL_TIM_IC_CaptureCallback()
函数 | 主要寄存器 | 主要功能 |
---|---|---|
HAL_TIM_IC_Init() | CR1、ARR、PSC | 初始化定时器基础参数 |
HAL_TIM_IC_MspInit() | 无 | 存放NVIC、CLOCK、GPIO初始化代码 |
HAL_TIM_IC_ConfigChannel() | CCMRx、CCER | 配置通道映射、捕获边沿、分频、滤波等 |
__HAL_TIM_ENABLE_IT() | DIER | 使能更新中断等 |
HAL_TIM_IC_Start_IT() | CCER、DIER、CR1 | 使能输入捕获、捕获中断并启动计数器 |
HAL_TIM_IRQHandler() | SR | 定时器中断处理公用函数,处理各种中断 |
HAL_TIM_PeriodElapsedCallback() | 无 | 定时器更新中断回调函数,由用户重定义 |
HAL_TIM_IC_CaptureCallback() | 无 | 定时器输入捕获回调函数,由用户重定义 |
关键结构体介绍
typedef struct
{
uint32_t ICPolarity; /* 输入捕获触发方式选择,比如上升、下降沿捕获 */
uint32_t ICSelection; /* 输入捕获选择,用于设置映射关系 */
uint32_t ICPrescaler; /* 输入捕获分频系数 */
uint32_t ICFilter; /* 输入捕获滤波器设置 */
} TIM_IC_InitTypeDef;
具体实现源码
timer.c
#include "./BSP/TIMER/timer.h"
TIM_HandleTypeDef g_timer_handle;
void timer_init(uint16_t psc,uint16_t arr)
{
g_timer_handle.Instance = TIM5;
g_timer_handle.Init.Prescaler = psc;
g_timer_handle.Init.CounterMode = TIM_COUNTERMODE_UP;
g_timer_handle.Init.Period = arr;
HAL_TIM_IC_Init(&g_timer_handle);
TIM_IC_InitTypeDef g_timer_channel_handle = {0};
g_timer_channel_handle.ICPolarity = TIM_ICPOLARITY_RISING;
g_timer_channel_handle.ICSelection = TIM_ICSELECTION_DIRECTTI;
g_timer_channel_handle.ICPrescaler = TIM_ICPSC_DIV1;
g_timer_channel_handle.ICFilter = 0;
HAL_TIM_IC_ConfigChannel(&g_timer_handle,&g_timer_channel_handle,TIM_CHANNEL_1);
__HAL_TIM_ENABLE_IT(&g_timer_handle,TIM_IT_UPDATE);
HAL_TIM_IC_Start_IT(&g_timer_handle,TIM_CHANNEL_1);
}
void HAL_TIM_IC_MspInit(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM5)
{
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitTypeDef gpio_handle;
gpio_handle.Pin = GPIO_PIN_0;
gpio_handle.Mode = GPIO_MODE_AF_PP;
gpio_handle.Pull = GPIO_PULLDOWN;
gpio_handle.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &gpio_handle);
__HAL_RCC_TIM5_CLK_ENABLE();
HAL_NVIC_SetPriority(TIM5_IRQn,2,2);
HAL_NVIC_EnableIRQ(TIM5_IRQn);
}
}
void TIM5_IRQHandler()
{
HAL_TIM_IRQHandler(&g_timer_handle);
}
uint8_t g_timxchy_cap_sta = 0; /* 输入捕获状态 */
uint16_t g_timxchy_cap_val = 0; /* 输入捕获值 */
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM5)
{
if ((g_timxchy_cap_sta & 0X80) == 0) /* 还未成功捕获 */
{
if (g_timxchy_cap_sta & 0X40) /* 捕获到一个下降沿 */
{
g_timxchy_cap_sta |= 0X80; /* 标记成功捕获到一次高电平脉宽 */
g_timxchy_cap_val = HAL_TIM_ReadCapturedValue(&g_timer_handle, TIM_CHANNEL_1); /* 获取当前的捕获值 */
TIM_RESET_CAPTUREPOLARITY(&g_timer_handle, TIM_CHANNEL_1); /* 一定要先清除原来的设置 */
TIM_SET_CAPTUREPOLARITY(&g_timer_handle, TIM_CHANNEL_1, TIM_ICPOLARITY_RISING); /* 配置TIM5通道1上升沿捕获 */
}
else /* 还未开始,第一次捕获上升沿 */
{
g_timxchy_cap_sta = 0; /* 清空 */
g_timxchy_cap_val = 0;
g_timxchy_cap_sta |= 0X40; /* 标记捕获到了上升沿 */
__HAL_TIM_SET_COUNTER(&g_timer_handle, 0); /* 定时器5计数器清零 */
TIM_RESET_CAPTUREPOLARITY(&g_timer_handle, TIM_CHANNEL_1); /* 一定要先清除原来的设置!! */
TIM_SET_CAPTUREPOLARITY(&g_timer_handle, TIM_CHANNEL_1, TIM_ICPOLARITY_FALLING); /* 定时器5通道1设置为下降沿捕获 */
}
}
}
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM5)
{
if ((g_timxchy_cap_sta & 0X80) == 0) /* 还未成功捕获 */
{
if (g_timxchy_cap_sta & 0X40) /* 已经捕获到高电平了 */
{
if ((g_timxchy_cap_sta & 0X3F) == 0X3F) /* 高电平太长了 */
{
TIM_RESET_CAPTUREPOLARITY(&g_timer_handle, TIM_CHANNEL_1); /* 一定要先清除原来的设置 */
TIM_SET_CAPTUREPOLARITY(&g_timer_handle, TIM_CHANNEL_1, TIM_ICPOLARITY_RISING);/* 配置TIM5通道1上升沿捕获 */
g_timxchy_cap_sta |= 0X80; /* 标记成功捕获了一次 */
g_timxchy_cap_val = 0XFFFF;
}
else /* 累计定时器溢出次数 */
{
g_timxchy_cap_sta++;
}
}
}
}
}
main.c
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/delay/delay.h"
#include "./SYSTEM/usart/usart.h"
#include "./BSP/LED/led.h"
#include "./BSP/TIMER/timer.h"
extern uint8_t g_timxchy_cap_sta; /* 输入捕获状态 */
extern uint16_t g_timxchy_cap_val; /* 输入捕获值 */
int main(void)
{
uint32_t temp = 0;
uint8_t t = 0;
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟,72M */
delay_init(72); /* 初始化延时函数 */
led_init(); /* 初始化LED */
usart_init(115200);
timer_init(71,0xFFFF);
while(1)
{
if (g_timxchy_cap_sta & 0X80) /* 成功捕获到了一次高电平 */
{
temp = g_timxchy_cap_sta & 0X3F;
temp *= 65536; /* 溢出时间总和 */
temp += g_timxchy_cap_val; /* 得到总的高电平时间 */
printf("HIGH:%d us\r\n", temp); /* 打印总的高点平时间 */
g_timxchy_cap_sta = 0; /* 开启下一次捕获*/
}
t++;
if (t > 20) /* 200ms进入一次 */
{
t = 0;
LED0_TOGGLE(); /* LED0闪烁 ,提示程序运行 */
}
delay_ms(10);
}
}
2.4.2 通用定时器脉冲计数
- 配置定时器基础工作参数:
HAL_TIM_IC_Init()
- 定时器输入捕获MSP初始化:
HAL_TIM_IC_MspInit()
- 配置定时器从模式等:
HAL_TIM_SlaveConfigSynchro()
- 使能输入捕获并启动计数器:
HAL_TIM_IC_Start()
- 获取计数器的值:
__HAL_TIM_GET_COUNTER()
- 设置计数器的值:
__HAL_TIM_SET_COUNTER()
函数 | 主要寄存器 | 主要功能 |
---|---|---|
HAL_TIM_IC_Init() | CR1、ARR、PSC | 初始化定时器基础参数 |
HAL_TIM_IC_MspInit() | 无 | 存放NVIC、CLOCK、GPIO初始化代码 |
HAL_TIM_SlaveConfigSynchro() | SMCR、CCMRx、CCER | 配置定时器从模式、触发选择、分频、滤波等 |
HAL_TIM_IC_Start() | CCER、CR1 | 使能输入捕获、启动计数器 |
__HAL_TIM_GET_COUNTER() | CNT | 获取计数器当前值 |
__HAL_TIM_SET_COUNTER() | CNT | 设置计数器的值 |
关键结构体
typedef struct
{
uint32_t SlaveMode; /* 从模式选择 */
uint32_t InputTrigger; /* 输入触发源选择 */
uint32_t TriggerPolarity;/* 输入触发极性 */
uint32_t TriggerPrescaler; /* 输入触发预分频 */
uint32_t TriggerFilter; /* 输入滤波器设置 */
} TIM_SlaveConfigTypeDef;
2.5 输入捕获和输出比较公用部分
2.5.1 输入捕获部分
首先看到捕获/比较预装载寄存器,我们以通道 1 为例,那么它就是 CCR1 寄存器,通道 2、通道 3、通道 4 就分别对应 CCR2、CCR3、CCR4。在图 21.1.1 中就可以看到 CCR1~4 是有影子寄存器的,所以这里就可以看到图 21.1.8 中有捕获/比较影子寄存器,该寄存器不可直接访问。
下面来解读一下输入捕获部分的信号流程。首先CC1E要使能捕获,并且当输入捕获部分传来信号(也就是IC1PS上有信号传来)时才会发生捕获,将计数器的值捕获到影子寄存器中。并且发生捕获条件有三个。
IC1PS传来有效电平,CC1E使能捕获或软件产生捕获事件
CC1S配置为输入
CCR1未进行读操作
满足以上三点,我们才能在捕获预装载寄存器内读取到信号到来时的计数器值。
2.5.2 输出比较部分
首先程序员写 CCR1 寄存器,即写入比较值。这个比较值需要转移到对应的捕获/比较影子寄存器后才会真正生效。
什么条件下才能转移?图 21.1.9 中可以看到 compare_transfer 旁边的与门,需要满足三个条件:
CCR1 不在写入操作期间
CC1S[1:0] = 0 配置为输出
OC1PE 位置0(或者 OC1PE 位置 1,并且需要发生更新事件,这个更新事件可以软件产生或者硬件产生)
当 CCR1 寄存器的值转移到其影子寄存器后,新的值就会和计数器的值进行比较。
2.6 输出比较
上图中,可以看到输出模式控制器,由 OC1M[2:0]位配置输出比较模式
oc1ref 是输出参考信号,高电平有效,为高电平时称之为有效电平,为低电平时称之为无效电平。它的高低电平受到三个方面的影响:OC1M[3:0]位配置的输出比较模式、第5部分比较器的比较结果、还有就是 OC1CE 位配置的 ETRF 信号。ETRF 信号可以将 Oc1ref 电平强制清零,该信号来自 IO 外部。
一般来说,当计数器的值和捕获/比较寄存器的值相等时,输出参考信号 oc1ref 的极性就会根据我们选择的输出比较模式而改变。如果开启了比较中断,还会发生比较中断。CC1P 位用于选择通道输出极性。CC1E 位置 1 使能通道输出。OC1 信号就会从 TIMx_CH1 输出到 IO 端口,再到 IO 外部。
2.6.1 通用定时器PWM模式
通用定时器PWM输出实验配置步骤
- 配置定时器基础工作参数:
HAL_TIM_PWM_Init()
- 定时器PWM输出MSP初始化:
HAL_TIM_PWM_MspInit()
配置NVIC、CLOCK、GPIO等 - 配置PWM模式/比较值等:
HAL_TIM_PWM_ConfigChannel()
- 使能输出并启动计数器:
HAL_TIM_PWM_Start()
- 修改比较值控制占空比(可选):
__HAL_TIM_SET_COMPARE()
- 使能通道预装载(可选):
__HAL_TIM_ENABLE_OCxPRELOAD()
函数 | 主要寄存器 | 主要功能 |
---|---|---|
HAL_TIM_PWM_Init() | CR1、ARR、PSC | 初始化定时器基础参数 |
HAL_TIM_PWM_MspInit() | 无 | 存放NVIC、CLOCK、GPIO初始化代码 |
HAL_TIM_PWM_ConfigChannel() | CCMRx、CCRx、CCER | 配置PWM模式、比较值、输出极性等 |
HAL_TIM_PWM_Start() | CCER、CR1 | 使能输出比较并启动计数器 |
__HAL_TIM_SET_COMPARE() | CCRx | 修改比较值 |
__HAL_TIM_ENABLE_OCxPRELOAD() | CCER | 使能通道预装载 |
关键结构体
typedef struct
{
uint32_t OCMode; /* 输出比较模式选择 */
uint32_t Pulse; /* 设置比较值 */
uint32_t OCPolarity; /* 设置输出比较极性 */
uint32_t OCNPolarity; /* 设置互补输出比较极性 */
uint32_t OCFastMode; /* 使能或失能输出比较快速模式 */
uint32_t OCIdleState; /* 空闲状态下OC1输出 */
uint32_t OCNIdleState; /* 空闲状态下OC1N输出 */
} TIM_OC_InitTypeDef;
具体代码
timer.c
#include "./BSP/TIMER/timer.h"
TIM_HandleTypeDef g_timer_handle;
TIM_OC_InitTypeDef g_oc_timer_handle;
void timer_pwm_init(uint16_t psc,uint16_t arr)
{
g_timer_handle.Instance = TIM3;
g_timer_handle.Init.Prescaler = psc;
g_timer_handle.Init.Period = arr;
g_timer_handle.Init.CounterMode = TIM_COUNTERMODE_UP;
HAL_TIM_PWM_Init(&g_timer_handle);
g_oc_timer_handle.OCMode = TIM_OCMODE_PWM1;
g_oc_timer_handle.Pulse = arr/2;
g_oc_timer_handle.OCPolarity = TIM_OCPOLARITY_LOW;
HAL_TIM_PWM_ConfigChannel(&g_timer_handle,&g_oc_timer_handle,TIM_CHANNEL_2);
HAL_TIM_PWM_Start(&g_timer_handle,TIM_CHANNEL_2);
}
void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim)
{
if(htim ->Instance == TIM3)
{
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_TIM3_IS_CLK_ENABLED();
GPIO_InitTypeDef gpio_init_struct;
gpio_init_struct.Pin = GPIO_PIN_5;
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(GPIOB, &gpio_init_struct);
__HAL_RCC_AFIO_CLK_ENABLE();
__HAL_AFIO_REMAP_TIM3_PARTIAL();
}
}
main.c
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/delay/delay.h"
#include "./SYSTEM/usart/usart.h"
#include "./BSP/LED/led.h"
#include "./BSP/TIMER/timer.h"
int main(void)
{
uint16_t i = 0;
HAL_Init();
sys_stm32_clock_init(RCC_PLL_MUL9);
delay_init(72);
led_init();
timer_pwm_init(71,499);
while(1)
{
for(i = 0; i < 500; i++)
{
__HAL_TIM_SET_COMPARE(&g_timer_handle, TIM_CHANNEL_2, i);
delay_ms(10);
}
for(i = 500; i > 0; i--)
{
__HAL_TIM_SET_COMPARE(&g_timer_handle, TIM_CHANNEL_2, i);
delay_ms(10);
}
}
}
其实呼吸灯做出来效果不是很好,手上也没有示波器0.0
作者:dianfu233