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.

  1. Begin with creating a normal Embedded project with VisualGDB project wizard:01-newprj
  2. Proceed with the default settings on the first page of the wizard:02-binprj
  3. On the device selection page select the default C library type and disable the C++ binary size reduction:03-toolchain
  4. Proceed with the default sample settings:04-sample
  5. Select a debugging method that matches your hardware:05-jtag
  6. 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:06-basicprjNote the memory utilization shown in the build output. Most likely you will use less than 2 kilobytes of FLASH.

  7. 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:
       virtualvoid 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:07-morecode

  8. We will now track down the source of the high memory consumption and show how to reduce it. Open the Embedded Memory Explorer pane:08-memexp
  9. Then click “Explore details”:09-memstatAs 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.
  10. 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:10-compactcpp
  11. If you now rebuild your project, you will notice that it requires about a hundred bytes of memory more than the original main() function:11-compactbuild
  12. 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:12-newdelYou will see that the exception-related functions have been linked again driving the image size to 70K+.

  13. 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:13-nano
  14. However, if you build your project now, you will get several ‘undefined symbol’ errors:14-sbrk
  15. 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:15-nosys
  16. Now the build will succeed resulting in a small binary again:16-nosysnewdel
  17. 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:17-outofmem
  18. 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:18-defaultlib
  19. Then set a breakpoint on the _sbrk() function (Ctrl-B):19-sbrk-bp
  20. Start debugging. Once the breakpoint is triggered, VisualGDB will suggest downloading the newlib source code:20-getlib
  21. Proceed with the download:21-instlib
  22. Once the source is downloaded, you will see that the current _sbrk() implementation does check for the out-of-memory condition:22-sbrk
  23. 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)
    {
       registerchar * stack_ptr asm ("sp");
       externchar end asm ("end");
       staticchar * 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;
    }
  24. If you build your project now, your binary will be slim again:23-sbrk-build
  25. 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:24-exit
  26. 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;

    You will notice that the %f specifier was simply ignored:25-format

  27. This can be fixed by enabling the corresponding version of newlib-nano via VisualGDB Project Properties:26-float-printf
  28. This costs ~10K of FLASH memory more, so only use it when you really need that functionality:27-printf-pi