Memory management can be either very simple or very difficult depending on your requirements in your project. If you do not allocate any memory dynamically during execution then memory management is normally very simple. But if you do have a lot of dynamic memory allocations in your tasks it can be very hard to make your application handle the memory correctly.
Types of Memory
Many types of memory exist, see figure 1.
All systems need RAM memory to able to store data, but many systems also place the code in RAM as this speeds up the execution time. When you decide which types of memory that you need in your system, there are a few things that you need to consider:
- Access time
- Amount of code and data
- Is it acceptable that data may be lost if there is a power loss?
- Dynamic upgrading of the software
- Data protection
For real-time applications execution speed normally need to be very high, so most applications execute in RAM.
An address layout needs to be done before the design of the hardware. You need to decide which types of memory that you need and how much of each type. For the boards that will be used during software development and test, it is normally very good to have much more RAM than what is needed on the production boards.
In address layout you decide how much memory you need of each type and what should be the address areas for each type of memory and also I/O, an example is shown in figure 2.
The next step is to specify how the memory should be used, i.e. decide where the code should be placed, both permanently and from where it should be executed (many times the code is placed in ROM or FLASH, but copied to RAM during system startup and then executed from RAM) and where data should be placed. Data also need to be split up into different sections, like vector table, initialized data, uninitialized data, constants. This should be well described in the manual for your linker. After that you have all the input data that you need to be able to create the linker file. An example is shown in figure 3.
To be able to see all the details of the linking you can specify in your link command that the linker should create a map file. The map file shows you all the addresses for both the code and the data sections.
Everything that is placed in RAM can be overwritten Sometimes this happens by mistakes caused by a bug in your program, and the consequences for memory corruption can sometimes be very drastic, and for many systems it is not acceptable at all. Protecting your memory by software routines will not work, as it would increase the execution time dramatically, so it needs to be done in hardware. To be able to do this you need a MMU (Memory Management Unit). Most modern CPU:s include a MMU. Using a MMU you can decide which parts of memory that are readable and/or writeable for each task. If a task tries to write to a memory address that has been configured to be read-only an exception will be caused by the MMU.
Using an MMU will slow down your application a little bit, as at every task switch the MMU needs to be reconfigured.
Some applications allocate all the memory needed (except stacks) during linking time and startup of the RTOS. Others may need to allocate memory during initialization of the application, memory that will never be returned. In these cases it is normally safe to use malloc and new.
But many applications need to allocate memory during execution time and after a short while return it. In these cases it may not be recommendable to use malloc/free and new/delete. One simple reason is that you almost never know how these functions are implemented, i.e. how do they really allocate memory, which algorithm do they use. And when you free the memory you may not get to know if this call was successful or not, in other words: Did you create a memory leak or not?
There are many ways of creating your own memory handler instead of using malloc and new, from very simple implementations to more sophisticated ones. A few simple ones (and do not forget that simple sometimes means better):
- Declare a large array that should be used for allocation of memory. Use a pointer to point to the first free byte and move the pointer every time you allocate memory. Works fine if you just need to allocate memory, not free it.
- Allocate a large piece of memory. Split it up into to buffers, either of one size or multiple sizes. Create one or multiple queues, and send the pointers to the buffers into the queue. Every time you need to allocate memory, just try to receive a message from the queue, and when you get a message, you also get a pointer to a free buffer. One benefit using this model, is that the queue also offers you a wait option, which may not be the case with malloc and new. Of course it is possible to create much more sophisticated memory handlers, which may help you to detect memory leaks, fragmentation, and other problems
I guess that almost every programmer has run into memory problems in their applications. For those of you who does not have this experience, probably you did have a problem, but maybe you did not look for it, or maybe the symptoms never showed up. My recommendation is that you should always analyze your memory usage if you do dynamic allocations, to make sure that everything is working correctly. So which type of problems can you run into:
- Memory leaks. Someone allocates a block of memory and either forgets to return it or fails to do it, and you did not get or check for an error code. If you have a lot of free memory, and there is a system reset from time to time, you may not even notice that there is a problem. Or maybe the problem shows up after x number of months, no one understands why, you make a system reset again, and everything seems to work fine again. Like a PC.
- Memory fragmentation. Your memory is so divided into to small blocks of memory that are allocated and memory blocks that are free, and when you need to allocate a larger block of memory, there is no block that is large enough, even if there totally may be a lot of free memory. Again, like your hard disc on PC.
- Memory corruption. Very typical e.g. when you program in C, a pointer that points on something totally different than it was supposed to do. These kind of problems can be very hard to find, a sophisticated memory handler may help you finding these kind of problems, but first of all make sure to check your pointers in the code.
- Spend time to make a good and working address layout and memory layout.
- In your design ask yourself:
- Can I avoid allocating memory dynamically?
- If not, do I need a simple or a sophisticated memory handler?
- If a sophisticated one, spend some time deciding what you really want the memory handler to do for you. Do not forget to add functionality that helps you analyze memory usage.
- In your tests of functions, tasks, system, actively look for memory problems. Maybe you have some good tools that can help you in doing that.