Using exceptions in C++ embedded software

For several years I have been the lead developer of RepRapFirmware, which is firmware for 3D printers running ARM Cortex processors. Until recently I saw no compelling reason to use exceptions, so I compiled the firmware with gcc options -fno-exceptions -fno-rtti in order to keep the code small.

Now I am implementing an expression parser to support a much richer GCode flavour including conditional statements and loops. This creates great opportunity for users to write GCode commands that violate syntax or semantic constraints, and it is important to provide good error messages when this happens. This meant that each function involved in parsing and checking the GCode needed to return details of any error encountered; and to check whether any of the functions it called returned error information so that the parse could be abandoned and the error passes up to the calling function, for eventual reporting to the user.

This is an obvious example of where C++ exceptions provide a much cleaner solution. Since a line of GCode that contains syntactical or semantic errors can’t be executed anyway, why not report an error by throwing an exception object, which is caught at the top level?

So I decided to change to compiling the project with exceptions enabled, provided this could be done without using too much of the remaining available flash memory. But it turned out to be less straightforward than I expected.

First issue: missing library function

Initial results of compiling using -fexceptions instead of -fno-exceptions looked resulted in the linker complaining about missing function __gxx_personality_v0. A quick Internet search revealed the solution: add supc++ to the library list in the linker options.

With this fixed, I was able to compile and link the firmware, and it ran. Flash memory usage was increased from about 425Kb to about 445Kb. I was hopeful that I had achieved my goal. But I wasn’t actually using exceptions yet, just compiling with exception support enabled.

Second issue: incorrect linker script

When I tried actually throwing an exception, it didn’t work. The code in the catch block wasn’t executed, instead the exception went to std::unexpected.

Another Internet search revealed a possible problem with the linker script. The original linker script included this:

/* .ARM.exidx is sorted, so has to go in its own output section. */
PROVIDE_HIDDEN (__exidx_start = .);
.ARM.exidx :
{
    *(.ARM.exidx* .gnu.linkonce.armexidx.*)
} > rom 
PROVIDE_HIDDEN (__exidx_end = .);

It seems that this doesn’t guarantee that symbols __exidx_start and __exidx_end will correspond exactly to the start and end of the .ARM.exidx section, which contains the exception tables. The correct form is:

/* .ARM.exidx is sorted, so has to go in its own output section. */
.ARM.exidx :
{
    __exidx_start = .;
    *(.ARM.exidx* .gnu.linkonce.armexidx.*)
    __exidx_end = .;
} > rom

But even with this change, exceptions were not being caught.

Third issue: use of newlib-nano instead of newlib

A further Internet search revealed that programs linked with newlib-nano (the cut-down version of the standard library for embedded systems) can’t use exceptions. Apparently this is because newlib-nano is compiled with exception support disabled. I find this odd, because I wasn’t trying to propagate exceptions through functions defined in newlib-nano. Nevertheless, it seems that if you want to use exceptions anywhere in your program, you can’t use newlib-nano.

A suggested workaround was to recompile newlib-nano with exception support enabled. But I would much rather use standard libraries as they are supplied, rather than hack them or the build process. So I decided to try using the full version of newlib instead of newlib-nano.

Fourth issue: newlib uses too much flash memory

Changing the linker options to use newlib instead of newlib-nano increased the size of the binary file from about 445Kb to 512Kb. As some of the processors targeted by RepRapFirmware have only 512Kb flash memory, this was unacceptable because it left no room for the new code that I wanted to add.

After poring through the linker map file, I established that much of the additional memory was being used by vsnprintf and related functions. This was a surprise, because I had already provided my own implementations of the printf family of functions. The standard implementations of these functions use a lot of code and allocate memory on the heap, which is undesirable for embedded software. My replacement versions are smaller and use no heap memory. They don’t provide the more obscure formatting functions, and they don’t guarantee the same degree of accuracy when printing very large or very small floating point numbers, but that doesn’t matter in this application.

Further investigation revealed that the reason for these functions being pulled in by the linker was that newlib provides a default handler for std::unexpected that tries to be helpful by printing out the call stack, and in doing so it uses sprintf. This is completely useless in an embedded system that has no output console except when debugging. So I wanted to replace the default handler by a call to my standard crash handler, which logs the problem to flash memory and restarts the system. This turned out to be straightforward. The following code accomplishes it:

[[noreturn]] void Terminate() noexcept
{
    // insert your own termination handler code here...
}

namespace __cxxabiv1
{
    std::terminate_handler __terminate_handler = Terminate;
}

This immediately saved around 50K of flash memory space. I was able to save a few kb more when I observed that the mktime function (which RepRapFirmware calls) was pulling in a lot of code to handle time zones (which RepRapFirmware doesn’t need). The following replacements remove most of that code:

extern "C" void __tzset() noexcept { }
extern "C" void __tz_lock() noexcept { }
extern "C" void __tz_unlock() noexcept { }
extern "C" void _tzset_unlocked() noexcept { }

The result was that exceptions could be thrown and caught on my ARM Cortex M4 test system, and the code has expanded from about 425kb to about 450kb. But that wasn’t quite the end of the story, because the build for the ARM Cortex M7 processor crashed.

Fifth issue: memcpy in newlib crashes on ARM Cortex M7 when accessing non-cacheable memory

Debugging the crash revealed that when memcpy is called to move a block of memory that ended 1 byte before a 4-byte aligned boundary, it copies the last 2 bytes using an unaligned word access. However, on the ARM Cortex M7, memory that is flagged as non-cacheable in the MPU (because it is accessed by DMA) may only be accessed using aligned reads and writes. This didn’t occur when I was using newlib-nano, because that has a much simpler implementation of memcpy that copies one byte at a time.

Recompiling newlib with compiler option -fno-unaligned-access would have fixed it. But again, I didn’t want the burden of maintaining a special build of newlib. Instead I copied the source code of memcpy into my project, so it has its own copy which is compiled using -fno-unaligned-access like the rest of the project. For safety I did the same for memmove, memset and memcmp too.

Final thoughts

The gcc compiler and newlib are great open source tools and generally work very well. Unfortunately, they suffer from poor documentation, as do the builds of newlib and newlib-nano for embedded processors. Here are some questions that I haven’t found answers to:

  • Is it permissible to compile some parts of a C++ project using -fno-exceptions and other parts using –fexceptions? Exceptions are handled by tables that describe the code through which exceptions are propagated, and the -fexceptions option causes these table to be generated (as well as enabling the throw and catch keywords). My expectation was that such a mix should work, with the proviso that if an exception tries to propagate through a function for which there is no exception table (because it has been compiled using -fno-exceptions), std::terminate will be called. But that doesn’t explain why I can’t catch exceptions at all if I link with newlib-nano instead of the full newlib. The exception handling and stack unwinding support is in libsupc++, so it shouldn’t be due to missing functions.
  • Why is newlib for the ARM Cortex M7 compiled with unaligned memory accesses enabled? It’s understandable for newlib-nano because unaligned accesses might save a few bytes of code; but it seems perverse to me to compile newlib in this way, given the existence of memory regions on the M7 for which unaligned accesses are prohibited, and that unaligned accesses are slower than aligned accesses.

Embedded processors get more capable every year; but embedded systems are still very different from desktop systems. They have long running programs (which makes them liable to heap fragmentation if dynamic memory allocation is used), no virtual memory, and limited RAM and flash memory. Perhaps it’s time for a library build in between newlib and newlib-nano: one that take a compromise position between the two. It would support exception handling, omit features that are very expensive on memory such as the default handler for std::terminate, and recognise that unaligned memory accesses are not always permitted even on processors that generally support them.

 

 

This entry was posted in Embedded software and tagged , , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s