Dealing with the GBA's various storage types
The GBA has virtually unlimited ROM space and comparitively tiny RAM space. Using both correctly and efficiently is important. Here is a little bit of background on them, and some issues I've faced.
ROM
Read only memory is exactly as it sounds, memory that can be read, but never changed. It was traditionally found in mask ROM chips on the cartridge board, but nowadays it's often simulated on a flash cart, or some other rewritable medium. Despite often being present on rewritable medium now, you still can't write to it.
The GBA memory map dedicates 32 megabytes to cartridge ROM. For modern homebrew GBA devs that are likely working in small teams, that pretty much equates to infinite space. Of course it isn't actually, but 32 megabytes is a lot of space that would take quite an ambitious project to fill. So far in my GBA game, I've not thought about saving ROM space whatsoever—quite the opposite actually— and my game is less than 2 megabytes without music. Music does tend to be quite large though. In my game, each song in the game is looking to require just shy of a megabyte of space.
RAM
The GBA has a total of 288kb of RAM, divided into two sections: IWRAM and EWRAM.
IWRAM
Internal work RAM is 32kb in size and is the fastest storage available on the GBA (other than the CPU's registers of course). This is because it is embedded directly within the CPU and has a 32 bit bus. The 32 bit bus means it's the ideal location to run ARM code, which is 32 bit. For space savings, GBA games mostly run in 16 bit thumb code. Putting thumb code here is possible, but with minimal benefit.
EWRAM
External work RAM is 256kb in size, has a 16 bit data bus, and is slower than IWRAM.
When does the speed matter?
I've not encountered any situations where I truly needed the speed of IWRAM or noticed any difference here. EWRAM is still perfectly fine 99% of the time. There is one exception though, interrupt handlers are usually written in ARM code, and stored in IWRAM. This is especially true for the hblank interrupt, which triggers after the screen has drawn a single line. Your hblank interrupt has a mere 272 CPU cycles to do its thing, which is roughly 0.07 milliseconds.
Choosing where to stick stuff
If you are using libtonc and devKitPro, it's actually very easy to decide where things should go: ROM, IWRAM or EWRAM.
Code goes to ROM
Code, ie functions, go into ROM. Unless you add the extension .iwram.c to the file, then the compiler will place this code in IWRAM instead.
Constant things go to ROM
If you add the keyword const to data, then that data will also go into ROM. It's very important to not forget this keyword, if something will truly never change, it almost always should go into ROM and not take up any precious RAM space.
Changable things by default go into IWRAM
If you declare something in your file like u32 x;, then that variable will by default go into IWRAM. I'm not sure why IWRAM is the default, but it is. If you're not careful, it's very easy to use up all of the IWRAM. Once you go over the limit, your game will fail during the linking step of your build.
But not within functions
This only applies to variables that live outside of functions. If you declare u32 x within a function, then that variable will go onto the stack when the function is executing, and be pulled out of memory once the function finishes.
Moving stuff to EWRAM
If you are using libtonc, the macro EWRAM_DATA will move that item into EWRAM instead of IWRAM.
If you're not using libtonc, here is the macro, easily defined yourself if needed:
//! Put variable in EWRAM. #define EWRAM_DATA __attribute__((section(".ewram")))There are other similar macros, here they are in tonc_types.h.
Putting too much into IWRAM
If your game puts too much stuff into IWRAM, then when you are building your game you will get an error like this.
linking cartridge /opt/devkitpro/devkitARM/bin/../lib/gcc/arm-none-eabi/15.1.0/../../../../arm-none-eabi/bin/ld: address 0x3008660 of /home/matt/dev/pppro/pppro.elf section `.data' is not within region `iwram'This fairly cryptic message is just telling you that your game is trying to put more stuff into IWRAM than will fit.
How to deal with this
A simple way to combat this is to add --print-memory-usage to your LDFLAGS. Then when you build your game, you'll get an overview of how the memory regions are looking.
linking cartridge Memory region Used Size Region Size %age Used rom: 3412416 B 32 MB 10.17% iwram: 29836 B 32 KB 91.05% ewram: 12352 B 256 KB 4.71%That's good to keep an eye on things, but when you go over it's not too helpful.
If you do go over, the easiest thing to do is seek out large stuff in your game and move it to EWRAM with the EWRAM_DATA macro.
Getting the details with a linker map file
The linker option -Map=file will cause the linker to output a map file. This is a very verbose text file showing exactly where everything in your game ended up.
Rather than explain this file here, keep reading below where I explain how I used this file to solve a conundrum I found myself it.
Const data can sometimes go in IWRAM?
I made a change to my game that seemed innocent enough, I changed around how the constant data was structured. But after I did that and ran a build, I got the ".data is not with region iwram" error. What?
My game contains a struct like this:
#define PUZZLE_COUNT 28
struct PuzzleCollection { ... const struct Puzzle *puzzles[PUZZLE_COUNT]; ...};But a change to my game required that array of puzzles to be of varying length. So I changed the struct to this.
struct PuzzleCollection { ... const u32 puzzleCount ; const struct Puzzle **puzzles; ...};This change allows this struct to refer to as many puzzles as it would like, my data then looked something like this.
const struct Puzzle somePuzzle = { ... };
const struct Puzzle *someCollectionPuzzles[] = { &somePuzzle };
const struct PuzzleCollection someCollection = { ... .puzzleCount = 1, .puzzles = someCollectionPuzzles ...};Seems fine ... I think? Everything has const and everything is known at compile time. But for some reason, someCollectionPuzzles[] ends up in IWRAM. I'm far from a C expert (or GBA expert), so I'm probably just doing something wrong, but I don't know what?
But when I first got the overflow error, the only thing I knew was something was being place into IWRAM that shouldn't be, but I didn't know what.
Solving this with the map file
I added -Map=mapfile.map to the linker arguments, and did a build. Once it finished, I examined mapfile.map and it told me what was unexpectedly going into IWRAM.
I searched for someCollectionPuzzles in the map file and found this.
.data 0x03006d48 0x40 someCollection.o 0x03006d48 someCollectionPuzzles .data means this array was placed in RAM. If it was placed in ROM instead, it would have been put into the .rodata section. It turns out arrays of pointers are always placed in .data for some reason, even const ones, I'm really not sure why.
The fix
I fixed this by changing the struct's defintion around. I defined a variable sized array of pointers in the struct itself.
struct PuzzleCollection { ... const u32 puzzleCount ; const struct Puzzle *puzzles[];};Then defined the data like this.
const struct Puzzle somePuzzle = { ... };
const struct PuzzleCollection someCollection = { ... .puzzleCount = 1, .puzzles = { &somePuzzle }};And for some reason, inlining the array into the struct moved that array into .rodata and solved my problem.
Oh here is why
After I posted this article on Bluesky, nytpu explained why the "const" data ended up in IWRAM, it's not truly const.
Inlining the array is one fix, but another fix would have been to define the array like this:
const struct Puzzle *const someCollectionPuzzles [] = { &somePuzzle };The added const keyword tells the compiler it's a constant array of constant pointers, and then that array ends up in the ROM as expected. Thanks nytpu!