Bareboard Topics

This appendix describes how to use the GNAT cross tools to build programs that do not require support from an embedded real-time operating system running on the target hardware.


The bareboard run-times are provided as customized run-times that are configured to target a specific microcontroller or processor as documented in the respective target appendix. This simplifies the build of an embedded application as explained in this appendix. For different microcontrollers and processors, the run-time libraries need to be ported to your target. Note that any such changes must be revisited (and likely re-ported) if you update the compiler. See Customized Bareboard Run-Time Libraries for adapting a run-time to your board.

Bareboard Run-Time Libraries

The pre-built run-time libraries have names that reflect their characteristics. For example, those that include ‘ravenscar’ in their names provide a subset of the Ada tasking facilities defined by the Ravenscar profile. Similarly, those libraries with ‘zfp’ in the name implement the ‘Zero Foot Print’ profile.

The pre-built run-time libraries also include the name of the target board they support. The names therefore vary with the target and corresponding toolchain.

For example, the name ‘ravenscar-full-tms570’ denotes a run-time library targeted to the Texas Instruments TM570 board. The library will provide the Ravenscar Profile tasking subset. The ‘full’ in the name indicates that the full sequential (i.e., non-tasking) part of Ada is supported. This library could be specified as follows:

project Demo is
   for Runtime ("Ada") use "ravenscar-full-tms570";
end Demo;

See Specifying the Run-Time Library for instructions on how to specify your selection of a run-time library.

Generic bareboard ZFP run-times

In addition to pre-built run-times targeting specific microcontrollers and processors, GNAT for bareboard ARM and GNAT for bareboard RISC-V include pre-built generic ZFP run-time libraries that target specific Cortex-M and RISC-V cores. These generic run-times lack microcontroller specific startup code and linker scripts, enabling them to be provided separately without creating and building a new run-time. See their respective target appendix for a list of supported Cortex-M and RISC-V cores.

To help create the microcontroller specific startup code and linker scripts, GNAT Pro includes Startup-gen that will generate these files based on the properties of the board and microcontroller. See the Startup-gen User’s Guide for more details on how to use the tool with generic ZFP run-times.

Getting Started with the Bareboard Toolchain

To get started on GNAT Pro for a bareboard target, let’s imagine we would like to build a program whose purpose is to print “Hello World” on standard output. For this, you’ll need to update your path so as to add both GNAT Pro and GNAT Studio Pro.

Once this is done, let’s start GNAT Studio. At startup, GNAT Studio opens a window allowing you to choose between using an existing project file, or creating a new one. Let’s select “Start with default project”, which will create one for us with the default settings.

Next, we need to tell GNAT Studio which platform we want to target. Select Menu “Edit->Project Properties…”. Click on the “Build->Toolchain” tab and select the GNAT Pro toolchain corresponding to your target.

The second element that needs to be provided is the runtime you want to use. In the “Build->Toolchain” tab we just opened, just click on the down arrow to the right of the “Ada Runtime” field to list all the run-times which were automatically detected, and select the one you’d like to use. Our Hello World program is simple enough that any runtime, including the smaller (ZFP) runtime, is sufficient.

Click on “Save” to exit the project editor.

Now that this is done, we can write our program. We want that program to be part of our project, so let’s use the project view to create the file. The project view is a tab showing a blue folder named “Default” (the name of our project), and you should see two yellow folders underneath. Right-click on the first one, which should not have a small ‘o’ inside, and select menu “New->Ada Main Unit”. Let’s name our program “hello”, so enter “Hello” in the window that GNAT Studio then pops up.

GNAT Studio has opened an editor for file “hello.adb”, and provided an implementation skeleton. We can complete that skeleton to contain the following program:

with GNAT.IO; use GNAT.IO;
procedure Hello is
   Put_Line ("Hello World");
end Hello;

Once done, we also need to tell GNAT Studio to include this unit as one of the executables we want to build when building the project. Select menu “Edit->Project Properties” again, the select the “Sources->Main” tab, click on the “+” sign in the “Main files” sections and select “hello.adb”. Click on “Save” to exit the project editor.

We’re now ready to build our program! The fastest way to trigger the build is to press the F4 key. A window allowing some customization of the build pops up, but the default settings should be good enough, so either click on “Execute” or just press Enter. You should see the output of the build in the “Messages” window, confirming that “hello” was successfully built.

GNAT Pro for bareboard comes with GNATemulator, AdaCore’s simulator. To run our program under that simulator, just select menu “Build->Emulator->Run with Emulator->hello.adb”. A new output window labeled “[…]-gnatemu” should be opened, and should show the output of our program.

It is also possible to debug the program while being run under GNATemulator, simply by selecting menu “Build->Emulator->Debug with Emulator->hello.adb”. This starts the debugging session after which it is possible to debug the program as usual.

It should also be possible to run your program on the real hardware, rather than GNATemulator. However, given the many variations on the host and target environment that you may be using, it is not possible to formulate a universally applicable sequence of commands for building, loading, and running a program.

Minimizing Object Code and Data Sizes

Bareboard targets typically have stringent constraints on available storage, both for data as well as object code. In addition to careful coding practices, such as sharing generic package instantiations as much as possible, you can request the removal of unused code and data at build time. The technique uses standard GCC switches.

To achieve this reduction, you must specify switches for both the compiler and the linker.

In the compiler switches, request removal of unused code by applying the "-ffunction-sections" switch. In the same way, request removal of unused data by applying the "-fdata-sections" switch. Specifying both switches is allowed.

In the linker switches, specify "-Wl,--gc-sections" to have the linker remove (“garbage collect”) unused code and/or data. Both this linker switch and the compiler switch(s) are essential for any reduction to take effect.

You can get a list of what is removed by including the "-Wl,--print-gc-sections" linker switch.

For example:

package Compiler is
   for Default_Switches ("Ada") use ("-O2", "-ffunction-sections", "-fdata-sections");
end Compiler;


package Linker is
   for Default_Switches ("Ada") use (
end Linker;

Viewing Object Code and Data Sizes

You can have the linker display the amounts of storage used for both code and data, as well as the total amounts available and the resulting usage percentages.

To get this information specify the "-Wl,--print-memory-usage" linker switch.

For example:

package Linker is
   for Default_Switches ("Ada") use ("-Wl,--print-memory-usage");
end Linker;

Of course, you can combine this switch with the switch requesting removal of unused sections. The Linker package in the project file would then appear as follows:

package Linker is
   for Default_Switches ("Ada") use (
end Linker;

With this switch applied the linker will print the information to standard output. The results will thus appear in the GNAT Studio Messages window.

For example:

Memory region         Used Size  Region Size  %age Used
           flash:       44268 B         2 MB      2.11%
            sram:       48784 B       256 KB     18.61%


Refer to The Primary and Secondary Stacks for information on setting task stack sizes.

GNAT does not perform stack overflow checking by default. This means that if the main environment task or some other task exceeds the available stack space, then unpredictable behavior will occur.

Support for stack overflow checking is provided in the ravenscar-full runtimes. To use, compile all units with the gcc option -fstack-check. Units compiled with this option will generate extra instructions to check that any use of the stack (for procedure calls or for declaring local variables in declare blocks) do not exceed the available stack space. If the space is exceeded, then a Storage_Error exception is raised.

Character Input/Output

The bareboard run-time libraries provide a customized version of package Ada.Text_IO that exports a minimal set of facilities. The package declaration of this customized version is as follows, for all the bareboard run-time libraries:

package Ada.Text_IO is

   procedure Get (C : out Character);
   --  Read from console

   procedure Put (Item : Character);
   --  Output character to the console

   procedure Put (Item : String);
   --  Output string to the console

   procedure Put_Line (Item : String);
   --  Output string followed by new line to the console

   procedure New_Line;
   --  Output new line character to the console

end Ada.Text_IO;

The cut-down package is provided for the sake of initial board check-out and development and is not intended to be a production I/O capability.

The implementations of the package body are target dependent.

For example, on some targets the implementation drives a USART for this purpose. You can use a USB-to-Serial conversion cable to connect those pins (and a ground pin) to a USB port on the host computer, and run an application on the host to monitor that port in order to interact with the board.

In contrast, on the ARM STM32F4x Discovery boards the implementation uses “semihosting”, in which the output goes to the screen showing the st-util outputs. No USB-to-Serial conversion cable is required in that case.

The package GNAT.IO is also provided by the bareboard run-time libraries. The interface is slightly different, as shown below:

package GNAT.IO is
   pragma Preelaborate;

   procedure Put (X : Integer);
   --  Output integer to specified file, or to current output file, same
   --  output as if Ada.Text_IO.Integer_IO had been instantiated for Integer.

   procedure Put (C : Character);
   --  Output character to specified file, or to current output file

   procedure Put (S : String);
   --  Output string to specified file, or to current output file

   procedure Put_Line (S : String);
   --  Output string followed by new line to specified file, or to
   --  current output file.

   procedure New_Line (Spacing : Positive := 1);
   --  Output new line character to specified file, or to current output file

end GNAT.IO;

Note, in particular, the ability to output an integer value, and the absence of any input routine.

In both the GNAT.IO and Ada.Text_IO cases, the underlying board-specific implementation part is in an internal System.Text_IO package provided by the run-time library. See s-textio.adb in particular. You can change that package to reflect your target’s hardware I/O capabilities. However, since the exported interfaces are minimal, you may want to consider using a more target-specific facility as an alternative to GNAT.IO and Ada.Text_IO.

Floating-Point Support

The bareboard run-times support floating-point arithmetic on processors that have a floating-point unit (FPU) and provides emulated support in software for targets that lack an FPU. By default, the provided run-time libraries are configured to use the hardware FPU if one is present on the target.

As documented in the GNAT Reference Manual, where possible GNAT is configured to follow the IEEE Standard for Floating-Point Arithmetic (IEEE 754). This holds true for the bareboard run-time libraries where the run-time libraries and FPU are configured to follow this standard where the target FPU supports this standard. When a target does not follow IEEE 754, the FPU and run-time configuration is documented explicitly in the target’s section.

Be aware that to conform with IEEE 754, exceptions will not be raised in cases where a floating-point operation overflows. Instead, infinities will generated instead.

Note: the configuration of the FPU is coupled to the configuration of the bareboard run-time library and should not be modified by the user.

Dynamic Memory Allocation

All bare-board run-times support dynamic memory allocation using standard Ada syntax for allocating and deallocating to and from the default storage pool (more commonly known as the “heap”). The package System.Memory implements this feature, in concert with the compiler.

Note that the compiler creates calls to procedures declared within System.Memory and that the package defines a number of global symbols. ** Do not change the package spec.** The implementation in the package body is run-time dependent and can be modified if desired.

For the Ravenscar-Full run-times, package System.Memory implements dynamic memory allocation using calls to the C standard library Newlib, which is included with the Ravenscar-Full run-times. This implementation is generally sufficient for most users, with the memory allocated from the start of the _end symbol defined in a linker script. Note: Newlib does not bound the amount of memory that can be allocated by malloc.

For the Ravenscar-SFP and ZFP run-times, dynamic memory allocation is implemented directly within the package body. (The package exports the procedures for use by C code.) The location and size of the heap is defined by the __heap_start and __heap_end symbols in the linker script. The implementation in the package uses these bounds to ensure the RAM is not over-allocated. All RAM not allocated to static objects and stacks is allocated to the heap. As a result, manual sizing of the heap is generally not required when using these run-times.

Bareboard Debugging

Debugging applications on bareboard targets involves connecting GDB to the board or an emulator via a gdb-server. You can then either use GDB on the command line, or use GNAT Studio or GNATbench to drive it. When connecting to a board, a debugging probe is used, typically over JTAG or a device specific connection. For compatibility with GDB, choose debugging hardware that provides a gdb-server, which may be either built into the debugging hardware or provided as a separate software program. The specific mechanisms for connecting, and whether an emulator and/or simulator is available, are target-specific and are discussed in the corresponding sections within this document.

Non-Symbolic Traceback

A non-symbolic traceback is a list of addresses of call instructions and the support to provide the generation of this information at runtime is provided in the ravenscar-full runtime library. To enable this feature you must use the -E* gnatbind’s option. With this option a stack traceback is stored as part of the exception occurrence.

Here is a simple example:

with GNAT.IO;
with Ada.Exceptions.Traceback;
with GNAT.Debug_Utilities;

procedure STB is

   procedure P1 is
      K : Positive := 1;
      K := K - 1;
      when E : others =>
            Buffer : constant Ada.Exceptions.Traceback.Tracebacks_Array :=
              Ada.Exceptions.Traceback.Tracebacks (E);
            GNAT.IO.Put_Line ("Call stack traceback locations:");

            for J in Buffer'Range loop
               GNAT.IO.Put (GNAT.Debug_Utilities.Image_C (Buffer (J)));
               GNAT.IO.Put (" ");
            end loop;
   end P1;

   procedure P2 is
   end P2;

end STB;
$ gprbuild -g --target=leon3-elf --RTS=ravenscar-full-leon3 stb -bargs -E
$ leon3-elf-gnatemu stb
Call stack traceback locations:
0x4000194C 0x40001518 0x400014F4 0x400014C0 0x4000116C

The location of these call instructions can be inspected with standard binary oriented tools, such as nm or objdump, or with the addr2line tool, that converts addresses into file names and line numbers. It is also possible to use GDB with these traceback addresses to debug the program. For example, we can break at a given code location, as reported in the stack traceback.

$ leon3-elf-addr2line -e stb 0x4000194C 0x40001518 0x400014F4 \
     0x400014C0 0x4000116C


$ leon3-elf-gdb stb
(gdb) break *0x4000194c
Breakpoint 1 at 0x4000194c: file stb.adb, line 10.

It is important to note that the stack traceback addresses do not change when debug information is included. This is particularly useful because it makes it possible to release software without debug information (to minimize object size), get a field report that includes a stack traceback whenever an internal bug occurs, and then be able to retrieve the sequence of calls with the same program compiled with debug information.

By default, unhandled exceptions display the stack traceback information stored within the exception occurrence.