blog/content/posts/avr-memory-model.md
Asger Gitz-Johansen 479a1a3d27 feat: I found my old blog
It only had 2 posts, but whatever
2025-04-13 19:51:55 +02:00

5.4 KiB

+++ date = '2019-03-24' draft = false title = 'AVR Memory Model: The Practical Explanation' tags = ['avr', 'programming', 'memory'] categories = ['technical'] +++

There is A LOT of people that have already explained this, but I personally don't feel like going through billions of forum posts, with most of them just dying out somewhere in 2008 with no better answer than "I figured it out, thanks".

Regardless. BIG shoutout to the amazing people at AVR Freaks. They are really cool. Seriously. Make a user and ask them about anything, and they'll help you.

Disclaimer

I have only been debugging the memory usage of a specific ATMega chip. I don't know if other AVR chip-types use the same, but this explanation should be valid for all MCUs that the avr-libc package supports.

I also assume that GNU/Linux is being used on the development computer.

Open-Source development tools

The avr-gcc compiler chain is an open source effort to have C/C++ for AVR Atmel chips. They do provide some rudimentary C++ support, but there's no STL and the new and delete keywords are not implemented by default. Even purely virtual functions doesn't work out of the box.

But don't fret! There are ways to implement those features manually. See my other post about getting the build environment up and running.

The Memory Model

As the avr-libc developers explain, there's typically not a lot of RAM available on most many devices and therefore it's very important to keep track of how much memory you are using.

{{< centered image="/malloc-std.png" >}}

All of these symbols SP, RAMEND, __data_start, __malloc_heap_start, etc. Can be modified in the compiler, but the picture above gives the default layout (for an ATMega128 MCU). It goes without saying, that if you don't have an external RAM chip, you won't be able to utilize the extra RAM space for that. Otherwise, the memory addresses are pretty straight forward: 0x0100 => 256 bytes is the start of the memory, 0x10FF => 4351 bytes is the end. If you're wondering where the RAM ends on your specific MCU, you can usually simply open the spec-sheet of the chip and see the amount of available memory is in it. For the ATMega128 that number is 4096 (4351 - 256 = 4095 (the spec-sheet also counts the 0th byte)).

The avr-libc Memory Allocators

Now for the juicy part. whenever you malloc something in your program, the allocator first writes a 2-byte free-list entry that tells the system how big your object is.

Example:

/* ... */
// Allocate an array of 5 integers
int* my_heap_object = static_cast<int*>(malloc(sizeof(int) * 5));
/* ... */

Assuming that the memory has been cleared on chip-startup, the above example ends up with the memory setup looking like this: (Don't mind the specifc memory addresses. If you're curious, you can try doing this, by attaching avr-gdb to a simulator or On Chip Debugger (OCD)).

gdb: > x/16xb my_heap_object
0x800100:	0a 00 00 00 00 00 00 00 
0x800108: 	00 00 00 00 00 00 00 00

The first bytes at address 0x800100 are 0a and 00. These bytes are the free-list entry and explains how "big" the object is. When reading this, we have to remember that the model is littleengine-based (meaning that the bytes are switched), so we actually have the value of 0x000a, meaning 10 in decimal. This makes a lot of sense, since we allocated 5 ints, that is of size 2 (16bit integers).

The memory dump shows 16 bytes in total, so the last 4 bytes displayed in the gdb example are not part of the object. However, if you look at the Memory Model picture again, you can see that the __brkval value points to the biggest memory address that has not been allocated. In our example, if you check where the __brkval points to after our allocation, we get:

gdb: > __brkval
$ 268

268 in hexadecimal is 0x10c, and if interpreted as an address we get 0x80010c, which fits very well with our example, since it is exactly 12 bytes away from where the free-list entry of my_heap_object is located at.

When free-ing the object again, the deallocator looks at the free-list entry at the given address, and wipes the free-list entry. This is why you should not free a dangling pointer. Freeing something that is not really free-list entry will result in undefined behaviour, and I think we all know how bad that is. (Even though the AVR environment is actually very good at handling it. In my experience, it usually just crashes and starts over.) However, as explained in the avrlibc documentation, freeing the NULL value, doesn't do anything. So remember to assign your free'd pointers to NULL afterwards.

Wrapping up

The memory allocators of AVR can be very confusing and if you don't keep your thoughts straight when programming, you can very easily get yourself into a lot of trouble. Since STL is not available to avr-gcc programmers, we dont have our glorious smart pointers, so we should implement them ourselves (or use arduino's implementations). That might become a future blogpost.

Regardless, I hope this helps the lost souls that are trying to actually use these tools.

{{< centered image="/6616144.png" >}}