Хабрахабр

[Из песочницы] ОС реального времени AQUA RTOS для МК AVR в среде BASCOM AVR

При написании для МК кода посложнее, чем «помигать лампочкой», разработчик сталкивается с ограничениями, присущими линейному программированию в стиле «суперцикл плюс прерывания». Обработка прерываний требует быстроты и лаконичности, что приводит к добавлению в код флагов и приведению проекта к стилю «суперцикл с прерываниями и флагами».

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

Однако все они написаны на языке Си или Ассемблер и не подходят тем, кто программирует МК в среде BASCOM AVR, лишая их столь полезного инструмента для написания серьезных приложений. Избавиться от «макаронного кода» и вернуть сложному проекту на МК гибкость и управляемость помогает использование операционных систем реального времени.
Для микроконтроллеров AVR разработаны и довольно популярны несколько кооперативных ОС реального времени.

Чтобы исправить этот недостаток, я разработала простую ОСРВ для среды программирования BASCOM AVR, которую и выношу на суд читателей.

суперцикл. image
Для многих привычный стиль программирования МК – т.н. При пуске сначала выполняется инициализация оборудования самого МК и внешних устройств, задаются константы и начальные значения переменных, а затем управление передается в этот бесконечный суперцикл.
Простота суперцикла очевидна. Код при этом состоит из набора функций, процедур и описателей (константы, переменные), возможно, библиотечных, в целом называемых «фоновым кодом», а также большого бесконечного цикла, заключенного в конструкцию типа do – loop. Недостатки тоже налицо: если какое-то устройство или сигнал требует немедленной реакции, МК обеспечит ее не раньше, чем обернется цикл. Большинство задач, выполняемых МК, ведь так или иначе цикличны. Если длительность сигнала окажется короче, чем период цикла, такой сигнал будет пропущен.

В приведенном ниже примере мы хотим проверить, нажата ли кнопка button:

do ' какой-то код if button = 1 then ' реакция на нажатие кнопки ' еще какой-то код
loop

Очевидно, что если «какой-то код» работает достаточно долго, МК может не заметить короткого нажатия кнопки.

Так появляется следующий уровень: суперцикл с прерываниями.
В примере ниже показана структура программы с суперциклом и прерыванием, обрабатывающим нажатие кнопки: К счастью, МК снабжен системой прерываний, которая позволяет решить эту проблему: все критичные сигналы можно «повесить» на прерывания и написать для каждого обработчик.

on button button_isr ' назначаем обработчик кнопки
enable interrupts ' *** суперцикл ***
do ' какой-то код
loop
end ' обработчик кнопки
button_isr: ' делаем что-то при нажатии кнопки
return

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

Запоминание прерываний

Однако все равно это прерывание не может быть обработано немедленно.
На самом деле, находясь внутри прерывания, МК способен отметить факт другого прерывания в специальном регистре, что позволяет обработать его позже.

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

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

Таким образом, возникает следующий уровень сложности – суперцикл с прерываниями и флагами.

Ниже показан подобный код:

on button button_isr ' назначаем обработчик кнопки
enable interrupts ' *** суперцикл ***
do ' какой-то код if button_flag = 1 then ' реакция на нажатие кнопки button_flag = 0 ' не забудем сбросить флаг end if ' еще какой-то код
loop
end ' *** обработчик прерывания кнопки ***
button_isr: button_flag = 1
return

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

На помощь приходит операционная система (ОС). Как же решить проблему «макаронного кода» и сделать его более читаемым и управляемым? В ней функционал, который должен реализовать МК, поделен на задачи, работой которых управляет ОС.

Виды операционных систем для МК

Операционные системы для МК можно поделить на два больших класса: ОС с вытеснением и кооперативные ОС. В любой из таких ОС задачами управляет специальная процедура, называемая диспетчер. В ОС с вытеснением диспетчер самостоятельно в произвольный момент переключает выполнение с одной задачи на другую, выделяя каждой какое-то количество тактов машинного времени (возможно разное, смотря по приоритету задачи). Такой подход в целом работает замечательно, позволяя вообще не оглядываться на содержание задач: в коде задачи можно написать хоть

1:
goto 1

– и все равно остальные задачи (включая и эту) будут выполняться. Однако вытесняющие ОС требуют много ресурсов (памяти и тактов процессора), поскольку при каждом переключении должны полностью сохранить контекст отключаемой задачи и загрузить контекст возобновляемой. Под контекстом здесь понимается содержимое машинных регистров и стека (BASCOM использует два стека – аппаратный для адресов возврата подпрограмм и программный – для передачи аргументов). Мало того, что такая загрузка требует множества тактов процессора, так еще и контекст каждой задачи нужно где-то хранить на то время, пока она не работает. В «больших» процессорах, изначально ориентированных на многозадачность, эти функции часто поддерживаются аппаратно, да и ресурсов у них гораздо больше. В МК AVR нет аппаратной поддержки многозадачности (все нужно делать «вручную»), а доступная память мала. Поэтому вытесняющие ОС, хотя и существуют, не слишком подходят для простых МК.

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

Поскольку здесь программист сам задает момент переключения, оно не может произойти внезапно, например, во время работы задачи с каким-нибудь ресурсом или посреди вычисления арифметического выражения. Однако у кооперативной ОС есть важное преимущество. Это существенно экономит процессорное время и память, а потому выглядит куда более подходящим для реализации на МК AVR. Поэтому в кооперативной ОС в большинстве случаев можно обойтись без сохранения контекста.

Переключение задач в BASCOM AVR

Чтобы реализовать переключение задач в среде BASCOM AVR, код задачи, каждая из которых реализована как обычная процедура, должен в каком-то месте вызывать диспетчер – тоже реализованный как обычная процедура.
Представим, что у нас есть две задачи, каждая из которых в каком-то месте своего кода вызывает диспетчер.

sub task1() do 'код Задачи 1 'вызов диспетчера loop
end sub ' ----------------------------------
sub task2() do 'код Задачи 2 'вызов диспетчера loop
end sub

Допустим, выполнялась Задача 1. Давайте поглядим, что окажется в стеке, когда она выполнит «вызов диспетчера»:

адрес возврата к основному коду (2 байта)
вершина стека –> адрес возврата к Задаче 1, вызвавшей диспетчер (2 байта)

Вершина стека будет указывать на адрес инструкции в Задаче 1, которая следует за вызовом диспетчера (инструкция loop в нашем примере).

Вопрос – как это сделать? Цель диспетчера в простейшем случае – передать управление Задаче 2. (предположим, диспетчеру уже известен адрес Задачи 2).

Процессор извлечет из стека помещенный туда адрес и, вместо возврата к Задаче 1, перейдет на выполнение Задачи 2. Для этого диспетчер должен вытащить из стека (и где-то запомнить) адрес возврата к Задаче 1, и поместить на это место в стек адрес Задачи 2, после чего дать команду return.

Дадим команду return – и окажемся в точке продолжения Задачи 1. В свою очередь, когда Задача 2 вызовет диспетчер, мы так же вытащим из стека и сохраним адрес, по которому можно будет вернуться к Задаче 2, и загрузим в стек ранее сохраненный адрес Задачи 1.

В итоге у нас получится такая чехарда:

Задача 1 –> Диспетчер –> Задача 2 –> Диспетчер –> Задача 1 ….

Неплохо! И это работает. Но, конечно, для сколь-нибудь пригодной к практическому использованию ОС этого мало. Ведь не всегда и не все задачи должны работать – например, они могут чего-то ожидать (истечения времени задержки, появления какого-нибудь сигнала и т.п.). Значит, у задач должен быть статус (РАБОТАЕТ, ГОТОВА, ОЖИДАЕТ и т.п). Кроме того, было бы неплохо, чтобы задачам назначался приоритет. Тогда, если более одной задачи готовы к выполнению, диспетчер продолжит ту задачу, которая имеет наибольший приоритет.

AQUA RTOS

Для реализации описанной идеи была разработана кооперативная ОС AQUA RTOS, предоставляющая задачам необходимые сервисы и позволяющая реализовать кооперативную многозадачность в среде BASCOM AVR.

Важное замечание касательно режима процедур в BASCOM AVR

Это регулируется опцией config submode = new | old.
В случае задания опции old компилятор, во-первых, будет компилировать весь код линейно, вне зависимости от того, используется он где-то или нет, во-вторых, процедуры без аргументов, оформленные в стиле sub name / end sub будет воспринимать как процедуры, оформленные в стиле name: / return. Перед тем, как начать описание AUQA RTOS, следует заметить, что среда BASCOM AVR поддерживает два типа адресации процедур. Это касается и процедур, оформленных в стиле в стиле sub name / end sub (в качестве метки нужно передать имя процедуры).
В то же время, режим submode = old налагает некоторые ограничения: процедуры задач не должны содержать аргументов; код файлов, подключенных через $include, включается в общий проект линейно, поэтому в подключенных файлах следует предусмотреть байпас – переход от начала к концу с помощью goto и метки.
Таким образом, в AQUA RTOS пользователь должен либо использовать для задач только старую нотацию процедур в стиле task_name: / return, либо использовать более общепринятое sub name / end sub, добавив в начало своего кода модификатор config submode = old, а во включаемые файлы – байпас goto метка / код включаемого файла / метка:.
Это позволяет нам передавать адрес процедуры как метку в качестве аргумента другой процедуре путем использования модификатора bylabel.

Статусы задач AQUA RTOS

Для задач в AQUA RTOS определены следующие статусы:

OSTS_UNDEFINE OSTS_READY OSTS_RUN OSTS_DELAY
OSTS_STOP
OSTS_WAIT OSTS_PAUSE OSTS_RESTART

Если задача еще не инициализирована, ей присвоен статус OSTS_UNDEFINE.
После инициализации задача имеет статус OSTS_STOP.
Если задача готова к исполнению, ей присваивается статус OSTS_READY.
Выполняющаяся в данный момент задача имеет статус OSTS_RUN.
Из него она может перейти в статусы OSTS_STOP, OSTS_READY, OSTS_DELAY, OSTS_WAIT, OSTS_PAUSE.
Статус OSTS_DELAY имеет задача, отрабатывающая задержку.
Статус OSTS_WAIT присваивается задачам, которые ожидают семафора, события или сообщения (подробнее о них ниже).

с самого начала. В чем различие статусов OSTS_STOP и OSTS_PAUSED?
Если по какой-то причине задача получает статус OSTS_STOP, то последующее возобновление работы задачи (при получении статуса OSTS_READY) будет осуществляться с точки ее входа, т.е. Из статуса OSTS_PAUSE задача продолжит работу в том месте, где была приостановлена.

Управление статусом задач

Управлять задачами может как сама ОС – автоматически, так и пользователь, путем вызова сервисов ОС. Сервисов управления задачами несколько (имена всех сервисов ОС начинаются с префикса OS_):

OS_InitTask(task_label, task_prio) OS_Stop() OS_StopTask(task_label) OS_Pause()
OS_PauseTask(task_label)
OS_Resume() OS_ResumeTask(task_label)
OS_Restart()

Каждый из них имеет два варианта: OS_сервис и OS_сервисTask (кроме сервиса OS_InitTask, который имеет только один вариант; сервис OS_Init инициализирует саму ОС).

Первый метод действует на саму вызывавшую его задачу; второй позволяет задать в качестве аргумента указатель на другую задачу и, таким образом, из одной задачи управлять иной. В чем разница между OS_сервис и OS_сервисTask ?

Про OS_Resume

В отличие от них, сервисы OS_Resume* только устанавливают задаче статус OSTS_READY. Все сервисы управления задачами, кроме OS_Resume и OS_ResumeTask, после отработки автоматически вызывают диспетчер задач. Этот статус будет обработан только при явном вызове диспетчера.

Приоритет и очередь задач

Как уже сказано выше, в реальной системе некоторые задачи могут оказаться более важными, а другие – второстепенными. Поэтому полезным свойством ОС является возможность назначить задачам приоритет. В этом случае, при наличии нескольких одновременно готовых задач, ОС сначала выберет задачу с наибольшим приоритетом. Если же все готовые задачи имеют равный приоритет, ОС будет ставить их на исполнение по кругу, в порядке, называемом «карусель» или round-robin.

Меньшее число означает больший приоритет. В AQUA RTOS приоритет назначается задаче при ее инициализации через вызов сервиса OS_InitTask, которому в качестве первого аргумента передается адрес задачи, а в качестве второго – число от 1 до 15. В ходе работы ОС изменение назначенного задаче приоритета не предусмотрено.

Задержки

В каждой задаче задержка отрабатывается независимо от других задач.
Таким образом, пока ОС отрабатывает задержку одной задачи, другие могут выполняться.
Для организации задержек предусмотрены сервисы OS_Delay | OS_DelayTask. В качестве аргумента передается число миллисекунд, на которое откладывается выполнение задачи. Поскольку размерность аргумента – dword, максимальная величина задержки составляет 4294967295 мс – или около 120 часов, что представляется вполне достаточным для большинства приложений. После вызова сервиса задержки автоматически вызывается диспетчер, который на время, пока будет отрабатываться задержка, передает управление другим задачам.

Семафоры

Семафоры в AQUA RTOS – это что-то вроде флагов и переменных, доступных задачам. Они бывают двух типов – бинарные и счетные. Первые имеют только два состояния: свободен и закрыт. Вторые представляют собой байтовый счетчик (сервис счетных семафоров в текущей версии AQUA RTOS не реализован (я ленивая задница), поэтому все сказанное ниже относится только к бинарным семафорам).

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

Как только задаче велено ждать семафор, управление автоматически передается диспетчеру, и он может запускать другие задачи – ровно до того момента, как указанный семафор освободится. При этом вся черная работа возлагается на диспетчер. внутри они реализованы как битовые флаги).
Бинарные семафоры работают через следующие сервисы: Как только состояние семафора изменится на свободен, диспетчер назначит всем ожидавшим этот семафор задачам статус готова (OSTS_READY), и они будут исполнены в порядке очереди и приоритета.
Всего в AQUA RTOS предусмотрено 16 двоичных семафоров (это число в принципе может быть увеличено путем изменения размерности переменной в блоке управления задач, т.к.

hBSem OS_CreateBSemaphore() OS_WaitBSemaphore(hBSem) OS_WaitBSemaphoreTask(task_label, hBSem)
OS_BusyBSemaphore(hBSem)
OS_FreeBSemaphore(hBSem)

Перед использованием семафор нужно создать. Это делается вызовом сервиса OS_CreateBSemaphore, который возвращает уникальный байтовый идентификатор (хэндл) созданного семафора hBSem, либо через пользовательский обработчик выдает ошибку OSERR_BSEM_MAX_REACHED, говорящую о том, что достигнуто максимально возможное число бинарных семафоров.

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

Если семафор свободен, передача управления не происходит, и задача просто продолжит выполнение. Сервис OS_WaitBSemaphore | OS_WaitBSemaphoreTask переводит (текущую | указанную) задачу в состояние ждать освобождения семафора hBSem, если этот семафор занят, а затем передает управление диспетчеру, чтобы он мог запускать другие задача.

Сервисы OS_BusyBSemaphore и OS_FreeBSemaphore устанавливают семафор hBSem в состояние занят или свободен соответственно.

Таким образом, все созданные семафоры статичны. Уничтожение семафоров в целях упрощения ОС и уменьшения объема кода не предусмотрено.

События

Помимо семафоров, задачи могут управляться событиями. Одной задаче можно указать ожидать некоторое событие, а другая задача (а также фоновый код) может сигналить об этом событии. При этом все задачи, которые ожидали данное событие, получат статус готова к исполнению (OSTS_READY) и будут поставлены диспетчером на исполнение в порядке очереди и приоритета.

Ну, например:
На какие события может реагировать задача?

  • прерывание;
  • возникновение ошибки;
  • освобождение ресурса (иногда для этого удобнее использовать семафор);
  • изменение состояния линии ввода-вывода или нажатие клавиши на клавиатуре;
  • прием или посылка символа по RS-232;
  • передача информации от одной части приложения к другой (см. тж. сообщения).

Система событий реализована через следующие сервисы:

hEvent OS_CreateEvent()
OS_WaitEvent(hEvent)
OS_WaitEventTask(task_label, hEvent)
OS_WaitEventTO(hEvent, dwTimeout)
OS_SignalEvent(hEvent)

Перед использованием события его нужно создать. Это делается вызовом функции OS_CreateEvent, которая возвращает уникальный байтовый идентификатор (хэндл) события hEvent, либо через пользовательский обработчик выдает ошибку OSERR_EVENT_MAX_REACHED, показывающую, что достигнут предел числа событий, какое может быть создано в ОС (максимум 255 разных событий).

После вызова этого сервиса управление будет автоматически передано диспетчеру. Чтобы заставить задачу ожидать событие hEvent, в ее коде следует вызвать OS_WaitEvent, передав хэндл события в качестве аргумента.

Для этого служит сервис OS_WaitEventTO. В отличие от сервиса семафоров, в сервисе событий предусмотрен вариант ожидания события с таймаутом. Если указанное время истекло, задача получит статус готова к исполнению так, как будто событие произошло, и будет поставлена диспетчером на продолжение исполнения в порядке очереди и приоритета. Вторым аргументом тут можно указать число миллисекунд, которые задача может ожидать событие. Узнать о том, что произошло не событие, а таймаут, задача может, проверив глобальный флаг OS_TIMEOUT.

При этом всем задачам, ожидающим данное событие, ОС установит статус готова к исполнению, так что они смогут продолжить исполнение в порядке очереди и приоритета. Сигналить о наступлении заданного события задача или фоновый код могут путем вызова сервиса OS_SignalEvent, которому в качестве аргумента передается хэндл события.

Сообщения

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

hTopic OS_CreateMessage()
OS_WaitMessage(hTopic)
OS_WaitMessageTask(task_label, hTopic)
OS_WaitMessageTO(hTopic, dwTimeout) OS_SendMessage(hTopic, wMessage)
word_ptr OS_GetMessage(hTopic) word_ptr OS_PeekMessage(hTopic) string OS_GetMessageString(hTopic) string OS_PeekMessageString(hTopic)

Чтобы воспользоваться сервисом сообщений, нужно сначала создать тему сообщения. Это делается через сервис OS_CreateMessage, который возвращает байтовый хэндл темы hTopic, либо через пользовательский обработчик выдает ошибку OSERR_TOPIC_MAX_REACHED, говорящую о том, что достигнуто максимально возможное число тем сообщений, и больше создать не получится.

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

Сервис ожидания с таймаутом OS_WaitMessageTO работает аналогично сервису OS_WaitEventTO системы событий.

Первым аргументом служит хэндл темы, на которую будет передано сообщение, а вторым – аргумент размерности word. Для посылки сообщений предусмотрен сервис OS_SendMessage. Это может быть как самостоятельное число, так и указатель на строку, которая, в свою очередь, уже и является сообщением.

Чтобы получить указатель строки, достаточно воспользоваться встроенной в BASCOM функцией varptr, например, так:

strMessage = "Hello, world!"
OS_SendMessage hTopic, varptr (strMessage)

Возобновив работу после вызова OS_WaitMessage, то есть, когда получено ожидаемое сообщение, задача может либо получить сообщение с его последующим автоматическим уничтожением, либо только просмотреть сообщение — в этом случае оно не уничтожится. Для этого служат последние четыре сервиса в списке. Первые два возвращают число размерности word, которое может быть либо самостоятельным сообщением, либо служить указателем строки, которая содержит сообщение. При этом OS_GetMessage автоматически удаляет сообщение, а OS_PeekMessage оставляет его.

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

Внутренняя таймерная служба

Для работы с задержками и отсчета времени AQUA RTOS использует встроенный в МК аппаратный таймер TIMER0. Таким образом, внешний код (фоновый и задач) не должен использовать этот таймер. Но обычно это и не требуется, т.к. ОС снабжает задачи всеми необходимыми средствами работы с временными интервалами. Разрешение таймера составляет 1 мс.

Примеры работы с AQUA RTOS

Начальные настройки

В самом начале пользовательского кода нужно определить, будет ли код исполняться во встроенном симуляторе или на реальном железе. Определите константу OS_SIM = TRUE | FALSE, которая задает режим симуляции.

Чем это число меньше, тем быстрее работает ОС (меньше накладные расходы), и тем меньше памяти она потребляет. Кроме того, в коде ОС отредактируйте константу OS_MAX_TASK, которая определяет максимальное число поддерживаемых ОС задач. Не забывайте изменить эту константу, если число задач изменилось. Поэтому не стоит указывать там большее число задач, чем вам потребуется.

Инициализация ОС

Перед началом работы AQUA RTOS должна быть инициализирована. Для этого нужно вызвать сервис OS_Init. Этот сервис настраивает начальные параметры ОС. Что еще более важно, у него есть аргумент – адрес пользовательской процедуры обработки ошибок. У нее, в свою очередь, тоже есть аргумент – код ошибки.

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

Итак, первым этапом работы с AQUA RTOS нужно добавить в пользовательскую программу код инициализации ОС и процедуру обработчика ошибок:

OS_Init my_err_trap '... '... '... sub my_err_trap(err_code as byte) print err_code end sub

Инициализация задач

Вторым этапом нужно инициализировать задачи, указав их имена и приоритет:

OS_InitTask task_1, 1
OS_InitTask task_2 , 1 '...
OS_InitTask task_N , 1

Тестовые задачи

Помигаем светодиодами

Итак, давайте создадим тестовое приложение, которое можно загрузить в стандартную плату Arduino Nano V3. Создайте в папке с файлом ОС какую-нибудь папку (например, test), и там создайте следующий bas-файл:

' начальные установки компилятора
config submode = old $include "..\aquaRTOS_1.05.bas" $regfile = "m328pdef.dat"
$crystal = 16000000
$hwstack = 48
$swstack = 48
$framesize = 64 ' объявление процедур
declare sub my_err_trap (byval err_code as byte)
declare sub task_1()
declare sub task_2() ' назначим светодиодам порты и режим работы
led_1 alias portd.4
led_2 alias portd.5
config portd.4 = output
config portd.5 = output ' *** начало кода приложения *** ' инициализируем ОС
OS_Init my_err_trap ' инициализируем задачи
OS_InitTask task_1, 1
OS_InitTask task_2 , 1 ' изначально все задачи имеют статус «остановлена» (OSTS_STOP) ' чтобы задачи начали работать, им нужно задать статус ' «готова к выполнению» (OSTS_READY) вызовом сервиса OS_ResumeTask
OS_ResumeTask task_1
OS_ResumeTask task_2 ' осталось запустить ОС вызовом диспетчера
OS_Sheduler
end ' *** задачи ***
sub task_1 () do toogle led_1 ' переключим светодиод 1 OS_delay 1000 ' приостановить на 1000 мс loop
end sub sub task_2 () do toogle led_2 ' переключим светодиод 2 OS_delay 333 ' приостановить на 333 мс loop
end sub ' **************************************************** ' обработчик ошибок
sub my_err_trap(err_code as byte) print "OS Error: "; err_code end sub

Подключите аноды светодиодов к выводам D4 и D5 платы Arduino (или к другим выводам, изменив соответствующие строки-определения в коде). Катоды через ограничительные резисторы 100...500 Ом подсоедините к шине GND. Скомпилируйте и залейте прошивку в плату. Светодиоды начнут асинхронно переключаться с периодом 2 и 0,66 с.

Итак, сначала мы инициализируем оборудование (задаем опции компилятора, режим работы портов и назначаем aliases), затем – саму ОС, и наконец – задачи. Давайте рассмотрим код.

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

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

Сразу бросается в глаза, что каждая из них оформлена как бесконечный цикл do – loop. Давайте посмотрим на задачи. В нашем случае это сервис задержки OS_Delay. Второе важное свойство – внутри такого цикла обязательно должен быть хотя бы один вызов либо диспетчера, либо сервиса ОС, автоматически вызывающего диспетчер после себя – иначе такая задача никогда не отдаст управление, и другие задачи не смогут выполняться. В качестве аргумента мы указали ему число миллисекнуд, на которые следует приостановить выполнение каждой задачи.

Если в начале кода задать константу OS_SIM = TRUE и запустить код не на реальном чипе, а в симуляторе, то можно проследить, как работает ОС.

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

Выбрав задачу, которую следует исполнить (допустим, task_1), диспетчер подменяет в стеке адрес возврата (первоначально он показывает на инструкцию end в основном коде) адресом точки входа задачи task_1, которую система узнает в процессе инициализации задачи, и исполняет команду return, которая заставляет МК вытащить из стека адрес возврата и перейти к нему – то есть, начать исполнение задачи task_1 (оператор do в коде task_1).

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

инструкцию loop), а затем, «повернув карусель», обнаруживает, что теперь надо выполнить задачу task_2. Диспетчер сохраняет адрес, который был в стеке, в блок управления задачи task_1 (указывает на инструкцию, следующую за вызовом OS_delay, т.е. Он помещает в стек адрес задачи task_2 (указывает в данный момент на инструкцию do в коде задачи task_2) и исполняет команду return, которая заставляет МК вытащить из стека адрес возврата и перейти к нему – то есть, начать исполнение задачи task_2.

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

инструкцию loop), а затем, «повернув карусель», обнаруживает, что теперь надо выполнить задачу task_2. Диспетчер сохраняет адрес, который был в стеке, в блок управления задачи task_1 (указывает на инструкцию, следующую за вызовом OS_delay, т.е. Туда (на инструкцию loop в коде задачи task_1), и будет передано управление. Отличие от первоначального состояния будет в том, что теперь в блоке управления задачей task_1 хранится не стартовый адрес задачи, а адрес точки, с которой произошел переход к диспетчеру.

Задача task_1 выполнит инструкцию loop, и далее весь цикл «Задача 1 – Диспетчер – Задача 2 – Диспетчер» будет повторяться бесконечно.

Посылаем сообщения

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

' начальные установки компилятора
config submode = old $include "..\aquaRTOS_1.05.bas" $regfile = "m328pdef.dat"
$crystal = 16000000
$hwstack = 48
$swstack = 48
$framesize = 64 ' объявление процедур
declare sub my_err_trap (byval err_code as byte)
declare sub task_1()
declare sub task_2() const OS_SIM = TURE ' будем запускать этот код в симуляторе ' объявление переменных
dim hTopic as byte ' переменная для темы сообщений
dim task_1_cnt as byte ' счетчик для задачи 1
dim strMessage as string * 16 ' сообщение ' *** начало кода приложения ***
OS_CreateMessage hTopic OS_Init my_err_trap OS_InitTask task_1 , 1
OS_InitTask task_2 , 1 OS_ResumeTask task_1
OS_ResumeTask task_2 OS_Sheduler
end ' *** задачи *** sub task_1() do print "task 1" OS_Sheduler incr task_1_cnt ' увеличим счетчик на 1 if task_1_cnt > 3 then print "task 1 is sending message to task 2" strMessage = "Hello, task 2!" ' посылаем сообщение задаче 2 OS_SendMessage hTopic , varptr(strMessage) task_1_cnt = 0 end if loop
end sub sub task_2() do print "task 2 is waiting messages..." ' будем ждать сообщений от задачи 1 OS_WaitMessage hTopic print "message recieved: " ; OS_GetMessageString (hTopic) loop
end sub ' **************************************************** ' обработчик ошибок
sub my_err_trap(err_code as byte) print "OS Error: "; err_code end sub

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

task 1
task 2 is waiting messages…
task 1
task 1
task 1
task 1 is sending message to task 2
task 1
message recieved: Hello, task 2!
task 2 is waiting messages…
task 1
task 1

Обратите внимание, в каком порядке происходит работа и переключение задач. Как только Задача 1 напечатает task 1, управление передается диспетчеру для того, чтобы он мог запустить вторую задачу. Задача 2 печатает task 2 is waiting messages..., затем вызывает сервис ожидания сообщений на тему hTopic, и управление автоматически передается диспетчеру, который снова вызывает Задачу 1. Та снова печатает task 1 и отдает управление в диспетчер. Однако, поскольку диспетчер обнаруживает, что Задача 2 теперь ожидает сообщений, он возвращает управление Задаче 1 на инструкцию incr, следующую за вызовом диспетчера.
Когда счетчик task_1_cnt в Задаче 1 превысит указанное значение, задача посылает сообщение, но продолжает исполняться – выполняет инструкцию loop и снова печатает task 1. После этого она вызывает диспетчер, который теперь обнаруживает, что для Задачи 2 имеется сообщение, и передает управление ей. Далее процесс выполняется циклически.

Обработка событий

Следующий код опрашивает две кнопки и переключает светодиоды при нажатии на соответствующую кнопку:

' начальные установки компилятора
config submode = old
$include "..\aquaRTOS_1.05.bas" $regfile = "m328pdef.dat"
$crystal = 16000000
$hwstack = 48
$swstack = 48
$framesize = 64 ' объявление процедур
declare sub my_err_trap (byval err_code as byte)
declare sub task_scankeys()
declare sub task_led_1()
declare sub task_led_2() ' зададим светодиодам порты и режим их работы
led_1 alias portd.4
led_2 alias portd.5
config portd.4 = output
config portd.5 = output
button_1 alias pind.6
button_2 alias pind.7
config portd.6 = input
config portd.7 = input ' объявление переменных
dim eventButton_1 as byte
dim eventButton_2 as byte ' *** начало кода приложения ***
eventButton_1 = OS_CreateEvent ' создадим по событию на каждую кнопку
eventButton_2 = OS_CreateEvent OS_Init my_err_trap OS_InitTask task_scankeys , 1
OS_InitTask task_led_1 , 1
OS_InitTask task_led_2 , 1 OS_ResumeTask task_scankeys
OS_ResumeTask task_led_1
OS_ResumeTask task_led_2 OS_Sheduler
end ' *** задачи *** sub task_scankeys() do debounce button_1 , 0 , btn_1_click , sub debounce button_2 , 0 , btn_2_click , sub OS_Sheduler loop btn_1_click: OS_SignalEvent eventButton_1
return btn_2_click: OS_SignalEvent eventButton_2 return
end sub sub task_led_1() do OS_WaitEvent eventButton_1 toggle led_1 loop
end sub sub task_led_2() do OS_WaitEvent eventButton_2 toggle led_2 loop
end sub ' **************************************************** ' обработчик ошибок
sub my_err_trap(err_code as byte) print "OS Error: "; err_code
end sub

Пример реального приложения под AQUA RTOS

Автомат должен показывать светодиодами в кнопках наличие вариантов кофе и выбор; принимать сигналы от приемника монет, готовить заказанный напиток, выдавать сдачу. Давайте попробуем представить, как бы могла выглядеть программа автомата по продаже кофе. Кроме того, автомат должен управлять внутренним оборудованием: например, поддерживать температуру водонагревателя на уровне 95…97°С; передавать данные о неисправностях оборудования и запасе ингредиентов и принимать команды через удаленный доступ (например, посредством GSM-модема), а также сигнализировать об актах вандализма.

Управляемый событиями подход

Поначалу разработчику бывает нелегко перейти от привычной схемы «суперцикл + флаги + прерывания» к подходу, основанному на задачах и событиях. Это требует выделения основных задач, которые должно выполнять устройство.
Давайте попробуем наметить такие задачи для нашего автомата:

  • контроль и управление нагревателем – ControlHeater()
  • индикация наличия и выбора напитков – ShowGoods()
  • прием монет/купюр и их суммирование – AcceptMoney()
  • опрос кнопок – ScanKeys()
  • выдача сдачи – MakeChange()
  • отпуск напитка – ReleaseCoffee()
  • защита от вандализма – Alarm()

Прикинем степень важности задач и частоту их вызова.
ControlHeater() очевидно важна, поскольку для приготовления кофе нам постоянно нужен кипяток. Но она не должна выполняться слишком часто, потому что нагреватель обладает большой инерционностью, а вода остывает медленно. Достаточно проверять температуру раз в минуту. Дадим этой задаче приоритет 5.
ShowGoods() не слишком важна. Предложение может измениться только после отпуска товара, если запас каких-то ингредиентов окажется исчерпан. Поэтому дадим этой задаче приоритет 8, и пусть она выполняется при пуске автомата и каждый раз при отпуске товара.
ScanKeys() должна иметь достаточно высокий приоритет, чтобы автомат быстро реагировал на нажатие кнопок. Дадим этой задаче приоритет 3, и будем выполнять ее каждые 40 миллисекунд.
AcceptMoney() также является частью интерфейса пользователя. Мы дадим ей тот же приоритет, что и ScanKeys(), и будем выполнять каждые 20 миллисекунд.
MakeChange () выполняется только после отпуска товара. Мы свяжем ее с ReleaseCoffee() и дадим приоритет 10.
ReleaseCoffee() нужна только тогда, когда было принято соответствующее количество денег и нажата кнопка выбора напитка. Для быстроты ответа дадим ей приоритет 2.
Поскольку вандалоустойчивость – довольно важная функция автомата, задаче Alarm() можно поставить самый высокий приоритет – 1, и активировать раз в секунду, чтобы проверить датчики наклона или вскрытия.

После старта, когда программа считала настройки из EEPROM и инициализировала оборудование, наступает время инициализировать ОС и запустить задачи. Таким образом, нам понадобится семь задач с различными приоритетами.

' объявление процедур задач
declare sub ControlHeater()
declare sub ShowGoods() declare sub AcceptMoney()
declare sub ScanKeys()
declare sub MakeChange ()
declare sub ReleaseCoffee()
declare sub Alarm()

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

Например, ReleaseCoffee() может выглядеть примерно так:

sub ReleaseCoffee() do OS_WaitMessage bCoffeeSelection wItem = OS_GetMessage(bCoffeeSelection) Release wItem loop end sub

Задача ReleaseCoffee в бесконечном цикле ожидает сообщение на тему bCoffeeSelection и не делает ничего, пока оно не поступит (управление автоматически возвращается диспетчеру, чтобы он мог запускать другие задачи). Как только сообщение послано, ReleaseCoffee() становится готовой к выполнению, и когда это случается, задача получает содержимое сообщения (код выбранного напитка) wItem с помощью сервиса OS_GetMessage и отпускает товар заказчику. Поскольку ReleaseCoffee() использует подсистему сообщений, сообщение должно быть создано перед запуском многозадачности:

dim bCoffeeSelection as byte
bCoffeeSelection = OS_CreateMessage()

Как было сказано выше, ShowGoods() должна выполняться один раз при пуске и каждый раз при отпуске товара. Чтобы связать ее с процедурой отпуска ReleaseCoffee(), используем сервис событий. Для этого создадим событие перед запуском многозадачности:

dim bGoodsReliased as byte
bGoodsReliased = OS_CreateEvent()

А в процедуру ReleaseCoffee() после строчки Release wItem добавим сигнализацию о событии bGoodsReliased:

OS_SignalEvent bGoodsReliased

Инициализация ОС

Для того, чтобы подготовить ОС к работе, мы должны инициализировать ее, указав при этом адрес обработчика ошибок, который находится в пользовательском коде. Сделаем это при помощи сервиса OS_Init:

OS_Init Mailfuncion

В пользовательском коде нужно добавить обработчик – процедуру, байтовым аргументом которой будет код ошибки:

sub Mailfuncion (bCoffeeErr) print "Mailfunction! Error #: "; bCoffeeErr if isErrCritical (bCoffeeErr) = 1 then CallService(bCoffeeErr) end if
end sub

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

Запуск задач

Мы уже помним, что события, семафоры и т.п. должны быть инициализированы до того, как будут использоваться. Кроме того, сами задачи перед запуском должны быть инициализированы с помощью сервиса OS_InitTask:

OS_InitTask ControlHeater , 5
OS_InitTask ShowGoods , 8 OS_InitTask AcceptMoney , 3
OS_InitTask ScanKeys , 3
OS_InitTask MakeChange, 10
OS_InitTask ReleaseCoffee , 2
OS_InitTask Alarm , 1

Поскольку многозадачный режим еще не начался, порядок, в котором задачи стартуют, несущественен, и в любом случае не зависит от их приоритетов. В этом месте все задачи еще находятся в остановленном состоянии. Чтобы подготовить их к выполнению, мы должны с помощью сервиса OS_ResumeTask задать им статус «готова к выполнению»:

OS_ResumeTask ControlHeater OS_ResumeTask ShowGoods
OS_ResumeTask AcceptMoney OS_ResumeTask ScanKeys
OS_ResumeTask MakeChange
OS_ResumeTask ReleaseCoffee
OS_ResumeTask Alarm

Как уже говорились, не все задачи обязательно должны стартовать при запуске многозадачности; некоторые из них могут произвольное время пребывать в состоянии «остановлена» и получать готовность лишь при определенных условиях. Сервис OS_ResumeTask может быть вызван в любое время из любого места кода (фонового или задачи), когда многозадачность уже работает. Главное, чтобы задача, на которую он ссылается, была предварительно инициализирована.

Пуск многозадачности

Теперь все готово для того, чтобы запустить многозадачность. Сделаем это путем вызова диспетчера:

OS_Sheduler

После этого мы смело можем поставить в коде программы end – управление дальнейшим исполнением кода теперь берет на себя ОС.

Давайте посмотрим на код целиком:

' начальные установки компилятора
config submode = old
$include "..\aquaRTOS_1.05.bas"
$include "coffee_hardware.bas" ' файл с процедурами управления оборудованием ' процедуры в этом файле имеют префикс Coffee_
$regfile = "m328pdef.dat" ' Arduino Nano v3
$crystal = 16000000
$hwstack = 48
$swstack = 48
$framesize = 64 Coffee_InitHardware ' инициализация оборудования автомата ' объявление процедур
declare sub Mailfuncion (byval bCoffeeErr as byte) ' обработчик ошибок
declare sub ControlHeater () ' управление водонагревателем
declare sub ShowGoods () ' показать наличие товара
declare sub AcceptMoney () ' прием наличных
declare sub ScanKeys () ' опрос кнопок
declare sub MakeChange () ' выдача сдачи после отпуска товара
declare sub ReleaseCoffee () ' отпуск товара
declare sub Alarm () ' обеспечение безопасности автомата ' проведем начальные настройки оборудования
Coffee_InitHardware () ' объявление переменных
dim wMoney as long ' счетчик введенных денег
dim wGoods as long ' номер товара ' *** начало кода приложения *** ' инициализация ОС
OS_Init Mailfuncion ' создадим тему сообщения о выборе напитка
dim bCoffeeSelection as byte
bCoffeeSelection = OS_CreateMessage() ' создадим событие отпуска товара
dim bGoodsReliased as byte
bGoodsReliased = OS_CreateEvent() ' инициализация задач
OS_InitTask ControlHeater , 5
OS_InitTask ShowGoods , 8 OS_InitTask AcceptMoney , 3
OS_InitTask ScanKeys , 3
OS_InitTask MakeChange, 10
OS_InitTask ReleaseCoffee , 2
OS_InitTask Alarm , 1 ' подготовка задач к выполнению
OS_ResumeTask ControlHeater OS_ResumeTask ShowGoods
OS_ResumeTask AcceptMoney OS_ResumeTask ScanKeys
OS_ResumeTask MakeChange
OS_ResumeTask ReleaseCoffee
OS_ResumeTask Alarm ' запуск ОС
OS_Sheduler end ' *** код задач *** ' -----------------------------------
sub ControlHeater() do select case GetWaterTemp() case is > 97 Coffee_HeaterOff ' выключить нагреватель case is < 95 Coffee_HeaterOn ' включить нагреватель case is < 5 CallServce (WARNING_WATER_FROZEN) ' угроза замерзания end select OS_Delay 60000 ' ждать 1 минуту loop
end sub ' -----------------------------------
sub ShowGoods() do LEDS = Coffee_GetDrinkSupplies() ' установить состояние порта D, ' к которому подключены светодиоды индикации наличия товаров и ' ассоциирована переменная LEDS OS_WaitEvent bGoodsReliased ' ожидать события "отпуск товара" loop
end sub ' -----------------------------------
sub AcceptMoney() do wMoney = wMoney + ReadMoneyAcceptor() OS_Delay 20 loop
end sub ' -----------------------------------
sub ScanKeys() do wGoods = ButtonPressed() if wMoney >= GostOf(wGoods) then OS_SendMessage bCoffeeSelection, wGoods ' отправляет сообщение на тему bCoffeeSelection, которое ' содержит код выбранного товара end if OS_Delay 40 loop
end sub ' -----------------------------------
sub MakeChange() do OS_WaitEvent bGoodsReliased ' ожидать события "отпуск товара" Refund wMoney loop
end sub ' -----------------------------------
sub ReleaseCoffee() do OS_WaitMessage bCoffeeSelection 'ждать сообщения bCoffeeSelection wItem = OS_GetMessage(bCoffeeSelection) ' прочесть сообщение Release wItem ' отпустить выбранный товар wMoney = wMoney – CostOf (wItem) ' уменьшить на цену товара OS_SignalEvent bGoodsReliased ' просигналить об этом задачам ' обратите внимание, что это событие могут ждать две задачи: ' MakeChange и ShowGoods ' обе они, получив сообщение, становятся готовыми к исполнению loop
end sub ' -----------------------------------
sub Alarm() do OS_Delay 1000 if Hijack() = 1 then CallPolice() end if loop
end sub ' ----------------------------------- ' *** обработчик ошибок ОС ***
sub Mailfuncion (bCoffeeErr) print "Mailfunction! Error #: "; bCoffeeErr if isErrCritical (bCoffeeErr) = 1 then CallService() end if
end sub

Конечно, более правильным был бы подход не с периодическим опросом кнопок и датчика наличных, а с использованием прерываний. В обработчиках этих прерываний мы могли бы использовать посылку сообщений при помощи сервиса OS_SendMessage() с содержимым, равным номеру нажатой клавиши или номиналу введенной монеты/купюры. Предлагаю читателю доработать программу самостоятельно. Благодаря подходу, ориентированному на задачи, и сервису, предоставляемому ОС, это потребует минимальных изменений кода.

Исходный код AQUA RTOS

Исходный код версии 1.05 доступен для скачивания по ссылке

Постскриптум

Q: Почему AQUA?
A: Ну, я делала контроллер аквариума, это такая как «умный дом», только не людям, а для рыбок. Полно всяких датчиков, часы реального времени, релейные и аналоговые силовые выходы, экранное меню, гибкая «программа событий» и даже WiFi-модуль. Интервалы должны отсчитываться, кнопки опрашиваться, датчики обрабатываться, программа событий читаться из EEPROM и выполняться, экран обновляться, вай-фай отвечать. Да еще контроллер должен переходить в многоуровневое меню для настроек и программирования. Делать на флагах и прерываниях – это как раз получить тот самый «макаронный код», в котором ни разобраться, ни модифицировать. Поэтому я и решила, что мне нужна ОС. Вот она и AQUA.

Я, как могла, придумывала разнообразные тесты и гоняла ОС на самых разных задачках, и даже прихлопнула заметное число багов, но это не означает, что всех и полностью. Q: Наверняка в коде полно логических ошибок и глюков?
A: Наверняка. Поэтому буду очень благодарна, если вместо того, чтобы тыкать меня в баги мордой, вы вежливо и тактично укажете на них, а лучше и подскажете, как их, по-вашему, лучше исправить. Более чем уверена, что их еще немало затаилось в закоулках кода. Например, кто-нибудь допишет сервис счетных семафоров (не забыли? Будет также замечательно, если проект получит дальнейшее развитие как продукт коллективного творчества. В любом случае буду очень благодарна за конструктивный вклад. – я ленивая задница) и предложит другие улучшения.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Хабрахабр

[Из песочницы] ОС реального времени AQUA RTOS для МК AVR в среде BASCOM AVR

При написании для МК кода посложнее, чем «помигать лампочкой», разработчик сталкивается с ограничениями, присущими линейному программированию в стиле «суперцикл плюс прерывания». Обработка прерываний требует быстроты и лаконичности, что приводит к добавлению в код флагов и приведению проекта к стилю «суперцикл с прерываниями и флагами».

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

Однако все они написаны на языке Си или Ассемблер и не подходят тем, кто программирует МК в среде BASCOM AVR, лишая их столь полезного инструмента для написания серьезных приложений. Избавиться от «макаронного кода» и вернуть сложному проекту на МК гибкость и управляемость помогает использование операционных систем реального времени.
Для микроконтроллеров AVR разработаны и довольно популярны несколько кооперативных ОС реального времени.

Чтобы исправить этот недостаток, я разработала простую ОСРВ для среды программирования BASCOM AVR, которую и выношу на суд читателей.

суперцикл. image
Для многих привычный стиль программирования МК – т.н. При пуске сначала выполняется инициализация оборудования самого МК и внешних устройств, задаются константы и начальные значения переменных, а затем управление передается в этот бесконечный суперцикл.
Простота суперцикла очевидна. Код при этом состоит из набора функций, процедур и описателей (константы, переменные), возможно, библиотечных, в целом называемых «фоновым кодом», а также большого бесконечного цикла, заключенного в конструкцию типа do – loop. Недостатки тоже налицо: если какое-то устройство или сигнал требует немедленной реакции, МК обеспечит ее не раньше, чем обернется цикл. Большинство задач, выполняемых МК, ведь так или иначе цикличны. Если длительность сигнала окажется короче, чем период цикла, такой сигнал будет пропущен.

В приведенном ниже примере мы хотим проверить, нажата ли кнопка button:

do ' какой-то код if button = 1 then ' реакция на нажатие кнопки ' еще какой-то код
loop

Очевидно, что если «какой-то код» работает достаточно долго, МК может не заметить короткого нажатия кнопки.

Так появляется следующий уровень: суперцикл с прерываниями.
В примере ниже показана структура программы с суперциклом и прерыванием, обрабатывающим нажатие кнопки: К счастью, МК снабжен системой прерываний, которая позволяет решить эту проблему: все критичные сигналы можно «повесить» на прерывания и написать для каждого обработчик.

on button button_isr ' назначаем обработчик кнопки
enable interrupts ' *** суперцикл ***
do ' какой-то код
loop
end ' обработчик кнопки
button_isr: ' делаем что-то при нажатии кнопки
return

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

Запоминание прерываний

Однако все равно это прерывание не может быть обработано немедленно.
На самом деле, находясь внутри прерывания, МК способен отметить факт другого прерывания в специальном регистре, что позволяет обработать его позже.

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

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

Таким образом, возникает следующий уровень сложности – суперцикл с прерываниями и флагами.

Ниже показан подобный код:

on button button_isr ' назначаем обработчик кнопки
enable interrupts ' *** суперцикл ***
do ' какой-то код if button_flag = 1 then ' реакция на нажатие кнопки button_flag = 0 ' не забудем сбросить флаг end if ' еще какой-то код
loop
end ' *** обработчик прерывания кнопки ***
button_isr: button_flag = 1
return

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

На помощь приходит операционная система (ОС). Как же решить проблему «макаронного кода» и сделать его более читаемым и управляемым? В ней функционал, который должен реализовать МК, поделен на задачи, работой которых управляет ОС.

Виды операционных систем для МК

Операционные системы для МК можно поделить на два больших класса: ОС с вытеснением и кооперативные ОС. В любой из таких ОС задачами управляет специальная процедура, называемая диспетчер. В ОС с вытеснением диспетчер самостоятельно в произвольный момент переключает выполнение с одной задачи на другую, выделяя каждой какое-то количество тактов машинного времени (возможно разное, смотря по приоритету задачи). Такой подход в целом работает замечательно, позволяя вообще не оглядываться на содержание задач: в коде задачи можно написать хоть

1:
goto 1

– и все равно остальные задачи (включая и эту) будут выполняться. Однако вытесняющие ОС требуют много ресурсов (памяти и тактов процессора), поскольку при каждом переключении должны полностью сохранить контекст отключаемой задачи и загрузить контекст возобновляемой. Под контекстом здесь понимается содержимое машинных регистров и стека (BASCOM использует два стека – аппаратный для адресов возврата подпрограмм и программный – для передачи аргументов). Мало того, что такая загрузка требует множества тактов процессора, так еще и контекст каждой задачи нужно где-то хранить на то время, пока она не работает. В «больших» процессорах, изначально ориентированных на многозадачность, эти функции часто поддерживаются аппаратно, да и ресурсов у них гораздо больше. В МК AVR нет аппаратной поддержки многозадачности (все нужно делать «вручную»), а доступная память мала. Поэтому вытесняющие ОС, хотя и существуют, не слишком подходят для простых МК.

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

Поскольку здесь программист сам задает момент переключения, оно не может произойти внезапно, например, во время работы задачи с каким-нибудь ресурсом или посреди вычисления арифметического выражения. Однако у кооперативной ОС есть важное преимущество. Это существенно экономит процессорное время и память, а потому выглядит куда более подходящим для реализации на МК AVR. Поэтому в кооперативной ОС в большинстве случаев можно обойтись без сохранения контекста.

Переключение задач в BASCOM AVR

Чтобы реализовать переключение задач в среде BASCOM AVR, код задачи, каждая из которых реализована как обычная процедура, должен в каком-то месте вызывать диспетчер – тоже реализованный как обычная процедура.
Представим, что у нас есть две задачи, каждая из которых в каком-то месте своего кода вызывает диспетчер.

sub task1() do 'код Задачи 1 'вызов диспетчера loop
end sub ' ----------------------------------
sub task2() do 'код Задачи 2 'вызов диспетчера loop
end sub

Допустим, выполнялась Задача 1. Давайте поглядим, что окажется в стеке, когда она выполнит «вызов диспетчера»:

адрес возврата к основному коду (2 байта)
вершина стека –> адрес возврата к Задаче 1, вызвавшей диспетчер (2 байта)

Вершина стека будет указывать на адрес инструкции в Задаче 1, которая следует за вызовом диспетчера (инструкция loop в нашем примере).

Вопрос – как это сделать? Цель диспетчера в простейшем случае – передать управление Задаче 2. (предположим, диспетчеру уже известен адрес Задачи 2).

Процессор извлечет из стека помещенный туда адрес и, вместо возврата к Задаче 1, перейдет на выполнение Задачи 2. Для этого диспетчер должен вытащить из стека (и где-то запомнить) адрес возврата к Задаче 1, и поместить на это место в стек адрес Задачи 2, после чего дать команду return.

Дадим команду return – и окажемся в точке продолжения Задачи 1. В свою очередь, когда Задача 2 вызовет диспетчер, мы так же вытащим из стека и сохраним адрес, по которому можно будет вернуться к Задаче 2, и загрузим в стек ранее сохраненный адрес Задачи 1.

В итоге у нас получится такая чехарда:

Задача 1 –> Диспетчер –> Задача 2 –> Диспетчер –> Задача 1 ….

Неплохо! И это работает. Но, конечно, для сколь-нибудь пригодной к практическому использованию ОС этого мало. Ведь не всегда и не все задачи должны работать – например, они могут чего-то ожидать (истечения времени задержки, появления какого-нибудь сигнала и т.п.). Значит, у задач должен быть статус (РАБОТАЕТ, ГОТОВА, ОЖИДАЕТ и т.п). Кроме того, было бы неплохо, чтобы задачам назначался приоритет. Тогда, если более одной задачи готовы к выполнению, диспетчер продолжит ту задачу, которая имеет наибольший приоритет.

AQUA RTOS

Для реализации описанной идеи была разработана кооперативная ОС AQUA RTOS, предоставляющая задачам необходимые сервисы и позволяющая реализовать кооперативную многозадачность в среде BASCOM AVR.

Важное замечание касательно режима процедур в BASCOM AVR

Это регулируется опцией config submode = new | old.
В случае задания опции old компилятор, во-первых, будет компилировать весь код линейно, вне зависимости от того, используется он где-то или нет, во-вторых, процедуры без аргументов, оформленные в стиле sub name / end sub будет воспринимать как процедуры, оформленные в стиле name: / return. Перед тем, как начать описание AUQA RTOS, следует заметить, что среда BASCOM AVR поддерживает два типа адресации процедур. Это касается и процедур, оформленных в стиле в стиле sub name / end sub (в качестве метки нужно передать имя процедуры).
В то же время, режим submode = old налагает некоторые ограничения: процедуры задач не должны содержать аргументов; код файлов, подключенных через $include, включается в общий проект линейно, поэтому в подключенных файлах следует предусмотреть байпас – переход от начала к концу с помощью goto и метки.
Таким образом, в AQUA RTOS пользователь должен либо использовать для задач только старую нотацию процедур в стиле task_name: / return, либо использовать более общепринятое sub name / end sub, добавив в начало своего кода модификатор config submode = old, а во включаемые файлы – байпас goto метка / код включаемого файла / метка:.
Это позволяет нам передавать адрес процедуры как метку в качестве аргумента другой процедуре путем использования модификатора bylabel.

Статусы задач AQUA RTOS

Для задач в AQUA RTOS определены следующие статусы:

OSTS_UNDEFINE OSTS_READY OSTS_RUN OSTS_DELAY
OSTS_STOP
OSTS_WAIT OSTS_PAUSE OSTS_RESTART

Если задача еще не инициализирована, ей присвоен статус OSTS_UNDEFINE.
После инициализации задача имеет статус OSTS_STOP.
Если задача готова к исполнению, ей присваивается статус OSTS_READY.
Выполняющаяся в данный момент задача имеет статус OSTS_RUN.
Из него она может перейти в статусы OSTS_STOP, OSTS_READY, OSTS_DELAY, OSTS_WAIT, OSTS_PAUSE.
Статус OSTS_DELAY имеет задача, отрабатывающая задержку.
Статус OSTS_WAIT присваивается задачам, которые ожидают семафора, события или сообщения (подробнее о них ниже).

с самого начала. В чем различие статусов OSTS_STOP и OSTS_PAUSED?
Если по какой-то причине задача получает статус OSTS_STOP, то последующее возобновление работы задачи (при получении статуса OSTS_READY) будет осуществляться с точки ее входа, т.е. Из статуса OSTS_PAUSE задача продолжит работу в том месте, где была приостановлена.

Управление статусом задач

Управлять задачами может как сама ОС – автоматически, так и пользователь, путем вызова сервисов ОС. Сервисов управления задачами несколько (имена всех сервисов ОС начинаются с префикса OS_):

OS_InitTask(task_label, task_prio) OS_Stop() OS_StopTask(task_label) OS_Pause()
OS_PauseTask(task_label)
OS_Resume() OS_ResumeTask(task_label)
OS_Restart()

Каждый из них имеет два варианта: OS_сервис и OS_сервисTask (кроме сервиса OS_InitTask, который имеет только один вариант; сервис OS_Init инициализирует саму ОС).

Первый метод действует на саму вызывавшую его задачу; второй позволяет задать в качестве аргумента указатель на другую задачу и, таким образом, из одной задачи управлять иной. В чем разница между OS_сервис и OS_сервисTask ?

Про OS_Resume

В отличие от них, сервисы OS_Resume* только устанавливают задаче статус OSTS_READY. Все сервисы управления задачами, кроме OS_Resume и OS_ResumeTask, после отработки автоматически вызывают диспетчер задач. Этот статус будет обработан только при явном вызове диспетчера.

Приоритет и очередь задач

Как уже сказано выше, в реальной системе некоторые задачи могут оказаться более важными, а другие – второстепенными. Поэтому полезным свойством ОС является возможность назначить задачам приоритет. В этом случае, при наличии нескольких одновременно готовых задач, ОС сначала выберет задачу с наибольшим приоритетом. Если же все готовые задачи имеют равный приоритет, ОС будет ставить их на исполнение по кругу, в порядке, называемом «карусель» или round-robin.

Меньшее число означает больший приоритет. В AQUA RTOS приоритет назначается задаче при ее инициализации через вызов сервиса OS_InitTask, которому в качестве первого аргумента передается адрес задачи, а в качестве второго – число от 1 до 15. В ходе работы ОС изменение назначенного задаче приоритета не предусмотрено.

Задержки

В каждой задаче задержка отрабатывается независимо от других задач.
Таким образом, пока ОС отрабатывает задержку одной задачи, другие могут выполняться.
Для организации задержек предусмотрены сервисы OS_Delay | OS_DelayTask. В качестве аргумента передается число миллисекунд, на которое откладывается выполнение задачи. Поскольку размерность аргумента – dword, максимальная величина задержки составляет 4294967295 мс – или около 120 часов, что представляется вполне достаточным для большинства приложений. После вызова сервиса задержки автоматически вызывается диспетчер, который на время, пока будет отрабатываться задержка, передает управление другим задачам.

Семафоры

Семафоры в AQUA RTOS – это что-то вроде флагов и переменных, доступных задачам. Они бывают двух типов – бинарные и счетные. Первые имеют только два состояния: свободен и закрыт. Вторые представляют собой байтовый счетчик (сервис счетных семафоров в текущей версии AQUA RTOS не реализован (я ленивая задница), поэтому все сказанное ниже относится только к бинарным семафорам).

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

Как только задаче велено ждать семафор, управление автоматически передается диспетчеру, и он может запускать другие задачи – ровно до того момента, как указанный семафор освободится. При этом вся черная работа возлагается на диспетчер. внутри они реализованы как битовые флаги).
Бинарные семафоры работают через следующие сервисы: Как только состояние семафора изменится на свободен, диспетчер назначит всем ожидавшим этот семафор задачам статус готова (OSTS_READY), и они будут исполнены в порядке очереди и приоритета.
Всего в AQUA RTOS предусмотрено 16 двоичных семафоров (это число в принципе может быть увеличено путем изменения размерности переменной в блоке управления задач, т.к.

hBSem OS_CreateBSemaphore() OS_WaitBSemaphore(hBSem) OS_WaitBSemaphoreTask(task_label, hBSem)
OS_BusyBSemaphore(hBSem)
OS_FreeBSemaphore(hBSem)

Перед использованием семафор нужно создать. Это делается вызовом сервиса OS_CreateBSemaphore, который возвращает уникальный байтовый идентификатор (хэндл) созданного семафора hBSem, либо через пользовательский обработчик выдает ошибку OSERR_BSEM_MAX_REACHED, говорящую о том, что достигнуто максимально возможное число бинарных семафоров.

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

Если семафор свободен, передача управления не происходит, и задача просто продолжит выполнение. Сервис OS_WaitBSemaphore | OS_WaitBSemaphoreTask переводит (текущую | указанную) задачу в состояние ждать освобождения семафора hBSem, если этот семафор занят, а затем передает управление диспетчеру, чтобы он мог запускать другие задача.

Сервисы OS_BusyBSemaphore и OS_FreeBSemaphore устанавливают семафор hBSem в состояние занят или свободен соответственно.

Таким образом, все созданные семафоры статичны. Уничтожение семафоров в целях упрощения ОС и уменьшения объема кода не предусмотрено.

События

Помимо семафоров, задачи могут управляться событиями. Одной задаче можно указать ожидать некоторое событие, а другая задача (а также фоновый код) может сигналить об этом событии. При этом все задачи, которые ожидали данное событие, получат статус готова к исполнению (OSTS_READY) и будут поставлены диспетчером на исполнение в порядке очереди и приоритета.

Ну, например:
На какие события может реагировать задача?

  • прерывание;
  • возникновение ошибки;
  • освобождение ресурса (иногда для этого удобнее использовать семафор);
  • изменение состояния линии ввода-вывода или нажатие клавиши на клавиатуре;
  • прием или посылка символа по RS-232;
  • передача информации от одной части приложения к другой (см. тж. сообщения).

Система событий реализована через следующие сервисы:

hEvent OS_CreateEvent()
OS_WaitEvent(hEvent)
OS_WaitEventTask(task_label, hEvent)
OS_WaitEventTO(hEvent, dwTimeout)
OS_SignalEvent(hEvent)

Перед использованием события его нужно создать. Это делается вызовом функции OS_CreateEvent, которая возвращает уникальный байтовый идентификатор (хэндл) события hEvent, либо через пользовательский обработчик выдает ошибку OSERR_EVENT_MAX_REACHED, показывающую, что достигнут предел числа событий, какое может быть создано в ОС (максимум 255 разных событий).

После вызова этого сервиса управление будет автоматически передано диспетчеру. Чтобы заставить задачу ожидать событие hEvent, в ее коде следует вызвать OS_WaitEvent, передав хэндл события в качестве аргумента.

Для этого служит сервис OS_WaitEventTO. В отличие от сервиса семафоров, в сервисе событий предусмотрен вариант ожидания события с таймаутом. Если указанное время истекло, задача получит статус готова к исполнению так, как будто событие произошло, и будет поставлена диспетчером на продолжение исполнения в порядке очереди и приоритета. Вторым аргументом тут можно указать число миллисекунд, которые задача может ожидать событие. Узнать о том, что произошло не событие, а таймаут, задача может, проверив глобальный флаг OS_TIMEOUT.

При этом всем задачам, ожидающим данное событие, ОС установит статус готова к исполнению, так что они смогут продолжить исполнение в порядке очереди и приоритета. Сигналить о наступлении заданного события задача или фоновый код могут путем вызова сервиса OS_SignalEvent, которому в качестве аргумента передается хэндл события.

Сообщения

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

hTopic OS_CreateMessage()
OS_WaitMessage(hTopic)
OS_WaitMessageTask(task_label, hTopic)
OS_WaitMessageTO(hTopic, dwTimeout) OS_SendMessage(hTopic, wMessage)
word_ptr OS_GetMessage(hTopic) word_ptr OS_PeekMessage(hTopic) string OS_GetMessageString(hTopic) string OS_PeekMessageString(hTopic)

Чтобы воспользоваться сервисом сообщений, нужно сначала создать тему сообщения. Это делается через сервис OS_CreateMessage, который возвращает байтовый хэндл темы hTopic, либо через пользовательский обработчик выдает ошибку OSERR_TOPIC_MAX_REACHED, говорящую о том, что достигнуто максимально возможное число тем сообщений, и больше создать не получится.

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

Сервис ожидания с таймаутом OS_WaitMessageTO работает аналогично сервису OS_WaitEventTO системы событий.

Первым аргументом служит хэндл темы, на которую будет передано сообщение, а вторым – аргумент размерности word. Для посылки сообщений предусмотрен сервис OS_SendMessage. Это может быть как самостоятельное число, так и указатель на строку, которая, в свою очередь, уже и является сообщением.

Чтобы получить указатель строки, достаточно воспользоваться встроенной в BASCOM функцией varptr, например, так:

strMessage = "Hello, world!"
OS_SendMessage hTopic, varptr (strMessage)

Возобновив работу после вызова OS_WaitMessage, то есть, когда получено ожидаемое сообщение, задача может либо получить сообщение с его последующим автоматическим уничтожением, либо только просмотреть сообщение — в этом случае оно не уничтожится. Для этого служат последние четыре сервиса в списке. Первые два возвращают число размерности word, которое может быть либо самостоятельным сообщением, либо служить указателем строки, которая содержит сообщение. При этом OS_GetMessage автоматически удаляет сообщение, а OS_PeekMessage оставляет его.

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

Внутренняя таймерная служба

Для работы с задержками и отсчета времени AQUA RTOS использует встроенный в МК аппаратный таймер TIMER0. Таким образом, внешний код (фоновый и задач) не должен использовать этот таймер. Но обычно это и не требуется, т.к. ОС снабжает задачи всеми необходимыми средствами работы с временными интервалами. Разрешение таймера составляет 1 мс.

Примеры работы с AQUA RTOS

Начальные настройки

В самом начале пользовательского кода нужно определить, будет ли код исполняться во встроенном симуляторе или на реальном железе. Определите константу OS_SIM = TRUE | FALSE, которая задает режим симуляции.

Чем это число меньше, тем быстрее работает ОС (меньше накладные расходы), и тем меньше памяти она потребляет. Кроме того, в коде ОС отредактируйте константу OS_MAX_TASK, которая определяет максимальное число поддерживаемых ОС задач. Не забывайте изменить эту константу, если число задач изменилось. Поэтому не стоит указывать там большее число задач, чем вам потребуется.

Инициализация ОС

Перед началом работы AQUA RTOS должна быть инициализирована. Для этого нужно вызвать сервис OS_Init. Этот сервис настраивает начальные параметры ОС. Что еще более важно, у него есть аргумент – адрес пользовательской процедуры обработки ошибок. У нее, в свою очередь, тоже есть аргумент – код ошибки.

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

Итак, первым этапом работы с AQUA RTOS нужно добавить в пользовательскую программу код инициализации ОС и процедуру обработчика ошибок:

OS_Init my_err_trap '... '... '... sub my_err_trap(err_code as byte) print err_code end sub

Инициализация задач

Вторым этапом нужно инициализировать задачи, указав их имена и приоритет:

OS_InitTask task_1, 1
OS_InitTask task_2 , 1 '...
OS_InitTask task_N , 1

Тестовые задачи

Помигаем светодиодами

Итак, давайте создадим тестовое приложение, которое можно загрузить в стандартную плату Arduino Nano V3. Создайте в папке с файлом ОС какую-нибудь папку (например, test), и там создайте следующий bas-файл:

' начальные установки компилятора
config submode = old $include "..\aquaRTOS_1.05.bas" $regfile = "m328pdef.dat"
$crystal = 16000000
$hwstack = 48
$swstack = 48
$framesize = 64 ' объявление процедур
declare sub my_err_trap (byval err_code as byte)
declare sub task_1()
declare sub task_2() ' назначим светодиодам порты и режим работы
led_1 alias portd.4
led_2 alias portd.5
config portd.4 = output
config portd.5 = output ' *** начало кода приложения *** ' инициализируем ОС
OS_Init my_err_trap ' инициализируем задачи
OS_InitTask task_1, 1
OS_InitTask task_2 , 1 ' изначально все задачи имеют статус «остановлена» (OSTS_STOP) ' чтобы задачи начали работать, им нужно задать статус ' «готова к выполнению» (OSTS_READY) вызовом сервиса OS_ResumeTask
OS_ResumeTask task_1
OS_ResumeTask task_2 ' осталось запустить ОС вызовом диспетчера
OS_Sheduler
end ' *** задачи ***
sub task_1 () do toogle led_1 ' переключим светодиод 1 OS_delay 1000 ' приостановить на 1000 мс loop
end sub sub task_2 () do toogle led_2 ' переключим светодиод 2 OS_delay 333 ' приостановить на 333 мс loop
end sub ' **************************************************** ' обработчик ошибок
sub my_err_trap(err_code as byte) print "OS Error: "; err_code end sub

Подключите аноды светодиодов к выводам D4 и D5 платы Arduino (или к другим выводам, изменив соответствующие строки-определения в коде). Катоды через ограничительные резисторы 100...500 Ом подсоедините к шине GND. Скомпилируйте и залейте прошивку в плату. Светодиоды начнут асинхронно переключаться с периодом 2 и 0,66 с.

Итак, сначала мы инициализируем оборудование (задаем опции компилятора, режим работы портов и назначаем aliases), затем – саму ОС, и наконец – задачи. Давайте рассмотрим код.

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

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

Сразу бросается в глаза, что каждая из них оформлена как бесконечный цикл do – loop. Давайте посмотрим на задачи. В нашем случае это сервис задержки OS_Delay. Второе важное свойство – внутри такого цикла обязательно должен быть хотя бы один вызов либо диспетчера, либо сервиса ОС, автоматически вызывающего диспетчер после себя – иначе такая задача никогда не отдаст управление, и другие задачи не смогут выполняться. В качестве аргумента мы указали ему число миллисекнуд, на которые следует приостановить выполнение каждой задачи.

Если в начале кода задать константу OS_SIM = TRUE и запустить код не на реальном чипе, а в симуляторе, то можно проследить, как работает ОС.

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

Выбрав задачу, которую следует исполнить (допустим, task_1), диспетчер подменяет в стеке адрес возврата (первоначально он показывает на инструкцию end в основном коде) адресом точки входа задачи task_1, которую система узнает в процессе инициализации задачи, и исполняет команду return, которая заставляет МК вытащить из стека адрес возврата и перейти к нему – то есть, начать исполнение задачи task_1 (оператор do в коде task_1).

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

инструкцию loop), а затем, «повернув карусель», обнаруживает, что теперь надо выполнить задачу task_2. Диспетчер сохраняет адрес, который был в стеке, в блок управления задачи task_1 (указывает на инструкцию, следующую за вызовом OS_delay, т.е. Он помещает в стек адрес задачи task_2 (указывает в данный момент на инструкцию do в коде задачи task_2) и исполняет команду return, которая заставляет МК вытащить из стека адрес возврата и перейти к нему – то есть, начать исполнение задачи task_2.

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

инструкцию loop), а затем, «повернув карусель», обнаруживает, что теперь надо выполнить задачу task_2. Диспетчер сохраняет адрес, который был в стеке, в блок управления задачи task_1 (указывает на инструкцию, следующую за вызовом OS_delay, т.е. Туда (на инструкцию loop в коде задачи task_1), и будет передано управление. Отличие от первоначального состояния будет в том, что теперь в блоке управления задачей task_1 хранится не стартовый адрес задачи, а адрес точки, с которой произошел переход к диспетчеру.

Задача task_1 выполнит инструкцию loop, и далее весь цикл «Задача 1 – Диспетчер – Задача 2 – Диспетчер» будет повторяться бесконечно.

Посылаем сообщения

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

' начальные установки компилятора
config submode = old $include "..\aquaRTOS_1.05.bas" $regfile = "m328pdef.dat"
$crystal = 16000000
$hwstack = 48
$swstack = 48
$framesize = 64 ' объявление процедур
declare sub my_err_trap (byval err_code as byte)
declare sub task_1()
declare sub task_2() const OS_SIM = TURE ' будем запускать этот код в симуляторе ' объявление переменных
dim hTopic as byte ' переменная для темы сообщений
dim task_1_cnt as byte ' счетчик для задачи 1
dim strMessage as string * 16 ' сообщение ' *** начало кода приложения ***
OS_CreateMessage hTopic OS_Init my_err_trap OS_InitTask task_1 , 1
OS_InitTask task_2 , 1 OS_ResumeTask task_1
OS_ResumeTask task_2 OS_Sheduler
end ' *** задачи *** sub task_1() do print "task 1" OS_Sheduler incr task_1_cnt ' увеличим счетчик на 1 if task_1_cnt > 3 then print "task 1 is sending message to task 2" strMessage = "Hello, task 2!" ' посылаем сообщение задаче 2 OS_SendMessage hTopic , varptr(strMessage) task_1_cnt = 0 end if loop
end sub sub task_2() do print "task 2 is waiting messages..." ' будем ждать сообщений от задачи 1 OS_WaitMessage hTopic print "message recieved: " ; OS_GetMessageString (hTopic) loop
end sub ' **************************************************** ' обработчик ошибок
sub my_err_trap(err_code as byte) print "OS Error: "; err_code end sub

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

task 1
task 2 is waiting messages…
task 1
task 1
task 1
task 1 is sending message to task 2
task 1
message recieved: Hello, task 2!
task 2 is waiting messages…
task 1
task 1

Обратите внимание, в каком порядке происходит работа и переключение задач. Как только Задача 1 напечатает task 1, управление передается диспетчеру для того, чтобы он мог запустить вторую задачу. Задача 2 печатает task 2 is waiting messages..., затем вызывает сервис ожидания сообщений на тему hTopic, и управление автоматически передается диспетчеру, который снова вызывает Задачу 1. Та снова печатает task 1 и отдает управление в диспетчер. Однако, поскольку диспетчер обнаруживает, что Задача 2 теперь ожидает сообщений, он возвращает управление Задаче 1 на инструкцию incr, следующую за вызовом диспетчера.
Когда счетчик task_1_cnt в Задаче 1 превысит указанное значение, задача посылает сообщение, но продолжает исполняться – выполняет инструкцию loop и снова печатает task 1. После этого она вызывает диспетчер, который теперь обнаруживает, что для Задачи 2 имеется сообщение, и передает управление ей. Далее процесс выполняется циклически.

Обработка событий

Следующий код опрашивает две кнопки и переключает светодиоды при нажатии на соответствующую кнопку:

' начальные установки компилятора
config submode = old
$include "..\aquaRTOS_1.05.bas" $regfile = "m328pdef.dat"
$crystal = 16000000
$hwstack = 48
$swstack = 48
$framesize = 64 ' объявление процедур
declare sub my_err_trap (byval err_code as byte)
declare sub task_scankeys()
declare sub task_led_1()
declare sub task_led_2() ' зададим светодиодам порты и режим их работы
led_1 alias portd.4
led_2 alias portd.5
config portd.4 = output
config portd.5 = output
button_1 alias pind.6
button_2 alias pind.7
config portd.6 = input
config portd.7 = input ' объявление переменных
dim eventButton_1 as byte
dim eventButton_2 as byte ' *** начало кода приложения ***
eventButton_1 = OS_CreateEvent ' создадим по событию на каждую кнопку
eventButton_2 = OS_CreateEvent OS_Init my_err_trap OS_InitTask task_scankeys , 1
OS_InitTask task_led_1 , 1
OS_InitTask task_led_2 , 1 OS_ResumeTask task_scankeys
OS_ResumeTask task_led_1
OS_ResumeTask task_led_2 OS_Sheduler
end ' *** задачи *** sub task_scankeys() do debounce button_1 , 0 , btn_1_click , sub debounce button_2 , 0 , btn_2_click , sub OS_Sheduler loop btn_1_click: OS_SignalEvent eventButton_1
return btn_2_click: OS_SignalEvent eventButton_2 return
end sub sub task_led_1() do OS_WaitEvent eventButton_1 toggle led_1 loop
end sub sub task_led_2() do OS_WaitEvent eventButton_2 toggle led_2 loop
end sub ' **************************************************** ' обработчик ошибок
sub my_err_trap(err_code as byte) print "OS Error: "; err_code
end sub

Пример реального приложения под AQUA RTOS

Автомат должен показывать светодиодами в кнопках наличие вариантов кофе и выбор; принимать сигналы от приемника монет, готовить заказанный напиток, выдавать сдачу. Давайте попробуем представить, как бы могла выглядеть программа автомата по продаже кофе. Кроме того, автомат должен управлять внутренним оборудованием: например, поддерживать температуру водонагревателя на уровне 95…97°С; передавать данные о неисправностях оборудования и запасе ингредиентов и принимать команды через удаленный доступ (например, посредством GSM-модема), а также сигнализировать об актах вандализма.

Управляемый событиями подход

Поначалу разработчику бывает нелегко перейти от привычной схемы «суперцикл + флаги + прерывания» к подходу, основанному на задачах и событиях. Это требует выделения основных задач, которые должно выполнять устройство.
Давайте попробуем наметить такие задачи для нашего автомата:

  • контроль и управление нагревателем – ControlHeater()
  • индикация наличия и выбора напитков – ShowGoods()
  • прием монет/купюр и их суммирование – AcceptMoney()
  • опрос кнопок – ScanKeys()
  • выдача сдачи – MakeChange()
  • отпуск напитка – ReleaseCoffee()
  • защита от вандализма – Alarm()

Прикинем степень важности задач и частоту их вызова.
ControlHeater() очевидно важна, поскольку для приготовления кофе нам постоянно нужен кипяток. Но она не должна выполняться слишком часто, потому что нагреватель обладает большой инерционностью, а вода остывает медленно. Достаточно проверять температуру раз в минуту. Дадим этой задаче приоритет 5.
ShowGoods() не слишком важна. Предложение может измениться только после отпуска товара, если запас каких-то ингредиентов окажется исчерпан. Поэтому дадим этой задаче приоритет 8, и пусть она выполняется при пуске автомата и каждый раз при отпуске товара.
ScanKeys() должна иметь достаточно высокий приоритет, чтобы автомат быстро реагировал на нажатие кнопок. Дадим этой задаче приоритет 3, и будем выполнять ее каждые 40 миллисекунд.
AcceptMoney() также является частью интерфейса пользователя. Мы дадим ей тот же приоритет, что и ScanKeys(), и будем выполнять каждые 20 миллисекунд.
MakeChange () выполняется только после отпуска товара. Мы свяжем ее с ReleaseCoffee() и дадим приоритет 10.
ReleaseCoffee() нужна только тогда, когда было принято соответствующее количество денег и нажата кнопка выбора напитка. Для быстроты ответа дадим ей приоритет 2.
Поскольку вандалоустойчивость – довольно важная функция автомата, задаче Alarm() можно поставить самый высокий приоритет – 1, и активировать раз в секунду, чтобы проверить датчики наклона или вскрытия.

После старта, когда программа считала настройки из EEPROM и инициализировала оборудование, наступает время инициализировать ОС и запустить задачи. Таким образом, нам понадобится семь задач с различными приоритетами.

' объявление процедур задач
declare sub ControlHeater()
declare sub ShowGoods() declare sub AcceptMoney()
declare sub ScanKeys()
declare sub MakeChange ()
declare sub ReleaseCoffee()
declare sub Alarm()

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

Например, ReleaseCoffee() может выглядеть примерно так:

sub ReleaseCoffee() do OS_WaitMessage bCoffeeSelection wItem = OS_GetMessage(bCoffeeSelection) Release wItem loop end sub

Задача ReleaseCoffee в бесконечном цикле ожидает сообщение на тему bCoffeeSelection и не делает ничего, пока оно не поступит (управление автоматически возвращается диспетчеру, чтобы он мог запускать другие задачи). Как только сообщение послано, ReleaseCoffee() становится готовой к выполнению, и когда это случается, задача получает содержимое сообщения (код выбранного напитка) wItem с помощью сервиса OS_GetMessage и отпускает товар заказчику. Поскольку ReleaseCoffee() использует подсистему сообщений, сообщение должно быть создано перед запуском многозадачности:

dim bCoffeeSelection as byte
bCoffeeSelection = OS_CreateMessage()

Как было сказано выше, ShowGoods() должна выполняться один раз при пуске и каждый раз при отпуске товара. Чтобы связать ее с процедурой отпуска ReleaseCoffee(), используем сервис событий. Для этого создадим событие перед запуском многозадачности:

dim bGoodsReliased as byte
bGoodsReliased = OS_CreateEvent()

А в процедуру ReleaseCoffee() после строчки Release wItem добавим сигнализацию о событии bGoodsReliased:

OS_SignalEvent bGoodsReliased

Инициализация ОС

Для того, чтобы подготовить ОС к работе, мы должны инициализировать ее, указав при этом адрес обработчика ошибок, который находится в пользовательском коде. Сделаем это при помощи сервиса OS_Init:

OS_Init Mailfuncion

В пользовательском коде нужно добавить обработчик – процедуру, байтовым аргументом которой будет код ошибки:

sub Mailfuncion (bCoffeeErr) print "Mailfunction! Error #: "; bCoffeeErr if isErrCritical (bCoffeeErr) = 1 then CallService(bCoffeeErr) end if
end sub

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

Запуск задач

Мы уже помним, что события, семафоры и т.п. должны быть инициализированы до того, как будут использоваться. Кроме того, сами задачи перед запуском должны быть инициализированы с помощью сервиса OS_InitTask:

OS_InitTask ControlHeater , 5
OS_InitTask ShowGoods , 8 OS_InitTask AcceptMoney , 3
OS_InitTask ScanKeys , 3
OS_InitTask MakeChange, 10
OS_InitTask ReleaseCoffee , 2
OS_InitTask Alarm , 1

Поскольку многозадачный режим еще не начался, порядок, в котором задачи стартуют, несущественен, и в любом случае не зависит от их приоритетов. В этом месте все задачи еще находятся в остановленном состоянии. Чтобы подготовить их к выполнению, мы должны с помощью сервиса OS_ResumeTask задать им статус «готова к выполнению»:

OS_ResumeTask ControlHeater OS_ResumeTask ShowGoods
OS_ResumeTask AcceptMoney OS_ResumeTask ScanKeys
OS_ResumeTask MakeChange
OS_ResumeTask ReleaseCoffee
OS_ResumeTask Alarm

Как уже говорились, не все задачи обязательно должны стартовать при запуске многозадачности; некоторые из них могут произвольное время пребывать в состоянии «остановлена» и получать готовность лишь при определенных условиях. Сервис OS_ResumeTask может быть вызван в любое время из любого места кода (фонового или задачи), когда многозадачность уже работает. Главное, чтобы задача, на которую он ссылается, была предварительно инициализирована.

Пуск многозадачности

Теперь все готово для того, чтобы запустить многозадачность. Сделаем это путем вызова диспетчера:

OS_Sheduler

После этого мы смело можем поставить в коде программы end – управление дальнейшим исполнением кода теперь берет на себя ОС.

Давайте посмотрим на код целиком:

' начальные установки компилятора
config submode = old
$include "..\aquaRTOS_1.05.bas"
$include "coffee_hardware.bas" ' файл с процедурами управления оборудованием ' процедуры в этом файле имеют префикс Coffee_
$regfile = "m328pdef.dat" ' Arduino Nano v3
$crystal = 16000000
$hwstack = 48
$swstack = 48
$framesize = 64 Coffee_InitHardware ' инициализация оборудования автомата ' объявление процедур
declare sub Mailfuncion (byval bCoffeeErr as byte) ' обработчик ошибок
declare sub ControlHeater () ' управление водонагревателем
declare sub ShowGoods () ' показать наличие товара
declare sub AcceptMoney () ' прием наличных
declare sub ScanKeys () ' опрос кнопок
declare sub MakeChange () ' выдача сдачи после отпуска товара
declare sub ReleaseCoffee () ' отпуск товара
declare sub Alarm () ' обеспечение безопасности автомата ' проведем начальные настройки оборудования
Coffee_InitHardware () ' объявление переменных
dim wMoney as long ' счетчик введенных денег
dim wGoods as long ' номер товара ' *** начало кода приложения *** ' инициализация ОС
OS_Init Mailfuncion ' создадим тему сообщения о выборе напитка
dim bCoffeeSelection as byte
bCoffeeSelection = OS_CreateMessage() ' создадим событие отпуска товара
dim bGoodsReliased as byte
bGoodsReliased = OS_CreateEvent() ' инициализация задач
OS_InitTask ControlHeater , 5
OS_InitTask ShowGoods , 8 OS_InitTask AcceptMoney , 3
OS_InitTask ScanKeys , 3
OS_InitTask MakeChange, 10
OS_InitTask ReleaseCoffee , 2
OS_InitTask Alarm , 1 ' подготовка задач к выполнению
OS_ResumeTask ControlHeater OS_ResumeTask ShowGoods
OS_ResumeTask AcceptMoney OS_ResumeTask ScanKeys
OS_ResumeTask MakeChange
OS_ResumeTask ReleaseCoffee
OS_ResumeTask Alarm ' запуск ОС
OS_Sheduler end ' *** код задач *** ' -----------------------------------
sub ControlHeater() do select case GetWaterTemp() case is > 97 Coffee_HeaterOff ' выключить нагреватель case is < 95 Coffee_HeaterOn ' включить нагреватель case is < 5 CallServce (WARNING_WATER_FROZEN) ' угроза замерзания end select OS_Delay 60000 ' ждать 1 минуту loop
end sub ' -----------------------------------
sub ShowGoods() do LEDS = Coffee_GetDrinkSupplies() ' установить состояние порта D, ' к которому подключены светодиоды индикации наличия товаров и ' ассоциирована переменная LEDS OS_WaitEvent bGoodsReliased ' ожидать события "отпуск товара" loop
end sub ' -----------------------------------
sub AcceptMoney() do wMoney = wMoney + ReadMoneyAcceptor() OS_Delay 20 loop
end sub ' -----------------------------------
sub ScanKeys() do wGoods = ButtonPressed() if wMoney >= GostOf(wGoods) then OS_SendMessage bCoffeeSelection, wGoods ' отправляет сообщение на тему bCoffeeSelection, которое ' содержит код выбранного товара end if OS_Delay 40 loop
end sub ' -----------------------------------
sub MakeChange() do OS_WaitEvent bGoodsReliased ' ожидать события "отпуск товара" Refund wMoney loop
end sub ' -----------------------------------
sub ReleaseCoffee() do OS_WaitMessage bCoffeeSelection 'ждать сообщения bCoffeeSelection wItem = OS_GetMessage(bCoffeeSelection) ' прочесть сообщение Release wItem ' отпустить выбранный товар wMoney = wMoney – CostOf (wItem) ' уменьшить на цену товара OS_SignalEvent bGoodsReliased ' просигналить об этом задачам ' обратите внимание, что это событие могут ждать две задачи: ' MakeChange и ShowGoods ' обе они, получив сообщение, становятся готовыми к исполнению loop
end sub ' -----------------------------------
sub Alarm() do OS_Delay 1000 if Hijack() = 1 then CallPolice() end if loop
end sub ' ----------------------------------- ' *** обработчик ошибок ОС ***
sub Mailfuncion (bCoffeeErr) print "Mailfunction! Error #: "; bCoffeeErr if isErrCritical (bCoffeeErr) = 1 then CallService() end if
end sub

Конечно, более правильным был бы подход не с периодическим опросом кнопок и датчика наличных, а с использованием прерываний. В обработчиках этих прерываний мы могли бы использовать посылку сообщений при помощи сервиса OS_SendMessage() с содержимым, равным номеру нажатой клавиши или номиналу введенной монеты/купюры. Предлагаю читателю доработать программу самостоятельно. Благодаря подходу, ориентированному на задачи, и сервису, предоставляемому ОС, это потребует минимальных изменений кода.

Исходный код AQUA RTOS

Исходный код версии 1.05 доступен для скачивания по ссылке

Постскриптум

Q: Почему AQUA?
A: Ну, я делала контроллер аквариума, это такая как «умный дом», только не людям, а для рыбок. Полно всяких датчиков, часы реального времени, релейные и аналоговые силовые выходы, экранное меню, гибкая «программа событий» и даже WiFi-модуль. Интервалы должны отсчитываться, кнопки опрашиваться, датчики обрабатываться, программа событий читаться из EEPROM и выполняться, экран обновляться, вай-фай отвечать. Да еще контроллер должен переходить в многоуровневое меню для настроек и программирования. Делать на флагах и прерываниях – это как раз получить тот самый «макаронный код», в котором ни разобраться, ни модифицировать. Поэтому я и решила, что мне нужна ОС. Вот она и AQUA.

Я, как могла, придумывала разнообразные тесты и гоняла ОС на самых разных задачках, и даже прихлопнула заметное число багов, но это не означает, что всех и полностью. Q: Наверняка в коде полно логических ошибок и глюков?
A: Наверняка. Поэтому буду очень благодарна, если вместо того, чтобы тыкать меня в баги мордой, вы вежливо и тактично укажете на них, а лучше и подскажете, как их, по-вашему, лучше исправить. Более чем уверена, что их еще немало затаилось в закоулках кода. Например, кто-нибудь допишет сервис счетных семафоров (не забыли? Будет также замечательно, если проект получит дальнейшее развитие как продукт коллективного творчества. В любом случае буду очень благодарна за конструктивный вклад. – я ленивая задница) и предложит другие улучшения.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть