中英文网站源码,ghost 博客wordpress,爱南宁app下载官网中小学,flash网站php源码嵌入式设备会按时间执行某些活动。对于真正简单且不准确的延迟#xff0c;繁忙的循环可以执行任务#xff0c;但是使用 CPU 内核执行与时间相关的活动从来都不是一个聪明的解决方案。因此#xff0c;所有微控制器都提供专用的硬件外设#xff1a;定时器。定时器不仅是时基生… 嵌入式设备会按时间执行某些活动。对于真正简单且不准确的延迟繁忙的循环可以执行任务但是使用 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 请求。 StateHAL 在内部使用它来跟踪计时器状态。 所有计时器配置活动都是通过使用 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到0xFFFF65535对于将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 寄存器确定定时器频率即溢出需要多长时间可以多久生成一次更新事件根据以下简单公式 例如假设一个定时器连接到 STM32F072 MCU 中的 APB1 总线HCLK 设置为 48MHzPrescaler 值等于 47999Period 值等于 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 1000Hzhtim6.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 例程的性能对于运行得非常快的 timerHAL_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 仅在高级计时器中可用这会导致计算更新事件频率的公式变为 让 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 1000Hzhtim6.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 – 引脚 2TI2FP2 • 外部 ETR 引脚 – ETR1 引脚 – ETR2 引脚
相反定时器可以由以下位置触发 • 内部触发输入 0 至 3 – ITR0、ITR1、ITR2 和 ITR3 使用另一个定时器作为主定时器 • 外部输入通道引脚 – 引脚 1 TI1FP1 或 TI1F_ED – 引脚 2TI2FP2 • 外部 ETR1 引脚
让我们通过分析实际示例来研究这些从外部源计时/触发定时器的方法。
3.1.1、外部时钟模式 2 通用定时器能够从外部源进行计时将它们设置为两种不同的模式 External Clock Source Mode 1 和 2。当定时器配置为 slave 模式时第一个可用。我们将在下一段中研究这种模式。 相反第二种模式只需使用 external clock source 即可激活。这允许使用更准确和专用的源并最终进一步降低计数频率。事实上当选择 External Clock Source Mode 2 时计算更新事件频率的公式变为 其中 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-InstanceTIM3) {/* Peripheral clock enable */__HAL_RCC_TIM3_CLK_ENABLE();__HAL_RCC_GPIOD_CLK_ENABLE();/**TIM3 GPIO ConfigurationPD2 ------ 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 时计算更新事件频率的公式变为 其中 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-InstanceTIM3) {/* Peripheral clock enable */__HAL_RCC_TIM3_CLK_ENABLE();__HAL_RCC_GPIOA_CLK_ENABLE();/**TIM3 GPIO ConfigurationPA6 ------ 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-InstanceTIM3) {__HAL_RCC_TIM3_CLK_ENABLE();}if(htim_base-InstanceTIM5) {__HAL_RCC_TIM5_CLK_ENABLE();__HAL_RCC_GPIOA_CLK_ENABLE();/**TIM5 GPIO ConfigurationPA0-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并使用以下公式 其中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指定计时器使用的输入。它可以采用下表中的值。可以选择性地将输入通道重新映射到不同的输入源即 IC1IC2 映射到 TI2TI1IC3IC4 映射到 TI4TI3。通常这用于区分 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-InstanceTIM3){__HAL_RCC_TIM3_CLK_ENABLE();__HAL_RCC_GPIOA_CLK_ENABLE();/**TIM3 GPIO ConfigurationPA6 ------ 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-InstanceTIM6){__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 的频率运行。(PCLK136MHz) 然后将第一个通道配置为在输入信号的每个上升沿触发捕获事件 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.hvoid 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 模式如下图所示。 相反要将耦合的另一个通道 IC1IC2 或 IC3IC4 映射到相同的输入即 IC1IC2 的 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 计数器匹配并且通道配置为在输出比较模式下工作时将生成特定中断如果启用。这允许独立控制每个通道的开关频率并最终在通道之间执行相移。通道频率可以使用以下公式计算 其中TIMx_CLK 是定时器的运行频率CCRx 是用于配置通道的 TIM_OnePulse_InitTypeDef 结构的 Pulse 值。这意味着我们可以通过以下方式计算给定通道频率的脉冲值 显然重要的是要强调必须设置定时器频率以便使用上式计算的脉冲值低于定时器周期值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; //PCLK148MHz;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-InstanceTIM3){__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-InstanceTIM3){__HAL_RCC_GPIOB_CLK_ENABLE();/**TIM3 GPIO ConfigurationPB0 ------ 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 是占空比 TON 是信号active的时间。因此50% 的占空比意味着信号在 50% 的时间内处于开启状态但在 50% 的时间内处于关闭状态。占空比没有说明它持续多长时间。50% 占空比的“on time”可能是几分之一秒、一天甚至一周具体取决于周期的长度。脉冲宽度是 TON 的持续时间给定实际周期。例如假设周期为 1s则 20% 的占空比会产生 200ms 的脉冲宽度。 上图显示了三种不同的占空比50%、20% 和 80%。 脉宽调制 PWM 是一种技术用于在给定时间段内或以给定频率生成具有不同占空比的多个脉冲。PWM 在数字电子学中有许多应用但它们都可以分为两大类 ---控制输出电压以及电流 ---在载波 以给定频率运行 上编码 即调制 消息 即数字电子学中的一系列字节。 这两个类别可以在 PWM 技术的几个实际用法中扩展。将注意力集中在输出电压的控制上我们可以发现几种应用 ---产生从 0V 到 VDD 的输出电压即 I/O 的最大允许电压在 STM32 中为 3.3VLED 调光电机控制功率转换 ---生成以给定频率运行的输出波 正弦波、三角形、方波等 ---声音输出。 通过适当的输出滤波通常涉及使用低通滤波器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/49919991144Hz。通过将 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/2000.016V因此预分频器值将为假设 MCU 以 48MHz 运行 以下示例显示了如何在以 48MHz 运行的MCU中产生 50Hz 纯正弦波。
#define PI 3.14159
#define ASR 1.8 //360 / 200 1.8int 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 的电容器它们的截止频率为 ∼159HzVpp 等于 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 和 OC2PA8、PA9通道连接到 TIM3 的 TI1 和 TI2 通道PA6、PA7并且它们被配置为产生两个具有相同周期但相位偏移的方波信号。然后在编码器模式下配置 TIM3。SysTick 计时器用于生成时基每 1 秒计算一次脉冲数以及编码器方向。然后推导出 RPM 的数量假设编码器每转产生 4 个脉冲。最后通过按下 USER 按钮可以改变 A 相和 B 相之间的相移这将反转编码器旋转。
#define PULSES_PER_REVOLUTION 4int 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;elsediff (65535 - cnt2) cnt1;} else {if (cnt2 cnt1) /* Check for counter overflow */diff cnt2 - cnt1;elsediff (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--);
} 但是如何确定计算 whilecycleCount- - 指令的一个步骤需要多少个 clock cycles 呢不幸的是给出答案并不简单。我们假设 cycleCount 等于 1。做一些测试在禁用编译器优化选项 -O0 到 GCC的情况下我们可以看到在这种情况下整个 C 指令需要 24 个周期来执行。这怎么可能呢必须弄清楚我们的 C 语句在几个汇编指令中展开看到反汇编固件二进制文件 此外另一个延迟来源与从内部 MCU 闪存获取指令有关这与“低成本”STM32 MCU 和更强大的 MCU 有很大不同例如带有 ART 加速器的 STM32F4 和 STM32F7ART 加速器旨在将闪存访问延迟归零。因此该指令的“基本成本”为 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)
这是编写 whilecounter-- 函数的最优化方法。使用示波器进行测试我发现当 MCU 以 84MHZ 执行此循环 16 次时可以获得 1μs 的延迟。但是如果您的处理器速度较低则必须重新排列此宏并且请记住作为一个宏每次使用时它都会“扩展”从而导致固件大小增加。