For the last few months we’ve been working (at AdaCore) on a driver library entirely written in Ada for the STM32F4 and STM32F7 with the intent of supporting additional Cortex-M and Cortex-R vendors in the future. With this driver library we want to show that Ada is a good choice for writing hardware drivers, and we also want to lower the entry barrier for Ada on embedded platforms. The project is hosted on GitHub.
We also started an Ada embedded project competition: Make with Ada. With more than 8000 euros in prizes this competition is an opportunity for every programmer to try Ada.
In this article we are going to present the qualities which make Ada a powerful tool for embedded programming. In a second article we will talk about the design and implementation details of the driver library itself.
Ada is a general purpose language that has been originally designed to address the needs of embedded systems. To this end, it has a number of features that increase readability, maintenance and code quality while remaining very efficient in constrained environments. The language itself has evolved considerably from its initial inception (Ada83) to the current version (Ada2012). In the following sections, we will concentrate on the main features that make this language an excellent choice for embedded programmers.
The name of the language is not an acronym, but rather refers to Lady Ada Lovelace, daughter of Lord Byron and considered as the first programmer. So please, no ada, or ADA, but Ada.
Ada has been designed for readability. To this purpose, the language defines the notion of packages. This concept somewhat resembles name spaces, but with some significant differences that make it closer to class definitions in C++ or Java.
Specification and implementation (respectively package spec and package body) are clearly separated in Ada. Only those entities declared in a spec (for example types and subprograms) can be used by other packages. Packages can contain code that is executed when the package is used (elaborated).
Unfortunately, ARM community blogs do not have syntax highlighting for Ada. This will make the code difficult to read...
package Pkg is procedure Hello_World (Name : String); end Pkg;
with Ada.Text_IO; package body Pkg is procedure Hello_World (Name : String) is begin Ada.Text_IO.Put_Line (“Hello, “ & Name & “!”); end Hello_World; begin Hello_World (“Elaboration time”); end Pkg;
with Pkg; procedure Prog is begin Pkg.Hello_World(“Main program”); end Prog;
When Prog is executed, it will output two lines:
Hello, Elaboration time! Hello, Main program!
Hello, Elaboration time!
Hello, Main program!
The first is displayed by the invocation of the Hello_World procedure during the elaboration of package Pkg before Prog starts, and the second is displayed by the call on Hello_World from Prog.
Some general-purpose features of the language work just fine in the embedded context. For example, Ada is a strongly typed language: the type of each variable is specified when the variable is declared and cannot be subsequently changed. Although this may seem constraining compared with loosely typed languages, this allows a greater understandability of the intent of the program, a higher level semantics, and powerful means to control the data flow, and check the usage of values (in terms of allowed bounds for example).
For example:
declare type Mile is new Float; type Kilometer is new Float; Length1 : Mile := 1.0; Length2 : Kilometer; begin Length2 := Length1; -- This produces a compilation error: miles are not kilometers, -- even though they’re both represented as floats. end;
This type system is great for maintenance and refactoring. As most of the checks are performed at compile time - the compiler knows the type of each variable or parameter, and reports errors in case of invalid assignments - it is fairly easy to perform large scale refactoring by introducing or modifying just a type. All you then need to do is to fix the compiler-reported errors as they appear.
Ada’s typing facility is very general. For example in Ada you can define a variety of scalar types: integer types with specific ranges, both signed and unsigned (modular), floating or fixed point types, and enumeration types. Below are some examples:
declare type Meter is new Float; type Height is new Meter range 0.0 .. Meter’Last; type Degree is mod 360; type Manhattan_Avenues is range 1 .. 12; type Midtown_Streets is range 42 .. 59; type Months is (January, February, March, April, May, June, July, August, September, October, November, December); subtype Summer_Months is Months range July .. September; begin Height := -1.0; -- Will raise an exception at run-time and a compilation warning Degree := 359; Degree := Degree + 1; -- Degree is now 0 Midtown_Streets := 59; Midtown_Streets := Midtown_Streets + 1; -- Raises an exception for Month in Months loop -- Enums are iterable Ada.Text_IO.Put_Line (Months’Image (Month)); end loop; for Month in Summer_Months loop -- This will iterate on July, August and September Ada.Text_IO.Put_Line (“Summer :” & Months’Image (Month)); -- As this is a subtype, using Months’Image or Summer_Months’Image is equivalent. -- Also you can assign to a value of type Months any value of type Summer_Months, -- but the reverse will raise an exception if the value is not in the expected range. end loop; end;
Designing an interface is like writing a contract with the user of that interface. For instance with this function:
function Image (A : Integer) return String;
The contract for the caller is that the function can only be called with an Integer as first and only argument. The contract for the callee is that the function will return a String. The compiler will check that both caller and callee respect the contract.
Most programming languages support, to some degree, contract-based programming, but the more you can specify in your contract the more information you give to the user of the interface and the to the compiler. The result is a better interfacing of your software modules and more error checking at compile time.
Every parameter of an Ada subprogram (function or procedure) can be “in”, “out” or “in out”. “In” means that the subprogram can only read the parameter and will not modify it. “Out” means that the subprogram can modify the parameter (and also read it, after it has been assigned). “In out” means that the subprogram can both read and assign to the parameter. This feature is a safe replacement for many of the pointer usages in C.
type My_List is private; function Is_Empty (List : in My_List) return Boolean; procedure Append (List : in out My_List; Element : in Integer); procedure Get_Last (List : in out My_List; Element : out Integer);
Whenever you are dealing with pointers (access) in Ada you can specify that the pointer should not be null.
This works for variable:
X : Integer := 32; Ptr : not null access Integer := X’Access;
Records:
type My_Data is record Ptr : not null access Integer; end record;
Or subprograms:
function Is_Empty (List : not null access My_List) return Boolean;
And it can be used with function pointers as well:
type My_Function_Ptr is access function (X : Integer) return Integer; procedure Map (List : in out My_Integer_List; Func : not null My_Function_Ptr);
This is very powerful because when dealing with pointers we often don’t know what to do when the pointer is null. For instance, if someone calls the Map procedure above with a null function pointer, what should implementation do? Raise an exception or do nothing? The answer is that the implementer of the Map procedure should not have to handle this case, because it doesn’t make sense to call Map with a null function pointer. Therefore the responsibility of not calling Map with a null pointer is transferred to the caller, which will have more context awareness to decide what to do.
In Ada it is possible to define ranges for every scalar type (integer, floating point, fixed point, enumeration).
type Integer_Percentage is new Integer range 0 .. 100; type Float_Percentage is new Float range 0.0 .. 1.0;
For example, when writing a driver for a temperature sensor, you may read in the datasheet that the sensor has a temperature range of -55C to 125C; in Ada you can define this type, with the assurance that the value returned by the driver will always be within this range.
type Sensor is private; -- encapsulated data type type Temperature is range -55 .. 125; function Read_Temperature (This : Sensor) return Temperature;
In the temperature sensor example, it’s likely that the sensor requires an initialization before users can read the temperature. This is how you can specify such a constraint in Ada:
type Sensor is private; procedure Initialize (This : in out Sensor) with Post => Initialized (This); function Initialized (This : Sensor) return Boolean; function Read_Temperature (This : Sensor) return Temperature with Pre => Initialized (This);
It’s is clear here that the sensor has to be initialized before reading the temperature. Not only is this information available to the user of this sensor driver, it is also possible to enable run-time checks of the pre and post conditions. In that case an exception will be raised if Read_Temperature is called on a uninitialized sensor, which means the error will be caught early in the software life cycle instead of lurking during the development and testing phase until someone realizes that the driver returns garbage data because the sensor is not initialized.
OO concepts are now present in most languages used for servers or desktop development, but are much less common in the embedded world. There are several reasons for that. Historically, OO languages have relied on automatic garbage collection for storage management, complicating (or precluding) execution time predictability and adding time and space overhead. OO features have also raised certification concerns (achieving full confidence of complete coverage of the testing is non trivial with inheritance and dynamic dispatching).
Ada and GNAT offer full OO support even on the most stringent target environments, without the intrusion of garbage collection. Predictable execution time is achieved through a combination of language features (for example storage pools for memory management) and implementation techniques (such as static dispatching tables).
It’s also important to notice that in Ada, programmers are not required to use OO to realize some of its benefits; for example encapsulation can be achieved simply through packages and private types..
Where Ada really flies compared to similar programming languages, is when it comes to embedded programming. The main features useful in this context are representation clause (being able to express exactly how a type is organized in memory), interrupt handling, and multitasking (right, on bare metal). There are several other features that are useful as well such as storage pools, and the general control over the memory usage, but in the context of this article I will only concentrate on the ‘block busters’.
Ada offers various means to control exactly how a particular data structure has to be interpreted by the compiler and represented in memory. This is extremely useful in the context of bare metal development as this is a powerful means to describe registers.
Following are example of such representation clauses.
First, let’s describe register fields. For example a 4 bit field that stores powers of 2 ranging from 2^1 to 2^4 can be expressed this way in Ada:
type Field is range 1 .. 16 with Size => 4;
In this example, having 16 as a value is allowed, even though its ‘natural’ representation would exceed the 4 bits range. This is possible because the range starts at 1, so all values can be represented by shifting their representation by 1. So 1 will be represented as ‘0’ by the compiler, 2 as ‘1’, etc. Ada (and GNAT) support these sorts of “biased” representations.
Enumeration types can also have representation clauses. For example a simple bit could be represented as:
type Edge_Detection is (Rising_Edge, Falling_Edge) with Size => 1;
This definition implicitly defines Rising_Edge as 0 and Falling_Edge as 1. Explicit representation values can be also given, in particular if some values are reserved.
type Power_Value is (Min_VCC, Max_VCC) with Size => 2; for Power_Value use (Min_VCC => 0, Max_VCC => 3); -- In this case, values 1 and 2 are reserved by the hardware -- manufacturer, so not represented at the software level.
In case we have a sequence of fields, each with the same structure, Ada arrays can be used, taking advantage of the ability to specify array bounds.
Let’s say we have a GPIO with 16 banks. Each bank is configured via a field that is 4 bits wide. This means we will need two 32-bit registers.
-- Definition of the GPIO config field type GPIO_Config is … with Size => 4; type GPIO_Low_List is array (1 .. 8) of GPIO_Config with Pack, Element_Size => 4, Size => 32; type GPIO_High_List is array (9 .. 16) of GPIO_Config with Pack, Element_Size => 4, Size => 32;
The above definition tells the compiler (and the reader) that:
Since a register is generally a collection of fields (the only exception being a register containing just one value), we will use Ada records to represent them. Such records can be defined with explicit representations this way:
type Field1 is … with Size => 5; type Field2 is .. with Size => 15; type Field3 is … with Size => 12; type The_Register is record F1 : Field1; F2 : Field2; F3 : Field3; end record with Size => 32, Volatile_Full_Access, Bit_Order => System.Low_Order_First; for The_Register use record F1 at 0 range 0 .. 4; F2 at 0 range 5 .. 19; F3 at 0 range 20 .. 31; end record;
Here several aspects are used to precisely represent the register:
This means that F1 is at bits [0-4], F2 at [5-19], etc. The Volatile_Full_Access aspect is currently not part of the language standard, but a GNAT compiler extension. This makes use of read-mask-assign-storage patterns when a field is set.
As an example, using the type above:
declare Register : The_Register with Address => Reg_Addr; begin Register.F2 := 5; end;
This has the same effect as writing in C:
#define F2_MASK 0xfffe0 #define F2_OFFSET 5 uint32_t* reg = F2_ADDR;*reg = (*reg & ~F2_MASK) | (5 << F2_OFFSET);
As briefly seen with the preceding example, one other representation aspect that is useful for embedded programmers is the ‘Address’ aspect, which allows you to place instances at specific addresses.This can be used in particular to represent the complete mapping of a peripheral:
type My_Peripheral is record Reg1 : Reg1_Type; Reg2 : Reg2_Type; end record with Volatile; for My_Peripheral use record Reg1 at 0 range 0 .. 31; Reg2 at 4 range 0 .. 31; end record; Periph1 : My_Peripheral with Import, Address => Base_Peripheral_Address;
Using the above tells the following:
Registers are then accessed as offsets from the base address, which is exactly what we want.
Users of the Cortex-M family of microcontrollers now have the opportunity to benefit from CMSIS/SVD hardware description files. We (AdaCore) have developed SVD2Ada, a tool that reads such descriptions and generates Ada specs with the proper representation clauses as described above. This tool is available on GitHub.
Ada includes tasking features as part of the language standard (task creation, synchronization, message passing, etc.), thus allowing portable multi-tasking applications. However, support for the full Ada tasking model is not practical for limited targets such as microcontrollers. Moreover, use of full Ada makes it difficult to demonstrate real-time properties and lock-free computation. For this reason, a particular subset of the concurrency features has been defined to allow embedded real-time multitasking with Ada. That subset is known as the Ravenscar profile, first defined in the late 1990s. It’s like having a RTOS embedded in the language.
Here is an example of how to create a periodic task in Ada:
task body Periodic_Hello is Print_Time : Time := Clock; Period : constant Time_Span := Milliseconds (500); begin loop Print_Time := Print_Time + Period; delay until Print_Time; Put_Line (“Periodic hello world!”); end loop; end Periodic_Hello;
Tasking management in Ada involves two types of structures:
User_Button_Interrupt : constant Interrupt_ID := EXTI0_Interrupt; protected User_Button is pragma Interrupt_Priority; function Get_State return Boolean; procedure Clear_State; entry Wait_Press; private procedure Interrupt; pragma Attach_Handler (Interrupt, User_Button_Interrupt); Pressed : Boolean := False; end User_Button;
The semantics of such a protected object is the following:
In order to ensure real-time properties, the Ravenscar profile disallows dynamic creation or destruction of task-related objects (e.g. the tasks themselves, and also the protected objects).
Jerome Lambourg is a senior engineer at AdaCore. After graduating from the french High School Telecom ParisTech in 2000, he worked first for Canal+Technologies, and then as a consultant for General Electrics Medical Systems, SAGEM Mobile, and Thales Naval. He then joined AdaCore in 2005. There he worked on various parts of the technology: GPS, GNAT Pro for .NET, AUnit, certification tools (the Qualifying Machine). He is now involved in cross and bare metal platforms, in particular as product manager of GNAT Pro for VxWorks.
Fabien Chouteau joined AdaCore in 2010 after his engineering degree at the EPITA (Paris). He is involved in real-time, embedded, hardware simulation technology and author of the Make with Ada blog post series. Maker/DIYer in his spare time, his projects include electronics, music and woodworking.
Wow, very interesting. Eager to try this once I get my F7 dev board!
Thanks for your feedback! This is not a know problem, please open an "issue" on the project's GitHub page.
Nice article. I tried building "wolf" example for stm32f429 discovery with GPS obtained from gnat-gpl-2016-arm-elf-windows-bin.exe. My build gets stuck while compiling start_rom.S. Any workaround for this problem?
Regards,
Nitin
Thanks Matt! There's more to come stay tuned!
Thanks for this nice intro to Ada for embedded. It's nice to see support for embedded software!
-- Matt
electronvector.com