【问题标题】:How to deal with a wrapping counter in embedded C如何处理嵌入式 C 中的包装计数器
【发布时间】:2011-03-06 22:54:45
【问题描述】:

我需要处理一个计数器,它可以为我的应用程序提供记号。计数器是 32 位的,所以我需要知道的是如何处理它。例如:

我有一个返回 (timestamp + shifttime) 的函数,我还有另一个函数将返回 1 或 0,具体取决于时间是否已过,但我的计数器有可能会换行,我该如何处理这个?


非常感谢大家的回复。我将在此编辑中提供更多详细信息。

我正在使用 STM32 Cortex-M3。我想使用 RTC 计数器将其用作我的应用程序的刻度,以安排需要在特定时间间隔发生的任务。 RTC 可以产生溢出中断,因此检测中断不是问题。我遇到的主要问题(或者至少我认为是一个问题)是某些任务获得(timestamp+shift)

int main( void )
{
    FlashLedTimeStamp = ReturnCounter( 20 );  // currentcounter value + a shift of 20
    StatusLedTimeStamp = ReturnCounter( 3 );  // currentcounter value + a shift of 3

// then later on ....
while(1)
{
    /* other tasks could go here */

    if( HasTimeElapsed( FlashLedTimeStamp ) )
    {
       /* do something and get another timestamp value */
       FlashLedTimeStamp = ReturnCounter( 20 );  // currentcounter value + a shift of 20
    }
    
    if( HasTimeElapsed( StatusLedTimeStamp ) )
    {
       /* do something and get another timestamp value */
       FlashLedTimeStamp = StatusLedTimeStamp( 3 );  // currentcounter value + a shift of 3
    }
}   
}

让我们假设我的 RTC 计数器只有 8 位长以便于计算。

如果我获得时间戳时当前计数器为 250,这意味着 FlashLedTimeStamp = 14StatusLedTimeStamp = 253 我将如何检查 FlashLedTimeStamp 是否已过期??

请记住,我不一定会一直检查以查看当前计数器是什么以及某些时间戳是否已过期。我希望这能说明我遇到的问题。

【问题讨论】:

  • 你的时钟滴答率是多少,计数器的起始值是多少?此外,您能否编辑有关您正在运行的平台(操作系统、芯片组、主板制造商等)的更多详细信息。可能有一些特定于平台的解决方案。

标签: c embedded integer-overflow


【解决方案1】:

只要开始计数和结束计数之间的差小于 232 并执行无符号 32 位算术,即使计数值跨越回绕点,这无关紧要。 (如果使用有符号算术,跨度必须小于 232/2)

例如:

Start count: 0xfffffff
End Count:   0x00000002 (incremented through 0,1,2 - i.e. three counts)

End - Start == 0x00000002 - 0xfffffff == 0x00000003

所以只要计数器是内置整数类型的位宽,并且使用该类型,就可以得到正确的答案。如果计数器寄存器的宽度可能不是内置整数类型的宽度,您可以通过屏蔽高阶“溢出”位来实现相同的效果。

如果您出于其他原因需要更大的计数,或者连续时间戳之间的差异太大,那么您可以简单地使用另一个整数,该整数在低阶计数器回绕时递增。这个整数会形成一个大整数的高位,所以第二个整数的LSB就是这个大整数的第33位。

【讨论】:

  • 这是正确答案。总是做减法并与零比较,而不是原始比较。减法“修复”了包装问题。
  • 可能3年后问了个傻问题,但是为什么start和end之间的差异需要小于2^32/2。例如:开始计数为 0xfffffff,结束计数为 0xffffffe,一切都会正常工作,结束和开始之间的差异是正确的。检查godbolt.org/z/zvYWTjeo7
  • @UrsescuIonut:你是对的。我显然是在假设有符号的算术 - 但经过反思,我不知道为什么。真正的问题也许是为什么当时没有人纠正我!修好了。
【解决方案2】:

如果您读取两个时间戳并且您的第一个读数大于第二个读数,那么您的计数器已经结束。这是检测包装计数器的基本方法。

但是,这不会检测计数器是否已回绕多次,或者计数器已回绕且恰好大于第一次读数的情况。既然您说这是一个嵌入式系统,并且您的描述使您的“计数器”听起来像一个时钟,请查看是否可以在时钟达到零时设置一个中断(这样每次时钟重置时您都会收到一个中断)。当此中断触发时,增加一个单独的计数器。这应该可以有效地为您的时钟增加额外的精度,并让您的计数器在不引起问题的情况下换行。

【讨论】:

  • 也许编辑说:“如果您读取两个时间戳读数递增计数器/计时器...” - 我从事的最后 3 个(嵌入式)项目使用一个 countdown 计时器,所以当第二个值大于第一个值时,就会发生回绕。例如,ARM Cortex M3 的内置 SYSTICK 计时器倒计时到 0,然后重新加载特定值,然后再次倒计时。不过答案很好。
  • @Dan- 好点。也许更清楚地说“如果您计算两个时间戳之间的(有符号)差异,并且结果的符号与您期望的相反,那么计数器就会被包装”。
  • 您需要计算或通过实验确定包装所需的时间,并确保您的采样速度比这更快。
【解决方案3】:

将无符号减法的结果转换为有符号并比较为零。当您足够频繁地检查溢出时应该处理溢出(并且您的超时小于计时器范围的一半)。

uint32_t timer( void);             // Returns the current time value
uint32_t timeout;

timeout = timer() + offset;

// wait until timer() reaches or exceeds timeout value
while ((int32_t)(timeout - timer()) > 0);

【讨论】:

  • 这样做的好处是只存储一个 uint32_t(超时)而不是两个(最后一个值和间隔)
【解决方案4】:

如果您使用无符号变量来存储您的计数器和计时器到期时间,那么您可以简单地使用这个测试:

if (current_time - expiry_time < 0x80000000UL)
    /* timer has expired */

这假设您每 0x80000000 个滴答声至少测试一次到期,并且您的最长计时器设置为在未来不到 0x80000000 个滴答刻到期。

【讨论】:

    【解决方案5】:

    这个问题有点模糊。一种可能性是在您第一次注意到时间已经过去时设置一个标志。一个万无一失的方法是添加第二个计数器,当第一个计数器溢出时递增。这实际上会创建一个不会溢出的 64 位计数器。

    【讨论】:

    • 这只是执行延迟。最终,第二个计数器也会溢出。
    • @Job:是的,但这是一个非常糟糕的延迟。 2 的 64 次方允许 18,446,744,073,709,551,616 个刻度。如果每个滴答声是一毫秒,那么这相当于 5.8 亿年多一点的时间跨度。
    • 当然,我只想说这不是问题的真正解决方案。
    • 适用于小于 5.8 亿年的时间段的解决方案不是解决问题的“真正”解决方案吗?为什么不呢?
    • 没错:这个解决方案起作用。我要说的是,这不是问题的真正 解决方案:您仍然无法检测到环绕。因为如果你找到了这样的解决方案,那么你的时钟是否变得更快并且每皮秒滴答一次都没关系(在这种情况下,计数器结束之前的时间只有半年!)。
    【解决方案6】:

    最简单的方法是创建一个“纪元计数器”,明确计算翻转次数。 (例如:您有一个计数秒数为 0..59 的硬件计数器。您的 epoch 计数器将计算分钟数,每次它注意到秒数计数器已翻转时递增。)

    你的 future_scheduler 函数然后读取当前的纪元和时间,并为你的事件计算一个新的纪元和时间。

    或者,您可以直接下注,并让您的计时功能在每个计时器滴答时将您的事件时间表计数为零。

    【讨论】:

      【解决方案7】:

      其中一种可能性是将两个变量都转换为 64 位长,然后求和。之后与最大 32 位值进行比较,以确定它是否已包装。

      【讨论】:

      • 大多数嵌入式目标不支持 64 位整数。
      • 在这种情况下,可以使用一个简单的包装器,在 2 x 32 位的帮助下模拟 64 位的操作(我想可以找到很多现成的实现)。
      【解决方案8】:

      让我们假设计数器倒计时(许多倒计时以节省逻辑中的门)。

      您首先需要知道达到 2^32 个刻度所需的时间,并且需要确保您对其进行过采样。

      如果要查找两个事件之间的时间段,请说开始和结束

      开始 = 读取计时器 上次 = 开始 翻转 = 0

      等待事情发生时

      nowtime = 读取计时器 if(nowtime>lasttime) rollover+=1(这是一个递减计数器) 上次=现在时间

      事件发生: end = 读取计时器

      总时间 = 开始 - 结束(这是一个递减计数器,请注意,即使在翻转时这个数学运算也有效)

      总时间 = 从滴答声到秒数、分钟数等的总时间/比例因子 总时间 += 翻转 * 秒/分钟/每 2^32 次计数

      如果你有一个向上计数器,那么现在

      如果您可以保证您的事件将在 2^32 计数内发生,那么您现在不需要进行翻转开始和结束之间从 0x00000000 到 0xFFFFFFFF。

      【讨论】:

        【解决方案9】:

        假设你正在处理无符号类型,你可以很容易地检查包装——

        if (timestamp + shifftime < timestamp) 
            it_wrapped();
        

        【讨论】:

        • 如果 shiftime 为负数怎么办?
        • 可能会考虑将其格式化为 Markdown 代码块而不是内联代码块,换行符(我知道这很讽刺)位于一个非常不幸的地方
        【解决方案10】:

        当您嵌入时,您可能会访问 CPU 溢出位。这将在添加溢出它的寄存器时设置。对于添加 AddCarry 链接很有用。

        【讨论】:

          【解决方案11】:

          我认为最简单的方法之一是使用另一个计数器(我们称之为包装计数器,让它成为计时器模块的静态全局),每次包装原始 32 位计数器时进行计数。

          在计数器滴答作响的函数中,每当此计数器达到其最大计数时,您的 Wrap 计数器就会增加。 因此,当您读取返回计时器是否已过的函数时,您还读取了 Wrap 计数器,以检查它被包裹了多少次。重要的是,也要这样做:每次阅读换行计数器时,您都想清除它,以供下次阅读。

          【讨论】:

          • 如果你只是把它当作一个更大的计数器的高位,你不需要清除它。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-12-21
          • 2014-08-21
          • 1970-01-01
          • 1970-01-01
          • 2017-11-24
          相关资源
          最近更新 更多