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 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.