一、定时器简介
STM32F1 系列中,除了互联型的产品,共有 8
个定时器,分为基本定时器,通用定时器和高级定时器。
基本定时器 TIM6
和 TIM7
是一个 16 位的只能向上计数的定时器,只能定时,没有外部 IO。
通用定时器 TIM2/3/4/5
是一个 16 位的可以向上/下计数的定时器,可以定时,可以输出比较,可以输入捕捉,每个定时器有四个外部 IO。
高级定时器 TIM1/8
是一个 16 位的可以向上/下计数的定时器,可以定时,可以输出比较,可以输入捕捉,还可以有三相电机互补输出信号,每个定时器有 8 个外部 IO。
二、新建工程
1. 打开 STM32CubeMX 软件,点击“新建工程”
2. 选择 MCU 和封装
3. 配置时钟
RCC 设置,选择 HSE(外部高速时钟) 为 Crystal/Ceramic Resonator(晶振/陶瓷谐振器)
选择 Clock Configuration,配置系统时钟 SYSCLK 为 72MHz
修改 HCLK 的值为 72 后,输入回车,软件会自动修改所有配置
4. 配置调试模式
非常重要的一步,否则会造成第一次烧录程序后续无法识别调试器
SYS 设置,选择 Debug 为 Serial Wire
5. 配置GPIO
GPIO 设置,在右边图中找到 LED 灯对应引脚,选择 GPIO_Output,输出低电平点亮,可以添加自定义标签
三、TIM6基本定时器
3.1 参数配置
在 Timers
中选择 TIM6
设置,并勾选 Activated
激活
在 Parameter Settings
进行具体参数配置。
Tclk 即内部时钟CK_INT,经过APB1预分频器后分频提供,如果APB1预分频系数等于1,则频率不变,否则频率乘以2,库函数中APB1预分频的系数是2,即PCLK1=36M,如图所以定时器时钟Tclk=36*2=72M。
定时器溢出时间:
Tout = 1 / (Tclk / (psc + 1)) ∗ (arr + 1)
- 定时器时钟Tclk:72MHz
- 预分频器psc:71
- 自动重装载寄存器arr:999
即 Tout = 1/(72MHz/(71+1))∗(999+1) = 1ms
-
Prescaler(时钟预分频数):72-1
则驱动计数器的时钟 CK_CNT = CK_INT(即72MHz)/(71+1) = 1MHz
-
Counter Mode(计数模式):Up(向上计数模式)
基本定时器只能是向上计数
-
Counter Period(自动重装载值):1000-1
则定时时间 1/CK_CLK*(999+1) = 1ms
- auto-reload-preload(自动重装载):Enable(使能)
-
TRGO Parameters(触发输出):不使能
在定时器的定时时间到达的时候输出一个信号(如:定时器更新产生TRGO信号来触发ADC的同步转换)
3.2 配置NVIC
使能定时器中断
3.3 生成代码
输入项目名和项目路径
选择应用的 IDE 开发环境 MDK-ARM V5
每个外设生成独立的
’.c/.h’
文件不勾:所有初始化代码都生成在 main.c
勾选:初始化代码生成在对应的外设文件。 如 GPIO 初始化代码生成在 gpio.c 中。
点击 GENERATE CODE 生成代码
3.4 修改中断回调函数
打开 stm32f1xx_it.c
中断服务函数文件,找到 TIM6 中断的服务函数 TIM6_IRQHandler()
中断服务函数里面就调用了定时器中断处理函数 HAL_TIM_IRQHandler()
打开 stm32f1xx_hal_tim.c
文件,找到定时器中断处理函数原型 HAL_TIM_IRQHandler()
,其主要作用就是判断是哪个定时器产生哪种事件中断,清除中断标识位,然后调用中断回调函数 HAL_TIM_PeriodElapsedCallback()
。
/* NOTE: This function Should not be modified, when the callback is needed,
the HAL_GPIO_EXTI_Callback could be implemented in the user file
*/
这个函数不应该被改变,如果需要使用回调函数,请重新在用户文件中实现该函数。
HAL_TIM_PeriodElapsedCallback()
按照官方提示我们应该再次定义该函数,__weak
是一个弱化标识,带有这个的函数就是一个弱化函数,就是你可以在其他地方写一个名称和参数都一模一样的函数,编译器就会忽略这一个函数,而去执行你写的那个函数;而 UNUSED(htim)
,这就是一个防报错的定义,当传进来的定时器号没有做任何处理的时候,编译器也不会报出警告。其实我们在开发的时候已经不需要去理会中断服务函数了,只需要找到这个中断回调函数并将其重写即可而这个回调函数还有一点非常便利的地方这里没有体现出来,就是当同时有多个中断使能的时候,STM32CubeMX会自动地将几个中断的服务函数规整到一起并调用一个回调函数,也就是无论几个中断,我们只需要重写一个回调函并判断传进来的定时器号即可。
接下来我们就在 stm32f1xx_it.c
这个文件的最下面添加 HAL_TIM_PeriodElapsedCallback()
/* USER CODE BEGIN 1 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
static uint32_t time = 0;
if(htim->Instance == TIM6) // 定时器6基地址
{
// 自定义应用程序
time++; // 每1ms进来1次
if(time == 1000) // 每1秒LED灯翻转一次
{
HAL_GPIO_TogglePin(LED_G_GPIO_Port,LED_G_Pin);
time = 0;
}
}
}
/* USER CODE END 1 */
3.5 添加定时器启动函数
现在进入 main 函数并在 while 循环前加入开启定时器函数 HAL_TIM_Base_Start_IT()
,这里所传入的 htim6 就是刚刚定时器初始化后的结构体。
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_TIM6_Init();
/* USER CODE BEGIN 2 */
HAL_TIM_Base_Start_IT(&htim6);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
现在实验现象是每1秒LED灯翻转一次
3.6 HAL库与标准库代码比较
STM32CubeMX 使用 HAL 库生成的代码:
/**
* @brief TIM6 Initialization Function
* @param None
* @retval None
*/
static void MX_TIM6_Init(void)
{
/* USER CODE BEGIN TIM6_Init 0 */
/* USER CODE END TIM6_Init 0 */
TIM_MasterConfigTypeDef sMasterConfig = {0};
/* USER CODE BEGIN TIM6_Init 1 */
/* USER CODE END TIM6_Init 1 */
htim6.Instance = TIM6;
htim6.Init.Prescaler = 72-1;
htim6.Init.CounterMode = TIM_COUNTERMODE_UP;
htim6.Init.Period = 1000-1;
htim6.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
if (HAL_TIM_Base_Init(&htim6) != HAL_OK)
{
Error_Handler();
}
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&htim6, &sMasterConfig) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN TIM6_Init 2 */
/* USER CODE END TIM6_Init 2 */
}
/**
* @brief This function handles TIM6 global interrupt.
*/
void TIM6_IRQHandler(void)
{
/* USER CODE BEGIN TIM6_IRQn 0 */
/* USER CODE END TIM6_IRQn 0 */
HAL_TIM_IRQHandler(&htim6);
/* USER CODE BEGIN TIM6_IRQn 1 */
/* USER CODE END TIM6_IRQn 1 */
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
static uint32_t time = 0;
if(htim->Instance == TIM6)
{
// 自定义应用程序
time++;
if(time == 1000)
{
HAL_GPIO_TogglePin(LED_G_GPIO_Port,LED_G_Pin);
time = 0;
}
}
}
HAL_TIM_Base_Start_IT(&htim6);
使用 STM32 标准库的代码:
/**
@brief 定时器中断配置(使用TIM6基本定时器)
@param 无
@return 无
*/
void BASIC_TIM_Config(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
/*
可以从上图看出基本定时器和通用定时器使用APB1总线,
高级定时器使用APB2总线。
*/
// 开启定时器时钟,即内部时钟 CK_INT=72M
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE);
/*
预分频将输入时钟频率按1~65536之间的值任意分频,分频值决定了计数频率。
计数值为计数的个数,当计数寄存器的值达到计数值时,产生溢出,发生中断。
如系统时钟为72MHz,预分频 TIM_Prescaler = 71,
计数值 TIM_Period = 1000,
则 TIM_Period * (TIM_Prescaler + 1) / 72000000 = 0.001,
即每1ms产生一次中断。
*/
// 自动重装载寄存器周的值(计数值)
TIM_TimeBaseStructure.TIM_Period = 1000;
// 累计 TIM_Period 个频率后产生一个更新或者中断
// 时钟预分频数为 71,
// 则驱动计数器的时钟 CK_CNT = CK_INT / (71+1)=1M
TIM_TimeBaseStructure.TIM_Prescaler = 71;
// 时钟分频因子 ,基本定时器没有,不用管
//TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1;
// 计数器计数模式,基本定时器只能向上计数,没有计数模式的设置
//TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up;
// 重复计数器的值,基本定时器没有,不用管
//TIM_TimeBaseStructure.TIM_RepetitionCounter=0;
/*
完成时基设置
*/
// 初始化定时器
TIM_TimeBaseInit(TIM6, &TIM_TimeBaseStructure);
/*
为了避免在设置时进入中断,这里需要清除中断标志位。
如果是向上计数模式(基本定时器采用向上计数),
则采用函数 TIM_ClearFlag(TIM6, TIM_FLAG_Update),
清除向上溢出中断标志。
*/
// 清除计数器中断标志位
TIM_ClearFlag(TIM6, TIM_FLAG_Update);
// 使能计数器
TIM_ITConfig(TIM6,TIM_IT_Update,ENABLE);
// 开启计数器
TIM_Cmd(TIM6, ENABLE);
// 暂时关闭定时器的时钟,等待使用
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, DISABLE);
}
/**
@brief NVIC初始化(使用TIM6基本定时器)
@param 无
@return 无
*/
void BASIC_TIM_NVIC_Config(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
// 设置中断组为 0
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);
// 设置中断来源
NVIC_InitStructure.NVIC_IRQChannel = TIM6_IRQn ;
// 设置主优先级为 0
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
// 设置抢占优先级为 3
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
#define BASIC_TIM_IRQHandler TIM6_IRQHandler
// 1ms发生一次中断,time 记录中断次数
uint16_t time;
void BASIC_TIM_IRQHandler(void)
{
if(TIM_GetITStatus(TIM6, TIM_IT_Update) != RESET)
{
time++;
TIM_ClearITPendingBit(TIM6, TIM_FLAG_Update);
}
}
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE);
MX_TIM6_Init();
对应 BASIC_TIM_Config();BASIC_TIM_NVIC_Config();
HAL_TIM_Base_Init(&htim6)
对应 TIM_TimeBaseInit(TIM6, &TIM_TimeBaseStructure)
HAL_TIM_Base_Start_IT(&htim6);
对应 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE);
四、注意事项
用户代码要加在 USER CODE BEGIN N
和 USER CODE END N
之间,否则下次使用 STM32CubeMX 重新生成代码后,会被删除。
• 由 Leung 写于 2021 年 1 月 14 日
• 参考:STM32CubeMX系列教程3:基本定时器
STM32CubeMX实战教程(四)——基本定时器(还是点灯)
《嵌入式-STM32开发指南》第二部分 基础篇 - 第4章 定时器(HAL库)