-1

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:

Although all of the methods found in these resources are good, I wanted some sort of "hybrid" that allows me to:

  1. Represent a hardware peripherals as an "instance" of a class (e.g. A typical MCU has multiple instances of GPIO ports)
  2. Refer to these instances in a lightweight manner within a "configuration" file for the project (i.e. a board support package)
  3. Provide some way to represent a configuration of this peripheral
  4. 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!

1
  • 2
    There is no generalized "best", only different trade-offs that are better or worse for a given application Commented Aug 17, 2021 at 22:04

1 Answer 1

1

Are you trying to design a framework for embedded development? I would start from the top and work down. That is to say, start by thinking in terms of the needs of application code, not by thinking about the capabilities of any particular microprocessor.

I've gotten a lot of mileage in the past by starting at the top with a few interfaces. E.g.;

class DigitalOutput {
public:
    virtual ~DigitalOutput();
    virtual void set() = 0;
    virtual void clear() = 0;
    virtual boolean get() = 0;
};

class DigitalInput {
public:
    virtual ~DigitalInput();
    virtual boolean get() = 0;
};

class AnalogOutput {
public:
    ...
};

...

All of whatever framework you provide that implements such interfaces can be aware of the capabilities of any given microprocessor. All of the application code that depends on such interfaces can be blissfully ignorant of the hardware that it runs on. The interface helps you to keep those two things separate from each other, and it helps a lot when the application developer wants to test the "blissfuly ignorant" part of the code.*


* Hopefully, that's most of the application code.

Not the answer you're looking for? Browse other questions tagged or ask your own question.