目录
一、硬件及工程
文章依赖的硬件及工程配置同本文作者的其他文章:细说ARM MCU的串口接收数据的实现过程-CSDN博客 https://wenchm.blog.csdn.net/article/details/139541112
二、STM32G4系列MCU的定时器
STM32G4系列MCU的定时器功能比较强大,有下面几种定时器:
- 1个高精度定时器(high-resolution timer);
- 3个高级控制定时器(advanced-control timer);
- 7个通用定时器(general-purpose timer);
- 2个基本定时器(basic timer);
- 2个看门狗定时器(watch-dog timer);
- 1个SysTick定时器。
|-------------------------|-----------------|--------|------------------|--------------------------------|----------|----------|------|
| 类 型 | 定时器 | 计数器 精度 | 计数器类型 | 预分频因子 | DMA 请求产生 | 捕捉/ 比较通道 | 互补输出 |
| 高精度 定时器 | HRTIM | 16位 | Up | 1/2/4(x2,x4, x8,x16,x32, 带DLL) | 是 | 12 | 有 |
| 高级 控制 | TIM1、TIM8、TIM20 | 16位 | Up、Down和 Up/Down | 1~65536 之间的整数 | 是 | 4 | 4 |
| 通用 | TIM2和TIM5 | 32位 | Up、Down和 Up/Down | 1~65536 之间的整数 | 是 | 4 | 无 |
| 通用 | TIM3和TIM4 | 16位 | Up、Down和 Up/Down | 1~65536 之间的整数 | 是 | 4 | 无 |
| 通用 | TIM15 | 16位 | Up | 1~65536 之间的整数 | 是 | 2 | 1 |
| 通用 | TIM16、TIM17 | 16位 | Up | 1~65536 之间的整数 | 是 | 1 | 1 |
| 基本 | TIM6、TIM7 | 16位 | Up | 1~65536 之间的整数 | 是 | 0 | 无 |
定时器最基本的功能是起到定时的作用,其中有一个关键模块:计数器(counter)。该计数器可以循环往复计数,计数的模式有三种类型:升、降和升/降。Up模式是从0到最大值递增计数,计到最大值后再从0重新开始计。
除了TIM2和TIM5以外,其余的定时器中,计数器都是16位,相应的计数最大值为65535。除了计数器的参数以外,定时器中的另一个比较重要的参数是预分频因子(prescaler factor),这个参数关系到两次计数之间的计时间隔(具体数值,还要结合定时器的时钟频率来计算)。此外,定时器还可用于输入捕捉,以及产生PWM波形(互补)输出;当然对这两个功能,不同的定时器是有差别的。
三、定时器中断的实现过程
本文利用 STM32G474RE上的通用定时器,以定时器中断的方式控制NUCLEO - G474RE板上的发光二极管LD2以不同的频率闪烁(该功能也可通过延时函数的方式实现)。
1、配置新工程.ioc
- 配置GPIO:配置PA5为输出,用PA5控制开发板上的LD2。Level=High,mode=PP,Pull_up,speed=High,Label=LED;
- 外部时钟,Serial Wire;
- 配置定时器:打开TIM3的配置界面,在模式(Mode)区,将时钟源(Clock Source)选择为Internal Clock;然后,在配置区中,将参数设置(Parameter Settings)选项卡中的预分频因子(Prescaler)和计数器周期(Counter Period)分别设置为999和16999。这两个参数从0开始计数,分频因子为999,实际为分频999+1倍;计数器周期的计算与此相同。这里的计数周期实际就是计数器计数时的最大值 ,在时钟频率确定的情况下,****预分频因子决定着两次计数之间的时间间隔。****所以,根据这两个参数以及定时器的时钟频率,就可以计算出定时器计数的周期。此外,把计数模式(Counter Mode)设置为升模式(Up),并使能自动重载(auto-reload preload)。
- 配置中断:使能TIM3的全局中断。将它的抢占式优先级设为1,响应优先级设为0。
- 配置系统时钟:将系统时钟(SYSCLK)频率配置为170 MHz。定时器的时钟来自高级外设总线(APB,Advanced Peripheral Bus),APB的时钟也有自己的预分频因子,如果该因子为1,则定时器的时钟频率就与APB时钟相同,也与系统时钟相同,都是170 MHz。
2、代码修改
至此,硬件配置便完成了。保存,启动代码生成过程,系统会将刚才配置硬件的信息自动转换成代码。
(1)时钟初始化函数MX_TIM3_Init()
该函数自动生成。
cpp
/**
* @brief TIM3 Initialization Function
* @param None
* @retval None
*/
static void MX_TIM3_Init(void)
{
/* USER CODE BEGIN TIM3_Init 0 */
/* USER CODE END TIM3_Init 0 */
TIM_ClockConfigTypeDef sClockSourceConfig = {0};
TIM_MasterConfigTypeDef sMasterConfig = {0};
/* USER CODE BEGIN TIM3_Init 1 */
/* USER CODE END TIM3_Init 1 */
htim3.Instance = TIM3;
htim3.Init.Prescaler = 999;
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
htim3.Init.Period = 16999;
htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
if (HAL_TIM_Base_Init(&htim3) != HAL_OK)
{
Error_Handler();
}
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
if (HAL_TIM_ConfigClockSource(&htim3, &sClockSourceConfig) != HAL_OK)
{
Error_Handler();
}
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&htim3, &sMasterConfig) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN TIM3_Init 2 */
/* USER CODE END TIM3_Init 2 */
}
MX_TIM3_Init()函数主要完成对TIM3的模式和参数配置,如预分频因子、计数模式、计数周期等参数。在MX_TIM3_Init()函数的定义中,用到了一个结构体变量htim3,该结构体变量也被称为定时器句柄。这个变量是在自动代码生成过程中自动生成的,位于main.c文件的最前面:
cpp
TIM_HandleTypeDef htim3;
在MX_TIM3_Init()函数的定义中,把设置的参数赋给了结构体变量htim3。其中一条if语句,在其条件表达式中调用了一个函数:HAL_TIM_Base_Init(&htim3);结构体变量htim3通过调用HAL_ TIM_Base_Init(&htim3)实现与实际硬件关联的。该函数只有一个参数。调用时,把刚配置的结构体变量htim3传递了过来。实际上,真正与硬件关联的,还不是HAL_TIM_Base_Init()函数,而是在HAL_TIM_Base_Init()函数中调用的TIM_Base_SetConfig()函数。正是通过TIM_Base_SetConfig()函数,才真正地把设置的参数传递给了相关寄存器。在库函数文件stm32g4xx_hal_tim.c中有对TIM_Base_Set-Config()函数的定义。
(2)使能定时器中断
虽然配置了TIM3的中断功能,但在默认情况下,中断不是开启的。所以,在使用时,还要开启该中断。开启定时器中断可以使用如下库函数:
cpp
HAL_TIM_Base_Start_IT(TIM_HandleTypeDef *htim);
该函数也只有一个参数,并且该参数也是一个结构体变量。对于TIM3来说,其实就可以用前面提到的htim3。开启定时器中断可以使用如下代码:
cpp
/*USER CODE BEGIN 2 */
HAL_TIM_Base_Start_IT(&htim3); //手动添加
/*USER CODE END 2*/
上述代码位于while(1)循环前面的注释对中。不过,需要将它放到TIM3初始化函数MX_TIM3_ Init()的后面。
(3)定时器中断服务函数
开启TIM3的中断后,当条件满足时,就会执行定时器中断服务函数TIM3_IRQHandler()。该函数是自动生成的,位置在stm32g4xx_it.c文件中有该函数的定义:
cpp
/**
* @brief This function handles TIM3 global interrupt.
*/
void TIM3_IRQHandler(void)
{
/* USER CODE BEGIN TIM3_IRQn 0 */
/* USER CODE END TIM3_IRQn 0 */
HAL_TIM_IRQHandler(&htim3);
/* USER CODE BEGIN TIM3_IRQn 1 */
/* USER CODE END TIM3_IRQn 1 */
}
TIM3_IRQHandler()函数的定义中又调用了HAL_TIM_IRQHandler()函数,此函数的定义在stm32g4xxhal_tim.c中。实际上,在HAL_TIM_IRQHandler()函数中,还会调用TIM中断的回调函数HAL_TIM_PeriodElapsedCallback()。这个函数的定义也是在stm32g4xxhal_tim.c中,不过,被定义为一个弱函数。这种方式与串口中断接收过程是类似的。就像对串口接收中断回调函数的处理一样,在定时器中断的使用中,需要做的是在main.c中重新定义TIM中断的回调函数。
(4)重定义定时器回调函数
在main.c中重新定义回调函数HAL_TIM_PeriodElapsedCallback()。在其中让PA5的输出状态翻转。具体实现如下:
cpp
/*USER CODE BEGIN 4 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin);
}
/*USER CODE END 4*/
本例中,定时器的预分频因子(Prescaler)和计数器周期(Counter Period)分别置为999和16999,定时器的时钟频率为170 MHz,最终TIM3中断的周期为
cpp
(999+1)/(170×10⁶)×(16999+1)=0.1(s),即频率为10 Hz。
3、下载并运行
编译工程并下载到硬件中运行,会看到LD2灯以5 Hz的频率闪烁。为什么是5 Hz因为控制PA5用的是Toggle。
4、修改定时器参数
修改定时器的预分频因子(Prescaler)和计数器周期(CounPeriod),改变LD2灯的闪烁频率为1 Hz、0.5 Hz等。
cpp
(19999+1)/(170×10⁶)×(16999+1)=2(s),即频率为2 Hz。
(9999+1)/(170×10⁶)×(16999+1)=1(s),即频率为1 Hz。