STM32定时器(寄存器与HAL库实现)

一、系统定时器

1.系统定时器简介

系统定时器(SysTick系统嘀嗒定时器)是属于CM3内核,内嵌在NVIC中。

系统定时器是一个24bit的向下递减的计数器,计数器每计数一次的时间为1 / SYSCLK,一般我们设置系统时钟SYSCLK(与AHB相同)等于72M。当重装载数值寄存器的值递减到0的时候,系统定时器就产生一次中断,以此循环往复。

Systick定时器的主要功能包括实现简单的延时、生成定时中断以及进行精确定时和周期定时操作。此外,Systick定时器还可以被用于其他目的,例如作为操作系统的时基(如FreeRTOS),或者用于软件看门狗等系统调度操作。在STM32中,Systick通常以HCLK(AHB时钟)或HCLK/8作为运行时钟。

2.系统定时器相关寄存器

LOAD寄存器主要用于存储当计数器递减到 0 时,要重新装载到 VAL 寄存器的值。

VAL寄存器是一个递减计数器,它会不断从 LOAD 寄存器获取初始值,然后进行递减操作。

SysTick 校准数值寄存器。很少用到。

3.系统定时器实验:LED闪烁

利用系统定时器的中断,每隔1s 让LED1灯闪烁一次

3.1 寄存器实现

c 复制代码
#include "systick.h"

void systick_init(void)
{
    // 1. 配置时钟源 1=AHB(72MHZ) 0=AHB/8
    SysTick->CTRL |= SysTick_CTRL_CLKSOURCE;
    // 2. 使能中断
    SysTick->CTRL |= SysTick_CTRL_TICKINT;
    // 3. 每1ms产生一次中断
    // 减到0后再经过一个时钟周期才处理中断
    SysTick->LOAD = 72000 - 1;
    // 4. 清除计数值
    SysTick->VAL = 0;
    // 5. 使能定时器
    SysTick->CTRL |= SysTick_CTRL_ENABLE;
}

uint16_t count = 0;
void SysTick_Handler(void)
{
    count++;
    // 1s
    if (count == 1000)
    {
        count = 0;
        // led翻转电平
        LED_Toggle();
    }
}

SysTick 是内核中断,优先级通过 SCB->SHP[11] 配置,默认优先级较低。若需高优先级,需显式设置:

c 复制代码
NVIC_SetPriority(SysTick_IRQn, 0);  // 设置为最高优先级

上面代码未显式设置优先级,但 SysTick 的默认优先级已经存在(通常为最低)。如果应用中没有其他中断或对优先级无特殊要求,默认配置即可工作。

3.2 hal库实现

在hal库初始化的时候,会初始化SysTick定时器

以下为自动生成的部分代码

c 复制代码
__weak HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority)
{
  /* Configure the SysTick to have interrupt in 1ms time basis*/
  // 配置滴答定时器:每1ms产生一次中断
  // SystemCoreClock = 72MHz
  // uwTickFreq = 1
  // 参数=72000 重装载寄存器的值 值减到0产生一次中断
  // 计数一次是(1/72000 0000)s = (1/72)us
  // 那么计数72000次就是1ms
  if (HAL_SYSTICK_Config(SystemCoreClock / (1000U / uwTickFreq)) > 0U)
  {
    return HAL_ERROR;
  }

  /* Configure the SysTick IRQ priority */
  if (TickPriority < (1UL << __NVIC_PRIO_BITS))
  {
    // 设置SysTick优先级:抢占优先级15(最低) 和响应优先级0
    HAL_NVIC_SetPriority(SysTick_IRQn, TickPriority, 0U);
    uwTickPrio = TickPriority;
  }
  else
  {
    return HAL_ERROR;
  }

  /* Return function status */
  return HAL_OK;
}

一般建议把SysTick定时器的抢占优先级设置为最高(数字越小,优先级越高)。否则在其他中断中使用延时函数的时候会阻塞卡死。

c 复制代码
uint32_t HAL_SYSTICK_Config(uint32_t TicksNumb)
{
   return SysTick_Config(TicksNumb);
}

__STATIC_INLINE uint32_t SysTick_Config(uint32_t ticks)
{
  if ((ticks - 1UL) > SysTick_LOAD_RELOAD_Msk)
  {
    return (1UL);                                                   /* Reload value impossible */
  }
  // 重装载寄存器的值
  SysTick->LOAD  = (uint32_t)(ticks - 1UL);                         /* set reload register */
  // 中断优先级
  NVIC_SetPriority (SysTick_IRQn, (1UL << __NVIC_PRIO_BITS) - 1UL); /* set Priority for Systick Interrupt */
  // 当前数值寄存器的值
  SysTick->VAL   = 0UL;                                             /* Load the SysTick Counter Value */
  // 设置时钟源
  // 计数至0,产生异常
  // 使能SysTick计数器
  SysTick->CTRL  = SysTick_CTRL_CLKSOURCE_Msk |
                   SysTick_CTRL_TICKINT_Msk   |
                   SysTick_CTRL_ENABLE_Msk;                         /* Enable SysTick IRQ and SysTick Timer */
  return (0UL);                                                     /* Function successful */
}

在stm32f1xx_it.c中定义了中断服务函数

复制代码
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 */
}
c 复制代码
// 递增全局变量uwTick
// 每1ms递增一次
__weak void HAL_IncTick(void)
{
  uwTick += uwTickFreq;
}

我们可以自己重新实现HAL_IncTick函数

在main.c中添加自己的实现

c 复制代码
void HAL_IncTick(void)
{
  uwTick += uwTickFreq;
  if(uwTick % 100 == 0)
  {
    // 产生1s计时
    // 翻转LED灯
    HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_1);
  }
}

注意这里是对uwTick取模,因为不能随便修改uwTick的值。

因为如果需要使用HAL_Delay延时的话,HAL_Delay函数中调用了HAL_GetTick,而HAL_GetTick就是要返回uwTick的值。

c 复制代码
__weak uint32_t HAL_GetTick(void)
{
  return uwTick;
}

4.延时函数实现

c 复制代码
// 延时函数,微秒作为单位,利用系统嘀嗒定时器,72MHz,一次嘀嗒 1/72 us
void Delay_us(uint16_t us)
{
	// 1. 装载一个计数值,72 * us
	SysTick->LOAD = 72 * us;
    
    // 2. 清除计数值
    SysTick->VAL = 0;

	// 3. 配置,使用系统时钟(1),计数结束不产生中断(0),使能定时器(1)
	SysTick->CTRL = 0x05;

	// 4. 等待计数值变为0,判断CTRL标志位COUNTFLAG是否为1
	while ((SysTick->CTRL & SysTick_CTRL_COUNTFLAG) == 0)
	{}
	
	// 5. 关闭定时器
	SysTick->CTRL &= ~SysTick_CTRL_ENABLE;
}

void Delay_ms(uint16_t ms)
{
	while (ms--)
	{
		Delay_us(1000);
	}
}

void Delay_s(uint16_t s)
{
	while (s--)
	{
		Delay_ms(1000);
	}
}

二、基本定时器

STM32F103系列提供了8个定时器:2个基本定时器(TIM6,7),4个通用定时器(TIM2-5),2个高级定时器(TIM1和TIM8)。

1. 基本定时器介绍

基本定时器TIM6和TIM7各包含一个16位自动装载计数器,由各自的可编程预分频器驱动。

这2个定时器是互相独立的,不共享任何资源。

这2个基本定时器只能向上计数,由于没有外部IO,所以只能计时,不能对外部脉冲进行计数。

功能:定时中断,主模式,触发DAC。

1.1 时钟源

只有一种时钟源,内部时钟,一般为72MHz

1.2 时基单元

  • 预分频寄存器

    将过来的时钟信号进行预分频,按照1到65536之间的任意值分频。

  • 计数器

    基本定时器只能向上计数,从0开始自增。自增到自动重装载寄存器的值时,下一个时钟上升沿到来后,计数器产生溢出,从0重新计数,产生更新事件。

    如果开启中断,也会产生更新中断。

  • 自动重装载寄存器

    包含预加载寄存器和影子寄存器。

    写数据到自动重装载寄存器时候先写到预加载寄存器,然后再更新到影子寄存器。

    计数器是否溢出,是查看影子寄存器的值。

    寄存器CR1的ARPE位决定是否预加载,没有预加载时,写入的值会立即更新到影子寄存器,有预加载时,写入的值会等到产生更新事件(计数器溢出)才更新到影子寄存器。

1.3 计算定时时间

计数器多久产生一次溢出

  • 计数器时钟频率:

    真正的分频值=预分频系数+1

    内部时钟频率/(预分频系数+1)

  • 计数器的周期:累加一次需要的时间

    (预分频系数+1)/内部时钟频率

  • 计数器累加多少次产生一次更新事件:

    自动重装载值+1

综上,定时时间为:

(预分频系数+1)/内部时钟频率\]\*(自动重装载值+1)

假设选择定时1s,且内部时钟频率为72MHz,怎么配置相关数值?

令预分频系数=7200-1,计数器频率=10000,则自动重装载值=10000-1

2. 基本定时器实验:LED灯闪烁

利用基本定时器功能,实现LED灯闪烁

2.1 寄存器实现

c 复制代码
#include "tim6.h"

// 定时1s
void TIM6_Init(void)
{
    // 1. 定时器6开启时钟
    RCC->APB1ENR |= RCC_APB1ENR_TIM6EN;

    // 2. 设置预分频值 7199表示7200分频
    TIM6->PSC = 7200 - 1;
    // 3. 设置自动重装载值
    // 表示计数器计数10000次发生一次中断 计数一次100us
    TIM6->ARR = 10000 - 1;
    // 4. 使能更新中断,让定时器自身能够产生中断信号
    TIM6->DIER |= TIM_DIER_UIE;
    // 5. 设置中断优先级分组
    NVIC_SetPriorityGrouping(3);
    // 6. 设置中断优先级
    NVIC_SetPriority(TIM6_IRQn, 1);
    // 7. 使能定时器中断,使处理器能够接受处理中断信号
    NVIC_EnableIRQ(TIM6_IRQn);
    // 8. 使能计数器
    TIM6->CR1 |= TIM_CR1_CEN;
}

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

2.2 hal库实现

Trigger Event Selection: 表示主模式下向其他定时器发送的触发信号,这里忽略

开启定时器中断

生成的定时器部分代码

c 复制代码
TIM_HandleTypeDef htim6;

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)
  {
    /* TIM6 clock enable */
    __HAL_RCC_TIM6_CLK_ENABLE();

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

相关中断服务函数

c 复制代码
void TIM6_IRQHandler(void)
{
// HAL库定时器处理总函数
  HAL_TIM_IRQHandler(&htim6);
}

进入HAL_TIM_IRQHandler,这里实现对各种回调函数的调用,中断标志位的清除。

选择我们需要实现的具体中断回调函数HAL_TIM_PeriodElapsedCallback

然后实现

c 复制代码
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM6)
  {
    // led翻转
    HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_1);
  }
}

然后再开启计数器

c 复制代码
int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_TIM6_Init();
  /* USER CODE BEGIN 2 */
  // 使能更新中断  __HAL_TIM_ENABLE_IT(htim, TIM_IT_UPDATE);
  // 开启计数器  __HAL_TIM_ENABLE(htim);
  HAL_TIM_Base_Start_IT(&htim6);
  /* USER CODE END 2 */

  while (1)
  {
   
  }
}

三、通用定时器

1. 通用定时器介绍

通用定时器有4个分别是:TIM2、TIM3、TIM4、TIM5。它们拥有基本定时器所有功能。并增加如下功能:

(1)多种时钟源。

(2)向上计数(加),向下计数(减),向上/向下(先加后减)

(3)输入捕获。

(4)输出比较。

(5)PWM生成。

(6)支持针对定位的增量(正交)编码器和霍尔传感器电路。

1.1 可选的时钟源

  • 内部时钟模式,一般是72MHz,与基本定时器一致,默认时钟源就是内部时钟。

  • 外部时钟源模式1

    使用定时器自身通道的输入信号作为时钟源,每个定时器有4个输入通道。只有通道1和通道2的信号可以作为时钟信号源,通道1和通道2的信号经过输入滤波和边缘检测器成为了时钟源。

  • 外部时钟源模式2

    使用定时器的特殊引脚ETR引脚的信号作为时钟源,每一个通用定时器都有一个ETR引脚。ETR引脚信号经过极性选择,边缘检测,预分频器,输入滤波,得到信号ETRF,ETRF就成为外部时钟源。

外部时钟源一般用于定时器级联,不配置时钟源的情况下,默认选择的就是内部时钟源。

1.2 计数器的3种计数模式

向上计数模式

从0开始加,一直加到自动重装载寄存器的值

然后再来一个时钟信号,计数器溢出,产生更新事件,重新从0计数

向下计数模式

从自动重装载寄存器的值开始计数,直到减到0

然后再来一个时钟信号,计数器溢出,产生更新事件,重新从自动重装载寄存器的值计数

中央对齐模式(向上和向下计数)

从0开始向上计数,一直计数到自动重装载寄存器的值-1

再来一个时钟信号会产生更新事件,然后继续从自动重装载寄存器的值向下计数

向下计数到1,再来一个时钟信号会产生更新事件,然后继续从0开始向上计数

默认计数方向就是向上计数

2. 实验:LED呼吸灯(PWM脉冲)

输出占空比可调的PWM波形,作用到二极管,使二极管(LED)呈现呼吸灯的效果

PB1复用的是TIM3_CH4和TIM8_ CH3N,我们选择TIM3_CH4

2.1 PWM(脉冲宽度调制)

利用微处理器的数字输出来对模拟电路进行控制,PWM常用于控制电机、LED亮度调节等应用

实际使用中,生成PWM波形就是生成一个方波信号

周期:连续的两个上升沿或连续两个下降沿之间的宽度。

占空比:高电平宽度t除以周期T

使用PWM驱动惯性电器时,一般不改变频率和周期。通过改变占空比达到对外输出的有效电压的值。

2.2 输出比较部分

计数器部分

​ 捕获比较寄存器:每个定时器有4个,可以同时实现4路比较

​ 输出部分:4路输出,分别对应4个引脚

输出比较原理

通过比较定时计数器的值 CNT 与设定的比较值 CCR,可以控制输出引脚的电平状态(置高或置低),从而实现生成一定频率和占空比的 PWM 波形。

以通道1为例说明:假设计数器向上计数,重装载寄存器的值为99,假设捕获/比较寄存器的值设置为60。

比较寄存器的值和计数器的值进行大小比较,根据比较结果不同,产生不同输出:高电平或低电平。

2.3 寄存器方式实现

c 复制代码
#include "tim3.h"

void TIM3_Init(void)
{
    // 1. 开启时钟
    // 1.1 定时器3时钟
    RCC->APB1ENR |= RCC_APB1ENR_TIM3EN;
    // 1.2 GPIO的时钟 PB1
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;

    // 2. 设置GPIO为复用推挽输出 CNF=10 MODE=11
    GPIOB->CRL &= ~GPIO_CRL_CNF1_0;
    GPIOB->CRL |= GPIO_CRL_CNF1_1;
    GPIOB->CRL |= GPIO_CRL_MODE1;

    // 3. 定时器配置
    // 3.1 预分频器配置
    TIM3->PSC = 7200 - 1;
    // 3.2 自动重装载寄存器
    TIM3->ARR = 100 - 1;
    // 3.3 计数器计数方向 0-向上
    TIM3->CR1 &= ~TIM_CR1_DIR;
    // 3.4 配置通道4的捕获比较寄存器
    TIM3->CCR4 = 50;
    // 3.5 通道4配置为输出 00
    TIM3->CCMR2 &= ~TIM_CCMR2_CC4S;
    // 3.6 通道的输出比较模式 110
    TIM3->CCMR2 |= TIM_CCMR2_OC4M_2;
    TIM3->CCMR2 |= TIM_CCMR2_OC4M_1;
    TIM3->CCMR2 &= ~TIM_CCMR2_OC4M_0;
    // 3.7 通道极性 0高电平有效 1低电平有效
    TIM3->CCER &= ~TIM_CCER_CC4P;
    // 3.8 使能通道4
    TIM3->CCER |= TIM_CCER_CC4E;
}

// 使能计数器
void Tim3_Start(void)
{
    TIM3->CR1 |= TIM_CR1_CEN;
}

// 失能计数器
void Tim3_Stop(void)
{
    TIM3->CR1 &= ~TIM_CR1_CEN;
}

// 设置工作周期
void Tim3_setDutyCycle(uint8_t cycle)
{
    TIM3->CCR4 = cycle;
}
c 复制代码
#include "delay.h"
#include "tim3.h"

int main(void)
{
	TIM3_Init();

	Tim3_Start();

	uint8_t duty_cycle = 0;
	uint8_t dir = 0;
	Tim3_setDutyCycle(duty_cycle);

	while (1)
	{
		if (dir == 0)
		{
			duty_cycle += 1;
			if (duty_cycle >= 99)
			{
				dir = 1;
			}
		}
		else
		{
			duty_cycle -= 1;
			if (duty_cycle <= 1)
			{
				dir = 0;
			}
		}
		Tim3_setDutyCycle(duty_cycle);
		Delay_ms(10);
	}
}

2.4 hal库方式实现

在tim.c中添加设置捕获比较寄存器的函数

c 复制代码
void setDutyCycle(uint8_t dutyCycle)
{
  // 设置捕获比较寄存器的值
  __HAL_TIM_SetCompare(&htim3, TIM_CHANNEL_4, dutyCycle);
}

主函数main.c

c 复制代码
int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_TIM3_Init();
  /* USER CODE BEGIN 2 */
  // 启动定时器的PWM模式输出
  HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_4);

  uint8_t dutycycle = 1;
  uint8_t step = 1;
  /* USER CODE END 2 */

  while (1)
  {
    if (dutycycle <= 0 || dutycycle >= 99)
    {
      step = -step;
    }

    dutycycle = dutycycle + step;
    setDutyCycle(dutycycle);

    HAL_Delay(10);
  }
}

3. 实验:测量PWM的频率/周期

使用输入捕获功能,来测量PWM的频率/周期。

如果要测量PWM的周期/频率,只要测量出连续的两个上升沿或连续的两个下降沿的时间间隔。

TIM3的CH4输出PWM,使用TIM4的CH1来捕获PWM信号

在这里需要把PB1连接到PB6

3.1 输入捕获功能

捕获输入通道上信号的上升沿或下降沿

输入捕获部分

包含3部分

  • 输入部分:

​ 共4路输入信号,每路都有自己的输入引脚,4路输入引脚和输出比较引脚是一致的。对于同一路引脚,只能处于输入捕获或输出比较。

  • 计数器部分
  • 捕获寄存器部分:共有4路,与比较寄存器共用

以通道1为例:

假设计数器向上计数,重装载寄存器值为65535,尽量避免计数器溢出

  • 信号经过通道1的引脚进入通道1,得到TI1

  • TI1信号进入滤波器和边沿检测器,其中滤波器用来滤掉一些毛刺信号,边沿检测器确定要捕获的是上升沿还是下降沿。

  • 从边沿检测器出来的上升沿或下降沿信号为TI1FP1

  • TI1FP1经过信号选择器得到IC1

  • IC1进入预分频器,可以对信号选择分频或不分频。如果信号的频率很高,可以分频。

  • 信号从预分频器出来,信号为IC1PS

    会产生一个捕获比较事件

    如果开启了中断也会产生捕获比较中断

    立即把计数器寄存器的值存入到捕获寄存器。在下次捕获事件产生之前,捕获寄存器的值不会产生变化。

测量PWM周期原理

假设计数器时钟72分频,则计数器时钟频率为1MHz,计数器累加一次时间为1us。

设置重装载寄存器值为65535,设置值为最大,尽量避免溢出。

假设测量的信号周期小于65535us

  • 在一个周期内,计数器不会溢出
  • 当第1个上升沿到来时,重置计数器的值(让计数器从0开始计数)
  • 当第2个上升沿到来时,计数器的值会自动复制到捕获寄存器,此时寄存器的值就是信号周期,单位us。

3.2 寄存器方式实现

使用TIM3的CH4输入PWM,参照LED呼吸灯案例

添加tim4.c、tim4.h文件,来捕获PWM信号

c 复制代码
#include "tim4.h"

void TIM4_Init(void)
{
    // 1. 开启时钟
    // 1.1 定时器4时钟
    RCC->APB1ENR |= RCC_APB1ENR_TIM4EN;
    // 1.2 GPIO的时钟 PB6
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;

    // 2. 设置GPIO为浮空输入 CNF=01 MODE=00
    GPIOB->CRL |= GPIO_CRL_CNF6_0;
    GPIOB->CRL &= ~GPIO_CRL_CNF6_1;
    GPIOB->CRL &= ~GPIO_CRL_MODE6;

    // 3. 定时器时基配置
    // 3.1 预分频器配置 分频后计数器时钟为1MHz,对应周期1us
    TIM4->PSC = 72 - 1;
    // 3.2 自动重装载寄存器:值设置为最大,尽量避免溢出
    TIM4->ARR = 65535;
    // 3.3 计数器计数方向 0-向上
    TIM4->CR1 &= ~TIM_CR1_DIR;

    // 4. 输入捕获部分
    // 4.1 TIM4_CH1引脚连到TI1输入
    TIM4->CR2 &= ~TIM_CR2_TI1S;
    // 4.2 输入捕获滤波器:不滤波
    TIM4->CCMR1 &= ~TIM_CCMR1_IC1F;
    // 4.3 边沿检测器:上升沿0 下降沿1
    TIM4->CCER &= ~TIM_CCER_CC1P;
    // 4.4 通道配置输入,IC1映射在TI1上:01
    TIM4->CCMR1 |= TIM_CCMR1_CC1S_0;
    TIM4->CCMR1 &= ~TIM_CCMR1_CC1S_1;
    // 4.5 输入捕获分频器
    TIM4->CCMR1 &= ~TIM_CCMR1_IC1PSC;
    // 4.6 使能捕获输入
    TIM4->CCER |= TIM_CCER_CC1E;
    // 4.7 使能捕获中断
    TIM4->DIER |= TIM_DIER_CC1IE;

    // 5. NVIC配置
    NVIC_SetPriorityGrouping(4);
    NVIC_SetPriority(TIM4_IRQn, 1);
    NVIC_EnableIRQ(TIM4_IRQn);
}

// 使能计数器
void TIM4_Start(void)
{
    TIM4->CR1 |= TIM_CR1_CEN;
}

// 失能计数器
void TIM4_Stop(void)
{
    TIM4->CR1 &= ~TIM_CR1_CEN;
}

// 上升沿个数
uint8_t raiseEdgeCount = 0;
uint16_t pwm_cycle = 0;

void TIM4_IRQHandler(void)
{
    // 判断是否是TIM4的通道1发生捕获中断
    if (TIM4->SR & TIM_SR_CC1IF)
    {
        // 中断标志位清0
        TIM4->SR &= ~TIM_SR_CC1IF;
        raiseEdgeCount++;
        // 第一个上升沿到来,清零计数器
        if (raiseEdgeCount == 1)
        {
            TIM4->CNT = 0;
        }
        // 第二个上升沿到来,读取捕获寄存器的值
        else if (raiseEdgeCount == 2)
        {
            raiseEdgeCount = 0;
            pwm_cycle = TIM4->CCR1;
        }
    }
}

// 返回PWM周期 ms
float TIM4_GetPWMCycle(void)
{
    return pwm_cycle / 1000.0;
}

// 返回PWM频率 Hz
float TIM4_GetPWMFreq(void)
{
    return 1000000.0 / pwm_cycle;
}

主函数main.c如下:

c 复制代码
#include "led.h"
#include "delay.h"
#include "usart.h"
#include "tim3.h"
#include "tim4.h"

int main(void)
{
	USART1_init();
	printf("hello world!\r\n");
	TIM3_Init();
	TIM4_Init();

	Tim3_Start();
	TIM4_Start();

	float t, f;

	while (1)
	{
		t = TIM4_GetPWMCycle();
		f = TIM4_GetPWMFreq();

		printf("t=%.4fms,f=%.4fHz\r\n", t, f);

		Delay_ms(1000);
	}
}

3.3 hal库实现

TIM4配置如下

开启中断

注意这里需要将引脚改为PB6,因为默认情况下输入引脚会重映射到PD12

TIM3(输出PWM)的配置中需要将占空比设置成一个数,默认情况下为0(PWM呼吸灯案例中采用了默认配置),显然无法形成PWM方波。

在tim.c文件中添加下面函数

c 复制代码
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM4)
  {
    // 将计数器清零
    __HAL_TIM_SetCounter(htim, 0);
  }
}

float TIM4_GetPWMCycle(void)
{
  return __HAL_TIM_GetCompare(&htim4, TIM_CHANNEL_1) / 1000.0;
}

float TIM4_GetPWMFreq(void)
{
  return 1000000.0 / __HAL_TIM_GetCompare(&htim4, TIM_CHANNEL_1);
}

需要注意因为使用了串口打印,所以需要开启串口并在代码中重写fputc

main.c中启动定时器

c 复制代码
int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_TIM3_Init();
  MX_TIM4_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */
  // 启动定时器3的PWM模式输出
  HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_4);
  // 启动定时器4用于输入捕获
  HAL_TIM_IC_Start_IT(&htim4, TIM_CHANNEL_1);

  float t, f;
  /* USER CODE END 2 */

  while (1)
  {
    /* USER CODE BEGIN 3 */
    t = TIM4_GetPWMCycle();
    f = TIM4_GetPWMFreq();

    printf("t=%.4fms,f=%.4fHz\r\n", t, f);
    HAL_Delay(1000);
  }
  /* USER CODE END 3 */
}

4. 实验:同时测量PWM的频率/周期和占空比

用一个定时器的2个通道同时测量频率和占空比

PWM占空比,只要测量出连续的一个上升沿和下降沿的时间间隔,然后除以周期。

因为测试频率需要连续的两个上升沿,而测试占空比就需要连续的一个上升沿和一个下降沿,所以需要在这个通道即要检测上升沿,也要检测下降沿。

这就需要使用定时器的从模式和PWM输入模式。

4.1 触发输入和从模式

定时器的触发信号分为两大类:

  • 触发输入信号(TRGI)

    从外部过来(也可能是自己输入通道过来)到本定时器的信号。用来控制本定时器一些动作,比如复位,使能。

    这个时候本定时器 就处于主从模式中的从模式

  • 触发输出信号(TRGO)

    本定时器输出到其他定时器或其他外设的信号

    用于与其他定时器的级联(触发其他定时器的一些工作)或者触发一些其他外设工作。

    这个时候本定时器 就处于主从模式中的主模式

4.1.1 触发输入信号

从模式控制寄存器的TS位用于配置触发选择

触发输入信号可分为以下几类

  • 第一类

TS[2:0]=000-011,4个

来源于其他定时器的TRGO信号。经过芯片内部连接,来到本定时器的ITR0/1/2/3。

内部连接是定死的,不能更改,连接情况如下

比如TIM1的TRGO连接到了TIM2(从定时器)的ITR0,ITRx中某个信号经过信号选择器最终成为TRGI信号

  • 第二类

TS[2:0]=111 1个

来源于外部触发引脚ETR,经过极性选择,边沿检测和预分频器,输入滤波,成为TRGI信号。

TRGI信号通过从模式控制器控制本定时器实现复位或使能。

  • 第三类

TS[2:0]=100 1个

来源于定时器自身的通道1信号。经过输入滤波器和边沿检测器,得到TI1F_ED信号。

上升沿和下降沿都会产生TI1F_ED信号(没有经过极性选择),经过信号选择器最终成为TRGI信号。

  • 第四类

TS[2:0]=101/110 2个

来源定时器自身的通道1信号或通道2信号。经过输入滤波器和边沿检测器,得到TI1FP1和TI2FP2信号,注意他们是上升沿或者下降沿,只能选择一种,最终成为TRGI信号。

在本次实验中就会使用第四类触发输入信号来完成。

4.1.2 定时器从模式

这些TRGI信号要控制定时器,需要把定时器配置为从模式

从模式控制寄存器的SMS位用于配置从模式工作模式

4.2 PWM输入模式

该模式是输入捕获模式的一个特例,操作与输入捕获模式相同。

以信号从通道1输入为例。

经过输入滤波器和边沿检测器得到2路信号:TI1FP1和TI1FP2。

TI1FP1和TI1FP2极性相反,一个得到输入的上升沿(TI1FP1),一个得到输入的下降沿(TI1FP2)。

TI1FP1得到IC1信号,在通道1,用来测量周期

TI1FP2得到IC2信号,在通道2,用来测量高电平时间

TI1FP1作为触发输入信号,开启从模式中复位模式

4.3 寄存器实现

修改tim4.c文件如下,tim3同样用于产生PWM,参考实验LED呼吸灯(PWM脉冲)

c 复制代码
#include "tim4.h"

void TIM4_Init(void)
{
    // 1. 开启时钟
    // 1.1 定时器4时钟
    RCC->APB1ENR |= RCC_APB1ENR_TIM4EN;
    // 1.2 GPIO的时钟 PB6
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;

    // 2. 设置GPIO为浮空输入 CNF=01 MODE=00
    GPIOB->CRL |= GPIO_CRL_CNF6_0;
    GPIOB->CRL &= ~GPIO_CRL_CNF6_1;
    GPIOB->CRL &= ~GPIO_CRL_MODE6;

    // 3. 定时器时基配置
    // 3.1 预分频器配置 分频后计数器时钟为1MHz,对应周期1us
    TIM4->PSC = 72 - 1;
    // 3.2 自动重装载寄存器:值设置为最大,尽量避免溢出
    TIM4->ARR = 65535;
    // 3.3 计数器计数方向 0-向上
    TIM4->CR1 &= ~TIM_CR1_DIR;

    // 4. 输入捕获部分
    // 4.1 TIM4_CH1引脚连到TI1输入
    TIM4->CR2 &= ~TIM_CR2_TI1S;
    // 4.2 输入捕获滤波器:不滤波
    TIM4->CCMR1 &= ~TIM_CCMR1_IC1F;
    // 4.3 边沿检测器:上升沿0 下降沿1
    TIM4->CCER &= ~TIM_CCER_CC1P; // IC1
    TIM4->CCER |= TIM_CCER_CC2P;  // IC2

    // 4.4 通道配置输入
    // IC1映射在TI1上:01
    TIM4->CCMR1 |= TIM_CCMR1_CC1S_0;
    TIM4->CCMR1 &= ~TIM_CCMR1_CC1S_1;
    // IC2映射到TI1上:10
    TIM4->CCMR1 &= ~TIM_CCMR1_CC2S_0;
    TIM4->CCMR1 |= TIM_CCMR1_CC2S_1;

    // 4.5 输入捕获分频器 IC1 IC2都不分频
    TIM4->CCMR1 &= ~TIM_CCMR1_IC1PSC;
    TIM4->CCMR1 &= ~TIM_CCMR1_IC2PSC;

    // 4.6 配置TRGI信号:TI1FP1 SMCR TS=101
    TIM4->SMCR |= TIM_SMCR_TS_0;
    TIM4->SMCR &= ~TIM_SMCR_TS_1;
    TIM4->SMCR |= TIM_SMCR_TS_2;

    // 4.7 配置从模式为复位模式 SMCR SMS=100
    TIM4->SMCR &= ~TIM_SMCR_SMS_0;
    TIM4->SMCR &= ~TIM_SMCR_SMS_1;
    TIM4->SMCR |= TIM_SMCR_SMS_2;

    // 4.8 使能捕获输入
    TIM4->CCER |= TIM_CCER_CC1E;
    TIM4->CCER |= TIM_CCER_CC2E;
}

// 使能计数器
void TIM4_Start(void)
{
    TIM4->CR1 |= TIM_CR1_CEN;
}

// 失能计数器
void TIM4_Stop(void)
{
    TIM4->CR1 &= ~TIM_CR1_CEN;
}

// 返回PWM周期 ms
float TIM4_GetPWMCycle(void)
{
    return TIM4->CCR1 / 1000.0;
}

// 返回PWM频率 Hz
float TIM4_GetPWMFreq(void)
{
    return 1000000.0 / TIM4->CCR1;
}

// 返回占空比
float TIM4_GetDutyCycle(void)
{
    return TIM4->CCR2 * 1.0 / TIM4->CCR1;
}
c 复制代码
#include "led.h"
#include "delay.h"
#include "usart.h"
#include "tim3.h"
#include "tim4.h"

int main(void)
{
	USART1_init();
	printf("hello world!\r\n");
	TIM3_Init();
	TIM4_Init();

	Tim3_Start();
	TIM4_Start();

	while (1)
	{
		printf("t=%.4fms,f=%.4fHz,duty=%.2f%%\r\n", TIM4_GetPWMCycle(), TIM4_GetPWMFreq(), TIM4_GetDutyCycle() * 100);

		Delay_ms(1000);
	}
}

4.4 hal库实现

tim.c添加代码

c 复制代码
float TIM4_GetPWMCycle(void)
{
  return (__HAL_TIM_GetCompare(&htim4, TIM_CHANNEL_1)) / 1000.0;
}

float TIM4_GetPWMFreq(void)
{
  return 1000000.0 / (__HAL_TIM_GetCompare(&htim4, TIM_CHANNEL_1));
}

float TIM4_GetDutyCycle(void)
{
  return (__HAL_TIM_GetCompare(&htim4, TIM_CHANNEL_2)) * 1.0 / (__HAL_TIM_GetCompare(&htim4, TIM_CHANNEL_1));
}
c 复制代码
int main(void)
{

  HAL_Init();
  SystemClock_Config();

  MX_GPIO_Init();
  MX_TIM3_Init();
  MX_TIM4_Init();
  MX_USART1_UART_Init();
  
  HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_4);
  HAL_TIM_IC_Start(&htim4, TIM_CHANNEL_1);
  HAL_TIM_IC_Start(&htim4, TIM_CHANNEL_2);

  printf("hello\r\n");

  while (1)
  {
    /* USER CODE BEGIN 3 */
    printf("t=%.4fms,f=%.4fHz,duty=%.2f%%\r\n",
           TIM4_GetPWMCycle(), TIM4_GetPWMFreq(),
           TIM4_GetDutyCycle() * 100);

    HAL_Delay(1000);
  }
  /* USER CODE END 3 */
}

四、高级定时器

1. 高级定时器介绍

高级定时器有2个分别是:TIM1、TIM8。

高级定时器除了拥有通用定时器的所有功能外,还具有以下功能:

  • 死区时间可编程的互补输出。
  • 断路输入信号(刹车输入信号)。
  • 重复计数器。

重复计数器

在基本定时器和通用定时器中,计数器每溢出1次,就产生1次更新事件。在高级定时器中,计数器每溢出1次,会产生一个信号,让重复计数器的值-1。当重复计数器的值减到0,如果计数器再溢出1次,就会产生更新事件。

重复计数器的初始化来源于RCR寄存器REP位。

如果REP=2,则CNT计数器溢出3次产生一次更新事件。可以用重复计数器生成有限个周期的PWM

互补输出

高级定时器的通道1/2/3可以分别输出2路互补信号:CH1和CH1N(通道4没有)

互补信号:频率周期相等,相位相差180°

互补输出一般用于驱动H桥电路,H桥通常用于驱动电流较大的负载,比如电机

2. 实验:输出有限个周期的PWM波

输出5个周期的PWM波,频率2Hz,LED闪烁5次。

需求实现思路:使用高级定时器的重复计数器,当计数器溢出时,在溢出中断中停止定时器工作。重复计数器寄存器的值设置为4,即可输出5个周期的PWM波,LED会闪烁5次。

高级定时器TIM8_CH1的端口为PC6,将PC6与LED端口连接起来。

c 复制代码
#include "tim8.h"
#include "stdio.h"

void TIM8_Init(void)
{
    // 1. 开启时钟
    // 1.1 定时器8时钟
    RCC->APB2ENR |= RCC_APB2ENR_TIM8EN;
    // 1.2 GPIO的时钟 PC6
    RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;

    // 2. 设置PC6为复用推挽输出 CNF=10 MODE=11
    GPIOC->CRL &= ~GPIO_CRL_CNF6_0;
    GPIOC->CRL |= GPIO_CRL_CNF6_1;
    GPIOC->CRL |= GPIO_CRL_MODE6;

    // 3. 定时器时基配置
    // 3.1 预分频器配置
    TIM8->PSC = 7200 - 1;
    // 3.2 自动重装载寄存器
    TIM8->ARR = 5000 - 1;
    // 3.3 计数器计数方向 0-向上
    TIM8->CR1 &= ~TIM_CR1_DIR;
    // 3.4 重复计数器配置 计数器溢出5次才会产生更新中断
    TIM8->RCR = 4;

    // 4. 输出部分
    // 4.1 通道1配置为输出
    TIM8->CCMR1 &= ~TIM_CCMR1_CC1S;
    // 4.2 输出比较模式 PWM模式1  110
    TIM8->CCMR1 &= ~TIM_CCMR1_OC1M_0;
    TIM8->CCMR1 |= TIM_CCMR1_OC1M_1;
    TIM8->CCMR1 |= TIM_CCMR1_OC1M_2;
    // 4.3 配置捕获比较寄存器的值
    TIM8->CCR1 = 2500;
    // 4.4 输出极性
    TIM8->CCER &= ~TIM_CCER_CC1P;
    // 4.5 使能通道
    TIM8->CCER |= TIM_CCER_CC1E;
    // 4.6 主输出使能(高级定时器需要)
    TIM8->BDTR |= TIM_BDTR_MOE;

    // 4.7 产生更新事件,从而将预分频和重复计数器等值更新到影子寄存器
    // 只产生更新事件,不产生中断
    TIM8->EGR |= TIM_EGR_UG;
    TIM8->SR &= ~TIM_SR_UIF; // 清除中断标志位

    // 5. 配置中断
    // 5.1 定时器更新中断使能
    TIM8->DIER |= TIM_DIER_UIE;
    // 5.2 NVIC配置
    NVIC_SetPriorityGrouping(4);
    NVIC_SetPriority(TIM8_UP_IRQn, 1);
    NVIC_EnableIRQ(TIM8_UP_IRQn);
}

// 使能计数器
void TIM8_Start(void)
{
    TIM8->CR1 |= TIM_CR1_CEN;
}

// 失能计数器
void TIM8_Stop(void)
{
    TIM8->CR1 &= ~TIM_CR1_CEN;
}

// 在中断中停掉计数器
void TIM8_UP_IRQHandler(void)
{
    printf("interrupt...\r\n");
    TIM8->SR &= ~TIM_SR_UIF;
    TIM8_Stop();
}
复制代码
    // 4.7 产生更新事件,从而将预分频和重复计数器等值更新到影子寄存器
    // 只产生更新事件,不产生中断
    TIM8->EGR |= TIM_EGR_UG;
    TIM8->SR &= ~TIM_SR_UIF; // 清除中断标志位

必须进行上面配置,否则观察不到现象。因为预分频和重复计数器等值是先存入预装载寄存器,只有当发生一个更新事件的时候,预装载寄存器才能被传送到影子寄存器。

单片机开始上电,预分频器还是1分频,为72MHZ,频率太高,而此时重复计数器的值也未进行刷新,会导致一上电进入中断,而在中断中关闭了计数器,自然就观察不到实验现象。

所以在计数器开始计数之前,必须通过设置TIMx_EGR寄存器中的UG位来初始化所有的寄存器。而此时我们不希望设置UG位后产生中断,打印多余的数据,可以在产生更新事件后,清除中断标志位。

开启更新中断

进入定时器初始化函数,追踪到stm32f1xx_hal_tim.c文件下TIM_Base_SetConfig函数

c 复制代码
  /* Generate an update event to reload the Prescaler
     and the repetition counter (only for advanced timer) value immediately */
  TIMx->EGR = TIM_EGR_UG;

可知在初始化阶段就已经帮我们产生更新事件,将相关寄存器的值刷新到影子寄存器。

c 复制代码
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM8)
  {
    printf("interrupt...\r\n");
    HAL_TIM_PWM_Stop(&htim8, TIM_CHANNEL_1);
  }
}
c 复制代码
int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  MX_TIM8_Init();
  /* USER CODE BEGIN 2 */
  printf("hello\r\n");
  // 启用更新中断
  __HAL_TIM_ENABLE_IT(&htim8, TIM_IT_UPDATE);
  HAL_TIM_PWM_Start(&htim8, 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 */
}
相关推荐
路过羊圈的狼4 小时前
STM32的HAL库驱动ADS124S08进行PT100温度采集
stm32·嵌入式硬件·mongodb
李永奉4 小时前
51单片机-实现红外遥控模块教程
单片机·嵌入式硬件·51单片机
辛集电子5 小时前
【STM32】位带操作
stm32·单片机·嵌入式硬件
MOS管-冠华伟业6 小时前
微硕WSF4012 N+P双沟MOS管,驱动汽车智能座椅“无感”升降气泵
单片机·嵌入式硬件
沐欣工作室_lvyiyi6 小时前
基于单片机的汽车防碰撞刹车系统(论文+源码)
单片机·嵌入式硬件·stm32单片机·汽车·毕业设计
点灯小铭6 小时前
基于51单片机宠物喂食系统设计
单片机·mongodb·毕业设计·51单片机·课程设计·宠物
机器视觉知识推荐、就业指导7 小时前
STM32 外设驱动模块:声音传感器模块
stm32·单片机·嵌入式硬件
亿道电子Emdoor7 小时前
【ARM】MDK-Functions界面设置
stm32·单片机·嵌入式硬件
学不动CV了7 小时前
ARM单片机中断及中断优先级管理详解
c语言·arm开发·stm32·单片机·嵌入式硬件·51单片机