Porting

Port darwinOS to a new board

Bring darwinOS up on a specific piece of hardware — platform expert, device tree, first driver, first shell.

Porting to a new board is three distinct pieces of work:

  1. A platform expert — the kernel module that knows how to initialise this specific family of machines.
  2. A device tree or ACPI stub — the runtime description of what’s on the board.
  3. At least one driver per subsystem you need — console, timer, interrupt controller, storage.

This guide walks the path from “I have a board on my desk” to “I have a shell prompt on its serial port.” It assumes the board’s architecture (arm64 or x86_64) is already supported — if not, start with bring up a new architecture first.

Prerequisites

  • A board with an accessible serial console (UART exposed on headers, JTAG, or USB-serial)
  • Its reference manual or TRM — you’ll live in it for a while
  • A working boot chain that can load an EFI application, a raw kernel image, or an ARM Image/Image.gz blob
  • darwinOS built from source for the board’s architecture

Step 1: Pick a platform

darwinOS groups boards into platforms — families that share an SoC or a common chipset. Each platform has one platform expert. Before you write anything, check whether your board falls under an existing platform:

ls darwinos/xnu/osfmk/kern/platforms/

If yours is listed but your specific board isn’t, you’re adding a board to an existing platform (easier). If nothing matches, you’re adding a new platform (this guide’s happy path).

Step 2: Write the platform expert

Create darwinos/xnu/osfmk/kern/platforms/<your-platform>/:

<your-platform>/
├── platform_expert.cpp
├── platform_expert.hpp
└── Makefile

A platform expert inherits from IOPlatformExpert and overrides a handful of methods:

class MyPlatformExpert : public IOPlatformExpert {
    OSDeclareDefaultStructors(MyPlatformExpert);
public:
    bool start(IOService *provider) override;
    bool configure(IOService *provider) override;
    IOByteCount savePlatformState(IOByteCount) override;
};

The work happens in configure(). Read the device tree (or ACPI tables), enumerate CPUs, register interrupt controllers, and attach IOKit nubs for every discovered peripheral. If the board has a single UART at a known physical address, this is where you document that and hand it off to the serial driver.

Refer to platforms/virt/ (the QEMU virt machine expert) for the canonical minimal example.

Step 3: Device tree

If your board’s firmware hands the kernel a device tree blob, you don’t have to do much — the platform expert reads it at boot. If it doesn’t, you either:

  • Write a static device tree (.dts) and embed it in the kernel image
  • Write a platform-specific enumerator that fills in IOPlatformExpert directly without a DT

The first option is what Linux does; the second is what classical XNU does on Apple platforms. Either is fine; pick based on whichever description of the hardware you already have.

Step 4: First driver — serial console

Without a working console you can’t see what’s happening, so start here. The kext guide covers the basics of driver structure; a serial driver is the simplest family.

Your driver inherits from IOSerialStreamSync, registers against your platform expert as the provider, and implements:

  • acquirePort / releasePort
  • executeEvent (baud rate, parity, etc.)
  • enqueueData / dequeueData

Once registered, XNU’s early-console machinery finds it and sends every subsequent IOLog out that UART. That’s the “moment of truth” of the port — when boot messages start appearing on your terminal emulator, you know the kernel is running.

Step 5: Storage and init

With the console working, the rest is incremental:

  • Timer / interrupt controller drivers if the platform expert didn’t handle them
  • Storage — MMC, SATA, NVMe, whatever the board has — so you can mount a real rootfs
  • Anything you need for launchd to start — most boards can stop here and boot to a shell

By this point you should have a bootable image for the board. Add instructions for it to the guide library under /guides/install-on-<your-board>/ and link it from the status page.

Step 6: Upstream

File a PR against darwinos with:

  • The platform expert under osfmk/kern/platforms/<your-platform>/
  • Any board-specific drivers under drivers/contrib/<your-platform>/
  • A new entry in the status matrix
  • A board-specific install guide

Port reviews focus on licensing (clean upstream), hardcoded-address hygiene (nothing outside the platform dir), and whether the platform expert actually documents what it discovered. Get early review before you finish — it saves rework.

Common pitfalls

  • UART clock mis-configured. The first character makes it out, subsequent ones are garbled. The clock-tree code in your platform expert is wrong.
  • Interrupts never arrive. The GIC isn’t initialised or the board’s interrupt routing is different from what you assumed. Check IOInterruptController registration.
  • Kernel boots then freezes silently. Early panic before console init. Build with -DEARLY_PRINTK=1 so panics spin-write to a hard-coded UART address.