Platform: the E and P cluster tag and the scheduler patch. Device tree: the unixnu,cluster-type property. GPU: the mapping, unmap and submission rules and the refusing encoder. Power: a microphone capture section with the sheng route, the host clock correction, and the touch, frame and fence-wait governor rules. Display: reading the DPU pipes and the experimental direct scanout. UFS: the stage 1 windows owned by the SMMU driver. Driver pages state current behavior only.
12 KiB
Device tree
Format 2 compiles a hardware tree and generates the Apple device tree centrally.
flowchart TD
A[SoC DTSI] --> C[dtc]
B[Board DTS] --> C
C --> D[Hardware DTB]
D --> E[Common ADT generator]
F[OS profile] --> E
G[Payload inputs] --> E
E --> H[kernel.macho.adt]
Firmware and bootloader interfaces use FDT. XNU receives the ADT sidecar next
to kernel.macho. DarwinBoot reads it from the payload entry directory.
Procedure
- Gather the board DTS, selected SoC DTSI and the properties consumed by the drivers. Set the source and include paths in Configuration.
- Describe hardware with the bindings: parent cell widths, ranges, interrupts, references and enabled nodes. Check the required XNU nodes.
- Place Apple platform properties in the selected OS profile, with board overrides where required. Match the root UUID and payload handoff options to the actual inputs.
- Run standalone conversion to compile a DTB and generate an ADT with the selected inputs. Review compiler diagnostics and generated node names against the driver matching requirements.
- Run
./unixnu payloadand./unixnu bootimgusing the saved build inputs. Inspectdevice-tree/hardware.dtband the payload report under the resolved output directory. The firmware DTB is a separate boot-image input.
Configuration
[device_tree]
source = "device:dts/sheng.dts"
include_dirs = ["soc:dts"]
The payload step runs dtc and writes device-tree/hardware.dtb under the
resolved output directory. scripts/unixnu/devicetree.py reads FDT, converts
hardware properties, adds the OS and payload properties, and encodes ADT.
The board DTS includes its SoC DTSI:
/dts-v1/;
/include/ "sm8550.dtsi"
/ {
model = "Xiaomi Pad 6S Pro";
compatible = "xiaomi,sheng", "qcom,sm8550";
};
The complete board source supplies memory, enabled CPUs, and peripheral inputs. See the device template and SoC support.
Hardware bindings
| Property | Input | ADT representation |
|---|---|---|
compatible, device_type, reg-names, interrupt-names, clock-names, method |
NUL-terminated strings | String bytes |
phandle, linux,phandle, #interrupt-cells, #iommu-cells, clock-frequency, timebase-frequency |
One big-endian cell | Little-endian u32 |
redistributor-stride |
One or two cells | Little-endian u64 |
interrupt-controller, dma-coherent |
Empty property | Empty property |
reg |
Address/size tuples using the parent's cell widths | Translated physical u64 address/size pairs |
interrupts |
Specifiers using the referenced controller's #interrupt-cells |
Little-endian cells |
interrupt-parent |
Controller phandle | Little-endian phandle |
iommus, clocks |
Phandle and provider-defined argument cells | Little-endian cells |
unixnu,adt-name |
Exported hardware node name | Child name under /arm-io |
unixnu,cluster-type |
"E" or "P" on a CPU node |
cluster-type, the character as a little-endian u32 |
status |
okay, ok, or disabled state |
Selects enabled nodes |
Every translated bus declares ranges. An empty ranges gives an identity
mapping. Each register range must have one matching translation. Referenced
providers require unique nonzero phandles and their argument-cell counts.
Interrupt specifiers must match the selected interrupt controller.
The generator exports enabled nodes with unixnu,adt-name. Exported names are
unique. Add a typed binding in the common converter when a driver needs another
property. FDT integer cells are decoded before ADT encoding.
The root supplies model and compatible. One enabled memory node provides a
contiguous DRAM range. /cpus supplies timebase-frequency; each enabled CPU
has reg containing its MPIDR affinity and compatible. The first enabled CPU
is the boot CPU. Multiple CPUs require /psci with method = "smc" or "hvc".
OS properties
The OS profile declares typed tables under [adt], such as:
[adt.chosen.string]
osenvironment = "main"
[adt.defaults.u32]
sleep-disabled = 1
Supported property types are string, strings, u32, u64, and hex bytes.
The tables populate root, chosen, defaults, options, product, and
buttons. The common generator creates CHRP NVRAM, a fresh 64-byte random seed,
platform identity, root matching, and payload memory-map placeholders.
Standalone conversion
dtc -I dts -O dtb -i ../UniXNU-sm8550-common/dts -o /tmp/sheng.dtb ../UniXNU-sheng/dts/sheng.dts
python3 scripts/make-adt.py --dtb /tmp/sheng.dtb --os-profile profiles/os/ipados-17.7.11-21H461/profile.toml --board ../UniXNU-sheng/board.toml --unique-chip-id <hex> --data-ramdisk --trust-cache --output /tmp/kernel.macho.adt
The command selects memory-map placeholders for the supplied payload inputs.
unixnu build payload uses the fully resolved profiles and its actual inputs.
Apple device tree encoding
An Apple device tree uses a compact node encoding:
u32 property_count
u32 child_count
for each property:
char name[32]
u32 length
u8 value[length]
padding to 4 bytes
for each child:
node
Every node carries a name property with its own name and a terminating NUL.
The common encoder writes little-endian ADT fields and four-byte property padding.
Required nodes
The generic ArmDTPlatform requires these top level nodes:
| Node | Required contents |
|---|---|
/chosen |
random-seed, CHRP NVRAM proxy, DRAM description, osenvironment |
/cpus |
One cpuN child per core |
/arm-io |
Bus node with #address-cells, #size-cells, ranges |
/defaults |
Platform defaults |
/options |
Platform options |
The platform expert fails configure() when one of these nodes is absent. It also fails when /chosen has no usable NVRAM proxy.
NVRAM proxy
XNU publishes the real IODTNVRAM from /chosen. The proxy uses the CHRP bank format:
| Property | Contents |
|---|---|
nvram-bank-size |
Bank size, 32-bit |
nvram-bank-count |
Number of banks, 32-bit |
nvram-current-bank |
Current bank index, 32-bit |
nvram-proxy-data |
The bank image |
The bank image contains a nvram header, a common header with a two kilobyte data area, and a free header. The common data area has an Adler-32 checksum at offset 16.
An empty proxy is enough for a device with no persistent NVRAM controller.
Chosen properties
| Property | Type | Purpose |
|---|---|---|
random-seed |
64 bytes | Kernel entropy seed |
unique-chip-id |
64-bit | Seeds IOPlatformUUID with chip-id |
chip-id |
32-bit | Platform identity seed |
osenvironment |
string | main, recoveryos or ramdisk |
dram-base |
64-bit | Physical DRAM base |
dram-size |
64-bit | Physical DRAM size |
crypto-hash-method |
string | Image4 hash method, for example sha2-384 |
root-matching |
dictionary | Root volume match dictionary |
ephemeral-storage |
32-bit | Marks the writable Data volume as ephemeral |
darwinboot-memory-size |
64-bit | Contiguous window request, 256 MiB to 8 GiB in 2 MiB steps |
amfi-only-platform-code |
32-bit | Keeps AMFI in its platform code policy |
boot-manifest-hash |
bytes | Boot manifest hash for the owner path |
unique-chip-id and chip-id seed IOPlatformUUID. Without them, callers that pass a zero timeout to gethostuuid() wait indefinitely.
Memory map
/chosen/memory-map holds the placeholders DarwinBoot fills:
| Property | Length | Filled with |
|---|---|---|
RAMDisk |
16 bytes | Address and size of the dense Data image |
PackedRAMDisk |
16 bytes | Address and size of the packed Data image |
TrustCache |
16 bytes | Address and size of the trust cache |
A placeholder is required for every input. A missing placeholder makes DarwinBoot return EFI_COMPROMISED_DATA.
CPUs
cpu0 {
name = "cpu0";
state = "running";
reg = <0>;
device_type = "cpu";
compatible = "arm,cortex-a715";
timebase-frequency = <19200000>;
}
The first CPU is the boot CPU. Its reg value is the MPIDR affinity value. More than one CPU adds a /psci node with compatible = "arm,psci-1.0" and method = "smc" or "hvc".
arm-io
arm-io {
name = "arm-io";
device_type = "arm-io";
#address-cells = <2>;
#size-cells = <2>;
ranges = <0 0 0x100000000>;
intc {
name = "intc";
compatible = "arm,gic-v3";
reg = <...>;
};
}
ranges maps the bus window. The common generator translates source registers to physical addresses, then emits arm-io with one window from the lowest exported register (rounded down to 16 MiB) to the highest. Child reg values are offsets from the window base. XNU reads that base as the SoC base and requires it to be non-zero, so an exported register below 16 MiB is an error.
product and buttons
/product supplies the product name and model. /buttons supplies the button names and home-button-type. MobileGestalt and the platform code read these nodes. The full property set, gesture setup and device family switch are in MobileGestalt identity.
Platform defaults
/defaults supplies platform options. For example, sleep-disabled = <1> prevents the CPU from parking when suspend is not implemented.
Format 1 ADT tool
Format 1 uses the device script selected by [payload.adt] tool. Its arguments
are listed in Payload. The script supplies the required
nodes, CHRP NVRAM, random seed, identity, root matching, and memory-map placeholders.
UEFI device tree
The firmware exposes its own FDT through EFI_DTB_TABLE. UniXNU can patch it and pass it to EFI or Linux entries.
An entry selects a tree:
[[entry]]
id = "linux"
type = "linux"
path = "Payloads/Image"
dtb = "DeviceTree/board.dtb"
dtb = "firmware" uses the firmware tree. A path uses a file on the payload volume. The tree is copied with 64 KiB of slack, global patches are applied, then entry patches, then [entry.chosen].
Patches support set, delete-prop, add-node, delete-node and overlay:
[[dt_patch]]
op = "set"
node = "/chosen"
prop = "bootargs"
value = "console=ttyAMA0"
[[dt_patch]]
op = "overlay"
file = "DeviceTree/board.dtbo"
DarwinBoot reads the .adt sidecar. An xnu entry that sets dtb passes a tree in the loader request, and the shipped DarwinBoot refuses a request with a non-null device tree.
Boot image device tree
The boot image carries a device tree after the gzip stream. The stock bootloader may require nodes that the firmware tree does not have.
[[bootimg.dtb_node]]
path = "/reserved-memory/splash_region"
props = [
{ name = "label", type = "s", value = "cont_splash_region" },
{ name = "reg", type = "x", value = ["0x0", "0xb8000000", "0x0", "0x2b00000"] },
]
unixnu bootimg applies these nodes with fdtput and verifies integer properties round-trip.
Reference generators
| Generator | Purpose |
|---|---|
scripts/qemu-entry/make-dt.py |
QEMU fixture with a single CPU and optional GICv3, VirtIO and NVRAM |
scripts/make-adt.py |
Hardware DTB and OS/board profiles to ADT |
Format 1 device make-<device>-adt.py |
Device-specific generator |
The QEMU generator is the smallest complete example. The device page in Devices records the values for a real board.