Обзор одной российской RTOS, часть 3. Структура простейшей программы
Я продолжаю публиковать цикл статей из «Книги знаний ОСРВ МАКС». Это неформальное руководство программиста, для тех, кто предпочитает живой язык сухому языку документации.
В этой части пришла пора положить теорию на реальный код. Рассмотрим, как всё сказанное раньше записывается на языке С++ (именно он является основным для разработки программ под ОСРВ МАКС). Здесь мы поговорим только о минимально необходимых вещах, без которых невозможна ни одна программа.
Содержание (опубликованные и неопубликованные статьи):
Так как у ОСРВ МАКС объектно-ориентированная модель, то и программа должна содержать классы. При этом базовые классы уже имеются в составе ОС, прикладной программист должен лишь создать от них наследников и дописать требуемую функциональность.
Для реализации приложения следует сделать наследника от класса Application (обязательно перекрыв в нём виртуальную функцию Initialize()) и один или несколько наследников класса Task (обязательно перекрыв в нём виртуальную функцию Execute()). И всем этим будет управлять планировщик, реализованный в классе Scheduler.
Рис. 1. Минимально необходимые для работы классы (серые — уже имеются, белые — следует дописать)
Класс ApplicationНа первый взгляд класс кажется совершенно ненужной прослойкой. В нём необходимо перекрыть метод Initialize(), в котором инициализируется приложение. Именно внутри данной функции удобно создавать задачи (хотя, это не догма, задачи можно создавать где угодно, просто внутри данной функции — удобнее всего).
Казалось бы, почему нельзя инициализировать приложение в функции main(), а данный класс — выкинуть, как лишний? Но не будем торопиться. Во-первых, эта функция всегда вызывается в привилегированном режиме, поэтому в ней можно настраивать аппаратуру, включая программирование NVIC, чего нельзя сделать в обычном режиме. Кроме того, этот класс выполняет намного больше функций, чем просто инициализация приложения.
В первую очередь, именно через объект этого класса ОС находит приложение. Я хотел было написать, что «находит приложение без глобальных переменных», но если заняться крючкотворством, то статическая переменная-член класса Application
всё-таки является глобальной. Но как там в классике: «Самоса — сукин сын, но это — наш сукин сын». Переменная — глобальная, но она хорошо структурирована и принадлежит классу приложения. Соответственно, её имя изолировано ото всех остальных классов. Для обращения к ней имеется функция
которую можно перекрыть, чтобы возвращать не исходный, а унаследованный тип класса. Например, один из тестов описывает класс со следующим перекрытием:
Таким образом, данный класс содержит функциональность, благодаря которой приложение всегда может найти ту ниточку, потянув за которую оно придёт к нужной своей части. Это может пригодиться, например, в обработчиках прерываний.
Следующая неочевидная вещь при использовании класса Application — его конструктор. В конструкторе передаётся тип многозадачности.
Класс Application содержит виртуальную функцию OnAlarm(). Она будет вызываться для информирования об исключительных ситуациях. Их перечень достаточно велик:
Перекрыв функцию, можно обеспечить обработку ошибок (либо аварийное выключение аппаратуры для того, чтобы она не вышла из строя). Для результата функции определены следующие значения:
Далее рассмотрим метод Run(). Именно его следует вызвать для того, чтобы ОС начала работу приложения. Собственно, типовая функция main() должна выглядеть следующим образом:
ОСРВ МАКС поддерживает только одно приложение. Поэтому следует объявлять только один экземпляр класса, унаследованного от Application.
Класс TaskНепосредственно код задачи. Класс в чистом виде никогда не используется, для работы следует создавать наследника от него (либо пользоваться готовыми наследниками, о которых будет сказано в конце раздела).
Функция Execute()Самая-самая главная функция в задаче — это, разумеется, виртуальная функция Execute().В классических процедурно-ориентированных ОС, программист должен реализовать функцию потока, а затем передать её в качестве аргумента для функции CreateThread(). При объектно-ориентированном подходе, алгоритм проще:
- Создать наследника от класса Task,
- Функция потока будет иметь имя Execute(). Достаточно перекрыть её. Ничего больше никуда передавать не требуется.
Таким образом, вместо одного указателя, который обрабатывается строго в потоковой функции, получаем широчайшие возможности инициализации данных, отделив их от непосредственно рабочего кода. Сама функция Execute(), соответственно, осталась без параметров.
Итак. Первое правило разработки любого класса задачи: Следует создать класс-наследник от Task и перекрыть в нём функцию Execute().
При выходе из функции Execute() задача удаляется из планировщика, но не удаляется из памяти, так как она может быть как на куче, так и на стеке, а оператор delete, применённый к стековому объекту, вызовет ошибку. Таким образом, удаление объекта задачи по окончании работы с ним — прикладного программиста.
Конструктор классаТеперь поговорим про конструктор класса. Все конструкторы класса Task находятся в секции protected, поэтому их нельзя вызвать напрямую. Для этого следует в классе-наследнике реализовать свой конструктор, который вызовет тот или иной конструктор класса Task. Примеры таких конструкторов-наследников:
class TaskYieldBeforeTestTask_1: public Task