Системный таймер высокого разрешения в STM32

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

Поскольку программное обеспечение будет работать некоторое время, я хочу использовать 64-битный таймер, и для меня имело смысл использовать встроенный таймер SysTick. Я создал SysTickMajor (64-битный счетчик), который увеличивается каждый раз, когда SysTick переполняется, и функцию getSystemTime, которая объединяет этот счетчик и текущее значение SysTick, чтобы получить точное 64-битное время:

#define SYSTICK_RANGE 0x1000000 // 24 bit
volatile long long SysTickMajor = SYSTICK_RANGE;

voif onInit(void) {
  SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);
  SysTick_Config(SYSTICK_RANGE-1);
  /* Super priority for SysTick - is done over absolutely everything. */
  NVIC_InitStructure.NVIC_IRQChannel = SysTick_IRQn;
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  NVIC_Init(&NVIC_InitStructure);
}

void SysTick_Handler(void) {
  SysTickMajor+=SYSTICK_RANGE;
}

long long getSystemTime() {
  long long t1 = SysTickMajor;
  long long time = (long long)SysTick->VAL;
  long long t2 = SysTickMajor;
  // times are different and systick has rolled over while reading
  if (t1!=t2 && time > (SYSTICK_RANGE>>1)) 
    return t2 - time;
  return t1-time;
}

Единственная проблема в том, что это не выглядит на 100% точным, например, если я сделаю следующее:

bool toggle = false;
while (true) {
  toggle = !toggle;
  LED1.write(toggle);
  long long t = getSystemTime()() + 1000000;
  while (getSystemTime() < t);
}

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


ОБНОВЛЕНИЕ: Когда это происходит (я записываю значение времени прерывания, вызванного внешним событием), я выгружаю последние 3 значения в последовательный порт. Повторяю, я получаю такие вещи, как:

>0x278-E2281A 
 0x278-E9999A 
 0x278-0001E5

>0x5BE-F11F51
 0x5BE-F89038
 0x5BE-0000FB

Я добавил тире, чтобы показать, какие биты поступают от SysTick.

Теперь, чтобы избежать сомнений, я использовал приведенный ниже код от starblue. Я также изменил его, чтобы использовать 32-битное значение для SysTickMajor, и я на всякий случай добавил SysTick->VAL.

Похоже, что SysTick_Handler не вытесняет другое прерывание!

Теперь я проверил это, и раньше у меня были неправильные приоритеты вытеснения, но теперь я установил, а NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);затем настроил прерывания - приоритет вытеснения SysTick равен 0, а другое прерывание - 7.

Есть ли что-то еще, что я должен сделать, чтобы заставить работать вытеснение?


ОБНОВЛЕНИЕ 2: я создал крошечный тестовый пример именно для этой проблемы (SysTick не вытесняет другие прерывания) и поместил его здесь:

STM32 Проблемы с приоритетом прерывания (вытеснение)


Так что же не так с кодом выше? Есть ли лучший способ получить хороший таймер с высоким разрешением? Кажется, что-то очевидное, что люди хотели бы сделать, но я изо всех сил пытаюсь найти примеры.

Может быть, вы попробуете ДМА? Увеличьте некоторую переменную при переполнении таймера.
У вас есть пример увеличения переменной с помощью DMA? Просто примечание: действительно должно быть отключено/включено прерывание при каждом упоминании SysTickMajor в getSystemTime, однако даже это не останавливает случайный сбой.
Ваша главная ошибка в том, что вы бежите while (getSystemTime() < t);: эта функция долго тянется, поэтому вы не сможете получить время идеально. Работайте с прерываниями или выполняйте атомарную операцию расчета времени (прерывание SysTick изменит некоторую long longпеременную с помощью DMA или другого механизма, и последняя ваша строка может выглядеть какwhile(SystemTime - SystemTime0 < 1000000)
Это не похоже на то, что он немного сбит - иногда импульс может быть где-то между 0 и длиной, которой он должен быть. Теперь я изменил вопрос после того, как попробовал некоторые другие вещи. Однако, если у вас есть решение, которое включает DMA (я не уверен, как это можно сделать?), Я был бы очень заинтересован.

Ответы (4)

Как вы уже поняли, обработка переполнения счетчика с помощью обработчика прерываний сложна и подвержена гонкам. Даже с повышением приоритета вы получите неправильный ответ, если прерывания отключены, пока вы читаете показания счетчика. Существует гораздо более простой метод, который работает, если вы не читаете часы из контекста прерывания (обратите внимание, что здесь я использую счетчик циклов DWT , который встроен в процессор так быстро и просто):

uint64_t last_cycle_count_64 = 0;

// Call at least every 2^32 cycles (every 59.6 seconds @ 72 MHz).
// Do not call from interrupt context!
uint64_t GetCycleCount64() {
  last_cycle_count_64 += DWT->CYCCNT - (uint32_t)(last_cycle_count_64);
  return last_cycle_count_64;
}

Если вам нужно вызвать его из контекста прерывания (или с вытесняющей ОС):

volatile uint64_t last_cycle_count_64 = 0;

// Call at least every 2^32 cycles (every 59.6 seconds @ 72 MHz).
uint64_t GetCycleCount64() {
  uint32_t primask;
  asm volatile ("mrs %0, PRIMASK" : "=r"(primask));
  asm volatile ("cpsid i");  // Disable interrupts.
  int64_t r = last_cycle_count_64;
  r += DWT->CYCCNT - (uint32_t)(r);
  last_cycle_count_64 = r;
  asm volatile ("msr PRIMASK, %0" : : "r"(primask));  // Restore interrupts.
  return r;
}

Возможно, вам потребуется сначала включить счетчик циклов:

DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;  // Set bit 0.
Хорошо - значит, само чтение счетчика очищает его? Я предполагаю, что потенциально это может быть вызвано на IRQ переполнения таймера SysTick, что гарантирует, что он никогда не достигнет точки переполнения.
Нет, чтение счетчика не очищает его. Этот код просто обновляет больший 64-битный счетчик, используя текущее значение меньшего 32-битного счетчика. В принципе, вы можете сделать то же самое с реестром SysTick, но вам придется выполнять обновление чаще.
Этот общий метод кажется хорошим, если вы создадите для него собственный таймер. Однако счетчик DWT->CYCCNT увеличивается только при подключении зонда отладки. Если вы хотите использовать его с отсоединенным зондом отладки, вам нужно включить его отдельно. Однако это может позволить злоумышленнику получить доступ ко всем регистрам отладки, поэтому полагаться на DWT->CYCCNT нецелесообразно.
Эван заявляет: «Счетчик DWT->CYCCNT увеличивается только при подключении отладочного зонда». По крайней мере, на моей платформе (STM32F407) счетчик CYCCNT увеличивается независимо от того, подключен ли отладочный зонд. Я просматриваю Справочное руководство по архитектуре ARMv7-M (DDI0403E_d) и не вижу упоминаний о необходимости отладочного зонда. Я вижу, что счетчик останавливается, когда процессор останавливается, что обычно желательно: «CYCCNT не увеличивается, когда процессор останавливается в состоянии отладки».
Ах, вы должны включить CYCCNT, установив младший бит DWT_CTRL. Добавил описание этого в ответ.

В вашем коде две проблемы:

  • Вы не знаете, произошло ли прерывание, которое увеличилось, SysTickMajorдо или после того, как вы прочитали значение таймера в time.

  • Операции с 64-битными значениями не являются атомарными в 32-битной системе. Таким образом, значение for SysTickMajorможет быть обновлено прерыванием между чтением двух 32-битных слов.

Я бы сделал это следующим образом:

major0 = SysTickMajor;
minor1 = SysTick->VAL;
major1 = SysTickMajor;

minor2 = SysTick->VAL;
major2 = SysTickMajor;

if ( major0 == major1 )
{
    time = major1 + minor1;
}
else
{
    time = major2 + minor2;
}

Здесь вы предполагаете, что если SysTickMajorизмениться в первом блоке присваиваний, то во втором блоке он не изменится снова. (Это предположение может быть неверным, если этот код прерывается во время этой процедуры и не может выполняться в течение длительного времени.)

См. также этот вопрос на Stackoverflow .

Спасибо - теперь я начал использовать ваш код (см. измененный вопрос), но проблема все еще есть! Я даже поменял 64-битные значения на 32-битные, чтобы убедиться, что это не проблема. Теперь похоже, что прерывание SysTick не вызывается при переполнении SysTick, а возникает проблема с синхронизацией.
Я предпочитаю читать низкий/высокий/низкий и повторять при необходимости. Среди прочего, этот подход будет работать корректно, даже если "логическое распространение переноса" будет медленным. Предположим, например, что когда процессор к моменту считывания данных из SysTick->VALнего уже начал выполнение инструкции на чтение SysTickMajorв major1. Значение minor1может отражать завершенный таймер, даже если major1это не так. При использовании low/high/low такого не произойдет, если только относительная разница во времени между действиями со старшими и младшими словами не превысит время между двумя чтениями младших слов.

Я только что нашел ответ на очень полезном плакате на форуме STM32:

Следующее неверно. SysTick — это «системный обработчик», и поэтому приоритет не устанавливается таким образом:

  NVIC_InitStructure.NVIC_IRQChannel = SysTick_IRQn;
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // Highest priority
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  NVIC_Init(&NVIC_InitStructure);

На самом деле он установлен с:

  NVIC_SetPriority(SysTick_IRQn, 0);

Вместо этого вызов этого кода решает проблему!

Таким образом, моя версия getSystemTime могла иметь проблемы, а метод starblue лучше, чем мой, однако фактическая причина проблем была указана выше.

Просто подумал, что добавлю еще одно возможное улучшение в getSystemTime, которое я тоже пробовал:

do {
 major0 = SysTickMajor;
 minor = SysTick->VAL;
 major1 = SysTickMajor;
while (major0 != major1);
return major0 - minor;

Это эффективно сведет на нет проблему, когда вы можете (каким-то образом) в конечном итоге реализовать SysTick дважды в очень быстрой последовательности.

Предыдущие ответы великолепны. Мне больше всего нравится вот это:

do {
 major0 = SysTickMajor;
 minor = SysTick->VAL;
 major1 = SysTickMajor;
while (major0 != major1);
return major0 - minor;

Вы можете еще немного оптимизировать его, поскольку знаете, что если у вас есть обновление SysTickMajor, вы не получите еще одно обновление «долгое время». Вы должны быть в безопасности, делая это:

major = SysTickMajor;
minor = SysTick->VAL;
if (major != SysTickMajor)
    return SysTickMajor - SysTick->Val;
return major - minor;

Вышеизложенное предполагает, конечно, что SysTickMajor и VAL объявлены как volatile. Несколько лет назад, работая на предыдущей работе, я обнаружил, что, к сожалению, все вышеперечисленное работает не на всех процессорах. Я использовал STM32F407, у которого есть кеш и конвейер. Иногда выборка второго SysTickMajor прибывала до VAL, даже если они выдавались по порядку. Введите барьер памяти (инструкция dmb):

major = SysTickMajor;
minor = SysTick->VAL;
__asm__ volatile("dmb");
if (major != SysTickMajor)
    return SysTickMajor - SysTick->Val;
return major - minor;

В моем случае я использовал 32-битный TIM2 и ISR с пролонгацией TIM2, который увеличивает старшие 32 бита моего 64-битного счетчика циклов, но я думаю, что он должен действовать так же, как вы делаете с SysTick.