Making robots with Ada, Part 3 – Working with analog sensors

This is the third in a series of articles about using a much more powerful hardware and software platform to make robots using NXT® Lego sensors and motors. We’ve replaced the Brick with a modern ARM processor and used an industrial-strength programming language. As a result of replacing the NXT Brick, however, we must write the device drivers and provide some of the electronic circuitry the Brick provided. This series explores doing precisely that, with complete, executable demonstrations included.

In part one of this series, we introduced the general Brick replacement idea and then demonstrated how to use a Discovery Kit and Ada to develop an object-oriented interface to the most basic NXT sensor: the touch sensor. Part two showed how to control the NXT Lego motors with drivers developed using the ADL and a third-party hardware interface board. In this third part, we turn again to the NXT sensors, this time the analog light and sound sensors. We will use primarily the ADC and DMA device drivers provided by the ADL to implement these higher-level sensor drivers, and then demonstrate them in two distinct programs.

Our replacement hardware is the very inexpensive “Discovery Kit” products from STMicroelectronics. The Discovery Kits have ARM Cortex processors and include many on-package devices for interfacing to the external world, including A/D and D/A converters, timers, UARTs, DMA controllers, I2C and SPI communication, and others. Sophisticated external components are also included, depending upon the specific kit. For example, the STM32F429I Discovery Kit has a Cortex M4 MCU, a gyroscope, and an LCD screen (among other components). The STM32F4 Discovery Kit also has a Cortex M4 MCU, with an accelerometer instead of a gyroscope and no LCD. It is even more inexpensive than the F429I kit – approximately $15. We use both kits in this series.

Our industrial-strength programming language is Ada 2012, an object-oriented real-time / embedded-systems language widely used in modern high-integrity applications, such as commercial aircraft – for example, the Boeing Dreamliner – and high-speed trains, among many others. AdaCore’s GNAT compiler implements Ada 2012 for newer ARM targets, including a freely available Community edition for hobbyists. This complete toolchain, including a full-strength IDE, is available for download at https://www.adacore.com/community.

The previous articles introduced many of the important fundamentals of Ada 2012, the most recent version of the language standard, and some advanced concepts as well. Please refer to the other articles if you have any questions. Note too that free learning material is available at http://university.adacore.com. Any new language capabilities used in this article will be explained here.

The NXT Brick firmware included drivers for the timers, I2C, A/D and D/A converters, PWM, and other low-level devices required to interact with the sensors and motors. The Ada Drivers Library (ADL) provided by AdaCore and the Ada community supplies many of these device drivers for a variety of development platforms, including the STM32 series boards. The ADL is available on GitHub for both non-proprietary and commercial use here: https://github.com/AdaCore/Ada_Drivers_Library.

Of course, we also require higher-level drivers for the NXT sensors and motors themselves. Those drivers are the primary artifacts developed in this article series. Although the NXT sensor and motor drivers are not part of the ADL (yet), the library does include numerous complete demonstrations for the low-level device drivers. Often a new higher-level client driver can be based directly on one or more of the lower-level driver demonstrations. That was certainly the case in this article series. These demonstrations are easily overlooked so here is the URL for them within the library on GitHub:

https://github.com/AdaCore/Ada_Drivers_Library/tree/master/arch/ARM/STM32/driver_demos

NXT Analog Sensors

A number of the NXT sensors are based on analog voltage values, including the sound and light sensors. Some third-party sensors are also analog devices, such as the HiTechnic gyroscope. Strictly speaking, we could have implemented the touch sensor that way, too, designating “pressed” as any value above a certain sensed voltage level, and “not pressed” as any value below that level. Using discrete inputs, though, allowed us to introduce a number of fundamental ARM interfacing topics, and their support in the ADL, in a simple manner.

Because the voltage levels are continuous, non-discrete values, we must have a way to convert them to digital values so that they can be manipulated by the software. Digitizing analog values is a widely-covered topic that has been around for many years. If you are not familiar with it, two good websites are:

https://en.wikipedia.org/wiki/Analog-to-digital_converter

https://learn.sparkfun.com/tutorials/analog-to-digital-conversion

The SparkFun page is good for those completely new to the topic because it has links to a number of prerequisite concepts too, but note that it does get specific to the Arduino boards.

Analog-to-Digital Conversion (ADC) Driver

Our Ada driver for converting analog signals to digital quantities in the ADL, for these targets, is in the STM32.ADC package. This package provides the abstract data type Analog_To_Digital_Converter as the primary artifact, along with many other ancillary types representing things such as the desired conversion resolution, input channels, outputs, and so forth. There are many routines provided as well. If that type name seems a bit long, don’t worry, you rarely have to write it and it is highly readable, which is where the bulk of developer time is spent. The package declaration in the ADL is here:

https://github.com/AdaCore/Ada_Drivers_Library/blob/master/arch/ARM/STM32/drivers/stm32-adc.ads

The device is quite sophisticated. For example, there are multiple ways to do the conversions, multiple ways to trigger them, and the device also allows DMA access, interrupt generation on various states, and many other capabilities. The STMF4xxx Reference Manual (RM0090) is the detailed reference for the ADC device but does not go into details of usage. For that information, the Application Note AN3116 “STM32’s ADC modes and their applications” (the document file name is cd00258017.pdf) is available online from STMicro and a number of other sites too. It describes several configurations for individual ADC use, as well as much more advanced configurations in which multiple ADC units are combined to work together. We are going to use one of the basic configurations for a single ADC.

One of the more important types and routines allows clients to configure their required conversions:

1.    type Regular_Channel_Conversions is2.      array (Regular_Channel_Rank range <>) of Regular_Channel_Conversion;3. 4.    procedure Configure_Regular_Conversions5.      (This        : in out Analog_To_Digital_Converter;6.       Continuous  : Boolean;7.       Trigger     : Regular_Channel_Conversion_Trigger;8.       Enable_EOC  : Boolean;9.       Conversions : Regular_Channel_Conversions)10.     with11.       Pre => Conversions'Length > 0,12.       Post =>13.         (if Conversions'Length > 1 then Scan_Mode_Enabled (This)) and14.         (if Enable_EOC then EOC_Selection_Enabled (This)) and15.          ...

For the sake of brevity we won’t discuss (much less show) all the types and routines, even for the above. Suffice it to say that the procedure configures so-called “regular” conversions, expressed as an array of individual conversion descriptions (lines 1 and 2). The array type is “unconstrained” so the client can have many, a few, or only one conversion description in the array value passed to the procedure (line 9).

Note the parameters for specifying whether the conversions are to be performed continuously or one-time only (line 6), and how the conversions are triggered (line 7). Triggering can be by software or by hardware, for example by a hardware timer for periodic conversions, or other ADC units in a combined mode.

Having configured the conversions to be performed, the client then starts the conversions on that ADC:

1.   procedure Start_Conversion (This : in out Analog_To_Digital_Converter) with2.     Pre => Enabled (This) and 3.             Regular_Conversions_Expected (This) > 0;4.   --  Starts the conversion(s) for the regular channels

The precondition expression on line 2 indicates that, before calling this procedure, we must enable the given ADC object and, on line 3, must have configured at least one “regular” conversion.

After starting the conversion(s) on an ADC we can poll a status flag for their completion, or wait for a completion interrupt. Even better, we can have the ADC device trigger a direct memory access (DMA) transfer from the ADC to one or more memory locations specified by the user. The DMA approach is highly advantageous, since as a separate hardware unit it requires no CPU cycles to transfer the converted values.

There is much more that Analog_To_Digital_Converter objects can do but the above should suffice for now. We will explain additional routines and types as they are encountered in the drivers.

NXT Input Port Circuit

The Ada Drivers Library provides the required ADC conversion capability similar to that provided by the NXT Brick’s firmware, but the Brick also provided necessary interfacing circuitry on the Brick input ports. We must replicate that circuitry since the Brick is no longer present. Fortunately, this “circuit” is as simple as it can be: a single pull-up resistor on one of the input lines, as shown in Figure 1:


Figure 1: NXT Input Port (Source: Lego)

Figure 1 is based on the schematics provided by the Hardware Developer Kit, freely available from Lego for download, here: https://www.lego.com/en-us/mindstorms/downloads

As Figure 1 shows, each Brick’s input port had a 10K pull-up resistor attached to line 1 so we must provide the same resistor on whatever we use for that line. This pull-up resistor is essential. Incoming voltage levels are incorrect without it. For our purposes we just put the resistor on a breadboard and ran jumper wires to it from the power pin header and the GPIO pin header attached to line 1.

Figure 1 also indicates that lines 2 and 3 are ground lines, line 4 provides power (+4.5V) to the sensor, and that lines 5 and 6 are digital I/O lines whose use varies with the kind of sensor attached. The NXT sound sensor uses both digital lines, for example, whereas the light sensor only uses one of them.

To connect the sensor to our Discovery Kit header pins we cut off the connector from one end of a standard NXT cable to expose the internal wires, stripped them, and then crimped female connectors to them. These can then be plugged directly into the header pins on the target boards. The other end retains the standard NXT connector so we can plug it into any standard sensor or motor.

We must connect at least one of the red or black ground lines to a ground pin on our target board headers, but either line is sufficient. (Connecting both is easy, so we did.) The green power line must be connected to a +5V header pin, otherwise there will be no voltage to sense on input line one. Line 1 is our analog input so it will be connected to a GPIO pin, as will the two discrete input lines. As mentioned earlier, the analog input line must also have the 10K pull-up resistor attached. The discrete input lines need no external pull-up resistors.

We will show the specific GPIO pin selections for the analog and discrete input lines when we discuss the demonstration programs. No other external electronics are required.

Abstract Base Class for NXT Analog Sensors

With the external electronics covered, we can turn to the driver software. The basic NXT Mindstorms kit included two analog sensors: one for sound and one for light. The operational similarities are considerable, and as mentioned, there are potentially many such sensors, especially third-party sensors. Therefore, we first define an abstract base type representing any NXT analog sensor, providing abstract operations as well as some concrete components and operations that will apply universally. We will then use that base type as the basis for further specialization, implementing the aspects that differ with regard to how the converted values are acquired. Ultimately we will derive concrete types for the sound and light sensors from those specialized, but nonetheless abstract, subclasses. Figure 2 shows these relationships.


Figure 2: Class Diagram for Analog Sensor Base Type and Subclasses (Source: Author)

The root abstract base-class type is declared in the package NXT.Analog, like so: 

1. package NXT.Analog is2.3.   type NXT_Analog_Sensor is abstract tagged limited private;4.   --  The root abstract baseclass type for NXT analog sensors.

Line 3 introduces the type name itself and specifies a number of properties for the type. As we discussed in the previous articles, the word “private” means that clients do not have compile-time visibility to the implementation of the type. It could be an array type, a record type, and so forth, but clients cannot access it in those terms. The word “limited” means that clients cannot use predefined assignment on objects of the type. These types represent actual hardware devices so copying doesn’t make sense. The word “tagged” indicates full OOP support for the type. Finally, the word “abstract” indicates that this type is indeed abstract in the OOP sense, meaning that it cannot be used to declare objects but must instead be specialized into concrete types before direct use by application code. As you will see, “abstract” can be applied to operations too, meaning that the operation’s declaration only defines the signature but not a concrete implementation. 

As Figure 2 illustrates, this abstract base type contains an ADC, specifically an access value (pointer) designating an object of type Analog_To_Digital_Converter provided by the Ada Drivers Library. All these ADC units, DMA controllers, and other on-package devices are already declared in the STM32.Device package so we just need a reference to the one chosen. It will also have an analog input channel and a GPIO input pin for that channel but the figure does not show those components, although they are certainly essential. If we examine the full type definition we see all these ADC-related components:

1.   ...2.3. private4.5.   NXT_Brick_ADC_Resolution : constant ADC_Resolution := ADC_Resolution_10_Bits;6.7.   Max_For_Resolution : constant := 1023;  -- for 10-bit resolution8.9.   type NXT_Analog_Sensor is abstract tagged limited record10.       Converter     : access Analog_To_Digital_Converter;11.       Input_Channel : Analog_Input_Channel;12.       Input_Pin     : GPIO_Point;13.       High          : Varying_Directly := Max_For_Resolution;14.       Low           : Varying_Directly := 0;15.   end record;16.17.   ...18.19. end NXT.Analog;

We will explain the High and Low components momentarily. As a result of this type declaration we have an abstract base-class type that defines the components required for performing analog input conversions. All derived subclasses, concrete or abstract, will inherit these components. The operations defined for this type will manipulate these components and will be inherited as well. If we know the implementations we can make the operations concrete, otherwise we mark them as abstract. For example, Figure 2 shows that each type has a concrete Initialize procedure. This initialization procedure can do some of the required calls for the designated ADC unit, so the routine is not marked abstract:

1.   procedure Initialize (This : in out NXT_Analog_Sensor); 

However, the initialization it provides may be incomplete because subclasses may require further, specialized steps. Subclasses will inherit and override the parent versions of such operations as needed. This root version’s implementation does only the common device setup and configuration that will be required for any such sensor:

1.   procedure Initialize2.     (This : in out NXT_Analog_Sensor)3.   is4.   begin5.       Enable_Clock (This.Input_Pin.all);6.       This.Input_Pin.Configure_IO ((Mode_Analog, Resistors => Floating));7.8.       Enable_Clock (This.Converter.all);9.       Configure_Unit10.         (This.Converter.all,11.         Resolution => NXT_Brick_ADC_Resolution,12.         Alignment  => Right_Aligned);13.   end Initialize;

In particular, the body enables the clock for the input pin (line 5) and then configures that pin for analog input (line 6) using procedure Configure_IO.  The procedure then does common configuration for the individual ADC unit (lines 9 through 12) after enabling the clock for the ADC device (line 8). The ADC unit to configure is specified on line 10, with “.all” to indicate dereferencing the access value (pointer) This.Converter. The NXT Brick used 10-bit ADC conversion resolution so we do as well (line 11). The Alignment parameter on line 12 specifies the data alignment for storing the converted values.

That call to Configure_Unit is necessary but may not be sufficient for using an ADC. However, further configuration will specify ADC properties and behavior that are dependent on the subclasses. We will show those calls in the discussions of those subclasses.

Going back to the API provided by the package, there is only one abstract operation, thus one for which no implementation is given at this point:

1.   procedure Get_Raw_Reading2.     (This       : in out NXT_Analog_Sensor;3.       Reading    : out Natural;4.       Successful : out Boolean)5.   is abstract6.   with Pre'Class => Enabled (This);

Procedure Get_Raw_Reading returns the digitized value directly acquired from the ADC sensor. Different subclass types will have different means of acquiring raw readings from the ADC so we only define the operation signature and mark it abstract (line 5). A subclass using polling to get a reading will implement the routine much differently from those that use interrupts or DMA, for example. The precondition on line 6 tells us that, regardless of the implementation, clients must enable the specified sensor some time prior to calling Get_Raw_Reading.

Perhaps surprisingly, the NXT analog sensors return digitized values that vary inversely with the intensity of the sensed inputs. For example, with the light sensor, the brighter the sensed light, the lower the digitized output value. Likewise, the darker the sensed light, the larger the returned value. The NXT Brick uses 10-bit ADC conversion so the values range from zero to 1023. Therefore, the most intense inputs correspond to a raw ADC output of zero, whereas the least intense inputs correspond to a raw output of 1023. This behavior can be confusing to users. As a result, essentially all third-party implementations like ours choose to reverse the results so that the outputs vary directly with the inputs: continuing the light sensor example, brighter input lights result in numerically larger converted values, darker inputs result in numerically smaller values. We define a “subtype” to give a name for these outputs:

1.   subtype Varying_Directly is Natural;

Subtypes in Ada are not “subclasses” but, rather, names for constraints on existing types. One might declare a subtype to say that some integer value can only have values ranging from -1 to +1, for example, or from zero to ten. The Varying_Directly declaration simply means that the associated values are integers that cannot be negative, with the largest most positive value being the largest integer value. This subtype is only defined for readability. There is nothing to say that the values somehow truly vary directly with some input. Other than the fact that values cannot be negative, it is a documentation and readability aid. Don’t underestimate the importance of that, though, because knowing the expected values is critical to using the code correctly.  

An important operation defined for the base-class type provides the converted value as a measure of intensity, specifically as a percentage of the sensed input’s range. We define another subtype, expressing both the fact that intensity values vary directly with the sensed inputs, and that they represent a percentage:

1.   subtype Intensity is Varying_Directly range 0 .. 100;

We then use this subtype in the declaration of the primary operation:

1.   procedure Get_Intensity2.     (This       : in out NXT_Analog_Sensor;3.       Reading    : out Intensity;4.       Successful : out Boolean)5.   with Pre'Class => Enabled (This);

The procedure is not abstract because a complete, concrete implementation can be known here at the root of the type hierarchy. All subclasses will inherit some version of Get_Intensity. (It can be overridden by subclasses, but that is not likely.)  The base implementation in the root package NXT.Analog is as follows:

1.   procedure Get_Intensity2.     (This       : in out NXT_Analog_Sensor;3.       Reading    : out Intensity;4.       Successful : out Boolean)5.   is6.       Raw    : Integer;7.       Scaled : Integer;8.   begin9.       Get_Raw_Reading (NXT_Analog_Sensor'Class (This), Raw, Successful);10.       if not Successful then11.         Reading := 0;12.         return;13.       end if;14.       Raw := As_Varying_Directly (Raw);15.       Scaled := Mapped (Raw, This.Low, This.High, Intensity'First, Intensity'Last);16.       Reading := Constrained (Scaled, Intensity'First, Intensity'Last);17.   end Get_Intensity;

The routine calls Get_Raw_Reading on line 9 to get the raw value from the sensor, using whatever implementation the subclass provided. (We will discuss how that subclass is applied momentarily.)  Assuming that the call is successful, the raw value is converted to one that varies directly, on line 14. The value is then mapped from the raw values’ range, as given by This.Low and This.High, to the range of Intensity values, i.e., zero and 100. The result is one that can exceed the range of Intensity, however, because calibrated Low and High values may be off somewhat, such that the raw reading is outside those bounds. Therefore, on line 16 the scaled value is constrained to be exactly within the range of Intensity and is then assigned to the output parameter.

So how is the concrete subclass version of Get_Raw_Reading called on line 9? The answer is standard OOP: dynamic dispatching. That is, the called routine is not identified until run-time during execution, rather than statically at compile-time. In Ada, dynamic dispatching only occurs when forced to occur and is a property of the call rather than the operation. One forces dynamic dispatching by passing a value that could be any member of the set of subclasses related by inheritance. In Ada, a set of types related by inheritance is represented by a “class-wide” type. Such types are defined automatically by the compiler for all tagged types. The names of class-wide types are formed by the tagged type name and the suffix “’Class”. For example, we have a tagged type named NXT_Analog_Sensor. Therefore, there is automatically another type named NXT_Analog_Sensor’Class, representing the type NXT_Analog_Sensor and any subclass type derived, directly or indirectly, from NXT_Analog_Sensor. (This is essentially part of the functionality provided by a pointer-to-base-class in C++.) Therefore, on line 9 the formal parameter “This” is converted to the class-wide type NXT_Analog_Sensor'Class in the call to Get_Raw_Version, forcing the binding identification of the routine to happen at run-time.

But which subtype? When clients actually call Get_Intensity they will pass an object of the subclass type to the formal parameter “This” on line 2.  That is so because whenever a subclass inherits an operation, the parent type name is effectively replaced by the subclass type name in the operation’s formal parameters. Hence, when calling Get_Intensity the value passed to the formal parameter “This” on line 2 will specify the subclass to use as the dynamic dispatching target.

Now that we know the base type API and how it works we can explore the subclasses.

Abstract Subclass Using Polling

Polling is the easiest to implement but the least likely used, because it consumes CPU cycles while waiting conversion completion. The CPU could profitably be doing something else, including perhaps the work being awaited! In this case the work is not done by the CPU – it is done by the ADC – so the awaited event will eventually happen. However, when the awaited state or event is expected to occur very quickly, relative to the time required to dispatch the processor to another thread, polling can make sense.

We have implemented a polling-based abstract analog sensor type but, to avoid lengthening an already long article, we will not examine the implementation here. We did use it to implement a driver for the HiTechnic gyro sensor, however, as an illustration.

Abstract Subclass Using DMA

Polling is simple but, as we said, not usually desirable. There are a couple of alternatives, though, a very attractive one being direct-memory access, DMA. In this design the hardware rather than the software transfers the ADC’s converted value to the sensor driver’s memory location. No CPU cycles are required because the DMA unit does the transfer. Using interrupts to wait for the conversion completion is the other alternative, which we will examine after the DMA approach.

To use DMA requires a DMA controller and a DMA stream, both provided by the ADL in package STM32.DMA. This requirement is reflected in the type’s declaration:

1. with STM32.DMA; use STM32.DMA;2.3. package NXT.Analog.DMA is4.5.   type NXT_Analog_Sensor_DMA is abstract new NXT_Analog_Sensor with private;6.7.   overriding8.   procedure Initialize (This : in out NXT_Analog_Sensor_DMA) with9.     Post => Enabled (This);10.11.   overriding12.   procedure Get_Raw_Reading13.     (This       : in out NXT_Analog_Sensor_DMA;14.       Reading    : out Natural;15.       Successful : out Boolean);16.17. private18.19.   type NXT_Analog_Sensor_DMA is new NXT_Analog_Sensor with record20.       Controller : access DMA_Controller;21.       Stream     : DMA_Stream_Selector;22.       Raw_Value  : UInt16 := 0 with Atomic;23.   end record;24.25. end NXT.Analog.DMA;

In this new package, the type NXT_Analog_Sensor_DMA is derived from the parent type NXT_Analog_Sensor (line 5). The same approach to hiding the components from clients is taken and the type is again abstract. We want to augment the Initialization steps defined for the parent type and we want to define a concrete version of Get_Raw_Reading for use by sensor-specific subclasses. Hence the overriding declarations on lines 7-9 and 11-15.

There are additional components, seen in the full declaration on lines 19-23. We see the DMA controller and stream components on lines 20 and 21 and an integer component on line 22. That integer component will be the target of the DMA transfers from the ADC to the sensor objects. We mark it as Atomic to be sure that it can be read and written indivisibly. That is pretty much guaranteed because Integer is the native type for this processor and is almost certainly read/written without multiple memory accesses. Marking it Atomic makes sure because the compiler will then tell us if that expectation is unfounded.

The DMA processing requires no CPU cycles for the transfers but does require more configuration. That configuration is done in the overridden version of procedure Initialize:

1.   overriding2.   procedure Initialize (This : in out NXT_Analog_Sensor_DMA) is3.       use STM32.Device.Mapping_Requests;4.5.       --  Only certain DMA channels can be mapped to ADC units, so we use this6.       --  function to find the channel for This.Converter7.       DMA_Channel : constant DMA_Channel_Selector := DMA2_ADC_Request_Mapping8.         (This.Converter, This.Controller, This.Stream);9.   begin10.       Initialize (NXT_Analog_Sensor (This));11.12.       Initialize_DMA (This.Controller, This.Stream, DMA_Channel);13.       Initialize_ADC (This.Converter, This.Input_Channel);14.15.       Enable (This.Converter.all);16.       Start_Transfer17.         (This.Controller.all,18.         This.Stream,19.         Source      => Data_Register_Address (This.Converter.all),20.         Destination => This.Raw_Value'Address,21.         Data_Count  => 1);  -- ie, 1 halfword22.       Start_Conversion (This.Converter.all);23.   end Initialize;

As for the polled subclass, we want the parent’s version of Initialize to be performed, so on line 10 we convert to the parent type and thereby invoke the parent’s version. That will enable the ADC and input pin, and do the common ADC configuration. We then do the DMA-specific setup steps. We initialize the ADC and DMA units via procedures declared within this package body, called on lines 12 and 13. We’ll see the details in a moment. Having done those device initializations, we then enable the ADC unit (line 15) and start the conversions (lines 16-21). The source of the DMA transfer is the address of the ADC data register for the converter we are using (line 19). The destination is the address of the Raw_Value record component defined as part of the new sensor type (line 20). On line 21 we tell the DMA controller to transmit one half-word (a 16-bit quantity) per transfer, because that is the size of ADC conversion values.  As a result of this call, the DMA unit is continuously transferring 16-bit values from the ADC to the lower half of the 32-bit record component within the sensor driver. Finally, on line 22, we start the ADC converter so that the values being transferred are actual conversions. Now, whenever we want the current conversion result, we simply read This.Raw_Value. That is what Get_Raw_Value does:

1.   overriding2.   procedure Get_Raw_Reading3.     (This       : in out NXT_Analog_Sensor_DMA;4.       Reading    : out Natural;5.       Successful : out Boolean)6.   is7.   begin8.       if Status (This.Converter.all, Overrun) or9.         Status (This.Controller.all, This.Stream, FIFO_Error_Indicated) or10.         Status (This.Controller.all, This.Stream, Direct_Mode_Error_Indicated) or11.         Status (This.Controller.all, This.Stream, Transfer_Error_Indicated) or12.         not Status (This.Controller.all, This.Stream, Transfer_Complete_Indicated)13.       then14.         Successful := False;15.       else16.         Reading := Integer (This.Raw_Value);17.         Successful := True;18.       end if;19.   end Get_Raw_Reading;

Note that the type declaration on line 3 does not include the word “abstract” so this is a concrete type, usable by applications. All the operations from the parent type are inherited and available to clients. We declare the overriding for procedure Initialize and add the two new operations for the mode. Finally, the record components in the private part include the two discrete output lines’ GPIO points for controlling the sound mode (lines 19 and 20), and a component for holding the current mode (line 21). The components from the parent type are inherited as usual.

The body of Initialize is much like the others: we first call the parent version and then do subclass-specific processing:

1.   overriding2.   procedure Initialize3.     (This : in out NXT_Sound_Sensor)4.   is5.   begin6.       Initialize (NXT_Analog_Sensor_DMA (This));7.8.       Enable_Clock (This.Digital_0);9.       Enable_Clock (This.Digital_1);10.11.       Configure_IO12.         (This.Digital_0 & This.Digital_1,13.         (Mode        => Mode_Out,14.           Resistors   => Pull_Down,15.           Speed       => Speed_Medium,16.           Output_Type => Push_Pull));17.18.       Set_Mode (This, dB);19.   end Initialize;

The code is self-explanatory by now, given your familiarity with the Ada Drivers Library. The implementations for Set_Mode and Current_Mode do not show anything new so we needn’t describe them.

Concrete Light Sensor Type

Like the sound sensor, the concrete light sensor uses the abstract DMA-based subclass as the parent type. The light sensor has a red “floodlight” LED just above the input. This floodlight can act as the light source when the ambient environment light is insufficient. The floodlight is controlled by one of the discrete input lines. Therefore, we need to augment Initialize and add operations to control this floodlight. Here is the resulting package declaration:

1. package NXT.Light_Sensors is2.3.   type NXT_Light_Sensor is new NXT_Analog_Sensor_DMA with private;4.5.   overriding6.   procedure Initialize (This : in out NXT_Light_Sensor) with7.     Post => not Floodlight_Enabled (This);8.9.   procedure Enable_Floodlight (This : in out NXT_Light_Sensor) with10.     Post => Floodlight_Enabled (This);11.12.   procedure Disable_Floodlight (This : in out NXT_Light_Sensor) with13.     Post => not Floodlight_Enabled (This);14.15.   function Floodlight_Enabled (This : NXT_Light_Sensor) return Boolean;16.17. private18.19.   type NXT_Light_Sensor is new NXT_Analog_Sensor_DMA with record20.       Floodlight_Pin     : GPIO_Point;21.       Floodlight_Enabled : Boolean := False;22.   end record;23.24. end NXT.Light_Sensors;

Note the postcondition added to the overriding of Initialize, on line 7, indicating that Initialize leaves the floodlight disabled. Much like the sound sensor, we add a GPIO point to control the floodlight line (line 20) and a component to hold the current floodlight setting (line 21).

The body of Initialize is similar to the one for the sound sensor, in that it calls the parent version and then does subclass-specific component initialization:

1.   overriding2.   procedure Initialize3.     (This : in out NXT_Light_Sensor)4.   is5.   begin6.       Initialize (NXT_Analog_Sensor_DMA (This));7.8.       Enable_Clock (This.Floodlight_Pin);9.       This.Floodlight_Pin.Configure_IO10.         ((Mode        => Mode_Out,11.           Resistors   => Pull_Down,12.           Speed       => Speed_Medium,13.           Output_Type => Push_Pull));14.            15.       Disable_Floodlight (This);16.   end Initialize;

The procedure enables the clock for the GPIO control pin and configures it as an output. Lastly, we disable the floodlight on line 15.

Demonstrations

Now let’s put the hardware and software together into a couple of demonstrations.

Required Global ADC Configurations

One final configuration is required for the ADC unit, and it is to be done by clients rather than the sensor drivers. In addition to the sensor-specific call to procedure Initialize, clients are responsible for making the following calls to the ADC routines provided by package STM32.ADC. They are not set internally by the various Initialize implementations because the functionality and settings are global across all ADC units provided by the STM32 device. We don't want to hard-code them globally based on any one sensor driver’s usage. These two calls are made by the main program in our demonstrations:

1. STM32.ADC.Reset_All_ADC_Units;2.3. STM32.ADC.Configure_Common_Properties4.   (Mode           => Independent,5.   Prescalar      => PCLK2_Div_2,6.   DMA_Mode       => Disabled,7.   Sampling_Delay => Sampling_Delay_5_Cycles);

Resetting all the ADC units (line 1) is not strictly required but is a good idea. Doing so ensures the units are in a known good state after any previous execution. The call on line 3 configures properties common to all the ADC units on the STM32 device. Recall that we said the ADC units can operate independently but can also be configured to operate in a combined, interdependent manner. The argument on line 4 clearly selects independent execution. The argument on line 5 selects the frequency of the common clock driving all the ADC units. The formal parameter DMA_Mode on line 6 looks pertinent because of our use of DMA but in fact that value specifies the DMA access used when working in one of the combined modes, not the independent mode we are using. Finally, the sampling delay specified on line 7 is the time to wait between any two sampling phases during the conversions. A delay of five cycles is the shortest delay supported. These arguments to procedure Configure_Common_Properties work with the NXT sound and light sensors, among others, and should work for many analog devices.

Device Selections

These analog sensors’ implementations use ADC and DMA devices, as well as GPIO points for the analog inputs and discrete outputs. We have declared “constructor” functions that take the device selections as inputs and construct an object of the corresponding analog sensor type. For example, here is the interface for the light sensor constructor function:

1. package NXT.Light_Sensors.Constructors is2.3.   function New_Light_Sensor4.     (Converter      : not null access Analog_To_Digital_Converter;5.       Input_Channel  : Analog_Input_Channel;6.       Input_Pin      : GPIO_Point;7.       Controller     : not null access DMA_Controller;8.       Stream         : DMA_Stream_Selector;9.       Floodlight_Pin : GPIO_Point)10.   return NXT_Light_Sensor;11.12. end NXT.Light_Sensors.Constructors;

The function New_Light_Sensor creates an object of the NXT_Light_Sensor type by assigning the input devices to the corresponding record components, as shown in the package body:

1. package body NXT.Light_Sensors.Constructors is2.3.   function New_Light_Sensor4.     (Converter      : not null access Analog_To_Digital_Converter;5.       Input_Channel  : Analog_Input_Channel;6.       Input_Pin      : GPIO_Point;7.       Controller     : not null access DMA_Controller;8.       Stream         : DMA_Stream_Selector;9.       Floodlight_Pin : GPIO_Point)10.   return NXT_Light_Sensor11.   is12.   begin13.       return Result : NXT_Light_Sensor do14.         Result.Assign_ADC (Converter, Input_Channel, Input_Pin);15.         Result.Assign_DMA (Controller, Stream);16.         Result.Floodlight_Pin := Floodlight_Pin;17.       end return;18.   end New_Light_Sensor;19.20. end NXT.Light_Sensors.Constructors;

The function is located in a package that is a “child” of the NXT.Light_Sensors package so it has the required compile-time visibility needed to do the assignment to the floodlight component (line 16).

The sound sensor has a similar “constructors” package and function.

The constructor function approach hides the details of how the device assignments are made to the sensors. However, care must be exercised when choosing the devices and GPIO points themselves because, although they are multiplexed and are thus configurable, not all possible combinations are supported. For example, on the STM32F4xxx devices, only DMA unit #2 can be mapped to an ADC unit. Similarly, the GPIO pin used for the analog input line must match the chosen analog input channel. The device literature and the STM32F4 Reference Manual document the restrictions.

To avoid making mistakes in this area, we simplify the application of the device and GPIO selections by doing it all in one place, i.e., a sensor “factory” function that calls the constructor functions with the corresponding device assignment inputs. This is the factory interface:

1. with NXT.Analog; use NXT.Analog;2.3. package Analog_Sensor_Factory is4.5.   type Known_Analog_Sensors is (Light, Sound);6.7.   function New_Sensor (Kind : Known_Analog_Sensors) return NXT_Analog_Sensor'Class;8.9. end Analog_Sensor_Factory;

Note the result type for the function on line 7 is a “class-wide” type. This function returns objects whose type is somewhere within the set of types rooted at NXT_Analog_Sensor. In other words, it can return any kind of analog sensor related by inheritance to the NXT_Analog_Sensor abstract base type. That’s how the one function can return different, specifically-typed objects in this strongly-typed, statically-typed language.

The body of the package is where the hardware selections are decided, in the form of constants and references to devices:

1. with STM32.Device; use STM32.Device;2. with STM32.ADC;    use STM32.ADC;3. with STM32.GPIO;   use STM32.GPIO;4. with STM32.DMA;    use STM32.DMA;5. 6.7. package body Analog_Sensor_Factory is8.9.   Selected_ADC_Unit      : Analog_To_Digital_Converter renames ADC_1;10.   Selected_Input_Channel : constant Analog_Input_Channel := 5;11.   Matching_Input_Pin     : GPIO_Point renames PA5;  -- must match the channel!12.13.   Required_DMA_Unit      : DMA_Controller renames DMA_2;14.   --  On the STM32F4 devices, only DMA_2 can attach to an ADC15.   Matching_Stream        : constant DMA_Stream_Selector := Stream_0;16.   --  maps to ADC_1 on DMA_2 (Stream_4 is the only alternative)17.18.   Digital_Line_0         : constant GPIO_Point := PC11;   -- arbitrary19.   Digital_Line_1         : constant GPIO_Point := PC12;   -- arbitrary20.21.   function New_Sensor (Kind : Known_Analog_Sensors) return NXT_Analog_Sensor'Class is22.   begin23.       …24.   end New_Sensor;25.26. end Analog_Sensor_Factory;

Specifically, lines 9 through 19 in the above (elided version) capture all the necessary choices. The function body applies those choices when calling the constructor functions. Note that the function is only meant to be called once, with the approach above, because the same exact hardware devices would be assigned to every sensor created. To support creating more sensor objects we could have a table and walk through that on the calls. The point is to have the device choices located in one place, to keep them straight.

Main Programs

There are two new demonstration programs added for part three. One works with either the light or the sound sensor but is initially set to use the light sensor. The other demonstration program works with the sound sensor. Both call the factory function to create the sensor object used in the rest of the program. The demo that works with either sensor is in the file named “demo_analog_sensors.adb” and, as mentioned, works with the light sensor initially. You can change the kind of sensor it uses by changing the call to the factory function. The other demonstration that works with the sound sensor is in the file named “demo_sound_sensor.adb” but, although it too calls the factory function, it is intended to work specifically with the sound sensor so you should not change that call.

The two programs run on different target boards. The program in “demo_analog_sensors.adb” is set up for the STM32F429I Discovery board because that board has an LCD. Some other supported board could be used instead. The program in “demo_sound_sensor.adb” is written to run on an STM32F4 Discovery board because it drives an LED via PWM, and that target board can do so. The F429I Discovery board cannot do that, but presumably others can.

The demo for the board with the LCD continuously displays the raw and percentage values obtained from the sensor. Thus it doesn’t do anything truly sensor-specific and can work with either the light or the sound sensor. The light sensor is used in the code as-provided. The program requires you calibrate the sensor for both least and greatest inputs, with prompts on the LCD for the input state and for pressing the blue user button when ready for sampling to occur. Samples are taken for two seconds, for each of the least/greatest input states.

The dedicated sound sensor demo runs on the STM32F4 Discovery board with four LEDs. It continuously drives one of them with PWM based on the sound sensor’s reading, so that the brightness varies with the sound level. In this demo we show using fixed previously-determined calibration values, rather than interactively setting determining them. You could change the program to do the calibration, but would not have the LCD available, of course.

Building and Running the Programs

You will need to download the Robotics_with_Ada project from GitHub. However, the first time you do so you will also need to download and install the Ada run-time libraries for the boards supported by the ADL. Fortunately, there is a Python script provided within the ADL for doing everything required. The script is in the scripts/ subdirectory of the ADL, so you invoke it on the command line as follows, from within the ADL root directory:

python ./scripts/install_dependencies.py

Everything is described in the Getting Started section of the ADL readme, located here: https://github.com/AdaCore/Ada_Drivers_Library/tree/master/examples so just follow the instructions in that readme file.

Each demonstration program has a dedicated project file that specifies everything: “demo_analog_sensors.gpr” and “demo_sound_pwm.gpr.”  You should first build the executables on the command line, before doing so in GPS, via the first of these commands. The subsequent steps convert and download the binary to the board:

1. gprbuild -p –P demo_sound_pwm.gpr2. cd objdemo_sound_pwmdebug3. arm-eabi-objcopy -O binary demo_sound_pwm demo_sound_pwm.bin4. st-flash write demo_sound_pwm.bin 0x8000000

Line 1 builds the entire project. The reason to do this on the command line – the first time, at least – is that it will create any missing object subdirectories (hence the “-p” switch). Line 3 converts the executable to a binary image suitable for downloading to the board. Line 4 downloads the converted image starting at the address indicated. Ensure you have the correct board attached to the host before executing that last step. Once downloaded, the program will begin executing immediately. Don’t forget the 10K pull-up resistor attached to the analog input line coming from the sensor cable.

Note that the generated image will be located in a subdirectory of the “obj” directory. By default that subdirectory is the “debug” directory under a program-specific subdirectory, as shown above in the command line step 2. There is another “production” subdirectory possible. The choice reflects the builder switches applied. You control those switches and the corresponding subdirectory by a “scenario variable” named “APP_BUILD” defined by the project files. To

2 thoughts on “Making robots with Ada, Part 3 – Working with analog sensors

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.