STM32定时器

摘要

最近学习了STM32的定时器部分,为了加强对定时器的理解,将学习的内容进行记录和总结。下面的笔记将从定时器结构、定时器中断、定时器输出比较和输入捕获四部分展开记录。

定时器结构

STM32的定时器有三类,分别是基本定时器、通用定时器和高级定时器,后者包含前者,结构逐渐复杂。通用定时器包含基本定时器的全部功能,而高级定时器又包含通用定时器的全部功能。STM32F10x系列最多有8个定时器,具体的定时器数量根据型号的不同而定,以STM32F103C8T6芯片为例,有1个高级定时器TIM1,3个通用定时器TIM2、TIM3、TIM4。可以理解为根据不同的定位对外设资源进行增加或减少而得到不同型号的芯片。

基本定时器结构

基本定时器的结构框图如图所示。

从图中可以看出,来自RCC时钟树的内部时钟CK_INT经过触发器后进入预分频器,再进入CNT计数器进行向上计数。当CNT计数器到达指定的计数值时(计数值溢出)可以产生一个更新事件UI(update Interrupt),并且产生更新中断。此外,该更新事件也可以映射到触发控制器的TRGO,再由TRGO触发其他外设,实现全硬件自动执行而不需要软件干预。自动重装载寄存器保存着计数器CNT的溢出值,当CNT计数值到底自动重装载寄存器的值时,就会产生更新事件,同时计数器重新从0开始计数。
总结该框图,基本定时器就是以由预分频器、计数器和重装载寄存器组成的时基单元(图中蓝色方框部分)为核心,再加上时钟输入电路、触发控制器等小组件组成。

通用定时器结构

通用定时器可以认为是在基本定时器的基础上加入了输入捕获模块、输出比较模块以及其他功能电路而得到的功能更复杂、更全面的定时器,其基本结构框图如下。

从结构框图可以看出,通用定时器有四个输入捕获/输出引脚TIMx_CH1、TIMx_CH2、TIMx_CH3和TIMx_CH4,当定时器使用输入捕获功能时,四个引脚需要被配置为输入,当定时器使用输出比较功能向外输出电平信息时,这四个引脚需要被配置为复用输出功能。此外还有一个外部输入引脚TIMX_ETR,可以作为外部时钟输入,用于对外部引脚电平跳变进行计次。有些通用定时器可能没有外部输入引脚TIMx_ETR,这是因为芯片厂商在设计某块芯片时外设上做了一定的裁剪。通用定时器根据功能可以配置为基本定时器、输入捕获和输出比较三种模式,并且相比基本定时器其还增加了编码器接口,可用于测量编码器信号。

高级定时器结构

高级定时器拥有通用定时器全部功能,并且额外拥有重复计数器、死区生成、互补输入、刹车输入等功能,可以理解为在通用定时器的结构上又增加了一些功能电路,其结构框图如下。

观察高级定时器结构框图,其比较输出通道1和通道2还增加了两个互补输出引脚TIMx_CH1N和TIMx_CH2N,其作用是输出与TIMx_CH1和TIMx_CH2相反的电平,通常用于复杂电机的控制。

定时器功能

基本定时器功能

基本定时器可以实现计时、定时器中断等功能,定时器又可以分为内部时钟中断和外部时钟中断。定时中断的基本结构如图所示:

该结构体以时基单元为核心,前面是时基单元的时钟输入相关模块,后面是时基单元通向的中断控制模块。因此在进行定时器中断的配置时,实际上就是将各个模块之间的通信链路(电路)连接起来。其实芯片上任何外设的初始化配置都是这么个原理,就是把CPU和外设以及各个外设之间的链路连接起来打通。因为芯片上面集成了很多晶体管,相当于有很多开关,通过不同的开关组合,就可以得到不同的链路,从而实现不同的功能。

定时器内部时钟中断

根据前面定时器定时中断的基本结构图,可以总结得到定时器内部时钟中断的配置流程如下:
1.开启定时器模块时钟
2.选择内部时钟作为定时器时钟输入
3.配置时基单元
4.配置定时器中断输出控制
5.配置NVIC中断控制器中有关定时器中断的通信链路
6.开启定时器
7.定时器中断处理函数

以STM32F103C8T6为例,定时器中断初始化的代码如下:

点击查看代码
void timerInit(void) {
	//开启定时器TIM2外设时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
	
	//配置定时器时基单元的输入时钟
	TIM_InternalClockConfig(TIM2);	//配置输入时钟为内部时钟
	
	//配置定时器时基单元
	TIM_TimeBaseInitTypeDef TIM_Structure;
	TIM_Structure.TIM_ClockDivision = TIM_CKD_DIV1;	//用于配置滤波器时钟是否分频,不影响时基单元的功能
	TIM_Structure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式,选择向上计数
	TIM_Structure.TIM_Period =  10000 - 1;	//计数值
	TIM_Structure.TIM_Prescaler = 7200 - 1;	//预分频
	TIM_Structure.TIM_RepetitionCounter = 0; //重复计数器,高级定时器才有的功能
	
	TIM_TimeBaseInit(TIM2, &TIM_Structure);
	
	//定时器中断输出控制配置
	TIM_ClearFlag(TIM2, TIM_FLAG_Update);	//清除更新中断标志位,因为TIM_TimeBaseInit初始化函数的末尾会手动产生一个更新中断标志
	
	TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); //开启TIM2(定时器2)的更新中断
	
	
	//内核中断控制器NVIC配置,连接NVIC和定时器输出控制器的链路
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);	//中断优先级分组
	
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStructure);
	
	//开启定时器
	TIM_Cmd(TIM2, ENABLE);
	
}

示例代码使用了定时器2,并开启了定时器中断,当计数器CNT的计数值溢出时,会产生更新信号,这个更新信号会触发定时器中断,也即将定时器内部更新信号那条线连接到了定时器向外输出的中断信号线,然后这条线又通向NVIC,因此还要配置NVIC中断控制器连接到定时器的中断输出线,可以理解为就是合上了开关使得电路联通了。

当出现定时器中断时,NVIC控制器最终将中断信息发送给CPU,CPU就会去指定的地址执行该地址上的指令,这个指令通常是一条跳转指令,跳转到真正的中断处理函数中,因此需要编写该中断处理函数。通常,芯片厂商给我们写的启动代码里面指定了中断处理函数的名称,因此我们的中断处理函数名称也需要和官方指定的一致。由于这个函数的执行是中断引起的,而中断的产生具有随机性,不好传递参数,因此中断处理函数是无参无返回值的。定时器2的中断处理函数名称是TIM2_IRQHandler

定时器外部时钟中断

定时器也可以使用外部输入信号作为时钟源,此时使用到定时器的外部输入引脚。根据前面定时器定时中断的基本结构图,可以总结得到定时器外部时钟中断的配置流程如下:
1.开启定时器时钟
2.开启GPIO模块时钟
3.配置外部输入引脚ETR
4.选择外部输入时钟作为定时器时钟输入
5.配置时基单元
6.配置定时器中断输出控制
7.配置NVIC中断控制器
8.开启定时器
9.定时器中断处理函数

以STM32F103C8T6为例,定时器外部输入中断初始化的代码如下:

点击查看代码
void timerInit(void) {
	//开启定时器TIM2外设时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
	
	//开启GPIOA控制器时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	
	//PA0初始化为上拉输入
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	//配置定时器时基单元的输入时钟为外部时钟模式2
	TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_Inverted, 0x0F);	//配置输入时钟为外部ETR引脚输入的时钟
	
	//配置定时器时基单元
	TIM_TimeBaseInitTypeDef TIM_Structure;
	TIM_Structure.TIM_ClockDivision = TIM_CKD_DIV1;	//用于配置滤波器时钟是否分频,不影响时基单元的功能
	TIM_Structure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式,选择向上计数
	TIM_Structure.TIM_Period =  10 - 1;
	TIM_Structure.TIM_Prescaler = 1- 1;
	TIM_Structure.TIM_RepetitionCounter = 0; //重复计数器,高级定时器才有的功能
	
	TIM_TimeBaseInit(TIM2, &TIM_Structure);
	
	//定时器中断输出控制配置
	TIM_ClearFlag(TIM2, TIM_FLAG_Update);	//清除更新中断标志位,因为TIM_TimeBaseInit初始化函数的末尾会手动产生一个更新中断标志
	
	TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); //开启TIM2(定时器2)的更新中断
	
	
	//内核中断控制器NVIC配置,连接NVIC和定时器输出控制器的链路
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);	//中断优先级分组
	
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStructure);
	
	//开启定时器
	TIM_Cmd(TIM2, ENABLE);
	
}

uint16_t getTimerCount(void) {
	return TIM_GetCounter(TIM2);
}

示例代码使用了定时器2,并且定时器2的外部时钟输入引脚ETR是A0,因此需要将该引脚初始化为上拉输入,并使用对射式红外传感器模拟外部时钟进行计次,通过读取计数器的当前值就可以得到当前的计数值。需要注意的是,对于STM32F10X,引脚作为输入功能时,会将电平信息传递到所有连接的外设,因此输入是不需要配置为复用功能的,只有输出由外设控制时才需要进行复用输出配置。

定时器输出比较

定时器输出比较通过比较计数器CNT和比较寄存器CCR的值的关系,来对输出电平进行置1、置0或翻转的操作,用于输出一定频率和占空比的PWM信号。对于STM32,每个通用定时器和高级定时器都拥有四个输出比较通道,并且高级定时器的前三个输出比较通道还额外拥有死区生成和互补输出功能。

根据通用定时器的结构框图,定时器输出比较功能的结构图如下。

从图中可以看出,输出比较功能的结构和定时器中断基本类似,只是时基单元后面的中断配置变成了输出比较通道。定时器输出比较功能的配置流程如下:
1.开启定时器时钟
2.开启GPIO模块时钟
3.配置比较输出引脚为复用输出功能,由定时器外设控制引脚的输出
4.选择(配置)定时器输入时钟
5.配置时基单元
6.配置输出比较通道,选择输出比较模式
7.开启定时器

以STM32F103C8T6为例,使用定时器输出比较功能实现输出PWM信号的代码如下:

点击查看代码
void PWM_Init(void) {
	//开启定时器时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
	
	//开启GPIO时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	
	//配置GPIO引脚为复用输出功能,由定时器外设控制引脚输出
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //配置为复用推挽输出,此时引脚电平由外设控制输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	//选择定时器内部时钟
	TIM_InternalClockConfig(TIM2); //选择内部时钟为TIM2的时钟源,72Mhz
	
	//配置定时器时基单元
	TIM_TimeBaseInitTypeDef TIM_InitStructure;
	TIM_InitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
	TIM_InitStructure.TIM_Prescaler = 720 - 1;
	TIM_InitStructure.TIM_Period = 100 - 1;
	TIM_InitStructure.TIM_CounterMode = TIM_CounterMode_Up;
	TIM_InitStructure.TIM_RepetitionCounter = 0;
	
	TIM_TimeBaseInit(TIM2, &TIM_InitStructure);
	
	
	//配置输出比较单元
	TIM_OCInitTypeDef TIM_Structure;
	//如果结构体中没有用到所有的项,最好都初始化赋值一个默认值
	TIM_OCStructInit(&TIM_Structure);	//结构体初始化
	TIM_Structure.TIM_OCMode = TIM_OCMode_PWM1;	//选择输出PWM模式
	TIM_Structure.TIM_OutputState = TIM_OutputState_Enable;	//输出比较使能
	TIM_Structure.TIM_Pulse = 0;	//初始的比较寄存器CCR的值
	TIM_Structure.TIM_OCPolarity = TIM_OCPolarity_High; //输出极性选择为高,计数值小于CCR时输出高电平,大于时输出低电平
	
	TIM_OC1Init(TIM2, &TIM_Structure);
	
	
	//使能定时器
	TIM_Cmd(TIM2, ENABLE);
}


void PWM_SetCompareOC1(uint16_t Compare) {
	TIM_SetCompare1(TIM2, Compare);	//设置CCR寄存器的值
}

示例代码使用了定时器2的输出比较通道1,对应的输出引脚是A0,因此需要将该引脚配置为复用输出以便由定时器的输出比较通道控制A0引脚电平。在配置输出比较通道时,初始时设置/捕获比较寄存器的值为0,后续可以通过修改CCR寄存器的值来实现输出不同占空比的PWM波形。

定时器输入捕获

定时器输入捕获可用于测量PWM信号的频率、占空比、脉冲间隔、计数、编码器转速等。在输入捕获模式下,当通道输入引脚出现指定电平跳变时,当前的CNT值会被锁存到捕获/输出比较寄存器CCR中,实现硬件自动执行,相比使用中断并在中断处理函数中计数更为精准。
定时器输入捕获模式的结构框图如下。

根据上图,在输入捕获模式下还可以选择触发源和从模式来让定时器硬件在指定信号下自动运行而不需要程序干预。在STM32F103C8T6中,定时器可选择的触发源和从模式如下图。

其中ITRx(x=0,1,2,3)、TI1F_ED、TI1FP1、TI2FP2、ETRF是可选择的触发源,其实就是将对应信号映射到TRGI触发信号,也即将触发源信号的线路链接到TRGI信号线,然后TRGI信号线去触发选中的从模式。因此也需要将TRGI信号链接到对应的从模式控制器。可选择的从模式有关闭、编码器、复位等。

基于定时器的输入捕获结构和从模式功能进行PWM波形的频率和占空比测量的结构图如下(此图来自江协科技PPT)。

根据上图,使用定时器输入捕获功能和从模式触发来测量PWM波形频率和占空比的配置流程如下:
1.开启定时器时钟
2.开启GPIO模块时钟
3.配置输入捕获引脚
4.选择内部时钟源作为定时器时钟
5.配置时基单元以设置基准时钟频率
6.配置输入捕获模块
7.选择触发源、从模式
8.开启定时器
9.读取捕获/比较寄存器以获取计数值从而实现PWM频率计算

以STM32F103C8T6为例,使用定时器输入捕获和从模式功能实现PWM频率和占空比测量的代码如下:

点击查看代码
#include "stm32f10x.h"
#include "inputCapture.h"

/*
	*定时器3输入捕获功能初始化
*/
void TIM_InputCaptureInit(void) {
	//开启TIM3时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
	
	//开启GPIO时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	
	//初始化TIM3输入通道1所使用的GPIO引脚
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	
	//配置定时器时钟源
	TIM_InternalClockConfig(TIM3);	//选择内部72MHz频率
	
	//配置定时器时基单元
	TIM_TimeBaseInitTypeDef TIM_InitStructure;
	TIM_InitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //该配置项是用于配置外部时钟输入时的预分频
	TIM_InitStructure.TIM_CounterMode = TIM_CounterMode_Up;
	TIM_InitStructure.TIM_Prescaler = 72 - 1;		//该参数是对输入时基单元的时钟的预分频
	TIM_InitStructure.TIM_Period = 65536 - 1;
	TIM_InitStructure.TIM_RepetitionCounter = 0;	//高级定时器才有的配置项
	
	TIM_TimeBaseInit(TIM3, &TIM_InitStructure);
	
	//定时器输入捕获模块初始化
	//通道1初始化
	TIM_ICInitTypeDef TIM_ICInitStruct;
	TIM_ICInitStruct.TIM_Channel = TIM_Channel_1;
	TIM_ICInitStruct.TIM_ICFilter = 0xF;
	TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Rising;
	TIM_ICInitStruct.TIM_ICPrescaler = TIM_ICPSC_DIV1;
	TIM_ICInitStruct.TIM_ICSelection = TIM_ICSelection_DirectTI;
	TIM_ICInit(TIM3, &TIM_ICInitStruct);
	
	//通道2初始化
	TIM_ICInitStruct.TIM_Channel = TIM_Channel_2;
	TIM_ICInitStruct.TIM_ICFilter = 0xF;
	TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Falling;
	TIM_ICInitStruct.TIM_ICPrescaler = TIM_ICPSC_DIV1;
	TIM_ICInitStruct.TIM_ICSelection = TIM_ICSelection_IndirectTI;
	TIM_ICInit(TIM3, &TIM_ICInitStruct);
	
	/*
		*选择从模式触发源及需要触发的模式(外设/功能)
		*分为触发源选择和触发模式选择两步
	*/
	//选择连接到TRGI的触发源
	TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);
	
	//选择TRGI信号的触发模式
	TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);
	
	//开启定时器使能
	TIM_Cmd(TIM3, ENABLE);
}

/*
	*使用测周法获取频率
	*频率=fc/(N+1)
*/
uint32_t getFrequency(void) {
	return FC / (TIM_GetCapture1(TIM3) + 1);
}

/*
	*计算占空比
	*占空比=((TIM_GetCapture2(TIM3) + 1)) / (TIM_GetCapture1(TIM3) + 1);
	*由于占空比是小数,因此分子乘100,这样计算得到的就是百分比
*/
uint32_t getDuty(void) {
	//
	return ((TIM_GetCapture2(TIM3) + 1) * 100) / (TIM_GetCapture1(TIM3) + 1);
}

示例代码使用了定时器3的输入捕获通道1,对应的引脚为A6,因此需要将该引脚配置为输入模式。时基单元的输入时钟选择内部时钟,并设置预分频值为72-1,这样计数器就是以1Mhz的时钟在进行计数。对输入捕获通道1和通道2分别进行初始化,其中通道1设置输入信号的上升沿触发捕获,并选择TI1FP1为触发源,触发时从模式使用复位模式让计数器重置,从0开始重新计数;通道2的输入捕获信号选择通道1的TI1FP2信号,并选择下降触发。这样通道1就可以用于计算PWM波形的周期,通道2用于计算PWM波形的占空比。

总结

定时器是芯片中非常重要的外设,应用非常广泛,比如时间基准、PWM生成、时序控制、操作系统进程切换、信号测量等都需要用到定时器。不同芯片的定时器外设可能存在差别,但是基本的结构大同小异,核心就在于时基单元,在此基础上加入外部输入、捕获、比较输出、中断触发、事件触发等功能电路。

对于嵌入式开发,对芯片上的外设基本结构进行一定的了解/理解是很重要的,这可以帮助我们理清芯片上外设的工作逻辑及各个外设之间的交互关系,帮助我们更好地写程序。芯片内部其实就是由一系列地晶体管和电子元件组成,这些晶体管就像一个个开关,通过不同的开关组合,形成不同的电路通路,使得外设与CPU、外设与外设之间形成通路,从而实现功能。因此嵌入式开发在配置外设时,就像是在组合一系列的开关,将需要的链路一条条打通,而这些组合开关的控制就是通过配置寄存器中某些位来实现。理解了这一点,也就理解了嵌入式开发中为什么需要配置寄存器以及寄存器的作用是什么。说白了,就是控制电路或储存数据。当然了,这里的寄存器是指外设寄存器而不是内核中CPU的那些通用寄存器,这些外设寄存器本质上其实就是内存。

posted @ 2025-06-13 11:37  tstars  阅读(172)  评论(0)    收藏  举报