Part 3: Memory Optimization in Embedded Systems in the C Language – ROM

In the previous articles, we discussed optimizing memory consumption in embedded systems in terms of development tools and ways to optimize RAM. Now it's time for the final topic – ROM optimization.

Challenges related to ROM optimization

In ROM, we will find constant values, the initialization data of the .data section, and the instructions of our application. Between the code written by the developer and the contents of memory is a compiler, which translates the code into instructions that the processor understands. Modern compilers are very good at optimizing code to reduce its size. Even if we manage to find some clever trick in the code to save a few bytes, it may turn out that such a change in our code will not function for long. This is because it will only work for a particular compiler. What about writing code for various platforms – with different architectures and compilers? The challenges only increase, because this kind of activity can make our code difficult to maintain.

When we are up against the wall and no more options are available, there is nothing left to do but learn the ins and outs of a particular compiler. If we need to support multiple platforms, we can optimize code fragments for a particular platform and use the preprocessor to define which code to use in a given situation. However, such practices mean generating additional paths in our application, which require further testing and therefore become a potential source of new bugs.

What is worth optimizing in ROM?

What certainly needs to be optimized are first and foremost the algorithms and logic of the application. What does that mean?

  • Simplify the operation of the code. It is worth trying to write the same functionality using fewer operations. Reducing the number of operations means not only less ROM used, but also a faster application.
  • Look for code sections in the application that perform similar operations. Common parts can then be encapsulated into functions, reducing ROM consumption.
  • If you have a lot of RAM and you store data in ROM that can be generated, it is worth spending some CPU time to generate such data and store it in RAM. If it is not possible to generate the data, you can store compressed data in ROM and then decompress it into RAM at initialization. We would then gain ROM at the expense of RAM and CPU time. The code to generate or decompress will consume some ROM, so this method can only work for large amounts of data.

Firmware update and the use of bootloader and apploader

Among the features found in embedded systems, it is worth looking at software updates. OTA (Over-the-Air) updating, or wireless updating, is common in wireless devices.

A popular method of updating software is to have two partitions in the ROM for an application – APP1 and APP2. The bootloader decides which partition contains the latest software version. During the software update process, the application image is downloaded and then stored in an adjacent partition. 

This solution is immune to data loss because it does not overwrite the current version of the application. After a loss of power or connection, the application will launch correctly and the update process can be safely repeated or canceled. However, this solution is not without its cost, as the partitions for the application are of the same size, so we use about 50% of the available memory.

An alternative approach is to use an apploader, which is an extended version of the bootloader. One feature of the apploader is a wireless stack that allows the new software version to be downloaded and written directly to the application partition (APP). This solution allows to increase the amount of memory allocated to the application, since the apploader implementation will be much lighter than the target application. 

This solution does not allow you to use the previous version of the application after an aborted update, because the apploader will overwrite the current version with the new application image. After a loss of power or connection, the device is assumed to be in update mode, and the process must be performed correctly to restore the device to full functionality.

Key Takeaways

Managing memory usage in embedded systems is a complicated subject and can present many challenges. However, with a good understanding of your system and optimization methods, most of these problems can be solved. Our series of articles is intended to provide guidance to those trying to solve memory issues in their system. Each part presents some best practices to avoid future complications. Not all places where memory is wasted are visible at first glance. This is why it is worth investing in automated solutions that detect memory problems early in the project.


Author

Mateusz Szpiech

Embedded Software Engineer at Comarch