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.
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"; -- Allocate the main stack in CCM with 8K size for Main_Stack_Memory use "CCM"; for Main_Stack_Size use "8K"; -- 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
Create the Ada application code #
We need some Ada code to run on the board, so let’s write a simple hello world in
with Ada.Text_IO; procedure Hello is begin Ada.Text_IO.Put_Line ("Hello world!"); end Hello;
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.
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
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.