7

概括

我正在尝试为MC9S12VR 微控制器编写嵌入式应用程序。这是一个 16 位微控制器,但我处理的一些值是 32 位宽,在调试时,我捕获了一些异常值,这些值似乎是由于读取撕裂造成的。

我正在用 C89 为这个微控制器编写固件并通过飞思卡尔 HC12 编译器运行它,我想知道是否有人对如何在这个特定的微控制器上防止它们有任何建议,假设是这种情况。

细节

我的部分应用程序涉及驱动电机并根据编码器产生的脉冲估计其位置和速度(电机每转一圈就会产生一个脉冲)。

为此,我需要配置一个 MCU 定时器,以便跟踪脉冲之间经过的时间。然而,定时器的时钟频率为 3 MHz(预分频后),定时器计数器寄存器只有 16 位,因此计数器每 ~22ms 溢出一次。作为补偿,我设置了一个在定时器计数器溢出时触发的中断处理程序,这会将“溢出”变量增加 1:

// TEMP
static volatile unsigned long _timerOverflowsNoReset;

// ...

#ifndef __INTELLISENSE__
__interrupt VectorNumber_Vtimovf
#endif
void timovf_isr(void)
{
  // Clear the interrupt.
  TFLG2_TOF = 1;

  // TEMP
  _timerOverflowsNoReset++;

  // ...
}

然后我可以从中计算出当前时间:

// TEMP
unsigned long MOTOR_GetCurrentTime(void)
{
  const unsigned long ticksPerCycle = 0xFFFF;
  const unsigned long ticksPerMicrosecond = 3; // 24 MHZ / 8 (prescaler)
  const unsigned long ticks = _timerOverflowsNoReset * ticksPerCycle + TCNT;
  const unsigned long microseconds = ticks / ticksPerMicrosecond;

  return microseconds;
}

main.c中,我临时编写了一些调试代码,以一个方向驱动电机,然后定期拍摄各种数据的“快照”:

// Test
for (iter = 0; iter < 10; iter++)
{
  nextWait += SECONDS(secondsPerIteration);
  while ((_test2Snapshots[iter].elapsed = MOTOR_GetCurrentTime() - startTime) < nextWait);
  _test2Snapshots[iter].position = MOTOR_GetCount();
  _test2Snapshots[iter].phase = MOTOR_GetPhase();
  _test2Snapshots[iter].time = MOTOR_GetCurrentTime() - startTime;
  // ...

在这个测试中,我MOTOR_GetCurrentTime()在两个非常接近的地方阅读代码并将它们分配给全局可用结构的属性。

在几乎所有情况下,我发现第一个读取的值比 while 循环应该终止的点多几微秒,而第二次读取是在那之后几微秒 - 这是预期的。但是,有时我发现第一次读取明显高于 while 循环应该终止的点,然后第二次读取小于第一个值(以及终止值)。

下面的屏幕截图给出了一个例子。在我能够重现它之前,我花了大约 20 次重复测试。在代码中,<snapshot>.elapsed是之前写入的,<snapshot>.time因此我希望它的值略小:

对于snapshot[8],我的应用程序首先读取20010014(超过应该终止忙循环的 10 毫秒),然后读取19988209. 正如我上面提到的,每 22 毫秒发生一次溢出 - 具体来说,_timerOverflowsNoReset一个单位的差异将产生65535 / 3计算出的微秒值的差异。如果我们考虑到这一点:

19988209 + \frac{65535}{3} - 20010014 = 20010054 - 20010014 = 40

40 的差异与我在其他读取对之间看到的差异(~23/24)相差不远,所以我的猜测是,涉及到_timerOverflowsNoReset. 就像在 while busy-looping 中一样,它会执行一次调用MOTOR_GetCurrentTime(),错误地认为_timerOverflowsNoReset比实际值大一,导致循环提前结束,然后在下一次读取时再次看到正确的值。

我的应用程序有其他问题,我无法确定,我希望如果我解决了这个问题,如果它们有类似的原因,它也可以解决这些其他问题。

编辑:在其他更改中,_timerOverflowsNoReset在我现在拥有的实现中,我已经将其他一些全局变量从 32 位无符号更改为 16 位无符号。

4

7 回答 7

4

您可以两次读取此值:

unsigned long GetTmrOverflowNo()
{
    unsigned long ovfl1, ovfl2;
    do {
        ovfl1 = _timerOverflowsNoReset;
        ovfl2 = _timerOverflowsNoReset;
    } while (ovfl1 != ovfl2);
    return ovfl1;
}

unsigned long MOTOR_GetCurrentTime(void)
{
  const unsigned long ticksPerCycle = 0xFFFF;
  const unsigned long ticksPerMicrosecond = 3; // 24 MHZ / 8 (prescaler)
  const unsigned long ticks = GetTmrOverflowNo() * ticksPerCycle + TCNT;
  const unsigned long microseconds = ticks / ticksPerMicrosecond;

  return microseconds;
}

如果_timerOverflowsNoReset增量慢得多然后执行GetTmrOverflowNo(),在最坏的情况下内部循环只运行两次。在大多数情况下ovfl1,并且ovfl2在第一次运行 while() 循环后将相等。

于 2018-07-02T09:40:28.510 回答
2

根据@AlexeyEsaulenko 和@jeb 提供的答案,我了解了这个问题的原因以及如何解决它。由于他们的答案都很有帮助,而我目前拥有的解决方案是两者的混合,我无法决定接受这两个答案中的哪一个,所以我会赞成这两个答案并保持这个问题的开放性。

这就是我现在实施的方式MOTOR_GetCurrentTime

unsigned long MOTOR_GetCurrentTime(void)
{
  const unsigned long ticksPerMicrosecond = 3; // 24 MHZ / 8 (prescaler)
  unsigned int countA;
  unsigned int countB;
  unsigned int timerOverflowsA;
  unsigned int timerOverflowsB;
  unsigned long ticks;
  unsigned long microseconds;

  // Loops until TCNT and the timer overflow count can be reliably determined.
  do
  {
    timerOverflowsA = _timerOverflowsNoReset;
    countA = TCNT;
    timerOverflowsB = _timerOverflowsNoReset;
    countB = TCNT;
  } while (timerOverflowsA != timerOverflowsB || countA >= countB);

  ticks = ((unsigned long)timerOverflowsA << 16) + countA;
  microseconds = ticks / ticksPerMicrosecond;
  return microseconds;
}

此功能可能不如其他建议的答案那么有效,但它让我相信它将避免一些已被曝光的陷阱。它通过重复读取定时器溢出计数和 TCNT 寄存器两次来工作,并且仅在满足以下两个条件时才退出循环:

  1. 在循环中第一次读取 TCNT 时,定时器溢出计数没有改变
  2. 第二个计数大于第一个计数

这基本上意味着如果MOTOR_GetCurrentTime在发生定时器溢出时调用,我们会等到安全地进入下一个周期,由第二个 TCNT 读取大于第一个(例如 0x0001 > 0x0000)表示。

这确实意味着该功能会阻塞,直到 TCNT 至少增加一次,但由于每 333 纳秒发生一次,我认为它没有问题。

我已经尝试连续运行 20 次测试并且没有发现任何撕裂,所以我相信这是可行的。如果我错了并且问题仍然存在,我将继续测试和更新这个答案。

编辑:正如 Vroomfondel 在下面的评论中指出的那样,我所做的检查也对我有用countA,如果阅读速度足够快,countB可能会导致循环无限期地重复。_timerOverflowsNoReset当我想出解决这个问题的方法时,我会更新这个答案。

于 2018-07-02T13:02:22.810 回答
2

计算滴答计数,然后检查是否溢出改变,如果是重复;

#define TCNT_BITS 16 ; // TCNT register width

uint32_t MOTOR_GetCurrentTicks(void)
{
   uint32_t ticks = 0 ;
   uint32_t overflow_count = 0;

   do
   {
       overflow_count = _timerOverflowsNoReset ;
       ticks = (overflow_count << TCNT_BITS) | TCNT;
   }
   while( overflow_count != _timerOverflowsNoReset ) ;

   return ticks ;
}

while 循环将不再重复一次或两次。

于 2018-07-02T19:13:07.660 回答
1

原子读取不是这里的主要问题。
这是溢出ISR和TCNT高度相关的问题。
当您先读取 TCNT 然后读取溢出计数器时,您会遇到问题。

三种示例情况:

TCNT=0x0000, Overflow=0   --- okay
TCNT=0xFFFF, Overflow=1   --- fails
TCNT=0x0001, Overflow=1   --- okay again

当您将顺序更改为:首先读取溢出,然后是 TCNT 时,您遇到了同样的问题。

您可以通过读取两倍的 totalOverflow 计数器来解决它。

disable_ints();
uint16_t overflowsA=totalOverflows;
uint16_t cnt = TCNT;
uint16_t overflowsB=totalOverflows;
enable_ints();

uint32_t totalCnt = cnt;

if ( overflowsA != overflowsB )
{
   if (cnt < 0x4000)
      totalCnt += 0x10000;
}

totalCnt += (uint32_t)overflowsA << 16;

如果在读取 TCNT 时 totalOverflowCounter 发生了变化,则有必要检查 in 中的值tcnt是否已经大于 0(但低于例如 0x4000)或者是否tcnt刚好在溢出之前。

于 2018-07-02T11:30:10.700 回答
1

一种有用的技术是维护两个或三个值,它们共同保存较大值的重叠部分。

如果知道一个值将单调递增,并且在调用“更新计时器”函数之间的计数永远不会超过 65,280,则可以使用类似的东西:

// Note: Assuming a platform where 16-bit loads and stores are atomic
uint16_t volatile timerHi, timerMed, timerLow;

void updateTimer(void)  // Must be only thing that writes timers!
{
  timerLow = HARDWARE_TIMER;
  timerMed += (uint8_t)((timerLow >> 8) - timerMed);
  timerHi  += (uint8_t)((timerMed >> 8) - timerHi);
}

uint32_t readTimer(void)
{
  uint16_t tempTimerHi  = timerHi;
  uint16_t tempTimerMed = timerMed;
  uint16_t tempTimerLow = timerLow;
  tempTimerMed += (uint8_t)((tempTimerLow >> 8) - tempTimerMed);
  tempTimerHi  += (uint8_t)((tempTimerMed >> 8) - tempTimerHi);
  return ((uint32_t)tempTimerHi) << 16) | tempTimerLow;
}

请注意,readTimer 在读取 timerLow 之前读取 timerHi。updateTimer 可能会在 readTimer 读取 timerHi 的时间和它读取其他值的时间之间更新 timerLow 或 timerMed,但如果发生这种情况,它会注意到 timerHi 的下半部分需要增加以匹配值的上半部分后来更新了。

这种方法可以级联到任意长度,并且不需要使用完整的 8 位重叠。然而,使用 8 位重叠可以通过使用上限值和下限值而简单地忽略中间值来形成 32 位值。如果使用较少的重叠,则所有三个值都需要参与最终计算。

于 2018-07-03T17:34:42.123 回答
0

问题是写入_timerOverflowsNoReset不是原子的,你不保护它们。这是一个错误。从 ISR 写入原子不是很重要,因为 HCS12 在中断期间会阻塞后台程序。但是在后台程序中读取 atomic 是绝对必要的。

另外,请记住 Codewarrior/HCS12 为 32 位算术生成的代码有些无效。

以下是您可以解决的方法:

  • 删除共享变量的 unsigned long。事实上,您根本不需要计数器,因为您的后台程序可以在 22 毫秒内实时为变量提供服务 - 应该是非常容易的要求。将 32 位计数器保持在本地并远离 ISR。
  • 确保共享变量的读取是原子的。拆卸!它必须是单个 MOV 指令或类似指令;否则你必须实现信号量。
  • 不要读取volatile复杂表达式中的任何变量。不仅是共享变量,还有 TCNT。就目前而言,您的程序在缓慢的 32 位算术算法的速度和计时器之间存在紧密耦合,这非常糟糕。您将无法准确可靠地读取 TCNT,更糟糕的是,您可以从其他复杂代码中调用此函数。

您的代码应更改为以下内容:

static volatile bool overflow;


void timovf_isr(void)
{
  // Clear the interrupt.
  TFLG2_TOF = 1;

  // TEMP
  overflow = true;

  // ...
}

unsigned long MOTOR_GetCurrentTime(void)
{
  bool of = overflow;   // read this on a line of its own, ensure this is atomic!
  uint16_t tcnt = TCNT; // read this on a line of its own

  overflow = false; // ensure this is atomic too

  if(of)
  {
    _timerOverflowsNoReset++;
  }

  /* calculations here */

  return microseconds;
}

如果您最终没有进行原子读取,则必须实现信号量,阻止计时器中断或在内联汇编器中编写读取代码(我的建议)。

总的来说,我会说您依赖 TOF 的设计有些问题。我认为最好设置一个专用的计时器通道并让它计数一个已知的时间单位(10ms?)。为什么您不能为此使用 8 个计时器通道之一?

于 2018-07-02T11:05:48.037 回答
0

这一切都归结为您阅读计时器的频率以及系统中的最大中断序列将持续多长时间(即计时器代码可以在没有“实质性”进展的情况下停止的最长时间)的问题。

如果您比硬件计时器的循环时间更频繁地测试时间戳,并且这些测试可以保证一个测试的结束与其前身的开始之间的距离不超过一个间隔(在您的情况下为 22 毫秒),那么一切都是好。如果您的代码被搁置了很长时间以至于这些先决条件不成立,则以下解决方案将不起作用 - 然而,问题是来自此类系统的时间信息是否有任何价值。

好消息是您根本不需要中断 - 任何试图弥补系统无法满足两个同样困难的 RT 问题的尝试 - 更新溢出计时器并提供硬件时间都是徒劳的或丑陋的加上不满足基本系统属性。

unsigned long MOTOR_GetCurrentTime(void)
{
  static uint16_t last;
  static uint16_t hi;
  volatile uint16_t now = TCNT;

  if (now < last)
  {
     hi++;
  }
  last = now;
  return now + (hi * 65536UL);
}

顺便说一句:我返回滴答声,而不是微秒。不要混淆顾虑。

PS:需要注意的是,这样的函数不是可重入的,并且在某种意义上是真正的单例。

于 2018-07-02T12:35:48.987 回答