An IOBoard specialization is quite simple to implement in general, as soon as you get implementations for Protocols it supports. This tutorial will guide you through the process of implementing a new IOBoard.

Before starting implementation

Before starting the implementation, it is important to know what is needed before starting the new IOBoard implementation.

identify boards IOs

The base action to perform is to identify all IOs provided by your board : ethernet/wifi controllers, serial/USB ports, GPIOs and so on.

Most of embedded boards provides pins with specific indexing. What you basically need is the complete description of your board pins with their corresponding identifiers. Also if your board is supported by a third party library like MRAA, you need to know the MRAA mapping for your pins.

Protocols

The other important thing to check are the protocols that can be provided by your board. For instance if your board support a linux OS, then this means you will at least need to export the hardio-linux package to make the protocols automatically available for your boards users.

If your card is MRAA compatible then your board package should also use/export hardio-mraa package.

If your board provide a specific implementation for at least some protocols, then you will need to define these protocols directly into the board library.

Implementation of a main IOBoard

Let’s take as example the Upboard provided in the hardio-upboard package.

Project description

The Intel Upboard is a MRAA supported board running a linux OS. So in its root CMakeLists.txt:

cmake_minimum_required(VERSION 3.19.8)
...
project(hardio-upboard)

PID_Package(
    ...
)

PID_Dependency(hardio-linux VERSION 1.0)
PID_Dependency(hardio-mraa VERSION 1.0)

PID_Publishing(
    PROJECT https://gite.lirmm.fr/hardio/boards/hardio-upboard
    FRAMEWORK 	hardio
    CATEGORIES 	ioboard
    ...
)

build_PID_Package()

It uses the hardio-linux and hardio-mraa packages as dependencies for getting libraries implementing protocols for linux and MRAA. Also we declare it as a member of the hardio framework, contributing to the ioboard category.

We declare the library upboard in src/CMakeLists.txt:

PID_Component(upboard
  EXPORT 
    hardio/linux
    hardio/mraa
  C_STANDARD 11
  CXX_STANDARD 17
)

And we define the root public header include/upboard/hardio/upboard.h that will contain the unique class of the library.

The Upboard class

Now we edit include/upboard/hardio/upboard.h to add the class description:

The library upboard in src/CMakeLists.txt:

#pragma once
#include <hardio/linux.h>
#include <hardio/mraa.h>
#include <map>

namespace hardio {
class Upboard : public IOBoard {
public:
  Upboard(std::string_view eth = "eth0");
  virtual ~Upboard() = default;

  LinuxEthernetBus &ethernet();
  USB &use_usb(std::string_view usb_bus);
  MRAAPinIO &pin(uint8_t index);

  MRAAI2CProtocol &use_i2c(uint8_t group_id);
  MRAAUARTProtocol &use_serial(size_t baudrate);

  std::vector<std::string> available_usb() const;
  std::vector<uint8_t> available_pins() const;
  std::vector<uint8_t> available_pins(uint16_t flags) const;
  std::vector<uint8_t> i2c_groups() const;

private:
  std::shared_ptr<LinuxEthernetBus> ethernet_;
  std::map<std::string, std::shared_ptr<USB>> usb_;
  std::map<uint8_t, std::shared_ptr<MRAAPinIO>> pins_;
};
} // namespace hardio

So first important things: since the Upboad is an IOBoard it must be a child of the IOBoard class. Also always put all end user related code directly into the hardio namespace.

Then according to the list of IOs you identified for your board, you need to create corresponding attributes and accessors. Simply remenber that internally to hardio all elements like protocols and interface are memorized using shared pointer, so it is quite straightforward to also manipulate attributes using std::shared_ptr objects. The Upboard provides IOs that are memorized using corresponding attributes:

  • an ethernet bus.
  • a set of USB bus
  • a set of pins.

Then the corresponding accessors are provided for each of these IOS (see ethernet(), use_usb() and pin() functions).

We decided that the ethernet network interface identifier must be specified when the object is constructed so we add an argument to the class constructor, but it is a pure design choice influenced by the fact that we want to create all IOs at construction time and EthernetBus IO need a network interface identifier at construction time. See next section to understand.

You can also provide more sophisticated functions that automatically bind adequate protocols or create interfaces, like use_i2c and use_serial. This is particularly usefull when you need to configure PinIOs in a correct way, using a specific Protocol implementation.

Finally you can provide convenient functions providing information on the boards, like the list of pins.

Implementing the Upboard class

Now we create the file src/upboard.cpp to provide the implementation:

Here is sum up code of the constructor:

namespace hardio {
Upboard::Upboard(std::string_view eth)
    : IOBoard(),
      ethernet_{add_io<LinuxEthernetBus>(eth).ptr<LinuxEthernetBus>()},
      usb_{ {
          {"usb3_b", add_io<USB>("usb3_b").ptr<USB>()},
          {"usb2_a1", add_io<USB>("usb2_a1").ptr<USB>()},
          ...
      } },
      pins_{ {
          {3, add_io<MRAAPinIO>("GPIO2", 3, PinIO::GPIO | PinIO::I2C_SDA, 0)
                  .ptr<MRAAPinIO>()},
          {5, add_io<MRAAPinIO>("GPIO3", 5, PinIO::GPIO | PinIO::I2C_SCL, 0)
                  .ptr<MRAAPinIO>()},
          {7, add_io<MRAAPinIO>("GPIO4", 7, PinIO::GPIO | PinIO::ANALOG_IN, 0)
                  .ptr<MRAAPinIO>()},
          {8, add_io<MRAAPinIO>("GPIO14", 8, PinIO::GPIO | PinIO::UART_TX, 0)
                  .ptr<MRAAPinIO>()},
          {10, add_io<MRAAPinIO>("GPIO15", 10, PinIO::GPIO | PinIO::UART_RX, 0)
                   .ptr<MRAAPinIO>()},
          {11, add_io<MRAAPinIO>("GPIO17", 11, PinIO::GPIO | PinIO::UART_RTS, 0)
                   .ptr<MRAAPinIO>()},
          ...

      } } {
}
...
}

The logic is quite simple: we use add_io function to declare all IOs provided by the board. We memorize shared pointers obtained with a call to ptr() function directly or in maps for collections of same types (USB and MRAAPinIO). MRAAPinIO is a PinIO specialization for MRAA compatible boards. It is used to memorize the internal index (e.g. 3 for “GPIO2”) used by MRAA to identify the pin. This identification is mandatory when one wants to use MRAA specific protocols.

Then we can define accessors:

LinuxEthernetBus &Upboard::ethernet() { return *ethernet_.get(); }

USB &Upboard::use_usb(std::string_view usb_bus) {
  std::string bus(usb_bus);
  auto it = usb_.find(std::string(usb_bus));
  if (it != usb_.end()) {
    return *it->second;
  }
  throw std::runtime_error(
      "Upboard::usb, invalid name for usb  " + bus +
      ", valid values are usb3_b, usb2_a1, usb2_a2, usb2_a3, usb2_a4");
}

MRAAPinIO &Upboard::pin(uint8_t index) {
  auto it = pins_.find(index);
  if (it == pins_.end()) {
    throw std::runtime_error(
        "Upboard::pin, invalid index " + std::to_string(index) +
        ", refer to upboard datasheet to know valid pin indexes");
  }
  return *it->second.get();
}

The code is straightforward to understand, the basic idea is to check that IOs identifiers are correct and then returning a reference on the corresponding object.

When we face more complex configuration, for instance when configurng pins in order to implement correct communication protocol, it is a good idea to provide dedicated functions to minimize end users errors:

MRAAI2CProtocol &Upboard::use_i2c(uint8_t group_id) {
  if (group_id > 1) {
    throw std::runtime_error("Upboard::use_i2c: invalid I2C id : " +
                             std::to_string(group_id));
  }
  // select the good SDA / SDC pins depending on target group
  uint8_t sda = -1, sdc = -1;
  if (group_id == 0) {
    sda = 3;
    sdc = 5;
  } else {
    sda = 27;
    sdc = 28;
  }
  // finnaly use those pins with I2C
  return this->def_group(*pins_[sda], *pins_[sdc]).bind<MRAAI2CProtocol>();
}

MRAAUARTProtocol &Upboard::use_serial(size_t baudrate) {
  return this->def_group(*pins_[8], *pins_[10])
      .bind<MRAAUARTProtocol>(baudrate);
}

Here for instance use_serial defines the group of correct PinIOs when one wants a serial communication. Indeed only pins 8 and 10 can support such communication protocol (because they support type UART_TX and UART_RX respectively). Then the adequate protocol implementation MRAAUARTProtocol is bound to this IO group.