Skip to content

Running it in the emulator

You do not need an iPod to work on Chikuma. The machine in qemu-ipod/ is a QEMU model of the S5L8702 that boots both Apple's RetailOS and Chikuma, against the same disk image -- which is the whole reason claims in this project are checkable.

The two commands

cd qemu-ipod
./retailos.sh                                   # Apple's firmware, in a window
./run.sh --payload ../retailos_re/payload.elf   # Chikuma instead

retailos.sh is run.sh with the device's identity and the savestate conveniences added. Go to run.sh directly for a payload or an unusual configuration. --help lists everything.

Headless, for a single frame:

./run.sh --headless --shot out.ppm

It reaches the Language screen in about 5 seconds.

Driving it

Live, in the window: two-finger scroll or a mouse wheel turns the click wheel, Up / Down step it, Enter or Space is SELECT, Left / Right are PREVIOUS / NEXT, M or Esc is MENU, P is PLAY/PAUSE. Left and right click are SELECT and MENU. The pointer is absolute, so the window never grabs your mouse.

Scripted, from Python -- tools/ipodmachine.py is the interface, and tools/drive.py is a thin CLI on top of it. Import it rather than re-implementing the QMP handshake; half a dozen throwaway scripts have done that and each got some of it wrong.

from ipodmachine import Machine

with Machine(audio="coreaudio", wav="/tmp/out.wav", debug="sm1") as m:
    m.wait_for_screen()      # boot, then the first frame with real content
    m.press("select")        # a real 120 ms press
    m.scroll(24)             # a dozen notches move one menu row
    m.shot("/tmp/menu.png")
    print(m.uart())          # what the guest printed

Sockets, logs and the gdb port are per process, so a probe never disturbs an emulator you already have running, and two probes can run at once.

Three things that will catch you

A press must have a duration

Sending down and up in one batch is a zero-length press, and the firmware's debounce never sees it. About 120 ms of guest time is a tap; a second is a press-and-hold. The monitor's sendkey gets this right on its own.

The wheel is coarse, and its gain is not fixed

A menu row costs somewhere between six and a dozen notches depending on the list, so an open-loop scroll(N) that works on one screen overshoots on the next. Drive to a known row by screenshotting and checking, not by counting notches. tools/menu_walk.py walks a path by row for exactly this reason.

--warp breaks interaction

icount keeps the instruction count exact, and by default virtual time is tied to real time. With --warp (sleep=off) QEMU skips the idle and the guest clock runs about 35× fast -- a 100 ms press arrives as a three-second hold, and the firmware's 150-second idle sleep lands about four seconds after boot. Use --warp only for a batch boot that takes no input.

Which disk you boot decides what is there to play

The library lives on the disk image. The default (retailos_re/build/ipod_apm.img) is a small test volume. A larger one with movies and podcasts is used for the media work and is passed explicitly.

If the library reads as empty -- menus present, songs absent -- the FireWire GUID is wrong: the retail database is bound to the GUID it was synced with, and the machine has to present the same one.

Debugging

tools/hook.sh attaches gdb hooks using the Ghidra project's own function names:

./tools/hook.sh 'Hook("volume_probe_and_register")' 200

Savestates are worth the habit. Anything that takes an hour of guest time to reach should be frozen just before the interesting moment; one such savestate turned a 90-minute end-of-track experiment into a three-minute one.

The retired emulator

emu/ is a Python + Unicorn machine that was the oracle until 2026-08-09. It is reference, not a tool -- read it, do not run it.

It was retired for one reason, learned twice: Unicorn delivers interrupts only at slice boundaries, while QEMU raises the line the instant a device asserts it, as the hardware does. Three bugs in a single session lived exactly in that gap, including an interrupt landing in the middle of a kernel service -- which faulted on every QEMU boot and passed the Unicorn gate every time. A model that cannot produce the hardware's timing cannot be the oracle for behaviour that depends on it.

What emu/ still is: the record. Its device models carry the measured comments the QEMU port was checked against, and the reports cite it throughout.