Project 1- The operating system

Introduction

The aim of this project is to run the entire operating system of the ETRAX unit in the manner it is going to be used during the entire course. The purpose of the code to be written is to make the Status and the CD LED blink in a defined pattern which may be chosen freely. The two LED's must blink independently of each other. Also it must be possible to force a short flash on the Network LED. This will be used in following projects to monitor the network activity.

The solution is an implementation in a thread context and will be accomplished with threads, semaphores and timers. An executable which may be loaded into the ETRAX unit is compiled, and linked in the same manner as in the previous overview of the system.

Controlling the LED's

An eight-bit register is available at the address 0x80000000 and three bits are used in order to control the LED's. This register address is write only which is the reason why an internal shadow variable is used to rember the state information. Each time the state is changed in the register the internal copy, ie the shadow variable, must be updated as well.

In the register, the bits are numbered from 0 to 7 where bit 0 is the least significant bit. The correspondence between the bit number and the LED on the frontpanel is:

 

x x C S N x x x
7 6 5 4 3 2 1 0
The eight-bit LED register

A bit which is set to 1 means the LED is off, and if the bit is cleared to 0 the LED is on. Thus, the proper initialisation value of the internal shadow variable is 0x38 meaning all LED's are off to start with.

A short introduction to threads

The threads in the operating system are based on voluntary time sharing. Thus, semaphores must be used if the execution of a thread takes a relatively long time in order to prevent other threads from starvation. All object instances executed as threads must inherit the class Job defined in the header file job.hh. The virtual method Job::doit() is implemented in the class inheriting it, and this is where the central functionality of the thread is implemented.

When an instance of a job is created, it must be entered into the job queue in order to be executed. The method to accomplish this is Job::schedule(Job*).

An example might clarify the job concept. Assume that the job instance aHelloJob is executing in its own thread and the result of the execution is the debug message Hello world on the cout of the unit, whch in our case is the serial port of the CD-server. The declaration of the class HelloJob in the header file hello.hh is

class HelloJob : public Job
{
 public:
   HelloJob(char* aString);
 private:
   virtual ~HelloJob(){}
   void doit();

   char* myString;
 }

The implementation of the class in the source code file hello.cc is

HelloJob::HelloJob(char* aString):
    myString(aString)
{
}

void
HelloJob::doit()
{
  cout << "Hello " << myString << endl;
}

An instance of the job is created and scheduled where threads are typically started, e.g. in the mainThread().

  ...
  HelloJob* aHelloJob = new HelloJob("world");
  Job::schedule(aHelloJob);
  ...

At some point, due to the voluntary time sharing, the Job::doit() method in the instance aHelloJob will be executed. It will execute in its own thread context and print the string Hello world on the cout of the system. Note that the argument in the constructor of the HelloJob must be stored in the instance aHelloJob in order to be accessible when the doit() method is executed later.

A general description of a job

Assume a queue of jobs. Any job in the queue is a time limited task to be executed later. When a job instance is created, it is inserted in queue. A number of servers (threads) execute the jobs in the queue. When a server has finished a job, it deletes it and fetches a new one from the queue. A server may have to wait for resources during the execution of a job, so jobs do not necessarely finish in the same order as they were created.

A general description of the threads

A thread is a "thread of execution" or a "light weight process". A thread has its own stack and cpu-context. The currently executing thread can at any time voluntarily hand over execution to another thread, or be forced to do so, though the latter alternative is not used in the CD-Server.

The switching of execution between threads is managed by a scheduler. The scheduler knows which threads are ready to execute.

Voluntary switching between threads is mainly performed via semaphores. A semaphore is a kind of "traffic light" for the threads, as it is used in order to have a thread wait on certain events. The operation wait() on a semaphore equals passing the "traffic light" (i.e. it may have to stop). The operation signal() on a semaphore equals controlling the "traffic light", one signal lets one thread through a wait().

Internally a semaphore contains a counter. The signal() instruction checks if there are any threads waiting for the semaphore, and if so the thread that has been waiting the longest time will be awaked. If no threads are waiting, the counter is increased. The wait() instruction checks if the counter is zero, and if so the executing thread waits and lets some other thread execute. If the count has the value one or higher, the count is decreased and the current thread continues to execute.

A short introduction to timers

Two types of timer classes are available: Timed and PeriodicTimed. An instance of the Timed class is initialised with a delay time and will time out once only. At the end of the delay time the overloaded method timeOut() is executed. An instance of PeriodicTimed will time out periodically after initialisation with a time interval between time outs. The method timerNotify() should be implemented with the response to a time out.

An example on how to use timers is the implementation of an ImportantTimer which is declared as

class ImportantTimer : public Timed
{
 public:
   ImportantTimer(Duration theTimeToWait);
 protected:
   virtual ~ImportantTimer();
   void timeOut();
};

and has the source code

ImportantTimer::ImportantTimer(Duration theTimeToWait)
{
  this->timeOutAfter(theTimeToWait);
}

void
ImportantTimer::timeOut()
{
  cout << "Important timer timed out." << endl;
}

An instance of the timer might be created and initialised in a job with

  ...
  ImportantTimer* aTimer = new ImportantTimer(Clock::seconds*5);
  ...

The timer created will time out after five seconds. The class instance Clock above describes time in general.

An instance of the class PeriodicTimed works in a similar way as an instance of  the Timed class. The differences are that a periodic timer has the methods timerInterval which specifies the interval between time outs, and startPeriodicTimer which is functionally equivalent to the Timed::timeOutAfter method. The timeOut() method in the class Timed is called timerNotify() in the PeriodicTimed class and contains the functionality in response to a time out event.

A short introduction to semaphores

The classes implementing semaphores are contained in the file threads.hh and the usage of semaphores may be illustrated with an extension of the previous example on threads. The extended instance of the HelloJob is executing in a thread of its own and prints Hello world after it has been awakened. The extended declaration of the class HelloJob in the header file hello.hh is

class HelloJob : public Job
{
 public:
   HelloJob(char* aString);
   void wakeUp();

 private:
   virtual ~HelloJob(){}
   void doit();

   char* myString;
   Semaphore* mySemaphore;
 };

The implementation of the class in the source code file hello.cc is

HelloJob::HelloJob(char* aString):
    myString(aString),
    mySemaphore(Semaphore::createQueueSemaphore("Hello",0))
{
}

void
HelloJob::doit()
{
  mySemaphore->wait();
  cout << "Hello " << myString << endl;
}

void
HelloJob::wakeUp()
{
  mySemaphore->signal();
}

The new instance of the HelloJob  is created and scheduled where threads are typically started, e.g. in the mainThread(), exactly as in the previous example. However, the schedule method will make the instance wait in the doit() method this time until the wakeUp() method is called. Thus, the information Hello world will not be shown until the semaphore receives a signal.

  ...
  aHelloJob = new HelloJob("world");
  Job::schedule(aHelloJob);
  ...
  aHelloJob->wakeUp();
  ...

Semaphores are always created with the method

Semaphore* Semaphore::createQueueSemaphore("name", 0);

Overview of the operating system

The operating system consists of the components memory, os, streams, and system.

The memory component

This component handles the memory, the heap, where dynamic allocation and de-allocation is performed with the new and delete methods.

The os component

This is the actual operating system which consists of a real time kernel called osys, the threads, the timers, and a parameterised queue class (a template). Four osys processes, tasks, are executed in the server (projos.c):

The startup of the operating system is a call of os_start() in the file start.c which loads the real time kernel osys and schedules the operating system tasks. The next step in the startup process is a start of the thread_main() task, implemented in the file init.cc, by the osys kernel. The kernel also provides support for message passing between the different processes executing. Ask a superviser if you are interested in the details.

The streams component

The streams component is providing support for streams not quite according to the C++ standard.

The systems component

This component contains system specific functions like the data types used in the ETRAX code (compiler.h), the definitions of different register locations in the chip (etrax.h), and other peculiarities due to the hardware design. Spend some time browsing the source code in order to get an impression of the different components in the system.

Suggested solution design

The only solution design requirement is the implementation of a singleton class called FrontPanel which should contain the method packetReceived() which will be called by the network driver instance in order to change the state of the network LED on the front panel of the unit. The creation and initialisation of the instance of the class FrontPanel is in the file init.cc where the call FrontPanel::instance() is performed.

A recommended solution design of the classes is

The classes to be implemented are:

with a header file containing the full descriptions of the classes provided in ~/kurs/src/lab1/frontpanel.hh. In addition, a suggested skeleton of the implementation of the classes is available in ~/kurs/src/lab1/frontpanel.cc.

Compilation, and linking

The target of the make process is defined in the file ~/kurs/make/lab1/modules. Make sure your present working directory is ~/kurs/make and type the commands: