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!

Curious
  • 61
  • 1
  • 3
  • 3
    There are some limitations about what you can do with C++ on an 8-bit Arduino, which [come from avr-libc](http://www.nongnu.org/avr-libc/user-manual/FAQ.html#faq_cplusplus). 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()`](https://github.com/arduino/Arduino/blob/master/hardware/arduino/avr/cores/arduino/new.cpp). – Edgar Bonet Jan 25 '18 at 08: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 03:15

4 Answers4

14

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.

Nick Gammon
  • 35,792
  • 12
  • 63
  • 121
  • 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](https://hackaday.com/2017/05/11/templates-speed-up-arduino-io/). – 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
  • @JAB Ignoring STL templates, it's still easy to introduce code bloat through templates. [Here's a dummy example](https://gist.github.com/mattKipper/02afc55cfc17882c2460074c4acfd0e6) that shows a pretty common issue — defining a template function which includes a large amount of code which doesn't need to be genericized. Those two programs are functionally identical, but through GCC with `-O2` on my desktop, `bloated.cc` generates a 31KB executable while `trimmed.cc` generates a 14KB executable. – Matt K Jan 25 '18 at 18:55
  • @MattK Ah, I see what you mean. (Though in that case the call to preamble seems like it could be kept in the template function with minimal code bloat rather than being a separate call, unless the compiler decided to inline those calls). – JAB Jan 25 '18 at 19:22
  • See amended reply for comments on some of those points. – Nick Gammon Jan 26 '18 at 01:58
  • 2
    @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
  • @Voo - I guess the same goes for using other features such polymorphism and inheritance and other OOP features? – Curious Feb 28 '23 at 23:39
  • 1
    @Curious The general gist is that C++ is complex enough that it can be rather hard to know what exactly the compiler is doing in the background. Then you also have to rely on your compiler to do certain optimizations for certain things to be feasible (do you know whether your compiler does COMDAT folding?). It can be done, usually by limiting yourself to certain features, but it's a bit fragile (i.e. a tiny change can lead to very different non-functional behavior). – Voo Mar 01 '23 at 11:26
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).

Dmitry Grigoryev
  • 1,248
  • 9
  • 30
  • AFAIK, the Arduino environment does not support exceptions. – Edgar Bonet Jan 25 '18 at 08: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 08: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
  • @Graham Well yes, having to write extra code *is* a bad thing. Your argument reads a bit like "you shouldn't use `default` in a `switch` statement, because if you've done your design properly, there's not such thing as an unexpected value". Exceptions are *not* unexpected errors, they are errors for which you have no cure but to abort the current action, and possibly try again. – Dmitry Grigoryev Jan 25 '18 at 13:06
  • @DmitryGrigoryev Having to write unnecessary code is a bad thing, sure; but having to write more code to do the job properly is not. Handling errors appropriately really isn't unnecessary. Perhaps it takes more code if you're reliant on downstream functions to throw an exception if "new" fails and you try to dereference a null pointer, for example. But that's where PC coding and embedded coding differ. On a modern PC with essentially infinite resources you can generally get away with bad habits like that because it rarely if ever happens; but in the embedded world you can't. – Graham Jan 25 '18 at 16:16
  • @DmitryGrigoryev "errors for which you have no cure but to abort the current action, and possibly try again" are/should not be very common in embedded environments. Any possible "error condition" (external hardware not ready, transmitted failure, etc) should probably be explicitly handled and thus not really be exceptional. – mbrig Jan 25 '18 at 20:12
5

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 !

Alexander
  • 51
  • 2
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.
Graham
  • 179
  • 2
  • 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 `int`s 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
  • @DmitryGrigoryev Sure, if you need features from C++11 or later, you can use them - you just need to be aware of the implications on a low-spec processor. Re STL containers, the std::vector container may live on the stack, but its data (which of course is the important bit) will be on the heap because it has to be resizable. Unless the STL implementation is a bit non-standard, or I guess if you've done something funky with the allocator, but that's probably beyond the skills of the OP at the moment. std::array could be entirely on the stack though (and yes, this is a C++11 addition). – Graham Jan 25 '18 at 13:16
  • `std::array` can be a stack-only allocation. `std::vector` is dynamically sized, so even if some flavor of `std::vector` can be stack-only on construction, causing a resize by adding elements will need dynamic memory. – Curt Nichols Jan 26 '18 at 20:26