NetBurner 3.3.8a
PDF Version
NetBurner RTOS

The NetBurner Real-Time Operating System (NBRTOS) a full featured preemptive multitasking real-time operating system, supporting multiple tasks, semaphores, mail boxes, FIFOs, queues, and critical sections just to name a few. As part of the NetBurner development package, the RTOS is pre-configured and running. The task you will normally start with is named UserMain().

There are 255 priority levels (1 through 255). The lower the number, the higher the priority. Some of these tasks are reserved by the system, such as the idle task. You can specify a priority when the task is created, and change the priority later if you wish. A priority level can only be used by one task at a time. Be sure to check the return values when creating a task or changing a task priority to verify that the operation was successful.



What is a Preemptive RTOS?

Preemptive Operation, Priorities and Blocking

In a preemptive operating system the highest priority task ready to run will always run. This is an extremely important point, so we will mention it again: the highest priority task ready to run will always run. This is different than a Windows or Unix system that employs a round-robin in which each task will run based on its time slice. If you create a high priority task that can always run and never blocks, then no lower priority tasks will ever run. Lower priority tasks can only run when a higher priority task blocks on a resource or blocking function call.

For example, there are two tasks A and B. Task A has priority 50 and Task B has priority 51. Since Task A is of higher priority, it will always run (and Task B will never run) unless it calls a blocking function. Task B will then run for as long as Task A blocks; this could be 1 second due to a call to OSTimeDly(TICKS_PER_SECOND), until a shared resource is available due to a call to OSSemPend(), or until data is available from a network connection due to a select() or read(). It both tasks are in a blocking state, then the RTOS idle task will run.

RTOS Blocking Functions

Generally any function that pends on a resource or creates a time delay:

I/O Functions That Can Block

Generally any function that does a read operation or pends on a file descriptor

  • select()
  • read(), including variants with timeouts
  • write(), blocks until at least one char can be written
  • gets()
  • getchar()
  • fgets()

Network Functions That Can Block

Generally calls that will pend on an incoming connection or received data

Functions That Can Enable a Task To Be "Ready To Run"

Generally functions that post to pending objects

System Task Priorities

The number and type of system tasks and priorities used by the system will depend on your platform and the system features used by your application. For example, if your application calls StartHTTP( ) to enable web services, then a system task will be created that handles web server requests. The system task priority levels are defined in \nburn\nbrtos\include\constants.h.



Task Creation

Whether you use the Application Wizard in NBEclipse to create a new application, or start with one of the example programs, you will notice that the point at which your application takes control is a function named UserMain(). UserMain() is a task created by the system. The first few lines will consist of system initialization functions such as init() and StartHttp().

Additional tasks are created with the OSTaskCreatewName() or OSSimpleTaskCreatewName() functions. The following is an example program that demonstrates the use of each function. When using OSTaskCreaewName() you specify all parameters:

  • Task function name
  • Optional parameter to pass to the task, NULL if not used
  • Top of the task stack
  • Bottom of the task stack
  • Task priority
  • Task name

A significant benefit of the full function call is that it returns the status of the task creation process. For example, one possible error is that you cannot create a second task at the same priority as any other task.

The 'OSSimpleTaskCreatewName()' implementation lets you specify just the task function, priority, and name.

/*
* This example demonstrates how to use two of the task creation
* functions:
* - OSSimpleTaskCreatewName()
* - OSTaskCreatewName()
*
*/
#include <init.h>
#include <stdlib.h>
#include <nbrtos.h>
#include <system.h>
#include <utils.h>
const char *AppName = "OSTaskCreate Example";
// Allocate stack space
uint32_t TaskAllParamsStack[USER_TASK_STK_SIZE];
/*-----------------------------------------------------------------------------
* Task created with OSTaskCreatewName() using all function parameters.
* The void *pd parameter can be cast to any type of data you wish to send to
* the task, or NULL if not used.
*----------------------------------------------------------------------------*/
void TaskAllParams(void *pd)
{
uint32_t loopCount = 0;
uint32_t delayTime = (uint32_t)pd;
iprintf("TaskAllParams delay time set to: %ld seconds\r\n", delayTime);
while (1)
{
iprintf("TaskAllParams, %ld\r\n", loopCount);
loopCount++;
OSTimeDly(TICKS_PER_SECOND * delayTime);
}
}
/*-----------------------------------------------------------------------------
* Task created with OSSimpleTaskCreatewName(). The primary difference from the
* full version is that it allocates the task stack for you.
*----------------------------------------------------------------------------*/
void TaskSimple(void *pd)
{
uint32_t loopCount = 0;
uint32_t delayTime = 6;
iprintf("TaskSimple delay time set to: %ld seconds\r\n", delayTime);
while (1)
{
iprintf("TaskSimple, %ld\r\n", loopCount);
loopCount++;
OSTimeDly(TICKS_PER_SECOND * delayTime);
}
}
/*-----------------------------------------------------------------------------
* UserMain
*----------------------------------------------------------------------------*/
void UserMain(void *pd)
{
uint32_t delayTime = 3;
int returnCode;
init(); // Initialize network stack
WaitForActiveNetwork(TICKS_PER_SECOND * 5); // Wait for DHCP address
iprintf("Creating TaskAllParams....");
returnCode = OSTaskCreatewName( TaskAllParams, // Task function name
(void *)delayTime, // Optional parameter to pass to the task function
&TaskAllParamsStack[USER_TASK_STK_SIZE], // Top of stack
TaskAllParamsStack, // Bottom of stack
MAIN_PRIO - 1, // Task priority
"TaskAllParams" ); // Task name
if (returnCode == OS_NO_ERR)
iprintf("Task creation successful\r\n");
else
iprintf("*** Error: status = %d\r\n", returnCode);
iprintf("Creating TaskSimple\r\n");
OSSimpleTaskCreatewName(TaskSimple, MAIN_PRIO - 2, "TaskSimple");
while (1)
{
OSTimeDly(TICKS_PER_SECOND * 1);
}
}
uint8_t OSTaskCreatewName(void(*task)(void *dptr), void *data, void *pstktop, void *pstkbot, uint8_t prio, const char *name)
This function creates a new task.
#define OSSimpleTaskCreatewName(x, p, n)
This macro functions the same as OSTaskCreatewName().
Definition: nbrtos.h:1410
void OSTimeDly(uint32_t to_count)
Delay the task until the specified value of the system timer ticks. The number of system ticks per se...
Definition: nbrtos.h:1471
#define OS_NO_ERR
No Error.
Definition: nbrtos.h:58
void init()
System initialization. Normally called at the beginning of all applications.
Definition: init.cpp:27
bool WaitForActiveNetwork(uint32_t ticks_to_wait=120 *TICKS_PER_SECOND, int interface=-1)
Wait for an active network connection on at least one interface.
Definition: dhcpc.cpp:1554
NetBurner System Initialization Header File.
NetBurner Real-Time Operating System API.
NetBurner System Functions.



Protecting Shared Data

The following RTOS mechanisms can be used to protect shared data resources. They are listed in a decreasing order of severity as regarding system latency (all pend and post functions are at the same level).

OSSemPend() OSSemPost() Protects an area of memory or resource. A task calls OSSemPend, which will block until the resource is available. OSSemPost releases the resource.
OSMboxPend() OSMboxPost() Same as semaphore, except a pointer variable is passed as the “message”. A task or ISR can post a message, but only a task can pend on a message. Both the posting task and pending task must agree on what the pointer points to.
OSQPend() OSQPost() A Queue is basically an array of mailboxes. It is used to post one or more messages. When you initialize a Queue, you must specify the maximum number of messages. The first message posted to the queue will be the first message extracted from the queue (FIFO).
OSFifoPend() OSFifoPost() OSFifoPostFirst() OSFifoPendNoWait() A FIFO is similar to a queue, but is specifically designed to pass pointers to OS_FIFO structures. The first parameter of the structure must be a (void *) element, which is used by the operating system to create a linked list of FIFOs. When initializing a FIFO, you do not specify the maximum number of entries as with a queue. Instead, your application has the ability (and responsibility) to allocate memory (static or dynamic) in which to store the structures. This can be done statically by declaring global variables, or dynamically by allocating memory from the heap. As with a queue, the first message posted to the FIFO will be the first message extracted from the queue.
OSCritEnter OSCritExit OSCritObj This is a counted critical section that restricts access to resources to one task at a time, sometimes called a “mutex”. For example, you have a linked list that is maintained by 3 separate tasks. If one task is manipulating the list, you could first call OSCirtEnter for that object (the list). If any other task tries to manipulate the list, it will block at the OSCritEnter until the task that previously called OSCritEnter, calls OSCritExit. Note that the number of enter calls must match number of exit calls. OSCritObj is a C++ implementation that uses scoping to automatically call the enter and exit functions. See example below.
OSLock() OSUnlock() OSLockObj Disables other tasks, but not interrupts. Increments for each OSLock, decrements for each OSUnlock. The C++ object OSLockObj was created to assist in making sure that an unlock is called for each lock. When an OSLockObj is created, the constructor calls OSLock( ). When the object goes out of scope, OSUnlock( ) is automatically called by the destructor.
USER_ENTER_CRITICAL() USER_EXIT_CRITICAL() Macro that disables other tasks and interrupts. Increments count on enter, decrements on exit.

How do you decide which type of mechanism to use? Some guidelines are:

  • If you need some type of signal, but do not need to pass any data, use a Semaphore. A semaphore is a single 32-bit integer that increments and decrements for each pend or post.
  • If you want to pass a single 32-bit number, you can use a Mailbox or Queue. Most applications use the 32-bit number as the data, but it could also be a pointer to a structure or object. A queue is like an array of mailboxes. You declare the number of queue entries a compile time.
  • If you want to pass a structure or object, then use a FIFO. You may be wondering how a FIFO differs from a Queue. The difference is that a Queue has a predefined number of entries. The FIFO implementation uses a linked list, so the only limit to the number of entries is available memory. Using a FIFO is not as simple as any of the other mechanisms, because your application must implement some type of memory management to allocate and deallocate the FIFO objects. This is usually done by managing a predeclared array of objects, or through dynamic memory allocation. We encourage all embedded designers to avoid dynamic memory allocation if at all possible, since in any embedded system memory fragmentation could eventually occur and the call to allocate a new object could fail. If you create an array of objects at compile time you will always be guaranteed the maximum number can exist.



Semaphore

A semaphore is a protected variable that is used to control access to shared system resources (such as memory or serial ports), to signal the occurrence of events and task synchronization. A task can request a semaphore and wait until the resource or event takes place (called pending). A task can also post to a semaphore to indicate it no longer needs a resource, or to signal an event has taken place. To create a semaphore you declare one of type OS_SEM and initialize with OSSemInit(): OS_SEM MySemaphore; OSSemInit( &MySemaphore, 0 ); // set initial value to 0

Your application tasks can now use the post and pend functions on the semaphore: OSSemPost( &MySemaphore ); // post to a semaphore OSSemPend( &MySemaphore, 0 ); // pend on a semaphore

The second parameter in the OSSemPend() function specifies the number of time ticks to wait. A value of 0 waits forever. A good way to express a wait value is to use the TICKS_PER_SECOND definition provided by the RTOS: OSSemPend( &MySemaphore, TICKS_PER_SECOND * 5) to wait 5 seconds.



Queues

A message queue is an object that enables tasks and interrupt service routines to pend and post pointer sized messages. The pointer values typically point to some type of object or structure that contains the actual message or data.



FIFO

A FIFO is similar to a queue, but is specifically designed to pass pointers to OS_FIFO structures. The first parameter of the structure must be a (void *) element, which is used by the operating system to create a linked list of FIFOs. When initializing a FIFO, you do not specify the maximum number of entries as with a queue. Instead, your application has the ability (and responsibility) to allocate memory (static or dynamic) in which to store the structures. This can be done statically by declaring global variables, or dynamically by allocating memory from the heap. As with a queue, the first message posted to the FIFO will be the first message extracted from the queue.



Critical Section

OSCritEnter(), OSCritExit() and an OSCritObj enable an application to use counted critical sections that restrict resource access to one task at a time (also called a “mutex”). For example, you have a linked list that is maintained by 3 separate tasks. If one task is manipulating the list, you call OSCritEnter() before modifying the list. If any other task tries to access the list, the task will block at the OSCritEnter() call until the task that previously called OSCritEnter(), calls OSCritExit(). Since this is a counting critical section implementation, the number of enter calls must match number of exit calls for each task.

OSCritObj is a C++ implementation that uses scoping to automatically call the enter and exit functions so you do not need to manually match each enter with an ext. In comparison with OsLock(), OsCritEnter() does not restrict task swapping unless two tasks want to access the same resource. OsLock() prevents all task swapping.



OS Flags

OSFlags enables a function or task to pend on multiple flags or events, in contrast to a OSSemaphore which can pend on only a single event. The OSFlag implementation is essentially a 32-bit bitmap in which each bit position represents a “flag”. You create a OSFlag object with OSFlagCreate(), then set, clean and read the flags with the appropriate function. There are a number of functions used to monitor or pend on the flags, and provide the ability to pend on any one or more of the flags being set, or pending on all of flags being set at one time.

Flag Functions

  • OSFlagSet() Set the bits asserted with bits_to_set
  • OSFlagState() Return the current value of the flags
  • OSFlagClear() Clear the bits asserted in bits_to_clr
  • OSFlagPendAll() Wait until all of the flags indicated by mask are set
  • OSFlagPendNoWait() Check (but do not wait) if all of the flags indicated by the mask are set
  • OSFlagPendAny() Wait until any of the flags indicated by the bit mask are set
  • OSFlagPendAnyNoWait() Check (but do not wait) if any of the flags indicated by the mask are set