How the code is laid out.
The template is running. Now find out what is in it: which half is which, how they share memory, and which limits you have to design around rather than discover at three in the morning. This step is the practical read of the upstream programming guide .
Two source trees
The split you read about in step 1 is literally the directory structure:
rp/ C, runs on the RP2040 (Pico W)
src/
main.c entry point and the main loop
chandler.c command handler: drains the ring, dispatches
commemul.c/.pio the command channel
romemul.c/.pio ROM emulation, do not touch
sdcard.c FatFs on the microSD card
network.c Wi-Fi
term.c terminal rendering
aconfig.c this app's settings
CMakeLists.txt
build.sh
target/atarist/ 68000 assembly, runs on the Atari
src/
inc/ helper macros
main.s boot and dispatch (2 KB)
userfw.s your Atari-side code (6 KB)
Makefile
build.sh
desc/
app.json your app's descriptor, edited by you (step 10)
app.json.template the blank form to copy from
version.txt the version stamped into the build
dist/ build output, wiped and rebuilt each time
.vscode/ launch.json, settings.json
rp/ is portable. It is the part that would survive a port to another host
computer, and it targets the RP2040 described in Raspberry Pi's
microcontroller chips documentation . target/atarist/ is Atari-specific by definition. If another
platform is ever supported, it appears alongside as a sibling directory, and the C half
barely changes.
romemul.c, romemul.pio, commemul.c and
commemul.pio are the cycle-accurate machinery that makes the board look
like a ROM chip. They are timed against the Atari's bus and there is no margin in
them. Nothing you want to build requires editing these files. If you want to know what
they are doing, the hardware interface documentation walks
through the PIO state machine step by step.
The shared memory map
Understand this part properly, because it is how your two halves actually communicate. One region of memory has two addresses: the Atari sees it in its cartridge address space, the microcontroller sees it in its RAM.
0x20032000 on the microcontroller
is writing at $FA2000 as far as the Atari is concerned.
The cartridge image is 8 KB in total, and target/atarist/src/userfw.ld splits
it: 2 KB for main.s, which boots and dispatches, and 6 KB for
userfw.s, which is yours. Six kilobytes is the real budget for your
Atari-side code, and it is the constraint people most often forget to mention
to an AI assistant.
How a command gets across
The C half registers a handler and then pumps a loop. Commands arriving from the Atari are drained from a ring buffer and dispatched:
init_romemul(false);
commemul_init();
chandler_init();
chandler_addCB(my_command_handler);
while (keepActive) {
chandler_loop(); // drain the ring, dispatch commands
term_loop(); // render output
// ... your logic
}
Commands are polled, not interrupt-driven. That changed in v1.1.0. It means your main loop has to keep turning: block inside it and the Atari stops being answered. Long work needs to be broken into pieces across iterations.
On the Atari side, sending a command is a macro:
send_sync APP_TERMINAL_KEYSTROKE, 4
It expands to set up registers and call the routine that talks to the board. The pattern is
always the same. The Atari raises a command, the microcontroller notices it in
chandler_loop(), does the work, then writes back into shared memory.
The rules you have to design around
These are not style preferences. Each one is a consequence of how the hardware works, and breaking one produces failures that are very hard to read.
The Atari side cannot use normal RAM
Your cartridge code executes in place, out of the ROM address space above, and is never copied into memory first. That is why it cannot rely on the Atari's ordinary RAM being available to it. Anything that needs storage either lives in the address space you already have, or gets pushed across to the microcontroller's shared memory. This surprises everyone once.
Six kilobytes of Atari-side code
userfw.s gets 6 KB of the 8 KB cartridge image. If you find yourself writing
a lot of 68000, that is usually a signal the work belongs in the C half instead.
128 KB of the microcontroller's RAM is spoken for
The chip has 264 KB, and 128 KB of it is reserved as the shared region the Atari reads. That leaves roughly half for your application. Generous compared to 6 KB, not unlimited.
Flash writes are not free
Writing to flash has a limited lifetime and cannot sit on the critical path of ROM emulation. Save settings at deliberate moments, not every frame and not in a loop.
All four trace back to a handful of hardware realities: the RP2040's internal bus contention, the number of usable GPIOs, the 500 ns window the Atari gives a cartridge to answer. The architecture and design documentation explains each properly, and the hardware interface page covers the timing side. You do not need it to build something, but it turns these rules from arbitrary into obvious.
Settings and the SD card
Two things almost every app ends up needing. Defaults are declared as a table:
static SettingsConfigEntry defaultEntries[] = {
{ACONFIG_PARAM_FOLDER, SETTINGS_TYPE_STRING, "/test"},
{ACONFIG_PARAM_MODE, SETTINGS_TYPE_INT, "255"}, // 255: Menu mode
};
And the filesystem is FatFs, mounted against a folder:
FATFS fsys;
char *folderName = "/test";
int sdcardErr = sdcard_initFilesystem(&fsys, folderName);
if (sdcardErr != SDCARD_INIT_OK) {
DPRINTF("Error initializing SD card: %i\n", sdcardErr);
}
DPRINTF is how you will see what your code is doing. Step 9 covers where that
output actually goes.