Конечный автомат изящно обрабатывает таймеры?

Я работал в основном с 8-битными микроконтроллерами, где большинство ОСРВ требуют слишком много накладных расходов.

Большинство приложений, с которыми я работал, были просто периодическим прерыванием с цепочками if/else для всей логики обработки, а затем MCU снова переходит в спящий режим.

Это хорошо сработало для многих вещей и имеет действительно минимальные накладные расходы. Но для одной системы я дошел до того, что стало так много управляющих флагов, что я готов назвать свою собственную систему «спагетти». Было бы ужасно, если бы кто-то новый взял эту систему и реализовал какой-то новый функционал.

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

Я рассматривал возможность создания конечного автомата и попытки отсеять так много управляющих флагов.

Я вижу одну концептуальную проблему, связанную с использованием таймеров в конечном автомате. В настоящее время у меня есть один аппаратный таймер, а затем куча счетчиков таймеров, определяемых переменными, которые увеличивают/уменьшают приращение, переменная флага управления переходит в 0/1, и поэтому мы проходим через цепочку if/else.

На этапе планирования более строгого конечного автомата не могли бы вы просто использовать больше аппаратных таймеров и запускать внешние прерывания как события для возврата в конечный автомат?

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

Я вижу, как вы все еще можете увеличивать/уменьшать переменные таймеры в вашей конечной машине, но разве это не противоречит этичному шаблону конечной машины?

Мне вполне комфортно обсуждать указатели на функции и операторы switch о том, как вы кодируете конечный автомат, или хотите ли вы использовать таблицу переходов и т. д.

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

+1 за красиво сформулированный вопрос, но он немного граничит с поиском мнений, поэтому, если вы сможете его улучшить, вы можете получить больше ответов?
Не хотите отредактировать? Я буквально не знаю, как сделать так, чтобы мнений было меньше, это своего рода вопрос дизайна.
У каждого конечного автомата есть код, определяющий следующее состояние, верно? Когда вы входите в состояние с тайм-аутом, вы делаете снимок системного таймера и сохраняете его. Затем, принимая решение о следующем состоянии, вы получаете текущее время, вычитаете моментальный снимок и сравниваете со значением времени ожидания. Не кажется супер сложным. Я полагаю, что это может израсходовать немного памяти, которой может не хватать. Если вы хотите сохранить идеал конечного автомата, вы можете посвятить состояние сохранению текущего времени для последующего расчета прошедшего времени.
@mkeith - Да, функции конечного автомата (я думаю, функции указателя, подход к конечному автомату) будут указывать на следующее состояние. Я буду лапшу этот подход.
Если таймер представляет собой 16-битное число без знака, то вычитание через границу переполнения все равно даст правильное прошедшее время.
«Было бы ужасно, если бы кто-то новый взял эту систему и реализовал некоторые новые функции». -> Реализуйте свою конечную машину с помощью инструмента, подобного IBM Rational Rhapsody. Он генерирует код на основе вашей диаграммы конечного автомата и, следовательно, также параллельно документируется.

Ответы (3)

Обычный способ сделать это — установить максимальное время выполнения для каждого состояния, а затем протестировать каждое из них (со 100-процентным покрытием кода) и убедиться, что они никогда не превышают максимальное время выполнения. Если повезет, вы даже можете использовать встроенный сторожевой таймер, чтобы обеспечить это, если он может работать с достаточно низкими тайм-аутами.

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

STATE_MACHINE[state++](); 
if(state == STATES_N) 
{ /* reset state machine */ 
} 

который ничего не делает, кроме как циклически переключает различные программные модули, давая каждому из них «квант времени». Либо вы можете запустить все различные аппаратные драйверы за один раз и вернуться в спящий режим, либо вы можете запустить только один из них. Это, конечно, зависит от требований реального времени.

Одним из таких состояний может быть led_execute(), которое представляет собой подпрограмму светодиодов, отслеживающую то, что происходит на светодиодах прямо сейчас. Эта процедура находится внутри драйвера светодиодов и, в свою очередь, может отслеживать состояние каждого светодиода, так что она выглядит примерно так:

typedef enum
{
  LED_OFF,
  LED_RED_LIT, // whatever names make sense
  LED_RED_BLUE_LIT,
  ...
  LED_DONE,
  LED_N
} led_state_t;
...    
static led_state_t led_state = LED_OFF;
...

void led_execute (void)
{
  led_state = LED_STATE_MACHINE[led_state]();
}

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

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

Допустим, вы одновременно устраняете дребезг кнопки. Скажите, что вам нужно закончить устранение дребезга до того, как загорятся светодиоды — это не значит, что кнопки должны знать о светодиодах или что светодиоды должны знать о кнопках. Код вызывающей стороны должен отслеживать эти вещи. Это означает, что вам может понадобиться некоторый уровень абстракции между самым внешним конечным автоматом и самими драйверами. Если драйвер светодиода получает только один вход «сделай это!» от вызывающего абонента, то ему все равно на причины, стоящие за этим.

Лундин, полностью согласен. Прошлой ночью я лежал в постели, представляя несколько конечных автоматов и думая об этом. Если у светодиода был отдельный конечный автомат, он брал состояния с другого автомата. По вашему мнению, если вы выберете этот путь, уменьшите ли вы сложность настолько, чтобы оно того стоило? В моей процедуре if/else я дошел до того, что есть 6 флагов, если большинство IF должны контролироваться для логического управления крайними случаями состояний. Я полностью согласен с тем, что светодиод принципиально отличается от всего остального, но флаги разбросаны повсюду, чтобы удержать корабль на плаву.
Итак, когда вы говорите о тестировании каждого состояния, в основном вы имеете в виду, что каждое состояние завершается и должно пнуть собаку до того, как истечет таймер? Это великолепная идея. В моем случае, я думаю, что мой сторожевой таймер не может работать так быстро, как мне нужно, но в других случаях он будет работать отлично.
@ Leroy105 Когда в прошлом мне приходилось иметь дело с запутанными базами кода в виде «флагетти», я использовал именно этот дизайн, чтобы распутать их. У меня есть один конкретный пример, когда программа страдала от всевозможных периодических ошибок, но когда флаги были заменены конечными автоматами, все ошибки просто исчезли, хотя я не касался фактической логики приложения. Так что да, я знаю по опыту, что это правильный путь.
@ Leroy105 Что касается бенчмаркинга, вам нужно убедиться, что какое-то состояние не выходит из строя и не разрушает всю производительность в реальном времени. Вышеупомянутое является своего рода грубой формой RTOS, если она реализована правильно. Вы также можете отслеживать каждое состояние из кода вызывающей стороны и регистрировать их — это то, что я использую в более важном коде, но также вместе с каким-то сторожевым таймером. Также следует отметить, что пнуть wdog изнутри ISR — не блестящая идея. Так что, если это все один большой ISR, возможно, стоит поискать альтернативный дизайн.
«флагетти» — бинго.
Лундин, вы изменили свое мнение об использовании указателей функций вместо переключателей? Я не религиозен в отношении аргументов скомпилированного кода операции (т. е. операторов case для перехода к таблицам и т. д.) — меня больше всего беспокоят опечатки и ремонтопригодность. Я не пишу код в соответствии со стандартами MISRA, но я определенно создал ошибки, не проверяя границы массива в своей жизни. Даже использование массивов дает мне некоторую паузу! Это может быть медведь, чтобы поймать ошибку массива и т. д. Мне нравится чистый характер указателей. Похоже на стирку, вероятно, с точки зрения исполнения. Ценю ваше мнение, так как вы видели эти системы впритык.
Вы знаете, что мне действительно не нравится, так это то, что я видел конечные автоматы, в которых состояния не возвращаются в функциях. Вся логика использует некоторую жестко типизированную матрицу состояний. Моему мозгу не нравится такой подход. Кажется, трудно отлаживать? Я в любом случае проверю границы в машине состояний. Узнал это на собственном горьком опыте (т.е. почему один бит переключается каким-то странным образом, но остальная часть системы работает, и она переключается только на 3-м цикле ISR. Мне пришлось вызвать старый таймер в моем офисном пакете, чтобы научить меня массивам в C... мы больше не в мире Java!).
Я посмеиваюсь про себя, четыре часа спустя, когда я ловлю какую-то серьезную неприятную ошибку флагетти, зависящую от времени, которая может оставить один цветной светодиод горящим, но перейти в режим мигания. Зависит от времени перехода. Боже, какое дерьмовое ведро я здесь закодировал без конечного автомата. ;)
@ Leroy105 Самая важная часть конечных автоматов заключается в том, что совершенно ясно, где происходят изменения состояния. Чаще всего в конце каждой функции состояния. Но также возможно иметь внешнего «планировщика», который выделяет каждому элементу оборудования временной интервал для работы. Это то, что я имел в виду, когда писал этот ответ, но я думаю, что такие проекты на самом деле не следует называть конечными автоматами, а планировщиками.
Это своего рода гибридный подход. Я видел, как один университетский профессор держал в каждой функции состояния счетчик статических переменных, который запускает ISR. Или он создает некоторые функции установки/получения для изменения некоторых глобальных определенных счетчиков. Я думаю, что у меня есть хватка, это разделить на несколько конечных автоматов, как вы говорите. Если вы когда-нибудь почувствуете желание создать глобальный флаг, вы должны сделать эту логику другим определенным состоянием, и ваш конечный автомат вернется к следующему состоянию и т. д. Мне понравилась эта статья: isa.uniovi.es/docencia/ красный/…

Если вам нужна супернизкотехнологичная «многозадачность», ваше прерывание таймера может выглядеть так:

timer_isr()
{
    process1();
    process2();
    process3();
}

Итак, если ваш таймер срабатывает, скажем, 100 раз в секунду, то каждый раз, когда вызывается каждая функция process(). Эти функции представляют собой FSM, которые реализуют ваши различные «многозадачные» задачи. Если они все независимы, то это легко.

Теперь, если ваши задачи зависят, вы можете сделать что-то вроде:

timer_isr()
{
    check_buttons();
    blink_red();
    blink_blue();
}

В этом случае задачи будут взаимодействовать через уродливые глобальные переменные (мы не собираемся делать окна сообщений и FIFO на 8-битной системе). Например, функция check_buttons() устраняет дребезг и т. д., устанавливает некоторые флаги и/или напрямую влияет на состояние двух других FSM, которые мигают светодиодами.

Мы даже можем использовать передовую технологию под названием C++:

timer_isr()
{
    check_buttons();
    red.blink();
    blue.blink();
}

В этом случае функция check_buttons() будет вызывать "red.setBlinkMode(некоторое значение)" при нажатии соответствующей кнопки, например. "красный" и "синий" являются глобальными объектами. В этом случае этот крошечный кусочек ООП позволяет вам реализовать один и тот же алгоритм для обоих без необходимости возиться с кучей глобальных переменных или указателей на структуры и т. д.

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

Метод .blink() будет, например, увеличивать счетчик, специфичный для светодиода, до тех пор, пока он не достигнет периода мигания, или настроить ШИМ в зависимости от времени, чтобы заставить его мигать причудливо, и тому подобное.

Например, если ваше время вызывается раз в миллисекунду, ваш метод blink() может быть:

LED::blink()
{
    if( led_on) {
        if( counter++ > period ) {
            counter=0;
            led_pin = !led_pin;
        }
    } else { led_pin = 0; counter=0; }
}

...что-то вроде того. Все они вызываются в один и тот же период таймера, поэтому все эти маленькие конечные автоматы знают о времени, подсчитывая количество вызовов. В этом случае состояние FSM — это led_on и counter.

Да, я понимаю, но вы видите, как вы ввели кучу управляющих переменных? У меня есть беспорядок if/else, но у меня также есть беспорядок с управляющими/логическими переменными. Я пытаюсь привести в порядок обе стопки. ;)
Попробуйте разрезать его на небольшие независимые блоки, которые обмениваются данными через «каналы» (в вашем случае — простые сигнальные переменные), чем меньше каждый блок/FSM, тем лучше.
@Leroy105 Если вам нужно использовать, скажем, 20 флаговых/сигнальных переменных для связи между вашими модулями, то вам ДОЛЖЕН использоваться для них 20 переменных либо в виде битовых масок, либо в виде отдельных изменчивых переменных, либо в виде отдельной структуры связи, либо даже отдельный коммуникационный модуль с несколькими конструкциями внутри - на ваше усмотрение, в зависимости от сложности. Вам просто нужно изолировать свои коммуникационные переменные/флаги за пределами вашей основной логики.
Тесная связь между драйвером светодиода и драйвером кнопки ни в коем случае не является лучшей конструкцией, чем оригинальные спагетти. Это не то , как вы делаете правильный ОО! Ваши классы не автономны, но знают то, о чем им знать не следует. Это не объектно-ориентированный дизайн ни в C, ни в C++. Для правильного дизайна код более высокого уровня должен отслеживать как кнопки, так и светодиоды.

Как правило, я считаю, что лучше всего выбрать небольшое приращение времени (может быть, от пары миллисекунд до, возможно, 250 мкс для 8-битного микропроцессора) и использовать его для большей части или всего времени. Это сопоставимо с гранулярностью в RTOS.

Это проще, если у вас есть микро с приличной архитектурой, которая позволяет вложенные прерывания.

Sphero, ты имеешь в виду, что будишь конечный автомат каждые 250 мкс? А если ваш светодиод должен мигать каждые 1000 мкс, вы бы использовали счетчик в своем коде конечного автомата? Когда LedCounter = 1, загорается светодиод и т. д. Я пытаюсь устранить так много проклятых переменных счетчиков и логических флагов. В настоящее время система в основном представляет собой таймер на 500 мкс, и все работает от него.
Да счетчик. Представьте себе массив счетчиков, каждый из которых уменьшен до нуля в коде прерывания (очевидно, объявите их изменчивыми).
Если бы я сказал — хорошо, у нас будет ISR таймера SysTick сейчас, а затем мы добавим ISR LedTimer вместо простого запуска переменных счетчика для LedTimer внутри таймера SysTick — в целом сложность системы увеличилась с двумя ISR. вводится? Это мое внутреннее чувство, что это плохой подход.