Porting to a new board is three distinct pieces of work:
- A platform expert — the kernel module that knows how to initialise this specific family of machines.
- A device tree or ACPI stub — the runtime description of what’s on the board.
- 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
└── MakefileA 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
IOPlatformExpertdirectly 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/releasePortexecuteEvent(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
IOInterruptControllerregistration. - Kernel boots then freezes silently. Early panic before console init. Build with
-DEARLY_PRINTK=1so panics spin-write to a hard-coded UART address.