Nucleus SE RTOS initialization and start-up

March 12, 2019

Colin Walls-March 12, 2019

For any kind of operating system, there is some type of start-up mechanism. Exactly how this works varies from one system to another. It is usual to say that an OS will “boot”. This is an abbreviation for “bootstrap”, which is a description of how a CPU gets from having a memory full of nothing in particular to a stable program execution state. Classically, a small piece of software is loaded into memory; it may simply be held in ROM. In years past, it may have been keyed in from the switches on the front panel of the computer. This “boot loader” would read in a more sophisticated bootstrap program, which, in turn, would load and start the operating system. This is the process whereby a desktop computer gets started today; code in the BIOS seeks bootable devices (hard drives or CD-ROMs) from which a bootstrap and, hence, an OS is loaded.

An OS for an embedded system may also be initialized in this way. Indeed, embedded OSes, which are derived from desktop operating systems, do exactly that. But for most “classic” RTOSes, a much simpler (and hence faster) process is used.

An OS is just a piece of software. If that software is already in memory – in some form of ROM, for example – it is simply a matter of arranging for the CPU’s reset sequence to end up with the execution of the OS’s initialization code. This is how most RTOSes work and Nucleus SE is no exception.

Most embedded software development toolkits include the necessary start-up code to handle a CPU reset and arrive at the entry point to the main() function. The Nucleus SE distribution code does not concern itself with this process, as it is intended to be as portable as possible. Instead, it provides a main() function, which takes control of the CPU and initializes and starts the OS; this is described in detail shortly.

Memory Initialization

The declarations of all the static variables in the Nucleus SE code are prefixed with ROM or RAM to indicate where they might be sensibly located. These two #define symbols are defined in nuse_types.h and should be set up to accommodate the capabilities of the development toolkit (compiler and linker) in use. Typically, ROM may be set to const and RAM left blank.

All ROM variables are statically initialized, which is logical. No RAM variables are statically initialized (as this will only work with certain toolkits, which arrange for an automatic copy from ROM to RAM); explicit initialization code is included, of which more in the course of this article.

Nucleus SE does not keep any “constant” data in RAM, which, in small systems, may be in short supply. Instead of using complex data structures to describe kernel objects, a series of tables (arrays) are employed, which are easily located in ROM or RAM, as appropriate.

The main() Function

Here is the complete code for the Nucleus SE main() function:

void main(void)
       NUSE_Init();        /* initialize kernel data */
       /* user initialization code here */
       NUSE_Scheduler();   /* start tasks */

Drag the corner of the box to expand as needed. ↑

The sequence of operations is quite straightforward:

  • The NUSE_Init() function is called first. This initializes all the Nucleus SE data structures and is outlined in more detail below.

  • Next, there is the opportunity for the user in insert any application-specific initialization code, which will be executed prior to the start of the task scheduler. More details on what can be achieved by this code may be found later in this article.

  • Lastly, the Nucleus SE scheduler (NUSE_Scheduler()) is started. This also is examined in more detail later in this article.

The NUSE_Init() Function

This function initializes all the Nucleus SE kernel variables and data structures. Here is the complete code:

void NUSE_Init(void)
       U8 index;
       /* global data */
       NUSE_Task_Active = 0;
           NUSE_Tick_Clock = 0;
           NUSE_Time_Slice_Ticks = NUSE_TIME_SLICE_TICKS;
       /* tasks */
           for (index=0; index<NUSE_TASK_NUMBER; index++)
       /* partition pools */
           for (index=0; index<NUSE_PARTITION_POOL_NUMBER; index++)
       /* mailboxes */
       #if NUSE_MAILBOX_NUMBER != 0
           for (index=0; index<NUSE_MAILBOX_NUMBER; index++)
       /* queues */
       #if NUSE_QUEUE_NUMBER != 0
           for (index=0; index<NUSE_QUEUE_NUMBER; index++)
       /* pipes */
       #if NUSE_PIPE_NUMBER != 0
           for (index=0; index<NUSE_PIPE_NUMBER; index++)
       /* semaphores */
           for (index=0; index<NUSE_SEMAPHORE_NUMBER; index++)
       /* event groups */
           for (index=0; index<NUSE_EVENT_GROUP_NUMBER; index++)
       /* timers */
       #if NUSE_TIMER_NUMBER != 0
           for (index=0; index<NUSE_TIMER_NUMBER; index++)

Drag the corner of the box to expand as needed. ↑

First, some global variables are initialized:

  • NUSE_Task_Active – the index of the currently active task – is set to zero; this may be modified by the scheduler in due course.

  • NUSE_Task_State is set to NUSE_STARTUP_CONTEXT, which indicates the limited API functionality to any following application initialization code.

  • If system time support is enabled, NUSE_Tick_Clock is set to zero.

  • If the time slice scheduler has been enabled, NUSE_Time_Slice_Ticks is set up to the configured time slice value, NUSE_TIME_SLICE_TICKS.

Then, a series of functions are called to initialize kernel objects:

  • NUSE_Init_Task() is called to initialize data structures for each task. This call is only omitted if the Run to Completion scheduler is selected and signals, task suspend, and schedule counting are all not configured (as this combination would result in there being no RAM data structures appertaining to tasks and, hence, no initialization to be done).

  • NUSE_Init_Partition_Pool() is called to initialize each partition pool object. The calls are omitted if no partition pools have been configured.

  • NUSE_Init_Mailbox() is called to initialize each mailbox object. The calls are omitted if no mailboxes have been configured.

  • NUSE_Init_Queue() is called to initialize each queue object. The calls are omitted if no queues have been configured.

  • NUSE_Init_Pipe() is called to initialize each pipe object. The calls are omitted if no pipes have been configured.

  • NUSE_Init_Semaphore() is called to initialize each semaphore object. The calls are omitted if no semaphores have been configured.

  • NUSE_Init_Event_Group() is called to initialize each event group object. The calls are omitted if no event groups have been configured.

  • NUSE_Init_Timer() is called to initialize each timer object. The calls are omitted if no timers have been configured.

Initializing Tasks

Here is the complete code for NUSE_Init_Task():

void NUSE_Init_Task(NUSE_TASK task)
           NUSE_Task_Context[task][15] =                 /* SR */
           NUSE_Task_Context[task][16] =                 /* PC */
           NUSE_Task_Context[task][17] =                 /* SP */
               (U32 *)NUSE_Task_Stack_Base[task] +
           NUSE_Task_Signal_Flags[task] = 0;
           NUSE_Task_Timeout_Counter[task] = 0;
               NUSE_Task_Status[task] =
               NUSE_Task_Status[task] = NUSE_READY;
           NUSE_Task_Schedule_Count[task] = 0;

Drag the corner of the box to expand as needed. ↑

Unless the Run to Completion scheduler has been configured, the context block – NUSE_Task_Context[task][] – for the task is initialized. Most entries are not set to a value, as they represent general machine registers which are assumed to have an indeterminate value when the task starts up. In the example (Freescale ColdFire) implementation of Nucleus SE (and this would be similar for any processor) the last three entries are set up explicitly:

  • NUSE_Task_Context[task][15] holds the status register (SR) and is set to the value in the #define symbol NUSE_STATUS_REGISTER.

  • NUSE_Task_Context[task][16] holds the program counter (PC) and is set to the address of the entry point of the task’s code: NUSE_Task_Start_Address[task].

  • NUSE_Task_Context[task][17] holds the stack pointer (SP), which is initialized to a value computed by adding the address of the task’s stack base (NUSE_Task_Stack_Base[task]) to the task’s stack size (NUSE_Task_Stack_Size[task]).

If signal support is enabled, the task’s signal flags (NUSE_Task_Signal_Flags[task]) are set to zero.

If task sleep (i.e. the API call NUSE_Task_Sleep()) is enabled, the task’s timeout counter (NUSE_Task_Timeout_Counter[task]) is set to zero.

If task suspend is enabled, the task’s status (NUSE_Task_Status[task]) is initialized. This initial value is user specified (in NUSE_Task_Initial_State[task]), if task initial task state support is enabled. Otherwise the status is set to NUSE_READY.

If task schedule counting is enabled, the task’s counter (NUSE_Task_Schedule_Count[task]) is set to zero.

Continue reading on page two >>

< Previous
Page 1 of 2
Next >

Loading comments...