Это мой развернутый ответ на тему организации программных таймеров который я обещал в комментариях (ссылка на комментарии будет ниже).
На самом деле это не совсем то что я изначально имел ввиду - хотел рассказать, то есть это на самом деле только часть ответа. Картинки я здесь не стал рисовать, так как они уже у меня нарисованы в одной из предыдущих статей, ссылка тоже будет, но в самом конце.
Если рассмотреть любое количество периодических событий которые должны генерироваться кодом (с использованием аппаратной функции таймера или нет не важно)...
возьмем для примера количество N=3 : одна 100 раз в секунду, вторая 7 раз, третья 38 как было предложено в комментарии: https://habr.com/ru/companies/yadro/articles/1001574/comments/#comment_29634340
Собственно код из этого комментария очень компактно демонстрирует суть задачи которую мы собираемся решить и записать это решение для выполнения процессором уже без нашего участия.
while (1) { poll_update(); if (poll_hz(100)) task1(); if (poll_hz(7)) task2(); if (poll_hz(38)) task3(); }
фактически наш код должен обеспечить расписание выполнения задач нужное количество раз в секунду для каждой задачи.
Для меня решением с более прямолинейной (что ли) логикой выглядело вот так:
TimeTicks timeVal1, timeVal2, timeVal3; timeVal1 = timeVal2 = timeVal3 = getNowTime(); while (1) { TimeTicks nowTime = getNowTime(); if(nowTime - timeVal1 > Ktick/100) {timeVal1 = nowTime; task1(); } if(nowTime - timeVal2 > Ktick/7) {timeVal2 = nowTime; task2(); } if(nowTime - timeVal3 > Ktick/38) {timeVal3 = nowTime; task3(); } }
где Ktick - это коэффициент перевода секунд в тики процессора/таймера.
Проблема с этим кодом в том, что и в том и в другом случае есть вероятность, в первом случае пропустить очередной вызов самого корот��ого периода, а во втором случае получить реальный период то же самого короткого события гораздо больше чем задано, то есть в среднем, и в том и в другом случае период будет больше заданного (частота событий меньше), но самое главное, что период будет не стабильным. Надо учитывать что задачи выполняются не мгновенно и если их длительность, в худшем случае суммарная длительность превышает самый короткий период это приведет к тому, что и в первом случае и во втором случае самый короткий период неопределенно растянется в среднем. Вопрос в том какого типа задача выполняется с самым коротким периодом. Если это опрос кнопок (пинов), скорее всего ничего страшного не случится, хотя опрос пинов будет достаточно странной задачей для фоновой программы, опрос пинов и установка соответствующих флагов можно оценить примерно в 10 инструкций ассемблера, эта логика вполне может поместиться в функции прерывания.
Но существует и другой тип задач когда период определяет, например, считывание значений АЦП, которые используются в каком-то алгоритме ПИД регулировки или в каком-то алгоритме Цифровой Обработки Сигнала (ЦОС). В таких случаях описанные ошибки установки периода (опроса АЦП, например) становятся неприемлемыми, так как математика регулировки или ЦОС просто не работает при наличии таких ошибок. Дифференциальное исчисление не работает если dt мы будем случайным образом менять, даже иногда - не очень часто. В этом случае такие ошибки не допустимы. Но программы пока еще пишут люди, а людям свойственно ошибаться, поэтому первая задача состоит не в том чтобы написать идеальный алгоритм-идеальную программу в которой не будет ошибок. Основная задача в этом случае состоит в том, чтобы написать программу которая будет вам сообщать что она не справляется с возложенной на нее задачей, если это все же случилось. А это неизбежно случается на первых версиях программы и если вы будете знать, что программа не справляется, а еще и сможете определить при каких условиях программа не справляется поддерживать установленный период определенных событий, скорее всего вы сможете исправить эту программу чтобы, либо такие условия не накладывались так фатально, либо что-то предпринять чтобы разгрузить логику при таком наложении — это две основные стратегии, хотя существуют и другие, более сложные. Общая идея проста, если вы имеете достаточно информации о том, что происходит в процессе выполнения программы вы можете эту информацию проанализировать, сформулировать эту новую для вас ситуацию как новую задачу и решить ее, в конце концов, либо поменять исходные условия-настройки чтобы эта ситуация-задача в принципе не могла складываться в процессе работы. По сути всегда можно подобрать расписание событий вручную (!) таким образом, чтобы ваша задача получила совершенно детерминированное решение которое досконально просчитано и которое, по этому, гарантированно работает.
Например для предложенных частот событий можно задаться вопросом а нужна ли нам частота 100 событий в секунду и что будет если мы снизим ее до, например, 80 событий в секунду. А может мы сможем четко разделить чтобы 2-я и 3-я задачи никогда не попадали в один самый короткий период? Давайте посмотрим.
Если у нас задана частота событий второй задачи как 38 в секунду, скорее всего будет только лучше если мы будем выполнять эту задачу совсем чуть чуть чаще, 40 раз в секунду. Обратите внимание что не очень удобно оперировать частотами, а не периодами между событиями, так как приходится напоминать себе что увеличение частоты уменьшает время между событиями, а мы должны еще помнить о максимальном (или хотя бы примерном среднем времени выполнения функции задачи по событию, которое не должно превышать в нашем случае время самого короткого периода в нашем наборе событий).
Но смотрите, стало намного проще-удобнее: число 40 кратно 80 и мы точно можем сказать что событие 2 будет происходить каждый второй самый короткий период. И мы наверно можем даже сделать так, чтобы событие 2 приходилось точно на середину самого короткого периода, тогда у нас будет точно известно что на одну задачу у нас приходится половина самого короткого периода. Но у нас есть еще и третье событие и вроде бы это ломает нам полученную схему. Но давайте посмотрим. Следуя той же логике давайте тоже маленько повысим частоту событий 3 с 7-ми в секунду до 8-ми в секунду. Мы опять получили число кратное 80-ти, и это событие будет происходить... , то есть мы можем назначить это событие на какой-то каждый десятый самый короткий период (далее период-80 вместо самый короткий период, для краткости). Не трудно догадаться что можно каждый десятый период-80 для события события 3 (период-80 когда должно произойти событие с периодом-8, это каждый десятый период-80) может приходиться на каждый второй ЧЕТНЫЙ период-80, а каждый второй период-8 для события 2 может приходиться на каждый второй НЕЧЕТНЫЙ период-80. Таким образом мы вручную распределили события на оси времени так, что у нас на обработку каждого события нашлось половина самого короткого периода. Мы распределили события во времени - составили четкое расписание событий и определили сколько времени у нас есть на обработку каждого события. А раз это теперь известно, мы знаем теперь что нам нужно контролировать-обеспечить, это значит задача теперь четко определена, ее надо просто решить. Как говорили мне когда-то старшие товарищи — правильно (понятно) сформулированная задача это 95% ее решения.
По поводу ручного подбора частот (и обратно пропорциональных им периодов) событий — кому-то может показаться это каким-то примитивным методом недостойным внимания, но я привел эти рассуждения для того чтобы продемонстрировать насколько это просто. А это на несколько порядков проще чем забить в программу непонятные-не просчитанные периоды и потом годами ловить странные проблемы связанные с тем что события происходят не совсем так как запланировано. На самом деле это обычная задача комбинаторики, а математика это древняя наука и это совершенно обычное ее решение, чем проще тем эффективнее. К тому же, это решение можно автоматизировать, ну, если у вас есть соответствующий навык и время нарисовать графику на которой будут отображаться соотношения периодов и полученных разрешенных длительностей задач, которые, вообще говоря, нужно хотя бы один раз измерить-оценить — поставить тайм-стампы и как-то их получить для оценки реальных длительностей в реальном железе. Я, для примера, никогда не пренебрегал этой работой в самом начале проекта, так как было бы странно писать программу, когда в самом начале выяснится что алгоритм не убирается в тайминги.
Кстати, разделение самого короткого периода пополам между задачами тоже можно менять, только надо помнить, что первая задача, с самым коротким периодом является парой ко всем остальным задачам и если мы прибавим ей длительность исполнения скажем до 75% периода-80 мы автоматически оставим любой оставшейся задаче только 25% того же периода.
Еще можно разобрать вопрос того насколько оправдано было снижение частоты событий первой задачи? Что если эта задача действительно требует частоты не ниже 100 событий в секунду? В этом случае мы можем рассмотреть вариант с кратностью именно этой частоте, так же как и повышение этой частоты до 120 Гц, например.
Но для варианта F1=100, F2=40, F3=8 замечательно будет работать абсолютно та же логика, просто события периода-40 будут всегда приходиться на каждый 20-й период-100, и мы сможем также развести события 2 и 3 на четные и нечетные периоды событий периода-100.
Если вспоминать практические задачи которые мне приходилось решать, наверно самой интересной была следующая. В конце ссылка на пояснения другого типа с картинками.
Это была задача в которой значение считывалось с АЦП по последовательной шине SPI, и на той же SPI шине висела Еепром-память, в которую эти значения процессор должен был записать для регистрации и проверки работы алгоритма управления (при аттестации-испытаниях прибора и просто контроля адекватности работы прибора — это был медицинский прибор).
То есть основное расписание работы процессора выглядело так:
Задана определенная частота измерения — чтения значений АЦП — транзакция на SPI шине достаточно короткая по времени пусть 1% - 2% периода опроса АЦП (далее период-АЦП).
Функция расчета управляющих воздействий на основе накопленного массива значений с АЦП— неизвестной длительности(!), потому что сама функция могла реализовать произвольный алгоритм, я сделал систему которая предоставляла сигнатуру функции, реализацию которой мог делать кто угодно. Хотя у меня была, конечно, одна референсная реализация. Для этой функции контролировалось что она не превышает 1.5 (полтора) периода-АЦП, а в среднем функция выполняется меньше чем за скажем 60% периода-АЦП.
транзакция на SPI шине записи блока данных в Еепром которая занимала, скажем, 33% периода АЦП.
Там был и какой-то экран и пара-тройка кнопок, но это все отнимало очень маленький процент от периода-АЦП, потому что управлялось в прерываниях просто записью в соответствующие регистры, и я уже совершенно не помню этих деталей.
Задача была обеспечить абсолютно стабильную частоту опроса АЦП, так как без этой стабильности у основного алгоритма работы прибора начинались, как сейчас модно говорить, галлюцинации — он начинал показывать то чего нет и/или не показывал то что есть. Кстати до меня эти галлюцинации безуспешно пытались победить несколько лет и добились очень сильного снижения их вероятности, надо признать, но полностью победить, фактически, отчаялись. А, как известно, если в программе существует вероятность сбоя ее работы, то вылазит этот сбой, обычно, в самый не подходящий момент.
Суть алгоритма чтобы победить эти галлюцинации окончательно и бесповоротно заключалась в том, что после того как отработала функция расчета-генерации управляющих воздействий, программа считывала значение таймера и определяла сколько времени осталось до следующего опроса АЦП простым чтением тиков таймера, который отсчитывал период до запуска следующего АЦП, если времени оставалось больше 33% периода-АЦП, запускалась транзакция записи блока данных, поскольку блок данных накапливался 128 измерений АЦП, у программы было чуть меньше 128 попыток получить достаточно времени после основных операций обработки очередного значения и выполнить необходимое сохранение. Поскольку запуск-чтение АЦП выполнялись в прерывании, основная функция обработки могла продолжаться и в следующем периоде АЦП, проверка оставшегося времени также проверяла что функция продолжается больше одного периода и в случае превышения времени целого периода с половиной прерывала работу прибора и сигнализировала аварию, но такого никогда не случалось так как это время задано с большим запасом для нормальной функции обработки значений. Но в тестах, которые должны были проверить что эта аварийная диагностика работает, использовалась специальная версия функции, которая и создавала эту задержку и соответственно аварию для проверки.
Картинки по теме можно посмотреть здесь, с дополнительными пояснениями.
В качестве вывода ��з всего сказанного я бы рекомендовал всем не пренебрегать самыми простыми методами математики-арифметики. Надо всегда помнить что самые простые методы, обычно, являются самыми эффективными. Другим термином характеризующим самые простые или даже примитивные методы математики является термин "фундаментальные". Да-да! именно так Примитивные Методы в любой научной дисциплине образуют базис-фундамент на котором и строится любая научная дисциплина, помните об этом, не отрывайтесь от фундамента.
