" title="Написать письмо">Написать письмо

Статистика

Пользователи : 1
Статьи : 1936
Просмотры материалов : 7052115
 
Программное реальное время в Arduino (03.11.2022). Печать E-mail
2022 - Ноябрь
03.11.2022 13:51
Save & Share
Существует библиотека от AlexGyver GyverTimers, позволяющая использовать встроенные таймеры Arduino. Достаточно задать период таймера (например, таймера 1 - как наиболее точного) и вставить код из loop() в функцию Timer1() - реальное время будет иметь минимальную погрешность 1мкс. Это означает погрешность 0.0001% в случае использования периода 1с.

Но в моем проекте эту библиотеку нельзя было использовать. Любое добавление значительного функционала в существующий проект способно переполнить память Arduino: настолько огромным проект получился. Для подключения аппаратного модуля реального времени типа DS3231 - на Arduino Nano нет лишних пинов: настолько огромным проект получился (и 27руб жалко). Поэтому было принято решение написать свою маленькую функцию реального времени.

Началось все с простого осознания, что кто-то облажался. Сделав в проекте в loop() миллисекундные задержки, была опущена микросекундная погрешность. А практика показала, что она составила средние +460мкс с каждой секунды. Несложно рассчитать, что такое "реальное" время будет идти вперед на 4.03ч/год. А проект должен быть рассчитан на срок работы 10-30 лет. И, с учетом увеличения частоты до 10Гц, - это было 40.3ч/год.

Продолжилось все с простого осознания, что everybody lies (привет доктору Хаусу). В интернете пестрят мануалы по Arduino, в которых написано: используй delay для миллисекундных задержек и delayMicroseconds для микросекундных. На очень малом количестве ресурсов написано, что сами функции жрут время во время своего выполнения - то есть, не оптимизированы (например, для delayMicroseconds заявлена потеря 4мкс, дополнительно к введенному числу). Но нигде не написано ключевое слово: "до". И еще "зависит от микропроцессора" и "в разное время по-разному".

Одни и те же функции в Arduino выполняются в разное время за разное количество микросекунд. Это подтверждается разным количеством микросекунд при выполнении одного и того же большого кода. На примере loop(), получаемые значения плавали от 93260мкс до 93660мкс. Значит, единственный шанс сделать часы реального времени - анализировать время выполнения loop() в самом loop(), а не в setup(): вычислить время выполнения 93мс с копейками - и дополнить его необходимым количеством микросекунд, при помощи часов реального времени, - для достижения периода выполнения loop() 100мс.

Сначала нужно определить время выполнения функции micros, чтобы убедиться в реальном ее поведении. Так как функция имеет дискретность 4мкс (значения 0, 4, 8, 12 и т.д. - до 16380), нужно тупо выполнить micros много раз по 100 раз и проанализировать полученные задержки. Ранее проведенный опыт с анализом одного micros (всегда 0мкс) и двух micros с присвоением результата в unsigned long (0-4мкс) указывал на малое время выполнения самого micros и большее время самого присвоения результата в переменную. Итоговые результаты: micros с присвоением - 2мкс (с округлением в большую сторону, т.к. погрешность нужно считать по-максимуму).

Раз указанные времена настолько малы - нужно отказаться от нестабильных функций delay (+8мкс) и delayMicroseconds (± разное количество мкс). Минус разное количество мкс при входном параметре <16мкс, плюс разное количество мкс при входном параметре >=100мкс (промежуток (16;99)мкс не анализировался). Примеры входных параметров delayMicroseconds: 13мкс - выполнится за 8мкс, 10мкс - за 6мкс, 16мкс - за 16мкс, 10000мкс - за 10060мкс.

Алгоритм подгонки выполнения цикла loop к определенному периоду, в большую сторону:
- в начале loop: unsigned long ulMCS_Begin = micros();
- в конце loop: unsigned long ulMCS_End = micros(). Далее - удлинение исходного кода loop анализатором времени и реализацией необходимой задержки в самом конце. Время выполнения этого кода определяется так же экспериментально - и оно несущественно (меньше 1мс). Если же произойдет выход за пределы необходимого периода - дальше проверка есть;
- расчет необходимого времени после выполнения всего loop: unsigned long ulMCS_Needed = ulMCS_Begin + g_fPeriod_Loop_MCS;
- расчет необходимой задержки с избавлением от unsigned long: float fMCS_Delay = g_fPeriod_Loop_MCS - (ulMCS_End - ulMCS_Begin - 4). Время выполнения двух micros с присвоением отдельно не учитывается, т.к. они содержатся в самом loop();
- if (fMCS_Delay < 0) vError("Цикл улетел за 100мс", String(g_fPeriod_Loop_MCS - fMCS_Delay, 0));
- крутиться максимально быстро до достижения необходимых 100мс: while (ulMCS_Needed > ulMCS_End) ulMCS_End = micros(). Здесь нужно учитывать, что каждый while занимает 4мкс - значит, это время должно быть отнято заранее: последним выполнится не тело цикла, а сравнение в цикле.

Таким образом, имеются часы реального времени с погрешностью 0.0004% - что составляет 4мкс при периоде 1с (126.144с/год) или 40мкс при периоде 100мс (21.024мин/год).

Однако в данном примере есть фатальная ошибка - и ее нужно учесть. Функция micros переполняется раз в 71.58мин - есть вероятность происхождения этого между ulMCS_Begin и ulMCS_End. В случае ulMCS_Begin > ulMCS_End: требуется вычесть из 4294967296 ulMCS_Begin (чтобы сохранить тип данных unsigned long) - и изменить его знак в расчете задержки: float fMCS_Delay = g_fPeriod_Loop_MCS - (ulMCS_End + ulMCS_Begin - 4).

И, для красивости, нужно написать проверку, что fMCS_Delay => 1мс. Это даст гарантию, что алгоритм не приближается к тому пределу, когда выполнение алгоритма подгонки к периоду может привести к выходу за пределы этого периода. То есть, else if (fMCS_Delay < 1) vError("Цикл слишком близко к 100мс", String(g_fPeriod_Loop_MCS - fMCS_Delay, 0)).

(добавлено 06.11.2022) Требуется дополнительная отладка, т.к. имеются 2 проблемы при долгосрочном использовании:
- во время переполнения micros() почему-то ulMCS_Needed всегда равно 0. В результате цикл loop() выполняется без задержки вообще 93мс;
- имеются единичные выходы за пределы 4мкс с амплитудой до 108мкс. При этом: как в большую, так и в меньшую сторону - выходы за пределы, при долгосрочном использовании, должны нивелировать друг друга. Ощущение, как будто частота процессора непостоянна, и ее колбасит.

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

Текущие результаты:
- попытка дебага алгоритма в огромном проекте привела к тому, что платформа Arduino Nano послала на фиг (Serial потребляет много ресурсов), - пришлось дебажить в пустом проекте;
- дебаг в реальном времени подтвердил, что выходы за пределы 4мкс существуют, независимо от проекта, и практически нивелируют друг друга (на периоде 1000мс и на периоде 100мс). Интерпретация результатов: 0мкс - сколько раз погрешность была меньше 4мкс, остальные мкс - в какую сторону выбрасывало цикл чаще и насколько (вычитание при задержке меньше нормы, прибавление при задержке выше нормы), >116мкс - переполнение micros и отрабатывание цикла вообще без задержки (или она ничтожно мала). Задержка 116мкс была 1 раз за несколько суток дебага;
- попытка работы с unsigned long и COM-портов приводит к провалу. String(unsigned long) - зависание Arduino. Использование ultoa() порождает странные результаты: счетчик micros сбрасывается при каждом новом выполнении loop(). Но если бы это было так - не было бы единичного бага, когда доводчик loop не доводит его и оставляет на времени 93мс;
- а потом обнаружился баг-2: баг выше возникает на периоде 100мс и не возникает на периоде 1000мс. А мне 100мс и нужно - то есть, при его использовании переполнения micros не произойдет никогда.

То есть, для большого проекта код уже стабилен и лучше по точности, чем использование delay. Осталось причесать и расписать граничные значения, выполнив цикл 100мс до конца.

(добавлено 16.11.2022) Все еще сложнее:
- не переполнение было зарегистрировано, а переход старшего бита unsigned long из 0 в 1;
- ровно после этого момента выполнение while (ulMCS_Needed > ulMCS_End) ulMCS_End = micros() начинает выполняться резко медленнее: 172мкс и выше;
- следя за задержками цикла в первой половине работы (до 2147483647), видится четкое увеличение задержки с увеличением числа регистров в unsigned long.

Получается, сброс счетчика таймера - единственное решение для устранения почти всех задержек. И тогда с малым количеством задействованных регистров unsigned long (или вообще отказом от него) алгоритм будет выдавать погрешность 0-4мкс, при этом подавляющее число раз <4мкс.

(добавлено 18.11.2022) Все еще сложнее:
- timer0 в Arduino - забагованная неточная какашка, завязанная на системных процессах внутри самой платформы. К этому таймеру, как назло, привязаны millis() и micros(). Попытка изменить его частоту завершилась неудачей (всегда 250кГц - отсюда и дискретность micros 4мкс). Попытка сброса в 0 завершилась неудачей (еще повезло, что платформа осталась жива: возможна несовместимая с его жизнью последовательность команд);
- значит, нужно переступить через себя: использовать другой таймер Arduino, частично отказавшись от уменьшенного энергопотребления платформы. Таким таймером является timer1, используемый для сервоприводов (которых у меня нет) и имеющий частоту счета равную частоте процессора. Но на эту частоту нужно еще выйти - и избавить его от предделителя (например, предделитель 64 заставляет timer0 работать на частоте 250кГц, а не 16МГц);
- но меня занесло сначала в timer2. Отключение предделителя - успешно. Попытка обнулить timer2 - успешно. Многочасовая работа - успешно. Максимальный скачок при использовании delay(1000) в цикле - всегда в плюс, 452 тика - 28.25мкс (при этом непонятно, кто врет: таймер или delay). Многовато - но уже неплохо, и точно лучше значений при использовании timer0;
- с timer2 есть проблема: иногда COM-порт зависает (требуется перезагрузка платформы и среды разработки). Причем сама плата продолжает работу (D1 моргает с частотой ~1с).

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

(добавлено 19.11.2022) Timer1 отказывается отключать свой предделитель 256. В итоге, самый точный 16-битный таймер работает с размером тика 15.26мкс - полная непригодность для использования (ведь тики же еще и колбасит, судя по таймерам 0 и 2). Размер тика же таймера 2 составляет 62.5нс.

Всё, перерыв.
Обновлено ( 19.11.2022 14:40 )
 
 

Последние новости


©2008-2024. All Rights Reserved. Разработчик - " title="Сергей Белов">Сергей Белов. Материалы сайта предоставляются по принципу "как есть". Автор не несет никакой ответственности и не гарантирует отсутствие неправильных сведений и ошибок. Вся ответственность за использование материалов лежит полностью на читателях. Размещение материалов данного сайта на иных сайтах запрещено без указания активной ссылки на данный сайт-первоисточник (ГК РФ: ст.1259 п.1 + ст.1274 п.1-3).

Много статей не имеет срока устаревания. Есть смысл смотреть и 2011, и даже 2008 год. Политика сайта: написать статью, а потом обновлять ее много лет.
Открыта карта ВТБ для донатов на дорогостоящие эксперименты: 5368 2902 0040 0838.

Рекламодателям! Перестаньте спамить мне на почту с предложениями о размещении рекламы на этом сайте. Я никогда спамером/рекламщиком не был и не буду!
Top.Mail.Ru