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 seeingCLOCK_ENABLE := True
rather thanCLOCK_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.