Programming the core system

The following chapter describes how to program a PXROS-HR application. For this purpose, the basic concepts and terms, as well as 'best practice' procedures are explained and illustrated by examples.

Basic concepts

Event:

An Event is represented by a 32 bit wide mask. Thus, several Events to be sent or to be waited for can be stored in a four bytes bitmask, i.e. a long value.

Priority:

The priority of a Task can have a value between 0 and 31, wherein a lower value means a higher priority.

Ticks:

The hardware-independent time basis of a PXROS-HR system; the attribution of ticks to milliseconds can be defined by the user.

Handles:

PXROS-HR objects cannot be accessed directly; to use PXROS-HR objects, Handles are utilised, which represent objects at the interface (see section Basics).

The first simple application

The simple application created below does not contain any functionality, yet. It implements an executable basic PXROS-HR system consisting of the following basic elements: initialisation of PXROS-HR, the Inittask, and an empty Task.

Initialising PXROS-HR

Before PXROS-HR system calls can be used, PXROS-HR has to be initialised. This is done via the function PxInit. This function receives a parameter of the PxInitSpec_T type.

PXROS specification

The PXROS-HR initialisation structure PxInitSpec_T is defined as follows:

PxMcType_t is_sysmc_type:

the type of default system memory class; must be PXMcVarsized, PXMcVarsizedAligned or PXMcVarsizedAdjusted
This requirement is for compatibility reasons and the actual value has no effect

PxSize_t is_sysmc_size:

alignment of allocated blocks; must be 8 (or any higher multiple of 8)
This requirement is for compatibility reasons and the actual value has no effect

PxMemAligned_t *is_sysmc_blk:

pointer to the memory block assigned to the system default memory class

PxSize_t is_sysmc_blksize:

size of the memory block for system default memory class

PxMcType_t is_taskmc_type:

the type of task default memory class; must be PXMcBuddyMemory
This memory class will be assigned as PXMcTaskdefault to the first task (Init task).

PxSize_t is_taskmc_size:

minimum allocatable size for task default memory class; must be PXMEM_ALIGN (32 bytes)

PxMemAligned_t *is_taskmc_blk:

pointer to the memory block assigned to the task default memory class

PxSize_t is_taskmc_blksize:

size of the memory block for task default memory class

PxUInt_t is_obj_number:

number of PXROS-HR objects

PxObjId_t is_obj_namelength:

maximum length of the object names including "null-terminators"; value of 0 disables object names

PxMemAligned_t *is_objmc_blk:

pointer to the memory block assigned to the system default object pool

PxSize_t is_objmc_blksize:

size of the memory block for system default object pool

PxTaskSpec_ct is_inittask:

task specification of the Inittask, see section The Inittask

PxProtectRegion_T is_sys_ro_protection[1]:

system memory protection settings (region and attributes) for code and read-only data. The protection settings is_sys_ro_protection covers the whole ARM code segment.

A sample initialization of the protection regions:

const PxProtectRegion_T is_sys_ro_protection =
{
    .lowerBound = ARM_CODE_SEGMENT_BEGIN,
    .upperBound = ARM_CODE_SEGMENT_END,
    .mattr      = READONLY_CODE_MEMORY_ATTRIBUTES()
};

Init function

Calling PxInit starts the Inittask and only returns if an error occurred during initialization. In this case, the function returns a corresponding error code:

PXERR_INIT_ILLALIGN

Erroneous alignment of system memory class

PXERR_INIT_ILLMCTYPE

System memory class is not of type "Varsized"

PXERR_INIT_NOMEM

Not enough memory available

PXERR_PROT_ILL_REGION

Invalid memory protection areas defined

PXERR_INIT_SCHEDULE_FAILED

the scheduling of the init task failed

PXERR_MC_ILLSIZE

size for PXMcSystemdefault is too small

PXERR_INIT_NOMEM

not enough memory for initialization

PXERR_OBJECT_SHORTAGE

not enough objects given in initstruct

PXERR_GLOBAL_ILLEGAL_CORE

number of cores not supported

PXERR_ILL_NULLPOINTER_PARAMETER

invalid system stack specification

PXERR_PROT_PERMISSION

memory protection unit cannot be activated

PXERR_ILLEGAL_ACCESS

incorrect access permission for _initspecs elements

PXERR_GLOBAL_OBJLIST_INCONSISTENCY

inconsistency between global and local init

After a successful PxInit call, the PXROS-HR system services are available.

The Inittask

The Inittask is the first Task of the system. It is automatically started after system initialization and corresponds in layout and handling to a normal Task (see section Layout and initialisation of a Task). The differences to other tasks are:

  • it is created and started by PxInit

  • it can only use resources available after PxInit

The Inittask can be used for initialising the application and for creating and starting the application Tasks. Typically, the priority of the Inittask is set to lowest priority after the initialization phase, i.e. it is only activated if no other Task or Handler is active (Idletask).

The existence of an Idletask is not compulsory. The Inittask could also be terminated after execution, yet it could also be used to do real work, e.g doing background calculations, system controlling etc.

Sample code

The example initialization of the PXROS-HR system is shown in the below code:

const PxTaskSpec_T InitTaskSpec =
{
    .ts_name           = (const PxChar_t *) "InitTask",
    .ts_fun            = InitTask_Func,
    .ts_mc             = PXMcTaskdefaultInit,
    .ts_opool          = PXOpoolSystemdefaultInit,
    .ts_privileges     = PXUserPrivilege,
    .ts_accessrights   = INITTASK_PXACCESS,
    .ts_context        = &InitTaskContext,
    .ts_protect_region = InitTaskRegions,
    .ts_taskstack =
    {
        .stk_type		= PXStackFall,
        .stk_size		= INITTASK_STACK_SIZE_BYTES,
        .stk_src.stk 	= &inittask_stack[INITTASK_STACK_ARRAYSIZE]
    },
    .ts_inttaskstack =
    {
        .stk_type		= PXStackFall,
        .stk_size		= INITTASK_INTERRUPTSTACK_SIZE_BYTES,
        .stk_src.stk 	= &inittask_interrupt_stack[INITTASK_INTERRUPTSTACK_ARRAYSIZE]
    }
};

const PxInitSpec_T InitSpec =
{
    /* System Default Memory Class */
    .is_sysmc_type        = PXMcVarsized,
    .is_sysmc_size        = 8,
    .is_sysmc_blk         = Sysmem,
    .is_sysmc_blksize     = SYSMEMSIZE,

    /* Task Default Memory Class */
    .is_taskmc_type       = PXMcBuddyMemory,
    .is_taskmc_size       = PXMEM_ALIGN,
    .is_taskmc_blk        = Taskmem,
    .is_taskmc_blksize    = TASKMEMSIZE,

    /* Memory block for System Default Object Pool */
    .is_objmc_blk         = (PxMemAligned_t *)PxObjmem,
    .is_objmc_blksize     = PX_OBJMEMSIZE,

    /* System Default Object Pool parameters */
    .is_obj_number        = NUM_OF_PXOBJS,
    .is_obj_namelength    = PXROS_NAMESIZE,

    /* InitTask specification  */
    .is_inittask          = &InitTaskSpec,

    /* Memory protection settings */
    .is_sys_ro_protection[0] = _SYS_RO_PROTECTION,
};

const PxInitSpecsArray_t InitSpecsArray =
{
    [0] = &InitSpec
};

PxError_t error;

/* the initialisation of PXROS-HR; if PxInit returns,
 * the corresponding error code is logged in error
 */
error = PxInit(InitSpecsArray, 1);

Startup-initialisation

Before the initialisation of PXROS-HR, possible hardware configurations might be executed. Since this usually has to be done with supervisor privileges, an application cannot carry out these procedures since it runs in user mode. This problem can be circumvented if the application passes arbitrary functions, which can be used to configure and initialize the hardware.

_PxInitcall(function, params…​) adds the function function with its parameters params to an internal table; params is an arbitrary list of parameters.

The user can start the execution of these functions by calling PxInitializeBeforePxInit(). The functions are executed with system permission and in the order, in which they were entered in the table.

The function PxInitializeBeforePxInit has to be called before the initialization of PXROS-HR, i.e. before PxInit!

Layout and initialisation of a Task

General

Essentially, a PXROS-HR Task consists of a Task function, which is called when the Task is started. After its creation a Task is started by sending an activation event, i.e. the Task function is called, provided one or more activation events were defined. If no activation event was defined when the Task was created, the Task is immediately started. Normally, this Task function does never return, i.e. it contains an infinite loop.

In exceptional cases a Task can be terminated by calling the system service PxDie. This function does not return. To be able to use PxDie, a Service Task has to be initialised beforehand.

The initialization of the Task is carried out during creation via the function PxTaskCreate. The necessary parameters for initializing the Task are passed to this function:

opool

the object pool, from which the Task object is taken

taskspec

the Task specification, which contains the necessary data for initialization (see section Task specification)

prio

the priority of the Task

actevents

Events activating the Task; usually only one

Task specification

Task specification is a structure of the type PxTaskSpec_T (the type designators PxTaskSpec_t and PxTaskSpec_ct mentioned in the reference manual relate to pointers or constant pointers to PxTaskSpec_T). This structure is defined as follows:

PxUChar_t *ts_name:

name of the Task; a string

void (*ts_fun)(PxTask_t task, PxMbx_t mbx, PxEvents_t events):

pointer to the Task function

PxMc_t ts_mc:

Default memory class of the Task

PxOpool_t ts_opool:

Default objectpool of the Task

PxStackSpec_T ts_taskstack:

Stack specification of the Task (see stack specification in this section)

PxStackSpec_T ts_inttaskstack:

Interrupt stack specification of the Task (see stack specification in this section)

PxUChar_t ts_prio:

Initial priority of the Task; this parameter is kept only for compatibility; it is ignored

PxEvents_t ts_actevents:

Activation events for the Task, this parameter is kept only for compatibility; it is ignored

PxTicks_t ts_timeslices:

size of the time slices for this Task; if this value is set to 0, no time slices are used

PxSize_t ts_abortstacksize:

size of the abort stacks, stated by the number of frames

PxTaskSchedExt_t ts_sched_extension:

not implemented

PxArg_t ts_sched_initarg:

not implemented

PxArg_t ts_privileges:

Privileges of the Task

PxUInt_t ts_accessrights:

Access rights of the Task

PxTaskContext_ct ts_context:

Description of the address space of the Task (see task context specification in this section)

PxProtectRegion_ct ts_protect_region:

extended memory areas of the Task

Notes:

  • A default memory class can be assigned via the macro PXMcTaskdefault, i.e inherit the memory class from the creator.

  • The default object pool of the Task can be assigned via the macro PXOpoolTaskdefault, i.e inherit the object pool from the creator.

  • Signalising an activation event starts the Task, i.e. after its creation the Task waits until it receives an activation event; usually only one activation event is stated

  • The size of the Abort stacks determines the maximum nesting depth of PxExpectAbort calls (see section The abort mechanism)

  • ts_protect_region defines additional memory protection areas of the Task. It is interpreted by PXROS-HR as a pointer to an arbitrarily large array terminated by an element whose lower bound address is set to 0.
    The specified memory areas serve as an extension to the static memory context of the Task and are applied to the MPU dynamically. However, using additional memory areas for heavily accessed data should be avoided due to the runtime overhead (granting permissions for a currently non-active area always requires a protection trap resolution). Common use cases for this mechanism are for instance register accesses and accessing configuration data.

The type PxStackSpec_T is used for describing Task’s stacks (normal and interrupt). It contains information on the stack type (ascending/descending), stack size, and from where the memory is taken (for details, see the PXROS-HR reference manual).

The PxTaskContext_t structure describes the memory context of the Task. It contains two protection regions (see below, PxProtectRegion_t) which can be inherited from the creator or defined individually for each task. If 0 is assigned as the lower bound address and memory attributes are set to NoAccessProtection, the area will be inherited from the parent task.

The structure PxProtectRegion_t defines protected memory areas, which can be made available to the Task. It contains the upper and lower boundary of the memory area (the first valid and the first invalid address), as well as the memory attributes that controls allowed memory operations (no access, read-only access, read-write access, etc.).

Sample code for creating and initialising the Task specification:

// Definition of the memory context of the Task;
// protection[0] is inherited from the creator of the Task
// protection[1] covers read and write access to Task1 data
static const PxTaskContext_T Task1_Context = {
    // data area, inherited from the creator of the Task
    .protection[0].lowerBound = 0,
    .protection[0].upperBound = 0,
    .protection[0].mattr = MEMORY_ATTRIBUTES(NoAccessPermission),
    // read-write data area
    .protection[1].lowerBound = (PxUInt_t)Task1_data_base,
    .protection[1].upperBound = (PxUInt_t)Task1_data_end,
    .protection[1].mattr = MEMORY_ATTRIBUTES(RWPermission)
};

#define TASK1_STACKSIZE     1024         // stacksize in 4-byte units

// Creating the Task specification
const PxTaskSpec_T Task1_Spec = {
    // Name of the Task
    .ts_name                        = "Task1",
    // Task function (see above)
    .ts_fun                         = Task1_Func,
    // Task's default memory class
    .ts_mc                          = PXMcTaskdefault,
    // Task's default object pool
    .ts_opool                       = PXOpoolTaskdefault,
    // Privileges of the Task
    .ts_privileges                  = PXUserPrivilege,
    // A pointer to the memory context of the Task
    .ts_context                     = &Task1_Context,
    // Stack type, will be allocated from memory class
    .ts_taskstack.stk_type          = PXStackAlloc,
    // Stack size in 4-byte units
    .ts_taskstack.stk_size          = TASK1_STACK_SIZE,
    // Stack will be allocated from PXMcTaskdefault
    .ts_taskstack.stk_src.mc       = PXMcTaskdefault,
};

The Task is created as follows:

// Creates a Task with the predefined specification,
// priority TASK1_PRIO and the activation events TASK1_ACTIVATION_EVENTS.
// The return value is the handle of the created Task
Task1_hnd = PxTaskCreate(opool, &Task1_Spec, TASK1_PRIO,  TASK1_ACTIVATION_EVENTS);

The Task function

A Task function has to conform to the following prototype:

void Task_Func(PxTask_t myID, PxMbx_t myMailbox, PxEvents_t myActivationEvents)

These parameters are passed from the system as soon as the function is called, i.e. if the corresponding Task is activated for the first time. The parameters have the following meaning:

myID

ID of the Task

myMailbox

Private mailbox of the Task

myActivationEvents

Mask of the received activation events

If activation events were defined when creating the Task (see section Task specification), the Task will only become active if one or more of these events are received.

The Task function can contain almost any code, yet it may not be terminated. This means, it must either contain an infinite loop or be terminated via a blocking system call (e.g. PxAwaitEvents(0) or PxDie()).

A typical Task function consists of a declaration and initialization section and an infinite loop. In this loop the function waits for events and/or messages. If an Event or a message is received, the Task becomes ready for execution. If it has the highest priority of all Tasks in ready state, it is executed.

Example for a Task function:

void InitTask_Func(PxTask_t myID,
                   PxMbx_t myMailbox,
                   PxEvents_t myActivationEvents)
{
    // Declarations and initialisations
    int foo = 0;
    int bar;

    if(myActivationEvents == EV_ACT_EV1)
    {
        bar = 1;
    }
    else
    {
        bar = 42;
    }

    // Main loop
    while(1)
    {
        // Parallel waiting for events and messages
        PxMsgEvent_t msgev;
        msgev = PxMsgReceive_EvWait(myMailbox,EV_EVENT_MASK);
        if(PxMsgIdIsValid(msgev.msg))
        {
            // message received
            do_sth_with_msg(msgev.msg);
        }

        if(msgev.events != 0)
        {
            // event received
            do_sth_with_events(msgev.events);
        }
    }
}

Summary

Summary of the Initialisation of a PXROS-HR system:

Initphase en neu
Fig. 1. Initialisation

A complete module for implementing the initialisation of PXROS-HR and for creating a Task can be found in Example code appendix.

Utilising system services

Basics

In principle, every PXROS-HR system service returns a value that should be evaluated. This value is either an object handle or a numeric type. A numeric value might be an error code, an event bit mask or a priority.

Handles are interface representations of PXROS-HR objects. If objects are requested from the system or passed on to system services, this is done via Handles.

If a service returns an error code, this code should be regarded in any case. If a service returns an object handle, the user should always check if the Handle is valid or if an error has occurred. PxMsgIdIsValid(handle) can be used for checking if a requested message object is valid.

Blocking and non-blocking system services

PXROS-HR system services can be blocking or non-blocking. Non-blocking system services return immediately after having been called, while blocking services can set a Task to the waiting state, which can cause scheduling. There are three reasons why a system call can be blocking:

  1. Waiting for an event: the function returns as soon as it receives an event

  2. Waiting at a mailbox: the function returns as soon as the mailbox contains message objects

  3. Waiting at an object pool: if an object (e.g. a message) is requested from an object pool, a system service can block if the object pool is empty; it will then only return if objects are available again

System Calls in Tasks and Handlers

Due to the execution state of the PXROS-HR system the service calls are devided into two classes. Handlers as well as functions called via "PxHndcall" (see PxHndcall) may only call system functions with the suffix _Hnd. Tasks, in contrast, may only call functions, which do not have the _Hnd suffix. If a call occurs at the wrong place, an error message will be returned: PXERR_HND_ILLCALL in case a Handler service was called in a Task, or PXERR_TASK_ILLCALL in the opposite case.

Object management

Object pools

PXROS-HR objects can, whenever they become necessary, be requested from an object pool.

A standard object pool is passed to a Task during its creation, yet the Task can also access other object pools if they are made available and if the Task has an access right to such resources (see Permissions).

An object pool can be used by several Tasks; thus a flexible distribution of objects can be achieved. The disadvantage of this method is that in the event of an object shortage, all the Tasks that have access to this object pool might be affected.

A typical application for object pools could be that Tasks, which are important for the system, have their own object pools assigned, while less important Tasks share a common object pool.

Requesting PXROS-Objects

PXROS-HR objects can be requested as specialized objects via functions. E.g. PxPeRequest will request an object of type PxPe_t.

Requesting objects is only possible within a Task; within a Handler this is not permitted (see section System Calls in Tasks and Handlers).

Memory management

Memory management under PXROS-HR is based on memory classes. A memory class is an object of the type PxMc_t. A memory class can manage memory in blocks of fixed or variable size. Fixed block size allows a quicker access to memory blocks, while variable block size makes it possible to access memory blocks in more flexible way. Memory classes are not usually used for requesting memory explicitly, but mainly for making memory available to message objects. Tasks only have access to PXMcTaskdefault, the task’s default memory class. In some cases, task can have access to other, dynamically created memory classes (of type PxMcBuddyMemory or PxMxFixSized), but never to the PXMcSystemdefault which is the system default memory class solely reserved for PXROS-HR kernel.

Communication

Communication and synchronisation of different Tasks is achieved via Events and messages. Messages are objects containing data, while Events are flags signalling events.

Events

An Event is a flag signalling a certain event. A Task can wait for certain Events by the function PxAwaitEvents(PXEvents_t events). The parameter events is a bitmask stating for which Events the Task shall wait. PxAwaitEvents blocks until one or more of the events defined in events occur. If events is set to zero, PxAwaitEvents will not return.

...

#define EV_EV1    1
#define EV_EV2    (1<<1)
#define EV_EV3    (1<<2)

...

PXEvents_t ret_ev;

//  wait for Event EV_EV1, EV_EV2 or EV_EV3
ret_ev = PxAwaitEvents(EV_EV1|EV_EV2|EV_EV3);

// check, which event has arrived
if( ret_ev & EV_EV1)
{
  ...
}
if( ret_ev & EV_EV2)
{
  ...
}
if( ret_ev & EV_EV3)
{
  ...
}

...

PxGetSavedEvents returns the Events received but not yet processed by the Task. This function returns immediately.

The non-blocking resetting of the Events of a Task is carried out by PxResetEvents(PXEvents_t events). The parameter events states, which Events are to be reset. This function returns a bitmask stating, which Events were reset.

...

PXEvents_t ret_ev;

// get all received Events
ret_ev = PxGetSavedEvents();

...

// all Events of ret_ev are reset
ret_ev = PxResetEvents(ret_ev);

...

Messages

Messages are PXROS-HR objects containing a data buffer and are used for data interchange between Tasks. A Task creating a message object is called the Owner of the message; the Task currently having access to a message object is called the User of the message.

Requesting and releasing message objects

Message objects can be requested in two different ways: They can either be created by PxMsgRequest or by PxMsgEnvelop. The difference between these two methods is that with PxMsgRequest the data memory of the message is taken from a memory class, while with the envelope mechanism the memory has to be provided by the requesting Task.

Message objects can be requested by Tasks but not by Handlers.

There are several ways of requesting message objects via PxMsgRequest:

PxMsg_t PxMsgRequest(PxSize_t msgsize, PxMc_t mcId, PxOpool_t opoolId)

This function returns a message object taken from the object pool opoolId. It contains a data buffer of the size msgsize. The data buffer is taken from the memory class mcId. Should the object pool be empty, this function will block until objects are available again.

PxMsg_t PxMsgRequest_NoWait(PxSize_t msgsize, PxMc_t mcId, PxOpool_t opoolId)

This function returns a message object taken from the object pool opoolId. It contains a data buffer of the size msgsize. The data buffer is taken from the memory class mcId. Should the object pool be empty, the returned Handle is invalid. This function returns immediately.

PxMsgEvent_t PxMsgRequest_EvWait(PxSize_t msgsize, PxMc_t mcId, PxOpool_t opoolId, PxEvents_t events)

This function returns a message object taken from the object pool opoolId. It contains a data buffer of the size msgsize. The data buffer is taken from the memory class mcId. Should the object pool be empty, this function will block until objects are available again or until one of the Events stated in events occurs.

If the envelop mechanism is to be used, one of the following functions can be executed:

PxMsg_t PxMsgEnvelop(PxMsgData_t data_area, PxSize_t msgsize, PxOpool_t opoolid)

This function packs the buffer data_area from the memory area of the Task into a message object taken from the object pool opoolid and returns this package. Should the object pool be empty, this function will block until objects are available again.

PxMsg_t PxMsgEnvelop_NoWait(PxMsgData_t data_area, PxSize_t msgsize, PxOpool_t opoolid)

This function packs the buffer data_area from the memory area of the Task into a message object taken from the object pool opoolid and returns this package. Should the object pool be empty, this function will return an invalid Handle.

PxMsgEvent_t PxMsgEnvelop_EvWait(PxMsgData_t data_area, PxSize_t msgsize, PxOpool_t opoolid, PxEvents_t events)

This function packs the buffer data_area from the memory area of the Task into a message object taken from the object pool opoolid and returns this package. Should the object pool be empty, this function will block until objects are available again or until one of the Events specified in events occurs.

If the envelop mechanism is used, the data area of the message is not taken from a memory class; instead the creating Task has to provide a buffer from its own memory area.

A message can be released by the function PxMsgRelease or PxMsgRelease_Hnd. If a message object is released, the Task or Handler abandons its access right to the data area of the message. By default the message object is stored in its original object pool, and its data buffer is returned to the memory class where it was taken from or, if the message was created via PxMsgEnvelop…​, data buffer is reallocated to the memory area of the original Owner of the message.

To be precise, the memory buffer packed via PxMsgEnvelop is not really taken from the memory area of the Task; the Task could, theoretically, still access this area. The buffer should, however, be treated as if it were extracted from the address space of the Task.

The behaviour of PxMsgRelease…​ can vary:

  • If a message is fitted with a Release mailbox (via the function PxMsgInstallRelmbx(PxMsg_t msgid, PxMbx_t mbxid)), the behaviour of PxMsgRelease…​ changes: The corresponding message object is not released but stored in the mentioned Release mailbox instead (see section Mailboxes).

  • After a message was sent, the owner of the message can wait for the release via the function PxMsgAwaitRel(PxMsg_t msg). To do so, PxMsgSetToAwaitRel(PxMsg_t msg) has to be called before sending. Then, if the recipient of the message calls PxMsgRelease, the message object is not released but returned to the sender instead.

If a message was marked via PxMsgSetToAwaitRel or requested by PxMsgEnvelop, the function PxMsgAwaitRel(PxMsg_t msg) has to be used to wait for the message release.

Mailboxes

Mailboxes are a central component of the communication mechanism. A message is not sent directly to the recipient Task, but to a mailbox, from which the recipient can read it. Mailboxes are organised according to the FIFO principle (first in, first out). A mailbox has unlimited capacity; an arbitrary number of messages can be stored within it.

Apart from message interchange between Tasks, mailboxes can also serve as message pools or release mailboxes.

Message pools

are mailboxes containing prefabricated message objects, which can be extracted and reused by a Task (see also in Messagepools). In comparison to the requesting of message objects, this has several advantages:

  • it is quicker since the message object already exists instead of having to be requested from the system first

  • message objects are available even if no PXROS-HR objects are available and a request for an object from the object pool would fail

  • the consumption of PXROS-HR objects is restricted, i.e. any object shortage caused by excessive use of message objects is of local relevance only

The disadvantages of using message pools:

  • the number of stored message objects may exceed the number of required ones

  • the size of the data areas of the message objects must be large enough for all use cases

Release Mailboxes

are mailboxes, where message objects are stored after release by the user, instead of being released. To utilise a release mailbox, the mailbox has to be assigned to the message via PxMsgInstallRelmbx.

To fulfill their purpose, message pools have to be specified as release mailboxes for the contained message objects, so that they can return to the pool after release by the message recipient.

The following code exemplifies how a message pool can be created and used:

...

  PxMbx_t mpool;
  PxMsg_t msg;

  msgpool = PxMbxRequest(my_opool); // request mailbox object

  if(PxMbxIdIsValid(msgpool))       // check, if object valid
  {

    PxMsg_t tmpmsg;
    for(i=0;i<10;i++)               // request 10 messages
    {
      tmpmsg = PxMsgRequest(MSG_SIZE, my_memclass,my_opool);
      if(PxMsgIdIsValid(tmpmsg))
      {
        PxMsgInstallRelmbx(tmpmsg,msgpoool); // set release mailbox
        tmpmsg = PxMsgSend(tmpmsg,msgpool)   // send message to message pool
        if(PxMsgIdIsValid(tmpmsg))
                do_error_handling();
      }
      else
      {
        do_error_handling();
      }
    }
  }
  else
  {
    do_error_handling();
  }

 ...

 msg = PxMsgReceive(msgpool); // a message is requested from the
                              // message pool

 fill_msg_data();

 PxMsgSend(msg,targetmbx)    // the message is sent to the mailbox
                             // targetmbx
 ...

Sending and receiving messages

Sending and receiving messages is always done via mailboxes. The sender sends a message to a mailbox, and the recipient receives the message from this mailbox.

A message is sent via the function PxMsgSend, the necessary parameters are the message object to be sent and the mailbox to which the message is to be sent. After sending, the sender does no longer have access to the message object, meaning the Handle of the message is invalid after sending. In addition, the sender passes the exclusive access right to the data area of the message to the recipient, i.e. a pointer to this area is invalid.

A message object and the corresponding data area belong exclusively to one user, i.e. they are allocated to one Task.

A Task can receive a message by taking it from a mailbox. PxMsgReceive(PxMbx_t mbx)` returns the next available message in accordance with the FIFO principle; prioritised messages are an exception to this rule. A prioritised message is sent to the mailbox in question via the function PxMsgSend_Prio. A prioritised message 'overtakes' the normal messages in the mailbox.

Messages can also be received in a non-blocking way: PxMsgReceive_NoWait(PxMbx_t mbx) returns immediately, either with a message object or with an invalid Handle if the mailbox is empty.

PxMsgReceive_EvWait(PxMbx_t mbx, PxEvents_t ev) makes it possible to wait for messages and Events at the same time. The function returns an object of the type PxMsgEvent_t. The following check helps find out whether a message or an Event (or both) was returned:

...

PxMsgEvent_t retval = PxMsgReceive_EvWait(mbx, EV_MASK);
if(PxMsgIdIsValid(retval.msg))
{
  // a message was received
  do_sth();
}

if(retval.events != 0)
{
  // one or more Events were received

  if(retval.event & EV1)
  {
    ...
  }

  if(retval.event & EV2)
  {
    ...
  }

  ...
}

Accessing the content of a message

A Task can access the content of a message as soon as the message was requested from the object pool or taken from a mailbox. The function PxMsgGetData(PxMsg_t msg) returns a pointer to the data area of the message object msg, PxMsgGetBuffersize(PxMsg_t msg) returns the size of the data buffer and PxMsgGetSize(PxMsg_t msg) returns the size of the data within the buffer. If handler want to access the message data PxMsgGetData_Hnd(PxMsg_t msg) must be used.

If access to a data area is no longer required, yet the message is not to be released or sent, the access to the data area should be released via the function PxMsgRelDataAccess(PxMsg_t msg) since parallel access is limited to a maximum of four message buffers.

If access to a data area was released via PxMsgRelDataAccess(PxMsg_t msg), a pointer to this area may only be used after it was regained via PxMsgGetData.

msg create send en neu
Fig. 2. Creating and sending a message
msg recv en neu
Fig. 3. Receiving a message

Sample Code

The following example shows how to create, send and receive a message and how to access the received data.

Task1 creates and sends a message to Task2:

// Task1
...

 struct comm_data data;
 PxMsg_t msg0, msg1;
 PxError_t error;

...

 // create message with data as data area of the message
 msg0 = PxMsgEnvelop(&data,sizeof(data),my_opool);

 if(!PxMsgIdIsValid(msg0))
 {
   do_error_handling();
   return;
 }

 error = PxMsgSetToAwaitRel(msg0);
 if(error != PXERR_NOERROR)
 {
   do_error_handling();
   return;
 }

 // Send the message; access to data and msg
 // is no longer permitted
 msg1 = PxMsgSend(msg0,mbx_task2);

 // if sending was successful, the message Handle
 // is invalid
 if(PxMsgIdIsValid(msg1))
 {
   do_error_handling();
   return;
 }

 // wait for recipient to release the message
 msg0 = PxMsgAwaitRel(msg0);

 if(!PxMsgIdIsValid(msg0))
 {
   do_error_handling();
   return;
 }

 ...

 // the message is finally released
 PxMsgRelease(msg0);

Task2 waits at its mailbox for messages from Task1:

// Task2
...

 struct comm_data* pdata;
 PxMsg_t msg;

 // wait for message
 msg = PxMsgReceive(my_mbx);
 if(!PxMsgIdIsValid(msg))
 {
   do_error_handling();
   return;
 }

 // read data area of the message
 pdata = (struct comm_data*)PxMsgGetData(msg);

...

 // Release message; access to msg and pdata
 // is no longer permitted
 PxMsgRelease(msg);

...

Interrupt and Trap Handler

Handlers are functions reacting to asynchronous events, i.e. to hardware or software interrupts or to traps. They are generally of higher priority than Tasks, meaning a running Task can always be interrupted by a Handler.

There are three types of interrupt Handlers: Context Handlers, Fast Context Handlers and Fast Handlers. Depending on the individual use case the according type should be chosen (see Handlers).

When starting the system, interrupts are enabled.

Fast Handlers

Fast Handlers are the fastest and highest prioritised interrupt Handlers. They run with system privileges in interrupt mode and are activated as soon as an interrupt occurs.

A Task can only install a Fast Handler if it has the appropriate permission (PXACCESS_HANDLERS, see Permissions).

A Fast Handler is installed via the function

PxError_t PxIntInstallFastHandler(PxUInt_t intno, void(* inthandler)(PxArg_t), PxArg_t arg)
intno

the interrupt number to be handled

inthandler

the Handler function

arg

the argument of the Handler function

The function returns the error code PXERR_REQUEST_INVALID_PARAMETER if intno lies outside the specification, otherwise PXERR_NOERROR.

The argument arg can be used for exchanging data between the Handler and a Task. An example for data exchange between Handler and Task can be found in section Data exchange between Handler and Task.

Instead of a self-implemented Handler, a service provided by PXROS-HR can also be installed as a Handler. To do so, the function

PxError_t PxIntInstallService(PxUInt_t intno, PxIntSvEnum_t service, PxArg_t arg, PxEvents_t events)

should be used. The parameters have the following meaning:

intno

the interrupt number to be handled

service

the service to be installed

arg

the argument of the service function

events

the Events possibly sent by the service function

The function returns the error code PXERR_REQUEST_INVALID_PARAMETER if intno lies outside the specification, or PXERR_ILLEGAL_SERVICE_CALLED if an invalid service was defined, otherwise PXERR_NOERROR.

To be able to install a service, the Task has to have the permission PXACCESS_INSTALL_SERVICES.

The following services are available:

PxTickDefine_IntHnd_SvNo

calls the function PxTickDefine_Hnd (see section Basic timer functionality), arg and events are ignored

PxTaskSignalEvents_IntHnd_SvNo

calls the function PxTaskSignalEvents_Hnd; the Handle of the recipient Task has to be passed in arg, the Events to be sent in events

Context Handlers

Context Handlers are executed with the privileges of the Task which installed it, and run in the address space of that Task. The Task must have the right to install Context Handlers (PXACCESS_INSTALL_HANDLERS, see Permissions).

A Context Handler is not immediately called if the corresponding interrupt has occurred; instead it is entered into a list, which is processed after returning from interrupt level to system level.

Due to the fact that a Context Handler is first entered into a list, no assumption can be made regarding its execution time. Real time behaviour is not given!

Before a Context Handler can be installed, an interrupt object has to be requested via PxInterruptRequest. A Handler is installed via:

PxError_t PxIntInstallHandler(PxUInt_t intno, PxInterrupt_t intObj, void(*inthandler)(PxArg_t), PxArg_t arg)
intno

the interrupt number to be handled

intobj

the Handle of the corresponding interrupt object

inthandler

the Handler function

arg

the argument of the Handler function

The function returns the error code PXERR_REQUEST_INVALID_PARAMETER if intno lies outside the specification, otherwise PXERR_NOERROR.

Fast Context Handlers

Fast Context Handlers are executed with the privileges of the Task which installed it, and run in the address space of that Task. The Task must have the right to install Handlers (PXACCESS_INSTALL_HANDLERS, see Permissions).

A Fast Context Handler is called immediately if the corresponding interrupt has occurred, depending on the hardware priority of the interrupt.

A Handler is installed via:

PxError_t PxIntInstallFastContextHandler(PxUInt_t intno, PxIntHandler_t inthandler, PxArg_t arg)
intno

the interrupt number to be handled

inthandler

the Handler function

arg

the argument of the Handler function

The function returns the error code PXERR_REQUEST_INVALID_PARAMETER if intno lies outside the specification, otherwise PXERR_NOERROR.

Trap handler

A trap handler is similar to an interrupt handler and, in principle, has the same functionality. The difference in the two lies in that an interrupt handler is triggered by a hardware or software interrupt, while a trap handler is triggered by an exception, such as a faulty memory access operation. When a trap is raised, at first all hardware interrupts are blocked for a few cycles. They are immediately unblocked by the PXROS-HR trap entry mechanism so that the trap handler is executed with interrupts enabled.

A trap handler is installed by the function

PxError_t PxTrapInstallHandler(PxUInt_t trapno, PxTrapHandler_t traphandler, PxUInt_t arg)

The three parameters are defined as:

trapno

the trap number the handler is installed for

traphandler

the handler which is installed for the trap

arg

the user defined argument which is passed to the handler

The trap handler is defined by the prototype

  • the function type of a trap handler

  • 1st trapno: trap number

  • 2nd arg: user supplied argument

  • 3rd tid: task ID

  • 4th fsr: FSR (fault status register)

  • 5th addr: MMFAR or BFAR

  • 6th saved_regs: pointer to the saved register frame */

    PxBool_t TrapHandler(PxUInt_t trapno, PxUInt_t arg, PxUInt_t tid, PxUInt_t fsr, PxUInt_t addr, PxSavedRegsPtr_t saved_regs)

    The parameters are defined as:

    trapno

    The trap number

    arg

    the user defined argument passed to the trap handler

    tid

    the object ID of the active Task at the time the trap was triggered

    fsr

    fsr: FSR (fault status register)

    addr

    MMFAR or BFAR (MemManage Fault Address Register or BusFault Address Register)

    saved_regs

    a pointer to the saved register frame

The handler function should return TRUE, if the trap condition has been solved by the handler, FALSE otherwise.

Timer functionality

The time unit used by PXROS-HR is a Tick. A Tick is an abstract time unit, the length of which has to be defined by the application.

Basic timer functionality

The internal time management of PXROS-HR is based on an internal, system-independent time unit, the Tick. The consecutive number of Ticks having passed since the start-up of the timer system is internally administered and can be read or incremented via system services.

The allocation of Ticks to a tangible time basis is done via function PxTickSetTicksPerSecond, which states how many Ticks correspond to one second. PxTickSetTicksPerSecond(100), for example, sets the length of a Ticks to 10 milliseconds.

It should be ensured that the time basis is the same for the initialization of the timer interrupts and when calling PxTickSetTicksPerSecond.

The continued counting of Ticks is performed by the function PxTickDefine_Hnd. This function has to be called within the Handler of the timer interrupts and registered as a service for the timer interrupt (see installation of an interrupt service).

The number of Ticks since the first call of PxTickDefine_Hnd can be queried via PxTickGetCount.

The time passed since the first call of PxTickDefine_Hnd can be queried via PxTickGetTimeInMilliSeconds.

The function PxGetTicksFromMilliSeconds(unsigned long ms) converts ms milliseconds to Ticks.

Delay Jobs

A delay job executes a function as a Handler after a predefined period of time. A Delay Job object can be requested from an object pool via PxDelayRequest (and PxDelayRequest_NoWait and PxDelayRequest_EvWait).

With the system services

PxError_t PxDelaySched(PxDelay_t delayId, PxTicks_t ticks, void (*handler)(PxArg_t), PxArg_t arg)

and

PxError_t PxDelaySched_Hnd(PxDelay_t delayId, PxTicks_t ticks, void (*handler)(PxArg_t), PxArg_t arg)

a function can be registered for subsequent processing. In this context, delayId is the Handle of the delay object, ticks the number of Ticks after which the function is to be executed, handler the pointer to the function to be executed, and arg the argument passed to the function.

In the below example, a Handler function is periodically called by repeatedly triggering execution from within in the function itself:

typedef struct _HndArg {
PxDelay_t delay;
PxTicks_t ticks;
} HndArg_t;

void handler_function(PxArg_t arg)
{
    HndArg_t *hndarg = (HndArg_t *)arg;
    PxError_t error;

  ...

   error = PxDelaySched_Hnd(hndarg->delay,hndarg->ticks,handler_function,arg);

  ...
}

...

void TaskFunc()
{
    ...

    PxDelay_t delay;
    PxTicks_t ticks;
    PxError_t error;
    HndArg_t  hndArg;

    ...

    delay = PxDelayRequest(defaultopool);

    if(PxDelayIdIsValid(delay))
    {
        ticks = PxGetTicksFromMillis(100);
        hndArg.delay = delay;
        hndArg.ticks = ticks;
        error = PxDelaySched(delay,ticks,handler_function,(PxArg_t)&hndArg);
        if(error != PXERR_NOERROR)
        {
          do_error_handling();
        }

        ...

    }

    ...
}

Timeout objects and periodical timers

A typical application for a Delay Job Handler is sending a timeout event to a Task. Timeout objects can be used to avoid having to implement this action over and over again. A Timeout object can be requested in the following way:

PxTo_t to = PxToRequest(PxOpool_t opool,PxTicks_t ticks,PxEvents_t ev)

The parameters have the following meaning:

opool

the object pool from which the timeout object is to be taken

ticks

the number of Ticks after which the Events are to be sent

ev

the Events to be sent after expiry of the timeout

The timer starts after the function call PxToStart(PxTo_t to). It can be terminated via PxToStop(PxTo_t to) if it is not yet expired.

Periodical timers (PxPe_t) differ from timeout objects in that they do not just send a single timeout Event but periodical ones.

Access to peripherals

On ARM Cortex-M architectures, there are two types of peripheral registers: external and internal (core registers). The external peripheral registers can be accessed from a task directly, provided the register address lies within the taks’s context or extended memory regions. This access is fast as there is no system call involved.

The internal (core) peripheral registers must be accessed by privileged code, i.e., in handler mode. There are two ways of accessing internal peripheral registers from a task: using PXROS-HR handler call (as the handler will run as privileged code) or via PXROS-HR kernel services PxRegisterWrite and PxRegisterRead. In both cases, the system call is involved, which makes the register access a bit slower compared to direct access to external registers.

Access via PxHndcall

The task can access internal (core) peripheral registers using "PxHndcall" call (function _PxHndcallVA or _PxHndcallPxArg). The extended memory region is not required in this case; however, the task must have PXACCESS_HANDLERS access right. The disadvantage of this method is that the Task has access to all the registers to which the hardware has granted read/write permission, not only to those the Task actually needs. Erroneous access operations are thus possible.

Access via kernel services

A Task can access internal (core) peripheral registers solely via dedicated kernel API functions. If a Task wishes to access peripheral registers this way, it has to have access permission PXACCESS_REGISTERS (see Permissions). Furthermore, the address areas of the desired registers must be defined with the required access rights (read and/or write) in the additional memory areas table of the Task (PxTaskSpec_T.ts_protect_region, see task specification).

The following system function is available for reading a peripheral register:

PxULong_t PxRegisterRead(volatile PxULong_t *addr)

This function receives the address of the desired register as a parameter and returns the content of the register, zero in case of an error.

If the function returns zero, it cannot be determined whether this is the actual content of the register or an erroneous access operation.

The function

PxError_t PxRegisterWrite(volatile PxULong_t *addr, PxULong_t value)

writes the value value into the register with the address addr. If the access was successful, the function will return PXERR_NOERROR; in case of an error, it will return either PXERR_PROT_ILL_REGION or PXERR_ACCESS_RIGHT.

With the function

PxError_t PxRegisterSetMask(volatile PxULong_t *addr, PxULong_t mask, PxULong_t value)

the bits masked in mask in the register with address addr are set to the value stated in value. If the access was successful, the function will return PXERR_NOERROR; in case of an error, it will return either PXERR_PROT_ILL_REGION or PXERR_ACCESS_RIGHT.

The abort mechanism

PxExpectAbort makes it possible to abort the execution of a function from within another function. The function must have been started via PxExpectAbort(ev, func, params …​). The parameters have the following meaning:

ev

the Events causing the abort of the function

func

the function to be executed. func may have arbitrary parameters, yet it may not return any values

params

a list of the parameters passed to func

PxExpectAbort returns the Event, which has caused the abort, or zero if the function was regularly terminated.

In the following example the function func is called in Task1 via PxExpectAbort. By sending the Event EV_ABORT Task2 aborts the execution of the function.

...

void func(int i)
{
  ...
}

void Task1Func()
{
  ...

  PxEvents_t retval;

  ...

  retval = PxExpectAbort(EV_ABORT,func,42);

  if(retval == 0) // function was regularly terminated
  {
    ...
  }
  else // the function was aborted
  {
    ...
  }

  ...
}

void Task2Func()
{

  ...

  PxTaskSignalEvents(Task1,EV_ABORT);

  ...
}
...

Suspend and resume tasks

Sometimes it may be useful, to suspend a task from the execution. A suspended task remains in the condition when it was suspended, but is no longer part of the scheduling.

To suspend a task the function PxTaskSuspend(PxTask_t) is used:

PxError_t   err;
PxTask_t    Task_ID;
    err = PxTaskSuspend(Task_ID);

A suspended task may be resumed, if it is necessary to let it be part of the scheduling again.

To resume a task the function PxTaskResume(PxTask_t) is used:

PxError_t   err;
PxTask_t    Task_ID;
    err = PxTaskResume(Task_ID);

The task must have the access permission PXACCESS_SYSTEM_CONTROL (see Permissions) to execute these functions.