AdaCore Blog

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

by Fabien Chouteau

In this blog post I want to present a new tool that allows one to very quick­ly and eas­i­ly start Ada pro­gram­ming on any ARM Cortex‑M or RISC‑V microcontroller.

To pro­gram a micro­con­troller with Ada, one must start with a run-time library. The run-time pro­vides var­i­ous Ada fea­tures (depend­ing on the run-time pro­file), and com­pi­la­tion options.

The run-time tra­di­tion­al­ly also pro­vides board spe­cif­ic code that has to be adapt­ed for each board.

The solu­tion I am pre­sent­ing here removes the board spe­cif­ic parts of the run-time and pro­vides a tool to gen­er­ate them from a sim­ple descrip­tion of the hardware.

start­up-gen #

The solu­tion is focused on the Zero-Foot­Print (ZFP) run-times. These run-times sup­port one of the sim­plest sub­sets of Ada and do not, in par­tic­u­lar, imple­ment Raven­scar task­ing. The Raven­scar run-times require much more board adap­ta­tions. They are, there­fore, not cov­ered by this solution.

Start­ing with GNAT Pro 21.0 or GNAT Com­mu­ni­ty 2020, in addi­tion to pre-built run-times tar­get­ing spe­cif­ic micro­con­trollers and proces­sors, GNAT for bare­board ARM and bare­board RISC‑V includes pre-built gener­ic ZFP run-time libraries that tar­get spe­cif­ic Cortex‑M and RISC‑V cores. These gener­ic run-times omit micro­con­troller spe­cif­ic start­up code and link­er scripts, enabling them to be pro­vid­ed sep­a­rate­ly with­out cre­at­ing and build­ing a new run-time.

A new tool, start­up-gen, is intro­duced to gen­er­ate the miss­ing start­up code and link­er scripts from a descrip­tion of the hard­ware pro­vid­ed in the project file.

You can get start­up-gen pack­aged with GNAT Pro 21.0 or from the Alire pack­age man­ag­er:

$ alr get --build startup_gen

Let’s look at an exam­ple #

For this exam­ple we will use the STM32F4-Dis­cov­ery devel­op­ment board from STmi­cro. The board is equipped with an ARM Cor­tex-M4F microcontroller.

Board spec­i­fi­ca­tions #

To begin with, we need to know the spec­i­fi­ca­tion of the board and micro­con­troller. We will need:

  • The name of the CPU core archi­tec­ture (ARM Cor­tex-M4F in our case)
  • Base address and size of mem­o­ry banks (flash, RAM, etc.)
  • The num­ber of interrupts

You can get the infor­ma­tion from the ven­dor doc­u­men­ta­tion or prod­uct page.

Anoth­er way to get the required infor­ma­tion is look in the XML-based pack­age descrip­tion (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 #

Giv­en that board descrip­tion we can then aug­ment the GNAT project (gpr) file.The project file will require some spe­cif­ic fields:

  • The list of lan­guages must con­tain ASM_CPP, because we will com­pile the start­up code (crt0) writ­ten in assem­bly language.
  • The run-time should be set to zfp-cor­tex-m4f because we are using an ARM Cor­tex-M4F micro­con­troller. This is one of the gener­ic ZFP run-time men­tioned above.
  • The link­er script must be spec­i­fied as a link­er option
  • The board spec­i­fi­ca­tions in a Device_​Configuration package

Here is what the result­ing 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;

Gen­er­ate the link­er script and start­up code with start­up-gen #

Once the project file is ready we can use start­up-gen to gen­er­ate the link­er script and start­up code. To do this, use the fol­low­ing com­mand line:

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

This means that start­up-gen will cre­ate a link­er script in src/link.ld and an assem­bly code file in src/crt0.S.

Cre­ate the Ada appli­ca­tion code #

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

with Ada.Text_IO;

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

Build #

We can now build our project:

$ gprbuild -P hello.gpr

It is also pos­si­ble to open this project in GNAT­stu­dio and build it from there.

Run #

We can now run the pro­gram, for exam­ple on GNATemulator:

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

Sce­nario Vari­ables #

startup-gen sup­ports the use of sce­nario vari­ables in the input project file. These can be used in mul­ti­ple ways, here are two examples:

Select boot mem­o­ry #

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 dif­fer­ent device con­fig­u­ra­tion #

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

Con­lu­sion #

With this new tool we want to reduce the bar­ri­er of entry for Ada/​SPARK pro­gram­ming on micro­con­trollers by remov­ing the run-time cus­tomiza­tion step. The code is avail­able on GitHub: https://​github​.com/​A​d​a​C​o​r​e​/​s​t​a​r​t​u​p-gen, don’t hes­i­tate to give us feed­back or contribute.

Posted in

About Fabien Chouteau

Fabien Chouteau

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.