Предотвращение разрывов чтения с помощью микроконтроллера HCS12 - PullRequest
0 голосов
/ 02 июля 2018

Резюме

Я пытаюсь написать встроенное приложение для MC9S12VR микроконтроллера . Это 16-битный микроконтроллер, но некоторые значения, с которыми я имею дело, имеют ширину 32 бита, и во время отладки я зафиксировал некоторые аномальные значения, которые, по-видимому, связаны с разорванным чтением.

Я пишу прошивку для этого микро-устройства в C89 и запускаю его через компилятор Freescale HC12 , и мне интересно, есть ли у кого-нибудь какие-либо предложения о том, как предотвратить их на этом конкретном микроконтроллере, предполагая, что это тот случай.

Подробнее

Часть моего приложения включает в себя управление двигателем и оценку его положения и скорости на основе импульсов, генерируемых датчиком (импульс генерируется при каждом полном обороте двигателя).

Чтобы это работало, мне нужно настроить один из таймеров MCU, чтобы я мог отслеживать время, прошедшее между импульсами. Однако таймер имеет тактовую частоту 3 МГц (после предварительного масштабирования), а регистр счетчика таймера является только 16-битным, поэтому счетчик переполняется каждые ~ 22 мс. Чтобы компенсировать это, я установил обработчик прерываний, который запускает переполнение счетчика таймера, и это увеличивает переменную «переполнение» на 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 должен завершиться, а второе чтение - через несколько микросекунд - это ожидаемо. Тем не менее, иногда я нахожу, что первое чтение значительно выше, чем точка, в которой цикл должен завершиться, а затем второе чтение на меньше первого значения (а также значения завершения).

На скриншоте ниже приведен пример этого. Потребовалось около 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. Как и во время цикла занятости, он будет выполнять один вызов MOTOR_GetCurrentTime(), который ошибочно воспринимает _timerOverflowsNoReset как единицу больше, чем есть на самом деле, вызывая преждевременное завершение цикла, а затем при следующем чтении после этого он видит правильное значение еще раз.

У меня есть другие проблемы с моим приложением, из-за которых у меня возникают проблемы с фиксацией, и я надеюсь, что, если я решу это, это может решить и другие проблемы, если они имеют аналогичную причину.

Редактировать: Среди прочих изменений я изменил _timerOverflowsNoReset и некоторые другие глобальные переменные с 32-битного без знака на 16-битный без знака в реализации, которую я сейчас имею.

Ответы [ 7 ]

0 голосов
/ 02 июля 2018

Рассчитайте количество тиков, затем проверьте, изменился ли при этом переполнение, и если да, повторите;

#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 будет повторяться один или два раза, не более.

0 голосов
/ 03 июля 2018

Один метод, который может быть полезен, - это сохранение двух или трех значений, которые в совокупности удерживают перекрывающихся частей большего значения.

Если известно, что значение будет монотонно увеличиваться, и между вызовами функции «обновить таймер» никогда не будет превышать 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 читает timerHi перед чтением timerLow. Возможно, что updateTimer может обновлять timerLow или timerMed между временем чтения readTimer timerHi и время считывания этих других значений, но если это произойдет, обратите внимание, что нижняя часть timerHi должна быть увеличена, чтобы соответствовать верхней часть значения, которая была обновлена ​​позже.

Этот подход может каскадироваться до произвольной длины и не должен использовать целые 8 бит перекрытия Использование 8 битов перекрытия, однако, позволяет формировать 32-битные значение, используя верхнее и нижнее значения, просто игнорируя среднее. Если бы использовалось меньшее перекрытие, все три значения должны были бы принять участие в окончательный расчет.

0 голосов
/ 02 июля 2018

Основываясь на ответах @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 раз подряд и не заметил разрывов, поэтому я считаю, что это работает. Я продолжу тестировать и обновлять этот ответ, если я ошибаюсь и проблема не устранена.

Редактировать: Как отмечает Врумфондель в комментариях ниже, проверка, которую я выполняю с участием countA и countB, также случайно работает для меня и может потенциально заставить цикл повторяться бесконечно, если _timerOverflowsNoReset читается достаточно быстро. Я обновлю этот ответ, когда придумаю что-нибудь для решения этой проблемы.

0 голосов
/ 02 июля 2018

Все сводится к вопросу о том, как часто вы читаете таймер и как долго будет максимальная последовательность прерываний в вашей системе (т. Е. Максимальное время, в течение которого код таймера может быть остановлен без «существенного» прогресса).

Если вы проверяете метки времени чаще, чем время цикла вашего аппаратного таймера, И эти тесты гарантируют, что конец одного теста не будет дальше от начала его предшественника, чем один интервал (в вашем случае 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: предостережение в том, что такая функция не реентерабельна и в некотором смысле является истинным синглтоном.

0 голосов
/ 02 июля 2018

Атомные чтения здесь не главная проблема.
Проблема в том, что переполнение 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;

Если totalOverflowCounter изменился во время чтения TCNT, то необходимо проверить, является ли значение в tcnt уже больше 0 (но ниже, например, 0x4000) или если tcnt находится непосредственно перед переполнением.

0 голосов
/ 02 июля 2018

Проблема в том, что записи в _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 каналов таймера для этого?

0 голосов
/ 02 июля 2018

Вы можете прочитать это значение ДВАЖДЫ:

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 ().

...