Working with Raspberry PI GPIO - Do I need a virtual / abstract / interface?

Introduction

I work with raspberries PI GPIO. So far, I have written code in the same way as in C, using functions to group sections of code.

My work went so far that I am happy that everything works, but now everything is starting to get confused, and therefore I would like to move on to an object-oriented approach.

Problem

Here is the problem I am facing.

At the moment, I have a class that represents my "device". (The hardware I built that is connected to the GPIO port.) The hardware has 2 separate partitions. One section is the "input" section, and the other is the "exit".

To help you better understand this, the input section is an ADC. (An analog-to-digital converter.) This is a device that converts an analog signal into a 10-bit binary representation. (If you are not familiar with electronics.)

The output section is simply a transistor that turns a set of LEDs on and off.

I wanted to have a class that represented the ADC Board, and one that represented the LED board, because they are two conceptually different devices, because they are not connected to each other.

This causes a problem, since the GPIO pins must be set to certain values ​​before their modes are set. By values ​​I mean "HIGH" or "LOW", 1 or 0. By modes, I mean "INPUT" or "OUTPUT". This sounds strange, but basically, the ADC will desynchronize if the control lines are not set to their correct LOGIC HIGH and LOGIC LOW values ​​before it is turned on. (This is a very strange device, it does not turn on until it is told, even if it is connected to a power source (VCC or VDD 5.0V). One of the control lines sends a signal to turn on the device.

To accomplish the above, make sure the GPIO pins are initially in INPUT mode. To “make ADC work correctly,” we first set the data values ​​(HIGH / LOW) that must be present on the pins before they are changed to OUTPUT mode. Thus, when the mode changes from INPUT to OUTPUT, data with the correct values ​​is present, and we will not violate the ADC.

My initial idea was to have a constructor for the ADC, which first sets the values ​​for the output, and then changes the required outputs from INPUT mode to OUTPUT mode. But this forces us to build the ADC Board class to the LED Board class.

This can be fixed with both constructors executing the same code to set the output modes, but this seems like a bad idea because we call two bits of code twice - not a very elegant solution.

Another solution is to have a GPIOPort class that combines input and output devices together, but that is also not very elegant, and it would be difficult to change if we ever added a second, identical LED board. (For instance.)

What I think I want, but I'm not sure ...

I think I want another class that represents GPIOPort itself. (In my opinion, is this an abstract idea?) Then I think that I want the "class inside the class" to represent the ADC Board and the "class in the class" to represent the LED Board. I don’t remember what this method is called, but usually the "outer class" looks like a wrapper with a pointer to an object of the type "inner class" and the create method and destroy method. The outer class does something like pointer = new type; in the create method and delete pointer in the destroy method. This allows you to call the constructor if necessary, and the class destructor will be called if necessary.

The fact is that the constructor of the GPIOPort class handles the order in which these objects are created, which hides everything from main (). Basically, a programmer just does something like GPIOPort myGPIOPort; and this handles everything you need, so you don’t need to include 20 lines of code in main () to set up data for output contacts, which is the only other solution. (What I did not mention above).

Questions

So my first question is that this method is known? I thought this was called a wrapper class, but my understanding is a wrapper class for using such fundamental types as double and int . (And adding methods like clear() or reset() or something like that.) Is this what I really want to do, or is there a better method? (I guess this boils down to "how do I fix my problem.")

My second question is that, as far as I remember, I have to use some of the methods (destructor?) Of the virtual methods, but I can’t remember why. (Or maybe I'm not doing this, and I'm just embarrassed.)

My third question is: are there any examples of this that I can use to help myself understand this, or, alternatively, where I can improve my understanding. (Resources).

Thanks, obviously this is a pretty long question. I tried to include as much information as possible to help explain the situation. If you want to clarify, I will try to improve what I said.

Edit: More device information

Data must be sent to the GPIO pins before their mode is changed from input to output.

The GPIO pins look like all zeros, since the pins have push-pull resistors and they are still set as inputs. Data that was sent is not displayed until its mode is changed.

Then the contacts are output. (Or some of them anyway.) Now the data that was sent is displayed on the contacts.

If the pins are set to output mode before sending data, we cannot prevent the ADC from turning on, since the data pin that controls the ADC can be turned on to HIGH. It can be set to LOW, but there is no way to tell its status is undefined until we tell GPIO what values ​​we want before setting the output mode. Fortunately, we can guarantee that all contacts will be in input mode.

+6
source share
3 answers

Some definitions:

  • Device: all device connected to GPIO (input and output)
  • LEDBoard: output parts of the device
  • ADCBoard: device inputs

I highly recommend using more than one single. On some day, you can connect the second device to other GPIO pins, and you will have problems.

If you are creating separate classes for LEDBoard and ADCBoard, you should ask: "What do I need to create a LEDBoard / ADCBoard?" Well ... you need a device!

So my design will be as follows:

 struct DeviceDescriptor { int portNumber; // add additional variables to identify the device } class Device { Device(DeviceDescriptor descriptor) { //Insert your initialization... //You can maintain a static vector of already opened Devices to throw an //error if an allready opened device is reopened } ~Device() { //Deinit device } // A device should not be copyable Device(const& Device) = delete; //TODO do the same for copy assignment //TODO implement move ctr and move assignment operator //TODO add needed mamber variables } class LEDBoard { LEDBoard(std::shared_ptr<Device> device) : m_device(device) { //Do init stuff } //Your functions private: std::shared_ptr<Device> m_device; } //ADCBoard is analog to LEDBoard 

You can use classes as follows:

 int main(void) { auto device = std::make_shared<Device>(DeviceDescriptor()); LEDBoard led1(device); ADCBoard adc1(device); //Your stuff... } 

Benefits:

  • You may have multiple devices
  • More LEDBoards can be easily added
  • When using shared_ptr, the device object will be destroyed when the last LEDBoard / ADCBoard is destroyed.
+3
source

Edit: I am doing a lot of editing this post because I believe that this is currently not the best solution. This edit really looks like @MarkusMayer answer, but I came up with a completely different way of thinking (I think), so maybe this will help you.

First, let's define the GPIO output, which can be anything, anything (the class will be good, then you can do pin.setOutput() or pin.set() , etc.). I let you define it the way you want, just suppose we have the GPIOPin class.

Firstly, I define an abstract board as a set of pins that look quite correct for me:

 template <int N> class Board { protected: Board (std::array <GPIOPin, N> const& pins) : _pins(pins) { } std::array <GPIOPin, N> _pins ; }; 

Then I define an interface for ADC and LEDs , which are also abstract:

 class ADC { public: ADC () { } float read () { } } ; class LEDs { public: LEDs () { } void set (int) { } } ; 

Now I can create what represents a real board with ADC and LED :

 class MyBoard : public Board <5> { // Let assume it connect to 5 bits public: MyBoard (std::array <GPIOPin, N> const& pins) : Board<5>(pins) { // Here you can initialize what you want } } ; 

Then you create your own ADC and LED :

 class AD7813 : public ADC { Board <5> _board ; public: AD7813 (Board <5> *board) : ADC(), _board(board) { } } ; // Same for the LED 

Finally, you can simply use it as follows:

 Board <5> *board = new MyBoard(/* The corresponding GPIO pins. */) ; ADC *adc = new AD7813(board) ; LEDs *led = new MyLEDs(board) ; 

I have not defined a destructor for MyBoard or Board , but of course you can. You can also use shared_ptr as @MarkusMayer.

The end of editing.

I think that there are different approaches to this problem, I will give here what I would do. It is often difficult to use OO design on an embedded system, firstly, you should have a singleton class almost everywhere, because you only have one ADC (you cannot initiate multiple ADCs), so your ADC class (and LEDBoard class) should look So:

 class ADC { public: static ADC *getInstance () { if (_instance == nullptr) { _instance = new ADC () ; } return _instance ; } private: ADC () ; }; 

To answer your problem, I would create a base class that will do your initialization and do it only once (using a static member to find out if the ports have already been initialized).

 class GPIOs { protected: GPIOs () { if (!GPIOs::_init) { /* Do what you want. */ GPIOs::_init = true ; } } private: static bool _init ; } ; bool GPIOs::_init = false ; 

Then your ADC and LEDBoard inherits from GPIOs :

 class ADC : public GPIOs { public: ADC *getInstance () { /* ... */ } private: ADC () : GPIOs () { } // Call constructor } ; 

Then in your code, you simply do:

 ADC *adc = ADC::getInstance () ; 

You can also use a singleton for the GPIOs class, but since it is an abstract class that uses only ADC and LEDBoard , which are already solid, this is not the most useful.

I'm sure there are many other ways to solve your problem, but the main idea I wanted to show is to use the init method / class, which you can call multiple times without having to perform multiple initialization due to _init boolean.

+1
source

I will sketch you an idea. I do not know any design patterns for this, but below may suit your needs.

First, I agree with your idea of ​​using GPIOPort to control the entire port, but I want to introduce a more modular approach than a "class in class". Instead of configuring the ports in the device constructor, I suggest creating an object describing the device and letting GPIOPort configure the devices based on these descriptors.

My idea is to encapsulate GPIO access through the GPIOPort class. But leave the open output open to the user for the code to work. This could be combined with other classes, but they should use GPIOPort in this design, and not vice versa.

One (of the many, sometimes conflicting) tips in OOP is that you should only subtype the class if its behavior has changed. If you can express the difference between two classes by simply changing the attributes, they will be of the same class. I'm not sure if this is the case, depending on how much work you need to do to initialize the device.

 using ports = uint64_t; // some suitable unsigned bit-maskable type. // Used to control the IO. struct DeviceDescriptor { ports in_mask, // Which pins does this device use for input out_mask, // Which pins does this device use for output init, // Initial state of the pins. shutdown; // State to send when device should power down. }; class GPIOPort { static const ports ALL_PORTS = ~static_cast<ports>(0); std::vector<Device> devices; public: // Initialize the devices. GPIOPort( std::vector<Device> & devices ) : devices(devices) { ports used_ports = 0, init = 0; for ( auto & device : devices ) { init |= device.init; // Assert no overlapping ports ports partition = device.init | device.in_mask | device.out_mask | device.shutdown; if ( used_ports & partition){ // Signal overlapping ports. } else { used_bits |= current; } } set_bits(init, ALL_PORTS); // Actually sets the output. } // Read the input of device number 'dev' ports read_state( int dev, ports mask = ALL_PORTS ) { return read_bits( devices.at(dev).input_mask & mask ); } // etc... ~GPIOPort() { ports shutdown; for ( auto & device : devices ) { shutdown |= device.shutdown; } set_bits(shutdown, ALL_PORTS); } }; 
0
source

Source: https://habr.com/ru/post/972246/


All Articles