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位用于配置触发选择

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

  • 第一类

TS2:0=000-011,4个

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

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

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

  • 第二类

TS2:0=111 1个

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

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

  • 第三类

TS2:0=100 1个

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

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

  • 第四类

TS2: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 */
}
相关推荐
国科安芯1 天前
航天器多路并联大功率电源系统设计与ASP4644均流特性分析
单片机·嵌入式硬件·fpga开发·安全性测试
梁朝辉1 天前
STM32H750VBT6中ADCINP与INN什么区别
stm32·单片机·嵌入式硬件
崇山峻岭之间2 天前
单片机无感FOC驱动之ADC
单片机·嵌入式硬件
Ligocious2 天前
1.点亮一颗小小的LED
单片机·嵌入式硬件
KaifuZeng2 天前
通信与接口协议面试八、CAN通信
单片机·嵌入式硬件·面试·通信与接口协议
智源单片机设计2 天前
基于单片机的直流电机双闭环调速系统设计
单片机·嵌入式硬件
kebidaixu2 天前
两轮车换电 BMS 的模拟前端中颖 SH367306 AFE 芯片详解
stm32
kebidaixu2 天前
两轮车换电 BMS 的模拟前端中颖 SH367306 AFE 芯片调试实录:四个关键问题与解决方案
stm32
wuyk5552 天前
24. C 语言模块化:不是拆几个.c 文件那么简单
c语言·开发语言·stm32·单片机