Виртуальные машины

Многозадачность в микроконтроллерах (для чего нужны виртуальные машины)

Как только мы переходим от простеньких программ в пару действий к более сложным, мы сталкиваемся с одной из основных проблем, возникающих при программировании микроконтроллеров: проблемой реализации многозадачности.
В «больших» программах обычно необходимо выполнять несколько действий одновременно: проверять входящую информацию (состояние портов, данные по 1-Wire, I2C, UART и т.д.), следить за отображением выходной информации (дисплеем и светодиодами, например), выполнять какие-то управляющие действия (включение-выключение чего-либо, отсылка сообщений) и ещё много чего!
На компьютерах за одновременность выполнения различных действий отвечает операционная система – именно она следит за тем, кто из программ получит сейчас процессорное время, она же выделяет каждому по потребностям память. При этом операционная система может управлять вызовом программ по-разному: есть многозадачность вытесняющая и невытесняющая.
При невытесняющей многозадачности процессы помогают операционной системе – они сами определяют, когда стоит закончить со своими делами и предложить другим процессам время и ресурсы – то есть операционной системе остается только вызывать подпрограммы в какой-то определённой последовательности.
При вытесняющей многозадачности каждый процесс считает себя самым важным и обычно готов отхватить времени и ресурсов по максимуму, а операционная система может грубо прерывать такие процессы и передавать ресурсы другим.
Для того, чтобы реализовать многозадачность, нам тоже придётся сделать что-то типа простенькой операционной системы-переключателя задач – будем использовать невытесняющую многозадачность. При этом первое и самое важное, что нужно сделать – это выделить так называемые виртуальные машины.

Что такое виртуальная машина

Виртуальная машина – это некоторая обособленная часть программы, выполняющая определённую задачу. Эта часть программы при каждом обращении выполняет небольшое количество действий. Таким образом, если обращаться последовательно к нескольким виртуальным машинам, каждая из которых за раз занимает совсем крошечный отрезок времени для своего выполнения, будет казаться, что все они работают параллельно.
Каждая виртуальная машина получает какую-то информацию на вход, и выдает какую-то информацию на выход; при этом достаточно часто информация с выхода одной машины подается на вход другой – можно сравнить с каким-нибудь производством: один цех очищает сырье, это очищенное сырье поступает на вход другому цеху; тот делает болванки, и передаёт их следующему цеху, который уже и преобразует их в конечный продукт. При этом работникам одного цеха не интересно, что делают другие, главное – получить нужное на вход и отправить обработанный «вход» на выход.
Виртуальная машина очень похожа на автомат (ссылка на Савельева «Прикладная теория конечных автоматов»): у машины есть несколько состояний, есть переходы от одного состояния к другому, есть входные сигналы, при получении которых происходят те или иные переходы, и есть выходные реакции машины – то, что она делает в определённом состоянии и/или при определённом входном сигнале.
Изучим теорию подробней: среди конечных автоматов выделяют автоматы Мура и автоматы Мили. Рассмотрим каждый автомат на примере: пусть наша задача – по нажатию на кнопку отправить сообщение.
Автомат Мура в каждом состоянии сначала выполняет определённые действия, прописанные для этого состояния, а потом на основе входных сигналов определяет, в какое состояние мы переходим дальше. Для нашего примера автомат будет выглядеть так:
«вход» и «выход»; однако в нашем случае при достижении результата – получения какой-либо выходной информации – машина обычно не прекращает работу.
Рассмотрим пример простой виртуальной машины, задача которой – по нажатию на кнопку отправить сообщение. Вот так она будет выглядеть для автомата Мура:


Рисунок 1. Пример автомата Мура.

Овалы – это состояния, переходы – соответственно, стрелки; красным выделен анализ переходов, синим – действия, которые мы выполняем в состоянии.
Всего у нас получилось три состояния: первое – это когда мы ждём нажатия на кнопку; в этом состоянии мы не делаем ничего, просто проверяем, не нажата ли кнопка. Если кнопка нажата, то переходим в состояние 2, а если нет – так и остаёмся в состоянии 1.
Состояние 2 – это непосредственно само отправление сообщения; после выполнения действий в состоянии происходит безусловный (вне зависимости от входных сигналов) переход в состояние 3.
В состоянии 3 мы ждём, когда кнопка будет отпущена; так же, как и в состоянии 1, мы ничего не делаем, только проверяем кнопку. Как только кнопка отпущена, переходим в состояние 1.
Теперь рассмотрим автомат Мили – в нём сначала происходит проверка входных сигналов, и уже по переходам происходит выполнение действий. Вот как это выглядит схематично:


Рисунок 2. Пример автомата Мили.

Однако в жизни очень часто бывает так, что легче скомбинировать автоматы Мура и Мили: иногда нужно при попадании в состояние сначала сделать какие-то действия, потом проверить входные сигналы, и по результатам этой проверки тоже сделать какие-то действия и потом уже перейти в другое состояние. Такие автоматы называются комбинированными, их очень удобно применять – однако нужно внимательно их проектировать!

А теперь вернёмся к нашему примеру – попробуем реализовать виртуальную машину на основе схемы автомата Мура.
Вся фишка в том, что мы используем переменную, отвечающую за текущее состояние виртуальной машины – назовем её, например, ButtonMachineState. Она может принимать три значения – например, 1 – когда мы ждём нажатия на кнопку, и 2 – когда знаем, что кнопка нажата, и отправляем сообщение, и 3 – когда ждём, когда кнопка будет отпущена.
Тогда сама функция для этой простенькой машины будет такой:

typedef enum {bmsWaitPressing, bmsSendMessage, bmsWaitReleasing} TButtonMachineState;
TButtonMachineState ButtonMachineState;

void ButtonMachine()
{
	switch ButtonMachineState
	{
	case bmsWaitPressing:
		if (PINB & 1)
			ButtonMachineState = bmsSendMessage;
		break;
	case bmsSendMessage:
		ShowOnDisplay('Button is pressed');
		ButtonMachineState = bmsWaitReleasing;
		break;
	case bmsWaitReleasing:
		if (~(PINB & 1))
			ButtonMachineState = bmsWaitPressing;
		break;		
	}
}

В нашем случае переменная, отвечающая за текущее состояние машины, будет глобальной (за неимением классов, как в С++) – хотя на самом деле состояние машины можно передавать в функцию через параметр, а возвращать новое состояние через результат. Для упрощения понимания кода в сделали её в виде перечисления – чтобы можно было кратко описать каждое состояние.
Получается, что при каждом вызове функции нашей машины мы выполняем пару-тройку действий и тратим на это совсем немного времени: например, нужная кнопка нажата, тогда в первый вызов функции мы только проверим, какое состояние у кнопки, и определим, что значение ButtonMachineState должно поменяться. А во второй вызов мы уже отправим сообщение, и снова изменим ButtonMachineState.
Если мы таким же образом реализуем ещё несколько машин, и будем последовательно вызывать их в основном цикле, то получится, что все работает практически одновременно.
Конечно, если у нас будут десятки таких машин, то программа будет «подтормаживать» - но это возникает даже на компах, верно?)
Едем дальше: в микроконтроллерах часто бывает, что выполнение некоторых действий критично по времени: например, переключение отображения разрядов на семисегментном дисплее, или работа с некоторыми интерфейсами типа 1-Wire, считывание значений с АЦП и т.д. При всей своей пользе обычные виртуальные машины не могут гарантировать одинаковую длительность выполнения каждого из своих состояний – поэтому в таких случаях используют прерывания, которые однозначно вызываются через равные промежутки времени или при каком-либо событии. Например, каждую секунду нужно отображать время, и у нас есть прерывание, которое вызывается раз в секунду.
Самое главное, что нужно запомнить – то, что нельзя всю машину запихивать в прерывание! Прерывание – такая страшная вещь, которая однозначно постоянно отнимает кусок времени у программы – а значит, и у всех «обычных» виртуальных машин.

Поэтому в прерывании в этом случае у нас будет только отправляться посылка – назовем её «OnShowTime».


Рисунок 3. Пример использования посылки.

Пару слов о флагах и посылках (сообщениях, если называть по аналогии с Windows): считаем, что флаг снимается и возводится одним и тем же «устройством» или машиной – таким образом, если какая-то другая машина не успела заметить взведение флага до того, как он был снят – то «сам виноват».
Посылка же, или сообщение – это флаг, который возводится одной машиной, но она при этом не озадачивается дальнейшей судьбой этой посылки – потребуется ли эта информация какой-нибудь другой машине. Такой флаг может устанавливаться несколько раз подряд, «не снимаясь» - зато это гарантирует, что информацию этой посылки – например, наступление некоторого события – точно успеют получить, даже если это событие уже прошло и даже наступило ещё раз: например, таким событием может быть «нажатие на кнопку», или «необходимость отображения времени», как в примере выше – даже если пользователь кнопку уже отпустил, или секунда уже прошла, соответствующие событию действия все равно нужно выполнить во что бы то ни стало. И только после выполнения действий посылка очищается – программа снова готова ждать её получения.
Используя данный подход, нужно учитывать существование следующих опасностей: при использовании флага есть опасность пропустить его кратковременное взведение; а при использовании посылки есть вероятность, что мы не заметим, как событие свершилось несколько раз. В таком случае, если количество событий важно, добавляют ещё переменную-счётчик.
Ну а самое главное при использовании флагов и посылок – это не забывать их очищать!

Как выделять виртуальные машины

Для того, чтобы выделить в разрабатываемом устройстве виртуальные машины, первым делом нужно его смоделировать в голове!

Вообще для того, чтобы начать работать над какой-то программой или устройством, если это не «Hello, world!», нужно… погасить дисплей, отставить клавиатуру и мышку в сторону, взять листочек и ручку, закрыть глаза и представить, что мы хотим получить в результате: как программа будет общаться с пользователем, что она будет получать на вход, какие результаты выдавать. Стоит нарисовать, какие состояния будут у программы – используя все те же кружочки и стрелочки; если программа общается с пользователем (а это происходит практически всегда) – как пользователь будет видеть этот процесс; пример моделирования представлен тут

Только представив, как будет работать программа и, если мы программируем микроконтроллер, как будет выглядеть устройство, можно переходить к выделению виртуальных машин. Обычно уже при моделировании начинают виднется очертания отдельных «кирпичиков» - тут мы работает с кнопками, тут – управляем дисплеем, особняком стоит управление через выводы и, например, общение через 1-Wire…

Помимо такого «сумбурного» выделения, стоит просмотреть устройство, например, от «физики» к «логике»: сначала смотрим, что можно выделить на низком уровне, физическом: управление выводами входа-выхода, защита от дребезга, низкий уровень протоколов – например, если мы реализуем программно интерфейс 1-Wire, здесь будет управление конкретным выводом, отправление и чтение «логического 0» и «логической 1» (жесткая работа по времени). Дальше на основе уже выделенных машин поднимаемся выше: машина кнопок, которая будет получать очищенный сигнал от машины защиты от дребезга и анализировать, как долго была нажата кнопка; машина механического энкодера, которая также получает входные данные от машины защиты от дребезга; машина более высокого уровня 1-Wire, которая работает уже на сетевом уровне протокола, – отсылка команд; дальше – выше – машина работы с пользователем (например, отображение нужной информации на дисплее и реакция на нажатия кнопок и движения энкодера) и так далее.

Наверх

Автор - Moriam  =ˆˆ=

Обсудить на форуме