Diving into the STM32 HAL-----Timers笔记

        嵌入式设备会按时间执行某些活动。对于真正简单且不准确的延迟,繁忙的循环可以执行任务,但是使用 CPU 内核执行与时间相关的活动从来都不是一个聪明的解决方案。因此,所有微控制器都提供专用的硬件外设:定时器。定时器不仅是时基生成器,而且还提供了一些额外的功能,用于与 Cortex-M 内核和 MCU 内部和外部的其他外设进行交互。

        根据所使用的系列和封装,STM32 微控制器实现了可变数量的定时器,每个定时器都有特定的特性。某些部件号可提供多达 14 个独立计时器。与其他外设不同,定时器在所有 STM32 系列中都具有几乎相同的实现,它们分为九个不同的类别。其中最相关的是:basic、general purpose 和 advanced timers。

        STM32 定时器是一款功能强大的外设,提供广泛的自定义功能。此外,它们的一些功能是特定于应用领域的。这一章无疑是本书中最长的章节之一,它试图塑造与 STM32 MCU 中的基本和通用定时器最相关的概念,并着眼于用于对它们进行编程的相关CubeHAL 模块。

1、定时器简介

        定时器是一个自由运行的计数器,其计数频率是其源时钟的一小部分。可以使用每个定时器的专用预分频器来降低计数速度。根据 timer 类型,它可以由内部 clock (从它所连接的总线派生)、外部 clock source 或用作 “master” 的另一个 timer 进行计时。

        通常,定时器从零计数到给定值,该值不能高于其分辨率的最大无符号值(例如,当计数器达到 65535 时,16 位定时器溢出),但它也可以相反地计数,我们接下来将看到其他方式。        

        STM32 微控制器中最先进的定时器具有几个功能:

        它们可以用作时基发生器(这是所有 STM32 定时器的共同功能)。

        它们可用于测量外部事件的频率 (输入捕获模式)。

        控制输出波形,或指示经过一段时间的时间 (输出比较模式)。单脉冲模式(OPM)是输入捕获模式和输出比较模式的一种特殊情况。它允许计数器响应激励而启动,并在可编程延迟后生成具有可编程长度的脉冲。

        在每个通道上独立生成边沿对齐模式或中心对齐模式的 PWM 信号(PWM 模式)。在某些 STM32 MCU 中(特别是 STM32F3 和最新的 STM32L4 系列),一些定时器可以生成具有可编程延迟和相移的中心对齐 PWM 信号。

        根据定时器类型,定时器可以在发生以下事件时生成中断或 DMA 请求:

        更新事件:计数器上溢/下溢;计数器初始化;其他

        触发器:计数器启动/停止;计数器初始化;其他

        输入捕获/输出比较

1.1、STM32 MCU中的定时器类别

        STM32 定时器主要可分为九类。

        基本定时器:此类定时器是 STM32 MCU 中最简单的定时器形式。它们是用作时基发生器的 16 位定时器,并且没有 output/input 引脚。基本定时器也用于馈送给 DAC 外设,因为它们的更新事件可以触发对 DAC 的 DMA 请求(因此,它们通常在至少提供 DAC 的 STM32 MCU 中可用)。Basic timers 也可以用作其他 timers 的 “masters”。

        通用定时器:它们是 16/32 位定时器(取决于 STM32 系列),提供现代嵌入式微控制器的定时器预期实现的经典功能。它们可用于任何应用,用于输出比较(定时和延迟生成)、单脉冲模式、输入捕获(用于外部信号频率测量)、传感器接口(编码器、霍尔传感器)等。显然,通用 timer 可以用作时基生成器,就像基本 timer 一样。此类定时器提供 4 个可编程输入/输出通道。

        – 1 通道/2 通道:它们是通用定时器的两个子组,仅提供一个/两个输入/输出通道。

        – 1 通道/2 通道,带一个互补输出:与以前的类型相同,但在一个通道上有一个死区时间发生器。这允许具有独立于高级定时器的时基的互补信号。

        高级定时器:这些定时器是 STM32 MCU 中最完整的定时器。除了通用定时器中的功能外,它们还包括与电机控制和数字电源转换应用相关的多项功能:三个互补信号,带死区时间插入、紧急关断输入。

        高分辨率定时器:高分辨率定时器 (HRTIM1) 是由 STM32F3/G4 系列(专用于电机控制和功率转换的系列)和 STM32H7 系列的一些微控制器提供的特殊定时器。它允许生成具有高精度时序的数字信号,例如 PWM 或相移脉冲。它由 6 个子定时器、1 个主定时器和 5 个从定时器组成,共计 10 个高分辨率输出,可成对耦合以进行死区时间插入。它还具有 5 个用于保护目的的故障输入和 10 个用于处理外部事件(如电流限制、零电压或零电流切换)的输入。HRTIM1 定时器由一个数字内核组成,该内核以内核速度计时,后跟延迟线。具有闭环控制的延迟线可保证 217ps 的分辨率,无论电压、温度或芯片到芯片制造工艺偏差如何。在所有操作模式下,10 个输出均提供高分辨率:可变占空比、可变频率和恒定导通时间。关于HRTIM1 计时器,ST 提供了一个写得很好的应用笔记 AN4539,它涵盖了 HRTIM 定时器的所有方面。https://bit.ly/2YjCdmM

        低功耗定时器:该组的定时器专为低功耗应用而设计。由于它们的 clock sources 多样化,这些 timers 能够在所有 power mode (Standby 模式除外) 下保持运行。鉴于这种即使在没有内部 clock source 的情况下也能运行的能力,Low-power timers 可以用作 “pulse counter”,这在某些应用中可能很有用。它们还具有将系统从低功耗模式唤醒的能力。

        下表总结了每个计时器类别最相关的功能。AN4013(http://bit.ly/1WAewd6) 

1.2、STM32 产品组合中定时器的有效可用性

        并非所有 STM32 MCU 都提供所有类型的定时器。这主要取决于 STM32 系列、销售类型和使用的封装。下表总结了所有 STM32 系列中 22 个定时器的分布。星号表示定时器并非在该系列的所有 MCU 中都可用。

        关于上表的一些事项很重要:

        给定一个特定的定时器(例如 TIM1、TIM8 等),其实现(功能、寄存器的数量和类型、生成的中断、DMA 请求、外设互连等)在所有 STM32 MCU 中都是相同的。这保证了为使用特定定时器而编写的固件可以移植到具有相同定时器的其他 MCU 或 STM32 系列。

        属于给定系列的 MCU 中是否存在定时器取决于销售类型和使用的封装。

        STM32F401RE 和 STM32F103RB 不提供基本的计时器。

        “MAX clock speed” 列报告给定 STM32 MCU 中所有定时器的最大时钟速度。这意味着 timer 最大 clock speed 取决于它所连接的 bus。查阅数据表以确定定时器连接到哪条总线,并使用 CubeMX 时钟配置视图来确定配置的总线速度。

        STM32G474RE MCU 于 2021 年初上市,实现了 STM32L 和 STM32F3 系列特有的两个功能:低功耗定时器和高分辨率定时器。

        在处理计时器时,重要的是要有一个务实的方法。否则,很容易迷失在它们的设置和相应的 HAL 例程中(HAL_TIM 和 HAL_TIM_EX 模块是 CubeHAL 中最清晰的模块之一)。因此,我们将开始研究如何使用基本定时器,其功能也与更高级的 STM32 定时器相同。

2、基本定时器

        基本定时器 TIM6、TIM7 和 TIM18 是 STM32 产品组合中最简单的定时器。即使并非所有 STM32 MCU 都提供它们,重要的是要强调 STM32 定时器的设计是为了让更高级的定时器实现与功能较弱的定时器相同的功能(以相同的方式),如下图所示。

        这意味着完全可以像使用基本 timer 一样使用通用 timer。CubeHAL 还反映了这种硬件实现:所有计时器上的基本操作都是使用 HAL_TIM_Base_XXX 函数执行的。使用 C 结构TIM_HandleTypeDef的实例引用单个计时器,该实例按以下方式定义:

typedef struct {
	TIM_TypeDef *Instance; /* Pointer to timer descriptor */
	TIM_Base_InitTypeDef Init; /* TIM Time Base required parameters */
	HAL_TIM_ActiveChannel Channel; /* Active channel */
	DMA_HandleTypeDef *hdma[7]; /* DMA Handlers array */
	HAL_LockTypeDef Lock; /* Locking object */
	__IO HAL_TIM_StateTypeDef State; /* TIM operation state */
} TIM_HandleTypeDef;

        Instance:是指向我们将要使用的 TIM 描述符的指针。例如,TIM6 是大多数 STM32 微控制器中可用的基本定时器之一。

        Init:是 C 结构TIM_Base_InitTypeDef的实例,用于配置基本计时器功能。

        Channel:它表示那些定时器中提供一个或多个输入/输出通道的活动通道的数量(基本定时器不是这种情况)。它可以从 enum HAL_TIM_ActiveChannel 中假设一个或多个值。

        *hdma[7]:这是一个数组,其中包含指向与计时器关联的 DMA 请求的 DMA_HandleTypeDef 描述符的指针。一个计时器最多可以生成 7 个 DMA 请求。

        State:HAL 在内部使用它来跟踪计时器状态。

        所有计时器配置活动都是通过使用 C 结构 TIM_Base_ 的实例 InitTypeDef 来执行的,该实例按以下方式定义:

typedef struct {
	uint32_t Prescaler; /* Specifies the prescaler value used to divide the TIM clock. */
	uint32_t CounterMode; /* Specifies the counter mode. */
	uint32_t Period; /* Specifies the period value to be loaded into the active Auto-Reload Register at the next update event. */
	uint32_t ClockDivision; /* Specifies the clock division. */
	uint32_t RepetitionCounter; /* Specifies the repetition counter value. */
} TIM_Base_InitTypeDef;

        Prescaler:它将定时器时钟除以 1 到 65535 之间的因子(这意味着 prescaler 寄存器具有 16 位分辨率)。例如,如果连接定时器的总线以 48MHz 运行,则等于 48 的预分频器值将计数频率降低到 1MHz。        

        CounterMode:它定义了定时器的计数方向,它可以采用下表中的值之一。某些计数模式仅在通用计时器和高级计时器中可用。对于基本计时器,仅定义 TIM_COUNTERMODE_UP。

        Period:设置计时器计数器再次重新开始计数之前的最大值。对于16位定时器,这可以假设其值从0x1到0xFFFF(65535),对于将TIM2和TIM5定时器实现为32位定时器的MCU中,该值从0x1到0xFFFF FFFF。如果 Period 设置为 0x0则计时器不会启动。

        ClockDivision:此位字段表示内部定时器时钟频率与 ETRx 和 TIx 引脚上的数字滤波器使用的采样时钟之间的分频比。请注意,它与为计时器提供的时钟频率无关。这是 STM32 新手的常见混淆。ClockDivision 可以采用下表中的一个值,并且仅在通用和高级计时器中可用。后面研究 timer 的 input pins 上的数字滤波器。死区时间生成器也使用此字段。

        RepetitionCounter:每个定时器都有一个特定的更新寄存器,用于跟踪定时器溢出/下溢情况。这也可以生成一个特定的 IRQ。RepetitionCounter 表示在设置 update register 之前 timer 溢出 / 下溢的次数,并引发相应的事件(如果启用)。RepetitionCounter 仅在高级计时器中可用。

2.1、在中断模式下使用定时器

        基本计时器:

        是一个自由运行的计数器,从 0 开始计数,直到 TIM_Base_InitTypeDef 初始化结构中 Period字段中指定的值,该值可以假定最大值为 0xFFFF (32 位计时器为 0xFFFF FFFF);

        计数频率取决于连接定时器的总线的速度,通过在初始化结构中设置 Prescaler 寄存器,最多可降低 65536 倍;

        当计时器达到 Period 值时,它会溢出,并设置更新事件 (UEV) 标志;更新事件 (UEV) 被锁存到 prescaler 时钟,并在下一个 clock edge 上自动清除。不要将 UEV 与更新中断标志 (UIF) 混淆,后者必须像其他 IRQ 一样手动清除。仅当启用相应的中断时,才会设置 UIF。UEV 事件与为其他外设设置的所有事件标志一样,允许在 MCU 进入低功耗模式时使用 WFE 指令唤醒 MCU。

        计时器会自动从初始值(基本计时器始终为零)重新开始计数。Period 和 Prescaler 寄存器确定定时器频率,即溢出需要多长时间(可以多久生成一次更新事件),根据以下简单公式:      

  UpdateEvent =\frac{Timer_{clock}}{(Prescaler + 1)(Period + 1)}

        例如,假设一个定时器连接到 STM32F072 MCU 中的 APB1 总线,HCLK 设置为 48MHz,Prescaler 值等于 47999,Period 值等于 499。我们有计时器将在每一次溢出: UpdateEvent = 48000000 /(47999 + 1)/(499 + 1)= 2Hz = 1/2s = 0.5s

        以下代码显示了使用 TIM6的完整示例。这个例子只不过是经典的闪烁 LED,但这次我们使用一个基本的 timer 来计算延迟。

extern TIM_HandleTypeDef htim6;
	
int main(void) {
	HAL_Init();	
	BSP_Init();
	
	htim6.Instance = TIM6;
	htim6.Init.Prescaler = 47999; //48MHz/48000 = 1000Hz
	htim6.Init.Period = 499; //1000HZ / 500 = 2Hz = 0.5s
	
	__HAL_RCC_TIM6_CLK_ENABLE();
	
	HAL_NVIC_SetPriority(TIM6_IRQn, 0, 0);
	HAL_NVIC_EnableIRQ(TIM6_IRQn);
	
	HAL_TIM_Base_Init(&htim6);
	HAL_TIM_Base_Start_IT(&htim6);
	
	while (1);
}       

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim == &htim6) {
	HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_5);
  }
}

        示例使用之前计算的 Prescaler 和 Period 值配置 TIM6。然后使用宏使能 timer peripheral 。然后配置定时器,并使用 HAL_TIM_Base_Start_IT() 函数以中断模式启动。当计时器溢出时,将触发 TIM6_DAC_IRQHandler() ISR,然后调用 HAL_TIM_IRQHandler()。HAL 将自动为我们处理正确管理 update 事件所需的所有操作,并将调用 HAL_TIM_PeriodElapsedCallback() 回调以通知我们计时器已溢出。

        HAL_TIM_IRQHandler() 例程的性能:对于运行得非常快的 timer,HAL_TIM_IRQHandler() 的开销不可忽略。该函数旨在检查多达 9 个不同的中断状态标志,这需要多个 ARM 汇编指令来执行任务。如果需要在最短的时间内处理中断,可能最好自己处理 IRQ。同样,HAL 旨在向用户抽象出许多细节,但它引入了每个嵌入式开发人员都应该了解的性能损失。

        如何选择 Prescaler 和 Period 字段的值?首先,请注意,并非所有 Prescaler 和 Period 值的组合都会导致 timer clock frequency 的整数除法。例如,对于以 48MHz 运行的定时器,等于 65535 的 Period 将定时器频率降低到 7324218 Hz。本文用于除以定时器的主频率,为 Prescaler 值设置一个整数分频器(例如,48MHz 定时器为 47999。根据公式,频率的计算方法是将 Prescaler 和 Period 值加 1),然后使用 Period 值以获得所需的频率。MikroElektronica 提供了一个很好的工具Timer Calculator - MIKROE,可以在给定特定 STM32 MCU 和 HCLK 频率的情况下自动计算这些值。

2.1.1、高级定时器中的时基生成

        到目前为止,我们已经看到定时器的所有基本功能都是通过 TIM_Base_InitTypeDef 结构体的实例配置的。此结构包含一个名为 RepetitionCounter 的字段,用于进一步增加两个连续更新事件之间的时间段:计时器将在设置事件并引发相应的中断之前计算给定次数。RepetitionCounter 仅在高级计时器中可用,这会导致计算更新事件频率的公式变为:

UpdateEvent =\frac{Timer_{clock}}{(Prescaler + 1)(Period + 1)(RepetitionCounter + 1)}

让 RepetitionCounter 等于零(默认行为),我们获得与基本计时器相同的工作模式。

2.2、在轮询模式下使用定时器

        CubeHAL 提供了三种使用计时器的方法:轮询、中断和 DMA 模式。因此,HAL 提供了三种不同的函数来启动计时器:HAL_TIM_Base_Start()、HAL_TIM_Base_Start_IT() 和 HAL_TIM_Base_Start_DMA()。轮询模式背后的想法是连续访问定时器计数器寄存器 (TIMx->CNT) 以检查给定值。但是在轮询计时器时必须小心。例如,在 Web 代码中可以找到如下代码是很常见的:

...
while (1) {
	if(__HAL_TIM_GET_COUNTER(&tim) == value)
...

这种轮询计时器的方式是完全错误的,即使它在某些示例中显然有效。为什么?

        定时器独立于 Cortex-M 内核运行。timer 可以快速计数,最高可达 CPU 内核的相同 clock frequency 。但是检查 timer counter 是否相等(即检查它是否等于给定值)需要几个 ARM 汇编指令,而这些指令又需要几个 clock cycles。无法保证 CPU 在达到配置值的同时访问 counter register (仅当 timer 运行得非常慢时才会发生这种情况)。更好的方法是检查定时器当前计数器值是否等于或大于给定值,或者检查 UIF 标志状态:在最坏的情况下,我们可以进行时间测量偏移,但我们根本不会丢失事件(除非定时器运行得非常快,并且由于中断被屏蔽而丢失了后续事件 - 也就是说, UIF 标记,它仍然设置,然后由我们手动或 HAL 自动清除)。

...
while (1) {
	if(__HAL_TIM_GET_FLAG(&tim) == TIM_FLAG_UPDATE) {
		//Clear the IRQ flag otherwise we lose other events
		__HAL_TIM_CLEAR_IT(htim, TIM_IT_UPDATE);
...

        话虽如此,定时器是异步外设,管理溢出/下溢事件的正确方法是使用中断。没有理由不在中断模式下使用定时器,除非定时器运行得非常快,并且在几微秒(甚至纳秒)后生成中断会完全淹没 MCU,阻止其处理其他指令。即使 Cortex-M MCU 中的异常处理具有确定性延迟(Cortex-M3/4/7/33 内核在 12 个 CPU 周期内提供中断,而 Cortex-M0 在 15 个周期内提供中断,Cortex-M0+ 在 16 个周期中提供中断),它也具有不可忽视的成本,这在“低速”MCU 中需要几纳秒(例如,对于运行频率为 48MHz 的 STM32F072 MCU, 中断大约需要 300 ns)。如前所述,此成本必须添加到 HAL 在中断管理期间引入的开销中。

2.3、在 DMA 模式下使用定时器

        定时器通常被编程为在 DMA 模式下工作,尤其是当它们用于触发其他外设时。此模式可保证计时器执行的操作是确定性的,并且具有尽可能小的延迟,尤其是在它们运行速度较快的情况下。此外,Cortex-M 内核被从计时器管理释放,只涉及处理可能使 CPU 拥塞的频繁 ISR。最后,在某些高级模式中,例如输出 PWM 模式,如果不在 DMA 模式下使用定时器,几乎不可能达到给定的开关频率。

        由于这些原因,计时器最多提供 7 个 DMA 请求,如 下表中所列。        基本定时器仅实现 TIM_DMA_UPDATE 请求,因为它们没有输入/输出 I/O。但是,在我们希望按时间执行 DMA 传输的情况下,利用 TIMx_UP 请求非常有用。

        以下示例是闪烁 LED 应用程序的另一种变体,但这次我们在 DMA 模式下使用定时器来打开/关闭 LED。在这里,我们将使用编程为每 500ms 溢出一次的 TIM6 定时器:发生这种情况时,定时器会生成 TIM6_UP 请求(在 STM32F072 MCU 中,该请求绑定到 DMA1 的第三个通道),缓冲区的下一个元素在 DMA 循环模式下传输到 GPIOB->ODR 寄存器,这会导致 LED无限期闪烁。        

        在 STM32F2/F4/F7/L1/L4 系列中,只有 DMA2 具有对总线矩阵的完全访问权限。这意味着只有其请求绑定到此 DMA 控制器的 timers 才能用于执行涉及其他外设的传输(内部和外部 volatile memorys 除外)。因此,基于 F2/F4/L1/L4 MCU 的demo板示例使用 TIM1 作为时基发生器。

int main(void) {
	uint8_t data[] = {0xFF, 0x0};
	
	HAL_Init();
	BSP_Init();
	
	htim6.Instance = TIM6;
	htim6.Init.Prescaler = 47999; //48MHz/48000 = 1000Hz
	htim6.Init.Period = 499; //1000HZ / 500 = 2Hz = 0.5s
	
	__HAL_RCC_TIM6_CLK_ENABLE();
	
	HAL_TIM_Base_Init(&htim6);
	
	hdma_tim6_up.Instance = DMA1_Channel3;
	hdma_tim6_up.Init.Direction = DMA_MEMORY_TO_PERIPH;
	hdma_tim6_up.Init.PeriphInc = DMA_PINC_DISABLE;
	hdma_tim6_up.Init.MemInc = DMA_MINC_ENABLE;
	hdma_tim6_up.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
	hdma_tim6_up.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
	hdma_tim6_up.Init.Mode = DMA_CIRCULAR;
	hdma_tim6_up.Init.Priority = DMA_PRIORITY_LOW;
	HAL_DMA_Init(&hdma_tim6_up);
	
	HAL_TIM_Base_Start(&htim6);
	HAL_DMA_Start(&hdma_tim6_up, (uint32_t)data, (uint32_t)&GPIOB->ODR, 2);
	__HAL_TIM_ENABLE_DMA(&htim6, TIM_DMA_UPDATE);
	
	while (1);
}

        示例在循环模式下配置DMA1_Channel3的 DMA_HandleTypeDef。然后开始 DMA 传输,以便每次生成 TIM6_UP 请求时,数据缓冲区的内容都会在 GPIOB->ODR 寄存器内传输,即定时器溢出。这会导致 LED LED 闪烁。请注意,我们在这里没有使用 HAL_TIM_Base_Start_DMA() 函数。为什么不呢?

        查看 HAL_TIM_Base_Start_DMA() 例程的实现,您可以看到 ST 已经对其进行了定义,以便执行从内存缓冲区到 TIM6->ARR 的 DMA 传输。

HAL_TIM_Base_Start_DMA(TIM_HandleTypeDef *htim, uint32_t *pData, uint16_t Length) {
...
	/* Enable the DMA channel */
	HAL_DMA_Start_IT(htim->hdma[TIM_DMA_ID_UPDATE], (uint32_t)pData, (uint32_t)&htim->Instance->ARR, Length);
	/* Enable the TIM Update DMA request */
	__HAL_TIM_ENABLE_DMA(htim, TIM_DMA_UPDATE);
...

        基本上,我们只能使用 HAL_TIM_Base_Start_DMA() 在每次溢出时更改计时器 Period。因此,我们需要自己配置 DMA 才能执行此传输。

        在下一章中,我们将看到一个更有用的应用,即如何在 DMA 模式下使用定时器来定期执行 ADC 转换。

2.4、停止计时器

        CubeHAL 提供了三个函数来停止正在运行的计时器:HAL_TIM_Base_Stop()、HAL_TIM_Base_Stop_IT() 和 HAL_TIM_Base_Stop_DMA()。我们根据我们使用的定时器模式选择其中之一(例如,如果我们在中断模式下启动了一个定时器,那么我们需要使用 HAL_TIM_Base_Stop_IT() 例程来停止它)。每个功能都旨在正确禁用 IRQ 和 DMA 配置。

2.5、使用 CubeMX 配置基本计时器

        CubeMX 可以将配置基本计时器所需的工作量减少到最低限度。通过选中标志 Activated 启用计时器后,可以从 Configuration 视图对其进行配置。timer 配置视图允许设置 Prescaler 和 Period 寄存器的值,如下图所示。CubeMX 将在 MX_TIMx_Init() 函数中生成所有必要的初始化代码。此外,始终在同一个 configuration 对话框中,可以启用与定时器相关的 IRQ 和 DMA 请求。

3、通用定时器

        大多数STM32定时器都是通用的。与之前看到的基本定时器不同,它们提供了更多的交互功能,这要归功于多达四个独立的通道,可用于测量输入信号,按时间输出信号,以生成脉宽调制 (PWM) 信号。然而,通用计时器提供了更多的功能,我们将在本章的这一部分逐步发现。

3.1、带外部时钟源的时基发生器

        下图显示了通用定时器的框图。图表的某些部分已被掩盖:我们稍后将更深入地研究它们。当选择 APB 时钟作为源时,以红色突出显示的路径用于馈送定时器:内部时钟CK_INT馈送给 Prescaler (PSC),这反过来又决定了 Counter Register (CNT) 的增加/减少速度。将此与 auto-reload register 的内容进行比较 (该寄存器填充了 TIM_Base_InitTypeDef.Period 字段的值)。当它们匹配时,将生成 UEV 事件,并触发相应的 IRQ(如果启用)。

        查看上图,我们可以看到定时器可以从其他来源接收 “激励”。这些可以分为两大类:

        Clock sources,用于为 timer 计时。它们可以来自连接到 MCU 引脚的外部源,也可以来自内部连接到 MCU 的其他定时器。请注意,没有时钟源,定时器就无法工作,因为时钟源是用来增加计数器寄存器的。

        触发源,用于将定时器与连接到 MCU 引脚的外部源或内部连接的其他定时器同步。例如,可以将计时器配置为在外部事件触发时开始计数。在这种情况下,定时器由另一个 clock source (可以是 APBx 总线或连接到 ETR2 pin的外部 clock source) 计时,并由另一个器件控制(即,当它开始计数等时)。

        根据定时器类型及其实际实现,定时器可以从以下位置进行计时:

       • RCC 提供的内部TIMx_CLK

       • 内部触发输入 0 至 3

        – ITR0、ITR1、ITR2 和 ITR3 使用另一个定时器(主)作为该定时器(从机)的预分频器        

       • 外部输入通道引脚

        – 引脚 1: TI1FP1 或 TI1F_ED

        – 引脚 2:TI2FP2

       • 外部 ETR 引脚:

        – ETR1 引脚

        – ETR2 引脚

相反,定时器可以由以下位置触发:

       • 内部触发输入 0 至 3

        – ITR0、ITR1、ITR2 和 ITR3 使用另一个定时器作为主定时器

       • 外部输入通道引脚

        – 引脚 1: TI1FP1 或 TI1F_ED

        – 引脚 2:TI2FP2

       • 外部 ETR1 引脚

让我们通过分析实际示例来研究这些从外部源计时/触发定时器的方法。

3.1.1、外部时钟模式 2

        通用定时器能够从外部源进行计时,将它们设置为两种不同的模式: External Clock Source Mode 1 和 2。当定时器配置为 slave 模式时,第一个可用。我们将在下一段中研究这种模式。

        相反,第二种模式只需使用 external clock source 即可激活。这允许使用更准确和专用的源,并最终进一步降低计数频率。事实上,当选择 External Clock Source Mode 2 时,计算更新事件频率的公式变为:

UpdateEvent =\frac{EXT_{clock}}{(EXT_{clock}Prescaler)(Prescaler + 1)(Period + 1)(RepetitionCounter + 1)}

其中 EXTclock 是外部源的频率,EXTclockPrescaler 是可以假设值 1 的源分频器, 2、4 和 8。

        通用定时器的时钟源可以通过函数 HAL_TIM_ConfigClockSource() 和结构体TIM_ClockConfigTypeDef的实例来选择,其定义方式如下:

typedef struct {
	uint32_t ClockSource; /* TIM clock sources */
	uint32_t ClockPolarity; /* TIM clock polarity */
	uint32_t ClockPrescaler; /* TIM clock prescaler */
	uint32_t ClockFilter; /* TIM clock filter */
} TIM_ClockConfigTypeDef;

        ClockSource:指定用于偏置定时器的时钟信号源。它可以假设下表中的值。默认情况下,TIM_CLOCKSOURCE_INTERNAL 模式处于选中状态。

        ClockPolarity:表示用于偏置定时器的时钟信号的极性。它可以采用下表中的值。默认情况下,TIM_CLOCKPOLARITY_RISING 模式处于选中状态。

        ClockPrescaler:指定外部 clock source 的 prescaler。它可以采用下表中的值。默认情况下,TIM_CLOCKPRESCALER_DIV1 值处于选中状态。

        ClockFilter:此 4 位字段定义用于对外部时钟信号进行采样的频率以及应用于它的数字滤波器的长度。数字滤波器由一个事件计数器组成,其中需要 N 个连续事件来验证输出上的转换。默认情况下,筛选器处于禁用状态。

        让我们构建一个示例,演示如何为 TIM3 timer使用外部 clock source。该示例包括将主时钟输出 (MCO) 引脚路由到 TIM3_ETR2 引脚。MCO pin 已启用并连接到 HSI clock source。下面的代码显示了该示例最相关的部分。

int main(void) {
	HAL_Init();
	SystemClock_Config();
	MX_GPIO_Init();
	MX_TIM3_Init();
	HAL_TIM_Base_Start_IT(&htim3);
	while (1);
}

void MX_TIM3_Init(void) {
	TIM_ClockConfigTypeDef sClockSourceConfig;
	
	htim3.Instance = TIM3;
	htim3.Init.Prescaler = 999;
	htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
	htim3.Init.Period = 3999;
	htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
	htim3.Init.RepetitionCounter = 0;
	HAL_TIM_Base_Init(&htim3);
	
	sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_ETRMODE2;
	sClockSourceConfig.ClockPolarity = TIM_CLOCKPOLARITY_NONINVERTED;
	sClockSourceConfig.ClockPrescaler = TIM_CLOCKPRESCALER_DIV1;
	sClockSourceConfig.ClockFilter = 0;
	HAL_TIM_ConfigClockSource(&htim3, &sClockSourceConfig);
	
	HAL_NVIC_SetPriority(TIM3_IRQn, 0, 0);
	HAL_NVIC_EnableIRQ(TIM3_IRQn);
}
	
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* htim_base) {
	GPIO_InitTypeDef GPIO_InitStruct;
	if(htim_base->Instance==TIM3) {
	/* Peripheral clock enable */
	__HAL_RCC_TIM3_CLK_ENABLE();
	__HAL_RCC_GPIOD_CLK_ENABLE();
	
	/**TIM3 GPIO Configuration
	PD2 ------> TIM3_ETR
	*/
	GPIO_InitStruct.Pin = GPIO_PIN_2;
	GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
	GPIO_InitStruct.Pull = GPIO_NOPULL;
	HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);
	}
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
	if (htim == &htim3) {
		HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_5);
	}
}

        示例配置 TIM3 定时器,将其预分频器设置为 999,将 period 设置为 3999。配置 TIM3 的外部 clock source。由于 HSI 振荡器以 8MHz运行,因此使用公式我们可以计算 UEV 频率,等于: UpdateEvent = 8000000 /(1)/(999 + 1)/(3999 + 1)/(0 + 1) = 2Hz = 0.5s。 最后启用 TIM3 并将 PD2 引脚(对应于 TIM3_ETR2 引脚)配置为输入源。

        需要注意的是,必须先启用 GPIO 端口 D,然后才能使用 __GPIOD_CLK_ENABLE() 宏将其用作 TIM3 的 clock source 。这同样适用于TIM3,它是通过使用__TIM3_CLK_ENABLE()使能的:这是必需的,因为外部时钟不直接馈送给预分频器,而是首先通过专用逻辑块与APBx时钟同步。

3.1.2、外部时钟模式 1

        STM32 通用和高级定时器可以配置为在主模式或从模式下工作。当配置为从属设备时,定时器可以由内部 ITR0、ITR1、ITR2 和 ITR3 线路、连接到 ETR1 引脚的外部时钟或连接到 TI1FP1 和 TI2FP2 源的其他时钟源馈送(对应于通道 1 和 2 输入引脚)。这种工作模式称为外部时钟模式1。

        外部时钟模式 1 和 2 对于 STM32 平台的所有新手来说都相当混乱。这两种模式都是一种使用外部时钟源为定时器计时的方法,但第一种是通过在 slave 模式下配置 timer 来实现的(这确实是一种 “triggering” 形式),而第二种模式是通过简单地选择不同的 clock source 获得的。需要注意的是,在 ETR1 或 ETR2 模式下配置定时器的方法完全不同。

        TI1FP1 和 TI2FP2 输入只不过是应用输入滤波器后定时器的 TI1 和 TI2 输入通道。要在 slave 模式下配置定时器,我们使用函数 HAL_TIM_SlaveConfigSynchro() 和 struct TIM_SlaveConfigTypeDef 的实例,其定义方式如下:

typedef struct {
	uint32_t SlaveMode; /* Slave mode selection */
	uint32_t InputTrigger; /* Input Trigger source */
	uint32_t TriggerPolarity; /* Input Trigger polarity */
	uint32_t TriggerPrescaler; /* Input trigger prescaler */
	uint32_t TriggerFilter; /* Input trigger filter */
} TIM_SlaveConfigTypeDef;

        SlaveMode:当定时器配置为 Slave 模式时,它可以由多个源计时/触发。此字段可以采用 下表中的值。本段是关于 TIM_SLAVEMODE_EXTERNAL1 模式的。

        InputTrigger:定义触发/计时在从模式下配置的计时器的源。它取值下表。

        TriggerPolarity: 表示触发器/时钟源的极性。它可以采用下表中的值。

        TriggerPrescaler:指定外部 clock source 的 prescaler。它可以采用下表中的值。默认情况下,TIM_TRIGGERPRESCALER_DIV1 值处于选中状态。        TriggerFilter:此 4 位字段定义用于对连接到输入引脚的外部时钟/触发信号进行采样的频率以及应用于它的数字滤波器的长度。数字滤波器由一个事件计数器组成,其中需要 N 个连续事件来验证输出上的转换。默认情况下,筛选器处于禁用状态。

        当选择外部时钟源模式 1 时,计算更新事件频率的公式变为:

UpdateEvent =\frac{TRGI_{clock}}{(Prescaler + 1)(Period + 1)(RepetitionCounter + 1)}

其中 TRGIclock 是连接到 ETR1 引脚的时钟源的频率,也是连接到内部线路 ITR0~ITR3 的内部/外部触发时钟源的频率或连接到外部通道 TI1FP1~T2FP2 的信号频率。

        那么,让我们回顾一下到目前为止所看到的:

        当定时器仅在主模式下工作时,通过将该源连接到 ETR2 引脚,可以由外部源计时;

        如果定时器工作在从属模式,则可以通过连接到 ETR1 引脚的信号、连接到内部线路 ITR0~ITR2 的任何触发源来计时(因此, clock source 只能是另一个 timer) 或通过连接到定时器通道 TI1 和 TI2 的 input 信号,如果 input filtering stage 被激活,则变为 TI1FP1 和 TI2FP2。

        让我们构建另一个示例,演示如何为 TIM3 timer使用外部 clock source。该示例包括将主时钟输出 (MCO) 引脚连接到 TI1FP1 引脚(即 TIM3 定时器的第一个通道),该引脚对应于 PA6 引脚。MCO pin 已启用并连接到 HSI clock source,如前面的示例所示。下面的代码显示了该示例最相关的部分。

int main(void) {
	HAL_Init();
	SystemClock_Config();
	MX_GPIO_Init();
	MX_TIM3_Init();
	HAL_TIM_Base_Start_IT(&htim3);
	while (1);
}

void MX_TIM3_Init(void) {
	TIM_SlaveConfigTypeDef sSlaveConfig;
	
	htim3.Instance = TIM3;
	htim3.Init.Prescaler = 999;
	htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
	htim3.Init.Period = 3999;
	htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
	HAL_TIM_Base_Init(&htim3);
	
	sSlaveConfig.SlaveMode = TIM_SLAVEMODE_EXTERNAL1;
	sSlaveConfig.InputTrigger = TIM_TS_TI1FP1;
	sSlaveConfig.TriggerPolarity = TIM_TRIGGERPOLARITY_RISING;
	sSlaveConfig.TriggerFilter = 0;
	HAL_TIM_SlaveConfigSynchro(&htim3, &sSlaveConfig);
	
	HAL_NVIC_SetPriority(TIM3_IRQn, 0, 0);
	HAL_NVIC_EnableIRQ(TIM3_IRQn);
}
	
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* htim_base) {
	GPIO_InitTypeDef GPIO_InitStruct;
	if(htim_base->Instance==TIM3) {
	/* Peripheral clock enable */
	__HAL_RCC_TIM3_CLK_ENABLE();
	__HAL_RCC_GPIOA_CLK_ENABLE();
	
	/**TIM3 GPIO Configuration
	PA6 ------> TIM3_CH1
	*/
	GPIO_InitStruct.Pin = GPIO_PIN_6;
	GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
	if (htim == &htim3) {
		HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_5);
	}
}

        示例在从模式下配置 TIM3。输入触发源设置为 TI1FP1,定时器与输入信号的上升沿同步。最后将 PA6 配置为 TIM3 第二个通道的输入引脚。

3.1.3、使用 CubeMX 配置通用定时器的源时钟

        配置通用定时器的时钟源可能是一场噩梦,尤其是对于 STM32 平台的新手。CubeMX 可以简化此过程,即使需要对主/从模式以及 ETR1 和 ETR2 模式有很好的了解。要在 External Clock Mode 2 中配置 timer,从 Configuration 窗格中选择 ETR2 作为 clock source 就足够了,如下图所示。

        选择时钟源后,可以从配置中设置外部时钟滤波器、极性和预分频器,如下图所示。

        要在外部时钟模式 1 中配置定时器,我们必须从 Slave 条目中选择此模式,然后选择 Trigger Source(在本例中是定时器的时钟源),如下图所示。

3.2、主/从同步模式

        一旦定时器在主模式下运行,它就可以通过称为触发输出 (TRGO)的专用输出线为另一个配置为从模式的定时器馈电,该输出线连接到称为 ITR0、ITR1、ITR2 和 ITR3 的内部专用线路。主 timer 既可以提供 clock source (因此充当一阶 prescaler - 这就是我们在上一段中学习的内容)或触发 slave timer。

        这些内部触发 (ITR) 线(ITR0、ITR1、ITR2 和 ITR3)精确地位于芯片内部,每条线都在两个定义的计时器之间硬连线。例如,在 STM32F072 MCU 中,TIM1 TRGO 线连接到 TIM0 定时器的 ITR2 线,如下图所示。

        配置为从属定时器的定时器也可以同时充当另一个定时器的主定时器,从而允许创建复杂的定时器网络。例如,下图显示了定时器如何级联连接,

        而下图显示了定时器如何使用主/从模式的组合形成层次结构。请注意,TIM1、TIM2 和 TIM3 通过同一条 ITR0 线路在内部互连。这允许在同一事件(reset、enable、update 等)上同步多个 timer。

        要在主模式下配置定时器,我们使用函数 HAL_TIMEx_MasterConfigSynchronization() 和结构体TIM_MasterConfigTypeDef实例,其定义方式如下:

typedef struct {
	uint32_t MasterOutputTrigger; /* Trigger output (TRGO) selection */
	uint32_t MasterSlaveMode; /* Master/slave mode selection */
} TIM_MasterConfigTypeDef;

        MasterOutputTrigger:指定 TRGO 输出的行为,它可以采用下表中的值。

        MasterSlaveMode:用于启用/禁用定时器的主/从模式。它可以采用 TIM_MASTERSLAVEMODE_ENABLE 或 TIM_MASTERSLAVEMODE_DISABLE 的值。

        让我们看一个例子,说明如何在级联模式下配置 TIM5 和 TIM3,其中 TIM5 作为 TIM3 定时器的主控。TIM5 通过 ITR2 线用作 TIM3 的 clock source 。此外,TIM5 经过配置,使其在其 TI1FP1 线路上的外部事件开始计数,对应于 PA0 引脚:当 PA0 引脚变为高电平(用按键实现)时,TIM5 开始计数,然后通过 ITR2 线路馈送给 TIM3 定时器。

int main(void) {
	HAL_Init();
	SystemClock_Config();
	
	MX_GPIO_Init();
	MX_TIM3_Init();
	MX_TIM5_Init();

	HAL_TIM_Base_Start_IT(&htim3);

  while (1);
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
	if (htim == &htim3) {
		HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_5);
	}
}

void MX_TIM5_Init(void) {
	TIM_ClockConfigTypeDef sClockSourceConfig;
	TIM_MasterConfigTypeDef sMasterConfig;
	TIM_SlaveConfigTypeDef sSlaveConfig;
	
	htim5.Instance = TIM5;
	htim5.Init.Prescaler = 7199;
	htim5.Init.CounterMode = TIM_COUNTERMODE_UP;
	htim5.Init.Period = 2499;
	htim5.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
	htim5.Init.RepetitionCounter = 0;
	HAL_TIM_Base_Init(&htim5);
	
	sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
	HAL_TIM_ConfigClockSource(&htim5, &sClockSourceConfig);
	
	sSlaveConfig.SlaveMode = TIM_SLAVEMODE_TRIGGER;
	sSlaveConfig.InputTrigger = TIM_TS_TI1FP1;
	sSlaveConfig.TriggerPolarity = TIM_TRIGGERPOLARITY_RISING;
	sSlaveConfig.TriggerFilter = 15;
	HAL_TIM_SlaveConfigSynchron(&htim5, &sSlaveConfig);
	
	sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE;
	sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_ENABLE;
	HAL_TIMEx_MasterConfigSynchronization(&htim5, &sMasterConfig);
}
	
void MX_TIM3_Init(void) {
	TIM_SlaveConfigTypeDef sSlaveConfig;
	
	htim3.Instance = TIM3;
	htim3.Init.Prescaler = 0;
	htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
	htim3.Init.Period = 1;
	htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
	HAL_TIM_Base_Init(&htim3);
	
	sSlaveConfig.SlaveMode = TIM_SLAVEMODE_EXTERNAL1;
	sSlaveConfig.InputTrigger = TIM_TS_ITR2;
	HAL_TIM_SlaveConfigSynchro(&htim3, &sSlaveConfig);
	
	HAL_NVIC_SetPriority(TIM3_IRQn, 0, 0);
	HAL_NVIC_EnableIRQ(TIM3_IRQn);
}
	
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* htim_base) {
	GPIO_InitTypeDef GPIO_InitStruct;
	if(htim_base->Instance==TIM3) {
		__HAL_RCC_TIM3_CLK_ENABLE();
	}
	
	if(htim_base->Instance==TIM5) {
		__HAL_RCC_TIM5_CLK_ENABLE();
		__HAL_RCC_GPIOA_CLK_ENABLE();
    /**TIM5 GPIO Configuration
    PA0-WKUP     ------> TIM5_CH1
    */
    GPIO_InitStruct.Pin = GPIO_PIN_0;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
	}
}

        示例将 TIM5 配置为从内部 APB1 总线进行计时。将 TIM5 配置为从模式,以便在 TI1FP1 线路变为高电平时(即被触发)开始计数。PA0 GPIO进行了相应的配置。请注意,TriggerFilter 设置为最大电平(如果将其设置为零,即使简单地触摸连接到 PA0 引脚的电线,也很容易意外触发计时器)。

        将 TIM5 配置为在主模式下工作。每次生成更新事件时,计时器都会触发其内部线路(连接到 TIM3 的 ITR2 线路)。最后,将 TIM3 配置为 External Clock Mode 1,选择 ITR2 行作为源时钟。

        为了使 LED 每 500ms (2Hz) 闪烁一次,TIM1 周期设置为 2499,因此 TIM5 的更新频率为 4Hz。应用等式,我们得到: UpdateEvent = 4Hz /(0 + 1)/(1 + 1)/(0 + 1) = 2Hz = 0.5s。 请记住,Period 字段不能设置为零。

        要触发 TIM5,您必须将 PA0 引脚连接到 +3V3 源(在我的实验中将PA0引脚连接到了轻触开关,这样按一下就相当于上升沿脉冲)。请注意,我们没有为 TIM5 定时器调用 HAL_TIM_Base_Start() 函数(参见 main() 例程),因为定时器是在通道 1 上生成触发事件时启动的(即,当我们将 PA0 引脚连接到 +3V3 源时)。

3.2.1、启用与触发相关的中断

        当定时器工作在从模式时,如果启用IRQ,则每次发生指定的触发事件时都会引发定时器 IRQ。例如,当主时钟因更新事件触发时(TRGO信号),从定时器的 IRQ 触发,我们可以通过定义回调来通知我们: 

void HAL_TIM_TriggerCallback(TIM_HandleTypeDef *htim) {
	...
}

        默认情况下,HAL_TIM_Base_Start_IT() 不启用这种类型的中断。我们必须使用函数 HAL_TIM_SlaveConfigSynchron_IT(),而不是函数 HAL_TIM_SlaveConfigSynchron()。显然,必须定义相应定时器的 ISR,并且必须从中调用函数 HAL_TIM_IRQHandler()。

3.2.2、使用 CubeMX 配置主/从同步

        要从 CubeMX 配置slave模式的定时器,只需从 IP 窗格树(从属模式组合框)中选择所需的触发模式(重置模式、门控模式、触发模式),然后选择触发源即可,如下图所示。

请记住,配置为 slave 模式且未在 External Clock Mode 1 下工作的 timer 必须从内部 clock 或 ETR2 clock source计时。

        相反,要启用主模式,我们必须从定时器配置视图中选择此模式,如下图所示。

选择主模式后,可以选择 TRGO 源事件。

3.3、通过软件生成定时器相关事件

        定时器通常会在满足给定条件时生成事件。例如,当计数器寄存器 (CNT) 与 Period 值匹配时,它们会生成更新事件 (UEV)。但是,我们可以强制定时器通过软件生成特定事件。每个定时器都提供了一个专用寄存器,名为 Event Generator (EGR)。此 register 的某些位用于触发与 timer 相关的事件。例如,第一个位名为 Update Generator (UG),允许在设置时生成 UEV 事件。一旦生成事件,此位就会自动清除。

        为了通过软件生成事件,HAL 提供了以下功能:

HAL_StatusTypeDef HAL_TIM_GenerateEvent(TIM_HandleTypeDef *htim, uint32_t EventSource);

它接受指向定时器句柄和要生成的事件的指针。EventSource 参数可以采用下表中的一个值。

        TIM_EVENTSOURCE_UPDATE 起着两个重要作用。第一个与定时器运行时 Period 寄存器(即 TIMx->ARR 寄存器)的更新方式有关。默认情况下,当生成 ARR 事件时,ARR 寄存器的内容将传输到内部影子寄存器TIM_EVENTSOURCE_UPDATE,除非定时器的配置不同。

        当主定时器的 TRGO 输出设置为 TIM_TRGO_RESET 模式时,TIM_EVENTSOURCE_UPDATE 事件也很有用:在这种情况下,只有当 TIMx->EGR 寄存器用于生成 TIM_EVENTSOURCE_UPDATE 事件(即设置 UG 位)时,才会触发从定时器。

        以下代码显示了软件事件生成的工作原理。TIM3 和 TIM5 是两个定时器,分别配置为主模式和从模式。TIM5 配置为在 ETR1 模式下工作(即,它由主定时器计时)。TIM3 配置为在设置 TIM3->EGR 寄存器的 UG 位时触发 TRGO 输出(内部连接到 TIM5 的 ITR1 线路)。最后,我们每 200 毫秒从 main() 例程手动生成一次 UEV 事件。

int main(void) {
	...
	HAL_TIM_Base_Start_IT(&htim3);
	HAL_TIM_Base_Start_IT(&htim5);
	while (1) {
		HAL_TIM_GenerateEvent(&htim3, TIM_EVENTSOURCE_UPDATE);
		HAL_Delay(200);
	}
	...
}
void MX_TIM3_Init(void){
	TIM_ClockConfigTypeDef sClockSourceConfig;
	TIM_MasterConfigTypeDef sMasterConfig;
	
	htim3.Instance = TIM3;
	htim3.Init.Prescaler = 7199;
	htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
	htim3.Init.Period = 9999;
	htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
	HAL_TIM_Base_Init(&htim3);
	
	sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
	HAL_TIM_ConfigClockSource(&htim3, &sClockSourceConfig);
	
	sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
	sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_ENABLE;
	HAL_TIMEx_MasterConfigSynchronization(&htim3, &sMasterConfig);
}
void MX_TIM5_Init(void) {
	TIM_SlaveConfigTypeDef sSlaveConfig;
	
	htim5.Instance = TIM5;
	htim5.Init.Prescaler = 0;
	htim5.Init.CounterMode = TIM_COUNTERMODE_UP;
	htim5.Init.Period = 1;
	htim5.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
	HAL_TIM_Base_Init(&htim5);
	
	sSlaveConfig.SlaveMode = TIM_SLAVEMODE_EXTERNAL1;
	sSlaveConfig.InputTrigger = TIM_TS_ITR1;
	HAL_TIM_SlaveConfigSynchro(&htim5, &sSlaveConfig);//HAL_TIM_SlaveConfigSynchro_IT
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
	if (htim == &htim5) {
		HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_5);
	}
}

3.4、计数模式

        在本章的开头,我们已经看到一个基本的计时器从 0 计数到给定的 Period 值。通用定时器和高级定时器可以以其他不同的方式计数。下图显示了三种主要的计数模式。

        当定时器在 TIM_COUNTERMODE_DOWN 模式下计数时,它从 Period 值开始并倒计时到零:当计数器到达末尾时,将引发计时器 IRQ 并设置 UIF 标志(即,生成更新事件并且 HAL 调用 HAL_TIM_PeriodElapsedCallback())。

        相反,当定时器在 TIM_COUNTERMODE_CENTERALIGNED 模式下计数时,它开始从 0 到 Period 值计数:这会导致引发计时器 IRQ 并设置 UIF 标志(即,生成更新事件并且 HAL_TIM_PeriodElapsedCallback() 由 HAL 调用)。然后 timer 开始倒计时到 0 并生成另一个 update 事件(以及相应的 IRQ)。

3.5、输入捕获模式

        通用定时器未设计为用作时基发生器。即使完全可以使用它们来完成这项工作,也可以使用其他定时器(如基本计时器和 SysTick 计时器)来执行此任务。通用计时器提供更高级的功能,可用于驱动其他与时间相关的重要活动。

        下图显示了通用定时器中输入通道的结构。

        每个 input 都连接到一个 edge detector,该检测器还配备了一个用于 “debounce” 输入信号的滤波器。边缘检测器的输出进入源多路复用器(IC1、IC2 等)。如果给定的 I/O 分配给另一个外设,这允许 “重新映射” 输入通道。最后,一个专用的预分频器允许 “减慢” 输入信号的频率,以便匹配定时器的运行频率。

        通用和高级定时器提供的输入捕获模式允许计算施加到这些定时器提供的 4 个通道中的每一个的外部信号的频率。并且每个通道的捕获都是独立执行的。

        上图显示了 capture 过程是如何工作的。TIMx 是一个定时器,配置为在给定的 TIMx_CLK 时钟频率下工作。(定时器时钟频率与定时器的工作方式无关,在本例中为输入捕获模式。定时器时钟取决于总线频率或外部时钟源以及相关的 prescaler 设置。) 这意味着它每 1 TIMx_CLK 秒递增一次 TIMx_CNT 寄存器,直到 Period 值。假设我们将方波信号应用于其中一个定时器通道,并且假设我们将定时器配置为在输入信号的每个上升沿触发。这样,在每次检测到的触发时,TIMx_CCRx寄存器将替换为 TIMx_CNT 寄存器的内容。发生这种情况时,定时器将生成相应的中断或 DMA 请求,从而允许跟踪计数器值。

        要获取外部信号周期,需要两次连续捕获。周期的计算方法是这两个值相减, CNT0(上图中的值 4)和 CNT1(上图中的值 20),并使用以下公式:

Period = Capture \cdot \left ( \frac{TIMx\; \; CLK}{\left ( Prescaler + 1 \right )\left ( CH_{Prescaler} \right )\left ( Polarity_{Index} \right )} \right )^{-1}

其中:Capture = CNT1 − CNT0,  if CNT0 < CNT1

           Capture = (TIMx_Period − CNT0) + CNT1,  if CNT0 > CNT1

        CHPrescaler是可应用于输入通道的进一步的预分频器。如果通道配置为在输入信号的上升沿或下降沿触发,则 PolarityIndex 等于 1, 或者,如果对两条边都进行了采样,则它等于 2。

        另一个相关条件是 UEV 频率应低于采样信号频率。这很重要的原因很明显: 如果 timer 运行得比采样信号快,那么它将在对信号边缘进行采样之前溢出(即,它用完了 Period 计数器)(参见下图)。因此,通常将 Period 值设置为最大值,并增加 Prescaler 因子以降低计数频率。

        为了配置输入通道,我们使用函数 HAL_TIM_IC_ConfigChannel() 和 C 结构体TIM_IC_InitTypeDef的实例,其定义如下:

typedef struct {
	uint32_t ICPolarity; /* Specifies the active edge of the input signal. */
	uint32_t ICSelection; /* Specifies the input. */
	uint32_t ICPrescaler; /* Specifies the Input Capture Prescaler. */
	uint32_t ICFilter; /* Specifies the input capture filter. */
} TIM_IC_InitTypeDef;

        ICPolarity:指定输入信号的极性,可以采用下表中的值。

        ICSelection:指定计时器使用的输入。它可以采用下表中的值。可以选择性地将输入通道重新映射到不同的输入源,即 (IC1,IC2) 映射到 (TI2,TI1),(IC3,IC4) 映射到 (TI4,TI3)。通常,这用于区分 Ton 不同于 Toff 的信号的上升沿和下降沿捕获。也可以捕获连接到ITR0~ITR3的同一内部通道TRC。

        ICPrescaler:配置给定输入的预分频器阶段。它可以为下表中的值。

        ICFilter:此 4 位字段定义用于对连接到TIMx_CHx引脚的外部时钟信号进行采样的频率以及应用于该引脚的数字滤波器的长度。对输入信号进行去抖很有用。

        重新编写本章的示例 2,以便通过 TIM3 定时器的通道 1 对 PB5 引脚(连接到 LED 的那个)的开关频率进行采样(在实验中,需要用跳线将PB5与 TIM3定时器通道1引脚PA6 引脚短接起来)。因此,我们将通道 1 配置为 input capture pin,并在 DMA 模式下对其进行配置,以便它在检测到 input 信号的上升沿时触发 TIM_DMA_ID_CC1 请求以自动填充临时缓冲区,该缓冲区存储 TIM3_CNT register 的值。在我们分析 main() 函数之前,最好先看一下 TIM3 初始化例程。

TIM_HandleTypeDef htim3;
TIM_HandleTypeDef htim6;
DMA_HandleTypeDef hdma_tim3_ch1_trig;
DMA_HandleTypeDef hdma_tim6_up;

//TIM3 input capture settings for frequency measurment
/* TIM3 init function */
void MX_TIM3_Init(void) {
	TIM_IC_InitTypeDef sConfigIC;
	
	htim3.Instance = TIM3;
	htim3.Init.Prescaler = 3599;
	htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
	htim3.Init.Period = 65535;
	htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
	HAL_TIM_IC_Init(&htim3);
	
	sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING;
	sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI;
	sConfigIC.ICPrescaler = TIM_ICPSC_DIV1;
	sConfigIC.ICFilter = 0;
	HAL_TIM_IC_ConfigChannel(&htim3, &sConfigIC, TIM_CHANNEL_1);
}

//TIM6 settings for LED toggling
void MX_TIM6_Init(void)
{
	__HAL_RCC_TIM6_CLK_ENABLE();
	
	TIM_MasterConfigTypeDef sMasterConfig;
  
	htim6.Instance = TIM6;
	htim6.Init.Prescaler = 3599;
	htim6.Init.CounterMode = TIM_COUNTERMODE_UP;
	htim6.Init.Period = 9999;
	htim6.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
	HAL_TIM_Base_Init(&htim6);
}


void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* tim_baseHandle)
{

	GPIO_InitTypeDef GPIO_InitStruct;
	if(tim_baseHandle->Instance==TIM3)
	{
		__HAL_RCC_TIM3_CLK_ENABLE();
		__HAL_RCC_GPIOA_CLK_ENABLE();
		
		/**TIM3 GPIO Configuration
		PA6     ------> TIM3_CH1
		*/
		GPIO_InitStruct.Pin = GPIO_PIN_6;
		GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
		GPIO_InitStruct.Pull = GPIO_NOPULL;
		HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

		/* TIM3 DMA Init */
		hdma_tim3_ch1_trig.Instance = DMA1_Channel6;
		hdma_tim3_ch1_trig.Init.Direction = DMA_PERIPH_TO_MEMORY;
		hdma_tim3_ch1_trig.Init.PeriphInc = DMA_PINC_DISABLE;
		hdma_tim3_ch1_trig.Init.MemInc = DMA_MINC_ENABLE;
		hdma_tim3_ch1_trig.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
		hdma_tim3_ch1_trig.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
		hdma_tim3_ch1_trig.Init.Mode = DMA_NORMAL;
		hdma_tim3_ch1_trig.Init.Priority = DMA_PRIORITY_LOW;
		HAL_DMA_Init(&hdma_tim3_ch1_trig);

		/* Several peripheral DMA handle pointers point to the same DMA handle. Be aware that there is only one channel to perform all the requested DMAs. */
		__HAL_LINKDMA(tim_baseHandle,hdma[TIM_DMA_ID_CC1],hdma_tim3_ch1_trig);
		//__HAL_LINKDMA(tim_baseHandle,hdma[TIM_DMA_ID_TRIGGER],hdma_tim3_ch1_trig);
	}
	else if(tim_baseHandle->Instance==TIM6)
	{
		__HAL_RCC_TIM6_CLK_ENABLE();

		/* TIM6 DMA Init */
		hdma_tim6_up.Instance = DMA2_Channel3;
		hdma_tim6_up.Init.Direction = DMA_MEMORY_TO_PERIPH;
		hdma_tim6_up.Init.PeriphInc = DMA_PINC_DISABLE;
		hdma_tim6_up.Init.MemInc = DMA_MINC_ENABLE;
		hdma_tim6_up.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
		hdma_tim6_up.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
		hdma_tim6_up.Init.Mode = DMA_CIRCULAR;
		hdma_tim6_up.Init.Priority = DMA_PRIORITY_LOW;
		HAL_DMA_Init(&hdma_tim6_up);

		__HAL_LINKDMA(tim_baseHandle,hdma[TIM_DMA_ID_UPDATE],hdma_tim6_up);
  }
}

        MX_TIM3_Init() 配置 TIM3 定时器,使其以等于 ∼0.153Hz 的频率运行。(PCLK1=36MHz) 然后,将第一个通道配置为在输入信号的每个上升沿触发捕获事件 (TIM_DMA_ID_CC1)。然后,HAL_TIM_IC_MspInit() 配置硬件部分( LED 引脚 PB5 连接到 TIM3 通道 1 的 PA6 引脚)和用于配置 TIM_DMA_ID_CC1请求的 DMA 描述符。

        在这里,我们有两件事需要注意。首先,DMA 配置为将外设和存储器数据对齐设置为执行 16 位传输,因为定时器计数器寄存器是 16 位宽的。在 TIM2 和 TIM5 定时器具有 32 位宽计数器寄存器的 MCU 中,您需要设置 DMA 以执行字对齐传输。接下来,由于我们使用了 HAL_TIM_IC_Init(),因此 HAL 旨在调用函数 HAL_TIM_IC_MspInit() 来执行低级初始化,而不是HAL_TIM_Base_MspInit初始化。

#include "main.h"
#include "dma.h"
#include "tim.h"
#include "usart.h"
#include "gpio.h"
#include "string.h"
#include "stdio.h"


void SystemClock_Config(void);

extern DMA_HandleTypeDef hdma_tim6_up;
extern DMA_HandleTypeDef hdma_tim3_ch1_trig;

uint8_t odrVals[] = { 0x0, 0xFF };
uint16_t captures[2];
volatile uint8_t captureDone = 0;

int main(void)
{
	uint16_t diffCapture = 0;
	float frequency;
	char msg[30];

	HAL_Init();
	
	SystemClock_Config();
	
	MX_GPIO_Init();
	MX_DMA_Init();
	MX_USART1_UART_Init();
	
	MX_TIM3_Init();
	MX_TIM6_Init();
	

	HAL_DMA_Start(&hdma_tim6_up, (uint32_t)odrVals, (uint32_t)&GPIOB->ODR, 2);
	__HAL_TIM_ENABLE_DMA(&htim6, TIM_DMA_UPDATE);
	HAL_TIM_Base_Start(&htim6);
	
	HAL_TIM_IC_Start_DMA(&htim3, TIM_CHANNEL_1, (uint32_t *)captures, 2);

  while (1)
  {
	if (captureDone != 0) 
		{
		if (captures[1] >= captures[0]) 
			diffCapture = captures[1] - captures[0];
		else 
			diffCapture = (htim3.Instance->ARR - captures[0]) + captures[1];

		frequency = 2*HAL_RCC_GetPCLK1Freq() / (htim3.Instance->PSC + 1);
		frequency = (float) frequency / diffCapture;
		
		sprintf(msg, "Input frequency: %.3f\r\n", frequency);
		HAL_UART_Transmit(&huart1, (uint8_t*) msg, strlen(msg), HAL_MAX_DELAY);
		while (1);
	}
  }
  
}

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) {
    if (htim->Instance == TIM3) {
        //HAL_UART_Transmit(&huart1, (uint8_t*) "Capture callback called\r\n", 22, HAL_MAX_DELAY);
        //if (HAL_DMA_GetState(&hdma_tim3_ch1_trig) == HAL_DMA_STATE_READY) {
        //    HAL_UART_Transmit(&huart1, (uint8_t*) "DMA transfer completed successfully\r\n", 33, HAL_MAX_DELAY);
        //    char buffer[50];
        //    sprintf(buffer, "Captures: %u, %u\r\n", captures[0], captures[1]);
        //    HAL_UART_Transmit(&huart1, (uint8_t*)buffer, strlen(buffer), HAL_MAX_DELAY);

            captureDone = 1;
			
        //    HAL_UART_Transmit(&huart1, (uint8_t*) "Capture done set to 1\r\n", 22, HAL_MAX_DELAY);
        //} else {
        //    HAL_UART_Transmit(&huart1, (uint8_t*) "DMA transfer error or not completed\r\n", 39, HAL_MAX_DELAY);
        //}
    }
}

        应用程序中最相关的部分是 main() 函数。我们首先使用 MX_TIM6_Init() 函数初始化 TIM6 定时器(配置为以 1Hz 运行------这意味着 PB5 引脚每 2s = 0.5Hz 设置为高电平),然后我们以 DMA 模式启动它。然后我们启动 TIM3,并使用 HAL_TIM_IC_Start_DMA() 函数在第一个通道上启用 DMA 模式。captures 数组用于存储在通道上获取的两个连续捕获。

        我们计算外部信号频率的部分。执行两个捕获时,全局变量 captureDone 由 HAL_TIM_IC_CaptureCallback() 回调函数设置为 1,该函数在捕获过程结束时调用。发生这种情况时,我们使用上面的方程计算采样信号的频率为0.5Hz。

        注意:为了使示例正常工作,需要连接 PB5 和 PA6 引脚。

3.5.1、使用 CubeMX 配置输入捕获模式

        借助 CubeMX,在输入捕获模式下配置通用定时器的输入通道变得很容易。要将一个通道绑定到相应的输入(即 IC1 到 TI1),您必须为所需的通道选择 Input capture direct 模式,如下图所示。

        相反,要将耦合的另一个通道 (IC1,IC2) 或 (IC3,IC4) 映射到相同的输入(即 (IC1,IC2) 的 TI1 或 TI2),可以在输入捕获间接模式下启用耦合耦合中的另一个通道,如下图所示。

        最后,从 TIMx 配置视图(此处未显示)中,可以配置其他输入捕获参数(通道极性、滤波器等)。

3.6、输出比较模式

        到目前为止,我们已经使用了几种技术来控制 GPIO 电平,一种使用中断,一种使用 DMA。它们都使用 UEV 事件的生成来切换配置为输出引脚的 GPIO。输出比较是通用和高级定时器提供的一种模式,当通道比较寄存器 (TIMx_CCRx) 与定时器计数器寄存器 (TIMx_CNT) 匹配时,允许控制输出通道的状态。

        程序员可以使用六种输出比较模式(输出比较模式实际上有八种,但其中两种与 PWM 输出有关):

        Output compare timing:输出比较寄存器 (CCRx) 和计数器 (CNT) 之间的比较对输出没有影响。CubeMX 中的此模式称为 Frozen 模式,此模式用于生成时基。

        Output compare active:将通道输出设置为匹配时的有效电平。当计数器 (CNT) 与捕获/比较寄存器 (CCRx) 匹配时,通道输出被强制为高电平。

        Output compare inactive:在匹配时将通道设置为非活动级别。当计数器 (CNT) 与捕获/比较寄存器 (CCRx) 匹配时,通道输出被强制为低电平。

        Output compare toggle:当计数器 (CNT) 与捕获/比较寄存器 (CCRx) 匹配时,通道输出切换。

        Output compare forced active/inactive:通道输出是强制高电平(活动模式)或低电平(非活动模式),与计数器值无关。

        定时器的每个通道都使用函数 HAL_TIM_OC_ConfigChannel() 和 C 结构体的一个实例TIM_OC_InitTypeDef配置为输出比较模式,其定义如下:

typedef struct {
	uint32_t OCMode; /* Specifies the TIM mode. */
	uint32_t Pulse; /* Specifies the pulse value to be loaded into the Capture Compare Register. */
	uint32_t OCPolarity; /* Specifies the output polarity. */
	uint32_t OCNPolarity; /* Specifies the complementary output polarity.*/
	uint32_t OCFastMode; /* Specifies the Fast mode state. */
	uint32_t OCIdleState; /* Specifies the TIM Output Compare pin state during Idle state.*/
	uint32_t OCNIdleState; /* Specifies the complementary TIM Output Compare pin state during Idle state. */
} TIM_OC_InitTypeDef;

        OCMode:指定输出比较模式,它可以采用下表中的值。

        Pulse:此字段的内容将存储在 CCRx 寄存器内,并确定何时触发输出。请确保将 timer 周期设置为 Pulse 字段的倍数,否则需要处理好整数除法。

        OCPolarity:定义 CCRx 寄存器与 CNT 寄存器匹配时的输出通道极性。它可以采用下表中的值。

        OCNPolarity:定义互补输出极性。这种模式仅在 TIM1 和 TIM8 高级定时器中可用,允许在额外的专用通道上生成互补信号(即,当 CH1 为高电平时,CH1n 为低电平,反之亦然)。此功能专为电机控制应用而设计。它可以采用下表中的值。

        OCFastMode:指定快速模式状态。此参数仅在 PWM1 和 PWM2 模式下有效,并且可以采用 TIM_OCFAST_DISABLE 和 TIM_OCFAST_ENABLE 的值。

        OCIdleState:指定计时器空闲状态期间的通道输出比较引脚状态。它可以采用值 TIM_OCIDLESTATE_SET 和 TIM_OCIDLESTATE_RESET。此参数仅在 TIM1 和 TIM8 高级计时器中可用。        

        OCNIdleState:指定在定时器空闲状态下比较引脚状态的互补通道输出。它可以采用值 TIM_OCNIDLESTATE_SET 和 TIM_OCNIDLESTATE_RESET。此参数仅在 TIM1 和 TIM8 高级计时器中可用。

        当 CCRx 寄存器与定时器 CNT 计数器匹配,并且通道配置为在输出比较模式下工作时,将生成特定中断(如果启用)。这允许独立控制每个通道的开关频率,并最终在通道之间执行相移。通道频率可以使用以下公式计算:

CHxUpdate = \frac{TIMxCLK}{CCRx}

其中:TIMx_CLK 是定时器的运行频率,CCRx 是用于配置通道的 TIM_OnePulse_InitTypeDef 结构的 Pulse 值。这意味着我们可以通过以下方式计算给定通道频率的脉冲值:

pulse = \frac{TIMxCLK}{CHxUpdate}

        显然,重要的是要强调必须设置定时器频率,以便使用上式计算的脉冲值低于定时器周期值(CCRx 值不能高于 TIM->ARR 值, 对应于计时器的 Period)。

        以下示例说明如何生成两个输出方波信号,一个以 25kHz 运行,另一个以 50kHz 运行。它使用 TIM3 定时器的通道 1 和 2(绑定到 OC1 和 OC2)。

#define  TIMx_CLK 48000000/2
volatile uint16_t CH1_FREQ = 0;
volatile uint16_t CH2_FREQ = 0;


int main(void) {
	HAL_Init();
	
	SystemClock_Config();
	MX_GPIO_Init();
	MX_TIM3_Init();
	
	HAL_TIM_OC_Start_IT(&htim3, TIM_CHANNEL_1);
	HAL_TIM_OC_Start_IT(&htim3, TIM_CHANNEL_2);
	
	while (1);
}
	
	/* TIM3 init function */
void MX_TIM3_Init(void) {
	TIM_OC_InitTypeDef sConfigOC;
	
	CH1_FREQ = computePulse(&htim3, 25000); /* 25kHZ switching frequency */
	CH2_FREQ = computePulse(&htim3, 50000); /* 50kHZ switching frequency */
	
	htim3.Instance = TIM3;
	htim3.Init.Prescaler = 2; //PCLK1=48MHz;
	htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
	htim3.Init.Period = 65535;
	htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
	HAL_TIM_OC_Init(&htim3);
	
	sConfigOC.OCMode = TIM_OCMODE_TOGGLE;
	sConfigOC.Pulse = 0;
	sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
	sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
	HAL_TIM_OC_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_1);
	
	sConfigOC.Pulse = 0;
	HAL_TIM_OC_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_2);
}

uint16_t computePulse(TIM_HandleTypeDef *htim, uint32_t frequency) {
	return (uint16_t)((float)TIMx_CLK / frequency / 3);
}

        示例将通道 1 和 2 配置为作为输出比较通道。两者都配置为切换模式(即,每次 CCRx 寄存器与 CNT 定时器寄存器匹配时,它们都会反转 GPIO 的状态)。TIM3 配置为以 16MHz 运行,因此使用计算公式的函数 computePulse() 将返回值 640 和 320,使通道开关频率分别等于 25kHz 和 50kHz。在这里,我们配置通道,以便每次定时器 CNT 寄存器等于通道 1 的 640 和通道 2 的 320 时,都会切换其输出。但这意味着开关频率等于:16000000 / (65535 + 1)= 244Hz,我们在两个通道之间只有 10μs 的偏移,如下图所示。该 65535 值对应于 timer Period 值,即 timer CNT 寄存器达到的最大值。

        为了达到所需的开关频率,我们需要在 TIM3 CNT 寄存器的每 640 和 320 个时钟周期内切换一次输出。(请确保配置 GPIO 速度,以便允许的最大开关频率与所需的定时器开关频率处于同一范围内。) 为此,我们可以定义以下回调例程:

void HAL_TIM_OC_DelayElapsedCallback(TIM_HandleTypeDef *htim) {
	uint32_t pulse;
	uint16_t arr = __HAL_TIM_GET_AUTORELOAD(htim);
	
	/* TIMx_CH1 toggling with frequency = 25KHz */
	if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) {
		pulse = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
	/* Set the Capture Compare Register value */
		if((pulse + CH1_FREQ) < arr)
			__HAL_TIM_SET_COMPARE(htim, TIM_CHANNEL_1, (pulse + CH1_FREQ));
		else
			__HAL_TIM_SET_COMPARE(htim, TIM_CHANNEL_1, (pulse + CH1_FREQ) - arr);
	}
	
	/* TIMx_CH2 toggling with frequency = 50KHz */
	if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2) {
		pulse = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2);
	/* Set the Capture Compare Register value */
		if((pulse + CH2_FREQ) < arr)
			__HAL_TIM_SET_COMPARE(htim, TIM_CHANNEL_2, (pulse + CH2_FREQ));
		else
			__HAL_TIM_SET_COMPARE(htim, TIM_CHANNEL_2, (pulse + CH2_FREQ) - arr);
	}
}

        每次通道 CCRx 寄存器与定时器计数器匹配时,HAL 都会自动调用 HAL_TIM_OC_DelayElapsedCallback()。因此,我们可以将 Pulse (即 CCRx 寄存器) 增加 CH1_FREQ (通道 1) 和 CH2_FREQ 通道 2 (通道 2)。这会导致相应的信道以所需的频率进行切换,如下图所示。

        使用 DMA 模式和预初始化的向量可以获得相同的结果,最终使用 const 修饰符存储在闪存中:

const uint16_t ch1IV[] = {320, 640, 960, ...};
...
HAL_TIM_OC_Start_DMA(&htim3, TIM_CHANNEL_1, (uint32_t)ch1IV, sizeof(ch1IV));

这里以12.5kHz为例,可得:

TIM_HandleTypeDef htim3;
DMA_HandleTypeDef hdma_tim3_ch3;

void SystemClock_Config(void);

const uint16_t ch1IV[] = {640,1280,1920,2560,3200,3840,4480,5120,5760,6400};

int main(void)
{
	HAL_Init();
	
	SystemClock_Config();
	MX_TIM3_Init();

	HAL_TIM_OC_Start_DMA(&htim3, TIM_CHANNEL_3, (uint32_t *)ch1IV, sizeof(ch1IV) / sizeof(uint16_t));
	
	while (1);
}

/* TIM3 init function */
void MX_TIM3_Init(void)
{
	TIM_MasterConfigTypeDef sMasterConfig;
	TIM_OC_InitTypeDef sConfigOC;
	
	htim3.Instance = TIM3;
	htim3.Init.Prescaler = 2;
	htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
	htim3.Init.Period = 6400;
	htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
	htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
	HAL_TIM_OC_Init(&htim3);
	
	sConfigOC.OCMode = TIM_OCMODE_TOGGLE;
	sConfigOC.Pulse = 0;
	sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
	sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
	HAL_TIM_OC_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_3);
	
	HAL_TIM_MspPostInit(&htim3);
}

void HAL_TIM_OC_MspInit(TIM_HandleTypeDef* tim_ocHandle)
{

	if(tim_ocHandle->Instance==TIM3)
	{
		__HAL_RCC_TIM3_CLK_ENABLE();
		
		/* TIM3 DMA Init */
		hdma_tim3_ch3.Instance = DMA1_Channel2;
		hdma_tim3_ch3.Init.Direction = DMA_MEMORY_TO_PERIPH;
		hdma_tim3_ch3.Init.PeriphInc = DMA_PINC_DISABLE;
		hdma_tim3_ch3.Init.MemInc = DMA_MINC_ENABLE;
		hdma_tim3_ch3.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
		hdma_tim3_ch3.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
		hdma_tim3_ch3.Init.Mode = DMA_CIRCULAR;
		hdma_tim3_ch3.Init.Priority = DMA_PRIORITY_LOW;
		HAL_DMA_Init(&hdma_tim3_ch3);
		
		__HAL_LINKDMA(tim_ocHandle,hdma[TIM_DMA_ID_CC3],hdma_tim3_ch3);
	}
}
void HAL_TIM_MspPostInit(TIM_HandleTypeDef* timHandle)
{

	GPIO_InitTypeDef GPIO_InitStruct;
	if(timHandle->Instance==TIM3)
	{
		__HAL_RCC_GPIOB_CLK_ENABLE();
    /**TIM3 GPIO Configuration
    PB0     ------> TIM3_CH3
    */
		GPIO_InitStruct.Pin = GPIO_PIN_0;
		GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
		GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
		HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
	}
}

3.6.1、使用 CubeMX 配置输出比较模式

        CubeMX 中输出比较模式的配置过程与输入捕获模式的配置过程相同。第一步是为所需通道选择 Output compare CHx 模式。接下来,从 TIMx 配置视图中,可以配置其他输出比较参数(输出模式、通道极性等)。

3.7、脉宽生成

        到目前为止产生的方波都有一个共同的特性:它们的 TON 周期等于 TOFF 周期。因此,它们具有 50% 的占空比。占空比是信号处于活动状态的一个周期(例如 1s)的百分比。作为公式,占空比表示为:D = \frac{T_{ON}}{Period}

其中 D 是占空比, TON 是信号active的时间。因此,50% 的占空比意味着信号在 50% 的时间内处于开启状态,但在 50% 的时间内处于关闭状态。占空比没有说明它持续多长时间。50% 占空比的“on time”可能是几分之一秒、一天甚至一周,具体取决于周期的长度。脉冲宽度是 TON 的持续时间,给定实际周期。例如,假设周期为 1s,则 20% 的占空比会产生 200ms 的脉冲宽度。

上图显示了三种不同的占空比:50%、20% 和 80%。

        脉宽调制 (PWM) 是一种技术,用于在给定时间段内或以给定频率生成具有不同占空比的多个脉冲。PWM 在数字电子学中有许多应用,但它们都可以分为两大类:

        ---控制输出电压(以及电流);

        ---在载波 (以给定频率运行) 上编码 (即调制) 消息 (即数字电子学中的一系列字节)。

        这两个类别可以在 PWM 技术的几个实际用法中扩展。将注意力集中在输出电压的控制上,我们可以发现几种应用:

        ---产生从 0V 到 VDD 的输出电压(即 I/O 的最大允许电压,在 STM32 中为 3.3V):LED 调光;电机控制;功率转换;

        ---生成以给定频率运行的输出波 (正弦波、三角形、方波等);

        ---声音输出。

        通过适当的输出滤波(通常涉及使用低通滤波器),PWM 可以复制 DAC 的行为,即使 MCU 不提供 DAC。通过改变输出引脚的占空比,可以按比例调节输出电压。放大器可以根据需要增加/减少电压范围,也可以使用功率晶体管控制大电流和负载。

        通过使用函数 HAL_TIM_PWM_ConfigChannel() 和上一段中所示的 C 结构TIM_OC_InitTypeDef实例,在 PWM 模式下配置定时器通道。TIM_OC_InitTypeDef.Pulse 字段定义占空比,范围从 0 到计时器 Period 字段。Period 越长,调音范围越宽。这意味着我们可以微调输出电压。

        周期的选择决定了输出信号的频率以及定时器时钟(内部、外部等),这不是一个可以听天由命的细节。这取决于具体的应用领域,并且会对整体 EMI 辐射产生严重影响。此外,一些使用 PWM 技术控制的设备可能会在给定频率下发出可闻噪声。电动机就是这种情况,当控制在听觉范围内的频率时,它可能会发出不需要的嗡嗡声。另一个例子,这里没有太多关系,但起源相似,是开关电源中功率电感器发出的噪声,它使用 PWM 的基本概念来调节其输出电压,从而调节电流。有时,输出噪声是不可避免的,需要使用消噪来减少问题。其他时候,正确的频率来自“自然限制”:以接近 100Hz 的频率调暗 LED 通常足以避免可见的灯光闪烁。

        有两种 PWM 模式可用:PWM 模式 1 和 2。两者都可以通过字段 TIM_OC_InitTypeDef.OCMode 使用值 TIM_OCMODE_PWM1 和 TIM_OCMODE_PWM2 进行配置。

        • PWM 模式 1:在递增时,只要 Period < Pulse,通道就处于活动状态,否则处于非活动状态。在向下计数时,只要 Period > Pulse,通道就处于非活动状态,否则处于活动状态。

        • PWM 模式 2:在向上计数中,只要 Period < Pulse,通道 1 就处于非活动状态,否则处于活动状态。在向下计数中,只要 Period > Pule ,通道 1 就处于活动状态,否则处于非活动状态。        

        以下示例显示了 PWM 技术的典型应用:LED 调光。

int main(void) {
	HAL_Init();
	
	SystemClock_Config();
	MX_GPIO_Init();
	MX_TIM2_Init();
	
	HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_3);
	
	uint16_t dutyCycle = HAL_TIM_ReadCapturedValue(&htim3, TIM_CHANNEL_3);
	
	while(1) {
		while(dutyCycle < __HAL_TIM_GET_AUTORELOAD(&htim3)) {
			__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_3, ++dutyCycle);
			HAL_Delay(1);
		}
	
		while(dutyCycle > 0) {
			__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_3, --dutyCycle);
			HAL_Delay(1);
		}
	}
}
	
	/* TIM3 init function */
void MX_TIM3_Init(void) {
	TIM_OC_InitTypeDef sConfigOC;
	
	htim2.Instance = TIM3;
	htim2.Init.Prescaler = 499;
	htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
	htim2.Init.Period = 999;
	htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
	HAL_TIM_PWM_Init(&htim3);
	
	sConfigOC.OCMode = TIM_OCMODE_PWM1;
	sConfigOC.Pulse = 0;
	sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
	sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
	HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_3);
}

        示例配置定时器 TIM3 的第一个通道在 PWM 模式 1 下工作。占空比的范围从 0 到 999,这与 Period 值相对应。这意味着如果输出经过良好的滤波(并且 PCB 具有良好的布局),我们可以以 ∼0.0033V 的步长调节输出电压。这接近 10 位 DAC 的性能。

        while函数里面是渐进效果发生的地方。第一个循环每 1 毫秒将 Pulse 的值(对应于 Capture Compare Register 1 (CCR1))增加到 Period 值(对应于 Auto Reload Register (ARR))。这意味着在不到 1 秒的时间内,LED 就会完全变亮。第二个循环以同样的方式递减 Pulse 字段,除非它达到零。

        定时器的更新频率设置为 72MHz/(499+1)(999+1)=144Hz。通过将 Prescaler 设置为 249 并将 Period 设置为 1999 可以获得相同的频率。但渐进效果发生了变化,会很明显。为什么会这样?如果你无法解释其中的区别,强烈建议你在继续之前先休息一下,自己做实验。

3.7.1、使用 PWM 生成正弦波

        使用 PWM 技术生成的输出方波可以过滤以生成平滑信号,即峰峰值电压 (Vpp) 降低的模拟信号。电阻电容 (RC) 低通滤波器 (见下图) 可以截止所有频率高于给定阈值的 AC 信号。RC 低通滤波器的一般经验法则是截止频率越低,Vpp越低(https://bit.ly/22breq2)。RC 低通滤波器利用电容器的一个重要特性:能够阻断直流电流,同时允许交流电流通过:给定电阻电容网络形成的 R/C 时间常数,滤波器将那些频率高于 RC 常数的交流信号短路接地,允许通过信号的直流分量和较低频率的交流电压。

        虽然这个电路非常简单,但为 R(电阻)和 C(电容)选择合适的值包括一些设计决策:我们可以容忍多少纹波以及滤波器需要多快的响应速度。这两个参数是互斥的。在大多数滤波器中,我们希望拥有完美的滤波器 – 一种能够通过截止频率以下的所有频率,并且没有电压纹波的滤波器。不幸的是,这个理想的滤波器并不存在:为了将纹波减少到零,我们必须选择一个非常大的滤波器,这会导致输出需要很长时间才能稳定。虽然这对于连续和固定电压来说是可以接受的,但如果我们试图从 PWM 信号生成复杂的波形,这会对输出信号的质量产生严重影响。

        一阶 RC 低通滤波器的截止频率 (fc) 由以下公式表示: fc = 1 / 2πRC。

        上图显示了低通滤波器对频率为 100Hz 的 PWM 信号的影响。这里我们选择了一个 1K 电阻和一个 10μF 电容。这意味着截止频率等于: fc = 1 / (2π103 × 10−5 )≈ 15.9Hz。

        上图显示了带有 4300K 电阻和 10μF 电容器的低通滤波器的效果。这意味着截止频率等于: fc = 1 / (2π(4.3 × 103) × 10−5) ≈ 3.7Hz。这个滤波器允许的 (Vpp) 等于约 160mV,这对于许多应用来说都是合格的。

        通过改变输出电压(这意味着我们改变占空比),我们可以产生一个任意的输出波形,其频率是 PWM 周期的一小部分。基本思想是将我们想要的波形(例如正弦波)划分为 'x' 个分区。对于每个分区,我们都有一个 PWM 周期。TON 时间(即占空比)直接对应于该除频中波形的幅度,该幅度使用 sin() 函数计算。

        考虑上图中所示的图表。这里正弦波被分为 10 个步骤。所以在这里,我们将需要 10 个不同的 PWM 脉冲以正弦方式增加/减少。占空比为 0% 的 PWM 脉冲表示最小幅度 (0V),占空比为 100% 的脉冲表示最大幅度 (3.3V)。由于输出 PWM 脉冲的电压摆幅在 0V 到 3.3V 之间,我们的正弦波也会在 0V 到 3.3V 之间摆动。

        正弦波需要 360 度才能完成一个周期。因此,对于 10 个刻度,我们需要以 36 度的步长增加角度。这称为 Angle Step Rate 或 Angle Resolution。我们可以增加分区以获得更准确的波形。但随着分区的增加,我们还需要提高分辨率,这意味着我们必须增加用于生成 PWM 信号的定时器的频率(定时器运行得越快,周期就越小)。

        通常,200 个分区是输出波的良好近似值。这意味着,如果我们想产生一个 50Hz 的正弦波,我们需要以 50Hz*200 = 10kHz 的速度运行定时器。脉冲周期将等于 200(这意味着我们将输出电压改变 3.3V/200=0.016V),因此预分频器值将为(假设 MCU 以 48MHz 运行):

以下示例显示了如何在以 48MHz 运行的MCU中产生 50Hz 纯正弦波。

#define PI 3.14159
#define ASR 1.8 //360 / 200 = 1.8
	
int main(void) {
	uint16_t IV[200];
	float angle;
	
	HAL_Init();
	SystemClock_Config();
	MX_GPIO_Init();
	MX_DMA_Init();
	MX_TIM3_Init();
	
	for (uint8_t i = 0; i < 200; i++) {
	angle = ASR*(float)i;
	IV[i] = (uint16_t) rint(100 + 99*sinf(angle*(PI/180)));
	}
	
	HAL_TIM_PWM_Start_DMA(&htim3, TIM_CHANNEL_3, (uint32_t *)IV, 200);
	
	while (1);
}
	
	/* TIM3 init function */
void MX_TIM3_Init(void) {
	TIM_OC_InitTypeDef sConfigOC;
	
	htim3.Instance = TIM3;
	htim3.Init.Prescaler = 23;
	htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
	htim3.Init.Period = 199;
	htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;//TIM_CLOCKDIVISION_DIV4;
	HAL_TIM_PWM_Init(&htim3);
	
	sConfigOC.OCMode = TIM_OCMODE_PWM1;
	sConfigOC.Pulse = 0;
	sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
	sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
	HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_3);
	
	hdma_tim3_ch3.Instance = DMA1_Channel2;
	hdma_tim3_ch3.Init.Direction = DMA_MEMORY_TO_PERIPH;
	hdma_tim3_ch3.Init.PeriphInc = DMA_PINC_DISABLE;
	hdma_tim3_ch3.Init.MemInc = DMA_MINC_ENABLE;
	hdma_tim3_ch3.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
	hdma_tim3_ch3.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
	hdma_tim3_ch3.Init.Mode = DMA_CIRCULAR;
	hdma_tim3_ch3.Init.Priority = DMA_PRIORITY_LOW;
	HAL_DMA_Init(hdma_tim3_ch3);
	
	/* Several peripheral DMA handle pointers point to the same DMA handle. Be aware that there is only one channel to perform all the requested DMAs. */
	__HAL_LINKDMA(&htim3, hdma[TIM_DMA_ID_CC3], hdma_tim3_ch3);
}

        for循环代码行用于生成初始化向量 (IV),即包含用于生成正弦波(对应于输出电压电平)的 Pulse 值的向量。sinf()返回以弧度表示的给定角度的正弦值。因此,我们需要使用以下公式将以度为单位的角度表示转换为弧度:弧度 = π / 180° × 角度。但是,在我们的例子中,我们将正弦波周期划分为 200 步(即,我们将周长划分为 200 步),因此我们需要计算每个步长的弧度值。但是由于正弦在 180° 和 360° 之间的角度是负值(见下图),我们需要平移它,因为 PWM 输出值不能为负。

        一旦生成了 IV 矢量,我们就可以在 DMA 模式下启动 PWM。DMA1_Channel4 配置为在循环模式下工作,因此它会根据 IV 中包含的 Pulse 值自动设置 TIMx_CCRx 寄存器的值。在 DMA 模式下使用定时器是生成任意函数的最佳方式,而不会引入延迟和影响 Cortex-M 内核。然而,IV 通常在程序内部进行硬编码,使用自动存储在 flash 中的 const 数组。您可以找到几种在线工具来执行此操作,例如https://bit.ly/1QPfm4k提供的工具。

        上图显示了 TIM3 通道 1 的输出:使用适当的滤波级,很容易产生纯 50Hz 正弦波。在这里,使用了一个 100 欧姆的电阻器和一个 10μF 的电容器,它们的截止频率为 ∼159Hz,Vpp 等于 0.08V。

3.7.2、使用 CubeMX 配置 PWM 模式

        一旦掌握了 PWM 生成的基本概念,CubeMX 中 PWM 模式的配置过程就很简单了。第一步是为所需通道选择 PWM Generation CHx 模式。接下来,从 TIMx 配置视图中,可以配置其他 PWM 设置(PWM 模式 1 或 2、通道极性等)。

3.8、单脉冲模式

        单脉冲模式 (OPM) 是通用和高级定时器提供的输入捕获和输出比较模式的混合。它允许计数器响应激励而启动,并在可编程延迟后生成具有可编程持续时间 (PWM) 的脉冲。OPM 是一种专门用于定时器的通道 1 和 2 的模式。我们可以使用以下函数来决定两个通道中哪个是输出,哪个是输入:

HAL_TIM_OnePulse_ConfigChannel(TIM_HandleTypeDef *htim, TIM_OnePulse_InitTypeDef* sConfig, uint32_t OutputChannel, uint32_t InputChannel);

这两个通道都配置了 C 结构TIM_OnePulse_InitTypeDef的实例,该实例按以下方式定义:

typedef struct {
	uint32_t Pulse; /* Specifies the pulse value to be loaded into the CCRx register.*/
	/* Output channel configuration */
	uint32_t OCMode; /* Specifies the TIM mode. */
	uint32_t OCPolarity; /* Specifies the output polarity. */
	uint32_t OCNPolarity; /* Specifies the complementary output polarity. */
	uint32_t OCIdleState; /* Specifies the TIM Output Compare pin state during Idle state.*/
	uint32_t OCNIdleState; /* Specifies the TIM Output Compare pin state during Idle state.*/
	/* Input channel configuration */
	uint32_t ICPolarity; /* Specifies the active edge of the input signal. */
	uint32_t ICSelection; /* Specifies the input. */
	uint32_t ICFilter; /* Specifies the input capture filter. */
} TIM_OnePulse_InitTypeDef;

该结构在逻辑上分为两部分:一部分与 input 通道的配置相关,另一部分与 output 相关。这里不会详细介绍 struct 字段,因为它们与到目前为止我们讨论 input capture 和 output compare 模式时看到的类似。

        需要了解的一个重要方面是 timer 计算 delay 和 pulse duration 的方式。延迟根据以下公式计算:

而脉冲的持续时间(即占空比)则使用以下公式计算:

这意味着,一旦输入通道检测到触发事件,定时器就开始计数,当 CNT 寄存器到达 CCRx 寄存器(脉冲)时,它会生成输出信号, 一直持续到 CNT 寄存器到达 ARR 寄存器 (Period),即 Period - Pulse。

        OPM 可以设置为单次拍摄或重复模式。这是通过使用

HAL_TIM_OnePulse_Init(TIM_HandleTypeDef *htim, uint32_t OnePulseMode);

来执行的;它接受指向定时器处理程序的指针和符号常量TIM_OPMODE_SINGLE以在单次拍摄中配置 OPM 或TIM_OPMODE_REPETITIVE以启用重复模式。

        以下示例展示了如何在 STM32F072 MCU 中以 OPM 模式配置 TIM3。

int main(void) {
	HAL_Init();
	
	BSP_Init();
	MX_TIM3_Init();
	
	HAL_TIM_OnePulse_Start(&htim3, TIM_CHANNEL_1);
	
	while (1);
}
	
/* TIM3 init function */
void MX_TIM3_Init(void) {
	TIM_OnePulse_InitTypeDef sConfig;
	
	htim3.Instance = TIM3;
	htim3.Init.Prescaler = 47;
	htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
	htim3.Init.Period = 65535;
	HAL_TIM_OnePulse_Init(&htim3, TIM_OPMODE_SINGLE);
	
	/* Configure the Channel 1 */
	sConfig.OCMode = TIM_OCMODE_PWM1;
	sConfig.OCPolarity = TIM_OCPOLARITY_LOW;
	sConfig.Pulse = 19999;
	
	/* Configure the Channel 2 */
	sConfig.ICPolarity = TIM_ICPOLARITY_RISING;
	sConfig.ICSelection = TIM_ICSELECTION_DIRECTTI;
	sConfig.ICFilter = 0;
	
	HAL_TIM_OnePulse_ConfigChannel(&htim3, &sConfig, TIM_CHANNEL_1, TIM_CHANNEL_2);
}

        示例配置输出通道1为PWM 模式1,配置输出通道2为输入通道。HAL_TIM_OnePulse_ConfigChannel() 配置两个通道,将通道 1 设置为输出,将通道 2 设置为输入。最后,HAL_TIM_OnePulse_Start()以 OPM 模式启动定时器。通过偏置PA7 引脚,定时器将在延迟 20ms 后启动,并产生约 45ms 的 PWM,如下图所示。

        运行在 One Pulse 模式中的定时器的输出通道甚至可以在与 PWM 模式不同的其他模式下进行配置。参考STM32定时器单脉冲输出_stm32单脉冲端口复用后输出长高-CSDN博客

3.8.1、使用 CubeMX 配置 OPM 模式

        要使用 CubeMX 启用 OPM 模式,第一步是分别配置两个通道 1 和 2,然后选中 One Pulse Mode 复选框,如下图所示。

接下来,从 TIMx 配置视图(此处未显示)中,可以配置其他通道设置。

3.9、编码器模式

        旋转编码器是具有广泛应用范围的设备。它们用于测量旋转物体的速度和角度位置。它们可用于测量电机的 RPM 和方向,控制伺服电机和步进电机等。旋转编码器有几种类型:光学、机械、磁性。

        增量编码器是一种旋转编码器,当它们检测到运动时提供循环输出。机械型需要去抖动,通常用作“数字电位计”。大多数现代家用和汽车音响使用机械旋转编码器进行音量控制。增量式旋转编码器是所有旋转编码器中使用最广泛的,因为它成本低,并且能够提供易于解释的信号,以提供与运动相关的信息,例如速度。

        它们使用两个输出,称为 A 和 B,称为正交输出,因为它们的相位相差 90 度,如下图所示。

电机的方向取决于 A 相是否领先于 B 相,或者 B 相领先于 A 相。可选的第三个通道,索引脉冲,每转发生一次,用作测量绝对位置的参考。有几种方法可以检测旋转编码器的方向和位置。通过将 A 和 B 引脚连接到两个 MCU I/O,可以检测信号何时变为高电平和低电平。这既可以手动执行(在通道更改状态时使用中断进行捕获),也可以使用定时器执行:其通道可以在输入捕获模式下进行配置,并比较捕获值以计算编码器的方向和速度。

        STM32 通用定时器提供了一种读取旋转编码器的便捷方式:这种模式确实称为编码器模式,它大大简化了捕获过程。当定时器配置为编码器模式时,定时器计数寄存器 (TIMx_CNT) 在输入通道的边缘递增/递减。

        有两种捕获模式可用:X2 和 X4。在 X2 模式下, CNT 寄存器仅在一个通道 (T1 或 T2) 的每个边沿上递增/递减。在 X4 模式下, CNT 寄存器在两个通道的每个边缘都更新: 这使得捕获频率加倍。移动的方向是自动派生的,并可供 TIMx_DIR 寄存器中的程序员使用,如下图所示。

通过定期比较计数器寄存器的值,可以得出 RPM 数,给定编码器每转发射的脉冲数。

        由于输出噪声大,增量式机械编码器通常需要去抖。比较器通常用作这些器件的滤波级,特别是当它们用于连接电机和其他噪声器件时。在某些情况下,STM32 定时器的输入滤波器级可用于过滤 A 和 B 通道,从而减少 BOM 元件的数量。

        编码器模式仅在 TI1 和 TI2 通道上可用,通过使用函数 HAL_TIM_Encoder_Init() 和 C 结构体的实例来激活TIM_Encoder_InitTypeDef,其定义方式如下。

typedef struct {
	/* T1 channel */
	uint32_t EncoderMode; /* Specifies the active edge of the input signal. */
	uint32_t IC1Polarity; /* Specifies the active edge of the input signal. */
	uint32_t IC1Selection; /* Specifies the input. */
	uint32_t IC1Prescaler; /* Specifies the Input capture prescaler. */
	uint32_t IC1Filter; /* Specifies the input capture filter. */
	/* T2 channel */
	uint32_t IC2Polarity; /* Specifies the active edge of the input signal. */
	uint32_t IC2Selection; /* Specifies the input. */
	uint32_t IC2Prescaler; /* Specifies the Input capture prescaler. */
	uint32_t IC2Filter; /* Specifies the input capture filter. */
} TIM_Encoder_InitTypeDef;

        值得注意的是 EncoderMode,它可以假设值 TIM_ENCODERMODE_TI1 或 TIM_ENCODERMODE_TI2 来设置两个通道之一上的 X2 编码器模式,并假设值 TIM_ENCODERMODE_TI12 来设置 X4 模式,以便在 TI1 和 TI2 通道的每个边沿上更新 TIMx_CNT 寄存器。

        以下示例设计为在 STM32F072RB 上运行,通过在输出比较模式下使用 TIM1 来模拟增量编码器。TIM1的 OC1 和 OC2(PA8、PA9)通道连接到 TIM3 的 TI1 和 TI2 通道(PA6、PA7),并且它们被配置为产生两个具有相同周期但相位偏移的方波信号。然后,在编码器模式下配置 TIM3。SysTick 计时器用于生成时基:每 1 秒计算一次脉冲数以及编码器方向。然后推导出 RPM 的数量,假设编码器每转产生 4 个脉冲。最后,通过按下 USER 按钮,可以改变 A 相和 B 相之间的相移:这将反转编码器旋转。

#define PULSES_PER_REVOLUTION 4
	
int main(void) {
	HAL_Init();
	
	BSP_Init();
	MX_TIM1_Init();
	MX_TIM3_Init();
	
	HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL);
	HAL_TIM_OC_Start(&htim1, TIM_CHANNEL_1);
	HAL_TIM_OC_Start(&htim1, TIM_CHANNEL_2);
	
	cnt1 = __HAL_TIM_GET_COUNTER(&htim3);
	tick = HAL_GetTick();
	
	while (1) {
		if (HAL_GetTick() - tick > 1000L) {
			cnt2 = __HAL_TIM_GET_COUNTER(&htim3);
			if (__HAL_TIM_IS_TIM_COUNTING_DOWN(&htim3)) {
				if (cnt2 < cnt1) /* Check for counter underflow */
					diff = cnt1 - cnt2;
				else
					diff = (65535 - cnt2) + cnt1;
			} else {
				if (cnt2 > cnt1) /* Check for counter overflow */
					diff = cnt2 - cnt1;
				else
					diff = (65535 - cnt1) + cnt2;
			}
			
			sprintf(msg, "Difference: %d\r\n", diff);
			HAL_UART_Transmit(&huart2, (uint8_t*) msg, strlen(msg), HAL_MAX_DELAY);
			
			speed = ((diff / PULSES_PER_REVOLUTION) / 60);
			
			/* If the first three bits of SMCR register are set to 0x3
			* then the timer is set in X4 mode (TIM_ENCODERMODE_TI12)
			* and we need to divide the pulses counter by two, because
			* they include the pulses for both the channels */
			if ((TIM3->SMCR & 0x3) == 0x3)
				speed /= 2;
			
			sprintf(msg, "Speed: %d RPM\r\n", speed);
			HAL_UART_Transmit(&huart2, (uint8_t*) msg, strlen(msg), HAL_MAX_DELAY);
			
			dir = __HAL_TIM_IS_TIM_COUNTING_DOWN(&htim3);
			sprintf(msg, "Direction: %d\r\n", dir);
			HAL_UART_Transmit(&huart2, (uint8_t*) msg, strlen(msg), HAL_MAX_DELAY);
			
			tick = HAL_GetTick();
			cnt1 = __HAL_TIM_GET_COUNTER(&htim3);
		}
		
		if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == GPIO_PIN_RESET) {
			/* Invert rotation by swapping CH1 and CH2 CCR value */
			tim1_ch1_pulse = __HAL_TIM_GET_COMPARE(&htim1, TIM_CHANNEL_1);
			tim1_ch2_pulse = __HAL_TIM_GET_COMPARE(&htim1, TIM_CHANNEL_2);
		
			__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, tim1_ch2_pulse);
			__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, tim1_ch1_pulse);
		}
	}
}
	
	/* TIM1 init function */
void MX_TIM1_Init(void) {
	TIM_OC_InitTypeDef sConfigOC;
	
	htim1.Instance = TIM1;
	htim1.Init.Prescaler = 9;
	htim1.Init.CounterMode = TIM_COUNTERMODE_UP;
	htim1.Init.Period = 999;
	HAL_TIM_Base_Init(&htim1);
	
	sConfigOC.OCMode = TIM_OCMODE_TOGGLE;
	sConfigOC.Pulse = 499;
	sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
	sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
	sConfigOC.OCIdleState = TIM_OCIDLESTATE_RESET;
	sConfigOC.OCNPolarity = TIM_OCNPOLARITY_HIGH;
	sConfigOC.OCNIdleState = TIM_OCNIDLESTATE_RESET;
	HAL_TIM_OC_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_1);
	
	sConfigOC.Pulse = 999; /* Phase B is shifted by 90° */
	HAL_TIM_OC_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_2);
}
	
	/* TIM3 init function */
void MX_TIM3_Init(void) {
	TIM_Encoder_InitTypeDef sEncoderConfig;
	
	htim3.Instance = TIM3;
	htim3.Init.Prescaler = 0;
	htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
	htim3.Init.Period = 65535;
	
	sEncoderConfig.EncoderMode = TIM_ENCODERMODE_TI12;
	
	sEncoderConfig.IC1Polarity = TIM_ICPOLARITY_RISING;
	sEncoderConfig.IC1Selection = TIM_ICSELECTION_DIRECTTI;
	sEncoderConfig.IC1Prescaler = TIM_ICPSC_DIV1;
	sEncoderConfig.IC1Filter = 0;
	
	sEncoderConfig.IC2Polarity = TIM_ICPOLARITY_RISING;
	sEncoderConfig.IC2Selection = TIM_ICSELECTION_DIRECTTI;
	sEncoderConfig.IC2Prescaler = TIM_ICPSC_DIV1;
	sEncoderConfig.IC2Filter = 0;
	
	HAL_TIM_Encoder_Init(&htim3, &sEncoderConfig);
}

        函数 MX_TIM1_Init() 配置 TIM1 定时器,使其 OC1 和 OC2 通道在输出比较模式下工作,每 ∼20μs 触发一次输出。通过设置两个不同的 Pulse 值来使两个输出同相偏移。MX_TIM3_Init() 功能将 TIM3 配置为编码器 X4 模式 (TIM_ENCODERMODE_TI12)。

        main() 函数的设计使得 SysTimer(配置为每 1 毫秒生成一个滴答)的每 1000 个滴答声将计数寄存器的当前内容 (cnt2) 与保存的值 (cnt1) 进行比较:根据编码器方向(向上或向下),计算差值,并计算速度。该代码还需要检测计数器的最终上溢/下溢,并相应地计算差异。另请注意,由于我们每 1 秒执行一次比较,因此必须配置 TIM1,以便通道 A 和 B 产生的脉冲之和应小于每秒 65535 个。因此,我们减慢 TIM1 的速度,将 Prescaler 设置为等于 9。最后,当按下 用户按钮时,反转 A 和 B(即 TIM1 定时器的 OC1 和 OC2 通道)之间的相移。

3.9.1、使用 CubeMX 配置编码器模式

        要使用 CubeMX 启用编码器模式,第一步是从 Combined Channels 组合框中启用此模式,如下图所示。接下来,从 TIMx 配置视图(此处未显示)中,可以配置其他通道设置。

3.10、通用定时器和高级定时器中可用的其他特性

        到目前为止所看到的特性代表了定时器最常见的用法。但是,STM32 通用和高级定时器提供了其他重要功能,在某些特定的应用领域中非常有用。现在,我们将快速概述这些附加功能。

3.10.1、霍尔传感器模式

        在有刷直流电机中,电刷通过在正确的时刻物理连接线圈来控制换向。在无刷直流 (BLDC) 电机中,换向由电子设备使用 PWM 控制。电子设备可以具有位置传感器输入,提供有关何时换向的信息,也可以使用线圈中产生的反电动势。位置传感器最常用于启动转矩变化很大或需要高初始转矩的应用。位置传感器也常用于使用电机进行定位的应用。

        霍尔效应传感器,或简称霍尔传感器,主要用于计算三相 BLDC 电机的位置(每相一个传感器)。STM32 通用定时器可以编程为在霍尔传感器模式下工作。通过将前三个输入设置为 XOR 模式,可以自动检测转子的位置。

        这是使用高级控制定时器 (TIM1) 生成 PWM 信号以驱动电机和另一个称为“接口定时器”的定时器(例如 TIM3)来完成的。这个接口定时器捕获通过 XOR 连接到 TI1 输入通道的三个定时器输入引脚(CC1、CC2、CC3)。TIM3 处于 slave 模式,配置为 reset 模式;从机输入为 TI1F_ED。(ED 是 Edge Detector 的首字母缩写词,它是一个内部滤波定时器输入,当 XOR 中的三个输入中只有一个为高电平时启用。)因此,每当三个 inputs中的一个切换时,计数器就会从 0 开始重新计数。这将创建一个由 Hall 输入的任何更改触发的时基。

        在“接口定时器”(TIM3) 上,捕获/比较通道 1 配置为捕获模式,捕获信号为 TRC。捕获的值对应于输入上的 2 次变化之间经过的时间,提供有关电机速度的信息。“接口定时器”可以在输出模式下使用,以产生一个脉冲,该脉冲会改变高级控制定时器 (TIM1) 的通道配置(通过触发 COM 事件)。TIM1 定时器用于产生 PWM 信号以驱动电机。为此,必须对接口定时器通道进行编程,以便在编程延迟后产生正脉冲(在输出比较或 PWM 模式下)。该脉冲通过 TRGO 输出发送到高级定时器 (TIM1)。

3.10.2、组合三相 PWM 模式和其他电机控制相关特性

        ST32F3 系列专用于高级功率转换和电机控制。一些STM32F3 MCU,特别是 STM32F30x 和 STM32F3x8,能够生成一到三个中心对齐的 PWM 信号,并在脉冲中间使用单个可编程信号进行 AND。此外,它们还可以通过插入死区时间生成多达三个互补输出。除了之前看到的霍尔传感器模式外,这些功能还允许构建适合电机控制的电子设备。https://bit.ly/1WAewd6

3.10.3、断路输入和定时器寄存器锁定

        Break Input是电机控制应用中的紧急输入。Break Input功能可保护由高级定时器生成的 PWM 信号驱动的电源开关。Break Input通常连接到功率级和三相逆变器的故障输出。激活后,断开电路会关闭 TIM 输出并强制它们进入预定义的安全状态。

        此外,高级定时器提供对其寄存器的逐步保护,对 BDTR 寄存器中的 LOCK 位进行编程。有三个锁定级别可用,可选择性地锁定到所有定时器寄存器。

3.10.4、自动重新加载寄存器的预加载

        在前面,ARR 寄存器以图形方式用阴影表示。发生这种情况是因为它已预加载,即写入 ARR 寄存器或从 ARR 寄存器读取先访问预加载寄存器。当且仅当在 TIMx->CR1 寄存器中启用了自动重新加载预加载位 (APRE) 时,预加载寄存器的内容将永久传输到影子寄存器(即定时器内部的寄存器,该寄存器实际上包含要匹配的计数器值)。如果是这样,则可以生成一个 UEV 事件,在 TIMx->EGR 寄存器中设置相应的位:这将导致预加载寄存器的内容在影子寄存器中传输,并且定时器将考虑新值。显然,如果你停止定时器,你可以自由地改变 ARR 寄存器的内容。

        这是一个需要搞清楚的重要方面。当定时器停止时,我们可以使用 TIM_Base_InitTypeDef.Period 结构配置 ARR 寄存器:Period 字段的内容通过 HAL_TIM_Base_Init() 函数在 TIMx->ARR 寄存器中传输。这将导致生成 UEV 事件,如果启用,将引发相应的 IRQ。需要注意的是,即使在外设重置后首次配置 timer 时,也会发生这种情况。让我们考虑一下这段代码:

htim6.Instance = TIM6;
htim6.Init.Prescaler = 47999; //48MHz/48000 = 1kHz
htim6.Init.Period = 4999; //1kHz / 5000 = 5s
htim6.Init.CounterMode = TIM_COUNTERMODE_UP;
__HAL_RCC_TIM6_CLK_ENABLE();
HAL_NVIC_SetPriority(TIM6_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(TIM6_IRQn);
HAL_TIM_Base_Init(&htim6);
HAL_TIM_Base_Start_IT(&htim6);

上面的代码配置了 TIM6 定时器,使其在 5 秒后到期。但是,如果在一个完整的示例中重写该代码,可以看到 IRQ 几乎在调用 HAL_TIM_Base_Start_IT() 函数后立即触发。这是因为 HAL_TIM_Base_Init() 例程生成一个 UEV 事件,以在内部影子寄存器内传输 TIM6->ARR 寄存器的内容。这会导致设置 UIF 标志,并在 HAL_TIM_Base_Start_IT() 启用它时触发 IRQ。

        可以通过在 TIMx->CR1 寄存器中设置 URS 位来绕过此行为:这仅在计数器到达上溢/下溢时生成 UEV 事件。

        可以通过在 TIMx->CR1 控制寄存器中设置 TIM_CR1_ARPE 位来配置定时器,以便缓冲 ARR 寄存器。这将导致影子寄存器的内容自动更新。不幸的是,HAL 似乎没有提供明确的宏来做到这一点,我们需要在底层访问定时器寄存器:

        当我们在输出比较模式下使用定时器时,预加载特别有用。这种情况下,启用了多个输出通道,并且每个输出通道都有自己的捕获值,并且我们必须确保对 CCRx 寄存器的任何更改都同时发生。如果我们使用定时器进行电机控制或电源转换,则尤其如此。启用预加载功能可保证 CCRx 寄存器中的新设置将在计数器的下一个上溢/下溢时发生。

3.11、调试和定时器

        在调试期间,当执行因硬件或软件断点而暂停时,默认情况下不会停止定时器。有时,在调试期间停止定时器很有用,特别是当它用于驱动外部设备时。

        STM32 定时器可以选择性地配置为在内核因断点而停止时停止。HAL 宏 __HAL_DBGMCU_FREEZE_TIMx() (其中 x 对应于定时器编号)启用计时器的此工作模式。此外,具有互补输出的 timer 的输出被禁用并强制进入非活动状态。此功能对于定时器控制电源开关或电动机的应用非常有用。它可以防止功率级因电流过大而损坏,或在遇到断点时使电机处于不受控制的状态。宏 __HAL_DBGMCU_UNFREEZE_TIMx() 恢复默认行为(即,定时器在断点期间不会停止)。

        请注意,在调用 __HAL_DBGMCU_FREEZE_TIMx() 宏之前,必须通过调用 __HAL_RCC_DBGMCU_CLK_ENABLE() 宏来启用 MCU 调试组件 (DBGMCU)。

4、SysTick 定时器

        SysTick 是 Cortex-M 内核内部的特殊定时器,由所有 STM32 微控制器提供。它主要用作 CubeHAL 和 RTOS(如果使用)的时基生成器。SysTick 定时器最重要的一点是,如果用作 HAL 的时基生成器,则必须将其配置为每 1ms 生成一次异常:异常处理程序将增加系统滴答计数器(一个全局的、32 位宽的静态变量),可以通过调用 HAL_GetTick() 例程来访问。

        SysTick 是一个 24 位 downcounter,由 AHB 总线计时(即,它与高(快速)时钟 - HCLK 具有相同的频率)。它的时钟速度最终可以使用函数

void HAL_SYSTICK_CLKSourceConfig(uint32_t CLKSource);

除以 8;它接受参数 SYSTICK_CLKSOURCE_HCLK 和 SYSTICK_CLKSOURCE_HCLK_DIV8。

        SysTick 更新频率由 SysTick 计数器的起始值决定,该计数器使用函数进行配置:

uint32_t HAL_SYSTICK_Config(uint32_t TicksNumb);

要配置 SysTick 定时器,使其每 1ms 生成一次更新事件,并假设它的定时速度与 AHB 总线相同,则通过以下方式调用 HAL_SYSTICK_Config() 就足够了:

HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);

HAL_SYSTICK_Config() 例程还负责启用定时器及其SysTick_IRQn exception。(SysTick_IRQn 是一个异常而不是中断,即使通常将其称为中断。这意味着我们不能使用 HAL_NVIC_EnableIRQ() 函数来启用它。)  异常的优先级可以在编译时在 include/stm32XXxx_hal_conf.h 文件中设置 TICK_INT_PRIORITY 符号常量,或者通过在 SysTick_IRQn 异常上调用 HAL_NVIC_SetPriority() 来配置。

        当 SysTick 计时器达到零时,将引发 SysTick_IRQn 异常,并调用相应的处理程序。CubeMX 已经为我们提供了正确的函数,其定义方式如下:

void SysTick_Handler(void) {
    HAL_IncTick();
    HAL_SYSTICK_IRQHandler();
}

HAL_IncTick() 会自动增加全局 SysTick 计数器,而 HAL_SYSTICK_IRQHandler() 只包含对 HAL_SYSTICK_Callback() 例程的调用,这是一个回调,我们可以选择实现该回调,以便在定时器下溢时收到通知。

        避免在 HAL_SYSTICK_Callback() 例程中使用慢速代码,否则可能会影响时基生成。这可能会导致某些 HAL 模块出现不可预测的行为,这些模块依赖于精确的 1 毫秒时基生成。

        此外,使用 HAL_Delay() 时必须小心。此函数根据 SysTick 计数器提供准确的延迟(以毫秒为单位)。这意味着,如果 HAL_Delay() 是从外设 ISR 进程调用的,则 SysTick 中断必须具有比外设中断更高的优先级(数值上更低)。否则,调用方 ISR 进程将被阻止 (因为全局时钟周期计数器永远不会递增) 。要暂停系统时基生成,可以使用 HAL_SuspendTick() 例程,而要恢复它,可以使用 HAL_ResumeTick() 例程。

4.1、使用另一个定时器作为系统时基源

        SysTick 定时器只有一个相关的应用程序:作为 HAL 的时基生成器或可选的 RTOS。由于 SysTick 时钟不能轻易地预缩放到更灵活的计数频率,因此它不适合用作传统定时器。但是,它有一个相关的限制:它不适合与某些 RTOS 为低功耗应用程序提供的无滴答模式一起使用。因此,有时使用另一个计时器(可能是 LPTIM)作为系统时基生成器很重要。最后,在使用 RTOS 时,将 HAL 和 RTOS 的时基源分开很方便。

        CubeMX 允许轻松使用另一个定时器而不是 SysTick。要执行此操作,请进入 Pinout 视图,然后从 Categories 窗格中打开 RCC 条目并选择 Timebase 源,如下图所示。

        CubeMX 将生成一个名为 stm32XXxx_hal_timebase_TIM.c 的附加文件,其中包含 HAL_InitTick() 的定义(包含初始化定时器的所有必要代码,使其每 1 毫秒溢出一次)、HAL_SuspendTick() 和 HAL_ResumeTick(),以及 HAL_TIM_PeriodElapsedCallback() 的定义,其中包含对 HAL_IncTick() 例程的调用。HAL 例程的这种“覆盖”是可能的,因为这些函数是在 HAL 源文件中__weak定义的。

5、案例研究:如何使用 STM32 MCU 精确测量微秒

        有时,尤其是在处理外设未在硬件中实现的通信协议时,我们需要精确测量从 1 到 1 微秒不等的延迟。这就引出了另一个更普遍的问题:如何在 STM32 MCU 中精确测量微秒?有几种方法可以做到这一点,但有些方法更准确,而另一些方法在不同的 MCUs 和 clock configurations 中更通用。

        让我们考虑一下 STM32F4 家族中的一个成员:STM32F401RE。该型能够使用内部 RC 时钟运行高达 84MHz。这意味着每 1μs , clock 振动周期为 84 次。因此,我们需要一种方法来计算 84 个 clock cycles 来断言已经过去了 1μs (假设可以容忍内部 RC clock 的 1% 精度)。有时,通常会发现像下面这样的 delay 例程:

void delay1US() {
	#define CLOCK_CYCLES_PER_INSTRUCTION X
	#define CLOCK_FREQ Y //IN MHZ (e.g., 16 for 16 MHZ)
	
	volatile int cycleCount = CLOCK_FREQ / CLOCK_CYCLE_PER_INSTRUCTION;
	
	while (cycleCount--);
}

        但是如何确定计算 while(cycleCount- -) 指令的一个步骤需要多少个 clock cycles 呢?不幸的是,给出答案并不简单。我们假设 cycleCount 等于 1。做一些测试,在禁用编译器优化(选项 -O0 到 GCC)的情况下,我们可以看到在这种情况下,整个 C 指令需要 24 个周期来执行。这怎么可能呢?必须弄清楚我们的 C 语句在几个汇编指令中展开,看到反汇编固件二进制文件:

        此外,另一个延迟来源与从内部 MCU 闪存获取指令有关(这与“低成本”STM32 MCU 和更强大的 MCU 有很大不同,例如带有 ART 加速器的 STM32F4 和 STM32F7,ART 加速器旨在将闪存访问延迟归零)。因此,该指令的“基本成本”为 24 个周期。如果 cycleCount 等于 2,则需要多少个周期?在这种情况下,MCU 需要 33 个周期,即 9 个额外的周期。这意味着,如果我们想延迟 84 个周期,cycleCount 必须等于 (84-24)/9,大约是 7。因此,我们可以用更通用的方式编写我们的延迟函数:

void delayUS(uint32_t us) {
    volatile uint32_t counter = 7*us;
    while(counter--);
}

使用以下代码测试此函数:

while(1) {
    delayUS(1);
    GPIOA->ODR = 0x0;
    delayUS(1);
    GPIOA->ODR = 0x20;
}

        我们可以使用连接到 PA5引脚的示波器检查我们是否获得了我们正在寻找的延迟:这种延迟 1μs 的方法是否一致?不幸的是,答案是否定的。首先,只有当这个特定的 MCU (STM32F401RE) 以全速 (84MHz) 工作时,它才能正常工作。如果我们决定使用不同的 clock speed,我们需要重新安排它进行测试。其次,它受编译器优化的影响(我们很快就会看到),以及某些STM32微控制器中D-Bus和I-Bus上的CPU内部缓存(这些缓存最终可以通过在include/stm32XXxx_hal_conf.h文件中设置(PREFETCH_ENABLE、INSTRUCTION_CACHE_ENABLE、DATA_CACHE_ENABLE)。

        让我们为 “size” (-Os) 启用 GCC 优化。我们得到什么结果?在这种情况下,delayUS() 函数只消耗 72 个 CPU 周期,即 ∼850ns。示波器证实了这一点:如果我们启用速度的最大优化 (-O3) 会发生什么?在这种情况下,我们只有 64 个 CPU 周期,也就是说我们的 delayUS() 只持续 ∼750ns。但是,可以使用特定的 GCC pragma 指令解决此问题:

#pragma GCC push_options
#pragma GCC optimize ("O0")
void delayUS(uint32_t us) {
    volatile uint32_t counter = 7*us;
    while(counter--);
}
#pragma GCC pop_options

        但是,如果我们想使用较低的 CPU 频率,或者我们想将代码移植到不同的 STM32 MCU,我们仍然需要再次重做测试并根据经验推导出周期数。

        考虑到 CPU 频率越低,就越难精确延迟 1μs,因为给定指令的周期数是固定的,但在同一时间单位内的周期数较少。那么,如果我们改变硬件设置,我们如何在不进行测试的情况下获得精确的 1μs 延迟呢?一个答案可以通过设置一个每 1μs 溢出一次的定时器来表示(只需将其 Period 设置为以 MHz 为单位的外设总线速度 - 例如,对于一个 STM32F401RE我们需要将 Period 设置为 (84 - 1)),我们可以增加一个跟踪经过的微秒的全局变量。这与使用 SysTick 计时器生成 HAL 的时基的方式相同。

        但是,这种方法不切实际,尤其是对于低速 STM32 MCU。每 1μs 生成一个中断(在全速运行的 STM32F0 MCU 中意味着每 48 个 CPU 周期)将使 MCU 拥塞,从而降低整体多编程程度。此外,中断管理的成本不可忽视(从 12 个周期到 16 个周期),这将影响 1μs 时基的生成。同样,轮询 timer 的 counter 值也是不切实际的:将花费大量时间根据起始值检查 counter ,并且 timer overflow/underflow 的处理会影响时基生成。

        更可靠的解决方案来自前面的测试。如何测量 CPU 周期?CortexM3/4/7 处理器可以有一个可选的调试单元,名为 Data Watchpoint and Tracing (DWT),它为处理器提供观察点、数据跟踪和系统分析。该单元的一个寄存器是 CYCCNT,它计算 CPU 执行的周期数。因此,我们可以使用这个可用的特殊单元来计算 MCU 在指令执行期间执行的周期数。

uint32_t cycles = 0;
/* DWT struct is defined inside the core_cm4.h file */
DWT->CTRL |= 1 ; // enable the counter
DWT->CYCCNT = 0; // reset the counter
delayUS(1);
cycles = DWT->CYCCNT;
cycles--; /* We subtract the cycle used to transfer CYCCNT content to cycles variable */

        使用 DWT,我们可以这样构建一个更通用的 delayUS() 例程:

#pragma GCC push_options
#pragma GCC optimize ("O3")
void delayUS_DWT(uint32_t us) {
	volatile uint32_t cycles = (SystemCoreClock/1000000L)*us;
	volatile uint32_t start = DWT->CYCCNT;
	do {
	} while(DWT->CYCCNT - start < cycles);
}
#pragma GCC pop_options

这个函数有多精确?如果对 1μs 的最佳分辨率感兴趣,则此函数对您没有帮助,如下 scope 所示。

当设置了更高的编译器优化级别时,可以获得最佳性能。对于1μs的所需延迟,该函数给出大约1.22μs的延迟(慢22%)。但是,如果我们需要旋转 10μs,我们会得到 10.5μs 的实际延迟(慢 5%),这更接近我们想要的。

从 100μs 的延迟开始,误差完全可以忽略不计。

        为什么这个函数不是那么精确呢?要理解为什么这个函数不如另一个精确,必须弄清楚我们正在使用一系列指令来检查自函数启动以来过期了多少个周期(while 条件)。这些指令需要消耗 CPU 周期,以使用 CYCCNT register 的内容更新内部 CPU 寄存器,并进行比较和分支。但是,此功能的优点是它会自动检测 CPU 速度,并且开箱即用,特别是当我们在更快的处理器上工作时。

        如果想完全控制编译器优化,可以使用这个完全用汇编器编写的宏来达到最佳的 1μs 延迟:

#define delayUS_ASM(us) do { \
	asm volatile ("MOV R0,%[loops]\n \
		1: \n \
		SUB R0, #1\n \
		CMP R0, #0\n \
		BNE 1b \t" \
		: : [loops] "r" (16*us) : "memory" \
		); \
} while(0)

这是编写 while(counter--) 函数的最优化方法。使用示波器进行测试,我发现当 MCU 以 84MHZ 执行此循环 16 次时,可以获得 1μs 的延迟。但是,如果您的处理器速度较低,则必须重新排列此宏,并且请记住,作为一个宏,每次使用时它都会“扩展”,从而导致固件大小增加。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/922797.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

质量留住用户:如何通过测试自动化提供更高质量的用户体验

在当今竞争异常激烈的市场中&#xff0c;用户手头有无数种选择&#xff0c;但有一条真理至关重要&#xff1a; 质量留住用户。 产品的质量&#xff0c;尤其是用户体验 (UX)&#xff0c;直接决定了客户是留在您的品牌还是转而选择竞争对手。随着业务的发展&#xff0c;出色的用户…

C++ 优先算法 —— 长度最小的子数组(滑动窗口)

目录 题目&#xff1a;长度最小的子数组 1. 题目解析 2. 算法原理 Ⅰ. 暴力枚举 Ⅱ. 滑动窗口&#xff08;同向双指针&#xff09; 滑动窗口正确性 3. 代码实现 Ⅰ. 暴力枚举(会超时&#xff09; Ⅱ. 滑动窗口&#xff08;同向双指针&#xff09; 题目&#xff1a;长…

GPT系列文章

GPT系列文章 GPT1 GPT1是由OpenAI公司发表在2018年要早于我们之前介绍的所熟知的BERT系列文章。总结&#xff1a;GPT 是一种半监督学习&#xff0c;采用两阶段任务模型&#xff0c;通过使用无监督的 Pre-training 和有监督的 Fine-tuning 来实现强大的自然语言理解。在 Pre-t…

进程间通信5:信号

引入 我们之前学习了信号量&#xff0c;信号量和信号可不是一个东西&#xff0c;不能混淆。 信号是什么以及一些基础概念 信号是一种让进程给其他进程发送异步消息的方式 信号是随时产生的&#xff0c;无法预测信号可以临时保存下来&#xff0c;之后再处理信号是异步发送的…

代理模式:静态代理和动态代理(JDK动态代理原理)

代理模式&#xff1a;静态代理和动态代理以及JDK动态代理原理 为什么要使用代理模式&#xff1f;静态代理代码实现优缺点 动态代理JDK动态代理JDK动态代理原理JDK动态代理为什么需要被代理的对象实现接口&#xff1f;优缺点 CGLIB动态代理优缺点 代理模式的应用 为什么要使用代…

【AI技术赋能有限元分析应用实践】pycharm终端与界面设置导入Abaqus2024自带python开发环境

目录 一、具体说明1. **如何在 Windows 环境中执行 Abaqus Python 脚本**2. **如何在 PyCharm 中配置并激活 Abaqus Python 环境**3. **创建 Windows 批处理脚本自动执行 Abaqus Python 脚本**总结二、方法1:通过下面输出获取安装路径导入pycharm方法2:终端脚本执行批处理脚本…

【消息序列】详解(6):深入探讨缓冲区管理与流量控制机制

目录 一、概述 1.1. 缓冲区管理的重要性 1.2. 实现方式 1.2.1. HCI_Read_Buffer_Size 命令 1.2.2. HCI_Number_Of_Completed_Packets 事件 1.2.3. HCI_Set_Controller_To_Host_Flow_Control 命令 1.2.4. HCI_Host_Buffer_Size 命令 1.2.5. HCI_Host_Number_Of_Complete…

虚拟局域网PPTP配置与验证(二)

虚拟局域网PPTP配置与验证(二) windows VPN客户端linux 客户端openwrt客户端性能验证虚拟局域网PPTP配置与验证(一)虚拟局域网PPTP配置与验证(二) : 本文介绍几种客户端连接PPTP服务端的方法,同时对linux/windows/openwrt 操作系统及x86、arm硬件平台下PPTP包转发性能进…

uniapp中使用uni-forms实现表单管理,验证表单

前言 uni-forms 是一个用于表单管理的组件。它提供了一种简化和统一的方式来处理表单数据&#xff0c;包括表单验证、字段绑定和提交逻辑等。使用 uni-forms可以方便地创建各种类型的表单&#xff0c;支持数据双向绑定&#xff0c;可以与其他组件及API进行良好的集成。开发者可…

Hive构建日搜索引擎日志数据分析系统

1.数据预处理 根据自己或者学校系统预制的数据 使用less sogou.txt可查看 wc -l sogou.txt 能够查看总行数 2.数据扩展部分 我的数据位置存放在 /data/bigfiles 点击q退出 将一个文件的内容传递到另一个目录文件下 原数据在 /data/bigfiles ->传递 到/data/workspac…

网络安全的学习方向和路线是怎么样的?

最近有同学问我&#xff0c;网络安全的学习路线是怎么样的&#xff1f; 废话不多说&#xff0c;先上一张图镇楼&#xff0c;看看网络安全有哪些方向&#xff0c;它们之间有什么关系和区别&#xff0c;各自需要学习哪些东西。 在这个圈子技术门类中&#xff0c;工作岗位主要有以…

深入浅出分布式缓存:原理与应用

文章目录 概述缓存分片算法1. Hash算法2. 一致性Hash算法3. 应用场景Redis集群方案1. Redis 集群方案原理2. Redis 集群方案的优势3. Java 代码示例:Redis 集群数据定位Redis 集群中的节点通信机制:Gossip 协议Redis 集群的节点通信:Gossip 协议Redis 集群的节点通信流程Red…

Mysql的加锁情况详解

最近在复习mysql的知识点&#xff0c;像索引、优化、主从复制这些很容易就激活了脑海里尘封的知识&#xff0c;但是在mysql锁的这一块真的是忘的一干二净&#xff0c;一点映像都没有&#xff0c;感觉也有点太难理解了&#xff0c;但是还是想把这块给啃下来&#xff0c;于是想通…

论文模型设置与实验数据:scBERT

Yang, F., Wang, W., Wang, F. et al. scBERT as a large-scale pretrained deep language model for cell type annotation of single-cell RNA-seq data. Nat Mach Intell 4, 852–866 (2022). https://doi.org/10.1038/s42256-022-00534-z 论文地址&#xff1a;scBERT as a…

TCP三次握手的过程是怎样的?

一开始&#xff0c;客户端和服务端都处于CLOSE状态。先是服务端主动监听某个端口&#xff0c;处于LISTEN状态。 &#xff08;1&#xff09;第一次握手 客户端会随机初始化序号&#xff08;client_isn&#xff09;&#xff0c;将此序号填入TCP首部的32位序号字段中&#xff0c…

Java核心知识详解:String类、StringBuffer、数组及日期时间的全面解析

&#x1f680; 作者 &#xff1a;“码上有前” &#x1f680; 文章简介 &#xff1a;Java &#x1f680; 欢迎小伙伴们 点赞&#x1f44d;、收藏⭐、留言&#x1f4ac; 标题 Java核心知识详解&#xff1a;String类、StringBuffer、数组及日期时间的全面解析 摘要 在Java中…

【MATLAB源码-第218期】基于matlab的北方苍鹰优化算法(NGO)无人机三维路径规划,输出做短路径图和适应度曲线.

操作环境&#xff1a; MATLAB 2022a 1、算法描述 北方苍鹰优化算法&#xff08;Northern Goshawk Optimization&#xff0c;简称NGO&#xff09;是一种新兴的智能优化算法&#xff0c;灵感来源于北方苍鹰的捕猎行为。北方苍鹰是一种敏捷且高效的猛禽&#xff0c;广泛分布于北…

SplatFormer: Point Transformer for Robust3D Gaussian Splatting 论文解读

目录 一、概述 二、相关工作 1、NVI新视角插值 2、稀疏视角重建 3、OOD-NVS 4、无约束重建下的正则化技术 5、基于学习的2D-to-3D模型 6、3D点云处理技术 三、SplatFormer 1、Point Transformer V3 2、特征解码器 3、损失函数 四、数据集 五、实验 一、概述 该论…

Azkaban部署

首先我们需要现在相关的组件&#xff0c;在这里已经给大家准备好了相关的安装包&#xff0c;有需要的可以自行下载。 只需要启动hadoop集群就可以&#xff0c;如果现在你的hive是打开的&#xff0c;那么请你关闭&#xff01;&#xff01;&#xff01; 如果不关会造成证书冲突…

目标检测模型优化与部署

目录 引言数据增强 随机裁剪随机翻转颜色抖动 模型微调 加载预训练模型修改分类器训练模型 损失函数 分类损失回归损失 优化器算法思路 RPN (Region Proposal Network)Fast R-CNN损失函数 部署与应用 使用 Flask 部署使用 Docker 容器化 参考资料 引言 目标检测是计算机视觉…