У меня есть некоторое программное обеспечение, работающее на 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 Проблемы с приоритетом прерывания (вытеснение)
Так что же не так с кодом выше? Есть ли лучший способ получить хороший таймер с высоким разрешением? Кажется, что-то очевидное, что люди хотели бы сделать, но я изо всех сил пытаюсь найти примеры.
Как вы уже поняли, обработка переполнения счетчика с помощью обработчика прерываний сложна и подвержена гонкам. Даже с повышением приоритета вы получите неправильный ответ, если прерывания отключены, пока вы читаете показания счетчика. Существует гораздо более простой метод, который работает, если вы не читаете часы из контекста прерывания (обратите внимание, что здесь я использую счетчик циклов 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.
В вашем коде две проблемы:
Вы не знаете, произошло ли прерывание, которое увеличилось, 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 .
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.
Эдди_Эм
Гордон Уильямс
Эдди_Эм
while (getSystemTime() < t);
: эта функция долго тянется, поэтому вы не сможете получить время идеально. Работайте с прерываниями или выполняйте атомарную операцию расчета времени (прерывание SysTick изменит некоторуюlong long
переменную с помощью DMA или другого механизма, и последняя ваша строка может выглядеть какwhile(SystemTime - SystemTime0 < 1000000)
Гордон Уильямс