What is the best way to handle GPIO in a library?

MBED GPIO appears very simple on the surface, first create a DigitalOut object with a constructor, argument being the preferred pin. Thenafter, simply treat the instance like a variable. looking at the .h implementation, looks like the “&operator= (int value)” which is kind of cool.

but how can I use GPIO inside of a class? Such that in main where the instance of said class is defined, the constructor will accept PinName and will toggle the GPIO as the library needs to. what is the “right” way to do this? do I have to play to implement some pointer magic? sorry I’m a bit rusty on my C++, haven’t touched it since college.

Take the following example:

#include "mbed.h"

#ifndef wait_ms
#define wait_ms(i) thread_sleep_for(i);
#endif

static BufferedSerial pc(PA_9, PA_10, 115200);

// connect serial_port to the output stream of printf (console)
FileHandle *mbed::mbed_override_console(int fd) { return &pc; }

class Foop {
    public:
    DigitalOut _a;
    Foop(PinName name) {
        _a(name);
    }
    void ledHigh() {
        _a = 1;
    }
    void ledLow() {
        _a = 0;
    }
};

int main() {
    Foop led1(LED1);
    while (true) {
        led1.ledHigh();
        wait_ms(100);
        led1.ledLow();
        wait_ms(100);
        printf("blinky example running on Mbed OS %d.%d.%d.\n", MBED_MAJOR_VERSION, MBED_MINOR_VERSION, MBED_PATCH_VERSION);
    }
}

This was my initial idea, but the problem is there is no default constructor defined for DigtalOut class. So then my next idea was to play some shenanigans with pointers like this:

class Foop {
    public:
    DigitalOut *a;
    Foop( DigitalOut * ptr ) {
        a = ptr;
    }
    void ledHigh(){
        (*a)=1;
    }
    void ledLow(){
        (*a)=0;
    }
};

int main() {   
    DigitalOut ledpin(LED1);
    Foop led1( &ledpin );
    while (true) {
        led1.ledHigh();
        wait_ms(50);
        led1.ledLow();
        wait_ms(50);
        printf(" blinky example running on Mbed OS %d.%d.%d.\n", MBED_MAJOR_VERSION, MBED_MINOR_VERSION, MBED_PATCH_VERSION);
    }
} 

This does work, and does so by passing a DigitalOut pointer to the class, which manipulates it. This way I can choose the pin to use within main.cpp. and have the library work with any pin. However this seems like the wrong way to do it. Is there a way set it up so that I just pass the pinName into it directly rather than define it in main separately? what’s the right way to do this?

How efficient is this method compared to Arduino digitalWrite? (which is notoriously slow given it has to convert an abstract pin name to port and register and mask) why does the assignment return *this? Is it to do with the fancy implementation that allows me to treat it like a variable?

This question does also bring up more philosophical question:

is it better to hard-code hardware-based peripheral and gpio within the libraries for things like certain breakout boards (IMU’s, sensors, displays, motor drivers, etc) or is it better to do the other extreme, which is to have hardware interface functions (for flipping pin states and controlling serial data lines) be functions with no definition until the instance is defined?

I actually have done the latter for one project in arduino, where I created a library that only had the helper functions to interface a particular chip, but was completly agnostic of the board it was running on, and even agnostic to the platform framework! simply because the low-level functions to flip GPIO pins and interface the SPI were nothing more than lamda function pointers with no defined implemention, which had to be defined in main when the instance was created. It was a group project and no one else liked the idea lol.

the problem is there is no default constructor defined for DigtalOut class

You can use initializer list to avoid calling a default constructor.

class Foop {
    public:
    DigitalOut _a;
    Foop(PinName name): _a(name) {} // <- 
    // ...
};
1 Like

Initializer lists… never heard of those before! That’s pretty cool, and is exactly what I needed to fix the code. thank you!

1 Like

implementing this solution, I found another potential issue. I am porting the Bolder Flight Systems code for the MPU9250 (it’s obsolete but I have a bunch of these on breakout boards) I found that the _CsPin is always defined, and the low-level functions simply check a variable whether to use SPI or I2C. I replaced the uint8_t type with DigitalOut and used the intitalizer-list for the SPI constructor. However since this object is not initialized with the I2C constructor is used a compilation error is thrown. since it doesn’t make sense to reserve an unused pin, will I need to use dynamic allocation (new, malloc) for this to work, or rework the library since I don’t need to change between I2C and SPI during runtime?

So, your class takes either SPI or I2C? I guess you can use dynamic allocation?

class Sensor {
private:
	enum Mode {
		spi,
		i2c,
	};

	Mode _mode;
	SPI *_spi;
	DigitalOut *_cs;
    I2C *_i2c;

public:	
    Sensor(PinName mosi, PinName miso, PinName sck, PinName cs) {
    	_mode = Mode::spi;
		_spi = new SPI(mosi, miso, sck);
		_cs = new DigitalOut(cs);
    }

    Sensor(PinName sda, PinName scl) {
    	_mode = Mode::i2c;
		_i2c = new I2C(sda, scl);
    }

    void write() {
    	switch (_mode) {
    	case Mode::spi:
    		_cs->write(0);
	        _spi->write('A');
	        _cs->write(1);
    		break;
    	case Mode::i2c:
    		// define I2C write
    		break;
    	default:
    		break;
        }
    }
};

int main() {

	Sensor sensor(p5, p6, p7, p8); // SPI
	// Sensor sensor(p28, p27);    // I2C

    while (true) {
    	sensor.write();
        wait_ms(1000);
    }
}

This is a solution I did consider, I was hoping to avoid using new for the _cs pin (people tell me I shouldn’t use dynamic memory allocation on microcontrollers and be careful if I do… although frankly it’s not like I am creating and destroying these classes willy-nilly during runtime. thoughts?)

Here, you create an SPI object within the class that’s “only” meant to initialize and configure the IC, then convert raw binary register values into useful values of acceleration, rotational rate, and magnetic flux density, etc (values which are later fed into a filter and fusion library which generates an estimate of attitude). But what if other libraries need to interface sensors on the same bus? Would it make sense to factor out the SPI and handle this in main? this is what I am doing as of now. This way I create only a single SPI instance and have that passed in to multiple libraries.

If I later choose to make this into an RTOS, a concern I would have is sharing this SPI resource. I added lock() and unlock() to the methods responsible for performing SPI transactions. but I don’t know what these calls actually do, and if they make this library thread-safe. My assumption is when lock() method is called, the OS checks to see if another thread currently has a lock on the SPI resource it blocks and moves on to it or another thread if available and basically waits until the resource if freed?

Yes, you can share the bus. You just need to make sure there are no conflicts.