Drivers in hardio are implemented as simple C++ classes, the framework defines no strict guideline that tells how to write drivers code.

Nevertheless we adopt some conventions for structuring code and packages meta-data in order to ease the use of drivers.

Conventions

Any driver or driver familly is implemented in a dedicated PID package which follows this convention:

  • The name of the package is hardio_<driver familly name>. For instance there is a package hardio_br_ping that regroups drivers for Blue Robotics echosounders. The package is added to the hardio framework, so hardio_br_ping root CMakeListst.txt looks like:
cmake_minimum_required(VERSION 3.19.8)
...
project(hardio_br_ping)

PID_Package(
	...
)
...
PID_Dependency(hardiocore VERSION 2.0)
...

PID_Publishing(PROJECT https://gite.lirmm.fr/hardio/devices/hardio_br_ping
    FRAMEWORK 	hardio
	...
)

build_PID_Package()
  • The package defines a library with name <driver familly name>. So package hardio_br_ping defines the br_ping component:
PID_Component(br_ping
    CXX_STANDARD 17
    C_STANDARD 99
    EXPORT  hardio/core
            ...
)
  • This library defines a root public header under the hardio folder that includes all its public headers and whose name is the name of the library. So hardio_br_ping provides a header file hardio/br_ping.h that includes all drivers definitions.

So now suppose you have a project that needs to use hardio_br_ping you will have to:

  • add the dependency in your project root CMakeLists.txt:
cmake_minimum_required(VERSION 3.19.8)
...
project(your_project)

PID_Package(
	...
)
...
PID_Dependency(hardiocore VERSION 2.0)
PID_Dependency(hardio_br_ping VERSION 1.0)
...

build_PID_Package()
  • for your application add the dependency to the library in your project apps/CMakeLists.txt:
PID_Component(my_super_app
    CXX_STANDARD 17
    C_STANDARD 99
    DEPEND  hardio/core
            hardio/br_ping
)
  • and finally include the header in your application code:
#include <hardio/br_ping.h>
int main(){
    //use BRPing driver !!
	...
}

RPC interfaces driver pattern

Most of the drivers in hardio follow the coding pattern proposed by the rpc-interfaces package, this is the case for hardio_br_ping. A detailed explanation of this pattern can be found in this tutiral. This pattern allows to have homogeneous management of drivers as well as standardized interfaces.

To sump up the idea each driver of driver familly is split into 2 main components:

  • a device, that represents the electronic device and is mainly used to model the current state/commands of the device.
  • a driver, configured for a given device, that enable to synchronize device object with its electronic counterpart. This is achieve through read (update device object state) and write (update electronic device state, notably used to send parameters and commands).

To follow this pattern the rpc-interfaces package must be used, in hardio_br_ping’s root CMakeListst.txt the dependeny is added:

...
PID_Dependency(hardiocore VERSION 2.0)
PID_Dependency(rpc-interfaces VERSION 1.2)#using RPC pattern !!
...

And the corresponding library must be used as well by br_ping library:

PID_Component(br_ping
    CXX_STANDARD 17
    C_STANDARD 99
    EXPORT  hardio/core
            rpc/interfaces
)

The resulting driver defined by br_ping library is so divided into two classes:

  • the device description:
class BRPing1dDriver;
class BRPing1D : public rpc::dev::Echosounder<>, public rpc::dev::Sonar<> {
public:
    struct Setting {
        bool measurement_active;
        // NOTE: default velocity in fresh water
        phyq::Velocity<> speed_of_sound = phyq::Velocity<>(1481.);
        phyq::Duration<> period;
    };

    Setting& settings() {
        return settings_;
    }
    const Setting& settings() const {
        return settings_;
    }

    const rpc::data::Confidence<>& confidence() const {
        return confidence_;
    }

private:
    Setting settings_;
    rpc::data::Confidence<> confidence_;
    friend class BRPing1dDriver;
};

The device inherits rpc-interfaces classes to follow RPC code standardization convention as much as possible. This way the device can be viewed by user code as a standardized rpc::Echosounder and/or and rpc::Sonar.

Apart from that it is a normal C++ class.

  • and the driver:
class BRPing1dDriver : public rpc::Driver<BRPing1D, rpc::AsynchronousIO> {
public:
    BRPing1dDriver(SerialInterface& interface, BRPing1D* device);
    ~BRPing1dDriver();
	...

private:
    bool connect_to_device() final;
    bool disconnect_from_device() final;
    bool read_from_device() final;
    bool write_to_device() final;
    rpc::AsynchronousProcess::Status async_process() final;
    ...
};

By inheriting rpc::Driver template class specialization BRPing1dDriver becomes a RPC driver and so has to define a set of functions (those marked final). The important part is the driver constructor:

  • it takes an interface as argument, this will be explained in next section.
  • it takes a pointer on the BRPing1D device. This is the device object that represents the electronic device the driver will synchronize with.

Once constructed, the end the user code can call connect, disconnect, read and write functions in order to manage synchronization with electronic device. Something like:

BRPing1D ping;
BRPing1dDriver ping_driver(interface, &ping);

ping.setting().measurement_active=true;
ping_driver.connect();
ping_driver.write();

while(true){

	ping_driver.read();//update state
	fmt::print("distance: {}\n", ping.distance());
	//cycle...
}

ping_driver.disconnect();

Device is created and we set the parameters to activate mearsurement using the settings function. The measurement becomes active after synchronizing with electronic device using the driver write() method. Then we can read distance from electronic device by using the read() method and finally print the corresponding value located in the synchronized ping device.

Any hardio driver following the RPC pattern (and again this is the case for most of them) can be used following the exact same logic. What you need to understand finally is the specific device content (states you can read, parameters an commands you can set), which is of course device dependent.

configuring drivers

Finally we need to talk about the hardio specific part of drivers, which is in fact really limited and straightforward. The only thing to understand is that a driver uses one (or eventually more) interface(s).

Let’s see a more complete code explaining that from previous ping driver:

IOBoard board;
//define the serial interface
auto& serial =
    board.add_io<SerialBus>("/dev/ttyS0")
		 .bind<PosixSerialProtocol>(115200)
		 .add_interface();

BRPing1D ping;
//use the serial interface
BRPing1dDriver ping_driver(serial, &ping);
//do things with the driver
...

That’s it ! Following the previous tutorial you know how to define interfaces to communicate with devices. So we create a serial interface and then simply pass the interface as argument to the BRPing1dDriver constructor. From user perspective there is no much more to understand, except that the serial interface is used internally by the driver to implement connect_to_device/disconnect_from_device/read_from_device/write_to_device/async_process operations. Those operations are in turn automatically called by read/write/connect/disconnect public functions of the driver.

One benefit of using hardio is that the drivers are implemented using interfaces, so they do not care about how the serial communication is implemented, in other words they don’t care about the protocol implementation. This allows to quickly adapt the driver for completely different boards or implementations. For instance if we consider using an upboard like in previous tutorial we could then use the same driver, the same way but configured with a completely different implementation:

Upboard board;
//define the serial interface
MRAAI2CProtocol& serial_protocol = board.use_serial(115200);
auto& serial =serial_protocol.add_interface();

BRPing1D ping;
//use the serial interface
BRPing1dDriver ping_driver(serial, &ping);
//do things with the driver
...

Now the interface and implementation have changed but the driver code as well as the application code can remain exactly the same. This demonstrates the main benefit of hardio, that is the hardware adaptation. This is a partcularly interesting aspect in the fast innovating world of embedded systems, to adapt existing code to new boards and protocols implementation.

Using extension boards

The last new concept that has never been discussed until now is the possibility to describe extension boards. The concept of ExtensionBoard generalizes any type of electronic board that can be plug into a main board to provide new capabilities. Basically you can think of embedded boards’s extensions called hats/capes/shields: all these terms refer to extension boards that can be “plugged” onto the main board to provide to the board user new capabilities. To do that the main board communicate with the extension board using dedicated communication interfaces (USB/serial or pins most of time for classical extension boards).

Provided capabilities are generally of two types:

  • new functionnalities, which means that the extension board can be considered as a driver.
  • new IOS, which means that the extension board can be considered as an IOBoard.

If the extension board only provides new functionnalities then in hardio we simply implement it as a normal driver, you can so refer to the previous section to know how to manage such extension boards. But if the extension board provides new IOs then it is considered as an ExtensionBoard.

As an example of ExtensionBoard we use the PowerSwitchBoard, which is defined in the hardio_powerswitch package. The powerswitch is an electronic device communicating as a modbus slave that:

  • provides PPM pinIO, classically used to control motors ESC.
  • provides analog pinIO used to read analog signals.
  • provides additionnal functionnalitites to read power supply voltage and current, and to power on/off each ESC.

Here is a sum up of the PowerSwitchBoard interface:

class PowerSwitchBoard : public ExtensionBoard<ModbusSlaveInterface> {
public:
    PowerSwitchBoard(ModbusSlaveInterface& dev);

	//provided hardio interfaces
    PPMInterface& ppm_interface(uint8_t pin_id);
    PPMInterface& ppm_interface(const std::string& pin_name);
    AnalogReadInterface& analog_interface(uint8_t pin_id);
    AnalogReadInterface& analog_interface(const std::string& pin_name);

    std::vector<std::string> ppm_interfaces() const;
    std::vector<std::string> analog_interfaces() const;

	//specific functionnalities
    void power(bool on, bool sync = false);
    phyq::Voltage<> battery_voltage(bool sync = false) const;
    phyq::Current<> battery_current(bool sync = false) const;
	...
};

The PowerSwitchBoard class that inherits the ExtensionBoard template specialization whose template parameter is the type of the interface through which new IOs will be available to the main board. It defines a constructor that takes as parameter an interface like any other hardio driver. Here it needs a ModbusSlaveInterface.

Then like the Upboard in previous tutorial it provides a set of functions to access its IOs, configure and bind them with the adequate protocol from which an interface is automatically created :

  • ppm_interface to access PPM PinIOs using a PPMInterface.
  • analog_interface to access its Analog input PinIO using an AnalogReadInterface.

It also provides additionnal functionnality to read power supply voltage/current and notably a power function used to power on/off the electronic devices controlled by its PPM interfaces.

Nothing really new until now if you simply consider that an extension board mix the concepts of IOBoard and driver : it uses an interface like a driver to implement its functionnalities but it also provides new IOs/interfaces that can be transparently accesses through this interface. What is not shown here is that PowerSwitchBoard provides an implementation for two protocols : PinPPMWrite and PinAnalogRead. This implementation uses the ModbusSlaveInterface to let the mainboard access those new interfaces. This aspect is beyond the scope of this tutorial but just remember that an extension board is indeed an IOBoard.

Now considering the previous PowerSwitchBoard declaration we can use it this way:

#include <hardio/powerswitch.h>
#include <hardio/core.h>
#include <hardio/linux.h>
using namespace std::chrono_literals;
using namespace hardio;
int main() {
    IOBoard main_board;
    auto& modbus = main_board.add_io<SerialBus>("/dev/ttyS0")
                       .bind<PosixSerialProtocol>(115200)
                       .add_interface()
					   .bind<ModbusOverSerialProtocolMaster>();

	// declare a powerswitch board
    auto pswitch = PowerSwitchBoard(
        modbus.add_interface(7) // modbus slave address 7
    );

    // access PPM interfaces for controlling actuators
    auto& interface1 = pswitch.ppm_interface(0);
    auto& interface2 = pswitch.ppm_interface(1);
    auto& interface3 = pswitch.ppm_interface(2);
    auto& interface4 = pswitch.ppm_interface(3);

    main_board.initialize();

	// start using motors
    pswitch.power(true, true);

	//send PPM pulses to ESC
	interface1.set_pulse(200us);
    interface2.set_pulse(200us);
    interface3.set_pulse(200us);
    interface4.set_pulse(200us);

	std::this_thread::sleep_for(2s);

	 // stop ESC
    interface1.set_pulse(0us);
    interface2.set_pulse(0us);
    interface3.set_pulse(0us);
    interface4.set_pulse(0us);

	std::this_thread::sleep_for(2s);

    pswitch.power(false, true);
}

There is nothing really special regarding the first part declaring the interfaces needed by the powerswitch : like any driver class we pass as argument the interface ( modbus.add_interface(7)) to the PowerSwitchBoard constructor.

We use the PowerSwitchBoard object ppm_interface function to access the interfaces connected to ESCs. We then use the power function to power on the ESC prior to send them pulses to control the motors velocity/force during 2 seconds before using same function to stop motors and finally again using power to switch off ESCs.

You can note that the PPMInterface object can in turn be used into a driver to control the corresponding ESC. So the PowerSwitchBoard object really extends the hardware architecture by providing new IOs/interfaces that can be transparently used by driver as if the PPMInterfaces were directly provided by the main board.

Conclusion: using other drivers and extensions boards

We have now covered all the general principles when using hardio framework.

It is impossible to cover usage of all kind of drivers and extension boards in a tutorial, so we recommend to use API doc example and package specific pages that you can find in this site to learn more about specific package. To navigate in this site, use the left panel, it is structured in following categories:

  • ioboard and its subcategories contains packages providing new IOBoards.
  • protocol and its subcategories contains packages providing new Protocols implementations or even completely new Protocols and their corresponding interfaces.
  • protocol and its subcategories contains packages providing new device drivers.
  • examples contains packages providing application examples.

Please note that a package can belong to more than one (sub)categories, depending on its content.