MaRTE OS

Minimal Real Time Operating System for Embedded Applications
Version 1.4 - Dec 2003
Copyright (C) 2003 Universidad de Cantabria, SPAIN
Authors:  Mario Aldea Rivas aldeam@unican.es

Michael González Harbour mgh@unican.es

User's Guide


Contents of this Guide

  1. Making Applications
  2. Debugging Applications
  3. Explaining the Boot Process
  4. Configuring a Stand-Alone Target
  5. Compiling MaRTE Kernel and Libraries
  6. Optimizing MaRTE Kernel for Speed and Size
  7. Enabling Debug Checks and Messages in the Kernel
  8. MaRTE OS device drivers
  9. Using the 'tasks_inspector' Tool
  10. Changing MaRTE OS Configuration Parameters
  11. Miscellany of Utilities

1.- Making Applications

Applications are made using the scripts mgcc and mgnatmake. They are invoked in almost the same way that gcc and gnatmake. They link the user's application with the MaRTE OS libraries and copy the program to the exported directory.

Examples of use:

$ mgcc -g -O2 extra_obj.o my_program.c

$ mgcc use_math.c -lm

$ mgnatmake -gnato my_program.adb

There are some restrictions:

$ mgnatmake extra_obj.o -llibrary my_program.adb

For Ada programs also mgnatbind and mgnatlink are provided.

When using some Ada or GNAT standard packages from an Ada program an error message like this could be produced at binding time:

error: "xxx.adb" must be recompiled ("s-osinte.ads" has been modified)

This is due to the fact the application is trying to use one standard package not compiled at MaRTE OS installation (only a part of Gnat Run-Time is compiled by default). Just compile your application with the "-a" flag to allow compilation of any required standard package:

$ mgnatmake <other flags> -a my_program.adb


2.- Debugging Applications

Debugging applications is performed using a serial line (RS-232) connecting host and target.

In order to debug an application it should be compiled with the -g flag. To synchronize the execution with the remote debugger running in the host is necessary to add these lines to the application code (possibly at the very beginning of your program):
 
Ada program:
...
with Debug_Marte; use Debug_Marte;
...
procedure My_Proc is
begin
  ...
  Debug_Marte.Init_Serial_Communication_With_Gdb (Serial_Port_1);
  Debug_Marte.Set_Break_Point_Here;

  Application's code;
end My_Proc;


 
C program:
...
#include <debug_marte.h>
...
int main()
{
  ...
  init_serial_communication_with_gdb (SERIAL_PORT_1);
  set_break_point_here;

  Application's code;
}

The application executes until the first Debug_Marte.Set_Break_Point_Here or set_break_point_here is reached. At this point the execution is stopped and the target is ready to synchronize with the debugger.

At the MaRTE installation path run gdb:

$ cd /installation/path/marte
$ gdb mprogram

The connection with the target is performed by executing the following "gdb" command:

(gdb) target remote /dev/ttyS0

Where '/dev/ttyS0' is the device file for the serial port 1. After this you should get a message like this:

Remote debugging using /dev/ttyS0
main () at hello_world.c:25
25 printf("\nHello, I'm a C program running on the MaRTE OS.\n\n");

All these steps can be performed at once executing:

$ gdb -x /installation/path/marte/utils/marte_db

When executing gdb as above it is possible to reconnect with the target using the macro connect:

(gdb) connect


3.- Explaining the Boot Process

Steps in application boot process using "netboot" [note]:
 


For more information look at the GRUB page: http://www.gnu.org/software/grub/.

The netboot program has been taken from the OSKit distribution: http://www.cs.utah.edu/flux/oskit/.

[Note] Also "Etherboot" can be used to boot MaRTE applications. An Etherboot image can be created at http://rom-o-matic.net/ as explained in the installation guide "INSTALL".


4.- Configuring a stand-alone target

The netbooting of user's applications is a very suitable method for the developing stage, but if you prefer it, the application can be executed in a target completely disconnected from any other computer. For this configuration neither the RS-232 nor the Ethernet are necessary for the target computer, only the floppy disk is required.

A very easy way of doing this is copying the user's application to the 'boot-floppy' with the name of 'netboot'. By doing this the user's application will be launched directly by GRUB instead the 'netboot' program:

$ mcopy mprogram a:netboot

The label at the GRUB prompt can be changed by editing the file 'boot/grub/menu.lst' in the 'bootfloppy' disk.

Remember that the modified 'boot-floppy' cannot be used any more to boot applications across the Ethernet. For this purpose you must create it again using the 'mkbootfloppy' script.
 


5.- Compiling MaRTE kernel and libraries

The kernel and libraries are compiled using full optimization options during the installation stage, so if you are planning to use MaRTE OS "as is" to build your applications, just skip the rest of this chapter.

But there are some reasons why you can be interested on recompile MaRTE OS:

There are some scripts in the 'utils/' directory that allow you to recompile the kernel and/or the libraries:

mkkernel:

Compiles the kernel and drivers (calling 'mkdrivers'). It accepts gnat and/or gcc options, so if you want a kernel optimized for speed execute:

$ mkkernel -gnatn -O3 -gnatp

The "-f" flag can be added to force the recompilation of all the kernel, otherwise only modified and related packages will be compiled.
 

mklibmc:

Generates the MaRTE OS version of the libc C standard library ('lib/libmc.a'). For full optimized library execute:

$ mklibmc -O3
 

mkall:

Makes the same as 'mkkernel' and 'mklibmc' together but forcing recompilation of everything, not only the modified files. Is like installing MaRTE OS again but without changing the target architecture, the host data and the gnat version. It accepts gnat and/or gcc options, so if you want a fully optimized kernel execute:

$ mkall -gnatn -O3 -gnatp

mkdrivers:

Automatically run 'make' in each driver directory containing a 'GNUmakefile'. After that, all the objects files found in drivers directories are copied to the drivers library 'lib/libdrivers.o'. (See also chapter  "MaRTE OS devices").



6.- Optimizing MaRTE kernel for speed and size

By default the kernel is optimized for speed (compiled with flags "-gnatn -gnatp -O3") and without any pragma "restrictions". If you desire to impose some restrictions to your application or use a simplified version of the run time with the use of "pragma Restricted_Run_Time" or "pragma Ravenscar" just put a 'gnat.adc' file with the desired pragmas in the application directory and force recompilation of everything (flags "-a -f").

For example, for a minimum size kernel a 'gnat.adc' file with the following pragmas can be used:

pragma Ravenscar;
pragma Restrictions (Max_Tasks => 2); -- Or the number of tasks of your application

And then everything (application code, MaRTE OS kernel and the part of the Gnat Run-Time required for your application), should be recompiled with the command:

$ mgnatmake -gnatp -O3 -a -f your_application.adb
 


7.- Enabling Debug Checks and Messages in the Kernel

Some debugging checks are included in the kernel in the form of "Assert" pragmas. They can be used to detect internal errors inside the kernel before they cause any unexpected behaviour impossible to analyse. To enable these checks the kernel have to be recompiled with the '-gnata' flag:

$ mkkernel -gnata -a -f ("-f" force recompilations)

Enabling "Assert" pragmas can be useful if you are modifying MaRTE OS to check the consistency of your changes.

The kernel also can be configured to display some (lot) debugging messages on console. They can report all relevant events about context switches, mutexes, signals, timed events, etc. This functionality can be useful for kernel developers in some specific situations. In the general case the number of messages displayed is too huge to be analysed, and the use of the debugger will be preferred. To enable the debugging messages edit the file 'kernel/debug_messages.ads', set to true some (or all) the boolean constants below the label "General Messages" and recompile the kernel with assertions enabled:

$ mkkernel -gnata -a -f


8.- MaRTE OS Devices

MaRTE OS provides a standard method for installing and using device drivers. This method allow programmers to share their drivers with other people in a simple way.

The implemented model is similar what is used in most UNIX-like operating systems. Applications access devices through "device files" using standard file operations (open, close, write, read, ioctl).

The drivers installation in MaRTE OS is explained bellow. The best way of understanding that process is looking at drivers included in MaRTE OS distribution. The simpler examples are "Demo_Driver_C" and "Demo_Driver_Ada".

Driver code

A driver can be written using both Ada or C programming languages. The code file(s) must be included in a subdirectory of 'drivers/'.

An Ada driver can provide some the following functions (usually all the functions will be provided):

function My_Driver_Create return Int;

function My_Driver_Remove return Int;

function My_Driver_Open (Fd   : in File_Descriptor;
                         Mode : in File_Access_Mode)
                        return Int;


function My_Driver_Close (Fd : in File_Descriptor) return Int;

function My_Driver_Read (Fd            : in File_Descriptor;
                         Buffer_Ptr    : in Buffer_Ac;
                         Bytes_To_Read : in Unsigned_32)
                        return Int;


function My_Driver_Write (Fd             : in File_Descriptor;
                          Buffer_Ptr     : in Buffer_Ac;
                          Bytes_To_Write : in Unsigned_32)
                         return Int;


function My_Driver_Ioctl (Fd             : in File_Descriptor;
                          Request        : in Ioctl_Option_Value;
                          Ioctl_Data_Ptr : in Buffer_Ac)
                         return Int;

For a C driver the function prototypes are:

int my_driver_create (int arg);

int my_driver_remove ();

int my_driver_open (int file_descriptor, int file_access_mode);

int my_driver_close (int file_descriptor);

ssize_t my_driver_read (int file_descriptor, void *buffer, size_t bytes);

ssize_t my_driver_write (int file_descriptor, void *buffer, size_t bytes);

int my_driver_ioctl (int file_descriptor, int request, void* argp);

For C drivers a specification Ada file must be written to make accessible the C functions to MaRTE kernel. To write that file you can use drivers/demo_driver_c/demo_driver_c_import.ads as a template. Also a GNUmakefile file should be included in driver's directory since mkdrivers will automatically run make in each driver directory containing a GNUmakefile. After that, all the objects files found in drivers directories are copied to the drivers library lib/libdrivers.o.

Installing device drivers

Drivers and device files included in the system are registered in kernel/k-devices_table.ads file. In this file there are two tables:

In order to include a new driver add and entry to The_Driver_Table, choosing an unused number. That number will be the major number that identifies your driver in the system. (You can take as example the commented entry for "Demo_Driver_Ada" or "Demo_Driver_C").

Now you must add unless one device file in The_Device_Files_Table. The name of your device file can be whatever you want ("/dev" prefix is recommended for analogy with UNIX systems).

As mayor number should be chosen the one previously associated with your driver. As minor number can be used whatever you want, this number is useful to distinguish between different files associated with the same major number.

In the field Ceiling you can put any value within the priorities range used in the system (usually between 0 and 31). To know more about this field read "Mutual exclusion among threads using the same file descriptor".

Installing device drivers for the standard input, output and error

A few special things is necessary to do in case you want to change one of the standard input, output or error devices.

Since we want to use these devices from the very beginning of an application execution, even before the file system has been properly setup, some direct hooks to basic functions of these devices have to be provided in file kernel/kernel_console.ads.

If you are changing the standard input device, you should modify the following "hook" in kernel/kernel_console.ads:

If you are changing the standard output device, you should modify the following "hooks" in kernel/kernel_console.ads:

If you are changing the standard error device, you should modify the following "hooks" in kernel/kernel_console.ads:

Restrictions for drivers and interrupt handlers code

There is an important restriction for interrupt handlers code: "they should not call any potentially blocking operation". This includes writing directly to stderr or stdout devices because a mutex is used to protect every driver in MaRTE OS. So, interrupt handlers are not allowed to use standard input/output functions as printf or the package Ada.Text_IO.

As alternative, C interrupt handlers must use printc (to write on console) and printe (to write on standard error). These functions write directly in the devices without passing through the file system. For Ada drivers package Kernel_Console must be used instead of Ada.Text_IO.

Apart from the explained before for interrupt handlers, there is no restrictions for code to be used inside drivers, so they could use any standard POSIX or Ada interfaces. Anyway, it is recommended for Ada drivers not to use the "POSIX.Ada" interface. There are two reasons for that:

To avoid this dependency MaRTE OS provides two packages: Marte_Hardware_Interrupts and Marte_Semaphores to be used inside drivers instead of their POSIX equivalents (POSIX_Hardware_Interrupts and POSIX_Semaphores).

Compiling kernel and drivers

Each driver with files not written in Ada should include in its directory a 'GNUmakefile' with the rules to compile those files.

MaRTE utilities 'mkkernel' and 'mkdrivers' automatically run 'make' in each driver directory containing a 'GNUmakefile'. After that, all the objects files found in drivers directories are copied to the drivers library 'lib/libdrivers.a'.

'mkkernel', apart from calling 'mkdrivers', makes all MaRTE OS kernel. Then it must be executed after changing drivers and device files definition tables in 'kernel/k-devices_table.ads'.

So, the general way of keeping everything updated after a change in device drivers code or definition tables is executing 'mkkernel' (see also chapter "Compiling MaRTE kernel and libraries"). However, if you have only changed your driver code (without touching 'kernel/k-devices_table.ads') running 'mkdrivers' would be enough.

It is important to notice that, when using Ada drivers from Ada applications, 'mgnatmake' will do all the work (it is not necessary to execute 'mkkernel' nor 'mkdrivers'). This is because all dependencies in an Ada application are known at compilation time (the Ada library records that information), and then, necessary compilations will be performed assuring the application consistency.

Mutual exclusion among threads using the same file descriptor

A different mutex is associated with every file descriptor. It is a "Priority Protection Protocol" mutex with the ceiling assigned to the device file in 'The_Device_Files_Table'.

A thread will take the mutex before performing some action on a file. So mutual exclusion is automatically achieved among threads using the same file descriptor to access a file.

It is important to notice there is no mutual exclusion among threads using different file descriptors (obtained in different calls to 'open' on the same device file). This fact can be useful, for example, in devices that allows simultaneous write and read operations without any kind of synchronization between them.

Operations available for drivers

MaRTE OS provides some operations to be specifically used inside drivers. These operations are provided by Ada package 'Drivers_Marte':

package Drivers_MaRTE is
   ...
   procedure Get_Major_Number (Fd         : in  File_Descriptor;
                               Mj         : out Major;
                               Invalid_Fd : out Boolean);
   --  Gets major number from a file descriptor

   procedure Get_Minor_Number (Fd         : in  File_Descriptor;
                               Mn         : out Minor;
                               Invalid_Fd : out Boolean);

   --  Gets major minor from a file descriptor


   procedure Get_Mutex_Descriptor
     (Fd         : in  File_Descriptor;
      Md         : out Kernel.Mutexes.Mutex_Descriptor;
      Invalid_Fd : out Boolean);
   --  Gets descriptor of mutex associated with a file descriptor

   procedure Set_POSIX_Error (Error : in Kernel.Error_Code);
   --  Sets the value of the POSIX error for the calling task.
   --  Useful to return
a specific value of POSIX error from
   --  a function of the driver.

   function Get_POSIX_Error return Kernel.Error_Code;

   ...
end Drivers_MaRTE;

For drivers written in C the headers file 'include/drivers/drivers_marte.h' with the following prototypes:

/* Gets the major number
 * Returns -1 for an invalid file descriptor */
int get_major (int filedes);

/* Gets the minor number
 * Returns -1 for an invalid file descriptor */
int get_minor (int filedes);

/* Gets the mutex associated with the file descriptor
 * Returns NULL for an invalid file descriptor */
pthread_mutex_t *get_mutex (int filedes);

Device drivers framework was a result of Francisco Guerreira's Master Degree Project.


9.- Using the 'tasks_inspector' Tool

This tool allows analysing the execution flow of an application. The relevant scheduling information is sent from the target to the host computer across the serial line, being stored to be analysed after the application execution finishes.

The scheduling information is parsed and transformed into a set of files that can serve as input for 'gnuplot', a plotting program with which the information is displayed graphically.

In order to use the 'tasks_inspector' tool first the kernel have to be recompiled with the boolean constant 'Tasks_Inspector_Messages' set to 'True' (file 'kernel/debug_messages.ads'). The serial port used to communicate the host and target computers can be configured with the 'Tasks_Inspector_Serial_Port' parameter in the same file. After changing these parameters the kernel should be recompiled executing:

$ mkkernel -gnata -a -f

Using 'tasks_inspector' involves the following steps:
 

  1. "cd" to the 'tasks_inspector/' directory and execute:
    $ tasks_inspector

    A new "xterm" window will appear displaying a message like this:

    Getting scheduling information from the serial port... (finish with Ctrl-C)

  1. Start the application in the target.
  1. When your application finishes (or whenever you want) press Ctrl-C on the new "xterm" window and a 'gnuplot' window will appear.
Sending the task inspector data over the serial line has a very high impact on the performance of the system. Data is sent in foreground each time a task performs any relevant scheduling action, so 'tasks_inspector' only can be used with low rate tasks.

A less intrusive way of analysing the scheduling behaviour of an application is using the MaRTE OS implementation of the POSIX Trace standard done by Agustín Espinosa (Universidad Politécnica de Valencia). A patch is available at MaRTE OS home page (currently only for version 0.86).


10.- Changing MaRTE OS Configuration Parameters

MaRTE OS is a static system in which the maximum number of resources (i.e., threads, mutexes, thread stacks, number of priority levels, timers, etc.) is set at compile time. If you want to change the default values the kernel have to be recompiled with the new ones.

The configuration parameters are defined as constants in the file kernel/configuration_parameters.ads. Below each parameter there is a short description of its meaning.

For example the maximum number of tasks (or threads) that is possible to create is set by constant Num_User_Tasks_Mx (10 by default). If you want to create more tasks just change this value and recompile the kernel with the command:

$ mkkernel -a -gnatn -gnatp -O3

(or whatever other compiler options you want)

It is important to notice that GNAT uses about 12Kb of dynamic memory per Ada task. So, as you increment the number of tasks it is necessary to increment the size of the dynamic memory pool. To do that, edit the file kernel/configuration_parameters.ads and change the value assigned to constant  Dynamic_Memory_Pool_Size_In_Bytes (300Kb by default).


11.- Miscellany of Utilities


In the directories 'misc/' and 'include/misc/' there are some extra utilities that although they do not belong to the kernel or the POSIX interface can be useful for some applications:
 

Console Management

Complete set of operations for the text console. They allow changing the text attributes, positioning the cursor and cleaning the screen. It also allows changing keyboard behavior between to modes:

Files 'misc/console_management.ads', 'misc/console_management.adb' and 'include/misc/console_management.h'.
 

Execution Load

Allows applications to consume CPU for a given amount of time. Useful in examples that do nothing but show the scheduling of tasks and resources.

Files 'misc/execution_load.ads', 'misc/execution_load.adb', 'misc/load.c' and 'include/misc/load.h'.
 

Serial Console

Allows applications to change the output system console between the monitor and the serial line.

Files 'misc/serial_console.ads', 'misc/serial_console.adb' and 'include/misc/serial_console.h'.
 


Contact Address:
MaRTE OS internet site:
aldeam@unican.es
http://marte.unican.es
Department of Electrónica y Computadores
Group of Computadores y Tiempo Real
University of Cantabria