一、系统定时器
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 */
}