Машины конечных состояний и микроконтроллеры

Машины конечных состояний и микроконтроллеры
Машины конечных состояний и микроконтроллеры
Anonim

Конечные машины и микроконтроллеры

Конечные машины: лучший способ кодирования!

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

Проблема

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

Реальный ответ на такие проблемы - использование конечных автоматов, которые я обнаружил два месяца назад при программировании контроллера ZBM IO. Машины с конечным состоянием выглядят очень сложными, и их объяснения одинаково сложны, но на удивление они являются одним из самых простых методов программирования для интеграции.

Примечание. В этой статье рассматриваются модели конечных состояний в C, но они возможны на любом языке (включая сборку).

Что такое конечные государственные машины «hljs»> STATE = «START»; Void main (void) {do {if (STATE == "START") {startCode (); } else if (STATE == "MIDDLE") {middleCode (); } else if (STATE == "END") {endCode (); } else {STATE = "START"; }} while (1); } void startCode () {// Некоторый код здесь STATE = "MIDDLE"; } void middleCode () {// Некоторый код здесь STATE = "END"; } void endCode () {// Некоторый код здесь STATE = "START"; }

Когда код инициализируется, состояние системы устанавливается на «start». Это гарантирует, что у нас есть точка входа, которая заставит систему двигаться. Затем, когда код в startCode () был выполнен, состояние будет изменено на «среднее». Это означает, что при следующей итерации цикла do-while в main будет выполняться функция middleCode (). Когда эта функция будет выполнена, состояние будет теперь установлено на «end». На другой итерации основного цикла будет вызываться функция endCode (). Когда это будет выполнено, состояние будет изменено на «start». Это повторяется всегда, когда каждая функция вызывается в порядке, указанном в коде.

Это так же полезно, как и код, который находится внутри функций состояния (startCode, middleCode и endCode). Если код внутри этих функций НЕ является FSM-совместимым, все еще можно встретить бесконечные петли и, таким образом, сделать весь этот код бессмысленным. Настоящий трюк делает «код, совместимый с конечным состоянием». Итак, как мы это сделаем? Начнем с взгляда на старый код бесконечного цикла.


sendI2CStartBit(); while(I2CSTARTWAIT);

Эта функция будет ждать, пока стартовый бит не будет отправлен модулем I2C на борту ПОС. Но что произойдет, если начальный бит не может быть запущен?

ПИК имеет привычку в некоторых сценариях, где шина I2C может блокироваться, а управление не может быть восстановлено. Это означает, что ваш код навсегда останется в этом цикле. Хуже всего то, что даже если вы попытаетесь перезапустить стартовый бит, нет никакой вероятности, что шина разблокируется. Именно здесь действительно качается конечная машина.

Давайте рассмотрим графический макет конечного конечного автомата I2C, который будет включен в нашу основную программу.

Image
Image

Программа как FSM

Во-первых, есть две машины с конечным состоянием! Первый FSM - это основной код, который перемещается вокруг, вызывая три основные функции. Второй конечный автомат - это обработчик I2C, который может находиться в нескольких возможных состояниях, причем самым важным состоянием является состояние бездействия.

Состояние бездействия важно, поскольку оно говорит нам, что система I2C ничего не делает - именно в этот момент мы можем попросить его отправить стартовые биты, отправить байт, получить байт или любую другую связанную с I2C операцию. Итак, давайте посмотрим на внутренности обработчика I2C в его простейшей форме!


Void I2cupdate() { if(i2cState == “STARTBIT”) { sendI2CStartBit(); i2cState = “CHECKSTART”; } if(i2cState == “CHECKSTART”) { if(I2CSTARTWAIT == 0) { i2cState = “I2CIDLE” } } if(i2cState == “I2CIDLE”) { } }

Итак, как мы получим обработчик I2C для отправки стартового бита? Просто. Если часть кода должна использовать I2C, вы сначала проверяете, что I2C простаивает, тогда вы установите i2cState на «startbit».


SomeFunction() { if(i2cState == “I2cIDLE”) { i2cState = “STARTBIT”; } }

Затем, когда основная программа цикла непрерывно, обработчик I2C будет обновляться на каждой итерации. И если шина I2C блокируется, обработчик I2C будет застревать в одном состоянии, но остальная часть программы будет продолжена. Как мы узнаем, когда обработчик I2C сделан? Задайте значение i2cState, но НЕ выполняйте цикл while:


// This is OK: SomeFunction() { if(i2cState == “I2CIDLE”) { next state for this function; } } // This is NOT ok: SomeFunction() { while(i2cState != “I2CIDLE”); }

Итак, теперь давайте рассмотрим проблему блокировки I2C, которая предотвратила правильную работу обработчика I2C. Хорошей новостью является то, что обработчик I2C по-прежнему будет проходить через свои внутренние проверки состояния, и он все равно пройдет через состояние «checkstart». Это означает, что мы можем включить счетчик таймера или цикла, который будет увеличиваться при прохождении. Затем, если этот счетчик циклов достигнет заданного значения (TIMEOUT), мы можем заключить, что произошло что-то плохое и, таким образом, сбросить состояние I2C на холостой ход. В то же время мы можем также установить флаг, чтобы все другие функции могли быть предупреждены об ошибке.

Итак, давайте посмотрим на измененный код I2C:


SomeFunction() { if(internalState == “STATE1”) { if(i2cState == “I2cIDLE”) { i2cState = “STARTBIT”; internalState = “STATE2”; } } else if(internalState == “STATE2”) { if(i2cState == “I2cIDLE”) { if(i2cError == 1) { // Error handling code here } else { // Send data, read data or any old code internalState = “STATE1”; } } } }

Итак, теперь, когда мы можем выразить наши бесконечные функции цикла как машины конечного состояния, наша программа начинает выглядеть следующим образом (графическая форма):

Image
Image

Конструкция FSM с функциями mini-FSM

Основная функция - конечный конечный автомат, который вызывает каждую из машин подзаголовка. Когда вызываются эти машины подгосударства, они выполняют свои собственные проверки и внутренние изменения состояния. Каждая из этих машин подгосударства может исследовать состояния других машин суб-состояний, чтобы они могли взаимодействовать друг с другом и управлять ими. Как правило, вы получите несколько уровней государственных машин, причем верхний уровень является основной функцией, за которой следует второй программный уровень, который будет запрашивать данные I2C, отправлять байты UART и запрашивать ввод пользователя. Заключительные слои (прямо внизу, которые обычно НЕ связывают ни с чем, кроме самих себя) обрабатывают IO и такие устройства, как I2C, SPI и IO.

Image
Image

Слоистый вид

Теперь, прочитав эту статью, продолжайте экспериментировать! Начните с простой функции, которая включает и выключает светодиод в зависимости от его внутреннего состояния. Затем, когда вы это сделали, получите вторичный конечный автомат для управления конечным автоматом с LED-индикатором, чтобы он включался и выключался в зависимости от другого внешнего состояния, полностью не связанного со светодиодом.

Просто помните: никогда, никогда не используйте цикл while, если вы не знаете, что он выйдет (например, после 5 итераций). Теперь за вашу награду: полный конечный автомат I2C! Все это требует вызова I2C_INIT () в начале, а затем просто вызовите i2c_update () внутри основного цикла.

Скачать код