•STM32F103C8T6定时器资源:TIM1、TIM2、TIM3、TIM4
内部时钟的来源是RCC_TIMxCLK,这里的频率值一般都是系统的主频72MHz ,所以通向时基单元的计数基准频率是72MHz。
预分频器:可以对这个72MHz的计数时钟进行预分频,这个寄存器写0,就是1分频,输出频率=输入频率=72MHz;写1,则是2分频,输出频率=输入频率/2=36MHz,以此类推。
计数器:对预分频后的计数时钟进行计次,计数时钟每来一个上升沿,计数器的值就加1,因为是16位的,所以最大可以加到65535,然后从0开始,当自增运行达到目标值时,产生中断。
自动重装载寄存器:16位寄存器,存的是我们写入的计数目标,在运行过程中,计数值不断自增,自动重装值就是固定的目标,当计数值达到自动重装值时,计时时间就到了,则会产生中断,并清零计数器。
更新中断:即图中自动重装载寄存器旁边的箭头UI,其为计数值等于自动重装值产生的中断,被称为更新中断,更新中断之后就会通往我们配置好的NVTI,则定时器的更新中断就可以得到CPU的响应了。
事件中断:即UI下边的向下的箭头U,会产生一个事件,被称为更新事件,不会触发中断,但是可以触发内部其他电路的工作。
通用定时器不仅可以使用内部时钟72MHz,还可以使用外部时钟TIMx_ETR;
查看引脚定义可以看到TIM2_ETR定义在引脚PA0上
我们可以在TIM2_ETR()PA0上接一个外部方波时钟,再配置一下极性选择、边沿检测和预分频器,再配置输入滤波电路(对输入的波形进行滤波)
除了外部ETR引脚可以提供时钟(外部时钟模式2),还可以 通过TRGI这一路来提供外部时钟(外部时钟模式1),通过这一路的外部时钟有:1、ETR引脚的信号;2、ITR信号,这一部分的时钟是来自其他定时器的,主模式的输出TRGO可以通向其他定时器,即通向其他定时器的ITR引脚上来了,各个定时器每个通道通向的定时器如下图所示。
通过这一路,我们就可以实现定时器级联的功能
比如,我们可以先初始化定时器3,如何使用主模式把它的更新事件映射到TRGO上,接着再初始化TIM2,这里选择ITR2,对应的就是TIM3的TRGO,然后再选择时钟为外部时钟模式1,这样TIM3的更新事件就可以驱动TIM2的时基单元,便是吸纳了定时器的级联。
CH1引脚的边沿
我们还可以选择TI1F_ED,连接着CH1引脚,并从该引脚获得时钟,通过这一路输入的时钟,上升沿和下降沿均有效。
其分别是CH1和CH2的时钟。
这里STM32为了不让定时器时钟因为预分频控制寄存器的变化而打乱之前的波形,增加了一个预分频缓冲器,即当在一个计时周期内(更新事件未执行时),预分频发生变化,不会立即是定时器时钟发生变化,而是做一个缓冲,当一个计时周期结束后(更新事件被执行),预分频才会作用(计数频率改变),如图所示。
预分频控制寄存器在一个计时周期内发生变化,定时器时钟的计数频率仍然会保持为原来的频率,知道本轮计数完成,在下一轮计数时,计数频率才会改变。
预分频器内部其实也是靠计数来分频的,当预分频值为0时,计数器就一直为0,预分频值为1时,就会01010101这样计数,在回到0时输出脉冲,定时器时钟下降沿。
自动重装载寄存器在定时器时钟运行的一个周期中将FF改为36,无预装的计数器将直接在这个周期执行该计划,即计数到36就重新计数
相比无预装,多了一个自动重装载影子寄存器(缓冲寄存器),在一个计数周期内改变自动重装载寄存器的值,将会在下一个计数周期才会执行。
RCC时钟树
初始化定时器:按照下图顺序进行初始化
void TIM_DeInit(TIM_TypeDef* TIMx);
//恢复初始配置
void TIM_TimeBaseInit(TIM_TypeDef* TIMx, TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);
//时基单元初始化
void TIM_TimeBaseStructInit(TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);
//给结构体变量赋予默认值
void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState);
//使能计数器
void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState);
//使能中断输出信号,即中断输出控制
时钟源选择
void TIM_InternalClockConfig(TIM_TypeDef* TIMx);
//选择内部时钟
void TIM_ITRxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource);
//选择ITRx其他定时器的时钟
void TIM_TIxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_TIxExternalCLKSource,
uint16_t TIM_ICPolarity, uint16_t ICFilter);
//选择TIx捕获通道的时钟
void TIM_ETRClockMode1Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler,
uint16_t TIM_ExtTRGPolarity,uint16_t ExtTRGFilter);
//选择ETR通过外部时钟模式1输入的时钟
void TIM_ETRClockMode2Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler,
uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter);
//选择ETR通过外部时钟模式2输入的时钟
void TIM_ETRConfig(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler,
uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter);
//配置ETR引脚的预分频器、极性、滤波器
更改某些关键参数的函数
void TIM_PrescalerConfig(TIM_TypeDef* TIMx, uint16_t Prescaler,
uint16_t TIM_PSCReloadMode);
//单独写预分频值(指定的TIM, 要写入的预分频值, 写入预分频的模式)(缓冲或者不缓冲)
void TIM_CounterModeConfig(TIM_TypeDef* TIMx, uint16_t TIM_CounterMode);
//改变计数器的计数模式(指定的TIM, 新的计数器模式)
void TIM_ARRPreloadConfig(TIM_TypeDef* TIMx, FunctionalState NewState);
//是否启用计数器的有预装模式(指定TIM, 使能或者失能)
void TIM_SetCounter(TIM_TypeDef* TIMx, uint16_t Counter);
//给计数器写入一个值
void TIM_SetAutoreload(TIM_TypeDef* TIMx, uint16_t Autoreload);
//给自动重装器写入一个值
uint16_t TIM_GetCounter(TIM_TypeDef* TIMx);
//获取当前计数器的值
uint16_t TIM_GetPrescaler(TIM_TypeDef* TIMx);
//获取当前预分频器的值
获取标志位和清除标志位
FlagStatus TIM_GetFlagStatus(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);
void TIM_ClearFlag(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);
ITStatus TIM_GetITStatus(TIM_TypeDef* TIMx, uint16_t TIM_IT);
void TIM_ClearITPendingBit(TIM_TypeDef* TIMx, uint16_t TIM_IT);
这样定时器的基准时钟和整个外设的工作时钟就会同时打开了
这里要使用APB1来开启时钟函数,因为TIM2是APB1总线的外设
//开启TIM2的时钟
//TIM2是APB1总线的外设,所以
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
对应定时中断,我们选择内部时钟模式
//选择时基单元的时钟,选择为内部时钟
//如果不写选择时钟源的代码,默认为内部时钟
TIM_InternalClockConfig(TIM2);
//时基单元初始化
TIM_TimeBaseInitTypeDef TimeBaseInitStructure;
//指定时钟分频(与本次操作没太大关系)
TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
//计数器模式
TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
//下面三个就是时基单元里每个关键寄存器的参数
//按照公式CK_CNT_OV = CK_PSC / (PSC + 1) / (ARR + 1)
//定时频率 = 72M/(PSC+1)/(ARR+1)
//定时1s,即定时频率为1Hz,则Period给10000,Prescaler给7200
//预分频是对72M进行7200分频,即10k的计数频率,在10k的频率下计10000个数,即1s
//因为两个参数都有一个数的偏差,所以都需要-1
//ARR自动重装载器的值
TimeBaseInitStructure.TIM_Period = 10000 - 1;
//PSC预分频器的值
TimeBaseInitStructure.TIM_Prescaler = 7200 - 1;
//重复计数器的值(高级计数器特有的,我们没有直接赋0)
TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TimeBaseInitStructure);
//使能更新中断
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
//这样就开启了更新中断到NVIC的通道
//NVIC
//优先级分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
//NVIC初始化
NVIC_InitTypeDef NVIC_InitStructure;
//中断通道
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
//指定中断通道是使能还是失能
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
//抢断优先级和响应优先级
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
//启动定时器
TIM_Cmd(TIM2, ENABLE);
在启动文件中找到定时器2的中断函数
DCD TIM2_IRQHandler ; TIM2
//中断函数
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)//判断是否是指定端口产生的中断
{//第二个参数是想看哪个中断的标志位
//内容写在这
//清除中断标志位,回归主函数,以防一直卡在中断函数中
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"
int main(void)
{
OLED_Init();
Timer_Init();
OLED_ShowString(1,1,"Hello, World!");
while(1)
{
}
}
问题来了,要实现在OLED上显示一个1s加一的数字,就要定义一个数字,而这个定义的数字必须在main函数中定义,在中断函数中++,为了修改方便和解决这个被定义的数字不用再在Timer中再定义一遍,可以直接把中断函数移到main函数中,我们在51单片机中就是这样做的。
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"
uint16_t Num;
int main(void)
{
OLED_Init();
Timer_Init();
OLED_ShowString(1,1,"Num:");
while(1)
{
OLED_ShowNum(1, 4, Num, 3);
}
}
//中断函数
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)//判断是否是指定端口产生的中断
{//第二个参数是想看哪个中断的标志位
//内容写在这
Num++;
//清除中断标志位,回归主函数,以防一直卡在中断函数中
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
这样就基本完成了我们想在OLED上显示一个1s加1的数字的目标。
但是我们会发现OLED上的数字是从1开始,而不是从0开始,问题出自
TIM_TimeBaseInit(TIM2, &TimeBaseInitStructure);
查看其定义中有
生成一个更新事件,来立刻重新装载预分频器和重复计数器的值 ;
因为有缓冲器,只有产生了事件,我们重新装载的值才会起作用,为了让值立刻起作用,就在该函数的最后手动生成了一个事件,这样预分频器的值就有效了。但是会产生副作用,更新事件和更新中断时同时发生的,更新中断会置中断标志位,一旦我们初始化完后,关系中断就会立即进入,即刚上电就立刻进入中断。
解决方法:
在TIM_TimeBaseInit之后,在NVIC之前,添加一个清除中断标志位的函数
TIM_ClearFlag(TIM2, TIM_FLAG_Update);
这样就可以从0开始计数了
main
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"
uint16_t Num;
int main(void)
{
OLED_Init();
Timer_Init();
OLED_ShowString(1,1,"Num:");
while(1)
{
OLED_ShowNum(1, 5, Num, 5);
}
}
//中断函数
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)//判断是否是指定端口产生的中断
{//第二个参数是想看哪个中断的标志位
//内容写在这
Num++;
//清除中断标志位,回归主函数,以防一直卡在中断函数中
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
Timer
#include "stm32f10x.h" // Device header
void Timer_Init(void)
{
//开启TIM2的时钟
//TIM2是APB1总线的外设,所以
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
//选择时基单元的时钟,选择为内部时钟
//如果不写选择时钟源的代码,默认为内部时钟
TIM_InternalClockConfig(TIM2);
//时基单元初始化
TIM_TimeBaseInitTypeDef TimeBaseInitStructure;
//指定时钟分频(与本次操作没太大关系)
TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
//计数器模式
TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
//下面三个就是时基单元里每个关键寄存器的参数
//按照公式CK_CNT_OV = CK_PSC / (PSC + 1) / (ARR + 1)
//定时频率 = 72M/(PSC+1)/(ARR+1)
//定时1s,即定时频率为1Hz,则Period给10000,Prescaler给7200
//预分频是对72M进行7200分频,即10k的计数频率,在10k的频率下计10000个数,即1s
//因为两个参数都有一个数的偏差,所以都需要-1
//ARR自动重装载器的值
TimeBaseInitStructure.TIM_Period = 10000 - 1;
//PSC预分频器的值
TimeBaseInitStructure.TIM_Prescaler = 7200 - 1;
//重复计数器的值(高级计数器特有的,我们没有直接赋0)
TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TimeBaseInitStructure);
TIM_ClearFlag(TIM2, TIM_FLAG_Update);
//使能更新中断
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
//这样就开启了更新中断到NVIC的通道
//NVIC
//优先级分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
//NVIC初始化
NVIC_InitTypeDef NVIC_InitStructure;
//中断通道
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
//指定中断通道是使能还是失能
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
//抢断优先级和响应优先级
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
//启动定时器
TIM_Cmd(TIM2, ENABLE);
}
/*
//中断函数
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)//判断是否是指定端口产生的中断
{//第二个参数是想看哪个中断的标志位
//内容写在这
//清除中断标志位,回归主函数,以防一直卡在中断函数中
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
*/
我们还可以加上查看CNT(计数器的值)的值的函数
OLED_ShowNum(2, 5, TIM_GetCounter(TIM2), 5);
可以看到这列数字加到10000时归零,且Num加1,这其中时长为1s。
我们可以修改初始化函数中的代码,观察有什么变化
仅修改Timer中的ARR自动重装载的值
TimeBaseInitStructure.TIM_Period = 1000 - 1;
可以看到现象为,Num增加的速度变为了原来的10倍,即1s加10,TIM2的值也变为加到1000置0,并Num加1
仅修改PSC预分频器的值
TimeBaseInitStructure.TIM_Prescaler = 720 - 1;
会发现现象和上面的一样,这就得以验证我们的公式
我们以先前的代码为基础做出修改
首先先把内部时钟模式改为外部时钟
//内部时钟
TIM_InternalClockConfig(TIM2);
//外部时钟
拓展知识:外部触发滤波器,以一个采样频率f采样N个点,若N个点的值都一样,才会输出有效。
//时钟配置——外部时钟
//通过ETR引脚外部时钟模式2配置
//第二个参数是外部触发预分频器,我们这里不需要分频
//第三参数是外部触发的极性,1、反向:低电平或下降沿有效,2、不反向,高电平或者上升沿有效
//第四个参数是外部触发滤波器,这个参数就是决定f和N的
//具体填什么由手册中的14.4.3中的对应关系可知
//我们这里不会用到滤波器,则填0x00
TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted, 0x00);
因为我们的对射式红外传感器接在PA0上,所以还要初始化GPIO口
//初始化GPIO
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
//模式要看数据手册,在8.1.11外设的GPIO配置中
//手册中推荐浮空输入,但是我们选择上拉输入
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
因为是手动的,所以ARR和PSC就可以不用给这么大
//ARR自动重装载器的值
TimeBaseInitStructure.TIM_Period = 10 - 1;
//PSC预分频器的值
TimeBaseInitStructure.TIM_Prescaler = 1 - 1;
为了更好的观察CNT的值,我们可以为其封装一个函数
uint16_t Timer_GetCounter(void)
{
return TIM_GetCounter(TIM2);
}
总体
main
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"
uint16_t Num;
int main(void)
{
OLED_Init();
Timer_Init();
OLED_ShowString(1,1,"Num:");
OLED_ShowString(2,1,"CNT:");
while(1)
{
OLED_ShowNum(1, 5, Num, 5);
OLED_ShowNum(2, 5, Timer_GetCounter(), 5);
}
}
//中断函数
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)//判断是否是指定端口产生的中断
{//第二个参数是想看哪个中断的标志位
//内容写在这
Num++;
//清除中断标志位,回归主函数,以防一直卡在中断函数中
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
Timer
#include "stm32f10x.h" // Device header
void Timer_Init(void)
{
//初始化GPIO
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
//模式要看数据手册,在8.1.11外设的GPIO配置中
//手册中推荐浮空输入,但是我们选择上拉输入
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
//开启TIM2的时钟
//TIM2是APB1总线的外设,所以
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
//时钟配置——外部时钟
//通过ETR引脚外部时钟模式2配置
//第二个参数是外部触发预分频器,我们这里不需要分频
//第三参数是外部触发的极性,1、反向:低电平或下降沿有效,2、不反向,高电平或者上升沿有效
//第四个参数是外部触发滤波器,这个参数就是决定f和N的
//具体填什么由手册中的14.4.3中的对应关系可知
//我们这里不会用到滤波器,则填0x00
TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted, 0x00);
//时基单元初始化
TIM_TimeBaseInitTypeDef TimeBaseInitStructure;
//指定时钟分频(与本次操作没太大关系)
TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
//计数器模式
TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
//下面三个就是时基单元里每个关键寄存器的参数
//按照公式CK_CNT_OV = CK_PSC / (PSC + 1) / (ARR + 1)
//定时频率 = 72M/(PSC+1)/(ARR+1)
//定时1s,即定时频率为1Hz,则Period给10000,Prescaler给7200
//预分频是对72M进行7200分频,即10k的计数频率,在10k的频率下计10000个数,即1s
//因为两个参数都有一个数的偏差,所以都需要-1
//ARR自动重装载器的值
TimeBaseInitStructure.TIM_Period = 10 - 1;
//PSC预分频器的值
TimeBaseInitStructure.TIM_Prescaler = 1 - 1;
//重复计数器的值(高级计数器特有的,我们没有直接赋0)
TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TimeBaseInitStructure);
TIM_ClearFlag(TIM2, TIM_FLAG_Update);
//使能更新中断
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
//这样就开启了更新中断到NVIC的通道
//NVIC
//优先级分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
//NVIC初始化
NVIC_InitTypeDef NVIC_InitStructure;
//中断通道
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
//指定中断通道是使能还是失能
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
//抢断优先级和响应优先级
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
//启动定时器
TIM_Cmd(TIM2, ENABLE);
}
uint16_t Timer_GetCounter(void)
{
return TIM_GetCounter(TIM2);
}
/*
//中断函数
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)//判断是否是指定端口产生的中断
{//第二个参数是想看哪个中断的标志位
//内容写在这
//清除中断标志位,回归主函数,以防一直卡在中断函数中
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
*/
现象:因为没有预分频,所以每遮挡一次红外线CNT都会加1,当CNT加到10后,Num加1。