一、定时器基本原理
首先我们来看一下ST官方给出的关于定时器的相关介绍:(以STM32F103C8T6为例)
STM32F103C8T6 含有 4 个 16 位定时器,分别是一个高级定时器 TIM1 和 3 个通用定时器 TIM2、TIM3、TIM4,高级定时器除了拥有通用定时器的功能之外,还支持死区控制和紧急刹车,用于产生控制电机的 PWM。但在大多数应用场合我们拿 TIM1 当通用定时器来用。 通用定时器是一个通过可编程预分频器驱动的 16 位自动装载计数器构成。它适用于多种场合,包括测量输入信号的脉冲长度(输入捕获)或者产生输出波形(输出比较和 PWM)。使用定时器预分频器和 RCC 时钟控制器预分频器,脉冲长度和波形周期可以在几个微秒到几 个毫秒间调整。每个定时器都是完全独立的,没有互相共享任何资源。
通用定时器功能包括:
● 16 位向上、向下、向上/向下自动装载计数器
● 16 位可编程(可以实时修改)预分频器,计数器时钟频率的分频系数为 1~65536 之间的 任意数值 ● 4 个独立通道:
─ 输入捕获(用于采集波形周期和占空比)
─ 输出比较
─ PWM 生成(边缘或中间对齐模式)
─ 单脉冲模式输出
● 使用外部信号控制定时器和定时器互连的同步电路
● 如下事件发生时产生中断/DMA:
─ 更新:计数器向上溢出/向下溢出,计数器初始化(通过软件或者内部/外部触发)
─ 触发事件(计数器启动、停止、初始化或者由内部/外部触发计数)
─ 输入捕获 ─ 输出比较
● 支持针对定位的增量(正交)编码器和霍尔传感器电路
● 触发输入作为外部时钟或者按周期的电流管理
STM32 的定时器非常强大,功能也异常丰富,但是我们平时用到的无外乎三种:
1、一个是定时器溢出中断用于执行定时任务,比如每隔固定时间采集一次传感器数据;
2、第二个是输入捕获,它可以获取外部输入信号的高低电平时间,进而计算出该波形的周期(相邻下降沿或者相邻上升沿的间隔)和占空比(相邻上升沿和下降沿或者相邻下降 沿和上升沿时间);
3、第三个就是通过输出比较产生 PWM,PWM 应用非常广泛,比如调节灯光亮度,调节电机转速(直流电机,步进电机,舵机),调节声音等等。
STM32 的定时器有很多,有高级定时器,基础定时器, RTC 定时器,SysTick 定时器,Systick 定时器和 TIMx 定时器一样,最基本的单元都是定时计数器,即通过计数来产生等间隔时间,这些时间累积得到我们想要的周期。在讲解 STM32 的定时器操作之前我们先理解一下什么是定时计数器。现在通过一幅图来加深对定时器计数器的理解:
定时计数器基本结构
1. 时钟源
这是任何一个定时计数器的基础部件,要计数就要有时钟,时钟来源根据单片机而异, 我们使用的 STM32 的时钟源有四种:
● 内部时钟(CK_INT)
● 外部时钟模式 1:外部输入脚(TIx)
● 外部时钟模式 2:外部触发输入(ETR)
● 内部触发输入(ITRx):使用一个定时器作为另一个定时器的预分频器,如可以配置 一个定时器 Timer1 作为另一个定时器 Timer2 的预分频器。
一般使用RCC的内部时钟CK_INT,也即定时器时钟,经APB1或APB2预分频器后分频提供。
其中内部时钟的结构:
STM32定时器内部时钟源
可以看到通用定时计数器和高级定时计数器的内部时钟来源是不同的。 另外提供给定时器的时钟是经过倍频的,只不过倍频的情况要依据 APB1 和 APB2 的分频系数决定,对于 APB2 来说,它的时钟分频系数为 1,因此 TIM1 的时钟最大就是 72MHz(1倍频),而对于 APB1 来说它的时钟分频系数为 2,因此 TIM2-4 的时钟频率为 2 倍 APB1 时钟, 即:72MHz,这么做的目的是保证当 APB1 为二分频和 APB2 为一分频的情况下,各个定时器的时钟频率一致。
2. 计数器(计数寄存器)
接下来是时基部分(计数寄存器,绿色框部分),这部分的任务就是一刻不停的向某个方向计数,图中展示的是向上计数(++),STM32 还支持向下计数(--)和双向计数,双向计 数也叫向上/向下计数,它的计数方式类似于往返跑,即从 0 记到设置值后产生一个向上溢 出中断,然后自动从设置值向下计数到 0,再产生一个向下溢出中断,以此往复。现假设此时给计数器的时钟周期是 1ms,计数最大值设置为 1000,则定时计数器的定时中断周期就是 1s。
3. 重装载寄存器
那么问题来了,计数器周期设置的值来自哪里,这就是重装载寄存器的作用(蓝色框部 分),它是这个设置值的一个备份,每当计数器溢出后,该寄存器会自动将其内部的数据一 次性的“倒入”计数器,告诉计数器:“按照这个工作量来计,计完通知我。”
4. 捕获/比较寄存器
前面讲了时钟-计数器-重装载寄存器,他们三个只能实现定时中断的功能,对于输入捕获或者输出比较我们还需使用捕获/比较寄存器(紫色框部分)。
捕获对应的是输入,即通过 IO 口捕获外部的数字信号,来计算外部信号的电平长度, 周期,占空比等,一般捕获的过程如下(假如此时设置的是下降沿捕获): 当第一个下降沿来临时,触发捕获比较寄存器将此时计数器中的计数值拷贝过来。记做 c1; 当第二个下降沿来临时,再次触发捕获比较寄存器将此时计数器中的计数值拷贝过来。 记做 c2; 则此时该电平的长度为(c2-c1)*计数器时钟周期(假设该过程计数器没有发生溢出,即: 两次触发在一个定时中断周期内)。
比较对应的是输出,通常我们通过输出比较来在引脚的产生 PWM(Pulse Width Modulation),这是一种周期和占空比可调的方波,根据占空比不同即等效电压不同的原理, 我们可以给电机调速,给灯光调节亮度。要想使用输出比较功能就要给捕获比较寄存器提供 一个比较的值,比如设置 1000,那么在计数器小于 1000 时为高电平(也可以通过寄存器设 置为高电平),大于 1000 小于溢出值时为低电平,这就实现了 PWM 的生成。
以上介绍的是标准定时计数器结构,下面是STM32的定时计数器结构:
STM32内部定时计数器结构
二、认识PWM
PWM(Pulse Width Modulation,脉冲宽度调制)是一种周期和占空比可变的方波,因此在 PWM 的调制上也分为两种:
周期调制:通过改变周期即频率可以控制蜂鸣器发出不同音调的声音,也可以调制不同 频率的信号(比如用 PWM 来模拟 NEC 红外协议),另外现在很多照明厂商宣传自己的 LED 照 明产品无频闪(打开手机照相功能,如果发现光源闪烁就是有频闪,比如传统的白炽灯), 其原理就是将 LED 的驱动信号频率设置的高一些而已,比如普遍在 1KHz。
占空比调制:占空比就是同一周期的高电平持续时间和周期的比值,比如周期 100ms, 高电平持续时间为 75ms,低电平持续时间为 25ms,则占空比为 75%。如下图所示:
PWM占空比示意图
占空比越高对应的等效电压就越高,反之占空比越低,对应的等效电压越低。因此通过 调节占空比的大小可以控制灯光的亮度和电机的转速等。平时我们拿 PWM 作为一种数模转换器来用
通常使用定时器的输出比较功能来产生 PWM,原理如下图:
PWM产生原理
这里最核心的就是两个寄存器,一个是计数器,一个是比较寄存器,计数器的计数周期 值对应的就是 PWM 的周期,比较寄存器当中的数值就是我们设置的占空比,当计数器的值小 于比较寄存器当中的值时输出高电平(也可输出低电平要看寄存器设置),反之,当计数器 的值大于比较寄存器中的值时输出就为低电平(也可输出高电平要看寄存器设置),比如计 数器的周期是 100,比较寄存器中存放的值时 75,那么当计数值在 0-75 时输出高电平,在 76-100 期间输出低电平。
三、软件程序
输出比较生成 PWM 的软件配置过程:
1. 设置定时计数器初始化结构体,包括定时器时钟,周期,是否使能各个中断等;
2. 设置输出比较初始化结构体,包括输出比较模式(PWM1 和 PWM2 二选一),输出的 极性; 3. 设置 PWM 输出对应的 GPIO 为复用推挽输出; 最后可以在程序运行时通过更改重装载寄存器 ARR 来改变 PWM 频率(周期),通过更改捕获比较寄存器 CCRx 来改变 PWM 占空比
void TIM3_PWM_Init(u16 arr,u16 psc){ //TIM3 PWM初始化 arr重装载值 psc预分频系数
GPIO_InitTypeDef GPIO_InitStrue;
TIM_OCInitTypeDef TIM_OCInitStrue;
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStrue;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);//使能TIM3和相关GPIO时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);//使能GPIOB时钟(LED在PB0引脚)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);//使能AFIO时钟(定时器3通道3需要重映射到BP5引脚)
GPIO_InitStrue.GPIO_Pin=GPIO_Pin_0; // TIM_CH3
GPIO_InitStrue.GPIO_Mode=GPIO_Mode_AF_PP; // 复用推挽
GPIO_InitStrue.GPIO_Speed=GPIO_Speed_50MHz; //设置最大输出速度
GPIO_Init(GPIOB,&GPIO_InitStrue); //GPIO端口初始化设置
// GPIO_PinRemapConfig(GPIO_PartialRemap_TIM3,ENABLE); //映射,重映射只用于64、100、144脚单片机
//当没有重映射时,TIM3的四个通道CH1,CH2,CH3,CH4分别对应PA6,PA7,PB0,PB1
//当部分重映射时,TIM3的四个通道CH1,CH2,CH3,CH4分别对应PB4,PB5,PB0,PB1 (GPIO_PartialRemap_TIM3)
//当完全重映射时,TIM3的四个通道CH1,CH2,CH3,CH4分别对应PC6,PC7,PC8,PC9 (GPIO_FullRemap_TIM3)
TIM_TimeBaseInitStrue.TIM_Period=arr; //设置自动重装载值
TIM_TimeBaseInitStrue.TIM_Prescaler=psc; //预分频系数
TIM_TimeBaseInitStrue.TIM_CounterMode=TIM_CounterMode_Up; //计数器向上溢出
TIM_TimeBaseInitStrue.TIM_ClockDivision=TIM_CKD_DIV1; //时钟的分频因子,起到了一点点的延时作用,一般设为TIM_CKD_DIV1
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStrue); //TIM3初始化设置(设置PWM的周期)
TIM_OCInitStrue.TIM_OCMode=TIM_OCMode_PWM1; // PWM模式1:CNT < CCR时输出有效电平
TIM_OCInitStrue.TIM_OCPolarity=TIM_OCPolarity_High;// 设置极性-有效电平为:高电平
TIM_OCInitStrue.TIM_OutputState=TIM_OutputState_Enable;// 输出使能
TIM_OC3Init(TIM3,&TIM_OCInitStrue); //TIM3的通道3 PWM 模式设置
TIM_OC3PreloadConfig(TIM3,TIM_OCPreload_Enable); //使能预装载寄存器
TIM_Cmd(TIM3,ENABLE); //使能TIM3
}
其中TIM_OCMode(模式)与TIM_OCPolarity(极性),这两个参数是配合使用的:
TIM_OCMode_PWM1: 在向上计数时,一旦TIMx_CNT<TIMx_CCR时为有效电平,否则为无效电平; 在向下计数时,一旦TIMx_CNT>TIMx_CCR时为无效电平,否则为有效电平。 TIM_OCMode_PWM2: 在向上计数时,一旦TIMx_CNT<TIMx_CCR时为无效电平,否则为有效电平; 在向下计数时,一旦TIMx_CNT>TIMx_CCR时为有效电平,否则为无效电平。
TIM_OCPolarity_High:有效电平为高电平(1)
TIM_OCPolarity_Low:有效电平为低电平(0)
主函数:
int main (void){//主程序
delay_ms(500); //上电时等待其他器件就绪
RCC_Configuration(); //系统时钟初始化
I2C_Configuration();//I2C初始化
OLED0561_Init(); //OLED初始化
OLED_DISPLAY_8x16_BUFFER(3," SG90 TEST "); //显示字符串
TOUCH_KEY_Init();//按键初始化
TIM3_PWM_Init(59999,23); //设置频率为50Hz,公式为:溢出时间Tout(单位秒)=(arr+1)(psc+1)/Tclk 20MS = (59999+1)*(23+1)/72000000
//Tclk为通用定时器的时钟,如果APB1没有分频,则就为系统时钟,72MHZ
//PWM时钟频率=72000000/(59999+1)*(23+1) = 50HZ (20ms),设置自动装载值60000,预分频系数24
while(1){
if(!GPIO_ReadInputDataBit(TOUCH_KEYPORT,TOUCH_KEY_A)){ //读触摸按键的电平
OLED_DISPLAY_8x16_BUFFER(6," Angle 0 "); //显示字符串
TIM_SetCompare3(TIM3,1500); //改变比较值TIM3->CCR2达到调节占空比的效果(1500为0度)
}
if(!GPIO_ReadInputDataBit(TOUCH_KEYPORT,TOUCH_KEY_B)){ //读触摸按键的电平
OLED_DISPLAY_8x16_BUFFER(6," Angle 45 "); //显示字符串
TIM_SetCompare3(TIM3,3000); //改变比较值TIM3->CCR2达到调节占空比的效果
}
if(!GPIO_ReadInputDataBit(TOUCH_KEYPORT,TOUCH_KEY_C)){ //读触摸按键的电平
OLED_DISPLAY_8x16_BUFFER(6," Angle 90 "); //显示字符串
TIM_SetCompare3(TIM3,4500); //改变比较值TIM3->CCR2达到调节占空比的效果
}
if(!GPIO_ReadInputDataBit(TOUCH_KEYPORT,TOUCH_KEY_D)){ //读触摸按键的电平
OLED_DISPLAY_8x16_BUFFER(6," Angle 180 "); //显示字符串
TIM_SetCompare3(TIM3,7500); //改变比较值TIM3->CCR2达到调节占空比的效果
}
}
}