Bareboard C and C++ Topics

This appendix describes how to use the GNAT Pro tool-chains for C or C++ to build programs that do not require support from an embedded real-time operating system running on the target hardware.

Introduction

The C++ standard defines a “freestanding” implementation as one that does not require a host system to provide resources or a backing. Execution may happen without an operating system in a freestanding implementation. As a result, the set of standard library headers required by the C++ standard is smaller in a freestanding implementation than in a “hosted” implementation.

Our bareboard C++ run-time is provided as a pre-compiled freestanding library implementation. Therefore, this run-time contains a limited set of the full C++ run-time and support libraries. Only simple standard classes, exceptions, and user defined classes are supported.

Bareboard C++ Run-Time Libraries

GNAT Pro’s freestanding run-time library is distributed with libsupc++ and an empty libstdc++. The empty libstdc++ is used to simplify the compiling/linking workflow. If libstdc++ was not provided, libsupc++ would need to be manually linked for each project. This manual step remains required if -nostdlib is used.

Details about the C++ freestanding library and available headers can be found using the CPP Reference - Freestanding documentation. GNAT Pro closely follows the implementations detailed in that documentation.

The intent with the freestanding library is to keep a small code-base, an important consideration in a certification context. For that same reason, some specific headers are known to work in a freestanding environment but are not included because of their code complexity.

The freestanding run-time library is selected automatically by the GNAT Pro builder. As a consequence, a GNAT Project file for C++ projects does not specify the Runtime attribute, unlike project files for Ada projects.

NewLib

The libc library comes pre-compiled with GNAT Pro for bare-board targets. However, a supplemental source file, named “newlib.c”, is required to cover target-specific functionality. This file is included with those targets directly supported by GNAT Pro. Although the file name suggests the purpose, it is not intended to be a complete Newlib implementation.

For targets that are not directly supported by GNAT, the customer must supply the implementations for some or all of the newlib.c functions. These can be provided as C functions. If provided in C++ make sure to use extern “C” {}, otherwise the expected symbols will not be generated for linking with libc.

Note that specific customer requirements may also lead the customer to change the GNAT-supplied functions’ implementations.

The following are templates for two of these functions, in which the ellipses indicate target-specific code to be added by the customer:

int read (int fd, char *buf, int count) {
  ...
  return count;
}

int write (int fd, char *buf, int nbytes) {
  ...
  return nbytes;
}

For example, if the system designer intends to use a UART for output, the body of function write could be implemented accordingly. The read function could be implemented that way too, or just set to return 0, or whatever is most appropriate for the application. The various functions that invoke read/write, such as printf, would then call these versions using a UART.

#include <usart.h>
#include <stm32f4xx_hal_uart.h>

int write (int fd, char *buf, int nbytes) {
  if (HAL_UART_Transmit(&huart1, (uint8_t*)buf, nbytes, 1000) == HAL_OK)
    return nbytes;

  return 0;
}


int read (int fd, char *buf, int count) {
  if (HAL_UART_Receive (&huart1, buf, count, 1000) == HAL_OK)
    return count;

  return 0;
}

In the code above, we call some of the STM32 HAL (Hardware Abstraction Layer) routines. More code would be required, both for the UART driver setup and the integration with the target-specific interrupt handler. This code is just for the sake of illustrating the change to newlib.c.

Similarly, if your startup code calls the main() function and that function returns for any reason, the startup code could then call the _exit() function. What the implementation of _exit() actually does will vary with application requirements, but for example the following will reset the Arm Cortex processor (and then wait for that to take effect):

void _exit(int status) {
  /* status parameter is ignored, as there is nothing we can do with it in
     this target context */

  /* designate the System Control Block's application interrupt/reset control
     register */
  volatile uint32_t* AIRCR = (volatile uint32_t*) 0xE000ED0C;

  /* a DBM barrier instruction might be necessary here */

  /* reset the processor */
  *AIRCR = 0x05FA0004;

  /* wait until reset actually happens */
  while (1) {
  }
  /* note that the hardware will reset, so the above loop "exits" eventually,
     because the startup code is invoked again (as if the first time) */
}

Finally, the functions that must be defined in newlib.c will depend on the explicit application code and functions implicitly called by that code. For example, the read() and write() functions can be omitted if no I/O routines are called by the application.

Exceptions

If exceptions are to be used in a project, the linker file must be updated to define the eh_frame sections. In addition, these sections must be registered/deregistered as part of init/fini calls. An example is shown below. A complete solution can be found distributed with the compiler in:

./share/examples/gnat-c

The integration of the eh_frames requires runtime routines, provided by crti.o and libgcc_eh.a, as well as the defined sections in the linker file.

For the linker file, something similar to the following is required:

.eh_frame_hdr : {
   __eh_frame_hdr = .;
   *(.eh_frame_hdr)
} > MEMORY REGION

.eh_frame : {
   __EH_FRAME__ = .;
   KEEP(*(.eh_frame))
   LONG(0);
} > MEMORY REGION

C/C++ Compiler Configuration

In a bare-metal context, program exit is not expected because there is no system to return to. Restarting the application is typical in these cases. Therefore, the compiler is configured to omit calls for cleaning up at the end of execution. For example:

--disable-__cxa_atexit

Likewise support for any dynamic shared objects is not provided.

--disable-__dso_handle

Startup and Linker Files

Customers are required to provide any target-specific startup code and linker scripts. To help create these files you can use the Startup-gen tool, documented in the Startup-gen User’s Guide available online and in your GNAT Pro installation hierarchy. The executable is included with the GNAT Pro installation. Use of the tool is optional.

Example_Project

A sample C++ project for a common microcontroller will serve to illustrate the overall approach. The specific target is an STM32F429 Discovery board. The example code itself does not exercise the board’s specific hardware. Instead, we use the board for the sake of a concrete example of the startup code and other code required, including for example the auxiliary code for newlib.

The example consists of two GNAT projects and their respective project files (“gpr files”). One of the projects, named STM32F4Disco, contains all the board-specific project content. The other project, named Example, contains only the simple example code that does a very minimal set of statements and then loops forever. Besides illustrating the fact that the program should never exit, the main function does enough to step through the debugger in case that is of interest.

The separation of the target-specific project from the target-independent application project allows makes the two simpler and more maintainable.

The Example project specifies the dependency upon the STM32F4Disco project via a “with-clause”, like so:

project STM32F429Disco is
   ...
end STM32F429Disco;


with "stm32f429disco.gpr";
project Example is
   ...
end Example;

As a result, when the Example project file is specified to the GNAT Pro builder, the builder will, if necessary, first build the STM32F429Disco project. It will only build STM32F429Disco if something has changed from a previous build of that project. Note in particular that you can explicitly build the STM32F429Disco project by itself, for example to test it.

The STM32F429Disco project also contains hardware setup code called before the startup routine branches to the main function. The SystemInit function initializes target, e.g., the FPU, the vector table location, and external memory configuration. The function BoardInit initializes the board’s configured peripherals (the GPIO and UART in particular):

...

        /* Call the clock system initialization function.*/
        bl      SystemInit

        /* Board initialization */
        bl      BoardInit

        /* Call static constructors */
        bl      __libc_init_array

        /* Call main, making sure that if main expects argc, argv they're
           both set to 0.  */
        movs    r0, #0
        movs    r1, #0
        bl      main

        /* save return value */
        mov     r4, r0

        /* static destructors */
        bl      __libc_fini_array

        /* pass main's status to _exit */
        mov     r0, r4
        bl      _exit

        /* Should never reach this point */
        bx       lr
        .size Reset_Handler, . - Reset_Handler

...

Note that this startup code example includes calls to functions defined in the newlib.c file. For example, the initialization function _init() is indirectly called by __libc_init_array(), and the _exit() function in this example resets the board.

Finally, the STM32F429Disco project contains the assembly code defining the vector table content and a default handler that enters an infinite loop, preserving the system state for examination by a debugger. Application code can define interrupt handlers that override the default handler.

You are not required to take this two-project approach but as you can see it does provide a good separation of concerns.