How to Emulate Playdate (Arm) with QEMU

This will be useful to you if you're testing Arm-specific code (like assembly, or a JIT), and you would like a proper debugger. (If you only code in Lua, then this likely isn't relevant to you.) I did this on Linux, but QEMU is cross platform, so it should work anywhere, as long as you're using an x86 or x86_64 machine (I don't know about M1).

Please note that this guide is still WIP, and I haven't even completed the final step myself (though for many purposes, the first N-1 steps ought to suffice.)

Step 1: Download QEMU. You'll want to get to the point where you can run the command qemu-system-arm -machine help.

Step 2: You have to add a new build configuration to your project to build specially for QEMU. (This is because, as of qemu 4.2.1, QEMU doesn't have support for the Playdate's specific CPU, and even if it did, it wouldn't know how to emulate the firmware or peripherals.) QEMU has support for the Cortex-M3 (specifically the board mps2-an385); while the Playdate uses the Cortex-M7, these are similar enough -- they both use Arm V7-M architecture.

Here's my build command, using gcc:

arm-none-eabi-gcc $(SRC_FILES) shnewlib.c $(UDEFS) -mthumb -mcpu=cortex-m3 -march=armv7-m -T mps2an385.ld -Wl,-Map=output.map -DTARGET_QEMU -g  -o PROJECT_NAME_HERE

Attached is my linker script (mps2an385.ld) and my implementation of newlib. (The newlib implementation is optional, but it should enable e.g. printf to work. It uses "semihosting," basically a stopgap that lets you print things to the console without dealing with a UART, among other abilities.) I really don't know what I'm doing when it comes to linker scripts, but this seemed to work...

qemu-compatible-source-files.zip (2.9 KB)

You'll need to implement these things, too, which I put in my main.c:

static void __attribute__ ((noinline)) __semihost(unsigned int swi, const void* arg)
{
    asm("bkpt 0xab");
}

void _printstr(const char* s)
{
    __semihost(4, s);
}

void _printf(const char* fmt, ...)
{
    va_list args;
    va_start(args, fmt);

    int length = vsnprintf(NULL, 0, fmt, args);

    va_end(args);

    if (length > 0)
    {
        char buffer[length+2];

        va_start(args, fmt);

        vsnprintf(buffer, length + 1, fmt, args);

        va_end(args);

        _printstr(buffer);
    }
}

extern unsigned long __stack;
void Reset_Handler(void) __attribute__((naked));
void Default_Handler(void) __attribute__((interrupt));
extern void __attribute__((noreturn)) _start(void);

// Define the interrupt vector table
__attribute__((section(".isr_vector")))
void (*const g_pfnVectors[])(void) = {
    (void (*)(void))((unsigned long)&__stack),  // Initial stack pointer
    _start,                                        // Reset Handler
    Default_Handler,                               // Non-maskable Interrupt (NMI)
    Default_Handler,                               // Hard Fault
    // Add other interrupt handlers here as needed
};

void Default_Handler(void)
{
    while (1);
}

(Thanks to ChatGPT for implementing _printf -- I didn't have the patience to untangle the variadic structures and such.)

Finally, before you can build, you need to temporarily remove all playdate commands, remove the include of pd_api.h, and replace eventHandler with main. Print statements can be replaced with the _printf implementation above.

You should now be able to build and produce a binary that won't run on your x86 machine -- yet.

Step 3: It's time to get emulating! Here's your ticket:

qemu-system-arm -cpu cortex-m3 -machine mps2-an385 -nographic -semihosting -monitor none -serial stdio -kernel ./PROJECT_NAME_HERE

This should run your code. To debug, run these commands instead:

qemu-system-arm -cpu cortex-m3 -machine mps2-an385 -nographic -semihosting -monitor none -serial stdio -gdb tcp::1234 -S -singlestep -kernel ./PROJECT_NAME_HERE &

sleep 0.5

gdb-multiarch -iex "file ./PROJECT_NAME_HERE" -iex "target remote localhost:1234" -iex "set arm force-mode thumb"

(It's possible that you'll need to use "gdb-arm" or something instead of "gdb-multiarch.")

At this point, you should have gdb running in the terminal, and you can e.g. type b main and then c to put a breakpoint at the start of main. n to step, s to step in, f to step out, c to continue, and so on. (Or you can configure your IDE to connect with gdb, which will likely be more convenient.) You can use display/i $pc or layout asm to see your assembly code directly.

Step 4: I haven't done this yet! But notice how we're not even using the PlaydateSimulator anymore, and we don't have a display. Here's some future work to figure out:

  • launch qemu concurrently with the simulator, and forward calls to eventHandler from the simulator to qemu using IPC (inter-process communication). Similarly, playdate system calls can be forwarded in the other direction. This should already be enough for most purposes
  • could go the extra mile and write a "bootloader" that is able to copy the pdex.bin file directly to address 0x6000000 in qemu-emulated memory and invoke its eventHandler routine.
  • Somebody apparently has done some work to add QEMU support for the playdate board. This is beyond my ken, but maybe this is actually necessary to get the previous two steps to work?

And there you have it... playdate emulation using the simulator, more or less.

2 Likes

I hadn't thought of using QEMU, but now that you mention it, it should've been glaringly obvious.

Oh yeah I haven’t finished that, I should have more time this summer to finish the screen support and the sd card so we won’t need the simulator at all, though I will note that it requires full flash dumps to actually boot. But you’ll need 0 code modifications and all the peripherals just work