Ada Driver Library for ARM Cortex-M/R - Part 1
This is part 2/2 of a series presenting the advantages of Ada for embedded software and introducing the Ada_Drivers_Library project for ARM microcontrollers.
The main objective of the Ada_Drivers_Library is to encourage the use of Ada in embedded systems by providing support for popular ARM MCUs. In this article we are going to talk about the Ada_Drivers_Library itself, its architecture and some of the design choices regarding the Ada features described in part 1.
SVD2Ada is a tool that uses the CMSIS-SVD hardware description files for Cortex-M MCUs and generates Ada bindings from them.
The tool benefits from the advanced representation clauses provided by the language (described in Part1 ) that speed-up the drivers’ development and make them more robust (by statically type-checking the register fields assignments).
Below is an example of generated code (USART registers for the ATMEL SAMG55 MCU).
-- USART Mode of Operation
type USART_MODE_Field is
-- Normal mode
-- RS485 mode
-- Hardware Handshaking
-- IS07816 Protocol: T = 0
-- IS07816 Protocol: T = 1
-- LIN master mode
-- LIN Slave mode
-- SPI master mode
-- SPI Slave mode
with Size => 4;
for USART_MODE_Field use
(Normal => 0,
Rs485 => 1,
Hw_Handshaking => 2,
Is07816_T_0 => 4,
Is07816_T_1 => 6,
Lin_Master => 10,
Lin_Slave => 11,
Spi_Master => 14,
Spi_Slave => 15);
-- USART Mode Register
type USART5_MR_Register is record
-- USART Mode of Operation
USART_MODE : USART_MODE_Field := Is07816_T_0;
-- Clock Selection
USCLKS : USCLKS_Field := Mck;
-- Character Length
CHRL : CHRL_Field := CHRL_Field_7_Bit;
-- Synchronous Mode Select
SYNC : Boolean := True;
with Volatile_Full_Access, Size => 32,
Bit_Order => System.Low_Order_First;
for USART5_MR_Register use record
USART_MODE at 0 range 0 .. 3;
USCLKS at 0 range 4 .. 5;
CHRL at 0 range 6 .. 7;
SYNC at 0 range 8 .. 8;
This example shows the various representation clauses that are used to ensure that every field is properly described and that each field is independent (e.g. writing a value to a field will never overflow to the neighbour fields). The description of the meaning of those representation clauses can be found in Part1 of this article.
When defined in the SVD file, the use of enum types for fields is also very nice, as very often the enumerals do not cover all numerical values (some are reserved), so this ensures that the user never uses a value not allowed by the MCU peripheral.
Finally, we use those SVD-generated specs as a basis for the device drivers in the Ada Drivers Library. An advantage here is that different MCUs have similar devices, but sometimes they contain small changes from one MCU to another. These small changes are easily captured by the automated tool, and the strong typing - the checks on numerical value size - allows us to verify at compile-time that the driver still works on the new MCU.
The SVD2Ada generated code is a low-level representation of a device’s hardware registers. As a result it is not very user friendly. Therefore, the MCU device driver layer provides a high level, functional view of each peripheral that hides the complexity of the underlying register details. For instance, the configuration of an I2C port is quite complex. This layer hides that complexity behind a single function call.
Note that these device drivers are vendor-specific. For example, the I2C port provided by one vendor does not have precisely the same capabilities or interface as that of another vendor. The packages that provide the device drivers are, consequently, located in vendor-specific folders. However, for a given vendor, the drivers’ interfaces are largely independent of the specific MCU. That I2C port, with minor differences, is largely the same across a vendor’s entire MCU family, and so to that extent the software interface is the same.
Each hardware peripheral is presented as an abstract data type (a “class”) with procedures and functions that manipulate objects (via parameters) of the type. As a result, we get good separation between the way the device is represented/implemented and the clients’ usage. That usage includes the number and names of the objects (i.e., variables) of these types. For example, a given MCU may have a certain number of GPIO ports and pins. Another MCU, also by that vendor, likely has a different number of such ports. The kind of variation appears for each peripheral type and MCU family. Some will have more I2C peripherals, timers, UARTs, and so on than another MCU.
To handle this difference we have a separate package representing each MCU, containing declarations for objects of the peripherals’ types. The number of these objects is determined by the MCU and specified in the corresponding datasheet. The STM32F407xx MCUs, for example, have three A/D converters and two D/A converters. Thus a package representing that MCU would contain three declarations for objects of the ADC peripheral type and two objects of the DAC type. Similarly, it would declare objects for two DMA controllers, twelve general-purpose 16-bit timers, two general-purpose 32-bit timers, and so forth, for every peripheral included within the MCU.
These abstract data types are typically concrete implementations of the interfaces defined in the HAL, as we will see in the next chapter.
In the Ada_Drivers_Library, there is (or will be) a fair amount of code that can be used across many different MCUs. This includes drivers for external ICs like a gyroscope, and also software stacks like file systems or in the future network, USB, and Bluetooth.
To make sure that the code is portable and easily reusable, we created a Hardware Abstraction Layer (HAL). The HAL is composed of Ada interfaces types defining features common to many microcontrollers. Every microcontroller supported will then provide an implementation of that interface as part of the MCU layer.
Our HAL is partially inspired by the CMSIS HAL. At some point it would be interesting to work on a more complete and standardized Ada version of the CMSIS HAL.
Component drivers are device drivers for devices that are external to an MCU, and thus are connected to the MCU with a standard bus and/or protocols such as I2C, SPI, UART, CAN, and so on.
Example component devices include IO expanders, LED controllers, thermal printers, GPS transceivers, gyroscopes, sensors, and screens, among others.
Because they are connected through a standard bus, they can be used with any MCU from any vendor. Therefore it’s important to isolate them from the MCU-specific drivers. This separation is achieved via the Hardware Abstraction Layer (HAL).
With the HAL, these component drivers can even be used on a bigger system like a Raspberry Pi running Linux. The “only” thing to do is to implement the required HAL interfaces.
The service layer provides some of the hardware-independent facilities that are usually part of an OS but are close to the drivers and thus still useful on a bare-metal microcontroller application. For example, services could include a file system, network stack, USB stack, or a Bluetooth stack. The service layer is lightly populated at the moment.
Any given board contains an MCU and external peripherals (of the types in the Components layer), and potentially other hardware capabilities (e.g., kinds of memories). Access to the MCU and its peripherals is via the general MCU-oriented packages described above, whereas the Board packages provide interfaces to the external peripherals and other hardware specific to any given board.
The board-specific packages may contain, among other things, the following:
Let’s go into more details with an example: the OpenMV board.
OpenMV is a computer vision platform based around the STM32F42X and an OV2640 camera module. The board is very small and features 2 headers with user accessible pins than can also be used to plug extension boards or shields (screen, Bluetooth, WiFi, etc.).
The root board package “OpenMV” provides renaming of the STM32F4 GPIO to the headers’ pin name:
Shield_PWM1 : GPIO_Point renames PD12;
Shield_PWM2 : GPIO_Point renames PD13;
The child package “OpenMV.Sensor” provides control of one of the feature of this board: the camera sensor. It declares an initialization function that will do the hardware initialization and configuration of all the sensor’s dependencies: an I2C port to talk to the camera module, the camera module itself, and DCMI and DMA to capture images. The package also provides subprograms to use the feature, for instance the “Capture” procedure that will take a snapshot and store it in a frame buffer. Optional features provided by the shields are implemented the same way.
When features or extension boards share hardware resources it is possible that they are not compatible with each other. Board packages will do the best to report such incompatibilities to the user, most of the time with an exception or error code during initialization.
As of today, the Ada_Drivers_Library has pretty good support of the STMicro STM32F4/7 family (Cortex-M4/7) and the Nordic nRF51 (Cortex-M0). We want this project to be driven by the community, so please don’t hesitate to contribute and/or discuss the implementation choices on GitHub: https://github.com/AdaCore/Ada_Drivers_Library.