I have been spending quite a bit of time recently researching the "best" (read most elegant. robust, simple to use, and resource friendly) ways to develop low-level hardware abstractions for embedded systems in C++. My current goal is to come up with some way to represent, configure, and interact with Port Pins.
Here are some great references I have found:
- https://www.feabhas.com/sites/default/files/uploads/EmbeddedWisdom/Feabhas%20Modern%20C%2B%2B%20white%20paper%20Making%20things%20do%20stuff.pdf
- https://www.springer.com/gp/book/9783662585931
Although all of the methods found in these resources are good, I wanted some sort of "hybrid" that allows me to:
- Represent a hardware peripherals as an "instance" of a class (e.g. A typical MCU has multiple instances of GPIO ports)
- Refer to these instances in a lightweight manner within a "configuration" file for the project (i.e. a board support package)
- Provide some way to represent a configuration of this peripheral
- Provide a way to "map" a configuration onto a peripheral
Based upon these requirements, I have come up with a method that works and seems "good" to me. My question is, based on other people's experiences, is what I have written a "good" way to go about this?
#include <iostream>
/**
* @brief Describes what port to use
*/
class PortDescriptor {
public:
constexpr PortDescriptor(unsigned int* p) : _p(p) {}
inline unsigned int* ptr() const { return _p; }
private:
unsigned int* _p;
};
/**
* @brief Describes what pin within a port to use
*/
class PinDescriptor {
public:
constexpr PinDescriptor(const PortDescriptor& port, unsigned int pin) :
_port(port),
_pin(pin)
{
}
inline const PortDescriptor& port() const { return _port; }
inline unsigned int index() const { return _pin; }
private:
const PortDescriptor& _port;
unsigned int _pin;
};
/**
* @brief Describes the configuration for a portpin
*/
class PinConfiguration {
public:
enum class Mode {
INPUT,
OUTPUT,
ANALOG
};
enum class Pull {
NONE,
UP,
DOWN
};
//Default constructor
constexpr PinConfiguration() :
_mode(Mode::OUTPUT),
_openDrain(false),
_pull(Pull::NONE)
{
}
//Builder methods
constexpr PinConfiguration& setMode(Mode v) { _mode = v; return *this; }
constexpr PinConfiguration& setOpenDrain(bool v) { _openDrain = v; return *this; }
constexpr PinConfiguration& setPull(Pull v) { _pull = v; return *this; }
//Accessors
inline constexpr Mode mode() const { return _mode; }
inline constexpr bool openDrain() const { return _openDrain; }
inline constexpr Pull pull() const { return _pull; }
private:
Mode _mode;
bool _openDrain;
Pull _pull;
};
/**
* @brief Class which represents a port peripheral within the MCU
*
* "Class overlay" will be used to map this class onto the actual hardware
* memory address.
*/
class Port {
public:
void configurePin(const PinDescriptor& pin, const PinConfiguration& config) {
std::cout << "port=" << pin.port().ptr() << " pin=" << pin.index() << std::endl;
//Example code
_modeRegister |= static_cast<unsigned int>(config.mode()) << (pin.index() * 2);
_drainRegister |= static_cast<unsigned int>(config.openDrain()) << (pin.index());
_pullRegister |= static_cast<unsigned int>(config.pull()) << (pin.index() * 2);
}
static Port* get(const PortDescriptor& p) { return reinterpret_cast<Port*>(p.ptr()); }
static Port* get(const PinDescriptor& p) { return reinterpret_cast<Port*>(p.port().ptr()); }
private:
unsigned int _modeRegister;
unsigned int _drainRegister;
unsigned int _pullRegister;
};
//------------------------------------------------------------------------------
//Fake peripheral registers (would normally be actual hardware registers)
unsigned int porta_registers[3];
unsigned int portb_registers[3];
//Available ports
static constexpr PortDescriptor PORTA(porta_registers);
static constexpr PortDescriptor PORTB(portb_registers);
//Available pins
static constexpr PinDescriptor PA0(PORTA, 0);
static constexpr PinDescriptor PA1(PORTA, 1);
static constexpr PinDescriptor PA2(PORTA, 2);
static constexpr PinDescriptor PB0(PORTB, 0);
static constexpr PinDescriptor PB1(PORTB, 1);
static constexpr PinDescriptor PB2(PORTB, 2);
//------------------------------------------------------------------------------
//Example configuration file for a project making use of the above HAL
static constexpr PinDescriptor RED_LED_PIN = PA0;
static constexpr PinConfiguration RED_LED_PIN_CONFIG = PinConfiguration().setMode(PinConfiguration::Mode::OUTPUT);
static constexpr PinDescriptor GREEN_LED_PIN = PB2;
static constexpr PinConfiguration GREEN_LED_PIN_CONFIG = PinConfiguration().setMode(PinConfiguration::Mode::OUTPUT).setOpenDrain(true);
int main() {
std::cout << "TEST 1" << std::endl;
std::cout << "porta_registers[0]=" << porta_registers[0] << std::endl;
std::cout << "porta_registers[1]=" << porta_registers[1] << std::endl;
std::cout << "porta_registers[2]=" << porta_registers[2] << std::endl;
Port::get(RED_LED_PIN)->configurePin(RED_LED_PIN, RED_LED_PIN_CONFIG);
std::cout << "porta_registers[0]=" << porta_registers[0] << std::endl;
std::cout << "porta_registers[1]=" << porta_registers[1] << std::endl;
std::cout << "porta_registers[2]=" << porta_registers[2] << std::endl;
std::cout << "TEST 2" << std::endl;
std::cout << "portb_registers[0]=" << portb_registers[0] << std::endl;
std::cout << "portb_registers[1]=" << portb_registers[1] << std::endl;
std::cout << "portb_registers[2]=" << portb_registers[2] << std::endl;
Port::get(GREEN_LED_PIN)->configurePin(GREEN_LED_PIN, GREEN_LED_PIN_CONFIG);
std::cout << "portb_registers[0]=" << portb_registers[0] << std::endl;
std::cout << "portb_registers[1]=" << portb_registers[1] << std::endl;
std::cout << "portb_registers[2]=" << portb_registers[2] << std::endl;
return 0;
}
The above example does NOT represent real hardware code but hopefully gives you the generally idea.
Thank you in advance for your input!