6

I have just started getting into Arduino and c++ programming!

For all the talk on the internet about c++ being incompatible with 8-bit and even 16-bit microcontrollers, how is it that Arduino Microcontrollers can safely handle c++ code, in terms of the ram usage? Is it that Arduino's library does not contain or utilize features such as constructors or objects (of C++) that (I believe) are very ram-usage-intensive?

Cheers!

  • 3
    There are some limitations about what you can do with C++ on an 8-bit Arduino, which come from avr-libc. Note that the linked page states that new and delete are not implemented, but the Arduino core provides an implementation based on malloc() and free(). – Edgar Bonet Jan 25 '18 at 8:40
  • 4
    Huh, it's almost as if all the talk on the internet wasn't always right. – Bartek Banachewicz Jan 25 '18 at 13:10
  • The width of a processor's instruction word ultimately says nothing about how much memory it has. There are for example "overgrown" 8051 cores in bluetooth chips with several times more memory than many of the low end 32-bit ARM cortex parts. That said, classic Arduinos do happen to have quite little RAM, and this makes some expensive patterns severely unwise. – Chris Stratton Jan 26 '18 at 3:15
13

It's a misconception that using constructors or C++ in general requires lots of RAM. Plus, to be clear, the AVR processors like the Micro and Uno have separate RAM and PROGMEM (program memory). So, code for functions does not, in itself, use RAM.

As an example, this small program for the Micro:

int main ()
  {
  }

That uses only 206 bytes of PROGMEM and 0 bytes of RAM (according to the IDE).

Sketch uses 206 bytes (0%) of program storage space. Maximum is 28,672 bytes.
Global variables use 0 bytes (0%) of dynamic memory, leaving 2,560 bytes for local variables. Maximum is 2,560 bytes.

Adding in a class, with a constructor, and using that class, increases the memory a bit:

class foo
  {
  public:
  int bar;  // some class variable

  foo ()  // constructor
    {
    bar = 1;
    }
  };  // end of foo


foo nick;  // a global variable
int main ()
  {
  PORTD = nick.bar;
  }

That uses 2 bytes of RAM, and 278 bytes of PROGMEM. Hardly a lot.

Sketch uses 278 bytes (0%) of program storage space. Maximum is 28,672 bytes.
Global variables use 2 bytes (0%) of dynamic memory, leaving 2,558 bytes for local variables. Maximum is 2,560 bytes.

Of course, the more things you do, the more RAM and PROGMEM it takes, however the compiler generates quite efficient code, and the linker strips out things that are not needed by your code.


Is it that Arduino's library does not contain or utilize features such as constructors or objects (of C++) ...

A lot of libraries use classes and constructors (for example the Serial library, the Print class, and a lot of others). There is nothing inherently RAM-intensive about a constructor. It's just a function that does something, with an implied this pointer to the current instance of the class.


do lambdas work? (from a comment)

Yes you can use lambdas:

// array of function pointers
void (*doActionsArray []) () =
 {
 [] { Serial.println (0); } ,
 [] { Serial.println (1); } ,
 [] { Serial.println (2); } ,
 [] { Serial.println (3); } ,
 [] { Serial.println (4); } ,
 };

void setup ()
  {
  Serial.begin (115200);
  Serial.println ();

  doActionsArray [3] ();
  }  // end of setup

void loop () { }

as long as you don't (extensively) use: the standard library, exceptions, templates ... (from a comment)

Templates can certainly simplify doing things like writing to, or reading from EEPROM and similar. For example:

#include <Arduino.h>  // for type definitions
#include <EEPROM.h>

template <typename T> unsigned int EEPROM_writeAnything (int ee, const T& value)
{
    const byte* p = (const byte*)&value;
    unsigned int i;
    for (i = 0; i < sizeof(value); i++)
        EEPROM.write(ee++, *p++);
    return i;
}

template <typename T> unsigned int EEPROM_readAnything (int ee, T& value)
{
    byte* p = (byte*)&value;
    unsigned int i;
    for (i = 0; i < sizeof(value); i++)
        *p++ = EEPROM.read(ee++);
    return i;
}

The standard library? There are implementations for the Arduino. We have to bear in mind that RAM is limited, however if you are going to do something anyway that involves linked lists, etc. then the Standard Template Library will probably do what you want as efficiently as you could if you coded it "by hand".


The bottom line is you need to be aware of what you are coding on. I have seen examples like this:

float foo [1000];  // hold my readings

That blows the available RAM on a Uno straight away. Nothing to do with C++ per se.


The other thing to be aware of is some tricks you can usefully employ. One common one is to replace:

Serial.println ("Program starting.");

With:

Serial.println (F("Program starting."));

The first example causes the compiler to copy the string "Program starting." from PROGMEM into RAM, using some of your valuable RAM. The second example doesn't.

  • 2
    Basically you can use C++ as long as you don't (extensively) use: the standard library, exceptions, templates or rtti (do lambdas work? never tried). Whether you'd want to call the remaining language really 'c++' is another question. – Voo Jan 25 '18 at 11:22
  • 2
    @Voo: On the Arduino, templates are not necessarily a bad thing. – Edgar Bonet Jan 25 '18 at 12:20
  • 2
    @Edgar Very clever trick, but certainly not the way you'd use templates in general code. A better way to phrase it might be that you have to be very careful with how you use templates since they can very easily lead to large quantities of code being generated behind your back. – Voo Jan 25 '18 at 13:46
  • @Voo Is that just a limitation of the AVR compiler? GCC and Clang do a pretty good job of optimizing template usage for desktop work. Or is it more an issue with standard-library/etc. templates, which are very general so may have lots of stuff you don't need? – JAB Jan 25 '18 at 17:46
  • 1
    @JAB It's generally recommended for efficient code to try and forward to non-generic functions (e.g. standard libraries cast T's to void* and deal with those). Absolutely doable but makes for awkward programming and is easily forgotten. Nick's template example is pretty similar: Instead of forwarding the byte* and size to a generic function we now have almost identical code multiple times in memory. If you had to write it by hand you'd notice the duplication. – Voo Jan 26 '18 at 21:35
5

The talk about C++ being incompatible with small MCUs implies that developers would use features it provides. In a good C++ project you're expected to use vectors and iterators instead of C arrays, throw exceptions instead of returning error codes, use lambda-functions, templates etc.

As long as you don't use those features, C++ memory consumption is manageable. The reason for this is simple: such limited C++ is just a syntactic sugar around C structures and function pointers.

The problems usually start when you write a class which is complex enough that its constructor can fail. Then you have a tough choice to make: start using exceptions and drastically increase memory consumption, or write classes which pretend they never fail to construct (which often results in a program which pretends to work).

  • AFAIK, the Arduino environment does not support exceptions. – Edgar Bonet Jan 25 '18 at 8:24
  • @EdgarBonet It wouldn't be a very useful feature to support anyway, considering most boards have about 2K RAM. – Dmitry Grigoryev Jan 25 '18 at 8:46
  • 1
    @DmitryGrigoryev Or a third choice - use the constructor to set up the class in a known but non-working state, and have a second "initialisation" function which does all the operations which can fail. This is a fairly standard design pattern. If you then use a factory which is a "friend", the second "initialisation" function isn't exposed to the rest of the world, and the factory can legitimately report "I'm sorry, I can't do that, Hal". This has minimal extra cost compared to just calling a constructor; certainly nothing like as heavy as adding exception handling. – Graham Jan 25 '18 at 11:15
  • @Graham Possible, but it would mean all error conditions must be explicitly handled in code. You won't be able to simply write f(g(x)), you'll have to wrap every action which could fail in the error-handling code. – Dmitry Grigoryev Jan 25 '18 at 12:01
  • @DmitryGrigoryev You say that like it's a bad thing. :) The second "initialisation" function could just report true/false, or it could be more detailed about the cause of failure. It's up to you as the coder. In general though, if you don't understand every error condition, then I'd say you haven't thought enough about your design or code. If you've done your design properly, there simply is no such thing as an unexpected error. Sure, there could be errors you think shouldn't happen, but a robust implementation will still cover them. – Graham Jan 25 '18 at 12:46
3

C++ is fine with Arduinos or other AVR based Systems. I've been use it for quite a while in a home automation project.

The avr runtime does not support exceptions nor dynamic memory allocation, as has been stated. you can provide an implementation though.

There is much of C++ left to make use of. here are some examples

Templates

They are very useful because, e.g. they allow to move work from runtime to compiletime. I use template for a replacement of the Arduino HAL functions DigitalWrite and others, without the runtime and code bloat.

compare this

int main(void)
{
        DDRB |= (1<<7);
        while(  (  ( PINB & (1<<6) ) !=(1<<6) ) )
                ;
        while(1)
        {
                _delay_ms(100);
                PORTB |= (1<<7);
                _delay_ms(100);
                PORTB &= ~(1<<7);
        }
}

to this

int main(void)
{
        IOPin<13> ledpin;
        IOPin<12> testpin;
        ledpin.SetDir(out);

        while(0 == testpin.State())
                ;
        while(1)
        {
                _delay_ms(100);
                ledpin.SetHigh();
                _delay_ms(100);
                ledpin.SetLow();
        }
}

The template based version is much more readable, and generates exactly the same assembler code. In the abovementioned linked article i explain the template and why this works.

more on templates

The following code uses the "curiously recurring template pattern" CRTP. It is used to create a job class, which i use for a primitive scheduling

This template builds a chain of registrable items.

template<class registered>
struct Registered {
    static registered *registry;
    registered *chain;
    Registered() : chain(registry) {registry=static_cast<registered*>(this);}
};

The job class applies the CRTP pattern. The constructor takes the actual callback where the work is done, and how often the job is done.

class Job : public Registered<Job>
{
    voidFuncPtr m_p;
    uint16_t m_periodic;
    uint32_t m_lastrun;
    uint16_t m_initialdelay;
    public:
    Job(voidFuncPtr p,uint16_t periode=0,uint16_t initialdelay=0):m_p(p),m_periodic(periode),m_lastrun(0),m_initialdelay(initialdelay)
    {

    }
    void run(uint32_t t_ms)
    {
        // tbd handle wraparound !!
        if(t_ms >= m_lastrun + m_periodic + m_initialdelay)
        {
            m_p();
            m_lastrun=t_ms;
            m_initialdelay=0;
        }
    }
};

to use the job class i just create the instances for the jobs and pass lambdas or regular function pointers

Job jserial([](){mqttrouteradapter.handleSerialMQTT();},10);
Job jupdate(updatemc,1);
Job jprint(printstate,500);

initialize the beginning of the chain of jobs

template<> Job *Registered< Job >::registry = 0;

and inside main, run an infinite loop which calls the jobs.

while (1)
    {
        for ( auto p = Job::registry; p; p=p->chain )
        p->run(millis());
    }

Lambdas

Nice to avoid a callback function

instead of

void handleserial(void)
{
    mqttrouteradapter.handleSerialMQTT();
}    
Job jserial(handleserial,10);

i can write something like

Job jserial([](){mqttrouteradapter.handleSerialMQTT();},10);

the new meaning of auto

Allows to deduce the type automagically. This function iterates over "something" passed the second parameter. It compiles, if the passed type provides the operations/attributes used in the function. And yes, this works only since c++17

void runRx(const char* data, auto msg_registry) {
    for (auto p = msg_registry; p; p = p->chain) {
        p->rx(data);
    }
}

using a recent compiler

If you want to use a recent compiler (The Arduino IDE uses gcc4.9) you can build your own gcc avr toolchain rather easily. Then you have gcc 7.2 with c++17 support ! An other very good reason to use gcc 7.2 is, that they have improved the error messages by a great deal.

My conclusion is, there is no excuse using c, except you havn't yet learned c++. And use a new compiler !

  • nor dynamic memory allocation - malloc/free and new/delete are both available in the standard IDE. – Nick Gammon Jan 26 '18 at 21:16
1

To write effective code with limited resources (RAM, program space, processing), you need to know enough about the processor to avoid features which it can't do. In this case you're limited on all three counts, so you want to get away from anything complex and slow.

A good start is to avoid any features later than C++98, or at least C++03. These features generally come at a cost.

Beyond that, particular issues are:-

  • Polymorphism. All classes using virtual members have a virtual method table, meaning every function call from that class needs to look up against a table. On a small processor, this does slow down your function calls. By all means use this where it's required, but if you have performance issues then you want your polymorphic classes to live at a higher level where the function calls happen less often. Don't do this for low-level functions which get called thousands of times in a loop.
  • Exceptions. Already covered by other answers.
  • Inline. This is a good thing. Where you can use this, it can claw back some processing cycles: not just from the function call itself; but also from further optimisation in the calling functions.

And of course general issues for embedded firmware on low-end micros:-

  • Dynamic memory allocation (new and delete, or most STL containers). This is always an anti-pattern in embedded development. It requires a chunk of RAM to be set aside for heap, and you always need to consider the possibility of allocation failing. If you really need some kind of dynamic storage, consider whether a FIFO buffer or something better bounded would be appropriate.
  • Longer integers.
  • Integer multiply and divide.
  • Anything floating-point.
  • Anything using a lot of stack, whether this is lots of local variables, lots of function parameters, or over-enthusiastic function partitioning.
  • Integer multiplication is OK on Arduinos, as the ATmegas have a hardware multiplier. But note that some Arduino-compatible boards (Trinket, Digispark, ...) are based on ATtinies, which do not have such multiplier. – Edgar Bonet Jan 25 '18 at 12:06
  • 2
    Arduino libraries relying on C++11 are not that uncommon. Also, a lot of STL containers can be used without dynamic memory allocation. Nothing stops you from creating an std::vector on stack. – Dmitry Grigoryev Jan 25 '18 at 12:43
  • @EdgarBonet Isn't that only an 8-bit x 8-bit multiplier though? Technically an 8-bit value is not an integer. ;) – Graham Jan 25 '18 at 12:49
  • 2
    1. Yes, it's a 8×8→16 bit multiplier. It is used by the compiler to implement all multiplications far more efficiently than a bitwise shift-and-add. Multiplying two ints requires only three 8×8→16 multiplications (two bytes of flash and two CPU cycles each) and a few additions. 2. Technically, both int8_t and an uint8_t are 8-bit integer types. But this is completely unrelated to my first comment. – Edgar Bonet Jan 25 '18 at 13:06
  • @EdgarBonet Error on my part - it's an integer, just not an "int". :) – Graham Jan 25 '18 at 13: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.