定时器的本质就是数数(基于STM32F103深入理解定时器)


参考手册

系统嘀嗒定时器SysTick Timer

该定时器和嵌套向量中断控制器NVIC一样,是Cortext-M3内核中内置的外设,在STM32F10xxx/20xxx/21xxx/L1xxxx Cortex®-M3 programming manual的如下章节中可以查阅相关的说明。

image-20241205082312825

The processor has a 24-bit system timer, SysTick, that counts down from the reload value to zero, reloads (wraps to) the value in the LOAD register on the next clock edge, then counts down on subsequent clocks.

When the processor is halted for debugging the counter does not decrement.

Cortex-M3处理器有一个24bit的系统级定时器SysTick,该定时器会从装载值向下计数到零,然后在下个时钟周期装载LOAD寄存器中指定的重新装载值,并在随后的时钟继续向下计数。

需要注意的时,当处理器因调试而中断执行时,该计数器不会递减。

重装载值寄存器LOAD

image-20241205083512747

LOAD寄存器用于指定定时器计数的重装载值RELOAD,当定时器被启用或递减到零时,LOAD寄存器指定的值会被加载到VAL寄存器中(该寄存器为定时器当前的计数值)。

RELOAD值需要根据使用场景来计算:

  • 如果想要每N个处理器时钟周期就产生触发事件,需要将RELOAD设置为N-1。例如每100个时钟脉冲需要触发一次SysTick中断,需要将RELOAD设置为99。(当RELOAD设置为99后,开启定时器,在第一个时钟脉冲RELOAD会被加载到VAL寄存器中,并在随后的每个时钟脉冲将99递减1,因此在开启定时器后的第100个时钟脉冲,VAL会变成0,并产生中断事件)。
  • 如果需要在N个处理器时钟周期的延时后触发一个单次的SysTick中断,需要将RELOAD设置为N。例如如果需要在400个时钟脉冲之后触发一次SysTick中断,需要将RELOAD设置为400。(开启定时器后,在第一个脉冲将RELOAD装载到VAL,在第401个脉冲VAL递减为0,并产生中断事件)

前者常用于周期性地产生中断(每N个时钟周期的最后一个周期触发次),而后者常用于单次不定长度的延时(等到K个时钟周期完整结束,第K+1个脉冲开始时触发一次事件)。

当前计数值寄存器VAL

image-20241205091342807

该寄存器存储的是SysTick计数器当前的计数值。读操作会返回当前计数值,任何写操作会将当前计数值清零,并清除CTRL寄存器中的COUNTFALG位。

该寄存器是SysTick内部的一个状态寄存器,我们通常不会直接操作该寄存器。

SysTick控制和状态寄存器CTRL

image-20241205083016981

定时器使能ENABLE

image-20241205083115544

将ENABLE置位后,定时器会将LOAD寄存器中RELOAD值加载到VAL寄存器中,然后开始向下计数。到达0后,会将COUNTFLAG置位,并根据TICKINT是否被置位来判断是否需要产生SysTick中断。然后继续重新装载RELOAD值并开始计数。

[!NOTE]

注意:如本章节开头所提到的,并不是将ENABLE置位后,定时器并不会马上加载RELOAD,而是要等一个时钟脉冲。同样的,当递减到0时,会置位COUNTFLAG并根据TICKINT产生中断,并在下个脉冲重新装载RELOAD值。

SysTick异常请求使能TICKINT

image-20241205094011349

将TICKINT置位后,当计数器递减到0时会发出SysTick异常请求,该请求会被递交NVIC,随后处理器会通过异常向量表找到相应的异常处理程序来执行。

[!NOTE]

软件可以通过轮询COUNTFLAG来判断SysTick是否计数到了0.

image-20241205094354149

[!NOTE]

关于ARM核异常处理机制,可参考本站文章《ARM核学习(五)异常处理》

SysTick时钟源选择CLKSOURCE

image-20241205094708579

参考手册-时钟树

这里我们可以配置为处理器时钟AHB,或其8分频。

计数标志位COUNTFLAG

image-20241205094958147

当定时器计数到0时,该标志位会被置位。这里还有个细节,读取到COUNTFLAG为1后会清除该位。

案例-SysTick实现LED每秒翻转一次

systick.h

#ifndef __SYSTICK_H__
#define __SYSTICK_H__

#include "stm32f10x.h"

void SysTick_Init(void);
void SysTick_Callback(void);

#endif /* __SYSTICK_H__ */

systick.c

#include "systick.h"
#include "led.h"

static uint32_t tick = 0;

void SysTick_Init(void) {
    SysTick->LOAD = SystemCoreClock / 1000 - 1;
    SysTick->CTRL |= (SysTick_CTRL_CLKSOURCE | SysTick_CTRL_TICKINT);
    SysTick->CTRL |= SysTick_CTRL_ENABLE;
}

void SysTick_Handler(void) {
    tick++;
    SysTick_Callback();
}

__weak void SysTick_Callback(void) {

}

main.c

#include "led.h"
#include "stm32f10x.h"
#include "systick.h"

uint16_t tick_ms = 0;

void SysTick_Callback(void) {
    tick_ms++;
    if (tick_ms >= 1000) {
        LED_Toggle(LED1);
        tick_ms = 0;
    }
}

int main() {
    LED_Init();
    SysTick_Init();
    while (1)
        ;
}

NVIC中断配置

由于SysTick为CM3内置的外设,NVIC默认注册了SysTick。

中断使能&默认优先级

image-20241205105610255

如上图,可以看出SysTick中断使能默认是置位的,并且优先级为0。

自定义SysTick优先级

void SysTick_Init(void) {
    SysTick->LOAD = SystemCoreClock / 1000 - 1;
    SysTick->CTRL |= (SysTick_CTRL_CLKSOURCE | SysTick_CTRL_TICKINT);

    NVIC_SetPriorityGrouping(3); // 4个bit都为抢占优先级
    NVIC_SetPriority(SysTick_IRQn, 10); // 将SysTick中断优先级设置为10
    // NVIC_EnableIRQ(SysTick_IRQn); // SysTick属于内核异常,不需要手动开启

    SysTick->CTRL |= SysTick_CTRL_ENABLE;
}

image-20241205110044145

HAL库实现

Cube配置

image-20241205110622283

image-20241205110642343

main.c

while (1)
{
  /* USER CODE END WHILE */
  HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
  HAL_Delay(1000);
  /* USER CODE BEGIN 3 */
}

HAL库延时函数分析

/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

HAL_Init做了SysTick的初始化:

/* Set Interrupt Group Priority */
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);

/* Use systick as time base source and configure 1ms tick (default clock after Reset is HSI) */
HAL_InitTick(TICK_INT_PRIORITY);

如上,将优先级分组设置为 #define NVIC_PRIORITYGROUP_4 0x00000003U,即优先级配置用的4个bit全部用于抢占优先级,不设置响应优先级(Subpriority)

#define NVIC_PRIORITYGROUP_4         0x00000003U /*!< 4 bits for pre-emption priority
                                                      0 bits for subpriority */

然后通过 HAL_InitTick初始化SysTick:

__weak HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority)
{
  /* Configure the SysTick to have interrupt in 1ms time basis*/
  if (HAL_SYSTICK_Config(SystemCoreClock / (1000U / uwTickFreq)) > 0U)
  {
    return HAL_ERROR;
  }

  /* Configure the SysTick IRQ priority */
  if (TickPriority < (1UL << __NVIC_PRIO_BITS))
  {
    HAL_NVIC_SetPriority(SysTick_IRQn, TickPriority, 0U);
    uwTickPrio = TickPriority;
  }
  else
  {
    return HAL_ERROR;
  }

  /* Return function status */
  return HAL_OK;
}

通过 HAL_SYSTICK_Config配置了SysTick相关的寄存器(每1ms触发一次中断),然后通过 HAL_NVIC_SetPriority设置了SysTick中断的优先级。

在中断回调函数中,将计数器 uwTick自增:

void SysTick_Handler(void)
{
  /* USER CODE BEGIN SysTick_IRQn 0 */

  /* USER CODE END SysTick_IRQn 0 */
  HAL_IncTick();
  /* USER CODE BEGIN SysTick_IRQn 1 */
    
  /* USER CODE END SysTick_IRQn 1 */
}

__weak void HAL_IncTick(void)
{
  uwTick += uwTickFreq;
}

在调用 HAL_Delay时,首先会获取 uwTick的当前值 tickstart,通过 while延时直到自增的uwTicktickstart之间的差值达到我们传入的 Delay毫秒数。

__weak void HAL_Delay(uint32_t Delay)
{
  uint32_t tickstart = HAL_GetTick();
  uint32_t wait = Delay;

  /* Add a freq to guarantee minimum wait */
  if (wait < HAL_MAX_DELAY)
  {
    wait += (uint32_t)(uwTickFreq);
  }

  while ((HAL_GetTick() - tickstart) < wait)
  {
  }
}

基本定时器

概述

image-20241204103009989

image-20241204103508116

image-20241204103554574

image-20241204103719970

image-20241204104016311

image-20241204104848858

image-20241204104746716

image-20241204105125520

image-20241204110522459

寄存器功能分析

image-20241205113657990

我们不妨从RCC时钟接入定时器开始分析,以每秒触发一次定时溢出事件为例,看看我们需要配置哪些寄存器。

image-20241205114035094

基本定时TIM6挂载在APB1低速外设总线上,其最大时钟频率为36MHz。

image-20241205114222915

但是APB1不是直接接入TM6的。如上图,如果APB1的预分配系数如果不是为1,那么会将其2倍频后再给到后面的TIM6。因此TIM6的输入时钟实际上为72MHz。

自动装载值寄存器ARR & 预分频寄存器PSC

为了得到周期为1s的定时器溢出率(即1Hz),我们可以将定时器的重装载值和预分频器看做输入时钟的两个分频系数,经过预分频器和定时器计数周期后,能够将72MHz输入时钟频率变为1Hz的定时器溢出率。

image-20241205133045398

image-20241205133101610

如上,由于定时器自动装载值和分频器为16bit,因此最大值为65535,为了将72MHz变为1Hz,我们可以通过预分频器(7200分频)先将72000000Hz先变为10000Hz,然后再通过10000个计数周期将其变为1Hz。

  • PSC配置为7200-1,PSC为0时代表不分频,因此PSC配置的值对应的实际分频系数为PSC+1
  • ARR配置为10000-1,因为计数到9999时,还需要一个脉冲才能计数器的值变为0,而随后计数到9999则需要9999个脉冲,加上变为0的,一共10000个脉冲

当前计数值寄存器CNT

image-20241205115451556

ARR中配置的值会被加载到该寄存器,该寄存器实时反映当前的计数值。通常,我们不会直接操作该寄存器。

中断使能寄存器DIER & 状态寄存器SR

image-20241205115848928

通过UIE我们可以开启定时器更新(计数值到达装载值)中断。

image-20241205115843854

在处理中断后,我们需要清除UIF中断标志位,避免重复触发中断。

手动产生更新事件EGR

image-20241205120551269

控制寄存器CR1

image-20241205120745275

案例-LED状态翻转

systick

#ifndef __SYSTICK_H__
#define __SYSTICK_H__

#include "stm32f10x.h"

void SysTick_Init(void);
void SysTick_Callback(void);

#endif /* __SYSTICK_H__ */
#include "systick.h"
#include "led.h"

static uint32_t tick = 0;

void SysTick_Init(void) {
    SysTick->LOAD = SystemCoreClock / 1000 - 1;
    SysTick->CTRL |= (SysTick_CTRL_CLKSOURCE | SysTick_CTRL_TICKINT);

    NVIC_SetPriorityGrouping(3); // 4个bit都为抢占优先级
    NVIC_SetPriority(SysTick_IRQn, 10); // 将SysTick中断优先级设置为10
    // NVIC_EnableIRQ(SysTick_IRQn); // SysTick属于内核异常,不需要手动开启

    SysTick->CTRL |= SysTick_CTRL_ENABLE;
}

void SysTick_Handler(void) {
    tick++;
    SysTick_Callback();
}

__weak void SysTick_Callback(void) {

}

TIM6

#ifndef __TIM6_H__
#define __TIM6_H__

#include "stm32f10x.h"

void TIM6_Init(void);

void TIM6_Start(void);

void TIM6_Stop(void);

void TIM6_UpdateCallback(void);

#endif /* __TIM6_H__ */
#include "tim6.h"

void TIM6_Init(void) {
    // 开启TIM6输入时钟
    RCC->APB1ENR |= RCC_APB1ENR_TIM6EN;
    // 配置中断频率
    // 72MHz / 7200 / 1000 = 1Hz = 1s(interrupt frequence)
    TIM6->PSC = 7200 - 1;
    TIM6->ARR = 10000 - 1;

    // 开启定时器更新中断
    TIM6->DIER |= TIM_DIER_UIE;
    // 注册到NVIC,优先级为2
    NVIC_SetPriorityGrouping(3);
    NVIC_SetPriority(TIM6_IRQn, 2);
    NVIC_EnableIRQ(TIM6_IRQn);
}

void TIM6_Start(void) {
    TIM6->CR1 |= TIM_CR1_CEN;
}

void TIM6_Stop(void) {
    TIM6->CR1 &= ~TIM_CR1_CEN;
}

void TIM6_IRQHandler(void) {
    TIM6->SR &= ~TIM_SR_UIF;
    TIM6_UpdateCallback();
}

__weak void TIM6_UpdateCallback(void) {

}

main

#include "led.h"
#include "stm32f10x.h"
#include "systick.h"
#include "tim6.h"

uint16_t tick_ms = 0;

void SysTick_Callback(void) {
    tick_ms++;
    if (tick_ms >= 1000) {
        LED_Toggle(LED1);
        tick_ms = 0;
    }
}

void TIM6_UpdateCallback(void) {
    LED_Toggle(LED2);
}

int main() {
    LED_Init();
    SysTick_Init();
    TIM6_Init();

    TIM6_Start();
    while (1)
        ;
}

PSC和ARR预装载值问题

image-20241205145640935

如上图所示,两个寄存器底下都有阴影,这表示ARR和PSC是预装载寄存器,ARR和PSC中配置的值会在每次更新事件发生时被加载底层工作的寄存器中(active register)。

image-20241205131723712

如果ARPE配置为1,那么写入ARR值不会立即被加载到Auto-reload Register,而是要等到更新事件发生。配置为0,则写入ARR的值会立即被加载到Auto-reload Register。

image-20241205132208017

[!NOTE]

但是,写入PSC的值只有在每次更新事件发生时才会被加载到运转中的PSC Prescaler(active prescaler register)中。

因此上述案例中,在TIM6启动后,Auto-reload Register中的值为9999,而PSC Prescale为0,等待9999*(1/72MHz)即大约(10000/72)us后,PSC Prescaler才会被加载为7199。

所以会导致LED1和LED2交替闪烁的现象,而不是我们预期的每隔1秒LED1和LED2同时翻转状态。

要想初始化TIM6时强制将PSC预装载值刷新到工作寄存器中,我们可以通过EGR手动产生一个更新事件:

void TIM6_Init(void) {
    // 开启TIM6输入时钟
    RCC->APB1ENR |= RCC_APB1ENR_TIM6EN;
    // 配置中断频率
    // 72MHz / 7200 / 1000 = 1Hz = 1s(interrupt frequence)
    TIM6->PSC = 7200 - 1;
    TIM6->ARR = 10000 - 1;

    // 通过EGR手动产生一次更新事件,将PSC预装载值刷新到Prescaler寄存器中
    TIM6->CR1 |= TIM_CR1_CEN; // 启用定时器
    TIM6->EGR |= TIM_EGR_UG; // 产生更新事件
    TIM6->SR &= ~TIM_SR_UIF; // 清除更新事件中断标志
    TIM6->CR1 &= ~TIM_CR1_CEN; // 关闭定时器

    // 开启定时器更新中断
    TIM6->DIER |= TIM_DIER_UIE;
    // 注册到NVIC,优先级为2
    NVIC_SetPriorityGrouping(3);
    NVIC_SetPriority(TIM6_IRQn, 2);
    NVIC_EnableIRQ(TIM6_IRQn);
}

HAL库实现

Cube配置

image-20241205144127438

image-20241205144135329

源码分析

TIM_HandleTypeDef htim6;

/* TIM6 init function */
void MX_TIM6_Init(void)
{
    TIM_MasterConfigTypeDef sMasterConfig = {0};
    htim6.Instance = TIM6;
    htim6.Init.Prescaler = 7200 - 1;
    htim6.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim6.Init.Period = 10000 - 1;
    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)
    {
        __HAL_RCC_TIM6_CLK_ENABLE();

        /* TIM6 interrupt Init */
        HAL_NVIC_SetPriority(TIM6_IRQn, 0, 0);
        HAL_NVIC_EnableIRQ(TIM6_IRQn);
    }
}

添加定时器6的中断回调函数

image-20241205144214052

tim.c中添加HAL库的定时器溢出中断回调函数。(定时器的回调函数比较多,这次我们只使用到了溢出中断回调函数)。

/* USER CODE BEGIN 1 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    // 定时器6产生中断。任何一个定时器产生中断都会进入到这个方法中,所以需要判断下定时器实例
    if (htim->Instance == TIM6)
    {
        // 翻转LEDB
        HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_1);
    }
}
/* USER CODE END 1 */

以中断方式开启定时器

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_TIM6_Init();
    // 启动定时器6
    HAL_TIM_Base_Start_IT(&htim6);
    while (1)
    {
    }
}

通用定时器

概述

image-20241205144420381

拥有基本定时器的所有功能

image-20241205145016647

三种可选的时钟源

内部时钟源

image-20241205145125349

定时器输入通道作为时钟源

[!NOTE]

TI1:Timer Input 1

TI1FP1:Timer Input 1 after input Filter and Polarity(edge) detector

image-20241204121957064

image-20241205153635462

应用场景

  1. 外部事件计数
    • 定时器可以通过 TIMx_CH1 捕获输入信号的每一个脉冲并进行计数。
    • 典型应用:
      • 计数外部传感器的信号,如编码器脉冲、转速信号。
      • 红外光电开关的触发计数。
  2. 频率测量
    • 使用 TIMx_CH1 捕获输入信号的频率或周期。
    • 实现方式:
      • 配置定时器为输入捕获模式,记录信号边沿之间的时间间隔。
    • 典型应用:
      • 测量输入信号频率(例如电机速度测量)。
  3. 占空比测量
    • TIMx_CH1 配合其他通道,可以测量信号的占空比。
    • 实现方式:
      • 配置为输入捕获模式,记录上升沿和下降沿的时间差。

外部触发输入端口ETR作为时钟源

[!NOTE]

ETR:External Trigger

ITR:Internal Timer

image-20241204141554812

image-20241205153719730

应用场景

  1. 外部时钟输入
    • 定时器使用 ETR 的信号作为主时钟,而非内部时钟(如 APB 时钟)。
    • 实现方式:
      • 配置定时器为 外部时钟模式 1,通过 ETR 引脚输入时钟信号。
    • 典型应用:
      • 使用外部晶振或其他高精度时钟源驱动定时器。
      • 测量高频输入信号的脉冲个数。
  2. 触发控制
    • ETR 信号可用作触发信号源,用于启动或复位定时器计数器。
    • 实现方式:
      • 配置定时器为 复位模式门控模式,根据 ETR 信号触发定时器操作。
    • 典型应用:
      • 用一个外部事件启动定时器计数(例如外部按钮触发计时)。
      • 用外部信号复位定时器(例如定时器同步)。
  3. 编码器接口与同步
    • 在编码器模式或定时器同步中,ETR 可用作从定时器的触发输入。
    • 实现方式:
      • 主从模式配置,将 ETR 信号作为触发源。
    • 典型应用:
      • 多定时器同步操作(如 PWM 和输入捕获协同工作)。

三种计数模式

向上计数

image-20241204142731898

向下计数

image-20241204142747437

中央对齐计数

image-20241204143158175

PWM介绍

概念

image-20241204144714300

PWM参数

image-20241204144910528

使用场景

image-20241204145019846

输出比较功能

image-20241204145214662

框图

image-20241204145422526

引脚复用

image-20241205154814340

输出比较原理

image-20241204145638909

输出比较的8种模式

image-20241204150232804

[!NOTE]

复位值为000,即默认没有启用该输出通道。

image-20241204150202816

image-20241205155618824

image-20241204150413815

image-20241204150500666

image-20241204150639079

image-20241204174825746

实验1-LED呼吸灯

计数模式配置

image-20241205160143270

捕获/比较模式选择

image-20241205160252799

比较输出模式设置

image-20241205160735003

比较输出模式下的比较值

image-20241205160836723

通道输出使能 & 工作电平极性

image-20241205161010527

[!NOTE]

CCER中的CCxP极性(负载工作时的电平极性):默认为高电平有效(active high,即高电平时驱动负载工作),即计数值小于比较值时输出高电平。如果是低电平驱动负载,那么极性可以配置为低电平有效。

寄存器一览

image-20241205164102625

示例代码

image-20241205170719406

tim5

#ifndef __TIM5_H__
#define __TIM5_H__

#include "stm32f10x.h"

void TIM5_Init(void);
void TIM5_Start(void);
void TIM5_Stop(void);
void TIM5_SetDutyCycle(uint8_t duty_cycle);

#endif /* __TIM5_H__ */
#include "tim5.h"

void TIM5_Init(void) {
    // ===========使能GPIOA和TIM5时钟
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
    RCC->APB1ENR |= RCC_APB1ENR_TIM5EN;

    // ===========GPIO复用推挽 mode=11 cnf=10
    GPIOA->CRL |= GPIO_CRL_MODE1;
    GPIOA->CRL |= GPIO_CRL_CNF1_1;
    GPIOA->CRL &= ~GPIO_CRL_CNF1_0;

    // ===========定时器基本配置
    // 定时器溢出频率 100Hz 10ms
    TIM5->PSC = 7200 - 1;
    TIM5->ARR = 100 - 1;
    // 计数方向为向上计数
    TIM5->CR1 &= ~TIM_CR1_DIR;

    // ===========定时器通道2配置
    // 默认占空比为0
    TIM5->CCR2 = 0;
    // 因为低电平点亮LED,因此输出工作电平极性为低电平
    TIM5->CCER |= TIM_CCER_CC2P; // active low
    // 通道输入输出选择:比较输出
    TIM5->CCMR1 &= ~TIM_CCMR1_CC2S;
    // 比较输出模式:PWM1 110
    TIM5->CCMR1 |= TIM_CCMR1_OC2M_2;
    TIM5->CCMR1 |= TIM_CCMR1_OC2M_1;
    TIM5->CCMR1 &= ~TIM_CCMR1_OC2M_0;
    // 使能通道比较输出
    TIM5->CCER |= TIM_CCER_CC2E;
}

void TIM5_Start(void) {
    TIM5->CR1 |= TIM_CR1_CEN;
}

void TIM5_Stop(void) {
    TIM5->CR1 &= ~TIM_CR1_CEN;
}

void TIM5_SetDutyCycle(uint8_t duty_cycle) {
    TIM5->CCR2 = duty_cycle;
}

main

#include "led.h"
#include "stm32f10x.h"
#include "systick.h"
#include "tim5.h"

int main() {
    SysTick_Init();
    TIM5_Init();

    TIM5_Start();
    uint8_t duty_cycle = 0;
    int8_t step = 1;

    while (1) {
        duty_cycle += step;
        TIM5_SetDutyCycle(duty_cycle);
        if(duty_cycle == 0 || duty_cycle == 100) {
            step = -step;
        }
        delay(20);
    }
}

delay

#ifndef __SYSTICK_H__
#define __SYSTICK_H__

#include "stm32f10x.h"

void SysTick_Init(void);
void SysTick_Callback(void);
void delay(uint16_t delay_ms);

#endif /* __SYSTICK_H__ */
#include "systick.h"
#include "led.h"

static volatile uint32_t tick = 0;

void SysTick_Init(void) {
    SysTick->LOAD = SystemCoreClock / 1000 - 1;
    SysTick->CTRL |= (SysTick_CTRL_CLKSOURCE | SysTick_CTRL_TICKINT);

    NVIC_SetPriorityGrouping(3);        // 4个bit都为抢占优先级
    NVIC_SetPriority(SysTick_IRQn, 10); // 将SysTick中断优先级设置为10
    // NVIC_EnableIRQ(SysTick_IRQn); // SysTick属于内核异常,不需要手动开启

    SysTick->CTRL |= SysTick_CTRL_ENABLE;
}

void SysTick_Handler(void) {
    tick++;
    SysTick_Callback();
}

__weak void SysTick_Callback(void) {}

void delay(uint16_t delay_ms) {
    uint32_t cur_tick = tick;
    while (tick - cur_tick < delay_ms) {
    }
}

HAL库实现

Cube配置

image-20241205181459371

设置占空比

tim.h

/* USER CODE BEGIN Prototypes */
void TIM5_SetDutyCycle(uint8_t dutyCycle);
/* USER CODE END Prototypes */

tim.c

/* USER CODE BEGIN 1 */
void TIM5_SetDutyCycle(uint8_t dutyCycle) {
    __HAL_TIM_SET_COMPARE(&htim5, TIM_CHANNEL_2, dutyCycle);
}
/* USER CODE END 1 */

启动定时器通道输出PWM

main.c

/* USER CODE BEGIN 2 */
    HAL_TIM_PWM_Start(&htim5, TIM_CHANNEL_2);
    uint8_t dutyCycle = 0;
    int8_t step = 1;
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    dutyCycle += step;
    TIM5_SetDutyCycle(dutyCycle);
    if (dutyCycle == 0 || dutyCycle == 100) {
        step = -step;
    }
    HAL_Delay(20);
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }

输入捕获

应用

image-20241205212053355

框图

image-20241204175246770

原理

image-20241204180530012

image-20241204180946906

测量PWM周期原理

image-20241205213337600

image-20241205213619896

image-20241205213846934

实验2-测量PWM周期/频率

  • PA1:通过TIM5_CH2的比较输出功能产生PWM
  • 短接PA1和PB6
  • PB6:通过TIM4_CH1的输入捕获功能测量输入的PWM周期

image-20241205214430424

输入通道滤波器配置

image-20241205214245028

通道比较/捕获使能 & 边沿检测极性

image-20241205215027795

输入捕获中断使能

image-20241205215143081

输入捕获通道映射

image-20241205215745688

对应下图中的多路选择器

image-20241205215850878

示例代码

image-20241206090405972

我们可以对照框图,根据信号从引脚输入到产生捕获事件,来串起其中涉及到的寄存器。

  • TI1S:Timer Input 1 Selection,选择引脚、还是XOR作为定时器输入1
  • IC1F:Input Capture 1 的前置滤波器
  • CC1P:Capture/Compare 1 的极性选择,这里为捕获上升沿(极性配置为高电平有效)还是下降沿
  • CC1S:Capture/Compare 1 Selction,捕获/比较模式选择
    • 比较输出模式
    • 输入捕获模式:选择TI1FP1(Timer Input 1 Filter Polarity 1)作为输入捕获IC1(Input Captrue)的来源
    • 输入捕获模式:选择TI2FP1(Timer Input 2 Filter Polarity 1)作为输入捕获IC1(Input Captrue)的来源
  • IC1PSC:Input Capture 1 Prescaler,输入捕获预分频,即多少次输入捕获事件后才触发一次CC1I中断事件

TIM5_CH2产生PWM

#ifndef __TIM5_H__
#define __TIM5_H__

#include "stm32f10x.h"

void TIM5_Init(void);
void TIM5_Start(void);
void TIM5_Stop(void);
void TIM5_CH1_SetDutyCycle(uint8_t duty_cycle);
void TIM5_CH2_SetDutyCycle(uint8_t duty_cycle);

#endif /* __TIM5_H__ */

#include "tim5.h"

void TIM5_Init(void) {
    // ===========使能GPIOA和TIM5时钟
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
    RCC->APB1ENR |= RCC_APB1ENR_TIM5EN;

    // ===========GPIO复用推挽 mode=11 cnf=10
    GPIOA->CRL |= GPIO_CRL_MODE1;
    GPIOA->CRL |= GPIO_CRL_CNF1_1;
    GPIOA->CRL &= ~GPIO_CRL_CNF1_0;

    GPIOA->CRL |= GPIO_CRL_MODE0;
    GPIOA->CRL |= GPIO_CRL_CNF0_1;
    GPIOA->CRL &= ~GPIO_CRL_CNF0_0;

    // ===========定时器基本配置
    // 定时器溢出频率 100Hz 10ms
    TIM5->PSC = 7200 - 1;
    TIM5->ARR = 100 - 1;
    // 计数方向为向上计数
    TIM5->CR1 &= ~TIM_CR1_DIR;

    // ===========定时器通道2配置
    // 默认占空比为50
    TIM5->CCR2 = 50;
    // 因为低电平点亮LED,因此输出有效电平极性为低电平
    TIM5->CCER |= TIM_CCER_CC2P; // active low
    // 通道输入输出选择:比较输出
    TIM5->CCMR1 &= ~TIM_CCMR1_CC2S;
    // 比较输出模式:PWM1 110
    TIM5->CCMR1 |= TIM_CCMR1_OC2M_2;
    TIM5->CCMR1 |= TIM_CCMR1_OC2M_1;
    TIM5->CCMR1 &= ~TIM_CCMR1_OC2M_0;
    // 使能通道比较输出
    TIM5->CCER |= TIM_CCER_CC2E;

        // ===========定时器通道1配置
    // 默认占空比为0
    TIM5->CCR1 = 0;
    // 因为低电平点亮LED,因此输出有效电平极性为低电平
    TIM5->CCER |= TIM_CCER_CC1P; // active low
    // 通道输入输出选择:比较输出
    TIM5->CCMR1 &= ~TIM_CCMR1_CC1S;
    // 比较输出模式:PWM1 110
    TIM5->CCMR1 |= TIM_CCMR1_OC1M_2;
    TIM5->CCMR1 |= TIM_CCMR1_OC1M_1;
    TIM5->CCMR1 &= ~TIM_CCMR1_OC1M_0;
    // 使能通道比较输出
    TIM5->CCER |= TIM_CCER_CC1E;
}

void TIM5_Start(void) {
    TIM5->CR1 |= TIM_CR1_CEN;
}

void TIM5_Stop(void) {
    TIM5->CR1 &= ~TIM_CR1_CEN;
}

void TIM5_CH2_SetDutyCycle(uint8_t duty_cycle) {
    TIM5->CCR2 = duty_cycle;
}

void TIM5_CH1_SetDutyCycle(uint8_t duty_cycle) {
    TIM5->CCR1 = duty_cycle;
}

TIM4_CH1测量PWM

#ifndef __TIM4_H__
#define __TIM4_H__

#include "stm32f10x.h"

void TIM4_Init(void);
void TIM4_Start(void);
void TIM4_Stop(void);
double TIM4_CH1_GetPWMCycle(void);
double TIM4_CH1_GetPWMFreq(void);

#endif /* __TIM4_H__ */

main.c

#include "tim4.h"
#include "logger.h"

void TIM4_Init(void) {
    // ===========使能GPIOA和TIM4时钟
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
    RCC->APB1ENR |= RCC_APB1ENR_TIM4EN;

    // ===========GPIO浮空输入 mode=00 cnf=01 PB6
    GPIOB->CRL &= ~GPIO_CRL_MODE6;
    GPIOB->CRL &= ~GPIO_CRL_CNF6_1;
    GPIOB->CRL |= GPIO_CRL_CNF6_0;

    // ===========定时器基本配置
    // 定时器输入频率 1MHz
    TIM4->PSC = 72 - 1;
    TIM4->ARR = 65535; // 尽量不要溢出
    // 计数方向为向上计数
    TIM4->CR1 &= ~TIM_CR1_DIR;

    // ===========定时器通道1配置
    TIM4->CR2 &= ~TIM_CR2_TI1S; // 选择CH1引脚作为TI1的来源
    TIM4->CCMR1 &= ~TIM_CCMR1_IC1F; // 不需要输入滤波,被测试的PWM信号没有噪声
    TIM4->CCER |= TIM_CCER_CC1P; // 捕获上升沿
    // 选择TI1的滤波/边沿检测输出1(TI1FP1)作为捕获来源(IC1): 01
    TIM4->CCMR1 &= ~TIM_CCMR1_CC1S_1;
    TIM4->CCMR1 |= TIM_CCMR1_CC1S_0;
    TIM4->CCMR1 &= ~TIM_CCMR1_IC1PSC; // 不需要对捕获事件分频
    TIM4->CCER |= TIM_CCER_CC1E; // 使能捕获事件发生时将计数值记录到CCR寄存器中

    //============中断配置
    TIM4->DIER |= TIM_DIER_CC1IE; // 使能捕获事件中断
    NVIC_SetPriorityGrouping(3);
    NVIC_SetPriority(TIM4_IRQn, 3);
    NVIC_EnableIRQ(TIM4_IRQn);
}

void TIM4_Start(void) { TIM4->CR1 |= TIM_CR1_CEN; }

void TIM4_Stop(void) { TIM4->CR1 &= ~TIM_CR1_CEN; }

void TIM4_IRQHandler(void) {
    if (TIM4->SR & TIM_SR_CC1IF) {
        TIM4->SR &= ~TIM_SR_CC1IF;
        // 计数值清零,开始对下个PWM周期计数
        TIM4->CNT &= ~TIM_CNT_CNT;
    }
}

double TIM4_CH1_GetPWMCycle(void) {
    // 通过CCR中捕获的计数值来计算PWM周期
    // 单次计数 1/1MHz = 1us,再除以1000 返回毫秒数
    return TIM4->CCR1 / 1000.0;
}

double TIM4_CH1_GetPWMFreq(void) {
    if (TIM4->CCR1) {
        // 1 / (CCR1 * 1000000) => Hz,将us转为s,再取倒数
        return 1000000.0 / TIM4->CCR1;
    }
    return 0;
}
#include "led.h"
#include "stm32f10x.h"
#include "systick.h"
#include "tim5.h"
#include "tim4.h"
#include "exti.h"
#include "logger.h"
#include "uart.h"

int main() {
    SysTick_Init();
    uart_init();
    TIM5_Init();
    TIM4_Init();
    
    TIM5_Start();
    TIM4_Start();

    LOG_DEBUG("main start")

    while (1) {
        delay(1000);
        double cycle = TIM4_CH1_GetPWMCycle();
        double freq = TIM4_CH1_GetPWMFreq();
        LOG_DEBUG("cycle = %.2f ms, freq = %.2f Hz", cycle, freq)
    }
}

思考事件和中断流程

image-20241206094121276

image-20241206094155768

在上述代码中,当捕获到上升沿时,会发生U更新事件,将计数器(CNT counter)的值捕获到CC1R中(相当于拍了个快照),该操作是由硬件电路自动完成的(硬件电路捕获到上升沿的时刻立即自动完成)。而如果我们通过CC1E使能了捕获/比较事件中断,那么该事件发生后(CC1R已保存了捕获的值),就会产生一个CC1I中断,递交NVIC,随后在合适的时机执行向量表中对应的中断处理函数(通过 B指令,关于ARM核异常处理机制,可参考本站文章《ARM核学习(五)异常处理》)。

所以在上述代码中,我们可以在中断处理函数中通过CC1R读取到捕获事件发生时计数器的快照值。

HAL库实现

Debug & RCC

image-20241206101625690

串口打印

image-20241206101717828

TIM5_CH2输出PWM

image-20241206102508269

TIM4_CH1输入捕获 & AFIO重映射

image-20241206103208047

TIM4中断使能

image-20241206105647297

printf重定向

image-20241206103342197

usart.c

/* USER CODE BEGIN 0 */
#include "stdio.h"
/* USER CODE END 0 */

/* USER CODE BEGIN 1 */
int fputc(int ch, FILE *stream) {
    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 10);
    return ch;
}
/* USER CODE END 1 */

示例代码

main.c

/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_TIM4_Init();
MX_TIM5_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
  // TIM5 CH2 输出PWM
  HAL_TIM_PWM_Start(&htim5, TIM_CHANNEL_2);
  // TIM4 CH1 输入捕获
  HAL_TIM_IC_Start_IT(&htim4, TIM_CHANNEL_1);
/* USER CODE END 2 */

/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
  HAL_Delay(1000);
  printf("cycle = %.2f ms, freq = %.2f Hz\n", TIM4_CH1_GetPWMCycle(), TIM4_CH1_GetPWMFreq());
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */

tim.c

/* USER CODE BEGIN 1 */
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) {
    if (htim == &htim4) {
        __HAL_TIM_SET_COUNTER(&htim4, 0);
    }
}
double TIM4_CH1_GetPWMCycle(void) {
    // 通过CCR中捕获的计数值来计算PWM周期
    // 单次计数 1/1MHz = 1us,再除以1000 返回毫秒数
    return __HAL_TIM_GET_COMPARE(&htim4, TIM_CHANNEL_1) / 1000.0;
}

double TIM4_CH1_GetPWMFreq(void) {
    uint32_t CCR = __HAL_TIM_GET_COMPARE(&htim4, TIM_CHANNEL_1);
    if (CCR) {
        // 1 / (CCR1 * 1000000) => Hz,将us转为s,再取倒数
        return 1000000.0 / CCR;
    }
    return 0;
}
/* USER CODE END 1 */

实验3-同时测量PWM周期/频率/占空比

定时器触发信号

image-20241206110818625

触发输入信号第一类——来源于其他定时器的TRGO

image-20241206111247477

触发输入信号第二类——外部触发引脚ETR

image-20241206111541318

[!NOTE]

TIMx_CHx引脚比ETR更具有针对性,主要用于输入捕获

触发输入信号第三类——TI1F_ED

image-20241206112051092

触发输入信号第四类——TI1FP1/TI2FP2

image-20241206112524528

定时器从模式

image-20241206113102739

image-20241206113306671

PWM输入模式

[!NOTE]

参考手册:15.3.6 PWM input mode

image-20241206113849114

image-20241206114143525

image-20241206132233091

示例代码

image-20241206130223507

在实验2的基础上,我们对照框图看看需要调整哪些配置:

  • 从模式控制器
    • 之前我们通过上升沿捕获事件中断来将计数器的值清零
    • 现在我们可以通过将TI1FP1作为从模式控制器的触发输入信号(TRGI),并配置从模式为复位模式,这样TI1FP1信号中的上升沿会触发从模式控制器自动将计数值清零(硬件自动完成
  • 增加输入捕获通道2,但是输入捕获来源IC2不是CH2引脚,而是TI1经过滤波和边沿检测后的TI1FP2信号,我们可以将通道2的极性(CCER-CC2P)配置为下降沿,这样IC2的下降沿就会产生捕获事件,将PWM高电平对应的计数值记录到CC2R中

tim4

#ifndef __TIM4_H__
#define __TIM4_H__

#include "stm32f10x.h"

void TIM4_Init(void);
void TIM4_Start(void);
void TIM4_Stop(void);
double TIM4_CH1_GetPWMCycle(void);
double TIM4_CH1_GetPWMFreq(void);
double TIM4_CH1_GetPWMDutyCycle(void);

#endif /* __TIM4_H__ */
#include "tim4.h"
#include "logger.h"

void TIM4_Init(void) {
    // ===========使能GPIOA和TIM4时钟
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
    RCC->APB1ENR |= RCC_APB1ENR_TIM4EN;

    // ===========GPIO浮空输入 mode=00 cnf=01 PB6
    GPIOB->CRL &= ~GPIO_CRL_MODE6;
    GPIOB->CRL &= ~GPIO_CRL_CNF6_1;
    GPIOB->CRL |= GPIO_CRL_CNF6_0;

    // ===========定时器基本配置
    // 定时器输入频率 1MHz
    TIM4->PSC = 72 - 1;
    TIM4->ARR = 65535; // 尽量不要溢出
    // 计数方向为向上计数
    TIM4->CR1 &= ~TIM_CR1_DIR;

    // ===========定时器通道1配置
    TIM4->CR2 &= ~TIM_CR2_TI1S; // 选择CH1引脚作为TI1的来源
    TIM4->CCMR1 &= ~TIM_CCMR1_IC1F; // 不需要输入滤波,被测试的PWM信号没有噪声
    TIM4->CCER |= TIM_CCER_CC1P; // 捕获上升沿
    // 选择TI1的滤波/边沿检测输出1(TI1FP1)作为捕获来源(IC1): 01
    TIM4->CCMR1 &= ~TIM_CCMR1_CC1S_1;
    TIM4->CCMR1 |= TIM_CCMR1_CC1S_0;
    TIM4->CCMR1 &= ~TIM_CCMR1_IC1PSC; // 不需要对捕获事件分频
    TIM4->CCER |=
        TIM_CCER_CC1E; // 使能输入捕获(事件发生时将计数值记录到CCR寄存器中)

    // ===========定时器通道2配置
    TIM4->CCER |= TIM_CCER_CC1P; // 捕获下降沿
    // 选择TI1FP2作为IC2 => 10
    TIM4->CCMR1 |= TIM_CCMR1_CC2S_1;
    TIM4->CCMR1 &= ~TIM_CCMR1_CC2S_0;
    TIM4->CCMR1 &= ~TIM_CCMR1_IC2PSC; // 不需要对捕获事件分频
    TIM4->CCER |=
        TIM_CCER_CC2E; // 使能输入捕获(事件发生时将计数值记录到CCR寄存器中)

    //============不需要中断来将计数值清零了,通过从模式+上升沿触发来自动完成
    // TIM4->DIER |= TIM_DIER_CC1IE; // 使能捕获事件中断
    // NVIC_SetPriorityGrouping(3);
    // NVIC_SetPriority(TIM4_IRQn, 3);
    // NVIC_EnableIRQ(TIM4_IRQn);

    // =============从模式控制器配置
    // 从模式选择:复位模式 => 100
    TIM4->SMCR |= TIM_SMCR_SMS_2;
    TIM4->SMCR &= ~TIM_SMCR_SMS_1;
    TIM4->SMCR &= ~TIM_SMCR_SMS_0;
    // 从模式触发输入多路选择:TI1FP1 => 101
    TIM4->SMCR |= TIM_SMCR_TS_2;
    TIM4->SMCR &= ~TIM_SMCR_TS_1;
    TIM4->SMCR |= TIM_SMCR_TS_0;
}

void TIM4_Start(void) { TIM4->CR1 |= TIM_CR1_CEN; }

void TIM4_Stop(void) { TIM4->CR1 &= ~TIM_CR1_CEN; }

void TIM4_IRQHandler(void) {
    if (TIM4->SR & TIM_SR_CC1IF) {
        TIM4->SR &= ~TIM_SR_CC1IF;
        // 计数值清零,开始对下个PWM周期计数
        TIM4->CNT &= ~TIM_CNT_CNT;
    }
}

double TIM4_CH1_GetPWMCycle(void) {
    // 通过CCR中捕获的计数值来计算PWM周期
    // 单次计数 1/1MHz = 1us,再除以1000 返回毫秒数
    return TIM4->CCR1 / 1000.0;
}

double TIM4_CH1_GetPWMFreq(void) {
    if (TIM4->CCR1) {
        // 1 / (CCR1 * 1000000) => Hz,将us转为s,再取倒数
        return 1000000.0 / TIM4->CCR1;
    }
    return 0;
}

double TIM4_CH1_GetPWMDutyCycle(void) {
    if (TIM4->CCR1) {
        return TIM4->CCR2 * 1.0 / TIM4->CCR1;
    }
    return 0;
}

main

#include "led.h"
#include "stm32f10x.h"
#include "systick.h"
#include "tim5.h"
#include "tim4.h"
#include "exti.h"
#include "logger.h"
#include "uart.h"

int main() {
    SysTick_Init();
    uart_init();
    TIM5_Init();
    TIM4_Init();
    
    TIM5_Start();
    TIM4_Start();

    LOG_DEBUG("main start")

    while (1) {
        double cycle = TIM4_CH1_GetPWMCycle();
        double freq = TIM4_CH1_GetPWMFreq();
        double duty = TIM4_CH1_GetPWMDutyCycle();
        LOG_DEBUG("cycle = %.2f ms, freq = %.2f Hz, duty = %.2f %%", cycle, freq, duty);
        delay(1000);
    }
}

HAL库实现

image-20241206141354596

高级定时器

image-20241206142745316

重复计数器

image-20241206142916268

image-20241206144030225

互补输出

image-20241206144550525

死区时间

死区时间指的是在两个开关器件(如 MOSFET 或 IGBT)切换过程中,故意引入的延时,用来确保一个开关完全关闭后,另一个开关才会导通。(MOS管具有关断慢,开通快的特性

例如:在 H 桥或半桥电路中,两个开关器件(上桥臂和下桥臂)不能同时导通,否则会造成直通故障,即电源正极直接短路到负极。

image-20241206145048925

刹车输入信号

image-20241206145502383

实验-输出有限个周期的PWM波

需求

image-20241206145801442

硬件

image-20241206145943045

寄存器分析

重复计数器REP

image-20241206145721960

主输出使能

image-20241206150128592

示例代码

tim1

#include "tim1.h"
#include "logger.h"

void TIM1_Init(void) {
    // ===========使能GPIOA和TIM1时钟
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
    RCC->APB2ENR |= RCC_APB2ENR_TIM1EN;

    // ===========GPIO复用推挽 PA8 mode=11 cnf=10
    GPIOA->CRH |= GPIO_CRH_MODE8;
    GPIOA->CRH |= GPIO_CRH_CNF8_1;
    GPIOA->CRH &= ~GPIO_CRH_CNF8_0;

    // ===========定时器配置
    // 定时器溢出频率 71MHz / 7200 / 5000 = 2Hz 0.5s
    TIM1->PSC = 7199;
    TIM1->ARR = 4999;
    // 计数方向为向上计数
    TIM1->CR1 &= ~TIM_CR1_DIR;
    // 重复计数器
    TIM1->RCR = 4; // 需要生成5个PWM周期

    // ===========定时器通道1输出配置
    TIM1->CCMR1 &= ~TIM_CCMR1_CC1S; // 配置通道为比较输出模式
    // 配置输出模式为PWM1 => 110
    TIM1->CCMR1 |= TIM_CCMR1_OC1M_2;
    TIM1->CCMR1 |= TIM_CCMR1_OC1M_1;
    TIM1->CCMR1 &= ~TIM_CCMR1_OC1M_0;
    TIM1->CCR1 = 2500; // 配置比较值
    // 配置输出使能,工作电平极性: 低电平点亮LED
    TIM1->CCER |= TIM_CCER_CC1P;
    TIM1->CR2 |= TIM_CR2_OIS1; // 关闭主输出使能MOE后,输出通道的空闲状态:OC1=1

    // 将所有预装载的寄存器加载到对应的工作寄存器中,并清除由此导致的中断标志
    TIM1->EGR |= TIM_EGR_UG;
    TIM1->SR &= ~TIM_SR_UIF;
    // 或者设置URS使UG不产生更新中断
    // TIM1->CR1 |= TIM_CR1_URS;
    // TIM1->EGR |= TIM_EGR_UG;

    TIM1->CCER |= TIM_CCER_CC1E;

    // ===========高级定时器相关配置
    // 重复计数器更新中断
    TIM1->DIER |= TIM_DIER_UIE;
    NVIC_SetPriorityGrouping(3);
    NVIC_SetPriority(TIM1_UP_IRQn, 3);
    NVIC_EnableIRQ(TIM1_UP_IRQn);
}

void TIM1_Start(void) {
    // 主输出使能,通道输出使能
    TIM1->BDTR |= TIM_BDTR_MOE;
    TIM1->CR1 |= TIM_CR1_CEN;
}

void TIM1_Stop(void) {
    TIM1->BDTR &= ~TIM_BDTR_MOE;
    TIM1->CR1 &= ~TIM_CR1_CEN;
}

void TIM1_UP_IRQHandler(void) {
    TIM1->SR &= ~TIM_SR_UIF;
    LOG_DEBUG("TIM1_UP_IRQHandler")
    TIM1_Stop();
}

main

int main() {
    SysTick_Init();
    uart_init();
    TIM1_Init();
    LOG_DEBUG("main start")

    TIM1_Start();
    while (1) {
        
    }
}

遗留问题

如下两个不同的配置顺序会导致不同的结果

// 配置输出模式为PWM1 => 110
TIM1->CCMR1 |= TIM_CCMR1_OC1M_2;
TIM1->CCMR1 |= TIM_CCMR1_OC1M_1;
TIM1->CCMR1 &= ~TIM_CCMR1_OC1M_0;
TIM1->CCR1 = 2500; // 配置比较值

image-20241206172619192

TIM1->CCR1 = 2500; // 配置比较值
// 配置输出模式为PWM1 => 110
TIM1->CCMR1 |= TIM_CCMR1_OC1M_2;
TIM1->CCMR1 |= TIM_CCMR1_OC1M_1;
TIM1->CCMR1 &= ~TIM_CCMR1_OC1M_0;

image-20241206172708938

使用UG强制刷新预装载值

// 将所有预加载的寄存器加载到对应的工作寄存器中,并清除由此导致的中断标志
TIM1->EGR |= TIM_EGR_UG;
TIM1->SR &= ~TIM_SR_UIF;
// 或者设置URS使UG不产生更新中断
// TIM1->CR1 |= TIM_CR1_URS;
// TIM1->EGR |= TIM_EGR_UG;

image-20241206184702361

HAL库实现

定时器和通道配置

image-20241206184508910

通过更新事件中断来停止定时器

image-20241206184152397

TIM1_CH1重映射为PA8

image-20241206184220481

代码实现

main.c
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_TIM1_Init();
/* USER CODE BEGIN 2 */
  // 清除定时器初始化时调用UG(Update Generation)强制刷新预装载值产生的更新中断
  __HAL_TIM_CLEAR_IT(&htim1, TIM_IT_UPDATE);
  __HAL_TIM_ENABLE_IT(&htim1, TIM_IT_UPDATE);
  HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
/* USER CODE END 2 */

/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
tim.c
/* USER CODE BEGIN 1 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
    if (htim == &htim1) {
        HAL_TIM_PWM_Stop(&htim1, TIM_CHANNEL_1);
    }
}
/* USER CODE END 1 */

THE END


文章作者: 安文
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 安文 !
  目录