Using the Nucleus SE real-time operating system
So far in this series of articles we have looked in detail at all the facilities that Nucleus SE has to offer. Now it is time to see how to use it for a real embedded software application.
What is Nucleus SE?
We know that Nucleus SE is a real time kernel, but it is important to understand how that fits in with the rest of an application. “Fitting in” is exactly what it does, because, unlike a desktop operating system, like Windows, you do not really run an application on Nucleus SE; the kernel is simply part of the application software that runs on the embedded device. This is very commonly the case with real time operating systems.
At the highest level, a conventional embedded application is some code, that is run when the CPU is reset. This initializes the hardware and software environment and then calls the main() function, which is the start of the application code. What is different, when using Nucleus SE (or many other similar kernels) is that the main() function is supplied as part of the kernel code. This function simply initializes all the kernel data structures, then it calls the scheduler, which results in the application code (tasks) being run. The user may wish to add any application specific initialization to the main() function.
Nucleus SE also includes a selection of functions – the application program interface (API) – that provide a range of facilities like inter-task communication and synchronization, timing, memory allocation etc. All the API functions have been described earlier in this series.
All the Nucleus SE software is provided as (mostly C language) source code. Conditional compilation is used to configure the code to the requirements of a specific application. This is described in detail in Configuration later in this article.
When the code has been compiled, the resulting Nucleus SE object modules are linked with those of the application code to result in a single binary image, which would normally be placed in flash memory in the embedded device. The result of such a static link is that all symbolic information may remain available – both from the application code and the kernel. This is a useful aid to debugging, but care is needed to avoid misuse of Nucleus SE data.
CPU and Tool Support
Since Nucleus SE is supplied in source code, the intention is for it to be portable. However, it is impossible for code that works at such a low level (i.e. with a scheduler other than run to completion, where context switches are required) to be totally free of assembly language. But I have minimized this, and very little low-level coding should be needed to port to a new CPU. Using a new development toolkit (compiler, assembler, linker etc.) may also raise portability issues.
Configuring a Nucleus SE Application
The key to using Nucleus SE efficiently is getting the configuration right. This may appear complex but is actually quite logical and just needs to be approached systematically. Almost all the configuration is performed by editing two files: nuse_config.h and nuse_config.c.
Setting up nuse_config.h
This file is simply a list of #define symbols, which are set to appropriate values to specify the desired kernel configuration. In the default nuse_config.h file, all the symbols are present, but set to a minimal configuration.
The number of each kernel object type is set by assigning values to symbols of the form NUSE_SEMAPHORE_NUMBER. For most objects, the value can be 0 to 15. Tasks are the exception, as there must be at least one. Signals are not really objects in their own right, as they are associated with tasks and enabled by setting NUSE_SIGNAL_SUPPORT to TRUE.
API Function Enables
Each Nucleus SE API function may be individually enabled by setting a symbol with the same name as the function – e.g. NUSE_PIPE_JAM – to TRUE. This results in the code for the function being included in the application.
Scheduler Selection and Settings
Nucleus SE supports four types of scheduler, as described in detail in an earlier article. The type of scheduler to be used for an application may be specified by setting NUSE_SCHEDULER_TYPE to one of these values: NUSE_RUN_TO_COMPLETION_SCHEDULER, NUSE_TIME_SLICE_SCHEDULER, NUSE_ROUND_ROBIN_SCHEDULER or NUSE_PRIORITY_SCHEDULER.
Other aspects of the scheduler may also be set up:
NUSE_TIME_SLICE_TICKS specifies the number of ticks per slot for the time slice scheduler. If another scheduler is in use, this must be set to 0.
NUSE_SCHEDULE_COUNT_SUPPORT can be set to TRUE or FALSE to enable/disable the task scheduler counting mechanism.
NUSE_SUSPEND_ENABLE facilitates support for task suspend. If this is set to FALSE, tasks can never be suspended and are always ready to be run. If the priority scheduler is used, this option must be set to TRUE.
NUSE_BLOCKING_ENABLE enables task blocking (suspend) on many API functions. This means that a call to such a function may result in suspension of the calling task, pending the availability of a resource. Selecting this option requires that NUSE_SUSPEND_ENABLE is also set to TRUE.
A few other options may be set to TRUE or FALSE to enable/disable other kernel functionality:
NUSE_API_PARAMETER_CHECKING enables inclusion of code to verify API function call parameters and would normally be set during debugging.
NUSE_INITIAL_TASK_STATE_SUPPORT enables the initial state of all tasks to be specified as NUSE_READY or NUSE_PURE_SUSPEND. If this facility is disabled, all tasks start with the status NUSE_READY.
NUSE_TIMER_EXPIRATION_ROUTINE_SUPPORT enables support for a call to a function to be made when an application timer expires. If it is disabled, no action is taken on timer expiry.
NUSE_SYSTEM_TIME_SUPPORT enables the system tick clock.
NUSE_INCLUDE_EVERYTHING is an option to include as much as possible into the Nucleus SE configuration. It results in the activation of all optional functionality and every API function for objects that have been configured. It is used as shorthand to create a Nucleus SE configuration to exercise a new port of the kernel code.
Setting up nuse_config.c
After specifying the kernel configuration in nuse_config.h, various ROM based data structures need to be initialized. This is done in nuse_config.c. Definition of the data structures is controlled by conditional compilation, so they are all present in the default copy of nuse_config.c.
The array NUSE_Task_Start_Address should be initialized with the start addresses for each task – this is normally just a list of function names (without the parentheses). Prototypes for the task entry functions must also be visible. In the default file, a single task is configured with the name NUSE_Idle_Task() – this may be replaced with an application task.
Unless the run to completion scheduler is in use, each task requires its own stack. An array in RAM much be created for each task’s stack. The arrays should be of type ADDR and the address of each one placed in NUSE_Task_Stack_Base. Estimating the size of arrays is difficult and best done by measurement – see Debugging later in this article. The size of each array (i.e. stack size in words) should be placed in NUSE_Task_Stack_Size.
If the facility to specify initial task status has been enabled (using NUSE_INITIAL_TASK_STATE_SUPPORT), the array NUSE_Task_Initial_State should be initialized to NUSE_READY or NUSE_PURE_SUSPEND.
Partition Pool Data
If any partition pools are configured, an array (of type U8) needs to be defined in RAM for each one. The size of these arrays is computed thus: (number of partitions * (partition size + 1)). The addresses of these arrays (i.e. just their names) should be assigned to the appropriate elements of NUSE_Partition_Pool_Data_Address. For each pool, the number of partitions and the size of those partitions should be assigned into NUSE_Partition_Pool_Partition_Number and NUSE_Partition_Pool_Partition_Size respectively.
If any queues are configured, an array (of type ADDR) needs to be defined in RAM for each one. The size of these arrays is simply the number of elements required in each queue. The addresses of these arrays (i.e. just their names) should be assigned to the appropriate elements of NUSE_Queue_Data. For each queue, its size should be assigned into the appropriate element of NUSE_Queue_Size.
If any pipes are configured, an array (of type U8) needs to be defined in RAM for each one. The size of these arrays is computed thus: (pipe size * pipe message size). The addresses of these arrays (i.e. just their names) should be assigned to the appropriate elements of NUSE_Pipe_Data. For each pipe, its size and message size should be assigned into the appropriate elements of NUSE_Pipe_Size and NUSE_Pipe_Message_Size respectively.
If any semaphores are configured, the array NUSE_Semaphore_Initial_Value needs to be initialized to the down-counter start values.
Application Timer Data
If any timers are configured, the array NUSE_Timer_Initial_Time needs to be initialized to the start values for the counters. Also, NUSE_Timer_Reschedule_Time needs to be set to the restart values. These are counter values to be used after the initial sequence expires. If the restart values are set to 0, the counter stops after one cycle.
If support for timer expiration routines is configured (by setting NUSE_TIMER_EXPIRATION_ROUTINE_SUPPORT to TRUE), two more arrays need to be initialized. The addresses of the expiration routines (just a list of function names (without the parentheses) should be assigned to NUSE_Timer_Expiration_Routine_Address. The array NUSE_Timer_Expiration_Routine_Parameter should be initialized to the expiration routine parameter values.