Моделирование контроллера печи

Сначала мы определились с тем, что нам нужно получить - контроллер печи, управляемый кнопками и энкодером, выдающий информацию на семисегментный дисплей; теперь задача, пожалуй, самая главная и тяжелая – представить в деталях, как контроллер должен работать.
Если честно, то раньше мне казалось, что работа программиста – это писать код, компилировать его, выискивать ошибки, ну или на крайний случай – писать документацию к уже сделанным продуктам. Но нет! Большую часть времени и усилий занимает процесс моделирования – КАК реализовать поставленную задачу; КАК по максимуму использовать ресурсы процессора; КАК сделать удобный интерфейс взаимодействия с рядовым пользователем; КАК не дать пользователю прострелить себе ногу что-либо сломать в настройках, при этом оставив ему саму возможность взаимодействовать с системой…

Представив, смоделировав и даже "попользовав" уже готовое устройство - "внешний" вид и "внешнее" взаимодействие - программист переходит на уровень глубже – начинает определять, из каких программных кирпичиков (виртуальных машин – об этом ниже) будет состоять программа. Так, отдельно выделяются средства взаимодействия с пользователем, средства управления, средства получения входных данных и т.д. Каждый из этих кирпичиков должен представлять собой более-менее независимую, обособленную часть, которая получает какие-то данные на вход и предоставляет данные на выход. Можно представить, что вы всем начальникам начальник и мочалкам командир, и у вас есть проект, отдельные задачи которого вы должны отдать на реализацию своим подчинённым программистам, а потом сформировать из этих задач цельную программу. При этом ваши программисты сидят за своими компьютерами и не взаимодействуют друг с другом, и только знают, что для их задач приходит на вход и какие данные должны быть на выход.

Задача моделирования, на самом деле, очень сложная – вроде как начинаешь формулировать с простого – "ну, нам нужно, чтобы дисплейчик тут горел и отображал температуру, а ещё надо, чтоб время задавалось", а потом это все выливается в целую тетрадь формата А4 на восемьдесят листов, всю исчерченную вариантами реализации (вообще крутые программисты в огромных корпорациях используют целый язык моделирования UML и его диаграммы; мы же обходимся простыми схемками с кружочками и стрелочками на бумажках – очень рекомендую если не первое, то хотя бы второе!)

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

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


Рисунок 1. Схема работы контроллера с точки зрения пользователя

Итак, у нас будет 5 режимов:

  • Выключенный режим – контроллер уже получает питание, однако никак не взаимодействует с печью; в этом режиме мы можем, например, выводить какое-нибудь сообщение на экране, чтобы пользователь понял, что нагревательный элемент печи выключен (например, мы задавали таймер – печь должна работать два часа; два часа прошло, и нужно показать, что печка отключилась)
  • Режим отображения текущей температуры
  • Режим настройки желаемой температуры
  • Режим отображения текущего значения таймера
  • Режим настройки таймера

Рабочие режимы: режимы отображения текущей температуры и текущего значения таймера – будем называть ещё режимами индикации. Также будем выделять режимы настройки – желаемой температуры и таймера. Очевидно, что нужно как-то визуально выделять режимы индикации от режимов настройки – чтобы пользователь понимал, что происходит. Звука и цвета у нас, к сожалению, нет… Что бы придумать… Хм! Будем использовать мигание!

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

Нам нужно как-то отделить включение и выключение устройства (на схеме пунктирная линия) – переходить в выключенный режим мы должны быть способны из любого другого состояния; поэтому было решено для вопросов включения-выключения использовать длинное нажатие на кнопку.

Для переходов из рабочих режимов в режимы настройки интуитивно выбираем поворот энкодера – начать крутить ручку для изменения циферок на дисплее - довольно естественное движение)
Вариантов для возврата в режим индикации из режима настройки не так уж много – короткое нажатие на кнопку. Но если мы посмотрим на уже готовые подобные устройства, то заметим, что достаточно часто переход из режима настройки в режим индикации происходит автоматически через некоторое время бездействия – "по таймеру" - так что в дальнейшем можно будет попробовать реализовать и такой подход.

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

Запутались? Снова используем диаграмму!


Рисунок 2. Схема работы контроллера с переходами

Вот, теперь уже можно представить, как будет работать наш контроллер.
И только теперь от идеи можно переходить к продумыванию технических проблем.

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

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

Подробней о виртуальных машинах можно почитать тут.

Подумаем, чем микроконтроллер может заниматься параллельно:

  • получать текущую температуру внутри печи (выполнять АЦП)
  • управлять температурой печи (включать-выключать нагревательный элемент)
  • отображать что-либо на дисплее
  • следить, было ли нажатие на кнопку
  • следить, был ли поворот энкодера
  • отсчитывать таймер
  • следить, в каком мы "состоянии" для пользователя

Так. Теперь из этого можно попробовать выделить основные виртуальные машины:

  • машина для АЦП
  • машина для управления нагревательным элементом
  • машина для отображения текста на семисегментном дисплее
  • машина для отслеживания нажатий на кнопку
  • машина для отслеживания поворота энкодера
  • машина для таймера
  • машина "состояний" контроллера

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

Теперь о сущности, "внутренности" виртуальной машины – будем рассматривать на простом примере. Допустим, нам нужно по нажатию на кнопку отправлять на дисплей какое-то сообщение. Не будем заморачиваться с дребезгом или вопросами, как мы работаем с дисплеем, рассмотрим сам принцип:


Рисунок 3. Пример принципа работы виртуальной работы

Выглядит просто. А теперь перейдем к реализации на языке программирования: вся фишка будет в том, что мы используем переменную, отвечающую за текущее состояние виртуальной машины – назовем её, например, 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.
Если мы таким же образом реализуем ещё несколько машин, и будем последовательно вызывать их в основном цикле, то получится, что все работает практически одновременно.
Конечно, если у нас будут десятки таких машин, то программа будет "подтормаживать" - но это возникает даже на компах, верно?)

А теперь отдельно рассмотрим ситуацию, когда наша машина зависима от времени – например, каждую секунду нужно отображать время, и у нас есть прерывание, которое вызывается раз в секунду.
Самое главное, что нужно запомнить – то, что нельзя всю машину запихивать в прерывание! Прерывание – такая страшная вещь, которая однозначно постоянно отнимает кусок времени у программы – а значит, и у всех "обычных" виртуальных машин.

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


Рисунок 4. Пример принципа работы виртуальной машины с использованием прерываний.

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

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


Наверх

Автор - Moriam  =ˆˆ=

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