Ada is an old programming language that prioritizes correctness and safety through specifications made at compile-time; for example, types can be constrained to a specific range (e.g. 0 .. 10), so if your Delay function takes a Milliseconds argument between zero and 10,000, then the Ada compiler will ensure that Delay's arguments are always of the type and range.

It turns out that Ada is well supported across numerous processor architectures, including ARM. I wanted to give it a try with my trusty NUCLEO-L476RG development board, so I found this AdaCore blog to start off with.

Ada has seemingly evolved since this blog post, evident by the hours I spent to adjust commands and figure out the right combination of packages to install. So, I've compiled here the steps I took to setup my Ada ARM environment for anyone else who is interested.

Alire

Alire is the "Ada LIbrary REpository", inspired by Rust's cargo and OCaml's opam. The download is a single binary which makes for easy installation. Alire is available in Debian's software repository, but using that version got me stuck with odd and possibly unsolved bugs. I recommend downloading the Alire binary directly and placing it somewhere in your PATH like ~/.local/bin. Alire won't ever require superuser permissions which is nice.

Setting up an environment

I initially created a new folder to keep my Ada projects in, which turned out to also be home to a couple of essential utilities:

startup-gen

This tool builds startup assembly and linker files for your given target.

Download and build it by running: alr get --build startup_gen

svd2ada

Most microcontroller manufacturers provide SVD files which specify all of the MCU's peripherals, register addresses, and bit-fields. This tool builds Ada modules out of the given SVD file, cutting out that potential gruntwork.

Download and build it by running: alr get --build svd2ada

Creating a project

Use Alire to create a new binary project: alr init --bin hello (hello is the project name I'll be using)

It will ask for some details in case your project somehow ends up in Alire's repositories. It's okay to leave all of these blank.

In your new project's folder, we will first edit the alire.toml file. All you need to do here is add these couple of lines to the bottom, which tell Alire that this project needs an ARM compiler:

[[depends-on]]
gnat_arm_elf = "*"

Next, we edit the hello.gpr file. There are enough changes going on here that I will just share my file's contents; replace your file's contents with this:

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"; -- The final ELF binary will be placed in here
   for Main use ("hello.adb");

   for Target use "arm-eabi";

   -- Other ARM runtimes can be found here:
   -- https://docs.adacore.com/gnat_ugx-docs/html/gnat_ugx/gnat_ugx/arm-elf_topics_and_tutorial.html
   for Runtime ("Ada") use "light-cortex-m4f";

   -- Like C, unoptimized binaries are large. The Ada compiler supports
   -- optimization flags which can really help with efficiency and size:
   -- (uncomment the below for optimizations)

   --package Compiler is
   --   for Switches ("Ada") use ("-Os");
   --end Compiler;

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

   -- Modify the configuration below according to your microcontroller.
   package Device_Configuration is
      for CPU_Name use "ARM Cortex-M4F";
      for Float_Handling use "hard";
      for Number_Of_Interrupts use "82";

      for Memories use ("SRAM", "FLASH");
      for Boot_Memory use "FLASH";

      for Mem_Kind ("SRAM") use "ram";
      for Address ("SRAM") use "0x20000000";
      for Size ("SRAM") use "96K";

      for Mem_Kind ("FLASH") use "rom";
      for Address ("FLASH") use "0x08000000";
      for Size ("FLASH") use "1024K";
   end Device_Configuration;
end Hello;

Running startup-gen

With the project configuration ready, simply run the following command to create your startup files:

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

Running svd2ada

Go find your microcontroller's SVD file from your MCU's manufacturer. You may also find them on GitHub from users who have compiled them together for easier access, e.g. https://github.com/modm-io/cmsis-svd-stm32.

Then, run svd2ada:

../svd2ada_*/bin/svd2ada --no-defaults --boolean -o src/ STM32L4x6.svd

I have enabled two options which I found useful:

  • --no-defaults: Do not generate default values for registers. I don't expect to need this feature, and setting this flag saved a few kilobytes of flash.
  • --boolean: Make 1-bit register fields booleans instead of integers. I like seeing CLOCK_ENABLE := True rather than CLOCK_ENABLE := 1.

Writing code

Now it's time to edit the main code file, in my case src/hello.adb.

To truly learn Ada, visit learn.adacore.com. There are great tutorials there and multiple sections or "courses" on embedded programming. Searching for help online might lead your to Ada's official standards instead, which are far more difficult to parse.

For STM32, here is some "blinky" code below. It goes a couple of steps further to show some language features: I wrote a rough Sleep procedure which uses the SysTick peripheral common to all ARM Cortex-M microcontrollers. It takes a Ticks type which is constrained to a slightly arbitrary range. Variables for the SysTick registers are defined locally and mapped to their respective memory addresses.

(by the way: procedures define sequences of operations. If you want a piece of code to return a value, you need to write a function instead).

with System,
STM32L4x6.GPIO,
STM32L4x6.RCC;

-- hello.adb's main procedure. Ada files cannot have multiple top-level
-- procedures; either nest your local procedures as I did with 'Sleep' or move
-- them out to separate files.
procedure Hello is
    -- 'Ticks' only needs to represent positive durations of up to one million
    -- ticks. Its size is 32 bits just to match the SysTick reload register.
    type Ticks is range 0 .. 1000000 with Size => 32;

    -- Spins in a busy loop for the given number of Ticks. Could be implemented
    -- better.
    procedure Sleep (C : Ticks) is
        -- Initialize Csr to 5 here to enable the clock.
        -- TODO: Make a SysTick interface that matches svd2ada's output.
        SysTickCsr : Integer := 5 with Address => System'To_Address (16#E000E010#);
        SysTickRvr : Ticks := C with Address => System'To_Address (16#E000E014#);
        -- Without this, the compiler thinks we wait in an infinite loop since
        -- SysTickCsr is never modified.
        pragma Volatile (SysTickCsr);
    begin
        loop
            -- Wait for COUNTFLAG to set.
            exit when SysTickCsr > 16#10000#;
        end loop;

        -- Disable SysTick.
        SysTickCsr := 4;
    end Sleep;

    use STM32L4x6.GPIO;
    use STM32L4x6.RCC;
begin
    RCC_Periph.AHB2ENR.GPIOAEN := True;

    -- GPIO pin PA5 controls an LED on my hardware.
    GPIOA_Periph.MODER.Arr (5) := 1;

    loop
        GPIOA_Periph.ODR.ODR.Arr (5) := True;
        Sleep (1000000);

        GPIOA_Periph.ODR.ODR.Arr (5) := False;
        Sleep (1000000);
    end loop;
end Hello;

AdaCore also has a repository of sample code and drivers on GitHub which may be helpful to browse.

Compile and upload

To build your project, run alr build. Alire will fetch the ARM toolchain on the first go, but otherwise compilation should be fast.

Upload

I generally use OpenOCD to program microcontrollers. First, I convert our ELF binary to an Intel HEX file (using ARM binutils):

(I have not searched for an Ada/Alire equivalent to this yet)

arm-none-eabi-objcopy -Oihex obj/hello obj/hello.hex

Then, upload with OpenOCD. This is a one-liner, though it would be better to put it in a shell or OpenOCD script file:

openocd -f interface/stlink.cfg -f target/stm32l4x.cfg -c "program obj/hello.hex verify reset exit"

Conclusion

Now you have what you need to get going with Ada. I previously stayed away from Ada due to its verbosity and age, but now I see its benefits of guiding you towards writing provably correct code and making you think more carefully of how to architect your firmware. Its age and continuous prescence shows that it is a language worth knowing and using, and perhaps it can still stand up to new languages like Zig and Rust.

Ada can interop with C too, so I'm hoping to integrate it into some existing projects that could benefit from it. If any of that is successful, I'll be sure to share it here.

Questions? Comments? Mail me anytime at clyne@bitgloo.com.