Nucleus SE RTOS initialization and start-up

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_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_Init_Task(index);
           }
       #endif
       /* partition pools */
       #if NUSE_PARTITION_POOL_NUMBER != 0
           for (index=0; index           {
                  NUSE_Init_Partition_Pool(index);
           }
       #endif
       /* mailboxes */
       #if NUSE_MAILBOX_NUMBER != 0
           for (index=0; index           {
                  NUSE_Init_Mailbox(index);
           }
       #endif
       /* queues */
       #if NUSE_QUEUE_NUMBER != 0
           for (index=0; index           {
                  NUSE_Init_Queue(index);
           }
       #endif
       /* pipes */
       #if NUSE_PIPE_NUMBER != 0
           for (index=0; index           {
                  NUSE_Init_Pipe(index);
           }
       #endif
       /* semaphores */
       #if NUSE_SEMAPHORE_NUMBER != 0
           for (index=0; index           {
                  NUSE_Init_Semaphore(index);
           }
       #endif
       /* event groups */
       #if NUSE_EVENT_GROUP_NUMBER != 0
           for (index=0; index           {
                  NUSE_Init_Event_Group(index);
           }
       #endif
       /* timers */
       #if NUSE_TIMER_NUMBER != 0
           for (index=0; index           {
                  NUSE_Init_Timer(index);
           }
       #endif
}
 

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)
{
       #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
}
 

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.

Initializing Partition Pools

Here is the complete code for 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
}

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

The partition pool’s “used” counter (NUSE_Partition_Pool_Partition_Used[pool] ) is set to zero.

If task blocking is enabled, the partition pool’s blocked task counter (NUSE_Partition_Pool_Blocking_Count[pool] ) is set to zero.

Initializing Mailboxes

Here is the complete code for 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
}

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

The mailbox’s data store (NUSE_Mailbox_Data[mailbox] ) is set to zero and its status (NUSE_Mailbox_Status[mailbox] ) is set to “unused” (i.e. zero).

If task blocking is enabled, the mailbox’s blocked task counter (NUSE_Mailbox_Blocking_Count[mailbox] ) is set to zero.

Initializing Queues

Here is the complete code for 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
}

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

The queue’s head and tail pointers (actually, they are indexes – NUSE_Queue_Head[queue] and NUSE_Queue_Tail[queue] ) are set to point to the start of the queue data area (i.e. given the value zero). The queue’s item counter (NUSE_Queue_Items[queue] ) is also set to zero.

If task blocking is enabled, the queue’s blocked task counter (NUSE_Queue_Blocking_Count[queue] ) is set to zero.

Initializing Pipes

Here is the complete code for 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
}

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

The pipe’s head and tail pointers (actually, they are indexes – NUSE_Pipe_Head[pipe] and NUSE_Pipe_Tail[pipe] ) are set to point to the start of the pipe data area (i.e. given the value zero). The pipe’s item counter (NUSE_Pipe_Items[pipe] ) is also set to zero.

If task blocking is enabled, the pipe’s blocked task counter (NUSE_Pipe_Blocking_Count[pipe] ) is set to zero.

Initializing Semaphores

Here is the complete code for 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
}

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

The semaphore’s counter (NUSE_Semaphore_Counter[semaphore] ) is initialized to the user-specified value (NUSE_Semaphore_Initial_Value[semaphore] ).

If task blocking is enabled, the semaphore’s blocked task counter (NUSE_Semaphore_Blocking_Count[semaphore] ) is set to zero.

Initializing Event Groups

Here is the complete code for 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
}

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

The event group’s flags are cleared; i.e. NUSE_Event_Group_Data[group] is set to zero.

If task blocking is enabled, the event group’s blocked task counter (NUSE_Event_Group_Blocking_Count[group] ) is set to zero.

Initializing Timers

Here is the complete code for 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;
}

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

The timer’s status (NUSE_Timer_Status[timer] ) is set to “unused”; i.e. FALSE .

Its count-down value (NUSE_Timer_Value[timer] ) is initialized to the user-specified value (NUSE_Timer_Initial_Time[timer] ).

Its expiration counter (NUSE_Timer_Expirations_Counter[timer] ) is set to zero.

Application Code Initialization

Once the Nucleus SE data structures have been initialized, there is the opportunity for code to be executed that performs initialization of the application prior to task execution. There are many possible uses for this capability:

  • Initialization of application data structures. Explicit assignments are easier to understand and debug than allowing automatic initialization of static variables.

  • Kernel object assignments. Given that all kernel objects are created statically at build time and are identified by index values, it may be useful to assign “ownership” or define the usage of these objects. This might be done using #define symbols, but, if there are multiple instances of tasks, the object indexes may be best assigned through global arrays (indexed by the task’s ID).

  • Device initialization. This may be a good opportunity to set up any peripheral devices.

Obviously, many of these things could have been achieved before Nucleus SE initialization has been performed, but the advantage of locating the application initialization code here is that kernel services (API calls) may now be employed. For example, a queue or a mailbox may be pre-loaded with data to be processed when a task starts.

There is a limitation on which API calls are permitted: no action may be taken that would normally result in invocation of the scheduler – e.g. task suspension/blocking. The global variable NUSE_Task_State has been set to NUSE_STARTUP_CONTEXT to reflect this restriction.

Starting the Scheduler

Once initialization has been completed, it only remains to start the scheduler in order to commence execution of the application code – the tasks. The scheduler options and operation of the various types of scheduler were covered in detail in an earlier article, so only a brief summery is required here.

The key points in the sequence are:

  • Set the global variable NUSE_Task_State to NUSE_TASK_CONTEXT .

  • Select the index of the first task to be run. If support for initial task state is enabled, a search is performed for the first ready task; otherwise the value 0 is used.

  • The scheduler – NUSE_Scheduler() – is called.

Exactly what occurs in the last step depends on which scheduler type has been selected. For Run to Completion, the scheduling loop is entered and tasks are called in sequence. For other scheduler types, the context for the first task is loaded and control passed to the task.

The next article in this series will take a look at diagnostics and error checking.


Colin Walls has nearly forty years’ experience in the electronics industry, largely dedicated to embedded software. A frequent presenter at conferences and seminars and author of numerous technical articles and two books on embedded software, Colin is an embedded software technologist with Mentor, a Siemens business, and is based in the UK. His regular blog is located at: http://blogs.mentor.com/colinwalls. He may be reached by email at colin_walls@mentor.com

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.