В предыдущей статье мы рассматривали различные типы планирования, поддерживаемые ОСРВ, и соответствующие возможности в Nucleus SE. В этой статье рассмотрим дополнительные возможности планирования в Nucleus SE и процесс сохранения и восстановления контекста.
При разработке Nucleus SE я сделал максимальное число функций опциональными, что позволяет экономить на памяти и/или времени.
Как упоминалось ранее в статье «Планировщик: реализация», Nucleus SE поддерживает различные варианты приостановки задач, но эта функция является опциональной и включается символом NUSE_SUSPEND_ENABLE в nuse_config.h. Если установлено значение TRUE, то структура данных определена как NUSE_Task_Status[]. Такой тип приостановки применяется для всех задач. Массив имеет тип U8, где 2 полубайта используются отдельно. Младшие 4 бита содержат статус задачи: NUSE_READY, NUSE_PURE_SUSPEND, NUSE_SLEEP_SUSPEND, NUSE_MAILBOX_SUSPEND и т.д. Если задача приостановлена вызовом API (например, NUSE_MAILBOX_SUSPEND), старшие 4 бита содержат индекс объекта, на котором задача приостановлена. Эта информация используется, когда ресурс становится доступен и для вызова API необходимо выяснить, какую из приостановленных задач нужно возобновить.
Для выполнения приостановки задач используется пара функций планировщика: NUSE_Suspend_Task() и NUSE_Wake_Task().
Код NUSE_Suspend_Task имеет следующий вид:
Функция сохраняет новое состояние задачи (все 8 бит), получаемое как параметр suspend_code. При включении блокировки (см. «API-вызовы блокировки» ниже) сохраняется код возврата NUSE_SUCCESS. Далее вызывается NUSE_Reschedule() для передачи управления следующей задаче.
Код NUSE_Wake_Task() достаточно прост:
Состояние задачи устанавливается в NUSE_READY. Если планировщик Priority не используется, текущая задача продолжает занимать процессор, пока не придет время освободить ресурс. Если используется планировщик Priority, вызывается NUSE_Reschedule() с индексом задачи в качестве указания на выполнение, поскольку задача может иметь более высокий приоритет и должна быть незамедлительно поставлена на выполнение.
В Nucleus RTOS поддерживается целый ряд вызовов API, с помощью которых разработчик может приостановить (заблокировать) задачу, если ресурсы недоступны. Задача возобновится, когда ресурсы снова будут доступны. Этот механизм реализуется и в Nucleus SE и применим к ряду объектов ядра: задача может быть заблокирована в разделе памяти, в группе событий, почтовом ящике, очереди, канале или семафоре. Но, как и большинство средств в Nucleus SE, он является опциональной и определяется символом NUSE_BLOCKING_ENABLE в nuse_config.h. Если установлено значение TRUE, то определяется массив NUSE_Task_Blocking_Return[], который содержит код возврата для каждой задачи; это может быть NUSE_SUCCESS или код NUSE_MAILBOX_WAS_RESET, показывающий, что объект был сброшен, когда задача была заблокирована. При включении блокировки соответствующий код включается в функции API с помощью условной компиляции.
Nucleus RTOS подсчитывает, сколько раз задача была запланирована с момента её создания и последнего сброса. Такая возможность реализована и в Nucleus SE, но является опциональной и определяется символом NUSE_SCHEDULE_COUNT_SUPPORT в nuse_config.h. Если установлено значение TRUE, то создается массив NUSE_Task_Schedule_Count[] типа U16, в котором хранится счетчик каждой задачи в приложении.
Когда в Nucleus RTOS создается задача, можно выбрать её состояние: готова или приостановлена. В Nucleus SE, по умолчанию, при запуске все задачи готовы. Опция, выбранная с помощью символа NUSE_INITIAL_TASK_STATE_SUPPORT в nuse_config.h, позволяет выбрать состояние запуска. Массив NUSE_Task_Initial_State[] определяется в nuse_config.c и требует инициализации NUSE_READY или NUSE_PURE_SUSPEND для каждой задачи в приложении.
Идея сохранения контекста задачи с любым типом планировщика, кроме RTC (Run to Completion), была представлена в статье #3 «Задачи и планирование». Как уже упоминалось, есть несколько способов сохранять контекст. Учитывая, что Nucleus SE не предназначен для 32-разрядных процессоров, я предпочёл для сохранения контекста использовать не стек, а таблицы.
Двумерный массив типа ADDR NUSE_Task_Context[][] используется для сохранения контекста для всех задач в приложении. Строки – NUSE_TASK_NUMBER (количество задач в приложении), столбцы – NUSE_REGISTERS (количество регистров, которые необходимо сохранить; зависит от процессора и устанавливается в nuse_types.h).
Само собой, сохранение контекста и восстановление кода, зависят от процессора. И это единственный код Nucleus SE, привязанный к конкретному устройству (и среде разработки). Приведу пример кода сохранения/восстановления для процессора ColdFire. Хотя такой выбор может показаться странным из-за устаревшего процессора, но его ассемблер читается легче, чем ассемблеры большинства современных процессоров. Код достаточно прост для использования в качестве основы для создания переключателя контекста и для других процессоров:
Когда требуется переключение контекста, этот код вызывается в NUSE_Context_Swap. Используется две переменные: NUSE_Task_Active, индекс текущей задачи, контекст которой необходимо сохранить; NUSE_Task_Next, индекс задачи, контекст которой необходимо загрузить (см. раздел «Глобальные данные»).
Процесс сохранения контекста работает следующим образом:
Процесс загрузки контекста – та же последовательность действий в обратном порядке:
Сложность при реализации переключения контекста заключается в непростом для многих процессоров доступе к регистру состояния (для ColdFire это SR). Общепринятым решением является прерывание, т. е. программное прерывание или прерывание условным переходом, что приводит к загрузке SR в стек вместе с PC. Так работает Nucleus SE на ColdFire. Макрос NUSE_CONTEXT_SWAP() задается в nuse_types.h, который расширяется до: asm(" trap #0");
Ниже представлен код инициализации (NUSE_Init_Task() в nuse_init.c) для блоков контекста:
Так происходит инициализация указателя стека, PC и SR. Первые два имеют значения, установленные пользователем в nuse_config.c. Значение SR определяется как символ NUSE_STATUS_REGISTER в nuse_types.h. Для ColdFire это значение – 0x40002000.
Планировщик Nucleus SE требует очень мало памяти для хранения данных, но, конечно, использует структуры данных, связанные с задачами, которые будут подробно рассмотрены в следующих статьях.
Планировщик не использует данные, расположенные в ПЗУ, а в ОЗУ размещено от 2 до 5 глобальных переменных (все задаются в nuse_globals.c), зависящих от того, какой планировщик используется:
Планировщик Nucleus SE не использует данные ПЗУ. Точный объем данных ОЗУ варьируется в зависимости от используемого планировщика:
Несмотря на то, что Nucleus SE предлагает на выбор 4 планировщика, охватывающих большинство случаев, открытая архитектура позволяет реализовывать возможности и для других случаев.
Как уже рассказывалось в статье #3 «Задачи и планирование», простой планировщик квантования времени (Time slice) имеет ограничения, поскольку он ограничивает максимальное время, в течение которого задача может занимать процессор. Более сложной опцией было бы добавление поддержки фоновой задачи. Такую задачу можно было бы запланировать на любом слоте, выделенном для приостановленных задач, и запустить при частичном освобождении слота. Этот подход позволяет запланировать задачи с регулярными интервалами и прогнозируемой долей времени процессорного ядра на выполнение.
В большинстве ядер реального времени планировщик приоритетов поддерживает несколько задач на каждом уровне приоритета, в отличие от Nucleus SE, где каждая задача имеет уникальный уровень. Я отдал предпочтение последнему варианту, поскольку это значительно упрощает структуры данных и, следовательно, код планировщика. Для поддержки же более сложных архитектур потребовались бы многочисленные таблицы ПЗУ и ОЗУ.
Об авторе: Колин Уоллс уже более тридцати лет работает в сфере электронной промышленности, значительную часть времени уделяя встроенному ПО. Сейчас он – инженер в области встроенного ПО в Mentor Embedded ( подразделение Mentor Graphics). Колин Уоллс часто выступает на конференциях и семинарах, автор многочисленных технических статей и двух книг по встроенному ПО. Живет в Великобритании. Профессиональный блог Колина: https://blogs.mentor.com/colinwalls/, e-mail: colin_walls@mentor.com
О переводе: этот цикл статей показался интересным тем, что, несмотря на местами устаревшие описываемые подходы, автор очень понятным языком знакомит малоподготовленного читателя с особенностями ОС реального времени. Я сам принадлежу к коллективу создателей российской ОСРВ, которую мы предполагаем сделать бесплатной, и надеюсь, что цикл будет полезен начинающим разработчикам.