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 寄存器实现
#include "systick.h"void systick_init(void)
{// 1. 配置时钟源 1=AHB(72MHZ) 0=AHB/8SysTick->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++;// 1sif (count == 1000){count = 0;// led翻转电平LED_Toggle();}
}
SysTick 是内核中断,优先级通过 SCB->SHP[11]
配置,默认优先级较低。若需高优先级,需显式设置:
NVIC_SetPriority(SysTick_IRQn, 0); // 设置为最高优先级
上面代码未显式设置优先级,但 SysTick 的默认优先级已经存在(通常为最低)。如果应用中没有其他中断或对优先级无特殊要求,默认配置即可工作。
3.2 hal库实现
在hal库初始化的时候,会初始化SysTick定时器
以下为自动生成的部分代码
__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次就是1msif (HAL_SYSTICK_Config(SystemCoreClock / (1000U / uwTickFreq)) > 0U){return HAL_ERROR;}/* Configure the SysTick IRQ priority */if (TickPriority < (1UL << __NVIC_PRIO_BITS)){// 设置SysTick优先级:抢占优先级15(最低) 和响应优先级0HAL_NVIC_SetPriority(SysTick_IRQn, TickPriority, 0U);uwTickPrio = TickPriority;}else{return HAL_ERROR;}/* Return function status */return HAL_OK;
}
一般建议把SysTick定时器的抢占优先级设置为最高(数字越小,优先级越高)。否则在其他中断中使用延时函数的时候会阻塞卡死。
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 */
}
// 递增全局变量uwTick
// 每1ms递增一次
__weak void HAL_IncTick(void)
{uwTick += uwTickFreq;
}
我们可以自己重新实现HAL_IncTick函数
在main.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的值。
__weak uint32_t HAL_GetTick(void)
{return uwTick;
}
4.延时函数实现
// 延时函数,微秒作为单位,利用系统嘀嗒定时器,72MHz,一次嘀嗒 1/72 us
void Delay_us(uint16_t us)
{// 1. 装载一个计数值,72 * usSysTick->LOAD = 72 * us;// 2. 清除计数值SysTick->VAL = 0;// 3. 配置,使用系统时钟(1),计数结束不产生中断(0),使能定时器(1)SysTick->CTRL = 0x05;// 4. 等待计数值变为0,判断CTRL标志位COUNTFLAG是否为1while ((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 寄存器实现
#include "tim6.h"// 定时1s
void TIM6_Init(void)
{// 1. 定时器6开启时钟RCC->APB1ENR |= RCC_APB1ENR_TIM6EN;// 2. 设置预分频值 7199表示7200分频TIM6->PSC = 7200 - 1;// 3. 设置自动重装载值// 表示计数器计数10000次发生一次中断 计数一次100usTIM6->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: 表示主模式下向其他定时器发送的触发信号,这里忽略
开启定时器中断
生成的定时器部分代码
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);}
}
相关中断服务函数
void TIM6_IRQHandler(void)
{
// HAL库定时器处理总函数HAL_TIM_IRQHandler(&htim6);
}
进入HAL_TIM_IRQHandler
,这里实现对各种回调函数的调用,中断标志位的清除。
选择我们需要实现的具体中断回调函数HAL_TIM_PeriodElapsedCallback
然后实现
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{if (htim->Instance == TIM6){// led翻转HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_1);}
}
然后再开启计数器
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 寄存器方式实现
#include "tim3.h"void TIM3_Init(void)
{// 1. 开启时钟// 1.1 定时器3时钟RCC->APB1ENR |= RCC_APB1ENR_TIM3EN;// 1.2 GPIO的时钟 PB1RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;// 2. 设置GPIO为复用推挽输出 CNF=10 MODE=11GPIOB->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配置为输出 00TIM3->CCMR2 &= ~TIM_CCMR2_CC4S;// 3.6 通道的输出比较模式 110TIM3->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 使能通道4TIM3->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;
}
#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中添加设置捕获比较寄存器的函数
void setDutyCycle(uint8_t dutyCycle)
{// 设置捕获比较寄存器的值__HAL_TIM_SetCompare(&htim3, TIM_CHANNEL_4, dutyCycle);
}
主函数main.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信号
#include "tim4.h"void TIM4_Init(void)
{// 1. 开启时钟// 1.1 定时器4时钟RCC->APB1ENR |= RCC_APB1ENR_TIM4EN;// 1.2 GPIO的时钟 PB6RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;// 2. 设置GPIO为浮空输入 CNF=01 MODE=00GPIOB->CRL |= GPIO_CRL_CNF6_0;GPIOB->CRL &= ~GPIO_CRL_CNF6_1;GPIOB->CRL &= ~GPIO_CRL_MODE6;// 3. 定时器时基配置// 3.1 预分频器配置 分频后计数器时钟为1MHz,对应周期1usTIM4->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 下降沿1TIM4->CCER &= ~TIM_CCER_CC1P;// 4.4 通道配置输入,IC1映射在TI1上:01TIM4->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){// 中断标志位清0TIM4->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如下:
#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文件中添加下面函数
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中启动定时器
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脉冲)
#include "tim4.h"void TIM4_Init(void)
{// 1. 开启时钟// 1.1 定时器4时钟RCC->APB1ENR |= RCC_APB1ENR_TIM4EN;// 1.2 GPIO的时钟 PB6RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;// 2. 设置GPIO为浮空输入 CNF=01 MODE=00GPIOB->CRL |= GPIO_CRL_CNF6_0;GPIOB->CRL &= ~GPIO_CRL_CNF6_1;GPIOB->CRL &= ~GPIO_CRL_MODE6;// 3. 定时器时基配置// 3.1 预分频器配置 分频后计数器时钟为1MHz,对应周期1usTIM4->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 下降沿1TIM4->CCER &= ~TIM_CCER_CC1P; // IC1TIM4->CCER |= TIM_CCER_CC2P; // IC2// 4.4 通道配置输入// IC1映射在TI1上:01TIM4->CCMR1 |= TIM_CCMR1_CC1S_0;TIM4->CCMR1 &= ~TIM_CCMR1_CC1S_1;// IC2映射到TI1上:10TIM4->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=101TIM4->SMCR |= TIM_SMCR_TS_0;TIM4->SMCR &= ~TIM_SMCR_TS_1;TIM4->SMCR |= TIM_SMCR_TS_2;// 4.7 配置从模式为复位模式 SMCR SMS=100TIM4->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;
}
#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添加代码
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));
}
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端口连接起来。
#include "tim8.h"
#include "stdio.h"void TIM8_Init(void)
{// 1. 开启时钟// 1.1 定时器8时钟RCC->APB2ENR |= RCC_APB2ENR_TIM8EN;// 1.2 GPIO的时钟 PC6RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;// 2. 设置PC6为复用推挽输出 CNF=10 MODE=11GPIOC->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 110TIM8->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
函数
/* Generate an update event to reload the Prescalerand the repetition counter (only for advanced timer) value immediately */TIMx->EGR = TIM_EGR_UG;
可知在初始化阶段就已经帮我们产生更新事件,将相关寄存器的值刷新到影子寄存器。
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{if (htim->Instance == TIM8){printf("interrupt...\r\n");HAL_TIM_PWM_Stop(&htim8, TIM_CHANNEL_1);}
}
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 */
}