Hello!
I am a programmer and I've recently started to tinker with Arduinos, as a a hobby (well, I actually needed some simple automation of the heating system of my house and anything I could find was outrageously expensive... so here I am).
One of the first things I've learnt when starting with Arduinos is how limited is the available memory, especially RAM. I also tend to love abstraction layers (being a C++ programmer takes it's toll). So, for starters, instead of using digitalWrite(), digitalRead() everywhere I've written some simple input/output abstractions.
I've analyzed 2 approaches to this:
- Plain old C++ - encapsulate some functionality in a class. Blink:
class DigitalOutputPin {
public:
DigitalOutputPin(uint8_t pin, bool inverted) :
_pin(pin),
_inverted(inverted) {
}
public:
// the pin number
const uint8_t _pin;
// if true then the output is inverted
const bool _inverted;
public:
void initialize() {
pinMode(_pin, OUTPUT);
}
public:
void set(bool state) {
digitalWrite(_pin, _inverted ? (state ? LOW : HIGH) : (state ? HIGH : LOW));
}
};
DigitalOutputPin outputPin(LED_BUILTIN, false);
void setup() {
outputPin.initialize();
}
void loop() {
outputPin.set(true);
delay(1000);
outputPin.set(false);
delay(1000);
}
/*
After compiling:
Sketch uses 1014 bytes (3%) of program storage space. Maximum is 32256 bytes.
Global variables use 11 bytes (0%) of dynamic memory, leaving 2037 bytes for local variables. Maximum is 2048 bytes.
*/
- Use C++ templates to avoid storing const parameters in members (thus freeing up some RAM in the process) and make all the members static. Still blinking here:
// PIN is the pin number and if INVERTED is true then the output is inverted
template<uint8_t PIN, bool INVERTED>
class DigitalOutputPinT {
public:
typedef DigitalOutputPinT<PIN, INVERTED> CLASS;
private:
DigitalOutputPinT();
public:
static void initialize() {
pinMode(PIN, OUTPUT);
}
public:
static void set(bool state) {
digitalWrite(PIN, INVERTED ? (state ? LOW : HIGH) : (state ? HIGH : LOW));
}
};
typedef DigitalOutputPinT<LED_BUILTIN, false> OUTPUT_PIN;
void setup() {
OUTPUT_PIN::initialize();
}
void loop() {
OUTPUT_PIN::set(true);
delay(1000);
OUTPUT_PIN::set(false);
delay(1000);
}
/*
After compiling:
Sketch uses 930 bytes (2%) of program storage space. Maximum is 32256 bytes.
Global variables use 9 bytes (0%) of dynamic memory, leaving 2039 bytes for local variables. Maximum is 2048 bytes.
*/
Interestingly enough the second approach seems to be optimal - the generated code is basically the same as it is when using digitalWrite() all over the place and no RAM is used at all.
This happens because gcc (the compiler used by the Arduino IDE) is mature and really good at optimizations. Thus the generated static DigitalOutputPinT<> members are inlined, the template parameters are used as immediate values in the generated code and (in this simple case) no RAM is actually used since there is no data stored anywhere. The downside of the template approach is that each generated class is a distinct type so there is no way to store instances in an array, etc...
So, to summarize:
Using constant parameters as template parameters of classes with static member functions will generate code that is really close to optimal (and sparing some RAM in the process), while still keeping things abstract enough. This is generally true for classes that have short, simple member functions that are good candidates for inlining.
Hope this helps!