STM32速成笔记(六)—定时器

简介: 本文介绍了定时器的概念,作用。针对STM32F1的通用定时器做了详细介绍。此外,介绍了PWM的概念,用途以及STM32F1的PWM,给出了PWM频率的计算方法。最后通过介绍利用定时器的更新中断和PWM这两种方法实现呼吸灯,展示了定时器和PWM的配置步骤,并给出了详细的程序设计。另外,介绍了利用定时器实现按键长短按的检测方法。


🎀 文章作者:二土电子
🐸 期待大家一起学习交流!


一、什么是定时器

关于什么是定时器,简单来讲,就是是用来定时的。STM32F103ZET6有两个基本定时器TIM6和TIM7,四个通用定时器TIM2~TIM5和两个高级定时器TIM1,TIM8。每一个定时器都是完全独立的,不共享任何资源。

根据中文参考手册介绍,基本定时器最为简单,类似于51单片机的定时器。通用定时器在基本定时器的基础上增加了输入捕获和输出比较功能。高级定时器相比通用定时器,又增加了可编程死区互补输出,重复计数器等功能。

STM32F103ZET6的通用定时器是一个通过可编程预分频器驱动的16位自动装载计数器构成。使用定时器预分频器和RCC时钟控制器预分频器,脉冲长度和波形周期可以在几个微秒到几个毫秒间调整。

这里介绍一下对于定时器的个人理解。定时器的定时原理实际可以理解为按照固定的频率数数。按照固定频率就说明定时器一定要有输入时钟。比如输入为一个1KHz的时钟,那么数一个数的时间就是1ms。另外,数数也不是无限地数,数值有一个上限。可以规定是从0开始数到上限值,还是从上限值数到0。而且每次数到头,需要重新开始。比如,需要控制灯亮200ms。那么只需要在点亮LED之后,等到数到200时熄灭即可。当数到上限值或者数到0时,重新开始数。

二、定时器有什么用

定时器有许多用途,以通用定时器为例。它可以测量输入信号的脉冲宽度,产生PWM波。此外定时器也可以用于触发ADC采集,按键检测等方面。

中文参考手册介绍如下

0fbfc6827e9fd02515bb14eb9e87b4be_13437ef6a02d47f7ba5d530bcb74d722.png

三、通用定时器详细介绍

速成选手可以线跳过这一部分,直接看下面,后来再返回来仔细看。

3.1 时钟来源

根据中文参考手册,通用定时器的时钟来源有四个。

  • 内部CK_INT
  • 外部触发时钟输入TIMx_ETR(外部时钟模式2)
  • 内部触发输入(ITRx):使用一个定时器作为另一个定时器的预分频器
  • 外部引脚输入(外部时钟模式1)

34af1032e9dc5479f75ba99fdd954ee7_30f7b90f921a4cdf819034c0dfff613c.png
通过配置TIMx_SMCR寄存器来选择,关于寄存器这里就不再详细介绍了,大家可以去看中文参考手册。

根据中文参考手册关于时钟的介绍,通用定时器挂接在APB1总线。对于APB1总线的时钟如下

b8ac2b8f7860144c9eb47f52a695377e_1da8c394d6af4bcea395ca5871d89056.png

如果APB1的预分频系数为1,那么通用定时器的输入时钟频率为36MHz,否则为72MHz。但是通常APB1总线的预分频系数我们不会设置成1,所以通用定时器的时钟频率为72MHz。

3.2 预分频器,计数器,自动重装载寄存器

9ff71f40968b5598100abef04558f050_e1cd46ba49944d02a3077708ebcb6464.png

3.2.1 预分频器

预分频器是对时钟进行分频,范围是1~65536。比如通用定时器输入时钟频率为72MHz,此时,将预分频值设置为72,那么最终计数时的时钟频率为72MHz / 72 = 1MHz。

3.2.2 计数器

计数器就是用来计数的,计数值取值范围是0~65535。有三种计数方式:向上计数,向下计数,中央对齐模式(向上/向下计数)。

向上计数模式中,计数器从0计数到自动加载值(TIMx_ARR计数器的内容),然后重新从0开始计数并且产生一个计数器溢出事件。

向下计数模式中,计数器从自动装入的值(TIMx_ARR计数器的值)开始向下计数到0,然后从自动装入的值重新开始并且产生一个计数器向下溢出事件。

中央对齐模式,计数器从0开始计数到自动加载的值(TIMx_ARR寄存器)−1,产生一个计数器溢出事件,然后向下计数到1并且产生一个计数器下溢事件;然后再从0开始重新计数。

3.2.3 自动重装载寄存器

比如,选择向下计数模式,初始值为2000。当计数到0时,会再次从2000开始向下计数。这就叫重装载。但是实际起作用的并不是这里的自动重装载寄存器,而是影子寄存器。关于影子寄存器这里就不再做介绍了,大家可以自行了解。

3.3 触发控制器

从图中的右上角可以注意到,有一个触发控制器。它可以用来触发一些外设,比如触发ADC采集,也可以用来给其他定时器提供时钟。

四、PWM

4.1 什么是PWM

PWM(脉冲宽度调制),它是一种利用微处理器的数字输出来对模拟电路进行控制的技术,也可以理解为是对模拟信号电平进行数字编码的方法。PWM可被应用于电机驱动,调光,通信等方面。

4.2 什么是占空比

一个PWM是有固定频率的,也就意味着周期一定,一个周期内有效电平持续时间占整个周期的比例可以称为占空比。比如一个周期100ms,其中50ms持续为有效电平,那么占空比就是50%。正是通过调节占空比,来调节电机转速,或者用不同占空比代表不同信号,用于通信。

4.3 STM32F1 PWM介绍

STM32F1系列单片机,除了基本定时器TIM6和TIM7外,都可以产生PWM输出。其中高级定时器TIM1和TIM8可以同时产生高达7路PWM输出。PWM输出其实就是对外输出占空比可调的方波,信号频率由自动重装载寄存器ARR的值决定,占空比由比较寄存器CCR的值决定。假设高电平为有效电平,见下图。ARR决定了周期(频率),CCR调节占空比。
5fe4aa9383cf6af9b52c98b9ff1a8551_398a7972e3864b6ba2fd89646ce71bfc.png
根据中文参考手册介绍,STM32F1的PWM比较输出模式共有8种。脉冲宽度调制模式可以产生一个由TIMx_ARR寄存器确定频率、由TIMx_CCRx寄存器确定占空比的信号。在TIMx_CCMRx寄存器中的OCxM位写入’110’(PWM模式1)或’111’(PWM模式2),能够独立地设置每个OCx输出通道产生一路PWM。有关寄存器的内容,这里就不不再做详细介绍。

这里介绍一下8种输出模式中比较常用的两种PWM输出模式,PWM1和PWM2。其实这两种输出模式差别不大,只不过输出电平的极性不同。
|模式|计数器CNT计数方式|说明

image.png

4.4 PWM频率计算

频率 = (主时钟频率(72MHz) / (分频系数 + 1)) / 自动重装载值(单位为Hz)

五、通用定时器输出引脚

通道1 通道2 通道3 通道4
TIM2 PA0 PA1 PA2 PA3
TIM3 PA6 PA7 PB0 PB1
TIM4 PB6 PB7 PB8 PB9
TIM5 PA0 PA1 PA2 PA3

这里需要注意的是,如果对引脚进行了重映射,则通道对应引脚会发生变化。以TIM3为例

复用功能 没有重映射 部分重映射 完全重映射
TIM3_CH1 PA6 PB4 PC6
TIM3_CH2 PA7 PB5 PC7
TIM3_CH3 PB0 PB0 PC8
TIM3_CH4 PB1 PB1 PC9

其他几个定时器如下
ee8a51f8f53689ff656a84059d17f505_1e64361b6e0f47b583fdd5fdaef3b948.png

ecc3ab7f1bd40d6c54a962aa63f9d180_7e26c2e415194b40909d3cb86b7e863a.png

75ca17b270ffa12cd21c9c05fbdef351_563e8a895b1e40398b13de6709e2948a.png

六、实战项目

这里以一个经典项目——呼吸灯,来一起熟悉一下定时器的配置和使用,要求灭—>亮—>灭,时间为2.5s。

6.1 呼吸灯

呼吸灯是指灯能够像人的呼吸一样,实现由暗到亮或由亮到暗的变化,通常用于消息提示功能,或者作为系统正在运行的提示。之前一篇博文介绍过三种呼吸灯的实现方式,这里针对普中核心板,来介绍一下如果实现呼吸灯。

6.2 实现思路

这里用两种方法来实现一下呼吸灯。分别是定时器的溢出中断和PWM。其实第一种和PWM类似,我非就是控制LED点亮时间。

  • 定时器中断实现
    配置好预分频系数和重装载值,使每0.25ms进入一次定时器中断,记录进入中断次数(count)。当进入次数满100次之后(2.5ms),控制LED点亮的变量(t)值加1。主函数的while(1)轮询中,如果t小于等于count的时候,LED点亮,否则LED熄灭。t的值累计100加次后(2.5s),开始递减,LED由亮到灭。控制t是递增还是递减的是一个标志位(flag),初始值为0,具体可以看程序设计。
  • PWM
    利用PWM实现呼吸灯就更加简单了,只需要不断调节占空比即可。

    6.3 定时器配置

    配置通用定时器,有以下步骤
  • 使能定时器时钟
  • 初始化定时器参数,包括自动重装载值,分频系数,计数方式等
  • 设置中断类型,并使能
  • 设置中断优先级,使能定时器中断通道
  • 开启定时器
  • 编写定时器中断服务函数

需要注意的是,配置预分频系数时,比如设置为6,实际是6 + 1。

定时时间T = 自动重装载值 * ((预分频系数 + 1) / 主时钟频率)。主时钟频率为72MHz。
(为了避免误导,这里写的主时钟频率为72MHz是APB1总线分频系数不是1的前提下。)

6.4 定时器中断实现呼吸灯

定时器配置程序如下,使用定时器2,控制LED1实现呼吸灯,由灭—>亮—>灭,时间为5秒。

/*
 *==============================================================================
 *函数名称:TIM2_Iint
 *函数功能:初始化定时器2
 *输入参数:per:自动重装载值;psc:预分频系数
 *返回值:无
 *备  注:无
 *==============================================================================
 */
void TIM2_Iint (u16 per,u16 psc)
{
   
   
    // 结构体定义
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);   // 使能TIM2时钟

    TIM_TimeBaseInitStructure.TIM_Period = per;   // 自动装载值
    TIM_TimeBaseInitStructure.TIM_Prescaler = psc;   // 分频系数
    TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;   // 不分频
    TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up;   // 设置向上计数模式
    TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);

    TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE);   // 开启定时器中断
    TIM_ClearITPendingBit(TIM2,TIM_IT_Update);   // 使能更新中断

    NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;   // 定时器中断通道
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2;   // 抢占优先级
    NVIC_InitStructure.NVIC_IRQChannelSubPriority =3;   // 子优先级
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;   // IRQ通道使能
    NVIC_Init(&NVIC_InitStructure);    

    TIM_Cmd(TIM2,ENABLE);   // 使能定时器    
}

初始化时定时器的程序如下

TIM2_Iint(250,71);   // TIM2初始化

预分频系数为71 + 1 = 72,计数到250进入一次中断,也就是0.25ms进入一次中断。累计进入100次(25ms)中断开始调节一点LED的亮度。由灭到亮,累计调节100次(2.5s)。主函数和中断服务函数如下

u8 gTimIrqCunt = 0;   // 进入中断次数计数变量
u8 gLedLightCtrl = 0;   // LED亮度控制变量
u8 gLedFlag = 0;   // LED亮灭控制标志位,0:灭—>亮;1:亮—>灭

int main(void)
{
   
   
    Med_Mcu_Iint();   // 系统初始化

    while(1)
  {
   
   
        if (gLedLightCtrl <= gTimIrqCunt)
        {
   
   
            Med_Led_StateCtrl (LED1,LED_OFF);   // 熄灭LED1
        }
        if (gLedLightCtrl > gTimIrqCunt)
        {
   
   
            Med_Led_StateCtrl (LED1,LED_ON);   // 熄灭LED1
        }
    }
}

// TIM2中断服务函数
void TIM2_IRQHandler(void)   // TIM2中断
{
   
   
    // 产生更新中断
    if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
    {
   
   
        gTimIrqCunt = gTimIrqCunt + 1;   // 进入中断次数加1

        // 累计进入100次中断,且是由灭到亮
        if (gTimIrqCunt >= 100 && gLedFlag < 100)
        {
   
   
            gTimIrqCunt = 0;   // 清零进入中断计数变量
            gLedLightCtrl = gLedLightCtrl + 1;   // LED亮度控制变量加1
            gLedFlag = gLedFlag + 1;   // LED亮灭控制标志位加1
        }

        // 累计进入100次中断,且是由亮到灭
        if (gTimIrqCunt >= 100 && gLedFlag >= 100)
        {
   
   
            gTimIrqCunt = 0;   // 清零进入中断计数变量
            gLedLightCtrl = gLedLightCtrl - 1;   // LED亮度控制变量加1
            gLedFlag = gLedFlag + 1;   // LED亮灭控制标志位加1
        }

        // 一个亮灭周期结束
        if (gLedFlag >= 200)
        {
   
   
            gLedFlag = 0;   // 清零LED亮灭控制标志位
        }
    }

    TIM_ClearITPendingBit(TIM2, TIM_IT_Update);   // 清除TIM2更新中断标志
}

6.5 使用PWM实现呼吸灯

PWM配置步骤

  • 使能定时器以及GPIO时钟,设置引脚复用映射
  • 初始化定时器参数,包括自动重装载值,分频系数,计数方式等
  • 初始化PWM输出参数,包括PWM模式,输出极性,使能等
  • 开启定时器
  • 修改CCRx的值来修改占空比
  • 使能TIMx在CCRx上的预装载寄存器
  • 使能TIMx在ARR上的预装载寄存器允许位

TIM3的通道1配置程序如下这里对引脚进行了重映射。

/*
 *==============================================================================
 *函数名称:TIM3_CH1_PWM_Init
 *函数功能:初始化定时器3的PWM通道1
 *输入参数:per:自动重装载值;psc:预分频系数
 *返回值:无
 *备  注:无
 *==============================================================================
 */
void TIM3_CH1_PWM_Init (u16 per,u16 psc)
{
   
   
    // 结构体定义
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
    TIM_OCInitTypeDef TIM_OCInitStructure;
    GPIO_InitTypeDef GPIO_InitStructure;

    // 开启时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);

    // 初始化GPIO
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;   // 复用推挽输出
    GPIO_Init(GPIOC,&GPIO_InitStructure);

    GPIO_PinRemapConfig(GPIO_FullRemap_TIM3,ENABLE);   // 改变指定管脚的映射    

    // 初始化定时器参数
    TIM_TimeBaseInitStructure.TIM_Period = per;   // 自动装载值
    TIM_TimeBaseInitStructure.TIM_Prescaler = psc;   // 分频系数
    TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;   // 设置向上计数模式
    TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);    

    // 初始化PWM参数
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;   // 比较输出模式
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low;   // 输出极性
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;   // 输出使能
    TIM_OC1Init(TIM3,&TIM_OCInitStructure);   // 输出比较通道1初始化

    TIM_OC1PreloadConfig(TIM3,TIM_OCPreload_Enable);   // 使能TIMx在 CCR1 上的预装载寄存器
    TIM_ARRPreloadConfig(TIM3,ENABLE);   // 使能预装载寄存器

    TIM_Cmd(TIM3,ENABLE);   // 使能定时器

}

实现呼吸灯时,只需要在main函数中不断调整占空比即可,调整占空比的函数为

void TIM_SetCompare1(TIM_TypeDef* TIMx, uint16_t Compare1)

这里main函数就不在列出来了。需要注意的是,设置的CCR的值,不能超过自动重装载值 - 1。

七、拓展

之前介绍按键检测时,介绍过检测按键长短按的方法。当时比较简单粗暴,这里介绍另一种,使用定时器来判断按键WK UP的长短按。假设规定,按下时间在10ms~500ms之间为短按,按下时间大于等于1s,为长按。短按LED1点亮,长按LED1熄灭。之前是利用delay来实现时间控制,现在改用定时器实现时间控制,但是基本思路都是相同的。

关于按键部分的程序这里就不再做介绍了。首先配置定时器,10ms进入一次更新中断,预分频系数为72,自动重装载值为10000。使用TIM2,定时器配置程序和上面一样,初始化程序如下填写

TIM2_Iint(10000,71);   // TIM2初始化

main函数以及中断服务函数如下

u32 gKeyDownTimeCunt = 0;   // 按键按下时间计数变量
u8 gKeyLongFlag = 0;   // 按键长按标志位
u8 gKeyShotFlag = 0;   // 按键短按标志位

int main(void)
{
   
   
    Med_Mcu_Iint();   // 系统初始化

    while(1)
  {
   
   
        // 短按
        if (gKeyShotFlag == 1)
        {
   
   
            Med_Led_StateCtrl (LED1,LED_ON);   // 点亮LED1
            gKeyShotFlag = 0;   // 清除短按标志位
        }

        // 长按
        if (gKeyLongFlag == 1)
        {
   
   
            Med_Led_StateCtrl (LED1,LED_OFF);   // 熄灭LED1
            gKeyLongFlag = 0;   // 清除长按标志位
        }
    }
}

// TIM2中断服务函数
    void TIM2_IRQHandler(void)   // TIM2中断
    {
   
   
        // 产生更新中断
        if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
        {
   
   
            if (KEY_UP == 1)
            {
   
   
                gKeyDownTimeCunt = gKeyDownTimeCunt + 1;   // 时间计数变量加1
            }
            // 松开后
            else
            {
   
   
                // 短按
                if (1 <= gKeyDownTimeCunt && gKeyDownTimeCunt <= 50)
                {
   
   
                    gKeyDownTimeCunt = 0;   // 清除时间计数变量
                    gKeyShotFlag = 1;   // 短按标志位置1
                }
                // 长按
                if (gKeyDownTimeCunt >= 100)
                {
   
   
                    gKeyDownTimeCunt = 0;   // 清除时间计数变量
                    gKeyLongFlag = 1;   // 长按标志位置1
                }
            }
        }

        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);   // 清除TIM2更新中断标志
    }
相关文章
|
18天前
STM32F103标准外设库——SysTick系统定时器(八)
STM32F103标准外设库——SysTick系统定时器(八)
33 0
STM32F103标准外设库——SysTick系统定时器(八)
|
1月前
|
传感器 物联网 开发者
【STM32基础 CubeMX】定时器的使用
【STM32基础 CubeMX】定时器的使用
|
4月前
|
存储 物联网 芯片
STM32速成笔记(十四)—串口IAP
本文介绍了什么是IAP,IAP有什么作用,如何实现IAP。最后,给出了IAP的实现程序。
83 0
STM32速成笔记(十四)—串口IAP
|
4月前
|
芯片 内存技术
STM32速成笔记(十三)—低功耗模式
本文介绍了三种STM32低功耗模式的进入和退出方法,针对待机唤醒给出了程序设计。
107 0
STM32速成笔记(十三)—低功耗模式
|
4月前
|
存储 芯片 内存技术
STM32速成笔记(十二)—Flash闪存
本文简单介绍了什么是Flash。针对STM32F1的Flash做了详细介绍,介绍了操作Flash的步骤,并且给出了程序设计。最后,介绍了一些注意事项。
29 0
STM32速成笔记(十二)—Flash闪存
|
4月前
|
存储 芯片
STM32速成笔记(十一)—EEPROM(AT24C02)
本文详细介绍了什么是AT24C02,介绍了它的引脚,读/写时序,给出了应用实例和详细的程序设计。最后,简单介绍了AT24C02的应用场景。
128 0
STM32速成笔记(十一)—EEPROM(AT24C02)
|
4月前
STM32速成笔记(十)—IWDG
本文详细介绍了什么是IWDG,STM32的IWDG特性,框图和配置步骤。此外,给出了STM32的IWDG配置程序。通过一个简单的应用实例,展示了IWDG的配置和使用方法。
25 0
STM32速成笔记(十)—IWDG
|
4月前
|
API
STM32速成笔记(九)—RTC
本文详细介绍了RTC模块,介绍了STM32的RTC的特性,框图,配置步骤,并给出了详细的程序设计。最后,针对实际使用时可能遇到的问题给出了解决方法以及程序。
46 0
STM32速成笔记(九)—RTC
|
4月前
|
存储 Perl
STM32速成笔记(八)—DMA
本文介绍了DMA的概念,用途。对于STM32F103ZET6的DMA做出了详细地介绍,给出了DMA配置步骤。最后,以配置DMA搬运ADC转换结果为例,给出了DMA的配置和使用方法。
71 0
STM32速成笔记(八)—DMA