定时器分类
STM32F1中除了互联型产品(STM32F103C8T6为64KB Flash 中容量产品),其余有8个定时器。
可以8个定时器分为高级,通用,基本三种。
高级定时器有两个,分别是TIM1和TIM8。
通用定时器有四个,是TIMx(x=2~4)
基本定时器有两个,是TIMx(x=6~7)
功能上高级>通用>基本
不过高级定时器一般也用不着,如果只是普通的计时的话基本定时器也就够用了,但是基本定时器没有输出比较,没法实现PWM(脉冲宽度调制),因此我们主要使用的是通用定时器,并且STM32F103中通用定时器的资源也是最多的。
下图截自《ARM Cortex-M3嵌入式原理及应用(基于STM32F103微控制器)》的第150页。
通用定时器
下图截自《STM32F10xxx参考手册(中文)》第253页
不太严谨地介绍一下定时器的工作流程。时钟源每传来n(n=预分频器的值)次脉冲,都会使得计数器工作一次。如果选择的是向上计数模式,那么计数器每工作一次都会使得计数器的值+1,直到值等于了自动重装载寄存器的值,那么触发一次中断,并且计数器清零。
如果选择的是向下计数模式,那么计数器每工作一次都会使得计数器的值-1,直到值等于0,那么触发一次中断,并且计数器的值变为自动重装载寄存器的值。
如果选择向上向下计数模式,那么计数器工作一次会使得计数器的值+1或者-1,计数器的值不会被重置,会一直在0和自动重装载寄存器的值之间上下跳动。
一般情况下这三种工作模式对于计数的效果都是一样的,但是当我们需要利用到计数器里的值的时候就有差别了(例如PWM),通常我们使用的是向上计数模式,这也比较符合我们的直觉。
上述的介绍中提到的是以下三个寄存器。
可以看出它们都是只有16位,也就是说可以设置的最大数值为2^16-1(65535)。
并且从工作流程中也可以得知,定时器溢出的频率公式为
时钟源频率/(自动重装载寄存器的值+1)/(预分频器的值+1)=CK_INT/(ARR+1)/(PSC+1)
PWM脉冲宽度调制
STM32F103中每个通用定时器都可以输出4路PWM,而每个高级定时器都可以输出7路PWM,所以理论上,STM32最多是可以同时产生30路PWM。
PWM简单来说就是一段有高电平也有低电平的脉冲信号。
我们一般点亮一个LED灯,就是把LED灯的短脚接地,然后通过GPIO口从LED灯的长脚传入高电平,我们假设此刻LED的亮度为100。如果我们改用PWM来代替原本的高电平,并且PWM传入的脉冲信号中高低电平各占50%,那么此刻LED的亮度就为50%了,因为人眼大概只能接受每秒25帧左右的图像,因此只要PWM够快,那么我们就看不出LED实际上是亮灭亮灭的,由于人眼的残留影像,我们看到的只是LED灯变得暗了些。
之所以把PWM和定时器放在一起,是因为定时器的硬件中就含有捕获比较寄存器。
我们可以给这个寄存器设置一个阈值,当定时器的计数器的值小于这个阈值的时候我们就输出高电平,反之输出低电平(具体看选择的PWM输出模式)。
综上我们可以得知,PWM输出的频率等于定时器的溢出中断频率。
固件库函数
定时器相关的函数还是非常多的,接下来就以计数1ms为目的来说明相关函数。
首先需要打开相关定时器的外设时钟,不过不属于定时器库函数里的,就不介绍。
下一步选择时钟源,默认是使用内部时钟源(72MHz),所以也可以不指定。
然后初始化时基单元(配置自动重装载寄存器,预分频器,时钟分频,计数模式)
接着是中断使能以及配置NVCI相关的中断优先级
最后是使能计数器就可以开始计数了。
TIM_InternalClockConfig
上面三个函数是常用的用来指定时钟源的函数。
第一个也是默认的,使用的是内部时钟作为时钟源。
第二个可以将其他定时器的溢出信号作为时钟源,也就是两个定时器联动,这样可以将最大的计时数平方一次,也就是可以记更久的时间,不过我们也是可以使用软件来完成这种效果的。
第三个可以将时钟源接到外部的时钟。
TIM_TimeBaseInit
初始化时基单元,参数一指定定时器资源,参数二传入一个TIM_TimeBaseInitTyepDef类型的结构体变量用于初始化配置。
TIM_Prescale:预分频器的值,0~65535。
TIM_CounteMode:计数器模式,一般使用向上计数TIM_CounterMode_Up
TIM_Period:自动重装载计数器的值,0~65535。
TIM_ClockDivision:时钟分频,TIM_CKD_DIV1。
TIM_RepetitionCounter:重复计数器的值,只有高级定时器才用的到,基本定时器和通用定时器随便给个0就行。
TIM_ITConfig
中断使能,参数一选择定时器资源,参数二选择中断源,参数三给个ENABLE。
中断源一般就指定第一个TIM_IT_Update
这样每计数一轮都会触发一次中断。
TIM_Cmd
使能计数器。
使能之后计数器就开始运作了,每次记满一轮之后就会进入一次中断,我们可以从“startup_stm32f10x_md.s”中找到对应的中断函数。
由于可进入这个中断函数的中断源有多个(参考上面中断使能的参数表),因为我们需要查询中断标志位是否是我们所规定的TIM_IT_Update触发的,是的话我们还需要手动把标志位清除(其他中断源也是一样)。
TIM_GetITStatus
查询中断标志位,参数二给的一样的TIM_IT_Update。
TIM_ClearITPendingBit
清除中断标志位,参数二给的一样的TIM_IT_Update。
上面的函数就是用来计数用的,接下来就是如何输出PWM的了。
把上面计数的流程走一遍,然后把中断相关的都删掉,再加上配置输出比较单元以及初始化GPIO口的函数即可。
TIM_OC1Init
配置通道1的输出比较单元。这里需要注意的是不同定时器资源的不同通道对应的GPIO输出口是不一样的,TIM2的通道1对应的是GPIOA的0号引脚,具体可以查询引脚定义表。并且需要配置为复用推挽输出模式GPIO_Mode_AF_PP。
参数一指定定时器资源,参数二传入TIM_OCInitTypeDef类型的参数。
一共有八个成员变量,但是我们只需要用到我圈起来的4个,其余为高级定时器使用的。
TIM_OCMode:设置输出比较的模式,一般我们选择TIM_OCMode_PWM1,在这个模式下,当计数器的值小于阈值时输出设置的极性(下面会设置),反之输出相反的极性。
TIM_OCPolarity:输出比较的极性,这个根据需求来设置,设置为高电平的参数为TIM_OCPolarity_High。
TIM_OutputState:设置输出使能,TIM_OutputState_Enable。
TIM_Pulse:设置阈值,0~65535。因此我们定时器的自动重装载寄存器的值最好不要超过这个阈值,并且由于容易计算PWM的占空比,一般我会把自动重装载寄存器的值设为10的倍数。
以上就足够我们输出PWM了,如果我们需要实现呼吸灯的效果,那么还需要动态地调整PWM的阈值。
TIM_SetCompare1
设置通道1的输出比较阈值。
参数一指定定时器资源,参数二重新设置阈值大小。
通用定时器计数1ms代码
#include "stm32f10x.h" // Device header
#include "OLED.h"
uint32_t count=0;
int main(void){
OLED_Init();
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); //打开TIM2的外设时钟
TIM_InternalClockConfig(TIM2); //选择内部时钟(72MHz)作为时钟源
TIM_TimeBaseInitTypeDef itd;
itd.TIM_ClockDivision=TIM_CKD_DIV1; //时钟1分频
itd.TIM_CounterMode=TIM_CounterMode_Up; //向上计数模式
itd.TIM_Period=1000-1; //设置自动重装器的值
itd.TIM_Prescaler=72-1; //设置预分频器的值
//TIM2的计数器溢出频率(每秒中断次数)为
//(72MHz/1(1分频)/(1000-1+1)(自动重装器的值+1)/(72-1+1)(预分频器的值+1))=1000(ms级,即1ms溢出一次)
itd.TIM_RepetitionCounter=0; //重复计数器的值,但是仅高级定时器有效
TIM_TimeBaseInit(TIM2,&itd);
TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE); //开启定时器中断,TIM为中断源
//配置中断优先级
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //抢占优先级和响应优先级各占两位
NVIC_InitTypeDef itd1;
itd1.NVIC_IRQChannel=TIM2_IRQn; //指定TIM2的中断通道
itd1.NVIC_IRQChannelCmd=ENABLE;
itd1.NVIC_IRQChannelPreemptionPriority=2; //抢占优先级为2
itd1.NVIC_IRQChannelSubPriority=2; //响应优先级为2
NVIC_Init(&itd1);
TIM_Cmd(TIM2,ENABLE); //使能定时器
while(1){
OLED_ShowNum(2,1,count,6);
}
}
void TIM2_IRQHandler(void){
if(TIM_GetITStatus(TIM2,TIM_FLAG_Update)){ //判断是否是TIM中断源引发的中断
++count;
TIM_ClearITPendingBit(TIM2,TIM_FLAG_Update); //清除标志位
}
}
效果
通用定时器使用PWM实现LED呼吸灯
#include "stm32f10x.h" // Device header
#include "Delay.h"
int main(void){
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); //打开TIM2的外设时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef gitd;
gitd.GPIO_Mode=GPIO_Mode_AF_PP; //配置为复用推挽输出
gitd.GPIO_Pin=GPIO_Pin_0;
gitd.GPIO_Speed=GPIO_Speed_2MHz;
GPIO_Init(GPIOA,&gitd);
TIM_InternalClockConfig(TIM2); //选择内部时钟(72MHz)作为时钟源
TIM_TimeBaseInitTypeDef itd;
itd.TIM_ClockDivision=TIM_CKD_DIV1; //时钟1分频
itd.TIM_CounterMode=TIM_CounterMode_Up; //向上计数模式
itd.TIM_Period=100-1; //设置自动重装器的值
itd.TIM_Prescaler=72-1; //设置预分频器的值
itd.TIM_RepetitionCounter=0; //重复计数器的值,但是仅高级定时器有效
TIM_TimeBaseInit(TIM2,&itd);
TIM_OCInitTypeDef itd1;
itd1.TIM_OCMode = TIM_OCMode_PWM1; //比较输出模式为PWM1
itd1.TIM_OCPolarity = TIM_OCPolarity_High; //输出极性为高电平
itd1.TIM_OutputState=TIM_OutputState_Enable; //使能
itd1.TIM_Pulse=0; //初始化输出比较的阈值
TIM_OC1Init(TIM2,&itd1);
TIM_Cmd(TIM2,ENABLE); //使能定时器
while(1){
for(int i=0;i<=100;++i){
TIM_SetCompare1(TIM2,i);
Delay_ms(10);
}
for(int i=100;i>=0;--i){
TIM_SetCompare1(TIM2,i);
Delay_ms(10);
}
}
}
效果
参考
《STM32F10xxx参考手册(中文)》
《STM32F103xx固件函数库用户手册》
b站江科大自化协
《STM32库开发实战指南(基于STM32F103)》
《ARM Cortex-M3嵌入式原理及应用(基于STM32F103微控制器)》