7

Though I've written C/C++ code for a long time, the unseen limitations on how memory was consumed on various MCU and SOC programming platforms have often tripped me up. As I'm about to build up code for my first large project for my NANO boards, which will likely use a fair amount of its resources, I'd like to be better prepared for any surprising and unexpected "gotchas" to look out for.

For example, on another SOC I recently worked on extensively (The Pololu.com wixel) I was very surprised to learn that function/method arguments and auto variables, which I'd normally expect to be stack allocated and recovered when a function returns were in fact permanently allocated for the life of the program! Wow! so on that platform, where I'd normally hate to "overwork" variables or reuse variables after their names no longer make sense, I had to adjust my coding to what I'd normally consider BAD for readability. Not to mention realizing that simple loop variables were better off allocated globally. Yecch!

Can those of you who have run into such unexpected coding issues in the Arduino environment share some "special" guidelines about things like this?

  • 1
    auto changed from a storage duration specifier to a variable type specifier in C++11. For simplicity don't use auto. – Majenko Feb 23 '18 at 18:21
  • 1
    a positive surprise for me is array allocation on stack with size from a variable gcc.gnu.org/onlinedocs/gcc/Variable-Length.html – Juraj Feb 23 '18 at 19:28
  • Oh... gee... sorry... I never meant to infer I ever used the 'auto' keyword (forgot it even existed!). I just meant whatever the default allocation was for non static vars declared within a function. – Randy Feb 23 '18 at 21:05
11

As you have noticed yourself, this can be somewhat platform-dependent. Since you are working on a Nano, my answer will be for the AVR architecture, when compiled with gcc, the avr-libc, and the Arduino core library.

First, let me reassure you: this weird behavior you saw of the Wixel doesn't happen on this platform. Local variables get allocated mostly in CPU registers, then on the stack.

Now, the first general rule may sound obvious: think twice about what your program really needs to remember. Also, use each time the smallest possible data type. The C99 types int8_t, uint16_t and co. are useful for this.

Another rule you will often hear on this platform: avoid dynamic allocation, i.e. malloc() and new, as quite often you cannot afford heap fragmentation. Avoid also the String class, as it relies on dynamic allocation. Prefer when possible static allocation, or stack allocation for small things.

Don't forget to use const for constants: with this qualifier the compiler can often optimize the constants out as immediate operands. For arrays of constants, use PROGMEM. Yes, it's not comfortable to use, but it can save you lots of RAM. The F() macro for printing constant messages is a special case of PROGMEM.

Lastly, be very careful with recursion.

  • Thanks. Good points! I'll usually avoid malloc or 'new' in any embedded program, especially one that might be running unattended for a long time. And also, I too have used codespace memory for any struct or arrays that won't ever change. Or even EEprom structs for things like settings. But just the fact that function vars are stack based is VERY good news. I began to worry seeing so much example code, where the same var name was used again and again, even when its name no longer made sense. – Randy Feb 23 '18 at 21:11
  • That last point is interesting too! I wonder if a recursive function that went blow past its stack limits would make the poor board nearly unreachable, as it would always run itself into the same hole everytime it saw power. Its not like the old devices that you could force to a halt with a stream of CTRL-C's sent during power up! – Randy Feb 23 '18 at 21:19
  • 1
    @Randy: There is no stack limit. If you overflow your stack, you will just start to overwrite the program's heap, if any, then the .bss section, then the .data section. – Edgar Bonet Feb 23 '18 at 21:24
  • It's not accurate to say things do or do not happen on a particular chip when they are in actuality results of the toolchain or even the language specification, rather than the hardware. – Chris Stratton Feb 24 '18 at 4:14
  • 1
    @ChrisStratton: You are right. I added a clarification to my answer. On the other hand, the AVR was designed with compilation in mind. It would be silly (though certainly possible) for a compiler to not use the register file and the stack pointer as they were designed to be used. – Edgar Bonet Feb 24 '18 at 9:46
3

Edgar's got some great tips, and I'll reiterate one before giving a few more:

avoid dynamic memory allocation

Don't use malloc. When you're designing a system, budget out your RAM like you do your monthly budget. Make sure you're able to do everything at once.

And a couple other tips:

global statics

Make full use of the BSS block; try to block out space for as much as possible in global static arrays. You may shudder or scoff at the idea of putting everything at a global level, but embedded is a paradigm shift, with its own set of rules.

Since you'll have so many variables at a global level, static helps you to avoid polluting your namespace too much.

avoid fancy C++ features

In fact, try to not use C++ at all. C++ takes you "farther from the metal," with its RAII and its constructors and its implicit type conversion... It's much harder to keep a tight inventory of all of your resources when your language does so much behind the scenes. If you really want to use a certain C++ language feature, try to keep the tone of your code more like "C with bits of C++" rather than "C++ shoehorned into an embedded system." That never goes well.

Especially avoid generics/templates, which will eat up lots of your memory. Every time you use a new type with a template in your code, another copy of the template class is compiled into your program, just for that one type. Ever wonder why C++ binaries can balloon to 20, 50, even 100 MB? Templates, man. Templates.

const

This keyword should go on as many variables as it can. Keeping this habit will save on both execution time and space consumption. Learn the difference between const T* foo, T* const foo, and const T* const foo.

  • 1
    Too much of this is nonsense to ignore. Some of this is good, but recommending a bunch of global state is just bad advice and many people use C++ for this kind of programming. Embedded is not an excuse to write hard to maintain code. – RubberDuck Feb 24 '18 at 2:54
  • 2
    Additionally, C++ templates need not be as heavy-weight as you make them out to be. The key is to make sure you compile with link-time optimization, to prevent each individual object file from having its own copy of each template specialization it uses. 10 different copies of vector<int>, one in each of 10 source files, gets a little unwieldy, but with LTO, the compiler/linker can deduplicate them down to a single shared copy. If you need the functionality, the template specialized code (especially after LTO inlining) is often pretty similar in size to what you'd write by hand. – ShadowRanger Feb 24 '18 at 4:10
  • 2
    Do not make a variable global if it is only used in one function. Make it static if the value needs to be remembered from invocation to invocation. Allocation-wise, a static local is the same as a global. – Edgar Bonet Feb 24 '18 at 9:55
  • Thanks for the tips. I see the merit in some of this for small systems. Though I've seen C++ compilers come a long way, I'm still reluctant to use "pure" C++, and be 100% OOP in every corner of my code. Sometimes its overkill. But still, building well established functionality into classes you can tuck away in a library does make your application a lot simpler to follow. – Randy Feb 25 '18 at 0:21

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.