Я работал в основном с 8-битными микроконтроллерами, где большинство ОСРВ требуют слишком много накладных расходов.
Большинство приложений, с которыми я работал, были просто периодическим прерыванием с цепочками if/else для всей логики обработки, а затем MCU снова переходит в спящий режим.
Это хорошо сработало для многих вещей и имеет действительно минимальные накладные расходы. Но для одной системы я дошел до того, что стало так много управляющих флагов, что я готов назвать свою собственную систему «спагетти». Было бы ужасно, если бы кто-то новый взял эту систему и реализовал какой-то новый функционал.
(У меня есть двухцветный светодиод, который должен иметь около 8 различных состояний и шаблонов мигания, зависящих от времени, в зависимости от того, в каком состоянии находится остальная часть системы. Это ужасное упражнение, потому что это должно быть так просто ...)
Я рассматривал возможность создания конечного автомата и попытки отсеять так много управляющих флагов.
Я вижу одну концептуальную проблему, связанную с использованием таймеров в конечном автомате. В настоящее время у меня есть один аппаратный таймер, а затем куча счетчиков таймеров, определяемых переменными, которые увеличивают/уменьшают приращение, переменная флага управления переходит в 0/1, и поэтому мы проходим через цепочку if/else.
На этапе планирования более строгого конечного автомата не могли бы вы просто использовать больше аппаратных таймеров и запускать внешние прерывания как события для возврата в конечный автомат?
Моя интуиция (правильная или нет) состоит в том, чтобы использовать как можно больше внешних прерываний для конечного автомата, если вы 1) представляете все виды потенциальных проблем с приоритетом прерывания, которые приносят свой собственный набор проблем, где в настоящее время время очень детерминировано, но логика управления просто сбивает с толку, и 2) вы используете более актуальную работу с кучей таймеров, а не просто обрабатываете логику таймера как переменные.
Я вижу, как вы все еще можете увеличивать/уменьшать переменные таймеры в вашей конечной машине, но разве это не противоречит этичному шаблону конечной машины?
Мне вполне комфортно обсуждать указатели на функции и операторы switch о том, как вы кодируете конечный автомат, или хотите ли вы использовать таблицу переходов и т. д.
Мне особенно интересно, как люди изящно справились с аспектом управления таймерами своих конечных автоматов.
Обычный способ сделать это — установить максимальное время выполнения для каждого состояния, а затем протестировать каждое из них (со 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]();
}
Если состояния зависят от внешнего ввода, то, возможно, пропустите часть состояния возврата и обновите состояние только через сеттеры/геттеры.
Это должно полностью устранить необходимость в флагах — в частности, в несвязанных флагах, расположенных в одной области видимости, что может стать кошмаром. Самое главное здесь — не путать сложность светодиода со сложностью другого оборудования.
Допустим, вы одновременно устраняете дребезг кнопки. Скажите, что вам нужно закончить устранение дребезга до того, как загорятся светодиоды — это не значит, что кнопки должны знать о светодиодах или что светодиоды должны знать о кнопках. Код вызывающей стороны должен отслеживать эти вещи. Это означает, что вам может понадобиться некоторый уровень абстракции между самым внешним конечным автоматом и самими драйверами. Если драйвер светодиода получает только один вход «сделай это!» от вызывающего абонента, то ему все равно на причины, стоящие за этим.
Если вам нужна супернизкотехнологичная «многозадачность», ваше прерывание таймера может выглядеть так:
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.
Как правило, я считаю, что лучше всего выбрать небольшое приращение времени (может быть, от пары миллисекунд до, возможно, 250 мкс для 8-битного микропроцессора) и использовать его для большей части или всего времени. Это сопоставимо с гранулярностью в RTOS.
Это проще, если у вас есть микро с приличной архитектурой, которая позволяет вложенные прерывания.
Энди ака
Лерой105
мкейт
Лерой105
мкейт
АльфаГоку