+7 (812) 670-9095
Обратная связьEnglish
Главная → Статьи → Системное ПО → Статья #33. Использование операционной системы реального времени Nucleus SE
Версия для печати

Статья #33. Использование операционной системы реального времени Nucleus SE

14 ноября 2019

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





RTOS_33_1.png


Что такое Nucleus SE?


Мы знаем, что Nucleus SE — это ядро операционной системы реального времени, но нужно понимать, как оно вписывается в остальную часть приложения. А оно именно вписывается, так как в отличие от настольной операционной системы (например, Windows), приложение не запускается на Nucleus SE; ядро просто является частью программы, работающего на встраиваемом устройстве. Это наиболее распространенный вариант использования ОСРВ.

С высокоуровневой точки зрения, встраиваемое приложение — это некий код, который запускается вместе с запуском ЦП. При этом инициализируется аппаратная и программная среда, а затем вызывается функция main(), запускающая основной код приложения.

При использовании Nucleus SE (и многих других похожих ядер) отличие заключается в том, что функция main() является частью кода ядра. Эта функция просто инициализирует структуры данных ядра, а затем вызывает планировщик, что приводит к запуску кода приложений (задач). Пользователь может добавить в функцию main() любой собственный код инициализации.

Nucleus SE также включает набор функций – программный интерфейс приложения (API), предоставляющий набор функций, таких как связь и синхронизация задач, работа с таймерами, выделение памяти и т.д. Все функции API были описаны ранее в статьях данного цикла.

Вся программная часть Nucleus SE предоставляется в виде исходного кода (в основном на С). Для конфигурации кода в соответствии с требованиями конкретного приложения используется условная компиляция. Это подробно описывается в этой статье в разделе «Конфигурация».

После того как код был скомпилирован, получившиеся объектные модули Nucleus SE связываются с модулями кода приложения, в результате чего получается один двоичный образ, который обычно помещается во флеш-память встраиваемого устройства. Результатом такого статического связывания является то, что вся символьная информация остается доступной как из кода приложения, так и из кода ядра. Это полезно при отладке, однако, чтобы избежать некорректного использования данных Nucleus SE, требуется осторожность.


Поддержка CPU и инструментов


Так как Nucleus SE поставляется в виде исходного кода, она должна быть портируемой. Однако код, работающий на таком низком уровне (при использовании планировщиков, где требуется переключение контекста, то есть любой, кроме Run to Completion), не может быть абсолютно независимым от языка ассемблера. Я минимизировал эту зависимость, и для портирования на новый CPU низкоуровневого программирования почти не требуется. Использование нового набора инструментов разработки (компилятор, ассемблер, компоновщик и т.д.) также может привести к проблемам при портировании.


Настройка приложения Nucleus SE


Ключом к эффективному использованию Nucleus SE является правильная настройка. Это может выглядеть сложным, но на самом деле все достаточно логично и лишь требует систематического подхода. Почти вся настройка выполняется путем редактирования двух файлов: nuse_config.h и nuse_config.c.


Настройка nuse_config.h


Этот файл – просто набор символов директивы #define, которым присваиваются соответствующие значения для получения необходимой конфигурации ядра. В файле nuse_config.h по умолчанию присутствуют все символы, но им присвоены минимальные настройки.

Счётчики объектов
Количество объектов ядра каждого типа устанавливается значениями символа вида NUSE_SEMAPHORE_NUMBER. Для большинства объектов это значение может варьироваться от 0 до 15. Задачи являются исключением, их должно быть не менее одной. Сигналы, по сути, не являются самостоятельными объектами, так как они ассоциируются с задачами и включаются путем присвоения NUSE_SIGNAL_SUPPORT значения TRUE.

Активаторы функций API
Каждая функция API Nucleus SE может быть активирована отдельно путем присвоения символу, имя которого совпадает с именем функции (например, NUSE_PIPE_JAM), значения TRUE. Это приводит к включению кода функции в приложение.

Выбор планировщика и его настройки
Nucleus SE поддерживает четыре типа планировщиков, как было описано в одной из предыдущих статей. Используемый планировщик задается при помощи присвоения NUSE_SCHEDULER_TYPE одного из следующих значений: NUSE_RUN_TO_COMPLETION_SCHEDULER, NUSE_TIME_SLICE_SCHEDULER, NUSE_ROUND_ROBIN_SCHEDULER или NUSE_PRIORITY_SCHEDULER.

Можно настраивать и другие параметры планировщика:
NUSE_TIME_SLICE_TICKS указывает количество тиков на слот для планировщика Time Slice. Если используется другой планировщик, этот параметр должен иметь значение 0.
NUSE_SCHEDULE_COUNT_SUPPORT может быть установлен в значение TRUE или FALSE для активирования/деактивирования механизма счётчика планировщика.
NUSE_SUSPEND_ENABLE включает блокировку задач (приостановку) для множества функций API. Это значит, что вызов такой функции может привести к приостановке вызывающей задачи до освобождения ресурса. Для выбора этого параметра необходимо, чтобы NUSE_SUSPEND_ENABLE тоже имел значение TRUE.

Другие параметры
Нескольким другим параметрам тоже можно присваивать значения TRUE или FALSE для активирования/деактивирования других функций ядра:
NUSE_API_PARAMETER_CHECKING добавляет код проверки параметров вызова функций API. Обычно используется при отладке.
NUSE_INITIAL_TASK_STATE_SUPPORT задает исходное состояние всех задач как NUSE_READY или NUSE_PURE_SUSPEND. Если этот параметр отключить, все задачи будут иметь исходное состояние NUSE_READY.
NUSE_SYSTEM_TIME_SUPPORT – поддержка системного времени.
NUSE_INCLUDE_EVERYTHING – параметр, добавляющий максимальное количество функций в конфигурацию Nucleus SE. Он приводит к активации всего опционального функционала и каждой функции API сконфигурированных объектов. Используется для быстрого создания конфигурации Nucleus SE для проверки нового портирования кода ядра.


Настройка nuse_config.c


После указания конфигурации ядра в nuse_config.h необходимо инициализировать различные структуры данных, хранящиеся в ПЗУ. Это выполняется в файле nuse_config.c. Определение структур данных контролируется условной компиляцией, поэтому все структуры содержатся в копии файла nuse_config.c по умолчанию.

Данные задач
Массив NUSE_Task_Start_Address[] должен быть инициализирован значением начальных адресов каждой задачи. Обычно это просто список имен функций, без скобок. Прототипы функций входа задач также должны быть видимыми. В файле по умолчанию задача конфигурируется с именем NUSE_Idle_Task(), это может быть изменено на задачу приложения.

Если используется любой планировщик, кроме Run to Completion, каждой задаче требуется собственный стек. Для каждого стека задачи необходимо создать массив в ОЗУ. Эти массивы должны иметь тип ADDR, а адрес каждого из них должен храниться в NUSE_Task_Stack_Base[]. Предугадать размер массива сложно, поэтому лучше воспользоваться измерениями (см. раздел «Отладка» далее в этой статье). Размер каждого массива (то есть количество слов в стеке) должно храниться в NUSE_Task_Stack_Size[].

Если была включена функция для указания исходного состояния задачи (при помощи параметра NUSE_INITIAL_TASK_STATE_SUPPORT), массив NUSE_Task_Initial_State[] должен инициализироваться с состоянием NUSE_READY или NUSE_PURE_SUSPEND.

Данные пулов разделов
Если сконфигурирован хотя бы один пул разделов, то для каждого из них должен быть создан массив (типа U8) в ПЗУ. Размер этих массивов вычисляется следующим образом: (количество разделов * (размер раздела + 1)). Адреса этих разделов (то есть их имена) должны быть присвоены соответствующим элементам NUSE_Partition_Pool_Data_Address[]. Для каждого пула количество разделов и их размер должен быть помещен в NUSE_Partition_Pool_Partition_Number[] и NUSE_Partition_Message_Size[], соответственно.

Данные очередей
Если сконфигурирована хотя бы одна очередь, то для каждой из них должен быть создан массив (типа ADDR) в ОЗУ. Размер этих массивов – число элементов в каждой очереди. Адреса этих массивов (то есть их имена) должны быть присвоены соответствующим элементам NUSE_Queue_Data[]. Размер каждой очереди должен быть присвоен соответствующему элементу NUSE_Queue_Size[].

Данные каналов передачи данных
Если сконфигурирован хотя бы один канал передачи данных, то для него (или для каждого из них) должен быть создан массив (типа U8) в ОЗУ. Размер этих массивов вычисляется следующим образом: (размер канала * размер сообщения в канале). Адреса этих массивов (то есть их имена) должны быть присвоены соответствующим элементам NUSE_Pipe_Data[]. Для каждого канала его размер и размер сообщения должен быть присвоен соответствующим элементам NUSE_Pipe_Size[] и NUSE_Pipe_Message_Size[], соответственно.

Данные семафоров
Если сконфигурирован хотя бы один семафор, массив NUSE_Semaphore_Initial_Value[] должен быть инициализирован начальными значениями обратного счетчика.

Данные таймеров приложения
Если сконфигурирован хотя бы один таймер, массив NUSE_Timer_Initial_Time[] должен быть инициализирован начальными значениями счетчиков. Кроме того, NUSE_Timer_Reschedule_Time[] должны быть присвоены значения перезапуска. Эти значения таймеров будут использоваться после того, как завершится первый цикл таймера. Если значениям перезапуска присвоены значения 0, счетчик будет остановлен после одного цикла.

Если настроена поддержка механизмов завершения счёта (присвоением параметру NUSE_TIMER_EXPIRATION_ROUTINE_SUPPORT значения TRUE), требуется создать еще два массива. Адреса механизмов завершения (просто список имен функций, без скобок) должны быть помещены в NUSE_Timer_Expiration_Routine_Address[]. Массив NUSE_Timer_Expiration_Routine_Parameter[] должен быть инициализирован значениями параметров завершения.


Какой API?


Все операционные системы в том или ином виде имеют API (программный интерфейс приложения). Nucleus SE не исключение, и функции, из которых состоит API, были подробно описаны в этом цикле статей.

Может показаться очевидным, что при написании приложения, использующего Nucleus SE, нужно использовать API так, как было описано в предыдущих статьях. Однако это не всегда так.

Для большинства пользователей API Nucleus SE будет чем-то новым, возможно даже их первым опытом использования API операционной системы. И так как он довольно простой, он может послужить хорошим введением в тему. В таком случае, порядок действий понятен.

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

  1. Приложение Nucleus SE является лишь частью системы, в которой для других компонентов используются другие операционные системы. Поэтому портируемость кода, и, что более важно, опыт использования различных операционных систем выглядят очень заманчиво.
  2. Пользователь имеет обширный опыт использования API другой операционной системы. Использование этого опыта также очень целесообразно.
  3. Пользователь хочет повторно использовать код, написанный для API другой операционной системы. Изменение вызовов API возможно, но потребует много времени.

Так как полный исходный код Nucleus SE доступен всем, ничто не мешает отредактировать каждую функцию API, чтобы она была похожей на свой эквивалент из другой операционной системы. Однако это займет много времени и будет очень непродуктивным. Более правильным подходом будет написать «обертку» (wrapper). Это можно сделать несколькими способами, однако проще всего сделать заголовочный файл (#include), содержащий набор макросов #define, которые будут сопоставлять функции стороннего API с функциями API Nucleus SE.

Обертка, которая переносит функции API Nucleus RTOS (частично) на Nucleus SE распространяется вместе с Nucleus SE. Она может оказаться полезной разработчикам, имеющим опыт использования Nucleus RTOS, либо там, где в будущем возможен переход на эту ОСРВ. Эта обертка также может послужить примером при разработке аналогичных вещей.

Отладка приложений Nucleus SE


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

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


Использование отладчика


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

Стоит заметить, что обычно отладчик – семейство инструментов, а не одна программа. Отладчик может иметь различные режимы работы, посредством которых он помогает при разработке кода на виртуальной системе либо на реальной аппаратуре.

Точки останова, учитывающие задачи


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

К счастью, возможность создания скриптов на современных отладчиках и доступность символьных данных Nucleus SE делают реализацию точек останова, учитывающих задачи, довольно простой вещью. Всё, что необходимо, это написать простой скрипт, который будет связан с точкой останова, которую вы хотите научить различать задачи. Этот скрипт будет принимать параметр: индекс (ID) интересующей вас задачи. Скрипт будет просто сравнивать это значение с индексом текущей задачи (NUSE_Task_Active). Если значения совпадают, выполнение программы приостанавливается. Если они различны, выполнение продолжается. Стоит заметить, что выполнение этого скрипта повлияет на выполнение приложения в реальном времени (прим. переводчика: имеется в виду, что исполнение программы будет подтормаживать относительно ее обычной работы). Однако, если скрипт не находится в цикле, который будет выполняться очень часто, это влияние будет минимальным.


Информация об объекте ядра


Очевидной необходимостью при отладке приложения Nucleus SE является возможность получения информации об объектах ядра: какие у них характеристики и каков их текущий статус. Это позволяет получить ответ на такие вопросы, как: «Насколько велика эта очередь и сколько в ней сейчас сообщений?»

Это можно использовать, добавив дополнительный код отладки в ваше приложение, который и будет использовать «информационные» вызовы API (такие как NUSE_Queue_Information). Само собой, это будет означать, что ваше приложение теперь содержит дополнительный код, в котором не будет необходимости после внедрения приложения. Использование #define для включения и выключения этого кода при помощи условной компиляции будет логичным решением.

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

Альтернативным более гибким, но менее «неустаревающим» подходом является прямой доступ к структурам данных объектов ядра. Вероятнее всего, лучше делать это при помощи скриптов отладчика. В нашем примере, размер очереди может быть получен из NUSE_Queue_Size[], а ее текущее использование из NUSE_Queue_Data[]. Кроме того, сообщения в очереди могут быть отображены при помощи адреса области данных очереди (из NUSE_Queue_Data[]).


Возвращаемые значения вызовов API

Многие функции API возвращают значение статуса, показывающее, насколько успешно был завершен вызов. Было бы полезно отслеживать эти значения и помечать случаи, в которых они не равны NUSE_SUCCESS (то есть имеют значение ноль). Так как это отслеживание предназначено только для отладки, условная компиляция вполне уместна. Определение глобальной переменной (скажем, NUSE_API_Call_Status) может быть условно скомпилировано (под управлением символа директивы #define). Затем, часть определения вызовов API, а именно NUSE_API_Call_Status =, также может быть условно скомпилирована. Например, с целью отладки, вызов, который обычно имеет вид:

NUSE_Mailbox_Send(mbox, msg, NUSE_SUSPSEND);

примет следующий вид:

NUSE_API_Call_Status = NUSE_Mailbox_Send(mbox, msg, NUSE_SUSPEND);

Если активирована блокировка задач, многие вызовы функций API могут возвращать только информацию об успешном завершении вызова либо о том, что объект был сброшен. Однако, если активирована проверка параметров API, вызовы API смогут возвращать множество других значений.

Задание размера стека задач и переполнение стека

Тема защиты от переполнения стека обсуждалась в одной из предыдущих статей (#31). Во время отладки существует несколько других возможностей.

Область памяти стека может быть заполнена характерным значением: чем-то иным, чем все единицы или все нули. После этого отладчик может быть использован для наблюдения за областями памяти и за тем, насколько значения будут изменены, что позволит понять степень заполненности стека. Если все области памяти были изменены, это не значит, что стек был переполнены, но может означать что его размера едва хватает, что опасно. Его следует увеличить и продолжить тестирование.

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

Чек-лист конфигурации Nucleus SE

Так как Nucleus SE разрабатывалась как очень гибкая и настраиваемая система для удовлетворения требованиям приложения, ей требуется значительное количество настраиваемых параметров. Именно поэтому вся эта статья, по сути, посвящена конфигурации Nucleus SE. Чтобы убедиться в том, что мы ничего не пропустили, ниже приведен чек-лист всех ключевых шагов, которые необходимо выполнить, чтобы создать встраиваемое приложение Nucleus SE.
  1. Скачать Nucleus SE. Несмотря на то, что практически весь код Nucleus SE был опубликован в рамках этого цикла статей, в следующей статье будет рассказано как скачать Nucleus SE в готовом к использованию виде.
  2. Рассмотреть поддержку CPU/инструментов. Может возникнуть необходимость переписать код на языке ассемблера и заново создать скрипты сборки.
  3. Собрать простую демоверсию. Это позволит убедиться в том, что у вас есть все необходимые компоненты, а все инструменты совместимы.
  4. Спланировать структуру задач. Сколько будет задач и что они будут делать. Установите начальные адреса задач и размеры стека. Само собой, вы сможете это изменить позже. В приложении можно создать до 16 задач.
  5. Инициализация приложения. Нужно ли вам добавить какой-либо код в main()?
  6. Выбрать планировщик. Вам на выбор дается 4 планировщика, при этом планировщик можно будет изменить позже.
  7. Проверить вектор прерывания таймера, если у вас есть таймер.
  8. Проверить вектор перехвата переключения контекста.
  9. Сигналы. Выключите поддержку сигналов, если вы собираетесь их использовать.
  10. Системное время. Включите поддержку системного времени, если оно вам необходимо.
  11. Счётчики объектов ядра. Определите, сколько объектов каждого типа вам нужно. Вы можете изменить это позже. Максимум — по 16 объектов каждого типа.
  12. ПЗУ объектов ядра. Инициализируйте данные в ПЗУ для каждого типа объектов, которые вы используете.
  13. Данные в ОЗУ. Определите место для данных в ОЗУ для объектов, которым это необходимо (очереди, каналы передачи данных и пулы разделов).
  14. Активаторы API. Включите все вызовы API, которые вам необходимы.

В следующей статье (последней в этом цикле) будет подведен итог всей истории с Nucleus SE, а также будет предоставлена информация, которая поможет при создании реализаций Nucleus SE и их использовании.

Об авторе: Колин Уоллс уже более тридцати лет работает в сфере электронной промышленности, значительную часть времени уделяя встроенному ПО. Сейчас он — инженер в области встроенного ПО в Mentor Embedded (подразделение Mentor Graphics). Колин Уоллс часто выступает на конференциях и семинарах, автор многочисленных технических статей и двух книг по встроенному ПО. Живет в Великобритании. Профессиональный блог Колина, e-mail: colin_walls@mentor.com.


Теги: ОСРВ, RTOS, встроенное ПО, embedded software, служебные вызовы, API