65.9K
CodeProject 正在变化。 阅读更多。
Home

ARM 教程第二部分:定时器

starIconstarIconstarIconstarIconstarIcon

5.00/5 (9投票s)

2023年3月12日

CPOL

14分钟阅读

viewsIcon

10874

downloadIcon

114

STM32 定时器外设概览

“我们花一半的时间试图找到事情做,而我们匆忙地度过一生试图节省时间。”
Will Rogers

在本系列ARM教程的第二部分中,我们将探讨STM32定时器外设。STM32定时器外设功能非常强大,并且“提供多种操作模式,可以在最小化接口电路需求的同时,减轻CPU的重复性任务负担”。

由于定时器外设是一个非常复杂的主题,因此我在这篇文章中采用了略有不同的方法,除了详细介绍Timer外设之外,我还提供了各种代码示例,涵盖了广泛的功能。直接内存访问(DMA)与Timer外设的结合将在另一篇文章中介绍,届时我将更详细地讨论DMA。

我试图在大多数示例中使用板载用户按钮和LED,但在某些情况下这是不可能的。STM32CubeIDE调试器对于查看STM32C031C6寄存器是必需的;但是,要查看LED以外的Timer输出,则需要示波器或逻辑分析仪。

代码是使用STM32CubeIDE编写、编程和调试的,这是一个来自STMicroelectonics的IDE,可以在此下载。您需要克服一些障碍才能获得它,但它是一个很棒的工具,免费且无使用限制。关于设置STM32CubeIDe环境的教程有很多,所以我不再赘述。

了解微控制器

我打算在每篇文章中使用不同的微控制器,以便人们可以品尝各种设备、它们的功能、能力以及它们提供的外设。在本文中,我选择了一个NUCLEO-C031C6开发板,其中STM32C031C6微控制器作为主处理器。它是一款相当新、价格便宜、入门级的处理器,号称“您的下一代8位MCU是32位”。它可以在STMicroelectronics直接购买,价格为12.14美元。

NUCLEO-C031C6的一些主要特性包括:

  • 32位Arm® Cortex®-M0+内核,运行时钟48MHz,复位时配置为12MHz运行
  • 32 KB闪存,12 KB RAM
  • 支持ARDUINO® Uno V3连接
  • 无需单独的探测器,因为它集成了ST-LINK调试器/编程器
  • STM32C0 MCU有8到48引脚的封装
  • 8个16位定时器;1个高级定时器,4个通用定时器,2个看门狗定时器和1个系统滴答定时器

图1. NUCLEO-C031C6引脚图

由于该MCU非常新,除了制造商的文档外,可用的信息不多。如果您打算使用此设备,现有文档至关重要。

STM32031C6参考手册 st.com (rm0490)
STM32031C6数据手册 st.com (ds13867)
NUCLEO-C031C6用户手册 st.com (um2953)

定时器外设概述

STM32微控制器都基于相同的架构,分辨率为16位或32位。它们可以独立配置为输入或输出,并且每个都可以拥有1到9个通道。定时器外设在内部连接以进行定时器同步或链式操作,并且还可以与其他外设互连以进行监控或触发目的。

图2显示了STM32C031C6板载的通用Timer外设。虽然所有STM32微控制器都实现不同的Timer配置,但它们都包含高级控制TIM1定时器外设。

图2. 定时器比较图

TIM1外设,近距离观察(数据手册第21页)

图3显示了STM32定时器外设TIM1的框图,它由四个单元组成:

  • 主/从控制器单元(蓝色)
  • 时基单元(绿色)
  • 定时器通道单元(紫色)
  • 中断(break)功能单元(黄色)

图3. NUCLEO-C031C6定时器框图(参考手册第299页)

在接下来的部分中,我们将更详细地探讨Master/Slave单元的每个部分。

主/从控制器单元

Master/Slave单元主要为时基单元提供控制信号。这包括为时基单元提供时基时钟信号以及计数方向控制信号。它根据主/从配置决定时基单元的正确计数配置。

该单元处理定时器间同步。它可以配置为通过TRG0向另一个外设输出同步信号,并控制外部信号的时基计数器事件。

所有STM32设备都包含定时器,但并非所有设备都具有相同的主/从能力。我建议您参考您使用的设备的参考手册,以了解实现的定时器及其功能。

时基单元

时基单元包含一个计数器、预分频器和重复计数器。输入信号首先通过预分频器,在那里可以调整频率,然后再到达时基单元。输入信号的频率必须是时钟频率的1/3。

定时器计数器由两个寄存器控制:

TIMx_CNT:这是存储当前计数的定时器寄存器。

  • TIMx_ARR:包含计数器的重载值。向上计数时,计数设置为0
  • TIMx_CNT = ARR时,并且计数器为0;当向下计数时,设置为ARR,当TIMx_CNT = 0时。

当计数器周期重新启动时,它会触发一个“更新事件”,可以在TIM_SR寄存器中找到为更新中断标志(UIF)。需要重置此值才能继续接收通知。

定时器通道单元

定时器通道是定时器与外部环境交互的工作单元,除了少数例外,它们通过MCU的引脚映射,这些引脚可以配置为输入或输出。

配置为输出的定时器通道

当定时器通道配置为输出时,它用于生成一个信号,该信号是比较TIMx_CCRy内容与定时器计数器结果的。

参考下面的图4,可以看到逻辑比较的结果是OCyREF,然后将其馈送到通道输出级,该级根据某些配置参数对OCyREF信号进行条件处理,然后作为备用功能输出到设备的引脚。请注意,某些通道也可能输出一个互补信号。

图4. 配置为输入的定时器通道

配置为输入的定时器通道

下面的图5显示了输入级,我们可以立即看到TIMx_CHy可以作为备用功能配置为输入或输出。当配置为输入时,它可以用于对外部信号的上升沿、下降沿或两个边缘进行时间戳。输入信号通过一个条件处理级,该级包括一个滤波器和一个边缘检测单元。可以使用TIMx-CCER寄存器配置边缘检测器的极性。条件处理电路输出两个信号;(来源:通用定时器手册)

  • FIvFPy:是经过滤波的TIy定时器输入信号,根据定时器通道“y”的极性检测到活动边缘。
  • TIyFPz:始终是经过滤波的TIy定时器输入信号,但根据定时器通道“z”的极性检测到活动边缘。

每个定时器通道可以配置为三个模式之一,预分频器多路复用器连接到定时器通道预分频器。通过预分频器后,预分频器会缩放输入信号并将其作为ICyPS发出,触发将计数传输到捕获/比较y寄存器。

图5. 定时器配置为输入的框图

中断(Break)单元

中断(Break)单元仅适用于至少有一个通道具有两个互补输出的通道。中断(Break)单元作用于输出通道,并且几乎与其名称一样,在执行中断后的第一个边缘,输出通道将被关闭或置于安全状态。

定时器时钟源

Timer外设可以通过内部时钟或外部时钟进行时钟驱动。

内部时钟

内部定时器时钟(TIMCLK)源自SYSCLK,可以通过AHB预分频器和APB预分频器进行缩放(参考图6)。如图6所示,APB预分频器存在一个异常:如果APB预分频器的值为1,则TIMCLK等于PCLK乘以1;如果值为其他,则TIMCLK等于PCLK乘以2。

图6. NUCLEO-C031C6时钟树(参考手册第108页)

外部时钟

有两种方法可以同步(或外部时钟)STM定时器:

  • 外部时钟模式1:外部信号从TIx输入。
  • 外部时钟模式2:外部信号从ETR输入。

所有输入的外部信号必须小于内部时钟频率的三倍。在所有情况下(ETR除外),定时器会先同步信号,允许输入信号大于内部时钟频率。在所有情况下,输入信号必须小于输入频率的三倍。

TIMCLK <= 3 <= Input Signal

即使Time由外部源驱动,APB时钟仍然需要提供同步。同步由一个D触发器提供,如图7所示。外部信号被馈送到第一个D触发器的输入端,同步信号从第二个D触发器的输出端检索。

图7. 同步块

TIx和ETR等定时器输入具有滤波器级,可以激活以过滤掉持续时间小于配置阈值的不需要的信号。输入捕获预分频器和信号滤波器都可以使用捕获/比较控制寄存器(CCMR)进行编程;预分频器使用ISxPSC,滤波器使用ICxF。滤波还取决于CR1寄存器中的CKD字段,该字段控制采样时钟源与有效脉冲的最小定时器持续时间之间的比例。STM32定时器的一个很好的参考是STM32通用定时器手册。

在图3中,TIMx_ETR信号经过名为ETRP的同步预分频器,然后通过再同步电路(称为ETRF信号,具有与上述相同的约束),预分频器的输出频率应小于内部频率的三倍。

示例代码

代码目录

如果您只对代码或特定实现感兴趣,请单击您感兴趣的链接。

不亲自动手,很难理解一项新技术,至少对于我的键盘来说是这样。在本节中,我将通过以下示例解释各种定时器的工作原理。我们将从简单的基本计数示例开始,每个示例都将变得更复杂,并学习定时器的功能。

基本计数示例是一个非常简单的例程,它将目标频率设置为10Hz,计数器向上计数直到达到配置值,当达到该值时,更新中断标志(UIF)被设置。在while循环中,我们等待UIF标志被设置,然后我们重置它并切换用户LED。这效率不高,有两个原因:一是这是一个阻塞例程,二是轮询和设置标志之间存在延迟。

void BasicCount()
{
    // Configure PA5 as output
    ConfigureUserLED();

    // TIM3 clock enable
    RCC->APBENR1 |= (1 << 1);

    // Set Prescaler and Auto Reload Register to slow the
    //	processor down so we can see the LED blink.
    // Freq = SYSCLK / ((PSC - 1) * (ARR - 1))
    // Freq = 12MHz / ((12000 - 1) * (100 - 1))
    // Freq = ~10Hz
    TIM3->PSC = 12000 - 1;
    TIM3->ARR = 100 - 1;

    // Start the timer
    TIM3->CR1 |= 1;

    while(1)
    {
        // Wait for the overflow event to be fired
        while(!(TIM3->SR & TIM_SR_UIF)){}

        // Reset the Update Interrupt Flag
        TIM3->SR &= ~TIM_SR_UIF;

        // Toggle the LED
        GPIOA->ODR ^= (1 << 5);
    }
}
列表1. 基本计数器

与输出比较

配置此例程涉及更多工作,但消除了轮询,并且输出直接控制用户LED。为此,我们需要将用户LED(PA5)配置为其备用功能,ConfigureAFUserLED的代码包含在下载文件中。此外,我们需要配置时间来向该引脚输出1Hz信号。

在列表2的第17行,定时器被配置为切换输出,而不是在while循环中手动切换,如BasicCounter示例中所做的那样。然后设置CCER寄存器以允许定时器在已配置为PA5的TIM1_CH1引脚上输出,最后设置主输出使能以开始该过程。

void BasicCountWithOutput()
{
    // Configure PA5 as alternate function TIM1 CH1 output
    ConfigureAFUserLED();

    // TIM1 clock enable
    RCC->APBENR2 |= (1 << 11);

    // Set Prescaler and Auto Reload Register to slow the
    // processor down so we can see the LED blink.
    // Freq = 12MHz / ((1200 - 1) * (10000 - 1)) = 1Hz
    TIM1->PSC = 1200 - 1;
    TIM1->ARR = 10000 - 1;

    // OC1M Toggle output
    TIM1->CCMR1 |= (3 << 4);

    // CC1E Enable output
    TIM1->CCER |= 1;
    // Master Output Enable
    TIM1->BDTR |= (1 << 15);

    // Start the timer
    TIM1->CR1 |= 1;

    while(1)
    {

    }
}
列表2. 输出比较

基本模式1脉冲宽度调制(PWM)

PWM是由通道生成的信号,其频率由TIMx_ARR寄存器中的值确定,占空比由TIMx_CCRx寄存器确定。

可以使用TIMx_CCMR1寄存器的OCxM字段配置两种PWN模式。

  • PWM模式1:向上计数时,通道保持活动状态,只要TIMx_CNT小于TIMx_CCRx,否则非活动;向下计数时,只要TIMx_CNT大于TIMx_CCRx,则活动,否则非活动。
  • PWM模式2:向上计数时,通道保持非活动状态,只要TIMx_CNT小于TIMx_CCRx,否则活动;向下计数时,只要TIMx_CNT大于TIMx_CCRx,则非活动,否则活动。
void BasicPWMMode1()
{
    // Configure PA5 as alternate function TIM1 CH1 output
    ConfigureAFUserLED();

    // Enable TIM1 clock
    RCC->APBENR2 |= (1 << 11);

    // Set Prescaler and Auto Reload Register to slow the
    //	processor down so we can see the LED blink.
    // Freq = 12MHz / ((12000 - 1) * (1000 - 1)) = 1Hz
    TIM1->PSC = 12000 - 1;
    TIM1->ARR = 1000 - 1;

    // Capture/Compare Register set the Duty Cycle
    // Duty Cycle = ARR / CCR1 = 999 / 500 = .50 = 50%
    TIM1->CCR1 = 500;
    // PWM Mode 1
    TIM1->CCMR1 |= (6 << 4) | (1 << 3);
    // CC1E Enable output
    TIM1->CCER |= 1;
    // Master Output Enable
    TIM1->BDTR |= (1 << 15);

    // Enable/Start Timer
    TIM1->CR1 |= TIM_CR1_CEN;
}
列表3. 基本模式1 PWM

演示代码将定时器1和3链接在一起,以创建一个32位定时器,每4秒切换一次用户LED。定时器1配置为主定时器,频率为1Hz,定时器3配置为从定时器,计数到4。

void LinkTimer1And3()
{
    // GPIOB clock enable
    RCC->IOPENR = 2;

    // Set PB8 alternate function
    GPIOB->MODER &= ~(3 << 16);
    GPIOB->MODER |= (2 << 16);
    // Alternate function to TIM3 CH1
    GPIOB->AFR[1] &= ~0x0f;
    GPIOB->AFR[1] |= 3;

    // Enable TIM1 and TIM3 clocks
    RCC->APBENR2 |= (1 << 11);
    RCC->APBENR1 |= (1 << 1);

    // Master Mode Selection - Select Update Event as Trigger output (TRG0)
    TIM1->CR2 |= (2 << 4);

    // Set Prescaler and Auto Reload Register
    // Freq = 12MHz / ((12000 - 1) * (1000 - 1)) = 1Hz
    TIM1->PSC = 12000 - 1;
    TIM1->ARR = 1000 - 1;

    // Slave Mode Control Register - Configure in slave mode using ITR1 as
    //	internal trigger. External Clock Mode 1 - Rising edges of the selected
    //	trigger (TRGI) clock the counter.
    TIM3->SMCR |= 7;

    TIM3->PSC = 0;
    TIM3->ARR = 4 - 1;

    // OC1M Toggle output
    TIM3->CCMR1 |= (3 << 4);
    // CC1E Enable output
    TIM3->CCER |= 1;
    // Master Output Enable
    TIM3->BDTR |= (1 << 15);

    TIM1->CR1 |= 1;
    TIM3->CR1 |= 1;

    while(1);
}
列表4. 将Timer1链接到Timer3

单次触发模式

单次脉冲或我称之为“一次性触发”,允许在响应某个刺激时启动计数器,并输出一个可编程长度和延迟的脉冲。

延迟 = CCRx寄存器中的值

  • 脉冲宽度 = TIMx_ARR - TIMx_CCRx

代码设置为通过按下用户按钮触发脉冲,并且输出可以在PA8上查看。使用演示代码需要示波器或逻辑分析仪来查看结果。

在演示代码中,延迟和脉冲宽度的计算为:

        MCU frequency = 12MHz with prescale of 100 = 1Mhz 
Delay = 1MHz / 250 = 480 =? 1/480 = 2.08mS
Pulse Width = 1Mhz / (500 - 250) = 2.08mS

由于重复控制寄存器(RCR)设置为0,因此脉冲只发生一次。

void OneShotMode()
{
    // Enable GPIOA and GPIOC clocks
    RCC->IOPENR = 5;
    // Enable Time1 and Timer3 clocks
    RCC->APBENR2 |= (1 << 11);

    // Using Timer14 as a basic counter to provide
    //	debounce for the User Button.
    ConfigDelay();

    // Configure PA8 as alt func TIM1 CH1
    GPIOA->MODER &= ~(3 << 16);
    GPIOA->MODER |= (2 << 16);
    GPIOA->AFR[1] |= 2;

    // Configure PC13 as input
    GPIOC->MODER &= ~(3 << 26);

    /******* Waveform setup ********
        * Delay defined by CCR1
        * Pulse defined by difference of ARR - CCR2
        * Repeat 1 time
        * Delay = 2.12mS
        * Pulse Width = 2.12mS
        *******************************/
    TIM1->PSC = 100;
    TIM1->ARR = 500;
    TIM1->CCR1 = 250;
    TIM1->RCR = 0;

    // PWM Mode 2
    TIM1->CCMR1 |= (7 << 4);
    // Select One-pulse mode
    TIM1->CR1   |= TIM_CR1_OPM;
    // Capture/Compare 1 Enable
    TIM1->CCER |= 1;
    // Master Output enable
    TIM1->BDTR |= TIM_BDTR_MOE;

    while(1)
    {
        if (!(GPIOC->IDR & (1 << 13)))
        {
            // Trigger the Timer by enabling, once complete the
            //	UIF flag is set and the Timer is automatically 
            //	reset/disabled.
            TIM1->CR1 |= TIM_CR1_CEN;

            // Wait for the Update flag to be set
			while(!(TIM1->SR & TIM_SR_UIF)){}

            // The UIF flag needs to be reset so we can repeat the process.
            TIM1->SR &= ~1;
            
            // Button debounce using Timer14
            Delay(100);
        }
    }
}
列表5. 由用户按钮触发的单次触发模式

输入捕获外部源

输入捕获演示代码将PA8配置为备用功能,然后在SMCR寄存器中将其映射到ITR2输入。要查看捕获结果,请将用户按钮(PC13 CN7 Pin 23)与用户LED(PA5 CN10 Pin 24)之间安装跳线。当按下用户按钮时,用户LED将切换。要查看CCR1寄存器中的时间结果,您需要以调试模式运行程序,并在第35行设置断点。

当按下用户按钮时,会在TI1上检测到捕获,CC1IF标志被设置,并且CCR1的内容包含时间戳值。如果检测到捕获时CC1IF标志已设置,则会设置过捕获标志(CCxOF)。CC1IF标志需要应用程序重置。

void InputCaptureExternalSource()
{
    uint16_t result = 0;

    // TIM1 clock enable
    RCC->APBENR2 |= (1 << 11);

    // Configure PA8 as alt func TIM1 CH1
    RCC->IOPENR = 1;
    GPIOA->MODER &= ~(3 << 16);
    GPIOA->MODER |= (2 << 16);
    GPIOA->AFR[1] &= ~(0x0f);
    GPIOA->AFR[1] |= 2;

    // Configure PA5 as output
    GPIOA->MODER &= ~(3 << 10);
    GPIOA->MODER |= (1 << 10);

    // Channel 1 (PA8) configured as input, mapped to ITR2
    TIM1->CCMR1 |= 1;
    // Trigger selection TI1FP1
    TIM1->SMCR |= (5 << 4) + (4 << 0);
    // CC1E Capture Enable
    TIM1->CCER |= 1;

    TIM1->CR1 |= 1;

    while (1)
    {
        while(!(TIM1->SR & TIM_SR_CC1IF)){}

        result = TIM1->CCR1;
        GPIOA->ODR ^= (1 << 5);
        // Reset the Update Interrupt Flag
        TIM1->SR &= ~TIM_SR_CC1IF;
    }
}
列表6. 输入捕获演示代码

输入捕获内部源

定时器1配置为通过TRG0每250毫秒输出一个信号,并且由于定时器1和定时器通过ITR2链接,定时器3设置为在ITR2上接收事件。发生这种情况时,我切换用户LED并重置CC1IF标志。

void InputCaptureInternalSource()
{
    // TIM1 clock enable
    RCC->APBENR2 |= (1 << 11);
    // Enable TIM3 clock
    RCC->APBENR1 |= RCC_APBENR1_TIM3EN;

    // Configure PA5 as output
    RCC->IOPENR = 1;
    GPIOA->MODER &= ~(3 << 10);
    GPIOA->MODER |= (1 << 10);

    // Every 250mS
    TIM3->PSC = 3000;
    TIM3->ARR = 1000;

    // The update event is selected as trigger output (TRGO).
    TIM3->CR2 |= (2 << 4);

    // Configured as input, IC1 is mapped on TI1
    TIM1->CCMR1 |= 3;
    // Trigger selection ITR2 Trigger mode
    // And Slave Mode selection Reset Mode
    TIM1->SMCR |= (2 << 4) | (4 << 0);
    // CC1E Capture Enable
    TIM1->CCER |= 1;

    TIM1->CR1 |= TIM_CR1_CEN;
    TIM3->CR1 |= TIM_CR1_CEN;

    while (1)
    {
        while(!(TIM1->SR & TIM_SR_CC1IF)){}

        GPIOA->ODR ^= (1 << 5);
        // Reset the Update Interrupt Flag
        TIM1->SR &= ~TIM_SR_CC1IF;
    }
}
列表7. 通过内部源输入捕获

通过中断闪烁

这是一个非常简单的中断例程,每次中断(每1秒发生一次)都会闪烁用户LED。

该例程基本上将PA5引脚设置为输出,设置中断使能标志,然后设置NVIC寄存器。

我在这篇文章中没有过多地讨论中断或DMA,因为我打算在后续文章中解决这两个主题。

void BlinkyWithInterrupt()
{
    ConfigureUserLED();

    // TIM1 clock enable
    RCC->APBENR2 |= (1 << 11);

    // Set Prescaller and Auto Reload for 1 sec
    TIM1->PSC = 12000;
    TIM1->ARR = 1000;

    // Reinitialize the counter and generates an update of the registers
    TIM1->EGR |= TIM_EGR_UG;

    // Update interrupt enable
    TIM1->DIER |= (1 << 0);

    // Set Interrupt Priority
    NVIC_SetPriority(TIM1_BRK_UP_TRG_COM_IRQn, 0);
    
    // Enable Interrupt
    NVIC_EnableIRQ(TIM1_BRK_UP_TRG_COM_IRQn);
    
    // Start Timer
    TIM1->CR1 |= (1 << 0);

    while(1);
}
列表8. 通过中断闪烁

以下是上述演示代码的中断处理程序:

void TIM1_BRK_UP_TRG_COM_IRQHandler()
{
    // Toggle USer LED
    GPIOA->ODR ^= (1 << 5);
    // Reset UIF flag
    TIM1->SR &= ~(1<<0);
}
列表9. 中断处理程序

结论

我本想在这篇文章中添加更多内容,但它变得太长了,所以我让代码来填补一些空白。我非常喜欢写这篇文章,但研究花了我很长时间,因为我几乎是从零开始。我曾使用过Arduino定时器,但它们的功能远不如STM32定时器。我希望您能从中获得和我一样的收获。

参考文献

  • 理解STM32命名约定,2020年5月20日,Maker.io员工 digikey.com (AN4013)
  • 应用节点,STM32跨系列定时器概述(AN4776)应用节点,STM32微控制器的通用定时器手册

历史

  • 2023年3月12日:初版
© . All rights reserved.