Переполнение при отсчете времени в STM32Cube

У меня есть статья, где я описываю, как замечательно, что в STM32Cube есть отсчет времени из коробки. Рекомендую ее в качестве введения.

STM32Cube считает время в миллисекундах. Время хранится в беззнаковой целочисленной 32-битной переменной:

static __IO uint32_t uwTick;

Это означает, что через 49 суток 17 часов 2 минуты 47 секунд и 295 миллисекунд произойдет переполнение этой переменной.

Хорошо, если устройство не может так долго работать, например, гарантировано сядет батарейка. Но, что если устройство должно работать непрерывно месяцами?


Время используется библиотекой в операциях ввода/вывода для организации таймаутов. Кроме того, библиотека предоставляет функцию задержки HAL_Delay().

Если переполнение обрабатывается неправильно, то мы получим неправильные таймауты и задержки.

Посмотрим, на код, отвечающий за обработку времени:

файл stm32***_it.c:

void SysTick_Handler(void)
{
  HAL_IncTick();
}

файл stm32***_hal.c:

static __IO uint32_t uwTick;
 
__weak void HAL_IncTick(void)
{
  uwTick++;
}
 
__weak uint32_t HAL_GetTick(void)
{
  return uwTick;
}
 
__weak void HAL_Delay(__IO uint32_t Delay)
{
  uint32_t tickstart = 0;
  tickstart = HAL_GetTick();
  while((HAL_GetTick() - tickstart) < Delay)
  {
  }
}

Обработчик прерывания SysTick_Handler() вызывается каждую миллисекунду, и вызывает функцию HAL_IncTick(), которая просто увеличивает uwTick на единицу (инкрементирует). Функция HAL_GetTick() возвращает текущее время в миллисекундах; эта функция обильно используется библиотекой следующим образом:

uint32_t tickstart = HAL_GetTick();
...
if(HAL_GetTick()-tickstart) >= Timeout)
{ ... }

Операция сравнения времени довольно проста. Аналогичная операция используется в функции HAL_Delay().

Видите обработку переполнения? И я не вижу. А она есть.

Рассмотрим функцию HAL_Delay(). Для начала ситуацию без переполнения.

Допустим:

Delay = 100,
tickstart = 1000.

Разница
HAL_GetTick()-tickstart

будет расти от 0 до 100. Цикл будет прерван, когда HAL_GetTick() вернет 1100:
1100 - 1000 = 100
100 < 100 = ложь,

Теперь рассмотрим ситуацию с переполнением.

Допустим:
Delay = 100,
tickstart = 4294967290.

Максимальное число в беззнаковой 32-битной переменной равно 4294967295. То есть до переполнения остается всего 5 инкрементов. При 6-м инкременте HAL_GetTick() вернет 0.

Что тогда вернет операция
HAL_GetTick()-tickstart

то есть, что будет если из 0 вычесть 4294967290?

Обе переменные беззнаковые и целочисленные. И операция вычитания вернет 6. А дальше, когда HAL_GetTick() возвратит 1, операция вычитания вернет 7.

То есть разница между текущим временем и tickstart все время будет расти, как и в нормальной ситуации. Функция задержки отработает правильно.

Важно, что такое поведение стандартизировано и должно реализовываться любым компилятор C и C++. Можете ознакомиться с обсуждением этого вопроса на Stack Overflow.

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

Вывод


Обработка времени в библиотеке STM32Cube происходит корректно, несмотря на то, что переменная uwTick, хранящая время, может переполнится.

Оригинал статьи.
  • -1
  • 07 февраля 2016, 21:29
  • bravikov

Комментарии (45)

RSS свернуть / развернуть
Простите дремучего, а чем STM32Cube от SPL отличается?
0
STM32Cube вытесняет SPL. Для новых МК есть только Cube. Для старых тоже ввели Cube, а SPL перестали поддерживать.

Библиотеки несовместимы, другие названия функций и принципы работы. Заново приходится осваивать работу с МК.

Мне больше всего нравятся колбеки, которые вызываются при ошибках и при завершении операций.
0
Только блин свыкся с SPL, а тут на тебе… Садюги мля! Инноваторы хреновы…
-1
интересно, когда cube они поддерживать перестанут? Когда кто-то из ведущих программеров уволится?
Сколько раз еще изучать МК заново? А как же переносимость кода? Бедолаги...(((
-1
Заново приходится осваивать работу с МК.

Не, ну это Вы перегнули. МК ведь не поменялся, и, например, режимы GPIO у него тоже не поменялись. Поменялось API, но если вы знакомы с МК, то перейти на новое API не такая уж и проблема. Как по мне, основное преимущество Cube это переносимость между камнями и наличие высокоуровневых вещей типа RTOS, USB, TCP/IP которые уже интегрированы в библиотеку. Таймер или ногодрыг не проблема поднять и без библиотек (написав за пол часа свой велосипед), а вот с каким нибудь USB CDC не все так тривиально.
0
e_mc2 — камень и его режимы да, но когда я представляю сколько кода я должен исправить из 220 кб сырцов существующего проекта, меня бросает в дрож.
А еще и изучить как с этим кубиком работать.

Все-бы ничего, поддерживали бы они свои SPL, я б и слова не сказал, а тут такая новость блин…
0
но когда я представляю сколько кода я должен исправить из 220 кб сырцов существующего проекта, меня бросает в дрож.

Здесь нужно хорошо подумать – а есть ли смысл переделывать готовый проект под новые библиотеки? Я (для своих поделок) не вижу смысла, то что они перестали поддерживать SPL меня не особо волнует… А вот если начну новый проект – попробую Cube (я пока с ним только поверхностно игрался).
0
это без-порно… но когда либа поддерживается, это есть гуд… там время от времени баги исправлялись… а теперь походу крышка.
Это и обидно.
0
Баги они вроде как фиксить будут.
Правда официальное подтверждение этому я нашел только в доке по F4, но там вроде говориться о STM32Cube HAL и SPL в общем случае:
Existing Standard Peripheral Libraries will be supported, but not recommended for new designs.
Или есть более актуальная информация по поддержке SPL?
0
А я бы сказал, что освоение камня по большей части освоение его API и представляет. Конечно, за счет того, что камень тот же, какая-то преемственность между API присутствует (ну, например, порядок инициализации и поддерживаемые режимы), что несколько облегчает задачу повторного освоения.
0
Конечно, за счет того, что камень тот же, какая-то преемственность между API присутствует (ну, например, порядок инициализации и поддерживаемые режимы), что несколько облегчает задачу повторного освоения
Да, я это и имел ввиду. Общее понимание принципов работы данного МК + преемственность между API облегчает переход. Говорить о «заново осваивать работу с МК» — перебор, ИМХО.
0
Потрогал Cube. С одной стороны работать приятнее. С другой — памяти жрет мама не горюй.
0
Поправьте меня, если неправ, но разве новые библиотеки не HAL называются?

И второе, хотелось бы немного остудить энтузиазм начинаюших разработчиков. Несмотря на то, что сам являюсь поклонником этих новых библиотек, они бесспорно хороши для быстрого старта, но к сожалению в некоторых случаях они даже неочевидно вредны. Потому как просто не позволяют реализовать некоторые вещи =(
В общем, пользуйтесь, но не увлекайтесь.
0
А использовать CMSIS не пробовали? Я сейчас M7 использую в разработке, и как-то без куба и кала все работает.
0
Да работать то будет, никуда не денется.
Вопрос в лёгкости написания кода. HAL облегчает жизнь в разы, имхо (особенно, когда мало знаком с STM32).
Но… когда доходит до сложных проектов, приходится по старинку вручную дергать регистры.
0
Вообще такой метод вычисления интервала — стар как мир. Хотя возможно для кого-то это действительно станет откровением, что правильнее измерять интервал, а не абсолютное значение. И время от времени интервал снова инициализировать.
struct timer {
  clock_time_t start;
  clock_time_t interval;
};
void
timer_set(struct timer *t, clock_time_t interval)
{
  t->interval = interval;
  t->start = clock_time();
}
void
timer_reset(struct timer *t)
{
  t->start += t->interval;
}
void
timer_restart(struct timer *t)
{
  t->start = clock_time();
}
/*---------------------------------------------------------------------------*/
/**
 * Check if a timer has expired.
 *
 * This function tests if a timer has expired and returns true or
 * false depending on its status.
 *
 * \param t A pointer to the timer
 *
 * \return Non-zero if the timer has expired, zero otherwise.
 *
 */
int
timer_expired(struct timer *t)
{
  return (clock_time_t)(clock_time() - t->start) >= (clock_time_t)t->interval;
}

где clock_time() — функция, возвращающая абсолютное значение.
0
использование:
timer_set(&timer, SECOND);//SECOND - интервал - секунда
...
if(timer_expired(&timer)
{
  timer_reset(&timer);
  if(++second >= 60)
  {
    second = 0;
    if(++minut >= 60)
    {
      minut = 0;
      if(++hour>=24)
      {
        hour = 0;
        day++;
      }
    }
  }
}
...
0
пардон, скобку одну забыл закрыть:
if(timer_expired(&timer))
0
К этому стоит добавить только то, что не следует в таком коде мешать знаковые и беззнаковые переменные. Иначе код внезапно перестанет работать на 25-е сутки (когда счетчик перевалит за MAXINT).
+1
  • avatar
  • Vga
  • 08 февраля 2016, 11:27
если речь идет о замере интервала — то это обстоятельство как раз не имеет значения
0
Я не помню, о чем речь шла в том случае, но с таким я сталкивался — программа не работала, если GetTickCount возвращал значение более MAXINT.
Кроме того, некоторые языки расчеты int32+uint32 ведут в 64-битных числах, что тоже может привести к проблемам.
0
P.S. Это была игрушка, так что скорее всего проблема росла именно из кода, считающего интервалы между обновлениями состояния. Помню только, что оно было связано именно со смешиванием int и uint.
0
если считать интервал, заведомо меньший, чем вся область значений, то ошибки как раз возникать не будет в принципе. Поскольку если возникнет переполнение (улет в область отрицательных), посредством инкремента, то при вычислении интервала произойдет снова переполнение, что в принципе, компенсирует первое. Результат всегда будет верным, независимо от того, сколько бит используется в счетчике. Главное условие — интервал всегда меньше.
0
а вот если абсолютное значение — то да, можно напороться. Например в unix time и им подобных
0
Пожалуйста:
program test;

{$APPTYPE CONSOLE}

var
  Cnt: Cardinal;
  Old: Integer;
  i: Integer;

begin
  ReadLn(Cnt);
  while Cnt <> 0 do
  begin
    Old := Cnt + 100;
    for i := 0 to 15 do
    begin
      Inc(Cnt, 10);
      Write(i);
      Write(#9);
      WriteLn(Old - Cnt);
    end;
    ReadLn(Cnt);
  end;
end.

Покуда вводишь Cnt меньше MaxInt — 100 все нормально, на i=9 дельта равна нулю. Стоит превысить порог — и дельта во всех итерациях примерно -4G. Если сделать Old беззнаковым — то все работает.
Впрочем, аналогичный код в С работает, там иначе выполняется приведение типов в смешанных расчетах.
0
не ну сам счетчик и интервал хранить в разных — это юродство. Какой смысл? Я имел ввиду что один могут быть любого типа, но одинакового.
0
Вот я о том и предупреждал, что не стоит смешивать разные типы переменных.
0
а вообще в делфях приведение типов — это дополнительный оверхед, который работает не так, как на сях
0
Я бы обратил внимание на другой момент.
В коде
if(HAL_GetTick()-tickstart) >= Timeout)

есть одна фича – этот код не будет работать по-разному в зависимости от типа переменной.

Вот такой код (по мотивам счетчика с переполнением)
uint16_t time = -1;
	uint16_t last;

	last = time;
	time++;
	if(time - last >= 1) {
		printf("Good!");
	} else {
		printf("Not good!");
	}

Выведет «Not good!», а если мы заменим тип на uint32_t, то получим «Good!». Правда весело? :)
Правильно сделано в примере у Mihail , где стоит явное приведение типов.
+1
очень хорошую проблему вы подняли, поскольку у многих нет понимания, в какой разрядности хранится число, вычисленное из «time — last», но еще не присвоенное конкретной переменной. Часто так бывает именно в условиях, где присвоение не требуется. Так вот, все зависит от разрядности процессора. Если это arm32 то стандартно — int32_t. Если TMS320 какой-нить то int16_t у AVR — char. Если участвующие переменные больше разрядности по умолчанию — то соответственно разрядность и тип присваиваются максимального, но не наоборот. т.е. если к char числа прибавить другой char — результат вполне может быть больше 255, поскольку результирующее int32_t например. Это уже потом, при присвоении в char старшие биты отсекутся. Бывает наоборот: uint16_t = uint8_t<<8 | uint8_t не получает нужного результата в семействах AVR, поскольку для AVR uint8_t << 8 — это 0.
0
Про TMS320 точно уверены?
0
лично я не прогал его, но мой коллега понавтыкался с ним изрядно
0
тогда первая строка из его feature list намекает:
High-Performance 32-Bit CPU (TMS320C28x)
0
а, там другая проблема была — напрочь отсутствует понятие байт. Только 16 и 32 бита у него
0
А байт оно вроде как понятие растяжимое=))) и проблемой бы я это не назвал, но это так, лирика=)
0
Нельзя говорить о «хранит в … arm32 то стандартно — int32_t», это не совсем так.

Там при вычитании происходит стандартное арифметическое преобразование типа, первым делом происходит изменение ранга каждого операнда. В данном случае
If an int can represent all values of the original type (as restricted by the width, for a bit-field), the value is converted to an int; otherwise, it is converted to an unsigned int. These are called the integer promotions.
В данном случае оба операнда могут быть представлены как int, поэтому оба и будут преобразованы в int. На этом арифметическое преобразование заканчивается (т. к. оба операнда имеют одинаковый тип). В результате вычитания мы получаем int равный -65535.

Далее происходить аналогичное преобразования для операции сравнения. В данном случае сравниваются два int (-65535 и 1) и результат «Not good!». Но если бы я написал if(time — last >= 1u) при арифметическом преобразовании оба типа сконвертились бы в unsigned, -65535 превратилось бы в 0xffff0001 и в результате мы получим «Good!».

Как-то так, все просто. :)
+1
ну в общем, тоже самое только другими словами. Я это и имел ввиду, что uint16_t в 32-битной системе может представить как int32_t. От этого и все проблемы и непонимания. А вот в AVR вами приведенный код будет работать корректно. Вот в чем загвоздка
0
Просто Вы уж слишком упростили даже для типов, которые просто integer promotions. А ведь есть еще куча исключений, всякие там float, double и long long. Фактически тип операндов и результата выводится по достаточно сложным правилам неявного преобразования типов в С. Более того, хотя большинство операций используют “Usual arithmetic conversions” — там есть и исключения (хотя не готов сходу назвать для каких операций в стандарте есть исключения, но помню, что они есть :)
0
я говорил не столько о типах, сколько о разных платформах. В мире микроконтроллеров это особенно актуально. Что именно от платформы сильно зависит, что же хранится в выражении a-b и т.п.
0
есть одна фича – этот код не будет работать по-разному в зависимости от типа переменной.
Упс, вкралось лишнее «не»
… этот код будет работать по-разному…
0
По моему, здесь проблемы начинаются с первой строчки. Для разных типов переменная time инициализируется по разному.
0
Да не, с первой строкой в данном случае все ОК, просто лень было писать UINT16_MAX, UINT32_MAX
0
Да, действительно. Жесть какая. Всегда думал, что тип результата операции — это тип одного из операндов, а тут вон какая штука.

Код:
uint8_t a = 1, b = 1;
printf("%zu, %zu, %zu \n", sizeof(a), sizeof(b), sizeof(a + b));
выводит: 1, 1, 4.
0
Определение типа, который возвращает выражение достаточно сложная штука. Для начала происходит преобразование аргументов по ряду правил. Потом, у многих операций есть своя специфика. Например (2 — 1.0) вернет double, а (2 == 1.0) вернет int
0
Начинающие, типа меня, просто записывают в счетчик максимальное значение — 10 и вызывают функцию с параметром 20. И тут же понятно поведение и тип и переполнение и корректность использования.
0
  • avatar
  • x893
  • 09 февраля 2016, 21:19
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.