Customized Run-Time Topics

This appendix describes how to use the GNAT cross tools to build programs that use the Ravenscar run-time library, and how to configure and port this library.

Porting the run-time on bareboard platforms

This section describes how to customize the run-time library for a given target board.

Sources organization

The sources and the binaries of the run-time library are provided in a hierarchy. Assuming that the base directory is <RTS>, the following files and directories are present:

  • <RTS>/ada_object_path

  • <RTS>/ada_source_path

    These files indicate where the object and source files of the run-time library are located. Each file simply contains a list of paths. You should not need to modify them (unless you want to add a new subdirectory).

  • <RTS>/common

    This subdirectory contains source files that do not depend on the particular board. You should not need to modify them.

  • <RTS>/gnatl-common

    Like common but for the tasking part of the runtime.

  • <RTS>/arch

    This subdirectory contains source files that depend on the board. You can edit them and add new source files (or remove some of them).

  • <RTS>/gnatl-arch

    Like arch but for the tasking part of the runtime.

  • <RTS>/runtime.xml

    This is an XML configuration file for gprbuild. It mostly defines the compiler switches needed to compile user source files, and the linker script used. You may need to modify it if you want to port the kernel.

  • <RTS>/runtime_build.gpr

    This is the project file used to build the run-time library. As it inherits from <RTS>/runtime.gpr, it is quite simple and you should not need to modify it. This project adds the -gnatg switch required to build run-time files.

  • <RTS>/adalib

  • <RTS>/obj

    These subdirectories contain all the object files. They are automatically populated when the run-time library is built. You can remove them to clean up the build. The obj/ subdirectory contains object files that are built, whereas the adalib/ subdirectory contains the archive and ALI files that are used when an application is built.

Building the library

You can build or rebuild the run-time library simply by using gprbuild:

$ gprbuild --target=powerpc-elf -P<RTS>/runtime_build.gpr

The target switch can be omitted if the Target attribute is specified in the project file. See Specifying the Target for the details.

Board setup overview

The first step is to be able to run a very simple program that does not use any tasking features. This consists of initializing the board, the CPU, and the stack; possibly copying the data from flash to RAM; and calling main. This is not Ada specific, and it depends heavily on the target configuration.

You can start from the ZFP project-based example provided with GNAT. This project is located in <gnat-root>/lib/gnat/powerpc-elf/zfp-mpc8641 in the case of PowerPc. This example is for an emulator, so it might be simpler than for a real board (for example it is not necessary to initialize a DRAM controller or to test the RAM).

This example provides two configurations: one ROM-based and one RAM-based. In order to use the RAM-based configuration, you have to dynamically load and run the program using, for example, a ROM-based gdb stub. As a result, the RAM-based configuration is slightly simpler.

To port the ROM-based configuration, you need to modify the qemu-rom.ld linker script (which is referenced only by <RTS>/runtime.xml). You should also modify start-rom.S which is the entry point (it defines the reset vector), set up the stack pointer, and copy data from ROM to RAM. This is where you can initialize the RAM if necessary. This file also calls _setup (defined in setup.S) which simply clears the BSS segments. You should not need to modify setup.S.

Then you can port Ada.Text_IO, whose body is in a-textio.adb. The example drives a simple 16450-based UART.

Once done, a simple ‘hello world’ program should work.

A note on debugging: the version of gdb provided with GNAT Cross is ‘Ada aware’. But you need to determine how gdb running on your host will interact with code on your target. Most likely, it will use gdb‘s serial protocol to interact with some agent that can control execution of your code. One example is the use of a hardware probe that controls the processor through a JTAG (BDM, COPS) port. GNAT Cross supports the Abatron BDI 2000, which is a hardware device that connects to the target through JTAG, implements the gdb serial protocol in firmware, and communicates using that protocol over its Ethernet port. For example, a BDI could be known on your local network by the hostname bdi, and when running powerpc-elf-gdb you would use the gdb command target remote bdi:2001. This tells gdb that it will control the execution of your program by using the serial protocol running on port 2001 of the host bdi. Another possibility is that there is a kernel running on your target hardware that controls program execution (your application) and which implements the gdb serial protocol over some connection – often a serial port.

Configuring for Specific Boards

The run-times are not tailored for every possible parameter for a given board. Different boards with the same processor ISA may have different capabilities, and covering all of the possible permutations would be prohibitively expensive for development, support, and testing. This is especially true for clock frequency and memory sizes. As a result, the pre-built run-time librararies may or may not be parameterized to the specific capabilities of the board you are actually using.

The STM32F4 Discovery and STM32F429 Discovery boards are good examples. Both are ARM boards built by the same vendor, but with different maximum clock frequencies and different amounts of FLASH and SRAM available. The pre-built run-time libraries provided with the compiler are configured for the STM32F4 Discovery so they do not reflect the additional capabilities of the STM32F429 Discovery board.

Therefore, you may want to change the clock frequency and memory layout. You can do so as a modification to an existing run-time library, i.e., without creating a new library, or as a configuration of a wholly new library.

We will illustrate the changes as part of creating a new run-time library tailored for an STM32F429 Discovery board. This new run-time will be based on an existing STM32F4 run-time library so we copy the run-time rooted in a directory named “ravenscar-sfp-stm32f4” that is located under the compiler’s installation. This directory, along with those for the other run-time libraries, is located under the <platform-name>/lib/gnat/ directory. The STM32F4 runtime is for an ARM target, so “platform-name” is “arm-eabi” in this case.

For example, on Windows, and assuming the default location, this existing run-time library implementation would be in the following directory tree:

C:\GNATPRO\7.4.0\arm-eabi\lib\gnat\ravenscar-sfp-stm32f4\

Be sure to copy one of the directory trees under the <platform-name>/lib/gnat/ directory. There are other directories, elsewhere under the GNAT Pro installation, that have the same run-time library names but that are not the run-time library implementations. You should see files named runtime_build.gpr and runtime.xml for example, among others.

The ultimate location for the library copy is an arbitrary choice, but note that if you place it with the other predefined libraries, under the existing GNAT Pro installation hierarchy, you will be able to reference it more conveniently. That is because the tools will search for the library in that location, and as a result the full path will not be required.

For illustration we will name the new library “ravenscar-sfp-stm32f429” so that will be the name of the run-time’s root directory as well.

You may want to delete all the files in the “adalib” and “obj” subdirectories after copying and renaming the directory tree. These files are left over from the original build.

Now that we have our new run-time library directory tree in place we can modify it and build it.

Changing Clock Frequency

To change the clock frequency, you change the source code containing a named number defining that frequency. The declaration for this named number is in a system package that reflects “bare board” parameters, but the specific package name, and thus the file name, varies slightly with the architecture involved. For example, with the PowerPC ELF compiler, the package is System.BB.Board_Parameters and thus the file name is s-bbbopa.ads in that case. For the ARM compiler, the package is System.BB.Parameters and s-bbpara.ads is the file name.

The source file is located either in the “arch” subdirectory or in the “gnarl-arch” subdirectory, depending on which run-time library you are using as the basis for the new library.

“Clock_Frequency” is the named number in question. Change this number to reflect the clock frequency for the specific board in use. For the STM32F4 Discovery boards the maximum clock is 168MHz. For the STM32F429 Discovery boards the maximum clock frequency is 180MHz, so we change the named number to that value.

Having made this change we can then build the run-time library using gprbuild:

gprbuild --target=arm-eabi -P ravenscar_build.gpr

The new run-time will now support applications running on STM32F4 Discovery boards at the higher clock frequency.

(Note that when building the Ravenscar “Full” run-times you will specify the “runtime_build.gpr” file instead. We have copied a Ravenscar “SFP” run-time in this specific case.)

Changing Memory Size

To change the memory size you must modify a linker script located in the run-time library directory tree. These scripts are located in the “arch” subdirectory, specifically. The details of modifying the linker scripts vary with the target architecture. We use the PowerPC ELF and STM32F4 Discovery run-times to illustrate the general approach.

In the root of the run-time directory tree is a file named runtime.xml that is read by gprbuild when you build your application executable. This XML file controls whether the executable will be loaded into ROM or RAM, using two different linker scripts for that purpose. On the PowerPC ELF compiler, for example, these two files are qemu-rom.ld and ram.ld. By modifying these files you can change the memory layout and sizes.

Because the XML file is an input into gprbuild, the choice of RAM versus ROM is controlled by an “external reference,” also known in GPS as a “scenario variable.” You can specify the values of these external references in various ways, at build time, thereby dynamically controlling which scripts are applied. There is also a default value if no explicit argument is specified.

For example, in the PowerPC case, the external reference is declared in the XML file as follows:

type Loaders is ("ROM", "RAM");

Loader : Loaders := external ("LOADER", "ROM");

The default value in this case is “ROM.”

Then, later in the same XML file, the value of “Loader” controls the choice in the Linker package:

package Linker is
    ...

    case Loader is
       when "ROM" =>
          for Required_Switches use Linker'Required_Switches &amp;
               ("-Wl,-u_start_rom",
               "-Wl,-T${RUNTIME_DIR(ada)}/arch/qemu-rom.ld");
       when "RAM" =>
          for Required_Switches use Linker'Required_Switches &amp;
              ("-Wl,-u_start_ram",
               "-Wl,-T${RUNTIME_DIR(ada)}/arch/ram.ld");
    end case;

end Linker;

When building our applications, we can specify the value for “Loader” on the command line using -X:

gprbuild --target=powerpc-elf -P some_application.gpr -XLOADER=RAM

Thus the ram.ld file will be applied when linking the application.

We can also use external references to tailor the application to a specific board. The STM32F4 run-time library is pre-configured for this purpose so we use it to illustrate the approach.

In addition to the “Loader” external reference, the runtime.xml file in the STM32F4 run-time directory also defines an external reference to specify the target board:

type Boards is ("STM32F4-DISCO", "STM32F429-DISCO", "STM32F7-EVAL");

Board : Boards := external ("BOARD", "STM32F4-DISCO");

There are, correspondingly, three additional linker scripts in the “arch” directory:

  • STM32F4-DISCO-memory-map.ld
  • STM32F7-EVAL-memory-map.ld
  • STM32F429-DISCO-memory-map.ld

Notice how the possible values for “Board” correspond to parts of the names of those three files. The value of “Board” in the XML file controls the choice in the Linker package by forming part of the file name for the linker script applied:

package Linker is
   ...

   case Loader is
      when "USER" =>
      when others =>
         for Required_Switches use Linker'Required_Switches &amp;
         (...
          "-T", Board &amp; "-memory-map.ld",
          ...
  end case;
end Linker;

The three board-specific linker scripts thus control the amount of memory available. For example, the STM32F4-DISCO-memory-map.ld script specifies limits specific to that board:

MEMORY
{
  flash (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
  sram (rwx) : ORIGIN = 0x20000000, LENGTH = 192K
}

whereas the STM32F429-DISCO-memory-map.ld script specifies more, per that board’s limits:

MEMORY
{
  flash (rx) : ORIGIN = 0x08000000, LENGTH = 2048K
  sram (rwx) : ORIGIN = 0x20000000, LENGTH = 256K
}

Linking your application with the file corresponding to your board will make those extra resources available. To do so, we specify the value for “Board” on the command line using -X:

gprbuild --target=arm-eabi -P some_application.gpr -XBOARD=STM32F429-DISCO

Thus the STM32F429-DISCO-memory-map.ld file would be applied when linking the application.

Although we have created a new run-time library specifically for the STM32F429 Discovery boards, the default value for “Board” in runtime.xml still specifies the STM32F4 Discovery board. Unless users override this value, any application built with this new run-time will have a memory size limited to that of an STM32F4 Discovery board. The simplest and most robust solution is to change the default for the external reference to specify the F429:

Board : Boards := external ("BOARD", "STM32F429-DISCO");

Now applications will have the extra memory available without having to specify the “Board” value on the command line.

Porting the GDB stub

The GDB stub provides remote loading and debugging facilities. The GDB stub is a tiny application that uses a serial line to communicate with a GDB hosted on the development machine.

On powerpc-elf the sources of the GDB stubs are in <gnat-root>/share/examples/gnat-cross/gdbstub/powerpc-elf. Two files have to be customized: the project file stub.gpr must be modified to extend the zfp-mpc8641 run-time library project for your board and the input/output file gdbstub_io.adb which must be ported to your board.

Once the GDB stub is working, you also need to write a simplified BSP for the stub that doesn’t initialize the board (as it is already done by the stub) and that maps the application in RAM.

Porting the Ravenscar run-time library

Overview

The Ravenscar run-time library is based on a very simple real-time kernel supporting preemptive fixed-priority scheduling. Tasks interact via protected objects, ensuring bounded priority inversion and absence of deadlock by using the priority ceiling locking policy (there are no explicit locks, but a task can block other tasks or interrupts by inheriting the ceiling priority while executing protected operations). Protected procedures can be used as statically bound interrupt handlers.

See the Ravenscar Profile section in the GNAT User’s Guide Supplement for GNAT Pro Safety-Critical and GNAT Pro High-Security for more details about the Ravenscar tasking model.

It is also assumed that you have a simple serial port driver if you have a serial I/O device.

Most of the Ravenscar kernel for PowerPC is portable, as it depends almost entirely on features available on every PowerPC:

  • It only handles one external interrupt. Although the kernel can be modified to handle more interrupts (discussed below), this basic scheme makes the porting effort easier.
  • It uses the decrementer as the timer.
  • The context saved is the one defined by the PowerPC UISA environment.

The following sections describe the parts that need to be adapted for a given PowerPC board.

Interrupts

The provided implementation of the Ravenscar run-time library for PowerPC uses only one interrupt priority for the external interrupt. Neither multiplexing interrupts nor attaching handlers are handled by this implementation. This simplifies the provided implementation and make it easy to port.

However, it is possible to extend the interrupt support in the run-time library.

The first prerequisite is to know the number of interrupt priorities. You can then define the priority constants of package System.

The subtype Any_Priority represents the subtype for the priorities. This includes the priorities for normal tasks (subtype Priority) as well as the priorities for the interrupts (subtype Interrupt_Priority). As interrupts have a higher priority than normal tasks (because an interrupt will preempt a normal task), the Interrupt_Priority subtype values are just above the range for the Priority subtype.

It is possible to use a priority within the Interrupt_Priority range for a task, although this is not usual. In this case this task will block all the interrupts with a lower priority. This must be done with care, particularly if the clock or the timer are masked.

The range 0 .. 255 is recommended for the Any_Priority subtype, since 256 level of priorities should be enough, but you can change these bounds. The range does not affect the size of the run-time library, since there is currently only a single queue for the ready tasks. According to the ARM, System.Priority shall include at least 30 values.

You can always use one level of priority for interrupts, and all interrupts will be masked at that level. Note that this is the minimum value, as according to the ARM, the range of System.Interrupt_Priority shall include at least one value.

The range of System.Interrupt_Priority should map to the range of hardware priority levels, in order to have a one-to-one correspondence between them.

You must also define the Default_Priority constant which is the priority used for a task when it is not explicitly set by a Pragma Priority.

So ideally you just have to adjust Nbr_Interrupt_Priorities in system.ads:

Nbr_Interrupt_Priorities : constant Positive := 32;
Max_Interrupt_Priority   : constant Positive := 255;

subtype Any_Priority       is Integer
  range   0 .. Max_Interrupt_Priority;
subtype Priority           is Any_Priority
  range   0 .. Max_Interrupt_Priority - Nbr_Interrupt_Priorities;
subtype Interrupt_Priority is Any_Priority
  range Priority'Last + 1 .. Max_Interrupt_Priority;

Default_Priority : constant Priority :=
  (Priority'Last - Priority'First) / 2;

You should not modify anything else in the system.ads file.

Once this is done, you can implement Disable_Interrupts and Enable_Interrupts in System.BB.CPU_Primitives. Disable_Interrupts is quite simple as it simply disables all interrupts, and the provided implementation should work. Enable_Interrupts has to enable all interrupts if the level is 0, and it has to disable all interrupt if the level is Interrupt_Priority’Last. In addition, Enable_Interrupts has to program the interrupt controller for levels between the two extremes of the range of interrupt levels (0 .. Nbr_Interrupt_Priorities). Of course things are simple if you only have one interrupt level.

You can also list the interrupt names and priorities in Ada.Interrupt.Names. That would be useful when you want to use interrupts.

You also have to define and implement the stack strategy. As the provided implementation has only one interrupt, it uses the application stack during the interrupt. But you could also have a stack dedicated to the execution of the interrupt, or even one stack per interrupt level.

Timer

Time is handled using the PowerPC decrementer and time base registers. Therefore it is very portable as it is available in the entire family of processors. The only implementation-defined parameter is the bus frequency, which also defines the frequency of the time registers.

The frequency is defined by the Clock_Frequency constant in the s-bbpara.ads file as in this example:

Clock_Frequency : constant Positive := 66_000_000;