概述
定时器基础参考:
51单片机内部外设:定时器和计数器_路溪非溪的博客-CSDN博客
外设篇:定时器、看门狗和RTC_路溪非溪的博客-CSDN博客
查阅手册
stm32共有8个定时器,TIM1~TIM8,分为三类,分别是高级定时器TIM1和TIM8、通用定时器TIM2~TIM5和基本定时器TIM6和TIM7。这三种定时器有什么联系和区别?
先说结论:
通用有的,高级都有,正常使用。
但是基本定时器真的只有计算一下时间,产生更新中断的作用。基本定时器几乎没有任何对外输入/输出,常用做时基,实现最基本的定时计数功能。通用定时器挂载在APB1总线,高级定时器挂载在APB2总线。
下面是具体细节
【增强型】TIM1和TIM8定时器的功能包括:
● 16位向上、向下、向上/下自动装载计数器
● 16位可编程(可以实时修改)预分频器,计数器时钟频率的分频系数为1~65535之间的任意数值
● 多达4个独立通道: ─ 输入捕获 ─ 输出比较 ─ PWM生成(边缘或中间对齐模式) ─ 单脉冲模式输出
● 死区时间可编程的互补输出
● 使用外部信号控制定时器和定时器互联的同步电路
● 允许在指定数目的计数器周期之后更新定时器寄存器的重复计数器
● 刹车输入信号可以将定时器输出信号置于复位状态或者一个已知状态
● 如下事件发生时产生中断/DMA: ─ 更新:计数器向上溢出/向下溢出,计数器初始化(通过软件或者内部/外部触发) ─ 触发事件(计数器启动、停止、初始化或者由内部/外部触发计数) ─ 输入捕获 ─ 输出比较─ 刹车信号输入
● 支持针对定位的增量(正交)编码器和霍尔传感器电路
● 触发输入作为外部时钟或者按周期的电流管理【通用型】TIMx主要功能通用TIMx (TIM2、TIM3、TIM4和TIM5)定时器功能包括:
● 16位向上、向下、向上/向下自动装载计数器
● 16位可编程(可以实时修改)预分频器,计数器时钟频率的分频系数为1~65536之间的任意数值
● 4个独立通道: ─ 输入捕获 ─ 输出比较 ─ PWM生成(边缘或中间对齐模式) ─ 单脉冲模式输出
● 使用外部信号控制定时器和定时器互连的同步电路
● 如下事件发生时产生中断/DMA: ─ 更新:计数器向上溢出/向下溢出,计数器初始化(通过软件或者内部/外部触发) ─ 触发事件(计数器启动、停止、初始化或者由内部/外部触发计数) ─ 输入捕获 ─ 输出比较
● 支持针对定位的增量(正交)编码器和霍尔传感器电路
● 触发输入作为外部时钟或者按周期的电流管理【精简型】TIM6和TIM7定时器的主要功能包括:
● 16位自动重装载累加计数器
● 16位可编程(可实时修改)预分频器,用于对输入的时钟按系数为1~65536之间的任意数值分频
● 触发DAC的同步电路 注:此项是TIM6/7独有功能.
● 在更新事件(计数器溢出)时产生中断/DMA请求可参考:STM32系统学习——TIM(基本定时器)_Yuk丶的博客-CSDN博客_stm32tim
更多详细内容查阅手册。
影子寄存器
细心的人可以发现预分频器、自动重载寄存器下面有一个阴影。
这表示在物理上这个寄存器对应2个寄存器:一个是我们可以可以写入或读出的寄存器,称为预装载寄存器,另一个是我们看不见的、无法真正对其读写操作的,但在使用中真正起作用的寄存器,称为影子寄存器。
预装载寄存器的内容可以随时传送到影子寄存器,即两者是连通的。在每一次更新事件(UEV)时会把预装载寄存器的内容传送到影子寄存器。
设计预装载寄存器和影子寄存器的好处是,所有真正需要起作用的寄存器(影子寄存器)可以在同一个时间(发生更新事件时)被更新为所对应的预装载寄存器的内容,这样可以保证多个通道的操作能够准确地同步。
如果没有影子寄存器,软件更新预装载寄存器时,则同时更新了真正操作的寄存器,因为软件不可能在一个相同的时刻同时更新多个寄存器,结果造成多个通道的时序不能同步,如果再加上例如中断等其它因素,多个通道的时序关系有可能会混乱,造成是不可预知的结果。
原理图
定时器属于内部外设,无外部原理图。
基本定时器甚至连引脚都没有,这两个定时器被封装在了单片机的内部。
配置MX
首先搞清楚,配置定时器要配哪些东西?
- 定时器的工作时钟;
- 定时器的定时时间;
- 定时时间结束产生的中断;
具体操作流程如下:
首先,时钟来自哪里?
在时钟树中,显示TIM6是来自APB1再次分频后的TIMXCLK
输入的APB1是72MHz:
MX开始配置定时器6
参数设置中,都是什么意思?
第一项,Prescaler是预分频器的值,即将时钟源进行多少分频,因为是16位自动装载计数器,所以能够实现1~65536的任意数值分频。不过要注意的是,其数值是0~65535,填入0时,实现的是1分频,填入1时,实现的是2分频……填入65535时,实现的是65536分频。
如果我们要设置计数时钟是1MHz,假设这里填的数是a,那么应该有如下计算:
72000000/(a + 1) = 1000000;
可知a = 72000000/100000 - 1,也就是72000000/要设置的计数频率 - 1;
设置成1MHz时,要填入的数应该是72/1 - 1 = 71。即进行71+1=72分频。
第二项,是选择向上计数还是向下计数,选择向上即可(TIM6和TIM7只有UP)。
第三项,要记的数。其实知道了计数频率,也就是一个计数周期就是1us,那么这里是直接填多长时间,还是填要计多少数呢?这三者之间关系为:定时时间 = 计数周期 * 计数值;
自动重装累加计数器也是16位的,所以最大的计数个数为65536。也就是累加计数器能到的最大的数是65535,这里的默认值就是65535,再根据51单片机的定时器,我猜测,这里填入的就是要计多少个数(如果直接填入时间,那么单位也没法确定,不合理)
先计算下,最大能计多长时间,65536 * 1us = 65.536ms
如果我想计5ms,那么应该记的数是5000us / 1us = 5000。因为是从0开始计数,所以这里应该填入4999。
第四项,是选择是否自动重装(使能预装载寄存器),选择使能。
结果如下:
接下来,配置中断
定时间时间有没有到,我们可以通过轮询方式,去查询状态标志位,从而判断。但是这种方式下CPU要一直在盯着,所以,定时器天然就跟中断有着不可分割的关系。定时器时间到了,触发个中断即可,CPU可以空闲出来去做其他的事情。
这里主要是分配各个中断的优先级,Priority Group是设置将中断放到哪个优先级组中。这里涉及到中断,详情后续讲解,暂时选择2bit组,优先级都设置为1。
这里也相应有了变化:
OK,生成代码。
定时器相关代码
打开工程。
同时,多了定时器头文件,定时间初始化。这种模式下各个外设都是同样的框架。后面不再赘述。
直接看tim.c中的定时器6初始化代码:
TIM_HandleTypeDef htim6; /* TIM6 init function */ 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 = 71; htim6.Init.CounterMode = TIM_COUNTERMODE_UP; htim6.Init.Period = 5000; 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 */ }
就是那些初始化的内容,定义寄存器地址,然后将寄存器组成结构体,然后初始化各个参数。底层都是通过地址去操作寄存器相关位。
在tim.c文件中,另外还有个初始化函数:
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* tim_baseHandle) { if(tim_baseHandle->Instance==TIM6) { /* USER CODE BEGIN TIM6_MspInit 0 */ /* USER CODE END TIM6_MspInit 0 */ /* TIM6 clock enable */ __HAL_RCC_TIM6_CLK_ENABLE(); /* TIM6 interrupt Init */ HAL_NVIC_SetPriority(TIM6_IRQn, 1, 1); HAL_NVIC_EnableIRQ(TIM6_IRQn); /* USER CODE BEGIN TIM6_MspInit 1 */ /* USER CODE END TIM6_MspInit 1 */ } }
这个函数里,对定时器的时钟和相关的中断进行了初始化,在定时器初始化中被调用。
在main.c中,定时器初始化过后,运行函数中的代码暂时不去写任何内容了。因为采用了定时器中断,所以,定时时间结束时,就会触发中断函数。这和51里面是类似的。
所以,先将system.c中的Run函数中的内容给注释掉。
接着,我们跳转去中断函数,然后在中断函数中写业务代码。
在stm32f1xx_it.c文件中找到了TIM6的中断处理函数。
/** * @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 */ }
这个中断函数又调用了stm32f1xx_hal_tim.c库函数中的函数。可以看到这个:
继续,打开HAL_TIM_PeriodElapsedCallback(htim)
/** * @brief Period elapsed callback in non-blocking mode * @param htim TIM handle * @retval None */ __weak void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { /* Prevent unused argument(s) compilation warning */ UNUSED(htim); /* NOTE : This function should not be modified, when the callback is needed, the HAL_TIM_PeriodElapsedCallback could be implemented in the user file */ }
这是个回调函数,非阻塞模式下的回调函数。而且,前面有_weak关键字修饰。而且,这个函数里面什么都没做。UNUSED(htim)打开是个这:
#define UNUSED(X) (void)X /* To avoid gcc/g++ warnings */
明白了吗?
就是让你重写这个函数。
定时器相关库函数
打开定时器库函数的头文件,找到相关函数。这里面定义了大概小一百个函数,目前,我们使用最基本的:
/** @addtogroup TIM_Exported_Functions_Group1 TIM Time Base functions * @brief Time Base functions * @{ */ /* Time Base functions ********************************************************/ HAL_StatusTypeDef HAL_TIM_Base_Init(TIM_HandleTypeDef *htim); HAL_StatusTypeDef HAL_TIM_Base_DeInit(TIM_HandleTypeDef *htim); void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim); void HAL_TIM_Base_MspDeInit(TIM_HandleTypeDef *htim); /* Blocking mode: Polling */ HAL_StatusTypeDef HAL_TIM_Base_Start(TIM_HandleTypeDef *htim); HAL_StatusTypeDef HAL_TIM_Base_Stop(TIM_HandleTypeDef *htim); /* Non-Blocking mode: Interrupt */ HAL_StatusTypeDef HAL_TIM_Base_Start_IT(TIM_HandleTypeDef *htim); HAL_StatusTypeDef HAL_TIM_Base_Stop_IT(TIM_HandleTypeDef *htim); /* Non-Blocking mode: DMA */ HAL_StatusTypeDef HAL_TIM_Base_Start_DMA(TIM_HandleTypeDef *htim, uint32_t *pData, uint16_t Length); HAL_StatusTypeDef HAL_TIM_Base_Stop_DMA(TIM_HandleTypeDef *htim);
我们使用非阻塞式的中断触发相关函数:
HAL_StatusTypeDef HAL_TIM_Base_Start_IT(TIM_HandleTypeDef *htim); HAL_StatusTypeDef HAL_TIM_Base_Stop_IT(TIM_HandleTypeDef *htim);
这两个是定时开始和定时结束函数。传入的是具体定时器的结构体指针。这个指针在初始化的时候已经被定义过了。
开始编程
定时器编程的时候,重点关注两个地方:
第一个是在我们自己的外设初始化中开启定时器,调用启动函数即可实现。
HAL_StatusTypeDef HAL_TIM_Base_Start_IT(TIM_HandleTypeDef *htim);
首先添加相关头文件至myapplication.h中。
接着添加两个文件,tim6.c和tim6.h
tim6.h
#ifndef _TIM6_H_ #define _TIM6_H_ #include "stdint.h" typedef struct { void (*tim6Start)(void); } Tim6_Handler; extern Tim6_Handler tim6Handler; #endif
tim6.c
#include "myapplication.h" static void Tim6Handler(void); Tim6_Handler tim6Handler = { Tim6Handler }; static void Tim6Handler(void) { HAL_TIM_Base_Start_IT(&htim6); }
外设初始化时开启时钟
static void Peripheral_Set(void) { //开启时钟 tim6Handler.tim6Start(); }
第二个是要在中断处理函数中写上我们的业务代码。
定时器定的是5ms,如何实现每隔1秒灯闪烁?
参考51中定时器的原理,我们可以在中断函数中每循环中断200次之后再进行电平翻转。
像这种的回调函数的重写,我们都放在一个文件中,取名叫callback.c,之前讲架构的时候说过。
//重写TIM6中断调用函数 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if((htim->Instance) == (htim6.Instance)) { if(++circleCount == 200) { led_operater_middle.ledMiddle(LED1, LedSwitch); led_operater_middle.ledMiddle(LED2, LedSwitch); led_operater_middle.ledMiddle(LED3, LedSwitch); circleCount = 0; } } }
一开始,我在这里写的代码是这样的:
//重写TIM6中断调用函数 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if((htim->Instance) == (htim6.Instance)) { uint16_t circleCount = 0; if(++circleCount == 200) { led_operater_middle.ledMiddle(LED1, LedSwitch); led_operater_middle.ledMiddle(LED2, LedSwitch); led_operater_middle.ledMiddle(LED3, LedSwitch); circleCount = 0; } } }
想着进来的时候,初始计数是0,之后重复200次,就是1s了,但是写完将程序烧录后,没有任何反应,调试时也只能到第一行。
可知,下面的代码根本就没有执行。
问题出在哪里?
不应该在这里面定义局部变量来循环次数,每次进来,值都是0,是永远也执行不到里面的代码的。
应该在外部定义一个全局变量,我在tim6中做了定义:
#include "myapplication.h" static void Tim6Handler(void); //增加一个计数的全局变量 uint16_t circleCount = 0u; Tim6_Handler tim6Handler = { Tim6Handler }; static void Tim6Handler(void) { HAL_TIM_Base_Start_IT(&htim6); }
这样,全局变量循环到200的时候,才会进行灯闪代码的执行。
至此,完成基本定时器的使用。
关于这里的循环次数,其实可以优化下,定义一个枚举或者宏定义,将常用的数字给定义一下,然后再使用,以后要调整的时候,修改枚举值/宏定义即可。
#ifndef _TIM6_H_ #define _TIM6_H_ #include "stdint.h" typedef struct { void (*tim6Start)(void); } Tim6_Handler; typedef enum { TIME_COUNT_1S = 200u } commonTime; extern uint16_t circleCount; extern Tim6_Handler tim6Handler; #endif
//重写TIM6中断调用函数 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if((htim->Instance) == (htim6.Instance)) { if(++circleCount == TIME_COUNT_1S) { led_operater_middle.ledMiddle(LED1, LedSwitch); led_operater_middle.ledMiddle(LED2, LedSwitch); led_operater_middle.ledMiddle(LED3, LedSwitch); circleCount = 0; } } }
再补充一个问题,我看有的教程里,在重写函数后,跳转定义会跳转到强函数中,但是我怎么跳转都只能跳转到原先的弱函数中,不过,这并不代表重写无效。不会影响结果。
最后
以上就是明理小蜜蜂为你收集整理的STM32实战总结:HAL之基本定时器查阅手册原理图配置MX定时器相关代码定时器相关库函数开始编程的全部内容,希望文章能够帮你解决STM32实战总结:HAL之基本定时器查阅手册原理图配置MX定时器相关代码定时器相关库函数开始编程所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复