Using IOBoards, protocols and interfaces
As a start, let’s suppose you have all drivers already implemented for your application. This tutorial explains how to describe your hardware architecture in terms of devices and protocols used to communicate with these devices.
Describing the communication architecture
First step when using hardio is always to describe the communication architecture : this consists in describing the IOs you want to use, the communication protocols and so on.
Long story short, here is the example code:
#include <hardio/core.h>
int main() {
IOBoard board;
auto& eth_ip = board.add_io<EthernetBus>("enx8c47be0d25cc")
.bind<IP>("193.49.107.74");
auto& wifi_ip = board.add_io<WifiController>("wlp4s0")
.bind<IP>(); // no specific IP defined (i.e. "we don't
// care" or "take the good one" strategy)
auto& native_serial =
board.add_io<SerialBus>("/dev/ttyS0").bind<FakeSerialProtocol>(115200);
auto& usb_serial =
board.add_io<USB>("/dev/ttyUSB0").bind<FakeSerialProtocol>(115200);
auto& i2c = board.add_io<I2CBus>("/dev/i2c-0");
....
board.initialize();
} IOBoards and IOs
The main component around which any hardio program revolves is an IOboard. This component represents an electronic board with computing capabilities and that provides a set of interfaces. In this example the IOBoard is generic (not related to a specific embedded card).
The following code simply tells that you use the hardio API:
#include <hardio/core.h>And then you declare IOBoard this way in the main program:
IOBoard board;Since this board is generic we can declare it as providing any kind of IO. For instance to declare an ethernet bus:
board.add_io<EthernetBus>("enx8c47be0d25cc")The template parameter defines the type of IO, in the example an ethernet bus : the template parameter is EthernetBus. Most of time you need to declare an identifier for the related IO, in the example enx8c47be0d25cc is the identifier of the ethernet controller you want to use.
Definition: IOs are hardware interfaces. Generally an Interface is an access point to a communication channel : an IO is a special interface for a physical communication channel. An IO is always directly attached to a IOBoard, using the add_io function. In the end IOs provide the base interfaces on top of which the communication architecture is built.
With this generic example board we define many IOs:
board.add_io<WifiController>("wlp4s0")
...
board.add_io<SerialBus>("/dev/ttyS0")
...
board.add_io<USB>("/dev/ttyUSB0")
...
board.add_io<I2CBus>("/dev/i2c-0")The template parameters for IOs types like WifiController or I2CBus are predefined in hardio. They are mostly descriptive elements that do not implement specific checks on hardware. We will see later in this tutorial that theses types can be specialized for specific boards implementation in order to perform automatic checks on hardware.
Now it’s time to learn how to use these IOs.
Protocols and virtual interfaces
For any interface in the system one can bind a Protocol to it. This is achieved by using the bind method, like in this example:
auto& eth_ip = board.add_io<EthernetBus>("enx8c47be0d25cc")
.bind<IP>("193.49.107.74");We bind a IP protocol to the EthernetBus. This means that this bus will be configureg for using an IP based protocol. The call to bind uses the template parameter IP to define the type of the protocol used. We can also pass as argument the IP address we want for the IP connection through the ethernet bus. Again IP class is mostly descriptive and can be specialized for specific board implementation in order to perform specific check or configuration actions, which will be demonstrated later.
The same protocol type can be bound to any interface supporting this protocol. For instance the IP protocol can be bound also to a WifiController IO. Similarly a USB or a Serial IO can be bound to a same SerialProtocol.
auto& native_serial =
board.add_io<SerialBus>("/dev/ttyS0").bind<FakeSerialProtocol>(115200);
auto& usb_serial =
board.add_io<USB>("/dev/ttyUSB0").bind<FakeSerialProtocol>(115200);Here the FakeSerialProtocol is a class implementing a serial communication for a given IOBoard type. We can see that the baudrate must be configured during the binding (115200 passed as argument to bind). Indeed, depending on the protocol, one needs to pass different configuration parameters (compare with IP).
The basic use of a Protocol is to implement a virtual communication channel (like FakeSerialProtocol) or represent a sublayer of a protocol stack (like IP). In both case, from architecture description point of view, a Protocol allows to provides new (virtual) interfaces. Those interfaces can then be bound to new Protocols and so on: this way you can precisely describe and implement protocols stacks.
Here is an example, extending the previous one:
//protocols and higher level interface with ethernet
auto& udp1 = eth_ip.add_interface<IPInterface>()
.bind<FakeUDPEndpoint>();
auto& endpt_to_server1 = udp1.add_interface("192.168.1.24", 25540);
//... with wifi
auto& tcp_conn = wifi_ip.add_interface().bind<FakeTCPClient>();
auto& udp2 = wifi_ip.add_interface().bind<FakeUDPEndpoint>(502);In this example the eth_ip and wifi_ip previously created represent the IP protocols bound to particular IOs. On top of these protocols we can call the add_interface function to create new interfaces. Please note that add_interface() and add_interface<IPInterface>() finally result in the same behavior: create an IPInterface object from the IP protocol. add_interface() signature is simply a shortcut method that creates the interface of default type for the given protocol (e.g. IPInterface for IP protocol), so most of time this signature can be used safely except if you want to extend the default interface type. This later topic is quickly explained in the using interfaces section.
The newly created IP interfaces are not directly usefull: they are used to bind new enduser IP based protocols like UDP and TCP. In the example the interface created by the call to eth_ip.add_interface<IPInterface>() is then bound to a UDPEndpoint protocol. Again the type FakeUDPEndpoint is not a real implementation of a UDP endpoint, we will see later how it is done.
We can then create interfaces on top of udp1 protocol to describe communications with remote endpoints:
auto& endpt_to_server1 = udp1.add_interface("192.168.1.24", 25540);endpt_to_server1 can now be used to communicate through UDP with an endpoint with ip address "192.168.1.24" and listening on port 25540.
One can also notice that many protocol layers can be defined for the same IP layer, like for tcp_conn and udp2 that are both using the same IP protocol layer. To achieve that you only needs to add multiple interfaces for the same protocol and then bind these interface with whatever higher level protocol you need:
auto& tcp_conn = wifi_ip.add_interface().bind<FakeTCPClient>();
auto& udp2 = wifi_ip.add_interface().bind<FakeUDPEndpoint>(502);Some protocols allow to define multiple interfaces, like IP protocol, and some other don’t, like SerialProtocol. Indeed when using a serial communication there is only ONE access point to the serial communication channel.
higher level protocols
Protocols implementations are not necessarily board dependent (like the previously shown protocols that are OS dependent), they can also be completely generic and just requiring to use speficic interface(s). Let’s consider the modbus protocol that is provided by default by the hardiocore package. There are 2 variants of the modbus master protocol that slightly differ (in the way they manage slave addressing): one using a UDP interface and another one using a serial interface. These variants both allow to create ModbusSlaveInterface objects.
Here is an example of a modbus over serial:
auto& modbus_serial =
usb_serial.add_interface().bind<ModbusOverSerialProtocolMaster>();
auto& modbus_slave1 = modbus_serial.add_interface(1);
auto& modbus_slave2 = modbus_serial.add_interface(2);
auto& modbus_slave3 = modbus_serial.add_interface<ModbusSlaveInterface>(3);In this example an interface is created on top of the usb_serial protocol layer and then binds the ModbusOverSerialProtocolMaster protocol. The modbus protocol allows for a specific addressing, using simple slave numbers. So whenever a new communication endpoint with a modbus slave is created using the add_interface function, the slave address must be specified as argument.
An interesting point to notice is that even if serial protocol allows only one interface to be created (basically because there is no addressing in a serial bus), using modbus protocol allows to finally communicate with many modbus slaves over a serial bus (because modbus protocol provides a specific addressing).
What is important here is that the same modbus implementation can be used on any board as long as this board provides an implementation for SerialProtocol.
The same logic can be used to implement any kind of higher level protocol, even one of your creation.
board initialization
Finally, the board needs to be configured. This results in initializing protocols to put in place the corresponding communication channels for the declared interfaces. This is simply achieved this way:
board.initialize();If something is wrong, for instance a communication channel cannot be created for any reason, an exception is thrown and provides a description of the problem.
If everything is OK, all interfaces are now configured and ready to be used.
Using interfaces
Basically an end user should never use an Interface directly. They should be directly used by drivers in order to communicate with corresponding devices. Any way this section briefly explains how interfaces are designed and how they can be used. Indeed user/driver code uses only interfaces to communicate with devices, it never uses Protocols or IOs directly.
An Interface is an access point to a communication channel that follows rules defined by a Protocol. In terms of code, the API of an Interface is more or less directly related to the corresponding Protocol API. Each protocol has its own logic, particularly in the management of addressing, even if most of them share general principles, that is why each Protocol subclass has its own API. Let’s take the SerialProtocol as example:
class SerialProtocol : public Protocol {
public:
using base_type = SerialProtocol;
SerialProtocol(size_t baudrate);
virtual ~SerialProtocol() = default;
virtual void flush() = 0;
virtual bool write(size_t length, const uint8_t* data) = 0;
virtual bool read(size_t length, uint8_t* data, size_t& read_data) = 0;
size_t baudrate() const;
//hardio implementation related
...
private:
size_t baudrate_;
};The SerialProtocol is a protocol abstract class (i.e. inherits Protocol) that is used as the base class for all implementations of the serial protocol. Those implementatiosn may vary depending on the board used or the OS for instance. The FakeSerialProtocol is the do nothing implementation used for this tutorial.
We can see that the constructor takes as parameter the baudrate (wich explains why we had to set the baudrate in the previous section) and provides a set of method used to read, write and flush the communication channel. The corresponding Interface code is:
class SerialInterface : public ProtocolInterface<SerialProtocol> {
public:
SerialInterface();
virtual ~SerialInterface() = default;
virtual void flush();
virtual bool write(size_t length, const uint8_t* data);
virtual bool read(size_t length, uint8_t* data, size_t& bytes_read);
bool read_until(size_t length, uint8_t* data, size_t& bytes_read,
std::chrono::milliseconds timeout);
};The serial interface is declared as the default interface created by a SerialProtocol (it inherits ProtocolInterface<SerialProtocol>).
You can see that both codes are quite similar: flush, read and write methods are still here : they are just forwarding any call on the interface to the corresponding protocol implementation object (e.g. FakeSerialProtocol). This simarity is normal as there is only one interface per serial protocol and no addressing takes place with this protocol, but in many other protocols, API of protocols and interfaces may vary a bit or even a lot.
But you can see that another method has been added to the interface: read_until. This method is simply used to simulate a blocking read until a timeout is reached. It has been added for user convenience as blocking read is sometime easier to use. This method is implemented on top of the read method, so nothing specific has to be added to the protocol in order to make it work.
Writing user code
So using an interface simply consists in using an Interface object like a SerialInterface instance and call its methods the adequate way. For example suppose a serial interface has been declared in previous code:
auto& serial_comm =
board.add_io<SerialBus>("/dev/ttyS1")
.bind<FakeSerialProtocol>(115200)
.add_interface();
...
//after board initialization
uint8_t data[100];
size_t bytes_read=0;
if(not serial_comm.read_until(100, &data[0], bytes_read, 100ms)){
return -1;//error
}
if(bytes_read >0){
uint8_t data_to_send[100];
size_t bytes_to_send=0;
if(interpret_message(data,bytes_read, data_to_send, bytes_to_send)){
if(not serial_comm.write(bytes_to_send, &data_to_send[0])){
return -1;
}
}
else{
serial_comm.flush();
}
}
return 0;This code is quite simple and should be mostly self explanatory: after creating the serial_comm interface the user code simply uses its API read/write on the corresponding serial bus.
One could question why using all this stuff just to create a serial communication ! Its obviously hard to give convincing arguments with such a simple example. But as a first answer you can see hardio way to manage protocols a good way to standardize protocols implementations. Indeed there may be various implementations of protocols depending on the board used, and using the hardio proposed idiom they will all have the same API. The direct benefit is so that you will not have to rewrite the user code if the protocol implementation changes. There are other benefits but they will be explained in following tutorials.
About interface extension
hardio provides default interface types for all protocols but, for any reason, users may want a more convenient interface for coding their application. Let’s take as example the SerialInterface class: it could have just provided the read/write/flush methods but not read_until. Using the extension mechanism the read_until could have been added to a class inheriting SerialInterface. The direct benefit of doing so is that you will not have to implement again and again the same communication pattern in your user code: by extending the interface you make it reusable (by you or others) in a straightforward way.
Here is a meaningless example code to demonstrate how to do that:
class SerialInterfaceWithQuestionAnswer : public SerialInterface {
public:
SerialInterfaceWithQuestionAnswer(const std::chrono::milliseconds& wait_time);
virtual ~SerialInterfaceWithQuestionAnswer() = default;
bool write_then_read(size_t length, const uint8_t* data, size_t read_bytes);
private:
std::chrono::milliseconds wait_time_;
};A method write_then_read has been added to the initial serial interface and can be called if the interface is created with adequate type like in:
auto& serial_comm =
board.add_io<SerialBus>("/dev/ttyS1")
.bind<FakeSerialProtocol>(115200)
.add_interface<SerialInterfaceWithQuestionAnswer>(50ms);
...
uint8_t data[50];
size_t read_bytes=0;
if(serial_comm.write_then_read(50,&data[0], read_bytes)){
if(read_bytes > 0){
...
}
}
...To use the new interface type (SerialInterfaceWithQuestionAnswer) you need to adequately specify the template parameter of the add_interface function call:
add_interface<SerialInterfaceWithQuestionAnswer>(50ms);You can note that, since the SerialInterfaceWithQuestionAnswer defines a non optional argument for its unique constructor (wait_time) you need to pass the corresponding value as argument of add_interface.
There is no much more to say on that point, just remember that using this extension mechanism is meaningfull only if your extension is really reusable accross various user codes.
Using real implementations
Until now the tutorial presented the basic concepts of hardio but did not fall into the details on how to use real boards and protocols implementations. We will now see how to use real implementations.
Using real implementation of protocols
We will first demonstrate the use of specific protocols implementations by using two new packages: hardio-linux and hardio-mraa. The first one provides common protocol implementation for linux systems and the second one provides protocol implementation for MRAA compatible cards, like raspberry pi boards.
First of all, to add these dependencies they must be declared in your package root CMakeLists.txt:
cmake_minimum_required(VERSION 3.1.8)
...
PID_Dependency(hardiocore VERSION 2.0)
PID_Dependency(hardio-linux VERSION 1.0)
PID_Dependency(hardio-mraa VERSION 1.0)
...
build_PID_Package()And then add their components as dependencies to the library or application component describing your hardware architecture:
PID_Component(your_description
CXX_STANDARD 17
DEPEND hardio/linux
hardio/mraa
hardio/core
)Now in your source code add the corresponding includes:
#include <hardio/core.h>
#include <hardio/linux.h>
#include <hardio/mraa.h>
int main() {
IOBoard board;
...
} linux protocols implementation
In hardio framework headers of libraries are always defined and included the same way: for example for the package hardio-linux the include directive for the corresponding library is #include <hardio/linux.h>. Now let’s see deeper how implementations are used, starting with linux provided implementations. We change a bit the description used in previous sections:
...
IOBoard board;
auto& eth_ip = board.add_io<EthernetBus>("enx8c47be0d25cc")
.bind<PosixIP>("193.49.107.74");
auto& wifi_ip = board.add_io<WifiController>("wlp4s0")
.bind<PosixIP>();
auto& native_serial =
board.add_io<SerialBus>("/dev/ttyS0").bind<PosixSerialProtocol>(115200);
auto& usb_serial =
board.add_io<USB>("/dev/ttyUSB0").bind<PosixSerialProtocol>(115200);
auto& i2c = board.add_io<LinuxI2CProtocol>("/dev/i2c-0");
auto& udp1 = eth_ip.add_interface()
.bind<PosixUDPEndpoint>();
auto& endpt_to_server1 = udp1.add_interface("192.168.1.24", 25540);
...That’s it ! The only thing we have to change is the type used when binding protocols to interfaces. The protocols implementation can be used for doing various things.
PosixIP for instance is usefull for :
- checking existance of the corresponding IP layer on top of the
EthernetBusused (i.e. there is a IP network connection defined for the given network interface) - if an IP is defined as parameter (see
eth_ip) then it checks that this IP is really the IP for the given network interface - otherwise it automatically deduces the IP of the network interface.
PosixSerialProtocol is an implementation of serial communication using posix standard API. It also checks that the serial identifier in use is valid and checks/configures the baudrate.
PosixUDPEndpoint is an implementation of UDP endpoint protocol using posix sockets.
The linux implementation of protocols should be really widely used since most of modern embedded boards are linux based, but you may need to provide other implementation for such protocols if for instance you intend to use a modern microntroller with dedicated OS (e.g. FreeRTOS,uCOS) or a specific real-time OS (e.g. VxWorks) used by industrial PLC. You may also simply need to use a linux RTOS extension (e.g. Xenomai, RTAI) providing more efficient implementations of protocols (e.g. RT-UDP, RT-USB).
MRAA protocols implementation
Until now we did not discuss about real embedded systems programming, but this is also possible with hardio. The framework provides IO for that called PinIO. A PinIO represents a pin on an electronic board. A pin has one or more types and can be configured differently depending on its type. For instance a pin can be a GPIO (digital IN/OUT pin) but can also sometimes be used as a UART_TX or UART_RX pin (i.e. can be used to implement a serial bus), or as a PWM pin. This pin configuration typically depends on the embedded board model in use. PinIOs being IOs they can bind a protocol depending on their type. So using of pins follows the exact same logic (using protocols and interfaces) as presented previously.
Here is an example code:
...
IOBoard board;
...
auto& pin_1 = board.add_io<PinIO>("GPIO1", PinIO::GPIO | PinIO::PWM_OUT);
auto& pin_2 = board.add_io<PinIO>("GPIO2", PinIO::GPIO);
auto& pin_3 = board.add_io<PinIO>("GPIO3", PinIO::ANALOG_IN | PinIO::GPIO);
auto& pin_4 = board.add_io<PinIO>("GPIO15", PinIO::GPIO | PinIO::UART_RX,0);
auto& pin_5 = board.add_io<PinIO>("GPIO16", PinIO::GPIO | PinIO::UART_TX,0);
auto& pin_6 = board.add_io<PinIO>("GPIO17", PinIO::GPIO | PinIO::UART_RX,1);
auto& pin_7 = board.add_io<PinIO>("GPIO18", PinIO::GPIO | PinIO::UART_TX,1);
...Here PinIOs descriptions are added “on the fly” to the board. Arguments are used to define the type of pins. For instance the pin_2 IO is a pure GPIO while pin_4 and pin_5 can be used as GPIO or as TX/RX pins for a serial bus.
Basic protocols can be bound to these pins. There are currently different protocols (and their corresponding default interfaces) supported for PinIOs: PinAnalogRead, PinAnalogWrite, PinDigitalRead, PinDigitalWrite, PinPWMWrite, PinPPMWrite. These protocols represent very simple communications using pins. PinAnalogRead for example allows to read an analog value from a pin. Of course not all protocols can be bound to any pin: the protocol bound must be supported by the pin type. PinAnalogRead cannot be used with a pin that has not the ANALOG_IN type, so in the example it can be bound only to pin_4.
hardio-MRAA provides implementations for such protocols for any electronic board that is supported by the MRAA project. It also provides a specialization of PinIO that is used to fill board implementation specific information. To use the specialization we change the above code into:
...
MRAABoard board;
...
auto& pin_1 = board.add_io<MRAAPinIO>("GPIO1", 3, PinIO::GPIO | PinIO::PWM_OUT);
auto& pin_2 = board.add_io<MRAAPinIO>("GPIO2", 4, PinIO::GPIO);
auto& pin_3 = board.add_io<MRAAPinIO>("GPIO3", 5, PinIO::ANALOG_IN | PinIO::GPIO);
auto& pin_4 = board.add_io<MRAAPinIO>("GPIO15", 14, PinIO::GPIO | PinIO::UART_RX,0);
auto& pin_5 = board.add_io<MRAAPinIO>("GPIO16", 17, PinIO::GPIO | PinIO::UART_TX,0);
auto& pin_6 = board.add_io<MRAAPinIO>("GPIO17", 18, PinIO::GPIO | PinIO::UART_RX,1);
auto& pin_7 = board.add_io<MRAAPinIO>("GPIO18", 19, PinIO::GPIO | PinIO::UART_TX,1);
...We first change the board implementation to be a MRAABoard. This object simply provides a way to check that the current board executing the code is a MRAA compatible board. Then we declare PinIOs using the MRAAPinIO type as template parameter. The only difference is that the second argument has been added to identify the pin number in the MRAA board.
Note: Using MRAABoard instead of the base type IOBoard is not mandatory if you do not need to check that the current board executing the software is a MRAA compatible board. On the contrary you must use MRAAPinIO type to clearly identify the pin as a MRAA pin. Otherwise no MRAA protocol implementation could be used with those pins.
Now we can bind protocols to those IOs:
...
auto& pwm_pin1 = pin_1.bind<MRAAPinPWMWrite>().add_interface();
auto& pin2_read = pin_2.bind<MRAAPinDigitalRead>().add_interface();
auto& pin3_read = pin_3.bind<MRAAPinAnalogRead>().add_interface();
...Nothing special in this description we simply use the MRAA based implementation of protocols PinPWMWrite, PinDigitalRead and PinAnalogRead.
Finally we can use them:
...
pwm_pin1.set_duty_cycle(0.5f);//set the duty cycle of the PWM
bool dval = pin2_read.digital_read();
int aval = pin3_read.analog_read();Groups of interfaces
hardio-mraa also support specific implementation for other higher level protocols. For instance there is an implementation for serial protocol, called MRAAUARTProtocol. The main difference with linux based implementation is that this implementation is based on PinIOs and not SerialBus IOs. To achieve that we need to bind the protocol to a group of IOs. In this example we can for instance bind it to the pin_4 and pin_5, because these two pins can be configured together to implement a serial commmunication (they have type UART_RX and UART_TX for the same group 0).
So now the problem is how to bind a protocol to two pins ? This is achieved with the concept of InterfaceGroup. Here is an example:
...
auto& pin_4 = board.add_io<MRAAPinIO>("GPIO15", 14, PinIO::GPIO | PinIO::UART_RX,0);
auto& pin_5 = board.add_io<MRAAPinIO>("GPIO16", 17, PinIO::GPIO | PinIO::UART_TX,0);
...
//create the group
auto& pins_serial =
board.def_group(pin_4, pin_5)
.bind<MRAAUARTProtocol>(115200)
.add_interface();The def_group method creates a virtual interface that regroups a set of interfaces (in this case PinIOs) passed as argument. Once done the regrouped interfaces can no more be used separately. But then we can bind a protocol to this virtual interface, here MRAAUARTProtocol. This protocol check that those pins have correct types. In the end, the call to add_interface() provides the same SerialInterface object as if we were using a SerialBus IO. Which means that the interface user code can safely be used without any change.
The underlying implementation of MRAAUARTProtocol is quite different from the Posix based one, but for the end user of the serial interface there is no difference.
Using real implementation of boards
We will now demonstrate the use of the Intel upboard implementation. This board is in the same time a linux board and a MRAA board so we can reuse everything explained in previous sections.
First of all we need to use the hardio-upboard package that provides the board description. We replace hardio-linux and hardio-mraa packages by hardio-upboard in project dependencies.
cmake_minimum_required(VERSION 3.19.8)
...
PID_Dependency(hardiocore VERSION 2.0)
PID_Dependency(hardio-upboard VERSION 1.0)
...
build_PID_Package()And then add their components as dependencies to the library or application component describing your hardware architecture:
PID_Component(your_description
CXX_STANDARD 17
DEPEND hardio/upboard
hardio/core
)Now in your source code add the corresponding includes:
#include <hardio/core.h>
#include <hardio/upboard.h>
...Finally the example code is:
...
int main(){
Upboard board;
//getting the pins numbers (or get them from the board spec)
auto gpio_pins = board.available_pins(PinIO::GPIO);
auto& pin_read = board.pin(gpio_pins[0]).bind<MRAAPinDigitalRead>.add_interface();
auto& pin_write = board.pin(gpio_pins[1]).bind<MRAAPinDigitalWrite>.add_interface();
auto aread_pins = board.available_pins(PinIO::ANALOG_IN);
auto& pin_aread = board.configure_pin<MRAAPinAnalogRead>(aread_pins[0]);
auto i2c_groups = board.i2c_groups();
auto& i2c = board.use_i2c(i2c_groups[1]).add_interface();
...
}The difference from previous section where we were using generic boards, is now that the IO description is embedded in the board description. Indeed embedded boards have a limited set of available IOs, including ethernet ports, USB/serial lines or pins. The board itself contains this description and makes it available to the end user using dedicated functions like pin or use_i2c.
These functions can return either :
- protocols bound to IOs. This should be the case for any higher level protocol using pins, like
use_i2cin the example. This ensures that the correct protocol implementation is used together with the correct set of pins. - references to IOs. Boards should provide access to
PinIOs objects, like thepinfunction in the example. - references to configured interfaces . Boards can also provide convenient functions to directly configure an IO with the adequate protocol and the related interface. This is the role of the
configure_pinfunction in the example.
Once you get the interfaces you can simply use them as already explained in previous section on this tutorial.
In the end you can see IOBoard specialization as convenient classes for dealing with specific electronic board, nothing more nothing less.