一、定时器介绍
- STM32总共有8个定时器
- 2个高级定时器(TIM1、TIM8):输出比较,输入捕获,可编程死区互补输出等,是通用定时器的扩展
- 4个通用定时器(TIM2-TIM5):输出比较,输入捕获等,是基本定时器的扩展
- 2个基本定时器(TIM6、TIM7):定时、计数
二、通用定时器简介
STM32F1的通用定时器TIMx (TIM2-TIM5)具有如下功能:
- 16 位向上、向下、向上/向下自动装载计数器(TIMx_CNT)。
- 16 位可编程(可以实时修改)预分频器(TIMx_PSC),计数器时钟频率的分频系数为 1~65536之间的任意数值。
- 4个独立通道(TIMx_CH1-4),这些通道可以用来作为:
- 输入捕获
- 输出比较
- PWM 生成(边缘或中间对齐模式)
- 单脉冲模式输出
- 可使用外部信号(TIMx_ETR)控制定时器,且可实现多个定时器互连(可以用1个定时器控制另外一个定时器)的同步电路。
- 发生如下事件时产生中断/DMA请求:
- 更新:计数器向上溢出/向下溢出,计数器初始化(通过软件或者内部/外部触发)
- 触发事件(计数器启动、停止、初始化或者由内部/外部触发计数)
- 输入捕获
- 输出比较
- 支持针对定位的增量(正交)编码器和霍尔传感器电路
- 触发输入作为外部时钟或者按周期的电流管理
三、通用定时器结构框图
- 向上箭头: 可以产生中断,DMA输出
- 向下箭头: 产生事件
部分1:时钟源选择
- 时钟来源:
- 内部时钟(CK_INT)
- 外部输入引脚(外部时钟模式1):TI1、TI2、TI3
- 外部触发输入引脚(外部时钟模式2):TIMx_ETR
- 内部触发输入:ITR0、ITR1、ITR2、ITR3
- 时钟来源最终都是流向预分频器的前端,前端经过预分频器分频之后,得到了CK_CNT(定时器计数时钟),由CNT进行计数。
- 使用的较多的是CK_INT(内部时钟)作为时钟源,它是由APB1时钟倍频得到,时钟为72MHz,再经过PSC分频得到时钟
- 通用定时器和基本定时器的时钟都来自于APB1,高级定时器的时钟来自与APB2
部分2:控制器部分
- 控制器部分包括: 触发控制器、从模式控制器、编码器接口
- 触发控制器: 用来针对片内外设输出触发信号的
- 从模式控制器: 复位、使能、向上/向下、计数
- 编码器接口: 针对于编码器计数,通用定时器支持正交编码器和霍尔传感器电路,通过编码器接口实现计数
部分3:时基单元
- 时基单元: 自动重装在寄存器,PSC预分频器,计数器CNT,都是16位的。
- PSC分频系数: 1~65536
- 从模式控制寄存器具有缓冲功能,预分频器可实现实时更改,新的预分频器的值会等到下一次更新事件发生时被采用。
- 例:当前预分频器的值是2,修改为3,3存储在缓存器当中,要更新到预分频器中要等到计数器计数溢出。
- CNT计数方式: 向上计数,向下计数,向上/向下计数(中心对齐计数)。
- 向上计数模式: 计数器从0计数到自动加载值,然后从0重新开始计数并产生一个计数器溢出事件;每来一个CK_CNT脉冲,比如来一个上升沿就加1。例如:重装载值是100,CNT计数到100,产生向上溢出。然后自动从0开始进行计数。
- 向下计数模式: 计数器从自动装入值开始向下计数到0,然后从自动装入的值重新开始并产生一个计数器向下溢出事件;
- 向上/向下计数(中心对齐计数): 计数器从0开始计数到自动装入的值-1,产生一个计数器溢出事件,然后向下计数到1并且产生一个计数器溢出事件;然后再从0开始重新计数。
- 例如:自动重载值是100,它将从0计数到99,产生一个上溢事件,然后从100开始向下计数,减到1时,然后产生一个下溢事件,然后再从0开始向上计数……
影子寄存器
- 例如:自动重载值是100,它将从0计数到99,产生一个上溢事件,然后从100开始向下计数,减到1时,然后产生一个下溢事件,然后再从0开始向上计数……
- 影子寄存器在图中没有看到,其实它在自动重装载寄存器和CNT计数器之间的位置,通过某个寄存器相应的位设置是否使用寄存器,当使用影子寄存器后,自动重装载寄存器的值首先会更新到影子寄存器当中,然后由影子寄存器再传递给CNT计数器。如果不使用影子寄存器,那么自动重装载寄存器的值会直接传递给CNT计数器。
部分4:输入捕获
- 输入捕获可以对输入信号的上升沿、下降沿、双边沿(上升下降沿)进行捕获。可用来测量信号的脉宽,或测量PWM的频率、占空比
- 在输入捕获模式下,当我们相应的捕获通道(IC1,IC2,IC3,IC4)检测到跳变沿(设置的边沿信号,如上升沿检测)之后,就会将CNT计数器的值锁定到捕获/比较寄存器当中,发生捕获事件后,如果你配置了中断也会相应的产生一些中断。
- 输入捕获由5部分构成
- 通用定时器的输入通道有4个,每一个通道都对应一个引脚。将4个通道称为TI1,TI2,TI3,TI4
- 如果输入的信号有高频干扰,那么就需要用输入滤波器的功能。
- 边缘检测器是用来设置信号在捕获的时候选择哪种边沿有效。
- 4个捕获通道一一对应4个捕获比较寄存器,捕获比较寄存器的值是由CNT传递。
- 当发生捕获的时候,CNT的值马上就会锁定到捕获比较寄存器当中,记录起始值,然后读取这个值,等到下一次捕获的时候再锁定一个值,两个值一比较,就可以计算出中间有多少个时钟脉冲,从而得到时间。
- 输入通道是用来输入信号的,外部信号可通过输入通道传递进来。捕获通道,用来捕获输入信号的通道。一个输入通道可以同时输入给两个捕获通道。TI1,TI2只能到IC1和IC2。TI3,TI4只能到IC3和IC4。
- 预分频器用来决定产生多少个事件的时候进行一次捕获,例如将预分频器设置为2,那么产生两个上升沿信号才进行一次捕获。
- 当发生第一次捕获的时候,计数器的值就会锁定到捕获比较寄存器当中,此时还会产生一个中断,当我们用软件去读取输入比较寄存器的值的时候,它就会自动清0,然后下一次捕获的值又会存入寄存器当中。
部分5:输出比较
- 通用定时器的外部引脚对外输出的控制信号,有4个输出引脚,和输入引脚是一样的,都是同一个引脚,输出的时候用的是比较寄存器。
- 输出寄存器的值和比较寄存器的值相等的时候,输出信号
四、通用定时器配置步骤
定时器相关库函数在 stm32f10x_tim.c 和 stm32f10x_tim.h 文件中。
- 使能定时器时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4,ENABLE);
- 初始化定时器参数,包含自动重装值,分频系数,计数方式等
void TIM_TimeBaseInit(TIM_TypeDef* TIMx,TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct); typedef struct { uint16_t TIM_Prescaler; // 定时器预分频器 uint16_t TIM_CounterMode; // 计数器模式 uint32_t TIM_Period; // 自动重装值 uint16_t TIM_ClockDivision; // 时钟分频因子,是否将时钟分频,例如将APB1分频,一般选择不分频,分片一般使用PSC uint8_t TIM_RepetitionCounter; // 重复计数器,高级定时器使用 } TIM_TimeBaseInitTypeDef;
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
// 自动重装载值设为1000
TIM_TimeBaseInitStructure.TIM_Period=1000;
// 预分频器:35999,即36000-1,其实是分频36000
TIM_TimeBaseInitStructure.TIM_Prescaler=35999;
// 时钟分频因子设置为1,即不分频
TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1;
// 计数方式:向上计数
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM4,&TIM_TimeBaseInitStructure);
- 预分频器-1: 预分频器相当于除数,除数不能为0,在芯片结构设计中会自动+1,所以预分频器为35999,即36000-1。
- 定时时间计算: Tout= ((per)*(psc+1))/Tclk;
- 时钟是72M,72000KHz,分频数:36000,72000/36000=2K=0.5ms,即计数一次是0.5ms,设置的重装载值是1000,即500ms,如果配置中断,即500ms产生一个中断。
- 根据定时器时钟的频率,比如时钟的频率是72MHZ,可以理解为一秒钟STM32会自己数72M次,预分频系数就是将频率分割,比如分频系数是72,则该时钟的频率会变成72MHZ/72=1MHZ,但是在设置的时候要注意,数值应该是72-1。假定分频系数是72-1,那么频率变成1MHZ,也就意味着STM32在一秒钟会数1M次,即1us数一次。
- 设置定时器中断类型,并使能 // 参数1:选择哪一个定时器;参数2:选择定时器中断类型(更新中断,触发中断,输入捕获中断);参数3:使能/失能定时器中断
void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT,FunctionalState NewState)
- 设置定时器中断优先级(NVIC),使能定时器中断通道
- 开启定时器 // 参数1:选择哪一个定时器;参数2:使能/失能定时器
void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState)
- 编写定时器中断服务函数(名称是固定的,在启动文件中去找)
TIM4_IRQHandler // 判断中断类型 ITStatus TIM_GetITStatus(TIM_TypeDef* TIMx, uint16_t TIM_IT); // 得到中断状态标志,判断标志是否置1。参数1:TIMx;参数2:中断类型 if(TIM_GetITStatus(TIM4,TIM_IT_Update)) { ...//执行TIM4更新中断内控制 } // 清除状态标志 void TIM_ClearITPendingBit(TIM_TypeDef* TIMx, uint16_t TIM_IT);
五、硬件设计
- 两个指示灯DS0和DS1,分别接在PB5和PE5
- 发光二极管的阳极连接一个限流电阻,连接到3.3V,阴极连接到一个GPIO,输出低电平,这个时候发光二极管导通发光,要让他熄灭,就给它一个高电平,阴极和阳极的电平一样,所以它会熄灭
六、软件设计
本实验所要实现的功能是:通过 TIM4 的更新中断控制 DS1 指示灯间隔 500ms 秒状态取反,主函数控制 DS0 指示灯不断闪烁。程序框架下:
- 初始化TIM4,并使能更新中断等
- 编写TIM4中断函数
- 编写主函数
- time.h 是C标准库的一部分,定义了与时间相关的函数和数据结构,如 time()、localtime()、struct tm 等。如果自定义 time.h,可能会与标准库中的同名头文件冲突,导致编译错误或运行时错误 。
TIMER.c
#include "TIMER.h"
#include "LED.h"
void TIM4_Init(u16 per, u16 psc)
{
// 定义结构体
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
// 开启定时器4的时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);
// 设置重装载值
TIM_TimeBaseInitStructure.TIM_Period = per;
// 设置预分频器
TIM_TimeBaseInitStructure.TIM_Prescaler = psc;
// 设置时钟分频因子,没有对它进行分频,所以是1,有1,2,4分频
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
// 设置计数模式
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM4, &TIM_TimeBaseInitStructure);
// 定时器中断配置,参数2:更新中断
TIM_ITConfig(TIM4, TIM_IT_Update, ENABLE);
// 清除中断标志,参数1:定时器选择,参数2:中断类型
TIM_ClearITPendingBit(TIM4, TIM_IT_Update);
// 配置中断
NVIC_InitStructure.NVIC_IRQChannel = TIM4_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// 开启定时器
TIM_Cmd(TIM4, ENABLE);
}
void TIM4_IRQHandler(void)
{
if(TIM_GetITStatus(TIM·4, TIM_IT_Update) == SET)
{
LED2 = !LED2;
}
// 清除状态标志
TIM_ClearITPendingBit(TIM4, TIM_IT_Update);
}
TIMER.h
#ifndef __TIMER_H
#define __TIMER_H
#include "system.h"
void TIM4_Init(u16 per, u16 psc);
void TIM4_IRQnHandler(void);
#endif
main.c
#include "LED.h" // 包含 LED 模块的头文件
#include "system.h"
#include "SysTick.h"
#include "TIMER.h"
// 主函数
int main()
{
u8 i = 0; //让LED1指示灯闪烁,确保程序在执行
SysTick_Init(72);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
LED_Init(); // 初始化 LED1 和 LED2
TIM4_Init(1000, 36000 - 1);
// 主循环
while(1)
{
i ++;
if(i % 20 == 0) // 每经过200ms,让LED1翻转一次
LED1 = !LED1;
delay_ms(10);
}
}