使用HCS12微控制器防止读取中断

时间:2018-07-02 09:13:05

标签: c embedded microcontroller c89 68hc12

摘要

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

我正在用C89编写该微型计算机的固件,并通过Freescale HC12 compiler运行它,我想知道是否有人在如何防止这种特定微控制器上出现任何建议的情况下提出建议。

详细信息

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

为此,我需要配置一个MCU定时器,以便可以跟踪脉冲之间经过的时间。但是,定时器的时钟速率为3 MHz(预分频后),定时器计数器寄存器仅为16位,因此计数器每〜22ms溢出一次。为了补偿,我设置了一个在计时器计数器溢出时触发的中断处理程序,这使“ 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中,我临时编写了一些调试代码,这些代码可以单向驱动电动机,然后以固定的时间间隔“捕获”各种数据:

// 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

与其他读对之间的差异(〜23/24)相差不远40,所以我的猜测是,涉及到一个不合一读的情况会持续不断_timerOverflowsNoReset中的。就像在忙循环中一样,它将对MOTOR_GetCurrentTime()进行一次调用,错误地将_timerOverflowsNoReset视为比实际多一个,导致循环提前结束,然后在下一次读取时再次看到正确的值。

我的应用程序还有其他问题,我无法确定,并且希望如果我解决此问题,也可以解决这些其他问题,只要它们有相似的原因。

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

7 个答案:

答案 0 :(得分: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()的情况最糟,内部循环只会运行两次。在大多数情况下,ovfl1ovfl2在第一次运行while()循环后将相等。

答案 1 :(得分: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在下面的评论中指出的那样,我所做的涉及countAcountB的检查也对我有用,并且如果_timerOverflowsNoReset可能会无限期地重复循环读取速度足够快。当我提出一些解决方案时,我将更新此答案。

答案 2 :(得分: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循环将不再重复一次或两次。

答案 3 :(得分: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发生了变化,则有必要检查tcnt中的值是否已经大于0(但小于ex。0x4000),或者tcnt是否恰好在溢出之前。

答案 4 :(得分:1)

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

如果人们知道某个值将单调递增,并且在调用“更新计时器”功能之间两次调用之间的计数永远不会超过65280,那么可以使用类似以下内容的方法:

// 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位 通过使用上限值和下限值而忽略中间值。 如果使用较少的重叠,则所有三个值都需要参与 最终计算。

答案 5 :(得分:0)

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

另外请记住,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的设计有些疑问。我认为最好设置一个专用的计时器通道,并让它计算一个已知的时间单位(10毫秒?)。有什么原因不能使用8个计时器通道之一?

答案 6 :(得分:0)

这全都归结为您读定时器的频率以及系统中最大中断序列将持续多长时间的问题(即,在不进行“实质性”进展的情况下可以停止定时器代码的最长时间)。 / p>

测试时间戳的时间要比硬件计时器的循环时间多,并且这些测试可以保证一项测试的 end start没什么不同,一切都很好。如果您的代码被搁置了很长时间而这些先决条件不成立,则以下解决方案将不起作用-但是,问题是来自这种系统的时间信息是否具有任何价值。

好处是,您根本不需要中断-尝试补偿系统无法满足两个同样困难的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:需要注意的是,这样的函数不是可重入的,从某种意义上说是真正的单例。