Implementing an extension board
In this tutorial we briefly explain how to create extension boards. We take as example the powerswitch device already shown in the the tutorial on using extension boards.
The process for creating the package providing the extension board is quite similar to the process shown in previous tutorials so we don’t explain it again. Also since an extension board is at the same time a driver and an IOBoard you should have finished tutorials on implementing an IOBoard and implementing drivers. Finally, as we will demonstrate, creating extension boards requires to create board specific procotols, so you should also have finished the tutorial on implementing protocols.
Anatomy of an extension board
Let’s start with the (simplified) declaration of the PowerSwitchBoard class that you can find in the hardio_powerswitch package include/hardio/board/powerswitch.h file:
#include <hardio/core.h>
...
namespace hardio {
class PowerSwitchPPMWrite;
class PowerSwitchAnalogRead;
class PowerSwitchBoard : public ExtensionBoard<ModbusSlaveInterface> {
public:
PowerSwitchBoard(ModbusSlaveInterface& dev);
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);
void power(bool on, bool sync = false);
...
std::vector<std::string> ppm_interfaces() const;
std::vector<std::string> analog_interfaces() const;
private:
friend class PowerSwitchPPMWrite;
friend class PowerSwitchAnalogRead;
void write_ppm(uint16_t target_register, int value);
int analog_read(uint16_t target_register) const;
uint16_t state(bool sync = false) const;
void command(uint16_t reg, bool sync = false);
std::vector<std::reference_wrapper<PPMInterface>> ppm_interfaces_;
std::vector<std::reference_wrapper<AnalogReadInterface>> analog_interfaces_;
ModbusSlaveInterface& modbus_interface_;
std::map<std::string, uint8_t> ppm_to_index_mapping_;
std::map<std::string, uint8_t> analog_to_index_mapping_;
};
} // namespace hardioPowerSwitchBoard extends the ExtensionBoard<ModbusSlaveInterface> base class which means this is an extension board that uses a unique ModbusSlaveInterface to communicate with main board. The ModbusSlaveInterface reference is passed as argument to the constructor in order to be memorized by the modbus_interface_ attribute, exactly the same as when you create a driver.
Since an extension board is an IOBoard it is supposed to provide accessors to its IOs or interfaces. In this case ppm_interface() and analog_interface() functions are these accessors, same as when you create a new IOBoard. Accessors functions provides references to the corresponding interfaces memorized respectively into ppm_interfaces_ and analog_interfaces_ attributes.
What is new here is the forward declaration of protocol classes PowerSwitchPPMWrite and PowerSwitchAnalogRead that are declared friend of the PowerSwitchBoard in order to let them access its private fields and functions. These classes implement the protocols used by the interfaces references returned by the accessor functions. The main role of these protocols is to translate instructions applied to the ppm_interface and analog_interface (i.e. set_pulse() for PPMInterface and analog_read() for AnalogReadInterface) into a modbus slave communication using the modbus_interface_.
Protocols of extension boards
Here is the declaration of these protocols you can find in include/hardio/impl/powerswitch_ppm.h :
#pragma once
#include <hardio/boards/powerswitch.h>
namespace hardio {
class PowerSwitchPPMWrite : public PinPPMWrite {
public:
PowerSwitchPPMWrite(PowerSwitchBoard& ps, uint16_t matching_register);
virtual ~PowerSwitchPPMWrite() = default;
void set_pulse(const std::chrono::microseconds& value) final;
private:
PowerSwitchBoard& pswitch_;
uint16_t register_;
};
}and in include/hardio/impl/powerswitch_analog_read.h
#pragma once
#include <hardio/boards/powerswitch.h>
namespace hardio {
class PowerSwitchAnalogRead : public PinAnalogRead {
public:
PowerSwitchAnalogRead(PowerSwitchBoard& ps, uint16_t matching_register);
virtual ~PowerSwitchAnalogRead() = default;
virtual int analog_read() const final;
private:
PowerSwitchBoard& pswitch_;
uint16_t register_;
};
} // namespace hardioThey are really simple protocol implementation classes, that follow the exact same logic as in tutorial on implementing a protocol.
Only thing to notice here is that these classes old a reference on a PowerSwitchBoard object (pswitch_) that is set at construction time and memorize a register that is an address of a pin on the powerswitch board.
Here is the implementation of the PowerSwitchPPMWrite protocol:
void PowerSwitchPPMWrite::set_pulse(const std::chrono::microseconds& value) {
int32_t ppm_val = 0;
if (value.count() > max_pulse_pose) {
ppm_val = max_pulse_pose;
} else if (value.count() < min_pulse_pose) {
ppm_val = min_pulse_pose;
}
ppm_val = 2 * (value.count() - neutral_pulse_pose);
// value for powerswitch in range -1000, +1000 (0 neutral)
pswitch_.write_ppm(register_, ppm_val);
}The set_pulse() function is quite simple: it first translates the set_pulse() generic argument value into a corresponding valid value for the powerswitch board. Then it asks the board to write this value by doing:
pswitch_.write_ppm(register_, ppm_val);The logic is the same for analog_read() function.
So in the end the biggest part of the protocol implementation is made in the PowerSwitchBoard that implements the correspoding write_ppm() and analog_read() functions.
Implementation of the extension board
Here is what the PowerSwitchBoard constructor looks like:
PowerSwitchBoard::PowerSwitchBoard(ModbusSlaveInterface& dev)
: ExtensionBoard<ModbusSlaveInterface>({dev}),
ppm_interfaces_{
add_io<PinIO>("PPM1", PinIO::PPM_OUT)
.bind<PowerSwitchPPMWrite>(*this, PPM1_ADDR)
.add_interface(),
add_io<PinIO>("PPM2", PinIO::PPM_OUT)
.bind<PowerSwitchPPMWrite>(*this, PPM2_ADDR)
.add_interface(),
add_io<PinIO>("PPM3", PinIO::PPM_OUT)
.bind<PowerSwitchPPMWrite>(*this, PPM3_ADDR)
.add_interface(),
...
},
analog_interfaces_{add_io<PinIO>("LINK1", PinIO::ANALOG_IN)
.bind<PowerSwitchAnalogRead>(
*this, LINK1_ADDR)
.add_interface(),
...
}
modbus_interface_{access<ModbusSlaveInterface>(0)},
ppm_to_index_mapping_{ {"PPM1", 0}, {"PPM2", 1}, {"PPM3", 2},
{"PPM4", 3}, {"PPM5", 4}, {"PPM6", 5} },
analog_to_index_mapping_{
{"LINK1", 0}, {"LINK2", 1}, {"ANA1", 2}, {"ANA2", 3} } {
// 8 fist registers of the board are read only
modbus_interface_.define_registers(1, {
hardio::modbus::READ,
hardio::modbus::READ,
hardio::modbus::READ,
hardio::modbus::READ,
hardio::modbus::READ,
hardio::modbus::READ,
hardio::modbus::READ,
hardio::modbus::READ,
hardio::modbus::RD_WR,
hardio::modbus::RD_WR,
hardio::modbus::RD_WR,
hardio::modbus::RD_WR,
hardio::modbus::RD_WR,
hardio::modbus::RD_WR,
hardio::modbus::RD_WR,
hardio::modbus::RD_WR,
});
modbus_interface_.initialized(
[this] {
modbus_interface_.cmd_write_reg16(9, 8, PWRS_MODBUS_STD_TIMOUT);
std::this_thread::sleep_for(PWRS_MODBUS_T_INTERTRAMES);
modbus_interface_.cmd_read_reg16(1, 16, PWRS_MODBUS_STD_TIMOUT);
std::this_thread::sleep_for(PWRS_MODBUS_T_INTERTRAMES);
},
[this]() -> bool {
return modbus_interface_.correct_frames_percentage() >= 50.0;
});
}As for any IOBoard the first part consists in defining IOs using the add_io() function. Here the powserswitch provides 6 PPM pins and 4 analog in pins, so we declare corresponding IOs with protocols previously defined. The important point here is that each protocol is configured with a unique register address (e.g. PPM1_ADDR) that will be used to know where data will be read from/written to.
The reference to interface used to communicate with the extension board is memorized. It is retrived using access<ModbusSlaveInterface>(0) utility functions of ExtensionBoard class.
Then we need to memorize mapping betwwen IOs names and their index the interfaces vector, in order to have fast access to the interface when using their name (see ppm_to_index_mapping_ and analog_to_index_mapping_).
The powerswitch is a modbus slave: it defines a set of register to read from/write to. These registers will be set or read during the communication with the modbus slave (executed in the powerswitch) and the modbus master (executed on the main board).
We also need to configure the way the modbus master communicates with the slave (this later being the physical powerswitch device) using the initialized() function. Basically it consists in defining the update loop of the modbus slave (setting value of RD_WR slave registers then reading value from slave READ and RD_WR registers).
So now all this description has been done the job consists in implementing the two functions write_ppm() and analog_read():
void PowerSwitchBoard::write_ppm(uint16_t target_register, int value) {
modbus_interface_.write_register16(target_register, value, false);
}
int PowerSwitchBoard::analog_read(uint16_t target_register) const {
uint16_t value;
modbus_interface_.read_register16(target_register, value, false);
return value;
}Writing to a PPM interface simply consists in writing the value into the modbus register corresponding to this interface. This is achieved calling modbus interface write_register16() funciton with the correct register address.
Reading an analog value simply consists in reading the value into the modbus register corresponding to this interface. This is achieved calling modbus interface read_register16() funciton with the correct register address.
Now the PowerSwitchBoard has been implementd it can be used as explained in this tutorial: the pins interfaces it provides can be used as if they were provided by main board, the communication through modbus bus is transparent for user code (typically code of a device driver).
Conclusion
As a final note you can see in following code that modbus register can also be used to do functionnal things, like for the power function (here using set_bits_register16() and clear_bits_register16() modbus interface functions):
void PowerSwitchBoard::power(bool on, bool sync) {
if (on) {
modbus_interface_.set_bits_register16(CMD_ADDR, PWSBIT_Power, sync);
} else {
modbus_interface_.clear_bits_register16(CMD_ADDR, PWSBIT_Power, sync);
}
}Each extension board has its own logic and way to communicate so it is hard to completely generalize the process, but this tutorial gave you the general idea.