AdaCore: Build Software that Matters
I Stock 866702538
Jan 11, 2021

Ada on any ARM Cortex-M device, in just a couple minutes

In this blog post I want to present a new tool that allows one to very quickly and easily start Ada programming on any ARM Cortex‑M or RISC‑V microcontroller.

To program a microcontroller with Ada, one must start with a run-time library. The run-time provides various Ada features (depending on the run-time profile), and compilation options.

The run-time traditionally also provides board specific code that has to be adapted for each board.

The solution I am presenting here removes the board specific parts of the run-time and provides a tool to generate them from a simple description of the hardware.

startup-gen

The solution is focused on the Zero-FootPrint (ZFP) run-times. These run-times support one of the simplest subsets of Ada and do not, in particular, implement Ravenscar tasking. The Ravenscar run-times require much more board adaptations. They are, therefore, not covered by this solution.

Starting with GNAT Pro 21.0 or GNAT Community 2020, in addition to pre-built run-times targeting specific microcontrollers and processors, GNAT for bareboard ARM and bareboard RISC‑V includes pre-built generic ZFP run-time libraries that target specific Cortex‑M and RISC‑V cores. These generic run-times omit microcontroller specific startup code and linker scripts, enabling them to be provided separately without creating and building a new run-time.

A new tool, startup-gen, is introduced to generate the missing startup code and linker scripts from a description of the hardware provided in the project file.

You can get startup-gen packaged with GNAT Pro 21.0 or from the Alire package manager:

$ alr get --build startup_gen

Let’s look at an example

For this example we will use the STM32F4-Discovery development board from STmicro. The board is equipped with an ARM Cortex-M4F microcontroller.

Board specifications

To begin with, we need to know the specification of the board and microcontroller. We will need:

  • The name of the CPU core architecture (ARM Cortex-M4F in our case)
  • Base address and size of memory banks (flash, RAM, etc.)
  • The number of interrupts

You can get the information from the vendor documentation or product page.

Another way to get the required information is look in the XML-based package description (PDSC) files of a CMSIS pack. For instance in the STM32F4XX PDSC we can see:

<device Dname="STM32F407VG">
  <memory id="IROM1"  start="0x08000000" size="0x00100000" startup="1" default="1"/>
  <memory id="IRAM1"  start="0x20000000" size="0x00020000" init   ="0" default="1"/>
  <memory id="IRAM2"  start="0x10000000" size="0x00010000" init   ="0" default="0"/>

The project file

Given that board description we can then augment the GNAT project (gpr) file.The project file will require some specific fields:

  • The list of languages must contain ASM_CPP, because we will compile the startup code (crt0) written in assembly language.
  • The run-time should be set to zfp-cortex-m4f because we are using an ARM Cortex-M4F microcontroller. This is one of the generic ZFP run-time mentioned above.
  • The linker script must be specified as a linker option
  • The board specifications in a Device_Configuration package

Here is what the resulting project file looks like:

project Hello is

   for Languages use ("Ada", "ASM_CPP"); -- ASM_CPP to compile the startup code
   for Source_Dirs use ("src");
   for Object_Dir use "obj";
   for Main use ("hello.adb");

   for Target use "arm-eabi";

   --  generic ZFP run-time compatible with our MCU
   for Runtime ("Ada") use "zfp-cortex-m4f";

   package Linker is
      --  Linker script generated by startup-gen
      for Switches ("Ada") use ("-T", Project'Project_Dir & "/src/link.ld");
   end Linker;

   package Device_Configuration is

      --  Name of the CPU core on the STM32F407
      for CPU_Name use "ARM Cortex-M4F";

      for Float_Handling use "hard";

      --  Number of interrupt lines on the STM32F407
      for Number_Of_Interrupts use "82";

      --  List of memory banks on the STM32F407
      for Memories use ("SRAM", "FLASH", "CCM");

      --  Specify from which memory bank the program will load
      for Boot_Memory use "FLASH";

      --  Specification of the SRAM
      for Mem_Kind ("SRAM") use "ram";
      for Address ("SRAM") use "0x20000000";
      for Size ("SRAM") use "128K";

      --  Specification of the FLASH
      for Mem_Kind ("FLASH") use "rom";
      for Address ("FLASH") use "0x08000000";
      for Size ("FLASH") use "1024K";

      --  Specification of the CCM RAM
      for Mem_Kind ("CCM") use "ram";
      for Address ("CCM") use "0x10000000";
      for Size ("CCM") use "64K";

   end Device_Configuration;
end Hello;

Generate the linker script and startup code with startup-gen

Once the project file is ready we can use startup-gen to generate the linker script and startup code. To do this, use the following command line:

$ startup-gen -P hello.gpr -l src/link.ld -s src/crt0.S

This means that startup-gen will create a linker script in src/link.ld and an assembly code file in src/crt0.S.

Create the Ada application code

We need some Ada code to run on the board, so let’s write a simple hello world in src/hello.adb:

with Ada.Text_IO;

procedure Hello is
begin
   Ada.Text_IO.Put_Line ("Hello world!");
end Hello;

Build

We can now build our project:

$ gprbuild -P hello.gpr

It is also possible to open this project in GNATstudio and build it from there.

Run

We can now run the program, for example on GNATemulator:

$ arm-eabi-gnatemu --board=STM32F4 obj/hello

Scenario Variables

startup-gen supports the use of scenario variables in the input project file. These can be used in multiple ways, here are two examples:

Select boot memory

project Prj is

   type Boot_Mem is ("flash", "sram");
   Boot : Boot_Mem := external ("BOOT_MEM", "flash");

   package Device_Configuration is

      for Memories use ("flash", "sram");

      for Boot_Memory use Boot;

      --  [...]
   end Device_Configuration;
end Prj;
$ startup-gen -P prj.gpr -l link.ld -s crt0.S -XBOOT_MEM=flash
$ startup-gen -P prj.gpr -l link.ld -s crt0.S -XBOOT_MEM=sram

Select boards with different device configuration

project Prj is

   type Board_Kind is ("dev_board", "production_board");
   Board : Board_Kind := external ("BOARD", "dev_board");

   package Device_Configuration is

      for Memories use ("flash", "sram");

      case Board is
         when "dev_board" =>
            for Size ("sram")     use "256K";
         when "production_board" =>
            for Size ("sram")     use "128K";
      end case;

      --  [...]
   end Device_Configuration;
end Prj;
$ startup-gen -P prj.gpr -l link.ld -s crt0.S -XBOARD=dev_board
$ startup-gen -P prj.gpr -l link.ld -s crt0.S -XBOARD=production_board

Conlusion

With this new tool we want to reduce the barrier of entry for Ada/SPARK programming on microcontrollers by removing the run-time customization step. The code is available on GitHub: https://github.com/AdaCore/startup-gen, don’t hesitate to give us feedback or contribute.

Author

Fabien Chouteau

Profile small

Fabien joined AdaCore in 2010 after his engineering degree at the EPITA (Paris). He is involved in real-time, embedded and hardware simulation technology. Maker/DIYer in his spare time, his projects include electronics, music and woodworking.

Blog_

Latest Blog Posts