19

 

 

Introduction

 

CONTENTS

19.1 An Object Oriented Interface to Threads and Other IPC Mechanisms

19.1.1 Linux Implementation

19.1.2 FreeRTOS Implementation

19.2 A Sample Multiplatform Application

19.3 Summary

Through this book we have presented examples based on two different open source operating systems: Linux and FreeRTOS. These two systems represent somewhat two opposite extremes in complexity. Linux is a full-fledged system supporting all those features that are required for handling complex systems, and is used in top-level applications such as large servers. FreeRTOS is a minimal system that is oriented towards small applications and microcontrollers, with minimal requirements for memory and computing resources, so that it can be used on very tiny systems such as microcontrollers.

Despite their different sizes, Linux and FreeRTOS provide a similar view of the systems, where computation is carried out by a number of threads that can interact by sharing memory and synchronizing via semaphores. Linux provides, of course, support for more sophisticated features such as virtual memory, processes, and a sophisticated I/O interface, but nevertheless the conceptual models of the system are basically the same. How they differ is in their Application Programming Interface (API), and therefore an application written for FreeRTOS cannot be ported to Linux as its is, even if all the features supported by FreeRTOS are supported by Linux as well.

The difference in API is not an issue in the case the development of the embedded system is targeted to the specific architecture. However there is a growing number of applications that require multiplatform support, that is, the ability of the application code to be used on different operating systems, for the following two reasons:

  1. Unless the application is explicitly targeted to a given architecture, several embedded systems may be involved in an embedded application. For example, an application for industrial control may involve not only microcontrollers mounted on board in the plant machinery, but also other general purpose computers for the coordination and the supervision of the system.

  2. Even if a single architecture is used in the application, it may happen that future releases will use a different platform, for example, because the current system is no more supported, or, more likely, a different solution has become more convenient.

As a consequence, it would be desirable to rely on an unified API for interacting with the operating system so that applications can be easily ported across different platforms. Developing a multiplatform application, however, restricts in some way the usable platform power since it is necessary to use primitives that are supported by all the target systems. For example, an application that is designed to run on both Linux and FreeRTOS cannot rely on virtual memory because this is not supported by the latter.

A significant interface unification effort has been carried out in the past years, and the POSIX specification is the most significant outcome [48]. Provided an operating system offers a POSIX interface, it can be interchangeably used in any application using POSIX calls. Despite the great effort in such a development and definition of standards, using POSIX as a true multiplatform interface faces some practical problems:

  • The amount of system resources required for providing a POSIX compliant interface can be an overkill for small systems. As it aims at providing a complete abstraction of the underlying system in the most general case, POSIX interface is very rich and provides support for a large number of mechanisms for Interprocess Communication (IPC). This comes, however, at a cost, and the amount of system resources, especially memory, may be too large in small systems targeted to microcontrollers.

  • A consequence of the fact that the POSIX interface must cover all possible applications, its interface is complicated, with a large number of parameters and configurations. Very often, in practice, only a subset of the supported features is required, which can be described by a simpler API. This is the case of the original API for several operating systems, which is often simpler than the corresponding POSIX interface.

For the above reasons, a more pragmatic approach is often taken when developing multiplatform embedded applications, that is, developing a software layer that offers a platform independent view of the underlying system. This layer will be implemented for every used platform, and will provide the functionality that is required by application. In this way, unnecessary requirements can be skipped, and the interface can be more targeted to the specific application.

In this chapter we shall provide an example of such an abstraction, taking it one step further in respect to the common approach, that is, providing an Object-Oriented (OO) abstraction of the underlying system. The use of OO languages and system is becoming more and more widespread, but has been rather limited in embedded applications directly interacting with the underlying hardware. One of the reasons for this was the fact that C++ compilers were not available in all the tool chains used to generate the application code in embedded system, and, above all, that the code produced by C++ compilers was rather inefficient, requiring, for example, a large number of libraries (and therefore more code in memory) for carrying out even simple operations. This is no more the case, and modern C++ compilers are able to produce highly efficient code which can be safely used in time critical applications.

In the following we shall present a generic OO interface for managing threads, semaphores, monitors, and message queues, as well as its implementation in Linux and FreeRTOS. This interface will then be used by an application implementing publish/subscribe multicast communication among threads that will work with no changes in its code in both Linux and FreeRTOS.

 

 

19.1 An Object Oriented Interface to Threads and Other IPC Mechanisms

Here, we present a set of C++ classes providing a OO view of threads and other IPC mechanisms, namely, semaphores, mutexes, conditions, message queues, and timers. Mapping such entities into C++ classes is straightforward, and the methods correspond to the associated primitives. A bit less intuitive is the way threads are mapped to C++ objects. It is natural to think about a thread as an object able to asynchronously execute some sort of code, but how is the thread code specified? Passing the address of a routine is contrary to the OO philosophy, where information is encapsulated into objects. So, instead of passing the address of a routine, an object is passed whose run() method will execute the associated code. The class of the passed object will define the specific implementation of the run() method, and therefore, the application code will first define a class whose method run() carries out the thread computation, and then will pass an instance of such class to the start() method of the class Thread. The implementation of class Thread, however, cannot be aware of the specific client class, but this problem is elegantly solved by defining a common abstract superclass, Runnable, defining one virtual method run(), and letting the application inherit from this class to override this method and pass an instance of the derived class to Thread’s start(). An abstract class is one in which some of its methods are declared but not implemented: it cannot live on its own but can only be inherited by subclasses that provide the actual implementation of such methods. The Thread class is therefore unaware of the actual class of the passed object: all it knows is that it has received an instance of Runnable class and that it is going to call its run() method.

19.1.1 Linux Implementation

In the following, the listing of the Linux implementation of the OS abstraction is presented. The first class is SystemException, and is used for error management. When an error is detected in any call to the OS routines, an Exception is thrown, that is, an Exception object is created, containing the description of the error, the execution of the method aborted, and the call stack unwound until a calling method able to manage this kind of exception is found. The C++ try construct is used to declare the ability of a method to manage the generation of Exceptions. Any exception generated by any called method will be “caught” by the catch clause of the try statement and the associated code executed. Here, the Exception class is named SystemException. Its constructor will take as argument a user-provided message and the system error number (contained in Linux variable errno), and will build the complete error message, which can be accessed via method what().

# ifndef  OS_ABSTRACTION_LINUX_H
# define  OS_ABSTRACTION_LINUX_H
# include   <stdio .h>
# include   <stdlib .h>
# include   <string .h>
# include   <semaphore.h>
# include   <pthread .h>
# include   <sys/ipc.h>
# include   <sys/msg.h>
# include   <errno .h>
# include   <sys/ time.h>
/*  Class SystemException is a support class for handling error
    management. An instance of this class will be thrown in an exception
    when an error  occurs. */
class  SystemException
{
/*   Error message  */
    char errorMsg [512];
public :
/*   Constructor : convert the passed code and appends it to the
     passed string  message */
    SystemException( const char *msg, int errNo )
    {
       memset (errorMsg, 0, 512);
       sprintf (errorMsg,  "%s: %s", msg,  strerror ( errNo ));
    }
/*   Get the error message */
    char * what ()
    {
       return  errorMsg ;
    }
};

Class Semaphore specifies a counting semaphore. Its private field is the handle of the underlying Linux semaphore, which is created by the class’ constructor. The class exports the public wait() and post() methods.

class  Semaphore
{
/*    The corresponding Linux semaphore handle  */
    sem_t  semHandle;
public :
/*   Semaphore constructor. The argument specifies the initial value  of
     the semaphore ( default  0) */
    Semaphore(int  initVal =  0)
    {
      int status = sem_init (&semHandle, 1, initVal );
      if( status !=  0)
        throw new  SystemException("    Error  initializing Semaphore",  errno );
    }
    void   wait ()
    {
      int status = sem_wait (&semHandle));
      if( status !=  0)
        throw new  SystemException(" Error     waiting  semaphore",  errno );
    }
    void   post ()
    {
      int status = sem_post (&   semHandle);
      if( status !=  0)
        throw new  SystemException(" Error     posting  Semaphore",  errno );
    }
/*   Destructor, automatically  called
     when the semaphore instance is discarded  */
    ~ Semaphore()
    {
      int status =  sem_destroy(&semHandle);
      if( status !=  0)
        throw new  SystemException("    Error  destroying Semaphore",     errno );
    }
};

Class Mutex implements mutual exclusion. It is implemented here using the pthread mutex object. Its public methods are lock() and unlock().As forthe other classes, the details of the mutex initialization and destruction are hidden in the constructor and destructor methods, called whenever the class instance is created and discarded, respectively. The C++ friend construct declares that another class has access to the private fields. This is used here because the ptread mutex object must be directly accessed in the implementation of the following class Condition.

class Mutex
{
/*    The corresponding pthread mutex  */
    pthread_mutex_t  mutex ;
/*  In the Linux implementation class Condition will require the
  private field mutex  */
friend class      Condition;
public :
/*   Constructor :  initialize the internal pthread mutex  */
    Mutex ()
    {
      int status =  pthread_mutex_init(&mutex, NULL );
      if( status != 0)
         throw   new SystemException(" Error Creating Mutex ",        errno );
    }
    void lock ()
    {
      pthread_mutex_lock(&mutex );
    }
    void  unlock ()
    {
       pthread_mutex_unlock(&mutex );
    }
    ~ Mutex ()
    {
       pthread_mutex_destroy(&mutex );
    }
};

Class Condition carries out the monitor functionality. Its public method wait() suspends the execution of the current thread until the monitor is signaled via public method signal().Method wait() can accept as parameter a reference of a Mutex object. This mutex will be unlocked prior to waiting and locked again before method wait() returns.

The Linux implementation is straightforward and the monitor functionality is mapped onto a pthread condition object.

class  Condition
{
    pthread_cond_t  cond;
public :
/*   Constructor :  initialize the internal pthread condition object */
    Condition()
    {
      int status =  pthread_cond_init(&cond,  NULL );
      if( status != 0)
         throw new  SystemException(" Error Creating  Condition",  errno );
    }
    void wait ( Mutex * mutex )
    {
/*   Here it is necessary to access the private field mutex
     of class Mutex  */
      pthread_cond_wait(&cond, mutex -> mutex );
    }
    void signal ()
    {
       pthread_cond_signal(&cond );
    }
/*  Destructor : destroy pthread conition object  */
    ~ Condition()
    {
       pthread_cond_destroy(&cond );
    }
};

Class MessageQueue describes a queue of equally sized messages. The dimension of the message buffer is passed to the constructor. The Linux implementation is a bit less straightforward than in the previous classes because Linux routines msgsnd() and msgrcv() expect a message class as the first longword of the message buffer. This is hidden outside the class, which uses an internal buffer for this purpose.

class  MessageQueue
{
/*   Linux message queue handler  */
    int msgId ;
/*   Message dimension for this message queue  */
    int  itemSize ;
/*   Private message buffer  */
    char  * msgBuf ;
public :
/*   Constructor : initialize the Linux Message queue, and allocate  the
     internal buffer  */
    MessageQueue(int  itemSize )
    {
       this -> itemSize =  itemSize ;
       msgId = msgget ( IPC_PRIVATE,    0666);
       if( msgId == -1)
          throw  new SystemException(" Error Creating Message Queue ",errno );
       msgBuf = new char[ sizeof ( long ) + itemSize ];
/*  The message class required by Linux is here always 1  */
      *(( long *) msgBuf ) = 1;
    }
/*   Send message : message dimension has been declared in the constructor */
    void send (void  * item)
    {
       memcpy (&msgBuf [ sizeof ( long )],  item,  itemSize );
       int status = msgsnd (  msgId,  msgBuf, itemSize, 0);
       if( status ==  -1)
          throw  new SystemException(" Error Sending Message ", errno );
    }
/*   Receive a message, possibly waiting for it, and return the number
     of bytes actually read  */
    int receive ( void * retItem )
    {
       int retBytes = msgrcv ( msgId,  msgBuf, itemSize, 0, 0);
       if( retBytes == -1)
          throw  new SystemException("    Error  Receiving  Message ", errno );
/*  Copy the message from the internal buffer into client 
    skipping the f i r s t longword containing the message class  */
      memcpy (retItem, &msgBuf [ sizeof ( long )], retBytes );
      return  retBytes ;
    }
/*   Destructor : Remove Linux message queue structures and
     deallocate  buffer */
    ~ MessageQueue()
    {
      msgctl (msgId,  IPC_RMID, NULL );
      delete []  msgBuf ;
    }
};

The following are the classes and structures used to implement Thread class. Class Runnable is declared as an abstract class and must be implemented by the client application. Thread’s method start() takes two arguments: the pointer of a Runnable subclass instance and a generic argument to be passed to Runnable’s method run(). Since the routine passed to pthread_create() accepts only one parameter, this is defined as the pointer to a structure containing both the address of the Runnable instance and the argument. A pointer to routine handlerWithArg() is passed to pthread_create(), and this routine will in turn call Runnable’s run() method with the specified argument. Thread stack size and priority can be set using setter methods setStackSize() and setPriority(). Finally, method join() will suspend caller’s execution until the created thread terminates.

/*  Abstract class for representing Runnable entities. It will be
    inherited by user-provided classes in order to specify
    specific thread code. The class has only one virtual method ,
    run (), which will receive a void*  generic argument  */
class Runnable
{
public :
    virtual void run( void * arg) = 0;
};
/*  Internal structure passed to the thread . It contains the pointers
to the runnable object and to the argument to be passed to method run ()  */
struct ThreadArgs{
    Runnable *rtn;
    void  * arg;
};
/*  The routine passed to pthread_create must be declared as C interface  */
extern    "C" void  handlerWithArg( ThreadArgs *);
/*  Thread abstraction. It manages the creation of a thread  */
class    Thread
{
/*   Internal pthread handle  */
    pthread_t   threadId ;
/*   Thread argument structure to be passed to the C trhead routine  */
    struct  ThreadArgs   args;
/*   Internal pthread attributes  */
    pthread_attr_t  attr;
/*   Thread priority  */
    int  priority ;
public :
/*   Constructor :  initialize pthread  attributes */
    Thread ()
    {
      pthread_attr_init (&   attr );
      priority =   -1;
    }
/*   Set the thread stack size  */
    void  setStackSize( int    stackSize)
    {
      pthread_attr_setstacksize(&     attr,    stackSize);
    }
/*  Set the thread priority  */
    void  setPriority( int  priority )
    {
/*  Priority is simply recorded, as the priority can be set only
    when the thread is    started */
      this -> priority =    priority ;
    }
/*   Start a new thread. The passed routine is embedded in an instance of
     ( subclass of ) Runnable class */
    void start ( Runnable *rtn, void *arg)
    {
/*  Prepare the argument structure  */
      args. rtn = rtn;
      args. arg = arg;
      int status =  pthread_create(&threadId, &attr ,
        ( void *(*)( void *)) handlerWithArg, ( void      *)&args );
      if( status != 0)
         throw   new SystemException(" Error Creating Thread ", errno );
      if( priority != -1)  /  * If not default      priority */
      {
        status =  pthread_setschedprio( threadId,        priority );
        if( status !=    0)
           throw new  SystemException(" Error setting Thread Priority ", errno );
      }
    }
/*   Wait the termination of the created thread  */
    void join ()
    {
      pthread_join( threadId,    NULL );
    }
/*   startScheduler method is meaningless in Linux ,
     but is required in the FreeRTOS implementation . See below.  */
    static void  startScheduler() {}
};
/*  The routine passed to pthread_create () is always the same. It  will then
    call  specific run() method  */
void  handlerWithArg( ThreadArgs *args )
{
/*   Call run () method of the Runnable object  */
    args ->rtn ->run(args -> arg);
/*   When run () returns, signal thread termination  */
    pthread_exit( NULL );
}

Finally, classes TimeInterval and Timer provide an abstract management of timed execution suspension. An interval of time is represented by class TimerInterval, internally specifying the number of seconds and of nanoseconds. An instance of TimerInterval is then passed to Timer’s sleep() method, which will suspend execution for the specified amount of time. In the Linux implementation, the representation of the interval is converted to a Linux specific timespec structure, which is then passed to Linux routine nanosleep().

/*  Class TimeInterval provides a platform independent representation  of
    a time interval  */
class  TimeInterval
{
/*   Seconds  */
    long secs ;
/*   Nanoseconds  */
    long  nanoSecs ;
public :
/*   Constructors and getter methods */
    TimeInterval(long secs, long nanoSecs )
    {
      this -> secs = secs + nanoSecs / 1000000000;
      this -> nanoSecs = nanoSecs % 1000000000;
    }
    TimeInterval( long milliSecs)
    {
      this -> secs = milliSecs / 1000;
      this -> nanoSecs = ( milliSecs % 1000) * 1000000;
    }
    long getSecs (){ return secs ;}
    long  getNanoSecs(){ return nanoSecs ;}
    long  getTotMilliSecs(){ return secs * 1000 + nanoSecs / 1000000;}
};
/*  Timer abstraction : defines the only method sleep () taking a
    TimeInterval instance as argument  */
class Timer
{
public :
    void sleep ( TimeInterval &tv)
    {
/*  Build Linux specific time structure and use Linux nanosleep ()  */
     struct timespec req, rem;
     req. tv_sec = tv. getSecs ();
     req. tv_nsec = tv. getNanoSecs();
     int status   =  nanosleep(&req,   &rem );
     if( status !=   0)
       throw new  SystemException(" Error in Sleep ", errno );
    }
};
# endif  //OS_ABSTRACTION_LINUX_H

19.1.2 FreeRTOS Implementation

The same C++ classes that have been implemented under Linux are now presented for a FreeRTOS system. Despite the differences between the two systems, the class interface will be the same, allowing then the development of common applications for Linux and FreeRTOS.

Class SystemException implementation here cannot rely on the transformation of the error number into the corresponding string because FreeRTOS does not support translation from error code to error string. Therefore, the error code is not managed the class in this implementation.

# ifndef  OS_ABSTRACTION_FREE_RTOS_H
# define  OS_ABSTRACTION_FREE_RTOS_H
# include  <stdio.h>
# include  <stdlib.h>
# include  <string.h>
# include  <errno.h>
# include  <FreeRTOS.h>
# include  <task.h>
# include  <semphr.h>
class  SystemException
{
    char errorMsg [512];
public :
    SystemException( const char * msg)
    {
       memset (errorMsg, 0, 512);
       sprintf (errorMsg,  "%s", msg );
    }
    char * what ()
    {
       return errorMsg ;
    }
};

Class Semaphore uses a Counting Semaphore, passing a default value as the maximum allowed value, required by the FreeRTOS counting semaphore creation routine. The maximum semaphore value in Linux is instead predefined.

# define  MAX_FREERTOS_SEM_VAL 256
class  Semaphore
{
/*   Internal semaphore handle  */
    xSemaphoreHandle semHandle;
public :
/*   Constructor : creates the semaphore object  */
    Semaphore(int  initVal =  0)
    {
      semHandle = xSemaphoreCreateCounting( MAX_FREERTOS_SEM_VAL,  initVal );
      if( semHandle ==  NULL)
         throw new  SystemException(" Error  initializing Semaphore");
    }
    void   wait ()
    {
       if( xSemaphoreTake( semHandle,  portMAX_DELAY) != pdTRUE )
         throw  new SystemException(" Error  waiting  semaphore");
    }
    void   post ()
    {
       if( xSemaphoreGive( semHandle) !=  pdTRUE )
         throw  new SystemException(" Error  posting  Semaphore");
    }
    ~ Semaphore()
    {
       vQueueDelete( semHandle);
    }
};

Class Mutex is implemented in FreeRTOS using a recursive binary semaphore. As usual, the constructor will create the semaphore object, and the destructor will discard it.

class Mutex
{
/*   Recursive binary semaphore handle  */
    xSemaphoreHandle semHandle;
public :
/*   Constructor : creates the recursive binary semaphore  */
    Mutex ()
    {
      semHandle = xSemaphoreCreateRecursiveMutex();
      if( semHandle ==  NULL)
        throw  new SystemException(" Error Creating Mutex ");
    }
    void lock ()
    {
      if( xSemaphoreTake( semHandle,  portMAX_DELAY) !=  pdTRUE )
        throw   new SystemException(" Error locking mutex ");
    }
    void  unlock ()
    {
      if( xSemaphoreGive( semHandle) !=  pdTRUE )
        throw   new SystemException(" Error  unlocking      mutex ");
    }
    ~ Mutex ()
    {
      vQueueDelete( semHandle);
    }
};

While in Linux monitors are directly supported by the pthread library, they are not natively available in FreeRTOS. Therefore, in the FreeRTOS implementation, monitors must be implemented using the available objects (Mutexes and Semaphores). It would be possible to use directly FreeRTOS semaphores, but using the interface classes makes the code more readable. In this way, there is no need for a destructor, the native semaphores discarded by the object fields destructors are automatically called whenever the Condition instance is discarded.

class  Condition
{
/*   The Mutex to protect condition data structures  */
    Mutex mutex ;
/*   The Semaphore used to wake waiting processes, initially set to
     zero ( default constructor value )  */
    Semaphore sem;
/*   Number of tasks waiting for this condition  */
    int waitingCount;
public :
/*   Constructor : the mutex and semaphore objects are created by the
     constructors of fields mutex and sem
     so the only required action is to initialize waitingCount to zero  */
    Condition()
    {
      waitingCount = 0;
    }
/*   Simulated wait procedure : increment the waiting counter, wait
     for the semaphore after releasing the passed Mutex, if any ,
     and acquire it afterwards  */
    void wait ( Mutex * userMutex)
    {
      waitingCount++;
      if( userMutex)
        userMutex -> unlock ();
      sem. wait ();
      if( userMutex)
        userMutex -> lock ();
    }
/*   Simulated Signal procedure : check writer counter. If greater
    than zero, there is at least one waiting task, which is awakened
    by posting the semaphore . The check and possible decrement of
    variable waitingCount must be performed in a critical  segment ,
    protected by the Mutex  */
    void signal ()
    {
      mutex . lock ();
      if( waitingCount == 0)
    {
/*  No waiting tasks  */
       mutex . unlock ();
       return ;
    }
/*  There is at least one waiting task  */
     waitingCount --;
     sem. post ();
     mutex . unlock ();
    }
};

Class MessageQueue is implemented in FreeRTOS using the native queue object. The maximum queue length is not exported by the interface and is therefore set to a default value. Unlike the Linux implementation, there is no need to handle an internal buffer.

# define  MAX_FREERTOS_QUEUE_LEN 16
class  MessageQueue
{
/*   The dimension of the items  */
    int itemSize ;
/*   The message queue native object  */
    xQueueHandle queue ;
public :
/*   Constructor : create native message queue object  */
    MessageQueue(int  itemSize )
    {
      this -> itemSize = itemSize ;
      queue =  xQueueCreate( MAX_FREERTOS_QUEUE_LEN,  itemSize );
      if( queue ==  NULL)
      throw  new SystemException(" Error Creating Message Queue ");
    }
/*   Send message : message dimension has been declared in    the
     constructor  */
    void send (void * item)
    {
      if( xQueueSendToBack( queue,  item, portMAX_DELAY)  != pdPASS )
        throw  new SystemException(" Error Sending Message ");
    }
/*   Receive a message, possibly waiting for it, and return the
     number of bytes actually read  */
    int receive ( void * retItem )
    {
      if( xQueueReceive(queue, retItem,  portMAX_DELAY)  !=  pdPASS )
        throw  new SystemException(" Error  Receiving  Message ");
      return itemSize ;
    }
    ~ MessageQueue()
    {
        vQueueDelete( semHandle);
    }
};

The basic concept in the Thread interface, that is, embedding the code to be executed by the thread into the method run() for a Runnable class, is retained in the FreeRTOS implementation. There are, however, three main differences with the Linux implementation:

  1. There is no native thread join mechanism in FreeRTOS. This is simulated in the class implementation by using a semaphore to signal the termination of the thread. This semaphore will be set by the thread routine after Runnable’s method run() returns and will be used by the Thread’s join() method.

  2. In FreeRTOS, the code associated with a thread can never return, but must call vTaskDelete() to let the system discard the task. This is different from the thread model offered by the interface, where threads terminate whenever the associated core returns. So the thread routine, after executing the Runnable’s run() method and setting the termination semaphore, will call vTaskDelete()

  3. FreeRTOS requires that the scheduler is started manually after the threads have been created. Afterwards, the main program does not exists anymore. Starting the scheduler is performed by static Thread method startScheduler(), which is void in the Linux implementation.

/*  Abstract class for representing Runnable entities. It will
    be inherited by user-provided classes in order to specify
    specific thread code. The class has only one virtual method run () ,
    which will receive a void*  generic argument  */
class Runnable
{
public :
    virtual void run( void * arg) = 0;
};
/*  Internal structure passed to the thread . It contains the
    pointers to the runnable object, the argument to be passed to
    method run (), the termination semaphore and the native thread
    object handler (used to delete the task upon termination )  */
struct ThreadArgs
{
    Runnable  *rtn;
    void * arg;
    Semaphore  *sem;
    xTaskHandle taskHandle;
};
/*  The routine passed to pthread_create must be declared as C interface  */
extern    "C"    void  handlerWithArg( ThreadArgs        *);
/*  Default values for ptiority and stack        size  */
# define  DEFAULT_STACK_SIZE ( configMINIMAL_STACK_SIZE +  512)
# define  DEFAULT_PRIO          tskIDLE_PRIORITY
/*  Thread abstraction . It manages the creation of a thread  */
class Thread
{
/*  Thread stack size  */
      int stackSize;
/*  Thread priority  */
      int    priority ;
/*  Internal thread routine arguments  */
      struct   ThreadArgs  args;
/*  Unique task index :  it is a static field  ( i.e.   shared by   all
    thread instances ) and is required because a unique task   name
    must be passed to xTaskCreate    */
    static int      taskIdx ;
public :
    Thread ()
    {
/*  Set default values for stack  size  and priority  */
      stackSize =  DEFAULT_STACK_SIZE;
      priority =  DEFAULT_PRIO;
    }
/*  Setter methods for stack size and priority  */
    void  setStackSize( int  stackSize)
    {
      this -> stackSize = stackSize;
    }
      void  setPriority( int  priority )
    {
        this -> priority =   priority ;
    }
/*   Start a new thread . The passed routine is embedded in
     an instance of ( subclass of ) Runnable class */
    void start ( Runnable *rtn, void     *arg)
    {
/*   Copy Runnable object reference and argument into the
     argument structure  */
       args. rtn =    rtn;
       args. arg =    arg;
/*  Create the termination semaphore  */
    args. sem = new  Semaphore(0);
/*  A task name is required by xTaskCreate () and created based
    in the unique value of taskIdx  */
      char     nameBuf [16];
      sprintf (nameBuf, " TASK_ %d",        taskIdx ++);
/*  Native Task       creation  */
      if( xTaskCreate(( void (*)( void *)) handlerWithArg ,
        ( signed char *) nameBuf, stackSize ,
        &args,  priority, &args. taskHandle)  !=  pdPASS )
            throw   new  SystemException(" Error Creating   Thread ");
    }
/*   Wait for the termination of this thread using the associated
     termination semaphore  */
    void  join ()
    {
        args .sem -> wait ();
        delete   args. sem;
    }
/*   Start the scheduler */
    static  void  startScheduler()
    {
      vTaskStartScheduler();
    }
};
/*  Routine actually executed by the thread  */
void  handlerWithArg( ThreadArgs *args )
{
/*  Execute User run() method */
      args ->rtn ->run(args -> arg);
/*  Set termination semaphore */
      args ->sem ->  post ();
/*  Destroy this thread */
      vTaskDelete(args -> taskHandle);
}

Timer classes are similar to the Linux implementation. FreeRTOS routine vTaskDelay(), used to suspend the calling thread, requires the specification of the delay expressed as the number of ticks. This number is derived by configTICK_RATE HZ constant specifying the tick frequency for the given system.

class  TimeInterval
{
    long secs ;
    long nanoSecs ;
public :
    TimeInterval( long secs,  long nanoSecs )
    {
       this -> secs = secs + nanoSecs /  1000000000;
       this -> nanoSecs = nanoSecs %  1000000000;
    }
 
   TimeInterval( long milliSecs)
    {
       this -> secs =  milliSecs /  1000;
       this -> nanoSecs = ( milliSecs %  1000) * 1000000;
    }
    long getSecs (){ return secs ;}
    long  getNanoSecs(){ return  nanoSecs ;}
    long  getTotMilliSecs(){ return secs * 1000 + nanoSecs /  1000000;}
};
class Timer
{
public :
    void sleep ( TimeInterval &tv)
    {
       portTickType  numTicks =
        (tv. getTotMilliSecs() * configTICK_RATE_HZ)/1000;
       vTaskDelay( numTicks );
    }
};
# endif //OS_ABSTRACTION_FREE_RTOS_H

 

 

19.2 A Sample Multiplatform Application

We have just defined an OO interface that abstracts the underlying operating system and allows the development of common applications, provided they interface to the system only through the objects exported by this interface. In the above sections, Linux and FreeRTOS have been mapped to the same interface despite their big differences. Nevertheless, some limitations arise in practice. For example, FreeRTOS requires that a multithreaded application be formed only by threads, and that the main program, once the scheduler has been started, disappear. This fact cannot be hidden by the interface, and therefore, an application to be executed by Linux and FreeRTOS must be organized in such a way that all the work is carried out by the created threads after an initialization phase performed by the main program.

Supposing that the above listings are stored in two header files named OSAbstractionLinux.h and OSAbstractionFreeRTOS.h, respectively, a common include file OSAbstraction.h may be the following:

# ifdef  HAVE_LINUX_H
# include " OSAbstractionLinux.h
# elif  HAVE_FREE_RTOS_H
# include " OsAbstractionFeeRTOS.h"
# endif

Compilation is then driven by the compiler definition (option -D in the compiler command). For example, if compiling the code for a Linux command, the compiler option will be -DHAVE_LINUX_H.

In the following, we shall use the abstraction layer to build a multicast mechanism based on publish subscribe. In this application, actors can subscribe to a multicast channel, as well as publish messages to that channel, so that they are received by all the actors that have subscribed to it. The multicast channel is represented by an instance of class MulticastManager. This class internally manages an array of message queues where every message queue is associated with one actor that subscribed to that channel. Whenever an actor subscribes, a new message queue is created and added to the array, and the subscriber gets an instance of the interface class MulticastReceiver, internally holding a reference to the created message queue. The subscriber can then read messages from that channel by calling MulticastReceiver’s method receive(). MulticastReceiver’s method receive() will in turn call the internal MessageQueue’s method receive(). To publish a message, an actor will call MulticastManager’s method publish(), which will send the passed message to all the message queues currently held by the instance, that is, to all the subscribers for that channel.

The reader may wonder why the subscriber should receive a new kind of object (of class MulticastReceiver) instead of directly a message queue instance. After all, class MulticastReceiver is just a container for the message queue, and doesn’t do anything more. The reason for this choice is that class MuticastReceiver allows hiding the actual implementation of the multicast mechanisms. The subscriber is not interested in knowing how multicast is implemented, and it makes no sense therefore to expose such knowledge. Moreover, if for any reason the internal structure of MulticastManager were changed, using a different mechanism in place of message queues, this change would be reflected in the subscriber’s interface unless not hidden by the interface class MulticastReceiver.

Classes MulticastReceiver and MulticastManager are listed below:

# ifndef  MULTICAST_H
# define  MULTICAST_H
# include " OSAbstraction.h"
/*  Multicast management : Object MulticastManager handles   publish
   and subscribe. The subscribe method returns an instance of
   MulticastReceiver. MulticastReceiver method receive () will then
   return the multicast message as soon as it becomes available .
   The dimension of the message buffer is specified in the
   MulticastManager constructor . MulticastManager internally manages
   an array of MessageQueue pointers, for each registered    listener .
   Method publish will send a message containing the published        item
   to all registered listeners. A Mutex object is used  to
   protect the internal pointer array .  */
class  MulticastManager;
class  MulticastReceiver
{
    MessageQueue *mq;
public :
/*   This class is instantiated only by MulticastManager ,
     passing the corresponding message queue  */
    MulticastReceiver( MessageQueue    *mq)
    {
      this -> mq = mq;
    }
/*   Called by the subscriber to receive muticast messages  */
    void receive ( void  * retItem )
    {
      mq -> receive ( retItem );
    }
};
# define  INITIAL_MAX_CLIENTS 100
class  MulticastManager
{
/*  Maximum number of clients before reallocating arrays  */
      int maxClients;
/*  Actual number of clients  */
      int currClients;
/*  All exchanged information is assumed of the same size  */
      int itemSize ;
/*  Mutex to protect data structures  */
      Mutex mutex ;
/*  Array of message queue references  */
      MessageQueue    ** msgQueues;
public :
/*  The dimension of the messages is passed to the constructor  */
    MulticastManager( int    itemSize )
    {
      this -> itemSize = itemSize ;
/*  Initial allocation  */
     maxClients =       INITIAL_MAX_CLIENTS;
     msgQueues = new MessageQueue *[ maxClients];
     currClients =    0;
    }
/*   Called by subscriber actor  */
    MulticastReceiver     * subscribe()
    {
/*  Create a message queue for this subscriber  */
      MessageQueue *mq = new MessageQueue(        itemSize );
/*  Update message queue, possibly reallocating  it  */
     mutex . lock ();
     if( currClients ==      maxClients)
     {
/*  Need reallocation : double the number of allocated message
    queue pointers  */
      int newMaxClients =        maxClients*2;
      MessageQueue ** newMsgQueues = new MessageQueue *[ newMaxClients];
      memcpy ( newMsgQueues,    msgQueues,    maxClients);
      delete []     msgQueues;
      msgQueues =      newMsgQueues;
      maxClients =      newMaxClients;
    }
/*  At this point there is room for sure */
     msgQueues[ currClients++] =      mq;
     mutex . unlock ();
     return new      MulticastReceiver(mq );
    }
/*   Publish a message: it will be received by all subscribers  */
    void publish ( void  * item)
    {
/*  lock data structure to avoid interferences with other
    publish /subscribe    operations */
     mutex . lock ();
/*  send the message to all subscribers  */
      for( int i = 0; i < currClients; i++)
        msgQueues[i]->    send( item );
      mutex . unlock ();
    }
};
# endif  //MULTICAST_H

Finally, a sample program handling a publisher thread and a number of subscriber threads is shown below. The program must first define two subclasses of class Runnable: MulticastListener and MulticastPublisher. MulticastPublisher’s run() method will publish a message (an integer value) every second for 10 times, and MulticastListener’s method run() will repeatedly read the message until a special QUIT code is received and the method terminated. The main program than creates a publisher thread, which in turn will create five subscriber threads and then will start publishing messages.

# define  NUM_MESSAGES  10
# define  QUIT_CODE -1
/*  The subscriber code, embedded in a subclass of Runnable  */
class  MulticastListener: public Runnable
{
/*   The passed reference to the Multicast Manager  */
    MulticastManager  *mm;
public :
/*   Constructor receiving a reference of the multicast manager  */
    MulticastListener( MulticastManager     *mm)
    {
      this -> mm =  mm;
    }
/*   Run method executed by the thread . The passed argument here
     is a pointer to the index of the     thread  */
    void run(void  *arg)
    {
      printf ("run Thread  %d
", *( int  *) arg );
/*  Subscribe and then wait for messages  */
     try  {
      int thisIdx = *( int    *) arg;
      MulticastReceiver *mr = mm -> subscribe();
      while (true )
      {
        int item ;
/*  Multicast messages are 4 byte       integers */
        mr -> receive (&item );
        printf (" Thread %d: Received    item: %d
",  thisIdx,  item );
/*  Check for     passed QUIT code  */
        if (item  ==  QUIT_CODE)
          return ;
      }
    } catch ( SystemException  * exc)
    {
/*  If an exception occurred, print the error message  */
       printf (" System error in    thread :  %s
", exc -> what ());
    }
    }
};
/*  The publisher code, again implemented as a subclass of Runnable  */
class  MulticastPublisher: public      Runnable
{
public :
    void run(void  *arg)
    {
/*  Instantiate  first  is the Multicast Manager used to
    publish/receive integer    messages */
     MulticastManager mm(    sizeof (int ));
/*  Create and start the threads, where every thread will
    receive as argument its  index */
     Thread threads [    NUM_THREADS];
     int threadIdx[ NUM_THREADS];
     try  {
            /*  Launch NUM_THREAD threads */
      for( int i = 0; i < NUM_THREADS;     i ++)
      {
        threadIdx[i] = i;
/*  Create a new subscriber  */
        threads [i]. start ( new  MulticastListener(&mm ), &threadIdx[i ]);
      }
/*  Threads cerated and started . Start publishing messages
    every second  */
      TimeInterval ti (1 ,0);
      Timer  timer ;
      for( int i = 0; i < NUM_MESSAGES;     i ++)
      {
        printf ("  Publishing  message  %d
",  i);
/*  Publish  */
        mm . publish (&i);
/*  Wait 1 second  */
       timer . sleep (ti );
      }
/*  The last message contains the    QUIT code */
      int quitCode  = QUIT_CODE;
      mm. publish (&quitCode );
/*  Wait for the termination of all threads */
      for( int i = 0; i < NUM_THREADS;    i ++)
        threads [i]. join ();
      printf ("End  of publisher
");
    }
    catch ( SystemException * exc)
    {
/*  If anything went wrong, print the error message  */
       printf (" System error  in  publisher: %s
", exc -> what ());
    }
  }
};
/*  Main program  */
int main( int  argc, char * argv [])
{
    Thread  publisher;
/*   Create and start the publisher thread.  */
/*   It will create the listeners, send them  messages ,
     and join with them  */
    try {
       publisher. start ( new    MulticastPublisher(), NULL );
/*  Start the scheduler (dummy in Linux       implementation */
     Thread ::  startScheduler();
    }
    catch ( SystemException  * exc)
    {
       printf (" System error in main :  %s
", exc -> what ());
    }
}

 

 

19.3 Summary

This chapter has presented a possible approach to handling multiplatform applications. Handling interaction with more than one operating systems may be desirable for several reasons among which are these:

  • Embedded applications for industrial control are likely to handle more than one platform, ranging from embedded systems for machine control to general-purpose computers for supervision and graphical user interfaces.

  • Even in the case the current implementation is targeted to a given platform, it may happen that future releases require a different system due to the rapid evolution of information technology.

  • Real-world systems may require a large investment in development and maintenance, and using an organization where the bulk of the code is independent of the adopted platform represents a sort of investment protection because the system code or part of it can be reused in different applications, possibly targeted to different systems.

A proper division in the system between platform-dependent and platform-independent parts is one of the facets of the larger problem of the proper management of the software. Software engineering techniques may be not required when developing small projects with a team of no more than 2–3 developers. They, however, become very important, possibly leading to the success or the failure of the entire project for larger applications.

The provided example should convince the reader that an abstraction layer for the underlying platform is feasible even when dealing with very different systems such as Linux and FreeRTOS. Moreover, the use of an object-oriented approach introduces a different perspective in the way the underlying system is presented. Object-oriented techniques are not yet very common in embedded application. However, thanks to a mature compiler technology, OO techniques, such as object encapsulation and inheritance for a better code organization can now be successfully employed in embedded and real-time systems.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.225.95.245