从 32 位定时器获取 64 位时间戳

Get 64bit timestamps from a 32bit timer

在我的 stm32wb55 上,我使用 32 位定时器“tim2”从系统启动后的 32 位寄存器“CNT”中读取时间。通过预缩放,我在 putty-console 上显示了以微秒为单位的时间,并且效果很好。但现在,我需要记住更高的价值。所以我想用64位整数来存储时间。

有人知道这样做的简单方法吗?

如果您仅从非 ISR [非中断服务] 上下文访问它,则非常简单。

如果您有 ISR,基础级别需要 lock/unlock 中断处理。 ISR not 必须与定时器中断相关。它可以用于 any ISR(例如 tty、磁盘、SPI、video/audio,等等)。

这里有一些简单的半裸机实现的代表性代码[这类似于我在一些 R/T 商业产品中所做的,特别是在 Xilinx FPGA 中的 microblaze 中]:

typedef unsigned int u32;
typedef unsigned long long u64;

volatile int in_isr;                    // 1=inside an ISR

volatile u32 oldlo;                     // old LSW timer value
volatile u32 oldhi;                     // MSW of 64 bit timer

// clear and enable the CPU interrupt flag
void cli(void);
void sti(void);

// tmr32 -- get 32 bit timer/counter
u32 tmr32(void);

// tmrget -- get 64 bit timer value
u64
tmrget(void)
{
    u32 curlo;
    u32 curhi;
    u64 tmr64;

    // base level must prevent interrupts from occurring ...
    if (! in_isr)
        cli();

    // get the 32 bit counter/timer value
    curlo = tmr32();

    // get the upper 32 bits of the 64 bit counter/timer
    curhi = oldhi;

    // detect rollover
    if (curlo < oldlo)
        curhi += 1;

    oldhi = curhi;
    oldlo = curlo;

    // reenable interrupts
    if (! in_isr)
        sti();

    tmr64 = curhi;
    tmr64 <<= 32;
    tmr64 |= curlo;

    return tmr64;
}

// isr -- interrupt service routine
void
isr(void)
{

    // say we're in an ISR ...
    in_isr += 1;

    u64 tmr = tmrget();
    // do stuff ...

    // leaving the ISR ...
    in_isr -= 1;
}

// baselevel -- normal non-interrupt context
void
baselevel(void)
{

    while (1) {
        u64 tmr = tmrget();
        // do stuff ...
    }
}

如果 tmrget 被足够频繁地调用以捕获 32 位计时器值的 each 次翻转,则此方法工作正常。

tim2定时器是32位分辨率定时器,你要64位分辨率。有两种方法可以模拟 64 位计数器,以跟踪您的正常运行时间。

  1. 每当您达到要跟踪的时间单位时,就会递增一个变量。但这将是极其低效的,因为微控制器将进行大量持续的上下文切换。

  2. 第二种方法是用 32 位变量扩展定时器。然后在溢出时增加这样的变量。

MSB                           LSB
+--------------+ +--------------+
|  32bit uint  | |  32bit timer |
+--------------+ +--------------+

它的工作方式是在计时器达到 0xffffffff 后,这是 32 位无符号计数器的最大值,计时器将溢出并从 0 开始。如果在第 32 位之后还有另一位位,它将打开(与递增相同)。您可以做的是通过递增变量来模拟这种确切的行为。

首先,设置您的计时器。

static TIM_HandleTypeDef s_TimerInstance = { 
    .Instance = TIM2
};

void setup_timer()
{
    __TIM2_CLK_ENABLE();
    s_TimerInstance.Init.Prescaler = ##; //Chose the correct value that fits your needs
    s_TimerInstance.Init.CounterMode = TIM_COUNTERMODE_UP;
    s_TimerInstance.Init.Period = 0xffffffff; //Chose the correct value that fits your needs
    s_TimerInstance.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; //Also choose this value
    s_TimerInstance.Init.RepetitionCounter = 0;
    HAL_TIM_Base_Init(&s_TimerInstance);
    HAL_TIM_Base_Start(&s_TimerInstance);
}

你的处理程序,每次你的计时器到达时都必须调用它 0xffffffff

extern void TIM2_IRQHandler();
void TIM2_IRQHandler()
{
    HAL_TIM_IRQHandler(&s_TimerInstance);
}

uint32_t extension;

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    extension++; //Increment
}

合并扩展变量和定时器值。每次要获取扩展器计数器值时都使用此函数。您可以将其设为内联以避免额外调用,或将其设为宏。

uint64_t get_time()
{
    return (extension << 32) & (__HAL_TIM_GET_COUNTER(&s_TimerInstance));
}

现在把所有东西粘在一起

int main(void)
{
    HAL_Init(); //Initialize HAL library
    InitializeTimer(); //Initialize timer
    
    HAL_NVIC_SetPriority(TIM2_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(TIM2_IRQn);

    while(1);
}

注意,现在 tim2 将被使用直到它溢出。它不应更改,否则以下代码将不起作用。此外,设置分频器,使计时器如您之前所述每微秒递增一次。

另外,您可以使用计时器来计算秒数,然后计算微秒。如果你数秒,你最多可以数 2^32 秒,即 4294967296。一年大约有 31536000 秒。使用 32 位计数器 (4294967296/31536000),您最多可以计算 136.19252 年的正常运行时间。然后通过将正常运行时间除以 1000000 (uptime/1000000) 得到微秒。我不知道你打算用微控制器做什么,但数秒对我来说听起来更有意义。

如果你真的想要精度,你仍然可以通过计算秒来实现,你可以将定时器计数器值添加到微秒计数,你可以通过将秒细分为微秒来获得微秒,这样你就可以抵消微秒还没有被添加到第二个计数中。