使用 HCS12 微控制器防止读取撕裂

Preventing torn reads with an HCS12 microcontroller

总结

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

我正在用 C89 和 运行 通过 Freescale HC12 compiler 为这个微控制器编写固件,我想知道是否有人对如何在这个特定的微控制器上防止它们有任何建议假设是这样。

详情

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

为此,我需要配置一个 MCU 计时器,以便我可以跟踪脉冲之间经过的时间。但是,定时器的时钟速率为 3 MHz(预分频后),定时器计数器寄存器仅为 16 位,因此计数器每约 22 毫秒溢出一次。为了补偿,我设置了一个在定时器计数器溢出时触发的中断处理程序,这将 "overflow" 变量递增 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中临时写了一些调试代码,驱动电机单向运行,然后定时取"snapshots"各种数据:

// 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。正如我上面提到的,每 22ms 就会发生一次溢出——具体来说,一个单位的 _timerOverflowsNoReset 的差异将导致计算出的微秒值产生 65535 / 3 的差异。如果我们考虑到这一点:

40 的差异与我在其他两对读数 (~23/24) 之间看到的差异相差不远,所以我的猜测是有某种撕裂正在发生,涉及到 off-by-一读 _timerOverflowsNoReset。就像在 while 忙循环中一样,它将执行一次对 MOTOR_GetCurrentTime() 的调用,错误地将 _timerOverflowsNoReset 视为比实际值大 1,导致循环提前结束,然后在下一次读取时它再次看到正确的值。

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

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

您可以读取此值两次:

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() 慢得多,在最坏的情况下,内部循环 运行 仅两次。在大多数情况下,ovfl1ovfl2 将在 while() 循环的第一个 运行 之后相等。

问题是对 _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 个定时器通道之一?

原子读取不是这里的主要问题。
就是overflow-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 发生变化,则有必要检查 tcnt 中的值是否已经大于 0(但低于 ex. 0x4000)或者 tcnt 是否正好在溢出。

这一切都归结为您多久读取一次定时器以及系统中的最大中断序列将持续多长时间的问题(即定时器代码可以停止的最长时间 "substantial"进度)。

如果您测试时间戳的频率高于硬件计时器的循环时间,并且这些测试可以保证一项测试的 endstart 它的前身比一个间隔(在你的例子中是 22ms),一切都很好。如果您的代码被搁置太久以至于这些先决条件不成立,则以下解决方案将不起作用 - 然而问题是来自此类系统的时间信息是否有任何价值。

好处是您根本不需要中断 - 任何试图弥补系统无法满足两个同样困难的 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);
}

顺便说一句:我 return 滴答声,而不是微秒。不要混淆问题。

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

根据@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 在下面的评论中指出的那样,我所做的涉及 countAcountB 的检查也顺便对我有用,如果 [=15] 可能会导致循环无限期重复=] 读取速度足够快。当我想出解决这个问题的办法时,我会更新这个答案。

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

#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 循环将不再重复一次或两次。

一种有用的技术是维护两个或三个值,这些值共同持有较大值的重叠部分。

如果知道一个值将单调递增,并且在调用 "update timer" 函数之间永远不会超过 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 读取之间更新 timerLow 或 timerMed timerHi 和它读取其他值的时间,但如果发生这种情况,它将 注意 timerHi 的下半部分需要递增以匹配上半部分 稍后更新的部分值。

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