5 OS Adaptation Layer

The OS adaptation layer controls how the NDK uses RTOS resources. This includes its use of tasks, semaphores, and memory. In most cases, the only API described in this chapter that your NDK application will call is TaskCreate().

5.1 About OS Adaptation

The NDK uses an OS adaptation layer so that applications can use any supported RTOS. Currently, the TI-RTOS Kernel and FreeRTOS are supported for NDK applications. This portability is achieved through OS adaptation APIs that are converted into native OS thread calls.

This chapter describes the APIs that perform OS adaptation.

In most cases, your user application code will call only TaskCreate(). However, you can modify the source code of the adaptation layer as needed. The source code for the OS adaptation layer is provided in the /ti/ndk/os directory:

5.1.1 Source Files

Source code for the OS library consists of the following files, which are located in the /ti/ndk/os directory:

File

Description

mem.c

Memory allocation and memory copy functions

mem_data.c

Memory allocation definitions and #pragmas

oem.c

OEM Cache and System functions

ossys.c

Additional OS support (debug logging, stricmp() function)

semaphore.c

Semaphore abstraction

task.c

Task thread abstraction

Two additional include files are located in the /ti/ndk/inc/os directory:

File

Description

osif.h

Interface specifications to the adaptation library

oskern.h

Semi-private declarations for use by functions like NETCTRL

5.2 Task Thread Abstraction: TASK.C

The task.c module contains a subset of the Task abstraction API documented in the NDK API Reference Guide. It also contains the source code for the stack’s exclusion method functions: llEnter() and llExit(). The latter are discussed in “Choosing the llEnter()/llExit() Exclusion Method” of this document.

In most cases, your user application code will call only TaskCreate().

Most of the Task and Semaphore functions defined in the NDK API Reference Guide are macros that call an RTOS. The macros are defined in inc/os/osif.h. The functions that may need special handling are described in the subsections that follow.

5.2.1 TaskCreate(), TaskExit(), and TaskDestroy()

The create, exit and destroy functions all call their native OS thread API equivalents.

If you are using the TI-RTOS Kernel, for TaskExit() and TaskDestroy() to function as expected, the Task.deleteTerminatedTasks configuration parameter must be set to “true”. This parameter setting instructs the Task module to delete completed Tasks in the Idle task. Part of cleaning up involves freeing Task stack memory.

The NDK configuration sets the Task.deleteTerminatedTasks to “true” automatically. If your application does not use the NDK Global module for configuration, it must manually set this parameter. For example:

var Task = xdc.useModule('ti.sysbios.knl.Task');
Task.deleteTerminatedTasks = true;

**NOTE:** In previous releases, the NDK did not require this configuration setting. If this configuration setting is not made, Task clean-up will not occur, and out of memory errors may occur because the task stacks and objects will not be freed when ``TaskExit()`` and/or ``TaskDestroy()`` is called.

5.2.2 Choosing the llEnter()/llExit() Exclusion Method

Although the NDK provides a reentrant environment, the core of the stack is not reentrant. Portions of the code must be protected from access by reentrant calls. Instead of using critical sections that block all other Task execution, the software defines an operating mode called kernel mode. Kernel mode is defined such that only one Task may be in kernel mode at any given time. It does nothing to prevent Tasks from running that do not use the NDK. This provides protection for the stack software, without affecting the execution of unrelated code.

The llEnter() and llExit() functions are used throughout the stack code to enter and exit kernel mode, and provide code exclusion without using critical sectioning. They are equivalent to the splhigh()/splx() Unix functions and their multiple cousins.

There are two implementations of the llEnter() and llExit() functions included in the NDK. These implementations provide exclusion through Task priority or by using Semaphores. Source code to both implementations is included in the Task abstraction source file: src/os/task.c

One method of exclusion is the priority method. Here, the Task that calls llEnter() is boosted to a priority level of OS_TASKPRIKERN, which guarantees that it will not be pre-empted since it is impossible for another Task to be running (all Tasks that can possibly call into the stack have a lower priority level). The stack is coded so that a Task at the kernel mode priority level will never block. When llExit() is called, the Task’s original priority is restored. Note that time critical Tasks can be assigned a priority higher than OS_TASKPRIKERN, but they are not allowed to call into the NDK.

Priority-based exclusion makes it important that your application use one of the NDK defined task priorities. If you use a priority greater than the NDK’s highest defined priority level (OS_TASKPRIHIGH), the priority-based exclusion is likely to break. Setting a thread to a higher priority than the NDK’s high-priority thread level may disrupt the system and cause unpredictable behavior if the thread calls any stack-related functions.

An alternate implementation of the enter and exit functions uses a Semaphore with an initial count of 1. When llEnter() is called, the Task calls a pend operation on the Semaphore. If some other Task is currently executing in kernel mode, the new Task will pend until llExit() is called by the original Task. A call to llExit() results in a post operation which frees up one Task to enter the stack. This form of the function pair is safer than the priority method, but may also be slower. In general, Semaphore operations are a little slower than Task priority changes. However, this method also has its advantages. The main advantage with the Semaphore method is that Tasks can be assigned priority levels more freely. There is no need to restrict Task priority or be concerned if a high priority Task is going to call into the NDK.

By altering the #if statements around the two implementations, the system developer can choose to use either implementation.

5.3 Memory Allocation System: MEM.C

The memory allocation system consists of allocation functions for small blocks of memory, large blocks, and for initializing and copying memory blocks. The API definitions for the files contained in this module is defined in the NDK API Reference Guide. These functions are used throughout the stack. The source code is provided so the systems programmer can adapt the memory system to fit a particular need.

The size and placement of this Memory Manager Buffer array can be configured. The Page size is depended upon by various stack entities, so you should be careful when changing it. The Number of pages used can be adjusted up or down to increase or decrease the scratchpad memory size.

The allocation functions for the small memory blocks (mmAlloc() and mmFree()) should not be altered. These functions are used by the NDK to allocate and free scratchpad type memory. They can be called at interrupt time and are not allowed to block. The memory is currently allocated out of a static array.

The memory manipulation functions mmZeroInit() and mmCopy() are both coded in C. A system programmer may recode these functions in assembly, or to use an EDMA channel to move memory.

The allocation functions for the large memory blocks (mmBulkAlloc() and mmBulkFree()) are currently defined to use malloc() and free(). These functions can be altered to use any memory allocation system of choice. They are not called at interrupt time and are allowed to block.

5.4 General OS Support: OSSYS.C

The OSSYS file is a generic catch-all for functions that do not have a home elsewhere. Currently, this module contains DbgPrintf() - a debug logging function and stricmp(), which is not contained in the RTS.

5.5 Jumbo Packet Buffer Manager (Jumbo PBM)

The jumbo packet buffer manager is responsible for handling memory allocation and de-allocation for packet buffers of size greater than MMALLOC_MAXSIZE (3068 bytes). This packet buffer manager is useful when the application intends on using Jumbo frames, i.e., packets larger than 1500 bytes in size that cannot be typically allocated using PBM or RTOS APIs in an interrupt context.

The following are some of the main features of the Jumbo PBM:

  • The Jumbo PBM implementation is by large similar to the PBM implementation itself, except for the block sizes it can handle are larger than the ones in PBM and ranges between 3K and 10K bytes by default.

  • Jumbo PBM does not use any RTOS APIs or dynamic memory allocation method for its memory allocation and thus can be used safely in interrupt context. It uses a static memory allocation method, i.e. it reserves a chunk of memory in the “far” section of the device memory and it further uses it to allocate for the packet buffers required.

  • The Jumbo PBM allocates memory off a separate section in the memory than the PBM itself. PBM uses the memory sections “NDK_PACKETMEM”, “NDK_MMBUFFER” for its memory allocation. On the other hand, Jumbo PBM defines and uses a section called “NDK_JMMBUFFER” for its memory allocation. The size of this section and its placement are all customizable.

  • A sample implementation of the Jumbo PBM is provided in the NDK OS AL. The customer is expected to customize this implementation according to their application needs and system’s memory constraints. The memory section sizes, block sizes and the allocation method itself may all be customized.

  • Jumbo PBM APIs are not expected to be invoked directly. The application and driver must call the PBM_alloc() / PBM_free() APIs only. These APIs in turn invoke the Jumbo PBM APIs to allocate/clean-up memory if the memory requested is larger than what PBM itself can handle, i.e., 3K bytes.

For a sample implementation of the Jumbo PBM please refer to the source file JUMBO_PBM.C in the /ti/ndk/stack/pbm directory.