У любой операционной системы есть определенный механизм запуска. Принцип работы этого механизма у каждой системы свой. Обычно говорят, что система загружается (англ. boot), это сокращение от «bootstrap», которое отсылает к выражению «pull oneself over a fence by one’s bootstraps» (перебраться через ограду, потянув себя за ремешки на обуви), что примерно описывает, как система самостоятельно переходит из состояния, в котором память заполнена пустотой (прим. переводчика: если совсем точно, то мусором) к стабильному выполнению программ. Традиционно в память загружается небольшая часть программы, она может храниться в ПЗУ. В прошлом она могла вводиться при помощи переключателей на передней панели компьютера. Этот начальный загрузчик считывал более сложную программу загрузки, которая уже загружала операционную систему. Сегодня настольный компьютер загружается следующим образом: код в BIOS ищет устройства (жесткие диски, CD-ROM, USB-флешки), с которых можно загрузиться, после чего загружается операционная система.
ОС для встраиваемых систем также может инициализироваться подобным образом. И в самом деле, встраиваемые операционные системы, разработанные на основе настольных операционных систем, так и загружаются. Но в большинстве «классических» ОСРВ используется гораздо более простой (а, следовательно, более быстрый) способ.
Операционная система – часть программного обеспечения. Если это программное обеспечение уже находится в памяти (например, в том или ином виде ПЗУ), то требуется всего лишь сделать так, чтобы последовательность команд ЦП после сброса заканчивалась выполнением кода инициализации ОС. Именно так работают большинство ОСРВ, в том числе и Nucleus SE (примечание переводчика: и к нашей ОСРВ МАКС это тоже относится).
Большинство средств разработки встраиваемого программного обеспечения включают в себя необходимый код запуска для обработки сброса ЦП и передачи управления во входную точку (Entry Point) в функции main(). Распространяемый код Nucleus SE не имеет дела с этим процессом, так как он должен быть максимально портируемым. Вместо этого, он содержит функцию main(), которая берет контроль над ЦП и инициализирует и запускает ОС. Эта функция будет подробно рассмотрена ниже.
Предыдущие статьи серии:
Статья #29. Прерывания в Nucleus SE
Статья #28. Программные таймеры
Статья #27. Системное время
Статья #26. Каналы: вспомогательные службы и структуры данных
Статья #25. Каналы передачи данных: введение и основные службы
Статья #24. Очереди: вспомогательные службы и структуры данных
Статья #23. Очереди: введение и базовые службы
Статья #22. Почтовые ящики: вспомогательные службы и структуры данных
Статья #21. Почтовые ящики: введение и базовые службы
Статья #20. Семафоры: вспомогательные службы и структуры данных
Статья #19. Семафоры: введение и базовые службы
Статья #18. Группы флагов событий: вспомогательные службы и структуры данных
Статья #17. Группы флагов событий: введение и базовые службы
Статья #16. Сигналы
Статья #15. Разделы памяти: службы и структуры данных
Статья #14. Разделы памяти: введение и базовые службы
Статья #13. Структуры данных задач и неподдерживаемые вызовы API
Статья #12. Службы для работы с задачами
Статья #11. Задачи: конфигурация и введение в API
Статья #10. Планировщик: дополнительные возможности и сохранение контекста
Статья #9. Планировщик: реализация
Статья #8. Nucleus SE: внутреннее устройство и развертывание
Статья #7. Nucleus SE: введение
Статья #6. Другие сервисы ОСРВ
Статья #5. Взаимодействие между задачами и синхронизация
Статья #4. Задачи, переключение контекста и прерывания
Статья #3. Задачи и планирование
Статья #2. ОСРВ: Структура и режим реального времени
Статья #1. ОСРВ: введение.
Статья #28. Программные таймеры
Статья #27. Системное время
Статья #26. Каналы: вспомогательные службы и структуры данных
Статья #25. Каналы передачи данных: введение и основные службы
Статья #24. Очереди: вспомогательные службы и структуры данных
Статья #23. Очереди: введение и базовые службы
Статья #22. Почтовые ящики: вспомогательные службы и структуры данных
Статья #21. Почтовые ящики: введение и базовые службы
Статья #20. Семафоры: вспомогательные службы и структуры данных
Статья #19. Семафоры: введение и базовые службы
Статья #18. Группы флагов событий: вспомогательные службы и структуры данных
Статья #17. Группы флагов событий: введение и базовые службы
Статья #16. Сигналы
Статья #15. Разделы памяти: службы и структуры данных
Статья #14. Разделы памяти: введение и базовые службы
Статья #13. Структуры данных задач и неподдерживаемые вызовы API
Статья #12. Службы для работы с задачами
Статья #11. Задачи: конфигурация и введение в API
Статья #10. Планировщик: дополнительные возможности и сохранение контекста
Статья #9. Планировщик: реализация
Статья #8. Nucleus SE: внутреннее устройство и развертывание
Статья #7. Nucleus SE: введение
Статья #6. Другие сервисы ОСРВ
Статья #5. Взаимодействие между задачами и синхронизация
Статья #4. Задачи, переключение контекста и прерывания
Статья #3. Задачи и планирование
Статья #2. ОСРВ: Структура и режим реального времени
Статья #1. ОСРВ: введение.
Инициализация памяти
Объявления всех статических переменных в коде Nucleus SE начинаются с префикса ROM или RAM, чтобы показать, где их следует размещать. Эти две директивы #define определены в файле nuse_types.h и должны быть сконфигурированы с учетом особенностей используемого набора инструментов для разработки (компилятор и компоновщик). Обычно ROM должен иметь тип const (прим. переводчика: из моего опыта, const – не всегда достаточно, лучше – static const), а RAM – пустое значение.
Все переменные ROM инициализируются статически, что логично. Переменные RAM не инициализируются статически (так как это работает только с определенными наборами инструментов, которые настроены на автоматическое копирование из ПЗУ в ОЗУ); явный код инициализации включен в приложение и будет подробно описан ниже.
Nucleus SE не хранит «константных» данных в ОЗУ, которая обычно в дефиците у небольших систем. Вместо использования сложных структур данных для описания объектов ядра используются наборы таблиц (массивов), которые без проблем размещаются в ПЗУ или ОЗУ, в зависимости от необходимости.
Функция main()
Ниже приведен полный код функции main() Nucleus SE:
void main(void)
{
NUSE_Init(); /* initialize kernel data */
/* user initialization code here */
NUSE_Scheduler(); /* start tasks */
}
Последовательность операций довольно проста:
- Сначала вызывается функция NUSE_Init(). Она инициализирует все структуры данных Nucleus SE и будет подробно описана ниже.
- Затем пользователь может вставить любой код инициализации приложения, который будет выполнен до запуска планировщика задач. Подробнее о том, чего можно достичь при помощи этого кода, см. далее в этой статье.
- Наконец, запускается планировщик Nucleus SE (NUSE_Scheduler()). Это также будет подробно рассмотрено далее в этой статье.
Функция NUSE_Init()
Эта функция инициализирует все переменные ядра и структуры данных Nucleus SE.
Ниже приведен полный код функции:
void NUSE_Init(void)
{
U8 index;
/* global data */
NUSE_Task_Active = 0;
NUSE_Task_State = NUSE_STARTUP_CONTEXT;
#if NUSE_SYSTEM_TIME_SUPPORT
NUSE_Tick_Clock = 0;
#endif
#if NUSE_SCHEDULER_TYPE == NUSE_TIME_SLICE_SCHEDULER
NUSE_Time_Slice_Ticks = NUSE_TIME_SLICE_TICKS;
#endif
/* tasks */
#if ((NUSE_SCHEDULER_TYPE != NUSE_RUN_TO_COMPLETION_SCHEDULER)
|| NUSE_SIGNAL_SUPPORT || NUSE_TASK_SLEEP
|| NUSE_SUSPEND_ENABLE || NUSE_SCHEDULE_COUNT_SUPPORT)
for (index=0; index<NUSE_TASK_NUMBER; index++)
{
NUSE_Init_Task(index);
}
#endif
/* partition pools */
#if NUSE_PARTITION_POOL_NUMBER != 0
for (index=0; index<NUSE_PARTITION_POOL_NUMBER; index++)
{
NUSE_Init_Partition_Pool(index);
}
#endif
/* mailboxes */
#if NUSE_MAILBOX_NUMBER != 0
for (index=0; index<NUSE_MAILBOX_NUMBER; index++)
{
NUSE_Init_Mailbox(index);
}
#endif
/* queues */
#if NUSE_QUEUE_NUMBER != 0
for (index=0; index<NUSE_QUEUE_NUMBER; index++)
{
NUSE_Init_Queue(index);
}
#endif
/* pipes */
#if NUSE_PIPE_NUMBER != 0
for (index=0; index<NUSE_PIPE_NUMBER; index++)
{
NUSE_Init_Pipe(index);
}
#endif
/* semaphores */
#if NUSE_SEMAPHORE_NUMBER != 0
for (index=0; index<NUSE_SEMAPHORE_NUMBER; index++)
{
NUSE_Init_Semaphore(index);
}
#endif
/* event groups */
#if NUSE_EVENT_GROUP_NUMBER != 0
for (index=0; index<NUSE_EVENT_GROUP_NUMBER; index++)
{
NUSE_Init_Event_Group(index);
}
#endif
/* timers */
#if NUSE_TIMER_NUMBER != 0
for (index=0; index<NUSE_TIMER_NUMBER; index++)
{
NUSE_Init_Timer(index);
}
#endif
}
Сначала инициализируются глобальные переменные:
- NUSE_Task_Active – индекс активной задачи, инициализируется нулевым значением; позднее это может поменять планировщик.
- NUSE_Task_State – инициализируется значением NUSE_STARTUP_CONTEXT, которое ограничивает функционал API для любого последующего кода инициализации приложения.
- Если активирована поддержка системного времени, NUSE_Tick_Clock присваивается нулевое значение.
- Если активирован планировщик Time Slice, NUSE_Time_Slice_Ticks присваивается сконфигурированное значение NUSE_TIME_SLICE_TICKS.
Затем вызываются функции для инициализации объектов ядра:
- NUSE_Init_Task() вызывается для инициализации структур данных каждой задачи. Этот вызов пропускается, только если используется планировщик Run to Completion, а сигналы, приостановка задач и счетчик планировок не сконфигурированы (так как эта комбинация функций приведет отсутствию структур данных задач в ОЗУ, следовательно, инициализация не будет осуществлена).
- NUSE_Init_Partition_Pool() вызывается для инициализации каждого объекта пула разделов. Эти вызовы пропускаются, если нет сконфигурированных пулов разделов.
- NUSE_Init_Mailbox() вызывается для инициализации каждого объекта почтовых ящиков. Эти вызовы пропускаются, если нет сконфигурированных почтовых ящиков.
- NUSE_Init_Queue() вызывается для инициализации каждого объекта очереди. Эти вызовы пропускаются, если нет сконфигурированных очередей.
- NUSE_Init_Pipe() вызывается для инициализации каждого объекта канала. Эти вызовы пропускаются, если нет сконфигурированных каналов.
- NUSE_Init_Semaphore() вызывается для инициализации каждого объекта семафоров. Эти вызовы пропускаются, если нет сконфигурированных семафоров.
- NUSE_Init_Event_Group() вызывается для инициализации каждого объекта групп событий. Эти вызовы пропускаются, если нет сконфигурированных групп событий.
- NUSE_Init_Timer() вызывается для инициализации каждого объекта таймеров. Эти вызовы пропускаются, если нет сконфигурированных таймеров.
Инициализация задач
Ниже приведен полный код функции NUSE_Init_Task():
void NUSE_Init_Task(NUSE_TASK task)
{
#if NUSE_SCHEDULER_TYPE != NUSE_RUN_TO_COMPLETION_SCHEDULER
NUSE_Task_Context[task][15] = /* SR */
NUSE_STATUS_REGISTER;
NUSE_Task_Context[task][16] = /* PC */
NUSE_Task_Start_Address[task];
NUSE_Task_Context[task][17] = /* SP */
(U32 *)NUSE_Task_Stack_Base[task] +
NUSE_Task_Stack_Size[task];
#endif
#if NUSE_SIGNAL_SUPPORT || NUSE_INCLUDE_EVERYTHING
NUSE_Task_Signal_Flags[task] = 0;
#endif
#if NUSE_TASK_SLEEP || NUSE_INCLUDE_EVERYTHING
NUSE_Task_Timeout_Counter[task] = 0;
#endif
#if NUSE_SUSPEND_ENABLE || NUSE_INCLUDE_EVERYTHING
#if NUSE_INITIAL_TASK_STATE_SUPPORT ||
NUSE_INCLUDE_EVERYTHING
NUSE_Task_Status[task] =
NUSE_Task_Initial_State[task];
#else
NUSE_Task_Status[task] = NUSE_READY;
#endif
#endif
#if NUSE_SCHEDULE_COUNT_SUPPORT || NUSE_INCLUDE_EVERYTHING
NUSE_Task_Schedule_Count[task] = 0;
#endif
}
Если планировщик Run to Completion не был сконфигурирован, инициализируется контекстный блок для задачи NUSE_Task_Context[task][]. Большинству элементов не присваиваются значения, так как они представляют общие машинные регистры, которые должны иметь промежуточное значение при запуске задачи. В примере (Freescale ColdFire) реализации Nucleus SE (но и у других процессоров механизм будет аналогичным) последние три записи заданы явным образом:
- NUSE_Task_Context[task][15] содержит регистр состояния (SR, status register) и имеет значение директивы #define NUSE_STATUS_REGISTER.
- NUSE_Task_Context[task][16] содержит счетчик программ (PC, program counter) и имеет значение адреса входной точки кода задачи: NUSE_Task_Start_Address[task].
- NUSE_Task_Context[task][17] содержит указатель стека (SP, stack pointer) и инициализируется значением, вычисленным как сумма адреса стека задачи (NUSE_Task_Stack_Base[task]) и размера стека задачи (NUSE_Task_Stack_Size[task]).
Если активирована поддержка сигналов, флагам сигналов задачи (NUSE_Task_Signal_Flags[task]) присваивается нулевое значение.
Если активирована приостановка задачи (т.е. служебный вызов API NUSE_Task_Sleep()), счетчику таймаута задачи (NUSE_Task_Timeout_Counter[task]) присваивается нулевое значение.
Если активировано состояние ожидания задачи (task suspend), статус задачи (NUSE_Task_Status[task]) инициализируется. Это начальное значение задается пользователем (в NUSE_Task_Initial_State[task]), если активирована поддержка начального состояния задачи. В противном случае состоянию присваивается NUSE_READY.
Если активирован счетчик планировок, счетчику задачи (NUSE_Task_Schedule_Count[task]) присваивается нулевое значение.
Инициализация пулов разделов
Ниже приведен полный код функции NUSE_Init_Partition_Pool():
void NUSE_Init_Partition_Pool(NUSE_PARTITION_POOL pool)
{
NUSE_Partition_Pool_Partition_Used[pool] = 0;
#if NUSE_BLOCKING_ENABLE
NUSE_Partition_Pool_Blocking_Count[pool] = 0;
#endif
}
«Использованному» счетчику пула разделов (NUSE_Partition_Pool__Partition_Used[pool]) присваивается нулевое значение.
Если активирована блокировка задач, счетчику заблокированных задач пулов разделов (NUSE_Partition_Pool_Blocking_Count[pool]) присваивается нулевое значение.
Инициализация почтовых ящиков
Ниже приведен полный код NUSE_Init_Mailbox():
void NUSE_Init_Mailbox(NUSE_MAILBOX mailbox)
{
NUSE_Mailbox_Data[mailbox] = 0;
NUSE_Mailbox_Status[mailbox] = 0;
#if NUSE_BLOCKING_ENABLE
NUSE_Mailbox_Blocking_Count[mailbox] = 0;
#endif
}
Хранилищу данных почтовых ящиков (NUSE_Mailbox_Data[mailbox]) присваивается нулевое значение, и состояние (NUSE_Mailbox_Status[mailbox]) становится «неиспользуемым» (т.е. нулевым).
Если активирована блокировка задач, счетчику заблокированных задач почтовых ящиков (NUSE_Mailbox_Blocking_Count[mailbox]) присваивается нулевое значение.
Инициализация очередей
Ниже приведен полный код функции NUSE_Init_Queue():
void NUSE_Init_Queue(NUSE_QUEUE queue)
{
NUSE_Queue_Head[queue] = 0;
NUSE_Queue_Tail[queue] = 0;
NUSE_Queue_Items[queue] = 0;
#if NUSE_BLOCKING_ENABLE
NUSE_Queue_Blocking_Count[queue] = 0;
#endif
}
Указателям на начало и конец очереди (на самом деле, это индексы NUSE_Queue_Head[queue] и NUSE_Queue_Tail[queue]) присваиваются значения, указывающие на начало области данных очередей (т.е. они принимают нулевое значение). Счетчику элементов в очереди (NUSE_Queue_Items[queue]) также присваивается нулевое значение.
Если активирована блокировка задач, счетчику заблокированных задач очередей (NUSE_Queue_Blocking_Count[queue]) присваивается нулевое значение.
Инициализация каналов
Ниже приведен полный код функции NUSE_Init_Pipe():
void NUSE_Init_Pipe(NUSE_PIPE pipe)
{
NUSE_Pipe_Head[pipe] = 0;
NUSE_Pipe_Tail[pipe] = 0;
NUSE_Pipe_Items[pipe] = 0;
#if NUSE_BLOCKING_ENABLE
NUSE_Pipe_Blocking_Count[pipe] = 0;
#endif
}
Указателям на начало и конец канала (на самом деле, это индексы – NUSE_Pipe_Head[pipe] и NUSE_Pipe_Tail[pipe]) присваивается значение, указывающее на начало области данных канала (т.е. они принимают нулевое значение). Счетчику элементов в канале (NUSE_Pipe_Items[pipe]) также присваивается нулевое значение.
Если активирована блокировка задач, счетчику заблокированных задач канала (NUSE_Pipe_Blocking_Count[pipe]) присваивается нулевое значение.
Инициализация семафоров
Ниже приведен полный код функции NUSE_Init_Semaphore():
void NUSE_Init_Semaphore(NUSE_SEMAPHORE semaphore)
{
NUSE_Semaphore_Counter[semaphore] =
NUSE_Semaphore_Initial_Value[semaphore];
#if NUSE_BLOCKING_ENABLE
NUSE_Semaphore_Blocking_Count[semaphore] = 0;
#endif
}
Счетчик семафоров (NUSE_Semaphore_Counter[semaphore]) инициализируется значением, заданным пользователем (NUSE_Semaphore_Initial_Value[semaphore]).
Если активирована блокировка задач, счетчику заблокированных задач семафора (NUSE_Semaphore_Blocking_Count[semaphore]) присваивается нулевое значение.
Инициализация групп событий
Ниже приведен полный код функции NUSE_Init_Event_Group():
void NUSE_Init_Event_Group(NUSE_EVENT_GROUP group)
{
NUSE_Event_Group_Data[group] = 0;
#if NUSE_BLOCKING_ENABLE
NUSE_Event_Group_Blocking_Count[group] = 0;
#endif
}
Флаги группы событий сбрасываются, т.е. NUSE_Event_Group_Data[group] присваивается нулевое значение.
Если активирована блокировка задач, счетчику заблокированных задач группы флагов событий (NUSE_Event_Group_Blocking_Count[group]) присваивается нулевое значение.
Инициализация таймеров
Ниже приведен полный код NUSE_Init_Timer();
void NUSE_Init_Timer(NUSE_TIMER timer)
{
NUSE_Timer_Status[timer] = FALSE;
NUSE_Timer_Value[timer] = NUSE_Timer_Initial_Time[timer];
NUSE_Timer_Expirations_Counter[timer] = 0;
}
Состояние таймера (NUSE_Timer_Status[timer]) устанавливается в значение «неиспользуемое», т.е. FALSE.
Значение обратного отсчета (NUSE_Timer_Value[timer]) инициализируется значением, заданным пользователем (NUSE_Timer_Initial_Time[timer]).
Счетчику завершений (NUSE_Timer_Expirations_Counter[timer]) присваивается нулевое значение.
Инициализация кода приложения
После того как структуры данных Nucleus SE были инициализированы, появляется возможность выполнить код, отвечающий за инициализацию приложения до начала выполнения задачи. Это возможность может пригодиться для следующих задач:
- Инициализация структур данных приложения. Явное заполнение структур данных проще понять и отладить по сравнению с автоматической инициализацией статических переменных.
- Назначение объектов ядра. Учитывая, что все объекты ядра создаются статически на этапе сборки и идентифицируются при помощи значений индексов, может быть полезным назначить «владельца» или определить использование этих объектов. Это можно сделать при помощи директивы #define, однако, если существует несколько экземпляров задач, индексы объектов лучше назначить через глобальные массивы (индексируемые по ID задачи).
- Инициализация устройства. Это может быть полезно для начальной установки периферийных устройств.
Очевидно, многие из этих целей могут быть достигнуты и до инициализации Nucleus SE, но преимущество в расположении кода приложения здесь заключается в том, что теперь можно использовать службы ядра (вызовы API). Например, очередь или почтовый ящик могут быть предварительно заполнены данными, которые нужно будет обработать, когда задача запустится.
У вызовов API есть ограничение: все действия, которые обычно приводят к активации планировщика, запрещены (например, приостановка/блокировка задач). Глобальной переменной NUSE_Task_State было присвоено значение NUSE_STARTUP_CONTEXT, чтобы отметить это ограничение.
Запуск планировщика
После того, как инициализация была завершена, остается только запустить планировщик, чтобы приступить к выполнению кода приложения – задач. Конфигурация планировщика и работа различных видов планировщиков была подробно описана в одной из предыдущих статей (#9), так что здесь потребуется лишь краткий итог.
Последовательность ключевых шагов следующая:
- Присваивание глобальной переменной NUSE_Task_State значения NUSE_TASK_CONTEXT.
- Выбор индекса первой запускаемой задачи. Если поддержка начальной задачи активирована, выполняется поиск первой готовой задачи, в противном случае — используется нулевое значение.
- Вызывается планировщик – NUSE_Scheduler().
Что именно происходит на последнем шаге зависит от того, какой планировщик выбран. При использовании планировщика Run to Completion запускается цикл планировки и задачи вызываются последовательно. При использовании других планировщиков загружается контекст первой задачи и управление передается задаче.
В следующей статье будет рассматриваться диагностика и проверка ошибок.
Об авторе: Колин Уоллс уже более тридцати лет работает в сфере электронной промышленности, значительную часть времени уделяя встроенному ПО. Сейчас он — инженер в области встроенного ПО в Mentor Embedded (подразделение Mentor Graphics). Колин Уоллс часто выступает на конференциях и семинарах, автор многочисленных технических статей и двух книг по встроенному ПО. Живет в Великобритании. Профессиональный блог Колина, e-mail: colin_walls@mentor.com.