Ada Driver Library for ARM Cortex-M/R - Part 1/2

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: https://github.com/AdaCore/Ada_Drivers_Library.

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 for embedded

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

General-purpose features

Encapsulation

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!

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.

Strong typing

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;
 

Contract-based programming

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.

Parameter modes

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);
 

 

Not null access

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.

Type ranges and subtypes

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;
 

 

Preconditions, and postconditions

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.

Object oriented features

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

Embedded programming features

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

Representation clauses

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.

Representing register fields

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:

  • The array is packed: the compiler should not try to align the values, but instead will make sure that values are consecutive in memory, even though they’re not even byte-aligned.
  • Each element is 4 bits wide, the total size is 32 bits. If the programmer makes a mistake here (for example by trying to fit too many bits in the structure because one of the values is incorrect), the compiler will complain.
  • The bounds keep the natural numbering of the GPIO banks: if we identify the last bank as ‘bank 16’, then GPIO_High_List (16) will return the proper value.

Representing registers

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:

  •     Size => 32: thus a 32-bit register
  •     Volatile_Full_Access: the register is volatile (can change without the software doing anything), and has to be fully accessed when written (so the full 32-bit needs to be written at once). I’ll come back to this aspect a bit later.
  •     Bit_Order => Low_Order_First: it’s a LSB (“little endian”) representation.

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);
 

Mapping peripherals

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:

  • Volatile: values contained in this structure can change asynchronously
  • Import: the actual values are defined outside of the application, so the compiler should not try to perform any initialization when creating instances of the type.
  • Address: base peripheral address is set for the instance.

Registers are then accessed as offsets from the base address, which is exactly what we want.

SVD2Ada

 

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 (github.com/adacore/svd2ada) and you can see an example of the generated code here: stm32_svd-dcmi.ads.

Tasking support and interrupt handling: the Ravenscar profile

 

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:

  • Tasks: as you would expect from the name, those are the actual active objects (control threads, requiring a stack and a processor).
  • Protected types: providing mutually exclusive access to data, possibly state based. Protected types also provide a means to handle interrupts from the hardware.

For example:

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:

  • functions only perform read access of values stored in the object. Hence simultaneous calls to functions are allowed.
  • procedures can change the values. Hence function calls are not allowed when a procedure is called, simultaneous calls to procedures are not allowed either.
  • entries have the same properties as procedures, and also have barriers that make a task wait for the barrier to be lifted before entering the entry.

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

To be continued...

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