STM32 CubeMX学习:3. 定时器闪烁LED
系列文章目录文章目录
0.前言
这次的博文,我们要了解定时器的基本功能及其配置方法,还接触STM32中最重要的概念之一——中断,介绍在cubeMX中如何对中断进行设置,如何开启中断以及配置中断的优先级等,最后将实现由定时器触发的定时器中断,控制LED灯的闪烁。
1.基础学习
1.1 定时器功能讲解
对于定时器,这里我换种说法吧。我们可以把定时器比作闹铃,定时器的基本功能就是定时,在设定好对应的时间后,会在设定的时刻响起铃声。例如上次博文的滴答定时器,设定为1ms的定时时间,便每隔1ms引起中断函数。
在使用定时器时,我们会涉及到三个非常重要的概念——分频,计数,重载。这三个概念可以结合生活中使用的时钟来理解。
- 分频
时钟上不同的指针需要有不同的速度,也就是不同的频率,从而精确的表示时间,比如秒针,分针,时针,这三者相邻的频率之比都是60:1,即秒针每转过60格分针转动1格,分针转动60格时针转动1格,所以分针对于秒针的分频为60。 - 计数
时钟所对应的值都是与工作时间成正比的,比如秒针转动10格,意味着过了10秒,同样定时器中的计数也是和计数时间成正比的值,频率越高增长速度越快。 - 重载
时、分、秒的刻度都是有上限的,一个表盘最多记12小时,60分钟,60秒,如果继续增加的话就会回到0。同样的在定时器中也需要重载,当定时器中的计数值达到重载值时,计数值就会被清零。
现在,我们介绍三个与定时器有关的三个重要的寄存器
- 预分频寄存器TIMx_PSC
- 计数器寄存器TIMx_CNT
- 自动重装载寄存器TIMx_ARR
时钟源处的时钟信号经过预分频寄存器,按照预分频寄存器内部的值进行分频。比如时钟源的频率为12MHz,而预分频寄存器中设置的值为12:1,那么通过预分频后进入定时器的时钟频率就下降到了1MHz。
在已经分频后的定时器时钟驱使下,TIMx_CNT根据该时钟的频率向上计数,直到TIMx_CNT的值增长到与设定的自动重装载寄存器TIMx_ARR相等时,TIMx_CNT被清空,并重新从0开始向上计数,TIMx_CNT增长到TIMx_ARR中的值后被清空时产生一个定时中断触发信号。
综上定时器触发中断的时间是由设定的TIMx_PSC中的分频比和TIMx_ARR中的自动重装载值共同决定的。
定时器是STM32中非常重要的外设。在大多数应用场景中,部分任务需要周期性的执行,比如上一讲中提到的LED闪烁,这个功能就可以依靠定时器来实现,此外STM32的定时器还能够提供PWM输出,输入捕获,输出比较等多种功能。
1.2 中断的讲解
由单片机控制的机器人往往需要处理多种信号的输入,比如各种传感器信号等等,此外还需要进行多种信号的输出,比如控制电机的CAN信号,控制舵机的PWM等等,那么STM32是如何有序安排这些任务的呢?就是依赖于中断构成的前后台机制。
在STM32中,对信号的处理可以分为轮询方式和中断方式,轮询方式就是不断去访问一个信号的端口,看看有没有信号进入,有则进行处理,中断方式则是当输入产生的时候,产生一个触发信号告诉STM32有输入信号进入,需要进行处理。
例如,厨房里烧着开水,主人在客厅里看电视。为了防止开水烧干,他有两种方式,第一种是每隔10分钟就去厨房看一眼,另一种是等水壶烧开了之后开始发出响声再去处理。前者是轮询的方式,后者是中断的方式。
每一种中断都有对应的中断函数,当中断发生时,程序会自动跳转到处理函数处运行,而不需要人为进行调用。
当定时器的计数值增长到重载值时,在清空计数值的同时,会触发一次定时器中断,即定时器更新中断。只要设定好定时器的重载值,就可以保证定时器中断以固定的频率被触发。
2 程序的学习
2.1 定时器在CubeMX里的配置
首先我们按照之前的配置配置一个LED工程,忘记的小伙伴可以去看我之前的博客,相信大家一定没问题。
下面,我们进行如下的设置
-
在左侧的标签页中选择Timer,点击标签页下的TIM1。
-
在弹出的TIM1 Mode and Configuration中,在ClockSouce的右侧下拉菜单中选中Internal Clock。
-
此时TIM1得到使能,接下来需要配置TIM1的运转周期。需要在Clock Configuration标签页进行时钟树的配置,具体操作见我的第0篇博客。(前期的准备 ),这里我默认大家已经完成了相关的配置。
下面通过设置分频比和重载值来控制定时器的周期。
如果想要得到周期为500毫秒的定时器,我们需要回到Pinout&Configuration标签页下,对应TIMx_PSC寄存器的Prescaler项和对应TIMx_ARR寄存器的Counter Period项。500ms对应的频率为2Hz,为了得到2Hz的频率,可以将分频值设为16799,重载值设为4999。具体的原理我会在这篇博客后面的进阶学习中进行讲解。
按如图方式配置
2.2 中断优先级讲解
回顾刚才所说的中断概念,在STM32专门用于处理中断的控制器叫做NVIC,即嵌套向量中断控制器 (Nested Vectored Interrupt Controller)。
NVIC的功能非常强大,支持中断优先级和中断嵌套的功能,中断优先级即给不同的中断划分不同的响应等级,如果多个中断同时产生,则STM32优先处理高优先级的中断。
中断嵌套即允许在处理中断时,如果有更高优先级的中断产生,则挂起当前中断,先去处理产生的高优先级中断,处理完后再恢复到原来的中断继续处理。
这个过程理解起来就像是在上文的情境中,主人听到水烧开了,正打算去厨房时突然听到门口响起了急促的敲门声,那么主人就会先去执行开门的操作,然后再去厨房处理开水。
为了在有限的寄存器位数中实现更加丰富的中断优先级,NVIC使用了中断分组机制。STM32将先将中断进行分组,然后又将优先级划分为抢占优先级 (Prem priority) 和响应优先级 (Subpriority),抢占优先级和响应优先级的数量均可以通过NVIC中AIRCR寄存器的PRIGROUP[8:10]位进行配置,从而规定了两种优先级对NVIC_IPRx[7:4]的划分,根据划分决定两种优先级的数量。总共可以分成下表中的5种情况
抢占优先级级数 | 响应优先级级数 |
---|---|
000 | 0 |
001 | 0-1 |
010 | 0-3 |
011 | 0-7 |
100 | 0-15 |
拥有相同抢占优先级的中断处于同一个中断分组下。
当多个中断发生时,先根据抢占优先级判断哪个中断分组能够优先响应,再到这个中断分组中根据各个中断的响应优先级判断哪个中断优先响应。
2.3 CubeMX中的中断配置以及中断函数管理
2.3.1 CubeMX的中断配置
我们可以在CubeMX的NVIC标签页下可以看到当前系统中的中断配置,如下图所示:
列表中显示了当前系统中所有中断的使能情况与优先级设置。要使能中断则在Enable一栏打勾,这里选中TIM1 update interrupt,打勾,开启该中断。此外还可以在该页面下进行抢占优先级和响应优先级的分配和中断的两种优先级的配置。这里为定时器1的中断保持默认的0,0优先级。
点击Generate code,生成代码。
下面来看一下HAL库是如何对中断进行处理的。
在stm32f4xx_it.c中,找到cubeMX自动生成的中断处理函数
/**
* @brief This function handles TIM1 update interrupt and TIM10 global interrupt.
*/
void TIM1_UP_TIM10_IRQHandler(void)
{
/* USER CODE BEGIN TIM1_UP_TIM10_IRQn 0 */
/* USER CODE END TIM1_UP_TIM10_IRQn 0 */
HAL_TIM_IRQHandler(&htim1);
/* USER CODE BEGIN TIM1_UP_TIM10_IRQn 1 */
/* USER CODE END TIM1_UP_TIM10_IRQn 1 */
}
该函数调用了HAL库提供的HAL_TIM_IRQHandler这一函数
void HAL_TIM_IRQHandler(TIM_HandleTypeDef *htim)
该函数返回值为void,其作用在于HAL对涉及中断的寄存器进行处理,有一个参数,*htim 定时器的句柄指针,如定时器1就输入&htim1,定时器2就输入&htim2 。
在HAL_TIM_IRQHandler对各个涉及中断的寄存器进行了处理之后,会自动调用中断回调函数HAL_TIM_PeriodElapsedCallback,该函数使用__weak修饰符修饰,即用户可以在别处重新声明该函数,调用时将优先进入用户声明的函数。
__weak void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
一般我们需要在中断回调函数中判断中断来源并执行相应的用户操作。
2.3.2 定时器回调函数介绍
如前文所介绍的,HAL库在完成定时器的中断服务函数后会自动调用定时器回调函数。
通过配置TIM1的分频值和重载值,使得TIM1的中断以500ms的周期被触发。因此中断回调函数也是以500ms为周期被调用。在main.c中重新声明定时器回调函数,并编写内容如下:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim == &htim1)
{
//500ms trigger
bsp_led_toggle();
}
}
注意,这个函数需要我们自己在main.c文件中定义声明。
可以看到首先在回调函数中进行了中断来源的判断,判断其来源是否是定时器1。如果有其他的定时器产生中断,同样会调用该定时器回调函数,因此需要进行来源的判断。
在确认了中断源为定时器1后,调用bsp_led_toggle函数,翻转LED引脚的输出电平。
2.3.3 HAL_TIM_Base_Start函数
如果不开启中断,仅让定时器以定时功能工作,为了使定时器开始工作,需要调用HAL库提供的函数。
HAL_StatusTypeDef HAL_TIM_Base_Start(TIM_HandleTypeDef *htim)
该函数的返回值为HAL_StatusTypeDef,HAL库定义的几种状态,如果成功使定时器开始工作,则返回HAL_OK。其作用在于使对应的定时器开始工作。该函数有一个参数,*htim 定时器的句柄指针,如定时器1就输入&htim1,定时器2就输入&htim2。
如果需要使用定时中断,则需要调用函数
HAL_StatusTypeDef HAL_TIM_Base_Start_IT(TIM_HandleTypeDef *htim)
该函数的返回值为HAL_StatusTypeDef,HAL库定义的几种状态,如果成功使定时器开始工作,则返回HAL_OK。其作用在于使对应的定时器开始工作,并使能其定时中断。该函数有一个参数,*htim 定时器的句柄指针,如定时器1就输入&htim1,定时器2就输入&htim2
以上两个函数如果要使用则都需要在主循环while(1)之前调用。
3. 效果展示
定时器会以500ms定时触发中断,使得开发板C型上的LED灯会以500ms跳变一次,呈现闪烁的效果。使用定时器触发中断,时间精度决定于晶振的精度,比软件模拟延时精度大大提升。
4. 进阶学习
4.1 APB总线计算定时器定时时间
前面我们提到可以通过设置分频比和自动重装载值来控制定时器的定时周期,在这里结合芯片手册(Datasheet)来详细介绍一下定时周期的计算过程。芯片手册可以在Keil的book标签页下获取。
在手册中我们可以看到APB1和APB2总线上挂载在了大量的外设。定时器1,8,9,10,11挂载在APB2总线上。根据datasheet中给出的定时器所挂载的总线就可以确定定时器在分频前的时钟源频率。
在cubeMX的clock configuration标签页下,可以看到时钟树的结构。在整个时钟树的最右端,可以看到APB1和APB2两个总线的时钟频率设置,其中APBx peripheral clocks为挂载在总线上的定时器以外的外设提供时钟源,APBx timer clocks为挂载在总线上的定时器提供时钟源。
确定时钟源频率之后,根据数据手册的内容,分频值为TIMx_PSC中的分频值+1。即TIMx_SPC为0时,分频比刚好为1:1,如果TIMx_SPC为15,则分频比为16:1,进入的16MHz的频率信号会被分频为1MHz。
分频后的频率就是TIMx_CNT自增的频率,根据之前介绍的内容,当TIMx_CNT的值增长到TIMx_ARR中的值后,就会发生重载,并触发中断信号,相当于使用TIMx_ARR中的值又进行了一次分频。因此产生这个中断信号的频率应该为(需要加1是因为CNT是从0开始计数的)。
结合上面两个式子,可以得到最终的用于计算定时器触发频率的公式
其中f_CKPSK的值通过之前所说的方法,找到对应的APB总线进行确认。
完整的main.c代码如下所示
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
******************************************************************************
* @attention
*
* <h2><center>© Copyright (c) 2021 STMicroelectronics.
* All rights reserved.</center></h2>
*
* This software component is licensed by ST under BSD 3-Clause license,
* the "License"; You may not use this file except in compliance with the
* License. You may obtain a copy of the License at:
* opensource.org/licenses/BSD-3-Clause
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "tim.h"
#include "gpio.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
void bsp_led_toggle(void);
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
/**
* @brief Toggle the red led, green led and blue led
* @param[in] none
* @retval none
*/
/**
* @brief 反转红灯,绿灯和蓝灯电平
* @param[in] none
* @retval none
*/
void bsp_led_toggle(void)
{
HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_9);
HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_10);
}
/**
* @brief Period elapsed callback in non-blocking mode
* @param[in] htim TIM handle
* @retval none
*/
/**
* @brief 定时器周期定时回调
* @param[in] htim:定时器指针
* @retval none
*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim == &htim1)
{
//500ms trigger
bsp_led_toggle();
}
}
/* USER CODE END 0 */
/**
* @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_TIM1_Init();
/* USER CODE BEGIN 2 */
// HAL_TIM_Base_Start(&htim1);
HAL_TIM_Base_Start_IT(&htim1);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Configure the main internal regulator output voltage
*/
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
/** Initializes the CPU, AHB and APB busses clocks
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLM = 6;
RCC_OscInitStruct.PLL.PLLN = 168;
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
RCC_OscInitStruct.PLL.PLLQ = 4;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB busses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5) != HAL_OK)
{
Error_Handler();
}
}
/* USER CODE BEGIN 4 */
/* USER CODE END 4 */
/**
* @brief This function is executed in case of error occurrence.
* @retval None
*/
void Error_Handler(void)
{
/* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error return state */
/* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t *file, uint32_t line)
{
/* USER CODE BEGIN 6 */
/* User can add his own implementation to report the file name and line number,
tex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/
代码我已经放到了我的GitHub仓库,如有需要可以下载使用:
CubeMX学习
总结
这篇博客主要讲解了STM32的定时器外设,学习了如何通过配置几个重要的参数使定时器按照期望的周期运行,利用定时器可以实现嵌入式系统中重要的后台机制,即按照稳定的周期执行定时任务。 此外我们还学习了中断这一重要的概念,通过开启多样的中断,并合理制定其优先级,才能够在机器人上实时地运行丰富的任务。 阿巴阿巴,这次的博客的确有点长,大家可能已经晕菜 不过大家不要担心,现在继续开始敲黑板啦其实我们主要掌握的内容是
- 定时器基本功能
- 定时器的配置
- 定时器中断以及中断优先级讲解
- CubeMX中的中断管理