炸鸡派-定时器基础例程

定时器简介

基本定时器,计数中断、产生DMA请求。

通用定时器,PWM输出、输入捕获、脉冲计数。

高级定时器,输出比较、互补输出带死区控制、PWM输入。

中心对齐的计数模式可以生成对称的PWM波形信号。计数可以先增后减。

这种模式下,PWM 的高电平和低电平时间相等,从而使得 PWM 波形在中心线上对称。

main.c

cpp 复制代码
int main(void)
{
  // 初始化FLASH、SYSTIC中断,使能SYSCFG时钟和PWR时钟
  HAL_Init();

  // 配置系统时钟,包括PWR等级,各AHB、APB1/2时钟参数
  SystemClock_Config();

  // 初始化GPIO
  MX_GPIO_Init();
  // 初始化DMA
  MX_DMA_Init();
  // 初始化USART1 UART
  MX_USART1_UART_Init();
  // 初始化TIM11定时器
  MX_TIM11_Init();
  // 使用DMA接收UART数据,接收50字节
  HAL_UART_Receive_DMA(&huart1, RXbuf, 50);
  // 使能UART空闲中断
  __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
  // 启动TIM11定时器中断
  HAL_TIM_Base_Start_IT(&htim11);

  // 主循环
  while (1)
  {
    // 判断时间计数是否达到500ms
    if (timecount >= 500)
    {
      // 重置时间计数
      timecount = 0;
      // 打印提示信息
      printf("500ms time up\r\n");
      // 切换GPIOC_PIN_13的状态(闪烁LED)
      HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
    }
  }
}

DMA.c

MX_DMA_Init

可以看到 DMA的初始化,其实就是

使能DMA时钟、

DMA中断优先级、

使能DMA中断。

cpp 复制代码
void MX_DMA_Init(void)
{
  // 使能DMA2控制器时钟
  __HAL_RCC_DMA2_CLK_ENABLE();

  // 配置DMA中断
  // 设置DMA2_Stream2中断优先级为最高(0)
  HAL_NVIC_SetPriority(DMA2_Stream2_IRQn, 0, 0);
  // 使能DMA2_Stream2中断
  HAL_NVIC_EnableIRQ(DMA2_Stream2_IRQn);
}

在串口的MSP_Init中,DMA配置为普通模式。

根据 main函数中的

50字节接收完后,DMA自动停止。

USART

cpp 复制代码
void MX_USART1_UART_Init(void)
{

  /* USER CODE BEGIN USART1_Init 0 */

  /* USER CODE END USART1_Init 0 */

  /* USER CODE BEGIN USART1_Init 1 */

  /* USER CODE END USART1_Init 1 */
  huart1.Instance = USART1;
  huart1.Init.BaudRate = 115200;
  huart1.Init.WordLength = UART_WORDLENGTH_8B;
  huart1.Init.StopBits = UART_STOPBITS_1;
  huart1.Init.Parity = UART_PARITY_NONE;
  huart1.Init.Mode = UART_MODE_TX_RX;
  huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart1.Init.OverSampling = UART_OVERSAMPLING_16;
  if (HAL_UART_Init(&huart1) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN USART1_Init 2 */

  /* USER CODE END USART1_Init 2 */

}

在上面的代码中,调用了下面的UART相关的硬件初始化。 MCU Specific Package Initialization,MCU特定包初始化。

串口硬件(及DMA)配置

cpp 复制代码
void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{

  GPIO_InitTypeDef GPIO_InitStruct = {0};
  if(uartHandle->Instance==USART1)
  {
  /* USER CODE BEGIN USART1_MspInit 0 */

  /* USER CODE END USART1_MspInit 0 */
    /* USART1 clock enable */
    __HAL_RCC_USART1_CLK_ENABLE();

    __HAL_RCC_GPIOA_CLK_ENABLE();
    /**USART1 GPIO Configuration
    PA9     ------> USART1_TX
    PA10     ------> USART1_RX
    */
    GPIO_InitStruct.Pin = GPIO_PIN_9|GPIO_PIN_10;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
    GPIO_InitStruct.Alternate = GPIO_AF7_USART1;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    /* USART1 DMA Init */
    /* USART1_RX Init */
    hdma_usart1_rx.Instance = DMA2_Stream2;
    hdma_usart1_rx.Init.Channel = DMA_CHANNEL_4;
    hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
    hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_usart1_rx.Init.Mode = DMA_NORMAL;
    hdma_usart1_rx.Init.Priority = DMA_PRIORITY_LOW;
    hdma_usart1_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
    if (HAL_DMA_Init(&hdma_usart1_rx) != HAL_OK)
    {
      Error_Handler();
    }

    __HAL_LINKDMA(uartHandle,hdmarx,hdma_usart1_rx);

    /* USART1 interrupt Init */
    HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(USART1_IRQn);
  /* USER CODE BEGIN USART1_MspInit 1 */

  /* USER CODE END USART1_MspInit 1 */
  }
}
cpp 复制代码
hdma_usart1_rx.Instance = DMA2_Stream2;          // 指定使用的 DMA 控制器和流(DMA2 的 Stream2)
hdma_usart1_rx.Init.Channel = DMA_CHANNEL_4;     // 指定 DMA 通道(通道 4,通常对应 USART1 的接收)
hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY; // 数据传输方向:外设到内存
hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE;    // 外设地址不递增(USART 接收寄存器固定)
hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE;       // 内存地址递增(数据存储到连续内存)
hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; // 外设数据对齐方式:字节对齐
hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;    // 内存数据对齐方式:字节对齐
hdma_usart1_rx.Init.Mode = DMA_NORMAL;              // DMA 模式:普通模式(非循环)
hdma_usart1_rx.Init.Priority = DMA_PRIORITY_LOW;    // DMA 优先级:低
hdma_usart1_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;// 禁用 FIFO 模式
cpp 复制代码
#define DMA_NORMAL                    0x00000000U                  /*!< Normal mode: 普通模式,传输完成后停止 */
#define DMA_CIRCULAR                  ((uint32_t)DMA_SxCR_CIRC)    /*!< Circular mode: 循环模式,传输完成后自动重新开始 */
#define DMA_PFCTRL                    ((uint32_t)DMA_SxCR_PFCTRL)  /*!< Peripheral flow control mode: 外设流控模式,外设控制传输 */

串口中断

中断间的调用关系

USART1_IRQHandler

HAL_UART_IRQHandler

UART_Receive_IT

DMA2_Stream2_IRQHandler

HAL_DMA_IRQHandler

HAL_UART_RxCpltCallback

HAL_UART_RxHalfCpltCallback

HAL_UART_ErrorCallback

USART1_IRQHandler

通用的处理函数中判断了接收中断的使能和接收状态的挂起。

USART1_IRQHandler 的调用时机

USART1_IRQHandler 是 USART1 的中断处理函数,用于处理与 USART1 相关的中断事件。它的调用时机取决于 USART1 的中断标志位是否被置位。以下是具体的触发条件:

触发条件
  • 接收中断(RXNE) :当 USART1 的接收缓冲寄存器中有新数据时,接收中断标志位(UART_FLAG_RXNE)会被置位。

  • 发送中断(TC) :当 USART1 的发送缓冲寄存器中的数据被发送完成时,发送完成中断标志位(UART_FLAG_TC)会被置位。

  • 空闲中断(IDLE) :当 USART1 检测到一个空闲信号(即接收到一个持续的低电平)时,空闲中断标志位(UART_FLAG_IDLE)会被置位。

  • 错误中断(如帧错误、奇偶校验错误等):当 USART1 检测到通信错误时,相应的错误标志位会被置位。

DMA2_Stream2_IRQHandler 的调用时机

DMA2_Stream2_IRQHandler 是 DMA2 Stream2 的中断处理函数,用于处理与 DMA2 Stream2 相关的中断事件。它的调用时机取决于 DMA2 Stream2 的中断标志位是否被置位。以下是具体的触发条件:

触发条件
  • 传输完成中断(TC) :当 DMA2 Stream2 完成一次数据传输时,传输完成中断标志位(DMA_FLAG_TCIF2)会被置位。

  • 半传输中断(HT) :当 DMA2 Stream2 完成一半数据传输时,半传输中断标志位(DMA_FLAG_HTIF2)会被置位。

  • 传输错误中断(TE) :当 DMA2 Stream2 发生传输错误时,传输错误中断标志位(DMA_FLAG_TEIF2)会被置位。

  1. 处理中断事件

    • 如果是传输完成中断(DMA_FLAG_TCIF2),HAL 库会调用 HAL_UART_RxCpltCallback 回调函数。

    • 如果是半传输中断(DMA_FLAG_HTIF2),HAL 库会调用 HAL_UART_RxHalfCpltCallback 回调函数。

    • 如果是传输错误中断(DMA_FLAG_TEIF2),HAL 库会调用 HAL_UART_ErrorCallback 回调函数。

MX_TIM11_Init

TIM1, TIM8, TIM9, TIM10, TIM11 连接到 APB2 总线。

系统时钟频率的计算

根据系统时钟配置,系统时钟来源于高速内部晶振

STM32F4的HSI频率为16Mhz。

因此 PLL输出频率等于 800Mhz,也即系统时钟频率。

根据配置,AHB 总线时钟(HCLK)等于 SYSCLK。APB2的时钟等于HCLK的时钟。

即APB2的时钟频率也为 800Mhz。

定时器周期计算

cpp 复制代码
void MX_TIM11_Init(void)
{
  // 设置定时器实例为TIM11
  htim11.Instance = TIM11;

  // 配置定时器参数
  htim11.Init.Prescaler = 99;  // 预分频器值为99
  htim11.Init.CounterMode = TIM_COUNTERMODE_UP;  // 向上计数模式
  htim11.Init.Period = 999;  // 自动重装载值为999
  htim11.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;  // 时钟分频因子为1
  htim11.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;  // 禁用自动重装载寄存器的预装载

  // 初始化定时器
  if (HAL_TIM_Base_Init(&htim11) != HAL_OK)
  {
    Error_Handler();  // 初始化失败则调用错误处理函数
  }
}

最后得 0.5s。

HAL_UART_Receive_DMA

初始化串口的接收DMA。

主要是初始化 接收数据缓冲区、接收数据大小和各种回调(如传输完成、半完成、结束和终止)。

cpp 复制代码
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, 
                                                  uint8_t *pData, 
                                                    uint16_t Size)
{
  /* 检查是否已有接收过程在进行 */
  if (huart->RxState == HAL_UART_STATE_READY)
  {
    /* 检查接收数据指针和接收大小是否有效 */
    if ((pData == NULL) || (Size == 0U))
    {
      return HAL_ERROR;
    }

    /* 锁定当前处理过程 */
    __HAL_LOCK(huart);

    /* 设置接收类型为标准接收 */
    huart->ReceptionType = HAL_UART_RECEPTION_STANDARD;

    /* 启动DMA接收 */
    return (UART_Start_Receive_DMA(huart, pData, Size));
  }
  else
  {
    /* 如果已有接收过程在进行,则返回忙状态 */
    return HAL_BUSY;
  }
}
cpp 复制代码
HAL_StatusTypeDef UART_Start_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
  uint32_t *tmp;

  // 初始化接收缓冲区指针和接收数据大小
  huart->pRxBuffPtr = pData;
  huart->RxXferSize = Size;

  // 重置错误码和设置接收状态
  huart->ErrorCode = HAL_UART_ERROR_NONE;
  huart->RxState = HAL_UART_STATE_BUSY_RX;

  // 设置DMA传输完成、半完成、错误和中止的回调函数
  huart->hdmarx->XferCpltCallback = UART_DMAReceiveCplt;
  huart->hdmarx->XferHalfCpltCallback = UART_DMARxHalfCplt;
  huart->hdmarx->XferErrorCallback = UART_DMAError;
  huart->hdmarx->XferAbortCallback = NULL;

  // 启动DMA接收
  tmp = (uint32_t *)&pData;
  HAL_DMA_Start_IT(huart->hdmarx, (uint32_t)&huart->Instance->DR, *(uint32_t *)tmp, Size);

  // 清除溢出错误标志
  __HAL_UART_CLEAR_OREFLAG(huart);

  // 解锁处理
  __HAL_UNLOCK(huart);

  // 如果需要,启用奇偶校验错误中断
  if (huart->Init.Parity != UART_PARITY_NONE)
  {
    ATOMIC_SET_BIT(huart->Instance->CR1, USART_CR1_PEIE);
  }

  // 启用UART错误中断(帧错误、噪声错误、溢出错误)
  ATOMIC_SET_BIT(huart->Instance->CR3, USART_CR3_EIE);

  // 通过设置UART CR3寄存器中的DMAR位来启用接收DMA传输
  ATOMIC_SET_BIT(huart->Instance->CR3, USART_CR3_DMAR);

  return HAL_OK;
}

__HAL_UART_ENABLE_IT

在UART通信中,空闲中断通常在接收到数据之后,数据线上出现一段空闲时间时触发。这个空闲时间可以由用户配置,用于检测数据包的结束或链接的空闲状态。

cpp 复制代码
/*使能中断*/
__HAL_UART_ENABLE_IT(&huart1,  //串口1
                UART_IT_IDLE); //空闲中断

HAL_TIM_Base_Start_IT

启动定时器的基本计数功能,并使能中断。

cpp 复制代码
HAL_StatusTypeDef HAL_TIM_Base_Start_IT(TIM_HandleTypeDef *htim)
{
  uint32_t tmpsmcr;

  // 参数检查:确保提供的定时器实例有效
  assert_param(IS_TIM_INSTANCE(htim->Instance));

  // 状态检查:确保定时器处于就绪状态
  if (htim->State != HAL_TIM_STATE_READY)
  {
    return HAL_ERROR;
  }

  // 设置定时器状态为忙碌
  htim->State = HAL_TIM_STATE_BUSY;

  // 使能定时器更新中断(溢出中断)
  __HAL_TIM_ENABLE_IT(htim, TIM_IT_UPDATE);

  // 使能定时器外设,除非处于触发模式(在触发模式下,使能会在触发时自动完成)
  if (IS_TIM_SLAVE_INSTANCE(htim->Instance))
  {
    // 如果定时器是从模式,检查是否需要手动使能
    tmpsmcr = htim->Instance->SMCR & TIM_SMCR_SMS;
    if (!IS_TIM_SLAVEMODE_TRIGGER_ENABLED(tmpsmcr))
    {
      __HAL_TIM_ENABLE(htim);
    }
  }
  else
  {
    // 如果定时器不是从模式,直接使能
    __HAL_TIM_ENABLE(htim);
  }

  // 返回函数执行状态
  return HAL_OK;
}

在从模式下,定时器的计数行为(如计数频率、计数方向和计数周期)可以由外部信号或另一个定时器的输出控制。

触发从模式:外部触发信号可以启动、停止或重新初始化定时器的计数。

外部输入从模式:定时器的计数行为(如计数方向、计数值)可以由外部输入信号控制。

内部触发从模式:定时器可以由内部事件(如另一个定时器的更新或捕获事件)触发。

TIM1_TRG_COM_TIM11_IRQHandler

中断在xxx_it.c

当中断源是 TIM1/TIM11 的触发/通信事件时,就会执行这个 ISR。

1. 触发事件(TRG)

  • 功能:处理定时器的触发输入事件。

  • 示例

    • 定时器的触发输入信号(如外部信号)导致的中断。

    • 例如,当外部信号通过 TRG 输入引脚触发定时器时,会调用此中断服务例程。

2. 通信事件(COM)

  • 功能:处理定时器的通信事件。

  • 示例

    • 输出比较事件(OC):定时器的输出比较事件,例如定时器的通道输出匹配。

    • 捕获事件(IC):定时器的输入捕获事件,例如定时器的通道输入捕获。

    • 通信事件(COM):例如,定时器的主从模式通信事件。

3. 不包括的事件

  • 更新事件(UIF):定时器的更新事件(如定时器溢出事件)通常由其他中断服务例程处理,例如:

    • TIM1 的更新事件由 TIM1_UP_IRQHandler 处理。

    • TIM11 的更新事件由 TIM11_IRQHandler 处理。

cpp 复制代码
void TIM1_TRG_COM_TIM11_IRQHandler(void)
{
  /* USER CODE BEGIN TIM1_TRG_COM_TIM11_IRQn 0 */
	timecount += 1;
  /* USER CODE END TIM1_TRG_COM_TIM11_IRQn 0 */
  HAL_TIM_IRQHandler(&htim11);
  /* USER CODE BEGIN TIM1_TRG_COM_TIM11_IRQn 1 */

  /* USER CODE END TIM1_TRG_COM_TIM11_IRQn 1 */
}

注意,基础的溢出事件执行的是 TIMx_UP_IRQHandler 或 TIMx_IRQHandler。

TIM1的触发(Trigger)

触发功能允许一个定时器(或外部信号)触发另一个定时器的特定事件。在 TIM1 中,触发事件可以是启动、停止或重新初始化定时器的计数。触发可以由以下来源产生:

  • 内部触发:来自同一微控制器内其他定时器的事件。

  • 外部触发:来自微控制器外部的信号,如 GPIO 引脚上的信号。

例如,假设我们有两个定时器 TIM1 和 TIM2,我们希望在 TIM2 每次更新(计数溢出)时,启动 TIM1 的计数。这可以通过配置 TIM1 的触发输入来实现,使其响应 TIM2 的更新事件。

TIM1的换向(Commutation)

换向功能通常用于无刷直流电机(BLDC)控制,它涉及到在正确的时刻改变电机相位的电流方向。在 TIM1 中,换向可以通过配置定时器的互补输出来实现,这些输出可以连接到电机驱动器的开关器件。

例如,对于一个三相电机,我们需要在特定的时刻改变三相绕组的电流方向。TIM1 可以配置为在检测到特定的转子位置信号时,通过其互补输出改变电流方向,从而实现换向。

举例

假设我们正在控制一个无刷直流电机,我们使用 TIM1 来生成 PWM 信号,控制电机的三相绕组。以下是如何使用 TIM1 的触发和换向功能的示例:

  1. 配置 TIM1 为 PWM 模式:设置 TIM1 的通道为 PWM 模式,以生成控制电机绕组的信号。

  2. 配置触发输入:如果我们需要 TIM1 的 PWM 输出与另一个定时器(如 TIM2)同步,我们可以配置 TIM1 的触发输入来响应 TIM2 的更新事件。

  3. 配置换向 :使用 TIM1 的互补输出和输入捕获功能来检测转子的位置。根据转子位置信号,通过软件或硬件逻辑来控制 TIM1 的输出,以在正确的时刻改变电流方向。

  4. 启动 TIM1:使能 TIM1 的更新中断,并启动定时器。在中断服务例程中,根据转子位置更新 TIM1 的捕获比较寄存器,以改变 PWM 输出的占空比,从而控制电机的速度和方向。

定时器知识点

2 个基本定时器(TIM6 和 TIM7)、

10 个通用定时 器(TIM2~TIM5,TIM9~TIM14)、

2 个高级控制定时器(TIM1 和 TIM8)。
定时器的中断服务函数声明在.s文件的中断向量表中

用的时候可以自己在it.c或者自己的驱动中去实现声明和定义。

计数中断

初始化,配置重载值、计数值,到点了执行中断。

不同定时器的中断函数不同,不同模式的中断函数也不同。

直接查手册去做。

通用定时器PWM输出

引脚的复用功能一般在GPIO配置中设置,如

cpp 复制代码
void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == GTIM_TIMX_PWM)
    {
        GPIO_InitTypeDef gpio_init_struct;
        GTIM_TIMX_PWM_CHY_GPIO_CLK_ENABLE();                            /* 使能通道y的GPIO时钟 */
        GTIM_TIMX_PWM_CHY_CLK_ENABLE();                                 /* 使能定时器时钟 */

        gpio_init_struct.Pin = GTIM_TIMX_PWM_CHY_GPIO_PIN;              /* 使能通道y的GPIO口 */
        gpio_init_struct.Mode = GPIO_MODE_AF_PP;                        /* 复用推挽输出 */
        gpio_init_struct.Pull = GPIO_PULLUP;                            /* 上拉 */
        gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;                  /* 高速 */
        gpio_init_struct.Alternate = GTIM_TIMX_PWM_CHY_GPIO_AF;         /* 定时器x通道y的GPIO复用 */
        HAL_GPIO_Init(GTIM_TIMX_PWM_CHY_GPIO_PORT, &gpio_init_struct);
    }
}

这里PWM引脚被设置为服用推挽,并设置了复用参数。

而定时器的PWM初始化设置中,

cpp 复制代码
void gtim_timx_pwm_chy_init(uint16_t arr, uint16_t psc)
{
    TIM_OC_InitTypeDef timx_oc_pwm_chy = {0};                                               /* 定时器输出句柄 */
    
    g_timx_pwm_chy_handle.Instance = GTIM_TIMX_PWM;                                         /* 定时器x */
    g_timx_pwm_chy_handle.Init.Prescaler = psc;                                             /* 预分频系数 */
    g_timx_pwm_chy_handle.Init.CounterMode = TIM_COUNTERMODE_UP;                            /* 递增计数模式 */
    g_timx_pwm_chy_handle.Init.Period = arr;                                                /* 自动重装载值 */
    HAL_TIM_PWM_Init(&g_timx_pwm_chy_handle);                                               /* 初始化PWM */

    timx_oc_pwm_chy.OCMode = TIM_OCMODE_PWM1;                                               /* PWM1模式 */
    timx_oc_pwm_chy.Pulse = arr / 2;                                                        /* 设置比较值,此值用来确定占空比 */

    timx_oc_pwm_chy.OCPolarity = TIM_OCPOLARITY_LOW;                                        /* 输出比较极性为低 */

    HAL_TIM_PWM_ConfigChannel(&g_timx_pwm_chy_handle,  //定时器配置句柄
                                   &timx_oc_pwm_chy,   //
                                GTIM_TIMX_PWM_CHY); /* 配置TIMx通道y */

    HAL_TIM_PWM_Start(&g_timx_pwm_chy_handle, GTIM_TIMX_PWM_CHY);                           /* 开启对应PWM通道 */
}

PWM模式1和PWM模式2的区别在于,

PWM 模式 1:简单直接,适用于大多数基本 PWM 应用,其中输出引脚在计数器匹配到 CC 寄存器值时切换状态。

PWM 模式 2:提供更复杂的控制,允许更灵活的 PWM 波形生成,适用于需要特殊 PWM 波形的应用,如中心对齐模式。
输出比较极性决定了当定时器的计数器匹配到捕获/比较(CC)寄存器的值时,输出引脚是置高还是置低。

再就是定时器通道和PWM引脚的关系,定时器一般4个PWM通道,每个通道可以配置为不同的功能。定时器通道可以配置一个具备该功能的复用引脚,来进行输出。

  1. 多任务处理

多个通道允许定时器同时执行多个任务。例如,在一个通道上生成一个PWM信号来控制电机速度,同时在另一个通道上进行输入捕获来测量旋转编码器的脉冲。

  1. 同步操作

在需要同步多个事件的应用中,多个通道可以协调工作。例如,在电机控制中,可能需要同时控制多个相位的电流,每个通道控制一相。

  1. 复杂的PWM控制

对于需要复杂PWM控制的应用,如无刷直流电机(BLDC)控制,多个通道可以用于生成相位差的PWM信号,以实现电机的换向。

通用定时器输入捕获

输入捕获模式可以用来测量脉冲宽度或者测量频率。

先配置通道为上升沿触发中断, 触发后开始计时,同时配置下降沿触发中断,触发后计时结束。时间差就是我们测量的脉冲宽度。

cpp 复制代码
void gtim_timx_cap_chy_init(uint16_t arr, uint16_t psc)
{
    TIM_IC_InitTypeDef timx_ic_cap_chy = {0};
    
    g_timx_cap_chy_handle.Instance = GTIM_TIMX_CAP;                                        /* 定时器5 */
    g_timx_cap_chy_handle.Init.Prescaler = psc;                                            /* 预分频系数 */
    g_timx_cap_chy_handle.Init.CounterMode = TIM_COUNTERMODE_UP;                           /* 递增计数模式 */
    g_timx_cap_chy_handle.Init.Period = arr;                                               /* 自动重装载值 */
    HAL_TIM_IC_Init(&g_timx_cap_chy_handle);                                               /* 初始化定时器 */
    
    timx_ic_cap_chy.ICPolarity = TIM_ICPOLARITY_RISING;                                    /* 上升沿捕获 */
    timx_ic_cap_chy.ICSelection = TIM_ICSELECTION_DIRECTTI;                                /* 映射到TI1上 */
    timx_ic_cap_chy.ICPrescaler = TIM_ICPSC_DIV1;                                          /* 配置输入分频,不分频 */
    timx_ic_cap_chy.ICFilter = 0;                                                          /* 配置输入滤波器,不滤波 */
    HAL_TIM_IC_ConfigChannel(&g_timx_cap_chy_handle, &timx_ic_cap_chy, GTIM_TIMX_CAP_CHY); /* 配置TIM5通道1 */

    __HAL_TIM_ENABLE_IT(&g_timx_cap_chy_handle, TIM_IT_UPDATE);                            /* 使能更新中断 */
    HAL_TIM_IC_Start_IT(&g_timx_cap_chy_handle, GTIM_TIMX_CAP_CHY);                        /* 开始捕获TIM5的通道1 */
}

备注的映射到TI1上,这里的TI1是Timer Input 1的缩写,

也就是输入捕获通道1,是一个特定引脚。(在GPIO中配置复用)
定时器中断服务函数

HAL_tim.c中,定时器共用处理函数,根据配置情况执行了不同配置下的回调。
捕获比较中断函数

通用定时器脉冲计数

脉冲计数,需要将定时器配置为从模式,外部触发模式1。

cpp 复制代码
/* 从模式:外部触发模式 1 */
 tim_slave_config.SlaveMode = TIM_SLAVEMODE_EXTERNAL1;/* 外部触发模式 1 */
 tim_slave_config.InputTrigger = TIM_TS_TI1FP1;/* TI1FP1 作为触发输入源 */
 tim_slave_config.TriggerPolarity = TIM_TRIGGERPOLARITY_RISING;/* 上升沿 */
 tim_slave_config.TriggerPrescaler = TIM_TRIGGERPRESCALER_DIV1;/* 不分频 */
 tim_slave_config.TriggerFilter = 0x0; /* 滤波:本例中不需要任何滤波 */
HAL_TIM_SlaveConfigSynchronization(&g_timx_cnt_chy_hand

TIM_SLAVEMODE_EXTERNAL1 是定时器的最基本的从模式,允许定时器通过外部输入(如 TI1FP1)来触发更新事件。

TIM_TS_TI1FP1 表示定时器的触发输入源是来自定时器的输入捕获通道 1(TI1)的滤波输入(FP1)。这里的 FP 指的是滤波器(Filter),它用于减少噪声或干扰对触发信号的影响。数字 1 表示这是第一个滤波输入。

GPIO也要配置为输入捕获通道。

cpp 复制代码
    gpio_init_struct.Alternate = GTIM_TIMX_CNT_CHY_GPIO_AF;                /* 复用为捕获TIMx的通道 */
    HAL_GPIO_Init(GTIM_TIMX_CNT_CHY_GPIO_PORT, &gpio_init_struct);

TIM的复用引脚在数据手册上能查到,N是代表互补输出的

在实际应用中,TIM1_CH1N 与 TIM1_CH1 是一对互补通道。

如果死区时间(Deadtime)为0,则 TIM1_CH1N 是 TIM1_CH1 的反相信号;如果死区时间不为0,则会在 TIM1_CH1N 上插入死区时间,以防止上下功率管同时导通 。

死区时间内,两个互补信号不同时导通。

中断逻辑程序的逻辑代码是 放在更新中断回调函数里面的,这是 HAL 库回调机制标准的做法。

因为我们在通用定时器输 入捕获实验中使用过 HAL_TIM_PeriodElapsedCallback 更新中断回调函数,所以本实验我们不 使用 HAL 库这套回调机制,而是直接将中断处理写在定时器中断服务函数中。(标准做法是在定时器中断服务函数中调用更新中断回调函数,这里把那个函数删了)

高级定时器输出比较模式

高级定时器输出比较模式下翻转功能,通过定时器 4 个通道分别输 出 4 个 50%占空比、不同相位的 PWM。

输出比较模式下翻转功能作用是:当计数器的值等于捕获/比较寄存器影子寄存器的值时,OC1REF 发生翻转,进而控制通道输出(OCx)翻转。

通过翻转功能实现输出 PWM 的具体原理如下:

PWM 频率由自动重载寄存器(TIMx_ARR)的值决定,在这个过程中,只要自动重 载寄存器的值不变,那么 PWM 占空比就固定为 50%。

我们可以通过捕获/比较寄存器 (TIMx_CCRx)的值改变 PWM 的相位。

输出比较模式的初始化函数

高级定时器互补输出带死区控制

main函数

例程功能

1,利用 TIM1_CH1(PE9)输出 70%占空比的 PWM,它的互补输出通道(PE8)则是输出 30% 占空比的 PWM。

2,刹车功能,当给刹车输入引脚(PE15)输入低电平时,进行刹车,即 PE9 和 PE8 停止输 出 PWM。

使能时钟

时钟使能,后面仨代码其实重复了,但为了便于理解使用了DEFINE,其实就是开了E口的使能

复用和初始化GPIO

GPIO端口复用和初始化
复用了一个刹车,两个TIM通道

定时器的 PWM初始化,

这里 arr = 1000-1,psc = 180-1。

初始化定时器模式和周期

cpp 复制代码
g_timx_cplm_pwm_handle.Instance = ATIM_TIMX_CPLM;                               /* 定时器x */
g_timx_cplm_pwm_handle.Init.Prescaler = psc;                                    /* 预分频系数 */
g_timx_cplm_pwm_handle.Init.CounterMode = TIM_COUNTERMODE_UP;                   /* 递增计数模式 */
g_timx_cplm_pwm_handle.Init.Period = arr;                                       /* 自动重装载值 */
g_timx_cplm_pwm_handle.Init.ClockDivision = TIM_CLOCKDIVISION_DIV4;             /* CKD[1:0] = 10, tDTS = 4 * tCK_INT = Ft / 4 = 45Mhz */
g_timx_cplm_pwm_handle.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;  /* 使能影子寄存器TIMx_ARR */
HAL_TIM_PWM_Init(&g_timx_cplm_pwm_handle) ;

这里用的定时器1,挂载在APB2总线。

虽然主通道和互补通道的极性都设置为低电平有效,但它们的输出信号是互补的。

  • 当主通道(OCy)为低电平时,互补通道(OCyN)为高电平。

  • 当主通道(OCy)为高电平时,互补通道(OCyN)为低电平。

这种互补关系是通过定时器的硬件逻辑实现的,而不是通过软件直接设置的。即使它们的极性设置相同,硬件会自动确保它们的输出信号是互补的。

PWM通道配置

输出通道(OCx)和互补输出通道(OCxN)在主输出使能(MOE,Master Output Enable)为0时的空闲状态(Idle State),

TIM_OCIDLESTATE_SET:

当MOE=0时,主输出通道(OCx)的输出电平被设置为高电平。

主输出使能(Master Output Enable,MOE)通常位于定时器的基本死区和锁存器寄存器(TIMx_BDTR)中。通过设置或清除MOE位,可以启用或禁用定时器的所有输出信号,包括主输出通道(OCx)和互补输出通道(OCxN)。

设置定时器死区参数

设置定时器的死区时间

死区参数解释

TIM_OSSR_DISABLE (禁用运行模式下的关闭输出状态)

运行模式下的关闭输出状态(Off-State in Run Mode)。

那么刹车信号被触发时,定时器的输出不会被强制关闭。相反,输出信号的状态将由其他配置决定。
TIM_OSSI_DISABLE(禁用空闲模式下的关闭输出状态)

空闲模式下的关闭输出状态(Off-State in Idle Mode)

在定时器处于空闲模式(如定时器停止计数)时,输出不会被强制关闭。输出信号的状态由其他配置决定
寄存器锁就是锁住寄存器,禁止写入。
TIM_BREAK_ENABLE(使能刹车功能)
刹车输入信号的有效极性
自动输出功能。刹车事件结束后,定时器的输出将自动恢复到正常状态。

开启PWM和PWMN信号输出

开始输出PWM信号
开始输出PWM互补信号

设置定时器死区时间

带死区互补输出情况

死区时间内,两互补输出不同时导通。

由于开关器件的开关速度和寄生电容的影响,Q1的漏极和源极之间的电压不会立即下降到0,可能需要一定时间才能完全关闭。如果Q2在Q1完全关闭之前就导通,可能会导致Q1和Q2同时导通,从而产生直通电流,导致断路。

因此设置死区,确保一方完全关闭后,另一方才导通。

由上到下分别是 PE9 输出 70%占空比的 PWM 波和 PE8 互补输出 30%占空 比的 PWM 波。

互补输出的 PWM 波的正脉宽减去正常的 PWM 的负脉宽的值除以 2 就是死区 时间,也可以是正常的 PWM 的正脉宽减去互补输出的 PWM 波的负脉宽的值除以 2。

我们使用第一种方法得到:死区时间 =(702--698)/2 us= 2us。与我们理论计算得到的值 2.22us 差不多,这样的误差是正常的。

高级定时器PWM输入

例程:

首先通过 TIM3_CH4(PB1)输出 PWM 波。然后把 PB1 输出的 PWM 波用杜邦线接入 PC6 (定时器 8 通道 1),最后通过串口打印 PWM 波的脉宽和频率等信息。

cpp 复制代码
/*定时器初始化和通道配置*/
gtim_timx_pwm_chy_init(10 - 1, 90 - 1); /* 1Mhz的计数频率, 100Khz的PWM */
atim_timx_pwmin_chy_init();             /* 初始化PWM输入捕获 */
cpp 复制代码
void gtim_timx_pwm_chy_init(uint16_t arr, uint16_t psc)
{
    TIM_OC_InitTypeDef timx_oc_pwm_chy = {0};                       /* 定时器输出句柄 */
    
    g_timx_pwm_chy_handle.Instance = GTIM_TIMX_PWM;                 /* 定时器x */
    g_timx_pwm_chy_handle.Init.Prescaler = psc;                     /* 预分频系数 */
    g_timx_pwm_chy_handle.Init.CounterMode = TIM_COUNTERMODE_UP;    /* 递增计数模式 */
    g_timx_pwm_chy_handle.Init.Period = arr;                        /* 自动重装载值 */
    HAL_TIM_PWM_Init(&g_timx_pwm_chy_handle);                       /* 初始化PWM */

    timx_oc_pwm_chy.OCMode = TIM_OCMODE_PWM1;                       /* 选择PWM1模式 */
    timx_oc_pwm_chy.Pulse = arr / 2;                                /* 设置比较值,此值用来确定占空比 */

    timx_oc_pwm_chy.OCPolarity = TIM_OCPOLARITY_LOW;                                        /* 输出比较极性为低 */
    HAL_TIM_PWM_ConfigChannel(&g_timx_pwm_chy_handle, &timx_oc_pwm_chy, GTIM_TIMX_PWM_CHY); /* 配置TIMx通道y */
    HAL_TIM_PWM_Start(&g_timx_pwm_chy_handle, GTIM_TIMX_PWM_CHY);                           /* 开启对应PWM通道 */
}
cpp 复制代码
void atim_timx_pwmin_chy_init(void)
{
    GPIO_InitTypeDef gpio_init_struct = {0};
    TIM_SlaveConfigTypeDef slave_config = {0};
    TIM_IC_InitTypeDef tim_ic_pwmin_chy = {0};

    ATIM_TIMX_PWMIN_CHY_CLK_ENABLE();
    ATIM_TIMX_PWMIN_CHY_GPIO_CLK_ENABLE();

    gpio_init_struct.Pin = ATIM_TIMX_PWMIN_CHY_GPIO_PIN;
    gpio_init_struct.Mode = GPIO_MODE_AF_PP; 
    gpio_init_struct.Pull = GPIO_PULLDOWN;
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
    gpio_init_struct.Alternate = ATIM_TIMX_PWMIN_CHY_GPIO_AF;
    HAL_GPIO_Init(ATIM_TIMX_PWMIN_CHY_GPIO_PORT, &gpio_init_struct);

    g_timx_pwmin_chy_handle.Instance = ATIM_TIMX_PWMIN;             /* 定时器8 */
    g_timx_pwmin_chy_handle.Init.Prescaler = 0;                     /* 定时器预分频系数 */
    g_timx_pwmin_chy_handle.Init.CounterMode = TIM_COUNTERMODE_UP;  /* 递增计数模式 */
    g_timx_pwmin_chy_handle.Init.Period = 65535;                    /* 自动重装载值 */
    HAL_TIM_IC_Init(&g_timx_pwmin_chy_handle);
    
    /* 从模式配置,IT1触发更新 */
    slave_config.SlaveMode = TIM_SLAVEMODE_RESET;                   /* 从模式:复位模式 */
    slave_config.InputTrigger = TIM_TS_TI1FP1;                      /* 定时器输入触发源:TI1FP1 */
    slave_config.TriggerPolarity = TIM_INPUTCHANNELPOLARITY_RISING; /* 上升沿检测 */
    slave_config.TriggerFilter = 0;                                 /* 不滤波 */
    HAL_TIM_SlaveConfigSynchro(&g_timx_pwmin_chy_handle, &slave_config);

    /* IC1捕获:上升沿触发TI1FP1 */
    tim_ic_pwmin_chy.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING;  /* 上升沿检测 */
    //表示输入捕获的信号来源为一个直接信号
    tim_ic_pwmin_chy.ICSelection = TIM_ICSELECTION_DIRECTTI;        /* 选择输入端IC1映射到TI1 */
    tim_ic_pwmin_chy.ICPrescaler = TIM_ICPSC_DIV1;                  /* 不分频 */
    tim_ic_pwmin_chy.ICFilter = 0;                                  /* 不滤波 */
    HAL_TIM_IC_ConfigChannel(&g_timx_pwmin_chy_handle, &tim_ic_pwmin_chy, TIM_CHANNEL_1);
    
    /* IC2捕获:上升沿触发TI1FP2 */
    tim_ic_pwmin_chy.ICPolarity = TIM_INPUTCHANNELPOLARITY_FALLING; /* 下降沿检测 */
    tim_ic_pwmin_chy.ICSelection = TIM_ICSELECTION_INDIRECTTI;      /* 选择输入端IC2映射到TI1 */
    HAL_TIM_IC_ConfigChannel(&g_timx_pwmin_chy_handle, &tim_ic_pwmin_chy, TIM_CHANNEL_2);
    
    HAL_NVIC_SetPriority(ATIM_TIMX_PWMIN_IRQn, 1, 3);               /* 设置中断优先级,抢占优先级1,子优先级3 */
    HAL_NVIC_EnableIRQ( ATIM_TIMX_PWMIN_IRQn );                     /* 开启TIMx中断 */
    
    /* TIM1/TIM8有独立的输入捕获中断服务函数 */
    if ( ATIM_TIMX_PWMIN == TIM1 || ATIM_TIMX_PWMIN == TIM8)
    {
        HAL_NVIC_SetPriority(ATIM_TIMX_PWMIN_CC_IRQn, 1, 3);        /* 设置中断优先级,抢占优先级1,子优先级3 */
        HAL_NVIC_EnableIRQ(ATIM_TIMX_PWMIN_CC_IRQn);                /* 开启TIMx中断 */
    }

    __HAL_TIM_ENABLE_IT(&g_timx_pwmin_chy_handle, TIM_IT_UPDATE);
    HAL_TIM_IC_Start_IT(&g_timx_pwmin_chy_handle, TIM_CHANNEL_1);
    HAL_TIM_IC_Start_IT(&g_timx_pwmin_chy_handle, TIM_CHANNEL_2);
}

输入捕获通道映射

直接映射 TIM_ICSELECTION_DIRECTTI

  • 含义:表示输入捕获通道直接连接到对应的输入引脚。

  • 具体映射

    • 对于通道1(IC1),信号来源是通道1的输入引脚(TI1)。

    • 对于通道2(IC2),信号来源是通道2的输入引脚(TI2)。

    • 对于通道3(IC3),信号来源是通道3的输入引脚(TI3)。

    • 对于通道4(IC4),信号来源是通道4的输入引脚(TI4)。

间接映射 TIM_ICSELECTION_INDIRECTTI

  • 含义:表示输入捕获通道的信号来源于另一个通道的间接输入信号。

  • 具体映射

    • 对于通道2(IC2),信号来源是通道1的滤波输入信号(TI1FP1)。

    • 对于通道3(IC3),信号来源是通道2的滤波输入信号(TI2FP2)。

    • 对于通道4(IC4),信号来源是通道3的滤波输入信号(TI3FP3)。

这里通道配置的时候,把定时器8配置成了 从模式:复位模式,

并给出了输入触发源TI1FP1,意思就是通道1(TI1)的输入信号经过内部过滤(FP1)后的版本。

进行定时器的输入捕获配置的时候,给通道1配置了直接映射。

表示输入捕获通道直接连接到对应的输入引脚,因此通道1的信号来源就是通道1,即TI1。

通道2配置了间接映射,因此通道2的信号来源是TI1FP1。

TIM1/TIM8独立的捕获中断服务函数

cpp 复制代码
 /* TIM1/TIM8 有独立的捕获中断服务函数,需要单独定义,对于TIM2~5等,则不需要以下定义 */
#define ATIM_TIMX_PWMIN_CC_IRQn                 TIM8_CC_IRQn
#define ATIM_TIMX_PWMIN_CC_IRQHandler           TIM8_CC_IRQHandler

设置优先级,开启TIM中断,

cpp 复制代码
/*启用定时器的更新中断*/
__HAL_TIM_ENABLE_IT(&g_timx_pwmin_chy_handle, TIM_IT_UPDATE);

/*开启通道1的输入捕获模式*/
HAL_TIM_IC_Start_IT(&g_timx_pwmin_chy_handle, TIM_CHANNEL_1);

/*开启通道2的输入捕获模式*/
HAL_TIM_IC_Start_IT(&g_timx_pwmin_chy_handle, TIM_CHANNEL_2);
相关推荐
猫猫的小茶馆20 分钟前
【STM32】通用定时器基本原理
c语言·stm32·单片机·嵌入式硬件·mcu·51单片机
jingshaoqi_ccc1 小时前
stm32的USART使用DMA配置成循环模式时发送和接收有着本质区别
stm32·单片机·嵌入式硬件
MingYue_SSS4 小时前
开关电源抄板学习
经验分享·笔记·嵌入式硬件·学习
小宋同学在不断学习5 小时前
stm32-掌握SPI原理(一)
stm32·单片机·spi
is08155 小时前
STM32的 syscalls.c 和 sysmem.c
c语言·stm32·嵌入式硬件
学不动CV了6 小时前
数据结构---链表结构体、指针深入理解(三)
c语言·arm开发·数据结构·stm32·单片机·链表
szxinmai主板定制专家7 小时前
【精密测量】基于ARM+FPGA的多路光栅信号采集方案
服务器·arm开发·人工智能·嵌入式硬件·fpga开发
工业互联网专业10 小时前
汇编与接口技术:8259中断实验
汇编·单片机·嵌入式硬件·8259中断实验
brave and determined10 小时前
国产MCU学习Day6——CW32F030C8T6: I2C功能详解与应用案例
单片机·eeprom·i2c·cw32f030c8t6·cw32·cw32f030·中断读取eeprom