STM32的定时器真心强大,它共有8个16位定时器,分别为TIM0~TIM7。其中TIM6、TIM7是基本定时器,TIM2、TIM3、TIM4和TIM5是通用定时器,而TIM1和TIM8是高级定时器。这些定时器使STM32具有定时、信号的频率测量、信号的PWM测量、PWM输出、三相6步电机控制及编码器接口等功能,都是专门为工控领域量身定做的(引用自《STM32库开发实战指南》)。
=================阶段一:基本定时器触发中断==============
基本定时器只具备基本的定时功能,也就是在时钟源的驱动下,从0开始累加脉冲计数,直到超过预定值,然后触发中断或者触发DMA请求。基本定时器和通用定时器的时钟源都是TIMxCLK,TIMxCLK在时钟树中的位置如下:
当APB1的预分频系数为1时,则TIMxCLK就是APB1的频率(也等于AHB的频率);而当APB1的预分频系数不为1时,则TIMxCLK就是APB1的频率的2倍。比如在常见的配置中,AHB=72Mhz,而APB1二分频为36Mhz,那么TIMxCLK就为36Mhz*2=72Mhz。
TIMxCLK的脉冲还会再经过一道分频,然后被TIMx_CNT寄存器累加计数。这个分频器叫做PSC预分频器。PSC预分频器可以设置为任意16位值。当TIMx_CNT的值等于TIMx_ARR寄存器中的值时,产生溢出事件,可用于触发中断。
本节要做一个实验,就是通过基本定时器来进行精确延时,以使LED灯每1s翻转一次。其实该实验类似于《STM32F10x系列SysTick(系统滴答定时器)的使用》中的实验,只不过SysTick是所有ARM处理器都有的部件,而本实验的定时器是STM32独有的东西。
由于我手头的STM32F103C8T6的容量是64KB,属于中等密度产品,所以只有1个高级定时器TIM1和3个通用定时器TIM2、TIM3和TIM4,没有基本定时器。其实通用定时器拥有基本定时的所有功能,所以这个实验就用TIM3来演示。
实验代码如下:
#include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h" #include "stm32f10x_tim.h" #include "misc.h" void GPIO_Conf() { GPIO_InitTypeDef t_gpio; //开启GPIOA的时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //设置PA1为推挽输出 t_gpio.GPIO_Pin=GPIO_Pin_1; t_gpio.GPIO_Mode=GPIO_Mode_Out_PP; t_gpio.GPIO_Speed=GPIO_Speed_10MHz; GPIO_Init(GPIOA,&t_gpio); } void TIM_Conf() { TIM_TimeBaseInitTypeDef t_timebase; //开启TIM3时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE); //对TIM3CLK进行10000分频,72Mhz/10000=7200hz t_timebase.TIM_Prescaler=10000-1; //周期为7200个脉冲,则一次溢出需要7200/7200Hz=1s t_timebase.TIM_Period=7200-1; //数字滤波器不分频 t_timebase.TIM_ClockDivision=TIM_CKD_DIV1; //向上计数 t_timebase.TIM_CounterMode=TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3,&t_timebase); //自动重装 TIM_ARRPreloadConfig(TIM3,ENABLE); //清空中断标志 TIM_ClearITPendingBit(TIM3,TIM_IT_Update); //启用中断 TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE); //启用TIM3 TIM_Cmd(TIM3,ENABLE); } void NVIC_Conf() { NVIC_InitTypeDef t_nvic; //优先级组为1 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); //中断向量为TIM3_IRQn t_nvic.NVIC_IRQChannel=TIM3_IRQn; //中断优先级 t_nvic.NVIC_IRQChannelPreemptionPriority=0; t_nvic.NVIC_IRQChannelSubPriority=1; //启用中断 t_nvic.NVIC_IRQChannelCmd=ENABLE; NVIC_Init(&t_nvic); } uint8_t g_state=0; void TIM3_IRQHandler(void) { //清空中断标志位 TIM_ClearITPendingBit(TIM3,TIM_IT_Update); //设置引脚电位 GPIO_WriteBit(GPIOA,GPIO_Pin_1,g_state); //翻转 g_state=!g_state; } int main() { GPIO_Conf(); TIM_Conf(); NVIC_Conf(); while(1); }
代码中,首先把PA1设置为推挽输出模式,然后配置TIM3定时器每1s触发一次中断,并在中断中翻转PA1电平。实验结果就是LED灯闪烁。
===============阶段二:通用定时器输出PWM波=============
通过上面的例子已经得知,在脉冲的驱动下,TIMx_CNT会从0开始累加,直到等于TIMx_ARR寄存器中的值,然后重新归0。那么,如果再引入一个寄存器TIMx_CCR,当TIMx_CNT中的值小于TIMx_CCR中的值时,就输出某种电平,而当TIMx_CNT中的值大于(或等于)TIMx_CCR中值时,就输出另一种电平,那么不就能周而复始地输出稳定的PWM波了吗?这就是通用定时器输出PWM波的原理。
至于当TIMx_CNT的值小于TIMx_CCR中的值时,输出的是高电平还是低电平,这个由“极性”决定。
由此可知,通用寄存器输出PWM的代码,应该只是在多一些配置,没有太大变化。
接下来的实验中,我要输出一个占空比为20%的PWM波,以驱动LED。注意,本实验直接使用定时器输出PWM,没有使用中断手动输出。实验代码如下:
#include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h" #include "stm32f10x_tim.h" void GPIO_Conf() { GPIO_InitTypeDef t_gpio; //开启GPIOA的时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //设置PA6为复用推挽输出 t_gpio.GPIO_Pin=GPIO_Pin_6; t_gpio.GPIO_Mode=GPIO_Mode_AF_PP; t_gpio.GPIO_Speed=GPIO_Speed_50MHz; GPIO_Init(GPIOA,&t_gpio); } void TIM_Conf() { TIM_TimeBaseInitTypeDef t_timebase; TIM_OCInitTypeDef t_oc; //开启TIM3时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE); //对TIM3CLK进行10000分频,72Mhz/10000=7200hz t_timebase.TIM_Prescaler=10000-1; //周期为7200个脉冲,则一次溢出需要7200/7200Hz=1s t_timebase.TIM_Period=7200-1; //数字滤波器不分频 t_timebase.TIM_ClockDivision=TIM_CKD_DIV1; //向上计数 t_timebase.TIM_CounterMode=TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3,&t_timebase); //自动重装 TIM_ARRPreloadConfig(TIM3,ENABLE); //启用TIM3 TIM_Cmd(TIM3,ENABLE); //通道模式为PWM1 t_oc.TIM_OCMode=TIM_OCMode_PWM1; //使能输出 t_oc.TIM_OutputState=TIM_OutputState_Enable; //分界点为1440 t_oc.TIM_Pulse=1440-1; //极性为高电平 t_oc.TIM_OCPolarity=TIM_OCPolarity_High; //开启通道1预装载 TIM_OC1PreloadConfig(TIM3,TIM_OCPreload_Enable); //初始化TIM3的通道1 TIM_OC1Init(TIM3,&t_oc); } int main() { GPIO_Conf(); TIM_Conf(); while(1); }
代码中,首先把PA6引脚设置为推挽输出。为什么是PA6引脚呢?这个是因为我们使用了TIM3的通道1,查看下表:
第一行可以看到,在没有重映射时,TIM3_CH1的引脚就是PA6。
接着,把TIM3配置成7200个脉冲为一个周期。TIM3CLK的72Mhz频率,通过10000分频后,输出到TIM3_CNT时成了7200Hz,当把TIM3配置成7200个脉冲一个周期时,TIM3_CNT从0开始累加到7200,然后又重新回到0,。这样一个周期是1秒。
接着配置通道1为PWM1输出模式,并使能输出。除了PWM1外,还有PWM2模式,它们的区别在于,PWM1模式下,当TIMx_CNT的值小于TIMx_ARR的值时为有效电平,大于或等于时为无效电平;而在PWM2则相反。我把TIMx_ARR寄存器,也就是TIM_Pulse成员设置为1440-1,这样当计数器小于1440-1时输出有效电平,否则输出无效电平。而通过
t_oc.TIM_OCPolarity=TIM_OCPolarity_High;
这行代码,把极性设置为高电平,也就规定了有效电平为高电平。
至此,定时器开始工作,1s为一个周期,其中前1440/7200=20%的时间(也就是0.2s)里,输出高电平,之后80%的时间(也就是0.8s)里,输出低电平。
==================阶段三:跳变捕获之测量脉冲频率================
除了输出脉冲之外,STM32的定时器也可以输入脉冲,最常见的应用就是测量脉冲频率和测量脉冲宽度。
测量脉冲频率的原理很简单:当遇到某次上升沿(或下降沿)时记录定时器的值,当再次遇到上升沿(或下降沿)时再次记录定时器的值,假设整个过程中定时器都没有溢出,那么两个值相减就是脉冲的周期,即可得到频率。如果整个过程中有溢出,那么做相应的处理即可。
实验大致如下:在“阶段二”的实验基础上稍作修改,用一根导线把PA6和PA1相连。PA6输出周期为0.1s、占空比为20%的PWM波,PA1测量该PWM波,并计算得到周期。之所以选择使用PA1引脚测量,是因为PA1对应于TIM2的通道2(如下图),独立于PA6对应的TIM3的通道1,可以避免理解上的混淆。
实验代码如下:
#include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h" #include "stm32f10x_tim.h" #include "misc.h" void GPIO_Conf() { GPIO_InitTypeDef t_gpio; //开启GPIOA的时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //设置PA6为复用推挽输出 t_gpio.GPIO_Pin=GPIO_Pin_6; t_gpio.GPIO_Mode=GPIO_Mode_AF_PP; t_gpio.GPIO_Speed=GPIO_Speed_50MHz; GPIO_Init(GPIOA,&t_gpio); //设置PA1为浮空输入 t_gpio.GPIO_Pin=GPIO_Pin_1; t_gpio.GPIO_Mode=GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA,&t_gpio); } void TIM3_Conf() { TIM_TimeBaseInitTypeDef t_timebase; TIM_OCInitTypeDef t_oc; //开启TIM3时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE); //对TIM3CLK进行1000分频,72Mhz/1000=72khz t_timebase.TIM_Prescaler=1000-1; //周期为7200个脉冲,则一次溢出需要7200/72kHz=0.1s t_timebase.TIM_Period=7200-1; //数字滤波器不分频 t_timebase.TIM_ClockDivision=TIM_CKD_DIV1; //向上计数 t_timebase.TIM_CounterMode=TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3,&t_timebase); //自动重装 TIM_ARRPreloadConfig(TIM3,ENABLE); //启用TIM3 TIM_Cmd(TIM3,ENABLE); //通道模式为PWM1 t_oc.TIM_OCMode=TIM_OCMode_PWM1; //使能输出 t_oc.TIM_OutputState=TIM_OutputState_Enable; //分界点为1440 t_oc.TIM_Pulse=1440-1; //极性为高电平 t_oc.TIM_OCPolarity=TIM_OCPolarity_High; //开启通道1预装载 TIM_OC1PreloadConfig(TIM3,TIM_OCPreload_Enable); //初始化TIM3的通道1 TIM_OC1Init(TIM3,&t_oc); } void TIM2_Conf() { TIM_TimeBaseInitTypeDef t_timebase; TIM_ICInitTypeDef t_ic; //开启TIM2时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); //对TIM2CLK进行720分频,72Mhz/720=100khz,可精确到10us t_timebase.TIM_Prescaler=720-1; //周期为20000个脉冲,则一次溢出需要20000/100kHz=0.2s t_timebase.TIM_Period=20000; //数字滤波器不分频 t_timebase.TIM_ClockDivision=TIM_CKD_DIV1; //向上计数 t_timebase.TIM_CounterMode=TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2,&t_timebase); //自动重装 TIM_ARRPreloadConfig(TIM2,ENABLE); //启用TIM2 TIM_Cmd(TIM2,ENABLE); //通道2 t_ic.TIM_Channel=TIM_Channel_2; //上升沿触发 t_ic.TIM_ICPolarity=TIM_ICPolarity_Rising; //直连 t_ic.TIM_ICSelection=TIM_ICSelection_DirectTI; //不分频,每次检测到一次跳变就捕获 t_ic.TIM_ICPrescaler=TIM_ICPSC_DIV1; //选择输入比较滤波器,滤波设置,经历几个周期跳变认定波形稳定0x0~0xF t_ic.TIM_ICFilter=0; TIM_ICInit(TIM2,&t_ic); //清空比较器中断标志位 TIM_ClearITPendingBit(TIM2,TIM_IT_CC2); //启用比较器中断 TIM_ITConfig(TIM2,TIM_IT_CC2,ENABLE); //清空定时器溢出中断标志位 TIM_ClearITPendingBit(TIM2,TIM_IT_Update); //启用定时器溢出中断 TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE); } void NVIC_Conf() { NVIC_InitTypeDef t_nvic; //优先级组为1 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); //中断向量为TIM2_IRQn t_nvic.NVIC_IRQChannel=TIM2_IRQn; //中断优先级 t_nvic.NVIC_IRQChannelPreemptionPriority=0; t_nvic.NVIC_IRQChannelSubPriority=1; //启用中断 t_nvic.NVIC_IRQChannelCmd=ENABLE; NVIC_Init(&t_nvic); } //是否遇到了第一个上升沿 uint8_t g_has_started=0; //遇到第一个上升沿时定时器的值 uint16_t g_start_time; //第一个上升沿到第二个上升沿之间定时器溢出的次数 uint16_t g_overflow_count; //测得的频率 uint32_t g_freq; void TIM2_IRQHandler(void) { //如果之前还未遇到第一个上升沿 if(!g_has_started) { //如果现在已经遇到了第一个上升沿 if(TIM_GetITStatus(TIM2,TIM_IT_CC2)==SET) { g_has_started=1; g_start_time=TIM_GetCapture2(TIM2); g_overflow_count=0; } } else { //如果定时器溢出 if(TIM_GetITStatus(TIM2,TIM_IT_Update)==SET) g_overflow_count++; //如果现在已经遇到了第二个上升沿 if(TIM_GetITStatus(TIM2,TIM_IT_CC2)==SET) { //遇到第二个上升沿时定时器的值 uint16_t t_end_time; //两次上升沿之间脉冲个数 uint32_t t_interval; g_has_started=0; t_end_time=TIM_GetCapture2(TIM2); //计算两次上升沿之间的脉冲个数(一次溢出相当于20000个脉冲) t_interval=((uint32_t)g_overflow_count)*20000+t_end_time-g_start_time; g_freq=100000/t_interval; } } //清空定时器溢出中断标志位 TIM_ClearITPendingBit(TIM2,TIM_IT_Update); //清空比较器中断标志位 TIM_ClearITPendingBit(TIM2,TIM_IT_CC2); } int main() { GPIO_Conf(); TIM3_Conf(); TIM2_Conf(); NVIC_Conf(); while(1); }
代码的原理还是比较简单的——监听上升沿中断和定时器溢出中断。当监听到第一次上升沿时,记录定时器的值为time1,监听到第二次上升沿时记录定时器的值为time2,并且记录两次上升沿之间定时器溢出次数为count,那么两次上升沿之间经历的定时器计数为
n=count*20000+time2-time1
之所以是20000,是因为TIM2的设置中,溢出值设定的是20000,也就是定时器每次到20000就溢出。而每次定时器计数的时间间隔是10us,所以两次上升沿之间经历的时间为
interval=n*10us=10-5*n s
所以频率就是
freq=1/interval=105/n Hz
为了验证结果的正确性,可以把程序下载到开发板中,然后连接PA1和PA6,让程序运行一段时间后,使用JLink硬件调试去查看g_freq全局变量的值,如图:
g_freq的值为0x0A,也就是10。而PA6引脚输出的PWM波的周期是0.1s,频率正是10Hz,结果完全正确!
=============阶段四:跳变捕获之PWM占空比测量================
除了测量脉冲周期外,STM32的定时器还可以测量占空比。要知道占空比,那么就得知道高电平的时间,也就是脉宽。
一个想法就是把“阶段三”中的配置稍作修改,把
//上升沿触发 t_ic.TIM_ICPolarity=TIM_ICPolarity_Rising;
改为
//上升沿、下降沿都触发 t_ic.TIM_ICPolarity=TIM_ICPolarity_BothEdge;
但是这样的话,就无法确定两次中断之间的电平是高电平还是低电平了。如果你说,我在中断中进一步判断是高电平还是低电平,额,好吧,你赢了~不过,既然STM32在硬件上已经提供了“主从模式”,那么就使用之呗~
所谓“主从模式”,就是说某个寄存器作为主寄存器,另一个寄存器作为从寄存器。当主寄存器发生某个事件时,触发从寄存器的某个动作。
在测量PWM的占空比时,需要同时知道PWM的高电平脉宽和整个周期长度。
在下面的例子中,我把通道2配置为主通道,把通道1配置为从通道。当通道2(PA1引脚)检测到上升沿时,复位TIM2_CNT计数器;当检测到下降沿时,把TIM2_CNT计数器的值放入通道1的寄存器IC1中;当检测到上升沿时,把TIM2_CNT计数器的值放入通道2的寄存器IC2中。该流程如下:
#include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h" #include "stm32f10x_tim.h" #include "misc.h" void GPIO_Conf() { GPIO_InitTypeDef t_gpio; //开启GPIOA的时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //设置PA6为复用推挽输出 t_gpio.GPIO_Pin=GPIO_Pin_6; t_gpio.GPIO_Mode=GPIO_Mode_AF_PP; t_gpio.GPIO_Speed=GPIO_Speed_50MHz; GPIO_Init(GPIOA,&t_gpio); //设置PA1为浮空输入 t_gpio.GPIO_Pin=GPIO_Pin_1; t_gpio.GPIO_Mode=GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA,&t_gpio); } void TIM3_Conf() { TIM_TimeBaseInitTypeDef t_timebase; TIM_OCInitTypeDef t_oc; //开启TIM3时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE); //对TIM3CLK进行1000分频,72Mhz/1000=72khz t_timebase.TIM_Prescaler=1000-1; //周期为7200个脉冲,则一次溢出需要7200/72kHz=0.1s t_timebase.TIM_Period=7200-1; //数字滤波器不分频 t_timebase.TIM_ClockDivision=TIM_CKD_DIV1; //向上计数 t_timebase.TIM_CounterMode=TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3,&t_timebase); //自动重装 TIM_ARRPreloadConfig(TIM3,ENABLE); //启用TIM3 TIM_Cmd(TIM3,ENABLE); //通道模式为PWM1 t_oc.TIM_OCMode=TIM_OCMode_PWM1; //使能输出 t_oc.TIM_OutputState=TIM_OutputState_Enable; //分界点为1440 t_oc.TIM_Pulse=1440-1; //极性为高电平 t_oc.TIM_OCPolarity=TIM_OCPolarity_High; //开启通道1预装载 TIM_OC1PreloadConfig(TIM3,TIM_OCPreload_Enable); //初始化TIM3的通道1 TIM_OC1Init(TIM3,&t_oc); } void TIM2_Conf() { TIM_TimeBaseInitTypeDef t_timebase; TIM_ICInitTypeDef t_ic; //开启TIM2时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); //对TIM2CLK进行720分频,72Mhz/720=100khz,可精确到10us t_timebase.TIM_Prescaler=720-1; //周期为20000个脉冲,则一次溢出需要20000/100kHz=0.2s t_timebase.TIM_Period=20000; //数字滤波器不分频 t_timebase.TIM_ClockDivision=TIM_CKD_DIV1; //向上计数 t_timebase.TIM_CounterMode=TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2,&t_timebase); //自动重装 TIM_ARRPreloadConfig(TIM2,ENABLE); //启用TIM2 TIM_Cmd(TIM2,ENABLE); //通道2 t_ic.TIM_Channel=TIM_Channel_2; //上升沿触发 t_ic.TIM_ICPolarity=TIM_ICPolarity_Rising; //直连 t_ic.TIM_ICSelection=TIM_ICSelection_DirectTI; //不分频,每次检测到一次跳变就捕获 t_ic.TIM_ICPrescaler=TIM_ICPSC_DIV1; //选择输入比较滤波器,滤波设置,经历几个周期跳变认定波形稳定0x0~0xF t_ic.TIM_ICFilter=0; //配置PWM输入模式 TIM_PWMIConfig(TIM2,&t_ic); //选择输入触发模式:通道2触发(通道2为主通道) TIM_SelectInputTrigger(TIM2,TIM_TS_TI2FP2); //选择从模式:通道2触发后重置 TIM_SelectSlaveMode(TIM2,TIM_SlaveMode_Reset); //使能主从模式 TIM_SelectMasterSlaveMode(TIM2,TIM_MasterSlaveMode_Enable); //清空比较器中断标志位 TIM_ClearITPendingBit(TIM2,TIM_IT_CC2); //启用比较器中断 TIM_ITConfig(TIM2,TIM_IT_CC2,ENABLE); } void NVIC_Conf() { NVIC_InitTypeDef t_nvic; //优先级组为1 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); //中断向量为TIM2_IRQn t_nvic.NVIC_IRQChannel=TIM2_IRQn; //中断优先级 t_nvic.NVIC_IRQChannelPreemptionPriority=0; t_nvic.NVIC_IRQChannelSubPriority=1; //启用中断 t_nvic.NVIC_IRQChannelCmd=ENABLE; NVIC_Init(&t_nvic); } //测得的频率 uint32_t g_freq; //测得的占空比 uint32_t g_duty; void TIM2_IRQHandler(void) { //获取通道2的值 uint16_t t_value=TIM_GetCapture2(TIM2); //如果不为0,说明一个PWM周期结束 if(t_value!=0) { //计算频率 g_freq=100000/t_value; //计算占空比(百分比) g_duty=TIM_GetCapture1(TIM2)*100/t_value; } //清空比较器中断标志位 TIM_ClearITPendingBit(TIM2,TIM_IT_CC2); } int main() { GPIO_Conf(); TIM3_Conf(); TIM2_Conf(); NVIC_Conf(); while(1); }
注意这个代码中没有考虑定时器溢出的问题。事实上,要检测定时器溢出需要有点复杂的代码。因为当高电平的下降沿到达时,计数器的TIM2_CNT的值被放入IC1寄存器,但是并不会触发中断,也就无法通过寄存器溢出次数来矫正。好在实际应用中,一般都知道频率的范围,通过适当的分频系数来保证测量时不会溢出。