Developing an LED interface

To illustrate the initial concept in detail, let's take a look at a simple driver that we've been using since the first examples introduced in this book—an LED driver. A simplified version of an interface to drive the LEDs on our Nucleo board has been used since the very first examples in earlier chapters. This interface is located at BSPNucleo_F767ZI_GPIO.c/h. This code fully abstracted the LEDs from the underlying hardware with a struct named LED. The LED struct has two function pointers: On and Off. As expected, the intention of these two functions is to turn an LED on and off. The beauty of this is that the calling code doesn't need to be concerned with the implementation of the LED at all. Each LED could have a completely different hardware interface. It might require positive or negative logic to drive an external transistor or be on a serial bus of some sort. The LED could even be on a remote panel requiring remote procedure calls (RPCs) to another board entirely. However, regardless of how the LED is turned on and off, the interface remains the same. 

To try and keep things simple, Nucleo_F767ZI_GPIO.c/h defined the LED struct in the header file. As we move through this current example, we'll extract the interface definition from the header file, making it completely standalone, requiring no external dependencies. The lack of dependencies will guarantee that we can move the new interface definition to entirely different platforms, without requiring any code specific to a particular MCU at all.

Our new, independent LED interface will be called iLED

The lowercase "i" is a convention used by some C++ programmers to indicate a class that only contains virtual functions, which is effectively an interface definition. Since we're only dealing with C in this book (not C++), we'll stick to structs and function pointers to provide the necessary decoupling. The methods outlined here are conceptually similar to pure virtual classes in C++.

The interface is defined in the new Interfaces/iLed.h file; the core of the contents is as follows:

typedef void (*iLedFunc)(void);

typedef struct
{
//On turns on the LED - regardless of the driver logic
const iLedFunc On;

//Off turns off the LED, regardless of the driver logic
const iLedFunc Off;
}iLed;

Let's break down exactly what is going on in the preceding definition:

  1.  We create a new type:iLedFunc. Now, typedef void (*iLedFunc)(void); defines the iLedFunc type as a function pointer to a function that takes no arguments and returns nothing.
  2. The iLed struct is defined as any other struct—we can now create instances of this struct. We're defining a struct so it is convenient to bundle together all of the function pointers and pass a reference to the structs around it.
  3. Each iLedFunc member is defined as const so it can only be set once at the time of definition. This protects us (or other developers) from accidentally overwriting the value of the function pointer (which can be potentially disastrous). The compiler will catch any attempts to write to the On or Off function pointers and throw an error.
It is extremely important that the header file defining the interface includes as few dependencies as possible to keep it as loosely coupled as possible. The more dependencies this file has, the less future flexibility there will be.

That does it for the interface definition. There is no functionality provided by the preceding code; it only defined an interface. In order to create an implementation of the iLed interface, we'll need two more files.

The following is an excerpt from ledImplementation.h:

#include <iLed.h>
extern iLed BlueLed;
extern iLed GreenLed;
extern iLed RedLed;

This header file brings in the iLed.h interface definition and declares three instances of iLed, which are BlueLedGreenLed, and RedLed. These implementations of the iLed interface can be used by any piece of code that includes ledImplementation.h.  The extern keyword ensures only one copy is ever created, regardless of how many different code modules use ledImplementation.h.

Next, we need to provide definitions for the iLed instances; this is done in ledImplementation.c.

Only the code for GreenLed is shown here. The BlueLed and RedLed implementations only differ in the GPIO pin they set:

void GreenOn ( void ) {HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);}
void GreenOff ( void ) {HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0,
GPIO_PIN_RESET);}
iLed GreenLed = { GreenOn, GreenOff };

Breaking it down, we can observe the following:

  1. GreenOn defines an inline function that turns on the green LED on our Nucleo development board. It takes no parameters and returns nothing, so it can be used as iLedFunc, as defined in the previous code.
  2. GreenOff defines an inline function that turns off the green LED on our Nucleo development board. It is can also be used as iLedFunc.
  3. An instance of iLed is created and named GreenLed. The iLedFunc function pointers GreenOn and GreenOff are passed in during initialization. The order of the functions defined in iLed is critical. Since On is defined in the iLed struct first, the first function pointer passed in (GreenOn) will be assigned to On.
The only code that relies on specific hardware so far is ledImplementation.c

A pointer to GreenLed can now be passed to different pieces of code that only bring in iLed.h—they won't be tied to HAL_GPIO_WritePin in any way. An example of this is hardwareAgnosticLedDriver.c/h.

The following is an excerpt from hardwareAgnosticLedDriver.h:

#include <iLed.h>
void doLedStuff( iLed* LedPtr );

The only include function required by this hardware-agnostic driver is iLed.h.  

For hardwareAgnosticLedDriver.h to be truly hardware agnostic, it must not include any hardware-specific files. It must only access hardware through hardware-independent interfaces, such as iLed.  

The following is a trivial example that simply turns a single LED on or off. The excerpt is from hardwareAgnosticLedDriver.c:

void doLedStuff( iLed* LedPtr )
{
if( LedPtr != NULL )
{
if(LedPtr->On != NULL)
{
LedPtr->On();
}

if( LedPtr->Off != NULL )
{
LedPtr->Off();
}
}
}

Breaking it down, we can observe the following:

  1.  doLedStuff takes in a pointer to a variable of the iLed type as a parameter. This allows any implementation of the iLed interface to be passed in doLedStuff, which provides complete flexibility in how the On and Off functions are implemented without tying hardwareAgnosticLedDriver to any specific hardware.
  2. If your interface definition supports leaving out functionality by setting pointers to NULL, they will need to be checked to ensure they are not set to NULL. Depending on the design, these checks might not be necessary since the values for On and Off are only able to be set during initialization.
  3. The actual implementations of On and Off are called by using the LedPtr pointer and calling them like any other function.

A full example using doLedStuff is found in mainLedAbstraction.c:

#include <ledImplementation.h>
#include <hardwareAgnosticLedDriver.h>

HWInit();

while(1)
{
doLedStuff(&GreenLed);
doLedStuff(&RedLed);
doLedStuff(&BlueLed);
}

Breaking it down, we can observe the following:

  1. The implementations for GreenLedRedLed, and BlueLed are brought in by including ledImplementation.h.
  2. doLedStuff is brought in by including hardwareAgnosticLedDriver.h.
  3. We provide the implementation for doLedStuff by passing in a pointer to the desired instance of iLed. In this example, we're toggling each of the green, red, and blue LEDs on the development board by passing the GreenLed, RedLed, and BlueLed implementations to doLedStuff.

This example simply toggled the single LEDs, but the complexity is arbitrary. By having well-defined interfaces, tasks can be created that take in pointers to instances of the interface. The tasks can be reused across multiple projects without touching them all—only a new implementation of the interface needs to be created when support for new hardware is required. When there is a considerable amount of code implemented by the hardware-agnostic task, this can dramatically decrease the total amount of time spent on the project.

Let's take a look at a simple example of passing instances of interfaces into tasks.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.218.61.16