0xAA55 发表于 2021-1-30 00:31:21

【单片机】STM32F103实现精确到微秒级的时间值获取

# 定时器和计时函数介绍

定时器和计时器对于单片机而言是非常重要的功能。其中,定时器可以实现 **精确到时钟周期的中断向量调用** ,而计时器则相当于我们平时进行 Windows 编程时使用的 `GetTickCount()` 或者 `timeGetTime()` 函数的功能,被广泛用于各种各样的场合。

单片机由于其高实时性和更加接近底层物理电路实现的特性以及要求,通常会包含多种多样的计时器实现。此处介绍常见的 **STM32F103 常用的时间相关函数的思路实现**。

## 使用系统嘀嗒定时器 SysTick 实现精确到时钟周期的计时器

通常情况下在使用HAL的时候,获得时间值的函数是 `HAL_GetTick()` 。其为精确到毫秒级别的时间值获取,类似于 Windows 的 `GetTickCount()` 。虽然可以通过重新调用 `SysTick_Config()` 来设置自己的系统嘀嗒计时器的中断频率,但由于各种各样的第三方库都使用 `HAL_GetTick()` 方式来获得毫秒级时间值,所以 **一般不建议修改默认的系统嘀嗒频率** 。

但,在单纯使用系统嘀嗒、不借助TIM外设的情况下,依然可以通过读取 `SysTick->VAL` 的值来获得当前系统嘀嗒计数值,配合 `HAL_GetTick()` **可以得到精确到时钟周期的时间值** 。

首先我们可以观察 `SysTick_Type` 的结构体(在`core_cm3.h`里)的构造:

    typedef struct
    {
      __IOM uint32_t CTRL;/* 偏移:0x000 (读写)SysTick的控制与状态寄存器 */
      __IOM uint32_t LOAD;/* 偏移:0x004 (读写)SysTick的重新加载数值寄存器 */
      __IOM uint32_t VAL;   /* 偏移:0x008 (读写)SysTick的当前值寄存器 */
      __IMuint32_t CALIB; /* 偏移:0x00C (只读)SysTick的校正寄存器 */
    } SysTick_Type;

在单片机运行的时候,默认情况下(由 HAL 库进行初始化后的情况下)的 SysTick 的 `VAL` 寄存器会随每个时钟周期不断自增,当它达到 `LOAD` 的数值后,会被重置到 0 ,然后触发一次 SysTick 中断,此时HAL库会自动把Tick计数增加 1 。

而 HAL 库会把 `LOAD` 数值初始化为 `SystemCoreClock / (1000U / uwTickFreq)` ,其中 `SystemCoreClock` 值为单片机核心频率,比如通常的 `72 MHz` ,而 `uwTickFreq` 值为 1 (意味着 SysTick 中断的频率是 1 KHz),相当于 `LOAD` 数值被赋值为 `72000000 / 1000` 也就是 `72000` 。

此时,我们的获取时间值的函数可以这样编写:

        uint64_t GetTimeVal()
        {
          // 我们需要记录老的时间值,因为不保证这个函数被调用的期间SysTick的中断不会被触发。
          static uint64_t OldTimeVal;
          uint64_t NewTimeVal;

          // 新的时间值以Tick计数为毫秒部分,以SysTick的计数器值换算为微秒部分,获得精确的时间。
          NewTimeVal = (HAL_GetTick() * 1000) + (SysTick->VAL * 1000 / SysTick->LOAD);

          // 当计算出来的时间值小于上一个时间值的时候,说明在函数计算的期间发生了SysTick中断,此时应该补正时间值。
          if (NewTimeVal < OldTimeVal) NewTimeVal += 1000;
          OldTimeVal = NewTimeVal;

          // 返回正确的时间值
          return NewTimeVal;
        }

## 使用定时器外设 TIMx 实现精确到微秒的计时器

**注意:** 标题的 `TIMx` 的 `x` 指代定时器号,比如你的定时器可以是 `TIM1` `TIM2` `TIM3`, **根据你的使用情况** 来判断想用的 `TIM` 定时器。

**注意:** 文中使用 `TIM2` 作为例子,但你可以改用 `TIM1` `TIM3` 请根据 `STM32` 文档判断应该使用哪个 `TIM` 定时器。

定时器外设有各种各样的功能,包括但不限于生成PWM信号、比对输入的信号的PWM占比、PPM位置、生成特定频率的中断向量调用或者输出波形等。

由于定时器可以像系统嘀嗒一样设置中断向量的调用频率和读出其计时数值,在系统滴答被别的库占用、其工作方式发生了变化后,依然可以使用 `TIM` 定时器用作计时器数值来源。

一个最简单的用法就是设置 `TIM_Base` 的计数器 `CNT` 配合其 `Update` 事件中断来获得定时器的计数和其中断。

在使用 `STM32CubeMX` 的时候,将一个 `TIM` 比如 `TIM2` 当作此次需求所使用的定时器。把它的时钟来源 `Clock Source` 设置为内部时钟 `Internal Clock`,其它的比如通道的功能视情况调整,也可以不使用它,也就是将所有通道都设置为 `Disable` 。

在设置 `TIM2` 配置的时候,下方的 `Configuration` 里,把 `TIM2` 定时器的 `Prescaler` 设置为 `71` ,把计数模式 `Counter Mode` 设为向上增加计数 `Up` ,把计数周期 `Counter Period` 设为 `9999` 。

其中,`Prescaler` 这个参数的含义是 `经过多少个时钟周期后,计数器变动` ,而数值 `71` 的含义,参照手册的要求,是 `计数器变动所需周期数减去1` 。我把 `计数器变动所需周期数` 定为 `72`,是因为我的单片机的核心工作频率是 `72 MHz` ,而我希望 `TIM2` 的计数器变动频率为 `1 MHz` 。

而计数周期 `Counter Period` 设为 `9999` 的理由是我希望降低它的中断触发频率(原先考虑设置为 `999` ,即每个定时器周期需要1000次计数,但这样的话就相当于 TIM 中断的频率像系统嘀嗒那样是 `1 KHz`,但其实可以调大这个数值使其为 `0.1 KHz` ),而计数周期是一个 16 bit 寄存器,最大值是 `65535` ,所以我设置为范围内可接受的数值 `9999` (也就是 `0.1 KHz` 的中断频率)。

在 `NVIC Settings` 选项卡里,将对应你用的 `TIM` 定时器的 `TIM2 global interrupt` 勾上,`STM32CubeMX` 就可以在 `stm32f1xx_it.c` 源码文件里生成能和HAL库的中断处理函数联动的中断服务函数代码了。



生成代码后,打开 `main.c` ,在 `MX_TIM2_Init()` 函数末尾添加一句 `HAL_TIM_Base_Start_IT(&htim2)` ,此时 `TIM2` 的初始化代码就完成了。



由于定时器中断 `TIM2_IRQHandler` 调用 `HAL` 库的 `HAL_TIM_IRQHandler()` 处理过程,而 `HAL_TIM_IRQHandler()` 则会根据 `TIM` 中断的具体事件再调用各种不同的中断回调函数来通知使用者发生了定时器中断,所以我们要根据当前会发生的事件 `Update` 来编写对应的事件处理回调。

完整的根据 `TIM2` 来获取时间值的函数如下所示:

    // 用于表示特定精度的时间值数据类型
    // 此处使用 uint64_t 作为精确到微秒的时间值类型
    // 注意 uint32_t 计算更快,但是它能表示的时间范围过短
    // 可以根据需求修改为 uint32_t 变量类型
    typedef uint64_t microsec_t;
   
    // 定时器中断计数,类似于系统嘀嗒计数的用法
    // 使用volatile修饰是为了保证编译器优化的时候确保不会将它优化掉
    static volatile microsec_t TimerCounter = 0;

    // 事件处理回调函数
    void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
    {
      TimerCounter ++;
    }
   
    // 获取时间值
    microsec_t GetTimeVal()
    {
          // 我们需要记录老的时间值,因为不保证这个函数被调用的期间TIM2事件中断不会被触发。
          static uint64_t OldTimeVal;
          uint64_t NewTimeVal;

          NewTimeVal = (TimerCounter * 10000) + TIM2->CNT;

          // 当计算出来的时间值小于上一个时间值的时候,说明在函数计算的期间发生了TIM2事件中断,此时应该补正时间值。
          if (NewTimeVal < OldTimeVal) NewTimeVal += 10000;
          OldTimeVal = NewTimeVal;

          // 返回正确的时间值
          return NewTimeVal;
    }

在实现了功能后,可以配合使用 `USART` 实现串口输出字符串来看看获得的时间值是否正常。

如何使用 `STM32CubeMX` 使用串口外设的步骤此处省略。我封装了自己的串口 `printf()` 方式实现了串口打印字符串。

    static int UART_printf(const char *format, ...)
    {
      static char buf;
      int count;
      va_list ap;
      va_start(ap, format);
      count = vsnprintf(buf, sizeof buf, format, ap);
      va_end(ap);

      HAL_UART_Transmit(&huart1, (uint8_t*)buf, count, 100);
      return count;
    }

在 `RealTerm` 里可以看到它打印的时间值是真正按照时间走的:



页: [1]
查看完整版本: 【单片机】STM32F103实现精确到微秒级的时间值获取