6

I recently started playing with Arduinos, and, coming from the Java world, I am struggling to contend with the constraints of microcontroller programming. I am slipping ever closer to the Arduino 2-kilobyte RAM limit.

A puzzle I face constantly is how to make code more reusable and reconfigurable, without increasing its compiled size, especially when it is used in only one particular configuration in a particular build.

For example, a generic driver class for 7-segment number displays will need, at minimum, configuration for the I/O pin number for each LED segment, to make the class usable with different circuits:

class SevenSeg {
private:
    byte pinA; // top
    byte pinB; // upper right
    byte pinC; // lower right
    byte pinD; // bottom
    byte pinE; // lower left
    byte pinF; // upper left
    byte pinG; // middle
    byte pinDP; // decimal point
    
public:
    void setSegmentPins(byte a, byte b, byte c, byte d, byte e, byte f, byte g, byte dp) {
        /* ... init fields ... */
    }
    
    ...
};

SevenSeg display;
display.setSegmentPins(12, 10, 7, 6, 5, 9, 8, 13);
...

The price I'm paying for flexibility here is 8 extra RAM bytes for extra fields, and more code bytes and overhead every time the class accesses those fields. But during any particular compilation of this class on any particular circuit, this class is only instantiated with one set of values, and those values are initialized before ever being read. They are effectively constant, as if I had written:

class SevenSeg {
private:
    static const byte pinA = 12;
    static const byte pinB = 10;
    static const byte pinC = 7;
    static const byte pinD = 6;
    static const byte pinE = 5;
    static const byte pinF = 9;
    static const byte pinG = 8;
    static const byte pinDP = 13;
    
    ...
};

Unfortunately, GCC does not share this understanding.

I considered using a "template":

template <byte pinA, byte pinB, byte pinC, byte pinD, byte pinE, byte pinF, byte pinG, byte pinDP> class SevenSeg {
    ...
};

SevenSeg<12, 10, 7, 6, 5, 9, 8, 13> display;

For this reduced example, where the particular parameters are homogeneous, and always specified, this is not too cumbersome. But I want more parameters: For example I also need to be able to configure the numbers of the common pins for the display's digits (for a configurable amount of digits), and configure the LED polarity: common anode or common cathode. And maybe more options in the future. It will get ugly cramming that into the template initialization line. And this problem is not limited to this one class: I am falling into this rift everywhere.

I want to make my code configurable, reusable, beautiful, but every time I add configurable fields to something, it eats up more RAM bytes just to get back to the same level of functionality.

Watching the free memory number creep down feels like being punished for writing code, and that's not fun.

I feel like I'm missing some tricks.


I've added a bounty to this question because although I quite like the template config struct thing shown by @alterigel, I don't like that it forces respecification of the precise types of each field, which is verbose and feels brittle. It's particularly icky with arrays (compounded by some Arduino limitations, such as not supporting constexpr inline or std::array, apparently).

The config struct ends up consisting almost entirely of structural boilerplate, rather than what I would ideally like: just a concise description of keys and values.

I must be missing some alternatives due to not knowing C++. More templates? Macros? Inheritance? Inlining tricks? To avoid this question becoming too broad, I'm specifically interested in ways of doing this that have zero run-time overhead.


EDIT: I have removed the rest of the example code from here. I included it to avoid getting shut down by the "too broad" police, but it seemed to be distracting people. My question has nothing to do with 7-segment displays, or even Arduinos necessarily. I just want to know the ways in C++ to configure class behavior at compile time that have zero run-time overhead.

Boann
  • 48,794
  • 16
  • 117
  • 146
  • 2
    You could probably store the data as an array in flash memory (which you probably have more of). For example, the ATMega168 has 16k flash vs. 1k sram. See: [PROGMEM](https://www.arduino.cc/reference/en/language/variables/utilities/progmem/) – 001 Mar 24 '21 at 21:58
  • 1
    Welcome to tiny systems. It is quite a bit harder than most modern environments that essentially unlimited computing resources. To achieve reliability you might have to compromise on beauty. In embedded work, reliability under ANY possible software and electrical condition is really all that matters, and everything -- everything else falls after that. The answer below is a good one, but as an embedded patent holder, (firmware a hardware), I advise that reliability is all that matters in the end. – TomServo Mar 27 '21 at 02:01
  • 1
    Maybe I'm missing something, but a 7-segment display has 8 values including the decimal point. Why are you using whole bytes for each value? You could just store the whole thing in a `std::uint8_t` and using bit twiddling get the on/off state, saving you 7 bytes. – Casey Mar 29 '21 at 15:50
  • @Casey For the LED state, I do do that (`byte chars[numDigits];`). The whole-byte config values are for defining which pin numbers the LED segments are connected to on the chip. Anyway, 7-segment displays are just an example. I'm looking for a general approach to class configuration. – Boann Mar 29 '21 at 22:10
  • Maybe you want to be more explicit about what you still want to achieve. It is not really clear to me what you would like to have improved. – n314159 Mar 30 '21 at 10:54
  • @Boann: `byte chars[numDigits]` still uses 8 bytes. Casey's suggestion is that you store all 8 values in 1 byte, a `std::uint8_t`. – Mooing Duck Apr 01 '21 at 15:39
  • @MooingDuck `byte chars[numDigits]` uses 1 bit per LED and is 100.0% memory efficient. And please I just want ideas for the configuration problem. – Boann Apr 01 '21 at 18:12
  • 1
    @Boann: I'm not sure I understand 100% what your needs are but I feel that a policy based design may help you. You'd create a template class where each parameter would specify a certain policy, for instance how many segments you'd need, what is the LED polarity, etc. https://en.wikipedia.org/wiki/Modern_C%2B%2B_Design – linuxfever Apr 05 '21 at 13:57

3 Answers3

8

You can use a single struct to encapsulate these constants as named static constants, rather than as individual template parameters. You can then pass this struct type as a single template parameter, and the template can expect to find each constant by name. For example:

struct YesterdaysConfig {
    static const byte pinA = 3;
    static const byte pinB = 4;
    static const byte pinC = 5;
    static const byte pinD = 6;
    static const byte pinE = 7;
    static const byte pinF = 8;
    static const byte pinG = 9;
    static const byte pinDP = 10;
};

struct TodaysConfig {
    static const byte pinA = 12;
    static const byte pinB = 10;
    static const byte pinC = 7;
    static const byte pinD = 6;
    static const byte pinE = 5;
    static const byte pinF = 9;
    static const byte pinG = 8;
    static const byte pinDP = 13;

    // Easy to extend:
    static const byte extraData = 0xFF;
    using customType = double;
};

Your template can expect any type which provides the required fields as named static variables within the struct's scope.

An example template implementation:

template<typename ConfigT>
class SevenSeg {
public:
    SevenSeg() {
        theHardware.setSegmentPins(
            ConfigT::pinA,
            ConfigT::pinB,
            ConfigT::pinC,
            ConfigT::pinD,
            ConfigT::pinE,
            ConfigT::pinF,
            ConfigT::pinG,
            ConfigT::pinDP
        );
    }
};

And an example usage:

auto display = SevenSeg<TodaysConfig>{};

Live Example

alter_igel
  • 6,899
  • 3
  • 21
  • 40
  • Oooh! I like this trick! Unlimited flexibility with no RAM cost, no ROM cost, and no performance lost. And it prevents users of the class skipping a vital setter call. I wish Java could do this. – Boann Mar 24 '21 at 22:43
  • I'm wondering, is there any way to enforce more structure on the config struct supplied to the class? It's currently quite free-form. For example, if a field type were specified differently, it could cause different behavior in the class, but with no compile-time error. – Boann Mar 24 '21 at 22:44
  • You could consider using [C++20 constraints](https://en.cppreference.com/w/cpp/language/constraints), though I'm not very familiar with them – alter_igel Mar 24 '21 at 23:04
  • 2
    @Boann: I'm a big fan of using `static_assert` very aggressively, especially with anything involving templates. – Mooing Duck Apr 01 '21 at 18:14
4

If I understand your situation correctly, whenever you compile your program, you target a single, specific architecture/device with one specific setting. There is never a case where you program would deal with multiple settings at the same time, is that right? I also assume that your whole project is ultimately relatively small.

If that is the case, I would probably forgo any fancy templates or objects. Instead, for every device you desire to compile for, create a separate header file with all settings given as global constexpr constants or enums. If you change your target, you need to supply a different config header file and recompile the whole program.

The only missing component is how to make your program include appropriate config header? That can be solved with the preprocessor: Depending on the desired device, you can pass a different command line -D<setting_identification_macro> when invoking the compiler. Then, create a header file which acts as a selector. In there you list all supported devices in a form of

#ifdef setting_identification_macro
#include "corresponding_config.h"
#endif

You might cringe at this "hacky" solution, but it has many advantages:

  • No run-time overhead as you desired
  • Absolutely no boilerplate code. No structs to pass around or template arguments.
  • No change in code required when switching between settings. You just change the command line parameter when invoking the compiler.
  • Can be done in old/limited C++ or plain C
CygnusX1
  • 20,968
  • 5
  • 65
  • 109
  • Thank you for understanding the question. Yes, I do cringe at it, but macros do seem to be the done thing. Other Arduino libraries encourage you to #define some values just before including their header, or even say to straight-up edit the library header. I just assumed that if I knew C++ better, there would be some clean, structured, modern way to do this type of thing. Templates come close, but incur a lot of grammatical scaffolding. – Boann Apr 01 '21 at 14:42
  • @Boann If you want to be able to change the code you generate by passing a different parameter at the command line (or make, or cmake, etc...) then at some point you do need the preprocessor. And having that ability, in contrast of editing the source, is actually quite clean in my opinion. – CygnusX1 Apr 01 '21 at 15:33
  • Note that even if you use a template solution, as soon as you want to compile code for some different hardware (with the constraints you set), you'll probably need some `#define`'s. – ariels Apr 04 '21 at 15:05
1

This does nothing for the whole problem, but improves the pgm_read:

template<class T = type>
auto pgm_read(const T *p) {
    if constexpr (std::is_same<T, float>::value) {
        return pgm_read_float(p);
    } else if constexpr (sizeof(T) == 1) {
        return pgm_read_byte(p);
    } else if constexpr (sizeof(T) == 2) {
        return pgm_read_word(p);
    } else if constexpr (sizeof(T) == 4) {
        return pgm_read_dword(p);
    }
}

This has to be a template for the if constexpr to work correctly.

n314159
  • 4,990
  • 1
  • 5
  • 20
  • This is more elegant, and will maybe work in the future, but Arduino's current stable (1.8.13) compiler complains `error: 'is_same_v' is not a member of 'std'` and `warning: 'if constexpr' only available with -std=c++1z or -std=gnu++1z`. – Boann Mar 29 '21 at 22:06
  • 1
    Ah, the `is_same_v`-issue can be worked around by using `is_same<...>::value`. `if constexpr` needs c++17 and it seems like you can activate that in the compiler (it is only a warning). – n314159 Mar 30 '21 at 10:49