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.
Introduction
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
For supported boards, AdaCore strives to make available the Light, Light-Tasking and Embedded run-times. However, Embedded and Light-Tasking may not be available for some supported boards that do not have the memory or hardware resources required to support these run-time libraries.
Prebuilt run-time libraries are named after the run-time and the board that they support. For example, the name ‘embedded-tms570’ denotes an Embedded run-time library targeted to the Texas Instruments TM570 board. This library can be specified as follows:
project Demo is
...
for Runtime ("Ada") use "embedded-tms570";
...
end Demo;
See Specifying the Run-Time Library for further details on how to specify your selection of a run-time library.
Generic bareboard Light 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 Light 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 Light 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 run-time 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 run-time, including the smaller (Light) run-time, 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
begin
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.
Linker Scripts
Multiple linker scripts may be included with a run-time to support
different memory layout and loading scenarios. For example, scripts for
the ARM Cortex-M support loading a program to Flash
(common-ROM.ld
) or to RAM (common-RAM.ld
).
The LOADER
scenario variable selects which linker script is used
when building an application project with gprbuild. The variable is
defined within an XML file named run-time.xml
: a file that
provides target-specific project settings to gprbuild and can be found
at the root of every run-time directory tree.
As can be seen in the abridged version of this XML file, the scenario
variable LOADER
controls the choice of linker script:
1 type Loaders is ("ROM", "RAM", "USER");
2 Loader : Loaders := external("LOADER", "ROM");
3
4 ...
5
6 package Linker is
7 for Required_Switches use Linker'Required_Switches &
8 ("-Wl,-L${RUNTIME_DIR(Ada)}/adalib",
9 "-nostartfiles", "-lc", "-lgnat",
10 "-L${RUNTIME_DIR(ada)}/ld_user",
11 "-L${RUNTIME_DIR(ada)}/ld") &
12 Compiler.Common_Required_Switches;
13
14 case Loader is
15 when "ROM" =>
16 for Required_Switches use Linker'Required_Switches &
17 ("-T", "common-ROM.ld");
18 when "RAM" =>
19 for Required_Switches use Linker'Required_Switches &
20 ("-T", "common-RAM.ld");
21 when "USER" =>
22 end case;
23 end Linker;
You can specify the value of this scenario variable on the command line
when invoking gprbuild, thereby controlling which script is applied. The
default value (ROM
) is used if no explicit argument is specified,
while the value USER
allows a project specific linker script to be
specified in the gpr file of the project.
For example, given a project file app.gpr
describing the
application, we can invoke gprbuild and supply one or more scenario
variable values:
$ gprbuild -P app.gpr -XLOADER=RAM
Or to take the default:
$ gprbuild -P app.gpr
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 (
"-Wl,--gc-sections",
"-Wl,--print-gc-sections");
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 (
"-Wl,--gc-sections",
"-Wl,--print-memory-usage");
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%
Stacks
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 Embedded run-times.
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 Embedded run-times, package System.Memory implements dynamic
memory allocation using calls to the included C standard library Newlib.
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 Light and Light-Tasking 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 run-time is provided
in the Embedded run-time 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;
begin
K := K - 1;
exception
when E : others =>
declare
Buffer : constant Ada.Exceptions.Traceback.Tracebacks_Array :=
Ada.Exceptions.Traceback.Tracebacks (E);
begin
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;
end P1;
procedure P2 is
begin
P1;
end P2;
begin
P2;
end STB;
$ gprbuild -g --target=leon3-elf --RTS=embedded-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
stb.adb:10
stb.adb:28
stb.adb:32
b~stb.adb:106
??:0
$ 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.