Building ARM Projects with Newlib-Nano
This tutorial demonstrates how to use newlib-nano, a lightweight version of the standard C/C++ library that is included with our ARM toolchain. We will show the main differences compared to the full library and demonstrate some efficient debugging techniques. We will first create a very basic C++ project with the full-size library and then reduce its size by using newlib-nano. We will target the STM32F4Discovery board, however the techniques demonstrated here will work with any other ARM device as well.
To use all features described in this tutorial please update to GCC 4.9.2 and VisualGDB 4.3.
- Begin with creating a normal Embedded project with VisualGDB project wizard:
- Proceed with the default settings on the first page of the wizard:
- On the device selection page select the default C library type and disable the C++ binary size reduction:
- Proceed with the default sample settings:
- Select a debugging method that matches your hardware:
- Press Finish to create the project. Replace the contents of the main source file with a very basic main() function:
#include<stm32f4xx_hal.h> int main(void) { HAL_Init(); return 0; }
Then build the project:Note the memory utilization shown in the build output. Most likely you will use less than 2 kilobytes of FLASH.
- Now add some basic C++ code to the project:
#include <stm32f4xx_hal.h> #include <stdio.h> class BaseClass { public: virtualvoid SomeMethod() = 0; }; class ChildClass : public BaseClass { public: virtual void SomeMethod() { } }; int main(void) { HAL_Init(); ChildClass inst; inst.SomeMethod(); return 0; }
Build the project again. You will see that the amount of used memory increased by more than 70K:
- We will now track down the source of the high memory consumption and show how to reduce it. Open the Embedded Memory Explorer pane:
- Then click “Explore details”:As you can see, most of the memory is used by various library functions that we don’t call directly. Those functions come from the default C++ exception handling mechanism. The default implementation for the pure virtual SomeMethod() actually throws an exception and this automatically involves lots of heavy code that deals with name demangling and information formatting.
- While using 70K of code to get user-friendly printing of unhandled exceptions could be a good tradeoff for desktop programs, it is certainly not for embedded targets. We can disable it by linking with a special library that provides an alternate implementation for the pure virtual method that simply activates a hardware breakpoint. Enable the corresponding checkbox in the VisualGDB Project Properties:
- If you now rebuild your project, you will notice that it requires about a hundred bytes of memory more than the original main() function:
- Another scenario that may involve the heavy exception-related code is using C++ allocation operators. Add the following code to your main() function:
char *p = newchar[128]; delete[] p;
Then build your project:You will see that the exception-related functions have been linked again driving the image size to 70K+.
- In this case you can reduce the binary size by switching from the classical C/C++ library to a reduced-size version, newlib-nano. It provides many trade-offs that reduce the size of your binaries, e.g. contains a C++ library built without exception support. You can select newlib-nano via VisualGDB project properties:
- However, if you build your project now, you will get several ‘undefined symbol’ errors:
- This happens because newlib-nano comes without default implementations for the system calls required by the memory allocation code. This can be fixed by linking with a library that provides default versions of those system calls:
- Now the build will succeed resulting in a small binary again:
- The problem now is that the default syscall stubs provided by the toolchain are not smart enough to detect the out-of-memory condition. Hence the memory allocation will always succeed leading to memory damage in case the memory runs out:
- We can fix that by copying the implementation of _sbrk(), a function responsible for memory allocation from the full version of newlib. First switch back to full newlib without syscall stubs in the VisualGDB Project Properties:
- Then set a breakpoint on the _sbrk() function (Ctrl-B):
- Start debugging. Once the breakpoint is triggered, VisualGDB will suggest downloading the newlib source code:
- Proceed with the download:
- Once the source is downloaded, you will see that the current _sbrk() implementation does check for the out-of-memory condition:
- Now switch back to newlib-nano + syscall stubs and copy the “smart” _sbrk() implementation to your main source file:
#include <errno.h> extern "C" caddr_t _sbrk (int incr) { register char * stack_ptr asm ("sp"); extern char end asm ("end"); static char *heap_end; char *prev_heap_end; if (heap_end == NULL) heap_end = &end; prev_heap_end = heap_end; if(heap_end + incr > stack_ptr) { errno = ENOMEM; return (caddr_t) -1; } heap_end += incr; return (caddr_t) prev_heap_end; }
- If you build your project now, your binary will be slim again:
- And trying to allocate too much memory will now result in a call to _abort() (as we are using libc++ without exception support) that by default generates a division-by-zero exception:
- One last thing: by default newlib-nano does not support float/double types in printf()/scanf() functions. stepping through the following code (requires <math.h>):
char *p = newchar[1024]; sprintf(p, "pi = %f", acosf(-1)); delete[] p;
- This can be fixed by enabling the corresponding version of newlib-nano via VisualGDB Project Properties:
- This costs ~10K of FLASH memory more, so only use it when you really need that functionality: