STM32 基本定时器简介

基本定时器

在 STM32F1 系列大容量 MCU 中,TIM6, TIM7 是最简单的定时器,称为基本定时器。STM32 拥有基本、通用、高级三种定时器,它们之间的关系是相互包含,高级的定时器拥有低级定时器的所有功能

STM32F1 的基本定时器拥有 16bit 自动重装装载累加计数器、16bit 可编程预分频器、触发 DAC 的同步电路和溢出时产生中断和 DMA 请求的功能。基本计时器只允许从下往上计数,最大分频系数达到了 65535 。计数器寄存器 TIMx_CNT 、预分频器 TIMx_PSC 和自动重装载寄存器 TIMx_ARR均可以在定时器运行时读写

基本定时器没有自己的 I/O ,但在芯片内部连接到了 DAC

基本定时器的时钟来源是系统时钟 SYSCLK ,在 STM32F103 中最高可达 72MHz

HAL库中对基本定时器的配置

定时器句柄类型定义

typedef struct
#endif /* USE_HAL_TIM_REGISTER_CALLBACKS */
{
  TIM_TypeDef                 *Instance;     /*!< Register base address            */
  TIM_Base_InitTypeDef        Init;          /*!< TIM Time Base required parameters*/
  HAL_TIM_ActiveChannel       Channel;       /*!< Active channel                   */
  DMA_HandleTypeDef           *hdma[7];      /*!< DMA Handlers array
                                                  This array is accessed by a @ref 													  DMA_Handle_index */
  HAL_LockTypeDef             Lock;          /*!< Locking object                   */
  __IO HAL_TIM_StateTypeDef   State;         /*!< TIM operation state              */

} TIM_HandleTypeDef;

基本定时器没有多通道,因此 Channel 没有意义。主要关注的是 TIM_Base_InitTypeDef Init

定时器初始化类型定义

typedef struct
{
  uint32_t Prescaler;        
  uint32_t CounterMode;       
  uint32_t Period;           
  uint32_t ClockDivision;    
  uint32_t RepetitionCounter;  
  uint32_t AutoReloadPreload;  
} TIM_Base_InitTypeDef;

基本定时器只有最基本的功能,因此在基本定时器的初始化结构体中,仅有 Prescaler CounterMode Period AutoReloadPreload 四项有效,分别代表了预分频系数、计数模式、自动重装载值和是否自动重装载四个选项,其中计数模式又被限定为向上计数

我们可以在同时初始化 TIM_HandleTypeDefTIM_Base_InitTypeDef 后使用下面的初始化函数完成对基本定时器的初始化,STM32CubeMX 生成的代码将自动完成这一操作

HAL_StatusTypeDef HAL_TIM_Base_Init(TIM_HandleTypeDef *htim);

定时器主从模式类型定义

typedef struct
{
  uint32_t  MasterOutputTrigger;  
  uint32_t  MasterSlaveMode;       
} TIM_MasterConfigTypeDef;

基本定时器并不支持主从触发模式,但使用 STM32CubeMX 初始化代码时仍会使用一个主从触发结构体初始化定时器,将主从触发模式设定为禁用(此段存疑)

主从触发模式仍待补充

启动/停止基本定时器

以上部分均可由 STM32CubeMX 自动配置完成。在 main 函数中,我们还需要调用定时器启动函数以启动定时器,也可以使用停止函数停止定时器。如果要使用中断或者 DMA 功能,则要使用相应的启动和停止函数

HAL_StatusTypeDef HAL_TIM_Base_Start(TIM_HandleTypeDef *htim);
HAL_StatusTypeDef HAL_TIM_Base_Stop(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_DMA(TIM_HandleTypeDef *htim, uint32_t *pData, uint16_t Length);
HAL_StatusTypeDef HAL_TIM_Base_Stop_DMA(TIM_HandleTypeDef *htim);

以中断模式使用基本定时器

原理

基本定时器的工作原理相当简单。计时器启动后,来自系统时钟的时钟信号经预分频器分频,驱动计数器寄存器递增,累加至自动重装载寄存器数值后,计数器清零并产生一个计数器溢出事件,进而产生更新事件(为什么叫更新事件?这跟定时器的影子寄存器有关,对于 STM32F1 系列详情可参考手册 RM0008 )。在不使用主从模式、TRGO 参数设定为 TIM_TRGO_RESET 的情况下,事件产生寄存器 TIMx_EGRUG 位会被置 1 ,引发对计数器的重置和中断请求或 DMA 请求

如果我们需要定期执行某些任务,例如每一秒闪烁一次 LED ,使用基本定时器产生中断会比使用延时函数好得多,因为 CPU 资源没有浪费在无意义的循环中。基本定时器的溢出频率可以使用下面的公式计算

功能实现

假设我们现在希望板载 LED 能够以 2Hz 的频率闪烁,基本思路是配置基本定时器以 2Hz 的频率产生更新事件并使能中断,再在中断回调函数中翻转 LED 引脚电平

定时器初始化

以下代码使用 STM32CubeMX 自动生成

TIM_HandleTypeDef htim6;

/* TIM6 init function */
void MX_TIM6_Init(void)
{
  TIM_MasterConfigTypeDef sMasterConfig = {0};

  htim6.Instance = TIM6;
  htim6.Init.Prescaler = 35999;
  htim6.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim6.Init.Period = 999;
  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();
  }

}

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, 0, 0);
    HAL_NVIC_EnableIRQ(TIM6_IRQn);
  /* USER CODE BEGIN TIM6_MspInit 1 */

  /* USER CODE END TIM6_MspInit 1 */
  }
}

主函数

以下代码使用 STM32CubeMX 自动生成

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 */
}

中断回调函数

STM32 的中断回调函数均为弱定义函数,要实现自己的中断回调功能,只需自己重新定义即可

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
   HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_5);
}

性能问题

HAL 库使用 HAL_TIM_IRQHandler() 处理定时器中断,这个函数会检查多个中断标志,执行多条汇编语句。对于那些运行得非常快的定时器,这将带来不可忽略的性能损失。在这样的情况下,应当自行处理中断请求以提升性能

以轮询模式使用基本定时器

尽管计数器寄存器允许在程序运行时被读取,读取计数器的值,查看其是否与希望的值相等的做法仍然是相当愚蠢的:高速运行的计数器完全可能错过 CPU 的读取。更好的方式是查看计数器的值是否大于设定值,或者直接获取定时器的标志位。 Mastering STM32 的作者 Carmine Noviello 认为中断才是定时器最好的打开方式,除非定时器运转速度相当高,以至于如果使用中断模式,毫秒级甚至纳秒级的中断触发会完全掩盖掉 MCU 的其他工作

下面代码来自于 Mastering STM32 ,暂未明白为何要清除标志

while (1) {
    if(__HAL_TIM_GET_FLAG(&tim) == TIM_FLAG_UPDATE) {
        //Clear the IRQ flag otherwise we lose other events
        __HAL_TIM_CLEAR_IT(htim, TIM_IT_UPDATE);

以DMA模式使用基本定时器

DMA 模式同样是定时器的常用使用模式,STM32 的定时器可以提供 7 种 DMA 请求来源,而基本定时器由于没有自己的 I/O ,只有更新事件来源一种

上文提到过,如果我们想使用定时器的 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_TIM_Base_Start_DMA() 函数的定义,会发现该 DMA 函数只能用于改变定时器的自动重装载值。因此,当我们要使用 DMA 进行其他外设的操作时,并不需要使用这个启动函数。

(待补充)