Skip to content

Rockchip RK3588 EVB10 V10 ​

Ken-u/UniXNU-evb10

Rockchip RK3588 EVB10 V10 runs the iPadOS 17.7.11 userland on a source-built XNU, started from edk2-rk3588 firmware (uefi-evb10). Shared SoC support is in UniXNU-rk3588-common; this repository owns the memory layout, the eMMC root, the VOP2 display, touch, the Mali-G610 integration and the packaging. Every area in scope is device-verified on the board with a system built from these repositories (docs/reproduce.md): it boots to SpringBoard with the eMMC root and the Data volume mounted, the Mali composites the interface at about 50 frames per second while scrolling, and the panel and the touch screen were checked by hand and work normally. Wi-Fi works at 2.4 GHz and the audio path is implemented; the CPU clusters are under a frequency and voltage governor. Not done: GPU frequency control, interrupt-driven GPU completion, 5 GHz Wi-Fi, an audible check of the audio path, thermal limits and a measurement of the power the governor saves.

Source record: porting.toml, 37befa9162d6b4b080cb63a95cccff0ebb4d34f5.

This page is generated from the device record.

AreaStatus
Configurationdevice-verified
Firmware and bootdevice-verified
CPU and interruptsdevice-verified
Memory and DMAdevice-verified
Storage and rootdevice-verified
Displaydevice-verified
Inputdevice-verified
GPUdevice-verified
Networkdevice-verified
Audioimplemented
Powerdevice-verified
Image assemblydevice-verified
Diagnosticsdevice-verified

Configuration ​

device-verified

Hardware and dependencies ​

  • ARM64 profile, rk3588 SoC source pinned in feeds.toml, iPadOS 17.7.11 build 21H461 profile, evb10_defconfig and evb10_display_defconfig, submodules package/Mali and package/Rk3588Emmc.

Configuration ​

  • board.soc_source = rk3588; board.os_profile = ipados-17.7.11-21H461; schema_version = 2.
  • kernel.config_fragments = arch common, soc rk3588, os ipados; the defconfig applies last and switches off what the OS profile enables for other hardware (NATIVE_ADRENO, SOURCE_MOCKFS).
  • kernel.patch_series = arch, soc, os, device in that order.
  • payload.kernel_args carries the frame-rate group as defaults: zero-copy scanout with the GPU job as the fence (evb10-vop-direct=1, evb10-vop-fence=1), the GPU at 750 MHz (mali-pll=1, mali-div=1), the device's YCbCr filter (mali-ycbcr-filter=device), asynchronous surface flush (mali-aflush=1), cpumask=31, mali-epri=50 and sched-slowcpus=240; the comment above the list says what each one does.

Procedure ​

  • From the UniXNU checkout run ./unixnu defconfig evb10_defconfig -D ../UniXNU-evb10, then ./unixnu fetch and ./unixnu status --json with the same -D.
  • For a local SoC checkout copy local.toml.example to local.toml.
  • Run scripts/build-evb10-kernel.sh for the two-pass kernel build.

Development findings ​

  • The [adt.defaults.u32] table of board.toml must carry sleep-disabled = 1 and vmm-present = 1; the OS profile only supplies kern.maxvnodes. vmm-present backs kern.hv_vmm_present: without it watchdogd traps with SIGTRAP three times and the kernel panics when the third instance exits (seen in a rebuild from a clean tree, fixed in the clean-directory rerun).
  • Arrays replace inherited values, so board.toml lists every archive, fragment and trust-cache digest it needs.
  • The display archive is made from the kernel of an earlier display-less pass, which is why two defconfigs exist and why the build script runs them in order.
  • A device script named like a UniXNU script shadows it in the build tree; the board driver builder is therefore named build-evb10-drivers.py and does not replace build-arm-driver.py.
  • A boot with the committed defaults and nothing else but the scripted swipes and the command-buffer timeline (run v1) held the GPU at 750 MHz, the Cortex-A76 cores at 2.23 GHz and 52.6 frames per second while the finger was down (swipes 3 to 11), and ended its 235 s capture without a panic or a stalled GPU group.
  • The evidence/ directory holds one curated excerpt per verified checkpoint, named <topic>-<date>.txt. Excerpts of kernel runs name the run command and tag; the Wi-Fi identity excerpt (evidence/wifi-sdio-id-20261030.txt) comes from U-Boot with no kernel running, so it names the probe script and the uefi-evb10 revision that holds it.
  • libEvb10Wifi.a (package/Evb10Wifi, built by a board.toml step like the touch archive) is linked by configs/evb10_display_defconfig only, because the display probe is its only caller; evb10-wifi=1 is a per-run argument, not part of the 884-byte defaults.
  • board.toml runs scripts/build-wifi-firmware.py before build-evb10-drivers.py Evb10Wifi: it turns the AP6398S firmware and nvram of .cache/firmware (scripts/extract-files.py --section "AP6398S Wi-Fi") into the git-ignored package/Evb10Wifi/wifi_fw.inc; without the files the driver embeds nothing and level 2 of evb10-wifi stops after the bus switch.
  • board.toml builds SoftwareAES and the Wi-Fi network side (build-evb10-wifinet.py), [components] include lists SoftwareAES, and configs/evb10_display_defconfig force-loads libEvb10WifiNet.a next to libEvb10Wifi.a; the SoC repository's rk3588.config keeps CONFIG_XNU_NATIVE_AES=y (the earlier comment claiming the RK3588 has no such device described the Qualcomm crypto engine, not this software implementation).
  • The audio codec driver is a step of board.toml (build-evb10-drivers.py Evb10Audio) and an archive of configs/evb10_display_defconfig, like the touch and Wi-Fi drivers; the display kernel with it built and booted (docs/audio.md, evidence/audio-es8388-i2s0-20261101.txt).
  • board.toml carries evb10-audio=4 and evb10-audio-hal=1: the audio service and the audio server plug-in that the payload tool carries in the GPU addon (uefi-evb10 merge-addons.py); the command line is 930 of the 1000 bytes the loader keeps (rk3588-dvfs=6 included). usertest/build.sh in MaliMetal takes out/audio/Evb10Audio.driver/Evb10Audio into the owner trust bundle when it exists, so scripts/build-evb10-audio-hal.py runs before it (docs/reproduce.md).
  • The frequency governors need no argument beyond the committed rk3588-dvfs=6: board.toml's comment for the cpu group now names what it starts (the CPU and GPU governors and the over-temperature limit) and what turns them off (rk3588-dvfs=0 for everything, rk3588-gpu=0 for the GPU only); the run v1 finding above predates them, the committed defaults now start the governors (docs/power.md).

Completion criteria ​

  • Every source pin resolves, ./unixnu status lists the intended patch order, and both defconfigs materialise.

Current limits ​

  • The format 2 configuration has been materialised and checked with status and tree; a full kernel build with it is recorded under Image assembly.

Next action ​

  • Pin the Mali and Rk3588Emmc submodules to released commits when they change.

Evidence ​

Firmware and boot ​

device-verified

Hardware and dependencies ​

  • edk2-rk3588 firmware built by uefi-evb10 (SPI NOR image), TF-A at EL3 with PSCI over SMC, UEFI GOP frame buffer and the Mali CSF firmware listed in proprietary-files.txt.

Configuration ​

  • board.toml has no [firmware] section: the unixnu firmware step builds Mu-Silicium, and this board boots edk2-rk3588 built by uefi-evb10.
  • payload.loader = firmware: DarwinBoot is inside the firmware volume.
  • proprietary-files.txt pins mali_csffw.bin by SHA-256; scripts/extract-files.py places it in .cache/firmware.

Procedure ​

  • Build the firmware in uefi-evb10 (build.sh) and flash or boot it as that repository describes.
  • Run scripts/extract-files.py SOURCE, then the kernel step; scripts/build-mali-firmware.py writes package/Mali/mali_csffw.inc.

Development findings ​

  • The firmware hands the kernel EL1 with the MMU off and PSCI on SMC; secondary cores started by CPU_ON arrive at EL2 and the SoC patch drops them to EL1.
  • The firmware leaves the eMMC in HS400 and UART2 at 1.5 Mbaud; the drivers rely on both.
  • proprietary-files.txt also lists the AP6398S Wi-Fi firmware and nvram from the Rockchip Android vendor tree as optional entries (fw_bcm4359c0_ag.bin for the BCM4359 C0, revision 9, read over SDIO); no driver loads them yet. The fw_bcm4359c51a2_ag.bin of that directory is a 4362a2 build and does not fit this module.

Completion criteria ​

  • Firmware enters DarwinBoot, publishes the GOP frame buffer and keeps the memory reservations of the board.

Current limits ​

  • Firmware and its boot flow are documented in uefi-evb10; this repository only pins the inputs.

Next action ​

  • Record the firmware commit next to each image verification.

Evidence ​

CPU and interrupts ​

device-verified

Components: ArmCpu, ArmDTPlatform.

Hardware and dependencies ​

  • Four Cortex-A55 and four Cortex-A76 at affinities 0x000 to 0x700, GICv3 at 0xfe600000 with redistributors at 0xfe680000 (128 KiB stride), 24 MHz architected timer, PSCI over SMC.

Configuration ​

  • The CPU, GIC and PSCI nodes come from the SoC dtsi (rk3588-common dts/rk3588.dtsi); the board DTS enables nothing extra.
  • Kernel arguments sheng-smp=1, armcpu-self-ipi=1 and armcpu-mmio-sgi=0 select the secondary-core and IPI paths.
  • cpumask=31 starts logical cores 0 to 4 only: the four Cortex-A76 cores, which the SoC device tree lists first, and the boot core, a Cortex-A55. sched-slowcpus=240 marks logical cores 4 to 7 (the Cortex-A55 cores) as slow for the scheduler.

Procedure ​

  • Compile dts/evb10.dts through ./unixnu tree and inspect /cpus, /psci and /arm-io/intc in the generated device tree.
  • Boot and read the per-core entry lines on the serial console.

Development findings ​

  • Inline assembly in this target goes through the Darwin assembler, where ';' starts a comment: "dsb ishst; msr icc_sgi1r_el1, %0; isb" assembled to a lone dsb. Earlier conclusions that the GIC ignores ICC_SGI1R, that the A55 CNTFRQ is zero and that counters differ per core all came from this mistake and were withdrawn. The SoC repository carries the ArmCpu fix; scripts/build-evb10-kernel.sh refuses new inline assembly with ';'.
  • With the fix all eight cores run and inter-processor interrupts go through ICC_SGI1R_EL1.
  • XNU takes the lowest-numbered idle core and the device tree order numbers the cores, so the SoC device tree lists the Cortex-A76 cores first (logical 0 to 3) and the boot core, the fifth node, is found by matching its MPIDR with reg. The render server's encoding thread replays a command buffer in 4 ms on a Cortex-A76 and 11 to 14 ms on a Cortex-A55; sched-slowcpus keeps user-interface threads off an idle slow core while a fast one is free or can be preempted (rk3588-slow-cores.patch, SoC repository).
  • The firmware's regulator table for the Cortex-A76 clusters was compiled as zeros (the DSC used a macro that exists only in C), so the clusters were clocked up without more voltage and calibrated at 1.78 GHz. With the table fixed in uefi-evb10 the A76 cores calibrate at 2.23 GHz (the A55 at 1.86 GHz); a Cortex-A76 integer loop takes 20 % less time, the 4 ms replay stays the same because it waits for memory.

Completion criteria ​

  • Every enabled core reaches its entry checkpoint and receives timer and inter-processor interrupts.

Current limits ​

  • CPU frequency and voltage are managed by the governor of the power area (docs/power.md); with rk3588-dvfs=0 the cores run at the clock the firmware left (Cortex-A76 2.23 GHz measured under the 2.4 GHz request, Cortex-A55 1.86 GHz: the PVTPLL clocks follow the rail voltage, which the firmware leaves at 1.0 V). No cluster-type properties are set, so the scheduler sees one cluster type, and the slow-core mask is a boot argument. cpumask=31 leaves three Cortex-A55 cores off.

Next action ​

  • Decide whether to describe the A55 and A76 clusters to the scheduler.

Evidence ​

Memory and DMA ​

device-verified

Hardware and dependencies ​

  • DRAM bank 0x1_00000000 with size 0x1_00000000 in the firmware memory map, 2 GiB owned window for the kernel, eMMC controller with 32-bit DMA.

Configuration ​

  • dts/evb10.dts declares the memory node; payload.dram_size must equal it.
  • payload.adt.owned_memory_size = 0x80000000: DarwinBoot allocates this contiguous window from the low bank.

Procedure ​

  • Compare the firmware memory map, dts/evb10.dts and the owned window reported by DarwinBoot.
  • Check DMA buffers of the eMMC driver against the 4 GiB address limit.

Development findings ​

  • The 4 GiB bank above 4 GiB is the one large region without TF-A, OP-TEE, firmware or Rockchip carve-outs.
  • A 256 MiB window left too little memory for the iPadOS userland; 2 GiB fits the firmware's low bank, 3 GiB was refused with Out of Resources. All pages sit below 4 GiB, where the eMMC DMA reaches.
  • No IOMMU is used for the eMMC controller; the driver allocates a bounce window below 4 GiB.
  • dts/evb10.dts also carries the wlan node that makes the userland start wifid (a launchd job limited to devices with such a node in the Apple device tree); it has no registers and no interrupts and is described in the network area.

Completion criteria ​

  • All handoff regions are accounted for and every DMA engine works inside its allocated range.

Current limits ​

  • Memory above the 2 GiB window is unused.

Next action ​

  • Test a larger window by placing the kernel window above 4 GiB.

Evidence ​

Storage and root ​

device-verified

Components: AppleKeyStoreState.

Hardware and dependencies ​

  • DesignWare MSHC at 0xfe2e0000, 8-bit eMMC, GPT with userdata at LBA 8459264 to 122142655 (512-byte blocks), SHNGROOT manifest image written by unixnu build userdata.

Configuration ​

  • userdata.first_lba, last_lba and windows (OS, SystemOS) describe the root image.
  • Kernel arguments: emmc=4, emmc-root-trust=1, emmc-systemos-union=1, emmc-data-mount=1, emmc-data-size=2048, emmc-data-persist=1.
  • payload.adt.root_match_volume_uuid names the OS volume; packed_data_ramdisk = false.

Procedure ​

  • Build with build-evb10-drivers.py Rk3588Emmc followed by bind-emmc-storage.py; regenerate scripts/data/evb10-storage-interface-audit.json with audit-evb10-storage.py when the kernelcache pin changes.
  • scripts/peek-root-image.py reads the manifest of a userdata image without a board.

Development findings ​

  • The driver subclasses the original IOBlockStorageDevice, so its archive is made by binding the object to the original code at audited addresses.
  • Only the last 2049 MiB of userdata, the Data window, is ever written: the first start formats it and the kernel copies /private/var once, after which restarts mount it. Writes showed zero failures in the observed runs.
  • A 256 MiB Data volume ran out of space (ENOSPC) and stalled SpringBoard; 512 MiB was enough to reach the lock screen, 2048 MiB is the configured size. vnode jetsam killed daemons until /defaults/kern.maxvnodes was set to 66560, which the OS profile now provides.
  • The tools directory is split by owner: the thread-sample include belongs to the diagnostics area, while the userland-mounts include keeps, next to the mounts, the periodic process table and the screenshot over the serial line.
  • The Data window is a persistent volume: the kernel keeps the tree and preferences of earlier boots (evb10: Data: persistent volume, seeded by an earlier boot) and, because a run ends by resetting the board, mounts the container after an unclean unmount by reloading its newest checkpoint.
  • The addon mount is generalised (evb10_ngpu_mount): a second bindfs union of /private/var/ngpu/audio over /System/Library/Audio/Plug-Ins/HAL (boot argument evb10-audio-hal=1, after the Metal plug-in's mount) and an mmap grant for /System/Library/Audio/Plug-Ins/HAL/Evb10Audio.driver/, the same as for the Metal bundle. Observed on the board: the mount reaches stage 5 (the sealed BuiltinAudioPlugin.driver and Evb10Audio.driver/Info.plist both visible) and the audio server loads the plug-in (evidence/audio-ipados-20261101.txt).

Completion criteria ​

  • The root volume mounts read-only from the OS window and the Data volume mounts read-write across restarts.

Current limits ​

  • Only eMMC is supported; PCIe NVMe is not used. Every run ends by resetting the board, so the Data container has always been mounted after an unclean unmount; a mount after an orderly shutdown was not exercised.

Next action ​

  • Repeat the persistence check after a clean unmount, which no run has exercised.

Evidence ​

Display ​

device-verified

Components: GopFramebuffer.

Hardware and dependencies ​

  • VOP2 with the Esmart3 window on the DSI panel, 1080x1920; UEFI GOP frame buffer; original IOMobileFramebufferLegacy bound to NativeLegacyDisplay.

Configuration ​

  • Kernel arguments evb10-vop=2, evb10-vop-es2=1, evb10-fb-w=1072, evb10-fb-h=1904, offline-legacy-* and sheng-vsync=60.
  • evb10_display_defconfig links the display archive and the link stubs.

Procedure ​

  • Build both passes with scripts/build-evb10-kernel.sh and boot with a monitor or panel attached.

Development findings ​

  • The firmware assigns Esmart3 to VP2; writing back the layer-mixer policy of U-Boot makes the window take effect.
  • Underruns (POST_BUF_EMPTY) on the panel path came from the firmware's HDMI0 window (Esmart2) fetching at the same time; disabling it with evb10-vop-es2=1 gave zero underruns.
  • A frame size of exactly 1080x1920 makes backboardd render the 1242x2208 geometry of other devices and ask for scaling, which the full-frame swap validation rejects; 1072x1904 avoids it.
  • Tearing in scanout was removed by waiting for the GPU jobs of a frame before the page flip.
  • The kernel prints the gaps of every moving window of frames (evb10: display gaps), on the clock of the Metal plug-in's command-buffer timeline, so the hitches of a swipe can be matched with the command buffers. A screenshot over the serial line (evb10-shot-img) reads the live frame buffer row by row for several seconds, so moving content tears in it; compare static frames only.
  • The late probe of package/LegacyDisplayProbe.cpp also calls evb10_wifi_start (package/Evb10Wifi), next to the touch and Mali starts; it does nothing unless evb10-wifi=1.
  • The display probe also starts package/Evb10Audio (evb10-audio=N), after the touch and Wi-Fi drivers; the display kernel's archive list gained libEvb10Audio.a and the display kernel booted with it (docs/audio.md).
  • The display probe (package/LegacyDisplayProbe.cpp) is also where the board starts its late drivers, among them the SoC's frequency governor (rk3588_dvfs_start, power area); evb10_display_defconfig force-loads libRk3588Dvfs.a next to the touch, Wi-Fi, audio and Mali archives, and board.toml builds it before them.

Completion criteria ​

  • The panel shows the lock screen without flicker or tearing.

Current limits ​

  • Frame rate while scrolling is limited by how fast a heavy frame is encoded and drawn against its deadline (see GPU). The panel timing is the firmware's.

Next action ​

  • Shorten the encoding of heavy frames (see GPU), take screenshots from a snapshot, and move presentation to vsync interrupts.

Evidence ​

Input ​

device-verified

Hardware and dependencies ​

  • Goodix GT1158 on I2C5, polled; coordinate grid 720x1280 on a 1080x1920 panel.

Configuration ​

  • Kernel arguments evb10-touch=1, evb10-touch-xmax=720, evb10-touch-ymax=1280, evb10-hid=1, evb10-hid-input=1, sheng-touch-hid=1.
  • evb10-touch-khz sets the I2C5 clock (100 to 400, default 100). evb10-touch-demo=SECS plays scripted gestures without a finger from SECS seconds after the first poll (a swipe up, then left and right swipes of 0.8 s every 7 s), which is how the frame rate is measured without a person.

Procedure ​

  • Boot and watch the evb10: touch lines on the console; touch the panel and check the pointer in SpringBoard.

Development findings ​

  • The controller reports a configured 800x480 but delivers X 9..696 and Y 10..1270: the real grid is 720x1280.
  • The input device is one HID device on the IOHIDResource; the original IOHIDEventDriver attaches to it and feeds backboardd.
  • The poll runs as a thread call at priority 93 about every 11.5 ms and the master busy-waits: at 100 kHz one tick costs 535 us on average (the slowest tick of a window of 1000 was 1.2 to 6.4 ms when it was preempted), about 4.6 % of a fast core; at 400 kHz it costs 150 us (191 us at most). The kernel prints the average and the maximum every 1000 ticks.
  • At 400 kHz the bus scan still sees 0x14 and 0x5d, reads Goodix product id 1158 and ten touch points, and counts no bus errors.
  • evb10-touch-script=SECS plays taps and swipes from a script compiled in from the git-ignored package/Evb10Touch/touch_script.inc (scripts/gen-wifi-touch-script.py writes it from the join file), to drive screens without a person; with evb10-ps and evb10-shot-img the serial log carries screenshots. On the board the lock screen needs two swipes up (the first is always lost); taps then open Settings, the Wi-Fi pane, a network's row, the password sheet and its keyboard, so the Settings Wi-Fi flow was checked this way (docs/network.md, checkpoint 7). The taps that earlier seemed to miss were Settings failing to start (see GPU). One launch in nine of the clean runs (t45) showed no Preferences process after the Dock tap and was not explained; a row that disappears from a list because the signal is weak makes a fixed tap land on the next row.
  • The touch tick line is followed by a line from the event driver (package/Evb10Hid): wrapper installed, contacts begun, contacts at an edge, skipped events and reports with two or more fingers.

Completion criteria ​

  • Touch coordinates map to the panel and SpringBoard reacts.

Current limits ​

  • No keyboard, buttons or pen; the controller is polled every 10 ms. The 400 kHz setting was checked on the bus only: a finger on the panel at that speed was not tried.

Next action ​

  • Use the controller interrupt instead of polling.

Evidence ​

GPU ​

device-verified

Hardware and dependencies ​

  • Mali-G610 (Valhall CSF) at 0xfb000000, CSF firmware from proprietary-files.txt, MaliMetal user-space stack (PanVK, metal2vulkan, Metal plug-in).

Configuration ​

  • Kernel arguments mali=8 and mali-metal=1; evb10-gpu-addon=1 makes the payload tool carry the Metal plug-in.
  • mali-pll and mali-div choose the clock source and the divider minus one (default mali-pll=1 mali-div=1: CPLL 1500 MHz over two, 750 MHz); mali-npll=MHz (400 to 1100) retunes NPLL for source 3; mali-epri sets the priority of the encoding thread; mali-aflush=1 flushes surfaces asynchronously. The UEFI build option GPU_RAIL_UV sets the GPU rail in microvolts (default 750000).
  • package/Mali is a submodule; scripts/build-mali-firmware.py generates the firmware include.

Procedure ​

  • Provide the firmware, build the kernel, then build the user-space stack from MaliMetal and the add-on it describes.

Development findings ​

  • The stack composites the iPadOS lock screen on the Mali: backboardd, AGXAccelerator, NativeMaliMetal, metal2vulkan, PanVK and the Mali driver.
  • PanVK counts a colour attachment with write mask 0 as written; a Mesa patch in MaliMetal fixes the black rectangle behind the lock screen clock.
  • Scrolling the home screen with the frame-rate arguments, now the defaults of board.toml, gets about 50 frames per second in the steady state on the board, 58 at best; the scripted swipes 3 to 11 of run v1 gave 52.6 frames per second with the finger down and 57.5 at best.
  • The heavy render passes scale with the GPU clock (84.9, 28.4 and 22.5 ms at 198, 594 and 750 MHz). The wallpaper pass was metal2vulkan's bit-exact YCbCr filter (about 570 instructions a sample), not memory: the device's own linear filter takes it from 22.5 to 7.9 ms.
  • PanVK let a draw kill the fragments of earlier draws only if it wrote every bound colour attachment, which CoreAnimation's passes never do (they bind a write-masked memoryless second one). A Mesa patch in MaliMetal lets a draw kill when its write set covers the earlier draw's: a matrix of nine write-set pairs on the board shows the pixels only the earlier draw writes never differ, and the median command-buffer GPU time falls from 14.1 to 9.7 ms.
  • What is left of a heavy frame is on the CPU: the render server's encoding thread replays the command buffer (4 ms on a Cortex-A76, 11 ms on an A55) and submits it (1.4 ms) before the GPU starts, 0.7 ms after the submit returns.
  • A heavy command buffer (50 draws or more) has to go from commit to completion in about 19.3 ms to be shown on the second vsync after it. At 750 MHz the 504 of run v1 took 14.9 ms (median) and 18.9 ms (90th percentile) and 8.7 % were over 19.3 ms; at 1 GHz (run v2) 12.7 and 15.6 ms and 2.5 % (1.6 % in an earlier run); the median GPU time of a heavy command buffer is 10.1 ms at 750 MHz and 7.9 ms at 1 GHz.
  • The encoding thread landed on a Cortex-A55 for 8 % of the heavy command buffers (replay 11 to 14 ms instead of 4 ms), the main source of late frames. Listing the Cortex-A76 cores first, raising the thread's priority to 50 (mali-epri) and keeping user-interface threads off idle slow cores (sched-slowcpus) together bring that to 0.5 to 1.8 %; neither of the last two helps alone.
  • mali-npll=1000 retunes NPLL before the GPU is switched onto it and needs the GPU rail at 0.85 V (vendor operating points: 0.75 V up to 800 MHz, 0.85 V for 1 GHz), so the firmware is built with GPU_RAIL_UV=850000; 750 MHz at 0.75 V is the default. A 12-minute capture at 1 GHz (81 scripted swipes, 3553 heavy command buffers) ended without a panic or a stalled group and with vkpipe failed=0 throughout.
  • From the start of a swipe to the first command buffer takes about 60 ms for a left swipe and 150 to 170 ms for a right swipe at every GPU clock. The CPU times of the processes suggest the Today View widgets are rendered on each right swipe (WidgetRenderer_Default used 0.17 s of CPU per 13 s of swiping against 0.003 s idle), but no thread stack was taken to confirm it.
  • A host buffer (BO_CREATE_HOST) over a file's pages, such as an application's mapped assets that the render server shares, is copied instead of pinned: the pin holds the file object's activity count, and unmapping the file's last mapping waits for it in the thread that retires the GPU work holding the pin, so the render server stopped at the first application launch (run t17). The Mali driver's sandbox grant and the plug-in's process list also name iconservicesagent and Preferences: without a Metal device the icon agent of Settings falls back to OpenGL ES, which this device has none of, and crashes in CI::GLContext::init (run t19). A build without these two changes starts Settings and leaves its main thread waiting in CASGetDisplays for the render server (runs t39 to t41); with them Settings is on the screen within 4 seconds of the tap (runs t35 to t38, t42 to t44).

Completion criteria ​

  • The lock screen and its animations render on the Mali at a steady frame rate.

Current limits ​

  • No DVFS: the GPU clock is fixed (750 MHz by default at the 0.75 V the firmware sets; 1 GHz with a firmware built for 0.85 V, not tried on the 0.75 V rail); no interrupt-driven completion (waits poll), at most eight groups. At 750 MHz 8.7 % of the heavy command buffers miss their 19.3 ms deadline (1.6 to 2.5 % at 1 GHz) and a right swipe starts about 100 ms later than a left one. The panel was checked by hand and works normally (see Display).

Next action ​

  • Submit a command buffer while it is still being replayed, find what SpringBoard does in the 100 ms after a right swipe starts, and move completion to the GPU interrupts.

Evidence ​

Network ​

device-verified

Hardware and dependencies ​

  • AP6398S module (Broadcom BCM4359 C0, Wi-Fi on SDIO 3.0 and Bluetooth on UART9) behind the DW-MSHC at 0xfe2d0000 (SPI 204); WL_REG_ON = GPIO0_C4, 32.768 kHz low-power clock from the HYM8563 RTC CLKOUT, WL_HOST_WAKE = GPIO0_B2 (stock device tree).
  • Firmware fw_bcm4359c0_ag.bin and nvram_ap6398s.txt from the Rockchip Android vendor tree, listed as optional entries in proprietary-files.txt; the network to join (SSID, passphrase) in the local, git-ignored .cache/firmware/join.txt.
  • The catalog has no SDIO or Broadcom component (Ath12kWifi is Qualcomm PCIe); the protocol follows Linux brcmfmac; package/Evb10Wifi/nw_abi.h is the interface of Ath12kWifi and WifiNet.cpp is adapted from UniXNU-sheng.

Configuration ​

  • package/Evb10Wifi is built by build-evb10-drivers.py Evb10Wifi and Evb10WifiNet (steps of board.toml; build-evb10-wifinet.py removes the static initializer from the WifiNet.cpp object and archives it), linked by configs/evb10_display_defconfig only, and started by the display probe when the boot argument evb10-wifi=N is given (not a default): 1 identify, 2 download and start the firmware, 3 talk to it and scan, 4 join the network of the join file and run a self-test (DHCP, ARP, ping), 5 join and serve the interface en0 to the OS without the self-test. The images and the join file are embedded by build-wifi-firmware.py (output git-ignored).
  • The bus: sdiom0 pins on function 2 through GPIO2A_IOMUX_SEL_H / GPIO2B_IOMUX_SEL_L in BUS_IOC, WL_REG_ON high, identification at 400 kHz 1-bit from xin24m, then CCCR 0x07 = 0x82 (4-bit), CTYPE 1, CLKSEL_CON(172) = 0x5c (GPLL / 24, about 24.75 MHz at the card), function 1 block size 64, function 2 enabled (IOEx = 6) after the firmware starts.
  • The network side needs the software AES of the board configuration (CONFIG_XNU_NATIVE_AES=y in the SoC repository's rk3588.config and the SoftwareAES component) so that IOKit goes quiet; see Image assembly.
  • evb10-wifi-join=0 (with 4 or 5) does not join the join file's network at boot, so the join comes from the OS (Settings); 2 is the same and the scans list only the join file's network, which puts it in a fixed row of the Settings list for scripted taps.

Procedure ​

  • Fill UniXNU-evb10/.cache/firmware: scripts/extract-files.py <vendor dir> --section "AP6398S Wi-Fi" for the images, and a join.txt with the SSID on the first line and the passphrase on the second.
  • Build with scripts/build-evb10-kernel.sh, then cd uefi-evb10 && EXTRA_KERNEL_ARGS="evb10-wifi=5" CAPTURE_SECS=200 EARLY_STOP=0 tools/xnu-probe/run-kernel.sh TAG and read the evb10: wifi and wifi-net lines of runs/TAG.kernel.log (level 3 lists access points, level 4 runs the self-test, level 5 lets the userland use en0).
  • U-Boot: reset the board (uefi-evb10 tools/board/hw-reset.sh reset) and run uefi-evb10 tools/board/sdio-probe.py --enter for the identity checkpoint.
  • Settings: cd UniXNU-evb10 && scripts/gen-wifi-touch-script.py forget (or join, info, rejoin), rebuild the kernel, then cd uefi-evb10 && EXTRA_KERNEL_ARGS="evb10-wifi=5 evb10-wifi-join=2 evb10-ps=12 evb10-touch-script=99 evb10-shot-img=1 evb10-shot-scale=2" CAPTURE_SECS=420 tools/xnu-probe/run-kernel.sh TAG and python3 tools/xnu-probe/shot2png.py runs/TAG.kernel.log OUTDIR (the passphrase of the join file can show in a screenshot if a tap misses its row: keep the images out of commits).

Development findings ​

  • The module answers: CMD5 returns OCR 0xb0ffff00 (3 IO functions), the CIS manufacturer ID is 02d0:4359 and ChipCommon CHIPID is 0x17294359, chip 0x4359 revision 9 (C0), with the ALP clock available after the request (CHIPCLKCSR 0x48).
  • The IOMUX registers of GPIO2 are in BUS_IOC (0xfd5f8044 and 0xfd5f8048). The offsets of the rk3588_ds_regs and rk3588_p_regs tables of pinctrl-rockchip.c are drive strength and pull in other IOC blocks; written as function selects they leave the SDIO CLK pad low and the card never answers CMD5.
  • The HYM8563 CLKOUT (the module's low-power clock) is enabled after reset, so U-Boot and the kernel need no setup for it.
  • CMD53 in byte mode with a count above the function 1 block size answers R5 OUT_OF_RANGE and a data timeout and leaves the host FIFO full; set the block size (64), use block mode for 64 bytes and up and reset the FIFO before each transfer.
  • The firmware of the C0 module starts with the reset values of SDIO_CON0/1 at about 25 MHz in 4-bit mode: after the CR4 release the HT clock is available in 34 ms and function 2 ready in 232 ms, with the images written in 90 ms and read back identical.
  • The firmware (wl0 9.87.51.11.102) answers SDPCM/BCDC on function 2 with bus:txglom 0 and mpc 0; cur_etheraddr is the module's OTP address, not the default in the nvram. An escan lists 35 to 43 networks of both bands in 1.5 to 1.7 s.
  • Without WLC_SET_INFRA 1 / WLC_SET_AP 0 before WLC_UP a join makes an ad-hoc network (a link-up event with a random BSSID and no key exchange). With them, bsscfg:wpa_auth 0x80, wsec 4, sup_wpa 1 and WLC_SET_WSEC_PMK, the firmware's supplicant keys the link (event 46 status 6) 2.1 s after the join command and the driver's own DHCP, ARP and ping through the BCDC data path work.
  • SDPCM transmit is credit based; under a bulk download the chip's credits arrive slowly and a 32-frame queue without waiting dropped 12006 of 38850 frames. The queue holds 128 frames and if_output waits up to 40 ms for room: 19 of 2315 frames were given up in a run that received 8 MB; the 105 MB case was not repeated.
  • en0 is an ifnet made with IFNET_INIT_LEGACY | IFNET_INIT_NX_NOAUTO and bpf_attach; IONetworkStack, IO80211Controller and IO80211Interface are classes registered late (static initializer run between OSMetaClass::preModLoad and postModLoad). With it the userland's DHCP client, DNS and TCP work: one run received 105 MB in 240 s (best 10 s interval 34 Mbit/s).
  • The userland's interface namer and wireless daemon wait for IOKit to go quiet. The original AppleImage4 matches on /defaults and its start waits without a timeout for IOAESAccelerator, which nothing provided, so the registry never went quiet (the first lease came at 330 s); with SoftwareAES IOKit is quiet 26 s into the boot log.
  • With a network the system downloads assets in the background and the 2 GiB Data window (about 250 MB free after a normal boot) fills within minutes (ENOSPC for those daemons; the next boot had its free space back).
  • wifid is a launchd job limited to devices whose device tree has a node named wlan (_LimitLoadToDeviceTree), so it never started on this board; dts/evb10.dts now carries a wlan node (no registers, no interrupts), and from run set3 on wifid is in the process table at about 75 s and sends Apple80211 requests to en0.
  • The daemon asks for the current network (103) while the link is up, and the file's join leaves the access point out of the BSS list, so the answer is now built from the link state; the property lists (383) and two flags (470, 471) it sets at every state change are accepted. The channels-info structure (207) and a few other types are still refused.
  • Scripted touches (evb10-touch-script, runs set4 to set10) unlocked the screen with two swipes up (the first one is always lost) but taps on the Settings icon did not open the app; that was Settings failing to start (a Metal device for iconservicesagent and Preferences, and the copy of file-backed host buffers in the Mali driver), not the taps.
  • Settings drives the whole join through the driver: the Wi-Fi pane lists the network, the password sheet takes the typed passphrase, Join arrives as an association request (type 20, cipher 6: the 32-byte key the daemon derived from the SSID and passphrase), the firmware joins 1.7 s later, DHCP binds about 3 s after that, and the check mark replaces the spinner when IO80211InterfaceIPConfigurationBusy goes false 4.5 to 9.5 s after the link (runs t37, t42, t43). Forget This Network arrives as a disassociate 0.6 s after the confirmation tap and the pane returns to the list (run t43).
  • Settings has a Wi-Fi row only if MobileGestalt's wifi capability is true, and in a virtual machine (vmm-present) it reads that from the property mac-address-wifi0 of /chosen; WifiNet.cpp sets it from the module's address once the firmware reports it. MobileGestalt caches the answer on the Data volume at the first boot, so a Data volume that booted without the property keeps answering false until the cache file is deleted or the volume rebuilt.
  • The spinner stayed 64 s (run t36) because the address configuration's ARP monitor reads the router's replies through /dev/bpf and the interface gave the packet filter no frames: an ifnet that is not a stock Ethernet driver must supply set_bpf_tap and call it with every frame in both directions, Ethernet header included. With the tap the busy flag clears in 4.5 to 9.5 s. The firmware also passes only the multicast groups it was told about, so allmulti is set to 1; IPv6 neighbour discovery then arrives.
  • 5 GHz does not work yet. After a successful join to the 2.4 GHz access point (-80 dBm) the firmware roamed to the same network's 5 GHz access point (event 19) and the TCP payload counter then rose by 96 bytes in 30 s, from 188826 to 188922 (run t42); a join after Forget This Network went to the 5 GHz access point and its association was not acknowledged (event 7 status 5), so the pane said "Unable to join the network" (run t43). The scan lists the 2.4 GHz access point at -44 dBm in one run and -80 dBm in another, and the firmware's link reading is -80 dBm. The CLM blob is not loaded (the vendor's is data 1.39.1 of 2021, the firmware's built-in one 9.7.7 of 2023) and the nvram has ccode=0; neither was tried. Locking the radio to 2.4 GHz with WLC_SET_BAND (tried as a boot argument and removed again) did not help: the scans then listed only 2412 MHz entries, but both joins of run t46 ended after 0.5 s with event 0 status 3 (no network found), where the first joins of runs t36, t37, t42 and t43, without the lock, took 1.7 to 1.8 s and succeeded.

Completion criteria ​

  • Device-verified for the join and the data path: a scan lists access points, a WPA2-PSK join carries traffic, and iPadOS's own DHCP client, DNS and TCP use the interface for a whole run without a panic. wifid is attached to en0 and answered, and the Settings Wi-Fi pane lists the network, joins it with a typed passphrase, shows the address and forgets it (scripted touches and screenshots, runs t37 to t43); throughput was not looked at.

Current limits ​

  • Opt-in with evb10-wifi=N and a network from the local join file; the committed arguments leave the module off. One module and one 2.4 GHz network at -80 dBm, in a series of runs; 5 GHz, WPA3 and open networks were not tried. The SDIO host is polled at about 25 MHz with no DMA and no SPI 204 interrupt; WL_HOST_WAKE and power save are not used; the Settings pane was checked with the passphrase of a WPA2-PSK network on one test router: open networks, WPA3, hidden networks and whether a joined network is remembered across boots were not tried, and the pane's Private Wi-Fi Address is not programmed into the firmware; 5 GHz fails (a roam to it stops the traffic, a join to it is not acknowledged) so the pane can report "Unable to join" when the firmware picks the 5 GHz access point; the CLM blob is not loaded; Bluetooth is not part of this area.

Next action ​

  • Interrupt (SPI 204 and the in-band interrupt), the host's DMA and 50 MHz so the data path no longer polls, and 5 GHz. First find why 5 GHz fails (CLM blob, nvram regulatory settings, TX power); then an open network and WPA3 from the pane.

Evidence ​

Audio ​

implemented

Hardware and dependencies ​

  • The board's es8388-sound card: an Everest ES8388 at 0x11 of I2C7 (0xfec90000) and the I2S0 controller (0xfe470000, power domain 38) as its digital side, MCLK 12.288 MHz from the SoC, from the stock device tree; the clock tree and register layout of the vendor kernel (clk-rk3588.c, rockchip_i2s_tdm.c, es8328.c).
  • UniXNU-sheng's audio (ShengAudioService, ShengAudioPlugIn.c, ShengAudioAbi.h, BSD-2-Clause-Patent) as the design reference for the kernel service, the ring protocol and the properties the audio server needs; Apple's AudioServerPlugIn.h, AudioHardwareBase.h and AudioHardware.h from the macOS 11.3 SDK (fetched at build time, not stored); the board's amplifier enables hp-con-gpio GPIO4_A7 and spk-con-gpio GPIO4_A2 and the jack detect GPIO1_C4 from the stock device tree.

Configuration ​

  • package/Evb10Audio is built by build-evb10-drivers.py Evb10Audio (a step of board.toml), linked by the display kernel only (configs/evb10_display_defconfig) and started by the display probe with evb10-audio=N (1: the codec over I2C; 2: also the I2S side with a polled tone and capture, evb10-audio-ms=N sets its length; 4: the audio service for iPadOS). board.toml carries evb10-audio=4 and evb10-audio-hal=1 (the plug-in, with evb10-gpu-addon=1). Tuning: evb10-audio-out (0 by the headphone detect, 1 headphone, 2 speaker, 3 both), evb10-audio-vol (0..36, default 28), evb10-audio-linger (ms, default 3000), evb10-audio-grant, evb10-audio-selftest=N (a tone through the service's own rings, no audio server).
  • hal/Evb10AudioPlugIn.c is built by scripts/build-evb10-audio-hal.py (LLVM_BIN and IOS_SDK from the environment or ../MaliMetal/.env; Apple's CoreAudio headers are fetched into .cache/coreaudio-headers against pinned sha256) into out/audio/Evb10Audio.driver and out/audio/audio.addon. uefi-evb10 tools/board/make-payload-boot.sh merges the addon into the GPU addon (merge-addons.py), MaliMetal usertest/build.sh adds the executable's CDHash to the owner trust bundle and pins it in board.toml, and the kernel mounts audio/ over /System/Library/Audio/Plug-Ins/HAL.

Procedure ​

  • Build scripts/build-evb10-audio-hal.py, then MaliMetal usertest/build.sh (trust bundle and board.toml pin), then the kernel (scripts/build-evb10-kernel.sh); run uefi-evb10 tools/xnu-probe/run-kernel.sh and read the evb10: audio lines of the serial log: the service line, 'audiomxd opens the service', the [props] and [trace] lines, the hardware on/off lines and the statistics of the engine and of the plug-in every 5 s while a sound plays. A sound needs a client: the scripted touch run of docs/network.md (evb10-wifi=5 evb10-wifi-join=2 evb10-touch-script=99, CAPTURE_SECS=330) types on the on-screen keyboard and the keyboard clicks play.
  • evb10-audio=2 alone repeats checkpoint 2 (the codec and a tone, no service); evb10-audio-selftest=N with evb10-audio=4 exercises the engine without the audio server.

Development findings ​

  • The codec answers: one device on I2C7, 0x11; all 53 registers (0x00 to 0x34) read; a write to LOUT1VOL reads back and is put back. The firmware leaves the I2C7 clocks and pins ready. The registers are those of a configured, stopped codec (presumably an earlier Linux or Android boot's), not of a freshly reset one.
  • The I2S side works as the master of a 48 kHz, 16 bit, two channel link: power domain 38 is on at the hand-over; 12.288 MHz comes from GPLL (1188 MHz / 4, fraction 512/12375) once CLKGATE_CON(7) is open, the MCLK out mux takes the transmit clock and SYS_GRF 0x318 bit 0 is cleared. The transmit FIFO took 48012 words/s (a word is a frame) with no underrun, and the receive FIFO delivered 47998 words/s of varying small values, which the slave codec only sends when it gets MCLK, BCLK and LRCK.
  • No sound was heard: the headphone and speaker enable GPIOs of the card are not driven and nobody was at the board. Nothing is connected to iPadOS: the HAL directory has no plug-in for this codec and audiomxd runs without an output device. iPadOS decodes H.264 and HEVC through the kernel service behind AppleAVDUserClient, which this kernel does not have (read from the entitlements of videodecodeservice and videocodecd); only the AV1 software decoder (a Metal program) is in the image.
  • Checkpoint 3: the kernel service Evb10Audio and the plug-in work together on the board. The mount shows Evb10Audio.driver next to the sealed BuiltinAudioPlugin.driver (stage 5), audiomxd opens the service, the audio server reads the device, sets the output volume and starts it; its IO thread (priority 97, time constraint) writes 1024-frame blocks every 21 ms always ahead of the play position (write-ahead 1531 to 2049 frames); the largest sample put into the transmit FIFO was 727 for a plug-in mix peak of 8748 under a -21.6 dB volume, so the sound of iPadOS reaches the I2S0 FIFO with the volume applied; both FIFOs run at 48 kHz (1890782 and 1890791 frames in 39 s); the audio server reports the 24 MHz host clock the timer already has, so nothing of its clock is patched.
  • The engine is a polled kernel thread at priority 95 waking every 200 us: in 171906 ticks 171901 were under 300 us and 4 were over 2 ms (longest 4.1 ms), each of which ran the 667 us FIFO dry once, 643 frames (13 ms) in 39 s. The first version printed its statistics and the plug-in's 106 trace lines from that thread and stalled 68 ms: the printing moved to its own thread.
  • The audio server accepts the device with the properties UniXNU-sheng found it needs (Thunderbolt transport type, private 'accs' and 'auto' answered, empty custom-property, box and clock lists); it asks one more it is not answered ('cfsz') and keeps the device. Its routing layer's own HAL calls fail on objects that are not this plug-in's and that does not stop playback.

Completion criteria ​

  • The kernel reads the codec and passes frames between I2S0 and the codec at 48 kHz (run a2), and iPadOS's audio server plays into and records from it through a kernel service and a HAL plug-in: PCM from the audio server reaches the I2S0 transmit FIFO at 48 kHz with the volume applied and the capture ring fills (runs aud1 to aud3, no panic). Hearing it is not done.

Current limits ​

  • Polled (no DMA, no interrupt): a kernel scheduling stall over 667 us drops frames (4 in 39 s with the display running). Nobody has listened: the headphone detect polarity (the pin read 0 with nothing known to be plugged in), the amplifier pin functions (left as the firmware set them), pops at start and stop, the volume curve and the capture quality are unchecked, and only the keyboard clicks were played. The audio power domain is not turned on by this driver (it was on). Other rates and formats and the HDMI, DisplayPort and S/PDIF audio paths are not done. The plug-in is built against the macOS 11.3 CoreAudio headers and the iOS 17 audio server; arm64e servers (newer iPadOS) are not covered. Video codecs are not started.

Next action ​

  • A person with headphones or a speaker listens (evb10-audio-selftest=10 plays a tone without iPadOS; evb10-audio-out=3 drives both amplifiers) and reports the jack detect polarity and pops; then DMA for the transmit and receive rings (PL330, the device tree names the channels and SPI 180) to remove the drop-outs.

Evidence ​

Power ​

device-verified

Hardware and dependencies ​

  • SCMI over SMC clock service of the firmware's BL31 (CPUL, DSU, CPUB01, CPUB23, DDR, GPU, NPU clocks); RK806 PMIC on SPI2 (the Cortex-A55 rail is its DCDC2, the GPU rail its DCDC1); RK8602 and RK8603 on I2C0 (the Cortex-A76 rails at 0x42 and 0x43); the TSADC at 0xfec00000; the Mali-G610's GPU_STATUS and cycle counter; vendor operating-point tables and regulator limits. The board is mains powered and has no power meter.

Configuration ​

  • package/Rk3588DvfsBoard.cpp holds the rails (clock, regulator, limits, ramp; three CPU rails and the GPU rail) and the input-activity counter; board.toml builds the SoC's build-rk3588-dvfs.py with it and carries rk3588-dvfs=6; configs/evb10_display_defconfig force-loads libRk3588Dvfs.a; the display probe calls rk3588_dvfs_start(). rk3588-dvfs=0 turns everything off, rk3588-gpu=0 only the GPU governor; rk3588-dvfs-min=MHz, rk3588-dvfs-boost-ms=N (default 300), rk3588-dvfs-tmax=C (default 85), rk3588-gpu-min/-max=MHz and rk3588-gpu-boost-ms=N tune the governors. The ADT keeps sleep-disabled=1.

Procedure ​

  • Boot with rk3588-dvfs=1 to 5 for the staged CPU probes (clock report, rate sweep, read-only regulator probe, operating-point sweep with voltage, DSU latency), 7 for the temperature sensors, 8 to 10 for the GPU probes (rates of the clock service, busy share, voltage walk), then rk3588-dvfs=6 for the governors; compare the scripted swipes with and without them (docs/power.md, Reproduce).

Development findings ​

  • The rate the clock service reports is a PVTPLL setting: with the Cortex-A76 rails at the firmware's 1.0 V every setting from 1608 MHz up measures 2230 MHz and 816 MHz measures 1220 MHz; with the table's voltage written first the measured clock is within 5 % of the rate on all three clusters from 400 MHz to 2.4 GHz (run dvfs5). The RK806 (SPI2) and RK8602/03 (I2C0) are read and written from the kernel with the firmware's bus setup kept.
  • The DSU follows the Cortex-A55 clock and sits on its rail: a Cortex-A76 core's L3 load takes 18.1 ns with the DSU at 1800 MHz and 51.9 ns at 396 MHz, memory 120 and 181 ns (run dvfs7; run gd6 after the probe's compiler-removed-loop fix gives 18.0 and 51.7 ns), so the governor holds the DSU at three quarters of the faster A76 cluster.
  • The CPU governor (16 ms windows from the CPUs' idle time, rate and voltage ordered, touch boost) ran with the scripted swipes at 52.9 frames per second against 52.8 without it (gestures 3 to 11, runs gov2 and base1), and for 600 s with 63 swipes at 52.1 frames per second and no panic (gov3). In the last 300 s of gov3 CPUB01 spent 23 % of the time at 408, 44 % at 1200 and 30 % at 2400 MHz, CPUL 91 % at 408 MHz, the DSU averaged 1161 MHz.
  • The first governor run (gov1) lowered the voltage but did not raise it again on the way up (the DSU bookkeeping of a domain without a DSU held a stale point): the cores then ran at the lower clock the PVTPLL allowed and the swipes still reached 51.1 frames per second, with no fault; the governor lines now show the rail voltage next to each cluster's point, which is how it was found.
  • The GPU's clock is a PVTPLL as well: the settings 300 to 900 MHz of the clock service set bit 14 of CLKSEL_CON(158), 200 MHz is GPLL / 6, 1000 MHz is ignored, and the Mali driver sets CPLL / 2 = 750 MHz itself and never uses the service. Measured against the cycle counter at the vendor table's voltage: 310 MHz at 300 (675 mV), 396 at 400, 502 at 500, 598 at 600, 719 at 700 (700 mV), 852 at 800 (750 mV) and 957 at 900 (800 mV); about 2.1 to 2.3 MHz per millivolt (runs gs1, gv1).
  • The GPU's busy share is readable from GPU_STATUS: 0 % idle, 45 to 84 % in the 50 ms windows of the scripted swipes (run gw1). The GPU governor built on it (200 to 800 MHz settings, top at 40 % busy, otherwise the lowest point that keeps it about 30 % busy, up at once, down after 75 ms, touch hold 200 ms) kept the frame rate (53.8 frames per second on swipes 3 to 11 in gg1 against 52.8 before; 53.2 over 61 swipes in the 600 s run gf1, no panic) and shortened the median command buffer from 9.8 ms to 8.7 ms (852 MHz at the 750 mV the GPU has today); the GPU sat at 200 MHz and 675 mV for 65 to 100 % of each ten seconds. The 900 setting (gm900) gave 7.8 ms and 53.3 frames per second, so the ceiling stays at 800.
  • The TSADC was clocked but not sampling when the kernel got it; with sampling on it reads 30 C on the idle board with 0.93 C per step. The over-temperature limit (one point off the top of every cluster and the GPU per 128 ms at or above rk3588-dvfs-tmax, one back below tmax minus 2 C) took up to five points off with the limit at 33 C and gave them back as the sensors fell (run gt33, hottest reading 34.2 C); at the default 85 C it did not act in the idle runs tb1 and tg1 (hottest 36.0 C).
  • Idle temperature does not say what the governor saves (0.4 C between tb1 and tg1, less than the 0.9 C between their starting temperatures, one step is 0.93 C). The log carries a model of the busy power of the cores (P = C f V^2 with the vendor device tree's coefficients) against the same work at the top points: 12 % lower with a 1000 ms touch boost (gg1), 15 % with 300 ms (gb300), 23 % with none (gb0), 13 % over the 600 s run gf1 with the 300 ms default; frame rate on swipes 3 to 11 53.8 / 53.3 / 51.1 and gaps over 25 ms 4 / 6 / 16 (single runs). The GPU's idle saving is not in the model.
  • The DDR clock was not changed: the clock service lists no rates for clock 4, and the vendor kernel sets the DDR rate with a SIP call into BL31 after waiting for the display's vertical blank, which would have to be coordinated with the VOP scanout.

Completion criteria ​

  • The three CPU clusters, the DSU and the GPU change rate and voltage together under load without a panic or a lost frame rate on the board; the temperature sensors are read and the limit lowers and restores the points; the governors' per-point times, the model power and the temperatures are in the serial log. What they save in watts is not measured.

Current limits ​

  • Power was not measured (no meter): the saving is known as time at each point and as a model of the cores' busy power, and the GPU's idle saving is in neither; the temperature does not resolve it. The operating points are Rockchip's typical-die tables (a die that needs more voltage at a point runs it slower, not wrongly, and nothing reports it). The DDR clock is not managed; suspend and resume are not implemented and there is no battery. Cortex-A55 cores 5 to 7 are off under cpumask=31. Frame rates are single runs; the run-to-run spread was not measured.

Next action ​

  • Measure the supply power with and without the governors on the scripted swipes (an external meter), then the DDR clock through the SIP call and the display's vertical blank.

Evidence ​

Image assembly ​

device-verified

Components: XnuPrelink, DarwinBoot, SoftwareAES.

Hardware and dependencies ​

  • XNU of the OS profile, ARM64, SoC and device patch series, generated Apple device tree, payload volume of 96 MiB, owner trust-cache digests.

Configuration ​

  • payload.* in board.toml and the [adt] tables; the device tree is generated from dts/evb10.dts.
  • payload.kernel_args lists the boot arguments grouped by purpose.

Procedure ​

  • Run scripts/build-evb10-kernel.sh, then ./unixnu build payload with --unique-chip-id.

Development findings ​

  • A rebuild from the six pushed repositories in an empty directory (docs/reproduce.md) reaches launchd with about 110 processes and Mali-composited frames; the feeds.toml pin of the SoC repository has to include the dts change that stops exporting interrupts on the eMMC and UART nodes, because an exported interrupts property without AAPL,phandle makes IOKit start-up take a data abort. The second trust_cache_sha256 entry changes with every rebuild because the user-space binaries are not bit-for-bit reproducible.
  • The device series must not touch the context of the hunks of earlier series: the userland-mounts patch is generated by patches/tools/make-patches.py from a tree that has the earlier series applied.
  • The Mali driver carries weak fallbacks for the display driver's GPU hand-off, because the display-less pass links it too (kernel patches call its test hooks); the display archive's strong definitions replace them.
  • Changing the XNU patch list recreates the prepared source tree, and preparing it downloads the DriverKit headers the feed leaves out, so that rebuild needs network access (through the proxy, if there is one, even when the build runs under env -i).
  • Each defconfig name is its own output directory (out/evb10/<os>/evb10 and .../evb10_display), so the display scripts read the display-less kernel from the sibling directory.
  • The payload rides in a boot image through U-Boot's fastboot, whose download buffer is 112 MiB; 96 MiB of payload fits, 128 MiB was refused.
  • The payload step of unixnu refuses a kernel command line longer than 1000 bytes ('the kernel command line is 1018 bytes; the limit is 1000') before the board is touched. The defaults of board.toml are 884 bytes, so debugging arguments are added per run through EXTRA_KERNEL_ARGS, which replaces defaults of the same name.
  • A driver that only the display probe calls (Evb10Wifi) is built by a plain build-evb10-drivers.py step and force-loaded by the display defconfig alone; a build with only the display-less defconfig does not contain it, and its log strings (evb10: wifi) tell which kernel a run used.
  • package/Evb10Wifi gained CMD53, 4-bit mode, backplane and ARM CR4 control and the firmware download (evb10-wifi=2); the images enter the kernel as wifi_fw.inc, generated into the build tree's kits/evb10 (a link to package/) and ignored by git.
  • The original AppleImage4 (it matches on /defaults) waits without a timeout for IOAESAccelerator while it starts; without a provider IOKit never goes quiet and the userland's network configuration waits for it. The SoftwareAES component (a software AES-CBC behind IOAESAccelerator, CONFIG_XNU_NATIVE_AES=y in the SoC repository's rk3588.config, build-software-aes-bindings.py in board.toml) provides it: IOKit is quiet 26 s into the boot log.
  • package/Evb10Audio is a third driver of this kind (after Evb10Touch and Evb10Wifi): kernel-side code with a polled I2C master for the ES8388 and the I2S0 controller, built into the display kernel and started with evb10-audio=N; the OS image and the userland are not changed (docs/audio.md).
  • package/Evb10Audio is a driver archive like the others (build-evb10-drivers.py Evb10Audio, force-loaded by the display kernel): checkpoint 3 adds the IOService Evb10Audio with its user client and a polled engine thread, and the header Evb10AudioAbi.h that the audio server plug-in (hal/) shares. Observed on the board: audiomxd opens the service and the audio server's PCM reaches the I2S0 transmit FIFO (evidence/audio-ipados-20261101.txt).
  • board.toml builds the SoC's frequency management with build-rk3588-dvfs.py kits/evb10/Rk3588DvfsBoard.cpp (the board's rail table, package/Rk3588DvfsBoard.cpp) after the audio driver; it needs the kernel record of the first pass like the other driver archives, so it sits in the same archive list. The SoC series gained rk3588-cpu-idle.patch (a CPU's idle time for the governor): scripts/build-evb10-kernel.sh re-clones and re-applies the series on every run, so a patch that fails to apply or to compile stops the build, and a failed compile of an XNU object leaves the archive steps without it until the object is built again (compile it from its .o.json record, then rerun).
  • The rail table of package/Rk3588DvfsBoard.cpp has a fourth entry, the GPU rail (RK806 DCDC_REG1, 550 to 950 mV, 12.5 mV/us); the SoC series' GPU governor (Rk3588Gpu.cpp, compiled by the same build-rk3588-dvfs.py step) uses it, and the display kernel with it ran for 600 s with the governors without a panic (run gf1, docs/power.md).

Completion criteria ​

  • The payload boots on the board to the lock screen.

Current limits ​

  • The two-pass kernel build of this layout completes on Linux (evb10_defconfig, then evb10_display_defconfig; kernel.macho 55.0 MB, sha256 db5e4c058e6516d9cb886d2819d1ca02366a29b9748fbd6b15123e0023e5bf2f, the one booted in run v1). The payload needs a platform seed and the owner trust-cache digests, which are in no repository.

Next action ​

  • Make the user-space build reproducible, or compute the second trust_cache_sha256 entry in the payload step, so that board.toml does not change with every rebuild.

Evidence ​

Diagnostics ​

device-verified

Hardware and dependencies ​

  • UART2 serial console at 1.5 Mbaud; DEVELOPMENT kernel.

Configuration ​

  • Kernel arguments -v, serial=3, vc_mirror=1, debug=0x10014e, -enable_kprintf_spam.
  • evb10-ps=N, evb10-bt, evb10-bt2, evb10-bt3, evb10-bt4 and mali-timeline=1 are added per run through EXTRA_KERNEL_ARGS: the process table costs the serial line about 3 MB a run and moves the frame rate, and the command line is limited to 1000 bytes.
  • evb10-thr=N sets how many threads of each process the table shows (default 8): the thread that holds the lock or the paging reference is often not among the first eight.

Procedure ​

  • Read the serial console. evb10-ps prints the process table, evb10-bt prints user stacks of the named processes for symbolication on the host.

Development findings ​

  • Console lines longer than about 255 characters lose their tail.
  • The serial input of XNU is dead on this board, so there is no command channel; judge the kernel's progress from the serial output only.
  • The reset-path breadcrumbs of a secondary core are 16 words (128 bytes) per core, as rk3588-smp-el2.patch stores them; the dump reads them with that stride and prints the TTBR1, TCR and TTBR0 the core had when it was about to enable the MMU.
  • A thread that waits in vm_object_destroy (a file's last mapping went away and its vnode is reclaimed) waits for the object's paging and activity counts; the table now prints the object (references, paging, activity, resident and wired pages, the first wired pages) next to such a thread, which is how a buffer object pinning a file's pages was found to hold up the render server.
  • evb10_range_is_file_backed (in the same patch) tells a driver whether a task's address range sits on a vnode pager's pages; package/Mali uses it to copy such pages instead of pinning them.

Completion criteria ​

  • Every boot stage prints a checkpoint to the serial console.

Current limits ​

  • Panics reset the board after the text is out; no panic screen is drawn.

Next action ​

  • Add a panic screen on the frame buffer.

Evidence ​

Released under the BSD-2-Clause-Patent License.