Skip to the content of the web site.

The Keil Real-time Library and the RL-RTX RTOS

This describes the real-time libraries that come with the Keil MCB1700 evaluation board, specifically discussing the Keil RL-RTX (Real-Time eXecutive) operating system. Keil provides the RL-ARM real-time library; a collection of routines, data-structures, and tools designed to operate in a real-time environment. The available libraries include:

RL-RTXA real-time operating system.
RL-FlashFSA file system for dealing with standard Flash, RAM, and memory card devices.
RL-TCPnetA library allowing the microcontroller to communicate with the Internet through TCP/IP protocols.
RL-CANA library for interfacing with a Controller Area Network (CAN).
RL-USBA library for USB devices and USB hosting.

You can read more about these libraries in RL-ARM User's Guide. We will focus on the real-time operating system.

RL-RTX

The purpose of an operating system is to contain the core functionality to manage and allocate the resources of the system. A real-time operating system is one that performs this within specified time constraints. In most cases, developing the tasks within the framework of a real-time operating system will simplify numerous issues including those dealing with scheduling (processor managment), memory, resources and communications (resource management).

To examine the Real-time Library Real-time Executive, We will look at two header files:

RTL.h

The interface for communicating with the core RTOS functionality (the RTX kernel) is defined in the RTL.h header file. This file may, at first, seem daunting—you are no doubt asking yourself what everything means. However, a closer inspection reveals some organization in the file.

The first block of code are macros (using #define) and type definitions (using typedef).

The division of the balance of the code, however, leaves something to be desired. The only means of differentiating between major headers and minor headers is that the major headers are centered:

  • RTX Kernel API
  • Flash File System API
  • TCPnet API

The first section, that on the RTX Kernel API, begins with a number of type definitions and macros specific to the kernel API. We will look at this section, as this is relevant to the material in this course. The RTX Kernel API is followed by two subsections:

  • Functions ARM
  • Functions Cortex-M

You will note that surrounding these two is the pre-processor directives

#if !(__TARGET_ARCH_6S_M || __TARGET_ARCH_7_M || __TARGET_ARCH_7E_M)

/*----------------------------------------------------------------------------
 *      Functions ARM
 *---------------------------------------------------------------------------*/

...

#else

/*----------------------------------------------------------------------------
 *      Functions Cortex-M
 *---------------------------------------------------------------------------*/

...

#endif

As the architecture we're working on the Cortex-M3, we need only concern ourselves with the second half.

The functions are divided according to the tasks they perform:

  1. task management,
  2. event flag management,
  3. semaphore management,
  4. mailbox management,
  5. mutex management,
  6. time management,
  7. user timer management,
  8. system functions, and
  9. fixed memory block management functions.

We will look in depth at the first of these, task management.

Task Management

Inspecting the header file, we see a number of functions related to task management (all starting with rt_tsk_).


/* Task Management */
#define os_sys_init( tsk )                  os_sys_init0(tsk, 0, NULL )
#define os_sys_init_prio( tsk, prio )       os_sys_init0(tsk, prio, NULL )
#define os_sys_init_user( tsk, prio, stk, size )                                                \
                                            os_sys_init0(tsk, prio|(size << 8), stk )
#define os_tsk_create( tsk, prio )          os_tsk_create0(tsk, prio, NULL, NULL )
#define os_tsk_create_user( tsk, prio, stk, size )                                              \
                                            os_tsk_create0( tsk, prio|(size << 8), stk, NULL )
#define os_tsk_create_ex( tsk, prio, argv ) os_tsk_create_ex0(tsk, prio, NULL, argv )
#define os_tsk_create_user_ex( tsk, prio, stk, size, argv )                                     \
                                            os_tsk_create_ex0( tsk, prio|(size << 8), stk, argv )
#define os_tsk_delete_self()                os_tsk_delete( 0 )
#define os_tsk_prio_self( prio )            os_tsk_prio( 0, prio )
#define isr_tsk_get()                       os_tsk_self()

extern void      os_sys_init0( void (*task)(void), U32 prio_stksz, void *stk );
extern OS_TID    os_tsk_create0( void (*task)(void), U32 prio_stksz, void *stk, void *argv );
extern OS_TID    os_tsk_create_ex0( void (*task)(void *), U32 prio_stksz, void *stk, void *argv );
extern OS_TID    os_tsk_self( void );
extern void      os_tsk_pass( void );
extern OS_RESULT os_tsk_prio( OS_TID task_id, U8 new_prio );
extern OS_RESULT os_tsk_delete( OS_TID task_id );

You can read more about these at the Keil Real-time Library Reference.

The first step in reading any header file is to determine the lingo:

os
operating system
rt
real time
tsk
task
env
environment
prio
priority
stk
stack
argv
argument vector

Looking back up in the file, OS_TID and OS_RESULT are defined to be unsigned 32-bit integer ( U32). The argument void (*task)( void ) may seem odd: to understand such parameters, you should visit cdecl.org which allows you type in a C or C++ declaration and it will give you an English language explanation; in this case, "declare task as pointer to function (void) returning void".

What follows are a number of macros that simplify tasks:

#define os_sys_init( tsk )           os_set_env();                                                             \
                                    _os_sys_init( (U32)rt_sys_init, tsk, 0, NULL )

#define os_sys_init_user( tsk, prio, stk, size )                                                               \
                                     os_set_env();                                                             \
                                    _os_sys_init( (U32)rt_sys_init, tsk, prio|(size << 8), stk )

#define os_sys_init_prio( tsk, prio )                                                                          \
                                     os_set_env();                                                             \
                                    _os_sys_init( (U32)rt_sys_init, tsk, prio, NULL )

#define os_tsk_create( tsk, prio )  _os_tsk_create( (U32)rt_tsk_create, tsk, prio, NULL, NULL )

#define os_tsk_create_user( tsk, prio, stk, size )                                                             \
                                    _os_tsk_create( (U32)rt_tsk_create, tsk, prio|(size << 8), stk, NULL )

#define os_tsk_create_ex( tsk, prio, argv )                                                                    \
                                    _os_tsk_create_ex( (U32)rt_tsk_create, tsk, prio, NULL, argv )

#define os_tsk_create_user_ex( tsk, prio, stk, size, argv )                                                    \
                                    _os_tsk_create_ex( (U32)rt_tsk_create, tsk, prio|(size << 8 ), stk, argv )

#define os_tsk_self()               _os_tsk_self( (U32)rt_tsk_self )
#define os_tsk_pass()               _os_tsk_pass( (U32)rt_tsk_pass )

#define os_tsk_prio( task_id, new_prio )                                                                       \
                                    _os_tsk_prio( (U32)rt_tsk_prio, task_id, new_prio )
#define os_tsk_prio_self( prio )    _os_tsk_prio( (U32)rt_tsk_prio, 0, prio )

#define os_tsk_delete( task_id )    _os_tsk_delete( (U32)rt_tsk_delete, task_id )
#define os_tsk_delete_self()        _os_tsk_delete( (U32)rt_tsk_delete,  0 )

#define isr_tsk_get()                rt_tsk_self()

and this is followed by additional functions:

void      _os_sys_init      ( U32 p, void (*task)( void ), U32 prio_stksz,
                              void *stk )                                  __SVC_0;
OS_TID    _os_tsk_create    ( U32 p, void (*task)( void ), U32 prio_stksz,
                              void *stk, void *argv )                      __SVC_0;
OS_TID    _os_tsk_create_ex ( U32 p, void (*task)( void * ), U32 prio_stksz,
                              void *stk, void *argv )                      __SVC_0;
OS_TID    _os_tsk_self      ( U32 p )                                      __SVC_0;
void      _os_tsk_pass      ( U32 p )                                      __SVC_0;
OS_RESULT _os_tsk_prio      ( U32 p, OS_TID task_id, U8 new_prio )         __SVC_0;
OS_RESULT _os_tsk_delete    ( U32 p, OS_TID task_id )                      __SVC_0;

First, the __SVC_0 at the end of each line appears confusing—this is not standard C. If you look above, it doesn't help, as we see that it appears to be a macro:

#define __SVC_0         __svc_indirect(0)

It becomes even more confusing until you find a file armcc_intr.h. This file is one that is included automatically with all compilations—it is not necessary to include this file explicitly. The name comes from C-compiler intrinsic functions, that is, a function made available for a programming language where the implementation is handled by the compiler.

In this file, we see that __svc_indirect(x) is a macro:

#define __svc_indirect(x)

That is, it is a null.

In the declaration of functions, it is not necessary to give parameters names; it is only necessary to give the type. However, giving parameter names is often useful in describing the purpose of the parameter. Thus, the type of the parameter informs the compiler while the name of the parameter informs the programmer.

RTX_Conf_CM.c

The real-time executive configuration file for the Cortex-M series of processors is given in the file RTX_Conf_CM.c. This begins with a number of user-configurable components. You will notice that the comments are marked up: this allows the user to modify these values using the configuration wizard (we will look at this later). The user-configurable parameters are:

  • Task Configuration
    • The maximum number of concurrently running tasks: from 0 to 250 with a default of 6.
    • The number of tasks that where the memory for the stack is provided by the user: from 0 to 250 with a default of 0.
    • The memory allocated by the system for the stack size of running tasks: from 20 to 4096 with a default of 512.
    • The system can also be required to check whether or not the stack overflows (that is, as a result of function calls, more memory is placed on the stack than is allocated by the system). By default, this is true, but this also reduces the performance of the kernel.
    • The system can be set up so that all tasks run in privileged (or supervisor) mode. By default, this is false.
  • Tick Timer Configuration
    • The system can be set up to us the core SysTick timer or a peripheral timer. By default, it is set to use core SysTick.
    • The frequency of the timer in Hz: 1 Hz to 1 GHz with a default of 6 MHz.
    • The timer tick value in microseconds: from 1 us to 1 s with a default of 10 ms.
  • System Configuration
    • Is round-robin task switching used, by default, true.
    • How many ticks a task executes before a task is switched out using the round-robin scheduling: from 1 to 1000 with a default of 5.
    • The maximum number of user timers that will run simultaneously with the clock: from 0 to 250 with a default of 0.
    • The size of the queue for the storage of requests by interrupt service routines (ISRs) when called by an interrupt handler: one of 4, 8, 12, 16, 24, 32, 48, 64, or 96 with a default of 16.

You will note that the comments preceding each of these configurable declarations has included in it a number of markups: <h>, <e>, <i>, and <o>. These are markups that the μVision4 IDE will use to generate a Configuration Wizard associated with the configurable parameters. This wizard is shown in Figure 1.


Figure 1. The configuration wizard for the file RTX_Conf_CM.c, accessible by selecting the Configuration Wizard tab underneath the file content.

Question: Immediately following the part of the file for RTX user configuration, there is the symbol OS_TRV defined to be a much more complex expression. What does this evaluate to?

The description of this value is that it specifies the timer reload value for the peripheral timer. Peripheral timer counts up to a reload value, then overflows to 0, and then generates a tick interrupt. The reload value should be calculated to generate the desired interval length (for example 10 ms). Based on this definition, what is the purpose of the -1 in the macro?

You can read more about configuration macros in the RL-ARM User's Guide at the Keil web site.

Following this section, five functions are defined:

__task void os_idle_demon  ( void );
       int  os_tick_init   ( void );
       void os_tick_irqack ( void );
       void os_tmr_call    ( U16 info );
       void os_error       ( U32 err_code );

You will see that at the bottom of the RTX_Conf_CM.c file is the inclusion of another source file, RTX_lib.c, which we will discuss next.

1.2 RTX_lib.c

The RTX_lib.c file is broken into a number of sections:

  1. Definitions (macro declarations),
  2. Global variables,
  3. Tick Timer configuration (not for Cortex-M),
  4. RT Agent interface (again, not for Cortex-M),
  5. RTX Optimizations, and
  6. Standard library multithreading interface.

The macros are, for the most part, alternate variations of function calls—perhaps to maintain some form of backward compatibility or perhaps to adhere to a specific interface.

Under the global variables, we see a macro just seen above:

#if (__CM__) U32 const os_trv = OS_TRV; #endif

Question: why define a macro OS_TRV in the previous file and then define a constant here?