Zaid Saadallah | Dreamstime.com
Brain Cpu Promo

Interfacing with Devices Using Ada

June 2, 2021
Ada was built to interface with embedded devices. Learn about the Size aspect and attribute, register overlays, and the svd2ada Ada binding generator.

This article is part of the Embedded Software series: Ada for the Embedded C Developer

Previously, we've seen that we can use representation clauses to specify a particular layout for a record type. As mentioned before, this is useful when interfacing with hardware, drivers, or communication protocols. In this article, we'll extend this concept for two specific use-cases: register overlays and data streams. Before we discuss those use cases, though, we'll first explain the Size aspect and the Size attribute.

Size Aspect and Attribute

The Size aspect indicates the minimum number of bits required to represent an object. When applied to a type, the Size aspect is telling the compiler to not make record or array components of a type T any smaller than X bits. Therefore, a common usage for this aspect is to just confirm expectations: Developers specify 'Size to tell the compiler that T should fit X bits, and the compiler will tell them if they are right (or wrong).

When the specified size value is larger than necessary, it can cause objects to be bigger in memory than they would be otherwise. For example, for some enumeration types, we could say for type Enum'Size use 32; when the number of literals would otherwise have required only a byte. That's useful for unchecked conversions because the sizes of the two types need to be the same. Likewise, it's useful for interfacing with C, where enum types are just mapped to the int type, and thus larger than otherwise might bey required by Ada. We'll discuss unchecked conversions later in the series. 

Let's look at an example:

-- my_device_types.ads
package My_Device_Types is
  type UInt10 is mod 2 ** 10
    with Size => 10;
end My_Device_Types;

Here, we're saying that objects of type UInt10 must have at least 10 bits. In this case, if the code compiles, it’s a confirmation that such values can be represented in 10 bits when packed into an enclosing record or array type.

If the size specified was larger than what the compiler would use by default, then it could affect the size of objects. For example, for UInt10, anything up to and including 16 would make no difference on a typical machine. However, anything over 16 would then push the compiler to use a larger object representation. That would be important for unchecked conversions, for example.

The Size attribute indicates the number of bits required to represent a type or an object. We can use the size attribute to retrieve the size of a type or of an object:

-- show_device_types.adb
with Ada.Text_IO;     use Ada.Text_IO;
with My_Device_Types; use My_Device_Types;

procedure Show_Device_Types is
  UInt10_Obj : constant UInt10 := 0;
begin
  Put_Line ("Size of UInt10 type:   " & 
    Positive'Image (UInt10'Size));
  Put_Line ("Size of UInt10 object: " &
    Positive'Image (UInt10_Obj'Size));
end Show_Device_Types;

Here, we're retrieving the actual sizes of the UInt10 type and an object of that type. Note that the sizes don't necessarily need to match. For example, although the size of UInt10 type is expected to be 10 bits, the size of UInt10_Obj may be 16 bits, depending on the platform. Also, components of this type within composite types (arrays, records) will probably be 16 bits as well unless they are packed.

Register Overlays

Register overlays use representation clauses to create a structure that facilitates manipulating bits from registers. Let's look at a simplified example of a power-management controller containing registers such as a system clock enable register. Note that this example is based on an actual architecture:

-- registers.adb
type Bit    is mod 2 ** 1
  with Size => 1;
type UInt5  is mod 2 ** 5
  with Size => 5;
type UInt10 is mod 2 ** 10
  with Size => 10; 

subtype USB_Clock_Enable is Bit;

-- System Clock Enable Register
type PMC_SCER_Register is record
  -- Reserved bits
  Reserved_0_4   : UInt5            := 16#0#;
  -- Write-only. Enable USB F5 Clock
  USBCLK         : USB_CLOCK_ENABLE := 16#0#;
  -- Reserved bits
  Reserved_6_15  : UInt10           := 16#0#;
end record
  with 
    Volatile,
    Size    => 16,
    Bit_Order => System.Low_Order_First;

  for PMC_SCER_Register use record
    Reserved_0_4  at 0 range 0 .. 4;
    USBCLK        at 0 range 5 .. 5;
    Reserved_6_15 at 0 range 6 .. 15;
  end record;

  -- Power Management Controller
  type PMC_Peripheral is record
    -- System Clock Enable Register
    PMC_SCER   : aliased PMC_SCER_Register;
    -- System Clock Disable Register
    PMC_SCDR   : aliased PMC_SCER_Register;
  end record
    with Volatile;

  for PMC_Peripheral use record
    -- 16-bit register at byte 0
    PMC_SCER     at 16#0# range 0 .. 15;
    -- 16-bit register at byte 2
    PMC_SCDR     at 16#2# range 0 .. 15;
  end record

  -- Power Management Controller
  PMC_Periph : aliased PMC_Periphal
    with Import, Address => System'To_Address (16#400E0600#);

First, we declare the system clock enable register — this is the PMC_SCER_Register type in the code example. Most of the bits in that register are reserved. However, we're interested in bit #5, which is used to activate or deactivate the system clock. To achieve a correct representation of this bit, we do the following:

  • We declare the USBCLK component of this record using the USB_Clock_Enable type, which has a size of one bit.
  • We use a representation clause to indicate that the USBCLK component is specifically at bit #5 of byte #0.

After declaring the system clock enable register and specifying its individual bits as components of a record type, we declare the power-management controller type—PMC_Peripheral record type in the code example. Here, we declare two 16-bit registers as record components of PMC_Peripheral. These registers are used to enable or disable the system clock. The strategy we use in the declaration is similar to the one we've just seen above:

  • We declare these registers as components of the PMC_Peripheral record type.
  • We use a representation clause to specify that the PMC_SCER register is at byte #0 and the PMC_SCDR register is at byte #2.

Since these registers have 16 bits, we use a range of bits from 0 to 15.

The actual power-management controller becomes accessible by the declaration of the PMC_Periph object of PMC_Peripheral type. Here, we specify the actual address of the memory-mapped registers (400E0600 in hexadecimal) using the Address aspect in the declaration. When we use the Address aspect in an object declaration, we're indicating the address in the memory of that object.

Because we specify the address of the memory-mapped registers in the declaration of PMC_Periph, this object is now an overlay for those registers. This also means that any operation on this object corresponds to an actual operation on the registers of the power-management controller. We'll discuss more details about overlays in the article about mapping structures to bit-fields (to come).

Finally, in a test application, we can access any bit of any register of the power-management controller with simple record component selection. For example, we can set the USBCLK bit of the PMC_SCER register by using PMC_Periph.PMC_SCER.USBCLK:

-- enable_usb_clock.adb
with Registers; 

procedure Enable_USB_Clock is
begin
  Registers.PMC_Periph.PMC_SCER.USBCLK := 1;
end Enable_USB_Clock;

This code example employs many aspects and keywords of the Ada language. One of them is the Volatile aspect, which we've discussed in the section about volatile and atomic objects. Using the Volatile aspect for the PMC_SCER_Register type ensures that objects of this type won't be stored in a register.

In the declaration of the PMC_SCER_Register record type of the example, we use the Bit_Order aspect to specify the bit ordering of the record type. Here, we can select one of these options:

  • High_Order_First: first bit of the record is the most significant bit.
  • Low_Order_First: first bit of the record is the least significant bit.

The declarations from the Registers package also uses the Import, which is sometimes necessary when creating overlays. When utilized in the context of object declarations, it avoids default initialization (for data types that have it). Aspect Import also are discussed in the coming article that explains how to map structures to bit-fields.

In the declaration of the components of the PMC_Peripheral record type, we use the aliased keyword to specify that those record components are accessible via other paths besides the component name. Therefore, the compiler won't store them in registers. This makes sense because we want to ensure that we're accessing specific memory-mapped registers, and not registers assigned by the compiler. Note that, for the same reason, we also use the aliased keyword in the declaration of the PMC_Periph object.

ARM and svd2ada

As we've seen in the previous section about interfacing with devices, Ada offers powerful features to describe low-level details about the hardware architecture without giving up its strong typing capabilities. However, it can be cumbersome to create a specification for all of those low-level details when you have a complex architecture.

Fortunately, for Arm Cortex-M devices, the GNAT toolchain offers an Ada binding generator called “svd2ada,” which takes CMSIS-SVD descriptions for those devices and creates Ada specifications that match the architecture. CMSIS-SVD description files are based on the Cortex Microcontroller Software Interface Standard (CMSIS), which is a hardware-abstraction layer for Arm Cortex microcontrollers.

Please refer to the svd2ada project page for details about this tool.

Read more from the Embedded Software series: Ada for the Embedded C Developer

Sponsored Recommendations

TTI Transportation Resource Center

April 8, 2024
From sensors to vehicle electrification, from design to production, on-board and off-board a TTI Transportation Specialist will help you keep moving into the future. TTI has been...

Cornell Dubilier: Push EV Charging to Higher Productivity and Lower Recharge Times

April 8, 2024
Optimized for high efficiency power inverter/converter level 3 EV charging systems, CDE capacitors offer high capacitance values, low inductance (< 5 nH), high ripple current ...

TTI Hybrid & Electric Vehicles Line Card

April 8, 2024
Components for Infrastructure, Connectivity and On-board Systems TTI stocks the premier electrical components that hybrid and electric vehicle manufacturers and suppliers need...

Bourns: Automotive-Grade Components for the Rough Road Ahead

April 8, 2024
The electronics needed for transportation today is getting increasingly more demanding and sophisticated, requiring not only high quality components but those that interface well...

Comments

To join the conversation, and become an exclusive member of Electronic Design, create an account today!