1

Often I want Arduino board to control several peripherals. Writing program for all of them in one document using #define SOME_PIN 4 constructions seems pretty inconvenient to me. Especially in such a case when you have to modify old code which was written a long time ago.

I chose to use the following approach to organize programming part of a project. So writing a code could be divided into three parts.

1.Interface structure

At this stage programmer should describe all the devices used in his project. Of course, only those of them which Arduino programmaticaly interacts with (sensors, chips and not intermediate resistors, for example). Each device at this stage is represented by a class - description-class. Description-class has special member functions - pin-functions. Those functions are pure virtual and each is intended to return 8-bit unsigned integer value from the set of Arduino pin numbers. Pin-functions play a role of wires between Arduino and peripherals. Description-class should also contain functions which are Arduino instructions and intended for this concrete device.

Arduino also has a description-class but now it is used as a namespace with constants. No object of this class is needed so far.

Below is an example of using the interface.

  • Controller: Arduino Uno R3
  • Peripheral: Ultrasonic sensor HC-SR04

1.1 Arduino Uno R3 class-description

Arduino Uno R3 description-class consists only of static constants that are pin numbers.

1.1.1 Uno.h

#ifndef UNO_H
#define UNO_H

#include <inttypes.h>

class Uno
{
    public :
        //Digital pins
        static const uint8_t D0 = 0;
        static const uint8_t D1 = 1;
        static const uint8_t D2 = 2;
        static const uint8_t D3 = 3;
        static const uint8_t D4 = 4;
        static const uint8_t D5 = 5;
        static const uint8_t D6 = 6;
        static const uint8_t D7 = 7;
        static const uint8_t D8 = 8;
        static const uint8_t D9 = 9;
        static const uint8_t D10 = 10;
        static const uint8_t D11 = 11;
        static const uint8_t D12 = 12;
        static const uint8_t D13 = 13;
        //Analog pins
        static const uint8_t A0 = 0;
        static const uint8_t A1 = 1;
        static const uint8_t A2 = 2;
        static const uint8_t A3 = 3;
        static const uint8_t A4 = 4;
        static const uint8_t A5 = 5;
        //SPI
        static const uint8_t SS = D10;
        static const uint8_t MOSI = D11;
        static const uint8_t MISO = D12;
        static const uint8_t SCK = D13;
        //I2C
        static const uint8_t SDA = A4;
        static const uint8_t SCL = A5;
        //Serial
        static const uint8_t TX = D0;
        static const uint8_t RX = D1;
};
#endif

1.2.Ultrasonic sensor class-description (abstract device)

For this example I chose to use ultrasonic sensor HC-SR04. It has four pins: VCC, GND, TRIG and ECHO. Though in reality (whatever it is) all four pins are connected to Arduino only TRIG and ECHO are used in program. I don't know what to do with those pins so far. Maybe they should never appear in code.

1.2.1 HC_SR04.h

#ifndef HC_SR04_H
#define HC_SR04_H

#include <inttypes.h>

#include <Arduino.h>

class HC_SR04
{
    public :
        //pin-functions
        virtual uint8_t VCC() = 0;//???
        virtual uint8_t TRIG() = 0;
        virtual uint8_t ECHO() = 0;
        virtual uint8_t GND() = 0;//???
        //device specific functions
        void setup();

        long get_distance();
};
#endif

1.2.2 HC_SR04.cpp

#include "HC_SR04.h"

void HC_SR04::setup()
{
    pinMode( TRIG(), OUTPUT );
    pinMode( ECHO(), INPUT );
}


long HC_SR04::get_distance()
{
    digitalWrite( TRIG(), LOW );
    delayMicroseconds( 2 );
    digitalWrite( TRIG(), HIGH );
    delayMicroseconds( 10 );
    digitalWrite( TRIG(), HIGH );

    return 0.017 * pulseIn( ECHO(), HIGH );
}

2.Commutation description

At this stage programmer should "plug in" all the real devices on the table by overloading pin-funcions in a subclass derived from corresponding class-description.

2.1 Ultrasonic sensor (real device)

2.1.1 US.cpp

#include "Uno.h"
#include "HC_SR04.h"

class US : public HC_SR04
{
    public :
        uint8_t VCC() { return 0; }//What to do with this pin
        uint8_t GND() { return 0; }//What to do with this pin
        uint8_t TRIG() { return Uno::D10; }//wire between TRIG-pin on US and 10th digital pin on Uno
        uint8_t ECHO() { return Uno::D11; }//...
};

3. Main file (.ino file)

#include "US.cpp"//bad line?

US us;//Create ultrasonic device plugged in as descripted in US.cpp file

void setup()
{
    Serial.begin( 9600 );

    us.setup();
}


void loop()
{
    Serial.println( us.get_distance() );
    delay( 1000 );
}

4. Project directory structure

enter image description here

5. Discussion

Here is my sight on the approach above. Of course, could be wrong.

Disadvantages:

  • Additional level of abstraction = (perhaps) bad for low-memory controllers
  • Whole program is divided into parts. Each part is responsible for the communication with the concrete device. (perhaps) Bad for a project with dependent devices

Advantages:

  • Whole program is divided into parts. Each part is responsible for the communication with the concrete device. Good for a project with many independent peripherals
  • Self-descriptive code
  • Reusable code for similar devices or other projects with the same devices

Please, tell me what do you think. As always any ideas, corrections, critic and etc. would be appreciated.

P.S. To upload program to Arduino I use Makefile for Arduino.

  • 1
    While you certainly should identify and use the pins, you should also abstract quickly, which is to say, the rest of your program shouldn't even know about which pins to set. Dedicate a class to convert a request call to the physical setting of the pins. After this point, the rest is simply calling it and when. – Neil Sep 10 '18 at 8:20
  • @Neil, Sorry, but I couldn't fully understand what you are talking about. Could you explain in detail (maybe in an answer)? – LRDPRDX Sep 10 '18 at 8:46
  • I mean the communication via pins is not crucial to your program beyond simple communication. It means you can and should use a single class to "translate" higher level requests to their basic pin signals. – Neil Sep 10 '18 at 8:58
  • @Neil, Sorry, did not understand anyway. I use Arduino functions to regulate pin state.P.S. I don't understand the first sentence at all. Maybe coz I am not a native speaker. – LRDPRDX Sep 10 '18 at 9:41
1

You basic idea of encapsulating the behavior of a device is a good idea. However, using inheritance to provide configuration values, like which pins the device is connected to, is not the correct approach.

First of all, having to create a new class just because you connected a second sensor does not match with the principles of OO design. Singleton classes (classes of which only a single instance should ever be created) should be a rarity and are generally frowned upon.

Secondly, and more important to small embedded systems, virtual functions cost quite a bit of memory compared to non-virtual functions. For a typical compiler, a virtual function costs 1 pointer per function plus 1 pointer per instance of a class containing virtual functions plus some extra overhead when calling a virtual function.

A better, and more common approach would be to pass the pin numbers to the constructor of the HC_SR04 class, like this:

#ifndef HC_SR04_H
#define HC_SR04_H

#include <inttypes.h>

#include <Arduino.h>

class HC_SR04
{
    private:
       const uint8_t pin_trig;
       const uint8_t pin_echo;

    public :
        HC_SR04(uint8_t trig, uint8_t echo) :
          pin_trig(trig), pin_echo(echo) {}

        //device specific functions
        void setup();

        long get_distance();
};
#endif

I have taken out the VCC and GND pins, because they should typically not be controlled by the software but connected to respectively the power and ground planes.

Creating an instance of such a sensor would look like

HL_SR04 us(Uno::D10, Uno::D11);
  • Thank you for an answer. First of all, did not know about memory cost of a virtual function. Thx. Secondly, I also thought of an approach you've provided. But changed my mind when realized that there are peripherals with many pins. And then the order of arguments in the constructor should be of a great attention. For example, I have an ADC with 13 pins. How to handle it, I have no idea. Of course, I agree your change is more pedantic. – LRDPRDX Sep 10 '18 at 9:48
  • Hmm. Though almost every device has enumeration of pins in its datasheet. Could be a convention that arguments in the constructor always are of the order in datasheet. – LRDPRDX Sep 10 '18 at 9:51
  • @LRDPRDX: The descriptor class should only have arguments for those pins that are meant to be controlled by the software. I have yet to find a peripheral chip where that is more than a handful. Your ADC is probably a multi-channel ADC (for example, an 8-channel ADC with I2C connection), so multiple of those 13 pins are meant to be connected to the outside world. – Bart van Ingen Schenau Sep 10 '18 at 10:08
  • Arduino is C++, does it support templates? Use templates to make the compile time constants, constant. – Bill Door Sep 10 '18 at 16:04
  • Singleton classes are absolutely a norm when working with an 8bit micro. I create a driver class and it should be the one and only instance of the class allowed to manipulate its hardware resources. – RubberDuck Sep 11 '18 at 21:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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