diff --git a/boot/m1n1/README.md b/boot/m1n1/README.md index bd78f93b..0a7c6e1c 100644 --- a/boot/m1n1/README.md +++ b/boot/m1n1/README.md @@ -28,3 +28,16 @@ gives the same tree as Hoolock's tip before updating `sources.toml`. Scratch stays in ignored `boot/m1n1/.work//`. `--resume` reuses it only if the recorded patch set is unchanged. + +## Resident PSCI on A7-A9 + +Orchard patch 0015 adds the resident EL3 PSCI monitor (CPU_ON, CPU_OFF, +AFFINITY_INFO); 0016-0019 make it the default whenever m1n1 is entered at EL3 +and add SYSTEM_RESET (warm reboot back into m1n1, which boots the default BLS +entry with the last launch's DTB) and SYSTEM_OFF (halt). Any PSCI-aware +kernel reboots, powers off and kexecs through its ordinary paths; m1n1 takes +IPI-stopped CPUs back by trapping EL1 WFI/WFE to EL3. EL1/EL2 entries keep +the spin table. Payload variables (append to `m1n1.bin`, e.g. with +`psci-loader`): `psci=0` turns PSCI off, `psci_reset=0` keeps only the CPU +lifecycle (no WFI/WFE traps), `psci_reset=proxy` stops in the proxy after a +reset. 0020 marks retained hardware in the device tree after a re-entry. diff --git a/boot/m1n1/patches/orchard/0015-psci-Add-opt-in-resident-EL3-CPU-lifecycle-for-A7-A9.patch b/boot/m1n1/patches/orchard/0015-psci-Add-opt-in-resident-EL3-CPU-lifecycle-for-A7-A9.patch new file mode 100644 index 00000000..4eb87533 --- /dev/null +++ b/boot/m1n1/patches/orchard/0015-psci-Add-opt-in-resident-EL3-CPU-lifecycle-for-A7-A9.patch @@ -0,0 +1,880 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Sat, 7 Nov 2026 15:00:00 +0100 +Subject: [PATCH] psci: Add opt-in resident EL3 CPU lifecycle for A7-A9 + +On A7-A9 m1n1 is entered at EL3 and stays resident: its image and the +per-CPU EL3 stacks are memreserved, and VBAR_EL3 points at its vectors +while Linux runs at EL1. Use that to give Linux PSCI 1.0 CPU_ON, CPU_OFF +and AFFINITY_INFO (plus VERSION, FEATURES and MIGRATE_INFO_TYPE). The +spin table has no cpu_die, so without this the kernel refuses kexec on +an SMP system, can't offline CPUs, and a crash kernel inherits CPUs +stuck in the old kernel. + +CPU_OFF does not power the core down. It cleans and invalidates its L1D, +stops its timers, and waits in WFE at EL3 (MMU off) on a one-line +mailbox. CPU_ON posts an entry point; the core leaves with a clean +I-cache and TLB, SCTLR_EL1 at its reset value, DAIF masked and SP_EL3 on +a static per-CPU stack. SYSTEM_OFF/SYSTEM_RESET, CPU_SUSPEND and +SMCCC_VERSION return NOT_SUPPORTED, so callers fall back to their own +methods and assume SMCCC 1.0. + +Opt-in with the payload variable psci=1, and only used when m1n1 itself +was entered at EL3. kboot then parks the secondaries in the pen (a core +that doesn't park is pruned), sets enable-method = "psci" on every CPU, +adds /psci (method = "smc") and records the re-entry address and the +original boot_args in /chosen. + +Re-entry: _reentry can be jumped to from a kexec'd trampoline (EL1, MMU +off, x0 = boot_args copy). It drops this image's lines from the caches, +flags the re-entry and restarts m1n1, which then skips the (stale) +payload area and runs the proxy. smp_start_secondaries() takes cores +back from the pen with CPU_ON, and the mailboxes and the psci=1 setting +live in .data so they survive the .bss clear. + +Rebase the resident boot shim onto Orchard's complete m1n1 series +without changing its runtime behavior. The payload variable is not a +kernel command-line option; a bare proxy loader also needs it appended. + +Assisted-by: LLM +--- + Makefile | 1 + + src/kboot.c | 56 +++++++++++++- + src/main.c | 8 +- + src/payload.c | 3 + + src/psci.c | 229 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + src/psci.h | 78 ++++++++++++++++++++ + src/psci_asm.S | 170 ++++++++++++++++++++++++++++++++++++++++++ + src/smp.c | 73 ++++++++++++++++++ + src/smp.h | 1 + + src/start.S | 6 +- + src/startup.c | 6 +- + 11 files changed, 626 insertions(+), 5 deletions(-) + +diff --git a/Makefile b/Makefile +index bb72266..4aefb37 100644 +--- a/Makefile ++++ b/Makefile +@@ -166,6 +166,7 @@ OBJECTS := \ + pcie.o \ + pmgr.o \ + proxy.o \ ++ psci.o psci_asm.o \ + ringbuffer.o \ + rtkit.o \ + sart.o \ +diff --git a/src/kboot.c b/src/kboot.c +index b75bb45..2316709 100644 +--- a/src/kboot.c ++++ b/src/kboot.c +@@ -21,6 +21,7 @@ + #include "memory.h" + #include "pcie.h" + #include "pmgr.h" ++#include "psci.h" + #include "sep.h" + #include "sio.h" + #include "smc.h" +@@ -574,6 +575,53 @@ static int dt_set_smbios(void) + return 0; + } + ++/* ++ * With psci=1 and m1n1 owning EL3 (A7-A9), CPUs boot through m1n1's PSCI ++ * (see psci.c) instead of the spin table, so the kernel can offline them ++ * for kexec and CPU hotplug. Also tells Linux how to re-enter m1n1. ++ */ ++static int dt_set_psci(void) ++{ ++ static const char compat[] = "arm,psci-1.0\0arm,psci-0.2"; ++ int node; ++ ++ if (!psci_active()) { ++ printf("PSCI: off (needs psci=1 and m1n1 entered at EL3)\n"); ++ return 0; ++ } ++ ++ int cpus = fdt_path_offset(dt, "/cpus"); ++ if (cpus < 0) ++ bail("FDT: /cpus node not found in devtree\n"); ++ ++ fdt_for_each_subnode(node, dt, cpus) ++ { ++ if (strncmp(fdt_get_name(dt, node, NULL), "cpu@", 4)) ++ continue; ++ if (fdt_setprop_string(dt, node, "enable-method", "psci")) ++ bail("FDT: couldn't set enable-method\n"); ++ } ++ ++ node = fdt_path_offset(dt, "/psci"); ++ if (node < 0) ++ node = fdt_add_subnode(dt, 0, "psci"); ++ if (node < 0) ++ bail("FDT: couldn't add /psci\n"); ++ if (fdt_setprop(dt, node, "compatible", compat, sizeof(compat)) || ++ fdt_setprop_string(dt, node, "method", "smc")) ++ bail("FDT: couldn't fill /psci\n"); ++ ++ node = fdt_path_offset(dt, "/chosen"); ++ if (node < 0) ++ bail("FDT: /chosen node not found in devtree\n"); ++ if (fdt_setprop_u64(dt, node, "linux-a9,m1n1-reentry", (u64)_reentry) || ++ fdt_setprop(dt, node, "linux-a9,m1n1-boot-args", &cur_boot_args, sizeof(cur_boot_args))) ++ bail("FDT: couldn't set the m1n1 re-entry properties\n"); ++ ++ printf("PSCI: on, enable-method = \"psci\", m1n1 re-entry at %p\n", _reentry); ++ return 0; ++} ++ + static int dt_set_cpus(void) + { + int ret = 0; +@@ -587,6 +635,10 @@ static int dt_set_cpus(void) + if (!pruned_phandles) + bail("FDT: out of memory\n"); + ++ /* Secondaries wait in the PSCI pen; one that can't get there is pruned below */ ++ if (psci_active()) ++ printf("PSCI: parked %d secondary CPU(s)\n", smp_park_secondaries_psci()); ++ + /* Prune CPU nodes */ + int node, cpu = 0; + for (node = fdt_first_subnode(dt, cpus); node >= 0;) { +@@ -618,7 +670,7 @@ static int dt_set_cpus(void) + } else { + printf("FDT: Reserving stack for CPU %d 0x%lx\n", cpu, (uint64_t)secondary_stacks[cpu]); + fdt_add_mem_rsv(dt, (uint64_t)secondary_stacks[cpu], SECONDARY_STACK_SIZE); +- if (has_el3()) { ++ if (has_el3() && secondary_stacks_el3[cpu]) { + printf("FDT: Reserving EL3 stack for CPU %d 0x%lx\n", cpu, + (uint64_t)secondary_stacks_el3[cpu]); + fdt_add_mem_rsv(dt, (uint64_t)secondary_stacks_el3[cpu], SECONDARY_STACK_SIZE); +@@ -3206,6 +3258,8 @@ int kboot_prepare_dt(void *fdt) + return -1; + if (dt_set_cpus()) + return -1; ++ if (dt_set_psci()) ++ return -1; + if (dt_set_mac_addresses()) + return -1; + if (dt_set_wifi()) +diff --git a/src/main.c b/src/main.c +index d4e828c..3f32380 100644 +--- a/src/main.c ++++ b/src/main.c +@@ -21,6 +21,7 @@ + #include "pcie.h" + #include "pmgr.h" + #include "sep.h" ++#include "psci.h" + #include "smp.h" + #include "string.h" + #include "tps6598x.h" +@@ -117,9 +118,12 @@ void run_actions(void) + #endif + #endif + +- printf("Checking for payloads...\n"); ++ if (psci_reentered()) ++ printf("Re-entered from Linux: whatever follows m1n1 is stale, not loading payloads\n"); ++ else ++ printf("Checking for payloads...\n"); + +- if (payload_run() == 0) { ++ if (!psci_reentered() && payload_run() == 0) { + printf("Valid payload found\n"); + return; + } +diff --git a/src/payload.c b/src/payload.c +index 5b1547f..4ffc9e2 100644 +--- a/src/payload.c ++++ b/src/payload.c +@@ -12,6 +12,7 @@ + #include "heapblock.h" + #include "kboot.h" + #include "mitigations.h" ++#include "psci.h" + #include "smp.h" + #include "utils.h" + +@@ -205,6 +206,8 @@ static bool check_var(u8 **p) + mitigations_configure(val); + } else if (IS_VAR("tso=")) { + enable_tso = val[0] == '1'; ++ } else if (IS_VAR("psci=")) { ++ psci_configure(val); + } else { + printf("Unknown variable %s\n", *p); + } +diff --git a/src/psci.c b/src/psci.c +new file mode 100644 +index 0000000..2b8ca0d +--- /dev/null ++++ b/src/psci.c +@@ -0,0 +1,229 @@ ++/* SPDX-License-Identifier: MIT */ ++ ++#include "psci.h" ++#include "smp.h" ++#include "types.h" ++#include "utils.h" ++ ++struct psci_cpu { ++ volatile u64 state; ++ volatile u64 entry; ++ volatile u64 context; ++ u64 pad[PSCI_CPU_SIZE / 8 - 3]; ++}; ++ ++_Static_assert(sizeof(struct psci_cpu) == PSCI_CPU_SIZE, "psci_cpu layout"); ++ ++/* ++ * Mailboxes, one cache line each, only ever accessed at EL3 with the MMU off. ++ * They live in .data rather than .bss so that a parked CPU's state survives ++ * m1n1 being re-entered from Linux (which clears .bss). ++ */ ++struct psci_cpu psci_cpus[PSCI_MAX_CPUS] ++ __attribute__((section(".data"), aligned(PSCI_CPU_SIZE))); ++ ++/* Written with the MMU off only (_reentry sets reentry_flag, the first member) */ ++struct { ++ u64 reentry_flag; ++ u64 boot_el3; ++} psci_persist __attribute__((section(".data"), aligned(PSCI_CPU_SIZE))); ++ ++/* psci=1 from the payload, kept across a re-entry */ ++static u32 psci_mode __attribute__((section(".data"))); ++ ++/* EL3 stacks a CPU gets when it leaves the pen */ ++static u8 psci_stacks[PSCI_MAX_CPUS][PSCI_STACK_SIZE] __attribute__((aligned(16))); ++ ++static bool psci_have_el3; ++static bool reentered; ++ ++/* EL3 side */ ++ ++static inline void psci_line_sync(volatile void *p) ++{ ++ __asm__ volatile("dc civac, %0\n\tdsb sy" ::"r"(p) : "memory"); ++} ++ ++static int psci_mpidr_index(u64 mpidr) ++{ ++ mpidr &= 0xff00ffffffUL; ++ ++ /* A7-A9: one cluster, Aff0 is the core */ ++ if (mpidr >= PSCI_MAX_CPUS) ++ return -1; ++ ++ return mpidr; ++} ++ ++static s64 psci_cpu_off(void) ++{ ++ int idx = psci_mpidr_index(mrs(MPIDR_EL1)); ++ ++ if (idx < 0) ++ return PSCI_RET_DENIED; ++ ++ psci_pen(&psci_cpus[idx], (u64)&psci_stacks[idx][PSCI_STACK_SIZE]); ++} ++ ++static s64 psci_cpu_on(u64 mpidr, u64 entry, u64 context) ++{ ++ int idx = psci_mpidr_index(mpidr); ++ ++ if (idx < 0 || !entry) ++ return PSCI_RET_INVALID_PARAMS; ++ ++ struct psci_cpu *c = &psci_cpus[idx]; ++ ++ psci_line_sync(c); ++ switch (c->state) { ++ case PSCI_AFF_OFF: ++ break; ++ case PSCI_AFF_ON_PENDING: ++ return PSCI_RET_ON_PENDING; ++ default: ++ /* Running, or never seen by us (still in m1n1 or never started) */ ++ return PSCI_RET_ALREADY_ON; ++ } ++ ++ /* State before entry: the pen sets ON as soon as it sees the entry */ ++ c->context = context; ++ c->state = PSCI_AFF_ON_PENDING; ++ psci_line_sync(c); ++ c->entry = entry; ++ psci_line_sync(c); ++ sysop("sev"); ++ ++ return PSCI_RET_SUCCESS; ++} ++ ++static s64 psci_affinity(u64 mpidr, u64 level) ++{ ++ int idx = psci_mpidr_index(mpidr); ++ ++ if (idx < 0 || level) ++ return PSCI_RET_INVALID_PARAMS; ++ ++ psci_line_sync(&psci_cpus[idx]); ++ return psci_cpus[idx].state; ++} ++ ++static s64 psci_features(u32 fid) ++{ ++ switch (fid) { ++ case PSCI_0_2_FN_PSCI_VERSION: ++ case PSCI_0_2_FN_CPU_OFF: ++ case PSCI_0_2_FN_CPU_ON: ++ case PSCI_0_2_FN64_CPU_ON: ++ case PSCI_0_2_FN_AFFINITY_INFO: ++ case PSCI_0_2_FN64_AFFINITY_INFO: ++ case PSCI_0_2_FN_MIGRATE_INFO_TYPE: ++ case PSCI_1_0_FN_PSCI_FEATURES: ++ return PSCI_RET_SUCCESS; ++ default: ++ /* ++ * Includes SMCCC_VERSION (so callers assume SMCCC 1.0 and make ++ * no other SMCs), CPU_SUSPEND, SYSTEM_OFF and SYSTEM_RESET. ++ */ ++ return PSCI_RET_NOT_SUPPORTED; ++ } ++} ++ ++u64 psci_handle(u64 fid, u64 a1, u64 a2, u64 a3) ++{ ++ u32 fn = fid; ++ ++ /* SMC32 calls carry 32-bit arguments */ ++ if (!(fn & 0x40000000)) { ++ a1 = (u32)a1; ++ a2 = (u32)a2; ++ a3 = (u32)a3; ++ } ++ ++ switch (fn) { ++ case PSCI_0_2_FN_PSCI_VERSION: ++ return 0x10000; /* 1.0 */ ++ case PSCI_0_2_FN_CPU_OFF: ++ return psci_cpu_off(); ++ case PSCI_0_2_FN_CPU_ON: ++ case PSCI_0_2_FN64_CPU_ON: ++ return psci_cpu_on(a1, a2, a3); ++ case PSCI_0_2_FN_AFFINITY_INFO: ++ case PSCI_0_2_FN64_AFFINITY_INFO: ++ return psci_affinity(a1, a2); ++ case PSCI_0_2_FN_MIGRATE_INFO_TYPE: ++ return 2; /* no trusted OS, nothing to migrate */ ++ case PSCI_1_0_FN_PSCI_FEATURES: ++ return psci_features(a1); ++ default: ++ /* SYSTEM_RESET/SYSTEM_OFF too: the caller falls back to its own */ ++ return PSCI_RET_NOT_SUPPORTED; ++ } ++} ++ ++/* EL1 side */ ++ ++void psci_note_boot_el(u64 el) ++{ ++ /* MMU still off: these .data accesses are uncached, like the writers' */ ++ if (psci_persist.reentry_flag) { ++ reentered = true; ++ psci_persist.reentry_flag = 0; ++ } ++ if (el == 3) ++ psci_persist.boot_el3 = 1; ++ ++ psci_have_el3 = psci_persist.boot_el3 && has_el3(); ++} ++ ++void psci_configure(const char *val) ++{ ++ psci_mode = val[0] == '1'; ++} ++ ++bool psci_active(void) ++{ ++ return psci_mode && psci_have_el3; ++} ++ ++bool psci_reentered(void) ++{ ++ return reentered; ++} ++ ++int psci_affinity_info(u64 mpidr) ++{ ++ return psci_smc(PSCI_0_2_FN64_AFFINITY_INFO, mpidr, 0, 0); ++} ++ ++static void psci_self_off(void) ++{ ++ /* Only returns on failure; the core then stays in m1n1's loop */ ++ psci_smc(PSCI_0_2_FN_CPU_OFF, 0, 0, 0); ++} ++ ++int psci_park_cpu(int cpu, u64 mpidr) ++{ ++ if (psci_affinity_info(mpidr) == PSCI_AFF_OFF) ++ return 0; ++ ++ smp_call0(cpu, psci_self_off); ++ ++ for (int i = 0; i < 100; i++) { ++ if (psci_affinity_info(mpidr) == PSCI_AFF_OFF) ++ return 0; ++ udelay(1000); ++ } ++ ++ return -1; ++} ++ ++int psci_unpark_cpu(int index, u64 mpidr, void *stack_top) ++{ ++ if (psci_affinity_info(mpidr) != PSCI_AFF_OFF) ++ return -1; ++ ++ s64 ret = psci_smc(PSCI_0_2_FN64_CPU_ON, mpidr, (u64)psci_secondary_entry, ++ (u64)stack_top | (u64)index); ++ ++ return ret == PSCI_RET_SUCCESS ? 0 : -1; ++} +diff --git a/src/psci.h b/src/psci.h +new file mode 100644 +index 0000000..6769ebe +--- /dev/null ++++ b/src/psci.h +@@ -0,0 +1,78 @@ ++/* SPDX-License-Identifier: MIT */ ++ ++#ifndef PSCI_H ++#define PSCI_H ++ ++#ifndef __ASSEMBLER__ ++#include "types.h" ++#endif ++ ++/* ++ * Minimal PSCI 1.0 for SoCs where m1n1 owns EL3 (A7-A9 entered at EL3). ++ * Only CPU_ON/CPU_OFF/AFFINITY_INFO and the discovery calls are implemented. ++ * A CPU that calls CPU_OFF is not powered down: it waits at EL3, MMU and ++ * caches off, until CPU_ON hands it a new EL1 entry point. That is what the ++ * kernel needs to offline CPUs for kexec and CPU hotplug. ++ */ ++ ++#define PSCI_0_2_FN_PSCI_VERSION 0x84000000 ++#define PSCI_0_2_FN_CPU_SUSPEND 0x84000001 ++#define PSCI_0_2_FN_CPU_OFF 0x84000002 ++#define PSCI_0_2_FN_CPU_ON 0x84000003 ++#define PSCI_0_2_FN_AFFINITY_INFO 0x84000004 ++#define PSCI_0_2_FN_MIGRATE_INFO_TYPE 0x84000006 ++#define PSCI_0_2_FN_SYSTEM_OFF 0x84000008 ++#define PSCI_0_2_FN_SYSTEM_RESET 0x84000009 ++#define PSCI_1_0_FN_PSCI_FEATURES 0x8400000a ++#define PSCI_0_2_FN64_CPU_ON 0xc4000003 ++#define PSCI_0_2_FN64_AFFINITY_INFO 0xc4000004 ++ ++#define PSCI_RET_SUCCESS 0 ++#define PSCI_RET_NOT_SUPPORTED -1 ++#define PSCI_RET_INVALID_PARAMS -2 ++#define PSCI_RET_DENIED -3 ++#define PSCI_RET_ALREADY_ON -4 ++#define PSCI_RET_ON_PENDING -5 ++#define PSCI_RET_INTERNAL_FAILURE -6 ++ ++#define PSCI_AFF_ON 0 ++#define PSCI_AFF_OFF 1 ++#define PSCI_AFF_ON_PENDING 2 ++ ++#define PSCI_MAX_CPUS 4 ++#define PSCI_STACK_SIZE 0x4000 ++ ++/* Layout shared with psci_asm.S */ ++#define PSCI_CPU_STATE 0x00 ++#define PSCI_CPU_ENTRY 0x08 ++#define PSCI_CPU_CONTEXT 0x10 ++#define PSCI_CPU_SIZE 0x80 ++ ++#ifndef __ASSEMBLER__ ++ ++/* Payload variable psci=1 */ ++void psci_configure(const char *val); ++/* m1n1 entered at EL3 and psci=1 given: the kernel may use PSCI */ ++bool psci_active(void); ++/* Called once from _start_c with the EL m1n1 was entered at */ ++void psci_note_boot_el(u64 el); ++/* Set when m1n1 was re-entered from Linux (see psci_asm.S _reentry) */ ++bool psci_reentered(void); ++ ++/* EL1 side: SMC into our own EL3 */ ++s64 psci_smc(u64 fid, u64 a1, u64 a2, u64 a3); ++int psci_affinity_info(u64 mpidr); ++/* Park a secondary that is spinning in smp_secondary_entry() at EL3 */ ++int psci_park_cpu(int cpu, u64 mpidr); ++/* Bring a parked CPU back into m1n1 (re-entry); returns 0 if it was parked */ ++int psci_unpark_cpu(int index, u64 mpidr, void *stack_top); ++ ++/* EL3 side */ ++u64 psci_handle(u64 fid, u64 a1, u64 a2, u64 a3); ++void psci_pen(void *cpu, u64 stack_top) __attribute__((noreturn)); ++void psci_secondary_entry(void); ++void _reentry(void); ++ ++#endif ++ ++#endif +diff --git a/src/psci_asm.S b/src/psci_asm.S +new file mode 100644 +index 0000000..14f842f +--- /dev/null ++++ b/src/psci_asm.S +@@ -0,0 +1,170 @@ ++/* SPDX-License-Identifier: MIT */ ++ ++#include "psci.h" ++ ++#define DCSW_OP_DCCISW 0x1 // memory.h ++ ++.extern _v_sp0_sync ++.extern _start ++.extern _cpu_reset_c ++.extern psci_handle ++.extern psci_persist ++ ++.text ++ ++/* ++ * Lower-EL synchronous exceptions. At EL3, an SMCCC call (smc #0 from EL1) ++ * goes to the PSCI dispatcher; everything else, and every exception taken ++ * at EL1/EL2, takes the usual path. ++ */ ++.globl _v_lower_sync ++.type _v_lower_sync, @function ++_v_lower_sync: ++ stp x16, x17, [sp, #-16]! ++ mrs x16, CurrentEL ++ cmp x16, #0xc ++ b.ne 1f ++ mrs x16, esr_el3 ++ lsr x17, x16, #26 ++ cmp x17, #0x17 // EC: SMC from AArch64 ++ b.ne 1f ++ tst x16, #0xffff // imm16 == 0: SMCCC ++ b.ne 1f ++ ldp x16, x17, [sp], #16 ++ b psci_smc_entry ++1: ++ ldp x16, x17, [sp], #16 ++ b _v_sp0_sync ++ ++/* ++ * SMCCC 1.0: x0-x17 may be clobbered, x18-x30 and SP_EL1 belong to the ++ * caller. x0-x3 go straight to psci_handle(). ELR/SPSR are saved in case ++ * anything below takes an exception at EL3. ++ */ ++.type psci_smc_entry, @function ++psci_smc_entry: ++ stp x29, x30, [sp, #-16]! ++ mrs x29, elr_el3 ++ mrs x30, spsr_el3 ++ stp x29, x30, [sp, #-16]! ++ str x18, [sp, #-16]! ++ bl psci_handle ++ ldr x18, [sp], #16 ++ ldp x29, x30, [sp], #16 ++ msr elr_el3, x29 ++ msr spsr_el3, x30 ++ ldp x29, x30, [sp], #16 ++ eret ++ ++/* ++ * psci_pen(struct psci_cpu *me, u64 stack_top): CPU_OFF. Never returns. ++ * ++ * The core is not powered down. It writes its L1D back, marks itself OFF and ++ * waits in WFE at EL3 (MMU off, so every access here is non-cacheable) for ++ * CPU_ON to post an entry point. It then enters EL1 there with the MMU off, ++ * a clean I-cache and TLB, and x0 = context id, like a core coming out of ++ * reset. SP_EL3 is reset to this core's own static stack. ++ */ ++.globl psci_pen ++.type psci_pen, @function ++psci_pen: ++ mov x19, x0 ++ mov x20, x1 ++ ++ // A pending timer interrupt must not keep waking WFE. ++ msr cntp_ctl_el0, xzr ++ msr cntv_ctl_el0, xzr ++ isb ++ ++ // Clean and invalidate this core's L1D (L2 is shared and coherent). ++ mov x0, #DCSW_OP_DCCISW ++ bl dcsw_op_level1 ++ dsb sy ++ ++ str xzr, [x19, #PSCI_CPU_ENTRY] ++ mov x1, #PSCI_AFF_OFF ++ str x1, [x19, #PSCI_CPU_STATE] ++ dc civac, x19 ++ dsb sy ++ sev ++ ++1: ++ wfe ++ dc civac, x19 ++ dsb sy ++ ldr x2, [x19, #PSCI_CPU_ENTRY] ++ cbz x2, 1b ++ ++ ldr x0, [x19, #PSCI_CPU_CONTEXT] ++ str xzr, [x19, #PSCI_CPU_ENTRY] ++ mov x1, #PSCI_AFF_ON ++ str x1, [x19, #PSCI_CPU_STATE] ++ dc civac, x19 ++ dsb sy ++ ++ // Nothing of the previous kernel's EL1 context may survive. ++ ic iallu ++ tlbi alle1 ++ dsb sy ++ mov x1, #0x0800 // SCTLR_EL1: RES1 bits only, MMU and caches off ++ movk x1, #0x30d0, lsl #16 ++ msr sctlr_el1, x1 ++ mov x1, #0x3c5 // EL1h, DAIF masked ++ msr spsr_el3, x1 ++ msr elr_el3, x2 ++ mov sp, x20 ++ isb ++ eret ++ ++/* EL1 side: s64 psci_smc(u64 fid, u64 a1, u64 a2, u64 a3) */ ++.globl psci_smc ++.type psci_smc, @function ++psci_smc: ++ smc #0 ++ ret ++ ++/* ++ * CPU_ON target when m1n1 takes a parked core back after a re-entry. ++ * x0 = EL1 stack top | m1n1 CPU index (stack tops are 16 KiB aligned). ++ */ ++.globl psci_secondary_entry ++.type psci_secondary_entry, @function ++psci_secondary_entry: ++ and x1, x0, #0xf ++ bic x0, x0, #0xf ++ msr tpidr_el1, x1 ++ mov sp, x0 ++ bl _cpu_reset_c ++ b . ++ ++/* ++ * Re-entry from Linux (a kexec'd trampoline, see docs/boot/warm-reboot.md): ++ * EL1, MMU and caches off, x0 = a copy of the original boot_args. ++ * Linux may hold clean lines of this image in its linear map, so drop them ++ * before m1n1 writes its .bss uncached and then reads it with caches on. ++ */ ++.globl _reentry ++.type _reentry, @function ++_reentry: ++ mov x19, x0 ++ adrp x1, _base ++ adrp x2, _end ++1: ++ dc civac, x1 ++ add x1, x1, #64 ++ cmp x1, x2 ++ b.lo 1b ++ dsb sy ++ ic iallu ++ dsb sy ++ isb ++ ++ adrp x1, psci_persist // .reentry_flag ++ add x1, x1, :lo12:psci_persist ++ mov x2, #1 ++ str x2, [x1] ++ dc civac, x1 ++ dsb sy ++ ++ mov x0, x19 ++ b _start +diff --git a/src/smp.c b/src/smp.c +index 83eadff..7f5b336 100644 +--- a/src/smp.c ++++ b/src/smp.c +@@ -8,6 +8,7 @@ + #include "malloc.h" + #include "memory.h" + #include "pmgr.h" ++#include "psci.h" + #include "soc.h" + #include "string.h" + #include "types.h" +@@ -227,6 +228,74 @@ static void smp_stop_cpu(int index, int die, int cluster, int core, u64 impl, u6 + } + } + ++/* ++ * After a re-entry from Linux, a secondary that Linux offlined waits in the ++ * PSCI pen at EL3 rather than in smp_secondary_entry(). It can't be reset ++ * through the PMGR (it is running), so hand it an EL1 entry with CPU_ON. ++ */ ++static bool smp_reclaim_cpu(int index, u64 mpidr) ++{ ++ int i; ++ ++ if (index >= MAX_CPUS || spin_table[index].flag) ++ return false; ++ ++ if (psci_affinity_info(mpidr) != PSCI_AFF_OFF) ++ return false; ++ ++ printf("Taking CPU %d back from the PSCI pen... ", index); ++ ++ memset(&spin_table[index], 0, sizeof(struct spin_table)); ++ target_cpu = index; ++ secondary_stacks[index] = memalign(0x4000, SECONDARY_STACK_SIZE); ++ ++ /* The core starts with its MMU off */ ++ dc_civac_range(&spin_table[index], sizeof(struct spin_table)); ++ dc_civac_range(&target_cpu, sizeof(target_cpu)); ++ dc_civac_range(&boot_cpu_idx, sizeof(boot_cpu_idx)); ++ dc_civac_range(&boot_cpu_mpidr, sizeof(boot_cpu_mpidr)); ++ ++ if (psci_unpark_cpu(index, mpidr, secondary_stacks[index] + SECONDARY_STACK_SIZE) < 0) { ++ printf("Failed (CPU_ON)\n"); ++ return true; ++ } ++ ++ for (i = 0; i < 100; i++) { ++ sysop("dmb ld"); ++ if (spin_table[index].flag) ++ break; ++ udelay(1000); ++ } ++ ++ printf("%s\n", i >= 100 ? "Failed!" : " Back."); ++ return true; ++} ++ ++/* ++ * Put every live secondary into the PSCI pen, for a kernel that boots them ++ * with enable-method = "psci". A CPU that doesn't make it is dropped (and ++ * then pruned from the DT by the caller). ++ */ ++int smp_park_secondaries_psci(void) ++{ ++ int parked = 0; ++ ++ for (int i = 0; i < MAX_CPUS; i++) { ++ if (i == boot_cpu_idx || !smp_is_alive(i)) ++ continue; ++ ++ u64 mpidr = spin_table[i].mpidr; ++ if (psci_park_cpu(i, mpidr) < 0) { ++ printf("PSCI: CPU %d (MPIDR 0x%lx) did not park, dropping it\n", i, mpidr); ++ memset(&spin_table[i], 0, sizeof(struct spin_table)); ++ continue; ++ } ++ parked++; ++ } ++ ++ return parked; ++} ++ + static u64 smp_get_iorvbar(int node, int cpu) + { + u64 cpu_impl_reg[2]; +@@ -405,6 +474,10 @@ void smp_start_secondaries(void) + u8 cluster = FIELD_GET(CPU_REG_CLUSTER, reg); + u8 die = FIELD_GET(CPU_REG_DIE, reg); + ++ if (psci_active() && psci_reentered() && ++ smp_reclaim_cpu(i, core | (cluster << 8) | (die << 16))) ++ continue; ++ + smp_start_cpu(i, die, cluster, core, iorvbar, pmgr_reg + cpu_start_off); + } + } +diff --git a/src/smp.h b/src/smp.h +index 0296978..6c90557 100644 +--- a/src/smp.h ++++ b/src/smp.h +@@ -18,6 +18,7 @@ void smp_secondary_prep_el3(void); + + void smp_start_secondaries(void); + void smp_stop_secondaries(bool deep_sleep); ++int smp_park_secondaries_psci(void); + + #define smp_call0(i, f) smp_call4(i, f, 0, 0, 0, 0) + #define smp_call1(i, f, a) smp_call4(i, f, a, 0, 0, 0) +diff --git a/src/start.S b/src/start.S +index aa11e18..a3f35ae 100644 +--- a/src/start.S ++++ b/src/start.S +@@ -11,6 +11,7 @@ + .extern _v_sp0_irq + .extern _v_sp0_fiq + .extern _v_sp0_serr ++.extern _v_lower_sync + .extern _reset_stack + .extern _cpu_reset_c + .extern wdt_reboot +@@ -41,7 +42,7 @@ _vectors_start: + .align 7 + b _v_sp0_serr + .align 7 +- b _v_sp0_sync ++ b _v_lower_sync + .align 7 + b _v_sp0_irq + .align 7 +@@ -103,6 +104,8 @@ _start: + + mov x0, x19 + mov x1, x20 ++ mrs x2, CurrentEL ++ lsr x2, x2, #2 + bl _start_c + b . + +@@ -115,6 +118,7 @@ start_el3: + msr tpidr_el3, xzr + adrp x6, _stack_bot + adr x7, _start_c ++ mov x2, #3 // _start_c: entered at EL3 + b el3_eret_to_el1 + + .globl exc_unk +diff --git a/src/startup.c b/src/startup.c +index 61afe16..167fae8 100644 +--- a/src/startup.c ++++ b/src/startup.c +@@ -5,6 +5,7 @@ + #include "dockchannel_uart.h" + #include "exception.h" + #include "firmware.h" ++#include "psci.h" + #include "smp.h" + #include "string.h" + #include "types.h" +@@ -159,12 +160,15 @@ void dump_boot_args(struct boot_args *ba) + } + + extern void get_device_info(void); +-void _start_c(void *boot_args, void *base) ++void _start_c(void *boot_args, void *base, u64 boot_el) + { + UNUSED(base); + u32 cpu_id = 0; + + memset64(_bss_start, 0, _bss_end - _bss_start); ++ psci_note_boot_el(boot_el); ++ if (psci_reentered()) ++ boot_cpu_idx = -1; // .data keeps the last run's value; find it again + boot_args_addr = (u64)boot_args; + memcpy(&cur_boot_args, boot_args, sizeof(cur_boot_args)); + diff --git a/boot/m1n1/patches/orchard/0016-psci-Turn-the-resident-EL3-PSCI-on-by-default.patch b/boot/m1n1/patches/orchard/0016-psci-Turn-the-resident-EL3-PSCI-on-by-default.patch new file mode 100644 index 00000000..edfc7592 --- /dev/null +++ b/boot/m1n1/patches/orchard/0016-psci-Turn-the-resident-EL3-PSCI-on-by-default.patch @@ -0,0 +1,84 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Mon, 9 Nov 2026 15:56:40 +0100 +Subject: [PATCH] psci: Turn the resident EL3 PSCI on by default + +When m1n1 was entered at EL3 (A7-A9 booted by Pongo) it stays resident +and already implements CPU_ON/CPU_OFF/AFFINITY_INFO for the kernel, but +only when the payload asked for it with psci=1. Without it the kernel +gets the spin table and can neither offline CPUs nor kexec on SMP, so +every loader had to be built with the variable appended. + +Make it the default. psci=0 turns it off again (spin table, no /psci +node), psci=1 is still accepted. Nothing changes where m1n1 is entered +at EL1 or EL2: psci_active() still requires the EL3 entry. + +Assisted-by: LLM +--- + src/kboot.c | 4 ++-- + src/psci.c | 6 +++--- + src/psci.h | 4 ++-- + 3 files changed, 7 insertions(+), 7 deletions(-) + +diff --git a/src/kboot.c b/src/kboot.c +index 7123d60..a5f4238 100644 +--- a/src/kboot.c ++++ b/src/kboot.c +@@ -576,7 +576,7 @@ static int dt_set_smbios(void) + } + + /* +- * With psci=1 and m1n1 owning EL3 (A7-A9), CPUs boot through m1n1's PSCI ++ * When m1n1 owns EL3 (A7-A9), CPUs boot through m1n1's PSCI + * (see psci.c) instead of the spin table, so the kernel can offline them + * for kexec and CPU hotplug. Also tells Linux how to re-enter m1n1. + */ +@@ -586,7 +586,7 @@ static int dt_set_psci(void) + int node; + + if (!psci_active()) { +- printf("PSCI: off (needs psci=1 and m1n1 entered at EL3)\n"); ++ printf("PSCI: off (m1n1 not entered at EL3, or psci=0)\n"); + return 0; + } + +diff --git a/src/psci.c b/src/psci.c +index 2b8ca0d..25e2a4f 100644 +--- a/src/psci.c ++++ b/src/psci.c +@@ -28,8 +28,8 @@ struct { + u64 boot_el3; + } psci_persist __attribute__((section(".data"), aligned(PSCI_CPU_SIZE))); + +-/* psci=1 from the payload, kept across a re-entry */ +-static u32 psci_mode __attribute__((section(".data"))); ++/* On unless the payload says psci=0; kept across a re-entry */ ++static u32 psci_mode __attribute__((section(".data"))) = 1; + + /* EL3 stacks a CPU gets when it leaves the pen */ + static u8 psci_stacks[PSCI_MAX_CPUS][PSCI_STACK_SIZE] __attribute__((aligned(16))); +@@ -177,7 +177,7 @@ void psci_note_boot_el(u64 el) + + void psci_configure(const char *val) + { +- psci_mode = val[0] == '1'; ++ psci_mode = val[0] != '0'; + } + + bool psci_active(void) +diff --git a/src/psci.h b/src/psci.h +index 6769ebe..9a213af 100644 +--- a/src/psci.h ++++ b/src/psci.h +@@ -50,9 +50,9 @@ + + #ifndef __ASSEMBLER__ + +-/* Payload variable psci=1 */ ++/* Payload variable psci=0 turns PSCI off; anything else (psci=1) leaves it on */ + void psci_configure(const char *val); +-/* m1n1 entered at EL3 and psci=1 given: the kernel may use PSCI */ ++/* m1n1 entered at EL3 and not psci=0: the kernel uses PSCI */ + bool psci_active(void); + /* Called once from _start_c with the EL m1n1 was entered at */ + void psci_note_boot_el(u64 el); diff --git a/boot/m1n1/patches/orchard/0017-psci-Trap-EL1-WFI-WFE-to-EL3-for-a-firmware-CPU-rend.patch b/boot/m1n1/patches/orchard/0017-psci-Trap-EL1-WFI-WFE-to-EL3-for-a-firmware-CPU-rend.patch new file mode 100644 index 00000000..a1c26412 --- /dev/null +++ b/boot/m1n1/patches/orchard/0017-psci-Trap-EL1-WFI-WFE-to-EL3-for-a-firmware-CPU-rend.patch @@ -0,0 +1,286 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Mon, 9 Nov 2026 15:58:57 +0100 +Subject: [PATCH] psci: Trap EL1 WFI/WFE to EL3 for a firmware CPU rendezvous + +An ordinary arm64 reboot or power-off does not offline the secondaries +with CPU_OFF: smp_send_stop() leaves them spinning in "wfe; wfi" at EL1 +with interrupts masked, still ON as far as the monitor knows. A9 has no +way to reset a core from another one (Apple and m1n1 power cores down +from the core itself), so SYSTEM_RESET and SYSTEM_OFF need those CPUs to +come to EL3 on their own. + +Give the monitor that hook: a private SiP call, made by m1n1 on the boot +CPU right before it starts a kernel, sets SCR_EL3.TWI and TWE there and +on every CPU that later leaves the pen, and records the AIC and watchdog +bases for later EL3 use. A trapped WFE returns at once (a spurious +wake-up is architectural, and the exception return may have cleared the +exclusive monitor whose event the caller waits for); a trapped WFI waits +at EL3 with IRQ/FIQ routed there only around the wfi, so an interrupt +meant for EL1 still wakes it. While a rendezvous is pending, a trapped +CPU instead parks itself in the pen exactly as CPU_OFF would, cleaning +its L1D on the way. + +The trap enable has no user yet. The CPUs that have been in the pen are +recorded, so a rendezvous knows which ones the kernel was given. + +Assisted-by: LLM +--- + src/psci.c | 56 ++++++++++++++++++++++++++++++++++++++ + src/psci.h | 20 ++++++++++++++ + src/psci_asm.S | 73 ++++++++++++++++++++++++++++++++++++++++++++++++-- + 3 files changed, 147 insertions(+), 2 deletions(-) + +diff --git a/src/psci.c b/src/psci.c +index 25e2a4f..af4f325 100644 +--- a/src/psci.c ++++ b/src/psci.c +@@ -28,6 +28,24 @@ struct { + u64 boot_el3; + } psci_persist __attribute__((section(".data"), aligned(PSCI_CPU_SIZE))); + ++/* ++ * Rendezvous state, one line, only accessed at EL3 with the MMU off. A CPU ++ * that wants all others in the pen (SYSTEM_RESET/SYSTEM_OFF) sets pending; ++ * every other CPU then parks itself at its next trapped WFI/WFE. ++ */ ++struct psci_rdv { ++ volatile u64 pending; ++ volatile u64 traps; /* SCR_EL3.TWI|TWE for CPUs leaving the pen */ ++ volatile u64 cpus; /* MPIDR indices that have been in the pen: handed to the kernel */ ++ u64 aic; ++ u64 wdt; ++ u64 pad[PSCI_CPU_SIZE / 8 - 5]; ++} psci_rdv __attribute__((section(".data"), aligned(PSCI_CPU_SIZE))); ++ ++_Static_assert(sizeof(struct psci_rdv) == PSCI_CPU_SIZE, "psci_rdv layout"); ++_Static_assert(offsetof(struct psci_rdv, pending) == PSCI_RDV_PENDING, "psci_rdv layout"); ++_Static_assert(offsetof(struct psci_rdv, traps) == PSCI_RDV_TRAPS, "psci_rdv layout"); ++ + /* On unless the payload says psci=0; kept across a re-entry */ + static u32 psci_mode __attribute__((section(".data"))) = 1; + +@@ -62,6 +80,10 @@ static s64 psci_cpu_off(void) + if (idx < 0) + return PSCI_RET_DENIED; + ++ psci_line_sync(&psci_rdv); ++ psci_rdv.cpus |= BIT(idx); ++ psci_line_sync(&psci_rdv); ++ + psci_pen(&psci_cpus[idx], (u64)&psci_stacks[idx][PSCI_STACK_SIZE]); + } + +@@ -128,6 +150,32 @@ static s64 psci_features(u32 fid) + } + } + ++/* Arms (or disarms) the WFx traps on this CPU and on every CPU leaving the pen */ ++static s64 psci_m1n1_handoff(u64 traps, u64 aic, u64 wdt) ++{ ++ u64 bits = traps ? (SCR_EL3_TWI | SCR_EL3_TWE) : 0; ++ ++ psci_rdv.pending = 0; ++ psci_rdv.traps = bits; ++ psci_rdv.aic = aic; ++ psci_rdv.wdt = wdt; ++ psci_line_sync(&psci_rdv); ++ ++ msr(SCR_EL3, (mrs(SCR_EL3) & ~(u64)(SCR_EL3_TWI | SCR_EL3_TWE)) | bits); ++ sysop("isb"); ++ ++ return PSCI_RET_SUCCESS; ++} ++ ++/* A trapped WFI/WFE saw the rendezvous flag: become CPU_OFF right here */ ++void psci_rendezvous_off(void) ++{ ++ psci_cpu_off(); ++ /* Not a CPU we know (can't happen after the pen saw it): stay out of the way */ ++ for (;;) ++ sysop("wfe"); ++} ++ + u64 psci_handle(u64 fid, u64 a1, u64 a2, u64 a3) + { + u32 fn = fid; +@@ -154,6 +202,8 @@ u64 psci_handle(u64 fid, u64 a1, u64 a2, u64 a3) + return 2; /* no trusted OS, nothing to migrate */ + case PSCI_1_0_FN_PSCI_FEATURES: + return psci_features(a1); ++ case PSCI_M1N1_SIP_HANDOFF: ++ return psci_m1n1_handoff(a1, a2, a3); + default: + /* SYSTEM_RESET/SYSTEM_OFF too: the caller falls back to its own */ + return PSCI_RET_NOT_SUPPORTED; +@@ -190,6 +240,12 @@ bool psci_reentered(void) + return reentered; + } + ++void psci_handoff(bool wfx_traps, u64 aic_base, u64 wdt_base) ++{ ++ if (psci_active()) ++ psci_smc(PSCI_M1N1_SIP_HANDOFF, wfx_traps, aic_base, wdt_base); ++} ++ + int psci_affinity_info(u64 mpidr) + { + return psci_smc(PSCI_0_2_FN64_AFFINITY_INFO, mpidr, 0, 0); +diff --git a/src/psci.h b/src/psci.h +index 9a213af..1d3a88e 100644 +--- a/src/psci.h ++++ b/src/psci.h +@@ -27,6 +27,13 @@ + #define PSCI_0_2_FN64_CPU_ON 0xc4000003 + #define PSCI_0_2_FN64_AFFINITY_INFO 0xc4000004 + ++/* ++ * m1n1-private SiP call, made by m1n1 itself on the boot CPU right before it ++ * starts a kernel: x1 = 1 to trap EL1 WFI/WFE to EL3 on this and every CPU ++ * that later leaves the pen, x2 = AIC base, x3 = primary watchdog base. ++ */ ++#define PSCI_M1N1_SIP_HANDOFF 0xc200ff00 ++ + #define PSCI_RET_SUCCESS 0 + #define PSCI_RET_NOT_SUPPORTED -1 + #define PSCI_RET_INVALID_PARAMS -2 +@@ -48,6 +55,15 @@ + #define PSCI_CPU_CONTEXT 0x10 + #define PSCI_CPU_SIZE 0x80 + ++/* struct psci_rdv, one line, EL3 only (MMU off) */ ++#define PSCI_RDV_PENDING 0x00 ++#define PSCI_RDV_TRAPS 0x08 ++ ++#define SCR_EL3_IRQ (1 << 1) ++#define SCR_EL3_FIQ (1 << 2) ++#define SCR_EL3_TWI (1 << 12) ++#define SCR_EL3_TWE (1 << 13) ++ + #ifndef __ASSEMBLER__ + + /* Payload variable psci=0 turns PSCI off; anything else (psci=1) leaves it on */ +@@ -67,9 +83,13 @@ int psci_park_cpu(int cpu, u64 mpidr); + /* Bring a parked CPU back into m1n1 (re-entry); returns 0 if it was parked */ + int psci_unpark_cpu(int index, u64 mpidr, void *stack_top); + ++/* EL1, boot CPU, last thing before the kernel: see PSCI_M1N1_SIP_HANDOFF */ ++void psci_handoff(bool wfx_traps, u64 aic_base, u64 wdt_base); ++ + /* EL3 side */ + u64 psci_handle(u64 fid, u64 a1, u64 a2, u64 a3); + void psci_pen(void *cpu, u64 stack_top) __attribute__((noreturn)); ++void psci_rendezvous_off(void) __attribute__((noreturn)); + void psci_secondary_entry(void); + void _reentry(void); + +diff --git a/src/psci_asm.S b/src/psci_asm.S +index 14f842f..ff31f3a 100644 +--- a/src/psci_asm.S ++++ b/src/psci_asm.S +@@ -9,13 +9,16 @@ + .extern _cpu_reset_c + .extern psci_handle + .extern psci_persist ++.extern psci_rdv ++.extern psci_rendezvous_off + + .text + + /* + * Lower-EL synchronous exceptions. At EL3, an SMCCC call (smc #0 from EL1) +- * goes to the PSCI dispatcher; everything else, and every exception taken +- * at EL1/EL2, takes the usual path. ++ * goes to the PSCI dispatcher and a trapped WFI/WFE to psci_wfx_entry; ++ * everything else, and every exception taken at EL1/EL2, takes the usual ++ * path. + */ + .globl _v_lower_sync + .type _v_lower_sync, @function +@@ -26,6 +29,8 @@ _v_lower_sync: + b.ne 1f + mrs x16, esr_el3 + lsr x17, x16, #26 ++ cmp x17, #0x01 // EC: trapped WFI/WFE ++ b.eq 2f + cmp x17, #0x17 // EC: SMC from AArch64 + b.ne 1f + tst x16, #0xffff // imm16 == 0: SMCCC +@@ -35,6 +40,59 @@ _v_lower_sync: + 1: + ldp x16, x17, [sp], #16 + b _v_sp0_sync ++2: ++ ldp x16, x17, [sp], #16 ++ b psci_wfx_entry ++ ++/* ++ * WFI/WFE from EL1/EL0, trapped by SCR_EL3.TWI/TWE (see PSCI_M1N1_SIP_HANDOFF). ++ * This is how firmware gets hold of a CPU that the kernel stopped with an ++ * IPI and left spinning in "wfe; wfi" with interrupts masked (an ordinary ++ * reboot does that; only kexec and hotplug use CPU_OFF): while a ++ * SYSTEM_RESET/SYSTEM_OFF rendezvous is pending, the CPU parks itself as if ++ * it had called CPU_OFF. Otherwise: ++ * - WFE completes at once. A spurious WFE wake-up is architectural, and ++ * waiting here could miss the event the caller waits for (the exception ++ * return may clear its exclusive monitor). ++ * - WFI waits here, with IRQ/FIQ routed to EL3 only around the wfi so that ++ * an interrupt for EL1 wakes it (DAIF is masked at EL3, nothing is taken). ++ * Both then return past the instruction. Only x0-x3 are used. ++ */ ++.type psci_wfx_entry, @function ++psci_wfx_entry: ++ stp x0, x1, [sp, #-16]! ++ stp x2, x3, [sp, #-16]! ++ adrp x0, psci_rdv ++ add x0, x0, :lo12:psci_rdv ++ dc civac, x0 ++ dsb sy ++ ldr x1, [x0, #PSCI_RDV_PENDING] ++ cbnz x1, 3f ++ mrs x1, esr_el3 ++ tbnz x1, #0, 2f // ISS.TI bit 0 set: WFE (or WFET) ++ mrs x2, scr_el3 ++ orr x3, x2, #(SCR_EL3_IRQ | SCR_EL3_FIQ) ++ msr scr_el3, x3 ++ isb ++ dsb sy ++ wfi ++ msr scr_el3, x2 ++ isb ++ dc civac, x0 ++ dsb sy ++ ldr x1, [x0, #PSCI_RDV_PENDING] ++ cbnz x1, 3f ++2: ++ mrs x1, elr_el3 ++ add x1, x1, #4 ++ msr elr_el3, x1 ++ ldp x2, x3, [sp], #16 ++ ldp x0, x1, [sp], #16 ++ eret ++3: ++ add sp, sp, #32 ++ bl psci_rendezvous_off ++ b . + + /* + * SMCCC 1.0: x0-x17 may be clobbered, x18-x30 and SP_EL1 belong to the +@@ -106,6 +164,17 @@ psci_pen: + ic iallu + tlbi alle1 + dsb sy ++ ++ // WFI/WFE traps as the boot CPU set them for the kernel (psci_rdv.traps) ++ adrp x3, psci_rdv ++ add x3, x3, :lo12:psci_rdv ++ dc civac, x3 ++ dsb sy ++ ldr x4, [x3, #PSCI_RDV_TRAPS] ++ mrs x1, scr_el3 ++ bic x1, x1, #(SCR_EL3_TWI | SCR_EL3_TWE) ++ orr x1, x1, x4 ++ msr scr_el3, x1 + mov x1, #0x0800 // SCTLR_EL1: RES1 bits only, MMU and caches off + movk x1, #0x30d0, lsl #16 + msr sctlr_el1, x1 diff --git a/boot/m1n1/patches/orchard/0018-psci-Implement-SYSTEM_RESET-and-SYSTEM_OFF-with-a-CP.patch b/boot/m1n1/patches/orchard/0018-psci-Implement-SYSTEM_RESET-and-SYSTEM_OFF-with-a-CP.patch new file mode 100644 index 00000000..e5dd449e --- /dev/null +++ b/boot/m1n1/patches/orchard/0018-psci-Implement-SYSTEM_RESET-and-SYSTEM_OFF-with-a-CP.patch @@ -0,0 +1,497 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Mon, 9 Nov 2026 16:01:53 +0100 +Subject: [PATCH] psci: Implement SYSTEM_RESET and SYSTEM_OFF with a CPU + rendezvous + +Give the kernel's ordinary reboot and power-off paths somewhere to go. +On SYSTEM_RESET or SYSTEM_OFF the calling CPU sets the rendezvous flag, +sends an event and an AIC IPI to every other CPU the kernel was given, +and waits up to a second for all of them to park in the pen through the +WFI/WFE traps. If one does not arrive, it clears the flag and returns +without having changed anything, so the kernel goes on to its next +restart or power-off method as it does today. + +Once they are parked it disarms the primary watchdog (the kernel, or +systemd's reboot watchdog, may have armed it), stops its timers and +drops the traps. SYSTEM_RESET then turns the EL1 MMU and caches off, +writes back every data cache level by set/way, invalidates the EL1 TLB +and I-cache and restarts m1n1 at EL1h with the original iBoot boot_args, +saved in .data on the first cold start before top_of_memory_alloc() +changes them. psci_reentered() and the new psci_reset_reentered() tell +the restarted m1n1 where it came from. SYSTEM_OFF parks the caller too: +there is no PMU driver here, so this halts rather than powers off. + +The payload variable psci_reset selects what SYSTEM_RESET leads to: boot +(the default, used by the next change), proxy, or 0 for NOT_SUPPORTED +with no WFI/WFE traps. It is reported by PSCI_FEATURES and armed by +kboot_boot() through the private handoff call, which also records the +boot CPU and needs an AIC v1 and a watchdog in the ADT. + +Assisted-by: LLM +--- + src/kboot.c | 1 + + src/payload.c | 2 + + src/psci.c | 220 +++++++++++++++++++++++++++++++++++++++++++++++-- + src/psci.h | 25 +++++- + src/psci_asm.S | 44 +++++++++- + src/startup.c | 1 + + 6 files changed, 280 insertions(+), 13 deletions(-) + +diff --git a/src/kboot.c b/src/kboot.c +index a5f4238..299e388 100644 +--- a/src/kboot.c ++++ b/src/kboot.c +@@ -3301,6 +3301,7 @@ int kboot_boot(void *kernel) + + printf("Setting SMP mode to WFE...\n"); + smp_set_wfe_mode(true); ++ psci_handoff(); + printf("Preparing to boot kernel at %p with fdt at %p\n", kernel, dt); + + next_stage.entry = kernel; +diff --git a/src/payload.c b/src/payload.c +index 4ffc9e2..0b7dc14 100644 +--- a/src/payload.c ++++ b/src/payload.c +@@ -208,6 +208,8 @@ static bool check_var(u8 **p) + enable_tso = val[0] == '1'; + } else if (IS_VAR("psci=")) { + psci_configure(val); ++ } else if (IS_VAR("psci_reset=")) { ++ psci_reset_configure(val); + } else { + printf("Unknown variable %s\n", *p); + } +diff --git a/src/psci.c b/src/psci.c +index af4f325..1821ebc 100644 +--- a/src/psci.c ++++ b/src/psci.c +@@ -1,9 +1,19 @@ + /* SPDX-License-Identifier: MIT */ + + #include "psci.h" ++#include "adt.h" ++#include "aic_regs.h" ++#include "memory.h" + #include "smp.h" ++#include "string.h" + #include "types.h" + #include "utils.h" ++#include "xnuboot.h" ++ ++#define WDT_CTL 0x1c ++ ++/* SYSTEM_RESET/SYSTEM_OFF wait this long for the other CPUs to park */ ++#define RENDEZVOUS_TIMEOUT_US 1000000 + + struct psci_cpu { + volatile u64 state; +@@ -39,7 +49,8 @@ struct psci_rdv { + volatile u64 cpus; /* MPIDR indices that have been in the pen: handed to the kernel */ + u64 aic; + u64 wdt; +- u64 pad[PSCI_CPU_SIZE / 8 - 5]; ++ u64 reset; /* enum psci_reset_mode the kernel was started with */ ++ u64 pad[PSCI_CPU_SIZE / 8 - 6]; + } psci_rdv __attribute__((section(".data"), aligned(PSCI_CPU_SIZE))); + + _Static_assert(sizeof(struct psci_rdv) == PSCI_CPU_SIZE, "psci_rdv layout"); +@@ -49,11 +60,18 @@ _Static_assert(offsetof(struct psci_rdv, traps) == PSCI_RDV_TRAPS, "psci_rdv lay + /* On unless the payload says psci=0; kept across a re-entry */ + static u32 psci_mode __attribute__((section(".data"))) = 1; + ++/* psci_reset=0|proxy|boot, kept across a re-entry */ ++static u32 psci_reset_mode __attribute__((section(".data"))) = PSCI_RESET_BOOT; ++ ++/* The iBoot boot_args m1n1 was first started with; SYSTEM_RESET starts m1n1 with them again */ ++static struct boot_args reset_boot_args __attribute__((section(".data"), aligned(PSCI_CPU_SIZE))); ++ + /* EL3 stacks a CPU gets when it leaves the pen */ + static u8 psci_stacks[PSCI_MAX_CPUS][PSCI_STACK_SIZE] __attribute__((aligned(16))); + + static bool psci_have_el3; + static bool reentered; ++static bool reset_reentered; + + /* EL3 side */ + +@@ -129,9 +147,113 @@ static s64 psci_affinity(u64 mpidr, u64 level) + return psci_cpus[idx].state; + } + ++/* ++ * Gets every other CPU the kernel was given into the pen: they park at their ++ * next trapped WFI/WFE (see psci_wfx_entry). A CPU waiting in WFI is woken ++ * with an IPI, one in WFE with an event. If one doesn't arrive in time, ++ * nothing has been changed that the kernel cares about. ++ */ ++static bool psci_rendezvous(int self) ++{ ++ psci_line_sync(&psci_rdv); ++ u64 others = psci_rdv.cpus & ~BIT(self); ++ ++ psci_rdv.pending = 1; ++ psci_line_sync(&psci_rdv); ++ sysop("sev"); ++ ++ for (int i = 0; i < PSCI_MAX_CPUS; i++) { ++ if (!(others & BIT(i))) ++ continue; ++ psci_line_sync(&psci_cpus[i]); ++ if (psci_cpus[i].state != PSCI_AFF_OFF) ++ write32(psci_rdv.aic + AIC_IPI_SEND, AIC_IPI_SEND_CPU(i)); ++ } ++ ++ u64 timeout = timeout_calculate(RENDEZVOUS_TIMEOUT_US); ++ do { ++ bool all = true; ++ ++ for (int i = 0; i < PSCI_MAX_CPUS; i++) { ++ if (!(others & BIT(i))) ++ continue; ++ psci_line_sync(&psci_cpus[i]); ++ if (psci_cpus[i].state != PSCI_AFF_OFF) ++ all = false; ++ } ++ if (all) { ++ psci_rdv.pending = 0; ++ psci_line_sync(&psci_rdv); ++ return true; ++ } ++ sysop("sev"); ++ udelay(100); ++ } while (!timeout_expired(timeout)); ++ ++ psci_rdv.pending = 0; ++ psci_line_sync(&psci_rdv); ++ return false; ++} ++ ++/* Everything that must happen before this CPU leaves the kernel for good */ ++static s64 psci_system_stop(void) ++{ ++ int self = psci_mpidr_index(mrs(MPIDR_EL1)); ++ ++ psci_line_sync(&psci_rdv); ++ if (psci_rdv.reset == PSCI_RESET_OFF) ++ return PSCI_RET_NOT_SUPPORTED; ++ if (self < 0 || !psci_rendezvous(self)) ++ return PSCI_RET_INTERNAL_FAILURE; ++ ++ msr(CNTP_CTL_EL0, 0); ++ msr(CNTV_CTL_EL0, 0); ++ /* The kernel (or systemd's reboot watchdog) may have armed it */ ++ write32(psci_rdv.wdt + WDT_CTL, 0); ++ read32(psci_rdv.wdt + WDT_CTL); ++ ++ /* ++ * The WFx traps go away with the kernel; the next handoff sets them ++ * again, and the next kboot records which CPUs the next kernel gets. ++ */ ++ psci_rdv.traps = 0; ++ psci_rdv.cpus = 0; ++ psci_line_sync(&psci_rdv); ++ msr(SCR_EL3, mrs(SCR_EL3) & ~(u64)(SCR_EL3_TWI | SCR_EL3_TWE)); ++ sysop("isb"); ++ return PSCI_RET_SUCCESS; ++} ++ ++/* Returns only if the other CPUs could not be stopped: the kernel tries its next method */ ++static s64 psci_system_reset(void) ++{ ++ s64 ret = psci_system_stop(); ++ ++ if (ret != PSCI_RET_SUCCESS) ++ return ret; ++ ++ psci_system_reset_entry(&reset_boot_args); ++} ++ ++/* No PMU driver here: this halts the SoC with every CPU parked, it does not power off */ ++static s64 psci_system_off(void) ++{ ++ s64 ret = psci_system_stop(); ++ ++ if (ret != PSCI_RET_SUCCESS) ++ return ret; ++ ++ for (;;) ++ sysop("wfi"); ++} ++ + static s64 psci_features(u32 fid) + { + switch (fid) { ++ case PSCI_0_2_FN_SYSTEM_OFF: ++ case PSCI_0_2_FN_SYSTEM_RESET: ++ psci_line_sync(&psci_rdv); ++ return psci_rdv.reset != PSCI_RESET_OFF ? PSCI_RET_SUCCESS : PSCI_RET_NOT_SUPPORTED; + case PSCI_0_2_FN_PSCI_VERSION: + case PSCI_0_2_FN_CPU_OFF: + case PSCI_0_2_FN_CPU_ON: +@@ -144,21 +266,35 @@ static s64 psci_features(u32 fid) + default: + /* + * Includes SMCCC_VERSION (so callers assume SMCCC 1.0 and make +- * no other SMCs), CPU_SUSPEND, SYSTEM_OFF and SYSTEM_RESET. ++ * no other SMCs), CPU_SUSPEND and SYSTEM_RESET2. + */ + return PSCI_RET_NOT_SUPPORTED; + } + } + +-/* Arms (or disarms) the WFx traps on this CPU and on every CPU leaving the pen */ +-static s64 psci_m1n1_handoff(u64 traps, u64 aic, u64 wdt) ++/* ++ * Arms (or disarms) the WFx traps on this CPU and on every CPU leaving the ++ * pen, and with them SYSTEM_RESET/SYSTEM_OFF (reset = enum psci_reset_mode). ++ */ ++static s64 psci_m1n1_handoff(u64 reset, u64 aic, u64 wdt) + { +- u64 bits = traps ? (SCR_EL3_TWI | SCR_EL3_TWE) : 0; ++ if (reset > PSCI_RESET_PROXY || (reset && (!aic || !wdt))) ++ return PSCI_RET_INVALID_PARAMS; ++ ++ u64 bits = reset ? (SCR_EL3_TWI | SCR_EL3_TWE) : 0; ++ int self = psci_mpidr_index(mrs(MPIDR_EL1)); ++ ++ if (self < 0) ++ return PSCI_RET_DENIED; + ++ psci_line_sync(&psci_rdv); ++ /* The secondaries were added when kboot parked them */ ++ psci_rdv.cpus |= BIT(self); + psci_rdv.pending = 0; + psci_rdv.traps = bits; + psci_rdv.aic = aic; + psci_rdv.wdt = wdt; ++ psci_rdv.reset = reset; + psci_line_sync(&psci_rdv); + + msr(SCR_EL3, (mrs(SCR_EL3) & ~(u64)(SCR_EL3_TWI | SCR_EL3_TWE)) | bits); +@@ -202,10 +338,13 @@ u64 psci_handle(u64 fid, u64 a1, u64 a2, u64 a3) + return 2; /* no trusted OS, nothing to migrate */ + case PSCI_1_0_FN_PSCI_FEATURES: + return psci_features(a1); ++ case PSCI_0_2_FN_SYSTEM_RESET: ++ return psci_system_reset(); ++ case PSCI_0_2_FN_SYSTEM_OFF: ++ return psci_system_off(); + case PSCI_M1N1_SIP_HANDOFF: + return psci_m1n1_handoff(a1, a2, a3); + default: +- /* SYSTEM_RESET/SYSTEM_OFF too: the caller falls back to its own */ + return PSCI_RET_NOT_SUPPORTED; + } + } +@@ -216,6 +355,7 @@ void psci_note_boot_el(u64 el) + { + /* MMU still off: these .data accesses are uncached, like the writers' */ + if (psci_persist.reentry_flag) { ++ reset_reentered = psci_persist.reentry_flag == PSCI_REENTRY_RESET; + reentered = true; + psci_persist.reentry_flag = 0; + } +@@ -240,10 +380,72 @@ bool psci_reentered(void) + return reentered; + } + +-void psci_handoff(bool wfx_traps, u64 aic_base, u64 wdt_base) ++void psci_reset_configure(const char *val) ++{ ++ if (val[0] == '0') ++ psci_reset_mode = PSCI_RESET_OFF; ++ else if (!strncmp(val, "proxy", 5)) ++ psci_reset_mode = PSCI_RESET_PROXY; ++ else ++ psci_reset_mode = PSCI_RESET_BOOT; ++} ++ ++enum psci_reset_mode psci_reset_mode_get(void) + { +- if (psci_active()) +- psci_smc(PSCI_M1N1_SIP_HANDOFF, wfx_traps, aic_base, wdt_base); ++ return psci_reset_mode; ++} ++ ++bool psci_reset_reentered(void) ++{ ++ return reset_reentered; ++} ++ ++void psci_reset_save_boot_args(void) ++{ ++ if (reentered) ++ return; ++ memcpy(&reset_boot_args, &cur_boot_args, sizeof(reset_boot_args)); ++ dc_civac_range(&reset_boot_args, sizeof(reset_boot_args)); ++} ++ ++/* ++ * The CPU rendezvous needs the AIC v1 IPI (A7-A9) and the primary watchdog ++ * (wdt-version 1 there, nothing else to stop). Without them SYSTEM_RESET and ++ * SYSTEM_OFF stay NOT_SUPPORTED and the kernel uses its own methods. ++ */ ++void psci_handoff(void) ++{ ++ enum psci_reset_mode reset = psci_reset_mode; ++ u64 aic = 0, wdt = 0; ++ int path[8]; ++ ++ if (!psci_active()) ++ return; ++ ++ if (reset != PSCI_RESET_OFF) { ++ int node = adt_path_offset_trace(adt, "/arm-io/aic", path); ++ if (node < 0 || !adt_is_compatible(adt, node, "aic,1") || ++ adt_get_reg(adt, path, "reg", 0, &aic, NULL) < 0) { ++ printf("PSCI: SYSTEM_RESET/SYSTEM_OFF unavailable: no AIC v1\n"); ++ reset = PSCI_RESET_OFF; ++ } ++ node = adt_path_offset_trace(adt, "/arm-io/wdt", path); ++ if (node < 0 || adt_get_reg(adt, path, "reg", 0, &wdt, NULL) < 0) { ++ printf("PSCI: SYSTEM_RESET/SYSTEM_OFF unavailable: no watchdog\n"); ++ reset = PSCI_RESET_OFF; ++ } ++ } ++ ++ if (psci_smc(PSCI_M1N1_SIP_HANDOFF, reset, aic, wdt) != PSCI_RET_SUCCESS) { ++ printf("PSCI: handoff refused, SYSTEM_RESET/SYSTEM_OFF unavailable\n"); ++ psci_smc(PSCI_M1N1_SIP_HANDOFF, PSCI_RESET_OFF, 0, 0); ++ return; ++ } ++ ++ static const char *const names[] = {"off", "warm reboot into the default entry", ++ "warm reboot into the proxy"}; ++ printf("PSCI: SYSTEM_RESET: %s%s\n", names[reset], ++ reset ? "; EL1 WFI/WFE trap to EL3" : ""); + } + + int psci_affinity_info(u64 mpidr) +diff --git a/src/psci.h b/src/psci.h +index 1d3a88e..7c81af7 100644 +--- a/src/psci.h ++++ b/src/psci.h +@@ -29,8 +29,9 @@ + + /* + * m1n1-private SiP call, made by m1n1 itself on the boot CPU right before it +- * starts a kernel: x1 = 1 to trap EL1 WFI/WFE to EL3 on this and every CPU +- * that later leaves the pen, x2 = AIC base, x3 = primary watchdog base. ++ * starts a kernel: x1 = enum psci_reset_mode (nonzero also traps EL1 WFI/WFE ++ * to EL3 on this and every CPU that later leaves the pen), x2 = AIC base, ++ * x3 = primary watchdog base. + */ + #define PSCI_M1N1_SIP_HANDOFF 0xc200ff00 + +@@ -59,6 +60,10 @@ + #define PSCI_RDV_PENDING 0x00 + #define PSCI_RDV_TRAPS 0x08 + ++/* psci_persist.reentry_flag */ ++#define PSCI_REENTRY_KEXEC 1 ++#define PSCI_REENTRY_RESET 2 ++ + #define SCR_EL3_IRQ (1 << 1) + #define SCR_EL3_FIQ (1 << 2) + #define SCR_EL3_TWI (1 << 12) +@@ -83,13 +88,27 @@ int psci_park_cpu(int cpu, u64 mpidr); + /* Bring a parked CPU back into m1n1 (re-entry); returns 0 if it was parked */ + int psci_unpark_cpu(int index, u64 mpidr, void *stack_top); + ++/* What SYSTEM_RESET does: payload variable psci_reset=0|proxy|boot (default boot) */ ++enum psci_reset_mode { ++ PSCI_RESET_OFF = 0, /* NOT_SUPPORTED: the kernel resets the SoC its own way */ ++ PSCI_RESET_BOOT = 1, /* back into m1n1, which boots the default boot entry */ ++ PSCI_RESET_PROXY = 2, /* back into m1n1, which only runs the proxy */ ++}; ++void psci_reset_configure(const char *val); ++enum psci_reset_mode psci_reset_mode_get(void); ++/* Set when m1n1 was re-entered by SYSTEM_RESET (psci_reentered() is too) */ ++bool psci_reset_reentered(void); ++/* _start_c: keep the first boot_args, SYSTEM_RESET starts m1n1 with them */ ++void psci_reset_save_boot_args(void); ++ + /* EL1, boot CPU, last thing before the kernel: see PSCI_M1N1_SIP_HANDOFF */ +-void psci_handoff(bool wfx_traps, u64 aic_base, u64 wdt_base); ++void psci_handoff(void); + + /* EL3 side */ + u64 psci_handle(u64 fid, u64 a1, u64 a2, u64 a3); + void psci_pen(void *cpu, u64 stack_top) __attribute__((noreturn)); + void psci_rendezvous_off(void) __attribute__((noreturn)); ++void psci_system_reset_entry(void *boot_args) __attribute__((noreturn)); + void psci_secondary_entry(void); + void _reentry(void); + +diff --git a/src/psci_asm.S b/src/psci_asm.S +index ff31f3a..61a6a02 100644 +--- a/src/psci_asm.S ++++ b/src/psci_asm.S +@@ -230,10 +230,52 @@ _reentry: + + adrp x1, psci_persist // .reentry_flag + add x1, x1, :lo12:psci_persist +- mov x2, #1 ++ mov x2, #PSCI_REENTRY_KEXEC + str x2, [x1] + dc civac, x1 + dsb sy + + mov x0, x19 + b _start ++ ++/* ++ * psci_system_reset_entry(boot_args): the end of SYSTEM_RESET, at EL3 on the ++ * calling CPU once every other CPU is in the pen (L1D clean) and nothing ++ * else runs. The kernel's EL1 MMU and caches may still be on; its stack and ++ * memory are never used again. Turn EL1 translation and caching off, write ++ * all data caches back, flag the reset and restart m1n1 at EL1h with the ++ * original boot_args, like a cold boot. _start resets the stack and reapplies ++ * the relocations; SP_EL3 goes back to the boot stack. ++ */ ++.globl psci_system_reset_entry ++.type psci_system_reset_entry, @function ++psci_system_reset_entry: ++ msr daifset, #0xf ++ mov x19, x0 ++ mov x1, #0x0800 // SCTLR_EL1: RES1 bits only, MMU and caches off ++ movk x1, #0x30d0, lsl #16 ++ msr sctlr_el1, x1 ++ isb ++ mov x0, #DCSW_OP_DCCISW ++ bl dcsw_op_all ++ tlbi alle1 ++ ic iallu ++ dsb sy ++ isb ++ ++ adrp x1, psci_persist // .reentry_flag ++ add x1, x1, :lo12:psci_persist ++ mov x2, #PSCI_REENTRY_RESET ++ str x2, [x1] ++ dc civac, x1 ++ dsb sy ++ ++ adrp x1, _start ++ add x1, x1, :lo12:_start ++ msr elr_el3, x1 ++ mov x1, #0x3c5 // EL1h, DAIF masked ++ msr spsr_el3, x1 ++ adrp x1, _stack_bot_el3 ++ mov sp, x1 ++ mov x0, x19 ++ eret +diff --git a/src/startup.c b/src/startup.c +index 167fae8..4d9ac39 100644 +--- a/src/startup.c ++++ b/src/startup.c +@@ -171,6 +171,7 @@ void _start_c(void *boot_args, void *base, u64 boot_el) + boot_cpu_idx = -1; // .data keeps the last run's value; find it again + boot_args_addr = (u64)boot_args; + memcpy(&cur_boot_args, boot_args, sizeof(cur_boot_args)); ++ psci_reset_save_boot_args(); + + adt = + (void *)(((u64)cur_boot_args.devtree) - cur_boot_args.virt_base + cur_boot_args.phys_base); diff --git a/boot/m1n1/patches/orchard/0019-bootmgr-Boot-the-default-entry-after-a-PSCI-SYSTEM_R.patch b/boot/m1n1/patches/orchard/0019-bootmgr-Boot-the-default-entry-after-a-PSCI-SYSTEM_R.patch new file mode 100644 index 00000000..a8039549 --- /dev/null +++ b/boot/m1n1/patches/orchard/0019-bootmgr-Boot-the-default-entry-after-a-PSCI-SYSTEM_R.patch @@ -0,0 +1,298 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Mon, 9 Nov 2026 16:03:26 +0100 +Subject: [PATCH] bootmgr: Boot the default entry after a PSCI SYSTEM_RESET + +SYSTEM_RESET brings m1n1 back with every other CPU parked and the +original boot_args, so m1n1 can finish the reboot itself the way the +host's orchard launch starts a system: bring main storage up, take the +first bootable entry in BLS order (the same default the host suggests; +there is still no remembered default) and start it with bootmgr_start(). +The board DTB file name comes from the last launch, which m1n1 now keeps +in .data; if no entry was ever launched (a kernel from the payload, say) +m1n1 doesn't guess the board and stays in the proxy. + +A re-entered m1n1 also skips the stale payload area for the logo, keeps +the picture on screen instead of clearing it and drawing the logo (the +entry's splash replaces it), and leaves SEP alone, which was never +reset. A 96x96 square at the top left shows how far it got: cyan back in +m1n1, green loading the entry, magenta no previous launch, red storage +or entry failure. Any failure ends in the USB proxy, as a cold boot with +nothing to do does; m1n1 never turns it into a cold reset. + +With psci_reset=proxy it stops in the proxy right after the re-entry. + +Assisted-by: LLM +--- + rust/src/bls.rs | 48 +++++++++++++++++++++++++++++ + src/bootmgr.c | 81 +++++++++++++++++++++++++++++++++++++++++++++++++ + src/bootmgr.h | 2 ++ + src/fb.c | 4 ++- + src/main.c | 16 +++++++--- + 5 files changed, 146 insertions(+), 5 deletions(-) + +diff --git a/rust/src/bls.rs b/rust/src/bls.rs +index 35ddc1b..4570d63 100644 +--- a/rust/src/bls.rs ++++ b/rust/src/bls.rs +@@ -629,6 +629,54 @@ pub unsafe extern "C" fn rust_boot_entries( + text.leak().as_mut_ptr() as *mut c_void + } + ++/// The entry a boot without a host starts: the first bootable one in BLS ++/// order, as the host's `orchard launch` suggests (there is no remembered ++/// default). ++pub fn default_entry(listing: &Listing) -> Option<&Entry> { ++ listing.entries.iter().find(|entry| entry.bootable()) ++} ++ ++/// Writes the id of main storage's default entry (see `default_entry`), ++/// NUL-terminated, to `id` (`size` bytes). Main storage must be up; ++/// `devicetree` may be NULL. Returns 0, or -1 with the reason on the console. ++#[no_mangle] ++pub unsafe extern "C" fn rust_boot_default_entry( ++ devicetree: *const c_char, ++ id: *mut c_char, ++ size: c_size_t, ++) -> c_int { ++ let devicetree = match devicetree.is_null() { ++ true => None, ++ false => unsafe { CStr::from_ptr(devicetree) }.to_str().ok(), ++ }; ++ let listing = read_entries(storage::MainStorage::new, devicetree); ++ log(&listing); ++ let entry = match &listing { ++ Err(error) => { ++ println!("bls: {}", error); ++ return -1; ++ } ++ Ok(listing) => match default_entry(listing) { ++ Some(entry) => entry, ++ None => { ++ println!("bls: no bootable entry"); ++ return -1; ++ } ++ }, ++ }; ++ let name = entry.id.as_bytes(); ++ if name.len() >= size || name.contains(&0) { ++ println!("bls: entry id too long"); ++ return -1; ++ } ++ unsafe { ++ core::ptr::copy_nonoverlapping(name.as_ptr(), id as *mut u8, name.len()); ++ *id.add(name.len()) = 0; ++ } ++ println!("bls: default entry {}", entry.id); ++ 0 ++} ++ + #[repr(C)] + pub struct BootFiles { + kernel: *mut c_void, +diff --git a/src/bootmgr.c b/src/bootmgr.c +index b91b6a6..a746673 100644 +--- a/src/bootmgr.c ++++ b/src/bootmgr.c +@@ -8,6 +8,7 @@ + #include "heapblock.h" + #include "kboot.h" + #include "mitigations.h" ++#include "psci.h" + #include "smp.h" + #include "string.h" + #include "utils.h" +@@ -32,6 +33,32 @@ struct boot_files { + + int rust_boot_entry_load(const char *id, const char *devicetree, bool splash, + struct boot_files *files); ++int rust_boot_default_entry(const char *devicetree, char *id, size_t size); ++ ++/* ++ * The board DTB file name of the last launch (the host's orchard launch ++ * passes it), kept across a SYSTEM_RESET so m1n1 can boot an entry without ++ * the host and without guessing the board. ++ */ ++static struct { ++ bool launched; ++ bool have_devicetree; ++ char devicetree[MAX_NAME]; ++} last_launch __attribute__((section(".data"))); ++ ++/* Warm reset progress, top left */ ++#define MARK_REENTERED 0x00b7ff ++#define MARK_LOADING 0x00c853 ++#define MARK_NO_LAUNCH 0xff00ff ++#define MARK_FAILED 0xff1744 ++ ++static void bootmgr_mark(u32 color) ++{ ++ if (!fb.ptr) ++ return; ++ fb_fill(0, 0, min(fb.width, 96U), min(fb.height, 96U), int2rgb(color)); ++ fb_update(); ++} + + /* Called from Rust: the entry's splash, pixels as b, g, r, 0, centred on black. */ + void bootmgr_paint_splash(const void *pixels, u32 width, u32 height) +@@ -104,6 +131,11 @@ int bootmgr_start(const char *id_arg, const char *devicetree_arg, u64 flags) + return -1; + } + ++ last_launch.launched = true; ++ last_launch.have_devicetree = devicetree_arg; ++ if (devicetree_arg) ++ memcpy(last_launch.devicetree, devicetree, sizeof(devicetree)); ++ + /* The kernel, initrd and DT are m1n1's now, past any limit the host set. */ + heapblock_set_limit(NULL); + +@@ -136,8 +168,57 @@ int bootmgr_start(const char *id_arg, const char *devicetree_arg, u64 flags) + return kboot_boot(kernel); + } + ++/* ++ * m1n1 was restarted by a PSCI SYSTEM_RESET from the kernel: every other CPU ++ * is parked, the watchdog is off and memory is as after a cold start. Do ++ * what the host's orchard launch would: bring up main storage and start the ++ * default entry with the last launch's board DTB. Returns 0 when the kernel ++ * is set up to run, -1 (proxy) otherwise. ++ */ ++int bootmgr_warm_reset(void) ++{ ++ char id[MAX_NAME]; ++ const char *devicetree = last_launch.have_devicetree ? last_launch.devicetree : NULL; ++ ++ bootmgr_mark(MARK_REENTERED); ++ if (psci_reset_mode_get() == PSCI_RESET_PROXY) { ++ printf("bootmgr: warm reset: psci_reset=proxy, not booting\n"); ++ return -1; ++ } ++ if (!last_launch.launched) { ++ printf("bootmgr: warm reset: no boot entry was launched before, not guessing the board\n"); ++ bootmgr_mark(MARK_NO_LAUNCH); ++ return -1; ++ } ++ ++ printf("bootmgr: warm reset: starting the default entry (devicetree %s)\n", ++ devicetree ? devicetree : "from the entry"); ++ if (!main_storage_init()) { ++ printf("bootmgr: warm reset: main storage did not come up\n"); ++ bootmgr_mark(MARK_FAILED); ++ return -1; ++ } ++ if (rust_boot_default_entry(devicetree, id, sizeof(id)) < 0) { ++ main_storage_shutdown(); ++ bootmgr_mark(MARK_FAILED); ++ return -1; ++ } ++ bootmgr_mark(MARK_LOADING); ++ if (bootmgr_start(id, devicetree, 0) < 0) { ++ bootmgr_mark(MARK_FAILED); ++ return -1; ++ } ++ return 0; ++} ++ + #else + ++int bootmgr_warm_reset(void) ++{ ++ printf("bootmgr: not supported in this build\n"); ++ return -1; ++} ++ + int bootmgr_start(const char *id, const char *devicetree, u64 flags) + { + UNUSED(id); +diff --git a/src/bootmgr.h b/src/bootmgr.h +index 8a03baa..f3b86ee 100644 +--- a/src/bootmgr.h ++++ b/src/bootmgr.h +@@ -8,5 +8,7 @@ + #define BOOTMGR_NO_SPLASH BIT(0) + + int bootmgr_start(const char *id, const char *devicetree, u64 flags); ++/* After a PSCI SYSTEM_RESET: boot the default entry the way the last launch did */ ++int bootmgr_warm_reset(void); + + #endif +diff --git a/src/fb.c b/src/fb.c +index ba606b2..0aa88b8 100644 +--- a/src/fb.c ++++ b/src/fb.c +@@ -1,6 +1,7 @@ + /* SPDX-License-Identifier: MIT */ + + #include "fb.h" ++#include "psci.h" + #include "assert.h" + #include "iodev.h" + #include "malloc.h" +@@ -464,14 +465,16 @@ void fb_init(bool clear) + &orig_logo); + } + ++ // After a re-entry from Linux the payload area behind m1n1 is stale ++ bool payload_ok = !psci_reentered(); + u32 *splash, splash_w, splash_h; +- if (payload_splash(&splash, &splash_w, &splash_h) && splash_w <= fb.width && ++ if (payload_ok && payload_splash(&splash, &splash_w, &splash_h) && splash_w <= fb.width && + splash_h <= fb.height) { + custom_logo.ptr = splash; + custom_logo.width = splash_w; + custom_logo.height = splash_h; + logo = &custom_logo; +- } else if (payload_logo(&custom_128, &custom_256)) { ++ } else if (payload_ok && payload_logo(&custom_128, &custom_256)) { + custom_logo = *logo; + if (custom_logo.width == 256) { + custom_logo.ptr = custom_256; +diff --git a/src/main.c b/src/main.c +index 3f32380..8966e41 100644 +--- a/src/main.c ++++ b/src/main.c +@@ -7,6 +7,7 @@ + + #include "adt.h" + #include "aic.h" ++#include "bootmgr.h" + #include "cpufreq.h" + #include "display.h" + #include "exception.h" +@@ -127,6 +128,9 @@ void run_actions(void) + printf("Valid payload found\n"); + return; + } ++ ++ if (psci_reset_reentered() && bootmgr_warm_reset() == 0) ++ return; + fb_set_active(true); + + printf("No valid payload found\n"); +@@ -173,9 +177,11 @@ void m1n1_main(void) + // Kick DCP to sleep, so dodgy monitors which cause reconnect cycles don't cause us to lose the + // framebuffer. + display_shutdown(DCP_SLEEP_IF_EXTERNAL); +- // On idevice we need to always clear, because otherwise it looks scuffed on white devices +- fb_init(!is_mac); +- fb_display_logo(); ++ // On idevice we need to always clear, because otherwise it looks scuffed on white devices. ++ // After a warm reset, keep the picture until the next system's splash. ++ fb_init(!is_mac && !psci_reset_reentered()); ++ if (!psci_reset_reentered()) ++ fb_display_logo(); + #ifdef FB_SILENT_MODE + fb_set_active(!cur_boot_args.video.display); + #else +@@ -184,7 +190,9 @@ void m1n1_main(void) + #endif + + cpufreq_fixup(); +- sep_init(); ++ // SEP was never reset; it is still running what it ran at the first boot ++ if (!psci_reset_reentered()) ++ sep_init(); + #endif + + printf("Initialization complete.\n"); diff --git a/boot/m1n1/patches/orchard/0020-kboot-Mark-retained-hardware-after-PSCI-re-entry.patch b/boot/m1n1/patches/orchard/0020-kboot-Mark-retained-hardware-after-PSCI-re-entry.patch new file mode 100644 index 00000000..08935588 --- /dev/null +++ b/boot/m1n1/patches/orchard/0020-kboot-Mark-retained-hardware-after-PSCI-re-entry.patch @@ -0,0 +1,38 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Wed, 11 Nov 2026 12:00:00 +0100 +Subject: [PATCH] kboot: Mark retained hardware after PSCI re-entry + +SYSTEM_RESET re-enters m1n1 without resetting all peripherals. Mark the +next kernel's /chosen with the existing linux,booted-from-kexec boolean +so retained hardware is not mistaken for the cold iBoot baseline. + +Use psci_reentered(), which covers both SYSTEM_RESET and the kexec +trampoline. This also preserves the contract when kboot loads a fresh +BLS devicetree instead of inheriting the outgoing kernel's tree. + +The Orchard kernel's G5P, PMGR, PCIe/ANS and AOP consumers all use this +property to select retained-state handling, with their existing hardware +and certification guards. The only other reader is PowerPC-only. No +kernel consumer needs a separate PSCI-specific property. + +Assisted-by: LLM +--- +diff --git a/src/kboot.c b/src/kboot.c +index 1d4b26f..7dc9874 100644 +--- a/src/kboot.c ++++ b/src/kboot.c +@@ -278,6 +278,13 @@ static int dt_set_chosen(void) + printf("FDT: %s = '%s'\n", name, value); + } + ++ /* Re-entry retains peripherals, just like a kernel-to-kernel handoff. */ ++ if (psci_reentered()) { ++ if (fdt_setprop(dt, node, "linux,booted-from-kexec", NULL, 0)) ++ bail("FDT: couldn't mark retained hardware after m1n1 re-entry\n"); ++ printf("FDT: linux,booted-from-kexec (m1n1 re-entry)\n"); ++ } ++ + if (initrd_start && initrd_size) { + if (fdt_setprop_u64(dt, node, "linux,initrd-start", (u64)initrd_start)) + bail("FDT: couldn't set chosen.linux,initrd-start property\n"); diff --git a/boot/m1n1/psci-loader b/boot/m1n1/psci-loader new file mode 100755 index 00000000..c21f1801 --- /dev/null +++ b/boot/m1n1/psci-loader @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""Append m1n1 PSCI payload variables to a bare m1n1.bin. + +m1n1 entered at EL3 (A7-A9 from Pongo) provides PSCI by default, including +SYSTEM_RESET as a warm reboot into the default boot entry; a bare m1n1.bin +needs no framing for that. Use this to get the other settings, before +assembling any kernel/DTB/logo payload. The output works with +orchard run/build --loader or launch's loaders/m1n1.bin. m1n1 entered at EL1 +always uses the spin table. +""" +import argparse +from pathlib import Path +import shutil + +RESET = {'boot': b'', 'proxy': b'psci_reset=proxy\n', 'off': b'psci_reset=0\n'} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--device', required=True, choices=('j71t',)) + parser.add_argument('--loader', type=Path, required=True) + parser.add_argument('--output', type=Path, required=True) + group = parser.add_mutually_exclusive_group() + group.add_argument('--system-reset', choices=tuple(RESET), default='boot', + help='boot: warm reboot into the default entry (default); proxy: stop in ' + 'the m1n1 proxy; off: NOT_SUPPORTED, kernel resets the SoC itself') + group.add_argument('--no-psci', action='store_true', help='psci=0: spin table, no PSCI') + args = parser.parse_args() + if not args.loader.is_file() or not args.loader.stat().st_size: + parser.error('input must be a nonempty bare m1n1.bin') + # Exclusive creation: never replace the original or another boot artifact. + with args.loader.open('rb') as source, args.output.open('xb') as output: + shutil.copyfileobj(source, output) + output.write(b'psci=0\n' if args.no_psci else b'psci=1\n' + RESET[args.system_reset]) + print(args.output) + + +if __name__ == '__main__': + main() diff --git a/kernel/patches/devices/s800x/0002-arm64-dts-apple-preserve-PMU-and-RTC-interrupt-owner.patch b/kernel/patches/devices/s800x/0002-arm64-dts-apple-preserve-PMU-and-RTC-interrupt-owner.patch new file mode 100644 index 00000000..ff7c75cc --- /dev/null +++ b/kernel/patches/devices/s800x/0002-arm64-dts-apple-preserve-PMU-and-RTC-interrupt-owner.patch @@ -0,0 +1,42 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Sun, 8 Nov 2026 17:15:00 +0100 +Subject: [PATCH] arm64: dts: apple: preserve PMU and RTC interrupt ownership + +Shared wake events must have one interrupt owner. Preserve the PMU and +RTC event split so that acknowledgement by one consumer cannot steal the +other consumer's wake event. + +Assisted-by: LLM +--- + arch/arm64/boot/dts/apple/s800x-ipad5.dtsi | 7 +++++-- + 1 file changed, 5 insertions(+), 2 deletions(-) + +diff --git a/arch/arm64/boot/dts/apple/s800x-ipad5.dtsi b/arch/arm64/boot/dts/apple/s800x-ipad5.dtsi +index 2ce40b469..20de6953f 100644 +--- a/arch/arm64/boot/dts/apple/s800x-ipad5.dtsi ++++ b/arch/arm64/boot/dts/apple/s800x-ipad5.dtsi +@@ -191,11 +191,11 @@ + interrupt-parent = <&pmu_gpio>; + /* + * The PMU line, ADC done, then the events the iPadOS +- * charger subscribes to. ++ * charger subscribes to, except RTC event 16: Linux gives that ++ * event exclusively to rtc@500, not to source identification. + */ + interrupts = <240 IRQ_TYPE_EDGE_RISING>, + <176 IRQ_TYPE_EDGE_RISING>, +- <16 IRQ_TYPE_EDGE_RISING>, + <40 IRQ_TYPE_EDGE_RISING>, + <41 IRQ_TYPE_EDGE_RISING>, + <42 IRQ_TYPE_EDGE_RISING>, +@@ -218,6 +218,9 @@ + reg = <0x500 0x10>; + nvmem-cells = <&rtc_offset>; + nvmem-cell-names = "rtc_offset"; ++ interrupt-parent = <&pmu_gpio>; ++ interrupts = <16 IRQ_TYPE_EDGE_RISING>; ++ wakeup-source; + }; + + /* D2365 register 0x31c bit 0 */ diff --git a/kernel/patches/drivers/rtc/0001-rtc-preserve-PMU-and-RTC-interrupt-ownership.patch b/kernel/patches/drivers/rtc/0001-rtc-preserve-PMU-and-RTC-interrupt-ownership.patch new file mode 100644 index 00000000..e1ea9505 --- /dev/null +++ b/kernel/patches/drivers/rtc/0001-rtc-preserve-PMU-and-RTC-interrupt-ownership.patch @@ -0,0 +1,206 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Sun, 8 Nov 2026 17:15:00 +0100 +Subject: [PATCH] rtc: preserve PMU and RTC interrupt ownership + +Shared wake events must have one interrupt owner. Preserve the PMU and +RTC event split so that acknowledgement by one consumer cannot steal the +other consumer's wake event. + +Assisted-by: LLM +--- + drivers/rtc/rtc-apple-pmic.c | 127 ++++++++++++++++++++++++++++++++++- + 1 file changed, 126 insertions(+), 1 deletion(-) + +diff --git a/drivers/rtc/rtc-apple-pmic.c b/drivers/rtc/rtc-apple-pmic.c +index ee56618c8..d31d4c716 100644 +--- a/drivers/rtc/rtc-apple-pmic.c ++++ b/drivers/rtc/rtc-apple-pmic.c +@@ -12,6 +12,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -24,22 +25,30 @@ + #include + + #define RTC_COUNTER_SIZE 6 ++#define RTC_ALARM_SIZE 4 ++#define RTC_CONTROL_RUN BIT(0) ++#define RTC_CONTROL_ALARM BIT(6) + + /** + * struct apple_pmic_rtc_regs - RTC Hardware registers + * @control: RTC Control + * @counter: Time Counter ++ * @alarm: Alarm seconds counter ++ * @has_alarm: Alarm registers are present + */ + + struct apple_pmic_rtc_regs { + u32 control; + u32 counter; ++ u32 alarm; ++ bool has_alarm; + }; + + /** + * struct apple_pmic_rtc - RTC driver internal structure + * @rtc: RTC device + * @regmap: regmap used to access registers ++ * @dev: device used for logging and wake setup + * @nvmem_cell: nvmem cell for offset + * @regs: hardware registers + * @base: RTC base address +@@ -183,17 +192,107 @@ static int apple_pmic_rtc_read_time(struct device *dev, struct rtc_time *tm) + return 0; + } + ++static int apple_pmic_rtc_alarm_irq_enable(struct device *dev, ++ unsigned int enabled) ++{ ++ struct apple_pmic_rtc *ap_rtc = dev_get_drvdata(dev); ++ ++ /* AppleD2365PMU 0xfffffff006bbbc1c writes these exact values. */ ++ return regmap_write(ap_rtc->regmap, ++ ap_rtc->base + ap_rtc->regs->control, ++ RTC_CONTROL_RUN | ++ (enabled ? RTC_CONTROL_ALARM : 0)); ++} ++ ++static int apple_pmic_rtc_read_alarm(struct device *dev, ++ struct rtc_wkalrm *alarm) ++{ ++ struct apple_pmic_rtc *ap_rtc = dev_get_drvdata(dev); ++ u8 buf[RTC_ALARM_SIZE]; ++ unsigned int control; ++ time64_t secs; ++ int rc; ++ ++ rc = regmap_bulk_read(ap_rtc->regmap, ++ ap_rtc->base + ap_rtc->regs->alarm, ++ buf, sizeof(buf)); ++ if (rc) ++ return rc; ++ rc = regmap_read(ap_rtc->regmap, ++ ap_rtc->base + ap_rtc->regs->control, &control); ++ if (rc) ++ return rc; ++ ++ secs = (time64_t)get_unaligned_le32(buf) + ap_rtc->offset; ++ rtc_time64_to_tm(secs, &alarm->time); ++ alarm->enabled = !!(control & RTC_CONTROL_ALARM); ++ alarm->pending = 0; ++ return 0; ++} ++ ++static int apple_pmic_rtc_set_alarm(struct device *dev, ++ struct rtc_wkalrm *alarm) ++{ ++ struct apple_pmic_rtc *ap_rtc = dev_get_drvdata(dev); ++ u8 buf[RTC_ALARM_SIZE]; ++ time64_t secs; ++ int rc; ++ ++ if (!alarm->enabled) ++ return apple_pmic_rtc_alarm_irq_enable(dev, 0); ++ ++ secs = rtc_tm_to_time64(&alarm->time) - ap_rtc->offset; ++ if (secs <= 0 || secs > U32_MAX) ++ return -ERANGE; ++ put_unaligned_le32(secs, buf); ++ ++ /* iOS programs 0x508 first, then enables the one-shot at 0x500. */ ++ rc = regmap_bulk_write(ap_rtc->regmap, ++ ap_rtc->base + ap_rtc->regs->alarm, ++ buf, sizeof(buf)); ++ if (rc) ++ return rc; ++ return apple_pmic_rtc_alarm_irq_enable(dev, 1); ++} ++ ++static irqreturn_t apple_pmic_rtc_alarm_irq(int irq, void *data) ++{ ++ struct apple_pmic_rtc *ap_rtc = data; ++ int rc; ++ ++ /* The PMU event read is the acknowledge; iOS then clears alarm bit 6. */ ++ rc = regmap_update_bits(ap_rtc->regmap, ++ ap_rtc->base + ap_rtc->regs->control, ++ RTC_CONTROL_ALARM, 0); ++ if (rc) ++ dev_warn_ratelimited(ap_rtc->dev, ++ "failed to disable fired alarm: %d\n", rc); ++ rtc_update_irq(ap_rtc->rtc, 1, RTC_IRQF | RTC_AF); ++ return IRQ_HANDLED; ++} ++ + static const struct rtc_class_ops apple_pmic_rtc_ops = { + .read_time = apple_pmic_rtc_read_time, + .set_time = apple_pmic_rtc_set_time, + }; + ++static const struct rtc_class_ops apple_pmic_rtc_alarm_ops = { ++ .read_time = apple_pmic_rtc_read_time, ++ .set_time = apple_pmic_rtc_set_time, ++ .read_alarm = apple_pmic_rtc_read_alarm, ++ .set_alarm = apple_pmic_rtc_set_alarm, ++ .alarm_irq_enable = apple_pmic_rtc_alarm_irq_enable, ++}; ++ + static const struct apple_pmic_rtc_regs amber_regs = { + .counter = 0x6 + }; + + static const struct apple_pmic_rtc_regs antigua_regs = { +- .counter = 0x2 ++ .control = 0x0, ++ .counter = 0x2, ++ .alarm = 0x8, ++ .has_alarm = true, + }; + + static const struct apple_pmic_rtc_regs calpe_regs = { +@@ -230,6 +329,7 @@ static int apple_pmic_rtc_probe(struct platform_device *pdev) + struct device_node *node = pdev->dev.of_node; + const struct of_device_id *match; + struct apple_pmic_rtc *ap_rtc; ++ int irq; + int rc; + + match = of_match_node(apple_pmic_id_table, node); +@@ -264,6 +364,31 @@ static int apple_pmic_rtc_probe(struct platform_device *pdev) + ap_rtc->rtc->ops = &apple_pmic_rtc_ops; + ap_rtc->rtc->range_min = INT_MIN; + ap_rtc->rtc->range_max = INT_MAX; ++ if (ap_rtc->regs->has_alarm) { ++ irq = platform_get_irq_optional(pdev, 0); ++ if (irq == -EPROBE_DEFER) ++ return irq; ++ if (irq > 0) { ++ rc = devm_request_threaded_irq(&pdev->dev, irq, NULL, ++ apple_pmic_rtc_alarm_irq, ++ IRQF_ONESHOT, ++ "apple-pmic-rtc", ap_rtc); ++ if (rc) ++ return dev_err_probe(&pdev->dev, rc, ++ "cannot request alarm IRQ\n"); ++ device_init_wakeup(&pdev->dev, true); ++ rc = devm_pm_set_wake_irq(&pdev->dev, irq); ++ if (rc) ++ return dev_err_probe(&pdev->dev, rc, ++ "cannot set alarm wake IRQ\n"); ++ ap_rtc->rtc->ops = &apple_pmic_rtc_alarm_ops; ++ dev_info(&pdev->dev, ++ "D2365 alarm at 0x%03x on PMU event IRQ %d\n", ++ ap_rtc->base + ap_rtc->regs->alarm, irq); ++ } else if (irq != -ENXIO) { ++ return irq; ++ } ++ } + + return devm_rtc_register_device(ap_rtc->rtc); + } diff --git a/kernel/patches/drivers/usb/0014-usb-preserve-PMU-and-RTC-interrupt-ownership.patch b/kernel/patches/drivers/usb/0014-usb-preserve-PMU-and-RTC-interrupt-ownership.patch new file mode 100644 index 00000000..58875bd3 --- /dev/null +++ b/kernel/patches/drivers/usb/0014-usb-preserve-PMU-and-RTC-interrupt-ownership.patch @@ -0,0 +1,36 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Sun, 8 Nov 2026 17:15:00 +0100 +Subject: [PATCH] usb: preserve PMU and RTC interrupt ownership + +Shared wake events must have one interrupt owner. Preserve the PMU and +RTC event split so that acknowledgement by one consumer cannot steal the +other consumer's wake event. + +Assisted-by: LLM +--- + drivers/usb/gadget/composite.c | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/drivers/usb/gadget/composite.c b/drivers/usb/gadget/composite.c +index 1da3effb2..14e87fbc8 100644 +--- a/drivers/usb/gadget/composite.c ++++ b/drivers/usb/gadget/composite.c +@@ -968,7 +968,7 @@ static int composite_apple_current_limit(struct usb_composite_dev *cdev, + u16 w_value, u16 w_index) + { + cdev->apple_extra_ma = (s16)w_index; +- DBG(cdev, "Apple host current grant: extra %d mA, suspend %d mA\n", ++ dev_info(&cdev->gadget->dev, "Apple host current grant: extra %d mA, suspend %d mA\n", + (s16)w_index, (s16)w_value); + composite_vbus_draw(cdev, cdev->vbus_draw_ma); + return 0; +@@ -2221,7 +2221,7 @@ unknown: + goto check_value; + } + +- if (ctrl->bRequestType == (USB_DIR_OUT | USB_TYPE_VENDOR | ++ if ((ctrl->bRequestType & ~USB_DIR_IN) == (USB_TYPE_VENDOR | + USB_RECIP_DEVICE) && + ctrl->bRequest == USB_APPLE_REQ_CURRENT_LIMIT && !w_length && + le16_to_cpu(cdev->desc.idVendor) == USB_APPLE_VENDOR_ID) { diff --git a/tests/test_m1n1_psci.py b/tests/test_m1n1_psci.py new file mode 100644 index 00000000..85091696 --- /dev/null +++ b/tests/test_m1n1_psci.py @@ -0,0 +1,81 @@ +"""Host-only framing checks for the J71t PSCI loader variables.""" +from pathlib import Path +import json +import subprocess +import sys +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +TOOL = ROOT / 'boot/m1n1/psci-loader' + + +class PSCILoader(unittest.TestCase): + def test_payload_and_no_overwrite(self): + with tempfile.TemporaryDirectory() as tmp: + loader, output = Path(tmp) / 'm1n1.bin', Path(tmp) / 'psci.bin' + original = b'bare loader\0' + loader.write_bytes(original) + cmd = [sys.executable, str(TOOL), '--device', 'j71t', '--loader', str(loader), + '--output', str(output)] + result = subprocess.run(cmd, capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(output.read_bytes(), original + b'psci=1\n') + self.assertEqual(loader.read_bytes(), original) + self.assertNotEqual(subprocess.run(cmd, capture_output=True).returncode, 0) + self.assertEqual(output.read_bytes(), original + b'psci=1\n') + + def test_reset_and_off_variables(self): + cases = {('--system-reset', 'proxy'): b'psci=1\npsci_reset=proxy\n', + ('--system-reset', 'off'): b'psci=1\npsci_reset=0\n', + ('--system-reset', 'boot'): b'psci=1\n', + ('--no-psci',): b'psci=0\n'} + with tempfile.TemporaryDirectory() as tmp: + loader = Path(tmp) / 'm1n1.bin' + loader.write_bytes(b'bare') + for n, (extra, records) in enumerate(cases.items()): + output = Path(tmp) / f'out{n}.bin' + result = subprocess.run([sys.executable, str(TOOL), '--device', 'j71t', '--loader', + str(loader), '--output', str(output), *extra], + capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(output.read_bytes(), b'bare' + records) + both = subprocess.run([sys.executable, str(TOOL), '--device', 'j71t', '--loader', str(loader), + '--output', str(Path(tmp) / 'x.bin'), '--no-psci', '--system-reset', + 'proxy'], capture_output=True) + self.assertNotEqual(both.returncode, 0) + + def test_series_has_reset_patches(self): + names = sorted(p.name for p in (ROOT / 'boot/m1n1/patches/orchard').glob('*.patch')) + for prefix in ('0015-psci-', '0016-psci-Turn-the-resident-EL3-PSCI-on-by-default', + '0017-psci-Trap-EL1-WFI-WFE', '0018-psci-Implement-SYSTEM_RESET', + '0019-bootmgr-Boot-the-default-entry'): + self.assertTrue(any(n.startswith(prefix) for n in names), prefix) + + def test_manifest_psci_record_keeps_logo_first(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + for name, data in {'loader': b'M1N1', 'dtb': b'DTB', 'kernel': b'KERNEL', + 'initrd': b'INITRD'}.items(): + (root / name).write_bytes(data) + logo = b'm1n1_logo_256128' + bytes(4 * (256 * 256 + 128 * 128)) + (root / 'logo').write_bytes(logo) + records = b'chosen.bootargs=console=tty0\npsci=1\n' + (root / 'profile.json').write_text(json.dumps({'bootargs': records.decode()})) + result = subprocess.run([sys.executable, '-I', str(ROOT / 'src/orchard/image/assemble.py'), + '--manifest', str(root / 'profile.json'), '--loader', str(root / 'loader'), + '--logo', str(root / 'logo'), '--dtb', str(root / 'dtb'), + '--kernel', str(root / 'kernel'), '--initramfs', str(root / 'initrd'), + '--output', str(root / 'payload')], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertTrue((root / 'payload').read_bytes().startswith(b'M1N1' + logo + records + b'DTB')) + + def test_board_requires_explicit_opt_in(self): + for device in ([], ['--device', 'j71b']): + result = subprocess.run([sys.executable, str(TOOL), *device, '--loader', 'unused', + '--output', 'unused'], capture_output=True) + self.assertNotEqual(result.returncode, 0) + + +if __name__ == '__main__': + unittest.main()