From ca3266cab0cdc75ed9424a2e674883ab937f518d Mon Sep 17 00:00:00 2001 From: brkosebay Date: Sun, 8 Nov 2026 16:00:00 +0100 Subject: [PATCH 01/10] AOP sensors and certified warm reload kernel/configs/apple-16k/config: CONFIG_APPLE_AOP_GYRO=m CONFIG_APPLE_AOP_PRESSURE=m These options provide aop and its Kconfig dependencies on both page sizes. Assisted-by: LLM --- ...9-AOP-s-boot-arguments-and-sensor-ca.patch | 116 + chips/s8003.toml | 3 + devices/j71t/device.toml | 7 +- devices/j71t/firmware.json | 34 + docs/compatibility.md | 3 +- kernel/configs/apple-16k/config | 2 + .../s8000/0008-aop-implementation.patch | 121 + .../drivers/iio/0005-aop-implementation.patch | 760 ++++ .../pmdomain/0001-aop-implementation.patch | 24 + .../drivers/soc-apple/0014-aop-bindings.patch | 127 + .../soc-apple/0015-aop-implementation.patch | 3285 +++++++++++++++++ tests/test_j71t_data.py | 31 + 12 files changed, 4510 insertions(+), 3 deletions(-) create mode 100644 boot/m1n1/patches/orchard/0015-kboot-Pass-the-A9-AOP-s-boot-arguments-and-sensor-ca.patch create mode 100644 kernel/patches/devices/s8000/0008-aop-implementation.patch create mode 100644 kernel/patches/drivers/iio/0005-aop-implementation.patch create mode 100644 kernel/patches/drivers/pmdomain/0001-aop-implementation.patch create mode 100644 kernel/patches/drivers/soc-apple/0014-aop-bindings.patch create mode 100644 kernel/patches/drivers/soc-apple/0015-aop-implementation.patch create mode 100644 tests/test_j71t_data.py diff --git a/boot/m1n1/patches/orchard/0015-kboot-Pass-the-A9-AOP-s-boot-arguments-and-sensor-ca.patch b/boot/m1n1/patches/orchard/0015-kboot-Pass-the-A9-AOP-s-boot-arguments-and-sensor-ca.patch new file mode 100644 index 00000000..13245cd5 --- /dev/null +++ b/boot/m1n1/patches/orchard/0015-kboot-Pass-the-A9-AOP-s-boot-arguments-and-sensor-ca.patch @@ -0,0 +1,116 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Mon, 2 Nov 2026 15:07:08 +0100 +Subject: [PATCH] kboot: Pass the A9 AOP's boot arguments and sensor + calibration + +The A9's AOP firmware takes more than the ECID from its loader: the AOP +target number (iop-aop-nub aop-target), the chip's ID (/chosen chip-id) +and revision (/arm-io chip-revision). Its accelerometer, gyroscope and +pressure sensor also send no reports until the driver has given them +the unit's factory calibration, which iBoot resolves from syscfg into +the sensors' nodes below iop-aop-nub. + +Copy the three numbers into the AOP's node (alias aop) as +apple,aop-target, apple,chip-id and apple,chip-revision, and each +sensor's calibration properties into the AOP's child node of the same +name, prefixed with "apple,". Unprovisioned "syscfg/" references and +nodes the device tree doesn't have are skipped. The orientation +properties are not copied: the A9 firmware rejects them and the board's +mount-matrix describes the mounting. + +The values are the unit's own and never leave the device: the kernel +sends them to the AOP and nothing else. + +Assisted-by: LLM +--- + src/kboot.c | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++- + 1 file changed, 68 insertions(+), 1 deletion(-) + +diff --git a/src/kboot.c b/src/kboot.c +index ebd6282..7ce9d72 100644 +--- a/src/kboot.c ++++ b/src/kboot.c +@@ -1005,6 +1005,73 @@ static int dt_set_als(void) + return 0; + } + ++/* ++ * The A9's AOP firmware also takes the AOP target number and the chip's ID and ++ * revision in its boot arguments, and each motion sensor's factory ++ * calibration, which iBoot resolves from syscfg into the sensor's node below ++ * iop-aop-nub. Copy them into the AOP's node and its sensor child nodes of the ++ * same name; the calibration keeps its name with an "apple," prefix. ++ */ ++static const char *const aop_sensor_calibrations[] = { ++ "low-temp-accel-offset", ++ "accel-sensitivity-calibration", ++ "accel-range-sensitivity-cal", ++ "accel-interrupt-calibration", ++ "gyro-sensitivity-calibration", ++ "gyro-range-sensitivity-cal", ++ "gyro-interrupt-calibration", ++ "gyro-trim-calibration", ++ "pressure-offset-calibration", ++ "pressure-accel-sensitivity", ++ "pressure-heater-resistance", ++}; ++ ++static int dt_set_aop_sensors(int node) ++{ ++ int nub = adt_path_offset(adt, "/arm-io/aop/iop-aop-nub"); ++ if (nub < 0) ++ return 0; ++ ++ u32 val; ++ if (ADT_GETPROP(adt, nub, "aop-target", &val) >= 0 && ++ fdt_setprop_u32(dt, node, "apple,aop-target", val)) ++ bail("FDT: failed to set apple,aop-target\n"); ++ ++ int chosen = adt_path_offset(adt, "/chosen"); ++ if (chosen >= 0 && ADT_GETPROP(adt, chosen, "chip-id", &val) >= 0 && ++ fdt_setprop_u32(dt, node, "apple,chip-id", val)) ++ bail("FDT: failed to set apple,chip-id\n"); ++ ++ int arm_io = adt_path_offset(adt, "/arm-io"); ++ if (arm_io >= 0 && ADT_GETPROP(adt, arm_io, "chip-revision", &val) >= 0 && ++ fdt_setprop_u32(dt, node, "apple,chip-revision", val)) ++ bail("FDT: failed to set apple,chip-revision\n"); ++ ++ int sensor = nub; ++ ADT_FOREACH_CHILD(adt, sensor) ++ { ++ const char *name = adt_get_name(adt, sensor); ++ ++ for (size_t i = 0; i < ARRAY_SIZE(aop_sensor_calibrations); i++) { ++ u32 len; ++ const u8 *blob = adt_getprop(adt, sensor, aop_sensor_calibrations[i], &len); ++ if (!blob || !len || (len >= 7 && !memcmp(blob, "syscfg/", 7))) ++ continue; ++ ++ /* Earlier properties may have moved the sensor's node. */ ++ int child = fdt_subnode_offset(dt, node, name); ++ if (child < 0) ++ break; ++ ++ char prop[64]; ++ snprintf(prop, sizeof(prop), "apple,%s", aop_sensor_calibrations[i]); ++ if (fdt_setprop(dt, child, prop, blob, len)) ++ bail("FDT: failed to set %s/%s\n", name, prop); ++ } ++ } ++ return 0; ++} ++ + /* + * The AOP's firmware carries the chip's ECID in its boot arguments. Pass it + * only to the AOP's node; the driver removes it from the tree once read. +@@ -1028,7 +1095,7 @@ static int dt_set_aop(void) + + if (fdt_setprop_u64(dt, node, "apple,ecid", ecid)) + bail("FDT: failed to set apple,ecid\n"); +- return 0; ++ return dt_set_aop_sensors(node); + } + + static int dt_set_multitouch(void) diff --git a/chips/s8003.toml b/chips/s8003.toml index e152fbed..13220105 100644 --- a/chips/s8003.toml +++ b/chips/s8003.toml @@ -15,3 +15,6 @@ stages = ["iBSS", "iBEC"] page_size = "xnu-6153.141.1 osfmk/arm/proc_reg.h" platform = "J71t on hardware" stages = "remote_boot two-stage CPID list" + +# Local capability delta: aop. +# Integration evidence / qualification limits: linux-a9 docs/release/orchard-prs-20261108-final/aop.md. diff --git a/devices/j71t/device.toml b/devices/j71t/device.toml index 1a78c1d1..d4fa8ae2 100644 --- a/devices/j71t/device.toml +++ b/devices/j71t/device.toml @@ -34,8 +34,8 @@ screen_off = "untested" backlight = "untested" touch = "untested" buttons = "untested" -accelerometer = "missing" -gyroscope = "missing" +accelerometer = "untested" +gyroscope = "untested" light_sensor = "untested" touch_id = "missing" storage_read = "untested" @@ -54,3 +54,6 @@ usb_network = "untested" audio_output = "untested" audio_input = "untested" camera = "missing" + +# Local capability delta: aop. +# Integration evidence / qualification limits: linux-a9 docs/release/orchard-prs-20261108-final/aop.md. diff --git a/devices/j71t/firmware.json b/devices/j71t/firmware.json index 3321e246..c03fb880 100644 --- a/devices/j71t/firmware.json +++ b/devices/j71t/firmware.json @@ -113,6 +113,12 @@ }, "device": "/usr/share/firmware/multitouch/J71t.mtprops", "sha256": "69f27fe207cc5810a748dac3dfe640b1c5a755709f49513b4b5125660784f0fc" + }, + "aop.im4p": { + "ipsw": { + "zip": "Firmware/AOP/aopfw-ipad6faop.im4p" + }, + "sha256": "5a4d8378bb83e175fc589cc1cf978430639d7ffcef07b2d94e9d6a4801124efa" } }, "outputs": [ @@ -279,6 +285,34 @@ "calibration_size": 1024 } ] + }, + { + "destination": "lib/firmware/apple/aop-ipad6f.bin", + "sha256": "af5d51a368ceb89dc5c359e7c84e54cb6082e308956be742c17622eb4a6047c8", + "steps": [ + { + "step": "im4p-payload@1", + "input": "aop.im4p" + }, + { + "step": "aop-image@1", + "coprocessor": "AOP" + } + ] + }, + { + "destination": "lib/firmware/apple/aop-ipad6f-ext.bin", + "sha256": "2528d2686ed1262de9910c2d60e45b68137e50aa601a0f8d54ea7d1347b9de22", + "steps": [ + { + "step": "im4p-payload@1", + "input": "aop.im4p" + }, + { + "step": "aop-image@1", + "coprocessor": "AOP_ext" + } + ] } ] } diff --git a/docs/compatibility.md b/docs/compatibility.md index 52850565..1f8d2a8c 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -161,7 +161,7 @@ Boards Orchard builds for. States: working, untested, missing, none (no such har | `n66` | untested | missing | untested | missing | untested | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | untested | untested | untested | untested | missing | missing | missing | | `n69u` | untested | missing | untested | missing | untested | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | untested | untested | untested | untested | missing | missing | missing | | `n71` | untested | missing | untested | missing | untested | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | untested | untested | untested | untested | missing | missing | missing | -| `j71t` | untested | untested | untested | untested | untested | missing | missing | untested | missing | untested | untested | untested | untested | untested | none | untested | untested | untested | untested | untested | untested | untested | untested | untested | missing | +| `j71t` | untested | untested | untested | untested | untested | untested | untested | untested | missing | untested | untested | untested | untested | untested | none | untested | untested | untested | untested | untested | untested | untested | untested | untested | missing | | `j72t` | untested | untested | untested | untested | untested | missing | missing | untested | missing | untested | untested | untested | untested | untested | missing | untested | untested | untested | untested | untested | untested | untested | untested | untested | missing | | `n66m` | untested | missing | untested | missing | untested | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | untested | untested | untested | untested | missing | missing | missing | | `n69` | untested | missing | untested | missing | untested | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | untested | untested | untested | untested | missing | missing | missing | @@ -248,6 +248,7 @@ A yes means an enabled device tree node binds a driver in `7.3.0-rc1-orchard-app | Board | Feature | | | --- | --- | --- | +| `j71t` | Accelerometer | untested, but no driver binds | | `d10` | Bluetooth input devices | missing, but a driver binds | | `d10` | Bluetooth audio | missing, but a driver binds | | `d101` | Bluetooth input devices | missing, but a driver binds | diff --git a/kernel/configs/apple-16k/config b/kernel/configs/apple-16k/config index aae562f8..ddbbb543 100644 --- a/kernel/configs/apple-16k/config +++ b/kernel/configs/apple-16k/config @@ -5620,6 +5620,7 @@ CONFIG_APPLE_AOP_ACCEL=m # CONFIG_ADIS16260 is not set # CONFIG_ADXRS290 is not set # CONFIG_ADXRS450 is not set +CONFIG_APPLE_AOP_GYRO=m # CONFIG_BMG160 is not set # CONFIG_FXAS21002C is not set # CONFIG_MPU3050_I2C is not set @@ -5855,6 +5856,7 @@ CONFIG_TSL2583=m # CONFIG_HP206C is not set # CONFIG_ZPA2326 is not set # CONFIG_ADP810 is not set +CONFIG_APPLE_AOP_PRESSURE=m # end of Pressure sensors # diff --git a/kernel/patches/devices/s8000/0008-aop-implementation.patch b/kernel/patches/devices/s8000/0008-aop-implementation.patch new file mode 100644 index 00000000..a2182740 --- /dev/null +++ b/kernel/patches/devices/s8000/0008-aop-implementation.patch @@ -0,0 +1,121 @@ +From: brkosebay +Date: Sun, 08 Nov 2026 16:00:00 +0100 +Subject: [PATCH] arm64: dts: apple: AOP sensors and certified warm reload + +Expose S8000 acceleration, gyroscope and pressure through standard IIO, with mount matrices and original board calibration supplied by m1n1. Fold the cold PMGR provider-publication correction and final retained-grant RTKit/AFK shutdown ordering into the warm-reload implementation. Only a certified previous quiesce may reload; unknown and uncertified warm states refuse before register writes. This avoids replacing proven warm support with a blanket sensor blacklist. + +Integration fixups are folded into the final delta. Device observations apply +to the integration build; the isolated topic requires qualification. + +Assisted-by: LLM +--- + +diff --git a/arch/arm64/boot/dts/apple/s800-0-3-common.dtsi b/arch/arm64/boot/dts/apple/s800-0-3-common.dtsi +index 354533f26..338182cf5 100644 +--- a/arch/arm64/boot/dts/apple/s800-0-3-common.dtsi ++++ b/arch/arm64/boot/dts/apple/s800-0-3-common.dtsi +@@ -11,6 +11,7 @@ + + / { + aliases { ++ aop = &aop; /* Used by loader */ + ausbphy0 = &ausbphy0; + nvme-hmb = &nvme_hmb; /* Used by loader */ + serial0 = &serial0; +diff --git a/arch/arm64/boot/dts/apple/s800-0-3.dtsi b/arch/arm64/boot/dts/apple/s800-0-3.dtsi +index bec208568..2a0a83383 100644 +--- a/arch/arm64/boot/dts/apple/s800-0-3.dtsi ++++ b/arch/arm64/boot/dts/apple/s800-0-3.dtsi +@@ -446,6 +446,43 @@ wdt: watchdog@2102b0000 { + interrupt-parent = <&aic>; + interrupts = ; + }; ++ ++ aop: aop@210800000 { ++ compatible = "apple,s8000-aop"; ++ reg = <0x2 0x10800000 0x0 0x4000>, ++ <0x2 0x10000600 0x0 0x4>, ++ <0x2 0x10e00000 0x0 0x9b000>; ++ reg-names = "akf", "control", "sram"; ++ mboxes = <&aop_mbox>; ++ mbox-names = "mbox"; ++ status = "disabled"; ++ ++ aop_accel: accel { ++ compatible = "apple,s8000-aop-accel"; ++ }; ++ ++ aop_gyro: gyro { ++ compatible = "apple,s8000-aop-gyro"; ++ }; ++ ++ aop_pressure: pressure { ++ compatible = "apple,s8000-aop-pressure"; ++ }; ++ }; ++ ++ aop_mbox: mbox@210804000 { ++ compatible = "apple,s8000-a7iop-mailbox", "apple,t8010-a7iop-mailbox"; ++ reg = <0x2 0x10804000 0x0 0x1000>; ++ interrupt-parent = <&aic>; ++ interrupts = , ++ , ++ , ++ ; ++ interrupt-names = "send-empty", "send-not-empty", ++ "recv-empty", "recv-not-empty"; ++ #mbox-cells = <0>; ++ status = "disabled"; ++ }; + }; + + timer { +diff --git a/arch/arm64/boot/dts/apple/s8003-j71t.dts b/arch/arm64/boot/dts/apple/s8003-j71t.dts +index 7e4d67d7f..347ec084c 100644 +--- a/arch/arm64/boot/dts/apple/s8003-j71t.dts ++++ b/arch/arm64/boot/dts/apple/s8003-j71t.dts +@@ -16,6 +16,44 @@ / { + model = "Apple iPad 5 (Wi-Fi) (TSMC)"; + }; + ++/ { ++ reserved-memory { ++ #address-cells = <2>; ++ #size-cells = <2>; ++ ranges; ++ /* Same tail page as release6, never a firmware DMA grant. */ ++ aop_shutdown: aop-shutdown@8583fc000 { ++ reg = <0x8 0x583fc000 0 0x4000>; ++ no-map; ++ }; ++ }; ++}; ++ ++&aop { ++ memory-region = <&aop_shutdown>; ++ apple,observe-domains = <&ps_aop_gpio &ps_aop &ps_aop_busif ++ &ps_aop_cpu &ps_aop_filter>; ++ apple,spi0-domain = <&ps_spi0>; ++ firmware-name = "apple/aop-ipad6f.bin", "apple/aop-ipad6f-ext.bin"; ++ status = "okay"; ++}; ++ ++/* ++ * The AOP reports the accelerometer in its sensor frame, which is the ++ * device frame (x right, y up, z out of the screen) negated: flat, screen up ++ * reads z = -1 g, upright y = -1 g. iio-sensor-proxy takes upright as ++ * negative y and needs z flipped. ++ */ ++&aop_accel { ++ mount-matrix = "1", "0", "0", ++ "0", "1", "0", ++ "0", "0", "-1"; ++}; ++ ++&aop_mbox { ++ status = "okay"; ++}; ++ + &touchscreen { + firmware-name = "apple/j71t-fwstart.z2fw"; + }; diff --git a/kernel/patches/drivers/iio/0005-aop-implementation.patch b/kernel/patches/drivers/iio/0005-aop-implementation.patch new file mode 100644 index 00000000..6d6ff334 --- /dev/null +++ b/kernel/patches/drivers/iio/0005-aop-implementation.patch @@ -0,0 +1,760 @@ +From: brkosebay +Date: Sun, 08 Nov 2026 16:00:00 +0100 +Subject: [PATCH] iio: apple-aop: AOP sensors and certified warm reload + +Expose S8000 acceleration, gyroscope and pressure through standard IIO, with mount matrices and original board calibration supplied by m1n1. Fold the cold PMGR provider-publication correction and final retained-grant RTKit/AFK shutdown ordering into the warm-reload implementation. Only a certified previous quiesce may reload; unknown and uncertified warm states refuse before register writes. This avoids replacing proven warm support with a blanket sensor blacklist. + +Integration fixups are folded into the final delta. Device observations apply +to the integration build; the isolated topic requires qualification. + +Assisted-by: LLM +--- + +diff --git a/drivers/iio/accel/Kconfig b/drivers/iio/accel/Kconfig +index b01598194..ed328a590 100644 +--- a/drivers/iio/accel/Kconfig ++++ b/drivers/iio/accel/Kconfig +@@ -213,7 +213,7 @@ config APPLE_AOP_ACCEL + select IIO_KFIFO_BUF + help + Say yes here to get support for the accelerometer that the Always-On +- Processor (AOP) of Apple A10 iPads and iPhones serves. ++ Processor (AOP) of Apple A9 and A10 iPads and iPhones serves. + + To compile this driver as a module, choose M here: the module will be + called apple_aop_accel. +diff --git a/drivers/iio/accel/apple_aop_accel.c b/drivers/iio/accel/apple_aop_accel.c +index 96c901361..e1b0e136d 100644 +--- a/drivers/iio/accel/apple_aop_accel.c ++++ b/drivers/iio/accel/apple_aop_accel.c +@@ -9,9 +9,13 @@ + * + * 0 u32 sequence + * 4 u16 reserved +- * 6 s32 x, y, z in 1/65536 g, in the device frame (x right, y up, +- * z out of the screen; +1 g opposite to gravity) ++ * 6 s32 x, y, z in 1/65536 g + * 18 u32 timestamp ++ * ++ * The T8010's firmware reports in the device frame (x right, y up, z out of ++ * the screen; +1 g opposite to gravity). The S8000's firmware first needs the ++ * unit's factory calibration and reports in the sensor's frame; the board's ++ * mount-matrix maps it. + */ + + #include +@@ -21,6 +25,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -41,8 +46,16 @@ + /* 9.80665 m/s^2 / 65536 */ + #define AOP_ACCEL_SCALE_NANO 149637 + ++struct apple_aop_accel_hw { ++ const struct apple_aop_calibration *calibration; ++ unsigned int num_calibration; ++ /* No separate raw sample stream (raw interval 0) */ ++ bool no_raw_stream; ++}; ++ + struct apple_aop_accel { + struct device *dev; ++ const struct apple_aop_accel_hw *hw; + const struct apple_aop_service *service; + struct iio_mount_matrix orientation; + +@@ -75,7 +88,9 @@ static int apple_aop_accel_set_interval(struct apple_aop_accel *accel, u32 inter + int i, ret; + + for (i = 0; i < ARRAY_SIZE(tags); i++) { +- ret = apple_aop_accel_set_property(accel, tags[i], interval_us); ++ ret = apple_aop_accel_set_property(accel, tags[i], ++ tags[i] == AOP_PROP_RAW_INTERVAL && ++ accel->hw->no_raw_stream ? 0 : interval_us); + if (ret) + return ret; + } +@@ -230,6 +245,7 @@ static int apple_aop_accel_probe(struct platform_device *pdev) + return -ENOMEM; + accel = iio_priv(indio_dev); + accel->dev = dev; ++ accel->hw = device_get_match_data(dev); + accel->service = service; + mutex_init(&accel->lock); + spin_lock_init(&accel->sample_lock); +@@ -252,6 +268,15 @@ static int apple_aop_accel_probe(struct platform_device *pdev) + if (ret) + return ret; + ++ if (accel->hw->num_calibration) { ++ ret = apple_aop_calibrate(service, dev, accel->hw->calibration, ++ accel->hw->num_calibration); ++ if (ret) { ++ apple_aop_unlisten(service); ++ return dev_err_probe(dev, ret, "failed to calibrate\n"); ++ } ++ } ++ + ret = apple_aop_accel_set_interval(accel, AOP_ACCEL_DEFAULT_US); + if (ret) { + apple_aop_unlisten(service); +@@ -265,8 +290,25 @@ static int apple_aop_accel_probe(struct platform_device *pdev) + return devm_iio_device_register(dev, indio_dev); + } + ++/* Offset at low temperature, sensitivity (either), interrupt calibration */ ++static const struct apple_aop_calibration apple_aop_accel_s8000_calibration[] = { ++ { "apple,low-temp-accel-offset", 0x1a, 1 }, ++ { "apple,accel-sensitivity-calibration", 0x0b, 2 }, ++ { "apple,accel-range-sensitivity-cal", 0x27, 2 }, ++ { "apple,accel-interrupt-calibration", 0x19, 3 }, ++}; ++ ++static const struct apple_aop_accel_hw apple_aop_accel_s8000 = { ++ .calibration = apple_aop_accel_s8000_calibration, ++ .num_calibration = ARRAY_SIZE(apple_aop_accel_s8000_calibration), ++ .no_raw_stream = true, ++}; ++ ++static const struct apple_aop_accel_hw apple_aop_accel_t8010 = { }; ++ + static const struct of_device_id apple_aop_accel_of_match[] = { +- { .compatible = "apple,t8010-aop-accel" }, ++ { .compatible = "apple,s8000-aop-accel", .data = &apple_aop_accel_s8000 }, ++ { .compatible = "apple,t8010-aop-accel", .data = &apple_aop_accel_t8010 }, + { } + }; + MODULE_DEVICE_TABLE(of, apple_aop_accel_of_match); +diff --git a/drivers/iio/gyro/Kconfig b/drivers/iio/gyro/Kconfig +index 3e193ee0f..8f506a65d 100644 +--- a/drivers/iio/gyro/Kconfig ++++ b/drivers/iio/gyro/Kconfig +@@ -63,6 +63,18 @@ config ADXRS450 + This driver can also be built as a module. If so, the module + will be called adxrs450. + ++config APPLE_AOP_GYRO ++ tristate "Apple AOP gyroscope" ++ depends on APPLE_AOP_T8010 ++ select IIO_BUFFER ++ select IIO_KFIFO_BUF ++ help ++ Say yes here to get support for the gyroscope that the Always-On ++ Processor (AOP) of Apple A9 iPads and iPhones serves. ++ ++ To compile this driver as a module, choose M here: the module will be ++ called apple_aop_gyro. ++ + config BMG160 + tristate "BOSCH BMG160 Gyro Sensor" + depends on (I2C || SPI_MASTER) +diff --git a/drivers/iio/gyro/Makefile b/drivers/iio/gyro/Makefile +index 0319b397d..212bbb814 100644 +--- a/drivers/iio/gyro/Makefile ++++ b/drivers/iio/gyro/Makefile +@@ -10,6 +10,7 @@ obj-$(CONFIG_ADIS16136) += adis16136.o + obj-$(CONFIG_ADIS16260) += adis16260.o + obj-$(CONFIG_ADXRS290) += adxrs290.o + obj-$(CONFIG_ADXRS450) += adxrs450.o ++obj-$(CONFIG_APPLE_AOP_GYRO) += apple_aop_gyro.o + obj-$(CONFIG_BMG160) += bmg160_core.o + obj-$(CONFIG_BMG160_I2C) += bmg160_i2c.o + obj-$(CONFIG_BMG160_SPI) += bmg160_spi.o +diff --git a/drivers/iio/gyro/apple_aop_gyro.c b/drivers/iio/gyro/apple_aop_gyro.c +new file mode 100644 +index 000000000..edbda1f43 +--- /dev/null ++++ b/drivers/iio/gyro/apple_aop_gyro.c +@@ -0,0 +1,291 @@ ++// SPDX-License-Identifier: GPL-2.0-only OR MIT ++/* ++ * Gyroscope served by the Apple Always-On Processor (AOP) ++ * ++ * The AOP firmware samples the gyroscope itself. Once it has the unit's ++ * factory calibration and its batch, raw and report intervals (EPIC ++ * set-property tags 0x16, 0x5c and 0), it sends one motion report (EPIC ++ * subtype 0xc1) per report interval, laid out like the accelerometer's: ++ * ++ * 0 u16 sequence ++ * 2 u32 status ++ * 6 s32 x, y, z in 1/65536 degree per second ++ * 18 s32 temperature (unit unknown, not exported) ++ */ ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#define AOP_PROP_REPORT_INTERVAL 0x00 ++#define AOP_PROP_BATCH_INTERVAL 0x16 ++#define AOP_PROP_RAW_INTERVAL 0x5c ++ ++#define AOP_REPORT_GYRO 0xc1 ++#define AOP_GYRO_LEN 22 ++#define AOP_GYRO_XYZ 6 ++ ++#define AOP_GYRO_DEFAULT_US 100000 /* 10 Hz */ ++#define AOP_GYRO_MIN_US 10000 /* 100 Hz */ ++#define AOP_GYRO_MAX_US 1000000 /* 1 Hz */ ++ ++/* pi / 180 rad/s / 65536 = 1171270 / 2^42 rad/s */ ++#define AOP_GYRO_SCALE 1171270 ++#define AOP_GYRO_SCALE_LOG2 42 ++ ++struct apple_aop_gyro { ++ struct device *dev; ++ const struct apple_aop_service *service; ++ struct iio_mount_matrix orientation; ++ ++ struct mutex lock; /* interval */ ++ u32 interval_us; ++ ++ spinlock_t sample_lock; /* sample, have_sample */ ++ s32 sample[3]; ++ bool have_sample; ++ ++ IIO_DECLARE_BUFFER_WITH_TS(s32, scan, 3); ++}; ++ ++static int apple_aop_gyro_set_u32(struct apple_aop_gyro *gyro, u32 key, u32 value) ++{ ++ __le32 le = cpu_to_le32(value); ++ ++ return apple_aop_set_property(gyro->service, key, &le, sizeof(le)); ++} ++ ++/* No separate raw sample stream: raw interval 0. */ ++static int apple_aop_gyro_set_interval(struct apple_aop_gyro *gyro, u32 interval_us) ++{ ++ int ret; ++ ++ ret = apple_aop_gyro_set_u32(gyro, AOP_PROP_BATCH_INTERVAL, interval_us); ++ if (!ret) ++ ret = apple_aop_gyro_set_u32(gyro, AOP_PROP_RAW_INTERVAL, 0); ++ if (!ret) ++ ret = apple_aop_gyro_set_u32(gyro, AOP_PROP_REPORT_INTERVAL, interval_us); ++ if (!ret) ++ gyro->interval_us = interval_us; ++ return ret; ++} ++ ++static void apple_aop_gyro_report(void *cookie, u16 subtype, const void *data, size_t len) ++{ ++ struct iio_dev *indio_dev = cookie; ++ struct apple_aop_gyro *gyro = iio_priv(indio_dev); ++ unsigned long flags; ++ int i; ++ ++ if (subtype != AOP_REPORT_GYRO || len < AOP_GYRO_LEN) ++ return; ++ ++ spin_lock_irqsave(&gyro->sample_lock, flags); ++ for (i = 0; i < 3; i++) ++ gyro->sample[i] = get_unaligned_le32(data + AOP_GYRO_XYZ + 4 * i); ++ gyro->have_sample = true; ++ spin_unlock_irqrestore(&gyro->sample_lock, flags); ++ ++ if (iio_buffer_enabled(indio_dev)) { ++ memcpy(gyro->scan, gyro->sample, sizeof(gyro->sample)); ++ iio_push_to_buffers_with_ts(indio_dev, gyro->scan, sizeof(gyro->scan), ++ iio_get_time_ns(indio_dev)); ++ } ++} ++ ++static int apple_aop_gyro_read_raw(struct iio_dev *indio_dev, ++ struct iio_chan_spec const *chan, ++ int *val, int *val2, long mask) ++{ ++ struct apple_aop_gyro *gyro = iio_priv(indio_dev); ++ unsigned long flags; ++ int ret; ++ ++ switch (mask) { ++ case IIO_CHAN_INFO_RAW: ++ spin_lock_irqsave(&gyro->sample_lock, flags); ++ *val = gyro->sample[chan->scan_index]; ++ ret = gyro->have_sample ? IIO_VAL_INT : -EAGAIN; ++ spin_unlock_irqrestore(&gyro->sample_lock, flags); ++ return ret; ++ case IIO_CHAN_INFO_SCALE: ++ *val = AOP_GYRO_SCALE; ++ *val2 = AOP_GYRO_SCALE_LOG2; ++ return IIO_VAL_FRACTIONAL_LOG2; ++ case IIO_CHAN_INFO_SAMP_FREQ: ++ mutex_lock(&gyro->lock); ++ *val = USEC_PER_SEC / gyro->interval_us; ++ *val2 = (u64)(USEC_PER_SEC % gyro->interval_us) * USEC_PER_SEC / ++ gyro->interval_us; ++ mutex_unlock(&gyro->lock); ++ return IIO_VAL_INT_PLUS_MICRO; ++ default: ++ return -EINVAL; ++ } ++} ++ ++static int apple_aop_gyro_write_raw(struct iio_dev *indio_dev, ++ struct iio_chan_spec const *chan, ++ int val, int val2, long mask) ++{ ++ struct apple_aop_gyro *gyro = iio_priv(indio_dev); ++ u64 micro_hz; ++ u32 interval; ++ int ret; ++ ++ if (mask != IIO_CHAN_INFO_SAMP_FREQ) ++ return -EINVAL; ++ if (val < 0 || val2 < 0) ++ return -EINVAL; ++ ++ micro_hz = (u64)val * USEC_PER_SEC + val2; ++ if (!micro_hz) ++ return -EINVAL; ++ interval = clamp_t(u64, div64_u64((u64)USEC_PER_SEC * USEC_PER_SEC, micro_hz), ++ AOP_GYRO_MIN_US, AOP_GYRO_MAX_US); ++ ++ mutex_lock(&gyro->lock); ++ ret = apple_aop_gyro_set_interval(gyro, interval); ++ mutex_unlock(&gyro->lock); ++ return ret; ++} ++ ++static const struct iio_mount_matrix * ++apple_aop_gyro_get_mount_matrix(const struct iio_dev *indio_dev, ++ const struct iio_chan_spec *chan) ++{ ++ struct apple_aop_gyro *gyro = iio_priv(indio_dev); ++ ++ return &gyro->orientation; ++} ++ ++static const struct iio_chan_spec_ext_info apple_aop_gyro_ext_info[] = { ++ IIO_MOUNT_MATRIX(IIO_SHARED_BY_TYPE, apple_aop_gyro_get_mount_matrix), ++ { } ++}; ++ ++#define APPLE_AOP_GYRO_CHANNEL(axis, index) { \ ++ .type = IIO_ANGL_VEL, \ ++ .modified = 1, \ ++ .channel2 = IIO_MOD_##axis, \ ++ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW), \ ++ .info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE), \ ++ .info_mask_shared_by_all = BIT(IIO_CHAN_INFO_SAMP_FREQ), \ ++ .scan_index = index, \ ++ .scan_type = { \ ++ .sign = 's', \ ++ .realbits = 32, \ ++ .storagebits = 32, \ ++ .endianness = IIO_CPU, \ ++ }, \ ++ .ext_info = apple_aop_gyro_ext_info, \ ++} ++ ++static const struct iio_chan_spec apple_aop_gyro_channels[] = { ++ APPLE_AOP_GYRO_CHANNEL(X, 0), ++ APPLE_AOP_GYRO_CHANNEL(Y, 1), ++ APPLE_AOP_GYRO_CHANNEL(Z, 2), ++ IIO_CHAN_SOFT_TIMESTAMP(3), ++}; ++ ++static const struct iio_info apple_aop_gyro_info = { ++ .read_raw = apple_aop_gyro_read_raw, ++ .write_raw = apple_aop_gyro_write_raw, ++}; ++ ++/* Sensitivity and interrupt calibration make it complete */ ++static const struct apple_aop_calibration apple_aop_gyro_calibration[] = { ++ { "apple,gyro-sensitivity-calibration", 0x0b, 1 }, ++ { "apple,gyro-interrupt-calibration", 0x19, 2 }, ++ { "apple,gyro-range-sensitivity-cal", 0x27, 0 }, ++ { "apple,gyro-trim-calibration", 0x20, 0 }, ++}; ++ ++static void apple_aop_gyro_stop(void *data) ++{ ++ struct apple_aop_gyro *gyro = data; ++ ++ apple_aop_gyro_set_u32(gyro, AOP_PROP_REPORT_INTERVAL, 0); ++ apple_aop_unlisten(gyro->service); ++} ++ ++static int apple_aop_gyro_probe(struct platform_device *pdev) ++{ ++ struct device *dev = &pdev->dev; ++ const struct apple_aop_service *service = dev_get_platdata(dev); ++ struct apple_aop_gyro *gyro; ++ struct iio_dev *indio_dev; ++ int ret; ++ ++ if (!service) ++ return -ENODEV; ++ ++ indio_dev = devm_iio_device_alloc(dev, sizeof(*gyro)); ++ if (!indio_dev) ++ return -ENOMEM; ++ gyro = iio_priv(indio_dev); ++ gyro->dev = dev; ++ gyro->service = service; ++ mutex_init(&gyro->lock); ++ spin_lock_init(&gyro->sample_lock); ++ ++ ret = iio_read_mount_matrix(dev, &gyro->orientation); ++ if (ret) ++ return ret; ++ ++ indio_dev->name = "apple-aop-gyro"; ++ indio_dev->info = &apple_aop_gyro_info; ++ indio_dev->channels = apple_aop_gyro_channels; ++ indio_dev->num_channels = ARRAY_SIZE(apple_aop_gyro_channels); ++ indio_dev->modes = INDIO_DIRECT_MODE; ++ ++ ret = devm_iio_kfifo_buffer_setup(dev, indio_dev, NULL); ++ if (ret) ++ return ret; ++ ++ ret = apple_aop_listen(service, apple_aop_gyro_report, indio_dev); ++ if (ret) ++ return ret; ++ ++ ret = apple_aop_calibrate(service, dev, apple_aop_gyro_calibration, ++ ARRAY_SIZE(apple_aop_gyro_calibration)); ++ if (!ret) ++ ret = apple_aop_gyro_set_interval(gyro, AOP_GYRO_DEFAULT_US); ++ if (ret) { ++ apple_aop_unlisten(service); ++ return dev_err_probe(dev, ret, "failed to start reports\n"); ++ } ++ ++ ret = devm_add_action_or_reset(dev, apple_aop_gyro_stop, gyro); ++ if (ret) ++ return ret; ++ ++ return devm_iio_device_register(dev, indio_dev); ++} ++ ++static const struct of_device_id apple_aop_gyro_of_match[] = { ++ { .compatible = "apple,s8000-aop-gyro" }, ++ { } ++}; ++MODULE_DEVICE_TABLE(of, apple_aop_gyro_of_match); ++ ++static struct platform_driver apple_aop_gyro_driver = { ++ .driver = { ++ .name = "apple-aop-gyro", ++ .of_match_table = apple_aop_gyro_of_match, ++ }, ++ .probe = apple_aop_gyro_probe, ++}; ++module_platform_driver(apple_aop_gyro_driver); ++ ++MODULE_AUTHOR("Berkay Kösebay "); ++MODULE_DESCRIPTION("Apple AOP gyroscope driver"); ++MODULE_LICENSE("Dual MIT/GPL"); +diff --git a/drivers/iio/pressure/Kconfig b/drivers/iio/pressure/Kconfig +index 838a8340c..18535ad99 100644 +--- a/drivers/iio/pressure/Kconfig ++++ b/drivers/iio/pressure/Kconfig +@@ -384,4 +384,16 @@ config ADP810 + To compile this driver as a module, choose M here: the module will + be called adp810 + ++config APPLE_AOP_PRESSURE ++ tristate "Apple AOP barometric pressure sensor" ++ depends on APPLE_AOP_T8010 ++ select IIO_BUFFER ++ select IIO_KFIFO_BUF ++ help ++ Say yes here to get support for the barometric pressure sensor that ++ the Always-On Processor (AOP) of Apple A9 iPads and iPhones serves. ++ ++ To compile this driver as a module, choose M here: the module will be ++ called apple_aop_pressure. ++ + endmenu +diff --git a/drivers/iio/pressure/Makefile b/drivers/iio/pressure/Makefile +index bc0d11a20..bf3e27ba9 100644 +--- a/drivers/iio/pressure/Makefile ++++ b/drivers/iio/pressure/Makefile +@@ -9,6 +9,7 @@ obj-$(CONFIG_ABP2030PA) += abp2030pa.o + obj-$(CONFIG_ABP2030PA_I2C) += abp2030pa_i2c.o + obj-$(CONFIG_ABP2030PA_SPI) += abp2030pa_spi.o + obj-$(CONFIG_ADP810) += adp810.o ++obj-$(CONFIG_APPLE_AOP_PRESSURE) += apple_aop_pressure.o + obj-$(CONFIG_ROHM_BM1390) += rohm-bm1390.o + obj-$(CONFIG_BMP280) += bmp280.o + bmp280-objs := bmp280-core.o bmp280-regmap.o +diff --git a/drivers/iio/pressure/apple_aop_pressure.c b/drivers/iio/pressure/apple_aop_pressure.c +new file mode 100644 +index 000000000..8cd983c57 +--- /dev/null ++++ b/drivers/iio/pressure/apple_aop_pressure.c +@@ -0,0 +1,257 @@ ++// SPDX-License-Identifier: GPL-2.0-only OR MIT ++/* ++ * Barometric pressure sensor served by the Apple Always-On Processor (AOP) ++ * ++ * The AOP firmware samples the pressure sensor itself. Once it has the unit's ++ * factory calibration and its batch, raw and report intervals (EPIC ++ * set-property tags 0x16, 0x5c and 0), it sends one report (EPIC subtype ++ * 0xc4) per report interval: ++ * ++ * 0 u16 sequence ++ * 2 u32 pressure in 1/65536 kPa ++ * 6 s32 temperature (unit unknown, not exported) ++ */ ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#define AOP_PROP_REPORT_INTERVAL 0x00 ++#define AOP_PROP_BATCH_INTERVAL 0x16 ++#define AOP_PROP_RAW_INTERVAL 0x5c ++ ++#define AOP_REPORT_PRESSURE 0xc4 ++#define AOP_PRESSURE_LEN 10 ++#define AOP_PRESSURE_VALUE 2 ++ ++#define AOP_PRESSURE_DEFAULT_US 100000 /* 10 Hz */ ++#define AOP_PRESSURE_MIN_US 10000 /* 100 Hz */ ++#define AOP_PRESSURE_MAX_US 1000000 /* 1 Hz */ ++ ++struct apple_aop_pressure { ++ struct device *dev; ++ const struct apple_aop_service *service; ++ ++ struct mutex lock; /* interval */ ++ u32 interval_us; ++ ++ spinlock_t sample_lock; /* sample, have_sample */ ++ u32 sample; ++ bool have_sample; ++ ++ IIO_DECLARE_BUFFER_WITH_TS(u32, scan, 1); ++}; ++ ++static int apple_aop_pressure_set_u32(struct apple_aop_pressure *pressure, u32 key, u32 value) ++{ ++ __le32 le = cpu_to_le32(value); ++ ++ return apple_aop_set_property(pressure->service, key, &le, sizeof(le)); ++} ++ ++/* No separate raw sample stream: raw interval 0. */ ++static int apple_aop_pressure_set_interval(struct apple_aop_pressure *pressure, u32 interval_us) ++{ ++ int ret; ++ ++ ret = apple_aop_pressure_set_u32(pressure, AOP_PROP_BATCH_INTERVAL, interval_us); ++ if (!ret) ++ ret = apple_aop_pressure_set_u32(pressure, AOP_PROP_RAW_INTERVAL, 0); ++ if (!ret) ++ ret = apple_aop_pressure_set_u32(pressure, AOP_PROP_REPORT_INTERVAL, interval_us); ++ if (!ret) ++ pressure->interval_us = interval_us; ++ return ret; ++} ++ ++static void apple_aop_pressure_report(void *cookie, u16 subtype, const void *data, size_t len) ++{ ++ struct iio_dev *indio_dev = cookie; ++ struct apple_aop_pressure *pressure = iio_priv(indio_dev); ++ unsigned long flags; ++ ++ if (subtype != AOP_REPORT_PRESSURE || len < AOP_PRESSURE_LEN) ++ return; ++ ++ spin_lock_irqsave(&pressure->sample_lock, flags); ++ pressure->sample = get_unaligned_le32(data + AOP_PRESSURE_VALUE); ++ pressure->have_sample = true; ++ spin_unlock_irqrestore(&pressure->sample_lock, flags); ++ ++ if (iio_buffer_enabled(indio_dev)) { ++ pressure->scan[0] = pressure->sample; ++ iio_push_to_buffers_with_ts(indio_dev, pressure->scan, sizeof(pressure->scan), ++ iio_get_time_ns(indio_dev)); ++ } ++} ++ ++static int apple_aop_pressure_read_raw(struct iio_dev *indio_dev, ++ struct iio_chan_spec const *chan, ++ int *val, int *val2, long mask) ++{ ++ struct apple_aop_pressure *pressure = iio_priv(indio_dev); ++ unsigned long flags; ++ int ret; ++ ++ switch (mask) { ++ case IIO_CHAN_INFO_RAW: ++ spin_lock_irqsave(&pressure->sample_lock, flags); ++ *val = pressure->sample; ++ ret = pressure->have_sample ? IIO_VAL_INT : -EAGAIN; ++ spin_unlock_irqrestore(&pressure->sample_lock, flags); ++ return ret; ++ case IIO_CHAN_INFO_SCALE: ++ /* kPa */ ++ *val = 1; ++ *val2 = 16; ++ return IIO_VAL_FRACTIONAL_LOG2; ++ case IIO_CHAN_INFO_SAMP_FREQ: ++ mutex_lock(&pressure->lock); ++ *val = USEC_PER_SEC / pressure->interval_us; ++ *val2 = (u64)(USEC_PER_SEC % pressure->interval_us) * USEC_PER_SEC / ++ pressure->interval_us; ++ mutex_unlock(&pressure->lock); ++ return IIO_VAL_INT_PLUS_MICRO; ++ default: ++ return -EINVAL; ++ } ++} ++ ++static int apple_aop_pressure_write_raw(struct iio_dev *indio_dev, ++ struct iio_chan_spec const *chan, ++ int val, int val2, long mask) ++{ ++ struct apple_aop_pressure *pressure = iio_priv(indio_dev); ++ u64 micro_hz; ++ u32 interval; ++ int ret; ++ ++ if (mask != IIO_CHAN_INFO_SAMP_FREQ) ++ return -EINVAL; ++ if (val < 0 || val2 < 0) ++ return -EINVAL; ++ ++ micro_hz = (u64)val * USEC_PER_SEC + val2; ++ if (!micro_hz) ++ return -EINVAL; ++ interval = clamp_t(u64, div64_u64((u64)USEC_PER_SEC * USEC_PER_SEC, micro_hz), ++ AOP_PRESSURE_MIN_US, AOP_PRESSURE_MAX_US); ++ ++ mutex_lock(&pressure->lock); ++ ret = apple_aop_pressure_set_interval(pressure, interval); ++ mutex_unlock(&pressure->lock); ++ return ret; ++} ++ ++static const struct iio_chan_spec apple_aop_pressure_channels[] = { ++ { ++ .type = IIO_PRESSURE, ++ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | BIT(IIO_CHAN_INFO_SCALE), ++ .info_mask_shared_by_all = BIT(IIO_CHAN_INFO_SAMP_FREQ), ++ .scan_index = 0, ++ .scan_type = { ++ .sign = 'u', ++ .realbits = 32, ++ .storagebits = 32, ++ .endianness = IIO_CPU, ++ }, ++ }, ++ IIO_CHAN_SOFT_TIMESTAMP(1), ++}; ++ ++static const struct iio_info apple_aop_pressure_info = { ++ .read_raw = apple_aop_pressure_read_raw, ++ .write_raw = apple_aop_pressure_write_raw, ++}; ++ ++/* The offset or the acceleration sensitivity makes it complete */ ++static const struct apple_aop_calibration apple_aop_pressure_calibration[] = { ++ { "apple,pressure-offset-calibration", 0x0b, 1 }, ++ { "apple,pressure-accel-sensitivity", 0xd3, 1 }, ++ { "apple,pressure-heater-resistance", 0xd4, 0 }, ++}; ++ ++static void apple_aop_pressure_stop(void *data) ++{ ++ struct apple_aop_pressure *pressure = data; ++ ++ apple_aop_pressure_set_u32(pressure, AOP_PROP_REPORT_INTERVAL, 0); ++ apple_aop_unlisten(pressure->service); ++} ++ ++static int apple_aop_pressure_probe(struct platform_device *pdev) ++{ ++ struct device *dev = &pdev->dev; ++ const struct apple_aop_service *service = dev_get_platdata(dev); ++ struct apple_aop_pressure *pressure; ++ struct iio_dev *indio_dev; ++ int ret; ++ ++ if (!service) ++ return -ENODEV; ++ ++ indio_dev = devm_iio_device_alloc(dev, sizeof(*pressure)); ++ if (!indio_dev) ++ return -ENOMEM; ++ pressure = iio_priv(indio_dev); ++ pressure->dev = dev; ++ pressure->service = service; ++ mutex_init(&pressure->lock); ++ spin_lock_init(&pressure->sample_lock); ++ ++ indio_dev->name = "apple-aop-pressure"; ++ indio_dev->info = &apple_aop_pressure_info; ++ indio_dev->channels = apple_aop_pressure_channels; ++ indio_dev->num_channels = ARRAY_SIZE(apple_aop_pressure_channels); ++ indio_dev->modes = INDIO_DIRECT_MODE; ++ ++ ret = devm_iio_kfifo_buffer_setup(dev, indio_dev, NULL); ++ if (ret) ++ return ret; ++ ++ ret = apple_aop_listen(service, apple_aop_pressure_report, indio_dev); ++ if (ret) ++ return ret; ++ ++ ret = apple_aop_calibrate(service, dev, apple_aop_pressure_calibration, ++ ARRAY_SIZE(apple_aop_pressure_calibration)); ++ if (!ret) ++ ret = apple_aop_pressure_set_interval(pressure, AOP_PRESSURE_DEFAULT_US); ++ if (ret) { ++ apple_aop_unlisten(service); ++ return dev_err_probe(dev, ret, "failed to start reports\n"); ++ } ++ ++ ret = devm_add_action_or_reset(dev, apple_aop_pressure_stop, pressure); ++ if (ret) ++ return ret; ++ ++ return devm_iio_device_register(dev, indio_dev); ++} ++ ++static const struct of_device_id apple_aop_pressure_of_match[] = { ++ { .compatible = "apple,s8000-aop-pressure" }, ++ { } ++}; ++MODULE_DEVICE_TABLE(of, apple_aop_pressure_of_match); ++ ++static struct platform_driver apple_aop_pressure_driver = { ++ .driver = { ++ .name = "apple-aop-pressure", ++ .of_match_table = apple_aop_pressure_of_match, ++ }, ++ .probe = apple_aop_pressure_probe, ++}; ++module_platform_driver(apple_aop_pressure_driver); ++ ++MODULE_AUTHOR("Berkay Kösebay "); ++MODULE_DESCRIPTION("Apple AOP barometric pressure sensor driver"); ++MODULE_LICENSE("Dual MIT/GPL"); diff --git a/kernel/patches/drivers/pmdomain/0001-aop-implementation.patch b/kernel/patches/drivers/pmdomain/0001-aop-implementation.patch new file mode 100644 index 00000000..2fd722aa --- /dev/null +++ b/kernel/patches/drivers/pmdomain/0001-aop-implementation.patch @@ -0,0 +1,24 @@ +From: brkosebay +Date: Sun, 08 Nov 2026 16:00:00 +0100 +Subject: [PATCH] pmdomain: apple: AOP sensors and certified warm reload + +Expose S8000 acceleration, gyroscope and pressure through standard IIO, with mount matrices and original board calibration supplied by m1n1. Fold the cold PMGR provider-publication correction and final retained-grant RTKit/AFK shutdown ordering into the warm-reload implementation. Only a certified previous quiesce may reload; unknown and uncertified warm states refuse before register writes. This avoids replacing proven warm support with a blanket sensor blacklist. + +Integration fixups are folded into the final delta. Device observations apply +to the integration build; the isolated topic requires qualification. + +Assisted-by: LLM +--- + +diff --git a/drivers/pmdomain/apple/pmgr-pwrstate.c b/drivers/pmdomain/apple/pmgr-pwrstate.c +index 65dc32d30..a0559873c 100644 +--- a/drivers/pmdomain/apple/pmgr-pwrstate.c ++++ b/drivers/pmdomain/apple/pmgr-pwrstate.c +@@ -417,6 +417,7 @@ static int apple_pmgr_ps_probe(struct platform_device *pdev) + goto err_remove; + } + ++ platform_set_drvdata(pdev, ps); + return 0; + err_remove: + if (ps->has_genpd) { diff --git a/kernel/patches/drivers/soc-apple/0014-aop-bindings.patch b/kernel/patches/drivers/soc-apple/0014-aop-bindings.patch new file mode 100644 index 00000000..aaf9d2d5 --- /dev/null +++ b/kernel/patches/drivers/soc-apple/0014-aop-bindings.patch @@ -0,0 +1,127 @@ +From: brkosebay +Date: Sun, 08 Nov 2026 16:00:00 +0100 +Subject: [PATCH] dt-bindings: soc-apple: AOP sensors and certified warm reload + +Expose S8000 acceleration, gyroscope and pressure through standard IIO, with mount matrices and original board calibration supplied by m1n1. Fold the cold PMGR provider-publication correction and final retained-grant RTKit/AFK shutdown ordering into the warm-reload implementation. Only a certified previous quiesce may reload; unknown and uncertified warm states refuse before register writes. This avoids replacing proven warm support with a blanket sensor blacklist. + +Integration fixups are folded into the final delta. Device observations apply +to the integration build; the isolated topic requires qualification. + +Assisted-by: LLM +--- + +diff --git a/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml b/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml +index ba035ff80..ea414a8f4 100644 +--- a/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml ++++ b/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml +@@ -59,6 +59,12 @@ properties: + items: + - const: apple,t8010-a7iop-mailbox + ++ - description: ++ The same A7 IOP mailbox on the A9. ++ items: ++ - const: apple,s8000-a7iop-mailbox ++ - const: apple,t8010-a7iop-mailbox ++ + reg: + maxItems: 1 + +diff --git a/Documentation/devicetree/bindings/soc/apple/apple,t8010-aop.yaml b/Documentation/devicetree/bindings/soc/apple/apple,t8010-aop.yaml +index 14726afa0..5d2485ac9 100644 +--- a/Documentation/devicetree/bindings/soc/apple/apple,t8010-aop.yaml ++++ b/Documentation/devicetree/bindings/soc/apple/apple,t8010-aop.yaml +@@ -4,14 +4,14 @@ + $id: http://devicetree.org/schemas/soc/apple/apple,t8010-aop.yaml# + $schema: http://devicetree.org/meta-schemas/core.yaml# + +-title: Apple T8010 Always-On Processor ++title: Apple S8000 and T8010 Always-On Processor + + maintainers: + - Dalton Alexandre + + description: +- The Always-On Processor (AOP) of the A10 is an ARMv7 coprocessor that +- runs RTKit firmware and serves the motion sensors (accelerometer, ++ The Always-On Processor (AOP) of the A9 and A10 is an ARMv7 coprocessor ++ that runs RTKit firmware and serves the motion sensors (accelerometer, + gyroscope, compass, pressure) and the always-on microphones. It runs from + local SRAM, with a further firmware segment in system memory, and talks + to the application processor through an A7 IOP mailbox. Its firmware is +@@ -19,7 +19,9 @@ description: + + properties: + compatible: +- const: apple,t8010-aop ++ enum: ++ - apple,s8000-aop ++ - apple,t8010-aop + + reg: + items: +@@ -50,6 +52,38 @@ properties: + The chip's unique ID (ECID), filled in by the bootloader. The + firmware's boot arguments carry it. + ++ apple,aop-target: ++ $ref: /schemas/types.yaml#/definitions/uint32 ++ description: ++ The AOP target number from the Apple device tree, filled in by the ++ bootloader. The S8000 firmware's boot arguments carry it. ++ ++ apple,chip-id: ++ $ref: /schemas/types.yaml#/definitions/uint32 ++ description: ++ The SoC's chip ID (e.g. 0x8003), filled in by the bootloader. The S8000 ++ firmware's boot arguments carry it. ++ ++ apple,chip-revision: ++ $ref: /schemas/types.yaml#/definitions/uint32 ++ description: ++ The SoC's chip revision, filled in by the bootloader. The S8000 ++ firmware's boot arguments carry it. ++ ++ memory-region: ++ maxItems: 1 ++ description: Reserved 16 KiB shutdown transcript page for J71t warm reload. ++ ++ apple,observe-domains: ++ $ref: /schemas/types.yaml#/definitions/phandle-array ++ minItems: 5 ++ maxItems: 5 ++ description: AOP GPIO, AOP, bus interface, CPU and filter in lock order. ++ ++ apple,spi0-domain: ++ $ref: /schemas/types.yaml#/definitions/phandle ++ description: Read-only SPI0 power-state provider observation. ++ + patternProperties: + "^(accel|gyro|compass|pressure)$": + type: object +@@ -58,6 +92,9 @@ patternProperties: + properties: + compatible: + enum: ++ - apple,s8000-aop-accel ++ - apple,s8000-aop-gyro ++ - apple,s8000-aop-pressure + - apple,t8010-aop-accel + - apple,t8010-aop-gyro + - apple,t8010-aop-compass +@@ -65,6 +102,15 @@ patternProperties: + + mount-matrix: + description: The sensor's mounting rotation in the device. ++ ++ patternProperties: ++ "^apple,(low-temp-accel-offset|(accel|gyro)-(sensitivity|interrupt)-calibration|(accel|gyro)-range-sensitivity-cal|gyro-trim-calibration|pressure-(offset-calibration|accel-sensitivity|heater-resistance))$": ++ $ref: /schemas/types.yaml#/definitions/uint8-array ++ maxItems: 64 ++ description: ++ The unit's factory calibration of the sensor, copied by the ++ bootloader from the property of the same name (without the ++ prefix) in the Apple device tree. The S8000 firmware takes it. + required: + - compatible + additionalProperties: false diff --git a/kernel/patches/drivers/soc-apple/0015-aop-implementation.patch b/kernel/patches/drivers/soc-apple/0015-aop-implementation.patch new file mode 100644 index 00000000..31a196d9 --- /dev/null +++ b/kernel/patches/drivers/soc-apple/0015-aop-implementation.patch @@ -0,0 +1,3285 @@ +From: brkosebay +Date: Sun, 08 Nov 2026 16:00:00 +0100 +Subject: [PATCH] soc: apple: AOP sensors and certified warm reload + +Expose S8000 acceleration, gyroscope and pressure through standard IIO, with mount matrices and original board calibration supplied by m1n1. Fold the cold PMGR provider-publication correction and final retained-grant RTKit/AFK shutdown ordering into the warm-reload implementation. Only a certified previous quiesce may reload; unknown and uncertified warm states refuse before register writes. This avoids replacing proven warm support with a blanket sensor blacklist. + +Integration fixups are folded into the final delta. Device observations apply +to the integration build; the isolated topic requires qualification. + +Assisted-by: LLM +--- + +diff --git a/MAINTAINERS b/MAINTAINERS +index a4a69643c..12c292825 100644 +--- a/MAINTAINERS ++++ b/MAINTAINERS +@@ -2068,6 +2068,8 @@ APPLE T8010 ALWAYS-ON PROCESSOR + M: Dalton Alexandre + S: Maintained + F: Documentation/devicetree/bindings/soc/apple/apple,t8010-aop.yaml ++F: drivers/iio/gyro/apple_aop_gyro.c ++F: drivers/iio/pressure/apple_aop_pressure.c + F: drivers/soc/apple/aop.c + F: include/linux/soc/apple/aop.h + +diff --git a/drivers/pmdomain/apple/pmgr-pwrstate.c b/drivers/pmdomain/apple/pmgr-pwrstate.c +index 17c017425..a4f6fbce4 100644 +--- a/drivers/pmdomain/apple/pmgr-pwrstate.c ++++ b/drivers/pmdomain/apple/pmgr-pwrstate.c +@@ -10,6 +10,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -17,6 +18,7 @@ + #include + #include + #include ++#include + + #define APPLE_PMGR_RESET BIT(31) + #define APPLE_PMGR_AUTO_ENABLE BIT(28) +@@ -479,3 +481,84 @@ MODULE_AUTHOR("Hector Martin "); + MODULE_DESCRIPTION("PMGR power state driver for Apple SoCs"); + + module_platform_driver(apple_pmgr_ps_driver); ++ ++/* ++ * Read-only observer for bring-up. The caller's nodes follow the same ++ * child -> parent order as genpd power transitions. No domain is attached, ++ * resumed, pinned on, reset, or written by this function. ++ */ ++int apple_pmgr_observe(struct device_node **nodes, unsigned int count, ++ struct apple_pmgr_observation *states, ++ void (*read)(void *), void *data) ++{ ++ struct platform_device *providers[5]; ++ struct apple_pmgr_ps *ps[5]; ++ unsigned long flags; ++ unsigned int i, j, found = 0; ++ int ret = 0; ++ bool accessible = true; ++ ++ if (!count || count > ARRAY_SIZE(providers) || !read) ++ return -EINVAL; ++ for (i = 0; i < count; i++) { ++ for (j = 0; j < i; j++) ++ if (nodes[i] == nodes[j]) { ++ ret = -EINVAL; ++ goto put; ++ } ++ providers[i] = of_find_device_by_node(nodes[i]); ++ if (!providers[i]) { ++ ret = -EPROBE_DEFER; ++ goto put; ++ } ++ device_lock(&providers[i]->dev); ++ found++; ++ if (!providers[i]->dev.driver || ++ strcmp(providers[i]->dev.driver->name, "apple-pmgr-pwrstate")) { ++ ret = -EPROBE_DEFER; ++ goto put; ++ } ++ ps[i] = platform_get_drvdata(providers[i]); ++ if (!ps[i]) { ++ ret = -EPROBE_DEFER; ++ goto put; ++ } ++ } ++ ++ /* PMGR domains use GENPD_FLAG_IRQ_SAFE, i.e. genpd.slock. */ ++ local_irq_save(flags); ++ for (i = 0; i < count; i++) ++ spin_lock_nested(&ps[i]->genpd.slock, i); ++ for (i = 0; i < count; i++) ++ spin_lock_nested(&ps[i]->lock, i); ++ for (i = 0; i < count; i++) { ++ states[i].genpd_status = ps[i]->genpd.status; ++ ret = regmap_read(ps[i]->regmap, ps[i]->offset, &states[i].raw); ++ if (ret) ++ goto unlock; ++ if (states[i].genpd_status != GENPD_STATE_ON || ++ FIELD_GET(APPLE_PMGR_PS_ACTUAL, states[i].raw) != APPLE_PMGR_PS_ACTIVE || ++ FIELD_GET(APPLE_PMGR_PS_TARGET, states[i].raw) != APPLE_PMGR_PS_ACTIVE || ++ states[i].raw & (APPLE_PMGR_RESET | APPLE_PMGR_DEV_DISABLE | ++ APPLE_PMGR_PARENT_OFF)) ++ accessible = false; ++ } ++ if (accessible) ++ read(data); ++ else ++ ret = -EHOSTDOWN; ++unlock: ++ for (i = count; i > 0; i--) ++ spin_unlock(&ps[i - 1]->lock); ++ for (i = count; i > 0; i--) ++ spin_unlock(&ps[i - 1]->genpd.slock); ++ local_irq_restore(flags); ++put: ++ while (found) { ++ found--; ++ device_unlock(&providers[found]->dev); ++ put_device(&providers[found]->dev); ++ } ++ return ret; ++} ++EXPORT_SYMBOL_GPL(apple_pmgr_observe); +diff --git a/drivers/soc/apple/Kconfig b/drivers/soc/apple/Kconfig +index e68a49f1b..c0af67b62 100644 +--- a/drivers/soc/apple/Kconfig ++++ b/drivers/soc/apple/Kconfig +@@ -39,16 +39,16 @@ config APPLE_RTKIT + Say 'y' here if you have an Apple SoC. + + config APPLE_AOP_T8010 +- tristate "Apple T8010 Always-On Processor" ++ tristate "Apple S8000 and T8010 Always-On Processor" + depends on APPLE_RTKIT + depends on ARCH_APPLE || COMPILE_TEST + help +- The Always-On Processor (AOP) of the Apple A10 runs the firmware for +- the motion sensors and the always-on microphones. This driver loads +- and starts that firmware and provides its services to the sensor +- drivers. ++ The Always-On Processor (AOP) of the Apple A9 and A10 runs the ++ firmware for the motion sensors and the always-on microphones. This ++ driver loads and starts that firmware and provides its services to ++ the sensor drivers. + +- Say 'y' or 'm' here if you have an A10-based iPad or iPhone. ++ Say 'y' or 'm' here if you have an A9- or A10-based iPad or iPhone. + + config APPLE_PMP_T8015_V1 + tristate "Apple T8015 PMP" +diff --git a/drivers/soc/apple/aop-warm-record.h b/drivers/soc/apple/aop-warm-record.h +new file mode 100644 +index 000000000..a735809d5 +--- /dev/null ++++ b/drivers/soc/apple/aop-warm-record.h +@@ -0,0 +1,64 @@ ++/* SPDX-License-Identifier: GPL-2.0-only */ ++#include ++#include ++#define A9_HO_PAGE 0x4000U ++#define A9_HO_LOG_OFFSET 0x800U ++#define A9_SL_MAGIC 0x4c533941U /* "A9SL" */ ++#define A9_SL_VERSION 1U ++ ++enum a9_ho_step { ++ A9_HO_STEP_BEGIN, ++ A9_HO_STEP_RATES, ++ A9_HO_STEP_AFK_SENT, ++ A9_HO_STEP_AFK_ACKED, ++ A9_HO_STEP_DISABLED, ++ A9_HO_STEP_AP_SENT, ++ A9_HO_STEP_AP_ACKED, ++ A9_HO_STEP_IOP_SENT, ++ A9_HO_STEP_IOP_ACKED, ++ A9_HO_STEP_RAM_STABLE, ++ A9_HO_STEP_MAILBOX_IDLE, ++ A9_HO_STEP_HALTED, ++ A9_HO_STEP_DRAINED, ++ A9_HO_STEPS, ++}; ++ ++static const char * const a9_ho_step_names[A9_HO_STEPS] = { ++ "begin", "rates-zero", "afk-sent", "afk-acked", "endpoint-disable", ++ "ap-sent", "ap-ack", "iop-sent", "iop-ack", "ram-stable", "mailbox-idle", ++ "halt", "drain", ++}; ++ ++struct a9_sl_header { ++ u32 magic, version, capacity, length; ++ u32 lines, truncated, checksum, mode; ++ u64 begin_ns; ++ u8 writer[16]; ++ char entry[64]; ++}; ++ ++_Static_assert(sizeof(struct a9_sl_header) == 120, "shutdown log ABI"); ++ ++#define A9_SL_CAPACITY (A9_HO_PAGE - A9_HO_LOG_OFFSET - sizeof(struct a9_sl_header)) ++ ++/* FNV-1a; incremental over appended text. */ ++static inline u32 a9_sl_hash(u32 hash, const char *text, size_t length) ++{ ++ size_t i; ++ ++ for (i = 0; i < length; i++) ++ hash = (hash ^ (u8)text[i]) * 16777619U; ++ return hash; ++} ++ ++#define A9_SL_HASH_SEED 2166136261U ++ ++/* A replayable previous transcript: complete header, bounded, intact. */ ++static inline bool a9_sl_valid(const struct a9_sl_header *h, const char *text) ++{ ++ return h->magic == A9_SL_MAGIC && h->version == A9_SL_VERSION && ++ h->capacity == A9_SL_CAPACITY && h->length <= h->capacity && ++ !h->entry[sizeof(h->entry) - 1] && ++ a9_sl_hash(A9_SL_HASH_SEED, text, h->length) == h->checksum; ++} ++ +diff --git a/drivers/soc/apple/aop-warm-spi0.c.h b/drivers/soc/apple/aop-warm-spi0.c.h +new file mode 100644 +index 000000000..bf883ffe8 +--- /dev/null ++++ b/drivers/soc/apple/aop-warm-spi0.c.h +@@ -0,0 +1,1340 @@ ++/* SPDX-License-Identifier: GPL-2.0-only */ ++/* Ported from a9/aop-rc1: wake16, release6-d254a9f790. */ ++static void a9_aop_spi0_snapshot(void __iomem *base, u32 *values) ++{ ++ unsigned int i; ++ ++ for (i = 0; i < A9_SPI0_REGS; i++) ++ values[i] = readl(base + a9_spi0_offsets[i]); ++} ++ ++/* ++ * AOPSPIMaster_reset (0102f2f4) under the AOP-domain PMGR guard with IRQs ++ * off and the AOP CPU halted. Its provider power transition 3 is an ++ * AOP-local call with no AP register equivalent and is not replayed. A poll ++ * timeout still restores the saved control word. ++ */ ++static void a9_aop_spi0_reset_locked(void *data) ++{ ++ struct a9_aop_spi0 *s = data; ++ void __iomem *control = s->base + A9_SPI0_CONTROL; ++ u32 value = 0; ++ int ret; ++ ++ a9_aop_spi0_snapshot(s->base, s->before); ++ s->saved = s->before[A9_SPI0_I_CONTROL]; ++ s->asserted = a9_spi0_base_word(s->saved) | A9_SPI0_RESET; ++ writel(s->asserted, control); ++ ret = readl_poll_timeout_atomic(control, value, value & A9_SPI0_RESET, ++ 1, A9_SPI0_POLL_US); ++ if (!ret) ++ ret = readl_poll_timeout_atomic(control, value, ++ !(value & A9_SPI0_ENABLE), 1, ++ A9_SPI0_POLL_US); ++ s->polled = value; ++ if (!ret) { ++ udelay(A9_SPI0_DELAY_US); ++ writel(a9_spi0_base_word(s->saved), control); ++ udelay(A9_SPI0_DELAY_US); ++ } ++ writel(s->saved, control); ++ s->reset_done_ns = ktime_get_ns(); ++ a9_aop_spi0_snapshot(s->base, s->after); ++ if (!ret && (s->after[A9_SPI0_I_FAULT] & A9_SPI0_CH3_PENDING)) ++ ret = -EIO; ++ s->error = ret; ++} ++ ++/* ++ * Wake8 device evidence: SCM was 0 before/after the first controller reset, ++ * then became 3 at the channel-3 stop store. There is no firmware-derived ++ * SCM status write. Replay AOPSPIMaster_reset after the teardown so the ++ * controller reset, rather than an invented SCM write, is the one variable. ++ */ ++static void a9_aop_spi0_post_reset_locked(void *data) ++{ ++ struct a9_aop_spi0 *s = data; ++ struct a9_aop_spi0_post_reset *r = &s->post_reset; ++ void __iomem *control = s->base + A9_SPI0_CONTROL; ++ u32 value = 0; ++ int ret; ++ ++ a9_aop_spi0_snapshot(s->base, r->before); ++ r->saved = r->before[A9_SPI0_I_CONTROL]; ++ r->asserted = a9_spi0_base_word(r->saved) | A9_SPI0_RESET; ++ writel(r->asserted, control); ++ ret = readl_poll_timeout_atomic(control, value, value & A9_SPI0_RESET, ++ 1, A9_SPI0_POLL_US); ++ if (!ret) ++ ret = readl_poll_timeout_atomic(control, value, ++ !(value & A9_SPI0_ENABLE), 1, ++ A9_SPI0_POLL_US); ++ r->polled = value; ++ if (!ret) { ++ udelay(A9_SPI0_DELAY_US); ++ writel(a9_spi0_base_word(r->saved), control); ++ udelay(A9_SPI0_DELAY_US); ++ } ++ writel(r->saved, control); ++ a9_aop_spi0_snapshot(s->base, r->after); ++ if (!ret && (r->after[A9_SPI0_I_FAULT] & A9_SPI0_CH3_PENDING)) ++ ret = -EIO; ++ r->error = ret; ++} ++ ++static void a9_aop_spi0_ch3_snapshot(void __iomem *base, u32 *values) ++{ ++ unsigned int i; ++ ++ for (i = 0; i < A9_SPI0_CH3_REGS; i++) ++ values[i] = readl(base + a9_spi0_ch3_offsets[i]); ++} ++ ++static void a9_aop_spi0_trace_read(void *data) ++{ ++ struct a9_aop_spi0 *s = data; ++ unsigned int channel, i; ++ ++ s->trace.control = readl(s->base + A9_SPI0_CONTROL); ++ s->trace.clock = readl(s->base + A9_SPI0_CLOCK); ++ for (i = 0; i < A9_SPI0_SCM_REGS; i++) ++ s->trace.scm[i] = readl(s->base + a9_spi0_scm_offsets[i]); ++ a9_aop_spi0_ch3_snapshot(s->base, s->trace.ch3); ++ for (channel = 0; channel < A9_SPI0_CHANNELS; channel++) ++ for (i = 0; i < A9_SPI0_CHANNEL_REGS; i++) ++ s->trace.channel[channel][i] = ++ readl(s->base + A9_SPI0_CHANNEL_BASE + ++ A9_SPI0_CHANNEL_STRIDE * channel + ++ a9_spi0_channel_offsets[i]); ++} ++ ++/* Warm path only, CPU halted. Log outside the PMGR guard; no SCM writes. */ ++static int a9_aop_spi0_trace(struct apple_aop *aop, const char *stage) ++{ ++ struct apple_pmgr_observation states[AOP_DOMAINS] = {}; ++ struct a9_aop_spi0 *s = &aop->spi0; ++ const u32 *c = s->trace.ch3, *m = s->trace.scm; ++ const u32 *v; ++ unsigned int channel; ++ int ret; ++ ++ ret = apple_pmgr_observe(aop->domains, AOP_DOMAINS, states, ++ a9_aop_spi0_trace_read, s); ++ if (ret) { ++ s->intc.failed = "failed-spi0-trace-guard"; ++ dev_info(aop->dev, "SPI0 SCM stage=%s errno=%d: guard refused\n", stage, ret); ++ return ret; ++ } ++ dev_info(aop->dev, "SPI0 SCM stage=%s errno=0 ctl=%08x clk=%08x scm_lo=%08x scm_hi=%08x c3_interval=%08x c3_cursor=%08x c3=%08x route=%08x rate=%08x c3c=%08x desc=%08x 8d0=%08x 8d4=%08x cpu=halted\n", ++ stage, s->trace.control, s->trace.clock, m[0], m[1], m[2], m[3], ++ c[0], c[1], c[2], c[3], c[4], c[5], c[6]); ++ for (channel = 0; channel < A9_SPI0_CHANNELS; channel++) { ++ v = s->trace.channel[channel]; ++ dev_info(aop->dev, "SPI0 SCM channel stage=%s ch=%02u ctl=%08x route=%08x rate=%08x cfg=%08x interval=%08x cursor=%08x desc=%08x pending0=%u pending1=%u\n", ++ stage, channel, v[0], v[1], v[2], v[3], v[4], v[5], v[6], ++ !!(c[A9_CH3_I_8D0] & BIT(2 * channel)), ++ !!(c[A9_CH3_I_8D4] & BIT(2 * channel))); ++ } ++ return 0; ++} ++ ++static const char *a9_aop_spi0_selected_verify(struct a9_aop_spi0 *s); ++ ++/* ++ * Wake10 safety gate, strengthened in wake11 to retain every selected-channel ++ * teardown invariant. A fault observed by a copy sample remains terminal even ++ * if it is transient and the final read happens to clear. ++ */ ++static int a9_aop_spi0_gate(struct apple_aop *aop, const char *stage, ++ const char *check, bool include_copy) ++{ ++ struct a9_aop_spi0 *s = &aop->spi0; ++ const u32 *m, *c; ++ int ret; ++ ++ ret = a9_aop_spi0_trace(aop, stage); ++ if (ret) ++ return ret; ++ m = s->trace.scm; ++ c = s->trace.ch3; ++ if (m[0] || m[1]) ++ s->gate_failed = "failed-spi0-scm"; ++ else if (c[A9_CH3_I_8D0] || c[A9_CH3_I_8D4]) ++ s->gate_failed = "failed-spi0-fault"; ++ else if (include_copy && s->copy_faulted) ++ s->gate_failed = "failed-spi0-copy-sample"; ++ else { ++ s->gate_failed = a9_spi0_ch3_verify(c, s->teardown_stable & BIT(3)); ++ if (!s->gate_failed) ++ s->gate_failed = a9_aop_spi0_selected_verify(s); ++ } ++ ret = s->gate_failed ? -EIO : 0; ++ dev_info(aop->dev, "SPI0 %s check errno=%d scm_lo=%08x scm_hi=%08x 8d0=%08x 8d4=%08x copy_faulted=%u result=%s\n", ++ check, ret, m[0], m[1], c[A9_CH3_I_8D0], c[A9_CH3_I_8D4], ++ s->copy_faulted, s->gate_failed ?: "pass"); ++ return ret; ++} ++ ++#define A9_SPI0_COPY_CHUNK 256U ++#define A9_SPI0_COPY_SAMPLE_US 5000U ++ ++struct a9_aop_spi0_copy_probe { ++ void __iomem *base; ++ u32 scm_lo, scm_hi, fault, fault_8d4; ++}; ++ ++static void a9_aop_spi0_copy_probe_read(void *data) ++{ ++ struct a9_aop_spi0_copy_probe *p = data; ++ ++ p->scm_lo = readl(p->base + A9_SPI0_SCM_LO); ++ p->scm_hi = readl(p->base + A9_SPI0_SCM_HI); ++ p->fault = readl(p->base + A9_SPI0_FAULT); ++ p->fault_8d4 = readl(p->base + A9_SPI0_FAULT_8D4); ++} ++ ++/* ++ * Read-only samples spaced by at least 5 ms across both halves of the SRAM ++ * copy. Once a nonzero state is seen it remains a terminal fact, but the copy ++ * completes so later samples and the full after-copy channel snapshot expose ++ * whether the source is level, transient or moves between channels. ++ */ ++static int a9_aop_spi0_copy_sample(struct apple_aop *aop, const char *phase, ++ size_t offset, size_t size, u64 start_ns, ++ u64 *last_us, unsigned int *sample, ++ bool force) ++{ ++ struct apple_pmgr_observation states[AOP_DOMAINS] = {}; ++ struct a9_aop_spi0 *s = &aop->spi0; ++ struct a9_aop_spi0_copy_probe p = { .base = s->base }; ++ u64 elapsed_us = div_u64(ktime_get_boottime_ns() - start_ns, ++ NSEC_PER_USEC); ++ bool first_fault, initial = !*sample; ++ int ret; ++ ++ if (!force && *sample && elapsed_us - *last_us < A9_SPI0_COPY_SAMPLE_US) ++ return 0; ++ ret = apple_pmgr_observe(aop->domains, AOP_DOMAINS, states, ++ a9_aop_spi0_copy_probe_read, &p); ++ first_fault = !s->copy_faulted && ++ (p.scm_lo || p.scm_hi || p.fault || p.fault_8d4); ++ if (!ret && first_fault) ++ s->copy_faulted = true; ++ dev_info(aop->dev, "SPI0 COPY sample=%u phase=%s elapsed_us=%llu offset=0x%zx size=0x%zx scm_lo=%08x scm_hi=%08x 8d0=%08x 8d4=%08x result=%s\n", ++ *sample, phase, elapsed_us, offset, size, p.scm_lo, p.scm_hi, ++ p.fault, p.fault_8d4, ret ? "guard" : s->copy_faulted ? "fault" : "pass"); ++ *last_us = elapsed_us; ++ (*sample)++; ++ if (ret) ++ return ret; ++ if (first_fault) { ++ ret = a9_aop_spi0_trace(aop, "copy-fault"); ++ if (ret) ++ return ret; ++ } ++ /* The offset-zero sample is the last no-write gate before the copy. */ ++ return !initial || !s->copy_faulted ? 0 : -EIO; ++} ++ ++static int a9_aop_copy_verify_wake10(struct apple_aop *aop, const u8 *src, ++ size_t size) ++{ ++ u32 chunk[A9_SPI0_COPY_CHUNK / 4]; ++ u64 start_ns = ktime_get_boottime_ns(), last_us = 0; ++ unsigned int sample = 0; ++ size_t done, n; ++ int ret; ++ ++ ret = a9_aop_spi0_copy_sample(aop, "write", 0, size, start_ns, &last_us, &sample, true); ++ if (ret) ++ return ret; ++ for (done = 0; done < size; done += n) { ++ n = min_t(size_t, size - done, sizeof(chunk)); ++ __iowrite32_copy(aop->sram + done, src + done, n / 4); ++ ret = a9_aop_spi0_copy_sample(aop, "write", done + n, size, start_ns, ++ &last_us, &sample, done + n == size); ++ if (ret) ++ return ret; ++ } ++ ret = a9_aop_spi0_copy_sample(aop, "verify", 0, size, start_ns, &last_us, &sample, true); ++ if (ret) ++ return ret; ++ for (done = 0; done < size; done += n) { ++ n = min_t(size_t, size - done, sizeof(chunk)); ++ __ioread32_copy(chunk, aop->sram + done, n / 4); ++ if (memcmp(chunk, src + done, n)) ++ return -EIO; ++ ret = a9_aop_spi0_copy_sample(aop, "verify", done + n, size, start_ns, ++ &last_us, &sample, done + n == size); ++ if (ret) ++ return ret; ++ } ++ return 0; ++} ++ ++static void __iomem *a9_aop_spi0_channel_base(struct a9_aop_spi0 *s, ++ unsigned int channel) ++{ ++ return s->base + A9_SPI0_CHANNEL_BASE + A9_SPI0_CHANNEL_STRIDE * channel; ++} ++ ++/* ++ * Wake15: which other channels were RUN with a counting interval when this ++ * stop was issued, and which of them advanced across the stop. Read only; it ++ * separates a stop that had a ticking peer from the stop of the last live ++ * channel in the log. It never gates anything in-kernel. ++ */ ++static void a9_aop_spi0_peers_begin(struct a9_aop_spi0 *s, unsigned int channel, ++ u32 *interval, u32 *run) ++{ ++ unsigned int i; ++ u32 ctl; ++ ++ *run = 0; ++ for (i = 0; i < A9_SPI0_CHANNELS; i++) { ++ void __iomem *base = a9_aop_spi0_channel_base(s, i); ++ ++ interval[i] = 0; ++ if (i == channel) ++ continue; ++ ctl = readl(base + a9_spi0_channel_offsets[A9_CHANNEL_I_CTL]); ++ interval[i] = readl(base + a9_spi0_channel_offsets[A9_CHANNEL_I_INTERVAL]); ++ if ((ctl & A9_SPI0_CH3_RUN) && interval[i]) ++ *run |= BIT(i); ++ } ++} ++ ++static void a9_aop_spi0_stop_timing(struct a9_aop_spi0 *s, ++ struct a9_aop_spi0_ch3 *c, ++ const u32 *interval) ++{ ++ unsigned int i; ++ ++ c->peers_ticking = 0; ++ for (i = 0; i < A9_SPI0_CHANNELS; i++) { ++ void __iomem *base = a9_aop_spi0_channel_base(s, i); ++ ++ if ((c->peers_run & BIT(i)) && ++ readl(base + a9_spi0_channel_offsets[A9_CHANNEL_I_INTERVAL]) != ++ interval[i]) ++ c->peers_ticking |= BIT(i); ++ } ++ c->reset_to_stop_us = c->stop_ns > s->reset_done_ns ? ++ div_u64(c->stop_ns - s->reset_done_ns, NSEC_PER_USEC) : 0; ++} ++ ++/* ++ * Wake12/13 captured channel 7, and wake13 channel 3, stopped but never idle: ++ * for 20 complete slices the control/progress word, config, interval, cursor, ++ * SCM and both fault banks were unchanged. This predicate is intentionally ++ * narrower than "RUN clear and no progress": the channel must be one of the ++ * two device-observed periodic clients, must carry that client's route, rate ++ * and config before the stop, and every sample must be byte-identical with SCM ++ * 3/0 (every earlier non-idle stop, which followed a channel-3 stop that had ++ * latched it) or 0/0 (a first stop that latches nothing), no fault, no cursor ++ * movement and a retained nonzero interval. ++ * It only permits the callback masks, descriptor clear, config acknowledge and ++ * the established controller reset; every later SCM, fault and copy gate stays. ++ */ ++static const char *a9_aop_spi0_stable_why(struct a9_aop_spi0_ch3 *c, ++ const struct a9_aop_channel_diag_slice *diag, ++ unsigned int channel) ++{ ++ const struct a9_aop_ch3_slice *first; ++ unsigned int i; ++ u32 route; ++ ++ if (channel == A9_SPI0_PHOSPHORUS_CHANNEL) ++ route = A9_SPI0_PHOSPHORUS_ROUTE; ++ else if (channel == A9_SPI0_MPU_ASYNC_CHANNEL) ++ route = A9_SPI0_MPU_ASYNC_ROUTE; ++ else ++ return "channel"; ++ if (c->poll != -ETIMEDOUT || c->slices != A9_SPI0_CH3_SLICES) ++ return "no-timeout"; ++ if (!(c->before[A9_CH3_I_C3] & A9_SPI0_CH3_RUN) || ++ c->before[A9_CH3_I_ROUTE] != route || ++ c->before[A9_CH3_I_RATE] != A9_SPI0_PHOSPHORUS_RATE || ++ c->before[A9_CH3_I_C3C] != A9_SPI0_PHOSPHORUS_CFG || ++ c->before[A9_CH3_I_DESC] || (c->stop & A9_SPI0_CH3_RUN)) ++ return "before-identity"; ++ if (c->before[A9_CH3_I_8D4] || (c->before[A9_CH3_I_8D0] & ~BIT(2 * channel))) ++ return "before-fault"; ++ ++ first = &c->slice[0]; ++ if ((first->c3 & A9_SPI0_STABLE_CTL_MASK) != A9_SPI0_STABLE_CTL_STOPPED || ++ (first->c3 & (A9_SPI0_CH3_RUN | A9_SPI0_CH3_IDLE | A9_SPI0_CH3_IRQ1))) ++ return "stopped-control"; ++ if ((first->scm_lo != 0 && first->scm_lo != 3) || first->scm_hi) ++ return "scm"; ++ if (first->fault || diag[0].fault_8d4) ++ return "sample-fault"; ++ if (diag[0].cfg != A9_SPI0_PHOSPHORUS_CFG || diag[0].cursor || !diag[0].interval) ++ return "cfg-cursor-interval"; ++ ++ for (i = 1; i < A9_SPI0_CH3_SLICES; i++) { ++ const struct a9_aop_ch3_slice *p = &c->slice[i]; ++ ++ if (p->c3 != first->c3 || p->scm_lo != first->scm_lo || ++ p->scm_hi != first->scm_hi || p->fault != first->fault || ++ diag[i].cfg != diag[0].cfg || ++ diag[i].interval != diag[0].interval || ++ diag[i].cursor != diag[0].cursor || ++ diag[i].fault_8d4 != diag[0].fault_8d4) ++ return "progress"; ++ } ++ if (c->slice[A9_SPI0_CH3_SLICES - 1].ns < ++ (u64)A9_SPI0_CH3_SLICES * A9_SPI0_CH3_SLICE_US * NSEC_PER_USEC) ++ return "short"; ++ return NULL; ++} ++ ++/* ++ * AOPSPIMaster_teardownChannel (0102e4f8) for channel 3, then the fault ++ * handler's acknowledge (0102f20a), under the same guard as the reset with ++ * the CPU halted. Every store is a read-modify-write except the descriptor, ++ * which the firmware zeroes. The idle wait runs in 1 ms slices, up to twice ++ * the firmware's own 10 ms SCMto bound; each slice ends with read-only ++ * samples of C3, the SCM state and +0x8d0 (wake15: and config, interval, ++ * cursor and +0x8d4). An idle timeout stops before the masks unless the ++ * stable-stop predicate holds. The acknowledge bit self-clears; +0x8d0 bit 6 ++ * shows whether it took effect. ++ */ ++static void a9_aop_spi0_ch3_locked(void *data) ++{ ++ struct a9_aop_spi0 *s = data; ++ struct a9_aop_spi0_ch3 *c = &s->ch3; ++ void __iomem *c3 = s->base + A9_SPI0_CH3; ++ void __iomem *c3c = s->base + A9_SPI0_CH3_0C; ++ void __iomem *base = a9_aop_spi0_channel_base(s, 3); ++ struct a9_aop_ch3_slice *p; ++ struct a9_aop_channel_diag_slice *d; ++ u32 interval[A9_SPI0_CHANNELS]; ++ u32 value = 0; ++ u64 start; ++ ++ a9_aop_spi0_ch3_snapshot(s->base, c->before); ++ c->run_before = c->before[A9_CH3_I_C3] & A9_SPI0_CH3_RUN; ++ c->stop = readl(c3) & ~A9_SPI0_CH3_STOP; ++ c->stop_ns = ktime_get_ns(); ++ writel(c->stop, c3); ++ start = ktime_get_ns(); ++ a9_aop_spi0_peers_begin(s, 3, interval, &c->peers_run); ++ c->poll = -ETIMEDOUT; ++ for (c->slices = 0; c->poll && c->slices < A9_SPI0_CH3_SLICES; c->slices++) { ++ p = &c->slice[c->slices]; ++ c->poll = readl_poll_timeout_atomic(c3, value, value & A9_SPI0_CH3_IDLE, ++ 1, A9_SPI0_CH3_SLICE_US); ++ p->ns = ktime_get_ns() - start; ++ p->c3 = value; ++ p->scm_lo = readl(s->base + A9_SPI0_SCM_LO); ++ p->scm_hi = readl(s->base + A9_SPI0_SCM_HI); ++ p->fault = readl(s->base + A9_SPI0_FAULT); ++ d = &s->ch3_diag[c->slices]; ++ d->cfg = readl(c3c); ++ d->interval = readl(base + a9_spi0_channel_offsets[A9_CHANNEL_I_INTERVAL]); ++ d->cursor = readl(base + a9_spi0_channel_offsets[A9_CHANNEL_I_CURSOR]); ++ d->fault_8d4 = readl(s->base + A9_SPI0_FAULT_8D4); ++ } ++ c->idle = value; ++ a9_aop_spi0_stop_timing(s, c, interval); ++ c->stable_why = a9_aop_spi0_stable_why(c, s->ch3_diag, 3); ++ c->stable = !c->stable_why; ++ if (c->stable) ++ s->teardown_stable |= BIT(3); ++ if (!c->poll || c->stable) { ++ /* A normal idle or the captured stopped state: mask callbacks. */ ++ c->idle_us = div_u64(c->slice[c->slices - 1].ns, NSEC_PER_USEC); ++ c->class0 = readl(c3) & ~A9_SPI0_CH3_CLASS0; ++ writel(c->class0, c3); ++ c->class1 = readl(c3) & ~A9_SPI0_CH3_CLASS1; ++ writel(c->class1, c3); ++ writel(0, s->base + A9_SPI0_CH3_DESC); ++ c->ack = readl(c3c) | A9_SPI0_CH3_ACK; ++ writel(c->ack, c3c); ++ } ++ a9_aop_spi0_ch3_snapshot(s->base, c->after); ++ c->failed = c->poll && !c->stable ? "failed-ch3-teardown-idle" : ++ a9_spi0_ch3_verify(c->after, c->stable); ++ if (!c->failed) ++ c->error = 0; ++ else if (c->stable) ++ c->error = -EIO; ++ else ++ c->error = c->poll ?: -EIO; ++} ++ ++ ++static void a9_aop_spi0_channel_snapshot(struct a9_aop_spi0 *s, ++ unsigned int channel, u32 *values) ++{ ++ void __iomem *base = a9_aop_spi0_channel_base(s, channel); ++ ++ values[A9_CH3_I_C3] = readl(base + a9_spi0_channel_offsets[A9_CHANNEL_I_CTL]); ++ values[A9_CH3_I_ROUTE] = readl(base + a9_spi0_channel_offsets[A9_CHANNEL_I_ROUTE]); ++ values[A9_CH3_I_RATE] = readl(base + a9_spi0_channel_offsets[A9_CHANNEL_I_RATE]); ++ values[A9_CH3_I_C3C] = readl(base + a9_spi0_channel_offsets[A9_CHANNEL_I_CFG]); ++ values[A9_CH3_I_DESC] = readl(base + A9_SPI0_CHANNEL_DESC); ++ values[A9_CH3_I_8D0] = readl(s->base + A9_SPI0_FAULT); ++ values[A9_CH3_I_8D4] = readl(s->base + A9_SPI0_FAULT_8D4); ++} ++ ++static const char *a9_spi0_channel_state_verify(u32 control, u32 desc, ++ u32 fault, u32 fault_8d4, ++ unsigned int channel, ++ bool stable_stop) ++{ ++ u32 pending = BIT(2 * channel); ++ ++ if (control & A9_SPI0_CH3_RUN) ++ return "failed-channel-teardown-run"; ++ if (control & A9_SPI0_CH3_IRQ0) ++ return "failed-channel-teardown-class0"; ++ if (control & A9_SPI0_CH3_IRQ1) ++ return "failed-channel-teardown-class1"; ++ if (!(control & A9_SPI0_CH3_IDLE) && !stable_stop) ++ return "failed-channel-teardown-idle"; ++ if (desc) ++ return "failed-channel-teardown-desc"; ++ if ((fault | fault_8d4) & pending) ++ return "failed-channel-teardown-pending"; ++ return NULL; ++} ++ ++static const char *a9_spi0_channel_verify(const u32 *after, ++ unsigned int channel, ++ bool stable_stop) ++{ ++ return a9_spi0_channel_state_verify(after[A9_CH3_I_C3], ++ after[A9_CH3_I_DESC], ++ after[A9_CH3_I_8D0], ++ after[A9_CH3_I_8D4], channel, ++ stable_stop); ++} ++ ++/* ++ * Wake11 applies the channel-3 firmware sequence unchanged to another slot. ++ * Wake12 only appends read-only scheduler samples to that established poll. ++ */ ++static void a9_aop_spi0_channel_locked(void *data) ++{ ++ struct a9_aop_spi0_channel_op *op = data; ++ struct a9_aop_spi0 *s = op->spi0; ++ struct a9_aop_spi0_ch3 *c = op->state; ++ void __iomem *base = a9_aop_spi0_channel_base(s, op->channel); ++ void __iomem *control = base + a9_spi0_channel_offsets[A9_CHANNEL_I_CTL]; ++ void __iomem *config = base + a9_spi0_channel_offsets[A9_CHANNEL_I_CFG]; ++ struct a9_aop_ch3_slice *p; ++ struct a9_aop_channel_diag_slice *d; ++ u32 interval[A9_SPI0_CHANNELS]; ++ u32 value = 0; ++ u64 start; ++ ++ a9_aop_spi0_channel_snapshot(s, op->channel, c->before); ++ c->run_before = c->before[A9_CH3_I_C3] & A9_SPI0_CH3_RUN; ++ c->stop = readl(control) & ~A9_SPI0_CH3_STOP; ++ c->stop_ns = ktime_get_ns(); ++ writel(c->stop, control); ++ start = ktime_get_ns(); ++ a9_aop_spi0_peers_begin(s, op->channel, interval, &c->peers_run); ++ c->poll = -ETIMEDOUT; ++ for (c->slices = 0; c->poll && c->slices < A9_SPI0_CH3_SLICES; c->slices++) { ++ p = &c->slice[c->slices]; ++ c->poll = readl_poll_timeout_atomic(control, value, ++ value & A9_SPI0_CH3_IDLE, 1, ++ A9_SPI0_CH3_SLICE_US); ++ p->ns = ktime_get_ns() - start; ++ p->c3 = value; ++ p->scm_lo = readl(s->base + A9_SPI0_SCM_LO); ++ p->scm_hi = readl(s->base + A9_SPI0_SCM_HI); ++ p->fault = readl(s->base + A9_SPI0_FAULT); ++ d = &s->channel_diag[c->slices]; ++ d->cfg = readl(config); ++ d->interval = readl(base + a9_spi0_channel_offsets[A9_CHANNEL_I_INTERVAL]); ++ d->cursor = readl(base + a9_spi0_channel_offsets[A9_CHANNEL_I_CURSOR]); ++ d->fault_8d4 = readl(s->base + A9_SPI0_FAULT_8D4); ++ } ++ c->idle = value; ++ a9_aop_spi0_stop_timing(s, c, interval); ++ c->stable_why = a9_aop_spi0_stable_why(c, s->channel_diag, op->channel); ++ c->stable = !c->stable_why; ++ if (c->stable) ++ s->teardown_stable |= BIT(op->channel); ++ if (!c->poll || c->stable) { ++ /* A normal idle or the captured stopped state: mask callbacks. */ ++ c->idle_us = div_u64(c->slice[c->slices - 1].ns, NSEC_PER_USEC); ++ c->class0 = readl(control) & ~A9_SPI0_CH3_CLASS0; ++ writel(c->class0, control); ++ c->class1 = readl(control) & ~A9_SPI0_CH3_CLASS1; ++ writel(c->class1, control); ++ writel(0, base + A9_SPI0_CHANNEL_DESC); ++ c->ack = readl(config) | A9_SPI0_CH3_ACK; ++ writel(c->ack, config); ++ } ++ a9_aop_spi0_channel_snapshot(s, op->channel, c->after); ++ c->failed = c->poll && !c->stable ? "failed-channel-teardown-idle" : ++ a9_spi0_channel_verify(c->after, op->channel, ++ c->stable); ++ if (!c->failed) ++ c->error = 0; ++ else if (c->stable) ++ c->error = -EIO; ++ else ++ c->error = c->poll ?: -EIO; ++} ++ ++/* Called only if ps_spi0 is fully on; reads nothing. */ ++static void a9_aop_spi0_pmgr_on(void *data) ++{ ++ *(bool *)data = true; ++} ++ ++/* AP PMGR ps_spi0, read-only: never requested, resumed or written. ++ * 0 = on, -EHOSTDOWN = off (raw still filled), -ENODEV = no node. ++ */ ++static int a9_aop_spi0_pmgr(struct apple_aop *aop, u32 *raw) ++{ ++ struct apple_pmgr_observation state = {}; ++ bool on = false; ++ int ret; ++ ++ if (!aop->spi0_pmgr) ++ return -ENODEV; ++ ret = apple_pmgr_observe(&aop->spi0_pmgr, 1, &state, ++ a9_aop_spi0_pmgr_on, &on); ++ *raw = state.raw; ++ return ret ?: on ? 0 : -EIO; ++} ++ ++/* ++ * After a reset that cleared +0x8d0 bit 6, before the image copy: CPU halted, ++ * AP notifications masked. A refusal sets ch3.failed and nothing is loaded. ++ */ ++static int a9_aop_spi0_ch3_teardown(struct apple_aop *aop) ++{ ++ struct apple_pmgr_observation states[AOP_DOMAINS] = {}; ++ struct a9_aop_spi0 *s = &aop->spi0; ++ struct a9_aop_spi0_ch3 *c = &s->ch3; ++ const u32 *b = c->before, *a = c->after; ++ const struct a9_aop_ch3_slice *p; ++ const struct a9_aop_channel_diag_slice *d; ++ unsigned int i; ++ bool source_progress = false; ++ int ret; ++ ++ aop->experiment_step = "spi0-ch3-teardown"; ++ dev_info(aop->dev, "SPI0 CH3TD begin pa=%llx cpu=halted: first channel-3 store follows\n", ++ A9_SPI0_PA + A9_SPI0_CH3); ++ if (cold_defer) { ++ ret = a9_aop_cold_checkpoint_noflush(aop, "spi0-ch3-teardown"); ++ if (ret) { ++ c->failed = "failed-ch3-teardown-checkpoint"; ++ return ret; ++ } ++ } ++ ret = apple_pmgr_observe(aop->domains, AOP_DOMAINS, states, ++ a9_aop_spi0_ch3_locked, s); ++ if (ret) { ++ c->failed = "failed-ch3-teardown-guard"; ++ dev_info(aop->dev, "SPI0 CH3TD errno=%d ran=0: guard refused, channel 3 not touched\n", ++ ret); ++ return ret; ++ } ++ c->ran = true; ++ dev_info(aop->dev, "SPI0 CH3TD before c3=%08x route=%08x rate=%08x c3c=%08x desc=%08x 8d0=%08x 8d4=%08x\n", ++ b[0], b[1], b[2], b[3], b[4], b[5], b[6]); ++ for (i = 0; i < c->slices; i++) { ++ p = &c->slice[i]; ++ dev_info(aop->dev, "SPI0 CH3TD idle-sample slice=%u at_us=%llu c3=%08x scm_lo=%08x scm_hi=%08x 8d0=%08x idle=%u\n", ++ i, div_u64(p->ns, NSEC_PER_USEC), p->c3, p->scm_lo, ++ p->scm_hi, p->fault, !!(p->c3 & A9_SPI0_CH3_IDLE)); ++ if (c->poll) { ++ d = &s->ch3_diag[i]; ++ dev_info(aop->dev, "SPI0 CH3TD diagnostic slice=%u ctl=%08x cfg=%08x interval=%08x cursor=%08x scm_lo=%08x scm_hi=%08x 8d0=%08x 8d4=%08x\n", ++ i, p->c3, d->cfg, d->interval, d->cursor, p->scm_lo, ++ p->scm_hi, p->fault, d->fault_8d4); ++ if (i && (p->c3 != c->slice[0].c3 || ++ d->interval != s->ch3_diag[0].interval || ++ d->cursor != s->ch3_diag[0].cursor)) ++ source_progress = true; ++ } ++ } ++ if (c->poll && c->slices) ++ dev_info(aop->dev, "SPI0 CH3TD diagnostic mode=scheduler-read-only samples=%u ctl_hi=%02x->%02x interval=%08x->%08x cursor=%08x->%08x source_progress=%u result=%s\n", ++ c->slices, ++ c->slice[0].c3 >> A9_SPI0_CHANNEL_PROGRESS_SHIFT, ++ c->slice[c->slices - 1].c3 >> A9_SPI0_CHANNEL_PROGRESS_SHIFT, ++ s->ch3_diag[0].interval, ++ s->ch3_diag[c->slices - 1].interval, ++ s->ch3_diag[0].cursor, ++ s->ch3_diag[c->slices - 1].cursor, source_progress, ++ c->stable ? "stable-stop-reset-only" : "refused"); ++ /* beyond_fw_bound=1: idle, but later than the firmware would wait. */ ++ dev_info(aop->dev, "SPI0 CH3TD idle-poll run_before=%u slices=%u/%u idle_us=%llu bound_us=%u fw_bound_us=%u beyond_fw_bound=%u result=%s\n", ++ c->run_before, c->slices, A9_SPI0_CH3_SLICES, c->idle_us, ++ A9_SPI0_CH3_SLICES * A9_SPI0_CH3_SLICE_US, A9_SPI0_CH3_IDLE_FW_US, ++ !c->poll && c->idle_us > A9_SPI0_CH3_IDLE_FW_US, ++ c->poll ? "timeout" : "idle"); ++ if (c->poll && !c->stable) ++ dev_info(aop->dev, "SPI0 CH3TD writes stop=%08x idle_poll=%08x poll_errno=%d masks=skipped: idle bit 1 never set, no further store\n", ++ c->stop, c->idle, c->poll); ++ else if (c->stable) ++ dev_info(aop->dev, "SPI0 CH3TD writes stop=%08x idle_poll=%08x poll_errno=%d stable_stop=1 class0=%08x class1=%08x desc=00000000 c3c=%08x\n", ++ c->stop, c->idle, c->poll, c->class0, c->class1, c->ack); ++ else ++ dev_info(aop->dev, "SPI0 CH3TD writes stop=%08x idle_poll=%08x poll_errno=0 class0=%08x class1=%08x desc=00000000 c3c=%08x\n", ++ c->stop, c->idle, c->class0, c->class1, c->ack); ++ dev_info(aop->dev, "SPI0 CH3TD after c3=%08x route=%08x rate=%08x c3c=%08x desc=%08x 8d0=%08x 8d4=%08x\n", ++ a[0], a[1], a[2], a[3], a[4], a[5], a[6]); ++ dev_info(aop->dev, "SPI0 CH3TD timing reset_to_stop_us=%llu late=%u peers_run=%04x peers_ticking=%04x stable=%u checkpoint=noflush stable_why=%s\n", ++ c->reset_to_stop_us, c->reset_to_stop_us > A9_SPI0_LATE_STOP_US, ++ c->peers_run, c->peers_ticking, c->stable, ++ c->stable ? "stable" : c->poll ? c->stable_why : "idle"); ++ /* c3c_bit5 is the pulse's readback, logged only (0 at 19:50). */ ++ dev_info(aop->dev, "SPI0 CH3TD check errno=%d run=%u class0=%u class1=%u idle=%u desc=%08x c3c_bit5=%u ch3_pending=%u result=%s\n", ++ c->error, !!(a[A9_CH3_I_C3] & A9_SPI0_CH3_RUN), ++ !!(a[A9_CH3_I_C3] & A9_SPI0_CH3_IRQ0), ++ !!(a[A9_CH3_I_C3] & A9_SPI0_CH3_IRQ1), !!(a[A9_CH3_I_C3] & A9_SPI0_CH3_IDLE), ++ a[A9_CH3_I_DESC], !!(a[A9_CH3_I_C3C] & A9_SPI0_CH3_ACK), ++ !!(a[A9_CH3_I_8D0] & A9_SPI0_CH3_PENDING), c->failed ?: ++ c->stable ? "pass-stable-reset-only" : "pass"); ++ return c->error; ++} ++ ++static int a9_aop_spi0_channel_teardown(struct apple_aop *aop, ++ unsigned int channel) ++{ ++ struct apple_pmgr_observation states[AOP_DOMAINS] = {}; ++ struct a9_aop_spi0 *s = &aop->spi0; ++ struct a9_aop_spi0_ch3 *c = &s->channel; ++ struct a9_aop_spi0_channel_op op = { ++ .spi0 = s, ++ .state = c, ++ .channel = channel, ++ }; ++ const u32 *b, *a; ++ const struct a9_aop_ch3_slice *p; ++ const struct a9_aop_channel_diag_slice *d; ++ char checkpoint[32]; ++ unsigned int i; ++ bool source_progress = false; ++ bool stable_stop; ++ int ret; ++ ++ memset(c, 0, sizeof(*c)); ++ memset(s->channel_diag, 0, sizeof(s->channel_diag)); ++ s->teardown_failed_channel = channel; ++ aop->experiment_step = "spi0-channel-teardown"; ++ snprintf(checkpoint, sizeof(checkpoint), "spi0-ch%u-teardown", channel); ++ dev_info(aop->dev, "SPI0 CH%uTD begin pa=%llx cpu=halted: first channel-%u store follows\n", ++ channel, A9_SPI0_PA + A9_SPI0_CHANNEL_BASE + ++ A9_SPI0_CHANNEL_STRIDE * channel, channel); ++ if (cold_defer) { ++ ret = a9_aop_cold_checkpoint_noflush(aop, checkpoint); ++ if (ret) { ++ c->failed = "failed-channel-teardown-checkpoint"; ++ return ret; ++ } ++ } ++ ret = apple_pmgr_observe(aop->domains, AOP_DOMAINS, states, ++ a9_aop_spi0_channel_locked, &op); ++ if (ret) { ++ c->failed = "failed-channel-teardown-guard"; ++ dev_info(aop->dev, "SPI0 CH%uTD errno=%d ran=0: guard refused, channel %u not touched\n", ++ channel, ret, channel); ++ return ret; ++ } ++ c->ran = true; ++ stable_stop = s->teardown_stable & BIT(channel); ++ b = c->before; ++ a = c->after; ++ dev_info(aop->dev, "SPI0 CH%uTD before ctl=%08x route=%08x rate=%08x cfg=%08x desc=%08x 8d0=%08x 8d4=%08x\n", ++ channel, b[0], b[1], b[2], b[3], b[4], b[5], b[6]); ++ for (i = 0; i < c->slices; i++) { ++ p = &c->slice[i]; ++ dev_info(aop->dev, "SPI0 CH%uTD idle-sample slice=%u at_us=%llu ctl=%08x scm_lo=%08x scm_hi=%08x 8d0=%08x idle=%u\n", ++ channel, i, div_u64(p->ns, NSEC_PER_USEC), p->c3, ++ p->scm_lo, p->scm_hi, p->fault, ++ !!(p->c3 & A9_SPI0_CH3_IDLE)); ++ if (c->poll) { ++ d = &s->channel_diag[i]; ++ dev_info(aop->dev, "SPI0 CH%uTD diagnostic slice=%u ctl=%08x cfg=%08x interval=%08x cursor=%08x scm_lo=%08x scm_hi=%08x 8d0=%08x 8d4=%08x\n", ++ channel, i, p->c3, d->cfg, d->interval, d->cursor, ++ p->scm_lo, p->scm_hi, p->fault, d->fault_8d4); ++ if (i && (p->c3 != c->slice[0].c3 || ++ d->interval != s->channel_diag[0].interval || ++ d->cursor != s->channel_diag[0].cursor)) ++ source_progress = true; ++ } ++ } ++ if (c->poll && c->slices) ++ dev_info(aop->dev, "SPI0 CH%uTD diagnostic mode=scheduler-read-only samples=%u ctl_hi=%02x->%02x interval=%08x->%08x cursor=%08x->%08x source_progress=%u result=%s\n", ++ channel, c->slices, ++ c->slice[0].c3 >> A9_SPI0_CHANNEL_PROGRESS_SHIFT, ++ c->slice[c->slices - 1].c3 >> A9_SPI0_CHANNEL_PROGRESS_SHIFT, ++ s->channel_diag[0].interval, ++ s->channel_diag[c->slices - 1].interval, ++ s->channel_diag[0].cursor, ++ s->channel_diag[c->slices - 1].cursor, ++ source_progress, ++ stable_stop ? "stable-stop-reset-only" : "refused"); ++ dev_info(aop->dev, "SPI0 CH%uTD idle-poll run_before=%u slices=%u/%u idle_us=%llu bound_us=%u fw_bound_us=%u beyond_fw_bound=%u result=%s\n", ++ channel, c->run_before, c->slices, A9_SPI0_CH3_SLICES, c->idle_us, ++ A9_SPI0_CH3_SLICES * A9_SPI0_CH3_SLICE_US, A9_SPI0_CH3_IDLE_FW_US, ++ !c->poll && c->idle_us > A9_SPI0_CH3_IDLE_FW_US, ++ c->poll ? "timeout" : "idle"); ++ if (c->poll && !stable_stop) ++ dev_info(aop->dev, "SPI0 CH%uTD writes stop=%08x idle_poll=%08x poll_errno=%d masks=skipped: idle bit 1 never set, no further store\n", ++ channel, c->stop, c->idle, c->poll); ++ else if (stable_stop) ++ dev_info(aop->dev, "SPI0 CH%uTD writes stop=%08x idle_poll=%08x poll_errno=%d stable_stop=1 class0=%08x class1=%08x desc=00000000 cfg=%08x\n", ++ channel, c->stop, c->idle, c->poll, c->class0, c->class1, ++ c->ack); ++ else ++ dev_info(aop->dev, "SPI0 CH%uTD writes stop=%08x idle_poll=%08x poll_errno=0 class0=%08x class1=%08x desc=00000000 cfg=%08x\n", ++ channel, c->stop, c->idle, c->class0, c->class1, c->ack); ++ dev_info(aop->dev, "SPI0 CH%uTD after ctl=%08x route=%08x rate=%08x cfg=%08x desc=%08x 8d0=%08x 8d4=%08x\n", ++ channel, a[0], a[1], a[2], a[3], a[4], a[5], a[6]); ++ dev_info(aop->dev, "SPI0 CH%uTD timing reset_to_stop_us=%llu late=%u peers_run=%04x peers_ticking=%04x stable=%u checkpoint=noflush stable_why=%s\n", ++ channel, c->reset_to_stop_us, c->reset_to_stop_us > A9_SPI0_LATE_STOP_US, ++ c->peers_run, c->peers_ticking, c->stable, ++ c->stable ? "stable" : c->poll ? c->stable_why : "idle"); ++ dev_info(aop->dev, "SPI0 CH%uTD check errno=%d run=%u class0=%u class1=%u idle=%u desc=%08x cfg_bit5=%u pending0=%u pending1=%u result=%s\n", ++ channel, c->error, !!(a[A9_CH3_I_C3] & A9_SPI0_CH3_RUN), ++ !!(a[A9_CH3_I_C3] & A9_SPI0_CH3_IRQ0), ++ !!(a[A9_CH3_I_C3] & A9_SPI0_CH3_IRQ1), ++ !!(a[A9_CH3_I_C3] & A9_SPI0_CH3_IDLE), a[A9_CH3_I_DESC], ++ !!(a[A9_CH3_I_C3C] & A9_SPI0_CH3_ACK), ++ !!(a[A9_CH3_I_8D0] & BIT(2 * channel)), ++ !!(a[A9_CH3_I_8D4] & BIT(2 * channel)), c->failed ?: ++ stable_stop ? "pass-stable-reset-only" : "pass"); ++ return c->error; ++} ++ ++static const char *a9_aop_spi0_selected_verify(struct a9_aop_spi0 *s) ++{ ++ const u32 *v; ++ const char *failed = NULL; ++ unsigned int channel; ++ ++ if (s->teardown_selected != s->teardown_done) ++ return "failed-channel-teardown-incomplete"; ++ for (channel = 0; channel < A9_SPI0_CHANNELS; channel++) { ++ if (channel == 3 || !(s->teardown_selected & BIT(channel))) ++ continue; ++ v = s->trace.channel[channel]; ++ failed = a9_spi0_channel_state_verify(v[A9_CHANNEL_I_CTL], ++ v[A9_CHANNEL_I_DESC], ++ s->trace.ch3[A9_CH3_I_8D0], ++ s->trace.ch3[A9_CH3_I_8D4], channel, ++ s->teardown_stable & BIT(channel)); ++ if (failed) { ++ s->teardown_failed_channel = channel; ++ return failed; ++ } ++ } ++ return NULL; ++} ++ ++/* ++ * Channel 3 remains mandatory and executes first. Wake11 additionally selects ++ * every snapshot slot with run bit 0 (RUN) set and a nonzero interval, which ++ * excludes configured-but-idle slots while selecting any periodic client still ++ * owned by the old firmware. Wake16 returns to wake11's order after wake15's ++ * channel-7-first stop failed: all ten device stops fit "channel 3 idles when ++ * stopped within ~2 ms of the reset (5/5; 0/2 at 11 and 33 ms) and channel 7 ++ * never idles (0/3, at 0.3 ms, 1.9 ms and 15 ms)". Channel 3 goes first and ++ * early; channel 7 is then the stopped-without-idle channel the stable-stop ++ * fingerprint was captured from (wake11/12). ++ */ ++static int a9_aop_spi0_teardown_running(struct apple_aop *aop) ++{ ++ struct a9_aop_spi0 *s = &aop->spi0; ++ const u32 *v; ++ unsigned int channel; ++ int ret; ++ ++ s->teardown_selected = BIT(3); ++ s->teardown_done = 0; ++ s->teardown_stable = 0; ++ s->teardown_failed_channel = A9_SPI0_CHANNELS; ++ for (channel = 0; channel < A9_SPI0_CHANNELS; channel++) { ++ if (channel == 3) ++ continue; ++ v = s->trace.channel[channel]; ++ if ((v[A9_CHANNEL_I_CTL] & A9_SPI0_CH3_RUN) && ++ v[A9_CHANNEL_I_INTERVAL]) ++ s->teardown_selected |= BIT(channel); ++ } ++ dev_info(aop->dev, "SPI0 teardown select stage=after-controller-reset selected=%04x mandatory=0008 rule=ch3-or-run-and-interval order=ch3-then-others\n", ++ s->teardown_selected); ++ ret = a9_aop_spi0_ch3_teardown(aop); ++ if (ret) { ++ s->teardown_failed_channel = 3; ++ dev_info(aop->dev, "SPI0 teardown refused selected=%04x done=%04x failed_channel=3 errno=%d result=%s\n", ++ s->teardown_selected, s->teardown_done, ret, ++ s->ch3.failed ?: "failed-ch3-teardown"); ++ return ret; ++ } ++ s->teardown_done |= BIT(3); ++ for (channel = 0; channel < A9_SPI0_CHANNELS; channel++) { ++ if (channel == 3 || !(s->teardown_selected & BIT(channel))) ++ continue; ++ ret = a9_aop_spi0_channel_teardown(aop, channel); ++ if (ret) { ++ dev_info(aop->dev, "SPI0 teardown refused selected=%04x done=%04x failed_channel=%u errno=%d result=%s\n", ++ s->teardown_selected, s->teardown_done, channel, ret, ++ s->channel.failed ?: "failed-channel-teardown"); ++ return ret; ++ } ++ s->teardown_done |= BIT(channel); ++ } ++ s->teardown_failed_channel = A9_SPI0_CHANNELS; ++ dev_info(aop->dev, "SPI0 teardown complete selected=%04x done=%04x stable=%04x result=pass\n", ++ s->teardown_selected, s->teardown_done, s->teardown_stable); ++ return 0; ++} ++ ++/* ++ * Second copy of the firmware controller reset, after the stop has completed. ++ * The preceding after-ch3-teardown trace is the read-only before snapshot; ++ * after-post-teardown-reset is the matching read-only after snapshot. Refuse ++ * the image copy unless SCM is zero and every channel quiet-state gate holds. ++ */ ++static int a9_aop_spi0_post_reset(struct apple_aop *aop) ++{ ++ struct apple_pmgr_observation states[AOP_DOMAINS] = {}; ++ struct a9_aop_spi0 *s = &aop->spi0; ++ struct a9_aop_spi0_post_reset *r = &s->post_reset; ++ const u32 *b = r->before, *a = r->after; ++ int ret; ++ ++ if (!aop->force_halted || !aop->force_drained || !s->ch3.ran || ++ s->ch3.error || s->teardown_selected != s->teardown_done || r->ran) { ++ r->failed = "failed-post-reset-state"; ++ return -EPERM; ++ } ++ aop->experiment_step = "spi0-post-teardown-reset"; ++ dev_info(aop->dev, "SPI0 POSTRST begin pa=%llx reason=ch3-stop-latched-scm cpu=halted\n", ++ A9_SPI0_PA); ++ if (cold_defer) { ++ ret = a9_aop_cold_checkpoint(aop, "spi0-post-teardown-reset"); ++ if (ret) { ++ r->failed = "failed-post-reset-checkpoint"; ++ return ret; ++ } ++ } ++ ret = apple_pmgr_observe(aop->domains, AOP_DOMAINS, states, ++ a9_aop_spi0_post_reset_locked, s); ++ r->ran = !ret; ++ r->error = ret ?: r->error; ++ dev_info(aop->dev, "SPI0 POSTRST errno=%d ran=%d before ctl=%08x clk=%08x 8d0=%08x 8d4=%08x ch3=%08x ch3c=%08x\n", ++ r->error, r->ran, b[0], b[1], b[2], b[3], b[4], b[5]); ++ dev_info(aop->dev, "SPI0 POSTRST after ctl=%08x clk=%08x 8d0=%08x 8d4=%08x ch3=%08x ch3c=%08x saved=%08x asserted=%08x polled=%08x ch3_pending=%u->%u\n", ++ a[0], a[1], a[2], a[3], a[4], a[5], r->saved, r->asserted, ++ r->polled, !!(b[A9_SPI0_I_FAULT] & A9_SPI0_CH3_PENDING), ++ !!(a[A9_SPI0_I_FAULT] & A9_SPI0_CH3_PENDING)); ++ if (r->error) { ++ r->failed = "failed-post-reset-controller"; ++ return r->error; ++ } ++ ret = a9_aop_spi0_trace(aop, "after-post-teardown-reset"); ++ if (ret) ++ return ret; ++ if (s->trace.scm[0] || s->trace.scm[1]) ++ r->failed = "failed-post-reset-scm"; ++ else { ++ r->failed = a9_spi0_ch3_verify(s->trace.ch3, ++ s->teardown_stable & BIT(3)); ++ if (!r->failed) ++ r->failed = a9_aop_spi0_selected_verify(s); ++ } ++ ret = r->failed ? -EIO : 0; ++ dev_info(aop->dev, "SPI0 POSTRST check errno=%d scm_lo=%08x scm_hi=%08x ch3_pending=%u result=%s\n", ++ ret, s->trace.scm[0], s->trace.scm[1], ++ !!(s->trace.ch3[A9_CH3_I_8D0] & A9_SPI0_CH3_PENDING), ++ r->failed ?: "pass"); ++ dev_info(aop->dev, "SPI0 POSTRST channels selected=%04x done=%04x stable=%04x result=%s\n", ++ s->teardown_selected, s->teardown_done, s->teardown_stable, ++ r->failed ?: "pass"); ++ return ret; ++} ++ ++/* ++ * The ten INTC words of a9_intc_offsets into state[@at], read only. The ++ * INTC is the AOP wrapper block (0xc1800000 in the firmware, space 2 as ++ * SPI0's verified 0xc1100000 = 0x210100000), which this driver reads ++ * unguarded for the mailbox and CPU control; no separate PMGR domain. ++ */ ++static void a9_aop_intc_snapshot(struct apple_aop *aop, unsigned int at) ++{ ++ struct a9_aop_intc *n = &aop->spi0.intc; ++ u32 *v = n->state[at]; ++ unsigned int i; ++ ++ for (i = 0; i < A9_INTC_REGS; i++) ++ v[i] = aop_rd(aop, a9_intc_offsets[i]); ++ n->states |= BIT(at); ++ dev_info(aop->dev, "AOP INTC state at=%s reset808=%08x current81c=%08x raise900=%08x ack980=%08x mask_a00=%08x mask_a04=%08x mask_a08=%08x mask_a80=%08x g7_c020=%08x g7_c024=%08x src_masked=%u view=%s\n", ++ a9_intc_at_name(at), v[0], v[1], v[2], v[3], v[4], v[5], v[6], ++ v[7], v[8], v[9], !!(v[A9_INTC_I_MASK0] & A9_INTC_SPI0_SCM), ++ a9_intc_view(v)); ++} ++ ++/* ++ * Round 4 (ds-aop-spi0-ch3-round4.md): the old session's SPI0 global IRQ ++ * 0x10014 stays latched in the AOP interrupt controller, and fresh firmware ++ * takes it as soon as SPI lifecycle event 1 enables it, before its own SPI0 ++ * reset (SCMErr:0x3,3). With the CPU halted and before the first SPI0 ++ * access, disable that source (group 1 disable bank, 0102c9ce). The bank ++ * reads back the mask, as the firmware's own reads of it show: bit 20 must ++ * read 1 after the store. ++ */ ++static int a9_aop_intc_disable(struct apple_aop *aop) ++{ ++ struct a9_aop_intc *n = &aop->spi0.intc; ++ const u32 *b = n->state[A9_INTC_AT_PRE_DISABLE]; ++ const u32 *a = n->state[A9_INTC_AT_POST_DISABLE]; ++ int ret; ++ ++ aop->experiment_step = "intc-disable"; ++ dev_info(aop->dev, "AOP INTC disable begin irq=0x10014 pa=%llx value=%08x cpu=halted: first INTC store follows\n", ++ A9_SPI0_WRAPPER_PA + A9_INTC_DISABLE, A9_INTC_SPI0_SCM); ++ if (cold_defer) { ++ ret = a9_aop_cold_checkpoint(aop, "intc-disable"); ++ if (ret) { ++ n->failed = "failed-intc-disable-checkpoint"; ++ return ret; ++ } ++ } ++ a9_aop_intc_snapshot(aop, A9_INTC_AT_PRE_DISABLE); ++ ret = aop_wr(aop, A9_INTC_DISABLE, A9_INTC_SPI0_SCM); ++ a9_aop_intc_snapshot(aop, A9_INTC_AT_POST_DISABLE); ++ if (ret) { ++ n->failed = "failed-intc-disable"; ++ } else if (!(a[A9_INTC_I_MASK0] & A9_INTC_SPI0_SCM)) { ++ n->failed = "failed-intc-disable-readback"; ++ ret = -EIO; ++ } ++ n->disabled = !ret; ++ dev_info(aop->dev, "AOP INTC disable errno=%d before=%08x value=%08x after=%08x mask_a80=%08x src_masked=%u view=%s result=%s\n", ++ ret, b[A9_INTC_I_MASK0], A9_INTC_SPI0_SCM, a[A9_INTC_I_MASK0], ++ a[A9_INTC_I_ENABLE0], !!(a[A9_INTC_I_MASK0] & A9_INTC_SPI0_SCM), ++ a9_intc_view(a), n->failed ?: "pass"); ++ return ret; ++} ++ ++/* ++ * Release policy for aop_spi0_reset=1. The reload is device-proven for exactly ++ * one retained state: the previous kernel quiesced and halted the firmware ++ * (wake16, 2026-10-29; decision prev-quiesce-halted). Auto-boot still set, a ++ * previous kernel that did not halt it, or an older kernel in the chain are ++ * different hardware states that no run has reloaded with the final teardown. ++ * Refuse them before the first halt or SPI0 access so the old firmware is ++ * left exactly as found; aop_spi0_untested=1 attempts them anyway. A skip ++ * (DFU cold, bad record, nothing ran) must also find the wrapper at its reset ++ * state: a record-less AOP that something already stopped is refused rather ++ * than cold-loaded. A stale record skips as before. ++ */ ++static int a9_aop_spi0_policy(struct apple_aop *aop, u32 auto_boot, u32 outbox) ++{ ++ enum a9_spi0_reason reason = a9_spi0_decide(auto_boot, outbox & AOP_OUTBOX_ENABLE, ++ &aop->spi0.prev); ++ ++ if (a9_spi0_policy_allows(reason, outbox & AOP_OUTBOX_ENABLE, spi0_untested)) ++ return 0; ++ dev_info(aop->dev, "SPI0 policy decision=refuse reason=%s auto_boot=%u out4020=%08x prev=%s prev_hello=%d prev_halted=%d: only prev-quiesce-halted reloads and a skip needs the wrapper at its reset state block not touched\n", ++ a9_spi0_reason_name(reason), auto_boot, outbox, ++ a9_spi0_prev_name(aop->spi0.prev.state), aop->spi0.prev.hello, ++ aop->spi0.prev.halted); ++ return -EPERM; ++} ++ ++/* ++ * After the proven halt/drain, before any SRAM write. Only firmware that ran ++ * since the last SoC reset leaves SPI0 state behind: auto-boot 1, or the ++ * previous kernel's transcript bound by +0x4020 (a9_spi0_decide). Without ++ * that evidence the block is never touched. @auto_boot and @outbox are the ++ * boot snapshot from before the stop. Nonzero means: do not load or ++ * release the AOP. ++ */ ++static int a9_aop_spi0_prereset(struct apple_aop *aop, u32 auto_boot, u32 outbox) ++{ ++ struct apple_pmgr_observation states[AOP_DOMAINS] = {}; ++ struct a9_aop_spi0 *s = &aop->spi0; ++ const u32 *b = s->before, *a = s->after; ++ const char *failed = NULL; ++ bool run; ++ int ret; ++ ++ s->auto_boot = auto_boot; ++ s->outbox = outbox; ++ s->reason = a9_spi0_decide(auto_boot, outbox & AOP_OUTBOX_ENABLE, &s->prev); ++ s->decided = true; ++ run = a9_spi0_reason_runs(s->reason); ++ dev_info(aop->dev, "SPI0 reset decision=%s reason=%s auto_boot=%u out4020=%08x prev=%s prev_hello=%d prev_halted=%d prev_steps=0x%x prev_carry=%d%s\n", ++ run ? "run" : "skip", a9_spi0_reason_name(s->reason), auto_boot, ++ outbox, a9_spi0_prev_name(s->prev.state), s->prev.hello, ++ s->prev.halted, s->prev.steps, s->prev.carried, ++ run ? "" : ": block not touched"); ++ if (!run) { ++ s->skipped = a9_spi0_reason_name(s->reason); ++ return 0; ++ } ++ if (!s->base || !aop->force_halted || !aop->force_drained || ++ !a9_aop_force_stopped(aop)) ++ return -EPERM; ++ ret = a9_aop_intc_disable(aop); ++ if (ret) ++ return ret; ++ aop->experiment_step = "spi0-reset"; ++ s->pmgr_before_errno = a9_aop_spi0_pmgr(aop, &s->pmgr_before); ++ /* Reaches ramoops before the first AP access to the block. */ ++ dev_info(aop->dev, "SPI0 reset begin pa=%llx cpu=halted ps_spi0=%08x errno=%d: first AP access follows\n", ++ A9_SPI0_PA, s->pmgr_before, s->pmgr_before_errno); ++ if (cold_defer) { ++ ret = a9_aop_cold_checkpoint(aop, "spi0-reset"); ++ if (ret) ++ return ret; ++ } ++ ret = a9_aop_spi0_trace(aop, "before-controller-reset"); ++ if (ret) ++ return ret; ++ /* ++ * Wake15: from the controller reset to the last channel stop nothing may ++ * drain a console in this task. Holding the console lock makes every ++ * dev_info() below only append to the log buffer; the flushed spi0-reset ++ * checkpoint above already put the begin line into ramoops, and the lines ++ * are emitted by console_unlock() once the stops are stored. ++ */ ++ console_lock(); ++ ret = apple_pmgr_observe(aop->domains, AOP_DOMAINS, states, ++ a9_aop_spi0_reset_locked, s); ++ s->ran = !ret; ++ s->error = ret ?: s->error; ++ s->pmgr_after_errno = a9_aop_spi0_pmgr(aop, &s->pmgr_after); ++ dev_info(aop->dev, "SPI0 reset errno=%d ran=%d pa=%llx before ctl=%08x clk=%08x 8d0=%08x 8d4=%08x ch3=%08x ch3c=%08x\n", ++ s->error, s->ran, A9_SPI0_PA, b[0], b[1], b[2], b[3], b[4], b[5]); ++ dev_info(aop->dev, "SPI0 reset after ctl=%08x clk=%08x 8d0=%08x 8d4=%08x ch3=%08x ch3c=%08x saved=%08x asserted=%08x polled=%08x ch3_pending=%u->%u provider3=not-replayed\n", ++ a[0], a[1], a[2], a[3], a[4], a[5], s->saved, s->asserted, ++ s->polled, !!(b[A9_SPI0_I_FAULT] & A9_SPI0_CH3_PENDING), ++ !!(a[A9_SPI0_I_FAULT] & A9_SPI0_CH3_PENDING)); ++ dev_info(aop->dev, "SPI0 pmgr ps_spi0 before=%08x errno=%d after=%08x errno=%d (read-only, never powered)\n", ++ s->pmgr_before, s->pmgr_before_errno, s->pmgr_after, ++ s->pmgr_after_errno); ++ if (s->error) { ++ console_unlock(); ++ return s->error; ++ } ++ ret = a9_aop_spi0_trace(aop, "after-controller-reset"); ++ if (!ret) ++ ret = a9_aop_spi0_teardown_running(aop); ++ console_unlock(); ++ if (!ret) ++ ret = a9_aop_spi0_trace(aop, "after-ch3-teardown"); ++ if (!ret) ++ failed = a9_aop_spi0_selected_verify(s); ++ if (failed) { ++ s->channel.failed = failed; ++ ret = -EIO; ++ } ++ if (!ret) ++ ret = a9_aop_spi0_post_reset(aop); ++ return ret; ++} ++ ++/* Wake10 samples both SCM words and both fault classes, not only channel 3. */ ++static void a9_aop_spi0_fault_read(void *data) ++{ ++ struct a9_aop_spi0 *s = data; ++ ++ s->prerun_scm_lo = readl(s->base + A9_SPI0_SCM_LO); ++ s->prerun_scm_hi = readl(s->base + A9_SPI0_SCM_HI); ++ s->prerun = readl(s->base + A9_SPI0_FAULT); ++ s->prerun_8d4 = readl(s->base + A9_SPI0_FAULT_8D4); ++ if (s->prerun & A9_SPI0_CH3_PENDING) ++ a9_aop_spi0_ch3_snapshot(s->base, s->ch3.reassert); ++} ++ ++/* ++ * Last AP look before RUN, CPU still halted, after the image copy: bit 6 ++ * must stay clear in every one of 31 guarded samples spaced >= 5 ms apart, ++ * spanning >= 150 ms. A reasserted channel-3 bit would crash the fresh ++ * image as before, so the first set sample refuses the release; nothing is ++ * retried. Any refusal sets ch3.failed. ++ */ ++static int a9_aop_spi0_recheck(struct apple_aop *aop) ++{ ++ struct apple_pmgr_observation states[AOP_DOMAINS] = {}; ++ struct a9_aop_spi0 *s = &aop->spi0; ++ struct a9_aop_spi0_ch3 *c = &s->ch3; ++ const u32 *r = c->reassert; ++ u64 start = ktime_get_boottime_ns(); ++ unsigned int i; ++ int ret = 0; ++ ++ for (i = 0; i < A9_SPI0_WINDOW_SAMPLES; i++) { ++ if (i) ++ usleep_range(A9_SPI0_WINDOW_US, A9_SPI0_WINDOW_US + 1000); ++ ret = apple_pmgr_observe(aop->domains, AOP_DOMAINS, states, ++ a9_aop_spi0_fault_read, s); ++ c->samples = i + 1; ++ c->elapsed_ms = div_u64(ktime_get_boottime_ns() - start, NSEC_PER_MSEC); ++ if (ret) { ++ c->failed = "failed-ch3-window-guard"; ++ break; ++ } ++ if (s->prerun_scm_lo || s->prerun_scm_hi || s->prerun || ++ s->prerun_8d4) { ++ s->reasserted = c->reasserted = true; ++ c->reassert_sample = i; ++ c->reassert_ms = c->elapsed_ms; ++ c->failed = "failed-spi0-window-reasserted"; ++ ret = -EIO; ++ dev_info(aop->dev, "SPI0 WINDOW reasserted sample=%u at_ms=%llu scm_lo=%08x scm_hi=%08x 8d0=%08x 8d4=%08x cpu=halted\n", ++ i, c->reassert_ms, s->prerun_scm_lo, s->prerun_scm_hi, ++ s->prerun, s->prerun_8d4); ++ if (s->prerun & A9_SPI0_CH3_PENDING) ++ dev_info(aop->dev, "SPI0 WINDOW channel3 c3=%08x route=%08x rate=%08x c3c=%08x desc=%08x 8d0=%08x 8d4=%08x\n", ++ r[0], r[1], r[2], r[3], r[4], r[5], r[6]); ++ break; ++ } ++ } ++ /* usleep_range() cannot return early; this only trips on a bad clock. */ ++ if (!ret && c->elapsed_ms < A9_SPI0_WINDOW_MS) { ++ c->failed = "failed-ch3-window-short"; ++ ret = -EIO; ++ } ++ c->window_done = !ret; ++ dev_info(aop->dev, "SPI0 CH3TD window errno=%d samples=%u/%u elapsed_ms=%llu min_ms=%u result=%s\n", ++ ret, c->samples, A9_SPI0_WINDOW_SAMPLES, c->elapsed_ms, ++ A9_SPI0_WINDOW_MS, ret ? c->failed : "pass"); ++ dev_info(aop->dev, "SPI0 pre-run errno=%d 8d0=%08x ch3_pending=%u cpu=halted\n", ++ ret, s->prerun, !!(s->prerun & A9_SPI0_CH3_PENDING)); ++ dev_info(aop->dev, "SPI0 WINDOW state errno=%d scm_lo=%08x scm_hi=%08x 8d0=%08x 8d4=%08x result=%s\n", ++ ret, s->prerun_scm_lo, s->prerun_scm_hi, s->prerun, ++ s->prerun_8d4, ret ? c->failed : "pass"); ++ if (ret && c->reasserted) ++ a9_aop_spi0_trace(aop, "window-fault"); ++ return ret; ++} ++ ++/* SCM state and the channel-3 interval pair, read-only, into scm[scm_at]. */ ++static void a9_aop_spi0_scm_read(void *data) ++{ ++ struct a9_aop_spi0 *s = data; ++ unsigned int i; ++ ++ for (i = 0; i < A9_SPI0_SCM_REGS; i++) ++ s->intc.scm[s->intc.scm_at][i] = readl(s->base + a9_spi0_scm_offsets[i]); ++} ++ ++/* ++ * After the clean pre-RUN window, CPU still halted: the firmware's group-1 ++ * pre-handler acknowledge for 0x10014 (0102c6de..0102c6f2) discards the ++ * old-session latch now that its SPI0 cause is quiet. Fresh firmware ++ * re-enables the source in event 1. The INTC words, the SCM state and the ++ * C3 interval pair are logged around it and never gated: with 0x10014 ++ * masked, no readable word shows whether it is still pending. ++ */ ++static int a9_aop_intc_ack(struct apple_aop *aop) ++{ ++ struct apple_pmgr_observation states[AOP_DOMAINS] = {}; ++ struct a9_aop_spi0 *s = &aop->spi0; ++ struct a9_aop_intc *n = &s->intc; ++ const u32 *p = n->scm[0], *q = n->scm[1]; ++ const u32 *b = n->state[A9_INTC_AT_PRE_ACK]; ++ const u32 *a = n->state[A9_INTC_AT_POST_ACK]; ++ int ret; ++ ++ aop->experiment_step = "intc-ack"; ++ n->scm_at = 0; ++ ret = apple_pmgr_observe(aop->domains, AOP_DOMAINS, states, ++ a9_aop_spi0_scm_read, s); ++ if (ret) { ++ n->failed = "failed-intc-ack-guard"; ++ dev_info(aop->dev, "AOP INTC pre-ack errno=%d: guard refused, no acknowledge\n", ++ ret); ++ return ret; ++ } ++ a9_aop_intc_snapshot(aop, A9_INTC_AT_PRE_ACK); ++ dev_info(aop->dev, "AOP INTC pre-ack scm_lo=%08x scm_hi=%08x c3_interval=%08x c3_cursor=%08x current=%08x cpu=halted\n", ++ p[0], p[1], p[2], p[3], b[A9_INTC_I_CURRENT]); ++ ret = aop_wr(aop, A9_INTC_ACK, A9_INTC_SPI0_SCM); ++ /* Also the posted-write barrier. */ ++ a9_aop_intc_snapshot(aop, A9_INTC_AT_POST_ACK); ++ n->acked = !ret; ++ dev_info(aop->dev, "AOP INTC ack errno=%d before=%08x value=%08x after=%08x\n", ++ ret, b[A9_INTC_I_ACK], A9_INTC_SPI0_SCM, a[A9_INTC_I_ACK]); ++ if (ret) { ++ n->failed = "failed-intc-ack"; ++ return ret; ++ } ++ n->scm_at = 1; ++ ret = apple_pmgr_observe(aop->domains, AOP_DOMAINS, states, ++ a9_aop_spi0_scm_read, s); ++ if (ret) ++ n->failed = "failed-intc-post-guard"; ++ dev_info(aop->dev, "AOP INTC post-ack errno=%d scm_lo=%08x scm_hi=%08x c3_interval=%08x c3_cursor=%08x current=%08x result=%s\n", ++ ret, q[0], q[1], q[2], q[3], a[A9_INTC_I_CURRENT], n->failed ?: "pass"); ++ return ret; ++} ++ ++/* ++ * Wake7 still took 0x10014 before SPI event 5 despite the targeted ack. ++ * Replay INTC lifecycle event 3 (0102c592/598/59c/5a2): 1/read/0/read at ++ * +0x808. aop_wr includes a posted-write read after each store. This resets ++ * all AOP-private interrupt state; it does not write the SCM status or MPU. ++ * No assumption that SCM must become zero. Only the final reset readback ++ * and the established channel-3 quiet conditions gate RUN. ++ */ ++static int a9_aop_intc_reset(struct apple_aop *aop) ++{ ++ struct a9_aop_spi0 *s = &aop->spi0; ++ struct a9_aop_intc *n = &s->intc; ++ int ret; ++ ++ if (!aop->force_halted || !aop->force_drained || !s->ch3.window_done || ++ !n->acked || n->reset_done || ++ (aop_rd(aop, AOP_CPU_CONTROL) & AOP_CPU_START) || readl(aop->control)) { ++ n->failed = "failed-intc-reset-state"; ++ return -EPERM; ++ } ++ ret = a9_aop_spi0_trace(aop, "pre-intc-reset"); ++ if (ret) ++ return ret; ++ aop->experiment_step = "intc-reset"; ++ dev_info(aop->dev, "AOP INTC reset begin pa=%llx sequence=1/read/0/read cpu=halted\n", ++ A9_SPI0_WRAPPER_PA + A9_INTC_RESET); ++ ret = aop_wr(aop, A9_INTC_RESET, 1); ++ n->reset_asserted = aop_rd(aop, A9_INTC_RESET); ++ if (ret) { ++ n->failed = "failed-intc-reset-assert"; ++ return ret; ++ } ++ ret = aop_wr(aop, A9_INTC_RESET, 0); ++ n->reset_after = aop_rd(aop, A9_INTC_RESET); ++ if (ret) { ++ n->failed = "failed-intc-reset-deassert"; ++ return ret; ++ } ++ if (n->reset_after) { ++ n->failed = "failed-intc-reset-readback"; ++ ret = -EIO; ++ } else { ++ ret = a9_aop_spi0_trace(aop, "post-intc-reset"); ++ if (!ret && a9_spi0_ch3_verify(s->trace.ch3, ++ s->teardown_stable & BIT(3))) { ++ n->failed = "failed-intc-reset-ch3"; ++ ret = -EIO; ++ } else if (!ret && a9_aop_spi0_selected_verify(s)) { ++ n->failed = "failed-intc-reset-channel"; ++ ret = -EIO; ++ } ++ } ++ n->reset_done = !ret; ++ dev_info(aop->dev, "AOP INTC reset errno=%d asserted=%08x after=%08x result=%s\n", ++ ret, n->reset_asserted, n->reset_after, n->failed ?: "pass"); ++ return ret; ++} ++ +diff --git a/drivers/soc/apple/aop-warm-spi0.h b/drivers/soc/apple/aop-warm-spi0.h +new file mode 100644 +index 000000000..2470165ce +--- /dev/null ++++ b/drivers/soc/apple/aop-warm-spi0.h +@@ -0,0 +1,543 @@ ++/* SPDX-License-Identifier: GPL-2.0-only */ ++/* ++ * J71t AOP reload/wake experiments, both opt-in and default off. Constants ++ * and evidence types only; the MMIO lives in apple-a9-aop-active.c. ++ * Evidence: docs/periph/ds-aop-reload-crash-and-wake.md. ++ * ++ * aop_spi0_reset=1: every halt + SRAM reload of a live AOP crashed in ++ * AOPSPIMaster_channelFaultIRQ (0102f252, DFAR 0x4c): the AOP-private SPI0 ++ * controller kept channel 3's +0x8d0 bit 6 while the fresh firmware's ++ * channel table was still empty. With the CPU halted, the AP replays the ++ * firmware's own controller reset AOPSPIMaster_reset (0102f2f4), then the ++ * firmware's own channel-3 teardown, and samples bit 6 for >= 150 ms after ++ * the image copy before it releases RUN once. Round 4: the old session's ++ * SPI0 global IRQ 0x10014 stays latched in the AOP interrupt controller ++ * (SCMErr:0x3,3 at the accel STARTEP), so the AP disables that source before ++ * the reset and acknowledges it after the clean window, just before RUN. ++ * Wake7: the channel-3 idle wait gets the firmware's own 10 ms bound twice ++ * over, sampled per 1 ms slice, and the disable is judged by the INTC mask ++ * readback, which the firmware itself reads. ++ * Wake8: after the targeted ack, replay INTC event 3 (+0x808: 1/read/0/read). ++ * This resets retained interrupt delivery, not the read-only SCM state. ++ * Wake9: wake8 device evidence showed the channel-3 stop itself changes SCM ++ * from 0 to 3. Replay the same SPI controller reset once more after teardown; ++ * require the post-reset SCM state to be zero and the teardown state to hold. ++ * Wake10: the second reset clears SCM, but a halted CPU then saw SCM 3 and ++ * +0x8d0 bit 14 (channel 7) during the firmware copy. Snapshot all 16 channel ++ * slots, sample global SCM/fault state across the copy, and gate the copy and ++ * final RUN on a completely quiet readback. ++ * Wake11: that snapshot proved channels 3 and 7 were both running. Keep the ++ * channel-3 sequence unchanged, then apply the same firmware teardown to every ++ * other run-bit + nonzero-interval channel before the post-teardown reset. ++ * Wake12: channel 7 is the phosphorus/BMP284 async client. Its firmware has no ++ * owner-specific stop path, so do not invent another store when the generic ++ * stop times out. Record the remaining scheduler state read-only and refuse. ++ * Wake13/14: every device stop of channel 3 that idled (wake8..wake12) came ++ * 1.5-2.2 ms after the controller reset; the two that did not (wake13/14) came ++ * 11/33 ms after it (pr_flush in the checkpoint), with class-0 state identical to ++ * wake12's passes. Wake15 closed that window (no pr_flush between the reset and ++ * the stops, console lock held), recorded the reset-to-stop latency and the peers ++ * per stop, and accepted one identity-pinned stopped-without-idle channel (3 or ++ * 7) as wake13 did for channel 7. Wake15's channel-7-first order then showed ++ * channel 7 never idles even at 0.3 ms (it is 0/3 over wake11/12/15), with ++ * nothing ticking. Wake16 therefore returns to channel 3 first (early, the only ++ * stop that ever idled) and then channel 7, which takes the stable-stop path ++ * (SCM 0/0 or 3/0, constant); every stop logs why the fingerprint did or did not ++ * hold (stable_why=). Wake14's class-0 prelude stays removed. ++ * ++ * aop_wake_wrapper=1: before 0x220, the AppleA7IOP status-3 resume wrapper ++ * writes (b4b854..b4b8b4). No CPU RUN, no auto-boot, no NMI. ++ */ ++#ifndef _A9_AOP_RELOAD_H ++#define _A9_AOP_RELOAD_H ++ ++#include ++#include ++#include "aop-warm-record.h" ++ ++/* Firmware device spi0: IOP 0x10100000 (space 2, 0x1000) via /arm-io ranges. */ ++#define A9_SPI0_PA 0x210100000ULL ++#define A9_SPI0_SIZE 0x1000U ++#define A9_SPI0_WRAPPER_PA 0x210800000ULL /* the only layout this applies to */ ++#define A9_SPI0_CONTROL 0x000U ++#define A9_SPI0_CLOCK 0x008U /* 0102f2aa programs 0x01b71b00 here */ ++#define A9_SPI0_FAULT 0x8d0U /* bit 2n = channel n, read by 0102f20a */ ++#define A9_SPI0_FAULT_8D4 0x8d4U ++#define A9_SPI0_CHANNEL_BASE 0x100U ++#define A9_SPI0_CHANNEL_STRIDE 0x080U ++#define A9_SPI0_CHANNELS 16U ++#define A9_SPI0_CHANNEL_REGS 7U ++#define A9_SPI0_CHANNEL_DESC 0x018U ++#define A9_SPI0_CH3 0x280U /* base + 0x100 + 0x80 * 3 */ ++#define A9_SPI0_CH3_0C 0x28cU ++#define A9_SPI0_RESET 0x100U /* control bit 8 */ ++#define A9_SPI0_ENABLE 0x001U /* control bit 0 */ ++#define A9_SPI0_CH3_PENDING 0x040U ++/* The firmware spins without bound; the AP gives each poll 1 ms. */ ++#define A9_SPI0_POLL_US 1000U ++#define A9_SPI0_DELAY_US 100U /* 0102b3f8(100, 0), twice */ ++#define A9_SPI0_REGS 6 ++ ++static const u32 a9_spi0_offsets[A9_SPI0_REGS] = { ++ A9_SPI0_CONTROL, A9_SPI0_CLOCK, A9_SPI0_FAULT, A9_SPI0_FAULT_8D4, ++ A9_SPI0_CH3, A9_SPI0_CH3_0C, ++}; ++ ++/* Snapshot indices into a9_spi0_offsets. */ ++enum { A9_SPI0_I_CONTROL, A9_SPI0_I_CLOCK, A9_SPI0_I_FAULT }; ++ ++/* ++ * Channel-3 teardown after the reset (ds-aop-spi0-ch3-round3.md section 5). ++ * The reset leaves C3 = 0x00010447: idle but still enabled, async-armed and ++ * with the class-0 fault IRQ on, so a level source re-latched +0x8d0 bit 6 ++ * within ~105 ms with the CPU halted. AOPSPIMaster_teardownChannel ++ * (0102e4f8) stops the channel and masks both IRQ classes; the fault ++ * handler (0102f20a) then acknowledges with C3C bit 5. ++ */ ++#define A9_SPI0_CH3_ROUTE 0x284U ++#define A9_SPI0_CH3_RATE 0x288U ++#define A9_SPI0_CH3_DESC 0x298U /* channel +0x18, zeroed on teardown */ ++#define A9_SPI0_CH3_RUN 0x001U /* C3 bit 0, channel run/enable */ ++#define A9_SPI0_CH3_IDLE 0x002U /* C3 bit 1, polled after the stop */ ++#define A9_SPI0_CH3_IRQ0 0x040U /* C3 bit 6, +0x8d0 class-0/fault enable */ ++#define A9_SPI0_CH3_IRQ1 0x100U /* C3 bit 8, +0x8d4 class-1 enable */ ++#define A9_SPI0_CHANNEL_PROGRESS_SHIFT 24U /* hardware-updated, not an IRQ class */ ++#define A9_SPI0_CH3_STOP 0x411U /* clear run bit 0 and bits 4, 10 */ ++#define A9_SPI0_CH3_CLASS0 0x450U /* mask class 0: bits 4, 6, 10 */ ++#define A9_SPI0_CH3_CLASS1 0x510U /* mask class 1: bits 4, 8, 10 */ ++#define A9_SPI0_CH3_ACK 0x020U /* C3C bit 5, ORed by 0102f20a; self-clearing */ ++#define A9_SPI0_PHOSPHORUS_CHANNEL 7U ++#define A9_SPI0_PHOSPHORUS_CTL_MASK 0x00ffffffU ++#define A9_SPI0_PHOSPHORUS_CTL_RUN 0x00010445U ++#define A9_SPI0_PHOSPHORUS_CTL_STOPPED 0x00010444U ++#define A9_SPI0_PHOSPHORUS_ROUTE 0x00000040U ++#define A9_SPI0_PHOSPHORUS_RATE 0x019000c8U ++#define A9_SPI0_PHOSPHORUS_CFG 0x00200009U ++/* Channel 3 is the MPU6900 async FIFO client (route 0x10, same rate/config). */ ++#define A9_SPI0_MPU_ASYNC_CHANNEL 3U ++#define A9_SPI0_MPU_ASYNC_ROUTE 0x00000010U ++/* Stopped control word, any top byte, class-0 enable optional (0x40). */ ++#define A9_SPI0_STABLE_CTL_STOPPED 0x00010404U ++#define A9_SPI0_STABLE_CTL_MASK 0x00ffffbfU ++/* Peer sampling and the "late stop" mark are reported, never gated, in-kernel. */ ++#define A9_SPI0_LATE_STOP_US 3000U ++/* ++ * AOPSPIMaster_setChannelEnabled(false) (0102ef7c) stores C3 & ~0x411 only ++ * if bit 0 was set, then spins on bit 1 until master +0x30, and panics ++ * "SCMto:%u" (0x010398da) past it. Event 1 sets +0x30 to 0x2710 us when it ++ * is 0 (0102e6fa, as in the live-cold object), through timer +0x18 = ++ * us * Hz / 1000000 (0102ff3c): a 10 ms bound. The AP polls in 1 ms slices, ++ * up to twice that, sampling C3, the SCM state and +0x8d0 after each slice. ++ */ ++#define A9_SPI0_CH3_IDLE_FW_US 10000U ++#define A9_SPI0_CH3_SLICE_US 1000U ++#define A9_SPI0_CH3_SLICES 20U /* 20 ms */ ++/* At least 150 ms (the 16:27 re-latch took ~105 ms) of samples before RUN. */ ++#define A9_SPI0_WINDOW_SAMPLES 31U ++#define A9_SPI0_WINDOW_US 5000U /* 30 sleeps >= 5 ms each */ ++#define A9_SPI0_WINDOW_MS 150U /* required first-to-last sample span */ ++#define A9_SPI0_CH3_REGS 7 ++ ++static const u32 a9_spi0_ch3_offsets[A9_SPI0_CH3_REGS] = { ++ A9_SPI0_CH3, A9_SPI0_CH3_ROUTE, A9_SPI0_CH3_RATE, A9_SPI0_CH3_0C, ++ A9_SPI0_CH3_DESC, A9_SPI0_FAULT, A9_SPI0_FAULT_8D4, ++}; ++ ++/* Every firmware-visible channel slot, read only in wake10 and selected in wake11. */ ++static const u32 a9_spi0_channel_offsets[A9_SPI0_CHANNEL_REGS] = { ++ 0x00U, 0x04U, 0x08U, 0x0cU, 0x10U, 0x14U, 0x18U, ++}; ++ ++enum { ++ A9_CHANNEL_I_CTL, A9_CHANNEL_I_ROUTE, A9_CHANNEL_I_RATE, A9_CHANNEL_I_CFG, ++ A9_CHANNEL_I_INTERVAL, A9_CHANNEL_I_CURSOR, A9_CHANNEL_I_DESC, ++}; ++ ++/* Snapshot indices into a9_spi0_ch3_offsets. */ ++enum { ++ A9_CH3_I_C3, A9_CH3_I_ROUTE, A9_CH3_I_RATE, A9_CH3_I_C3C, A9_CH3_I_DESC, ++ A9_CH3_I_8D0, A9_CH3_I_8D4, ++}; ++ ++/* One idle-poll slice: C3 as the slice ended, then SCM state and +0x8d0. */ ++struct a9_aop_ch3_slice { ++ u64 ns; /* since the stop store */ ++ u32 c3, scm_lo, scm_hi, fault; ++}; ++ ++/* Wake12 read-only scheduler evidence for an additional-channel stop poll. */ ++struct a9_aop_channel_diag_slice { ++ u32 cfg, interval, cursor, fault_8d4; ++}; ++ ++struct a9_aop_spi0_ch3 { ++ u32 before[A9_SPI0_CH3_REGS], after[A9_SPI0_CH3_REGS]; ++ u32 stop, idle, class0, class1, ack; /* words written / polled */ ++ int poll; /* idle-bit poll result */ ++ bool run_before; /* C3 bit 0 before the stop */ ++ unsigned int slices; /* idle-poll slices used */ ++ struct a9_aop_ch3_slice slice[A9_SPI0_CH3_SLICES]; ++ u64 idle_us; /* stop store to the idle read */ ++ int error; ++ bool ran; /* the guarded teardown executed */ ++ const char *failed; /* distinct refusal reason, or NULL */ ++ /* Wake15: stop latency after the controller reset and the peers' state. */ ++ u64 stop_ns, reset_to_stop_us; ++ u32 peers_run, peers_ticking; /* masks of the other 15 channels */ ++ bool stable; /* accepted stopped-without-idle identity */ ++ const char *stable_why; /* first failed fingerprint condition, NULL if stable */ ++ /* Pre-RUN window after the image copy; a refusal also sets failed. */ ++ unsigned int samples; ++ u64 elapsed_ms; ++ bool window_done, reasserted; ++ unsigned int reassert_sample; ++ u64 reassert_ms; ++ u32 reassert[A9_SPI0_CH3_REGS]; ++}; ++ ++struct a9_aop_spi0; ++ ++struct a9_aop_spi0_channel_op { ++ struct a9_aop_spi0 *spi0; ++ struct a9_aop_spi0_ch3 *state; ++ unsigned int channel; ++}; ++ ++/* ++ * AOP interrupt controller, wrapper offsets (ds-aop-spi0-ch3-round4.md ++ * section 3). IRQ 0x10014 is group 1, source 0x14: bank 0, bit 20. The ++ * group-1 disable/enable banks are write-one commands whose reads return ++ * the mask, 1 = disabled: the sleep command saves +0xa00..+0xa08 (0102c5ce) ++ * before masking all (0102c5da), the restore writes saved to +0xa00 and ++ * ~saved to +0xa80 (0102c9e6/0102c9f4), and aop_interrupt_write_enabled ++ * reads the target bank for the previous state (0102c9c0). So the disable ++ * is judged by bit 20 of the +0xa00 readback (wake6: ffffffff before the ++ * store, every source already masked by the quiesce). +0x81c only reports ++ * an unmasked pending source: the sleep path masks all, writes group 7 ++ * +0xc020/+0xc024 = 2 and spins until +0x81c reads 0 (0102c654), so ++ * all-ones there means the AP is not reading the INTC. +0x808 ends 0 after ++ * the event-3 pulse. +0x900 is the group-1 raise (0102c7c6) and +0x980 its ++ * pre-handler clear (0102c6f2); the firmware never reads them, so their ++ * readbacks are logged only. No register shows a masked source pending. ++ */ ++#define A9_INTC_RESET 0x808U /* event-3 pulse 1 then 0 (0102c592) */ ++#define A9_INTC_CURRENT 0x81cU /* current unmasked source */ ++#define A9_INTC_RAISE 0x900U /* group 1 raise, 0102c7c6 */ ++#define A9_INTC_ACK 0x980U /* group 1 pre-handler ack/clear, 0102c6f2 */ ++#define A9_INTC_DISABLE 0xa00U /* group 1 disable, store at 0102c9ce */ ++#define A9_INTC_ENABLE 0xa80U /* group 1 enable; reads the same mask */ ++#define A9_INTC_G7_DISABLE 0xc020U /* group 7 disable, 2 after sleep */ ++#define A9_INTC_SPI0_SCM 0x00100000U /* IRQ 0x10014 */ ++#define A9_INTC_ALL_ONES 0xffffffffU ++#define A9_INTC_REGS 10 ++ ++static const u32 a9_intc_offsets[A9_INTC_REGS] = { ++ A9_INTC_RESET, A9_INTC_CURRENT, A9_INTC_RAISE, A9_INTC_ACK, ++ A9_INTC_DISABLE, A9_INTC_DISABLE + 4, A9_INTC_DISABLE + 8, ++ A9_INTC_ENABLE, A9_INTC_G7_DISABLE, A9_INTC_G7_DISABLE + 4, ++}; ++ ++/* Snapshot indices into a9_intc_offsets. */ ++enum { ++ A9_INTC_I_RESET, A9_INTC_I_CURRENT, A9_INTC_I_RAISE, A9_INTC_I_ACK, ++ A9_INTC_I_MASK0, A9_INTC_I_MASK1, A9_INTC_I_MASK2, A9_INTC_I_ENABLE0, ++ A9_INTC_I_G7_0, A9_INTC_I_G7_1, ++}; ++ ++/* When the AP snapshots the INTC words. */ ++enum { ++ A9_INTC_AT_PRE_DISABLE, A9_INTC_AT_POST_DISABLE, A9_INTC_AT_PRE_ACK, ++ A9_INTC_AT_POST_ACK, A9_INTC_AT_N, ++}; ++ ++static inline const char *a9_intc_at_name(unsigned int at) ++{ ++ static const char * const names[] = { ++ "pre-disable", "post-disable", "pre-ack", "post-ack", ++ }; ++ ++ return at < A9_INTC_AT_N ? names[at] : "unknown"; ++} ++ ++/* "all-ones" if +0x81c reads ffffffff, which a live INTC cannot report. */ ++static inline const char *a9_intc_view(const u32 *state) ++{ ++ return state[A9_INTC_I_CURRENT] == A9_INTC_ALL_ONES ? "all-ones" : "intc"; ++} ++/* ++ * Read only, never written: the SCM state (also per idle-poll slice) and, ++ * around the acknowledge, the C3 interval pair. ++ */ ++#define A9_SPI0_SCM_LO 0x00cU ++#define A9_SPI0_SCM_HI 0x010U ++#define A9_SPI0_CH3_IVAL 0x290U ++#define A9_SPI0_CH3_CURSOR 0x294U ++#define A9_SPI0_SCM_REGS 4 ++ ++static const u32 a9_spi0_scm_offsets[A9_SPI0_SCM_REGS] = { ++ A9_SPI0_SCM_LO, A9_SPI0_SCM_HI, A9_SPI0_CH3_IVAL, A9_SPI0_CH3_CURSOR, ++}; ++ ++struct a9_aop_intc { ++ u32 state[A9_INTC_AT_N][A9_INTC_REGS]; /* around each store */ ++ unsigned int states; /* bit n: state[n] was read */ ++ u32 scm[2][A9_SPI0_SCM_REGS]; /* before / after the ack */ ++ unsigned int scm_at; /* scm[] row the guarded read fills */ ++ bool disabled, acked; ++ bool reset_done; ++ u32 reset_asserted, reset_after; ++ const char *failed; /* distinct refusal reason, or NULL */ ++}; ++ ++/* Read-only milestones. Wake10 gates the global SCM/fault words, not channels. */ ++struct a9_aop_spi0_trace { ++ u32 control, clock, scm[A9_SPI0_SCM_REGS], ch3[A9_SPI0_CH3_REGS]; ++ u32 channel[A9_SPI0_CHANNELS][A9_SPI0_CHANNEL_REGS]; ++}; ++ ++/* Wake9's second, post-channel-stop replay of AOPSPIMaster_reset. */ ++struct a9_aop_spi0_post_reset { ++ u32 before[A9_SPI0_REGS], after[A9_SPI0_REGS]; ++ u32 saved, asserted, polled; ++ int error; ++ bool ran; ++ const char *failed; ++}; ++ ++/* ++ * First failed readback after the teardown, or NULL if all hold. C3C bit 5 ++ * is a pulse: at 19:50 the write of 0x00200029 read back 0x00200009 while ++ * +0x8d0 bit 6 went 1 -> 0, so the acknowledge is judged by bit 6 alone and ++ * the C3C readback is only logged. ++ */ ++static inline const char *a9_spi0_ch3_verify(const u32 *after, bool stable) ++{ ++ if (after[A9_CH3_I_C3] & A9_SPI0_CH3_RUN) ++ return "failed-ch3-teardown-run"; ++ if (after[A9_CH3_I_C3] & A9_SPI0_CH3_IRQ0) ++ return "failed-ch3-teardown-class0"; ++ if (after[A9_CH3_I_C3] & A9_SPI0_CH3_IRQ1) ++ return "failed-ch3-teardown-class1"; ++ if (!(after[A9_CH3_I_C3] & A9_SPI0_CH3_IDLE) && !stable) ++ return "failed-ch3-teardown-idle"; ++ if (after[A9_CH3_I_DESC]) ++ return "failed-ch3-teardown-desc"; ++ if (after[A9_CH3_I_8D0] & A9_SPI0_CH3_PENDING) ++ return "failed-ch3-teardown-pending"; ++ return NULL; ++} ++ ++/* ++ * Has firmware run since the last SoC reset? auto-boot alone cannot say: ++ * the outgoing aop_quiesce=1 halt clears it, exactly like a DFU cold start. ++ * The previous kernel's reserved-RAM shutdown transcript says whether its ++ * firmware ran. That DRAM can outlive a DFU; wrapper +0x4020 outbox enable ++ * cannot: set after reset (12/12 DFU cold snapshots), cleared by the first ++ * store of every Linux stop. No SPI0 access is needed to decide. ++ */ ++enum a9_spi0_prev_state { ++ A9_SPI0_PREV_MISSING, /* no transcript magic, or none reserved */ ++ A9_SPI0_PREV_INVALID, /* magic present, header/checksum rejected */ ++ A9_SPI0_PREV_VALID, ++}; ++ ++struct a9_spi0_prev { ++ enum a9_spi0_prev_state state; ++ bool hello; /* "SHUTDOWN begin ... hello=1": its firmware ran */ ++ bool halted; /* "QUIESCE step=complete ... halted=1" + steps bit */ ++ bool carried; /* "SPI0-CARRY retained=1": ran earlier in the chain */ ++ u32 steps; /* "QUIESCE step=complete ... steps=0x..." */ ++}; ++ ++enum a9_spi0_reason { ++ /* Run the reset. */ ++ A9_SPI0_LIVE_AUTO_BOOT, /* auto-boot 1 before the stop */ ++ A9_SPI0_PREV_HALTED, /* previous kernel quiesced and halted it */ ++ A9_SPI0_PREV_LIVE, /* previous kernel's firmware ran, no halt */ ++ A9_SPI0_PREV_CARRIED, /* an earlier kernel in the chain ran it */ ++ /* Skip: the block is not touched. */ ++ A9_SPI0_FRESH, /* no transcript, auto-boot 0: DFU cold */ ++ A9_SPI0_PREV_BAD, /* transcript present but corrupt */ ++ A9_SPI0_PREV_NO_RUN, /* transcript: nothing ran in the chain */ ++ A9_SPI0_STALE, /* run evidence, wrapper at reset state */ ++}; ++ ++static inline const char *a9_spi0_reason_name(enum a9_spi0_reason reason) ++{ ++ static const char * const names[] = { ++ "live-auto-boot", "prev-quiesce-halted", "prev-live", "prev-carried", ++ "fresh-instance", "prev-transcript-invalid", "prev-no-run", ++ "stale-transcript", ++ }; ++ ++ return reason <= A9_SPI0_STALE ? names[reason] : "unknown"; ++} ++ ++static inline const char *a9_spi0_prev_name(enum a9_spi0_prev_state state) ++{ ++ return state == A9_SPI0_PREV_VALID ? "valid" : ++ state == A9_SPI0_PREV_INVALID ? "invalid" : "missing"; ++} ++ ++static inline bool a9_spi0_reason_runs(enum a9_spi0_reason reason) ++{ ++ return reason <= A9_SPI0_PREV_CARRIED; ++} ++ ++/* ++ * Release policy: a reason that would run the reload is allowed by default ++ * only for prev-quiesce-halted, the one state device-proven end to end. A ++ * skip (fresh, bad record, nothing ran) is allowed only while the wrapper is ++ * still at its reset state; stale records skip as before. Untested lifts both. ++ */ ++static inline bool a9_spi0_policy_allows(enum a9_spi0_reason reason, bool outbox_enabled, ++ bool untested) ++{ ++ if (untested || reason == A9_SPI0_PREV_HALTED || reason == A9_SPI0_STALE) ++ return true; ++ if (a9_spi0_reason_runs(reason)) ++ return false; ++ /* ++ * A skip means "nothing ran since the SoC reset", which only holds if the ++ * wrapper is still at its reset state (+0x4020 outbox enable set, 12/12 DFU ++ * cold starts). Without it some earlier kernel stopped the firmware and its ++ * record is lost: a cold load would land on a halted, never-torn-down AOP. ++ */ ++ return outbox_enabled; ++} ++ ++/* Token search inside one transcript line, which holds no NUL. */ ++static inline bool a9_spi0_line_has(const char *line, size_t length, ++ const char *token, size_t n, ++ const char **at) ++{ ++ size_t i; ++ ++ for (i = 0; n <= length && i <= length - n; i++) { ++ if (!memcmp(line + i, token, n)) { ++ *at = line + i + n; ++ return true; ++ } ++ } ++ return false; ++} ++ ++#define a9_spi0_has(line, length, token, at) \ ++ a9_spi0_line_has(line, length, token, sizeof(token) - 1, at) ++ ++static inline bool a9_spi0_line_starts(const char *line, size_t length, ++ const char *prefix, size_t n) ++{ ++ return length >= n && !memcmp(line, prefix, n); ++} ++ ++#define a9_spi0_starts(line, length, prefix) \ ++ a9_spi0_line_starts(line, length, prefix, sizeof(prefix) - 1) ++ ++static inline u32 a9_spi0_hex(const char *p, const char *end) ++{ ++ u32 value = 0; ++ ++ for (; p < end; p++) { ++ if (*p >= '0' && *p <= '9') ++ value = value << 4 | (u32)(*p - '0'); ++ else if (*p >= 'a' && *p <= 'f') ++ value = value << 4 | (u32)(*p - 'a' + 10); ++ else ++ break; ++ } ++ return value; ++} ++ ++/* Our own checksummed transcript; only whole-line prefixes count. */ ++static inline void a9_spi0_prev_parse(struct a9_spi0_prev *p, ++ const char *text, size_t length) ++{ ++ const char *end = text + length, *line, *next, *at; ++ size_t n; ++ ++ for (line = text; line < end; line = next < end ? next + 1 : end) { ++ next = memchr(line, '\n', end - line); ++ if (!next) ++ next = end; ++ n = next - line; ++ if (a9_spi0_starts(line, n, "SHUTDOWN begin ") && ++ a9_spi0_has(line, n, " hello=1 ", &at)) ++ p->hello = true; ++ if (a9_spi0_starts(line, n, "QUIESCE step=complete ")) { ++ if (a9_spi0_has(line, n, " steps=0x", &at)) ++ p->steps = a9_spi0_hex(at, next); ++ p->halted = a9_spi0_has(line, n, " halted=1 ", &at) && ++ (p->steps & (1U << A9_HO_STEP_HALTED)); ++ } ++ if (a9_spi0_starts(line, n, "SPI0-CARRY retained=1 ")) ++ p->carried = true; ++ } ++} ++ ++/* ++ * Skips unless there is positive evidence: running the reset on a fresh ++ * block would be the first AP access to SPI0 with nothing retained to ++ * clear (and ps_spi0 may gate it), while a skip on retained state only ++ * repeats the known, non-hanging accel STARTEP crash. ++ */ ++static inline enum a9_spi0_reason a9_spi0_decide(bool auto_boot, bool outbox_enabled, ++ const struct a9_spi0_prev *p) ++{ ++ if (auto_boot) ++ return A9_SPI0_LIVE_AUTO_BOOT; ++ if (p->state == A9_SPI0_PREV_MISSING) ++ return A9_SPI0_FRESH; ++ if (p->state != A9_SPI0_PREV_VALID) ++ return A9_SPI0_PREV_BAD; ++ if (!p->hello && !p->carried) ++ return A9_SPI0_PREV_NO_RUN; ++ if (outbox_enabled) ++ return A9_SPI0_STALE; ++ if (p->hello) ++ return p->halted ? A9_SPI0_PREV_HALTED : A9_SPI0_PREV_LIVE; ++ return A9_SPI0_PREV_CARRIED; ++} ++ ++struct a9_aop_spi0 { ++ void __iomem *base; /* mapped only with aop_spi0_reset=1 */ ++ u32 before[A9_SPI0_REGS], after[A9_SPI0_REGS]; ++ u32 saved; /* control word before the reset */ ++ u32 asserted; /* saved & ~0x101 | 0x100 */ ++ u32 polled; /* control word when the polls ended */ ++ u32 prerun; /* +0x8d0 just before RUN */ ++ u32 prerun_8d4, prerun_scm_lo, prerun_scm_hi; ++ u32 pmgr_before, pmgr_after; /* AP PMGR ps_spi0 raw, read-only */ ++ int pmgr_before_errno, pmgr_after_errno; ++ int error; ++ bool ran, reasserted, copy_faulted; ++ const char *gate_failed; ++ const char *skipped; ++ struct a9_spi0_prev prev; /* parsed at probe from the transcript */ ++ u32 auto_boot, outbox; /* boot snapshot, before the stop */ ++ enum a9_spi0_reason reason; ++ bool decided; ++ struct a9_aop_spi0_ch3 ch3; /* after a clean reset only */ ++ /* Wake11 reuses one bounded scratch record for each additional channel. */ ++ struct a9_aop_spi0_ch3 channel; ++ u32 teardown_selected, teardown_done; ++ unsigned int teardown_failed_channel; ++ struct a9_aop_spi0_post_reset post_reset; /* after the stop latches SCM */ ++ struct a9_aop_intc intc; /* IRQ 0x10014 around the reset */ ++ struct a9_aop_spi0_trace trace; /* latest read-only milestone */ ++ /* Appended so every pre-wake12 field, including ch3, keeps its layout. */ ++ struct a9_aop_channel_diag_slice channel_diag[A9_SPI0_CH3_SLICES]; ++ /* Wake13: exact phosphorus stopped-without-idle exception, by channel. */ ++ u32 teardown_stable; ++ /* Wake15: channel 3 shares the sampled diagnostics; reset completion time. */ ++ struct a9_aop_channel_diag_slice ch3_diag[A9_SPI0_CH3_SLICES]; ++ u64 reset_done_ns; ++}; ++ ++/* 0102f2f4: bits 8 and 0 clear; the reset write adds bit 8. */ ++static inline u32 a9_spi0_base_word(u32 saved) ++{ ++ return saved & ~(A9_SPI0_RESET | A9_SPI0_ENABLE); ++} ++ ++#endif +diff --git a/drivers/soc/apple/aop.c b/drivers/soc/apple/aop.c +index e8ae90098..8bcab7651 100644 +--- a/drivers/soc/apple/aop.c ++++ b/drivers/soc/apple/aop.c +@@ -1,19 +1,28 @@ + // SPDX-License-Identifier: GPL-2.0-only OR MIT + /* +- * Apple T8010 Always-On Processor (AOP) ++ * Apple S8000 and T8010 Always-On Processor (AOP) + * + * The AOP runs RTKit firmware that the operating system loads: a main image + * into the AOP's SRAM and an external segment into system memory, with boot +- * arguments filled in. Its services talk AFK/EPIC over shared ring buffers on +- * the RTKit application endpoints, as on later SoCs (see the Asahi Linux AOP ++ * arguments filled in. On the S8000 (A9) the AOP sees its SRAM through a window ++ * that has to be set up first, and its firmware takes more boot arguments. ++ * Its services talk AFK/EPIC over shared ring buffers on the RTKit ++ * application endpoints, as on later SoCs (see the Asahi Linux AOP + * driver). Each service with a device tree child node of the same name gets a + * child platform device; its driver receives the service's reports through + * apple_aop_listen() and sends requests with apple_aop_call(). + * +- * The firmware cannot be restarted once stopped, so the AOP starts once per +- * boot and the driver cannot be unbound or unloaded after that. ++ * J71t can reload after a certified quiesce/halt/drain across kexec. Other ++ * retained states are refused. Unbind/unload after starting remains forbidden. + */ + ++#include ++#include ++#include ++#include ++#include ++#include ++#include "aop-warm-spi0.h" + #include + #include + #include +@@ -41,6 +50,14 @@ + #define APPLE_AOP_IRQ_MASK_ALL 0x1111 + #define APPLE_AOP_IRQ_INBOX BIT(0) + #define APPLE_AOP_IRQ_OUTBOX BIT(12) ++/* S8000: the window through which the AOP sees its SRAM */ ++#define APPLE_AOP_REMAP_PA_LO 0x08 ++#define APPLE_AOP_REMAP_PA_HI 0x10 ++#define APPLE_AOP_REMAP_IOVA_LO 0x18 ++#define APPLE_AOP_REMAP_IOVA_HI 0x20 ++#define APPLE_AOP_REMAP_SIZE 0x28 ++#define APPLE_AOP_REMAP_RESERVED 0x30 ++#define APPLE_AOP_REMAP_ENABLE 0x38 + + #define APPLE_AOP_CONTROL_AUTO_BOOT 0x0 + +@@ -54,6 +71,13 @@ + #define APPLE_AOP_BOOTARG_ECID APPLE_AOP_FOURCC('E', 'C', 'I', 'D') + #define APPLE_AOP_BOOTARG_NONC APPLE_AOP_FOURCC('N', 'O', 'N', 'C') + #define APPLE_AOP_BOOTARG_EC0P APPLE_AOP_FOURCC('E', 'C', '0', 'p') ++#define APPLE_AOP_BOOTARG_AOPT APPLE_AOP_FOURCC('A', 'O', 'P', 't') ++#define APPLE_AOP_BOOTARG_NCAL APPLE_AOP_FOURCC('n', 'C', 'a', 'l') ++#define APPLE_AOP_BOOTARG_WRAD APPLE_AOP_FOURCC('W', 'r', 'A', 'd') ++#define APPLE_AOP_BOOTARG_SOC APPLE_AOP_FOURCC('S', 'O', 'C', '_') ++#define APPLE_AOP_BOOTARG_SOCR APPLE_AOP_FOURCC('S', 'O', 'C', 'R') ++#define APPLE_AOP_BOOTARG_STKG APPLE_AOP_FOURCC('S', 'T', 'K', 'G') ++#define APPLE_AOP_MAX_BOOTARGS 10 + + #define APPLE_AOP_HALT_TIMEOUT_US 10000 + +@@ -78,7 +102,9 @@ + #define AFK_OPC_GET_BUF_ACK 0xa1 + #define AFK_OPC_SEND 0xa2 + #define AFK_OPC_START 0xa3 +-#define AFK_OPC_SHUTDOWN_ACK 0xc1 ++#define AFK_OPC_SHUTDOWN 0xa4 ++#define AFK_OPC_SHUTDOWN_ACK 0x87 ++#define AFK_OPC_HIBERNATE_ACK 0xc3 + + #define AFK_BLOCK_STEP 0x40 + #define AFK_MAX_BUFFER SZ_1M +@@ -100,6 +126,12 @@ + #define APPLE_AOP_MAX_CALLS 8 + #define APPLE_AOP_CALL_TIMEOUT msecs_to_jiffies(1000) + ++/* Service properties */ ++#define APPLE_AOP_PROPERTY_MAX 64 ++#define APPLE_AOP_PROP_CALIBRATION 0x3d ++#define APPLE_AOP_CALIBRATION_PARTIAL 1 ++#define APPLE_AOP_CALIBRATION_COMPLETE 2 ++ + struct afk_qe_header { + __le32 magic; + __le32 size; +@@ -160,6 +192,8 @@ struct apple_aop_ep { + + struct mutex lock; /* tx ring, seq and calls */ + u16 seq; ++ bool initialized; ++ struct completion stopped; + struct apple_aop_call *calls[APPLE_AOP_MAX_CALLS]; + }; + +@@ -182,12 +216,25 @@ struct apple_aop_register_work { + struct apple_aop_service service; + }; + ++struct apple_aop_hw { ++ /* Set up the AOP's window onto its SRAM before starting it */ ++ bool sram_window; ++ /* A fresh NONC boot argument */ ++ bool nonce; ++ /* AOPt, SOC_, SOCR, WrAd, STKG and nCal boot arguments */ ++ bool soc_bootargs; ++}; ++ + struct apple_aop { + struct device *dev; ++ const struct apple_aop_hw *hw; + void __iomem *akf; + void __iomem *control; + void __iomem *sram; ++ resource_size_t akf_base; ++ resource_size_t sram_base; + resource_size_t sram_size; ++ u32 target, chip_id, chip_revision; + struct apple_rtkit *rtk; + + void *ext; +@@ -202,6 +249,16 @@ struct apple_aop { + struct mutex children_lock; /* children, removing */ + struct list_head children; + bool removing; ++ u64 accel_reports; ++ int shutdown_error; ++ /* J71t only; all other boards retain their original start/stop path. */ ++ bool warm, hello, iop_crashed, force_halted, force_drained; ++ void __iomem *warm_wrapper; ++ struct device_node *domains[5], *spi0_pmgr; ++ struct a9_aop_spi0 spi0; ++ const char *experiment_step; ++ struct a9_sl_header *shutdown_log; ++ char *shutdown_text; + }; + + static bool apple_aop_is_started(struct apple_aop *aop) +@@ -223,6 +280,409 @@ static bool apple_aop_halt(struct apple_aop *aop) + APPLE_AOP_HALT_TIMEOUT_US); + } + ++static bool a9_aop_ramoops_ready(void) ++{ ++ struct console *con; ++ bool ready = false; ++ int cookie; ++ ++ cookie = console_srcu_read_lock(); ++ for_each_console_srcu(con) { ++ if (!strcmp(con->name, "ramoops") && ++ (console_srcu_read_flags(con) & (CON_ENABLED | CON_SUSPENDED)) == ++ CON_ENABLED) { ++ ready = true; ++ break; ++ } ++ } ++ console_srcu_read_unlock(cookie); ++ return ready; ++} ++ ++/* SPDX-License-Identifier: GPL-2.0-only */ ++/* J71t warm reload integration. SPI0 machinery is the release6 wake16 code. */ ++#define AOP_DOMAINS 5 ++#define AOP_CPU_CONTROL 0x44 ++#define AOP_CPU_START BIT(4) ++#define AOP_OUTBOX_CONTROL 0x4020 ++#define AOP_INBOX_CONTROL 0x4008 ++#define AOP_OUTBOX_ENABLE BIT(0) ++#define AOP_MBOX_FAULTS GENMASK(19, 18) ++#define cold_defer 1 ++#define spi0_untested false ++ ++static u32 aop_rd(struct apple_aop *aop, u32 offset) ++{ ++ return readl(aop->warm_wrapper + offset); ++} ++ ++static bool a9_aop_force_stopped(struct apple_aop *aop) ++{ ++ return !(aop_rd(aop, AOP_CPU_CONTROL) & AOP_CPU_START) && ++ !readl(aop->control) && ++ !(aop_rd(aop, AOP_OUTBOX_CONTROL) & (AOP_OUTBOX_ENABLE | AOP_MBOX_FAULTS)) && ++ !(aop_rd(aop, AOP_INBOX_CONTROL) & AOP_MBOX_FAULTS); ++} ++ ++struct aop_warm_write { void __iomem *target; u32 value; }; ++static void aop_warm_store(void *data) ++{ ++ struct aop_warm_write *w = data; ++ ++ writel(w->value, w->target); ++ readl(w->target); ++} ++ ++static int aop_wr(struct apple_aop *aop, u32 offset, u32 value) ++{ ++ struct apple_pmgr_observation states[AOP_DOMAINS]; ++ struct aop_warm_write w = { aop->warm_wrapper + offset, value }; ++ ++ if ((aop_rd(aop, AOP_INBOX_CONTROL) | aop_rd(aop, AOP_OUTBOX_CONTROL)) & ++ AOP_MBOX_FAULTS) ++ return -EIO; ++ dev_info(aop->dev, "COLD-W step=%s bank=wrapper offset=%04x before=%08x value=%08x\n", ++ aop->experiment_step ?: "warm-stop", offset, readl(w.target), value); ++ { ++ int ret = apple_pmgr_observe(aop->domains, AOP_DOMAINS, states, aop_warm_store, &w); ++ ++ dev_info(aop->dev, "COLD-R offset=%04x after=%08x\n", offset, readl(w.target)); ++ return ret; ++ } ++} ++ ++static int a9_aop_cold_checkpoint_noflush(struct apple_aop *aop, const char *step) ++{ ++ if (!a9_aop_ramoops_ready()) ++ return -ENODEV; ++ dev_info(aop->dev, "COLD checkpoint=%s flush=skipped\n", step); ++ return 0; ++} ++ ++static int a9_aop_cold_checkpoint(struct apple_aop *aop, const char *step) ++{ ++ int ret = a9_aop_cold_checkpoint_noflush(aop, step); ++ ++ if (ret) ++ return ret; ++ if (!pr_flush(1000, false)) ++ return -ETIMEDOUT; ++ mb(); ++ return 0; ++} ++ ++#include "aop-warm-spi0.c.h" ++static int apple_aop_send(struct apple_aop_ep *ep, u8 opcode, u64 payload); ++ ++/* No cmdline override for untested states: default is the release6 policy. */ ++static atomic_t aop_warm_attempted = ATOMIC_INIT(0); ++ ++static int apple_aop_warm_stop(struct apple_aop *aop) ++{ ++ unsigned int fifo, i; ++ u32 offset, val; ++ int ret; ++ ++ ret = aop_wr(aop, 0x4020, aop_rd(aop, 0x4020) & ~BIT(0)); ++ if (!ret) ++ ret = aop_wr(aop, 0x4000, 0x1111); ++ if (!ret) ++ ret = aop_wr(aop, 0xc00, 0x1111); ++ if (!ret) ++ ret = aop_wr(aop, 0x44, aop_rd(aop, 0x44) & ~BIT(4)); ++ if (ret) ++ return ret; ++ writel(0, aop->control); ++ msleep(10); ++ if (!a9_aop_force_stopped(aop)) ++ return -EIO; ++ aop->force_halted = true; ++ for (fifo = 0; fifo < 2; fifo++) { ++ offset = fifo ? 0x4020 : 0x4008; ++ for (i = 0; i < 64; i++) { ++ val = aop_rd(aop, offset); ++ if (!a9_aop_force_stopped(aop) || (val & AOP_MBOX_FAULTS)) ++ return -EIO; ++ if (val & BIT(17)) ++ break; ++ readq(aop->warm_wrapper + (fifo ? 0x4038 : 0x4018)); ++ } ++ if (!(aop_rd(aop, offset) & BIT(17))) ++ return -ETIMEDOUT; ++ } ++ aop->force_drained = true; ++ return 0; ++} ++ ++static void apple_aop_warm_put_node(void *node) ++{ ++ of_node_put(node); ++} ++ ++static void apple_aop_warm_unmap(void *p) ++{ ++ memunmap(p); ++} ++ ++static int apple_aop_warm_prepare(struct apple_aop *aop) ++{ ++ struct device *dev = aop->dev; ++ struct device_node *np = dev->of_node, *mem; ++ struct apple_pmgr_observation states[AOP_DOMAINS]; ++ struct a9_sl_header header; ++ struct resource res; ++ char *text; ++ u32 auto_boot, outbox; ++ unsigned int i; ++ int ret; ++ ++ if (!of_machine_is_compatible("apple,j71t") || !aop->hw->sram_window) ++ return 0; ++ /* Missing the new DT is not permission to run a retained instance. */ ++ mem = of_parse_phandle(np, "memory-region", 0); ++ if (!mem) ++ return dev_err_probe(dev, -EINVAL, "warm reload requires the matching J71t DT\n"); ++ ret = of_address_to_resource(mem, 0, &res); ++ of_node_put(mem); ++ if (ret || resource_size(&res) != A9_HO_PAGE || res.start != 0x8583fc000ULL || ++ aop->akf_base != A9_SPI0_WRAPPER_PA) ++ return -EINVAL; ++ aop->shutdown_log = memremap(res.start, A9_HO_PAGE, MEMREMAP_WB); ++ if (!aop->shutdown_log) ++ return -ENOMEM; ++ ret = devm_add_action_or_reset(dev, apple_aop_warm_unmap, aop->shutdown_log); ++ if (ret) ++ return ret; ++ /* Keep release6's transcript location and validation ABI. */ ++ aop->shutdown_log = (void *)aop->shutdown_log + A9_HO_LOG_OFFSET; ++ aop->shutdown_text = (char *)(aop->shutdown_log + 1); ++ memcpy(&header, aop->shutdown_log, sizeof(header)); ++ text = devm_kmemdup(dev, aop->shutdown_text, A9_SL_CAPACITY, GFP_KERNEL); ++ if (!text) ++ return -ENOMEM; ++ if (header.magic == A9_SL_MAGIC) { ++ aop->spi0.prev.state = A9_SPI0_PREV_INVALID; ++ if (a9_sl_valid(&header, text)) { ++ aop->spi0.prev.state = A9_SPI0_PREV_VALID; ++ a9_spi0_prev_parse(&aop->spi0.prev, text, header.length); ++ for (i = 0; i < header.length;) { ++ char *next = memchr(text + i, '\n', header.length - i); ++ unsigned int n = next ? next - text - i : header.length - i; ++ ++ dev_info(dev, "AOP-PREV %.*s\n", n, text + i); ++ i += n + 1; ++ } ++ } ++ } ++ aop->warm_wrapper = devm_ioremap(dev, aop->akf_base, 0x1c000); ++ if (!aop->warm_wrapper) ++ return -ENOMEM; ++ for (i = 0; i < AOP_DOMAINS; i++) { ++ aop->domains[i] = of_parse_phandle(np, "apple,observe-domains", i); ++ if (!aop->domains[i]) ++ return -EINVAL; ++ ret = devm_add_action_or_reset(dev, apple_aop_warm_put_node, aop->domains[i]); ++ if (ret) ++ return ret; ++ } ++ aop->spi0_pmgr = of_parse_phandle(np, "apple,spi0-domain", 0); ++ if (!aop->spi0_pmgr) ++ return -EINVAL; ++ ret = devm_add_action_or_reset(dev, apple_aop_warm_put_node, aop->spi0_pmgr); ++ if (ret) ++ return ret; ++ /* Check all domains before reading the wrapper; no power transitions. */ ++ ret = apple_pmgr_observe(aop->domains, AOP_DOMAINS, states, a9_aop_spi0_pmgr_on, &(bool){false}); ++ if (ret) ++ return dev_err_probe(dev, ret, "AOP domain guard refused\n"); ++ auto_boot = readl(aop->control); ++ outbox = aop_rd(aop, AOP_OUTBOX_CONTROL); ++ ret = a9_aop_spi0_policy(aop, auto_boot, outbox); ++ if (ret) ++ return ret; /* Before first halt, mask, SRAM or SPI0 write. */ ++ /* Orchard's old halt leaves +0x4020 enabled. On a known kexec, ++ * that alone cannot certify the cold wrapper baseline. Release6's ++ * wrapper discriminator was tied to its own outgoing stop routine. ++ */ ++ if (of_property_read_bool(of_chosen, "linux,booted-from-kexec") && ++ a9_spi0_decide(auto_boot, outbox & BIT(0), &aop->spi0.prev) != A9_SPI0_PREV_HALTED) { ++ dev_info(dev, "SPI0 policy decision=refuse reason=uncertified-kexec auto_boot=%u out4020=%08x: block not touched\n", ++ auto_boot, outbox); ++ return -EPERM; ++ } ++ aop->warm = true; ++ aop->spi0.reason = a9_spi0_decide(auto_boot, outbox & BIT(0), &aop->spi0.prev); ++ if (!a9_spi0_reason_runs(aop->spi0.reason)) { ++ /* Original cold path: no halt, drain, reset, or SPI0 access. */ ++ dev_info(dev, "SPI0 reset decision=skip reason=%s: block not touched\n", ++ a9_spi0_reason_name(aop->spi0.reason)); ++ return 0; ++ } ++ if (atomic_xchg(&aop_warm_attempted, 1)) ++ return dev_err_probe(dev, -EPERM, "refused-second-experiment-attempt\n"); ++ if (!a9_aop_ramoops_ready()) ++ return dev_err_probe(dev, -ENODEV, "warm reload requires ramoops console\n"); ++ aop->spi0.base = devm_ioremap(dev, A9_SPI0_PA, A9_SPI0_SIZE); ++ if (!aop->spi0.base) ++ return -ENOMEM; ++ /* Consume certification before the first control write. Reloading the ++ * module after a failure must not reuse it as a second attempt. ++ */ ++ aop->shutdown_log->magic = 0; ++ wmb(); ++ ret = apple_aop_warm_stop(aop); ++ if (!ret) ++ ret = a9_aop_spi0_prereset(aop, auto_boot, outbox); ++ if (ret) ++ dev_err(dev, "warm reload refused errno=%d; no copy/RUN, recover with DFU\n", ret); ++ return ret; ++} ++ ++static int apple_aop_warm_release(struct apple_aop *aop) ++{ ++ int ret; ++ ++ ret = a9_aop_spi0_recheck(aop); ++ if (!ret) ++ ret = a9_aop_spi0_trace(aop, "after-window"); ++ if (!ret) ++ ret = a9_aop_intc_ack(aop); ++ if (!ret) ++ ret = a9_aop_intc_reset(aop); ++ if (!ret) ++ ret = a9_aop_cold_checkpoint(aop, "before-run"); ++ if (!ret) ++ ret = a9_aop_spi0_gate(aop, "pre-run-final", "FINAL", true); ++ return ret; ++} ++ ++static void apple_aop_warm_hash(struct apple_aop *aop, u8 *digest) ++{ ++ struct sha256_ctx ctx; ++ unsigned int i; ++ ++ sha256_init(&ctx); ++ dma_rmb(); ++ if (aop->ext) ++ sha256_update(&ctx, aop->ext, aop->ext_size); ++ for (i = 0; i < AFK_ENDPOINT_COUNT; i++) ++ if (aop->eps[i].buf) ++ sha256_update(&ctx, aop->eps[i].buf, aop->eps[i].buf_size); ++ sha256_final(&ctx, digest); ++} ++ ++static __printf(2, 3) void apple_aop_warm_log(struct apple_aop *aop, const char *fmt, ...) ++{ ++ struct a9_sl_header *h = aop->shutdown_log; ++ va_list args; ++ char line[512]; ++ size_t n; ++ ++ va_start(args, fmt); ++ n = vscnprintf(line, sizeof(line), fmt, args); ++ va_end(args); ++ dev_info(aop->dev, "%s", line); ++ if (n > h->capacity - h->length) { ++ h->truncated++; ++ return; ++ } ++ memcpy(aop->shutdown_text + h->length, line, n); ++ wmb(); ++ h->checksum = a9_sl_hash(h->checksum, line, n); ++ h->length += n; ++ h->lines++; ++ wmb(); ++} ++ ++static void apple_aop_warm_shutdown(struct apple_aop *aop) ++{ ++ struct a9_sl_header *h = aop->shutdown_log; ++ u8 before[SHA256_DIGEST_SIZE], after[SHA256_DIGEST_SIZE]; ++ unsigned long deadline = jiffies + msecs_to_jiffies(10000), start; ++ unsigned int i, acked = 0; ++ int ret = -EIO; ++ ++ memset(h, 0, sizeof(*h)); ++ h->version = A9_SL_VERSION; ++ h->capacity = A9_SL_CAPACITY; ++ h->checksum = A9_SL_HASH_SEED; ++ h->mode = 1; ++ h->begin_ns = ktime_get_boottime_ns(); ++ wmb(); ++ h->magic = A9_SL_MAGIC; ++ apple_aop_warm_log(aop, "SHUTDOWN begin mode=quiesce hello=%d phase=stopping iop_crashed=%d\n", ++ aop->hello, aop->iop_crashed); ++ if (!aop->hello || aop->iop_crashed || aop->shutdown_error) ++ goto done; ++ /* Rates-zero has stopped reports. Stop one AFK at a time, max 2s ++ * per endpoint and 10s in total, including send time. Never force halt ++ * after a failed ACK. Keep every grant allocated for the entire hook. ++ */ ++ for (i = 0; i < AFK_ENDPOINT_COUNT; i++) { ++ struct apple_aop_ep *ep = &aop->eps[i]; ++ unsigned long limit; ++ ++ if (!ep->initialized) ++ continue; ++ if (time_after_eq(jiffies, deadline)) { ++ ret = -ETIMEDOUT; ++ goto done; ++ } ++ reinit_completion(&ep->stopped); ++ start = jiffies; ++ ret = apple_aop_send(ep, AFK_OPC_SHUTDOWN, 0); ++ if (ret) ++ goto done; ++ limit = time_before(jiffies, deadline) ? min_t(unsigned long, ++ deadline - jiffies, msecs_to_jiffies(2000)) : 0; ++ if (!limit || !wait_for_completion_timeout(&ep->stopped, limit)) { ++ ret = -ETIMEDOUT; ++ goto done; ++ } ++ acked++; ++ apple_aop_warm_log(aop, "QUIESCE step=afk-ack ep=0x%x state=stopped ms=%u\n", ++ ep->index, jiffies_to_msecs(jiffies - start)); ++ } ++ /* release6: complete every AFK stop before disabling endpoint routing. */ ++ if (acked != 17) { ++ ret = -ENOTCONN; ++ goto done; ++ } ++ for (i = 0; i < AFK_ENDPOINT_COUNT; i++) { ++ struct apple_aop_ep *ep = &aop->eps[i]; ++ ++ if (!ep->initialized) ++ continue; ++ /* MGMT STARTEP bit 0 disables routing, no separate stop ACK. */ ++ ret = apple_rtkit_send_message(aop->rtk, 0, ++ (5ULL << 52) | ((u64)ep->index << 32) | 1, NULL, false); ++ if (ret) ++ goto done; ++ } ++ if (acked != 17) { ++ ret = -ENOTCONN; ++ goto done; /* Never certify an incompletely initialized instance. */ ++ } ++ ret = apple_rtkit_shutdown_preserve(aop->rtk); ++ if (ret) ++ goto done; ++ msleep(20); ++ apple_aop_warm_hash(aop, before); ++ msleep(20); ++ apple_aop_warm_hash(aop, after); ++ ret = memcmp(before, after, sizeof(before)) ? -EBUSY : 0; ++ apple_aop_warm_log(aop, "QUIESCE step=ram-stable window_ms=20 status=%d\n", ret); ++ if (ret || aop->iop_crashed) ++ goto done; ++ ret = apple_aop_warm_stop(aop); ++done: ++ apple_aop_warm_log(aop, "QUIESCE step=complete status=%d quiesced=%d halted=%d drained=%d steps=0x%x afk_acks=%u iop_crashed=%d\n", ++ ret, !ret, aop->force_halted, aop->force_drained, ++ !ret ? 0x1bff : 0, acked, aop->iop_crashed); ++ if (ret) ++ dev_err(aop->dev, "quiesce failed; next AOP must refuse, recover with DFU\n"); ++} ++ + /* Firmware loading */ + + struct apple_aop_bootarg { +@@ -232,11 +692,17 @@ struct apple_aop_bootarg { + bool found; + }; + ++static void apple_aop_add_bootarg(struct apple_aop_bootarg *args, unsigned int *count, ++ u32 key, const void *value, u32 size) ++{ ++ args[(*count)++] = (struct apple_aop_bootarg){ .key = key, .value = value, .size = size }; ++} ++ + static int apple_aop_patch_bootargs(struct apple_aop *aop, u8 *image, size_t size, +- struct apple_aop_bootarg *args, int count) ++ struct apple_aop_bootarg *args, unsigned int count) + { + size_t offset, end, pos; +- int i; ++ unsigned int i; + + offset = get_unaligned_le32(image + APPLE_AOP_IMAGE_BOOTARGS_OFFSET); + end = get_unaligned_le32(image + APPLE_AOP_IMAGE_BOOTARGS_SIZE); +@@ -269,13 +735,29 @@ static int apple_aop_patch_bootargs(struct apple_aop *aop, u8 *image, size_t siz + return 0; + } + ++/* S8000: point the AOP's window at its SRAM; the firmware runs from there. */ ++static void apple_aop_map_sram(struct apple_aop *aop) ++{ ++ writel(0, aop->akf + APPLE_AOP_REMAP_IOVA_LO); ++ writel(0, aop->akf + APPLE_AOP_REMAP_IOVA_HI); ++ writel(aop->sram_size & ~0xfffULL, aop->akf + APPLE_AOP_REMAP_SIZE); ++ writel(0, aop->akf + APPLE_AOP_REMAP_RESERVED); ++ writel(lower_32_bits(aop->sram_base), aop->akf + APPLE_AOP_REMAP_PA_LO); ++ writel(upper_32_bits(aop->sram_base), aop->akf + APPLE_AOP_REMAP_PA_HI); ++ writel(1, aop->akf + APPLE_AOP_REMAP_ENABLE); ++} ++ + static int apple_aop_load(struct apple_aop *aop, u64 ecid) + { ++ struct apple_aop_bootarg args[APPLE_AOP_MAX_BOOTARGS]; + const struct firmware *main_fw, *ext_fw; + const char *main_name, *ext_name; + struct device_node *np = aop->dev->of_node; +- u8 prng[32], nonce[8]; +- __le64 ecid_le = cpu_to_le64(ecid), ext_le; ++ u8 prng[32], nonce[8], guard[4], ncal = 0; ++ __le64 ecid_le = cpu_to_le64(ecid), ext_le, wrapper_le; ++ __le32 target_le, chip_le, revision_le; ++ unsigned int count = 0; ++ size_t offset; + u8 *image; + int ret; + +@@ -316,28 +798,58 @@ static int apple_aop_load(struct apple_aop *aop, u64 ecid) + } + + get_random_bytes(prng, sizeof(prng)); +- get_random_bytes(nonce, sizeof(nonce)); +- { +- struct apple_aop_bootarg args[] = { +- { APPLE_AOP_BOOTARG_PRNG, prng, sizeof(prng) }, +- { APPLE_AOP_BOOTARG_ECID, &ecid_le, sizeof(ecid_le) }, +- { APPLE_AOP_BOOTARG_NONC, nonce, sizeof(nonce) }, +- { APPLE_AOP_BOOTARG_EC0P, &ext_le, sizeof(ext_le) }, +- }; +- +- ret = apple_aop_patch_bootargs(aop, image, main_fw->size, args, +- ARRAY_SIZE(args)); ++ apple_aop_add_bootarg(args, &count, APPLE_AOP_BOOTARG_PRNG, &prng, sizeof(prng)); ++ apple_aop_add_bootarg(args, &count, APPLE_AOP_BOOTARG_ECID, &ecid_le, sizeof(ecid_le)); ++ apple_aop_add_bootarg(args, &count, APPLE_AOP_BOOTARG_EC0P, &ext_le, sizeof(ext_le)); ++ if (aop->hw->nonce) { ++ get_random_bytes(nonce, sizeof(nonce)); ++ apple_aop_add_bootarg(args, &count, APPLE_AOP_BOOTARG_NONC, &nonce, sizeof(nonce)); ++ } ++ if (aop->hw->soc_bootargs) { ++ /* A random stack guard with one zero byte */ ++ get_random_bytes(guard, sizeof(guard)); ++ guard[get_random_u32_below(sizeof(guard))] = 0; ++ target_le = cpu_to_le32(aop->target); ++ chip_le = cpu_to_le32(aop->chip_id); ++ revision_le = cpu_to_le32(aop->chip_revision); ++ wrapper_le = cpu_to_le64(aop->akf_base); ++ apple_aop_add_bootarg(args, &count, APPLE_AOP_BOOTARG_AOPT, ++ &target_le, sizeof(target_le)); ++ apple_aop_add_bootarg(args, &count, APPLE_AOP_BOOTARG_NCAL, &ncal, sizeof(ncal)); ++ apple_aop_add_bootarg(args, &count, APPLE_AOP_BOOTARG_WRAD, ++ &wrapper_le, sizeof(wrapper_le)); ++ apple_aop_add_bootarg(args, &count, APPLE_AOP_BOOTARG_SOC, ++ &chip_le, sizeof(chip_le)); ++ apple_aop_add_bootarg(args, &count, APPLE_AOP_BOOTARG_SOCR, ++ &revision_le, sizeof(revision_le)); ++ apple_aop_add_bootarg(args, &count, APPLE_AOP_BOOTARG_STKG, &guard, sizeof(guard)); + } ++ ++ ret = apple_aop_patch_bootargs(aop, image, main_fw->size, args, count); + if (ret) { + dev_err(aop->dev, "unexpected boot arguments in %s: %d\n", main_name, ret); + } else { +- memcpy_toio(aop->sram, image, main_fw->size); +- memset_io(aop->sram + main_fw->size, 0, aop->sram_size - main_fw->size); ++ /* The SRAM takes 32-bit accesses, as Apple's loader makes them. */ ++ if (aop->spi0.ran) { ++ ret = a9_aop_spi0_gate(aop, "pre-copy-final", "PRECOPY", false); ++ if (!ret) ++ ret = a9_aop_copy_verify_wake10(aop, image, main_fw->size); ++ if (!ret) ++ ret = a9_aop_spi0_gate(aop, "after-copy", "AFTERCOPY", true); ++ if (ret) ++ goto out_image; ++ } else { ++ __iowrite32_copy(aop->sram, image, main_fw->size / 4); ++ } ++ for (offset = main_fw->size; offset + 4 <= aop->sram_size; offset += 4) ++ writel_relaxed(0, aop->sram + offset); + } + ++out_image: + kfree_sensitive(image); + memzero_explicit(prng, sizeof(prng)); + memzero_explicit(nonce, sizeof(nonce)); ++ memzero_explicit(guard, sizeof(guard)); + memzero_explicit(&ecid_le, sizeof(ecid_le)); + out_ext: + release_firmware(ext_fw); +@@ -448,7 +960,8 @@ static void apple_aop_announce(struct apple_aop_ep *ep, u32 channel, const void + const struct epic_service_announce *announce = data; + struct apple_aop_register_work *rw; + +- if (len < sizeof(*announce)) ++ /* Only the name and channel are used; the S8000's announcements are shorter. */ ++ if (len < offsetofend(struct epic_service_announce, channel)) + return; + + rw = kzalloc_obj(*rw); +@@ -475,6 +988,8 @@ static void apple_aop_report(struct apple_aop_ep *ep, u32 channel, u16 subtype, + struct apple_aop_listener *listener; + bool handled = false; + ++ if (ep->index == 0x21 && subtype == 0xc2) ++ aop->accel_reports++; + mutex_lock(&aop->listeners_lock); + list_for_each_entry(listener, &aop->listeners, list) { + if (listener->endpoint == ep->index && listener->channel == channel) { +@@ -631,6 +1146,7 @@ static void apple_aop_recv_message(void *cookie, u8 endpoint, u64 msg) + + switch (FIELD_GET(AFK_MSG_OPCODE, msg)) { + case AFK_OPC_INIT: ++ ep->initialized = true; + apple_aop_send(ep, AFK_OPC_INIT_ACK, 0); + break; + case AFK_OPC_GET_BUF: +@@ -651,7 +1167,10 @@ static void apple_aop_recv_message(void *cookie, u8 endpoint, u64 msg) + break; + case AFK_OPC_INIT_UNK: + case AFK_OPC_START_ACK: ++ break; + case AFK_OPC_SHUTDOWN_ACK: ++ case AFK_OPC_HIBERNATE_ACK: ++ complete(&ep->stopped); + break; + default: + dev_dbg(aop->dev, "endpoint 0x%02x: message 0x%llx\n", endpoint, msg); +@@ -662,7 +1181,8 @@ static void apple_aop_crashed(void *cookie, const void *crashlog, size_t crashlo + { + struct apple_aop *aop = cookie; + +- dev_err(aop->dev, "firmware crashed\n"); ++ aop->iop_crashed = true; ++ dev_err(aop->dev, "firmware crashed iop_crashed=1\n"); + } + + static const struct apple_rtkit_ops apple_aop_rtkit_ops = { +@@ -767,6 +1287,8 @@ int apple_aop_call(const struct apple_aop_service *service, u16 subtype, const v + + if (!ep) + return -EINVAL; ++ if (service->aop->warm && READ_ONCE(service->aop->iop_crashed)) ++ return -EIO; + init_completion(&call.done); + + mutex_lock(&ep->lock); +@@ -797,6 +1319,58 @@ int apple_aop_call(const struct apple_aop_service *service, u16 subtype, const v + } + EXPORT_SYMBOL_GPL(apple_aop_call); + ++int apple_aop_set_property(const struct apple_aop_service *service, u32 key, ++ const void *value, size_t len) ++{ ++ u8 buf[8 + APPLE_AOP_PROPERTY_MAX]; ++ int ret; ++ ++ if (len > APPLE_AOP_PROPERTY_MAX) ++ return -EINVAL; ++ put_unaligned_le32(0, buf); ++ put_unaligned_le32(key, buf + 4); ++ memcpy(buf + 8, value, len); ++ ++ ret = apple_aop_call(service, APPLE_AOP_SET_PROPERTY, buf, 8 + len, NULL, 0); ++ return ret > 0 ? -EIO : ret; ++} ++EXPORT_SYMBOL_GPL(apple_aop_set_property); ++ ++int apple_aop_calibrate(const struct apple_aop_service *service, struct device *dev, ++ const struct apple_aop_calibration *cal, unsigned int count) ++{ ++ unsigned long needed = 0, applied = 0; ++ __le32 state; ++ unsigned int i; ++ int len, ret; ++ ++ for (i = 0; i < count; i++) { ++ const void *blob; ++ ++ if (cal[i].group) ++ needed |= BIT(cal[i].group); ++ blob = of_get_property(dev->of_node, cal[i].property, &len); ++ if (!blob) ++ continue; ++ ret = apple_aop_set_property(service, cal[i].key, blob, len); ++ if (ret) { ++ dev_warn(dev, "%s not applied: %d\n", cal[i].property, ret); ++ continue; ++ } ++ if (cal[i].group) ++ applied |= BIT(cal[i].group); ++ } ++ ++ if (needed && applied == needed) { ++ state = cpu_to_le32(APPLE_AOP_CALIBRATION_COMPLETE); ++ } else { ++ dev_warn(dev, "factory calibration incomplete\n"); ++ state = cpu_to_le32(APPLE_AOP_CALIBRATION_PARTIAL); ++ } ++ return apple_aop_set_property(service, APPLE_AOP_PROP_CALIBRATION, &state, sizeof(state)); ++} ++EXPORT_SYMBOL_GPL(apple_aop_calibrate); ++ + /* Driver */ + + static void apple_aop_free_buffers(struct apple_aop *aop) +@@ -825,27 +1399,35 @@ static int apple_aop_probe(struct platform_device *pdev) + if (!aop) + return -ENOMEM; + aop->dev = dev; ++ aop->hw = of_device_get_match_data(dev); + mutex_init(&aop->listeners_lock); + INIT_LIST_HEAD(&aop->listeners); + mutex_init(&aop->children_lock); + INIT_LIST_HEAD(&aop->children); + +- aop->akf = devm_platform_ioremap_resource_byname(pdev, "akf"); ++ res = platform_get_resource_byname(pdev, IORESOURCE_MEM, "akf"); ++ aop->akf = devm_ioremap_resource(dev, res); + if (IS_ERR(aop->akf)) + return PTR_ERR(aop->akf); ++ aop->akf_base = res->start; + aop->control = devm_platform_ioremap_resource_byname(pdev, "control"); + if (IS_ERR(aop->control)) + return PTR_ERR(aop->control); + aop->sram = devm_platform_get_and_ioremap_resource(pdev, 2, &res); + if (IS_ERR(aop->sram)) + return PTR_ERR(aop->sram); ++ aop->sram_base = res->start; + aop->sram_size = resource_size(res); + + ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(44)); + if (ret) + return ret; + +- if (apple_aop_is_started(aop)) ++ ret = apple_aop_warm_prepare(aop); ++ if (ret) ++ return ret; ++ ++ if (!aop->spi0.ran && apple_aop_is_started(aop)) + return dev_err_probe(dev, -EBUSY, + "already started; the AOP starts once per boot\n"); + +@@ -853,6 +1435,17 @@ static int apple_aop_probe(struct platform_device *pdev) + if (ret) + return dev_err_probe(dev, ret, "no apple,ecid from the bootloader\n"); + ++ if (aop->hw->soc_bootargs) { ++ ret = of_property_read_u32(np, "apple,aop-target", &aop->target); ++ if (!ret) ++ ret = of_property_read_u32(np, "apple,chip-id", &aop->chip_id); ++ if (!ret) ++ ret = of_property_read_u32(np, "apple,chip-revision", &aop->chip_revision); ++ if (ret) ++ return dev_err_probe(dev, ret, ++ "no AOP target or chip ID from the bootloader\n"); ++ } ++ + aop->eps = devm_kcalloc(dev, AFK_ENDPOINT_COUNT, sizeof(*aop->eps), GFP_KERNEL); + if (!aop->eps) + return -ENOMEM; +@@ -860,6 +1453,7 @@ static int apple_aop_probe(struct platform_device *pdev) + aop->eps[i].aop = aop; + aop->eps[i].index = AFK_ENDPOINT_START + i; + mutex_init(&aop->eps[i].lock); ++ init_completion(&aop->eps[i].stopped); + } + + ret = apple_aop_load(aop, ecid); +@@ -867,6 +1461,9 @@ static int apple_aop_probe(struct platform_device *pdev) + if (ret) + goto err_free; + ++ if (aop->hw->sram_window) ++ apple_aop_map_sram(aop); ++ + aop->rtk = apple_rtkit_init(dev, aop, "mbox", 0, &apple_aop_rtkit_ops); + if (IS_ERR(aop->rtk)) { + ret = dev_err_probe(dev, PTR_ERR(aop->rtk), "failed to init RTKit\n"); +@@ -884,10 +1481,24 @@ static int apple_aop_probe(struct platform_device *pdev) + prop = of_find_property(np, "apple,ecid", NULL); + if (prop) + of_remove_property(np, prop); +- writel(readl(aop->akf + APPLE_AOP_CPU_CONTROL) | APPLE_AOP_CPU_CONTROL_RUN, +- aop->akf + APPLE_AOP_CPU_CONTROL); ++ if (aop->spi0.ran) { ++ ret = apple_aop_warm_release(aop); ++ if (ret) ++ goto err_halt; ++ } ++ if (aop->spi0.ran) { ++ aop->experiment_step = "warm-run"; ++ ret = aop_wr(aop, APPLE_AOP_CPU_CONTROL, ++ readl(aop->akf + APPLE_AOP_CPU_CONTROL) | APPLE_AOP_CPU_CONTROL_RUN); ++ if (ret) ++ goto err_halt; ++ } else { ++ writel(readl(aop->akf + APPLE_AOP_CPU_CONTROL) | APPLE_AOP_CPU_CONTROL_RUN, ++ aop->akf + APPLE_AOP_CPU_CONTROL); ++ } + writel(1, aop->control + APPLE_AOP_CONTROL_AUTO_BOOT); + ++ dev_info(dev, "AOP released warm=%d\n", aop->spi0.ran); + ret = apple_rtkit_wake(aop->rtk); + if (ret) { + dev_err(dev, "firmware did not boot: %d\n", ret); +@@ -900,6 +1511,8 @@ static int apple_aop_probe(struct platform_device *pdev) + dev_warn(dev, "failed to start endpoint 0x%02x: %d\n", i, ret); + } + ++ aop->hello = true; ++ dev_info(dev, "AOP start complete warm=%d iop_crashed=0\n", aop->spi0.ran); + platform_set_drvdata(pdev, aop); + return 0; + +@@ -929,27 +1542,81 @@ static void apple_aop_shutdown(struct platform_device *pdev) + mutex_lock(&aop->children_lock); + aop->removing = true; + list_for_each_entry_safe(child, tmp, &aop->children, list) { ++ ++ if (aop->warm) { ++ const struct apple_aop_service *service = dev_get_platdata(&child->pdev->dev); ++ static const u32 keys[] = { 0x16, 0x5c, 0 }; ++ ++ /* release6 sets the three accelerometer rates only. */ ++ if (strcmp(service->name, "accel")) ++ continue; ++ __le32 zero = 0; ++ unsigned int i; ++ int ret; ++ ++ for (i = 0; i < ARRAY_SIZE(keys); i++) { ++ ret = apple_aop_set_property(service, keys[i], &zero, sizeof(zero)); ++ if (ret) { ++ aop->shutdown_error = ret; ++ break; ++ } ++ } ++ } ++ /* Shutdown retains listeners, children and DMA grants, as release6 does. ++ * Unregister is remove-time work and can wait in IIO/driver core. ++ */ ++ if (aop->warm) ++ continue; + platform_device_unregister(child->pdev); + list_del(&child->list); + kfree(child); + } + mutex_unlock(&aop->children_lock); + ++ if (aop->warm) { ++ apple_aop_warm_shutdown(aop); ++ return; ++ } + if (!apple_aop_halt(aop)) + dev_err(aop->dev, "failed to halt\n"); + } + ++static const struct apple_aop_hw apple_aop_s8000_hw = { ++ .sram_window = true, ++ .soc_bootargs = true, ++}; ++ ++static const struct apple_aop_hw apple_aop_t8010_hw = { ++ .nonce = true, ++}; ++ + static const struct of_device_id apple_aop_of_match[] = { +- { .compatible = "apple,t8010-aop" }, ++ { .compatible = "apple,s8000-aop", .data = &apple_aop_s8000_hw }, ++ { .compatible = "apple,t8010-aop", .data = &apple_aop_t8010_hw }, + {} + }; + MODULE_DEVICE_TABLE(of, apple_aop_of_match); + ++static ssize_t warm_state_show(struct device *dev, struct device_attribute *attr, char *buf) ++{ ++ struct apple_aop *aop = dev_get_drvdata(dev); ++ ++ if (!aop) ++ return sysfs_emit(buf, "owned=0\n"); ++ return sysfs_emit(buf, "owned=%u reload=%u iop_crashed=%u accel_reports=%llu\n", ++ aop->hello, aop->spi0.ran, READ_ONCE(aop->iop_crashed), ++ READ_ONCE(aop->accel_reports)); ++} ++static DEVICE_ATTR_RO(warm_state); ++static struct attribute *apple_aop_attrs[] = { &dev_attr_warm_state.attr, NULL }; ++ATTRIBUTE_GROUPS(apple_aop); ++ + static struct platform_driver apple_aop_driver = { + .driver = { + .name = "apple-aop", + .of_match_table = apple_aop_of_match, + .suppress_bind_attrs = true, ++ .dev_groups = apple_aop_groups, + }, + .probe = apple_aop_probe, + .shutdown = apple_aop_shutdown, +@@ -957,5 +1624,5 @@ static struct platform_driver apple_aop_driver = { + module_platform_driver(apple_aop_driver); + + MODULE_AUTHOR("Dalton Alexandre "); +-MODULE_DESCRIPTION("Apple T8010 Always-On Processor driver"); ++MODULE_DESCRIPTION("Apple S8000 and T8010 Always-On Processor driver"); + MODULE_LICENSE("Dual MIT/GPL"); +diff --git a/drivers/soc/apple/rtkit.c b/drivers/soc/apple/rtkit.c +index 725ab3fa7..d7928621a 100644 +--- a/drivers/soc/apple/rtkit.c ++++ b/drivers/soc/apple/rtkit.c +@@ -4,6 +4,8 @@ + * Copyright (C) The Asahi Linux Contributors + */ + ++#include ++#include + #include "rtkit-internal.h" + + enum { +@@ -867,6 +869,54 @@ int apple_rtkit_boot(struct apple_rtkit *rtk) + } + EXPORT_SYMBOL_GPL(apple_rtkit_boot); + ++/* Unlike shutdown(), retain all grants until the caller halts the CPU. ++ * Reinit frees DMA buffers and is unsafe before that point on S8000. ++ */ ++static void apple_rtkit_grants_hash(struct apple_rtkit *rtk, u8 *digest) ++{ ++ struct apple_rtkit_shmem *buffers[] = { ++ &rtk->ioreport_buffer, &rtk->crashlog_buffer, ++ &rtk->oslog_buffer, &rtk->syslog_buffer, ++ }; ++ struct sha256_ctx ctx; ++ unsigned int i; ++ ++ sha256_init(&ctx); ++ dma_rmb(); ++ for (i = 0; i < ARRAY_SIZE(buffers); i++) ++ if (buffers[i]->buffer) ++ sha256_update(&ctx, buffers[i]->buffer, buffers[i]->size); ++ sha256_final(&ctx, digest); ++} ++ ++int apple_rtkit_shutdown_preserve(struct apple_rtkit *rtk) ++{ ++ u8 before[SHA256_DIGEST_SIZE], after[SHA256_DIGEST_SIZE]; ++ int ret; ++ ++ if (rtk->crashed) ++ return -EIO; ++ ret = apple_rtkit_set_ap_power_state(rtk, APPLE_RTKIT_PWR_STATE_QUIESCED); ++ if (ret) ++ return ret; ++ dev_info(rtk->dev, "QUIESCE step=ap-ack state=0x10\n"); ++ ret = apple_rtkit_set_iop_power_state(rtk, APPLE_RTKIT_PWR_STATE_IDLE); ++ if (ret) ++ return ret; ++ dev_info(rtk->dev, "QUIESCE step=iop-ack state=0x201\n"); ++ msleep(20); ++ apple_rtkit_grants_hash(rtk, before); ++ msleep(20); ++ apple_rtkit_grants_hash(rtk, after); ++ if (rtk->crashed || memcmp(before, after, sizeof(before))) ++ return -EBUSY; ++ /* No more callback can race the caller's halt/drain. Do not reinit. */ ++ apple_mbox_stop(rtk->mbox); ++ flush_workqueue(rtk->wq); ++ return 0; ++} ++EXPORT_SYMBOL_GPL(apple_rtkit_shutdown_preserve); ++ + int apple_rtkit_shutdown(struct apple_rtkit *rtk) + { + int ret; +diff --git a/include/linux/soc/apple/aop.h b/include/linux/soc/apple/aop.h +index 8c8bc71b6..b5188a0b1 100644 +--- a/include/linux/soc/apple/aop.h ++++ b/include/linux/soc/apple/aop.h +@@ -7,6 +7,8 @@ + + #include + ++struct device; ++ + struct apple_aop; + + /* +@@ -40,4 +42,30 @@ void apple_aop_unlisten(const struct apple_aop_service *service); + int apple_aop_call(const struct apple_aop_service *service, u16 subtype, + const void *data, size_t len, void *reply, size_t reply_len); + ++/* The EPIC notification subtype that sets a service property */ ++#define APPLE_AOP_SET_PROPERTY 0x4 ++ ++/* Set property @key of a service to @len (at most 64) bytes at @value. */ ++int apple_aop_set_property(const struct apple_aop_service *service, u32 key, ++ const void *value, size_t len); ++ ++/* ++ * A factory calibration blob of a sensor: the bootloader copies it from the ++ * Apple device tree into the sensor's node as @property, and the firmware ++ * takes it as property @key. The calibration is complete once a blob of each ++ * @group (1 to 31) was applied; group 0 blobs are not needed for that. ++ */ ++struct apple_aop_calibration { ++ const char *property; ++ u32 key; ++ u8 group; ++}; ++ ++/* ++ * Send the factory calibration found in @dev's node, then tell the firmware ++ * whether it is complete. Some firmware (S8000) sends no reports before. ++ */ ++int apple_aop_calibrate(const struct apple_aop_service *service, struct device *dev, ++ const struct apple_aop_calibration *cal, unsigned int count); ++ + #endif /* _LINUX_SOC_APPLE_AOP_H_ */ +diff --git a/include/linux/soc/apple/pmgr-observe.h b/include/linux/soc/apple/pmgr-observe.h +new file mode 100644 +index 000000000..477b7fa22 +--- /dev/null ++++ b/include/linux/soc/apple/pmgr-observe.h +@@ -0,0 +1,27 @@ ++/* SPDX-License-Identifier: GPL-2.0-only */ ++#ifndef _LINUX_SOC_APPLE_PMGR_OBSERVE_H ++#define _LINUX_SOC_APPLE_PMGR_OBSERVE_H ++ ++#include ++ ++struct device_node; ++ ++struct apple_pmgr_observation { ++ u32 raw; ++ int genpd_status; ++}; ++ ++/* ++ * Diagnostic only. Nodes must be in child-before-parent lock order, with no ++ * duplicates (maximum five). Takes no power/runtime-PM reference. read() must ++ * not sleep or acquire genpd locks; it runs with IRQs disabled and all domain ++ * locks held, only if every genpd and hardware actual/target state is ON and ++ * reset/disable/parent-off are clear. Hardware auto-PM remains untouched and is ++ * recorded in raw; locks exclude software transitions, not autonomous gating. ++ * -EHOSTDOWN means read() was skipped. ++ */ ++int apple_pmgr_observe(struct device_node **nodes, unsigned int count, ++ struct apple_pmgr_observation *states, ++ void (*read)(void *), void *data); ++ ++#endif +diff --git a/include/linux/soc/apple/rtkit.h b/include/linux/soc/apple/rtkit.h +index 1f9ebb618..c8e8df255 100644 +--- a/include/linux/soc/apple/rtkit.h ++++ b/include/linux/soc/apple/rtkit.h +@@ -124,6 +124,8 @@ int apple_rtkit_wake(struct apple_rtkit *rtk); + * Shutdown the co-processor + */ + int apple_rtkit_shutdown(struct apple_rtkit *rtk); ++/* AP 0x10 / IOP 0x201, stable system RAM, stop RX; retain every grant. */ ++int apple_rtkit_shutdown_preserve(struct apple_rtkit *rtk); + + /* + * Stop the mailbox of a shut-down co-processor, so that its power domain can +diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c +index 3fcdf4b4e..fcc51b699 100644 +--- a/kernel/printk/printk.c ++++ b/kernel/printk/printk.c +@@ -4589,6 +4589,7 @@ bool pr_flush(int timeout_ms, bool reset_on_progress) + { + return __pr_flush(NULL, timeout_ms, reset_on_progress); + } ++EXPORT_SYMBOL_GPL(pr_flush); + + /* + * Delayed printk version, for scheduler-internal messages: diff --git a/tests/test_j71t_data.py b/tests/test_j71t_data.py new file mode 100644 index 00000000..c6d83c08 --- /dev/null +++ b/tests/test_j71t_data.py @@ -0,0 +1,31 @@ +"""J71t firmware recipe: the AOP images.""" +import json +from pathlib import Path +import tomllib +import unittest + +HARDWARE = Path(__file__).resolve().parents[1] +DEVICE = HARDWARE / 'devices/j71t/device.toml' + + +class J71tData(unittest.TestCase): + def setUp(self): + device = tomllib.loads(DEVICE.read_text()) + self.recipe = json.loads((DEVICE.parent / device['firmware']['recipe']).read_text()) + + def test_aop_images_come_from_the_board_firmware(self): + aop = self.recipe['inputs']['aop.im4p'] + self.assertEqual(aop['ipsw'], {'zip': 'Firmware/AOP/aopfw-ipad6faop.im4p'}) + self.assertEqual(aop['sha256'], '5a4d8378bb83e175fc589cc1cf978430639d7ffcef07b2d94e9d6a4801124efa') + for name, role, digest in ( + ('aop-ipad6f.bin', 'AOP', 'af5d51a368ceb89dc5c359e7c84e54cb6082e308956be742c17622eb4a6047c8'), + ('aop-ipad6f-ext.bin', 'AOP_ext', '2528d2686ed1262de9910c2d60e45b68137e50aa601a0f8d54ea7d1347b9de22')): + output, = [o for o in self.recipe['outputs'] if o['destination'] == 'lib/firmware/apple/' + name] + self.assertEqual(output['sha256'], digest) + self.assertEqual(output['steps'], [{'step': 'im4p-payload@1', 'input': 'aop.im4p'}, + {'step': 'aop-image@1', 'coprocessor': role}]) + self.assertFalse((HARDWARE / output['destination']).exists()) + + +if __name__ == '__main__': + unittest.main() From 6c1601e6484a28dfa8c73f0e955fbc5e0036d1f4 Mon Sep 17 00:00:00 2001 From: brkosebay Date: Sun, 8 Nov 2026 16:00:00 +0100 Subject: [PATCH 02/10] Optional resident m1n1 EL3 PSCI Assisted-by: LLM --- boot/m1n1/README.md | 7 + ...resident-EL3-CPU-lifecycle-for-A7-A9.patch | 880 ++++++++++++++++++ boot/m1n1/psci-loader | 29 + chips/s8003.toml | 3 + devices/j71t/device.toml | 3 + tests/test_m1n1_psci.py | 54 ++ 6 files changed, 976 insertions(+) create mode 100644 boot/m1n1/patches/orchard/0014-psci-Add-opt-in-resident-EL3-CPU-lifecycle-for-A7-A9.patch create mode 100755 boot/m1n1/psci-loader create mode 100644 tests/test_m1n1_psci.py diff --git a/boot/m1n1/README.md b/boot/m1n1/README.md index bd78f93b..04150148 100644 --- a/boot/m1n1/README.md +++ b/boot/m1n1/README.md @@ -28,3 +28,10 @@ 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. + +## Optional resident PSCI on A7-A9 + +Orchard patch 0014 adds the linux-a9 opt-in EL3 PSCI shim. It requires +`psci=1` as a payload variable and entry at EL3; EL1 keeps the spin table. +There is no per-board default. See [J71t loader instructions](../../devices/j71t/README.md) +for `psci-loader`, launch/RAM framing, Pongo evidence and the cold-boot gate. diff --git a/boot/m1n1/patches/orchard/0014-psci-Add-opt-in-resident-EL3-CPU-lifecycle-for-A7-A9.patch b/boot/m1n1/patches/orchard/0014-psci-Add-opt-in-resident-EL3-CPU-lifecycle-for-A7-A9.patch new file mode 100644 index 00000000..a677f9a7 --- /dev/null +++ b/boot/m1n1/patches/orchard/0014-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 linux-a9 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/psci-loader b/boot/m1n1/psci-loader new file mode 100755 index 00000000..1f628bc9 --- /dev/null +++ b/boot/m1n1/psci-loader @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +"""Make a J71t opt-in loader by appending m1n1's psci=1 payload variable. + +Use a bare m1n1.bin as input, before assembling any kernel/DTB/logo payload. +The output works with orchard run/build --loader or launch's loaders/m1n1.bin. +It still uses the spin table if m1n1 was entered at EL1. +""" +import argparse +from pathlib import Path +import shutil + + +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) + 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=1\n') + print(args.output) + + +if __name__ == '__main__': + main() diff --git a/chips/s8003.toml b/chips/s8003.toml index e152fbed..541b54da 100644 --- a/chips/s8003.toml +++ b/chips/s8003.toml @@ -15,3 +15,6 @@ stages = ["iBSS", "iBEC"] page_size = "xnu-6153.141.1 osfmk/arm/proc_reg.h" platform = "J71t on hardware" stages = "remote_boot two-stage CPID list" + +# Local capability delta: psci. +# Integration evidence / qualification limits: linux-a9 docs/release/orchard-prs-20261108-final/psci.md. diff --git a/devices/j71t/device.toml b/devices/j71t/device.toml index 1a78c1d1..40db2bbc 100644 --- a/devices/j71t/device.toml +++ b/devices/j71t/device.toml @@ -54,3 +54,6 @@ usb_network = "untested" audio_output = "untested" audio_input = "untested" camera = "missing" + +# Local capability delta: psci. +# Integration evidence / qualification limits: linux-a9 docs/release/orchard-prs-20261108-final/psci.md. diff --git a/tests/test_m1n1_psci.py b/tests/test_m1n1_psci.py new file mode 100644 index 00000000..0f491dbf --- /dev/null +++ b/tests/test_m1n1_psci.py @@ -0,0 +1,54 @@ +"""Host-only framing checks for the explicit J71t PSCI loader.""" +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_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() From eb6a0b1a68f87a420fc80639816b7808e1ca7e89 Mon Sep 17 00:00:00 2001 From: brkosebay Date: Sun, 8 Nov 2026 16:00:00 +0100 Subject: [PATCH 03/10] Quiet polled cover inputs and exclusive RTC event Assisted-by: LLM --- chips/s8003.toml | 3 + devices/j71t/device.toml | 3 + .../s800x/0002-pmu-rtc-implementation.patch | 232 ++++++++++++++++++ .../usb/0010-pmu-rtc-implementation.patch | 34 +++ 4 files changed, 272 insertions(+) create mode 100644 kernel/patches/devices/s800x/0002-pmu-rtc-implementation.patch create mode 100644 kernel/patches/drivers/usb/0010-pmu-rtc-implementation.patch diff --git a/chips/s8003.toml b/chips/s8003.toml index e152fbed..6ae21ec1 100644 --- a/chips/s8003.toml +++ b/chips/s8003.toml @@ -15,3 +15,6 @@ stages = ["iBSS", "iBEC"] page_size = "xnu-6153.141.1 osfmk/arm/proc_reg.h" platform = "J71t on hardware" stages = "remote_boot two-stage CPID list" + +# Local capability delta: pmu-rtc. +# Integration evidence / qualification limits: linux-a9 docs/release/orchard-prs-20261108-final/pmu-rtc.md. diff --git a/devices/j71t/device.toml b/devices/j71t/device.toml index 1a78c1d1..72069715 100644 --- a/devices/j71t/device.toml +++ b/devices/j71t/device.toml @@ -54,3 +54,6 @@ usb_network = "untested" audio_output = "untested" audio_input = "untested" camera = "missing" + +# Local capability delta: pmu-rtc. +# Integration evidence / qualification limits: linux-a9 docs/release/orchard-prs-20261108-final/pmu-rtc.md. diff --git a/kernel/patches/devices/s800x/0002-pmu-rtc-implementation.patch b/kernel/patches/devices/s800x/0002-pmu-rtc-implementation.patch new file mode 100644 index 00000000..48b731e4 --- /dev/null +++ b/kernel/patches/devices/s800x/0002-pmu-rtc-implementation.patch @@ -0,0 +1,232 @@ +From: brkosebay +Date: Sun, 08 Nov 2026 16:00:00 +0100 +Subject: [PATCH] arm64: dts: apple: Quiet polled cover inputs and exclusive RTC event + +Collapse the intermediate IRQ-driven cover change and its storm fix into the final polled-cover implementation, keeping PMU events 48/58 masked. Reserve event 16 exclusively for the RTC while retaining Tristar event-72 wake and parent-line protection. Add the one-shot RTC alarm interface and accept both directions of the known Apple current-grant request; do not fabricate a host grant or claim Mac charging is fixed. Cover wake remains deferred. + +Integration fixups are folded into the final delta. Device observations apply +to the integration build; the isolated topic requires qualification. + +Assisted-by: LLM +--- + +diff --git a/arch/arm64/boot/dts/apple/s800x-ipad5.dtsi b/arch/arm64/boot/dts/apple/s800x-ipad5.dtsi +index eb6b6fe92..ca2ab85c2 100644 +--- a/arch/arm64/boot/dts/apple/s800x-ipad5.dtsi ++++ b/arch/arm64/boot/dts/apple/s800x-ipad5.dtsi +@@ -191,11 +191,11 @@ tristar_charger: charger@180 { + 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>, +@@ -211,6 +211,9 @@ rtc@500 { + 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/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/0010-pmu-rtc-implementation.patch b/kernel/patches/drivers/usb/0010-pmu-rtc-implementation.patch new file mode 100644 index 00000000..4713453e --- /dev/null +++ b/kernel/patches/drivers/usb/0010-pmu-rtc-implementation.patch @@ -0,0 +1,34 @@ +From: brkosebay +Date: Sun, 08 Nov 2026 16:00:00 +0100 +Subject: [PATCH] usb: Quiet polled cover inputs and exclusive RTC event + +Collapse the intermediate IRQ-driven cover change and its storm fix into the final polled-cover implementation, keeping PMU events 48/58 masked. Reserve event 16 exclusively for the RTC while retaining Tristar event-72 wake and parent-line protection. Add the one-shot RTC alarm interface and accept both directions of the known Apple current-grant request; do not fabricate a host grant or claim Mac charging is fixed. Cover wake remains deferred. + +Integration fixups are folded into the final delta. Device observations apply +to the integration build; the isolated topic requires qualification. + +Assisted-by: LLM +--- + +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 @@ composite_setup(struct usb_gadget *gadget, const struct usb_ctrlrequest *ctrl) + 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) { From e4ea0b7c1c87f6318cda2db23e13ee8c83f3df1b Mon Sep 17 00:00:00 2001 From: brkosebay Date: Sun, 8 Nov 2026 16:00:00 +0100 Subject: [PATCH 04/10] PCI/NVMe: retain the stopped ANS handoff across kexec kernel/configs/apple-16k/config: # CONFIG_NVME_APPLE_ANS_EARLY_USB is not set These options provide warm-storage and its Kconfig dependencies on both page sizes. kernel/configs/apple-4k/config: # CONFIG_NVME_APPLE_ANS_EARLY_USB is not set These options provide warm-storage and its Kconfig dependencies on both page sizes. Assisted-by: LLM --- chips/s8003.toml | 3 + devices/j71t/device.toml | 3 + kernel/configs/apple-16k/config | 1 + kernel/configs/apple-4k/config | 1 + .../0032-warm-storage-implementation.patch | 1579 +++++++++++++++++ 5 files changed, 1587 insertions(+) create mode 100644 kernel/patches/drivers/pci/0032-warm-storage-implementation.patch diff --git a/chips/s8003.toml b/chips/s8003.toml index e152fbed..ac45860e 100644 --- a/chips/s8003.toml +++ b/chips/s8003.toml @@ -15,3 +15,6 @@ stages = ["iBSS", "iBEC"] page_size = "xnu-6153.141.1 osfmk/arm/proc_reg.h" platform = "J71t on hardware" stages = "remote_boot two-stage CPID list" + +# Local capability delta: warm-storage. +# Integration evidence / qualification limits: linux-a9 docs/release/orchard-prs-20261108-final/warm-storage.md. diff --git a/devices/j71t/device.toml b/devices/j71t/device.toml index 1a78c1d1..1953c00a 100644 --- a/devices/j71t/device.toml +++ b/devices/j71t/device.toml @@ -54,3 +54,6 @@ usb_network = "untested" audio_output = "untested" audio_input = "untested" camera = "missing" + +# Local capability delta: warm-storage. +# Integration evidence / qualification limits: linux-a9 docs/release/orchard-prs-20261108-final/warm-storage.md. diff --git a/kernel/configs/apple-16k/config b/kernel/configs/apple-16k/config index aae562f8..33b0a61b 100644 --- a/kernel/configs/apple-16k/config +++ b/kernel/configs/apple-16k/config @@ -1815,6 +1815,7 @@ CONFIG_BLK_DEV_NBD=y # CONFIG_NVME_CORE=m CONFIG_BLK_DEV_NVME=m +# CONFIG_NVME_APPLE_ANS_EARLY_USB is not set # CONFIG_NVME_MULTIPATH is not set # CONFIG_NVME_VERBOSE_ERRORS is not set # CONFIG_NVME_HWMON is not set diff --git a/kernel/configs/apple-4k/config b/kernel/configs/apple-4k/config index bf332e9c..e10187a1 100644 --- a/kernel/configs/apple-4k/config +++ b/kernel/configs/apple-4k/config @@ -1816,6 +1816,7 @@ CONFIG_BLK_DEV_NBD=y # CONFIG_NVME_CORE=m CONFIG_BLK_DEV_NVME=m +# CONFIG_NVME_APPLE_ANS_EARLY_USB is not set # CONFIG_NVME_MULTIPATH is not set # CONFIG_NVME_VERBOSE_ERRORS is not set # CONFIG_NVME_HWMON is not set diff --git a/kernel/patches/drivers/pci/0032-warm-storage-implementation.patch b/kernel/patches/drivers/pci/0032-warm-storage-implementation.patch new file mode 100644 index 00000000..9f3131e9 --- /dev/null +++ b/kernel/patches/drivers/pci/0032-warm-storage-implementation.patch @@ -0,0 +1,1579 @@ +From: brkosebay +Date: Sun, 08 Nov 2026 16:00:00 +0100 +Subject: [PATCH] PCI: apple-h9p: PCI/NVMe: retain the stopped ANS handoff across kexec + +Pair the S8003 H9P powered handoff with an ANS stop certificate covering SHST, CC/RDY, BME, queue/tag retirement, final NVMMU FIFO acknowledgement and endpoint DART shutdown. Include the final fatal-kexec drain/isolation correction and the modular kexec-state export required by NVMe. Failed stops withhold the certificate; failed powered resume stays under PERST. Real suspend/resume and the Orchard warm ladder remain pending, with no automatic suspend enablement. + +Integration fixups are folded into the final delta. Device observations apply +to the integration build; the isolated topic requires qualification. + +Assisted-by: LLM +--- + +diff --git a/drivers/iommu/apple-dart.c b/drivers/iommu/apple-dart.c +index 1ee6add55..7906595cd 100644 +--- a/drivers/iommu/apple-dart.c ++++ b/drivers/iommu/apple-dart.c +@@ -35,6 +35,7 @@ + #include + + #include ++#include + + #include "dma-iommu.h" + +@@ -1009,6 +1010,37 @@ apple_dart_setup_translation(struct apple_dart_domain *domain, + return dart->hw->invalidate_tlb(stream_map); + } + ++int apple_ans_dart_set_active(struct device *dev, bool active) ++{ ++ struct iommu_domain *domain = iommu_get_domain_for_dev(dev); ++ struct apple_dart_master_cfg *cfg = dev_iommu_priv_get(dev); ++ struct apple_dart_stream_map *map; ++ struct apple_dart_domain *d; ++ int i, ret; ++ ++ if (!of_machine_is_compatible("apple,j71t") || !domain || !cfg || ++ !cfg->stream_maps[0].dart || ++ (domain->type != IOMMU_DOMAIN_DMA && domain->type != IOMMU_DOMAIN_DMA_FQ)) ++ return -ENODEV; ++ d = to_dart_domain(domain); ++ for_each_stream_map(i, cfg, map) { ++ if (map->dart->hw->type != DART_S5L8960X) ++ return -EOPNOTSUPP; ++ if (!active) { ++ apple_dart_hw_disable_dma(map); ++ continue; ++ } ++ ret = apple_dart_setup_translation(d, map); ++ if (ret) ++ return ret; ++ } ++ if (active) ++ WRITE_ONCE(d->flush_error, 0); ++ dev_info(dev, "Apple ANS: DART force_active(%u) complete\n", active); ++ return 0; ++} ++EXPORT_SYMBOL_GPL(apple_ans_dart_set_active); ++ + static int apple_dart_finalize_domain(struct apple_dart_domain *dart_domain, + struct apple_dart_master_cfg *cfg) + { +diff --git a/drivers/nvme/host/Kconfig b/drivers/nvme/host/Kconfig +index 31974c7dd..0504eb40d 100644 +--- a/drivers/nvme/host/Kconfig ++++ b/drivers/nvme/host/Kconfig +@@ -14,6 +14,15 @@ config BLK_DEV_NVME + To compile this driver as a module, choose M here: the + module will be called nvme. + ++config NVME_APPLE_ANS_EARLY_USB ++ bool "Defer S8000/S8003 PCIe/ANS until initramfs USB rescue is ready" ++ depends on BLK_DEV_NVME && PCIE_APPLE_H9P ++ help ++ Default pcie_apple_h9p.probe and nvme.apple_ans_probe to false ++ on S8000/S8003. Requires an initramfs which starts USB rescue, opens ++ both gates and reprobes the host and endpoints. Leave off with ++ the ordinary root initrd. Other boards are unchanged. ++ + config NVME_MULTIPATH + bool "NVMe multipath support" + depends on NVME_CORE +diff --git a/drivers/nvme/host/pci-apple-ans.c b/drivers/nvme/host/pci-apple-ans.c +index 5ba239e47..0db0cb71b 100644 +--- a/drivers/nvme/host/pci-apple-ans.c ++++ b/drivers/nvme/host/pci-apple-ans.c +@@ -84,6 +84,7 @@ struct apple_ans_nvme { + u32 cc; + unsigned long cc_jiffies; + bool cc_written; ++ bool host_fault; + dma_addr_t hmb_dma; + u32 hmb_size; + struct apple_ans_nvme_req req[APPLE_ANS_NVMMU_MAX_REQS]; +@@ -218,13 +219,20 @@ static int nvme_pci_apple_ans_reset_dma(struct nvme_dev *dev) + { + struct apple_ans_nvme *ans = nvme_pci_apple_ans(dev); + unsigned long flags; +- unsigned int tag; + int ret; + +- for (tag = 0; tag < APPLE_ANS_NVMMU_MAX_REQS; tag++) { +- ret = apple_ans_nvmmu_map(dev->dev, tag, NULL, 0, false, NULL); ++ if (ans->host == &apple_ans_host_s8003) { ++ ret = apple_ans_nvmmu_reset(dev->dev); + if (ret) + return ret; ++ } else { ++ unsigned int tag; ++ ++ for (tag = 0; tag < APPLE_ANS_NVMMU_MAX_REQS; tag++) { ++ ret = apple_ans_nvmmu_map(dev->dev, tag, NULL, 0, false, NULL); ++ if (ret) ++ return ret; ++ } + } + + spin_lock_irqsave(&ans->req_lock, flags); +@@ -258,6 +266,14 @@ static int nvme_pci_apple_ans_config(struct nvme_dev *dev) + return 0; + } + ++static bool apple_ans_probe = !IS_ENABLED(CONFIG_NVME_APPLE_ANS_EARLY_USB); ++module_param(apple_ans_probe, bool, 0644); ++ ++bool nvme_pci_apple_ans_probe_allowed(struct pci_dev *pdev) ++{ ++ return !nvme_pci_apple_ans_match(pdev) || READ_ONCE(apple_ans_probe); ++} ++ + static int nvme_pci_apple_ans_preinit(struct nvme_dev *dev) + { + struct apple_ans_nvme *ans = nvme_pci_apple_ans(dev); +@@ -326,10 +342,24 @@ static int nvme_pci_apple_ans_prepare_enable(struct nvme_dev *dev) + return 0; + } + ++static int nvme_pci_apple_ans_check_link(struct nvme_dev *dev) ++{ ++ struct apple_ans_nvme *ans = nvme_pci_apple_ans(dev); ++ ++ if (ans && ans->host == &apple_ans_host_s8003 && ++ (ans->host_fault || apple_ans_link_up(dev->dev) <= 0)) { ++ ans->host_fault = true; ++ return -EIO; ++ } ++ return 0; ++} ++ + static int nvme_pci_apple_ans_write_cc(struct nvme_dev *dev, u32 cc) + { + struct apple_ans_nvme *ans = nvme_pci_apple_ans(dev); + ++ if (nvme_pci_apple_ans_check_link(dev)) ++ return -EIO; + writel(cc, dev->bar + NVME_REG_CC); + if (!ans) + return 0; +@@ -349,6 +379,8 @@ static int nvme_pci_apple_ans_read_csts(struct nvme_dev *dev, u32 *csts) + struct apple_ans_nvme *ans = nvme_pci_apple_ans(dev); + unsigned long deadline; + ++ if (nvme_pci_apple_ans_check_link(dev)) ++ return -EIO; + *csts = readl(dev->bar + NVME_REG_CSTS); + if (!ans || !ans->cc_written || !ans->host->rdy_timeout_ms || + (ans->cc & NVME_CC_SHN_MASK)) +@@ -360,6 +392,8 @@ static int nvme_pci_apple_ans_read_csts(struct nvme_dev *dev, u32 *csts) + time_after(jiffies, deadline)) { + dev_err(dev->dev, "ANS RDY timeout (CC %#x, CSTS %#x)\n", + ans->cc, *csts); ++ ans->host_fault = true; ++ apple_ans_host_dump(dev->dev, "RDY deadline"); + return -ETIMEDOUT; + } + +@@ -419,6 +453,7 @@ static int nvme_pci_apple_ans_free_req(struct nvme_dev *dev, + dev_err_ratelimited(dev->dev, + "failed to release ANS DMA slot %u: %d\n", + tag, ret); ++ nvme_reset_ctrl(&dev->ctrl); + return ret; + } + +diff --git a/drivers/nvme/host/pci-internal.h b/drivers/nvme/host/pci-internal.h +index a62777abd..ef3ea7b99 100644 +--- a/drivers/nvme/host/pci-internal.h ++++ b/drivers/nvme/host/pci-internal.h +@@ -74,6 +74,8 @@ struct nvme_dev { + struct nvme_ctrl ctrl; + u32 last_ps; + bool hmb; ++ unsigned long apple_ans_flags; ++ atomic_t apple_ans_irq_count; + struct sg_table *hmb_sgt; + mempool_t *dmavec_mempool; + const struct nvme_pci_dma_ops *dma_ops; +@@ -195,6 +197,7 @@ struct nvme_iod { + + extern const struct nvme_pci_dma_ops nvme_pci_apple_ans_ops; + ++bool nvme_pci_apple_ans_probe_allowed(struct pci_dev *pdev); + bool nvme_pci_apple_ans_match(struct pci_dev *pdev); + + #endif /* _NVME_PCI_INTERNAL_H */ +diff --git a/drivers/nvme/host/pci.c b/drivers/nvme/host/pci.c +index 9ffe2fb74..32cc6ba89 100644 +--- a/drivers/nvme/host/pci.c ++++ b/drivers/nvme/host/pci.c +@@ -5,6 +5,7 @@ + */ + + #include ++#include + #include + #include + #include +@@ -17,11 +18,17 @@ + #include + #include + #include ++#include ++#include ++#include ++#include + #include + #include + #include + #include + #include ++#include ++#include + #include + #include + #include +@@ -1559,6 +1566,9 @@ static irqreturn_t nvme_irq(int irq, void *data) + DEFINE_IO_COMP_BATCH(iob); + bool found; + ++ if (!nvmeq->qid && dev->dma_ops == &nvme_pci_apple_ans_ops) ++ atomic_inc(&dev->apple_ans_irq_count); ++ + if (nvme_apple_ans_shared_vector(dev)) + found = nvme_apple_ans_irq_vector(dev, nvmeq->cq_vector, &iob); + else +@@ -1836,6 +1846,40 @@ static void nvme_warn_reset(struct nvme_dev *dev, u32 csts) + "Try \"nvme_core.default_ps_max_latency_us=0 pcie_aspm=off pcie_port_pm=off\" and report a bug\n"); + } + ++/* No endpoint MMIO/config or doorbell reads: safe first evidence for ANS. */ ++static void nvme_apple_ans_dump(struct nvme_dev *dev, const char *reason) ++{ ++ unsigned int i; ++ ++ dev_info(dev->dev, "Apple ANS: snapshot %s state %d online %u allocated %u vectors %u recovery_used %u\n", ++ reason, nvme_ctrl_state(&dev->ctrl), dev->online_queues, ++ dev->ctrl.queue_count, dev->num_vecs, ++ test_bit(0, &dev->apple_ans_flags)); ++ apple_ans_host_dump(dev->dev, reason); ++ for (i = 0; i < dev->ctrl.queue_count; i++) { ++ struct nvme_queue *q = &dev->queues[i]; ++ u16 head = READ_ONCE(q->cq_head); ++ struct nvme_completion *cqe; ++ int irq = -1; ++ ++ if (!q->cqes || head >= q->q_depth) ++ continue; ++ cqe = &q->cqes[head]; ++ if (test_bit(NVMEQ_ENABLED, &q->flags) && ++ !test_bit(NVMEQ_POLLED, &q->flags)) ++ irq = pci_irq_vector(to_pci_dev(dev->dev), q->cq_vector); ++ dev_info(dev->dev, "Apple ANS: q%u flags %#lx depth %u SQ tail %u/%u CQ head %u phase %u next status %#x cid %#x sqhead %u vector %u irq %d ANS admin IRQ actions %u SQ %pad CQ %pad\n", ++ i, READ_ONCE(q->flags), q->q_depth, ++ READ_ONCE(q->sq_tail), READ_ONCE(q->last_sq_tail), ++ head, READ_ONCE(q->cq_phase), ++ le16_to_cpu(READ_ONCE(cqe->status)), ++ le16_to_cpu(READ_ONCE(cqe->command_id)), ++ le16_to_cpu(READ_ONCE(cqe->sq_head)), q->cq_vector, ++ irq, atomic_read(&dev->apple_ans_irq_count), ++ &q->sq_dma_addr, &q->cq_dma_addr); ++ } ++} ++ + static enum blk_eh_timer_return nvme_timeout(struct request *req) + { + struct nvme_iod *iod = blk_mq_rq_to_pdu(req); +@@ -1844,8 +1888,18 @@ static enum blk_eh_timer_return nvme_timeout(struct request *req) + struct request *abort_req; + struct nvme_command cmd = { }; + struct pci_dev *pdev = to_pci_dev(dev->dev); +- u32 csts = readl(dev->bar + NVME_REG_CSTS); ++ u32 csts = ~0U; + u8 opcode; ++ bool ans = dev->dma_ops == &nvme_pci_apple_ans_ops && ++ apple_ans_is_s8003(dev->dev); ++ ++ if (ans) { ++ nvme_apple_ans_dump(dev, "timeout before CSTS"); ++ if (dev->dma_ops->read_csts(dev, &csts)) ++ goto disable; ++ } else { ++ csts = readl(dev->bar + NVME_REG_CSTS); ++ } + + /* + * Shutdown the device immediately if we see it is disconnected. This +@@ -1921,6 +1975,11 @@ static enum blk_eh_timer_return nvme_timeout(struct request *req) + * returned to the driver, or if this is the admin queue. + */ + opcode = nvme_req(req)->cmd->common.opcode; ++ if (ans) { ++ if (test_and_set_bit(0, &dev->apple_ans_flags)) ++ nvme_change_ctrl_state(&dev->ctrl, NVME_CTRL_DELETING); ++ goto disable; ++ } + if (!nvmeq->qid || (iod->flags & IOD_ABORTED)) { + dev_warn(dev->ctrl.device, + "I/O tag %d (%04x) opcode %#x (%s) QID %d timeout, reset controller\n", +@@ -2804,7 +2863,32 @@ static umode_t nvme_pci_attrs_are_visible(struct kobject *kobj, + return a->mode; + } + ++static ssize_t apple_ans_diag_store(struct device *dev, ++ struct device_attribute *attr, ++ const char *buf, size_t count) ++{ ++ struct nvme_dev *ndev = to_nvme_dev(dev_get_drvdata(dev)); ++ u32 cc, csts; ++ ++ if (ndev->dma_ops != &nvme_pci_apple_ans_ops) ++ return -EOPNOTSUPP; ++ if (!sysfs_streq(buf, "host") && !sysfs_streq(buf, "endpoint")) ++ return -EINVAL; ++ mutex_lock(&ndev->shutdown_lock); ++ nvme_apple_ans_dump(ndev, "sysfs"); ++ if (sysfs_streq(buf, "endpoint") && apple_ans_link_up(ndev->dev) > 0) { ++ if (!ndev->dma_ops->read_csts(ndev, &csts)) { ++ cc = readl(ndev->bar + NVME_REG_CC); ++ dev_info(dev, "ANS endpoint CC %#x CSTS %#x\n", cc, csts); ++ } ++ } ++ mutex_unlock(&ndev->shutdown_lock); ++ return count; ++} ++static DEVICE_ATTR_WO(apple_ans_diag); ++ + static struct attribute *nvme_pci_attrs[] = { ++ &dev_attr_apple_ans_diag.attr, + &dev_attr_cmb.attr, + &dev_attr_cmbloc.attr, + &dev_attr_cmbsz.attr, +@@ -3283,11 +3367,69 @@ static bool nvme_pci_ctrl_is_dead(struct nvme_dev *dev) + return (csts & NVME_CSTS_CFS) || !(csts & NVME_CSTS_RDY); + } + +-static void nvme_dev_disable(struct nvme_dev *dev, bool shutdown) ++/* SPDX-License-Identifier: GPL-2.0-only */ ++static bool apple_ans_sleep_pair = true; ++module_param(apple_ans_sleep_pair, bool, 0644); ++static bool apple_ans_sleep_cc0_ok; ++module_param(apple_ans_sleep_cc0_ok, bool, 0644); ++ ++static int nvme_apple_ans_sleep_precheck(struct notifier_block *nb, ++ unsigned long action, void *unused) ++{ ++ if (action == PM_SUSPEND_TO_IDLE && ++ of_machine_is_compatible("apple,s8003") && ++ !READ_ONCE(apple_ans_sleep_pair)) ++ return notifier_from_errno(-EBUSY); ++ return NOTIFY_DONE; ++} ++ ++static struct notifier_block nvme_apple_ans_sleep_nb = { ++ .notifier_call = nvme_apple_ans_sleep_precheck, ++}; ++ ++/* SHST alone does not certify a stopped controller. Keep endpoint MMIO before ++ * clearing BME and retiring translations, as in suspend-r1c/r1d. */ ++static int nvme_apple_ans_sleep_disable(struct nvme_dev *dev, int shutdown_ret) ++{ ++ struct pci_dev *pdev = to_pci_dev(dev->dev); ++ u32 csts = ~0U, cc, expected; ++ int ret, err; ++ ++ ret = dev->dma_ops->read_csts(dev, &csts); ++ if (shutdown_ret || ret || (csts & NVME_CSTS_CFS) || ++ (csts & NVME_CSTS_SHST_MASK) != NVME_CSTS_SHST_CMPLT) { ++ pci_clear_master(pdev); ++ return -EBUSY; ++ } ++ dev->ctrl.ctrl_config &= ~(NVME_CC_ENABLE | NVME_CC_SHN_MASK); ++ expected = dev->ctrl.ctrl_config; ++ ret = dev->dma_ops->write_cc(dev, expected); ++ if (!ret) { ++ ret = read_poll_timeout(dev->dma_ops->read_csts, err, ++ err || !(csts & NVME_CSTS_RDY), ++ 1000, 2000000, false, dev, &csts); ++ if (!ret) ++ ret = err; ++ } ++ if (ret) ++ goto block_dma; ++ cc = readl(dev->bar + NVME_REG_CC); ++ if ((!kexec_in_progress && csts) || (csts & NVME_CSTS_CFS) || ++ (cc & (NVME_CC_ENABLE | NVME_CC_SHN_MASK)) || ++ (!kexec_in_progress && cc != expected && ++ !(READ_ONCE(apple_ans_sleep_cc0_ok) && !cc))) ++ ret = -EBUSY; ++block_dma: ++ pci_clear_master(pdev); ++ return ret; ++} ++ ++static int nvme_dev_disable_paired(struct nvme_dev *dev, bool shutdown, bool paired) + { + enum nvme_ctrl_state state = nvme_ctrl_state(&dev->ctrl); + struct pci_dev *pdev = to_pci_dev(dev->dev); + bool dead; ++ int ret = paired ? -EBUSY : 0; + + mutex_lock(&dev->shutdown_lock); + dead = nvme_pci_ctrl_is_dead(dev); +@@ -3298,15 +3440,26 @@ static void nvme_dev_disable(struct nvme_dev *dev, bool shutdown) + * Give the controller a chance to complete all entered requests + * if doing a safe shutdown. + */ +- if (!dead && shutdown) +- nvme_wait_freeze_timeout(&dev->ctrl); ++ if (!dead && shutdown && nvme_wait_freeze_timeout(&dev->ctrl) <= 0 && paired) ++ panic("Apple ANS: outgoing I/O did not drain; refusing kexec"); + } + + nvme_quiesce_io_queues(&dev->ctrl); + ++ if (paired && (dead || !dev->ctrl.queue_count)) ++ panic("Apple ANS: dead outgoing controller; refusing kexec"); + if (!dead && dev->ctrl.queue_count > 0) { ++ if (paired) { ++ nvme_sync_io_queues(&dev->ctrl); ++ ret = apple_ans_nvmmu_drain(dev->dev); ++ if (ret) ++ panic("Apple ANS: outgoing drain failed %d; refusing kexec", ret); ++ } + nvme_delete_io_queues(dev); +- nvme_disable_ctrl(&dev->ctrl, shutdown); ++ dev_info(dev->dev, "Apple ANS: shutdown starting NVMe SHN paired=%u\n", paired); ++ ret = nvme_disable_ctrl(&dev->ctrl, shutdown); ++ if (paired) ++ ret = nvme_apple_ans_sleep_disable(dev, ret); + nvme_poll_irqdisable(&dev->queues[0]); + } + nvme_suspend_io_queues(dev); +@@ -3329,7 +3482,15 @@ static void nvme_dev_disable(struct nvme_dev *dev, bool shutdown) + if (dev->ctrl.admin_q && !blk_queue_dying(dev->ctrl.admin_q)) + nvme_unquiesce_admin_queue(&dev->ctrl); + } ++ if (paired && !ret) ++ ret = apple_ans_sleep_stopped(dev->dev); + mutex_unlock(&dev->shutdown_lock); ++ return ret; ++} ++ ++static void nvme_dev_disable(struct nvme_dev *dev, bool shutdown) ++{ ++ nvme_dev_disable_paired(dev, shutdown, false); + } + + static int nvme_disable_prepare_reset(struct nvme_dev *dev, bool shutdown) +@@ -3775,7 +3936,10 @@ static int nvme_probe(struct pci_dev *pdev, const struct pci_device_id *id) + struct nvme_dev *dev; + int result = -ENOMEM; + ++ if (!nvme_pci_apple_ans_probe_allowed(pdev)) ++ return -EPROBE_DEFER; + dev = nvme_pci_alloc_dev(pdev, id); ++ + if (IS_ERR(dev)) + return PTR_ERR(dev); + +@@ -3920,6 +4084,12 @@ static void nvme_shutdown(struct pci_dev *pdev) + { + struct nvme_dev *dev = pci_get_drvdata(pdev); + ++ if (kexec_in_progress && apple_ans_is_s8003(dev->dev) && ++ dev->dma_ops == &nvme_pci_apple_ans_ops && !apple_ans_handoff_begin(dev->dev)) { ++ if (!nvme_wait_reset(&dev->ctrl) || nvme_dev_disable_paired(dev, true, true)) ++ panic("Apple ANS: refusing unsafe powered kexec handoff"); ++ return; ++ } + nvme_disable_prepare_reset(dev, true); + } + +@@ -3970,6 +4140,14 @@ static int nvme_resume(struct device *dev) + struct nvme_dev *ndev = pci_get_drvdata(to_pci_dev(dev)); + struct nvme_ctrl *ctrl = &ndev->ctrl; + ++ if (apple_ans_is_s8003(ndev->dev) && ndev->dma_ops == &nvme_pci_apple_ans_ops && ++ ndev->last_ps == U32_MAX) { ++ int ret = apple_ans_dart_set_active(ndev->dev, true); ++ ++ if (ret) ++ return ret; ++ } ++ + if (ndev->last_ps == U32_MAX || + nvme_set_power_state(ctrl, ndev->last_ps) != 0) + goto reset; +@@ -3989,6 +4167,19 @@ static int nvme_suspend(struct device *dev) + int ret = -EBUSY; + + ndev->last_ps = U32_MAX; ++ if (apple_ans_is_s8003(ndev->dev) && ndev->dma_ops == &nvme_pci_apple_ans_ops) { ++ if (!READ_ONCE(apple_ans_sleep_pair) || apple_ans_sleep_begin(ndev->dev)) ++ return -EBUSY; ++ if (nvme_ctrl_state(ctrl) != NVME_CTRL_LIVE || ++ !nvme_change_ctrl_state(ctrl, NVME_CTRL_RESETTING)) ++ return -EBUSY; ++ ret = nvme_dev_disable_paired(ndev, true, true); ++ if (ret) { ++ apple_ans_dart_set_active(ndev->dev, true); ++ nvme_try_sched_reset(ctrl); ++ } ++ return ret; ++ } + + /* + * The platform does not remove power for a kernel managed suspend so +@@ -4356,12 +4547,20 @@ static struct pci_driver nvme_driver = { + + static int __init nvme_init(void) + { ++ int ret; ++ + BUILD_BUG_ON(sizeof(struct nvme_create_cq) != 64); + BUILD_BUG_ON(sizeof(struct nvme_create_sq) != 64); + BUILD_BUG_ON(sizeof(struct nvme_delete_queue) != 64); + BUILD_BUG_ON(IRQ_AFFINITY_MAX_SETS < 2); + +- return pci_register_driver(&nvme_driver); ++ ret = register_pm_suspend_precheck_notifier(&nvme_apple_ans_sleep_nb); ++ if (ret) ++ return ret; ++ ret = pci_register_driver(&nvme_driver); ++ if (ret) ++ unregister_pm_suspend_precheck_notifier(&nvme_apple_ans_sleep_nb); ++ return ret; + } + + static void __exit nvme_exit(void) +@@ -4369,6 +4568,7 @@ static void __exit nvme_exit(void) + kfree(nvme_pci_quirk_list); + pci_unregister_driver(&nvme_driver); + flush_workqueue(nvme_wq); ++ unregister_pm_suspend_precheck_notifier(&nvme_apple_ans_sleep_nb); + } + + MODULE_AUTHOR("Matthew Wilcox "); +diff --git a/drivers/pci/controller/pcie-apple-h9p.c b/drivers/pci/controller/pcie-apple-h9p.c +index 15f7a0cbe..52a79fba6 100644 +--- a/drivers/pci/controller/pcie-apple-h9p.c ++++ b/drivers/pci/controller/pcie-apple-h9p.c +@@ -15,6 +15,8 @@ + + #include + #include ++#include ++#include + #include + #include + #include +@@ -38,12 +40,20 @@ + #include + #include + #include ++#include + #include + #include + #include ++#include ++#include ++#include ++#include ++#include ++#include + + #include + #include ++#include + + #include "pci-host-common.h" + +@@ -248,7 +258,7 @@ + #define S8003_PORT_LINKCDMSTS_LTSSM GENMASK(14, 9) + #define S8003_LTSSM_L0 0x11 + +-/* ADT perst-to-refclk-delay-us and pci-bridge t-refclk-to-perst (j71t) */ ++/* ADT perst-to-refclk-delay-us and pci-bridge t-refclk-to-perst (S8003) */ + #define S8003_PERST_TO_REFCLK_US 60 + #define S8003_REFCLK_TO_PERST_US 100 + #define S8003_PERST_HOLD_MS 100 +@@ -269,7 +279,7 @@ + + /* + * NVMMU invalidate FIFO status, in the port 0 block (iBoot sets the live ADT +- * nvme-mmu0 invalidate-fifo-workaround = 1 on j71t). iOS (AppleNVMeMMU, ++ * nvme-mmu0 invalidate-fifo-workaround = 1 on S8003). iOS (AppleNVMeMMU, + * 0xfffffff005eebc9c) waits up to 20 ms for it to drain before every + * invalidate and never reads the invalidate register back. + */ +@@ -429,7 +439,7 @@ static const struct apple_h9p_tunable t7000_port_tunables[] = { + }; + + /* +- * S8003 tunables, which iBoot puts in the live ADT; copied from the j71t ++ * S8003 tunables, which iBoot puts in the live ADT; copied from the S8003 + * live ADT (iOS 16.7.10 iBoot), where both bridges carry the same values. + * apcie-phy-tunables and apcie-common-tunables apply after the common init, + * in that order. +@@ -582,6 +592,7 @@ struct apple_h9p_tunables { + struct apple_h9p_nvmmu { + struct apple_h9p_pcie *pcie; + void __iomem *base; ++ bool poisoned; + /* Programmed: the ANS glue may map through it */ + bool ready; + /* Serializes the invalidate FIFO wait and the invalidate write */ +@@ -711,6 +722,8 @@ struct apple_h9p_pcie { + u32 late_ports; + unsigned int late_tries; + struct delayed_work late_work; ++ bool ans_sleep_stopped, ans_sleep_powered; ++ struct notifier_block sleep_precheck; + + struct apple_h9p_nvmmu nvmmu; + +@@ -1554,6 +1567,64 @@ static irqreturn_t apple_h9p_nvmmu_irq(int irq, void *data) + return IRQ_HANDLED; + } + ++static int apple_h9p_nvmmu_program(struct apple_h9p_pcie *pcie) ++{ ++ struct apple_h9p_nvmmu *nvmmu = &pcie->nvmmu; ++ u32 val; ++ int ret; ++ ++ if (pcie->hw->nvmmu_inv_fifo) { ++ /* ++ * iOS's S8003 order: the invalidate FIFO drained, the old SART ++ * aperture off before its address changes, the zeroed scratch ++ * page and TCBs visible, then scratch page, TCB base, init, SART. ++ */ ++ ret = readl_poll_timeout(pcie->base_port[0] + ++ S8003_PORT_NVMMU_INV_FIFO, val, ++ val & S8003_PORT_NVMMU_INV_FIFO_EMPTY, ++ 10, S8003_NVMMU_INV_FIFO_TIMEOUT_US); ++ if (ret) ++ return ret; ++ h9p_writel_flush(0, nvmmu->base + H9P_NVMMU_SART_CTRL); ++ dma_wmb(); ++ h9p_writel_flush(lower_32_bits(nvmmu->tcb_table_dma), ++ nvmmu->base + H9P_NVMMU_TCB_TABLE_LO); ++ h9p_writel_flush(upper_32_bits(nvmmu->tcb_table_dma), ++ nvmmu->base + H9P_NVMMU_TCB_TABLE_HI); ++ h9p_writel_flush(lower_32_bits(nvmmu->tcb_dma), ++ nvmmu->base + H9P_NVMMU_TCB_BASE_LO); ++ h9p_writel_flush(upper_32_bits(nvmmu->tcb_dma), ++ nvmmu->base + H9P_NVMMU_TCB_BASE_HI); ++ } else { ++ h9p_writel_flush(lower_32_bits(nvmmu->tcb_dma), ++ nvmmu->base + H9P_NVMMU_TCB_BASE_LO); ++ h9p_writel_flush(upper_32_bits(nvmmu->tcb_dma), ++ nvmmu->base + H9P_NVMMU_TCB_BASE_HI); ++ h9p_writel_flush(lower_32_bits(nvmmu->tcb_table_dma), ++ nvmmu->base + H9P_NVMMU_TCB_TABLE_LO); ++ h9p_writel_flush(upper_32_bits(nvmmu->tcb_table_dma), ++ nvmmu->base + H9P_NVMMU_TCB_TABLE_HI); ++ } ++ h9p_writel_flush(H9P_NVMMU_TCB_CTRL_INIT, ++ nvmmu->base + H9P_NVMMU_TCB_CTRL); ++ ++ ret = apple_h9p_wait(nvmmu->base + H9P_NVMMU_TCB_CTRL, ++ pcie->hw->nvmmu_init_busy, 0, 0, 250000); ++ if (ret) ++ return ret; ++ ++ h9p_writel_flush(nvmmu->va_base - 0x80000000U, ++ nvmmu->base + H9P_NVMMU_SART_VA_BASE); ++ h9p_writel_flush(round_up(nvmmu->va_base + nvmmu->size, ++ H9P_NVMMU_SART_ALIGNMENT) - 0x80100000U, ++ nvmmu->base + H9P_NVMMU_SART_VA_END); ++ h9p_writel_flush(nvmmu->pa_base >> 20, ++ nvmmu->base + H9P_NVMMU_SART_PA_BASE); ++ h9p_writel_flush(1, nvmmu->base + H9P_NVMMU_SART_CTRL); ++ ++ return 0; ++} ++ + static int apple_h9p_setup_nvmmu(struct apple_h9p_pcie *pcie) + { + struct apple_h9p_nvmmu *nvmmu = &pcie->nvmmu; +@@ -1561,7 +1632,7 @@ static int apple_h9p_setup_nvmmu(struct apple_h9p_pcie *pcie) + struct device_node *mem_np; + struct resource res; + resource_size_t size; +- u32 iova, val; ++ u32 iova; + int irq; + int ret; + +@@ -1630,55 +1701,10 @@ static int apple_h9p_setup_nvmmu(struct apple_h9p_pcie *pcie) + } + + spin_lock_init(&nvmmu->inv_lock); +- if (pcie->hw->nvmmu_inv_fifo) { +- /* +- * iOS's S8003 order: the invalidate FIFO drained, the old SART +- * aperture off before its address changes, the zeroed scratch +- * page and TCBs visible, then scratch page, TCB base, init, SART. +- */ +- ret = readl_poll_timeout(pcie->base_port[0] + +- S8003_PORT_NVMMU_INV_FIFO, val, +- val & S8003_PORT_NVMMU_INV_FIFO_EMPTY, +- 10, S8003_NVMMU_INV_FIFO_TIMEOUT_US); +- if (ret) +- goto out_put_node; +- h9p_writel_flush(0, nvmmu->base + H9P_NVMMU_SART_CTRL); +- dma_wmb(); +- h9p_writel_flush(lower_32_bits(nvmmu->tcb_table_dma), +- nvmmu->base + H9P_NVMMU_TCB_TABLE_LO); +- h9p_writel_flush(upper_32_bits(nvmmu->tcb_table_dma), +- nvmmu->base + H9P_NVMMU_TCB_TABLE_HI); +- h9p_writel_flush(lower_32_bits(nvmmu->tcb_dma), +- nvmmu->base + H9P_NVMMU_TCB_BASE_LO); +- h9p_writel_flush(upper_32_bits(nvmmu->tcb_dma), +- nvmmu->base + H9P_NVMMU_TCB_BASE_HI); +- } else { +- h9p_writel_flush(lower_32_bits(nvmmu->tcb_dma), +- nvmmu->base + H9P_NVMMU_TCB_BASE_LO); +- h9p_writel_flush(upper_32_bits(nvmmu->tcb_dma), +- nvmmu->base + H9P_NVMMU_TCB_BASE_HI); +- h9p_writel_flush(lower_32_bits(nvmmu->tcb_table_dma), +- nvmmu->base + H9P_NVMMU_TCB_TABLE_LO); +- h9p_writel_flush(upper_32_bits(nvmmu->tcb_table_dma), +- nvmmu->base + H9P_NVMMU_TCB_TABLE_HI); +- } +- h9p_writel_flush(H9P_NVMMU_TCB_CTRL_INIT, +- nvmmu->base + H9P_NVMMU_TCB_CTRL); +- +- ret = apple_h9p_wait(nvmmu->base + H9P_NVMMU_TCB_CTRL, +- pcie->hw->nvmmu_init_busy, 0, 0, 250000); ++ ret = apple_h9p_nvmmu_program(pcie); + if (ret) + goto out_put_node; + +- h9p_writel_flush(nvmmu->va_base - 0x80000000U, +- nvmmu->base + H9P_NVMMU_SART_VA_BASE); +- h9p_writel_flush(round_up(nvmmu->va_base + nvmmu->size, +- H9P_NVMMU_SART_ALIGNMENT) - 0x80100000U, +- nvmmu->base + H9P_NVMMU_SART_VA_END); +- h9p_writel_flush(nvmmu->pa_base >> 20, +- nvmmu->base + H9P_NVMMU_SART_PA_BASE); +- h9p_writel_flush(1, nvmmu->base + H9P_NVMMU_SART_CTRL); +- + irq = platform_get_irq_byname_optional(pcie->pdev, "nvmmu"); + if (irq > 0) { + ret = devm_request_irq(dev, irq, apple_h9p_nvmmu_irq, 0, +@@ -1726,6 +1752,7 @@ static int apple_h9p_nvmmu_invalidate(struct apple_h9p_pcie *pcie, + spin_unlock_irqrestore(&nvmmu->inv_lock, flags); + + if (ret) { ++ WRITE_ONCE(nvmmu->poisoned, true); + dev_err_ratelimited(pcie->dev, + "NVMMU invalidate FIFO not empty (%#x), tag %u\n", + val, tag); +@@ -1781,6 +1808,8 @@ int apple_ans_nvmmu_map(struct device *dev, unsigned int tag, + nvmmu = &pcie->nvmmu; + if (!nvmmu->ready) + return -EOPNOTSUPP; ++ if (READ_ONCE(nvmmu->poisoned)) ++ return -EIO; + + tcb = (u32 *)nvmmu->tcb + tag * H9P_NVMMU_TCB_DWORDS; + sgl = (u32 *)nvmmu->tcb_sgl + tag * H9P_NVMMU_SGL_WORDS; +@@ -2705,6 +2734,8 @@ static int apple_s8003_power_cycle_port(struct apple_h9p_pcie *pcie, + return ret; + } + ++ if (!port && pcie->nvmmu.base) ++ h9p_writel_flush(0, pcie->nvmmu.base + H9P_NVMMU_SART_CTRL); + apple_s8003_port_off(pcie, port); + usleep_range(S8003_POWER_OFF_SETTLE_US, S8003_POWER_OFF_SETTLE_US + 1000); + ret = apple_h9p_wait(rc + S8003_RC_PORT_CTL(port), +@@ -2887,6 +2918,81 @@ static int apple_s8003_start_link(struct apple_h9p_pcie *pcie, + return ret; + } + ++/* SPDX-License-Identifier: GPL-2.0-only */ ++/* S8003 adaptation of suspend-r1d/r1e/r1f to Orchard's host driver. */ ++static unsigned int h9p_suspend; ++module_param_named(suspend, h9p_suspend, uint, 0644); ++static bool ans_keep_power; ++module_param(ans_keep_power, bool, 0644); ++static char *ans_boot_mode = "handoff"; ++module_param(ans_boot_mode, charp, 0444); ++static unsigned int ans_sleep_flush = 3; ++module_param(ans_sleep_flush, uint, 0644); ++static unsigned int ans_kexec_flush; ++module_param(ans_kexec_flush, uint, 0644); ++ ++static bool apple_s8003_handoff_enabled(void) ++{ ++ return READ_ONCE(ans_keep_power) && !strcmp(ans_boot_mode, "handoff"); ++} ++ ++static bool apple_s8003_port_powered(struct apple_h9p_pcie *pcie) ++{ ++ void __iomem *rc = pcie->base_rc; ++ ++ return (readl(rc + S8003_RC_PORT_CTL(0)) & S8003_RC_PORT_CTL_ENABLE) && ++ (readl(rc + S8003_RC_PORT_PWR(0)) & S8003_RC_PORT_PWR_ON) && ++ (readl(rc + S8003_RC_PORT_REFCLK(0)) & S8003_RC_PORT_REFCLK_ON) && ++ !(readl(rc + S8003_RC_PORT_CTL(0)) & S8003_RC_PORT_CTL_RESET) && ++ (readl(rc + S8003_RC_PORT_PWR(0)) & S8003_RC_PORT_PWR_REFCLK_NOGATE) && ++ (readl(rc + S8003_RC_PORT_READY(0)) & S8003_RC_PORT_READY_DONE); ++} ++ ++/* Best effort, bounded, while PERST is asserted. Never cycle port power. */ ++static void apple_s8003_drain_link(struct apple_h9p_pcie *pcie, unsigned int flush) ++{ ++ u32 val; ++ int ret; ++ ++ if (!(flush & 1)) ++ return; ++ ret = readl_poll_timeout(pcie->base_port[0] + S8003_PORT_LINKSTS, val, ++ !(val & BIT(0)), 10, 100000); ++ dev_info(pcie->dev, "ANS link drain: status=%#x drained=%u\n", val, !ret); ++} ++ ++static int apple_s8003_flush_link(struct apple_h9p_pcie *pcie, unsigned int flush) ++{ ++ u32 val; ++ ++ if ((flush & 2) && (readl(pcie->base_port[0] + S8003_PORT_LINKSTS) & BIT(0))) { ++ h9p_rmw(pcie->base_port[0] + H9P_PORT_LTSSMCTL, 0, H9P_PORT_LTSSM_ENABLE); ++ readl_poll_timeout(pcie->base_port[0] + S8003_PORT_LINKSTS, val, ++ !(val & BIT(0)), 10, 100000); ++ return apple_s8003_port_stop(pcie, 0); ++ } ++ return 0; ++} ++ ++static int apple_s8003_powered_reset(struct apple_h9p_pcie *pcie, unsigned int flush) ++{ ++ int ret; ++ ++ if (!apple_s8003_port_powered(pcie)) ++ return -EBUSY; ++ ret = gpiod_direction_output(pcie->perst[0], 1); ++ if (!ret) ++ ret = apple_s8003_port_stop(pcie, 0); ++ if (!ret) ++ ret = apple_s8003_flush_link(pcie, flush); ++ if (!ret) ++ ret = apple_s8003_start_link(pcie, 0); ++ /* A failed powered reset stays under PERST; no full-cycle fallback. */ ++ if (ret) ++ gpiod_direction_output(pcie->perst[0], 1); ++ return ret; ++} ++ + /* Power the port up as validated for its kind, and train its link once */ + static int apple_s8003_setup_port_once(struct apple_h9p_pcie *pcie, + unsigned int port) +@@ -2902,6 +3008,15 @@ static int apple_s8003_setup_port_once(struct apple_h9p_pcie *pcie, + * validated on it, the other ports the restart sequence that is the + * only one validated on the Wi-Fi port. + */ ++ if (!port && apple_s8003_handoff_enabled() && ++ of_property_read_bool(of_chosen, "linux,booted-from-kexec") && ++ apple_s8003_port_powered(pcie) && ++ gpiod_get_value_cansleep(pcie->perst[0]) == 1) { ++ ret = apple_s8003_powered_reset(pcie, READ_ONCE(ans_kexec_flush)); ++ if (!ret || (ret != -ETIMEDOUT && ret != -ENOLINK)) ++ return ret; ++ dev_warn(pcie->dev, "powered reset failed %d; full port fallback\n", ret); ++ } + if (pcie->hw->nvme_mode_ports & BIT(port)) + ret = apple_s8003_power_cycle_port(pcie, port); + else +@@ -2945,6 +3060,8 @@ static int apple_s8003_setup_storage_port(struct apple_h9p_pcie *pcie, + if (ret != -ETIMEDOUT && ret != -ENOLINK) + return ret; + ++ /* Probe/kexec may fall back to full reset; sleep resume never uses this. */ ++ + dev_warn(pcie->dev, "port %u: no link (%d), power cycling the port\n", + port, ret); + if ((readl(rc + S8003_RC_PORT_CTL(port)) & S8003_RC_PORT_CTL_ENABLE) && +@@ -3059,6 +3176,11 @@ static void apple_s8003_late_ports_work(struct work_struct *work) + unsigned int port; + int ret; + ++ /* Lab 0019: no secondary bus publication without storage bus 1. */ ++ if (of_device_is_compatible(pcie->dev->of_node, "apple,s8003-pcie") && ++ !(READ_ONCE(pcie->enabled_ports) & BIT(0))) ++ return; ++ + for_each_set_bit(port, &pending, H9P_NUM_PORTS) { + ret = apple_s8003_setup_endpoint_port(pcie, port); + if (ret == -EPROBE_DEFER) { +@@ -3418,6 +3540,472 @@ static void apple_h9p_pcie_pwrctrl_cleanup(void *data) + pci_pwrctrl_destroy_devices(pcie->dev); + } + ++/* SPDX-License-Identifier: GPL-2.0-only */ ++static struct apple_h9p_pcie *apple_ans_endpoint_host(struct device *dev) ++{ ++ struct device *host = dev; ++ struct apple_h9p_pcie *pcie; ++ ++ while (host && host->bus == dev->bus) ++ host = host->parent; ++ if (!host || !host->parent) ++ return NULL; ++ pcie = apple_h9p_pcie_lookup(host->parent); ++ if (!pcie || !of_device_is_compatible(pcie->dev->of_node, "apple,s8003-pcie") || ++ apple_h9p_pcie_device_port(pcie, dev) != 0) ++ return NULL; ++ return pcie; ++} ++ ++int apple_ans_sleep_begin(struct device *dev) ++{ ++ struct apple_h9p_pcie *pcie = apple_ans_endpoint_host(dev); ++ ++ if (!pcie || h9p_suspend != 2 || !apple_s8003_handoff_enabled()) ++ return -EBUSY; ++ WRITE_ONCE(pcie->ans_sleep_stopped, false); ++ return 0; ++} ++EXPORT_SYMBOL_GPL(apple_ans_sleep_begin); ++ ++/* release6: caller froze and synchronized I/O, before SHN. */ ++/* Lab 0018: caller synchronized queues and stopped CC/RDY before this. */ ++int apple_ans_nvmmu_reset(struct device *dev) ++{ ++ struct apple_h9p_pcie *pcie = apple_ans_endpoint_host(dev); ++ struct apple_h9p_nvmmu *nvmmu; ++ int ret; ++ ++ if (!pcie || !(pcie->enabled_ports & BIT(0))) ++ return -ENODEV; ++ nvmmu = &pcie->nvmmu; ++ if (!nvmmu->ready) ++ return -ENODEV; ++ memset(nvmmu->tcb, 0, nvmmu->tcb_size); ++ memset(nvmmu->tcb_table, 0, nvmmu->tcb_table_size); ++ memset(nvmmu->tcb_sgl, 0, nvmmu->tcb_sgl_size); ++ ret = apple_h9p_nvmmu_program(pcie); ++ WRITE_ONCE(nvmmu->poisoned, !!ret); ++ if (ret) ++ return ret; ++ bitmap_zero(nvmmu->mapped, APPLE_ANS_NVMMU_MAX_REQS); ++ dev_info(dev, "NVMMU: stopped-controller cache/SART rebuilt\n"); ++ return 0; ++} ++EXPORT_SYMBOL_GPL(apple_ans_nvmmu_reset); ++ ++bool apple_ans_is_s8003(struct device *dev) ++{ ++ return apple_ans_endpoint_host(dev) != NULL; ++} ++EXPORT_SYMBOL_GPL(apple_ans_is_s8003); ++ ++int apple_ans_link_up(struct device *dev) ++{ ++ struct apple_h9p_pcie *pcie = apple_ans_endpoint_host(dev); ++ ++ if (!pcie) ++ return -ENODEV; ++ if (!(READ_ONCE(pcie->enabled_ports) & BIT(0))) ++ return -ENOLINK; ++ return !!(readl(pcie->base_port[0] + S8003_PORT_LINKSTS) & ++ S8003_PORT_LINKSTS_UP); ++} ++EXPORT_SYMBOL_GPL(apple_ans_link_up); ++ ++/* Host-only evidence, no endpoint config/BAR reads or invalidate register. */ ++void apple_ans_host_dump(struct device *dev, const char *reason) ++{ ++ struct apple_h9p_pcie *pcie = apple_ans_endpoint_host(dev); ++ ++ if (!pcie) ++ return; ++ dev_info(dev, "ANS host %s: ports %#x link %#x LTSSM %#x FIFO %#x poisoned %u mapped %*pb\n", ++ reason, pcie->enabled_ports, ++ readl(pcie->base_port[0] + H9P_PORT_LINKSTS), ++ readl(pcie->base_port[0] + H9P_PORT_LTSSMCTL), ++ readl(pcie->base_port[0] + S8003_PORT_NVMMU_INV_FIFO), ++ READ_ONCE(pcie->nvmmu.poisoned), APPLE_ANS_NVMMU_MAX_REQS, ++ pcie->nvmmu.mapped); ++} ++EXPORT_SYMBOL_GPL(apple_ans_host_dump); ++ ++int apple_ans_nvmmu_drain(struct device *dev) ++{ ++ struct apple_h9p_pcie *pcie = apple_ans_endpoint_host(dev); ++ u32 fifo; ++ int ret; ++ ++ if (!pcie || !pcie->nvmmu.ready) ++ return -ENODEV; ++ if (!bitmap_empty(pcie->nvmmu.mapped, APPLE_ANS_NVMMU_MAX_REQS)) ++ return -EBUSY; ++ ret = readl_poll_timeout(pcie->base_port[0] + S8003_PORT_NVMMU_INV_FIFO, ++ fifo, fifo & S8003_PORT_NVMMU_INV_FIFO_EMPTY, ++ 10, S8003_NVMMU_INV_FIFO_TIMEOUT_US); ++ dev_info(dev, "Apple ANS: outgoing NVMMU FIFO empty check (20ms) result %d\n", ret); ++ return ret; ++} ++EXPORT_SYMBOL_GPL(apple_ans_nvmmu_drain); ++ ++int apple_ans_sleep_stopped(struct device *dev) ++{ ++ struct apple_h9p_pcie *pcie = apple_ans_endpoint_host(dev); ++ unsigned int tag; ++ u32 fifo; ++ int ret; ++ ++ if (!pcie) ++ return -ENODEV; ++ /* All queues are drained and BME is clear before retiring translations. */ ++ for (tag = 0; tag < APPLE_ANS_NVMMU_MAX_REQS; tag++) { ++ ret = apple_ans_nvmmu_map(dev, tag, NULL, 0, false, NULL); ++ if (ret) ++ return ret; ++ } ++ /* The last invalidate also has to leave the FIFO before certification. */ ++ ret = readl_poll_timeout_atomic(pcie->base_port[0] + S8003_PORT_NVMMU_INV_FIFO, ++ fifo, fifo & S8003_PORT_NVMMU_INV_FIFO_EMPTY, ++ 1, S8003_NVMMU_INV_FIFO_TIMEOUT_US); ++ if (ret) ++ return ret; ++ ret = apple_ans_dart_set_active(dev, false); ++ if (!ret) ++ WRITE_ONCE(pcie->ans_sleep_stopped, true); ++ return ret; ++} ++EXPORT_SYMBOL_GPL(apple_ans_sleep_stopped); ++ ++int apple_ans_handoff_begin(struct device *dev) ++{ ++ struct apple_h9p_pcie *pcie = apple_ans_endpoint_host(dev); ++ ++ if (!pcie || strcmp(ans_boot_mode, "handoff")) ++ return -EBUSY; ++ WRITE_ONCE(pcie->ans_sleep_stopped, false); ++ return 0; ++} ++EXPORT_SYMBOL_GPL(apple_ans_handoff_begin); ++ ++static void apple_h9p_s8003_shutdown(struct platform_device *pdev) ++{ ++ struct apple_h9p_pcie *pcie = apple_h9p_pcie_lookup(&pdev->dev); ++ unsigned int port; ++ ++ if (!kexec_in_progress || !pcie || ++ !of_device_is_compatible(pdev->dev.of_node, "apple,s8003-pcie")) ++ return; ++ cancel_delayed_work_sync(&pcie->late_work); ++ if (!strcmp(ans_boot_mode, "handoff") && ++ (!READ_ONCE(pcie->ans_sleep_stopped) || ++ (READ_ONCE(ans_keep_power) && !apple_s8003_port_powered(pcie)))) ++ panic("Apple ANS: refusing uncertified powered handoff"); ++ for (port = 0; port < H9P_NUM_PORTS; port++) { ++ void __iomem *cfg = pcie->base_config + port * H9P_CFG_PORT_STRIDE; ++ ++ if (!(pcie->enabled_ports & BIT(port))) ++ continue; ++ /* release6: block root routing, retain refclk, then isolate. */ ++ writew(0, cfg + PCI_COMMAND); ++ h9p_rmw(cfg + PCI_PRIMARY_BUS, GENMASK(23, 8), 0); ++ h9p_rmw(pcie->base_rc + S8003_RC_PORT_PWR(port), 0, ++ S8003_RC_PORT_PWR_REFCLK_NOGATE); ++ if (gpiod_direction_output(pcie->perst[port], 1)) ++ panic("Apple PCIe: PERST failed during handoff"); ++ if (!port && apple_s8003_handoff_enabled()) ++ apple_s8003_drain_link(pcie, READ_ONCE(ans_kexec_flush)); ++ if (apple_s8003_port_stop(pcie, port)) ++ panic("Apple PCIe: LTSSM stop failed during handoff"); ++ udelay(S8003_PERST_TO_REFCLK_US); ++ if (!port && pcie->nvmmu.ready) { ++ h9p_writel_flush(0, pcie->nvmmu.base + H9P_NVMMU_SART_CTRL); ++ pcie->nvmmu.ready = false; ++ } ++ if (!port && !strcmp(ans_boot_mode, "handoff") && ++ !READ_ONCE(ans_keep_power)) ++ apple_s8003_port_off(pcie, port); ++ if (!port && apple_s8003_handoff_enabled() && ++ (!apple_s8003_port_powered(pcie) || ++ gpiod_get_value_cansleep(pcie->perst[0]) != 1)) ++ panic("Apple ANS: outgoing powered port certificate failed"); ++ pcie->enabled_ports &= ~BIT(port); ++ } ++ dev_info(pcie->dev, "kexec: PCIe PERST asserted, LTSSM stopped, ports powered; NVMMU SART disabled\n"); ++} ++ ++static void apple_h9p_capture_park(struct apple_h9p_pcie *pcie) ++{ ++ void __iomem *cfg = pcie->base_config; ++ ++ cancel_delayed_work_sync(&pcie->late_work); ++ if (!(pcie->enabled_ports & BIT(0))) ++ return; ++ writew(0, cfg + PCI_COMMAND); ++ h9p_rmw(cfg + PCI_PRIMARY_BUS, GENMASK(23, 8), 0); ++ h9p_rmw(pcie->base_rc + S8003_RC_PORT_PWR(0), 0, ++ S8003_RC_PORT_PWR_REFCLK_NOGATE); ++ if (gpiod_direction_output(pcie->perst[0], 1) || ++ apple_s8003_port_stop(pcie, 0)) ++ panic("ANS capture isolation failed"); ++ udelay(S8003_PERST_TO_REFCLK_US); ++ if (pcie->nvmmu.base) ++ h9p_writel_flush(0, pcie->nvmmu.base + H9P_NVMMU_SART_CTRL); ++ pcie->nvmmu.ready = false; ++ pcie->enabled_ports &= ~BIT(0); ++} ++ ++struct apple_h9p_crash_state { ++ struct apple_h9p_pcie *pcie; ++ int result; ++ bool busy; ++ bool found; ++}; ++ ++/* No NVMe driver, queues, SHN, or old DMA buffers are needed in phoenix. */ ++static int apple_h9p_crash_stop_ans(struct pci_dev *pdev, void *data) ++{ ++ struct apple_h9p_crash_state *state = data; ++ struct apple_h9p_pcie *pcie = state->pcie; ++ void __iomem *bar; ++ u32 csts = 0; ++ int i, ret; ++ ++ if (pdev->vendor != PCI_VENDOR_ID_APPLE || ++ ((pdev->class >> 8) != PCI_CLASS_STORAGE_EXPRESS && ++ (pdev->class >> 8) != PCI_CLASS_STORAGE_OTHER)) ++ return 0; ++ state->found = true; ++ if (pdev->driver) { ++ state->busy = true; ++ return 1; ++ } ++ if (apple_ans_link_up(&pdev->dev) <= 0) { ++ state->result = -ENOLINK; ++ return 0; ++ } ++ ret = pci_enable_device_mem(pdev); ++ if (ret) { ++ state->result = ret; ++ return 0; ++ } ++ bar = pci_iomap_range(pdev, 0, 0, SZ_8K); ++ if (!bar) { ++ ret = -ENOMEM; ++ goto disable; ++ } ++ /* An unknown queue must never be rung or sent a shutdown notification. */ ++ dev_info(pcie->dev, "phoenix ANS: CC %#x CSTS %#x, clearing EN without SHN\n", ++ readl(bar + NVME_REG_CC), readl(bar + NVME_REG_CSTS)); ++ writel(0, bar + NVME_REG_CC); ++ ret = -ETIMEDOUT; ++ for (i = 0; i < 2000; i++) { ++ if (apple_ans_link_up(&pdev->dev) <= 0) { ++ ret = -ENOLINK; ++ break; ++ } ++ csts = readl(bar + NVME_REG_CSTS); ++ if (csts == U32_MAX) { ++ ret = -EIO; ++ break; ++ } ++ if (!(csts & NVME_CSTS_RDY)) { ++ ret = 0; ++ break; ++ } ++ usleep_range(1000, 1100); ++ } ++ dev_info(pcie->dev, "phoenix ANS: disable result %d CSTS %#x; blocking bus master\n", ++ ret, csts); ++ pci_iounmap(pdev, bar); ++disable: ++ pci_clear_master(pdev); ++ pci_disable_device(pdev); ++ state->result = ret; ++ return 0; ++} ++ ++static ssize_t crash_quiesce_store(struct device *dev, ++ struct device_attribute *attr, ++ const char *buf, size_t count) ++{ ++ struct apple_h9p_pcie *pcie = apple_h9p_pcie_lookup(dev); ++ struct apple_h9p_crash_state state = { .pcie = pcie }; ++ ++ if (!pcie || !is_kdump_kernel() || ++ !of_device_is_compatible(dev->of_node, "apple,s8003-pcie") || ++ !sysfs_streq(buf, "1")) ++ return -EPERM; ++ /* The endpoint gate must stay closed; don't park under a live driver. */ ++ if (pcie->late_ports || work_busy(&pcie->late_work.work)) ++ return -EBUSY; ++ pci_walk_bus(pcie->bridge->bus, apple_h9p_crash_stop_ans, &state); ++ if (state.busy) ++ return -EBUSY; ++ /* Isolation is still required after a bounded RDY timeout. */ ++ /* Wi-Fi stays linked until its own shutdown callback has run. */ ++ apple_h9p_capture_park(pcie); ++ dev_info(dev, "phoenix: ANS/PCIe quiesced (controller found %d result %d)\n", ++ state.found, state.result); ++ return count; ++} ++static DEVICE_ATTR_WO(crash_quiesce); ++ ++static struct attribute *apple_h9p_crash_attrs[] = { ++ &dev_attr_crash_quiesce.attr, ++ NULL, ++}; ++ ++static const struct attribute_group apple_h9p_crash_group = { ++ .attrs = apple_h9p_crash_attrs, ++}; ++ ++ ++/* Orchestrator-only fault injection; never enabled by a normal boot. */ ++static bool panic_resume_once; ++module_param(panic_resume_once, bool, 0600); ++MODULE_PARM_DESC(panic_resume_once, ++ "S8003: deliberately panic before one PCIe resume, only with kdump armed"); ++ ++static void apple_h9p_crash_resume_test(struct device *dev) ++{ ++ if (!of_device_is_compatible(dev->of_node, "apple,s8003-pcie") || ++ !READ_ONCE(panic_resume_once)) ++ return; ++ WRITE_ONCE(panic_resume_once, false); ++ if (!kexec_crash_loaded()) { ++ dev_warn(dev, "resume panic test refused: no capture kernel armed\n"); ++ return; ++ } ++ panic("S8003: deliberate panic before PCIe resume"); ++} ++ ++ ++static bool apple_h9p_sleep_consumer_busy(void) ++{ ++ struct device_node *np; ++ struct platform_device *pdev; ++ struct usb_role_switch *sw; ++ struct path root; ++ bool busy; ++ ++ get_fs_root(current->fs, &root); ++ busy = root.dentry->d_sb->s_bdev && ++ MAJOR(root.dentry->d_sb->s_bdev->bd_dev) == NBD_MAJOR; ++ path_put(&root); ++ ++ np = of_find_compatible_node(NULL, NULL, "gpu,s8003"); ++ if (np) { ++ pdev = of_find_device_by_node(np); ++ if (pdev) { ++ busy |= pdev->dev.driver && pm_runtime_active(&pdev->dev); ++ put_device(&pdev->dev); ++ } ++ of_node_put(np); ++ } ++ np = of_find_compatible_node(NULL, NULL, "apple,s8000-usb-role-switch"); ++ if (np) { ++ sw = usb_role_switch_find_by_fwnode(of_fwnode_handle(np)); ++ if (!IS_ERR_OR_NULL(sw)) { ++ busy |= usb_role_switch_get_role(sw) == USB_ROLE_HOST; ++ usb_role_switch_put(sw); ++ } ++ of_node_put(np); ++ } ++ return busy; ++} ++ ++static int apple_h9p_sleep_precheck(struct notifier_block *nb, ++ unsigned long action, void *unused) ++{ ++ struct apple_h9p_pcie *pcie = container_of(nb, struct apple_h9p_pcie, ++ sleep_precheck); ++ ++ if (action != PM_SUSPEND_TO_IDLE) ++ return NOTIFY_DONE; ++ if (h9p_suspend != 2 || !apple_s8003_handoff_enabled() || ++ apple_h9p_sleep_consumer_busy()) { ++ dev_warn(pcie->dev, "sleep refused before sync: suspend=2 and ans_keep_power=1 ans_boot_mode=handoff required; active GPU/USB host or NBD root forbidden\n"); ++ return notifier_from_errno(-EBUSY); ++ } ++ return NOTIFY_DONE; ++} ++ ++static void apple_h9p_sleep_unregister(void *data) ++{ ++ struct apple_h9p_pcie *pcie = data; ++ ++ unregister_pm_suspend_precheck_notifier(&pcie->sleep_precheck); ++} ++ ++static int apple_h9p_sleep_register(struct apple_h9p_pcie *pcie) ++{ ++ int ret; ++ ++ if (!of_device_is_compatible(pcie->dev->of_node, "apple,s8003-pcie")) ++ return 0; ++ pcie->sleep_precheck.notifier_call = apple_h9p_sleep_precheck; ++ ret = register_pm_suspend_precheck_notifier(&pcie->sleep_precheck); ++ if (ret) ++ return ret; ++ return devm_add_action_or_reset(pcie->dev, apple_h9p_sleep_unregister, pcie); ++} ++ ++static int apple_h9p_s8003_suspend(struct device *dev) ++{ ++ struct apple_h9p_pcie *pcie = apple_h9p_pcie_lookup(dev); ++ int ret; ++ ++ if (!of_device_is_compatible(dev->of_node, "apple,s8003-pcie")) ++ return 0; ++ if (h9p_suspend != 2 || !apple_s8003_handoff_enabled() || ++ !READ_ONCE(pcie->ans_sleep_stopped) || !apple_s8003_port_powered(pcie)) ++ return -EBUSY; ++ cancel_delayed_work_sync(&pcie->late_work); ++ ret = gpiod_direction_output(pcie->perst[0], 1); ++ if (ret) ++ return ret; ++ apple_s8003_drain_link(pcie, READ_ONCE(ans_sleep_flush)); ++ ret = apple_s8003_port_stop(pcie, 0); ++ if (ret) { ++ /* PM skips resume for a parent whose suspend failed. Restore now. */ ++ apple_s8003_powered_reset(pcie, READ_ONCE(ans_sleep_flush)); ++ return ret; ++ } ++ pcie->ans_sleep_powered = true; ++ dev_info(dev, "ANS sleep powered under PERST; no full-cycle fallback\n"); ++ return 0; ++} ++ ++static int apple_h9p_s8003_resume(struct device *dev) ++{ ++ struct apple_h9p_pcie *pcie = apple_h9p_pcie_lookup(dev); ++ int ret; ++ ++ apple_h9p_crash_resume_test(dev); ++ ++ if (!of_device_is_compatible(dev->of_node, "apple,s8003-pcie") || ++ !pcie->ans_sleep_powered) ++ return 0; ++ ret = apple_s8003_powered_reset(pcie, READ_ONCE(ans_sleep_flush)); ++ pcie->ans_sleep_powered = false; ++ WRITE_ONCE(pcie->ans_sleep_stopped, false); ++ if (ret) ++ dev_err(dev, "ANS powered resume failed %d; endpoint isolated under PERST\n", ret); ++ else ++ dev_info(dev, "ANS powered resume trained, try=first\n"); ++ return ret; ++} ++ ++static DEFINE_SIMPLE_DEV_PM_OPS(apple_h9p_s8003_pm_ops, ++ apple_h9p_s8003_suspend, apple_h9p_s8003_resume); ++ ++static bool crash_quiesce_supported = true; ++module_param(crash_quiesce_supported, bool, 0444); ++ ++static bool h9p_probe = !IS_ENABLED(CONFIG_NVME_APPLE_ANS_EARLY_USB); ++module_param_named(probe, h9p_probe, bool, 0644); ++MODULE_PARM_DESC(probe, "Allow S8003 PCIe probe after USB rescue is ready"); ++ + static int apple_h9p_pcie_probe(struct platform_device *pdev) + { + struct device *dev = &pdev->dev; +@@ -3426,6 +4014,9 @@ static int apple_h9p_pcie_probe(struct platform_device *pdev) + u32 doorbell; + int ret; + ++ if (!READ_ONCE(h9p_probe) && of_device_is_compatible(dev->of_node, "apple,s8003-pcie")) ++ return -EPROBE_DEFER; ++ + bridge = devm_pci_alloc_host_bridge(dev, sizeof(*pcie)); + if (!bridge) + return -ENOMEM; +@@ -3578,6 +4169,16 @@ static int apple_h9p_pcie_probe(struct platform_device *pdev) + if (ret) + return ret; + ++ if (is_kdump_kernel() && of_device_is_compatible(pcie->dev->of_node, "apple,s8003-pcie")) { ++ ret = devm_device_add_group(dev, &apple_h9p_crash_group); ++ if (ret) ++ return ret; ++ } ++ ++ ret = apple_h9p_sleep_register(pcie); ++ if (ret) ++ return ret; ++ + if (pcie->late_ports) + schedule_delayed_work(&pcie->late_work, + msecs_to_jiffies(S8003_LATE_PORT_RETRY_MS)); +@@ -3660,8 +4261,10 @@ MODULE_DEVICE_TABLE(of, apple_h9p_pcie_of_match); + + static struct platform_driver apple_h9p_pcie_driver = { + .probe = apple_h9p_pcie_probe, ++ .shutdown = apple_h9p_s8003_shutdown, + .driver = { + .name = "pcie-apple-h9p", ++ .pm = pm_sleep_ptr(&apple_h9p_s8003_pm_ops), + .of_match_table = apple_h9p_pcie_of_match, + .suppress_bind_attrs = true, + }, +diff --git a/include/linux/apple-ans.h b/include/linux/apple-ans.h +index a59861010..788a343ee 100644 +--- a/include/linux/apple-ans.h ++++ b/include/linux/apple-ans.h +@@ -17,11 +17,36 @@ struct device; + * Map @npages pages for FlatDMA request @tag, or retire the tag if @npages is + * zero. @to_memory: the controller writes the pages (an NVMe read). + */ ++#if IS_REACHABLE(CONFIG_APPLE_DART) ++int apple_ans_dart_set_active(struct device *dev, bool active); ++#else ++static inline int apple_ans_dart_set_active(struct device *dev, bool active) ++{ ++ return -EOPNOTSUPP; ++} ++#endif ++ + #if IS_REACHABLE(CONFIG_PCIE_APPLE_H9P) ++int apple_ans_sleep_begin(struct device *dev); ++int apple_ans_handoff_begin(struct device *dev); ++int apple_ans_sleep_stopped(struct device *dev); ++int apple_ans_nvmmu_drain(struct device *dev); ++int apple_ans_nvmmu_reset(struct device *dev); ++int apple_ans_link_up(struct device *dev); ++bool apple_ans_is_s8003(struct device *dev); ++void apple_ans_host_dump(struct device *dev, const char *reason); + int apple_ans_nvmmu_map(struct device *dev, unsigned int tag, + const u64 *pages, unsigned int npages, + bool to_memory, dma_addr_t *iova); + #else ++static inline int apple_ans_sleep_begin(struct device *dev) { return -EBUSY; } ++static inline int apple_ans_handoff_begin(struct device *dev) { return -EBUSY; } ++static inline int apple_ans_sleep_stopped(struct device *dev) { return -EBUSY; } ++static inline int apple_ans_nvmmu_drain(struct device *dev) { return -EBUSY; } ++static inline int apple_ans_nvmmu_reset(struct device *dev) { return -EOPNOTSUPP; } ++static inline int apple_ans_link_up(struct device *dev) { return 1; } ++static inline bool apple_ans_is_s8003(struct device *dev) { return false; } ++static inline void apple_ans_host_dump(struct device *dev, const char *reason) {} + static inline int apple_ans_nvmmu_map(struct device *dev, unsigned int tag, + const u64 *pages, unsigned int npages, + bool to_memory, dma_addr_t *iova) +diff --git a/include/linux/suspend.h b/include/linux/suspend.h +index b02876f1a..0a4719b20 100644 +--- a/include/linux/suspend.h ++++ b/include/linux/suspend.h +@@ -449,6 +449,15 @@ void restore_processor_state(void); + /* kernel/power/main.c */ + extern int register_pm_notifier(struct notifier_block *nb); + extern int unregister_pm_notifier(struct notifier_block *nb); ++/** ++ * register_pm_suspend_precheck_notifier - register an early suspend veto ++ * @nb: notifier whose action is the target suspend_state_t ++ * ++ * Prechecks run before filesystem sync and console preparation. They must ++ * only validate current policy/state and return; they have no post callback. ++ */ ++int register_pm_suspend_precheck_notifier(struct notifier_block *nb); ++int unregister_pm_suspend_precheck_notifier(struct notifier_block *nb); + extern void ksys_sync_helper(void); + extern void pm_report_hw_sleep_time(u64 t); + extern void pm_report_max_hw_sleep(u64 t); +@@ -498,6 +507,16 @@ static inline int unregister_pm_notifier(struct notifier_block *nb) + return 0; + } + ++static inline int register_pm_suspend_precheck_notifier(struct notifier_block *nb) ++{ ++ return 0; ++} ++ ++static inline int unregister_pm_suspend_precheck_notifier(struct notifier_block *nb) ++{ ++ return 0; ++} ++ + static inline void pm_report_hw_sleep_time(u64 t) {}; + static inline void pm_report_max_hw_sleep(u64 t) {}; + +diff --git a/kernel/kexec_core.c b/kernel/kexec_core.c +index dc770b9a6..819650bcd 100644 +--- a/kernel/kexec_core.c ++++ b/kernel/kexec_core.c +@@ -53,6 +53,7 @@ atomic_t __kexec_lock = ATOMIC_INIT(0); + + /* Flag to indicate we are going to kexec a new kernel */ + bool kexec_in_progress = false; ++EXPORT_SYMBOL_GPL(kexec_in_progress); + + bool kexec_file_dbg_print; + +diff --git a/kernel/power/main.c b/kernel/power/main.c +index 5429e9f19..ae36afe43 100644 +--- a/kernel/power/main.c ++++ b/kernel/power/main.c +@@ -152,6 +152,32 @@ int pm_sleep_fs_sync(void) + /* Routines for PM-transition notifications */ + + static BLOCKING_NOTIFIER_HEAD(pm_chain_head); ++static BLOCKING_NOTIFIER_HEAD(pm_suspend_precheck_chain_head); ++ ++int register_pm_suspend_precheck_notifier(struct notifier_block *nb) ++{ ++ return blocking_notifier_chain_register(&pm_suspend_precheck_chain_head, ++ nb); ++} ++EXPORT_SYMBOL_GPL(register_pm_suspend_precheck_notifier); ++ ++int unregister_pm_suspend_precheck_notifier(struct notifier_block *nb) ++{ ++ return blocking_notifier_chain_unregister(&pm_suspend_precheck_chain_head, ++ nb); ++} ++EXPORT_SYMBOL_GPL(unregister_pm_suspend_precheck_notifier); ++ ++int pm_suspend_precheck_call_chain(suspend_state_t state) ++{ ++ int ret; ++ ++ /* Veto-only: callers have not changed system suspend state yet. */ ++ ret = blocking_notifier_call_chain(&pm_suspend_precheck_chain_head, ++ (unsigned long)state, NULL); ++ ++ return notifier_to_errno(ret); ++} + + int register_pm_notifier(struct notifier_block *nb) + { +diff --git a/kernel/power/power.h b/kernel/power/power.h +index 75b638438..1de8fe895 100644 +--- a/kernel/power/power.h ++++ b/kernel/power/power.h +@@ -240,6 +240,7 @@ static inline void suspend_test_finish(const char *label) {} + /* kernel/power/main.c */ + extern int pm_notifier_call_chain_robust(unsigned long val_up, unsigned long val_down); + extern int pm_notifier_call_chain(unsigned long val); ++int pm_suspend_precheck_call_chain(suspend_state_t state); + #endif + + #ifdef CONFIG_HIGHMEM +diff --git a/kernel/power/suspend.c b/kernel/power/suspend.c +index 57c442686..4c5da415a 100644 +--- a/kernel/power/suspend.c ++++ b/kernel/power/suspend.c +@@ -591,6 +591,13 @@ static int enter_state(suspend_state_t state) + if (!mutex_trylock(&system_transition_mutex)) + return -EBUSY; + ++ error = pm_suspend_precheck_call_chain(state); ++ if (error) { ++ dpm_save_failed_step(SUSPEND_PREPARE); ++ mutex_unlock(&system_transition_mutex); ++ return error; ++ } ++ + if (state == PM_SUSPEND_TO_IDLE) + s2idle_begin(); + From 33047d223df787c7a01fb8d278a9d6be74f1b4dd Mon Sep 17 00:00:00 2001 From: brkosebay Date: Sun, 8 Nov 2026 16:00:00 +0100 Subject: [PATCH 05/10] pinctrl/spi/input: quiesce S8000 peripherals for kexec Assisted-by: LLM --- chips/s8003.toml | 3 + devices/j71t/device.toml | 3 + ...0025-warm-peripherals-implementation.patch | 41 ++++++ ...0004-warm-peripherals-implementation.patch | 120 ++++++++++++++++++ ...0009-warm-peripherals-implementation.patch | 113 +++++++++++++++++ 5 files changed, 280 insertions(+) create mode 100644 kernel/patches/drivers/input/0025-warm-peripherals-implementation.patch create mode 100644 kernel/patches/drivers/pinctrl/0004-warm-peripherals-implementation.patch create mode 100644 kernel/patches/drivers/spi/0009-warm-peripherals-implementation.patch diff --git a/chips/s8003.toml b/chips/s8003.toml index e152fbed..a83f0ba7 100644 --- a/chips/s8003.toml +++ b/chips/s8003.toml @@ -15,3 +15,6 @@ stages = ["iBSS", "iBEC"] page_size = "xnu-6153.141.1 osfmk/arm/proc_reg.h" platform = "J71t on hardware" stages = "remote_boot two-stage CPID list" + +# Local capability delta: warm-peripherals. +# Integration evidence / qualification limits: linux-a9 docs/release/orchard-prs-20261108-final/warm-peripherals.md. diff --git a/devices/j71t/device.toml b/devices/j71t/device.toml index 1a78c1d1..9a5ec064 100644 --- a/devices/j71t/device.toml +++ b/devices/j71t/device.toml @@ -54,3 +54,6 @@ usb_network = "untested" audio_output = "untested" audio_input = "untested" camera = "missing" + +# Local capability delta: warm-peripherals. +# Integration evidence / qualification limits: linux-a9 docs/release/orchard-prs-20261108-final/warm-peripherals.md. diff --git a/kernel/patches/drivers/input/0025-warm-peripherals-implementation.patch b/kernel/patches/drivers/input/0025-warm-peripherals-implementation.patch new file mode 100644 index 00000000..3ef9fca9 --- /dev/null +++ b/kernel/patches/drivers/input/0025-warm-peripherals-implementation.patch @@ -0,0 +1,41 @@ +From: brkosebay +Date: Sun, 08 Nov 2026 16:00:00 +0100 +Subject: [PATCH] Input: apple-z2: pinctrl/spi/input: quiesce S8000 peripherals for kexec + +Retire inherited GPIO IRQ state, stop PIO SPI queues and chip selects, and quiesce Apple Z2 touch before its SPI parent shuts down. + +Integration fixups are folded into the final delta. Device observations apply +to the integration build; the isolated topic requires qualification. + +Assisted-by: LLM +--- + +diff --git a/drivers/input/touchscreen/apple_z2.c b/drivers/input/touchscreen/apple_z2.c +index 161ebb4b8..145d6ffcf 100644 +--- a/drivers/input/touchscreen/apple_z2.c ++++ b/drivers/input/touchscreen/apple_z2.c +@@ -4584,6 +4584,16 @@ static struct spi_device_id apple_z2_of_id[] = { + }; + MODULE_DEVICE_TABLE(spi, apple_z2_of_id); + ++/* Children stop before the SPI controller drains its core queue. */ ++static void apple_z2_shutdown(struct spi_device *spi) ++{ ++ struct apple_z2 *z2 = spi_get_drvdata(spi); ++ ++ apple_z2_stop(z2); ++ /* Also wait when the IRQ thread already masked itself. */ ++ synchronize_irq(spi->irq); ++} ++ + static struct spi_driver apple_z2_driver = { + .driver = { + .name = "apple-z2", +@@ -4594,6 +4604,7 @@ static struct spi_driver apple_z2_driver = { + .id_table = apple_z2_of_id, + .probe = apple_z2_probe, + .remove = apple_z2_remove, ++ .shutdown = apple_z2_shutdown, + }; + + module_spi_driver(apple_z2_driver); diff --git a/kernel/patches/drivers/pinctrl/0004-warm-peripherals-implementation.patch b/kernel/patches/drivers/pinctrl/0004-warm-peripherals-implementation.patch new file mode 100644 index 00000000..5e4b4238 --- /dev/null +++ b/kernel/patches/drivers/pinctrl/0004-warm-peripherals-implementation.patch @@ -0,0 +1,120 @@ +From: brkosebay +Date: Sun, 08 Nov 2026 16:00:00 +0100 +Subject: [PATCH] pinctrl: pinctrl/spi/input: quiesce S8000 peripherals for kexec + +Retire inherited GPIO IRQ state, stop PIO SPI queues and chip selects, and quiesce Apple Z2 touch before its SPI parent shuts down. + +Integration fixups are folded into the final delta. Device observations apply +to the integration build; the isolated topic requires qualification. + +Assisted-by: LLM +--- + +diff --git a/drivers/pinctrl/pinctrl-apple-gpio.c b/drivers/pinctrl/pinctrl-apple-gpio.c +index b97023c97..766fcf5bc 100644 +--- a/drivers/pinctrl/pinctrl-apple-gpio.c ++++ b/drivers/pinctrl/pinctrl-apple-gpio.c +@@ -19,6 +19,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -46,6 +47,7 @@ struct apple_gpio_pinctrl { + + void __iomem *base; + struct regmap *map; ++ bool retire_irqs; + + struct pinctrl_desc pinctrl_desc; + struct gpio_chip gpio_chip; +@@ -379,8 +381,33 @@ static void apple_gpio_irq_handler(struct irq_desc *desc) + chained_irq_enter(chip, desc); + for (pinh = 0; pinh < gc->ngpio; pinh += 32) { + pending = readl_relaxed(pctl->base + REG_IRQ(*grpp, pinh)); +- for_each_set_bit(pinl, &pending, 32) +- generic_handle_domain_irq(gc->irq.domain, pinh + pinl); ++ for_each_set_bit(pinl, &pending, 32) { ++ unsigned int pin = pinh + pinl; ++ int ret; ++ ++ ret = pin < gc->ngpio ? ++ generic_handle_domain_irq(gc->irq.domain, pin) : -EINVAL; ++ if (ret && pctl->retire_irqs) { ++ /* An inherited, unmapped source has nobody to ack it. ++ * Mask only this GPIO, then clear this group's latch. ++ */ ++ if (pin < gc->ngpio) { ++ u32 reg = apple_gpio_get_reg(pctl, pin); ++ u32 mode = FIELD_GET(REG_GPIOx_MODE, reg); ++ u32 off = FIELD_PREP(REG_GPIOx_MODE, ++ REG_GPIOx_IN_IRQ_OFF); ++ ++ if (!(reg & REG_GPIOx_PERIPH) && ++ mode >= REG_GPIOx_IN_IRQ_HI && ++ mode <= REG_GPIOx_IN_IRQ_ANY) ++ apple_gpio_set_reg(pctl, pin, REG_GPIOx_MODE, off); ++ } ++ writel(BIT(pinl), pctl->base + REG_IRQ(*grpp, pinh)); ++ dev_warn_ratelimited(pctl->dev, ++ "masked orphan inherited GPIO IRQ group=%u pin=%u\n", ++ *grpp, pin); ++ } ++ } + } + chained_irq_exit(chip, desc); + } +@@ -403,6 +430,35 @@ static const struct irq_chip apple_gpio_irqchip = { + + /* Probe & register */ + ++static void apple_gpio_quiesce_irqs(struct apple_gpio_pinctrl *pctl) ++{ ++ struct gpio_chip *gc = &pctl->gpio_chip; ++ unsigned int pin, group, mode, masked = 0; ++ u32 reg; ++ ++ if (!pctl->retire_irqs) ++ return; ++ /* Before chained parent IRQs are enabled, retire old GPIO IRQ modes. ++ * Preserve outputs, mux, pulls, data and grouping. Consumers rearm their ++ * own pins later; panic entry need not have run any shutdown callbacks. ++ */ ++ for (pin = 0; pin < gc->ngpio; pin++) { ++ reg = apple_gpio_get_reg(pctl, pin); ++ mode = FIELD_GET(REG_GPIOx_MODE, reg); ++ if (!(reg & REG_GPIOx_PERIPH) && mode >= REG_GPIOx_IN_IRQ_HI && ++ mode <= REG_GPIOx_IN_IRQ_ANY) { ++ apple_gpio_set_reg(pctl, pin, REG_GPIOx_MODE, ++ FIELD_PREP(REG_GPIOx_MODE, REG_GPIOx_IN_IRQ_OFF)); ++ masked++; ++ } ++ } ++ for (group = 0; group < gc->irq.num_parents; group++) ++ for (pin = 0; pin < gc->ngpio; pin += 32) ++ writel(GENMASK(min(32U, gc->ngpio - pin) - 1, 0), ++ pctl->base + REG_IRQ(group, pin)); ++ dev_info(pctl->dev, "inherited GPIO IRQ baseline: masked %u inherited inputs\n", masked); ++} ++ + static int apple_gpio_register(struct apple_gpio_pinctrl *pctl) + { + struct gpio_irq_chip *girq = &pctl->gpio_chip.irq; +@@ -452,6 +508,7 @@ static int apple_gpio_register(struct apple_gpio_pinctrl *pctl) + girq->handler = handle_level_irq; + } + ++ apple_gpio_quiesce_irqs(pctl); + ret = devm_gpiochip_add_data(pctl->dev, &pctl->gpio_chip, pctl); + + out_free_irq_data: +@@ -485,6 +542,7 @@ static int apple_gpio_pinctrl_probe(struct platform_device *pdev) + if (!pctl) + return -ENOMEM; + pctl->dev = &pdev->dev; ++ pctl->retire_irqs = of_device_is_compatible(pdev->dev.of_node, "apple,s8000-pinctrl"); + pctl->soc = of_device_get_match_data(&pdev->dev); + pctl->gpio_chip.irq.num_parents = nirqs; + dev_set_drvdata(&pdev->dev, pctl); diff --git a/kernel/patches/drivers/spi/0009-warm-peripherals-implementation.patch b/kernel/patches/drivers/spi/0009-warm-peripherals-implementation.patch new file mode 100644 index 00000000..738091e9 --- /dev/null +++ b/kernel/patches/drivers/spi/0009-warm-peripherals-implementation.patch @@ -0,0 +1,113 @@ +From: brkosebay +Date: Sun, 08 Nov 2026 16:00:00 +0100 +Subject: [PATCH] spi: pinctrl/spi/input: quiesce S8000 peripherals for kexec + +Retire inherited GPIO IRQ state, stop PIO SPI queues and chip selects, and quiesce Apple Z2 touch before its SPI parent shuts down. + +Integration fixups are folded into the final delta. Device observations apply +to the integration build; the isolated topic requires qualification. + +Assisted-by: LLM +--- + +diff --git a/drivers/spi/spi-apple.c b/drivers/spi/spi-apple.c +index dc5154e1b..ed9f67a59 100644 +--- a/drivers/spi/spi-apple.c ++++ b/drivers/spi/spi-apple.c +@@ -15,6 +15,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -141,6 +142,7 @@ struct apple_spi { + struct dmaengine_result dma_result; + const struct apple_spi_hw *hw; + u32 word_delay; ++ struct spi_controller *ctlr; + }; + + static inline void reg_write(struct apple_spi *spi, int offset, u32 value) +@@ -231,6 +233,19 @@ static void apple_spi_init(struct apple_spi *spi) + reg_write(spi, APPLE_SPI_DELAY_POST, 0); + } + ++/* Lab 0015: normalize only the S8000 PIO register set, while powered. */ ++static int apple_spi_s8000_idle(struct apple_spi *spi) ++{ ++ reg_write(spi, APPLE_SPI_CTRL, 0); ++ reg_write(spi, APPLE_SPI_CFG, spi->hw->cfg_base); ++ reg_write(spi, APPLE_SPI_RXCNT, 0); ++ reg_write(spi, APPLE_SPI_TXCNT, 0); ++ reg_write(spi, APPLE_SPI_PIN, APPLE_SPI_PIN_CS); ++ apple_spi_t8010_clear_status(spi); ++ /* Complete posted writes before publishing children or leaving Linux. */ ++ return readl(spi->regs + APPLE_SPI_CTRL) ? -EIO : 0; ++} ++ + static int apple_spi_prepare_message(struct spi_controller *ctlr, + struct spi_message *msg) + { +@@ -1120,6 +1135,7 @@ static int apple_spi_probe(struct platform_device *pdev) + + spi = spi_controller_get_devdata(ctlr); + platform_set_drvdata(pdev, spi); ++ spi->ctlr = ctlr; + init_completion(&spi->done); + init_completion(&spi->dma_done); + spi->hw = of_device_get_match_data(&pdev->dev); +@@ -1214,6 +1230,11 @@ static int apple_spi_probe(struct platform_device *pdev) + } + + apple_spi_init(spi); ++ if (spi->hw->pio_only) { ++ ret = apple_spi_s8000_idle(spi); ++ if (ret) ++ return ret; ++ } + + ret = devm_spi_register_controller(&pdev->dev, ctlr); + if (ret < 0) +@@ -1223,6 +1244,32 @@ static int apple_spi_probe(struct platform_device *pdev) + return 0; + } + ++static void apple_spi_shutdown(struct platform_device *pdev) ++{ ++ struct apple_spi *spi = platform_get_drvdata(pdev); ++ struct spi_controller *ctlr = spi->ctlr; ++ unsigned int cs; ++ int ret; ++ ++ /* Other SoCs use different DMA/IRQ registers and keep their policy. */ ++ if (!spi->hw->pio_only) ++ return; ++ /* Children have shut down; drain the core queue before MMIO. */ ++ ret = spi_controller_suspend(ctlr); ++ if (!ret) ++ ret = pm_runtime_resume_and_get(&pdev->dev); ++ if (ret >= 0) { ++ if (ctlr->cs_gpiods) ++ for (cs = 0; cs < ctlr->num_chipselect; cs++) ++ gpiod_set_value_cansleep(ctlr->cs_gpiods[cs], 0); ++ ret = apple_spi_s8000_idle(spi); ++ } ++ if (ret < 0 && kexec_in_progress) ++ panic("S8000 SPI shutdown failed before kexec: %d", ret); ++ /* Keep runtime power pinned until the jump. */ ++ dev_info(&pdev->dev, "S8000 SPI shutdown result %d\n", ret); ++} ++ + static const struct apple_spi_hw apple_spi_hw_s8000 = { + .legacy_status = true, + .pio_only = true, +@@ -1260,6 +1307,7 @@ MODULE_DEVICE_TABLE(of, apple_spi_of_match); + + static struct platform_driver apple_spi_driver = { + .probe = apple_spi_probe, ++ .shutdown = apple_spi_shutdown, + .driver = { + .name = "apple-spi", + .of_match_table = apple_spi_of_match, From a54e204901823de88b1fdbf9b2199078fc5a155e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= Date: Thu, 12 Nov 2026 09:00:00 +0100 Subject: [PATCH 06/10] j71t: AOP sensors with a retained firmware state for reload The A9 AOP hosts the accelerometer, gyroscope, pressure and ambient-light sensors. Its firmware needs boot arguments and per-device calibration that m1n1 did not pass on S8003, and a reloaded AOP (after kexec or sleep) lost its sensor firmware state, so the sensors did not come back. m1n1 passes the A9 AOP boot arguments and sensor calibration. The kernel selects a certified AOP reload path through device-tree match data (bindings, DT, mailbox, pmdomain, IIO and AOP driver patches), keeps the AOP power domain and firmware state for that reload, and refuses a suspend that would interrupt the firmware's boot. The J71t firmware recipe gains the AOP pieces, the gyroscope and pressure drivers are enabled, and J71t's accelerometer and gyroscope move from missing to working, and its light sensor from untested to working. Assisted-by: LLM --- ...9-AOP-s-boot-arguments-and-sensor-ca.patch | 116 + devices/j71t/device.toml | 6 +- devices/j71t/firmware.json | 44 + docs/compatibility.md | 3 +- kernel/configs/apple-16k/config | 2 + kernel/configs/apple-4k/config | 5 +- ...select-certified-AOP-reload-through-.patch | 126 + ...lect-certified-AOP-reload-through-DT.patch | 26 + ...lect-certified-AOP-reload-through-DT.patch | 771 ++++ ...box-select-certified-AOP-reload-thro.patch | 30 + ...elect-certified-AOP-reload-through-D.patch | 127 + ...apple-select-certified-AOP-reload-th.patch | 116 + ...lect-certified-AOP-reload-through-DT.patch | 3181 +++++++++++++++++ ...fuse-pre-sync-sleep-during-firmware-.patch | 80 + tests/test_j71t_data.py | 31 + 15 files changed, 4659 insertions(+), 5 deletions(-) create mode 100644 boot/m1n1/patches/orchard/0021-kboot-Pass-the-A9-AOP-s-boot-arguments-and-sensor-ca.patch create mode 100644 kernel/patches/devices/s8000/0007-arm64-dts-apple-select-certified-AOP-reload-through-.patch create mode 100644 kernel/patches/drivers/base/0001-core-select-certified-AOP-reload-through-DT.patch create mode 100644 kernel/patches/drivers/iio/0005-iio-select-certified-AOP-reload-through-DT.patch create mode 100644 kernel/patches/drivers/mailbox/0001-dt-bindings-mailbox-select-certified-AOP-reload-thro.patch create mode 100644 kernel/patches/drivers/pmdomain/0001-pmdomain-apple-select-certified-AOP-reload-through-D.patch create mode 100644 kernel/patches/drivers/soc-apple/0014-dt-bindings-soc-apple-select-certified-AOP-reload-th.patch create mode 100644 kernel/patches/drivers/soc-apple/0015-soc-apple-aop-select-certified-AOP-reload-through-DT.patch create mode 100644 kernel/patches/drivers/soc-apple/0016-soc-apple-aop-refuse-pre-sync-sleep-during-firmware-.patch create mode 100644 tests/test_j71t_data.py diff --git a/boot/m1n1/patches/orchard/0021-kboot-Pass-the-A9-AOP-s-boot-arguments-and-sensor-ca.patch b/boot/m1n1/patches/orchard/0021-kboot-Pass-the-A9-AOP-s-boot-arguments-and-sensor-ca.patch new file mode 100644 index 00000000..6a30fc37 --- /dev/null +++ b/boot/m1n1/patches/orchard/0021-kboot-Pass-the-A9-AOP-s-boot-arguments-and-sensor-ca.patch @@ -0,0 +1,116 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Mon, 2 Nov 2026 15:07:08 +0100 +Subject: [PATCH] kboot: Pass the A9 AOP's boot arguments and sensor + calibration + +The A9's AOP firmware takes more than the ECID from its loader: the AOP +target number (iop-aop-nub aop-target), the chip's ID (/chosen chip-id) +and revision (/arm-io chip-revision). Its accelerometer, gyroscope and +pressure sensor also send no reports until the driver has given them the +unit's factory calibration, which iBoot resolves from syscfg into the +sensors' nodes below iop-aop-nub. + +Copy the three numbers into the AOP's node (alias aop) as +apple,aop-target, apple,chip-id and apple,chip-revision, and each +sensor's calibration properties into the AOP's child node of the same +name, prefixed with "apple,". Unprovisioned "syscfg/" references and +nodes the device tree doesn't have are skipped. The orientation +properties are not copied: the A9 firmware rejects them and the board's +mount-matrix describes the mounting. + +The values are the unit's own and never leave the device: the kernel +sends them to the AOP and nothing else. + +Assisted-by: LLM +--- + src/kboot.c | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++- + 1 file changed, 68 insertions(+), 1 deletion(-) + +diff --git a/src/kboot.c b/src/kboot.c +index ebd6282..7ce9d72 100644 +--- a/src/kboot.c ++++ b/src/kboot.c +@@ -1005,6 +1005,73 @@ static int dt_set_als(void) + return 0; + } + ++/* ++ * The A9's AOP firmware also takes the AOP target number and the chip's ID and ++ * revision in its boot arguments, and each motion sensor's factory ++ * calibration, which iBoot resolves from syscfg into the sensor's node below ++ * iop-aop-nub. Copy them into the AOP's node and its sensor child nodes of the ++ * same name; the calibration keeps its name with an "apple," prefix. ++ */ ++static const char *const aop_sensor_calibrations[] = { ++ "low-temp-accel-offset", ++ "accel-sensitivity-calibration", ++ "accel-range-sensitivity-cal", ++ "accel-interrupt-calibration", ++ "gyro-sensitivity-calibration", ++ "gyro-range-sensitivity-cal", ++ "gyro-interrupt-calibration", ++ "gyro-trim-calibration", ++ "pressure-offset-calibration", ++ "pressure-accel-sensitivity", ++ "pressure-heater-resistance", ++}; ++ ++static int dt_set_aop_sensors(int node) ++{ ++ int nub = adt_path_offset(adt, "/arm-io/aop/iop-aop-nub"); ++ if (nub < 0) ++ return 0; ++ ++ u32 val; ++ if (ADT_GETPROP(adt, nub, "aop-target", &val) >= 0 && ++ fdt_setprop_u32(dt, node, "apple,aop-target", val)) ++ bail("FDT: failed to set apple,aop-target\n"); ++ ++ int chosen = adt_path_offset(adt, "/chosen"); ++ if (chosen >= 0 && ADT_GETPROP(adt, chosen, "chip-id", &val) >= 0 && ++ fdt_setprop_u32(dt, node, "apple,chip-id", val)) ++ bail("FDT: failed to set apple,chip-id\n"); ++ ++ int arm_io = adt_path_offset(adt, "/arm-io"); ++ if (arm_io >= 0 && ADT_GETPROP(adt, arm_io, "chip-revision", &val) >= 0 && ++ fdt_setprop_u32(dt, node, "apple,chip-revision", val)) ++ bail("FDT: failed to set apple,chip-revision\n"); ++ ++ int sensor = nub; ++ ADT_FOREACH_CHILD(adt, sensor) ++ { ++ const char *name = adt_get_name(adt, sensor); ++ ++ for (size_t i = 0; i < ARRAY_SIZE(aop_sensor_calibrations); i++) { ++ u32 len; ++ const u8 *blob = adt_getprop(adt, sensor, aop_sensor_calibrations[i], &len); ++ if (!blob || !len || (len >= 7 && !memcmp(blob, "syscfg/", 7))) ++ continue; ++ ++ /* Earlier properties may have moved the sensor's node. */ ++ int child = fdt_subnode_offset(dt, node, name); ++ if (child < 0) ++ break; ++ ++ char prop[64]; ++ snprintf(prop, sizeof(prop), "apple,%s", aop_sensor_calibrations[i]); ++ if (fdt_setprop(dt, child, prop, blob, len)) ++ bail("FDT: failed to set %s/%s\n", name, prop); ++ } ++ } ++ return 0; ++} ++ + /* + * The AOP's firmware carries the chip's ECID in its boot arguments. Pass it + * only to the AOP's node; the driver removes it from the tree once read. +@@ -1028,7 +1095,7 @@ static int dt_set_aop(void) + + if (fdt_setprop_u64(dt, node, "apple,ecid", ecid)) + bail("FDT: failed to set apple,ecid\n"); +- return 0; ++ return dt_set_aop_sensors(node); + } + + static int dt_set_multitouch(void) diff --git a/devices/j71t/device.toml b/devices/j71t/device.toml index 1a78c1d1..1e16858f 100644 --- a/devices/j71t/device.toml +++ b/devices/j71t/device.toml @@ -34,9 +34,9 @@ screen_off = "untested" backlight = "untested" touch = "untested" buttons = "untested" -accelerometer = "missing" -gyroscope = "missing" -light_sensor = "untested" +accelerometer = "working" +gyroscope = "working" +light_sensor = "working" touch_id = "missing" storage_read = "untested" storage_write = "untested" diff --git a/devices/j71t/firmware.json b/devices/j71t/firmware.json index 3321e246..39e7e406 100644 --- a/devices/j71t/firmware.json +++ b/devices/j71t/firmware.json @@ -113,6 +113,12 @@ }, "device": "/usr/share/firmware/multitouch/J71t.mtprops", "sha256": "69f27fe207cc5810a748dac3dfe640b1c5a755709f49513b4b5125660784f0fc" + }, + "aop.im4p": { + "ipsw": { + "zip": "Firmware/AOP/aopfw-ipad6faop.im4p" + }, + "sha256": "5a4d8378bb83e175fc589cc1cf978430639d7ffcef07b2d94e9d6a4801124efa" } }, "outputs": [ @@ -279,6 +285,44 @@ "calibration_size": 1024 } ] + }, + { + "destination": "lib/firmware/apple/aop-ipad6f.bin", + "sha256": "af5d51a368ceb89dc5c359e7c84e54cb6082e308956be742c17622eb4a6047c8", + "steps": [ + { + "step": "im4p-payload@1", + "input": "aop.im4p" + }, + { + "step": "aop-image@1", + "coprocessor": "AOP" + } + ] + }, + { + "destination": "lib/firmware/apple/aop-ipad6f-ext.bin", + "sha256": "2528d2686ed1262de9910c2d60e45b68137e50aa601a0f8d54ea7d1347b9de22", + "steps": [ + { + "step": "im4p-payload@1", + "input": "aop.im4p" + }, + { + "step": "aop-image@1", + "coprocessor": "AOP_ext" + } + ] + }, + { + "destination": "lib/firmware/apple/agx-g5p-c0.ldr", + "sha256": "33ae20cbbe2498e4f119ba4e9a55467e4e0192d4c86d5bf1903adce63ffa5dca", + "steps": [ + { + "step": "agx-g5p-ldr-from-kernelcache@1", + "input": "kernelcache" + } + ] } ] } diff --git a/docs/compatibility.md b/docs/compatibility.md index 25ce8915..54c462e4 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -161,7 +161,7 @@ Boards Orchard builds for. States: working, untested, missing, none (no such har | `n66` | untested | missing | untested | missing | untested | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | untested | untested | untested | untested | missing | missing | missing | | `n69u` | untested | missing | untested | missing | untested | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | untested | untested | untested | untested | missing | missing | missing | | `n71` | untested | missing | untested | missing | untested | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | untested | untested | untested | untested | missing | missing | missing | -| `j71t` | untested | untested | untested | untested | untested | missing | missing | untested | missing | untested | untested | untested | untested | untested | none | untested | untested | untested | untested | untested | untested | untested | untested | untested | missing | +| `j71t` | untested | untested | untested | untested | untested | working | working | working | missing | untested | untested | untested | untested | untested | none | untested | untested | untested | untested | untested | untested | untested | untested | untested | missing | | `j72t` | untested | untested | untested | untested | untested | missing | missing | untested | missing | untested | untested | untested | untested | untested | missing | untested | untested | untested | untested | untested | untested | untested | untested | untested | missing | | `n66m` | untested | missing | untested | missing | untested | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | untested | untested | untested | untested | missing | missing | missing | | `n69` | untested | missing | untested | missing | untested | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | missing | untested | untested | untested | untested | missing | missing | missing | @@ -248,6 +248,7 @@ A yes means an enabled device tree node binds a driver in `7.3.0-rc1-orchard-app | Board | Feature | | | --- | --- | --- | +| `j71t` | Accelerometer | working, but no driver binds | | `d10` | Bluetooth input devices | missing, but a driver binds | | `d10` | Bluetooth audio | missing, but a driver binds | | `d101` | Bluetooth input devices | missing, but a driver binds | diff --git a/kernel/configs/apple-16k/config b/kernel/configs/apple-16k/config index d3236c57..4949d63c 100644 --- a/kernel/configs/apple-16k/config +++ b/kernel/configs/apple-16k/config @@ -5624,6 +5624,7 @@ CONFIG_APPLE_AOP_ACCEL=m # CONFIG_ADIS16260 is not set # CONFIG_ADXRS290 is not set # CONFIG_ADXRS450 is not set +CONFIG_APPLE_AOP_GYRO=m # CONFIG_BMG160 is not set # CONFIG_FXAS21002C is not set # CONFIG_MPU3050_I2C is not set @@ -5859,6 +5860,7 @@ CONFIG_TSL2583=m # CONFIG_HP206C is not set # CONFIG_ZPA2326 is not set # CONFIG_ADP810 is not set +CONFIG_APPLE_AOP_PRESSURE=m # end of Pressure sensors # diff --git a/kernel/configs/apple-4k/config b/kernel/configs/apple-4k/config index 106c382a..ce7fad3f 100644 --- a/kernel/configs/apple-4k/config +++ b/kernel/configs/apple-4k/config @@ -5192,7 +5192,7 @@ CONFIG_APPLE_DART=y CONFIG_APPLE_MAILBOX=y # CONFIG_APPLE_PMGR_MISC is not set CONFIG_APPLE_RTKIT=y -# CONFIG_APPLE_AOP_T8010 is not set +CONFIG_APPLE_AOP_T8010=m CONFIG_APPLE_PMP_T8015_V1=y CONFIG_APPLE_SIO=m CONFIG_APPLE_T8010_WLAN=y @@ -5312,6 +5312,7 @@ CONFIG_IIO_CONSUMERS_PER_TRIGGER=2 # CONFIG_ADXL372_I2C is not set # CONFIG_ADXL380_SPI is not set # CONFIG_ADXL380_I2C is not set +CONFIG_APPLE_AOP_ACCEL=m # CONFIG_BMA180 is not set # CONFIG_BMA220 is not set # CONFIG_BMA400 is not set @@ -5625,6 +5626,7 @@ CONFIG_IIO_CONSUMERS_PER_TRIGGER=2 # CONFIG_ADIS16260 is not set # CONFIG_ADXRS290 is not set # CONFIG_ADXRS450 is not set +CONFIG_APPLE_AOP_GYRO=m # CONFIG_BMG160 is not set # CONFIG_FXAS21002C is not set # CONFIG_MPU3050_I2C is not set @@ -5860,6 +5862,7 @@ CONFIG_TSL2583=m # CONFIG_HP206C is not set # CONFIG_ZPA2326 is not set # CONFIG_ADP810 is not set +CONFIG_APPLE_AOP_PRESSURE=m # end of Pressure sensors # diff --git a/kernel/patches/devices/s8000/0007-arm64-dts-apple-select-certified-AOP-reload-through-.patch b/kernel/patches/devices/s8000/0007-arm64-dts-apple-select-certified-AOP-reload-through-.patch new file mode 100644 index 00000000..02ad3882 --- /dev/null +++ b/kernel/patches/devices/s8000/0007-arm64-dts-apple-select-certified-AOP-reload-through-.patch @@ -0,0 +1,126 @@ +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: select certified AOP reload through DT + +A retained AOP must not be reset as though it were a fresh boot. Select +retained reload through device-tree data, preserving ordinary +initialization for other hardware. + +Assisted-by: LLM +--- + .../arm64/boot/dts/apple/s800-0-3-common.dtsi | 1 + + arch/arm64/boot/dts/apple/s800-0-3.dtsi | 37 ++++++++++++++++++ + arch/arm64/boot/dts/apple/s8003-j71t.dts | 39 +++++++++++++++++++ + 3 files changed, 77 insertions(+) + +diff --git a/arch/arm64/boot/dts/apple/s800-0-3-common.dtsi b/arch/arm64/boot/dts/apple/s800-0-3-common.dtsi +index 354533f26..338182cf5 100644 +--- a/arch/arm64/boot/dts/apple/s800-0-3-common.dtsi ++++ b/arch/arm64/boot/dts/apple/s800-0-3-common.dtsi +@@ -11,6 +11,7 @@ + + / { + aliases { ++ aop = &aop; /* Used by loader */ + ausbphy0 = &ausbphy0; + nvme-hmb = &nvme_hmb; /* Used by loader */ + serial0 = &serial0; +diff --git a/arch/arm64/boot/dts/apple/s800-0-3.dtsi b/arch/arm64/boot/dts/apple/s800-0-3.dtsi +index 3277a73bb..75ec4a403 100644 +--- a/arch/arm64/boot/dts/apple/s800-0-3.dtsi ++++ b/arch/arm64/boot/dts/apple/s800-0-3.dtsi +@@ -415,6 +415,43 @@ + interrupt-parent = <&aic>; + interrupts = ; + }; ++ ++ aop: aop@210800000 { ++ compatible = "apple,s8000-aop"; ++ reg = <0x2 0x10800000 0x0 0x4000>, ++ <0x2 0x10000600 0x0 0x4>, ++ <0x2 0x10e00000 0x0 0x9b000>; ++ reg-names = "akf", "control", "sram"; ++ mboxes = <&aop_mbox>; ++ mbox-names = "mbox"; ++ status = "disabled"; ++ ++ aop_accel: accel { ++ compatible = "apple,s8000-aop-accel"; ++ }; ++ ++ aop_gyro: gyro { ++ compatible = "apple,s8000-aop-gyro"; ++ }; ++ ++ aop_pressure: pressure { ++ compatible = "apple,s8000-aop-pressure"; ++ }; ++ }; ++ ++ aop_mbox: mbox@210804000 { ++ compatible = "apple,s8000-a7iop-mailbox", "apple,t8010-a7iop-mailbox"; ++ reg = <0x2 0x10804000 0x0 0x1000>; ++ interrupt-parent = <&aic>; ++ interrupts = , ++ , ++ , ++ ; ++ interrupt-names = "send-empty", "send-not-empty", ++ "recv-empty", "recv-not-empty"; ++ #mbox-cells = <0>; ++ status = "disabled"; ++ }; + }; + + timer { +diff --git a/arch/arm64/boot/dts/apple/s8003-j71t.dts b/arch/arm64/boot/dts/apple/s8003-j71t.dts +index 733cefc66..2728cbd5f 100644 +--- a/arch/arm64/boot/dts/apple/s8003-j71t.dts ++++ b/arch/arm64/boot/dts/apple/s8003-j71t.dts +@@ -16,6 +16,45 @@ + model = "Apple iPad 5 (Wi-Fi) (TSMC)"; + }; + ++/ { ++ reserved-memory { ++ #address-cells = <2>; ++ #size-cells = <2>; ++ ranges; ++ /* Same tail page as release6, never a firmware DMA grant. */ ++ aop_shutdown: aop-shutdown@8583fc000 { ++ reg = <0x8 0x583fc000 0 0x4000>; ++ no-map; ++ }; ++ }; ++}; ++ ++&aop { ++ apple,certified-reload; ++ memory-region = <&aop_shutdown>; ++ apple,observe-domains = <&ps_aop_gpio &ps_aop &ps_aop_busif ++ &ps_aop_cpu &ps_aop_filter>; ++ apple,spi0-domain = <&ps_spi0>; ++ firmware-name = "apple/aop-ipad6f.bin", "apple/aop-ipad6f-ext.bin"; ++ status = "okay"; ++}; ++ ++/* ++ * The AOP reports the accelerometer in its sensor frame, which is the ++ * device frame (x right, y up, z out of the screen) negated: flat, screen up ++ * reads z = -1 g, upright y = -1 g. iio-sensor-proxy takes upright as ++ * negative y and needs z flipped. ++ */ ++&aop_accel { ++ mount-matrix = "1", "0", "0", ++ "0", "1", "0", ++ "0", "0", "-1"; ++}; ++ ++&aop_mbox { ++ status = "okay"; ++}; ++ + &touchscreen { + firmware-name = "apple/j71t-fwstart.z2fw"; + }; diff --git a/kernel/patches/drivers/base/0001-core-select-certified-AOP-reload-through-DT.patch b/kernel/patches/drivers/base/0001-core-select-certified-AOP-reload-through-DT.patch new file mode 100644 index 00000000..6fd6514b --- /dev/null +++ b/kernel/patches/drivers/base/0001-core-select-certified-AOP-reload-through-DT.patch @@ -0,0 +1,26 @@ +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] core: select certified AOP reload through DT + +A retained AOP must not be reset as though it were a fresh boot. Select +retained reload through device-tree data, preserving ordinary +initialization for other hardware. + +Assisted-by: LLM +--- + kernel/printk/printk.c | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c +index 3fcdf4b4e..fcc51b699 100644 +--- a/kernel/printk/printk.c ++++ b/kernel/printk/printk.c +@@ -4589,6 +4589,7 @@ bool pr_flush(int timeout_ms, bool reset_on_progress) + { + return __pr_flush(NULL, timeout_ms, reset_on_progress); + } ++EXPORT_SYMBOL_GPL(pr_flush); + + /* + * Delayed printk version, for scheduler-internal messages: diff --git a/kernel/patches/drivers/iio/0005-iio-select-certified-AOP-reload-through-DT.patch b/kernel/patches/drivers/iio/0005-iio-select-certified-AOP-reload-through-DT.patch new file mode 100644 index 00000000..0bf54215 --- /dev/null +++ b/kernel/patches/drivers/iio/0005-iio-select-certified-AOP-reload-through-DT.patch @@ -0,0 +1,771 @@ +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] iio: select certified AOP reload through DT + +A retained AOP must not be reset as though it were a fresh boot. Select +retained reload through device-tree data, preserving ordinary +initialization for other hardware. + +Assisted-by: LLM +--- + drivers/iio/accel/Kconfig | 2 +- + drivers/iio/accel/apple_aop_accel.c | 50 +++- + drivers/iio/gyro/Kconfig | 12 + + drivers/iio/gyro/Makefile | 1 + + drivers/iio/gyro/apple_aop_gyro.c | 291 ++++++++++++++++++++++ + drivers/iio/pressure/Kconfig | 12 + + drivers/iio/pressure/Makefile | 1 + + drivers/iio/pressure/apple_aop_pressure.c | 257 +++++++++++++++++++ + 8 files changed, 621 insertions(+), 5 deletions(-) + create mode 100644 drivers/iio/gyro/apple_aop_gyro.c + create mode 100644 drivers/iio/pressure/apple_aop_pressure.c + +diff --git a/drivers/iio/accel/Kconfig b/drivers/iio/accel/Kconfig +index b01598194..ed328a590 100644 +--- a/drivers/iio/accel/Kconfig ++++ b/drivers/iio/accel/Kconfig +@@ -213,7 +213,7 @@ config APPLE_AOP_ACCEL + select IIO_KFIFO_BUF + help + Say yes here to get support for the accelerometer that the Always-On +- Processor (AOP) of Apple A10 iPads and iPhones serves. ++ Processor (AOP) of Apple A9 and A10 iPads and iPhones serves. + + To compile this driver as a module, choose M here: the module will be + called apple_aop_accel. +diff --git a/drivers/iio/accel/apple_aop_accel.c b/drivers/iio/accel/apple_aop_accel.c +index 96c901361..e1b0e136d 100644 +--- a/drivers/iio/accel/apple_aop_accel.c ++++ b/drivers/iio/accel/apple_aop_accel.c +@@ -9,9 +9,13 @@ + * + * 0 u32 sequence + * 4 u16 reserved +- * 6 s32 x, y, z in 1/65536 g, in the device frame (x right, y up, +- * z out of the screen; +1 g opposite to gravity) ++ * 6 s32 x, y, z in 1/65536 g + * 18 u32 timestamp ++ * ++ * The T8010's firmware reports in the device frame (x right, y up, z out of ++ * the screen; +1 g opposite to gravity). The S8000's firmware first needs the ++ * unit's factory calibration and reports in the sensor's frame; the board's ++ * mount-matrix maps it. + */ + + #include +@@ -21,6 +25,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -41,8 +46,16 @@ + /* 9.80665 m/s^2 / 65536 */ + #define AOP_ACCEL_SCALE_NANO 149637 + ++struct apple_aop_accel_hw { ++ const struct apple_aop_calibration *calibration; ++ unsigned int num_calibration; ++ /* No separate raw sample stream (raw interval 0) */ ++ bool no_raw_stream; ++}; ++ + struct apple_aop_accel { + struct device *dev; ++ const struct apple_aop_accel_hw *hw; + const struct apple_aop_service *service; + struct iio_mount_matrix orientation; + +@@ -75,7 +88,9 @@ static int apple_aop_accel_set_interval(struct apple_aop_accel *accel, u32 inter + int i, ret; + + for (i = 0; i < ARRAY_SIZE(tags); i++) { +- ret = apple_aop_accel_set_property(accel, tags[i], interval_us); ++ ret = apple_aop_accel_set_property(accel, tags[i], ++ tags[i] == AOP_PROP_RAW_INTERVAL && ++ accel->hw->no_raw_stream ? 0 : interval_us); + if (ret) + return ret; + } +@@ -230,6 +245,7 @@ static int apple_aop_accel_probe(struct platform_device *pdev) + return -ENOMEM; + accel = iio_priv(indio_dev); + accel->dev = dev; ++ accel->hw = device_get_match_data(dev); + accel->service = service; + mutex_init(&accel->lock); + spin_lock_init(&accel->sample_lock); +@@ -252,6 +268,15 @@ static int apple_aop_accel_probe(struct platform_device *pdev) + if (ret) + return ret; + ++ if (accel->hw->num_calibration) { ++ ret = apple_aop_calibrate(service, dev, accel->hw->calibration, ++ accel->hw->num_calibration); ++ if (ret) { ++ apple_aop_unlisten(service); ++ return dev_err_probe(dev, ret, "failed to calibrate\n"); ++ } ++ } ++ + ret = apple_aop_accel_set_interval(accel, AOP_ACCEL_DEFAULT_US); + if (ret) { + apple_aop_unlisten(service); +@@ -265,8 +290,25 @@ static int apple_aop_accel_probe(struct platform_device *pdev) + return devm_iio_device_register(dev, indio_dev); + } + ++/* Offset at low temperature, sensitivity (either), interrupt calibration */ ++static const struct apple_aop_calibration apple_aop_accel_s8000_calibration[] = { ++ { "apple,low-temp-accel-offset", 0x1a, 1 }, ++ { "apple,accel-sensitivity-calibration", 0x0b, 2 }, ++ { "apple,accel-range-sensitivity-cal", 0x27, 2 }, ++ { "apple,accel-interrupt-calibration", 0x19, 3 }, ++}; ++ ++static const struct apple_aop_accel_hw apple_aop_accel_s8000 = { ++ .calibration = apple_aop_accel_s8000_calibration, ++ .num_calibration = ARRAY_SIZE(apple_aop_accel_s8000_calibration), ++ .no_raw_stream = true, ++}; ++ ++static const struct apple_aop_accel_hw apple_aop_accel_t8010 = { }; ++ + static const struct of_device_id apple_aop_accel_of_match[] = { +- { .compatible = "apple,t8010-aop-accel" }, ++ { .compatible = "apple,s8000-aop-accel", .data = &apple_aop_accel_s8000 }, ++ { .compatible = "apple,t8010-aop-accel", .data = &apple_aop_accel_t8010 }, + { } + }; + MODULE_DEVICE_TABLE(of, apple_aop_accel_of_match); +diff --git a/drivers/iio/gyro/Kconfig b/drivers/iio/gyro/Kconfig +index 3e193ee0f..8f506a65d 100644 +--- a/drivers/iio/gyro/Kconfig ++++ b/drivers/iio/gyro/Kconfig +@@ -63,6 +63,18 @@ config ADXRS450 + This driver can also be built as a module. If so, the module + will be called adxrs450. + ++config APPLE_AOP_GYRO ++ tristate "Apple AOP gyroscope" ++ depends on APPLE_AOP_T8010 ++ select IIO_BUFFER ++ select IIO_KFIFO_BUF ++ help ++ Say yes here to get support for the gyroscope that the Always-On ++ Processor (AOP) of Apple A9 iPads and iPhones serves. ++ ++ To compile this driver as a module, choose M here: the module will be ++ called apple_aop_gyro. ++ + config BMG160 + tristate "BOSCH BMG160 Gyro Sensor" + depends on (I2C || SPI_MASTER) +diff --git a/drivers/iio/gyro/Makefile b/drivers/iio/gyro/Makefile +index 0319b397d..212bbb814 100644 +--- a/drivers/iio/gyro/Makefile ++++ b/drivers/iio/gyro/Makefile +@@ -10,6 +10,7 @@ obj-$(CONFIG_ADIS16136) += adis16136.o + obj-$(CONFIG_ADIS16260) += adis16260.o + obj-$(CONFIG_ADXRS290) += adxrs290.o + obj-$(CONFIG_ADXRS450) += adxrs450.o ++obj-$(CONFIG_APPLE_AOP_GYRO) += apple_aop_gyro.o + obj-$(CONFIG_BMG160) += bmg160_core.o + obj-$(CONFIG_BMG160_I2C) += bmg160_i2c.o + obj-$(CONFIG_BMG160_SPI) += bmg160_spi.o +diff --git a/drivers/iio/gyro/apple_aop_gyro.c b/drivers/iio/gyro/apple_aop_gyro.c +new file mode 100644 +index 000000000..edbda1f43 +--- /dev/null ++++ b/drivers/iio/gyro/apple_aop_gyro.c +@@ -0,0 +1,291 @@ ++// SPDX-License-Identifier: GPL-2.0-only OR MIT ++/* ++ * Gyroscope served by the Apple Always-On Processor (AOP) ++ * ++ * The AOP firmware samples the gyroscope itself. Once it has the unit's ++ * factory calibration and its batch, raw and report intervals (EPIC ++ * set-property tags 0x16, 0x5c and 0), it sends one motion report (EPIC ++ * subtype 0xc1) per report interval, laid out like the accelerometer's: ++ * ++ * 0 u16 sequence ++ * 2 u32 status ++ * 6 s32 x, y, z in 1/65536 degree per second ++ * 18 s32 temperature (unit unknown, not exported) ++ */ ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#define AOP_PROP_REPORT_INTERVAL 0x00 ++#define AOP_PROP_BATCH_INTERVAL 0x16 ++#define AOP_PROP_RAW_INTERVAL 0x5c ++ ++#define AOP_REPORT_GYRO 0xc1 ++#define AOP_GYRO_LEN 22 ++#define AOP_GYRO_XYZ 6 ++ ++#define AOP_GYRO_DEFAULT_US 100000 /* 10 Hz */ ++#define AOP_GYRO_MIN_US 10000 /* 100 Hz */ ++#define AOP_GYRO_MAX_US 1000000 /* 1 Hz */ ++ ++/* pi / 180 rad/s / 65536 = 1171270 / 2^42 rad/s */ ++#define AOP_GYRO_SCALE 1171270 ++#define AOP_GYRO_SCALE_LOG2 42 ++ ++struct apple_aop_gyro { ++ struct device *dev; ++ const struct apple_aop_service *service; ++ struct iio_mount_matrix orientation; ++ ++ struct mutex lock; /* interval */ ++ u32 interval_us; ++ ++ spinlock_t sample_lock; /* sample, have_sample */ ++ s32 sample[3]; ++ bool have_sample; ++ ++ IIO_DECLARE_BUFFER_WITH_TS(s32, scan, 3); ++}; ++ ++static int apple_aop_gyro_set_u32(struct apple_aop_gyro *gyro, u32 key, u32 value) ++{ ++ __le32 le = cpu_to_le32(value); ++ ++ return apple_aop_set_property(gyro->service, key, &le, sizeof(le)); ++} ++ ++/* No separate raw sample stream: raw interval 0. */ ++static int apple_aop_gyro_set_interval(struct apple_aop_gyro *gyro, u32 interval_us) ++{ ++ int ret; ++ ++ ret = apple_aop_gyro_set_u32(gyro, AOP_PROP_BATCH_INTERVAL, interval_us); ++ if (!ret) ++ ret = apple_aop_gyro_set_u32(gyro, AOP_PROP_RAW_INTERVAL, 0); ++ if (!ret) ++ ret = apple_aop_gyro_set_u32(gyro, AOP_PROP_REPORT_INTERVAL, interval_us); ++ if (!ret) ++ gyro->interval_us = interval_us; ++ return ret; ++} ++ ++static void apple_aop_gyro_report(void *cookie, u16 subtype, const void *data, size_t len) ++{ ++ struct iio_dev *indio_dev = cookie; ++ struct apple_aop_gyro *gyro = iio_priv(indio_dev); ++ unsigned long flags; ++ int i; ++ ++ if (subtype != AOP_REPORT_GYRO || len < AOP_GYRO_LEN) ++ return; ++ ++ spin_lock_irqsave(&gyro->sample_lock, flags); ++ for (i = 0; i < 3; i++) ++ gyro->sample[i] = get_unaligned_le32(data + AOP_GYRO_XYZ + 4 * i); ++ gyro->have_sample = true; ++ spin_unlock_irqrestore(&gyro->sample_lock, flags); ++ ++ if (iio_buffer_enabled(indio_dev)) { ++ memcpy(gyro->scan, gyro->sample, sizeof(gyro->sample)); ++ iio_push_to_buffers_with_ts(indio_dev, gyro->scan, sizeof(gyro->scan), ++ iio_get_time_ns(indio_dev)); ++ } ++} ++ ++static int apple_aop_gyro_read_raw(struct iio_dev *indio_dev, ++ struct iio_chan_spec const *chan, ++ int *val, int *val2, long mask) ++{ ++ struct apple_aop_gyro *gyro = iio_priv(indio_dev); ++ unsigned long flags; ++ int ret; ++ ++ switch (mask) { ++ case IIO_CHAN_INFO_RAW: ++ spin_lock_irqsave(&gyro->sample_lock, flags); ++ *val = gyro->sample[chan->scan_index]; ++ ret = gyro->have_sample ? IIO_VAL_INT : -EAGAIN; ++ spin_unlock_irqrestore(&gyro->sample_lock, flags); ++ return ret; ++ case IIO_CHAN_INFO_SCALE: ++ *val = AOP_GYRO_SCALE; ++ *val2 = AOP_GYRO_SCALE_LOG2; ++ return IIO_VAL_FRACTIONAL_LOG2; ++ case IIO_CHAN_INFO_SAMP_FREQ: ++ mutex_lock(&gyro->lock); ++ *val = USEC_PER_SEC / gyro->interval_us; ++ *val2 = (u64)(USEC_PER_SEC % gyro->interval_us) * USEC_PER_SEC / ++ gyro->interval_us; ++ mutex_unlock(&gyro->lock); ++ return IIO_VAL_INT_PLUS_MICRO; ++ default: ++ return -EINVAL; ++ } ++} ++ ++static int apple_aop_gyro_write_raw(struct iio_dev *indio_dev, ++ struct iio_chan_spec const *chan, ++ int val, int val2, long mask) ++{ ++ struct apple_aop_gyro *gyro = iio_priv(indio_dev); ++ u64 micro_hz; ++ u32 interval; ++ int ret; ++ ++ if (mask != IIO_CHAN_INFO_SAMP_FREQ) ++ return -EINVAL; ++ if (val < 0 || val2 < 0) ++ return -EINVAL; ++ ++ micro_hz = (u64)val * USEC_PER_SEC + val2; ++ if (!micro_hz) ++ return -EINVAL; ++ interval = clamp_t(u64, div64_u64((u64)USEC_PER_SEC * USEC_PER_SEC, micro_hz), ++ AOP_GYRO_MIN_US, AOP_GYRO_MAX_US); ++ ++ mutex_lock(&gyro->lock); ++ ret = apple_aop_gyro_set_interval(gyro, interval); ++ mutex_unlock(&gyro->lock); ++ return ret; ++} ++ ++static const struct iio_mount_matrix * ++apple_aop_gyro_get_mount_matrix(const struct iio_dev *indio_dev, ++ const struct iio_chan_spec *chan) ++{ ++ struct apple_aop_gyro *gyro = iio_priv(indio_dev); ++ ++ return &gyro->orientation; ++} ++ ++static const struct iio_chan_spec_ext_info apple_aop_gyro_ext_info[] = { ++ IIO_MOUNT_MATRIX(IIO_SHARED_BY_TYPE, apple_aop_gyro_get_mount_matrix), ++ { } ++}; ++ ++#define APPLE_AOP_GYRO_CHANNEL(axis, index) { \ ++ .type = IIO_ANGL_VEL, \ ++ .modified = 1, \ ++ .channel2 = IIO_MOD_##axis, \ ++ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW), \ ++ .info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE), \ ++ .info_mask_shared_by_all = BIT(IIO_CHAN_INFO_SAMP_FREQ), \ ++ .scan_index = index, \ ++ .scan_type = { \ ++ .sign = 's', \ ++ .realbits = 32, \ ++ .storagebits = 32, \ ++ .endianness = IIO_CPU, \ ++ }, \ ++ .ext_info = apple_aop_gyro_ext_info, \ ++} ++ ++static const struct iio_chan_spec apple_aop_gyro_channels[] = { ++ APPLE_AOP_GYRO_CHANNEL(X, 0), ++ APPLE_AOP_GYRO_CHANNEL(Y, 1), ++ APPLE_AOP_GYRO_CHANNEL(Z, 2), ++ IIO_CHAN_SOFT_TIMESTAMP(3), ++}; ++ ++static const struct iio_info apple_aop_gyro_info = { ++ .read_raw = apple_aop_gyro_read_raw, ++ .write_raw = apple_aop_gyro_write_raw, ++}; ++ ++/* Sensitivity and interrupt calibration make it complete */ ++static const struct apple_aop_calibration apple_aop_gyro_calibration[] = { ++ { "apple,gyro-sensitivity-calibration", 0x0b, 1 }, ++ { "apple,gyro-interrupt-calibration", 0x19, 2 }, ++ { "apple,gyro-range-sensitivity-cal", 0x27, 0 }, ++ { "apple,gyro-trim-calibration", 0x20, 0 }, ++}; ++ ++static void apple_aop_gyro_stop(void *data) ++{ ++ struct apple_aop_gyro *gyro = data; ++ ++ apple_aop_gyro_set_u32(gyro, AOP_PROP_REPORT_INTERVAL, 0); ++ apple_aop_unlisten(gyro->service); ++} ++ ++static int apple_aop_gyro_probe(struct platform_device *pdev) ++{ ++ struct device *dev = &pdev->dev; ++ const struct apple_aop_service *service = dev_get_platdata(dev); ++ struct apple_aop_gyro *gyro; ++ struct iio_dev *indio_dev; ++ int ret; ++ ++ if (!service) ++ return -ENODEV; ++ ++ indio_dev = devm_iio_device_alloc(dev, sizeof(*gyro)); ++ if (!indio_dev) ++ return -ENOMEM; ++ gyro = iio_priv(indio_dev); ++ gyro->dev = dev; ++ gyro->service = service; ++ mutex_init(&gyro->lock); ++ spin_lock_init(&gyro->sample_lock); ++ ++ ret = iio_read_mount_matrix(dev, &gyro->orientation); ++ if (ret) ++ return ret; ++ ++ indio_dev->name = "apple-aop-gyro"; ++ indio_dev->info = &apple_aop_gyro_info; ++ indio_dev->channels = apple_aop_gyro_channels; ++ indio_dev->num_channels = ARRAY_SIZE(apple_aop_gyro_channels); ++ indio_dev->modes = INDIO_DIRECT_MODE; ++ ++ ret = devm_iio_kfifo_buffer_setup(dev, indio_dev, NULL); ++ if (ret) ++ return ret; ++ ++ ret = apple_aop_listen(service, apple_aop_gyro_report, indio_dev); ++ if (ret) ++ return ret; ++ ++ ret = apple_aop_calibrate(service, dev, apple_aop_gyro_calibration, ++ ARRAY_SIZE(apple_aop_gyro_calibration)); ++ if (!ret) ++ ret = apple_aop_gyro_set_interval(gyro, AOP_GYRO_DEFAULT_US); ++ if (ret) { ++ apple_aop_unlisten(service); ++ return dev_err_probe(dev, ret, "failed to start reports\n"); ++ } ++ ++ ret = devm_add_action_or_reset(dev, apple_aop_gyro_stop, gyro); ++ if (ret) ++ return ret; ++ ++ return devm_iio_device_register(dev, indio_dev); ++} ++ ++static const struct of_device_id apple_aop_gyro_of_match[] = { ++ { .compatible = "apple,s8000-aop-gyro" }, ++ { } ++}; ++MODULE_DEVICE_TABLE(of, apple_aop_gyro_of_match); ++ ++static struct platform_driver apple_aop_gyro_driver = { ++ .driver = { ++ .name = "apple-aop-gyro", ++ .of_match_table = apple_aop_gyro_of_match, ++ }, ++ .probe = apple_aop_gyro_probe, ++}; ++module_platform_driver(apple_aop_gyro_driver); ++ ++MODULE_AUTHOR("Berkay Kösebay "); ++MODULE_DESCRIPTION("Apple AOP gyroscope driver"); ++MODULE_LICENSE("Dual MIT/GPL"); +diff --git a/drivers/iio/pressure/Kconfig b/drivers/iio/pressure/Kconfig +index 838a8340c..18535ad99 100644 +--- a/drivers/iio/pressure/Kconfig ++++ b/drivers/iio/pressure/Kconfig +@@ -384,4 +384,16 @@ config ADP810 + To compile this driver as a module, choose M here: the module will + be called adp810 + ++config APPLE_AOP_PRESSURE ++ tristate "Apple AOP barometric pressure sensor" ++ depends on APPLE_AOP_T8010 ++ select IIO_BUFFER ++ select IIO_KFIFO_BUF ++ help ++ Say yes here to get support for the barometric pressure sensor that ++ the Always-On Processor (AOP) of Apple A9 iPads and iPhones serves. ++ ++ To compile this driver as a module, choose M here: the module will be ++ called apple_aop_pressure. ++ + endmenu +diff --git a/drivers/iio/pressure/Makefile b/drivers/iio/pressure/Makefile +index bc0d11a20..bf3e27ba9 100644 +--- a/drivers/iio/pressure/Makefile ++++ b/drivers/iio/pressure/Makefile +@@ -9,6 +9,7 @@ obj-$(CONFIG_ABP2030PA) += abp2030pa.o + obj-$(CONFIG_ABP2030PA_I2C) += abp2030pa_i2c.o + obj-$(CONFIG_ABP2030PA_SPI) += abp2030pa_spi.o + obj-$(CONFIG_ADP810) += adp810.o ++obj-$(CONFIG_APPLE_AOP_PRESSURE) += apple_aop_pressure.o + obj-$(CONFIG_ROHM_BM1390) += rohm-bm1390.o + obj-$(CONFIG_BMP280) += bmp280.o + bmp280-objs := bmp280-core.o bmp280-regmap.o +diff --git a/drivers/iio/pressure/apple_aop_pressure.c b/drivers/iio/pressure/apple_aop_pressure.c +new file mode 100644 +index 000000000..8cd983c57 +--- /dev/null ++++ b/drivers/iio/pressure/apple_aop_pressure.c +@@ -0,0 +1,257 @@ ++// SPDX-License-Identifier: GPL-2.0-only OR MIT ++/* ++ * Barometric pressure sensor served by the Apple Always-On Processor (AOP) ++ * ++ * The AOP firmware samples the pressure sensor itself. Once it has the unit's ++ * factory calibration and its batch, raw and report intervals (EPIC ++ * set-property tags 0x16, 0x5c and 0), it sends one report (EPIC subtype ++ * 0xc4) per report interval: ++ * ++ * 0 u16 sequence ++ * 2 u32 pressure in 1/65536 kPa ++ * 6 s32 temperature (unit unknown, not exported) ++ */ ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#define AOP_PROP_REPORT_INTERVAL 0x00 ++#define AOP_PROP_BATCH_INTERVAL 0x16 ++#define AOP_PROP_RAW_INTERVAL 0x5c ++ ++#define AOP_REPORT_PRESSURE 0xc4 ++#define AOP_PRESSURE_LEN 10 ++#define AOP_PRESSURE_VALUE 2 ++ ++#define AOP_PRESSURE_DEFAULT_US 100000 /* 10 Hz */ ++#define AOP_PRESSURE_MIN_US 10000 /* 100 Hz */ ++#define AOP_PRESSURE_MAX_US 1000000 /* 1 Hz */ ++ ++struct apple_aop_pressure { ++ struct device *dev; ++ const struct apple_aop_service *service; ++ ++ struct mutex lock; /* interval */ ++ u32 interval_us; ++ ++ spinlock_t sample_lock; /* sample, have_sample */ ++ u32 sample; ++ bool have_sample; ++ ++ IIO_DECLARE_BUFFER_WITH_TS(u32, scan, 1); ++}; ++ ++static int apple_aop_pressure_set_u32(struct apple_aop_pressure *pressure, u32 key, u32 value) ++{ ++ __le32 le = cpu_to_le32(value); ++ ++ return apple_aop_set_property(pressure->service, key, &le, sizeof(le)); ++} ++ ++/* No separate raw sample stream: raw interval 0. */ ++static int apple_aop_pressure_set_interval(struct apple_aop_pressure *pressure, u32 interval_us) ++{ ++ int ret; ++ ++ ret = apple_aop_pressure_set_u32(pressure, AOP_PROP_BATCH_INTERVAL, interval_us); ++ if (!ret) ++ ret = apple_aop_pressure_set_u32(pressure, AOP_PROP_RAW_INTERVAL, 0); ++ if (!ret) ++ ret = apple_aop_pressure_set_u32(pressure, AOP_PROP_REPORT_INTERVAL, interval_us); ++ if (!ret) ++ pressure->interval_us = interval_us; ++ return ret; ++} ++ ++static void apple_aop_pressure_report(void *cookie, u16 subtype, const void *data, size_t len) ++{ ++ struct iio_dev *indio_dev = cookie; ++ struct apple_aop_pressure *pressure = iio_priv(indio_dev); ++ unsigned long flags; ++ ++ if (subtype != AOP_REPORT_PRESSURE || len < AOP_PRESSURE_LEN) ++ return; ++ ++ spin_lock_irqsave(&pressure->sample_lock, flags); ++ pressure->sample = get_unaligned_le32(data + AOP_PRESSURE_VALUE); ++ pressure->have_sample = true; ++ spin_unlock_irqrestore(&pressure->sample_lock, flags); ++ ++ if (iio_buffer_enabled(indio_dev)) { ++ pressure->scan[0] = pressure->sample; ++ iio_push_to_buffers_with_ts(indio_dev, pressure->scan, sizeof(pressure->scan), ++ iio_get_time_ns(indio_dev)); ++ } ++} ++ ++static int apple_aop_pressure_read_raw(struct iio_dev *indio_dev, ++ struct iio_chan_spec const *chan, ++ int *val, int *val2, long mask) ++{ ++ struct apple_aop_pressure *pressure = iio_priv(indio_dev); ++ unsigned long flags; ++ int ret; ++ ++ switch (mask) { ++ case IIO_CHAN_INFO_RAW: ++ spin_lock_irqsave(&pressure->sample_lock, flags); ++ *val = pressure->sample; ++ ret = pressure->have_sample ? IIO_VAL_INT : -EAGAIN; ++ spin_unlock_irqrestore(&pressure->sample_lock, flags); ++ return ret; ++ case IIO_CHAN_INFO_SCALE: ++ /* kPa */ ++ *val = 1; ++ *val2 = 16; ++ return IIO_VAL_FRACTIONAL_LOG2; ++ case IIO_CHAN_INFO_SAMP_FREQ: ++ mutex_lock(&pressure->lock); ++ *val = USEC_PER_SEC / pressure->interval_us; ++ *val2 = (u64)(USEC_PER_SEC % pressure->interval_us) * USEC_PER_SEC / ++ pressure->interval_us; ++ mutex_unlock(&pressure->lock); ++ return IIO_VAL_INT_PLUS_MICRO; ++ default: ++ return -EINVAL; ++ } ++} ++ ++static int apple_aop_pressure_write_raw(struct iio_dev *indio_dev, ++ struct iio_chan_spec const *chan, ++ int val, int val2, long mask) ++{ ++ struct apple_aop_pressure *pressure = iio_priv(indio_dev); ++ u64 micro_hz; ++ u32 interval; ++ int ret; ++ ++ if (mask != IIO_CHAN_INFO_SAMP_FREQ) ++ return -EINVAL; ++ if (val < 0 || val2 < 0) ++ return -EINVAL; ++ ++ micro_hz = (u64)val * USEC_PER_SEC + val2; ++ if (!micro_hz) ++ return -EINVAL; ++ interval = clamp_t(u64, div64_u64((u64)USEC_PER_SEC * USEC_PER_SEC, micro_hz), ++ AOP_PRESSURE_MIN_US, AOP_PRESSURE_MAX_US); ++ ++ mutex_lock(&pressure->lock); ++ ret = apple_aop_pressure_set_interval(pressure, interval); ++ mutex_unlock(&pressure->lock); ++ return ret; ++} ++ ++static const struct iio_chan_spec apple_aop_pressure_channels[] = { ++ { ++ .type = IIO_PRESSURE, ++ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | BIT(IIO_CHAN_INFO_SCALE), ++ .info_mask_shared_by_all = BIT(IIO_CHAN_INFO_SAMP_FREQ), ++ .scan_index = 0, ++ .scan_type = { ++ .sign = 'u', ++ .realbits = 32, ++ .storagebits = 32, ++ .endianness = IIO_CPU, ++ }, ++ }, ++ IIO_CHAN_SOFT_TIMESTAMP(1), ++}; ++ ++static const struct iio_info apple_aop_pressure_info = { ++ .read_raw = apple_aop_pressure_read_raw, ++ .write_raw = apple_aop_pressure_write_raw, ++}; ++ ++/* The offset or the acceleration sensitivity makes it complete */ ++static const struct apple_aop_calibration apple_aop_pressure_calibration[] = { ++ { "apple,pressure-offset-calibration", 0x0b, 1 }, ++ { "apple,pressure-accel-sensitivity", 0xd3, 1 }, ++ { "apple,pressure-heater-resistance", 0xd4, 0 }, ++}; ++ ++static void apple_aop_pressure_stop(void *data) ++{ ++ struct apple_aop_pressure *pressure = data; ++ ++ apple_aop_pressure_set_u32(pressure, AOP_PROP_REPORT_INTERVAL, 0); ++ apple_aop_unlisten(pressure->service); ++} ++ ++static int apple_aop_pressure_probe(struct platform_device *pdev) ++{ ++ struct device *dev = &pdev->dev; ++ const struct apple_aop_service *service = dev_get_platdata(dev); ++ struct apple_aop_pressure *pressure; ++ struct iio_dev *indio_dev; ++ int ret; ++ ++ if (!service) ++ return -ENODEV; ++ ++ indio_dev = devm_iio_device_alloc(dev, sizeof(*pressure)); ++ if (!indio_dev) ++ return -ENOMEM; ++ pressure = iio_priv(indio_dev); ++ pressure->dev = dev; ++ pressure->service = service; ++ mutex_init(&pressure->lock); ++ spin_lock_init(&pressure->sample_lock); ++ ++ indio_dev->name = "apple-aop-pressure"; ++ indio_dev->info = &apple_aop_pressure_info; ++ indio_dev->channels = apple_aop_pressure_channels; ++ indio_dev->num_channels = ARRAY_SIZE(apple_aop_pressure_channels); ++ indio_dev->modes = INDIO_DIRECT_MODE; ++ ++ ret = devm_iio_kfifo_buffer_setup(dev, indio_dev, NULL); ++ if (ret) ++ return ret; ++ ++ ret = apple_aop_listen(service, apple_aop_pressure_report, indio_dev); ++ if (ret) ++ return ret; ++ ++ ret = apple_aop_calibrate(service, dev, apple_aop_pressure_calibration, ++ ARRAY_SIZE(apple_aop_pressure_calibration)); ++ if (!ret) ++ ret = apple_aop_pressure_set_interval(pressure, AOP_PRESSURE_DEFAULT_US); ++ if (ret) { ++ apple_aop_unlisten(service); ++ return dev_err_probe(dev, ret, "failed to start reports\n"); ++ } ++ ++ ret = devm_add_action_or_reset(dev, apple_aop_pressure_stop, pressure); ++ if (ret) ++ return ret; ++ ++ return devm_iio_device_register(dev, indio_dev); ++} ++ ++static const struct of_device_id apple_aop_pressure_of_match[] = { ++ { .compatible = "apple,s8000-aop-pressure" }, ++ { } ++}; ++MODULE_DEVICE_TABLE(of, apple_aop_pressure_of_match); ++ ++static struct platform_driver apple_aop_pressure_driver = { ++ .driver = { ++ .name = "apple-aop-pressure", ++ .of_match_table = apple_aop_pressure_of_match, ++ }, ++ .probe = apple_aop_pressure_probe, ++}; ++module_platform_driver(apple_aop_pressure_driver); ++ ++MODULE_AUTHOR("Berkay Kösebay "); ++MODULE_DESCRIPTION("Apple AOP barometric pressure sensor driver"); ++MODULE_LICENSE("Dual MIT/GPL"); diff --git a/kernel/patches/drivers/mailbox/0001-dt-bindings-mailbox-select-certified-AOP-reload-thro.patch b/kernel/patches/drivers/mailbox/0001-dt-bindings-mailbox-select-certified-AOP-reload-thro.patch new file mode 100644 index 00000000..f98a0dbf --- /dev/null +++ b/kernel/patches/drivers/mailbox/0001-dt-bindings-mailbox-select-certified-AOP-reload-thro.patch @@ -0,0 +1,30 @@ +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] dt-bindings: mailbox: select certified AOP reload through DT + +A retained AOP must not be reset as though it were a fresh boot. +Describe the compatible and retained-reload property. + +Assisted-by: LLM +--- + .../devicetree/bindings/mailbox/apple,mailbox.yaml | 6 ++++++ + 1 file changed, 6 insertions(+) + +diff --git a/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml b/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml +index ba035ff80..8821623d3 100644 +--- a/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml ++++ b/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml +@@ -22,6 +22,12 @@ description: + properties: + compatible: + oneOf: ++ - description: ++ The same A7 IOP mailbox on the A9. ++ items: ++ - const: apple,s8000-a7iop-mailbox ++ - const: apple,t8010-a7iop-mailbox ++ + - description: + ASC mailboxes are the most common variant found on the M1 used + for example for the display controller, the system management diff --git a/kernel/patches/drivers/pmdomain/0001-pmdomain-apple-select-certified-AOP-reload-through-D.patch b/kernel/patches/drivers/pmdomain/0001-pmdomain-apple-select-certified-AOP-reload-through-D.patch new file mode 100644 index 00000000..d9a8480b --- /dev/null +++ b/kernel/patches/drivers/pmdomain/0001-pmdomain-apple-select-certified-AOP-reload-through-D.patch @@ -0,0 +1,127 @@ +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] pmdomain: apple: select certified AOP reload through DT + +A retained AOP must not be reset as though it were a fresh boot. Select +retained reload through device-tree data, preserving ordinary +initialization for other hardware. + +Assisted-by: LLM +--- + drivers/pmdomain/apple/pmgr-pwrstate.c | 84 ++++++++++++++++++++++++++ + 1 file changed, 84 insertions(+) + +diff --git a/drivers/pmdomain/apple/pmgr-pwrstate.c b/drivers/pmdomain/apple/pmgr-pwrstate.c +index 65dc32d30..0ae0eda2d 100644 +--- a/drivers/pmdomain/apple/pmgr-pwrstate.c ++++ b/drivers/pmdomain/apple/pmgr-pwrstate.c +@@ -10,6 +10,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -17,6 +18,7 @@ + #include + #include + #include ++#include + + #define APPLE_PMGR_RESET BIT(31) + #define APPLE_PMGR_AUTO_ENABLE BIT(28) +@@ -417,6 +419,7 @@ static int apple_pmgr_ps_probe(struct platform_device *pdev) + goto err_remove; + } + ++ platform_set_drvdata(pdev, ps); + return 0; + err_remove: + if (ps->has_genpd) { +@@ -446,3 +449,84 @@ MODULE_AUTHOR("Hector Martin "); + MODULE_DESCRIPTION("PMGR power state driver for Apple SoCs"); + + module_platform_driver(apple_pmgr_ps_driver); ++ ++/* ++ * Read-only observer for bring-up. The caller's nodes follow the same ++ * child -> parent order as genpd power transitions. No domain is attached, ++ * resumed, pinned on, reset, or written by this function. ++ */ ++int apple_pmgr_observe(struct device_node **nodes, unsigned int count, ++ struct apple_pmgr_observation *states, ++ void (*read)(void *), void *data) ++{ ++ struct platform_device *providers[5]; ++ struct apple_pmgr_ps *ps[5]; ++ unsigned long flags; ++ unsigned int i, j, found = 0; ++ int ret = 0; ++ bool accessible = true; ++ ++ if (!count || count > ARRAY_SIZE(providers) || !read) ++ return -EINVAL; ++ for (i = 0; i < count; i++) { ++ for (j = 0; j < i; j++) ++ if (nodes[i] == nodes[j]) { ++ ret = -EINVAL; ++ goto put; ++ } ++ providers[i] = of_find_device_by_node(nodes[i]); ++ if (!providers[i]) { ++ ret = -EPROBE_DEFER; ++ goto put; ++ } ++ device_lock(&providers[i]->dev); ++ found++; ++ if (!providers[i]->dev.driver || ++ strcmp(providers[i]->dev.driver->name, "apple-pmgr-pwrstate")) { ++ ret = -EPROBE_DEFER; ++ goto put; ++ } ++ ps[i] = platform_get_drvdata(providers[i]); ++ if (!ps[i]) { ++ ret = -EPROBE_DEFER; ++ goto put; ++ } ++ } ++ ++ /* PMGR domains use GENPD_FLAG_IRQ_SAFE, i.e. genpd.slock. */ ++ local_irq_save(flags); ++ for (i = 0; i < count; i++) ++ spin_lock_nested(&ps[i]->genpd.slock, i); ++ for (i = 0; i < count; i++) ++ spin_lock_nested(&ps[i]->lock, i); ++ for (i = 0; i < count; i++) { ++ states[i].genpd_status = ps[i]->genpd.status; ++ ret = regmap_read(ps[i]->regmap, ps[i]->offset, &states[i].raw); ++ if (ret) ++ goto unlock; ++ if (states[i].genpd_status != GENPD_STATE_ON || ++ FIELD_GET(APPLE_PMGR_PS_ACTUAL, states[i].raw) != APPLE_PMGR_PS_ACTIVE || ++ FIELD_GET(APPLE_PMGR_PS_TARGET, states[i].raw) != APPLE_PMGR_PS_ACTIVE || ++ states[i].raw & (APPLE_PMGR_RESET | APPLE_PMGR_DEV_DISABLE | ++ APPLE_PMGR_PARENT_OFF)) ++ accessible = false; ++ } ++ if (accessible) ++ read(data); ++ else ++ ret = -EHOSTDOWN; ++unlock: ++ for (i = count; i > 0; i--) ++ spin_unlock(&ps[i - 1]->lock); ++ for (i = count; i > 0; i--) ++ spin_unlock(&ps[i - 1]->genpd.slock); ++ local_irq_restore(flags); ++put: ++ while (found) { ++ found--; ++ device_unlock(&providers[found]->dev); ++ put_device(&providers[found]->dev); ++ } ++ return ret; ++} ++EXPORT_SYMBOL_GPL(apple_pmgr_observe); diff --git a/kernel/patches/drivers/soc-apple/0014-dt-bindings-soc-apple-select-certified-AOP-reload-th.patch b/kernel/patches/drivers/soc-apple/0014-dt-bindings-soc-apple-select-certified-AOP-reload-th.patch new file mode 100644 index 00000000..ad082c85 --- /dev/null +++ b/kernel/patches/drivers/soc-apple/0014-dt-bindings-soc-apple-select-certified-AOP-reload-th.patch @@ -0,0 +1,116 @@ +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] dt-bindings: soc-apple: select certified AOP reload through + DT + +A retained AOP must not be reset as though it were a fresh boot. +Describe the compatible and retained-reload property. + +Assisted-by: LLM +--- + .../bindings/soc/apple/apple,t8010-aop.yaml | 58 +++++++++++++++++-- + 1 file changed, 54 insertions(+), 4 deletions(-) + +diff --git a/Documentation/devicetree/bindings/soc/apple/apple,t8010-aop.yaml b/Documentation/devicetree/bindings/soc/apple/apple,t8010-aop.yaml +index 14726afa0..df6621d74 100644 +--- a/Documentation/devicetree/bindings/soc/apple/apple,t8010-aop.yaml ++++ b/Documentation/devicetree/bindings/soc/apple/apple,t8010-aop.yaml +@@ -4,22 +4,28 @@ + $id: http://devicetree.org/schemas/soc/apple/apple,t8010-aop.yaml# + $schema: http://devicetree.org/meta-schemas/core.yaml# + +-title: Apple T8010 Always-On Processor ++title: Apple S8000 and T8010 Always-On Processor + + maintainers: + - Dalton Alexandre + + description: +- The Always-On Processor (AOP) of the A10 is an ARMv7 coprocessor that +- runs RTKit firmware and serves the motion sensors (accelerometer, ++ The Always-On Processor (AOP) of the A9 and A10 is an ARMv7 coprocessor ++ that runs RTKit firmware and serves the motion sensors (accelerometer, + gyroscope, compass, pressure) and the always-on microphones. It runs from + local SRAM, with a further firmware segment in system memory, and talks + to the application processor through an A7 IOP mailbox. Its firmware is + board-specific and is loaded by the operating system. + + properties: ++ apple,certified-reload: ++ type: boolean ++ description: Firmware supports the retained shutdown certificate for warm reload. ++ + compatible: +- const: apple,t8010-aop ++ enum: ++ - apple,s8000-aop ++ - apple,t8010-aop + + reg: + items: +@@ -50,6 +56,38 @@ properties: + The chip's unique ID (ECID), filled in by the bootloader. The + firmware's boot arguments carry it. + ++ apple,aop-target: ++ $ref: /schemas/types.yaml#/definitions/uint32 ++ description: ++ The AOP target number from the Apple device tree, filled in by the ++ bootloader. The S8000 firmware's boot arguments carry it. ++ ++ apple,chip-id: ++ $ref: /schemas/types.yaml#/definitions/uint32 ++ description: ++ The SoC's chip ID (e.g. 0x8003), filled in by the bootloader. The S8000 ++ firmware's boot arguments carry it. ++ ++ apple,chip-revision: ++ $ref: /schemas/types.yaml#/definitions/uint32 ++ description: ++ The SoC's chip revision, filled in by the bootloader. The S8000 ++ firmware's boot arguments carry it. ++ ++ memory-region: ++ maxItems: 1 ++ description: Reserved 16 KiB shutdown transcript page for J71t warm reload. ++ ++ apple,observe-domains: ++ $ref: /schemas/types.yaml#/definitions/phandle-array ++ minItems: 5 ++ maxItems: 5 ++ description: AOP GPIO, AOP, bus interface, CPU and filter in lock order. ++ ++ apple,spi0-domain: ++ $ref: /schemas/types.yaml#/definitions/phandle ++ description: Read-only SPI0 power-state provider observation. ++ + patternProperties: + "^(accel|gyro|compass|pressure)$": + type: object +@@ -58,6 +96,9 @@ patternProperties: + properties: + compatible: + enum: ++ - apple,s8000-aop-accel ++ - apple,s8000-aop-gyro ++ - apple,s8000-aop-pressure + - apple,t8010-aop-accel + - apple,t8010-aop-gyro + - apple,t8010-aop-compass +@@ -65,6 +106,15 @@ patternProperties: + + mount-matrix: + description: The sensor's mounting rotation in the device. ++ ++ patternProperties: ++ "^apple,(low-temp-accel-offset|(accel|gyro)-(sensitivity|interrupt)-calibration|(accel|gyro)-range-sensitivity-cal|gyro-trim-calibration|pressure-(offset-calibration|accel-sensitivity|heater-resistance))$": ++ $ref: /schemas/types.yaml#/definitions/uint8-array ++ maxItems: 64 ++ description: ++ The unit's factory calibration of the sensor, copied by the ++ bootloader from the property of the same name (without the ++ prefix) in the Apple device tree. The S8000 firmware takes it. + required: + - compatible + additionalProperties: false diff --git a/kernel/patches/drivers/soc-apple/0015-soc-apple-aop-select-certified-AOP-reload-through-DT.patch b/kernel/patches/drivers/soc-apple/0015-soc-apple-aop-select-certified-AOP-reload-through-DT.patch new file mode 100644 index 00000000..279d4de7 --- /dev/null +++ b/kernel/patches/drivers/soc-apple/0015-soc-apple-aop-select-certified-AOP-reload-through-DT.patch @@ -0,0 +1,3181 @@ +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] soc: apple: aop: select certified AOP reload through DT + +A retained AOP must not be reset as though it were a fresh boot. Select +retained reload through device-tree data, preserving ordinary +initialization for other hardware. + +Assisted-by: LLM +--- + MAINTAINERS | 2 + + drivers/soc/apple/Kconfig | 12 +- + drivers/soc/apple/aop-warm-record.h | 63 ++ + drivers/soc/apple/aop-warm-spi0.c.h | 1339 ++++++++++++++++++++++++ + drivers/soc/apple/aop-warm-spi0.h | 543 ++++++++++ + drivers/soc/apple/aop.c | 729 ++++++++++++- + drivers/soc/apple/rtkit.c | 50 + + include/linux/soc/apple/aop.h | 28 + + include/linux/soc/apple/pmgr-observe.h | 27 + + include/linux/soc/apple/rtkit.h | 2 + + 10 files changed, 2758 insertions(+), 37 deletions(-) + create mode 100644 drivers/soc/apple/aop-warm-record.h + create mode 100644 drivers/soc/apple/aop-warm-spi0.c.h + create mode 100644 drivers/soc/apple/aop-warm-spi0.h + create mode 100644 include/linux/soc/apple/pmgr-observe.h + +diff --git a/MAINTAINERS b/MAINTAINERS +index a4a69643c..12c292825 100644 +--- a/MAINTAINERS ++++ b/MAINTAINERS +@@ -2068,6 +2068,8 @@ APPLE T8010 ALWAYS-ON PROCESSOR + M: Dalton Alexandre + S: Maintained + F: Documentation/devicetree/bindings/soc/apple/apple,t8010-aop.yaml ++F: drivers/iio/gyro/apple_aop_gyro.c ++F: drivers/iio/pressure/apple_aop_pressure.c + F: drivers/soc/apple/aop.c + F: include/linux/soc/apple/aop.h + +diff --git a/drivers/soc/apple/Kconfig b/drivers/soc/apple/Kconfig +index e68a49f1b..c0af67b62 100644 +--- a/drivers/soc/apple/Kconfig ++++ b/drivers/soc/apple/Kconfig +@@ -39,16 +39,16 @@ config APPLE_RTKIT + Say 'y' here if you have an Apple SoC. + + config APPLE_AOP_T8010 +- tristate "Apple T8010 Always-On Processor" ++ tristate "Apple S8000 and T8010 Always-On Processor" + depends on APPLE_RTKIT + depends on ARCH_APPLE || COMPILE_TEST + help +- The Always-On Processor (AOP) of the Apple A10 runs the firmware for +- the motion sensors and the always-on microphones. This driver loads +- and starts that firmware and provides its services to the sensor +- drivers. ++ The Always-On Processor (AOP) of the Apple A9 and A10 runs the ++ firmware for the motion sensors and the always-on microphones. This ++ driver loads and starts that firmware and provides its services to ++ the sensor drivers. + +- Say 'y' or 'm' here if you have an A10-based iPad or iPhone. ++ Say 'y' or 'm' here if you have an A9- or A10-based iPad or iPhone. + + config APPLE_PMP_T8015_V1 + tristate "Apple T8015 PMP" +diff --git a/drivers/soc/apple/aop-warm-record.h b/drivers/soc/apple/aop-warm-record.h +new file mode 100644 +index 000000000..237d78d17 +--- /dev/null ++++ b/drivers/soc/apple/aop-warm-record.h +@@ -0,0 +1,63 @@ ++/* SPDX-License-Identifier: GPL-2.0-only */ ++#include ++#include ++#define A9_HO_PAGE 0x4000U ++#define A9_HO_LOG_OFFSET 0x800U ++#define A9_SL_MAGIC 0x4c533941U /* "A9SL" */ ++#define A9_SL_VERSION 1U ++ ++enum a9_ho_step { ++ A9_HO_STEP_BEGIN, ++ A9_HO_STEP_RATES, ++ A9_HO_STEP_AFK_SENT, ++ A9_HO_STEP_AFK_ACKED, ++ A9_HO_STEP_DISABLED, ++ A9_HO_STEP_AP_SENT, ++ A9_HO_STEP_AP_ACKED, ++ A9_HO_STEP_IOP_SENT, ++ A9_HO_STEP_IOP_ACKED, ++ A9_HO_STEP_RAM_STABLE, ++ A9_HO_STEP_MAILBOX_IDLE, ++ A9_HO_STEP_HALTED, ++ A9_HO_STEP_DRAINED, ++ A9_HO_STEPS, ++}; ++ ++static const char * const a9_ho_step_names[A9_HO_STEPS] = { ++ "begin", "rates-zero", "afk-sent", "afk-acked", "endpoint-disable", ++ "ap-sent", "ap-ack", "iop-sent", "iop-ack", "ram-stable", "mailbox-idle", ++ "halt", "drain", ++}; ++ ++struct a9_sl_header { ++ u32 magic, version, capacity, length; ++ u32 lines, truncated, checksum, mode; ++ u64 begin_ns; ++ u8 writer[16]; ++ char entry[64]; ++}; ++ ++_Static_assert(sizeof(struct a9_sl_header) == 120, "shutdown log ABI"); ++ ++#define A9_SL_CAPACITY (A9_HO_PAGE - A9_HO_LOG_OFFSET - sizeof(struct a9_sl_header)) ++ ++/* FNV-1a; incremental over appended text. */ ++static inline u32 a9_sl_hash(u32 hash, const char *text, size_t length) ++{ ++ size_t i; ++ ++ for (i = 0; i < length; i++) ++ hash = (hash ^ (u8)text[i]) * 16777619U; ++ return hash; ++} ++ ++#define A9_SL_HASH_SEED 2166136261U ++ ++/* A replayable previous transcript: complete header, bounded, intact. */ ++static inline bool a9_sl_valid(const struct a9_sl_header *h, const char *text) ++{ ++ return h->magic == A9_SL_MAGIC && h->version == A9_SL_VERSION && ++ h->capacity == A9_SL_CAPACITY && h->length <= h->capacity && ++ !h->entry[sizeof(h->entry) - 1] && ++ a9_sl_hash(A9_SL_HASH_SEED, text, h->length) == h->checksum; ++} +diff --git a/drivers/soc/apple/aop-warm-spi0.c.h b/drivers/soc/apple/aop-warm-spi0.c.h +new file mode 100644 +index 000000000..3c421d207 +--- /dev/null ++++ b/drivers/soc/apple/aop-warm-spi0.c.h +@@ -0,0 +1,1339 @@ ++/* SPDX-License-Identifier: GPL-2.0-only */ ++/* Ported from a9/aop-release: wake16, release6-d254a9f790. */ ++static void a9_aop_spi0_snapshot(void __iomem *base, u32 *values) ++{ ++ unsigned int i; ++ ++ for (i = 0; i < A9_SPI0_REGS; i++) ++ values[i] = readl(base + a9_spi0_offsets[i]); ++} ++ ++/* ++ * AOPSPIMaster_reset (0102f2f4) under the AOP-domain PMGR guard with IRQs ++ * off and the AOP CPU halted. Its provider power transition 3 is an ++ * AOP-local call with no AP register equivalent and is not replayed. A poll ++ * timeout still restores the saved control word. ++ */ ++static void a9_aop_spi0_reset_locked(void *data) ++{ ++ struct a9_aop_spi0 *s = data; ++ void __iomem *control = s->base + A9_SPI0_CONTROL; ++ u32 value = 0; ++ int ret; ++ ++ a9_aop_spi0_snapshot(s->base, s->before); ++ s->saved = s->before[A9_SPI0_I_CONTROL]; ++ s->asserted = a9_spi0_base_word(s->saved) | A9_SPI0_RESET; ++ writel(s->asserted, control); ++ ret = readl_poll_timeout_atomic(control, value, value & A9_SPI0_RESET, ++ 1, A9_SPI0_POLL_US); ++ if (!ret) ++ ret = readl_poll_timeout_atomic(control, value, ++ !(value & A9_SPI0_ENABLE), 1, ++ A9_SPI0_POLL_US); ++ s->polled = value; ++ if (!ret) { ++ udelay(A9_SPI0_DELAY_US); ++ writel(a9_spi0_base_word(s->saved), control); ++ udelay(A9_SPI0_DELAY_US); ++ } ++ writel(s->saved, control); ++ s->reset_done_ns = ktime_get_ns(); ++ a9_aop_spi0_snapshot(s->base, s->after); ++ if (!ret && (s->after[A9_SPI0_I_FAULT] & A9_SPI0_CH3_PENDING)) ++ ret = -EIO; ++ s->error = ret; ++} ++ ++/* ++ * Wake8 device evidence: SCM was 0 before/after the first controller reset, ++ * then became 3 at the channel-3 stop store. There is no firmware-derived ++ * SCM status write. Replay AOPSPIMaster_reset after the teardown so the ++ * controller reset, rather than an invented SCM write, is the one variable. ++ */ ++static void a9_aop_spi0_post_reset_locked(void *data) ++{ ++ struct a9_aop_spi0 *s = data; ++ struct a9_aop_spi0_post_reset *r = &s->post_reset; ++ void __iomem *control = s->base + A9_SPI0_CONTROL; ++ u32 value = 0; ++ int ret; ++ ++ a9_aop_spi0_snapshot(s->base, r->before); ++ r->saved = r->before[A9_SPI0_I_CONTROL]; ++ r->asserted = a9_spi0_base_word(r->saved) | A9_SPI0_RESET; ++ writel(r->asserted, control); ++ ret = readl_poll_timeout_atomic(control, value, value & A9_SPI0_RESET, ++ 1, A9_SPI0_POLL_US); ++ if (!ret) ++ ret = readl_poll_timeout_atomic(control, value, ++ !(value & A9_SPI0_ENABLE), 1, ++ A9_SPI0_POLL_US); ++ r->polled = value; ++ if (!ret) { ++ udelay(A9_SPI0_DELAY_US); ++ writel(a9_spi0_base_word(r->saved), control); ++ udelay(A9_SPI0_DELAY_US); ++ } ++ writel(r->saved, control); ++ a9_aop_spi0_snapshot(s->base, r->after); ++ if (!ret && (r->after[A9_SPI0_I_FAULT] & A9_SPI0_CH3_PENDING)) ++ ret = -EIO; ++ r->error = ret; ++} ++ ++static void a9_aop_spi0_ch3_snapshot(void __iomem *base, u32 *values) ++{ ++ unsigned int i; ++ ++ for (i = 0; i < A9_SPI0_CH3_REGS; i++) ++ values[i] = readl(base + a9_spi0_ch3_offsets[i]); ++} ++ ++static void a9_aop_spi0_trace_read(void *data) ++{ ++ struct a9_aop_spi0 *s = data; ++ unsigned int channel, i; ++ ++ s->trace.control = readl(s->base + A9_SPI0_CONTROL); ++ s->trace.clock = readl(s->base + A9_SPI0_CLOCK); ++ for (i = 0; i < A9_SPI0_SCM_REGS; i++) ++ s->trace.scm[i] = readl(s->base + a9_spi0_scm_offsets[i]); ++ a9_aop_spi0_ch3_snapshot(s->base, s->trace.ch3); ++ for (channel = 0; channel < A9_SPI0_CHANNELS; channel++) ++ for (i = 0; i < A9_SPI0_CHANNEL_REGS; i++) ++ s->trace.channel[channel][i] = ++ readl(s->base + A9_SPI0_CHANNEL_BASE + ++ A9_SPI0_CHANNEL_STRIDE * channel + ++ a9_spi0_channel_offsets[i]); ++} ++ ++/* Warm path only, CPU halted. Log outside the PMGR guard; no SCM writes. */ ++static int a9_aop_spi0_trace(struct apple_aop *aop, const char *stage) ++{ ++ struct apple_pmgr_observation states[AOP_DOMAINS] = {}; ++ struct a9_aop_spi0 *s = &aop->spi0; ++ const u32 *c = s->trace.ch3, *m = s->trace.scm; ++ const u32 *v; ++ unsigned int channel; ++ int ret; ++ ++ ret = apple_pmgr_observe(aop->domains, AOP_DOMAINS, states, ++ a9_aop_spi0_trace_read, s); ++ if (ret) { ++ s->intc.failed = "failed-spi0-trace-guard"; ++ dev_info(aop->dev, "SPI0 SCM stage=%s errno=%d: guard refused\n", stage, ret); ++ return ret; ++ } ++ dev_info(aop->dev, "SPI0 SCM stage=%s errno=0 ctl=%08x clk=%08x scm_lo=%08x scm_hi=%08x c3_interval=%08x c3_cursor=%08x c3=%08x route=%08x rate=%08x c3c=%08x desc=%08x 8d0=%08x 8d4=%08x cpu=halted\n", ++ stage, s->trace.control, s->trace.clock, m[0], m[1], m[2], m[3], ++ c[0], c[1], c[2], c[3], c[4], c[5], c[6]); ++ for (channel = 0; channel < A9_SPI0_CHANNELS; channel++) { ++ v = s->trace.channel[channel]; ++ dev_info(aop->dev, "SPI0 SCM channel stage=%s ch=%02u ctl=%08x route=%08x rate=%08x cfg=%08x interval=%08x cursor=%08x desc=%08x pending0=%u pending1=%u\n", ++ stage, channel, v[0], v[1], v[2], v[3], v[4], v[5], v[6], ++ !!(c[A9_CH3_I_8D0] & BIT(2 * channel)), ++ !!(c[A9_CH3_I_8D4] & BIT(2 * channel))); ++ } ++ return 0; ++} ++ ++static const char *a9_aop_spi0_selected_verify(struct a9_aop_spi0 *s); ++ ++/* ++ * Wake10 safety gate, strengthened in wake11 to retain every selected-channel ++ * teardown invariant. A fault observed by a copy sample remains terminal even ++ * if it is transient and the final read happens to clear. ++ */ ++static int a9_aop_spi0_gate(struct apple_aop *aop, const char *stage, ++ const char *check, bool include_copy) ++{ ++ struct a9_aop_spi0 *s = &aop->spi0; ++ const u32 *m, *c; ++ int ret; ++ ++ ret = a9_aop_spi0_trace(aop, stage); ++ if (ret) ++ return ret; ++ m = s->trace.scm; ++ c = s->trace.ch3; ++ if (m[0] || m[1]) ++ s->gate_failed = "failed-spi0-scm"; ++ else if (c[A9_CH3_I_8D0] || c[A9_CH3_I_8D4]) ++ s->gate_failed = "failed-spi0-fault"; ++ else if (include_copy && s->copy_faulted) ++ s->gate_failed = "failed-spi0-copy-sample"; ++ else { ++ s->gate_failed = a9_spi0_ch3_verify(c, s->teardown_stable & BIT(3)); ++ if (!s->gate_failed) ++ s->gate_failed = a9_aop_spi0_selected_verify(s); ++ } ++ ret = s->gate_failed ? -EIO : 0; ++ dev_info(aop->dev, "SPI0 %s check errno=%d scm_lo=%08x scm_hi=%08x 8d0=%08x 8d4=%08x copy_faulted=%u result=%s\n", ++ check, ret, m[0], m[1], c[A9_CH3_I_8D0], c[A9_CH3_I_8D4], ++ s->copy_faulted, s->gate_failed ?: "pass"); ++ return ret; ++} ++ ++#define A9_SPI0_COPY_CHUNK 256U ++#define A9_SPI0_COPY_SAMPLE_US 5000U ++ ++struct a9_aop_spi0_copy_probe { ++ void __iomem *base; ++ u32 scm_lo, scm_hi, fault, fault_8d4; ++}; ++ ++static void a9_aop_spi0_copy_probe_read(void *data) ++{ ++ struct a9_aop_spi0_copy_probe *p = data; ++ ++ p->scm_lo = readl(p->base + A9_SPI0_SCM_LO); ++ p->scm_hi = readl(p->base + A9_SPI0_SCM_HI); ++ p->fault = readl(p->base + A9_SPI0_FAULT); ++ p->fault_8d4 = readl(p->base + A9_SPI0_FAULT_8D4); ++} ++ ++/* ++ * Read-only samples spaced by at least 5 ms across both halves of the SRAM ++ * copy. Once a nonzero state is seen it remains a terminal fact, but the copy ++ * completes so later samples and the full after-copy channel snapshot expose ++ * whether the source is level, transient or moves between channels. ++ */ ++static int a9_aop_spi0_copy_sample(struct apple_aop *aop, const char *phase, ++ size_t offset, size_t size, u64 start_ns, ++ u64 *last_us, unsigned int *sample, ++ bool force) ++{ ++ struct apple_pmgr_observation states[AOP_DOMAINS] = {}; ++ struct a9_aop_spi0 *s = &aop->spi0; ++ struct a9_aop_spi0_copy_probe p = { .base = s->base }; ++ u64 elapsed_us = div_u64(ktime_get_boottime_ns() - start_ns, ++ NSEC_PER_USEC); ++ bool first_fault, initial = !*sample; ++ int ret; ++ ++ if (!force && *sample && elapsed_us - *last_us < A9_SPI0_COPY_SAMPLE_US) ++ return 0; ++ ret = apple_pmgr_observe(aop->domains, AOP_DOMAINS, states, ++ a9_aop_spi0_copy_probe_read, &p); ++ first_fault = !s->copy_faulted && ++ (p.scm_lo || p.scm_hi || p.fault || p.fault_8d4); ++ if (!ret && first_fault) ++ s->copy_faulted = true; ++ dev_info(aop->dev, "SPI0 COPY sample=%u phase=%s elapsed_us=%llu offset=0x%zx size=0x%zx scm_lo=%08x scm_hi=%08x 8d0=%08x 8d4=%08x result=%s\n", ++ *sample, phase, elapsed_us, offset, size, p.scm_lo, p.scm_hi, ++ p.fault, p.fault_8d4, ret ? "guard" : s->copy_faulted ? "fault" : "pass"); ++ *last_us = elapsed_us; ++ (*sample)++; ++ if (ret) ++ return ret; ++ if (first_fault) { ++ ret = a9_aop_spi0_trace(aop, "copy-fault"); ++ if (ret) ++ return ret; ++ } ++ /* The offset-zero sample is the last no-write gate before the copy. */ ++ return !initial || !s->copy_faulted ? 0 : -EIO; ++} ++ ++static int a9_aop_copy_verify_wake10(struct apple_aop *aop, const u8 *src, ++ size_t size) ++{ ++ u32 chunk[A9_SPI0_COPY_CHUNK / 4]; ++ u64 start_ns = ktime_get_boottime_ns(), last_us = 0; ++ unsigned int sample = 0; ++ size_t done, n; ++ int ret; ++ ++ ret = a9_aop_spi0_copy_sample(aop, "write", 0, size, start_ns, &last_us, &sample, true); ++ if (ret) ++ return ret; ++ for (done = 0; done < size; done += n) { ++ n = min_t(size_t, size - done, sizeof(chunk)); ++ __iowrite32_copy(aop->sram + done, src + done, n / 4); ++ ret = a9_aop_spi0_copy_sample(aop, "write", done + n, size, start_ns, ++ &last_us, &sample, done + n == size); ++ if (ret) ++ return ret; ++ } ++ ret = a9_aop_spi0_copy_sample(aop, "verify", 0, size, start_ns, &last_us, &sample, true); ++ if (ret) ++ return ret; ++ for (done = 0; done < size; done += n) { ++ n = min_t(size_t, size - done, sizeof(chunk)); ++ __ioread32_copy(chunk, aop->sram + done, n / 4); ++ if (memcmp(chunk, src + done, n)) ++ return -EIO; ++ ret = a9_aop_spi0_copy_sample(aop, "verify", done + n, size, start_ns, ++ &last_us, &sample, done + n == size); ++ if (ret) ++ return ret; ++ } ++ return 0; ++} ++ ++static void __iomem *a9_aop_spi0_channel_base(struct a9_aop_spi0 *s, ++ unsigned int channel) ++{ ++ return s->base + A9_SPI0_CHANNEL_BASE + A9_SPI0_CHANNEL_STRIDE * channel; ++} ++ ++/* ++ * Wake15: which other channels were RUN with a counting interval when this ++ * stop was issued, and which of them advanced across the stop. Read only; it ++ * separates a stop that had a ticking peer from the stop of the last live ++ * channel in the log. It never gates anything in-kernel. ++ */ ++static void a9_aop_spi0_peers_begin(struct a9_aop_spi0 *s, unsigned int channel, ++ u32 *interval, u32 *run) ++{ ++ unsigned int i; ++ u32 ctl; ++ ++ *run = 0; ++ for (i = 0; i < A9_SPI0_CHANNELS; i++) { ++ void __iomem *base = a9_aop_spi0_channel_base(s, i); ++ ++ interval[i] = 0; ++ if (i == channel) ++ continue; ++ ctl = readl(base + a9_spi0_channel_offsets[A9_CHANNEL_I_CTL]); ++ interval[i] = readl(base + a9_spi0_channel_offsets[A9_CHANNEL_I_INTERVAL]); ++ if ((ctl & A9_SPI0_CH3_RUN) && interval[i]) ++ *run |= BIT(i); ++ } ++} ++ ++static void a9_aop_spi0_stop_timing(struct a9_aop_spi0 *s, ++ struct a9_aop_spi0_ch3 *c, ++ const u32 *interval) ++{ ++ unsigned int i; ++ ++ c->peers_ticking = 0; ++ for (i = 0; i < A9_SPI0_CHANNELS; i++) { ++ void __iomem *base = a9_aop_spi0_channel_base(s, i); ++ ++ if ((c->peers_run & BIT(i)) && ++ readl(base + a9_spi0_channel_offsets[A9_CHANNEL_I_INTERVAL]) != ++ interval[i]) ++ c->peers_ticking |= BIT(i); ++ } ++ c->reset_to_stop_us = c->stop_ns > s->reset_done_ns ? ++ div_u64(c->stop_ns - s->reset_done_ns, NSEC_PER_USEC) : 0; ++} ++ ++/* ++ * Wake12/13 captured channel 7, and wake13 channel 3, stopped but never idle: ++ * for 20 complete slices the control/progress word, config, interval, cursor, ++ * SCM and both fault banks were unchanged. This predicate is intentionally ++ * narrower than "RUN clear and no progress": the channel must be one of the ++ * two device-observed periodic clients, must carry that client's route, rate ++ * and config before the stop, and every sample must be byte-identical with SCM ++ * 3/0 (every earlier non-idle stop, which followed a channel-3 stop that had ++ * latched it) or 0/0 (a first stop that latches nothing), no fault, no cursor ++ * movement and a retained nonzero interval. ++ * It only permits the callback masks, descriptor clear, config acknowledge and ++ * the established controller reset; every later SCM, fault and copy gate stays. ++ */ ++static const char *a9_aop_spi0_stable_why(struct a9_aop_spi0_ch3 *c, ++ const struct a9_aop_channel_diag_slice *diag, ++ unsigned int channel) ++{ ++ const struct a9_aop_ch3_slice *first; ++ unsigned int i; ++ u32 route; ++ ++ if (channel == A9_SPI0_PHOSPHORUS_CHANNEL) ++ route = A9_SPI0_PHOSPHORUS_ROUTE; ++ else if (channel == A9_SPI0_MPU_ASYNC_CHANNEL) ++ route = A9_SPI0_MPU_ASYNC_ROUTE; ++ else ++ return "channel"; ++ if (c->poll != -ETIMEDOUT || c->slices != A9_SPI0_CH3_SLICES) ++ return "no-timeout"; ++ if (!(c->before[A9_CH3_I_C3] & A9_SPI0_CH3_RUN) || ++ c->before[A9_CH3_I_ROUTE] != route || ++ c->before[A9_CH3_I_RATE] != A9_SPI0_PHOSPHORUS_RATE || ++ c->before[A9_CH3_I_C3C] != A9_SPI0_PHOSPHORUS_CFG || ++ c->before[A9_CH3_I_DESC] || (c->stop & A9_SPI0_CH3_RUN)) ++ return "before-identity"; ++ if (c->before[A9_CH3_I_8D4] || (c->before[A9_CH3_I_8D0] & ~BIT(2 * channel))) ++ return "before-fault"; ++ ++ first = &c->slice[0]; ++ if ((first->c3 & A9_SPI0_STABLE_CTL_MASK) != A9_SPI0_STABLE_CTL_STOPPED || ++ (first->c3 & (A9_SPI0_CH3_RUN | A9_SPI0_CH3_IDLE | A9_SPI0_CH3_IRQ1))) ++ return "stopped-control"; ++ if ((first->scm_lo != 0 && first->scm_lo != 3) || first->scm_hi) ++ return "scm"; ++ if (first->fault || diag[0].fault_8d4) ++ return "sample-fault"; ++ if (diag[0].cfg != A9_SPI0_PHOSPHORUS_CFG || diag[0].cursor || !diag[0].interval) ++ return "cfg-cursor-interval"; ++ ++ for (i = 1; i < A9_SPI0_CH3_SLICES; i++) { ++ const struct a9_aop_ch3_slice *p = &c->slice[i]; ++ ++ if (p->c3 != first->c3 || p->scm_lo != first->scm_lo || ++ p->scm_hi != first->scm_hi || p->fault != first->fault || ++ diag[i].cfg != diag[0].cfg || ++ diag[i].interval != diag[0].interval || ++ diag[i].cursor != diag[0].cursor || ++ diag[i].fault_8d4 != diag[0].fault_8d4) ++ return "progress"; ++ } ++ if (c->slice[A9_SPI0_CH3_SLICES - 1].ns < ++ (u64)A9_SPI0_CH3_SLICES * A9_SPI0_CH3_SLICE_US * NSEC_PER_USEC) ++ return "short"; ++ return NULL; ++} ++ ++/* ++ * AOPSPIMaster_teardownChannel (0102e4f8) for channel 3, then the fault ++ * handler's acknowledge (0102f20a), under the same guard as the reset with ++ * the CPU halted. Every store is a read-modify-write except the descriptor, ++ * which the firmware zeroes. The idle wait runs in 1 ms slices, up to twice ++ * the firmware's own 10 ms SCMto bound; each slice ends with read-only ++ * samples of C3, the SCM state and +0x8d0 (wake15: and config, interval, ++ * cursor and +0x8d4). An idle timeout stops before the masks unless the ++ * stable-stop predicate holds. The acknowledge bit self-clears; +0x8d0 bit 6 ++ * shows whether it took effect. ++ */ ++static void a9_aop_spi0_ch3_locked(void *data) ++{ ++ struct a9_aop_spi0 *s = data; ++ struct a9_aop_spi0_ch3 *c = &s->ch3; ++ void __iomem *c3 = s->base + A9_SPI0_CH3; ++ void __iomem *c3c = s->base + A9_SPI0_CH3_0C; ++ void __iomem *base = a9_aop_spi0_channel_base(s, 3); ++ struct a9_aop_ch3_slice *p; ++ struct a9_aop_channel_diag_slice *d; ++ u32 interval[A9_SPI0_CHANNELS]; ++ u32 value = 0; ++ u64 start; ++ ++ a9_aop_spi0_ch3_snapshot(s->base, c->before); ++ c->run_before = c->before[A9_CH3_I_C3] & A9_SPI0_CH3_RUN; ++ c->stop = readl(c3) & ~A9_SPI0_CH3_STOP; ++ c->stop_ns = ktime_get_ns(); ++ writel(c->stop, c3); ++ start = ktime_get_ns(); ++ a9_aop_spi0_peers_begin(s, 3, interval, &c->peers_run); ++ c->poll = -ETIMEDOUT; ++ for (c->slices = 0; c->poll && c->slices < A9_SPI0_CH3_SLICES; c->slices++) { ++ p = &c->slice[c->slices]; ++ c->poll = readl_poll_timeout_atomic(c3, value, value & A9_SPI0_CH3_IDLE, ++ 1, A9_SPI0_CH3_SLICE_US); ++ p->ns = ktime_get_ns() - start; ++ p->c3 = value; ++ p->scm_lo = readl(s->base + A9_SPI0_SCM_LO); ++ p->scm_hi = readl(s->base + A9_SPI0_SCM_HI); ++ p->fault = readl(s->base + A9_SPI0_FAULT); ++ d = &s->ch3_diag[c->slices]; ++ d->cfg = readl(c3c); ++ d->interval = readl(base + a9_spi0_channel_offsets[A9_CHANNEL_I_INTERVAL]); ++ d->cursor = readl(base + a9_spi0_channel_offsets[A9_CHANNEL_I_CURSOR]); ++ d->fault_8d4 = readl(s->base + A9_SPI0_FAULT_8D4); ++ } ++ c->idle = value; ++ a9_aop_spi0_stop_timing(s, c, interval); ++ c->stable_why = a9_aop_spi0_stable_why(c, s->ch3_diag, 3); ++ c->stable = !c->stable_why; ++ if (c->stable) ++ s->teardown_stable |= BIT(3); ++ if (!c->poll || c->stable) { ++ /* A normal idle or the captured stopped state: mask callbacks. */ ++ c->idle_us = div_u64(c->slice[c->slices - 1].ns, NSEC_PER_USEC); ++ c->class0 = readl(c3) & ~A9_SPI0_CH3_CLASS0; ++ writel(c->class0, c3); ++ c->class1 = readl(c3) & ~A9_SPI0_CH3_CLASS1; ++ writel(c->class1, c3); ++ writel(0, s->base + A9_SPI0_CH3_DESC); ++ c->ack = readl(c3c) | A9_SPI0_CH3_ACK; ++ writel(c->ack, c3c); ++ } ++ a9_aop_spi0_ch3_snapshot(s->base, c->after); ++ c->failed = c->poll && !c->stable ? "failed-ch3-teardown-idle" : ++ a9_spi0_ch3_verify(c->after, c->stable); ++ if (!c->failed) ++ c->error = 0; ++ else if (c->stable) ++ c->error = -EIO; ++ else ++ c->error = c->poll ?: -EIO; ++} ++ ++ ++static void a9_aop_spi0_channel_snapshot(struct a9_aop_spi0 *s, ++ unsigned int channel, u32 *values) ++{ ++ void __iomem *base = a9_aop_spi0_channel_base(s, channel); ++ ++ values[A9_CH3_I_C3] = readl(base + a9_spi0_channel_offsets[A9_CHANNEL_I_CTL]); ++ values[A9_CH3_I_ROUTE] = readl(base + a9_spi0_channel_offsets[A9_CHANNEL_I_ROUTE]); ++ values[A9_CH3_I_RATE] = readl(base + a9_spi0_channel_offsets[A9_CHANNEL_I_RATE]); ++ values[A9_CH3_I_C3C] = readl(base + a9_spi0_channel_offsets[A9_CHANNEL_I_CFG]); ++ values[A9_CH3_I_DESC] = readl(base + A9_SPI0_CHANNEL_DESC); ++ values[A9_CH3_I_8D0] = readl(s->base + A9_SPI0_FAULT); ++ values[A9_CH3_I_8D4] = readl(s->base + A9_SPI0_FAULT_8D4); ++} ++ ++static const char *a9_spi0_channel_state_verify(u32 control, u32 desc, ++ u32 fault, u32 fault_8d4, ++ unsigned int channel, ++ bool stable_stop) ++{ ++ u32 pending = BIT(2 * channel); ++ ++ if (control & A9_SPI0_CH3_RUN) ++ return "failed-channel-teardown-run"; ++ if (control & A9_SPI0_CH3_IRQ0) ++ return "failed-channel-teardown-class0"; ++ if (control & A9_SPI0_CH3_IRQ1) ++ return "failed-channel-teardown-class1"; ++ if (!(control & A9_SPI0_CH3_IDLE) && !stable_stop) ++ return "failed-channel-teardown-idle"; ++ if (desc) ++ return "failed-channel-teardown-desc"; ++ if ((fault | fault_8d4) & pending) ++ return "failed-channel-teardown-pending"; ++ return NULL; ++} ++ ++static const char *a9_spi0_channel_verify(const u32 *after, ++ unsigned int channel, ++ bool stable_stop) ++{ ++ return a9_spi0_channel_state_verify(after[A9_CH3_I_C3], ++ after[A9_CH3_I_DESC], ++ after[A9_CH3_I_8D0], ++ after[A9_CH3_I_8D4], channel, ++ stable_stop); ++} ++ ++/* ++ * Wake11 applies the channel-3 firmware sequence unchanged to another slot. ++ * Wake12 only appends read-only scheduler samples to that established poll. ++ */ ++static void a9_aop_spi0_channel_locked(void *data) ++{ ++ struct a9_aop_spi0_channel_op *op = data; ++ struct a9_aop_spi0 *s = op->spi0; ++ struct a9_aop_spi0_ch3 *c = op->state; ++ void __iomem *base = a9_aop_spi0_channel_base(s, op->channel); ++ void __iomem *control = base + a9_spi0_channel_offsets[A9_CHANNEL_I_CTL]; ++ void __iomem *config = base + a9_spi0_channel_offsets[A9_CHANNEL_I_CFG]; ++ struct a9_aop_ch3_slice *p; ++ struct a9_aop_channel_diag_slice *d; ++ u32 interval[A9_SPI0_CHANNELS]; ++ u32 value = 0; ++ u64 start; ++ ++ a9_aop_spi0_channel_snapshot(s, op->channel, c->before); ++ c->run_before = c->before[A9_CH3_I_C3] & A9_SPI0_CH3_RUN; ++ c->stop = readl(control) & ~A9_SPI0_CH3_STOP; ++ c->stop_ns = ktime_get_ns(); ++ writel(c->stop, control); ++ start = ktime_get_ns(); ++ a9_aop_spi0_peers_begin(s, op->channel, interval, &c->peers_run); ++ c->poll = -ETIMEDOUT; ++ for (c->slices = 0; c->poll && c->slices < A9_SPI0_CH3_SLICES; c->slices++) { ++ p = &c->slice[c->slices]; ++ c->poll = readl_poll_timeout_atomic(control, value, ++ value & A9_SPI0_CH3_IDLE, 1, ++ A9_SPI0_CH3_SLICE_US); ++ p->ns = ktime_get_ns() - start; ++ p->c3 = value; ++ p->scm_lo = readl(s->base + A9_SPI0_SCM_LO); ++ p->scm_hi = readl(s->base + A9_SPI0_SCM_HI); ++ p->fault = readl(s->base + A9_SPI0_FAULT); ++ d = &s->channel_diag[c->slices]; ++ d->cfg = readl(config); ++ d->interval = readl(base + a9_spi0_channel_offsets[A9_CHANNEL_I_INTERVAL]); ++ d->cursor = readl(base + a9_spi0_channel_offsets[A9_CHANNEL_I_CURSOR]); ++ d->fault_8d4 = readl(s->base + A9_SPI0_FAULT_8D4); ++ } ++ c->idle = value; ++ a9_aop_spi0_stop_timing(s, c, interval); ++ c->stable_why = a9_aop_spi0_stable_why(c, s->channel_diag, op->channel); ++ c->stable = !c->stable_why; ++ if (c->stable) ++ s->teardown_stable |= BIT(op->channel); ++ if (!c->poll || c->stable) { ++ /* A normal idle or the captured stopped state: mask callbacks. */ ++ c->idle_us = div_u64(c->slice[c->slices - 1].ns, NSEC_PER_USEC); ++ c->class0 = readl(control) & ~A9_SPI0_CH3_CLASS0; ++ writel(c->class0, control); ++ c->class1 = readl(control) & ~A9_SPI0_CH3_CLASS1; ++ writel(c->class1, control); ++ writel(0, base + A9_SPI0_CHANNEL_DESC); ++ c->ack = readl(config) | A9_SPI0_CH3_ACK; ++ writel(c->ack, config); ++ } ++ a9_aop_spi0_channel_snapshot(s, op->channel, c->after); ++ c->failed = c->poll && !c->stable ? "failed-channel-teardown-idle" : ++ a9_spi0_channel_verify(c->after, op->channel, ++ c->stable); ++ if (!c->failed) ++ c->error = 0; ++ else if (c->stable) ++ c->error = -EIO; ++ else ++ c->error = c->poll ?: -EIO; ++} ++ ++/* Called only if ps_spi0 is fully on; reads nothing. */ ++static void a9_aop_spi0_pmgr_on(void *data) ++{ ++ *(bool *)data = true; ++} ++ ++/* AP PMGR ps_spi0, read-only: never requested, resumed or written. ++ * 0 = on, -EHOSTDOWN = off (raw still filled), -ENODEV = no node. ++ */ ++static int a9_aop_spi0_pmgr(struct apple_aop *aop, u32 *raw) ++{ ++ struct apple_pmgr_observation state = {}; ++ bool on = false; ++ int ret; ++ ++ if (!aop->spi0_pmgr) ++ return -ENODEV; ++ ret = apple_pmgr_observe(&aop->spi0_pmgr, 1, &state, ++ a9_aop_spi0_pmgr_on, &on); ++ *raw = state.raw; ++ return ret ?: on ? 0 : -EIO; ++} ++ ++/* ++ * After a reset that cleared +0x8d0 bit 6, before the image copy: CPU halted, ++ * AP notifications masked. A refusal sets ch3.failed and nothing is loaded. ++ */ ++static int a9_aop_spi0_ch3_teardown(struct apple_aop *aop) ++{ ++ struct apple_pmgr_observation states[AOP_DOMAINS] = {}; ++ struct a9_aop_spi0 *s = &aop->spi0; ++ struct a9_aop_spi0_ch3 *c = &s->ch3; ++ const u32 *b = c->before, *a = c->after; ++ const struct a9_aop_ch3_slice *p; ++ const struct a9_aop_channel_diag_slice *d; ++ unsigned int i; ++ bool source_progress = false; ++ int ret; ++ ++ aop->experiment_step = "spi0-ch3-teardown"; ++ dev_info(aop->dev, "SPI0 CH3TD begin pa=%llx cpu=halted: first channel-3 store follows\n", ++ A9_SPI0_PA + A9_SPI0_CH3); ++ if (cold_defer) { ++ ret = a9_aop_cold_checkpoint_noflush(aop, "spi0-ch3-teardown"); ++ if (ret) { ++ c->failed = "failed-ch3-teardown-checkpoint"; ++ return ret; ++ } ++ } ++ ret = apple_pmgr_observe(aop->domains, AOP_DOMAINS, states, ++ a9_aop_spi0_ch3_locked, s); ++ if (ret) { ++ c->failed = "failed-ch3-teardown-guard"; ++ dev_info(aop->dev, "SPI0 CH3TD errno=%d ran=0: guard refused, channel 3 not touched\n", ++ ret); ++ return ret; ++ } ++ c->ran = true; ++ dev_info(aop->dev, "SPI0 CH3TD before c3=%08x route=%08x rate=%08x c3c=%08x desc=%08x 8d0=%08x 8d4=%08x\n", ++ b[0], b[1], b[2], b[3], b[4], b[5], b[6]); ++ for (i = 0; i < c->slices; i++) { ++ p = &c->slice[i]; ++ dev_info(aop->dev, "SPI0 CH3TD idle-sample slice=%u at_us=%llu c3=%08x scm_lo=%08x scm_hi=%08x 8d0=%08x idle=%u\n", ++ i, div_u64(p->ns, NSEC_PER_USEC), p->c3, p->scm_lo, ++ p->scm_hi, p->fault, !!(p->c3 & A9_SPI0_CH3_IDLE)); ++ if (c->poll) { ++ d = &s->ch3_diag[i]; ++ dev_info(aop->dev, "SPI0 CH3TD diagnostic slice=%u ctl=%08x cfg=%08x interval=%08x cursor=%08x scm_lo=%08x scm_hi=%08x 8d0=%08x 8d4=%08x\n", ++ i, p->c3, d->cfg, d->interval, d->cursor, p->scm_lo, ++ p->scm_hi, p->fault, d->fault_8d4); ++ if (i && (p->c3 != c->slice[0].c3 || ++ d->interval != s->ch3_diag[0].interval || ++ d->cursor != s->ch3_diag[0].cursor)) ++ source_progress = true; ++ } ++ } ++ if (c->poll && c->slices) ++ dev_info(aop->dev, "SPI0 CH3TD diagnostic mode=scheduler-read-only samples=%u ctl_hi=%02x->%02x interval=%08x->%08x cursor=%08x->%08x source_progress=%u result=%s\n", ++ c->slices, ++ c->slice[0].c3 >> A9_SPI0_CHANNEL_PROGRESS_SHIFT, ++ c->slice[c->slices - 1].c3 >> A9_SPI0_CHANNEL_PROGRESS_SHIFT, ++ s->ch3_diag[0].interval, ++ s->ch3_diag[c->slices - 1].interval, ++ s->ch3_diag[0].cursor, ++ s->ch3_diag[c->slices - 1].cursor, source_progress, ++ c->stable ? "stable-stop-reset-only" : "refused"); ++ /* beyond_fw_bound=1: idle, but later than the firmware would wait. */ ++ dev_info(aop->dev, "SPI0 CH3TD idle-poll run_before=%u slices=%u/%u idle_us=%llu bound_us=%u fw_bound_us=%u beyond_fw_bound=%u result=%s\n", ++ c->run_before, c->slices, A9_SPI0_CH3_SLICES, c->idle_us, ++ A9_SPI0_CH3_SLICES * A9_SPI0_CH3_SLICE_US, A9_SPI0_CH3_IDLE_FW_US, ++ !c->poll && c->idle_us > A9_SPI0_CH3_IDLE_FW_US, ++ c->poll ? "timeout" : "idle"); ++ if (c->poll && !c->stable) ++ dev_info(aop->dev, "SPI0 CH3TD writes stop=%08x idle_poll=%08x poll_errno=%d masks=skipped: idle bit 1 never set, no further store\n", ++ c->stop, c->idle, c->poll); ++ else if (c->stable) ++ dev_info(aop->dev, "SPI0 CH3TD writes stop=%08x idle_poll=%08x poll_errno=%d stable_stop=1 class0=%08x class1=%08x desc=00000000 c3c=%08x\n", ++ c->stop, c->idle, c->poll, c->class0, c->class1, c->ack); ++ else ++ dev_info(aop->dev, "SPI0 CH3TD writes stop=%08x idle_poll=%08x poll_errno=0 class0=%08x class1=%08x desc=00000000 c3c=%08x\n", ++ c->stop, c->idle, c->class0, c->class1, c->ack); ++ dev_info(aop->dev, "SPI0 CH3TD after c3=%08x route=%08x rate=%08x c3c=%08x desc=%08x 8d0=%08x 8d4=%08x\n", ++ a[0], a[1], a[2], a[3], a[4], a[5], a[6]); ++ dev_info(aop->dev, "SPI0 CH3TD timing reset_to_stop_us=%llu late=%u peers_run=%04x peers_ticking=%04x stable=%u checkpoint=noflush stable_why=%s\n", ++ c->reset_to_stop_us, c->reset_to_stop_us > A9_SPI0_LATE_STOP_US, ++ c->peers_run, c->peers_ticking, c->stable, ++ c->stable ? "stable" : c->poll ? c->stable_why : "idle"); ++ /* c3c_bit5 is the pulse's readback, logged only (0 at 19:50). */ ++ dev_info(aop->dev, "SPI0 CH3TD check errno=%d run=%u class0=%u class1=%u idle=%u desc=%08x c3c_bit5=%u ch3_pending=%u result=%s\n", ++ c->error, !!(a[A9_CH3_I_C3] & A9_SPI0_CH3_RUN), ++ !!(a[A9_CH3_I_C3] & A9_SPI0_CH3_IRQ0), ++ !!(a[A9_CH3_I_C3] & A9_SPI0_CH3_IRQ1), !!(a[A9_CH3_I_C3] & A9_SPI0_CH3_IDLE), ++ a[A9_CH3_I_DESC], !!(a[A9_CH3_I_C3C] & A9_SPI0_CH3_ACK), ++ !!(a[A9_CH3_I_8D0] & A9_SPI0_CH3_PENDING), c->failed ?: ++ c->stable ? "pass-stable-reset-only" : "pass"); ++ return c->error; ++} ++ ++static int a9_aop_spi0_channel_teardown(struct apple_aop *aop, ++ unsigned int channel) ++{ ++ struct apple_pmgr_observation states[AOP_DOMAINS] = {}; ++ struct a9_aop_spi0 *s = &aop->spi0; ++ struct a9_aop_spi0_ch3 *c = &s->channel; ++ struct a9_aop_spi0_channel_op op = { ++ .spi0 = s, ++ .state = c, ++ .channel = channel, ++ }; ++ const u32 *b, *a; ++ const struct a9_aop_ch3_slice *p; ++ const struct a9_aop_channel_diag_slice *d; ++ char checkpoint[32]; ++ unsigned int i; ++ bool source_progress = false; ++ bool stable_stop; ++ int ret; ++ ++ memset(c, 0, sizeof(*c)); ++ memset(s->channel_diag, 0, sizeof(s->channel_diag)); ++ s->teardown_failed_channel = channel; ++ aop->experiment_step = "spi0-channel-teardown"; ++ snprintf(checkpoint, sizeof(checkpoint), "spi0-ch%u-teardown", channel); ++ dev_info(aop->dev, "SPI0 CH%uTD begin pa=%llx cpu=halted: first channel-%u store follows\n", ++ channel, A9_SPI0_PA + A9_SPI0_CHANNEL_BASE + ++ A9_SPI0_CHANNEL_STRIDE * channel, channel); ++ if (cold_defer) { ++ ret = a9_aop_cold_checkpoint_noflush(aop, checkpoint); ++ if (ret) { ++ c->failed = "failed-channel-teardown-checkpoint"; ++ return ret; ++ } ++ } ++ ret = apple_pmgr_observe(aop->domains, AOP_DOMAINS, states, ++ a9_aop_spi0_channel_locked, &op); ++ if (ret) { ++ c->failed = "failed-channel-teardown-guard"; ++ dev_info(aop->dev, "SPI0 CH%uTD errno=%d ran=0: guard refused, channel %u not touched\n", ++ channel, ret, channel); ++ return ret; ++ } ++ c->ran = true; ++ stable_stop = s->teardown_stable & BIT(channel); ++ b = c->before; ++ a = c->after; ++ dev_info(aop->dev, "SPI0 CH%uTD before ctl=%08x route=%08x rate=%08x cfg=%08x desc=%08x 8d0=%08x 8d4=%08x\n", ++ channel, b[0], b[1], b[2], b[3], b[4], b[5], b[6]); ++ for (i = 0; i < c->slices; i++) { ++ p = &c->slice[i]; ++ dev_info(aop->dev, "SPI0 CH%uTD idle-sample slice=%u at_us=%llu ctl=%08x scm_lo=%08x scm_hi=%08x 8d0=%08x idle=%u\n", ++ channel, i, div_u64(p->ns, NSEC_PER_USEC), p->c3, ++ p->scm_lo, p->scm_hi, p->fault, ++ !!(p->c3 & A9_SPI0_CH3_IDLE)); ++ if (c->poll) { ++ d = &s->channel_diag[i]; ++ dev_info(aop->dev, "SPI0 CH%uTD diagnostic slice=%u ctl=%08x cfg=%08x interval=%08x cursor=%08x scm_lo=%08x scm_hi=%08x 8d0=%08x 8d4=%08x\n", ++ channel, i, p->c3, d->cfg, d->interval, d->cursor, ++ p->scm_lo, p->scm_hi, p->fault, d->fault_8d4); ++ if (i && (p->c3 != c->slice[0].c3 || ++ d->interval != s->channel_diag[0].interval || ++ d->cursor != s->channel_diag[0].cursor)) ++ source_progress = true; ++ } ++ } ++ if (c->poll && c->slices) ++ dev_info(aop->dev, "SPI0 CH%uTD diagnostic mode=scheduler-read-only samples=%u ctl_hi=%02x->%02x interval=%08x->%08x cursor=%08x->%08x source_progress=%u result=%s\n", ++ channel, c->slices, ++ c->slice[0].c3 >> A9_SPI0_CHANNEL_PROGRESS_SHIFT, ++ c->slice[c->slices - 1].c3 >> A9_SPI0_CHANNEL_PROGRESS_SHIFT, ++ s->channel_diag[0].interval, ++ s->channel_diag[c->slices - 1].interval, ++ s->channel_diag[0].cursor, ++ s->channel_diag[c->slices - 1].cursor, ++ source_progress, ++ stable_stop ? "stable-stop-reset-only" : "refused"); ++ dev_info(aop->dev, "SPI0 CH%uTD idle-poll run_before=%u slices=%u/%u idle_us=%llu bound_us=%u fw_bound_us=%u beyond_fw_bound=%u result=%s\n", ++ channel, c->run_before, c->slices, A9_SPI0_CH3_SLICES, c->idle_us, ++ A9_SPI0_CH3_SLICES * A9_SPI0_CH3_SLICE_US, A9_SPI0_CH3_IDLE_FW_US, ++ !c->poll && c->idle_us > A9_SPI0_CH3_IDLE_FW_US, ++ c->poll ? "timeout" : "idle"); ++ if (c->poll && !stable_stop) ++ dev_info(aop->dev, "SPI0 CH%uTD writes stop=%08x idle_poll=%08x poll_errno=%d masks=skipped: idle bit 1 never set, no further store\n", ++ channel, c->stop, c->idle, c->poll); ++ else if (stable_stop) ++ dev_info(aop->dev, "SPI0 CH%uTD writes stop=%08x idle_poll=%08x poll_errno=%d stable_stop=1 class0=%08x class1=%08x desc=00000000 cfg=%08x\n", ++ channel, c->stop, c->idle, c->poll, c->class0, c->class1, ++ c->ack); ++ else ++ dev_info(aop->dev, "SPI0 CH%uTD writes stop=%08x idle_poll=%08x poll_errno=0 class0=%08x class1=%08x desc=00000000 cfg=%08x\n", ++ channel, c->stop, c->idle, c->class0, c->class1, c->ack); ++ dev_info(aop->dev, "SPI0 CH%uTD after ctl=%08x route=%08x rate=%08x cfg=%08x desc=%08x 8d0=%08x 8d4=%08x\n", ++ channel, a[0], a[1], a[2], a[3], a[4], a[5], a[6]); ++ dev_info(aop->dev, "SPI0 CH%uTD timing reset_to_stop_us=%llu late=%u peers_run=%04x peers_ticking=%04x stable=%u checkpoint=noflush stable_why=%s\n", ++ channel, c->reset_to_stop_us, c->reset_to_stop_us > A9_SPI0_LATE_STOP_US, ++ c->peers_run, c->peers_ticking, c->stable, ++ c->stable ? "stable" : c->poll ? c->stable_why : "idle"); ++ dev_info(aop->dev, "SPI0 CH%uTD check errno=%d run=%u class0=%u class1=%u idle=%u desc=%08x cfg_bit5=%u pending0=%u pending1=%u result=%s\n", ++ channel, c->error, !!(a[A9_CH3_I_C3] & A9_SPI0_CH3_RUN), ++ !!(a[A9_CH3_I_C3] & A9_SPI0_CH3_IRQ0), ++ !!(a[A9_CH3_I_C3] & A9_SPI0_CH3_IRQ1), ++ !!(a[A9_CH3_I_C3] & A9_SPI0_CH3_IDLE), a[A9_CH3_I_DESC], ++ !!(a[A9_CH3_I_C3C] & A9_SPI0_CH3_ACK), ++ !!(a[A9_CH3_I_8D0] & BIT(2 * channel)), ++ !!(a[A9_CH3_I_8D4] & BIT(2 * channel)), c->failed ?: ++ stable_stop ? "pass-stable-reset-only" : "pass"); ++ return c->error; ++} ++ ++static const char *a9_aop_spi0_selected_verify(struct a9_aop_spi0 *s) ++{ ++ const u32 *v; ++ const char *failed = NULL; ++ unsigned int channel; ++ ++ if (s->teardown_selected != s->teardown_done) ++ return "failed-channel-teardown-incomplete"; ++ for (channel = 0; channel < A9_SPI0_CHANNELS; channel++) { ++ if (channel == 3 || !(s->teardown_selected & BIT(channel))) ++ continue; ++ v = s->trace.channel[channel]; ++ failed = a9_spi0_channel_state_verify(v[A9_CHANNEL_I_CTL], ++ v[A9_CHANNEL_I_DESC], ++ s->trace.ch3[A9_CH3_I_8D0], ++ s->trace.ch3[A9_CH3_I_8D4], channel, ++ s->teardown_stable & BIT(channel)); ++ if (failed) { ++ s->teardown_failed_channel = channel; ++ return failed; ++ } ++ } ++ return NULL; ++} ++ ++/* ++ * Channel 3 remains mandatory and executes first. Wake11 additionally selects ++ * every snapshot slot with run bit 0 (RUN) set and a nonzero interval, which ++ * excludes configured-but-idle slots while selecting any periodic client still ++ * owned by the old firmware. Wake16 returns to wake11's order after wake15's ++ * channel-7-first stop failed: all ten device stops fit "channel 3 idles when ++ * stopped within ~2 ms of the reset (5/5; 0/2 at 11 and 33 ms) and channel 7 ++ * never idles (0/3, at 0.3 ms, 1.9 ms and 15 ms)". Channel 3 goes first and ++ * early; channel 7 is then the stopped-without-idle channel the stable-stop ++ * fingerprint was captured from (wake11/12). ++ */ ++static int a9_aop_spi0_teardown_running(struct apple_aop *aop) ++{ ++ struct a9_aop_spi0 *s = &aop->spi0; ++ const u32 *v; ++ unsigned int channel; ++ int ret; ++ ++ s->teardown_selected = BIT(3); ++ s->teardown_done = 0; ++ s->teardown_stable = 0; ++ s->teardown_failed_channel = A9_SPI0_CHANNELS; ++ for (channel = 0; channel < A9_SPI0_CHANNELS; channel++) { ++ if (channel == 3) ++ continue; ++ v = s->trace.channel[channel]; ++ if ((v[A9_CHANNEL_I_CTL] & A9_SPI0_CH3_RUN) && ++ v[A9_CHANNEL_I_INTERVAL]) ++ s->teardown_selected |= BIT(channel); ++ } ++ dev_info(aop->dev, "SPI0 teardown select stage=after-controller-reset selected=%04x mandatory=0008 rule=ch3-or-run-and-interval order=ch3-then-others\n", ++ s->teardown_selected); ++ ret = a9_aop_spi0_ch3_teardown(aop); ++ if (ret) { ++ s->teardown_failed_channel = 3; ++ dev_info(aop->dev, "SPI0 teardown refused selected=%04x done=%04x failed_channel=3 errno=%d result=%s\n", ++ s->teardown_selected, s->teardown_done, ret, ++ s->ch3.failed ?: "failed-ch3-teardown"); ++ return ret; ++ } ++ s->teardown_done |= BIT(3); ++ for (channel = 0; channel < A9_SPI0_CHANNELS; channel++) { ++ if (channel == 3 || !(s->teardown_selected & BIT(channel))) ++ continue; ++ ret = a9_aop_spi0_channel_teardown(aop, channel); ++ if (ret) { ++ dev_info(aop->dev, "SPI0 teardown refused selected=%04x done=%04x failed_channel=%u errno=%d result=%s\n", ++ s->teardown_selected, s->teardown_done, channel, ret, ++ s->channel.failed ?: "failed-channel-teardown"); ++ return ret; ++ } ++ s->teardown_done |= BIT(channel); ++ } ++ s->teardown_failed_channel = A9_SPI0_CHANNELS; ++ dev_info(aop->dev, "SPI0 teardown complete selected=%04x done=%04x stable=%04x result=pass\n", ++ s->teardown_selected, s->teardown_done, s->teardown_stable); ++ return 0; ++} ++ ++/* ++ * Second copy of the firmware controller reset, after the stop has completed. ++ * The preceding after-ch3-teardown trace is the read-only before snapshot; ++ * after-post-teardown-reset is the matching read-only after snapshot. Refuse ++ * the image copy unless SCM is zero and every channel quiet-state gate holds. ++ */ ++static int a9_aop_spi0_post_reset(struct apple_aop *aop) ++{ ++ struct apple_pmgr_observation states[AOP_DOMAINS] = {}; ++ struct a9_aop_spi0 *s = &aop->spi0; ++ struct a9_aop_spi0_post_reset *r = &s->post_reset; ++ const u32 *b = r->before, *a = r->after; ++ int ret; ++ ++ if (!aop->force_halted || !aop->force_drained || !s->ch3.ran || ++ s->ch3.error || s->teardown_selected != s->teardown_done || r->ran) { ++ r->failed = "failed-post-reset-state"; ++ return -EPERM; ++ } ++ aop->experiment_step = "spi0-post-teardown-reset"; ++ dev_info(aop->dev, "SPI0 POSTRST begin pa=%llx reason=ch3-stop-latched-scm cpu=halted\n", ++ A9_SPI0_PA); ++ if (cold_defer) { ++ ret = a9_aop_cold_checkpoint(aop, "spi0-post-teardown-reset"); ++ if (ret) { ++ r->failed = "failed-post-reset-checkpoint"; ++ return ret; ++ } ++ } ++ ret = apple_pmgr_observe(aop->domains, AOP_DOMAINS, states, ++ a9_aop_spi0_post_reset_locked, s); ++ r->ran = !ret; ++ r->error = ret ?: r->error; ++ dev_info(aop->dev, "SPI0 POSTRST errno=%d ran=%d before ctl=%08x clk=%08x 8d0=%08x 8d4=%08x ch3=%08x ch3c=%08x\n", ++ r->error, r->ran, b[0], b[1], b[2], b[3], b[4], b[5]); ++ dev_info(aop->dev, "SPI0 POSTRST after ctl=%08x clk=%08x 8d0=%08x 8d4=%08x ch3=%08x ch3c=%08x saved=%08x asserted=%08x polled=%08x ch3_pending=%u->%u\n", ++ a[0], a[1], a[2], a[3], a[4], a[5], r->saved, r->asserted, ++ r->polled, !!(b[A9_SPI0_I_FAULT] & A9_SPI0_CH3_PENDING), ++ !!(a[A9_SPI0_I_FAULT] & A9_SPI0_CH3_PENDING)); ++ if (r->error) { ++ r->failed = "failed-post-reset-controller"; ++ return r->error; ++ } ++ ret = a9_aop_spi0_trace(aop, "after-post-teardown-reset"); ++ if (ret) ++ return ret; ++ if (s->trace.scm[0] || s->trace.scm[1]) ++ r->failed = "failed-post-reset-scm"; ++ else { ++ r->failed = a9_spi0_ch3_verify(s->trace.ch3, ++ s->teardown_stable & BIT(3)); ++ if (!r->failed) ++ r->failed = a9_aop_spi0_selected_verify(s); ++ } ++ ret = r->failed ? -EIO : 0; ++ dev_info(aop->dev, "SPI0 POSTRST check errno=%d scm_lo=%08x scm_hi=%08x ch3_pending=%u result=%s\n", ++ ret, s->trace.scm[0], s->trace.scm[1], ++ !!(s->trace.ch3[A9_CH3_I_8D0] & A9_SPI0_CH3_PENDING), ++ r->failed ?: "pass"); ++ dev_info(aop->dev, "SPI0 POSTRST channels selected=%04x done=%04x stable=%04x result=%s\n", ++ s->teardown_selected, s->teardown_done, s->teardown_stable, ++ r->failed ?: "pass"); ++ return ret; ++} ++ ++/* ++ * The ten INTC words of a9_intc_offsets into state[@at], read only. The ++ * INTC is the AOP wrapper block (0xc1800000 in the firmware, space 2 as ++ * SPI0's verified 0xc1100000 = 0x210100000), which this driver reads ++ * unguarded for the mailbox and CPU control; no separate PMGR domain. ++ */ ++static void a9_aop_intc_snapshot(struct apple_aop *aop, unsigned int at) ++{ ++ struct a9_aop_intc *n = &aop->spi0.intc; ++ u32 *v = n->state[at]; ++ unsigned int i; ++ ++ for (i = 0; i < A9_INTC_REGS; i++) ++ v[i] = aop_rd(aop, a9_intc_offsets[i]); ++ n->states |= BIT(at); ++ dev_info(aop->dev, "AOP INTC state at=%s reset808=%08x current81c=%08x raise900=%08x ack980=%08x mask_a00=%08x mask_a04=%08x mask_a08=%08x mask_a80=%08x g7_c020=%08x g7_c024=%08x src_masked=%u view=%s\n", ++ a9_intc_at_name(at), v[0], v[1], v[2], v[3], v[4], v[5], v[6], ++ v[7], v[8], v[9], !!(v[A9_INTC_I_MASK0] & A9_INTC_SPI0_SCM), ++ a9_intc_view(v)); ++} ++ ++/* ++ * Round 4 (ds-aop-spi0-ch3-round4.md): the old session's SPI0 global IRQ ++ * 0x10014 stays latched in the AOP interrupt controller, and fresh firmware ++ * takes it as soon as SPI lifecycle event 1 enables it, before its own SPI0 ++ * reset (SCMErr:0x3,3). With the CPU halted and before the first SPI0 ++ * access, disable that source (group 1 disable bank, 0102c9ce). The bank ++ * reads back the mask, as the firmware's own reads of it show: bit 20 must ++ * read 1 after the store. ++ */ ++static int a9_aop_intc_disable(struct apple_aop *aop) ++{ ++ struct a9_aop_intc *n = &aop->spi0.intc; ++ const u32 *b = n->state[A9_INTC_AT_PRE_DISABLE]; ++ const u32 *a = n->state[A9_INTC_AT_POST_DISABLE]; ++ int ret; ++ ++ aop->experiment_step = "intc-disable"; ++ dev_info(aop->dev, "AOP INTC disable begin irq=0x10014 pa=%llx value=%08x cpu=halted: first INTC store follows\n", ++ A9_SPI0_WRAPPER_PA + A9_INTC_DISABLE, A9_INTC_SPI0_SCM); ++ if (cold_defer) { ++ ret = a9_aop_cold_checkpoint(aop, "intc-disable"); ++ if (ret) { ++ n->failed = "failed-intc-disable-checkpoint"; ++ return ret; ++ } ++ } ++ a9_aop_intc_snapshot(aop, A9_INTC_AT_PRE_DISABLE); ++ ret = aop_wr(aop, A9_INTC_DISABLE, A9_INTC_SPI0_SCM); ++ a9_aop_intc_snapshot(aop, A9_INTC_AT_POST_DISABLE); ++ if (ret) { ++ n->failed = "failed-intc-disable"; ++ } else if (!(a[A9_INTC_I_MASK0] & A9_INTC_SPI0_SCM)) { ++ n->failed = "failed-intc-disable-readback"; ++ ret = -EIO; ++ } ++ n->disabled = !ret; ++ dev_info(aop->dev, "AOP INTC disable errno=%d before=%08x value=%08x after=%08x mask_a80=%08x src_masked=%u view=%s result=%s\n", ++ ret, b[A9_INTC_I_MASK0], A9_INTC_SPI0_SCM, a[A9_INTC_I_MASK0], ++ a[A9_INTC_I_ENABLE0], !!(a[A9_INTC_I_MASK0] & A9_INTC_SPI0_SCM), ++ a9_intc_view(a), n->failed ?: "pass"); ++ return ret; ++} ++ ++/* ++ * Release policy for aop_spi0_reset=1. The reload is device-proven for exactly ++ * one retained state: the previous kernel quiesced and halted the firmware ++ * (wake16, 2026-10-29; decision prev-quiesce-halted). Auto-boot still set, a ++ * previous kernel that did not halt it, or an older kernel in the chain are ++ * different hardware states that no run has reloaded with the final teardown. ++ * Refuse them before the first halt or SPI0 access so the old firmware is ++ * left exactly as found; aop_spi0_untested=1 attempts them anyway. A skip ++ * (DFU cold, bad record, nothing ran) must also find the wrapper at its reset ++ * state: a record-less AOP that something already stopped is refused rather ++ * than cold-loaded. A stale record skips as before. ++ */ ++static int a9_aop_spi0_policy(struct apple_aop *aop, u32 auto_boot, u32 outbox) ++{ ++ enum a9_spi0_reason reason = a9_spi0_decide(auto_boot, outbox & AOP_OUTBOX_ENABLE, ++ &aop->spi0.prev); ++ ++ if (a9_spi0_policy_allows(reason, outbox & AOP_OUTBOX_ENABLE, spi0_untested)) ++ return 0; ++ dev_info(aop->dev, "SPI0 policy decision=refuse reason=%s auto_boot=%u out4020=%08x prev=%s prev_hello=%d prev_halted=%d: only prev-quiesce-halted reloads and a skip needs the wrapper at its reset state block not touched\n", ++ a9_spi0_reason_name(reason), auto_boot, outbox, ++ a9_spi0_prev_name(aop->spi0.prev.state), aop->spi0.prev.hello, ++ aop->spi0.prev.halted); ++ return -EPERM; ++} ++ ++/* ++ * After the proven halt/drain, before any SRAM write. Only firmware that ran ++ * since the last SoC reset leaves SPI0 state behind: auto-boot 1, or the ++ * previous kernel's transcript bound by +0x4020 (a9_spi0_decide). Without ++ * that evidence the block is never touched. @auto_boot and @outbox are the ++ * boot snapshot from before the stop. Nonzero means: do not load or ++ * release the AOP. ++ */ ++static int a9_aop_spi0_prereset(struct apple_aop *aop, u32 auto_boot, u32 outbox) ++{ ++ struct apple_pmgr_observation states[AOP_DOMAINS] = {}; ++ struct a9_aop_spi0 *s = &aop->spi0; ++ const u32 *b = s->before, *a = s->after; ++ const char *failed = NULL; ++ bool run; ++ int ret; ++ ++ s->auto_boot = auto_boot; ++ s->outbox = outbox; ++ s->reason = a9_spi0_decide(auto_boot, outbox & AOP_OUTBOX_ENABLE, &s->prev); ++ s->decided = true; ++ run = a9_spi0_reason_runs(s->reason); ++ dev_info(aop->dev, "SPI0 reset decision=%s reason=%s auto_boot=%u out4020=%08x prev=%s prev_hello=%d prev_halted=%d prev_steps=0x%x prev_carry=%d%s\n", ++ run ? "run" : "skip", a9_spi0_reason_name(s->reason), auto_boot, ++ outbox, a9_spi0_prev_name(s->prev.state), s->prev.hello, ++ s->prev.halted, s->prev.steps, s->prev.carried, ++ run ? "" : ": block not touched"); ++ if (!run) { ++ s->skipped = a9_spi0_reason_name(s->reason); ++ return 0; ++ } ++ if (!s->base || !aop->force_halted || !aop->force_drained || ++ !a9_aop_force_stopped(aop)) ++ return -EPERM; ++ ret = a9_aop_intc_disable(aop); ++ if (ret) ++ return ret; ++ aop->experiment_step = "spi0-reset"; ++ s->pmgr_before_errno = a9_aop_spi0_pmgr(aop, &s->pmgr_before); ++ /* Reaches ramoops before the first AP access to the block. */ ++ dev_info(aop->dev, "SPI0 reset begin pa=%llx cpu=halted ps_spi0=%08x errno=%d: first AP access follows\n", ++ A9_SPI0_PA, s->pmgr_before, s->pmgr_before_errno); ++ if (cold_defer) { ++ ret = a9_aop_cold_checkpoint(aop, "spi0-reset"); ++ if (ret) ++ return ret; ++ } ++ ret = a9_aop_spi0_trace(aop, "before-controller-reset"); ++ if (ret) ++ return ret; ++ /* ++ * Wake15: from the controller reset to the last channel stop nothing may ++ * drain a console in this task. Holding the console lock makes every ++ * dev_info() below only append to the log buffer; the flushed spi0-reset ++ * checkpoint above already put the begin line into ramoops, and the lines ++ * are emitted by console_unlock() once the stops are stored. ++ */ ++ console_lock(); ++ ret = apple_pmgr_observe(aop->domains, AOP_DOMAINS, states, ++ a9_aop_spi0_reset_locked, s); ++ s->ran = !ret; ++ s->error = ret ?: s->error; ++ s->pmgr_after_errno = a9_aop_spi0_pmgr(aop, &s->pmgr_after); ++ dev_info(aop->dev, "SPI0 reset errno=%d ran=%d pa=%llx before ctl=%08x clk=%08x 8d0=%08x 8d4=%08x ch3=%08x ch3c=%08x\n", ++ s->error, s->ran, A9_SPI0_PA, b[0], b[1], b[2], b[3], b[4], b[5]); ++ dev_info(aop->dev, "SPI0 reset after ctl=%08x clk=%08x 8d0=%08x 8d4=%08x ch3=%08x ch3c=%08x saved=%08x asserted=%08x polled=%08x ch3_pending=%u->%u provider3=not-replayed\n", ++ a[0], a[1], a[2], a[3], a[4], a[5], s->saved, s->asserted, ++ s->polled, !!(b[A9_SPI0_I_FAULT] & A9_SPI0_CH3_PENDING), ++ !!(a[A9_SPI0_I_FAULT] & A9_SPI0_CH3_PENDING)); ++ dev_info(aop->dev, "SPI0 pmgr ps_spi0 before=%08x errno=%d after=%08x errno=%d (read-only, never powered)\n", ++ s->pmgr_before, s->pmgr_before_errno, s->pmgr_after, ++ s->pmgr_after_errno); ++ if (s->error) { ++ console_unlock(); ++ return s->error; ++ } ++ ret = a9_aop_spi0_trace(aop, "after-controller-reset"); ++ if (!ret) ++ ret = a9_aop_spi0_teardown_running(aop); ++ console_unlock(); ++ if (!ret) ++ ret = a9_aop_spi0_trace(aop, "after-ch3-teardown"); ++ if (!ret) ++ failed = a9_aop_spi0_selected_verify(s); ++ if (failed) { ++ s->channel.failed = failed; ++ ret = -EIO; ++ } ++ if (!ret) ++ ret = a9_aop_spi0_post_reset(aop); ++ return ret; ++} ++ ++/* Wake10 samples both SCM words and both fault classes, not only channel 3. */ ++static void a9_aop_spi0_fault_read(void *data) ++{ ++ struct a9_aop_spi0 *s = data; ++ ++ s->prerun_scm_lo = readl(s->base + A9_SPI0_SCM_LO); ++ s->prerun_scm_hi = readl(s->base + A9_SPI0_SCM_HI); ++ s->prerun = readl(s->base + A9_SPI0_FAULT); ++ s->prerun_8d4 = readl(s->base + A9_SPI0_FAULT_8D4); ++ if (s->prerun & A9_SPI0_CH3_PENDING) ++ a9_aop_spi0_ch3_snapshot(s->base, s->ch3.reassert); ++} ++ ++/* ++ * Last AP look before RUN, CPU still halted, after the image copy: bit 6 ++ * must stay clear in every one of 31 guarded samples spaced >= 5 ms apart, ++ * spanning >= 150 ms. A reasserted channel-3 bit would crash the fresh ++ * image as before, so the first set sample refuses the release; nothing is ++ * retried. Any refusal sets ch3.failed. ++ */ ++static int a9_aop_spi0_recheck(struct apple_aop *aop) ++{ ++ struct apple_pmgr_observation states[AOP_DOMAINS] = {}; ++ struct a9_aop_spi0 *s = &aop->spi0; ++ struct a9_aop_spi0_ch3 *c = &s->ch3; ++ const u32 *r = c->reassert; ++ u64 start = ktime_get_boottime_ns(); ++ unsigned int i; ++ int ret = 0; ++ ++ for (i = 0; i < A9_SPI0_WINDOW_SAMPLES; i++) { ++ if (i) ++ usleep_range(A9_SPI0_WINDOW_US, A9_SPI0_WINDOW_US + 1000); ++ ret = apple_pmgr_observe(aop->domains, AOP_DOMAINS, states, ++ a9_aop_spi0_fault_read, s); ++ c->samples = i + 1; ++ c->elapsed_ms = div_u64(ktime_get_boottime_ns() - start, NSEC_PER_MSEC); ++ if (ret) { ++ c->failed = "failed-ch3-window-guard"; ++ break; ++ } ++ if (s->prerun_scm_lo || s->prerun_scm_hi || s->prerun || ++ s->prerun_8d4) { ++ s->reasserted = c->reasserted = true; ++ c->reassert_sample = i; ++ c->reassert_ms = c->elapsed_ms; ++ c->failed = "failed-spi0-window-reasserted"; ++ ret = -EIO; ++ dev_info(aop->dev, "SPI0 WINDOW reasserted sample=%u at_ms=%llu scm_lo=%08x scm_hi=%08x 8d0=%08x 8d4=%08x cpu=halted\n", ++ i, c->reassert_ms, s->prerun_scm_lo, s->prerun_scm_hi, ++ s->prerun, s->prerun_8d4); ++ if (s->prerun & A9_SPI0_CH3_PENDING) ++ dev_info(aop->dev, "SPI0 WINDOW channel3 c3=%08x route=%08x rate=%08x c3c=%08x desc=%08x 8d0=%08x 8d4=%08x\n", ++ r[0], r[1], r[2], r[3], r[4], r[5], r[6]); ++ break; ++ } ++ } ++ /* usleep_range() cannot return early; this only trips on a bad clock. */ ++ if (!ret && c->elapsed_ms < A9_SPI0_WINDOW_MS) { ++ c->failed = "failed-ch3-window-short"; ++ ret = -EIO; ++ } ++ c->window_done = !ret; ++ dev_info(aop->dev, "SPI0 CH3TD window errno=%d samples=%u/%u elapsed_ms=%llu min_ms=%u result=%s\n", ++ ret, c->samples, A9_SPI0_WINDOW_SAMPLES, c->elapsed_ms, ++ A9_SPI0_WINDOW_MS, ret ? c->failed : "pass"); ++ dev_info(aop->dev, "SPI0 pre-run errno=%d 8d0=%08x ch3_pending=%u cpu=halted\n", ++ ret, s->prerun, !!(s->prerun & A9_SPI0_CH3_PENDING)); ++ dev_info(aop->dev, "SPI0 WINDOW state errno=%d scm_lo=%08x scm_hi=%08x 8d0=%08x 8d4=%08x result=%s\n", ++ ret, s->prerun_scm_lo, s->prerun_scm_hi, s->prerun, ++ s->prerun_8d4, ret ? c->failed : "pass"); ++ if (ret && c->reasserted) ++ a9_aop_spi0_trace(aop, "window-fault"); ++ return ret; ++} ++ ++/* SCM state and the channel-3 interval pair, read-only, into scm[scm_at]. */ ++static void a9_aop_spi0_scm_read(void *data) ++{ ++ struct a9_aop_spi0 *s = data; ++ unsigned int i; ++ ++ for (i = 0; i < A9_SPI0_SCM_REGS; i++) ++ s->intc.scm[s->intc.scm_at][i] = readl(s->base + a9_spi0_scm_offsets[i]); ++} ++ ++/* ++ * After the clean pre-RUN window, CPU still halted: the firmware's group-1 ++ * pre-handler acknowledge for 0x10014 (0102c6de..0102c6f2) discards the ++ * old-session latch now that its SPI0 cause is quiet. Fresh firmware ++ * re-enables the source in event 1. The INTC words, the SCM state and the ++ * C3 interval pair are logged around it and never gated: with 0x10014 ++ * masked, no readable word shows whether it is still pending. ++ */ ++static int a9_aop_intc_ack(struct apple_aop *aop) ++{ ++ struct apple_pmgr_observation states[AOP_DOMAINS] = {}; ++ struct a9_aop_spi0 *s = &aop->spi0; ++ struct a9_aop_intc *n = &s->intc; ++ const u32 *p = n->scm[0], *q = n->scm[1]; ++ const u32 *b = n->state[A9_INTC_AT_PRE_ACK]; ++ const u32 *a = n->state[A9_INTC_AT_POST_ACK]; ++ int ret; ++ ++ aop->experiment_step = "intc-ack"; ++ n->scm_at = 0; ++ ret = apple_pmgr_observe(aop->domains, AOP_DOMAINS, states, ++ a9_aop_spi0_scm_read, s); ++ if (ret) { ++ n->failed = "failed-intc-ack-guard"; ++ dev_info(aop->dev, "AOP INTC pre-ack errno=%d: guard refused, no acknowledge\n", ++ ret); ++ return ret; ++ } ++ a9_aop_intc_snapshot(aop, A9_INTC_AT_PRE_ACK); ++ dev_info(aop->dev, "AOP INTC pre-ack scm_lo=%08x scm_hi=%08x c3_interval=%08x c3_cursor=%08x current=%08x cpu=halted\n", ++ p[0], p[1], p[2], p[3], b[A9_INTC_I_CURRENT]); ++ ret = aop_wr(aop, A9_INTC_ACK, A9_INTC_SPI0_SCM); ++ /* Also the posted-write barrier. */ ++ a9_aop_intc_snapshot(aop, A9_INTC_AT_POST_ACK); ++ n->acked = !ret; ++ dev_info(aop->dev, "AOP INTC ack errno=%d before=%08x value=%08x after=%08x\n", ++ ret, b[A9_INTC_I_ACK], A9_INTC_SPI0_SCM, a[A9_INTC_I_ACK]); ++ if (ret) { ++ n->failed = "failed-intc-ack"; ++ return ret; ++ } ++ n->scm_at = 1; ++ ret = apple_pmgr_observe(aop->domains, AOP_DOMAINS, states, ++ a9_aop_spi0_scm_read, s); ++ if (ret) ++ n->failed = "failed-intc-post-guard"; ++ dev_info(aop->dev, "AOP INTC post-ack errno=%d scm_lo=%08x scm_hi=%08x c3_interval=%08x c3_cursor=%08x current=%08x result=%s\n", ++ ret, q[0], q[1], q[2], q[3], a[A9_INTC_I_CURRENT], n->failed ?: "pass"); ++ return ret; ++} ++ ++/* ++ * Wake7 still took 0x10014 before SPI event 5 despite the targeted ack. ++ * Replay INTC lifecycle event 3 (0102c592/598/59c/5a2): 1/read/0/read at ++ * +0x808. aop_wr includes a posted-write read after each store. This resets ++ * all AOP-private interrupt state; it does not write the SCM status or MPU. ++ * No assumption that SCM must become zero. Only the final reset readback ++ * and the established channel-3 quiet conditions gate RUN. ++ */ ++static int a9_aop_intc_reset(struct apple_aop *aop) ++{ ++ struct a9_aop_spi0 *s = &aop->spi0; ++ struct a9_aop_intc *n = &s->intc; ++ int ret; ++ ++ if (!aop->force_halted || !aop->force_drained || !s->ch3.window_done || ++ !n->acked || n->reset_done || ++ (aop_rd(aop, AOP_CPU_CONTROL) & AOP_CPU_START) || readl(aop->control)) { ++ n->failed = "failed-intc-reset-state"; ++ return -EPERM; ++ } ++ ret = a9_aop_spi0_trace(aop, "pre-intc-reset"); ++ if (ret) ++ return ret; ++ aop->experiment_step = "intc-reset"; ++ dev_info(aop->dev, "AOP INTC reset begin pa=%llx sequence=1/read/0/read cpu=halted\n", ++ A9_SPI0_WRAPPER_PA + A9_INTC_RESET); ++ ret = aop_wr(aop, A9_INTC_RESET, 1); ++ n->reset_asserted = aop_rd(aop, A9_INTC_RESET); ++ if (ret) { ++ n->failed = "failed-intc-reset-assert"; ++ return ret; ++ } ++ ret = aop_wr(aop, A9_INTC_RESET, 0); ++ n->reset_after = aop_rd(aop, A9_INTC_RESET); ++ if (ret) { ++ n->failed = "failed-intc-reset-deassert"; ++ return ret; ++ } ++ if (n->reset_after) { ++ n->failed = "failed-intc-reset-readback"; ++ ret = -EIO; ++ } else { ++ ret = a9_aop_spi0_trace(aop, "post-intc-reset"); ++ if (!ret && a9_spi0_ch3_verify(s->trace.ch3, ++ s->teardown_stable & BIT(3))) { ++ n->failed = "failed-intc-reset-ch3"; ++ ret = -EIO; ++ } else if (!ret && a9_aop_spi0_selected_verify(s)) { ++ n->failed = "failed-intc-reset-channel"; ++ ret = -EIO; ++ } ++ } ++ n->reset_done = !ret; ++ dev_info(aop->dev, "AOP INTC reset errno=%d asserted=%08x after=%08x result=%s\n", ++ ret, n->reset_asserted, n->reset_after, n->failed ?: "pass"); ++ return ret; ++} +diff --git a/drivers/soc/apple/aop-warm-spi0.h b/drivers/soc/apple/aop-warm-spi0.h +new file mode 100644 +index 000000000..2470165ce +--- /dev/null ++++ b/drivers/soc/apple/aop-warm-spi0.h +@@ -0,0 +1,543 @@ ++/* SPDX-License-Identifier: GPL-2.0-only */ ++/* ++ * J71t AOP reload/wake experiments, both opt-in and default off. Constants ++ * and evidence types only; the MMIO lives in apple-a9-aop-active.c. ++ * Evidence: docs/periph/ds-aop-reload-crash-and-wake.md. ++ * ++ * aop_spi0_reset=1: every halt + SRAM reload of a live AOP crashed in ++ * AOPSPIMaster_channelFaultIRQ (0102f252, DFAR 0x4c): the AOP-private SPI0 ++ * controller kept channel 3's +0x8d0 bit 6 while the fresh firmware's ++ * channel table was still empty. With the CPU halted, the AP replays the ++ * firmware's own controller reset AOPSPIMaster_reset (0102f2f4), then the ++ * firmware's own channel-3 teardown, and samples bit 6 for >= 150 ms after ++ * the image copy before it releases RUN once. Round 4: the old session's ++ * SPI0 global IRQ 0x10014 stays latched in the AOP interrupt controller ++ * (SCMErr:0x3,3 at the accel STARTEP), so the AP disables that source before ++ * the reset and acknowledges it after the clean window, just before RUN. ++ * Wake7: the channel-3 idle wait gets the firmware's own 10 ms bound twice ++ * over, sampled per 1 ms slice, and the disable is judged by the INTC mask ++ * readback, which the firmware itself reads. ++ * Wake8: after the targeted ack, replay INTC event 3 (+0x808: 1/read/0/read). ++ * This resets retained interrupt delivery, not the read-only SCM state. ++ * Wake9: wake8 device evidence showed the channel-3 stop itself changes SCM ++ * from 0 to 3. Replay the same SPI controller reset once more after teardown; ++ * require the post-reset SCM state to be zero and the teardown state to hold. ++ * Wake10: the second reset clears SCM, but a halted CPU then saw SCM 3 and ++ * +0x8d0 bit 14 (channel 7) during the firmware copy. Snapshot all 16 channel ++ * slots, sample global SCM/fault state across the copy, and gate the copy and ++ * final RUN on a completely quiet readback. ++ * Wake11: that snapshot proved channels 3 and 7 were both running. Keep the ++ * channel-3 sequence unchanged, then apply the same firmware teardown to every ++ * other run-bit + nonzero-interval channel before the post-teardown reset. ++ * Wake12: channel 7 is the phosphorus/BMP284 async client. Its firmware has no ++ * owner-specific stop path, so do not invent another store when the generic ++ * stop times out. Record the remaining scheduler state read-only and refuse. ++ * Wake13/14: every device stop of channel 3 that idled (wake8..wake12) came ++ * 1.5-2.2 ms after the controller reset; the two that did not (wake13/14) came ++ * 11/33 ms after it (pr_flush in the checkpoint), with class-0 state identical to ++ * wake12's passes. Wake15 closed that window (no pr_flush between the reset and ++ * the stops, console lock held), recorded the reset-to-stop latency and the peers ++ * per stop, and accepted one identity-pinned stopped-without-idle channel (3 or ++ * 7) as wake13 did for channel 7. Wake15's channel-7-first order then showed ++ * channel 7 never idles even at 0.3 ms (it is 0/3 over wake11/12/15), with ++ * nothing ticking. Wake16 therefore returns to channel 3 first (early, the only ++ * stop that ever idled) and then channel 7, which takes the stable-stop path ++ * (SCM 0/0 or 3/0, constant); every stop logs why the fingerprint did or did not ++ * hold (stable_why=). Wake14's class-0 prelude stays removed. ++ * ++ * aop_wake_wrapper=1: before 0x220, the AppleA7IOP status-3 resume wrapper ++ * writes (b4b854..b4b8b4). No CPU RUN, no auto-boot, no NMI. ++ */ ++#ifndef _A9_AOP_RELOAD_H ++#define _A9_AOP_RELOAD_H ++ ++#include ++#include ++#include "aop-warm-record.h" ++ ++/* Firmware device spi0: IOP 0x10100000 (space 2, 0x1000) via /arm-io ranges. */ ++#define A9_SPI0_PA 0x210100000ULL ++#define A9_SPI0_SIZE 0x1000U ++#define A9_SPI0_WRAPPER_PA 0x210800000ULL /* the only layout this applies to */ ++#define A9_SPI0_CONTROL 0x000U ++#define A9_SPI0_CLOCK 0x008U /* 0102f2aa programs 0x01b71b00 here */ ++#define A9_SPI0_FAULT 0x8d0U /* bit 2n = channel n, read by 0102f20a */ ++#define A9_SPI0_FAULT_8D4 0x8d4U ++#define A9_SPI0_CHANNEL_BASE 0x100U ++#define A9_SPI0_CHANNEL_STRIDE 0x080U ++#define A9_SPI0_CHANNELS 16U ++#define A9_SPI0_CHANNEL_REGS 7U ++#define A9_SPI0_CHANNEL_DESC 0x018U ++#define A9_SPI0_CH3 0x280U /* base + 0x100 + 0x80 * 3 */ ++#define A9_SPI0_CH3_0C 0x28cU ++#define A9_SPI0_RESET 0x100U /* control bit 8 */ ++#define A9_SPI0_ENABLE 0x001U /* control bit 0 */ ++#define A9_SPI0_CH3_PENDING 0x040U ++/* The firmware spins without bound; the AP gives each poll 1 ms. */ ++#define A9_SPI0_POLL_US 1000U ++#define A9_SPI0_DELAY_US 100U /* 0102b3f8(100, 0), twice */ ++#define A9_SPI0_REGS 6 ++ ++static const u32 a9_spi0_offsets[A9_SPI0_REGS] = { ++ A9_SPI0_CONTROL, A9_SPI0_CLOCK, A9_SPI0_FAULT, A9_SPI0_FAULT_8D4, ++ A9_SPI0_CH3, A9_SPI0_CH3_0C, ++}; ++ ++/* Snapshot indices into a9_spi0_offsets. */ ++enum { A9_SPI0_I_CONTROL, A9_SPI0_I_CLOCK, A9_SPI0_I_FAULT }; ++ ++/* ++ * Channel-3 teardown after the reset (ds-aop-spi0-ch3-round3.md section 5). ++ * The reset leaves C3 = 0x00010447: idle but still enabled, async-armed and ++ * with the class-0 fault IRQ on, so a level source re-latched +0x8d0 bit 6 ++ * within ~105 ms with the CPU halted. AOPSPIMaster_teardownChannel ++ * (0102e4f8) stops the channel and masks both IRQ classes; the fault ++ * handler (0102f20a) then acknowledges with C3C bit 5. ++ */ ++#define A9_SPI0_CH3_ROUTE 0x284U ++#define A9_SPI0_CH3_RATE 0x288U ++#define A9_SPI0_CH3_DESC 0x298U /* channel +0x18, zeroed on teardown */ ++#define A9_SPI0_CH3_RUN 0x001U /* C3 bit 0, channel run/enable */ ++#define A9_SPI0_CH3_IDLE 0x002U /* C3 bit 1, polled after the stop */ ++#define A9_SPI0_CH3_IRQ0 0x040U /* C3 bit 6, +0x8d0 class-0/fault enable */ ++#define A9_SPI0_CH3_IRQ1 0x100U /* C3 bit 8, +0x8d4 class-1 enable */ ++#define A9_SPI0_CHANNEL_PROGRESS_SHIFT 24U /* hardware-updated, not an IRQ class */ ++#define A9_SPI0_CH3_STOP 0x411U /* clear run bit 0 and bits 4, 10 */ ++#define A9_SPI0_CH3_CLASS0 0x450U /* mask class 0: bits 4, 6, 10 */ ++#define A9_SPI0_CH3_CLASS1 0x510U /* mask class 1: bits 4, 8, 10 */ ++#define A9_SPI0_CH3_ACK 0x020U /* C3C bit 5, ORed by 0102f20a; self-clearing */ ++#define A9_SPI0_PHOSPHORUS_CHANNEL 7U ++#define A9_SPI0_PHOSPHORUS_CTL_MASK 0x00ffffffU ++#define A9_SPI0_PHOSPHORUS_CTL_RUN 0x00010445U ++#define A9_SPI0_PHOSPHORUS_CTL_STOPPED 0x00010444U ++#define A9_SPI0_PHOSPHORUS_ROUTE 0x00000040U ++#define A9_SPI0_PHOSPHORUS_RATE 0x019000c8U ++#define A9_SPI0_PHOSPHORUS_CFG 0x00200009U ++/* Channel 3 is the MPU6900 async FIFO client (route 0x10, same rate/config). */ ++#define A9_SPI0_MPU_ASYNC_CHANNEL 3U ++#define A9_SPI0_MPU_ASYNC_ROUTE 0x00000010U ++/* Stopped control word, any top byte, class-0 enable optional (0x40). */ ++#define A9_SPI0_STABLE_CTL_STOPPED 0x00010404U ++#define A9_SPI0_STABLE_CTL_MASK 0x00ffffbfU ++/* Peer sampling and the "late stop" mark are reported, never gated, in-kernel. */ ++#define A9_SPI0_LATE_STOP_US 3000U ++/* ++ * AOPSPIMaster_setChannelEnabled(false) (0102ef7c) stores C3 & ~0x411 only ++ * if bit 0 was set, then spins on bit 1 until master +0x30, and panics ++ * "SCMto:%u" (0x010398da) past it. Event 1 sets +0x30 to 0x2710 us when it ++ * is 0 (0102e6fa, as in the live-cold object), through timer +0x18 = ++ * us * Hz / 1000000 (0102ff3c): a 10 ms bound. The AP polls in 1 ms slices, ++ * up to twice that, sampling C3, the SCM state and +0x8d0 after each slice. ++ */ ++#define A9_SPI0_CH3_IDLE_FW_US 10000U ++#define A9_SPI0_CH3_SLICE_US 1000U ++#define A9_SPI0_CH3_SLICES 20U /* 20 ms */ ++/* At least 150 ms (the 16:27 re-latch took ~105 ms) of samples before RUN. */ ++#define A9_SPI0_WINDOW_SAMPLES 31U ++#define A9_SPI0_WINDOW_US 5000U /* 30 sleeps >= 5 ms each */ ++#define A9_SPI0_WINDOW_MS 150U /* required first-to-last sample span */ ++#define A9_SPI0_CH3_REGS 7 ++ ++static const u32 a9_spi0_ch3_offsets[A9_SPI0_CH3_REGS] = { ++ A9_SPI0_CH3, A9_SPI0_CH3_ROUTE, A9_SPI0_CH3_RATE, A9_SPI0_CH3_0C, ++ A9_SPI0_CH3_DESC, A9_SPI0_FAULT, A9_SPI0_FAULT_8D4, ++}; ++ ++/* Every firmware-visible channel slot, read only in wake10 and selected in wake11. */ ++static const u32 a9_spi0_channel_offsets[A9_SPI0_CHANNEL_REGS] = { ++ 0x00U, 0x04U, 0x08U, 0x0cU, 0x10U, 0x14U, 0x18U, ++}; ++ ++enum { ++ A9_CHANNEL_I_CTL, A9_CHANNEL_I_ROUTE, A9_CHANNEL_I_RATE, A9_CHANNEL_I_CFG, ++ A9_CHANNEL_I_INTERVAL, A9_CHANNEL_I_CURSOR, A9_CHANNEL_I_DESC, ++}; ++ ++/* Snapshot indices into a9_spi0_ch3_offsets. */ ++enum { ++ A9_CH3_I_C3, A9_CH3_I_ROUTE, A9_CH3_I_RATE, A9_CH3_I_C3C, A9_CH3_I_DESC, ++ A9_CH3_I_8D0, A9_CH3_I_8D4, ++}; ++ ++/* One idle-poll slice: C3 as the slice ended, then SCM state and +0x8d0. */ ++struct a9_aop_ch3_slice { ++ u64 ns; /* since the stop store */ ++ u32 c3, scm_lo, scm_hi, fault; ++}; ++ ++/* Wake12 read-only scheduler evidence for an additional-channel stop poll. */ ++struct a9_aop_channel_diag_slice { ++ u32 cfg, interval, cursor, fault_8d4; ++}; ++ ++struct a9_aop_spi0_ch3 { ++ u32 before[A9_SPI0_CH3_REGS], after[A9_SPI0_CH3_REGS]; ++ u32 stop, idle, class0, class1, ack; /* words written / polled */ ++ int poll; /* idle-bit poll result */ ++ bool run_before; /* C3 bit 0 before the stop */ ++ unsigned int slices; /* idle-poll slices used */ ++ struct a9_aop_ch3_slice slice[A9_SPI0_CH3_SLICES]; ++ u64 idle_us; /* stop store to the idle read */ ++ int error; ++ bool ran; /* the guarded teardown executed */ ++ const char *failed; /* distinct refusal reason, or NULL */ ++ /* Wake15: stop latency after the controller reset and the peers' state. */ ++ u64 stop_ns, reset_to_stop_us; ++ u32 peers_run, peers_ticking; /* masks of the other 15 channels */ ++ bool stable; /* accepted stopped-without-idle identity */ ++ const char *stable_why; /* first failed fingerprint condition, NULL if stable */ ++ /* Pre-RUN window after the image copy; a refusal also sets failed. */ ++ unsigned int samples; ++ u64 elapsed_ms; ++ bool window_done, reasserted; ++ unsigned int reassert_sample; ++ u64 reassert_ms; ++ u32 reassert[A9_SPI0_CH3_REGS]; ++}; ++ ++struct a9_aop_spi0; ++ ++struct a9_aop_spi0_channel_op { ++ struct a9_aop_spi0 *spi0; ++ struct a9_aop_spi0_ch3 *state; ++ unsigned int channel; ++}; ++ ++/* ++ * AOP interrupt controller, wrapper offsets (ds-aop-spi0-ch3-round4.md ++ * section 3). IRQ 0x10014 is group 1, source 0x14: bank 0, bit 20. The ++ * group-1 disable/enable banks are write-one commands whose reads return ++ * the mask, 1 = disabled: the sleep command saves +0xa00..+0xa08 (0102c5ce) ++ * before masking all (0102c5da), the restore writes saved to +0xa00 and ++ * ~saved to +0xa80 (0102c9e6/0102c9f4), and aop_interrupt_write_enabled ++ * reads the target bank for the previous state (0102c9c0). So the disable ++ * is judged by bit 20 of the +0xa00 readback (wake6: ffffffff before the ++ * store, every source already masked by the quiesce). +0x81c only reports ++ * an unmasked pending source: the sleep path masks all, writes group 7 ++ * +0xc020/+0xc024 = 2 and spins until +0x81c reads 0 (0102c654), so ++ * all-ones there means the AP is not reading the INTC. +0x808 ends 0 after ++ * the event-3 pulse. +0x900 is the group-1 raise (0102c7c6) and +0x980 its ++ * pre-handler clear (0102c6f2); the firmware never reads them, so their ++ * readbacks are logged only. No register shows a masked source pending. ++ */ ++#define A9_INTC_RESET 0x808U /* event-3 pulse 1 then 0 (0102c592) */ ++#define A9_INTC_CURRENT 0x81cU /* current unmasked source */ ++#define A9_INTC_RAISE 0x900U /* group 1 raise, 0102c7c6 */ ++#define A9_INTC_ACK 0x980U /* group 1 pre-handler ack/clear, 0102c6f2 */ ++#define A9_INTC_DISABLE 0xa00U /* group 1 disable, store at 0102c9ce */ ++#define A9_INTC_ENABLE 0xa80U /* group 1 enable; reads the same mask */ ++#define A9_INTC_G7_DISABLE 0xc020U /* group 7 disable, 2 after sleep */ ++#define A9_INTC_SPI0_SCM 0x00100000U /* IRQ 0x10014 */ ++#define A9_INTC_ALL_ONES 0xffffffffU ++#define A9_INTC_REGS 10 ++ ++static const u32 a9_intc_offsets[A9_INTC_REGS] = { ++ A9_INTC_RESET, A9_INTC_CURRENT, A9_INTC_RAISE, A9_INTC_ACK, ++ A9_INTC_DISABLE, A9_INTC_DISABLE + 4, A9_INTC_DISABLE + 8, ++ A9_INTC_ENABLE, A9_INTC_G7_DISABLE, A9_INTC_G7_DISABLE + 4, ++}; ++ ++/* Snapshot indices into a9_intc_offsets. */ ++enum { ++ A9_INTC_I_RESET, A9_INTC_I_CURRENT, A9_INTC_I_RAISE, A9_INTC_I_ACK, ++ A9_INTC_I_MASK0, A9_INTC_I_MASK1, A9_INTC_I_MASK2, A9_INTC_I_ENABLE0, ++ A9_INTC_I_G7_0, A9_INTC_I_G7_1, ++}; ++ ++/* When the AP snapshots the INTC words. */ ++enum { ++ A9_INTC_AT_PRE_DISABLE, A9_INTC_AT_POST_DISABLE, A9_INTC_AT_PRE_ACK, ++ A9_INTC_AT_POST_ACK, A9_INTC_AT_N, ++}; ++ ++static inline const char *a9_intc_at_name(unsigned int at) ++{ ++ static const char * const names[] = { ++ "pre-disable", "post-disable", "pre-ack", "post-ack", ++ }; ++ ++ return at < A9_INTC_AT_N ? names[at] : "unknown"; ++} ++ ++/* "all-ones" if +0x81c reads ffffffff, which a live INTC cannot report. */ ++static inline const char *a9_intc_view(const u32 *state) ++{ ++ return state[A9_INTC_I_CURRENT] == A9_INTC_ALL_ONES ? "all-ones" : "intc"; ++} ++/* ++ * Read only, never written: the SCM state (also per idle-poll slice) and, ++ * around the acknowledge, the C3 interval pair. ++ */ ++#define A9_SPI0_SCM_LO 0x00cU ++#define A9_SPI0_SCM_HI 0x010U ++#define A9_SPI0_CH3_IVAL 0x290U ++#define A9_SPI0_CH3_CURSOR 0x294U ++#define A9_SPI0_SCM_REGS 4 ++ ++static const u32 a9_spi0_scm_offsets[A9_SPI0_SCM_REGS] = { ++ A9_SPI0_SCM_LO, A9_SPI0_SCM_HI, A9_SPI0_CH3_IVAL, A9_SPI0_CH3_CURSOR, ++}; ++ ++struct a9_aop_intc { ++ u32 state[A9_INTC_AT_N][A9_INTC_REGS]; /* around each store */ ++ unsigned int states; /* bit n: state[n] was read */ ++ u32 scm[2][A9_SPI0_SCM_REGS]; /* before / after the ack */ ++ unsigned int scm_at; /* scm[] row the guarded read fills */ ++ bool disabled, acked; ++ bool reset_done; ++ u32 reset_asserted, reset_after; ++ const char *failed; /* distinct refusal reason, or NULL */ ++}; ++ ++/* Read-only milestones. Wake10 gates the global SCM/fault words, not channels. */ ++struct a9_aop_spi0_trace { ++ u32 control, clock, scm[A9_SPI0_SCM_REGS], ch3[A9_SPI0_CH3_REGS]; ++ u32 channel[A9_SPI0_CHANNELS][A9_SPI0_CHANNEL_REGS]; ++}; ++ ++/* Wake9's second, post-channel-stop replay of AOPSPIMaster_reset. */ ++struct a9_aop_spi0_post_reset { ++ u32 before[A9_SPI0_REGS], after[A9_SPI0_REGS]; ++ u32 saved, asserted, polled; ++ int error; ++ bool ran; ++ const char *failed; ++}; ++ ++/* ++ * First failed readback after the teardown, or NULL if all hold. C3C bit 5 ++ * is a pulse: at 19:50 the write of 0x00200029 read back 0x00200009 while ++ * +0x8d0 bit 6 went 1 -> 0, so the acknowledge is judged by bit 6 alone and ++ * the C3C readback is only logged. ++ */ ++static inline const char *a9_spi0_ch3_verify(const u32 *after, bool stable) ++{ ++ if (after[A9_CH3_I_C3] & A9_SPI0_CH3_RUN) ++ return "failed-ch3-teardown-run"; ++ if (after[A9_CH3_I_C3] & A9_SPI0_CH3_IRQ0) ++ return "failed-ch3-teardown-class0"; ++ if (after[A9_CH3_I_C3] & A9_SPI0_CH3_IRQ1) ++ return "failed-ch3-teardown-class1"; ++ if (!(after[A9_CH3_I_C3] & A9_SPI0_CH3_IDLE) && !stable) ++ return "failed-ch3-teardown-idle"; ++ if (after[A9_CH3_I_DESC]) ++ return "failed-ch3-teardown-desc"; ++ if (after[A9_CH3_I_8D0] & A9_SPI0_CH3_PENDING) ++ return "failed-ch3-teardown-pending"; ++ return NULL; ++} ++ ++/* ++ * Has firmware run since the last SoC reset? auto-boot alone cannot say: ++ * the outgoing aop_quiesce=1 halt clears it, exactly like a DFU cold start. ++ * The previous kernel's reserved-RAM shutdown transcript says whether its ++ * firmware ran. That DRAM can outlive a DFU; wrapper +0x4020 outbox enable ++ * cannot: set after reset (12/12 DFU cold snapshots), cleared by the first ++ * store of every Linux stop. No SPI0 access is needed to decide. ++ */ ++enum a9_spi0_prev_state { ++ A9_SPI0_PREV_MISSING, /* no transcript magic, or none reserved */ ++ A9_SPI0_PREV_INVALID, /* magic present, header/checksum rejected */ ++ A9_SPI0_PREV_VALID, ++}; ++ ++struct a9_spi0_prev { ++ enum a9_spi0_prev_state state; ++ bool hello; /* "SHUTDOWN begin ... hello=1": its firmware ran */ ++ bool halted; /* "QUIESCE step=complete ... halted=1" + steps bit */ ++ bool carried; /* "SPI0-CARRY retained=1": ran earlier in the chain */ ++ u32 steps; /* "QUIESCE step=complete ... steps=0x..." */ ++}; ++ ++enum a9_spi0_reason { ++ /* Run the reset. */ ++ A9_SPI0_LIVE_AUTO_BOOT, /* auto-boot 1 before the stop */ ++ A9_SPI0_PREV_HALTED, /* previous kernel quiesced and halted it */ ++ A9_SPI0_PREV_LIVE, /* previous kernel's firmware ran, no halt */ ++ A9_SPI0_PREV_CARRIED, /* an earlier kernel in the chain ran it */ ++ /* Skip: the block is not touched. */ ++ A9_SPI0_FRESH, /* no transcript, auto-boot 0: DFU cold */ ++ A9_SPI0_PREV_BAD, /* transcript present but corrupt */ ++ A9_SPI0_PREV_NO_RUN, /* transcript: nothing ran in the chain */ ++ A9_SPI0_STALE, /* run evidence, wrapper at reset state */ ++}; ++ ++static inline const char *a9_spi0_reason_name(enum a9_spi0_reason reason) ++{ ++ static const char * const names[] = { ++ "live-auto-boot", "prev-quiesce-halted", "prev-live", "prev-carried", ++ "fresh-instance", "prev-transcript-invalid", "prev-no-run", ++ "stale-transcript", ++ }; ++ ++ return reason <= A9_SPI0_STALE ? names[reason] : "unknown"; ++} ++ ++static inline const char *a9_spi0_prev_name(enum a9_spi0_prev_state state) ++{ ++ return state == A9_SPI0_PREV_VALID ? "valid" : ++ state == A9_SPI0_PREV_INVALID ? "invalid" : "missing"; ++} ++ ++static inline bool a9_spi0_reason_runs(enum a9_spi0_reason reason) ++{ ++ return reason <= A9_SPI0_PREV_CARRIED; ++} ++ ++/* ++ * Release policy: a reason that would run the reload is allowed by default ++ * only for prev-quiesce-halted, the one state device-proven end to end. A ++ * skip (fresh, bad record, nothing ran) is allowed only while the wrapper is ++ * still at its reset state; stale records skip as before. Untested lifts both. ++ */ ++static inline bool a9_spi0_policy_allows(enum a9_spi0_reason reason, bool outbox_enabled, ++ bool untested) ++{ ++ if (untested || reason == A9_SPI0_PREV_HALTED || reason == A9_SPI0_STALE) ++ return true; ++ if (a9_spi0_reason_runs(reason)) ++ return false; ++ /* ++ * A skip means "nothing ran since the SoC reset", which only holds if the ++ * wrapper is still at its reset state (+0x4020 outbox enable set, 12/12 DFU ++ * cold starts). Without it some earlier kernel stopped the firmware and its ++ * record is lost: a cold load would land on a halted, never-torn-down AOP. ++ */ ++ return outbox_enabled; ++} ++ ++/* Token search inside one transcript line, which holds no NUL. */ ++static inline bool a9_spi0_line_has(const char *line, size_t length, ++ const char *token, size_t n, ++ const char **at) ++{ ++ size_t i; ++ ++ for (i = 0; n <= length && i <= length - n; i++) { ++ if (!memcmp(line + i, token, n)) { ++ *at = line + i + n; ++ return true; ++ } ++ } ++ return false; ++} ++ ++#define a9_spi0_has(line, length, token, at) \ ++ a9_spi0_line_has(line, length, token, sizeof(token) - 1, at) ++ ++static inline bool a9_spi0_line_starts(const char *line, size_t length, ++ const char *prefix, size_t n) ++{ ++ return length >= n && !memcmp(line, prefix, n); ++} ++ ++#define a9_spi0_starts(line, length, prefix) \ ++ a9_spi0_line_starts(line, length, prefix, sizeof(prefix) - 1) ++ ++static inline u32 a9_spi0_hex(const char *p, const char *end) ++{ ++ u32 value = 0; ++ ++ for (; p < end; p++) { ++ if (*p >= '0' && *p <= '9') ++ value = value << 4 | (u32)(*p - '0'); ++ else if (*p >= 'a' && *p <= 'f') ++ value = value << 4 | (u32)(*p - 'a' + 10); ++ else ++ break; ++ } ++ return value; ++} ++ ++/* Our own checksummed transcript; only whole-line prefixes count. */ ++static inline void a9_spi0_prev_parse(struct a9_spi0_prev *p, ++ const char *text, size_t length) ++{ ++ const char *end = text + length, *line, *next, *at; ++ size_t n; ++ ++ for (line = text; line < end; line = next < end ? next + 1 : end) { ++ next = memchr(line, '\n', end - line); ++ if (!next) ++ next = end; ++ n = next - line; ++ if (a9_spi0_starts(line, n, "SHUTDOWN begin ") && ++ a9_spi0_has(line, n, " hello=1 ", &at)) ++ p->hello = true; ++ if (a9_spi0_starts(line, n, "QUIESCE step=complete ")) { ++ if (a9_spi0_has(line, n, " steps=0x", &at)) ++ p->steps = a9_spi0_hex(at, next); ++ p->halted = a9_spi0_has(line, n, " halted=1 ", &at) && ++ (p->steps & (1U << A9_HO_STEP_HALTED)); ++ } ++ if (a9_spi0_starts(line, n, "SPI0-CARRY retained=1 ")) ++ p->carried = true; ++ } ++} ++ ++/* ++ * Skips unless there is positive evidence: running the reset on a fresh ++ * block would be the first AP access to SPI0 with nothing retained to ++ * clear (and ps_spi0 may gate it), while a skip on retained state only ++ * repeats the known, non-hanging accel STARTEP crash. ++ */ ++static inline enum a9_spi0_reason a9_spi0_decide(bool auto_boot, bool outbox_enabled, ++ const struct a9_spi0_prev *p) ++{ ++ if (auto_boot) ++ return A9_SPI0_LIVE_AUTO_BOOT; ++ if (p->state == A9_SPI0_PREV_MISSING) ++ return A9_SPI0_FRESH; ++ if (p->state != A9_SPI0_PREV_VALID) ++ return A9_SPI0_PREV_BAD; ++ if (!p->hello && !p->carried) ++ return A9_SPI0_PREV_NO_RUN; ++ if (outbox_enabled) ++ return A9_SPI0_STALE; ++ if (p->hello) ++ return p->halted ? A9_SPI0_PREV_HALTED : A9_SPI0_PREV_LIVE; ++ return A9_SPI0_PREV_CARRIED; ++} ++ ++struct a9_aop_spi0 { ++ void __iomem *base; /* mapped only with aop_spi0_reset=1 */ ++ u32 before[A9_SPI0_REGS], after[A9_SPI0_REGS]; ++ u32 saved; /* control word before the reset */ ++ u32 asserted; /* saved & ~0x101 | 0x100 */ ++ u32 polled; /* control word when the polls ended */ ++ u32 prerun; /* +0x8d0 just before RUN */ ++ u32 prerun_8d4, prerun_scm_lo, prerun_scm_hi; ++ u32 pmgr_before, pmgr_after; /* AP PMGR ps_spi0 raw, read-only */ ++ int pmgr_before_errno, pmgr_after_errno; ++ int error; ++ bool ran, reasserted, copy_faulted; ++ const char *gate_failed; ++ const char *skipped; ++ struct a9_spi0_prev prev; /* parsed at probe from the transcript */ ++ u32 auto_boot, outbox; /* boot snapshot, before the stop */ ++ enum a9_spi0_reason reason; ++ bool decided; ++ struct a9_aop_spi0_ch3 ch3; /* after a clean reset only */ ++ /* Wake11 reuses one bounded scratch record for each additional channel. */ ++ struct a9_aop_spi0_ch3 channel; ++ u32 teardown_selected, teardown_done; ++ unsigned int teardown_failed_channel; ++ struct a9_aop_spi0_post_reset post_reset; /* after the stop latches SCM */ ++ struct a9_aop_intc intc; /* IRQ 0x10014 around the reset */ ++ struct a9_aop_spi0_trace trace; /* latest read-only milestone */ ++ /* Appended so every pre-wake12 field, including ch3, keeps its layout. */ ++ struct a9_aop_channel_diag_slice channel_diag[A9_SPI0_CH3_SLICES]; ++ /* Wake13: exact phosphorus stopped-without-idle exception, by channel. */ ++ u32 teardown_stable; ++ /* Wake15: channel 3 shares the sampled diagnostics; reset completion time. */ ++ struct a9_aop_channel_diag_slice ch3_diag[A9_SPI0_CH3_SLICES]; ++ u64 reset_done_ns; ++}; ++ ++/* 0102f2f4: bits 8 and 0 clear; the reset write adds bit 8. */ ++static inline u32 a9_spi0_base_word(u32 saved) ++{ ++ return saved & ~(A9_SPI0_RESET | A9_SPI0_ENABLE); ++} ++ ++#endif +diff --git a/drivers/soc/apple/aop.c b/drivers/soc/apple/aop.c +index e8ae90098..d208eacb6 100644 +--- a/drivers/soc/apple/aop.c ++++ b/drivers/soc/apple/aop.c +@@ -1,19 +1,28 @@ + // SPDX-License-Identifier: GPL-2.0-only OR MIT + /* +- * Apple T8010 Always-On Processor (AOP) ++ * Apple S8000 and T8010 Always-On Processor (AOP) + * + * The AOP runs RTKit firmware that the operating system loads: a main image + * into the AOP's SRAM and an external segment into system memory, with boot +- * arguments filled in. Its services talk AFK/EPIC over shared ring buffers on +- * the RTKit application endpoints, as on later SoCs (see the Asahi Linux AOP ++ * arguments filled in. On the S8000 (A9) the AOP sees its SRAM through a window ++ * that has to be set up first, and its firmware takes more boot arguments. ++ * Its services talk AFK/EPIC over shared ring buffers on the RTKit ++ * application endpoints, as on later SoCs (see the Asahi Linux AOP + * driver). Each service with a device tree child node of the same name gets a + * child platform device; its driver receives the service's reports through + * apple_aop_listen() and sends requests with apple_aop_call(). + * +- * The firmware cannot be restarted once stopped, so the AOP starts once per +- * boot and the driver cannot be unbound or unloaded after that. ++ * J71t can reload after a certified quiesce/halt/drain across kexec. Other ++ * retained states are refused. Unbind/unload after starting remains forbidden. + */ + ++#include ++#include ++#include ++#include ++#include ++#include ++#include "aop-warm-spi0.h" + #include + #include + #include +@@ -41,6 +50,14 @@ + #define APPLE_AOP_IRQ_MASK_ALL 0x1111 + #define APPLE_AOP_IRQ_INBOX BIT(0) + #define APPLE_AOP_IRQ_OUTBOX BIT(12) ++/* S8000: the window through which the AOP sees its SRAM */ ++#define APPLE_AOP_REMAP_PA_LO 0x08 ++#define APPLE_AOP_REMAP_PA_HI 0x10 ++#define APPLE_AOP_REMAP_IOVA_LO 0x18 ++#define APPLE_AOP_REMAP_IOVA_HI 0x20 ++#define APPLE_AOP_REMAP_SIZE 0x28 ++#define APPLE_AOP_REMAP_RESERVED 0x30 ++#define APPLE_AOP_REMAP_ENABLE 0x38 + + #define APPLE_AOP_CONTROL_AUTO_BOOT 0x0 + +@@ -54,6 +71,13 @@ + #define APPLE_AOP_BOOTARG_ECID APPLE_AOP_FOURCC('E', 'C', 'I', 'D') + #define APPLE_AOP_BOOTARG_NONC APPLE_AOP_FOURCC('N', 'O', 'N', 'C') + #define APPLE_AOP_BOOTARG_EC0P APPLE_AOP_FOURCC('E', 'C', '0', 'p') ++#define APPLE_AOP_BOOTARG_AOPT APPLE_AOP_FOURCC('A', 'O', 'P', 't') ++#define APPLE_AOP_BOOTARG_NCAL APPLE_AOP_FOURCC('n', 'C', 'a', 'l') ++#define APPLE_AOP_BOOTARG_WRAD APPLE_AOP_FOURCC('W', 'r', 'A', 'd') ++#define APPLE_AOP_BOOTARG_SOC APPLE_AOP_FOURCC('S', 'O', 'C', '_') ++#define APPLE_AOP_BOOTARG_SOCR APPLE_AOP_FOURCC('S', 'O', 'C', 'R') ++#define APPLE_AOP_BOOTARG_STKG APPLE_AOP_FOURCC('S', 'T', 'K', 'G') ++#define APPLE_AOP_MAX_BOOTARGS 10 + + #define APPLE_AOP_HALT_TIMEOUT_US 10000 + +@@ -78,7 +102,9 @@ + #define AFK_OPC_GET_BUF_ACK 0xa1 + #define AFK_OPC_SEND 0xa2 + #define AFK_OPC_START 0xa3 +-#define AFK_OPC_SHUTDOWN_ACK 0xc1 ++#define AFK_OPC_SHUTDOWN 0xa4 ++#define AFK_OPC_SHUTDOWN_ACK 0x87 ++#define AFK_OPC_HIBERNATE_ACK 0xc3 + + #define AFK_BLOCK_STEP 0x40 + #define AFK_MAX_BUFFER SZ_1M +@@ -100,6 +126,12 @@ + #define APPLE_AOP_MAX_CALLS 8 + #define APPLE_AOP_CALL_TIMEOUT msecs_to_jiffies(1000) + ++/* Service properties */ ++#define APPLE_AOP_PROPERTY_MAX 64 ++#define APPLE_AOP_PROP_CALIBRATION 0x3d ++#define APPLE_AOP_CALIBRATION_PARTIAL 1 ++#define APPLE_AOP_CALIBRATION_COMPLETE 2 ++ + struct afk_qe_header { + __le32 magic; + __le32 size; +@@ -160,6 +192,8 @@ struct apple_aop_ep { + + struct mutex lock; /* tx ring, seq and calls */ + u16 seq; ++ bool initialized; ++ struct completion stopped; + struct apple_aop_call *calls[APPLE_AOP_MAX_CALLS]; + }; + +@@ -182,12 +216,25 @@ struct apple_aop_register_work { + struct apple_aop_service service; + }; + ++struct apple_aop_hw { ++ /* Set up the AOP's window onto its SRAM before starting it */ ++ bool sram_window; ++ /* A fresh NONC boot argument */ ++ bool nonce; ++ /* AOPt, SOC_, SOCR, WrAd, STKG and nCal boot arguments */ ++ bool soc_bootargs; ++}; ++ + struct apple_aop { + struct device *dev; ++ const struct apple_aop_hw *hw; + void __iomem *akf; + void __iomem *control; + void __iomem *sram; ++ resource_size_t akf_base; ++ resource_size_t sram_base; + resource_size_t sram_size; ++ u32 target, chip_id, chip_revision; + struct apple_rtkit *rtk; + + void *ext; +@@ -202,6 +249,16 @@ struct apple_aop { + struct mutex children_lock; /* children, removing */ + struct list_head children; + bool removing; ++ u64 accel_reports; ++ int shutdown_error; ++ /* J71t only; all other boards retain their original start/stop path. */ ++ bool warm, hello, iop_crashed, force_halted, force_drained; ++ void __iomem *warm_wrapper; ++ struct device_node *domains[5], *spi0_pmgr; ++ struct a9_aop_spi0 spi0; ++ const char *experiment_step; ++ struct a9_sl_header *shutdown_log; ++ char *shutdown_text; + }; + + static bool apple_aop_is_started(struct apple_aop *aop) +@@ -223,6 +280,409 @@ static bool apple_aop_halt(struct apple_aop *aop) + APPLE_AOP_HALT_TIMEOUT_US); + } + ++static bool a9_aop_ramoops_ready(void) ++{ ++ struct console *con; ++ bool ready = false; ++ int cookie; ++ ++ cookie = console_srcu_read_lock(); ++ for_each_console_srcu(con) { ++ if (!strcmp(con->name, "ramoops") && ++ (console_srcu_read_flags(con) & (CON_ENABLED | CON_SUSPENDED)) == ++ CON_ENABLED) { ++ ready = true; ++ break; ++ } ++ } ++ console_srcu_read_unlock(cookie); ++ return ready; ++} ++ ++/* SPDX-License-Identifier: GPL-2.0-only */ ++/* J71t warm reload integration. SPI0 machinery is the release6 wake16 code. */ ++#define AOP_DOMAINS 5 ++#define AOP_CPU_CONTROL 0x44 ++#define AOP_CPU_START BIT(4) ++#define AOP_OUTBOX_CONTROL 0x4020 ++#define AOP_INBOX_CONTROL 0x4008 ++#define AOP_OUTBOX_ENABLE BIT(0) ++#define AOP_MBOX_FAULTS GENMASK(19, 18) ++#define cold_defer 1 ++#define spi0_untested false ++ ++static u32 aop_rd(struct apple_aop *aop, u32 offset) ++{ ++ return readl(aop->warm_wrapper + offset); ++} ++ ++static bool a9_aop_force_stopped(struct apple_aop *aop) ++{ ++ return !(aop_rd(aop, AOP_CPU_CONTROL) & AOP_CPU_START) && ++ !readl(aop->control) && ++ !(aop_rd(aop, AOP_OUTBOX_CONTROL) & (AOP_OUTBOX_ENABLE | AOP_MBOX_FAULTS)) && ++ !(aop_rd(aop, AOP_INBOX_CONTROL) & AOP_MBOX_FAULTS); ++} ++ ++struct aop_warm_write { void __iomem *target; u32 value; }; ++static void aop_warm_store(void *data) ++{ ++ struct aop_warm_write *w = data; ++ ++ writel(w->value, w->target); ++ readl(w->target); ++} ++ ++static int aop_wr(struct apple_aop *aop, u32 offset, u32 value) ++{ ++ struct apple_pmgr_observation states[AOP_DOMAINS]; ++ struct aop_warm_write w = { aop->warm_wrapper + offset, value }; ++ ++ if ((aop_rd(aop, AOP_INBOX_CONTROL) | aop_rd(aop, AOP_OUTBOX_CONTROL)) & ++ AOP_MBOX_FAULTS) ++ return -EIO; ++ dev_info(aop->dev, "COLD-W step=%s bank=wrapper offset=%04x before=%08x value=%08x\n", ++ aop->experiment_step ?: "warm-stop", offset, readl(w.target), value); ++ { ++ int ret = apple_pmgr_observe(aop->domains, AOP_DOMAINS, states, aop_warm_store, &w); ++ ++ dev_info(aop->dev, "COLD-R offset=%04x after=%08x\n", offset, readl(w.target)); ++ return ret; ++ } ++} ++ ++static int a9_aop_cold_checkpoint_noflush(struct apple_aop *aop, const char *step) ++{ ++ if (!a9_aop_ramoops_ready()) ++ return -ENODEV; ++ dev_info(aop->dev, "COLD checkpoint=%s flush=skipped\n", step); ++ return 0; ++} ++ ++static int a9_aop_cold_checkpoint(struct apple_aop *aop, const char *step) ++{ ++ int ret = a9_aop_cold_checkpoint_noflush(aop, step); ++ ++ if (ret) ++ return ret; ++ if (!pr_flush(1000, false)) ++ return -ETIMEDOUT; ++ mb(); ++ return 0; ++} ++ ++#include "aop-warm-spi0.c.h" ++static int apple_aop_send(struct apple_aop_ep *ep, u8 opcode, u64 payload); ++ ++/* No cmdline override for untested states: default is the release6 policy. */ ++static atomic_t aop_warm_attempted = ATOMIC_INIT(0); ++ ++static int apple_aop_warm_stop(struct apple_aop *aop) ++{ ++ unsigned int fifo, i; ++ u32 offset, val; ++ int ret; ++ ++ ret = aop_wr(aop, 0x4020, aop_rd(aop, 0x4020) & ~BIT(0)); ++ if (!ret) ++ ret = aop_wr(aop, 0x4000, 0x1111); ++ if (!ret) ++ ret = aop_wr(aop, 0xc00, 0x1111); ++ if (!ret) ++ ret = aop_wr(aop, 0x44, aop_rd(aop, 0x44) & ~BIT(4)); ++ if (ret) ++ return ret; ++ writel(0, aop->control); ++ msleep(10); ++ if (!a9_aop_force_stopped(aop)) ++ return -EIO; ++ aop->force_halted = true; ++ for (fifo = 0; fifo < 2; fifo++) { ++ offset = fifo ? 0x4020 : 0x4008; ++ for (i = 0; i < 64; i++) { ++ val = aop_rd(aop, offset); ++ if (!a9_aop_force_stopped(aop) || (val & AOP_MBOX_FAULTS)) ++ return -EIO; ++ if (val & BIT(17)) ++ break; ++ readq(aop->warm_wrapper + (fifo ? 0x4038 : 0x4018)); ++ } ++ if (!(aop_rd(aop, offset) & BIT(17))) ++ return -ETIMEDOUT; ++ } ++ aop->force_drained = true; ++ return 0; ++} ++ ++static void apple_aop_warm_put_node(void *node) ++{ ++ of_node_put(node); ++} ++ ++static void apple_aop_warm_unmap(void *p) ++{ ++ memunmap(p); ++} ++ ++static int apple_aop_warm_prepare(struct apple_aop *aop) ++{ ++ struct device *dev = aop->dev; ++ struct device_node *np = dev->of_node, *mem; ++ struct apple_pmgr_observation states[AOP_DOMAINS]; ++ struct a9_sl_header header; ++ struct resource res; ++ char *text; ++ u32 auto_boot, outbox; ++ unsigned int i; ++ int ret; ++ ++ if (!of_property_read_bool(aop->dev->of_node, "apple,certified-reload") || !aop->hw->sram_window) ++ return 0; ++ /* Missing the new DT is not permission to run a retained instance. */ ++ mem = of_parse_phandle(np, "memory-region", 0); ++ if (!mem) ++ return dev_err_probe(dev, -EINVAL, "warm reload requires the matching J71t DT\n"); ++ ret = of_address_to_resource(mem, 0, &res); ++ of_node_put(mem); ++ if (ret || resource_size(&res) != A9_HO_PAGE || res.start != 0x8583fc000ULL || ++ aop->akf_base != A9_SPI0_WRAPPER_PA) ++ return -EINVAL; ++ aop->shutdown_log = memremap(res.start, A9_HO_PAGE, MEMREMAP_WB); ++ if (!aop->shutdown_log) ++ return -ENOMEM; ++ ret = devm_add_action_or_reset(dev, apple_aop_warm_unmap, aop->shutdown_log); ++ if (ret) ++ return ret; ++ /* Keep release6's transcript location and validation ABI. */ ++ aop->shutdown_log = (void *)aop->shutdown_log + A9_HO_LOG_OFFSET; ++ aop->shutdown_text = (char *)(aop->shutdown_log + 1); ++ memcpy(&header, aop->shutdown_log, sizeof(header)); ++ text = devm_kmemdup(dev, aop->shutdown_text, A9_SL_CAPACITY, GFP_KERNEL); ++ if (!text) ++ return -ENOMEM; ++ if (header.magic == A9_SL_MAGIC) { ++ aop->spi0.prev.state = A9_SPI0_PREV_INVALID; ++ if (a9_sl_valid(&header, text)) { ++ aop->spi0.prev.state = A9_SPI0_PREV_VALID; ++ a9_spi0_prev_parse(&aop->spi0.prev, text, header.length); ++ for (i = 0; i < header.length;) { ++ char *next = memchr(text + i, '\n', header.length - i); ++ unsigned int n = next ? next - text - i : header.length - i; ++ ++ dev_info(dev, "AOP-PREV %.*s\n", n, text + i); ++ i += n + 1; ++ } ++ } ++ } ++ aop->warm_wrapper = devm_ioremap(dev, aop->akf_base, 0x1c000); ++ if (!aop->warm_wrapper) ++ return -ENOMEM; ++ for (i = 0; i < AOP_DOMAINS; i++) { ++ aop->domains[i] = of_parse_phandle(np, "apple,observe-domains", i); ++ if (!aop->domains[i]) ++ return -EINVAL; ++ ret = devm_add_action_or_reset(dev, apple_aop_warm_put_node, aop->domains[i]); ++ if (ret) ++ return ret; ++ } ++ aop->spi0_pmgr = of_parse_phandle(np, "apple,spi0-domain", 0); ++ if (!aop->spi0_pmgr) ++ return -EINVAL; ++ ret = devm_add_action_or_reset(dev, apple_aop_warm_put_node, aop->spi0_pmgr); ++ if (ret) ++ return ret; ++ /* Check all domains before reading the wrapper; no power transitions. */ ++ ret = apple_pmgr_observe(aop->domains, AOP_DOMAINS, states, a9_aop_spi0_pmgr_on, &(bool){false}); ++ if (ret) ++ return dev_err_probe(dev, ret, "AOP domain guard refused\n"); ++ auto_boot = readl(aop->control); ++ outbox = aop_rd(aop, AOP_OUTBOX_CONTROL); ++ ret = a9_aop_spi0_policy(aop, auto_boot, outbox); ++ if (ret) ++ return ret; /* Before first halt, mask, SRAM or SPI0 write. */ ++ /* Orchard's old halt leaves +0x4020 enabled. On a known kexec, ++ * that alone cannot certify the cold wrapper baseline. Release6's ++ * wrapper discriminator was tied to its own outgoing stop routine. ++ */ ++ if (of_property_read_bool(of_chosen, "linux,booted-from-kexec") && ++ a9_spi0_decide(auto_boot, outbox & BIT(0), &aop->spi0.prev) != A9_SPI0_PREV_HALTED) { ++ dev_info(dev, "SPI0 policy decision=refuse reason=uncertified-kexec auto_boot=%u out4020=%08x: block not touched\n", ++ auto_boot, outbox); ++ return -EPERM; ++ } ++ aop->warm = true; ++ aop->spi0.reason = a9_spi0_decide(auto_boot, outbox & BIT(0), &aop->spi0.prev); ++ if (!a9_spi0_reason_runs(aop->spi0.reason)) { ++ /* Original cold path: no halt, drain, reset, or SPI0 access. */ ++ dev_info(dev, "SPI0 reset decision=skip reason=%s: block not touched\n", ++ a9_spi0_reason_name(aop->spi0.reason)); ++ return 0; ++ } ++ if (atomic_xchg(&aop_warm_attempted, 1)) ++ return dev_err_probe(dev, -EPERM, "refused-second-experiment-attempt\n"); ++ if (!a9_aop_ramoops_ready()) ++ return dev_err_probe(dev, -ENODEV, "warm reload requires ramoops console\n"); ++ aop->spi0.base = devm_ioremap(dev, A9_SPI0_PA, A9_SPI0_SIZE); ++ if (!aop->spi0.base) ++ return -ENOMEM; ++ /* Consume certification before the first control write. Reloading the ++ * module after a failure must not reuse it as a second attempt. ++ */ ++ aop->shutdown_log->magic = 0; ++ wmb(); ++ ret = apple_aop_warm_stop(aop); ++ if (!ret) ++ ret = a9_aop_spi0_prereset(aop, auto_boot, outbox); ++ if (ret) ++ dev_err(dev, "warm reload refused errno=%d; no copy/RUN, recover with DFU\n", ret); ++ return ret; ++} ++ ++static int apple_aop_warm_release(struct apple_aop *aop) ++{ ++ int ret; ++ ++ ret = a9_aop_spi0_recheck(aop); ++ if (!ret) ++ ret = a9_aop_spi0_trace(aop, "after-window"); ++ if (!ret) ++ ret = a9_aop_intc_ack(aop); ++ if (!ret) ++ ret = a9_aop_intc_reset(aop); ++ if (!ret) ++ ret = a9_aop_cold_checkpoint(aop, "before-run"); ++ if (!ret) ++ ret = a9_aop_spi0_gate(aop, "pre-run-final", "FINAL", true); ++ return ret; ++} ++ ++static void apple_aop_warm_hash(struct apple_aop *aop, u8 *digest) ++{ ++ struct sha256_ctx ctx; ++ unsigned int i; ++ ++ sha256_init(&ctx); ++ dma_rmb(); ++ if (aop->ext) ++ sha256_update(&ctx, aop->ext, aop->ext_size); ++ for (i = 0; i < AFK_ENDPOINT_COUNT; i++) ++ if (aop->eps[i].buf) ++ sha256_update(&ctx, aop->eps[i].buf, aop->eps[i].buf_size); ++ sha256_final(&ctx, digest); ++} ++ ++static __printf(2, 3) void apple_aop_warm_log(struct apple_aop *aop, const char *fmt, ...) ++{ ++ struct a9_sl_header *h = aop->shutdown_log; ++ va_list args; ++ char line[512]; ++ size_t n; ++ ++ va_start(args, fmt); ++ n = vscnprintf(line, sizeof(line), fmt, args); ++ va_end(args); ++ dev_info(aop->dev, "%s", line); ++ if (n > h->capacity - h->length) { ++ h->truncated++; ++ return; ++ } ++ memcpy(aop->shutdown_text + h->length, line, n); ++ wmb(); ++ h->checksum = a9_sl_hash(h->checksum, line, n); ++ h->length += n; ++ h->lines++; ++ wmb(); ++} ++ ++static void apple_aop_warm_shutdown(struct apple_aop *aop) ++{ ++ struct a9_sl_header *h = aop->shutdown_log; ++ u8 before[SHA256_DIGEST_SIZE], after[SHA256_DIGEST_SIZE]; ++ unsigned long deadline = jiffies + msecs_to_jiffies(10000), start; ++ unsigned int i, acked = 0; ++ int ret = -EIO; ++ ++ memset(h, 0, sizeof(*h)); ++ h->version = A9_SL_VERSION; ++ h->capacity = A9_SL_CAPACITY; ++ h->checksum = A9_SL_HASH_SEED; ++ h->mode = 1; ++ h->begin_ns = ktime_get_boottime_ns(); ++ wmb(); ++ h->magic = A9_SL_MAGIC; ++ apple_aop_warm_log(aop, "SHUTDOWN begin mode=quiesce hello=%d phase=stopping iop_crashed=%d\n", ++ aop->hello, aop->iop_crashed); ++ if (!aop->hello || aop->iop_crashed || aop->shutdown_error) ++ goto done; ++ /* Rates-zero has stopped reports. Stop one AFK at a time, max 2s ++ * per endpoint and 10s in total, including send time. Never force halt ++ * after a failed ACK. Keep every grant allocated for the entire hook. ++ */ ++ for (i = 0; i < AFK_ENDPOINT_COUNT; i++) { ++ struct apple_aop_ep *ep = &aop->eps[i]; ++ unsigned long limit; ++ ++ if (!ep->initialized) ++ continue; ++ if (time_after_eq(jiffies, deadline)) { ++ ret = -ETIMEDOUT; ++ goto done; ++ } ++ reinit_completion(&ep->stopped); ++ start = jiffies; ++ ret = apple_aop_send(ep, AFK_OPC_SHUTDOWN, 0); ++ if (ret) ++ goto done; ++ limit = time_before(jiffies, deadline) ? min_t(unsigned long, ++ deadline - jiffies, msecs_to_jiffies(2000)) : 0; ++ if (!limit || !wait_for_completion_timeout(&ep->stopped, limit)) { ++ ret = -ETIMEDOUT; ++ goto done; ++ } ++ acked++; ++ apple_aop_warm_log(aop, "QUIESCE step=afk-ack ep=0x%x state=stopped ms=%u\n", ++ ep->index, jiffies_to_msecs(jiffies - start)); ++ } ++ /* release6: complete every AFK stop before disabling endpoint routing. */ ++ if (acked != 17) { ++ ret = -ENOTCONN; ++ goto done; ++ } ++ for (i = 0; i < AFK_ENDPOINT_COUNT; i++) { ++ struct apple_aop_ep *ep = &aop->eps[i]; ++ ++ if (!ep->initialized) ++ continue; ++ /* MGMT STARTEP bit 0 disables routing, no separate stop ACK. */ ++ ret = apple_rtkit_send_message(aop->rtk, 0, ++ (5ULL << 52) | ((u64)ep->index << 32) | 1, NULL, false); ++ if (ret) ++ goto done; ++ } ++ if (acked != 17) { ++ ret = -ENOTCONN; ++ goto done; /* Never certify an incompletely initialized instance. */ ++ } ++ ret = apple_rtkit_shutdown_preserve(aop->rtk); ++ if (ret) ++ goto done; ++ msleep(20); ++ apple_aop_warm_hash(aop, before); ++ msleep(20); ++ apple_aop_warm_hash(aop, after); ++ ret = memcmp(before, after, sizeof(before)) ? -EBUSY : 0; ++ apple_aop_warm_log(aop, "QUIESCE step=ram-stable window_ms=20 status=%d\n", ret); ++ if (ret || aop->iop_crashed) ++ goto done; ++ ret = apple_aop_warm_stop(aop); ++done: ++ apple_aop_warm_log(aop, "QUIESCE step=complete status=%d quiesced=%d halted=%d drained=%d steps=0x%x afk_acks=%u iop_crashed=%d\n", ++ ret, !ret, aop->force_halted, aop->force_drained, ++ !ret ? 0x1bff : 0, acked, aop->iop_crashed); ++ if (ret) ++ dev_err(aop->dev, "quiesce failed; next AOP must refuse, recover with DFU\n"); ++} ++ + /* Firmware loading */ + + struct apple_aop_bootarg { +@@ -232,11 +692,17 @@ struct apple_aop_bootarg { + bool found; + }; + ++static void apple_aop_add_bootarg(struct apple_aop_bootarg *args, unsigned int *count, ++ u32 key, const void *value, u32 size) ++{ ++ args[(*count)++] = (struct apple_aop_bootarg){ .key = key, .value = value, .size = size }; ++} ++ + static int apple_aop_patch_bootargs(struct apple_aop *aop, u8 *image, size_t size, +- struct apple_aop_bootarg *args, int count) ++ struct apple_aop_bootarg *args, unsigned int count) + { + size_t offset, end, pos; +- int i; ++ unsigned int i; + + offset = get_unaligned_le32(image + APPLE_AOP_IMAGE_BOOTARGS_OFFSET); + end = get_unaligned_le32(image + APPLE_AOP_IMAGE_BOOTARGS_SIZE); +@@ -269,13 +735,29 @@ static int apple_aop_patch_bootargs(struct apple_aop *aop, u8 *image, size_t siz + return 0; + } + ++/* S8000: point the AOP's window at its SRAM; the firmware runs from there. */ ++static void apple_aop_map_sram(struct apple_aop *aop) ++{ ++ writel(0, aop->akf + APPLE_AOP_REMAP_IOVA_LO); ++ writel(0, aop->akf + APPLE_AOP_REMAP_IOVA_HI); ++ writel(aop->sram_size & ~0xfffULL, aop->akf + APPLE_AOP_REMAP_SIZE); ++ writel(0, aop->akf + APPLE_AOP_REMAP_RESERVED); ++ writel(lower_32_bits(aop->sram_base), aop->akf + APPLE_AOP_REMAP_PA_LO); ++ writel(upper_32_bits(aop->sram_base), aop->akf + APPLE_AOP_REMAP_PA_HI); ++ writel(1, aop->akf + APPLE_AOP_REMAP_ENABLE); ++} ++ + static int apple_aop_load(struct apple_aop *aop, u64 ecid) + { ++ struct apple_aop_bootarg args[APPLE_AOP_MAX_BOOTARGS]; + const struct firmware *main_fw, *ext_fw; + const char *main_name, *ext_name; + struct device_node *np = aop->dev->of_node; +- u8 prng[32], nonce[8]; +- __le64 ecid_le = cpu_to_le64(ecid), ext_le; ++ u8 prng[32], nonce[8], guard[4], ncal = 0; ++ __le64 ecid_le = cpu_to_le64(ecid), ext_le, wrapper_le; ++ __le32 target_le, chip_le, revision_le; ++ unsigned int count = 0; ++ size_t offset; + u8 *image; + int ret; + +@@ -316,28 +798,58 @@ static int apple_aop_load(struct apple_aop *aop, u64 ecid) + } + + get_random_bytes(prng, sizeof(prng)); +- get_random_bytes(nonce, sizeof(nonce)); +- { +- struct apple_aop_bootarg args[] = { +- { APPLE_AOP_BOOTARG_PRNG, prng, sizeof(prng) }, +- { APPLE_AOP_BOOTARG_ECID, &ecid_le, sizeof(ecid_le) }, +- { APPLE_AOP_BOOTARG_NONC, nonce, sizeof(nonce) }, +- { APPLE_AOP_BOOTARG_EC0P, &ext_le, sizeof(ext_le) }, +- }; +- +- ret = apple_aop_patch_bootargs(aop, image, main_fw->size, args, +- ARRAY_SIZE(args)); ++ apple_aop_add_bootarg(args, &count, APPLE_AOP_BOOTARG_PRNG, &prng, sizeof(prng)); ++ apple_aop_add_bootarg(args, &count, APPLE_AOP_BOOTARG_ECID, &ecid_le, sizeof(ecid_le)); ++ apple_aop_add_bootarg(args, &count, APPLE_AOP_BOOTARG_EC0P, &ext_le, sizeof(ext_le)); ++ if (aop->hw->nonce) { ++ get_random_bytes(nonce, sizeof(nonce)); ++ apple_aop_add_bootarg(args, &count, APPLE_AOP_BOOTARG_NONC, &nonce, sizeof(nonce)); ++ } ++ if (aop->hw->soc_bootargs) { ++ /* A random stack guard with one zero byte */ ++ get_random_bytes(guard, sizeof(guard)); ++ guard[get_random_u32_below(sizeof(guard))] = 0; ++ target_le = cpu_to_le32(aop->target); ++ chip_le = cpu_to_le32(aop->chip_id); ++ revision_le = cpu_to_le32(aop->chip_revision); ++ wrapper_le = cpu_to_le64(aop->akf_base); ++ apple_aop_add_bootarg(args, &count, APPLE_AOP_BOOTARG_AOPT, ++ &target_le, sizeof(target_le)); ++ apple_aop_add_bootarg(args, &count, APPLE_AOP_BOOTARG_NCAL, &ncal, sizeof(ncal)); ++ apple_aop_add_bootarg(args, &count, APPLE_AOP_BOOTARG_WRAD, ++ &wrapper_le, sizeof(wrapper_le)); ++ apple_aop_add_bootarg(args, &count, APPLE_AOP_BOOTARG_SOC, ++ &chip_le, sizeof(chip_le)); ++ apple_aop_add_bootarg(args, &count, APPLE_AOP_BOOTARG_SOCR, ++ &revision_le, sizeof(revision_le)); ++ apple_aop_add_bootarg(args, &count, APPLE_AOP_BOOTARG_STKG, &guard, sizeof(guard)); + } ++ ++ ret = apple_aop_patch_bootargs(aop, image, main_fw->size, args, count); + if (ret) { + dev_err(aop->dev, "unexpected boot arguments in %s: %d\n", main_name, ret); + } else { +- memcpy_toio(aop->sram, image, main_fw->size); +- memset_io(aop->sram + main_fw->size, 0, aop->sram_size - main_fw->size); ++ /* The SRAM takes 32-bit accesses, as Apple's loader makes them. */ ++ if (aop->spi0.ran) { ++ ret = a9_aop_spi0_gate(aop, "pre-copy-final", "PRECOPY", false); ++ if (!ret) ++ ret = a9_aop_copy_verify_wake10(aop, image, main_fw->size); ++ if (!ret) ++ ret = a9_aop_spi0_gate(aop, "after-copy", "AFTERCOPY", true); ++ if (ret) ++ goto out_image; ++ } else { ++ __iowrite32_copy(aop->sram, image, main_fw->size / 4); ++ } ++ for (offset = main_fw->size; offset + 4 <= aop->sram_size; offset += 4) ++ writel_relaxed(0, aop->sram + offset); + } + ++out_image: + kfree_sensitive(image); + memzero_explicit(prng, sizeof(prng)); + memzero_explicit(nonce, sizeof(nonce)); ++ memzero_explicit(guard, sizeof(guard)); + memzero_explicit(&ecid_le, sizeof(ecid_le)); + out_ext: + release_firmware(ext_fw); +@@ -448,7 +960,8 @@ static void apple_aop_announce(struct apple_aop_ep *ep, u32 channel, const void + const struct epic_service_announce *announce = data; + struct apple_aop_register_work *rw; + +- if (len < sizeof(*announce)) ++ /* Only the name and channel are used; the S8000's announcements are shorter. */ ++ if (len < offsetofend(struct epic_service_announce, channel)) + return; + + rw = kzalloc_obj(*rw); +@@ -475,6 +988,8 @@ static void apple_aop_report(struct apple_aop_ep *ep, u32 channel, u16 subtype, + struct apple_aop_listener *listener; + bool handled = false; + ++ if (ep->index == 0x21 && subtype == 0xc2) ++ aop->accel_reports++; + mutex_lock(&aop->listeners_lock); + list_for_each_entry(listener, &aop->listeners, list) { + if (listener->endpoint == ep->index && listener->channel == channel) { +@@ -631,6 +1146,7 @@ static void apple_aop_recv_message(void *cookie, u8 endpoint, u64 msg) + + switch (FIELD_GET(AFK_MSG_OPCODE, msg)) { + case AFK_OPC_INIT: ++ ep->initialized = true; + apple_aop_send(ep, AFK_OPC_INIT_ACK, 0); + break; + case AFK_OPC_GET_BUF: +@@ -651,7 +1167,10 @@ static void apple_aop_recv_message(void *cookie, u8 endpoint, u64 msg) + break; + case AFK_OPC_INIT_UNK: + case AFK_OPC_START_ACK: ++ break; + case AFK_OPC_SHUTDOWN_ACK: ++ case AFK_OPC_HIBERNATE_ACK: ++ complete(&ep->stopped); + break; + default: + dev_dbg(aop->dev, "endpoint 0x%02x: message 0x%llx\n", endpoint, msg); +@@ -662,7 +1181,8 @@ static void apple_aop_crashed(void *cookie, const void *crashlog, size_t crashlo + { + struct apple_aop *aop = cookie; + +- dev_err(aop->dev, "firmware crashed\n"); ++ aop->iop_crashed = true; ++ dev_err(aop->dev, "firmware crashed iop_crashed=1\n"); + } + + static const struct apple_rtkit_ops apple_aop_rtkit_ops = { +@@ -767,6 +1287,8 @@ int apple_aop_call(const struct apple_aop_service *service, u16 subtype, const v + + if (!ep) + return -EINVAL; ++ if (service->aop->warm && READ_ONCE(service->aop->iop_crashed)) ++ return -EIO; + init_completion(&call.done); + + mutex_lock(&ep->lock); +@@ -797,6 +1319,58 @@ int apple_aop_call(const struct apple_aop_service *service, u16 subtype, const v + } + EXPORT_SYMBOL_GPL(apple_aop_call); + ++int apple_aop_set_property(const struct apple_aop_service *service, u32 key, ++ const void *value, size_t len) ++{ ++ u8 buf[8 + APPLE_AOP_PROPERTY_MAX]; ++ int ret; ++ ++ if (len > APPLE_AOP_PROPERTY_MAX) ++ return -EINVAL; ++ put_unaligned_le32(0, buf); ++ put_unaligned_le32(key, buf + 4); ++ memcpy(buf + 8, value, len); ++ ++ ret = apple_aop_call(service, APPLE_AOP_SET_PROPERTY, buf, 8 + len, NULL, 0); ++ return ret > 0 ? -EIO : ret; ++} ++EXPORT_SYMBOL_GPL(apple_aop_set_property); ++ ++int apple_aop_calibrate(const struct apple_aop_service *service, struct device *dev, ++ const struct apple_aop_calibration *cal, unsigned int count) ++{ ++ unsigned long needed = 0, applied = 0; ++ __le32 state; ++ unsigned int i; ++ int len, ret; ++ ++ for (i = 0; i < count; i++) { ++ const void *blob; ++ ++ if (cal[i].group) ++ needed |= BIT(cal[i].group); ++ blob = of_get_property(dev->of_node, cal[i].property, &len); ++ if (!blob) ++ continue; ++ ret = apple_aop_set_property(service, cal[i].key, blob, len); ++ if (ret) { ++ dev_warn(dev, "%s not applied: %d\n", cal[i].property, ret); ++ continue; ++ } ++ if (cal[i].group) ++ applied |= BIT(cal[i].group); ++ } ++ ++ if (needed && applied == needed) { ++ state = cpu_to_le32(APPLE_AOP_CALIBRATION_COMPLETE); ++ } else { ++ dev_warn(dev, "factory calibration incomplete\n"); ++ state = cpu_to_le32(APPLE_AOP_CALIBRATION_PARTIAL); ++ } ++ return apple_aop_set_property(service, APPLE_AOP_PROP_CALIBRATION, &state, sizeof(state)); ++} ++EXPORT_SYMBOL_GPL(apple_aop_calibrate); ++ + /* Driver */ + + static void apple_aop_free_buffers(struct apple_aop *aop) +@@ -825,27 +1399,35 @@ static int apple_aop_probe(struct platform_device *pdev) + if (!aop) + return -ENOMEM; + aop->dev = dev; ++ aop->hw = of_device_get_match_data(dev); + mutex_init(&aop->listeners_lock); + INIT_LIST_HEAD(&aop->listeners); + mutex_init(&aop->children_lock); + INIT_LIST_HEAD(&aop->children); + +- aop->akf = devm_platform_ioremap_resource_byname(pdev, "akf"); ++ res = platform_get_resource_byname(pdev, IORESOURCE_MEM, "akf"); ++ aop->akf = devm_ioremap_resource(dev, res); + if (IS_ERR(aop->akf)) + return PTR_ERR(aop->akf); ++ aop->akf_base = res->start; + aop->control = devm_platform_ioremap_resource_byname(pdev, "control"); + if (IS_ERR(aop->control)) + return PTR_ERR(aop->control); + aop->sram = devm_platform_get_and_ioremap_resource(pdev, 2, &res); + if (IS_ERR(aop->sram)) + return PTR_ERR(aop->sram); ++ aop->sram_base = res->start; + aop->sram_size = resource_size(res); + + ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(44)); + if (ret) + return ret; + +- if (apple_aop_is_started(aop)) ++ ret = apple_aop_warm_prepare(aop); ++ if (ret) ++ return ret; ++ ++ if (!aop->spi0.ran && apple_aop_is_started(aop)) + return dev_err_probe(dev, -EBUSY, + "already started; the AOP starts once per boot\n"); + +@@ -853,6 +1435,17 @@ static int apple_aop_probe(struct platform_device *pdev) + if (ret) + return dev_err_probe(dev, ret, "no apple,ecid from the bootloader\n"); + ++ if (aop->hw->soc_bootargs) { ++ ret = of_property_read_u32(np, "apple,aop-target", &aop->target); ++ if (!ret) ++ ret = of_property_read_u32(np, "apple,chip-id", &aop->chip_id); ++ if (!ret) ++ ret = of_property_read_u32(np, "apple,chip-revision", &aop->chip_revision); ++ if (ret) ++ return dev_err_probe(dev, ret, ++ "no AOP target or chip ID from the bootloader\n"); ++ } ++ + aop->eps = devm_kcalloc(dev, AFK_ENDPOINT_COUNT, sizeof(*aop->eps), GFP_KERNEL); + if (!aop->eps) + return -ENOMEM; +@@ -860,6 +1453,7 @@ static int apple_aop_probe(struct platform_device *pdev) + aop->eps[i].aop = aop; + aop->eps[i].index = AFK_ENDPOINT_START + i; + mutex_init(&aop->eps[i].lock); ++ init_completion(&aop->eps[i].stopped); + } + + ret = apple_aop_load(aop, ecid); +@@ -867,6 +1461,9 @@ static int apple_aop_probe(struct platform_device *pdev) + if (ret) + goto err_free; + ++ if (aop->hw->sram_window) ++ apple_aop_map_sram(aop); ++ + aop->rtk = apple_rtkit_init(dev, aop, "mbox", 0, &apple_aop_rtkit_ops); + if (IS_ERR(aop->rtk)) { + ret = dev_err_probe(dev, PTR_ERR(aop->rtk), "failed to init RTKit\n"); +@@ -884,10 +1481,24 @@ static int apple_aop_probe(struct platform_device *pdev) + prop = of_find_property(np, "apple,ecid", NULL); + if (prop) + of_remove_property(np, prop); +- writel(readl(aop->akf + APPLE_AOP_CPU_CONTROL) | APPLE_AOP_CPU_CONTROL_RUN, +- aop->akf + APPLE_AOP_CPU_CONTROL); ++ if (aop->spi0.ran) { ++ ret = apple_aop_warm_release(aop); ++ if (ret) ++ goto err_halt; ++ } ++ if (aop->spi0.ran) { ++ aop->experiment_step = "warm-run"; ++ ret = aop_wr(aop, APPLE_AOP_CPU_CONTROL, ++ readl(aop->akf + APPLE_AOP_CPU_CONTROL) | APPLE_AOP_CPU_CONTROL_RUN); ++ if (ret) ++ goto err_halt; ++ } else { ++ writel(readl(aop->akf + APPLE_AOP_CPU_CONTROL) | APPLE_AOP_CPU_CONTROL_RUN, ++ aop->akf + APPLE_AOP_CPU_CONTROL); ++ } + writel(1, aop->control + APPLE_AOP_CONTROL_AUTO_BOOT); + ++ dev_info(dev, "AOP released warm=%d\n", aop->spi0.ran); + ret = apple_rtkit_wake(aop->rtk); + if (ret) { + dev_err(dev, "firmware did not boot: %d\n", ret); +@@ -900,6 +1511,8 @@ static int apple_aop_probe(struct platform_device *pdev) + dev_warn(dev, "failed to start endpoint 0x%02x: %d\n", i, ret); + } + ++ aop->hello = true; ++ dev_info(dev, "AOP start complete warm=%d iop_crashed=0\n", aop->spi0.ran); + platform_set_drvdata(pdev, aop); + return 0; + +@@ -929,27 +1542,81 @@ static void apple_aop_shutdown(struct platform_device *pdev) + mutex_lock(&aop->children_lock); + aop->removing = true; + list_for_each_entry_safe(child, tmp, &aop->children, list) { ++ ++ if (aop->warm) { ++ const struct apple_aop_service *service = dev_get_platdata(&child->pdev->dev); ++ static const u32 keys[] = { 0x16, 0x5c, 0 }; ++ ++ /* release6 sets the three accelerometer rates only. */ ++ if (strcmp(service->name, "accel")) ++ continue; ++ __le32 zero = 0; ++ unsigned int i; ++ int ret; ++ ++ for (i = 0; i < ARRAY_SIZE(keys); i++) { ++ ret = apple_aop_set_property(service, keys[i], &zero, sizeof(zero)); ++ if (ret) { ++ aop->shutdown_error = ret; ++ break; ++ } ++ } ++ } ++ /* Shutdown retains listeners, children and DMA grants, as release6 does. ++ * Unregister is remove-time work and can wait in IIO/driver core. ++ */ ++ if (aop->warm) ++ continue; + platform_device_unregister(child->pdev); + list_del(&child->list); + kfree(child); + } + mutex_unlock(&aop->children_lock); + ++ if (aop->warm) { ++ apple_aop_warm_shutdown(aop); ++ return; ++ } + if (!apple_aop_halt(aop)) + dev_err(aop->dev, "failed to halt\n"); + } + ++static const struct apple_aop_hw apple_aop_s8000_hw = { ++ .sram_window = true, ++ .soc_bootargs = true, ++}; ++ ++static const struct apple_aop_hw apple_aop_t8010_hw = { ++ .nonce = true, ++}; ++ + static const struct of_device_id apple_aop_of_match[] = { +- { .compatible = "apple,t8010-aop" }, ++ { .compatible = "apple,s8000-aop", .data = &apple_aop_s8000_hw }, ++ { .compatible = "apple,t8010-aop", .data = &apple_aop_t8010_hw }, + {} + }; + MODULE_DEVICE_TABLE(of, apple_aop_of_match); + ++static ssize_t warm_state_show(struct device *dev, struct device_attribute *attr, char *buf) ++{ ++ struct apple_aop *aop = dev_get_drvdata(dev); ++ ++ if (!aop) ++ return sysfs_emit(buf, "owned=0\n"); ++ return sysfs_emit(buf, "owned=%u reload=%u iop_crashed=%u accel_reports=%llu\n", ++ aop->hello, aop->spi0.ran, READ_ONCE(aop->iop_crashed), ++ READ_ONCE(aop->accel_reports)); ++} ++static DEVICE_ATTR_RO(warm_state); ++static struct attribute *apple_aop_attrs[] = { &dev_attr_warm_state.attr, NULL }; ++ATTRIBUTE_GROUPS(apple_aop); ++ + static struct platform_driver apple_aop_driver = { + .driver = { + .name = "apple-aop", + .of_match_table = apple_aop_of_match, + .suppress_bind_attrs = true, ++ .dev_groups = apple_aop_groups, + }, + .probe = apple_aop_probe, + .shutdown = apple_aop_shutdown, +@@ -957,5 +1624,5 @@ static struct platform_driver apple_aop_driver = { + module_platform_driver(apple_aop_driver); + + MODULE_AUTHOR("Dalton Alexandre "); +-MODULE_DESCRIPTION("Apple T8010 Always-On Processor driver"); ++MODULE_DESCRIPTION("Apple S8000 and T8010 Always-On Processor driver"); + MODULE_LICENSE("Dual MIT/GPL"); +diff --git a/drivers/soc/apple/rtkit.c b/drivers/soc/apple/rtkit.c +index 725ab3fa7..d7928621a 100644 +--- a/drivers/soc/apple/rtkit.c ++++ b/drivers/soc/apple/rtkit.c +@@ -4,6 +4,8 @@ + * Copyright (C) The Asahi Linux Contributors + */ + ++#include ++#include + #include "rtkit-internal.h" + + enum { +@@ -867,6 +869,54 @@ int apple_rtkit_boot(struct apple_rtkit *rtk) + } + EXPORT_SYMBOL_GPL(apple_rtkit_boot); + ++/* Unlike shutdown(), retain all grants until the caller halts the CPU. ++ * Reinit frees DMA buffers and is unsafe before that point on S8000. ++ */ ++static void apple_rtkit_grants_hash(struct apple_rtkit *rtk, u8 *digest) ++{ ++ struct apple_rtkit_shmem *buffers[] = { ++ &rtk->ioreport_buffer, &rtk->crashlog_buffer, ++ &rtk->oslog_buffer, &rtk->syslog_buffer, ++ }; ++ struct sha256_ctx ctx; ++ unsigned int i; ++ ++ sha256_init(&ctx); ++ dma_rmb(); ++ for (i = 0; i < ARRAY_SIZE(buffers); i++) ++ if (buffers[i]->buffer) ++ sha256_update(&ctx, buffers[i]->buffer, buffers[i]->size); ++ sha256_final(&ctx, digest); ++} ++ ++int apple_rtkit_shutdown_preserve(struct apple_rtkit *rtk) ++{ ++ u8 before[SHA256_DIGEST_SIZE], after[SHA256_DIGEST_SIZE]; ++ int ret; ++ ++ if (rtk->crashed) ++ return -EIO; ++ ret = apple_rtkit_set_ap_power_state(rtk, APPLE_RTKIT_PWR_STATE_QUIESCED); ++ if (ret) ++ return ret; ++ dev_info(rtk->dev, "QUIESCE step=ap-ack state=0x10\n"); ++ ret = apple_rtkit_set_iop_power_state(rtk, APPLE_RTKIT_PWR_STATE_IDLE); ++ if (ret) ++ return ret; ++ dev_info(rtk->dev, "QUIESCE step=iop-ack state=0x201\n"); ++ msleep(20); ++ apple_rtkit_grants_hash(rtk, before); ++ msleep(20); ++ apple_rtkit_grants_hash(rtk, after); ++ if (rtk->crashed || memcmp(before, after, sizeof(before))) ++ return -EBUSY; ++ /* No more callback can race the caller's halt/drain. Do not reinit. */ ++ apple_mbox_stop(rtk->mbox); ++ flush_workqueue(rtk->wq); ++ return 0; ++} ++EXPORT_SYMBOL_GPL(apple_rtkit_shutdown_preserve); ++ + int apple_rtkit_shutdown(struct apple_rtkit *rtk) + { + int ret; +diff --git a/include/linux/soc/apple/aop.h b/include/linux/soc/apple/aop.h +index 8c8bc71b6..b5188a0b1 100644 +--- a/include/linux/soc/apple/aop.h ++++ b/include/linux/soc/apple/aop.h +@@ -7,6 +7,8 @@ + + #include + ++struct device; ++ + struct apple_aop; + + /* +@@ -40,4 +42,30 @@ void apple_aop_unlisten(const struct apple_aop_service *service); + int apple_aop_call(const struct apple_aop_service *service, u16 subtype, + const void *data, size_t len, void *reply, size_t reply_len); + ++/* The EPIC notification subtype that sets a service property */ ++#define APPLE_AOP_SET_PROPERTY 0x4 ++ ++/* Set property @key of a service to @len (at most 64) bytes at @value. */ ++int apple_aop_set_property(const struct apple_aop_service *service, u32 key, ++ const void *value, size_t len); ++ ++/* ++ * A factory calibration blob of a sensor: the bootloader copies it from the ++ * Apple device tree into the sensor's node as @property, and the firmware ++ * takes it as property @key. The calibration is complete once a blob of each ++ * @group (1 to 31) was applied; group 0 blobs are not needed for that. ++ */ ++struct apple_aop_calibration { ++ const char *property; ++ u32 key; ++ u8 group; ++}; ++ ++/* ++ * Send the factory calibration found in @dev's node, then tell the firmware ++ * whether it is complete. Some firmware (S8000) sends no reports before. ++ */ ++int apple_aop_calibrate(const struct apple_aop_service *service, struct device *dev, ++ const struct apple_aop_calibration *cal, unsigned int count); ++ + #endif /* _LINUX_SOC_APPLE_AOP_H_ */ +diff --git a/include/linux/soc/apple/pmgr-observe.h b/include/linux/soc/apple/pmgr-observe.h +new file mode 100644 +index 000000000..477b7fa22 +--- /dev/null ++++ b/include/linux/soc/apple/pmgr-observe.h +@@ -0,0 +1,27 @@ ++/* SPDX-License-Identifier: GPL-2.0-only */ ++#ifndef _LINUX_SOC_APPLE_PMGR_OBSERVE_H ++#define _LINUX_SOC_APPLE_PMGR_OBSERVE_H ++ ++#include ++ ++struct device_node; ++ ++struct apple_pmgr_observation { ++ u32 raw; ++ int genpd_status; ++}; ++ ++/* ++ * Diagnostic only. Nodes must be in child-before-parent lock order, with no ++ * duplicates (maximum five). Takes no power/runtime-PM reference. read() must ++ * not sleep or acquire genpd locks; it runs with IRQs disabled and all domain ++ * locks held, only if every genpd and hardware actual/target state is ON and ++ * reset/disable/parent-off are clear. Hardware auto-PM remains untouched and is ++ * recorded in raw; locks exclude software transitions, not autonomous gating. ++ * -EHOSTDOWN means read() was skipped. ++ */ ++int apple_pmgr_observe(struct device_node **nodes, unsigned int count, ++ struct apple_pmgr_observation *states, ++ void (*read)(void *), void *data); ++ ++#endif +diff --git a/include/linux/soc/apple/rtkit.h b/include/linux/soc/apple/rtkit.h +index 1f9ebb618..c8e8df255 100644 +--- a/include/linux/soc/apple/rtkit.h ++++ b/include/linux/soc/apple/rtkit.h +@@ -124,6 +124,8 @@ int apple_rtkit_wake(struct apple_rtkit *rtk); + * Shutdown the co-processor + */ + int apple_rtkit_shutdown(struct apple_rtkit *rtk); ++/* AP 0x10 / IOP 0x201, stable system RAM, stop RX; retain every grant. */ ++int apple_rtkit_shutdown_preserve(struct apple_rtkit *rtk); + + /* + * Stop the mailbox of a shut-down co-processor, so that its power domain can diff --git a/kernel/patches/drivers/soc-apple/0016-soc-apple-aop-refuse-pre-sync-sleep-during-firmware-.patch b/kernel/patches/drivers/soc-apple/0016-soc-apple-aop-refuse-pre-sync-sleep-during-firmware-.patch new file mode 100644 index 00000000..8a4e6613 --- /dev/null +++ b/kernel/patches/drivers/soc-apple/0016-soc-apple-aop-refuse-pre-sync-sleep-during-firmware-.patch @@ -0,0 +1,80 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Tue, 10 Nov 2026 16:18:38 +0000 +Subject: [PATCH] soc: apple: aop: refuse pre-sync sleep during firmware boot + +A suspend while the AOP firmware is still starting stops it part way +through its boot, and the sensors behind it do not come back. Register a +suspend precheck notifier that refuses sleep with -EBUSY until the +firmware has reported ready. + +Assisted-by: LLM +--- + drivers/soc/apple/aop.c | 27 +++++++++++++++++++++++++++ + 1 file changed, 27 insertions(+) + +diff --git a/drivers/soc/apple/aop.c b/drivers/soc/apple/aop.c +--- a/drivers/soc/apple/aop.c ++++ b/drivers/soc/apple/aop.c +@@ -32,6 +32,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -226,6 +227,8 @@ + }; + + struct apple_aop { ++ struct notifier_block sleep_precheck; ++ bool sleep_ready; + struct device *dev; + const struct apple_aop_hw *hw; + void __iomem *akf; +@@ -1385,6 +1388,22 @@ + dma_free_coherent(aop->dev, aop->ext_size, aop->ext, aop->ext_dma); + } + ++static int apple_aop_sleep_precheck(struct notifier_block *nb, unsigned long action, void *data) ++{ ++ struct apple_aop *aop = container_of(nb, struct apple_aop, sleep_precheck); ++ ++ if (action == PM_SUSPEND_PRECHECK_COMPLETE) ++ return NOTIFY_DONE; ++ return READ_ONCE(aop->sleep_ready) ? NOTIFY_OK : notifier_from_errno(-EBUSY); ++} ++ ++static void apple_aop_sleep_unregister(void *data) ++{ ++ struct apple_aop *aop = data; ++ ++ unregister_pm_suspend_precheck_notifier(&aop->sleep_precheck); ++} ++ + static int apple_aop_probe(struct platform_device *pdev) + { + struct device *dev = &pdev->dev; +@@ -1399,6 +1418,13 @@ + if (!aop) + return -ENOMEM; + aop->dev = dev; ++ aop->sleep_precheck.notifier_call = apple_aop_sleep_precheck; ++ ret = register_pm_suspend_precheck_notifier(&aop->sleep_precheck); ++ if (ret) ++ return ret; ++ ret = devm_add_action_or_reset(dev, apple_aop_sleep_unregister, aop); ++ if (ret) ++ return ret; + aop->hw = of_device_get_match_data(dev); + mutex_init(&aop->listeners_lock); + INIT_LIST_HEAD(&aop->listeners); +@@ -1514,6 +1540,7 @@ + aop->hello = true; + dev_info(dev, "AOP start complete warm=%d iop_crashed=0\n", aop->spi0.ran); + platform_set_drvdata(pdev, aop); ++ WRITE_ONCE(aop->sleep_ready, true); + return 0; + + err_halt: diff --git a/tests/test_j71t_data.py b/tests/test_j71t_data.py new file mode 100644 index 00000000..c6d83c08 --- /dev/null +++ b/tests/test_j71t_data.py @@ -0,0 +1,31 @@ +"""J71t firmware recipe: the AOP images.""" +import json +from pathlib import Path +import tomllib +import unittest + +HARDWARE = Path(__file__).resolve().parents[1] +DEVICE = HARDWARE / 'devices/j71t/device.toml' + + +class J71tData(unittest.TestCase): + def setUp(self): + device = tomllib.loads(DEVICE.read_text()) + self.recipe = json.loads((DEVICE.parent / device['firmware']['recipe']).read_text()) + + def test_aop_images_come_from_the_board_firmware(self): + aop = self.recipe['inputs']['aop.im4p'] + self.assertEqual(aop['ipsw'], {'zip': 'Firmware/AOP/aopfw-ipad6faop.im4p'}) + self.assertEqual(aop['sha256'], '5a4d8378bb83e175fc589cc1cf978430639d7ffcef07b2d94e9d6a4801124efa') + for name, role, digest in ( + ('aop-ipad6f.bin', 'AOP', 'af5d51a368ceb89dc5c359e7c84e54cb6082e308956be742c17622eb4a6047c8'), + ('aop-ipad6f-ext.bin', 'AOP_ext', '2528d2686ed1262de9910c2d60e45b68137e50aa601a0f8d54ea7d1347b9de22')): + output, = [o for o in self.recipe['outputs'] if o['destination'] == 'lib/firmware/apple/' + name] + self.assertEqual(output['sha256'], digest) + self.assertEqual(output['steps'], [{'step': 'im4p-payload@1', 'input': 'aop.im4p'}, + {'step': 'aop-image@1', 'coprocessor': role}]) + self.assertFalse((HARDWARE / output['destination']).exists()) + + +if __name__ == '__main__': + unittest.main() From 7d41c329d7057dbba8237899b5d34a57a0651617 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= Date: Thu, 12 Nov 2026 09:00:00 +0100 Subject: [PATCH 07/10] m1n1: Resident EL3 PSCI for A7-A9, on by default A7-A9 have no EL2 and boot without any PSCI firmware, so Linux on an iPad 5 could not bring secondary CPUs down and up again, and a kexec had nowhere to park them. CPU hotplug, kexec and a clean poweroff all need a PSCI implementation that stays resident after m1n1 hands over. Carry m1n1 patches that keep a small EL3 PSCI implementation resident: CPU_ON/CPU_OFF with a firmware rendezvous (EL1 WFI/WFE trap to EL3), SYSTEM_OFF, and a guarded SYSTEM_RESET that boots the default entry again. The loader marks retained hardware in the device tree after a PSCI re-entry. The resident PSCI is on by default on these SoCs; the shipped boot framing keeps psci_reset=0, so SYSTEM_RESET stays off unless a loader enables it. Adds boot/m1n1/psci-loader and its tests. Assisted-by: LLM --- boot/m1n1/README.md | 13 + ...resident-EL3-CPU-lifecycle-for-A7-A9.patch | 880 ++++++++++++++++++ ...-the-resident-EL3-PSCI-on-by-default.patch | 84 ++ ...I-WFE-to-EL3-for-a-firmware-CPU-rend.patch | 286 ++++++ ...YSTEM_RESET-and-SYSTEM_OFF-with-a-CP.patch | 497 ++++++++++ ...-default-entry-after-a-PSCI-SYSTEM_R.patch | 298 ++++++ ...etained-hardware-after-PSCI-re-entry.patch | 38 + boot/m1n1/psci-loader | 39 + tests/test_m1n1_psci.py | 81 ++ 9 files changed, 2216 insertions(+) create mode 100644 boot/m1n1/patches/orchard/0015-psci-Add-opt-in-resident-EL3-CPU-lifecycle-for-A7-A9.patch create mode 100644 boot/m1n1/patches/orchard/0016-psci-Turn-the-resident-EL3-PSCI-on-by-default.patch create mode 100644 boot/m1n1/patches/orchard/0017-psci-Trap-EL1-WFI-WFE-to-EL3-for-a-firmware-CPU-rend.patch create mode 100644 boot/m1n1/patches/orchard/0018-psci-Implement-SYSTEM_RESET-and-SYSTEM_OFF-with-a-CP.patch create mode 100644 boot/m1n1/patches/orchard/0019-bootmgr-Boot-the-default-entry-after-a-PSCI-SYSTEM_R.patch create mode 100644 boot/m1n1/patches/orchard/0020-kboot-Mark-retained-hardware-after-PSCI-re-entry.patch create mode 100755 boot/m1n1/psci-loader create mode 100644 tests/test_m1n1_psci.py 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/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() From 190bb139b29337d3dfdb1eed690332d81427bab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= Date: Thu, 12 Nov 2026 09:00:00 +0100 Subject: [PATCH 08/10] j71t: One owner for the PMU and RTC wake interrupts The TriStar charger subscribed to the PMU's RTC event 16 for source identification. An acknowledgement there could take the RTC's wake event, and the polled cover inputs had no driver enabled. Give RTC event 16 to the RTC alone and keep the PMU and RTC event split (DT, RTC and USB patches), so one consumer's acknowledgement cannot take the other's wake event. Enable KEYBOARD_GPIO_POLLED for the polled cover inputs. Assisted-by: LLM --- kernel/configs/apple-16k/config | 2 +- kernel/configs/apple-4k/config | 2 +- ...preserve-PMU-and-RTC-interrupt-owner.patch | 42 ++++ ...erve-PMU-and-RTC-interrupt-ownership.patch | 206 ++++++++++++++++++ ...erve-PMU-and-RTC-interrupt-ownership.patch | 36 +++ 5 files changed, 286 insertions(+), 2 deletions(-) create mode 100644 kernel/patches/devices/s800x/0002-arm64-dts-apple-preserve-PMU-and-RTC-interrupt-owner.patch create mode 100644 kernel/patches/drivers/rtc/0001-rtc-preserve-PMU-and-RTC-interrupt-ownership.patch create mode 100644 kernel/patches/drivers/usb/0011-usb-preserve-PMU-and-RTC-interrupt-ownership.patch diff --git a/kernel/configs/apple-16k/config b/kernel/configs/apple-16k/config index 4949d63c..f11529b5 100644 --- a/kernel/configs/apple-16k/config +++ b/kernel/configs/apple-16k/config @@ -2118,7 +2118,7 @@ CONFIG_INPUT_KEYBOARD=y # CONFIG_KEYBOARD_DLINK_DIR685 is not set # CONFIG_KEYBOARD_LKKBD is not set CONFIG_KEYBOARD_GPIO=y -# CONFIG_KEYBOARD_GPIO_POLLED is not set +CONFIG_KEYBOARD_GPIO_POLLED=y # CONFIG_KEYBOARD_TCA8418 is not set # CONFIG_KEYBOARD_MATRIX is not set # CONFIG_KEYBOARD_CHARLIEPLEX is not set diff --git a/kernel/configs/apple-4k/config b/kernel/configs/apple-4k/config index ce7fad3f..f135bd7e 100644 --- a/kernel/configs/apple-4k/config +++ b/kernel/configs/apple-4k/config @@ -2119,7 +2119,7 @@ CONFIG_INPUT_KEYBOARD=y # CONFIG_KEYBOARD_DLINK_DIR685 is not set # CONFIG_KEYBOARD_LKKBD is not set CONFIG_KEYBOARD_GPIO=y -# CONFIG_KEYBOARD_GPIO_POLLED is not set +CONFIG_KEYBOARD_GPIO_POLLED=y # CONFIG_KEYBOARD_TCA8418 is not set # CONFIG_KEYBOARD_MATRIX is not set # CONFIG_KEYBOARD_CHARLIEPLEX is not set 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/0011-usb-preserve-PMU-and-RTC-interrupt-ownership.patch b/kernel/patches/drivers/usb/0011-usb-preserve-PMU-and-RTC-interrupt-ownership.patch new file mode 100644 index 00000000..58875bd3 --- /dev/null +++ b/kernel/patches/drivers/usb/0011-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) { From d2247b1c61e0af5811b670e2e22c4e370cb7784f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= Date: Thu, 12 Nov 2026 09:00:00 +0100 Subject: [PATCH 09/10] PCI/NVMe: Keep the ANS handoff across kexec, cold-reset when it fails On J71t the NVMe controller (ANS) sits behind the H9P PCIe port, and resetting that port loses the controller until a cold boot. A kexec therefore has to hand over a stopped but powered ANS, with its DART translations retained, or the next kernel loses its root filesystem. The first version of this handoff panicked when a step failed, which with panic=0 is an indefinite hang. Keep the certified ANS kexec handoff: DT and bindings describe it, the DART, H9P and NVMe drivers stop queues and translations in order and the next kernel picks them up, and core/printk export what the modules need. Every failure on the outgoing side now ends in a native cold reset through apple_cold_reset() instead of a panic, with explicit returns after it. The port is handed over powered, with PERST# asserted and the link stopped, and the incoming kernel brings it up with the cold-boot sequence (port power cycle, NVMe mode, port enable, then link training). Retraining the powered port by PERST# alone, as iOS never does, left the root port in Detect.Quiet and cost a timeout on every warm boot. Assisted-by: LLM --- ...keep-the-certified-ANS-kexec-handoff.patch | 32 + ...keep-the-certified-ANS-kexec-handoff.patch | 26 + ...sg_dump_desc-for-module-cold-reset-d.patch | 27 + ...u-keep-the-certified-ANS-kexec-hando.patch | 28 + ...-keep-the-certified-ANS-kexec-handof.patch | 56 ++ ...keep-the-certified-ANS-kexec-handoff.patch | 28 + ...keep-the-certified-ANS-kexec-handoff.patch | 668 ++++++++++++++++++ ...keep-the-certified-ANS-kexec-handoff.patch | 337 +++++++++ ...eset-after-an-uncertified-kexec-hand.patch | 85 +++ ...eset-after-unsafe-ANS-kexec-shutdown.patch | 76 ++ ...se-explicit-returns-after-cold-reset.patch | 62 ++ 11 files changed, 1425 insertions(+) create mode 100644 kernel/patches/devices/s8000/0008-arm64-dts-apple-keep-the-certified-ANS-kexec-handoff.patch create mode 100644 kernel/patches/drivers/base/0002-core-keep-the-certified-ANS-kexec-handoff.patch create mode 100644 kernel/patches/drivers/base/0003-printk-export-kmsg_dump_desc-for-module-cold-reset-d.patch create mode 100644 kernel/patches/drivers/iommu/0011-dt-bindings-iommu-keep-the-certified-ANS-kexec-hando.patch create mode 100644 kernel/patches/drivers/iommu/0012-iommu-apple-dart-keep-the-certified-ANS-kexec-handof.patch create mode 100644 kernel/patches/drivers/pci/0033-dt-bindings-pci-keep-the-certified-ANS-kexec-handoff.patch create mode 100644 kernel/patches/drivers/pci/0034-PCI-apple-h9p-keep-the-certified-ANS-kexec-handoff.patch create mode 100644 kernel/patches/drivers/pci/0035-nvme-pci-keep-the-certified-ANS-kexec-handoff.patch create mode 100644 kernel/patches/drivers/pci/0036-PCI-apple-cold-reset-after-an-uncertified-kexec-hand.patch create mode 100644 kernel/patches/drivers/pci/0037-nvme-pci-cold-reset-after-unsafe-ANS-kexec-shutdown.patch create mode 100644 kernel/patches/drivers/pci/0038-PCI-apple-use-explicit-returns-after-cold-reset.patch diff --git a/kernel/patches/devices/s8000/0008-arm64-dts-apple-keep-the-certified-ANS-kexec-handoff.patch b/kernel/patches/devices/s8000/0008-arm64-dts-apple-keep-the-certified-ANS-kexec-handoff.patch new file mode 100644 index 00000000..dab3a029 --- /dev/null +++ b/kernel/patches/devices/s8000/0008-arm64-dts-apple-keep-the-certified-ANS-kexec-handoff.patch @@ -0,0 +1,32 @@ +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: keep the certified ANS kexec handoff + +Resetting the ANS host or its translations destroys the state required +by the next kernel. Describe the board-selected retained handoff. + +Assisted-by: LLM +--- + arch/arm64/boot/dts/apple/s8003-j71t.dts | 8 ++++++++ + 1 file changed, 8 insertions(+) + +diff --git a/arch/arm64/boot/dts/apple/s8003-j71t.dts b/arch/arm64/boot/dts/apple/s8003-j71t.dts +index 733cefc66..0bc19727d 100644 +--- a/arch/arm64/boot/dts/apple/s8003-j71t.dts ++++ b/arch/arm64/boot/dts/apple/s8003-j71t.dts +@@ -11,6 +11,14 @@ + #include "s8003.dtsi" + #include "s800x-ipad5.dtsi" + ++&pcie0 { ++ apple,ans-kexec-handoff; ++}; ++ ++&pcie0_dart0 { ++ apple,ans-dart-handoff; ++}; ++ + / { + compatible = "apple,j71t", "apple,s8003", "apple,arm-platform"; + model = "Apple iPad 5 (Wi-Fi) (TSMC)"; diff --git a/kernel/patches/drivers/base/0002-core-keep-the-certified-ANS-kexec-handoff.patch b/kernel/patches/drivers/base/0002-core-keep-the-certified-ANS-kexec-handoff.patch new file mode 100644 index 00000000..c3550de8 --- /dev/null +++ b/kernel/patches/drivers/base/0002-core-keep-the-certified-ANS-kexec-handoff.patch @@ -0,0 +1,26 @@ +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] core: keep the certified ANS kexec handoff + +Resetting the ANS host or its translations destroys the state required +by the next kernel. Retain the stopped-DMA handoff only where the device +tree requests it. + +Assisted-by: LLM +--- + kernel/kexec_core.c | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/kernel/kexec_core.c b/kernel/kexec_core.c +index dc770b9a6..819650bcd 100644 +--- a/kernel/kexec_core.c ++++ b/kernel/kexec_core.c +@@ -53,6 +53,7 @@ atomic_t __kexec_lock = ATOMIC_INIT(0); + + /* Flag to indicate we are going to kexec a new kernel */ + bool kexec_in_progress = false; ++EXPORT_SYMBOL_GPL(kexec_in_progress); + + bool kexec_file_dbg_print; + diff --git a/kernel/patches/drivers/base/0003-printk-export-kmsg_dump_desc-for-module-cold-reset-d.patch b/kernel/patches/drivers/base/0003-printk-export-kmsg_dump_desc-for-module-cold-reset-d.patch new file mode 100644 index 00000000..054cba10 --- /dev/null +++ b/kernel/patches/drivers/base/0003-printk-export-kmsg_dump_desc-for-module-cold-reset-d.patch @@ -0,0 +1,27 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Thu, 12 Nov 2026 00:54:33 +0100 +Subject: [PATCH] printk: export kmsg_dump_desc for module cold-reset + diagnostics + +The VXD shutdown fail-safe is a GPL module. Allow it to dump emergency +evidence before a cold reset, matching the built-in GPU and PCIe paths. +No printk behavior changes. + +Assisted-by: LLM +--- + kernel/printk/printk.c | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c +index fcc51b699..a812ec1e5 100644 +--- a/kernel/printk/printk.c ++++ b/kernel/printk/printk.c +@@ -4882,6 +4882,7 @@ void kmsg_dump_desc(enum kmsg_dump_reason reason, const char *desc) + } + rcu_read_unlock(); + } ++EXPORT_SYMBOL_GPL(kmsg_dump_desc); + + /** + * kmsg_dump_get_line - retrieve one kmsg log line diff --git a/kernel/patches/drivers/iommu/0011-dt-bindings-iommu-keep-the-certified-ANS-kexec-hando.patch b/kernel/patches/drivers/iommu/0011-dt-bindings-iommu-keep-the-certified-ANS-kexec-hando.patch new file mode 100644 index 00000000..e442e50e --- /dev/null +++ b/kernel/patches/drivers/iommu/0011-dt-bindings-iommu-keep-the-certified-ANS-kexec-hando.patch @@ -0,0 +1,28 @@ +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] dt-bindings: iommu: keep the certified ANS kexec handoff + +Resetting the ANS host or its translations destroys the state required +by the next kernel. Describe the board-selected retained handoff. + +Assisted-by: LLM +--- + Documentation/devicetree/bindings/iommu/apple,dart.yaml | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/Documentation/devicetree/bindings/iommu/apple,dart.yaml b/Documentation/devicetree/bindings/iommu/apple,dart.yaml +index df72722fc..adcbd21fc 100644 +--- a/Documentation/devicetree/bindings/iommu/apple,dart.yaml ++++ b/Documentation/devicetree/bindings/iommu/apple,dart.yaml +@@ -21,6 +21,10 @@ description: |+ + fault conditions. + + properties: ++ apple,ans-dart-handoff: ++ type: boolean ++ description: Allow the storage endpoint to retire and rebuild this DART stream during kexec. ++ + compatible: + oneOf: + - enum: diff --git a/kernel/patches/drivers/iommu/0012-iommu-apple-dart-keep-the-certified-ANS-kexec-handof.patch b/kernel/patches/drivers/iommu/0012-iommu-apple-dart-keep-the-certified-ANS-kexec-handof.patch new file mode 100644 index 00000000..e5204e73 --- /dev/null +++ b/kernel/patches/drivers/iommu/0012-iommu-apple-dart-keep-the-certified-ANS-kexec-handof.patch @@ -0,0 +1,56 @@ +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] iommu/apple-dart: keep the certified ANS kexec handoff + +Resetting the ANS host or its translations destroys the state required +by the next kernel. Retain the stopped-DMA handoff only where the device +tree requests it. + +Assisted-by: LLM +--- + drivers/iommu/apple-dart.c | 24 ++++++++++++++++++++++++ + 1 file changed, 24 insertions(+) + +diff --git a/drivers/iommu/apple-dart.c b/drivers/iommu/apple-dart.c +index 1ee6add55..5aff4bc3e 100644 +--- a/drivers/iommu/apple-dart.c ++++ b/drivers/iommu/apple-dart.c +@@ -35,6 +35,7 @@ + #include + + #include ++#include + + #include "dma-iommu.h" + +@@ -1009,6 +1010,29 @@ apple_dart_setup_translation(struct apple_dart_domain *domain, + return dart->hw->invalidate_tlb(stream_map); + } + ++int apple_ans_dart_stop(struct device *dev) ++{ ++ struct iommu_domain *domain = iommu_get_domain_for_dev(dev); ++ struct apple_dart_master_cfg *cfg = dev_iommu_priv_get(dev); ++ struct apple_dart_stream_map *map; ++ int i; ++ ++ if (!domain || !cfg || ++ !cfg->stream_maps[0].dart || ++ !of_property_read_bool(cfg->stream_maps[0].dart->dev->of_node, ++ "apple,ans-dart-handoff") || ++ (domain->type != IOMMU_DOMAIN_DMA && domain->type != IOMMU_DOMAIN_DMA_FQ)) ++ return -ENODEV; ++ for_each_stream_map(i, cfg, map) { ++ if (map->dart->hw->type != DART_S5L8960X) ++ return -EOPNOTSUPP; ++ apple_dart_hw_disable_dma(map); ++ } ++ dev_info(dev, "Apple ANS: DART stopped for handoff\n"); ++ return 0; ++} ++EXPORT_SYMBOL_GPL(apple_ans_dart_stop); ++ + static int apple_dart_finalize_domain(struct apple_dart_domain *dart_domain, + struct apple_dart_master_cfg *cfg) + { diff --git a/kernel/patches/drivers/pci/0033-dt-bindings-pci-keep-the-certified-ANS-kexec-handoff.patch b/kernel/patches/drivers/pci/0033-dt-bindings-pci-keep-the-certified-ANS-kexec-handoff.patch new file mode 100644 index 00000000..c4b7b0ee --- /dev/null +++ b/kernel/patches/drivers/pci/0033-dt-bindings-pci-keep-the-certified-ANS-kexec-handoff.patch @@ -0,0 +1,28 @@ +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] dt-bindings: pci: keep the certified ANS kexec handoff + +Resetting the ANS host or its translations destroys the state required +by the next kernel. Describe the board-selected retained handoff. + +Assisted-by: LLM +--- + Documentation/devicetree/bindings/pci/apple,t8010-pcie.yaml | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/Documentation/devicetree/bindings/pci/apple,t8010-pcie.yaml b/Documentation/devicetree/bindings/pci/apple,t8010-pcie.yaml +index f6497a4e1..f9b72ce26 100644 +--- a/Documentation/devicetree/bindings/pci/apple,t8010-pcie.yaml ++++ b/Documentation/devicetree/bindings/pci/apple,t8010-pcie.yaml +@@ -140,6 +140,10 @@ allOf: + - apple,nvme-hmb + + properties: ++ apple,ans-kexec-handoff: ++ type: boolean ++ description: Retain storage port power across a certified stopped-DMA kexec handoff. ++ + compatible: + oneOf: + - enum: diff --git a/kernel/patches/drivers/pci/0034-PCI-apple-h9p-keep-the-certified-ANS-kexec-handoff.patch b/kernel/patches/drivers/pci/0034-PCI-apple-h9p-keep-the-certified-ANS-kexec-handoff.patch new file mode 100644 index 00000000..e030657f --- /dev/null +++ b/kernel/patches/drivers/pci/0034-PCI-apple-h9p-keep-the-certified-ANS-kexec-handoff.patch @@ -0,0 +1,668 @@ +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] PCI: apple-h9p: keep the certified ANS kexec handoff + +Resetting the ANS host or its translations destroys the state required +by the next kernel. Retain the stopped-DMA handoff only where the device +tree requests it. + +The storage port is handed over powered, with its reference clock running, +PERST# asserted and the LTSSM stopped: the state the boot loader leaves it +in, except that the link is down. The incoming kernel brings it up as on a +cold boot, with the full port power cycle, NVMe mode and the port enable +sequence before the link is trained. iOS trains this link only after that +sequence and never re-trains a powered port by toggling PERST# alone; +doing so after a kexec leaves the root port in Detect.Quiet with the LTSSM +enable not latched. + +Assisted-by: LLM +--- + drivers/pci/controller/pcie-apple-h9p.c | 459 +++++++++++++++++++++--- + include/linux/apple-ans.h | 23 ++ + 2 files changed, 432 insertions(+), 50 deletions(-) + +diff --git a/drivers/pci/controller/pcie-apple-h9p.c b/drivers/pci/controller/pcie-apple-h9p.c +index 15f7a0cbe..f0095394a 100644 +--- a/drivers/pci/controller/pcie-apple-h9p.c ++++ b/drivers/pci/controller/pcie-apple-h9p.c +@@ -15,6 +15,8 @@ + + #include + #include ++#include ++#include + #include + #include + #include +@@ -38,12 +40,15 @@ + #include + #include + #include ++#include + #include + #include + #include ++#include + + #include + #include ++#include + + #include "pci-host-common.h" + +@@ -248,7 +253,7 @@ + #define S8003_PORT_LINKCDMSTS_LTSSM GENMASK(14, 9) + #define S8003_LTSSM_L0 0x11 + +-/* ADT perst-to-refclk-delay-us and pci-bridge t-refclk-to-perst (j71t) */ ++/* ADT perst-to-refclk-delay-us and pci-bridge t-refclk-to-perst (S8003) */ + #define S8003_PERST_TO_REFCLK_US 60 + #define S8003_REFCLK_TO_PERST_US 100 + #define S8003_PERST_HOLD_MS 100 +@@ -269,7 +274,7 @@ + + /* + * NVMMU invalidate FIFO status, in the port 0 block (iBoot sets the live ADT +- * nvme-mmu0 invalidate-fifo-workaround = 1 on j71t). iOS (AppleNVMeMMU, ++ * nvme-mmu0 invalidate-fifo-workaround = 1 on S8003). iOS (AppleNVMeMMU, + * 0xfffffff005eebc9c) waits up to 20 ms for it to drain before every + * invalidate and never reads the invalidate register back. + */ +@@ -429,7 +434,7 @@ static const struct apple_h9p_tunable t7000_port_tunables[] = { + }; + + /* +- * S8003 tunables, which iBoot puts in the live ADT; copied from the j71t ++ * S8003 tunables, which iBoot puts in the live ADT; copied from the S8003 + * live ADT (iOS 16.7.10 iBoot), where both bridges carry the same values. + * apcie-phy-tunables and apcie-common-tunables apply after the common init, + * in that order. +@@ -582,6 +587,7 @@ struct apple_h9p_tunables { + struct apple_h9p_nvmmu { + struct apple_h9p_pcie *pcie; + void __iomem *base; ++ bool poisoned; + /* Programmed: the ANS glue may map through it */ + bool ready; + /* Serializes the invalidate FIFO wait and the invalidate write */ +@@ -711,6 +717,7 @@ struct apple_h9p_pcie { + u32 late_ports; + unsigned int late_tries; + struct delayed_work late_work; ++ bool ans_handoff_stopped; + + struct apple_h9p_nvmmu nvmmu; + +@@ -1554,6 +1561,64 @@ static irqreturn_t apple_h9p_nvmmu_irq(int irq, void *data) + return IRQ_HANDLED; + } + ++static int apple_h9p_nvmmu_program(struct apple_h9p_pcie *pcie) ++{ ++ struct apple_h9p_nvmmu *nvmmu = &pcie->nvmmu; ++ u32 val; ++ int ret; ++ ++ if (pcie->hw->nvmmu_inv_fifo) { ++ /* ++ * iOS's S8003 order: the invalidate FIFO drained, the old SART ++ * aperture off before its address changes, the zeroed scratch ++ * page and TCBs visible, then scratch page, TCB base, init, SART. ++ */ ++ ret = readl_poll_timeout(pcie->base_port[0] + ++ S8003_PORT_NVMMU_INV_FIFO, val, ++ val & S8003_PORT_NVMMU_INV_FIFO_EMPTY, ++ 10, S8003_NVMMU_INV_FIFO_TIMEOUT_US); ++ if (ret) ++ return ret; ++ h9p_writel_flush(0, nvmmu->base + H9P_NVMMU_SART_CTRL); ++ dma_wmb(); ++ h9p_writel_flush(lower_32_bits(nvmmu->tcb_table_dma), ++ nvmmu->base + H9P_NVMMU_TCB_TABLE_LO); ++ h9p_writel_flush(upper_32_bits(nvmmu->tcb_table_dma), ++ nvmmu->base + H9P_NVMMU_TCB_TABLE_HI); ++ h9p_writel_flush(lower_32_bits(nvmmu->tcb_dma), ++ nvmmu->base + H9P_NVMMU_TCB_BASE_LO); ++ h9p_writel_flush(upper_32_bits(nvmmu->tcb_dma), ++ nvmmu->base + H9P_NVMMU_TCB_BASE_HI); ++ } else { ++ h9p_writel_flush(lower_32_bits(nvmmu->tcb_dma), ++ nvmmu->base + H9P_NVMMU_TCB_BASE_LO); ++ h9p_writel_flush(upper_32_bits(nvmmu->tcb_dma), ++ nvmmu->base + H9P_NVMMU_TCB_BASE_HI); ++ h9p_writel_flush(lower_32_bits(nvmmu->tcb_table_dma), ++ nvmmu->base + H9P_NVMMU_TCB_TABLE_LO); ++ h9p_writel_flush(upper_32_bits(nvmmu->tcb_table_dma), ++ nvmmu->base + H9P_NVMMU_TCB_TABLE_HI); ++ } ++ h9p_writel_flush(H9P_NVMMU_TCB_CTRL_INIT, ++ nvmmu->base + H9P_NVMMU_TCB_CTRL); ++ ++ ret = apple_h9p_wait(nvmmu->base + H9P_NVMMU_TCB_CTRL, ++ pcie->hw->nvmmu_init_busy, 0, 0, 250000); ++ if (ret) ++ return ret; ++ ++ h9p_writel_flush(nvmmu->va_base - 0x80000000U, ++ nvmmu->base + H9P_NVMMU_SART_VA_BASE); ++ h9p_writel_flush(round_up(nvmmu->va_base + nvmmu->size, ++ H9P_NVMMU_SART_ALIGNMENT) - 0x80100000U, ++ nvmmu->base + H9P_NVMMU_SART_VA_END); ++ h9p_writel_flush(nvmmu->pa_base >> 20, ++ nvmmu->base + H9P_NVMMU_SART_PA_BASE); ++ h9p_writel_flush(1, nvmmu->base + H9P_NVMMU_SART_CTRL); ++ ++ return 0; ++} ++ + static int apple_h9p_setup_nvmmu(struct apple_h9p_pcie *pcie) + { + struct apple_h9p_nvmmu *nvmmu = &pcie->nvmmu; +@@ -1561,7 +1626,7 @@ static int apple_h9p_setup_nvmmu(struct apple_h9p_pcie *pcie) + struct device_node *mem_np; + struct resource res; + resource_size_t size; +- u32 iova, val; ++ u32 iova; + int irq; + int ret; + +@@ -1630,55 +1695,10 @@ static int apple_h9p_setup_nvmmu(struct apple_h9p_pcie *pcie) + } + + spin_lock_init(&nvmmu->inv_lock); +- if (pcie->hw->nvmmu_inv_fifo) { +- /* +- * iOS's S8003 order: the invalidate FIFO drained, the old SART +- * aperture off before its address changes, the zeroed scratch +- * page and TCBs visible, then scratch page, TCB base, init, SART. +- */ +- ret = readl_poll_timeout(pcie->base_port[0] + +- S8003_PORT_NVMMU_INV_FIFO, val, +- val & S8003_PORT_NVMMU_INV_FIFO_EMPTY, +- 10, S8003_NVMMU_INV_FIFO_TIMEOUT_US); +- if (ret) +- goto out_put_node; +- h9p_writel_flush(0, nvmmu->base + H9P_NVMMU_SART_CTRL); +- dma_wmb(); +- h9p_writel_flush(lower_32_bits(nvmmu->tcb_table_dma), +- nvmmu->base + H9P_NVMMU_TCB_TABLE_LO); +- h9p_writel_flush(upper_32_bits(nvmmu->tcb_table_dma), +- nvmmu->base + H9P_NVMMU_TCB_TABLE_HI); +- h9p_writel_flush(lower_32_bits(nvmmu->tcb_dma), +- nvmmu->base + H9P_NVMMU_TCB_BASE_LO); +- h9p_writel_flush(upper_32_bits(nvmmu->tcb_dma), +- nvmmu->base + H9P_NVMMU_TCB_BASE_HI); +- } else { +- h9p_writel_flush(lower_32_bits(nvmmu->tcb_dma), +- nvmmu->base + H9P_NVMMU_TCB_BASE_LO); +- h9p_writel_flush(upper_32_bits(nvmmu->tcb_dma), +- nvmmu->base + H9P_NVMMU_TCB_BASE_HI); +- h9p_writel_flush(lower_32_bits(nvmmu->tcb_table_dma), +- nvmmu->base + H9P_NVMMU_TCB_TABLE_LO); +- h9p_writel_flush(upper_32_bits(nvmmu->tcb_table_dma), +- nvmmu->base + H9P_NVMMU_TCB_TABLE_HI); +- } +- h9p_writel_flush(H9P_NVMMU_TCB_CTRL_INIT, +- nvmmu->base + H9P_NVMMU_TCB_CTRL); +- +- ret = apple_h9p_wait(nvmmu->base + H9P_NVMMU_TCB_CTRL, +- pcie->hw->nvmmu_init_busy, 0, 0, 250000); ++ ret = apple_h9p_nvmmu_program(pcie); + if (ret) + goto out_put_node; + +- h9p_writel_flush(nvmmu->va_base - 0x80000000U, +- nvmmu->base + H9P_NVMMU_SART_VA_BASE); +- h9p_writel_flush(round_up(nvmmu->va_base + nvmmu->size, +- H9P_NVMMU_SART_ALIGNMENT) - 0x80100000U, +- nvmmu->base + H9P_NVMMU_SART_VA_END); +- h9p_writel_flush(nvmmu->pa_base >> 20, +- nvmmu->base + H9P_NVMMU_SART_PA_BASE); +- h9p_writel_flush(1, nvmmu->base + H9P_NVMMU_SART_CTRL); +- + irq = platform_get_irq_byname_optional(pcie->pdev, "nvmmu"); + if (irq > 0) { + ret = devm_request_irq(dev, irq, apple_h9p_nvmmu_irq, 0, +@@ -1726,6 +1746,7 @@ static int apple_h9p_nvmmu_invalidate(struct apple_h9p_pcie *pcie, + spin_unlock_irqrestore(&nvmmu->inv_lock, flags); + + if (ret) { ++ WRITE_ONCE(nvmmu->poisoned, true); + dev_err_ratelimited(pcie->dev, + "NVMMU invalidate FIFO not empty (%#x), tag %u\n", + val, tag); +@@ -1781,6 +1802,8 @@ int apple_ans_nvmmu_map(struct device *dev, unsigned int tag, + nvmmu = &pcie->nvmmu; + if (!nvmmu->ready) + return -EOPNOTSUPP; ++ if (READ_ONCE(nvmmu->poisoned)) ++ return -EIO; + + tcb = (u32 *)nvmmu->tcb + tag * H9P_NVMMU_TCB_DWORDS; + sgl = (u32 *)nvmmu->tcb_sgl + tag * H9P_NVMMU_SGL_WORDS; +@@ -2705,6 +2728,8 @@ static int apple_s8003_power_cycle_port(struct apple_h9p_pcie *pcie, + return ret; + } + ++ if (!port && pcie->nvmmu.base) ++ h9p_writel_flush(0, pcie->nvmmu.base + H9P_NVMMU_SART_CTRL); + apple_s8003_port_off(pcie, port); + usleep_range(S8003_POWER_OFF_SETTLE_US, S8003_POWER_OFF_SETTLE_US + 1000); + ret = apple_h9p_wait(rc + S8003_RC_PORT_CTL(port), +@@ -2887,6 +2912,18 @@ static int apple_s8003_start_link(struct apple_h9p_pcie *pcie, + return ret; + } + ++static bool apple_s8003_port_powered(struct apple_h9p_pcie *pcie) ++{ ++ void __iomem *rc = pcie->base_rc; ++ ++ return (readl(rc + S8003_RC_PORT_CTL(0)) & S8003_RC_PORT_CTL_ENABLE) && ++ (readl(rc + S8003_RC_PORT_PWR(0)) & S8003_RC_PORT_PWR_ON) && ++ (readl(rc + S8003_RC_PORT_REFCLK(0)) & S8003_RC_PORT_REFCLK_ON) && ++ !(readl(rc + S8003_RC_PORT_CTL(0)) & S8003_RC_PORT_CTL_RESET) && ++ (readl(rc + S8003_RC_PORT_PWR(0)) & S8003_RC_PORT_PWR_REFCLK_NOGATE) && ++ (readl(rc + S8003_RC_PORT_READY(0)) & S8003_RC_PORT_READY_DONE); ++} ++ + /* Power the port up as validated for its kind, and train its link once */ + static int apple_s8003_setup_port_once(struct apple_h9p_pcie *pcie, + unsigned int port) +@@ -3059,6 +3096,11 @@ static void apple_s8003_late_ports_work(struct work_struct *work) + unsigned int port; + int ret; + ++ /* Lab 0019: no secondary bus publication without storage bus 1. */ ++ if (of_device_is_compatible(pcie->dev->of_node, "apple,s8003-pcie") && ++ !(READ_ONCE(pcie->enabled_ports) & BIT(0))) ++ return; ++ + for_each_set_bit(port, &pending, H9P_NUM_PORTS) { + ret = apple_s8003_setup_endpoint_port(pcie, port); + if (ret == -EPROBE_DEFER) { +@@ -3418,6 +3460,314 @@ static void apple_h9p_pcie_pwrctrl_cleanup(void *data) + pci_pwrctrl_destroy_devices(pcie->dev); + } + ++/* SPDX-License-Identifier: GPL-2.0-only */ ++static struct apple_h9p_pcie *apple_ans_endpoint_host(struct device *dev) ++{ ++ struct device *host = dev; ++ struct apple_h9p_pcie *pcie; ++ ++ while (host && host->bus == dev->bus) ++ host = host->parent; ++ if (!host || !host->parent) ++ return NULL; ++ pcie = apple_h9p_pcie_lookup(host->parent); ++ if (!pcie || !of_device_is_compatible(pcie->dev->of_node, "apple,s8003-pcie") || ++ apple_h9p_pcie_device_port(pcie, dev) != 0) ++ return NULL; ++ return pcie; ++} ++ ++ ++/* release6: caller froze and synchronized I/O, before SHN. */ ++/* Lab 0018: caller synchronized queues and stopped CC/RDY before this. */ ++int apple_ans_nvmmu_reset(struct device *dev) ++{ ++ struct apple_h9p_pcie *pcie = apple_ans_endpoint_host(dev); ++ struct apple_h9p_nvmmu *nvmmu; ++ int ret; ++ ++ if (!pcie || !(pcie->enabled_ports & BIT(0))) ++ return -ENODEV; ++ nvmmu = &pcie->nvmmu; ++ if (!nvmmu->ready) ++ return -ENODEV; ++ memset(nvmmu->tcb, 0, nvmmu->tcb_size); ++ memset(nvmmu->tcb_table, 0, nvmmu->tcb_table_size); ++ memset(nvmmu->tcb_sgl, 0, nvmmu->tcb_sgl_size); ++ ret = apple_h9p_nvmmu_program(pcie); ++ WRITE_ONCE(nvmmu->poisoned, !!ret); ++ if (ret) ++ return ret; ++ bitmap_zero(nvmmu->mapped, APPLE_ANS_NVMMU_MAX_REQS); ++ dev_info(dev, "NVMMU: stopped-controller cache/SART rebuilt\n"); ++ return 0; ++} ++EXPORT_SYMBOL_GPL(apple_ans_nvmmu_reset); ++ ++bool apple_ans_is_s8003(struct device *dev) ++{ ++ return apple_ans_endpoint_host(dev) != NULL; ++} ++EXPORT_SYMBOL_GPL(apple_ans_is_s8003); ++ ++int apple_ans_link_up(struct device *dev) ++{ ++ struct apple_h9p_pcie *pcie = apple_ans_endpoint_host(dev); ++ ++ if (!pcie) ++ return -ENODEV; ++ if (!(READ_ONCE(pcie->enabled_ports) & BIT(0))) ++ return -ENOLINK; ++ return !!(readl(pcie->base_port[0] + S8003_PORT_LINKSTS) & ++ S8003_PORT_LINKSTS_UP); ++} ++EXPORT_SYMBOL_GPL(apple_ans_link_up); ++ ++/* Host-only evidence, no endpoint config/BAR reads or invalidate register. */ ++void apple_ans_host_dump(struct device *dev, const char *reason) ++{ ++ struct apple_h9p_pcie *pcie = apple_ans_endpoint_host(dev); ++ ++ if (!pcie) ++ return; ++ dev_info(dev, "ANS host %s: ports %#x link %#x LTSSM %#x FIFO %#x poisoned %u mapped %*pb\n", ++ reason, pcie->enabled_ports, ++ readl(pcie->base_port[0] + H9P_PORT_LINKSTS), ++ readl(pcie->base_port[0] + H9P_PORT_LTSSMCTL), ++ readl(pcie->base_port[0] + S8003_PORT_NVMMU_INV_FIFO), ++ READ_ONCE(pcie->nvmmu.poisoned), APPLE_ANS_NVMMU_MAX_REQS, ++ pcie->nvmmu.mapped); ++} ++EXPORT_SYMBOL_GPL(apple_ans_host_dump); ++ ++int apple_ans_nvmmu_drain(struct device *dev) ++{ ++ struct apple_h9p_pcie *pcie = apple_ans_endpoint_host(dev); ++ u32 fifo; ++ int ret; ++ ++ if (!pcie || !pcie->nvmmu.ready) ++ return -ENODEV; ++ if (!bitmap_empty(pcie->nvmmu.mapped, APPLE_ANS_NVMMU_MAX_REQS)) ++ return -EBUSY; ++ ret = readl_poll_timeout(pcie->base_port[0] + S8003_PORT_NVMMU_INV_FIFO, ++ fifo, fifo & S8003_PORT_NVMMU_INV_FIFO_EMPTY, ++ 10, S8003_NVMMU_INV_FIFO_TIMEOUT_US); ++ dev_info(dev, "Apple ANS: outgoing NVMMU FIFO empty check (20ms) result %d\n", ret); ++ return ret; ++} ++EXPORT_SYMBOL_GPL(apple_ans_nvmmu_drain); ++ ++int apple_ans_handoff_stopped(struct device *dev) ++{ ++ struct apple_h9p_pcie *pcie = apple_ans_endpoint_host(dev); ++ unsigned int tag; ++ u32 fifo; ++ int ret; ++ ++ if (!pcie) ++ return -ENODEV; ++ /* All queues are drained and BME is clear before retiring translations. */ ++ for (tag = 0; tag < APPLE_ANS_NVMMU_MAX_REQS; tag++) { ++ ret = apple_ans_nvmmu_map(dev, tag, NULL, 0, false, NULL); ++ if (ret) ++ return ret; ++ } ++ /* The last invalidate also has to leave the FIFO before certification. */ ++ ret = readl_poll_timeout_atomic(pcie->base_port[0] + S8003_PORT_NVMMU_INV_FIFO, ++ fifo, fifo & S8003_PORT_NVMMU_INV_FIFO_EMPTY, ++ 1, S8003_NVMMU_INV_FIFO_TIMEOUT_US); ++ if (ret) ++ return ret; ++ ret = apple_ans_dart_stop(dev); ++ if (!ret) ++ WRITE_ONCE(pcie->ans_handoff_stopped, true); ++ return ret; ++} ++EXPORT_SYMBOL_GPL(apple_ans_handoff_stopped); ++ ++int apple_ans_handoff_begin(struct device *dev) ++{ ++ struct apple_h9p_pcie *pcie = apple_ans_endpoint_host(dev); ++ ++ if (!pcie) ++ return -EBUSY; ++ WRITE_ONCE(pcie->ans_handoff_stopped, false); ++ return 0; ++} ++EXPORT_SYMBOL_GPL(apple_ans_handoff_begin); ++ ++static void apple_h9p_s8003_shutdown(struct platform_device *pdev) ++{ ++ struct apple_h9p_pcie *pcie = apple_h9p_pcie_lookup(&pdev->dev); ++ unsigned int port; ++ ++ if (!kexec_in_progress || !pcie || ++ !of_device_is_compatible(pdev->dev.of_node, "apple,s8003-pcie")) ++ return; ++ cancel_delayed_work_sync(&pcie->late_work); ++ if (!READ_ONCE(pcie->ans_handoff_stopped) || ++ (of_property_read_bool(pcie->dev->of_node, "apple,ans-kexec-handoff") && ++ !apple_s8003_port_powered(pcie))) ++ panic("Apple ANS: refusing uncertified powered handoff"); ++ for (port = 0; port < H9P_NUM_PORTS; port++) { ++ void __iomem *cfg = pcie->base_config + port * H9P_CFG_PORT_STRIDE; ++ ++ if (!(pcie->enabled_ports & BIT(port))) ++ continue; ++ /* release6: block root routing, retain refclk, then isolate. */ ++ writew(0, cfg + PCI_COMMAND); ++ h9p_rmw(cfg + PCI_PRIMARY_BUS, GENMASK(23, 8), 0); ++ h9p_rmw(pcie->base_rc + S8003_RC_PORT_PWR(port), 0, ++ S8003_RC_PORT_PWR_REFCLK_NOGATE); ++ if (gpiod_direction_output(pcie->perst[port], 1)) ++ panic("Apple PCIe: PERST failed during handoff"); ++ if (apple_s8003_port_stop(pcie, port)) ++ panic("Apple PCIe: LTSSM stop failed during handoff"); ++ udelay(S8003_PERST_TO_REFCLK_US); ++ if (!port && pcie->nvmmu.ready) { ++ h9p_writel_flush(0, pcie->nvmmu.base + H9P_NVMMU_SART_CTRL); ++ pcie->nvmmu.ready = false; ++ } ++ if (!port && !of_property_read_bool(pcie->dev->of_node, ++ "apple,ans-kexec-handoff")) ++ apple_s8003_port_off(pcie, port); ++ if (!port && of_property_read_bool(pcie->dev->of_node, "apple,ans-kexec-handoff") && ++ (!apple_s8003_port_powered(pcie) || ++ gpiod_get_value_cansleep(pcie->perst[0]) != 1)) ++ panic("Apple ANS: outgoing powered port certificate failed"); ++ pcie->enabled_ports &= ~BIT(port); ++ } ++ dev_info(pcie->dev, "kexec: PCIe PERST asserted, LTSSM stopped, ports powered; NVMMU SART disabled\n"); ++} ++ ++static void apple_h9p_capture_park(struct apple_h9p_pcie *pcie) ++{ ++ void __iomem *cfg = pcie->base_config; ++ ++ cancel_delayed_work_sync(&pcie->late_work); ++ if (!(pcie->enabled_ports & BIT(0))) ++ return; ++ writew(0, cfg + PCI_COMMAND); ++ h9p_rmw(cfg + PCI_PRIMARY_BUS, GENMASK(23, 8), 0); ++ h9p_rmw(pcie->base_rc + S8003_RC_PORT_PWR(0), 0, ++ S8003_RC_PORT_PWR_REFCLK_NOGATE); ++ if (gpiod_direction_output(pcie->perst[0], 1) || ++ apple_s8003_port_stop(pcie, 0)) ++ panic("ANS capture isolation failed"); ++ udelay(S8003_PERST_TO_REFCLK_US); ++ if (pcie->nvmmu.base) ++ h9p_writel_flush(0, pcie->nvmmu.base + H9P_NVMMU_SART_CTRL); ++ pcie->nvmmu.ready = false; ++ pcie->enabled_ports &= ~BIT(0); ++} ++ ++struct apple_h9p_crash_state { ++ struct apple_h9p_pcie *pcie; ++ int result; ++ bool busy; ++ bool found; ++}; ++ ++/* No NVMe driver, queues, SHN, or old DMA buffers are needed in phoenix. */ ++static int apple_h9p_crash_stop_ans(struct pci_dev *pdev, void *data) ++{ ++ struct apple_h9p_crash_state *state = data; ++ struct apple_h9p_pcie *pcie = state->pcie; ++ void __iomem *bar; ++ u32 csts = 0; ++ int i, ret; ++ ++ if (pdev->vendor != PCI_VENDOR_ID_APPLE || ++ ((pdev->class >> 8) != PCI_CLASS_STORAGE_EXPRESS && ++ (pdev->class >> 8) != PCI_CLASS_STORAGE_OTHER)) ++ return 0; ++ state->found = true; ++ if (pdev->driver) { ++ state->busy = true; ++ return 1; ++ } ++ if (apple_ans_link_up(&pdev->dev) <= 0) { ++ state->result = -ENOLINK; ++ return 0; ++ } ++ ret = pci_enable_device_mem(pdev); ++ if (ret) { ++ state->result = ret; ++ return 0; ++ } ++ bar = pci_iomap_range(pdev, 0, 0, SZ_8K); ++ if (!bar) { ++ ret = -ENOMEM; ++ goto disable; ++ } ++ /* An unknown queue must never be rung or sent a shutdown notification. */ ++ dev_info(pcie->dev, "phoenix ANS: CC %#x CSTS %#x, clearing EN without SHN\n", ++ readl(bar + NVME_REG_CC), readl(bar + NVME_REG_CSTS)); ++ writel(0, bar + NVME_REG_CC); ++ ret = -ETIMEDOUT; ++ for (i = 0; i < 2000; i++) { ++ if (apple_ans_link_up(&pdev->dev) <= 0) { ++ ret = -ENOLINK; ++ break; ++ } ++ csts = readl(bar + NVME_REG_CSTS); ++ if (csts == U32_MAX) { ++ ret = -EIO; ++ break; ++ } ++ if (!(csts & NVME_CSTS_RDY)) { ++ ret = 0; ++ break; ++ } ++ usleep_range(1000, 1100); ++ } ++ dev_info(pcie->dev, "phoenix ANS: disable result %d CSTS %#x; blocking bus master\n", ++ ret, csts); ++ pci_iounmap(pdev, bar); ++disable: ++ pci_clear_master(pdev); ++ pci_disable_device(pdev); ++ state->result = ret; ++ return 0; ++} ++ ++static ssize_t crash_quiesce_store(struct device *dev, ++ struct device_attribute *attr, ++ const char *buf, size_t count) ++{ ++ struct apple_h9p_pcie *pcie = apple_h9p_pcie_lookup(dev); ++ struct apple_h9p_crash_state state = { .pcie = pcie }; ++ ++ if (!pcie || !is_kdump_kernel() || ++ !of_device_is_compatible(dev->of_node, "apple,s8003-pcie") || ++ !sysfs_streq(buf, "1")) ++ return -EPERM; ++ /* The endpoint gate must stay closed; don't park under a live driver. */ ++ if (pcie->late_ports || work_busy(&pcie->late_work.work)) ++ return -EBUSY; ++ pci_walk_bus(pcie->bridge->bus, apple_h9p_crash_stop_ans, &state); ++ if (state.busy) ++ return -EBUSY; ++ /* Isolation is still required after a bounded RDY timeout. */ ++ /* Wi-Fi stays linked until its own shutdown callback has run. */ ++ apple_h9p_capture_park(pcie); ++ dev_info(dev, "phoenix: ANS/PCIe quiesced (controller found %d result %d)\n", ++ state.found, state.result); ++ return count; ++} ++static DEVICE_ATTR_WO(crash_quiesce); ++ ++static struct attribute *apple_h9p_crash_attrs[] = { ++ &dev_attr_crash_quiesce.attr, ++ NULL, ++}; ++ ++static const struct attribute_group apple_h9p_crash_group = { ++ .attrs = apple_h9p_crash_attrs, ++}; ++ ++ + static int apple_h9p_pcie_probe(struct platform_device *pdev) + { + struct device *dev = &pdev->dev; +@@ -3426,6 +3776,7 @@ static int apple_h9p_pcie_probe(struct platform_device *pdev) + u32 doorbell; + int ret; + ++ + bridge = devm_pci_alloc_host_bridge(dev, sizeof(*pcie)); + if (!bridge) + return -ENOMEM; +@@ -3578,6 +3929,13 @@ static int apple_h9p_pcie_probe(struct platform_device *pdev) + if (ret) + return ret; + ++ if (is_kdump_kernel() && of_device_is_compatible(pcie->dev->of_node, "apple,s8003-pcie")) { ++ ret = devm_device_add_group(dev, &apple_h9p_crash_group); ++ if (ret) ++ return ret; ++ } ++ ++ + if (pcie->late_ports) + schedule_delayed_work(&pcie->late_work, + msecs_to_jiffies(S8003_LATE_PORT_RETRY_MS)); +@@ -3660,6 +4018,7 @@ MODULE_DEVICE_TABLE(of, apple_h9p_pcie_of_match); + + static struct platform_driver apple_h9p_pcie_driver = { + .probe = apple_h9p_pcie_probe, ++ .shutdown = apple_h9p_s8003_shutdown, + .driver = { + .name = "pcie-apple-h9p", + .of_match_table = apple_h9p_pcie_of_match, +diff --git a/include/linux/apple-ans.h b/include/linux/apple-ans.h +index a59861010..10be3ddef 100644 +--- a/include/linux/apple-ans.h ++++ b/include/linux/apple-ans.h +@@ -17,11 +17,34 @@ struct device; + * Map @npages pages for FlatDMA request @tag, or retire the tag if @npages is + * zero. @to_memory: the controller writes the pages (an NVMe read). + */ ++#if IS_REACHABLE(CONFIG_APPLE_DART) ++int apple_ans_dart_stop(struct device *dev); ++#else ++static inline int apple_ans_dart_stop(struct device *dev) ++{ ++ return -EOPNOTSUPP; ++} ++#endif ++ + #if IS_REACHABLE(CONFIG_PCIE_APPLE_H9P) ++int apple_ans_handoff_begin(struct device *dev); ++int apple_ans_handoff_stopped(struct device *dev); ++int apple_ans_nvmmu_drain(struct device *dev); ++int apple_ans_nvmmu_reset(struct device *dev); ++int apple_ans_link_up(struct device *dev); ++bool apple_ans_is_s8003(struct device *dev); ++void apple_ans_host_dump(struct device *dev, const char *reason); + int apple_ans_nvmmu_map(struct device *dev, unsigned int tag, + const u64 *pages, unsigned int npages, + bool to_memory, dma_addr_t *iova); + #else ++static inline int apple_ans_handoff_begin(struct device *dev) { return -EBUSY; } ++static inline int apple_ans_handoff_stopped(struct device *dev) { return -EBUSY; } ++static inline int apple_ans_nvmmu_drain(struct device *dev) { return -EBUSY; } ++static inline int apple_ans_nvmmu_reset(struct device *dev) { return -EOPNOTSUPP; } ++static inline int apple_ans_link_up(struct device *dev) { return 1; } ++static inline bool apple_ans_is_s8003(struct device *dev) { return false; } ++static inline void apple_ans_host_dump(struct device *dev, const char *reason) {} + static inline int apple_ans_nvmmu_map(struct device *dev, unsigned int tag, + const u64 *pages, unsigned int npages, + bool to_memory, dma_addr_t *iova) diff --git a/kernel/patches/drivers/pci/0035-nvme-pci-keep-the-certified-ANS-kexec-handoff.patch b/kernel/patches/drivers/pci/0035-nvme-pci-keep-the-certified-ANS-kexec-handoff.patch new file mode 100644 index 00000000..62d5bda6 --- /dev/null +++ b/kernel/patches/drivers/pci/0035-nvme-pci-keep-the-certified-ANS-kexec-handoff.patch @@ -0,0 +1,337 @@ +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] nvme-pci: keep the certified ANS kexec handoff + +Resetting the ANS host or its translations destroys the state required +by the next kernel. Retain the stopped-DMA handoff only where the device +tree requests it. + +Assisted-by: LLM +--- + drivers/nvme/host/pci-apple-ans.c | 33 +++++++- + drivers/nvme/host/pci-internal.h | 2 + + drivers/nvme/host/pci.c | 128 ++++++++++++++++++++++++++++-- + 3 files changed, 155 insertions(+), 8 deletions(-) + +diff --git a/drivers/nvme/host/pci-apple-ans.c b/drivers/nvme/host/pci-apple-ans.c +index 5ba239e47..fdd9fa091 100644 +--- a/drivers/nvme/host/pci-apple-ans.c ++++ b/drivers/nvme/host/pci-apple-ans.c +@@ -84,6 +84,7 @@ struct apple_ans_nvme { + u32 cc; + unsigned long cc_jiffies; + bool cc_written; ++ bool host_fault; + dma_addr_t hmb_dma; + u32 hmb_size; + struct apple_ans_nvme_req req[APPLE_ANS_NVMMU_MAX_REQS]; +@@ -218,13 +219,20 @@ static int nvme_pci_apple_ans_reset_dma(struct nvme_dev *dev) + { + struct apple_ans_nvme *ans = nvme_pci_apple_ans(dev); + unsigned long flags; +- unsigned int tag; + int ret; + +- for (tag = 0; tag < APPLE_ANS_NVMMU_MAX_REQS; tag++) { +- ret = apple_ans_nvmmu_map(dev->dev, tag, NULL, 0, false, NULL); ++ if (ans->host == &apple_ans_host_s8003) { ++ ret = apple_ans_nvmmu_reset(dev->dev); + if (ret) + return ret; ++ } else { ++ unsigned int tag; ++ ++ for (tag = 0; tag < APPLE_ANS_NVMMU_MAX_REQS; tag++) { ++ ret = apple_ans_nvmmu_map(dev->dev, tag, NULL, 0, false, NULL); ++ if (ret) ++ return ret; ++ } + } + + spin_lock_irqsave(&ans->req_lock, flags); +@@ -326,10 +334,24 @@ static int nvme_pci_apple_ans_prepare_enable(struct nvme_dev *dev) + return 0; + } + ++static int nvme_pci_apple_ans_check_link(struct nvme_dev *dev) ++{ ++ struct apple_ans_nvme *ans = nvme_pci_apple_ans(dev); ++ ++ if (ans && ans->host == &apple_ans_host_s8003 && ++ (ans->host_fault || apple_ans_link_up(dev->dev) <= 0)) { ++ ans->host_fault = true; ++ return -EIO; ++ } ++ return 0; ++} ++ + static int nvme_pci_apple_ans_write_cc(struct nvme_dev *dev, u32 cc) + { + struct apple_ans_nvme *ans = nvme_pci_apple_ans(dev); + ++ if (nvme_pci_apple_ans_check_link(dev)) ++ return -EIO; + writel(cc, dev->bar + NVME_REG_CC); + if (!ans) + return 0; +@@ -349,6 +371,8 @@ static int nvme_pci_apple_ans_read_csts(struct nvme_dev *dev, u32 *csts) + struct apple_ans_nvme *ans = nvme_pci_apple_ans(dev); + unsigned long deadline; + ++ if (nvme_pci_apple_ans_check_link(dev)) ++ return -EIO; + *csts = readl(dev->bar + NVME_REG_CSTS); + if (!ans || !ans->cc_written || !ans->host->rdy_timeout_ms || + (ans->cc & NVME_CC_SHN_MASK)) +@@ -360,6 +384,8 @@ static int nvme_pci_apple_ans_read_csts(struct nvme_dev *dev, u32 *csts) + time_after(jiffies, deadline)) { + dev_err(dev->dev, "ANS RDY timeout (CC %#x, CSTS %#x)\n", + ans->cc, *csts); ++ ans->host_fault = true; ++ apple_ans_host_dump(dev->dev, "RDY deadline"); + return -ETIMEDOUT; + } + +@@ -419,6 +445,7 @@ static int nvme_pci_apple_ans_free_req(struct nvme_dev *dev, + dev_err_ratelimited(dev->dev, + "failed to release ANS DMA slot %u: %d\n", + tag, ret); ++ nvme_reset_ctrl(&dev->ctrl); + return ret; + } + +diff --git a/drivers/nvme/host/pci-internal.h b/drivers/nvme/host/pci-internal.h +index a62777abd..d3fe17867 100644 +--- a/drivers/nvme/host/pci-internal.h ++++ b/drivers/nvme/host/pci-internal.h +@@ -74,6 +74,8 @@ struct nvme_dev { + struct nvme_ctrl ctrl; + u32 last_ps; + bool hmb; ++ unsigned long apple_ans_flags; ++ atomic_t apple_ans_irq_count; + struct sg_table *hmb_sgt; + mempool_t *dmavec_mempool; + const struct nvme_pci_dma_ops *dma_ops; +diff --git a/drivers/nvme/host/pci.c b/drivers/nvme/host/pci.c +index 9ffe2fb74..40b2448d1 100644 +--- a/drivers/nvme/host/pci.c ++++ b/drivers/nvme/host/pci.c +@@ -5,6 +5,7 @@ + */ + + #include ++#include + #include + #include + #include +@@ -17,6 +18,10 @@ + #include + #include + #include ++#include ++#include ++#include ++#include + #include + #include + #include +@@ -1559,6 +1564,9 @@ static irqreturn_t nvme_irq(int irq, void *data) + DEFINE_IO_COMP_BATCH(iob); + bool found; + ++ if (!nvmeq->qid && dev->dma_ops == &nvme_pci_apple_ans_ops) ++ atomic_inc(&dev->apple_ans_irq_count); ++ + if (nvme_apple_ans_shared_vector(dev)) + found = nvme_apple_ans_irq_vector(dev, nvmeq->cq_vector, &iob); + else +@@ -1836,6 +1844,40 @@ static void nvme_warn_reset(struct nvme_dev *dev, u32 csts) + "Try \"nvme_core.default_ps_max_latency_us=0 pcie_aspm=off pcie_port_pm=off\" and report a bug\n"); + } + ++/* No endpoint MMIO/config or doorbell reads: safe first evidence for ANS. */ ++static void nvme_apple_ans_dump(struct nvme_dev *dev, const char *reason) ++{ ++ unsigned int i; ++ ++ dev_info(dev->dev, "Apple ANS: snapshot %s state %d online %u allocated %u vectors %u recovery_used %u\n", ++ reason, nvme_ctrl_state(&dev->ctrl), dev->online_queues, ++ dev->ctrl.queue_count, dev->num_vecs, ++ test_bit(0, &dev->apple_ans_flags)); ++ apple_ans_host_dump(dev->dev, reason); ++ for (i = 0; i < dev->ctrl.queue_count; i++) { ++ struct nvme_queue *q = &dev->queues[i]; ++ u16 head = READ_ONCE(q->cq_head); ++ struct nvme_completion *cqe; ++ int irq = -1; ++ ++ if (!q->cqes || head >= q->q_depth) ++ continue; ++ cqe = &q->cqes[head]; ++ if (test_bit(NVMEQ_ENABLED, &q->flags) && ++ !test_bit(NVMEQ_POLLED, &q->flags)) ++ irq = pci_irq_vector(to_pci_dev(dev->dev), q->cq_vector); ++ dev_info(dev->dev, "Apple ANS: q%u flags %#lx depth %u SQ tail %u/%u CQ head %u phase %u next status %#x cid %#x sqhead %u vector %u irq %d ANS admin IRQ actions %u SQ %pad CQ %pad\n", ++ i, READ_ONCE(q->flags), q->q_depth, ++ READ_ONCE(q->sq_tail), READ_ONCE(q->last_sq_tail), ++ head, READ_ONCE(q->cq_phase), ++ le16_to_cpu(READ_ONCE(cqe->status)), ++ le16_to_cpu(READ_ONCE(cqe->command_id)), ++ le16_to_cpu(READ_ONCE(cqe->sq_head)), q->cq_vector, ++ irq, atomic_read(&dev->apple_ans_irq_count), ++ &q->sq_dma_addr, &q->cq_dma_addr); ++ } ++} ++ + static enum blk_eh_timer_return nvme_timeout(struct request *req) + { + struct nvme_iod *iod = blk_mq_rq_to_pdu(req); +@@ -1844,8 +1886,18 @@ static enum blk_eh_timer_return nvme_timeout(struct request *req) + struct request *abort_req; + struct nvme_command cmd = { }; + struct pci_dev *pdev = to_pci_dev(dev->dev); +- u32 csts = readl(dev->bar + NVME_REG_CSTS); ++ u32 csts = ~0U; + u8 opcode; ++ bool ans = dev->dma_ops == &nvme_pci_apple_ans_ops && ++ apple_ans_is_s8003(dev->dev); ++ ++ if (ans) { ++ nvme_apple_ans_dump(dev, "timeout before CSTS"); ++ if (dev->dma_ops->read_csts(dev, &csts)) ++ goto disable; ++ } else { ++ csts = readl(dev->bar + NVME_REG_CSTS); ++ } + + /* + * Shutdown the device immediately if we see it is disconnected. This +@@ -1921,6 +1973,11 @@ static enum blk_eh_timer_return nvme_timeout(struct request *req) + * returned to the driver, or if this is the admin queue. + */ + opcode = nvme_req(req)->cmd->common.opcode; ++ if (ans) { ++ if (test_and_set_bit(0, &dev->apple_ans_flags)) ++ nvme_change_ctrl_state(&dev->ctrl, NVME_CTRL_DELETING); ++ goto disable; ++ } + if (!nvmeq->qid || (iod->flags & IOD_ABORTED)) { + dev_warn(dev->ctrl.device, + "I/O tag %d (%04x) opcode %#x (%s) QID %d timeout, reset controller\n", +@@ -2804,6 +2861,7 @@ static umode_t nvme_pci_attrs_are_visible(struct kobject *kobj, + return a->mode; + } + ++ + static struct attribute *nvme_pci_attrs[] = { + &dev_attr_cmb.attr, + &dev_attr_cmbloc.attr, +@@ -3283,11 +3341,46 @@ static bool nvme_pci_ctrl_is_dead(struct nvme_dev *dev) + return (csts & NVME_CSTS_CFS) || !(csts & NVME_CSTS_RDY); + } + +-static void nvme_dev_disable(struct nvme_dev *dev, bool shutdown) ++/* Certify stopped DMA before the kexec PCIe handoff. */ ++static int nvme_apple_ans_handoff_disable(struct nvme_dev *dev, int shutdown_ret) ++{ ++ struct pci_dev *pdev = to_pci_dev(dev->dev); ++ u32 csts = ~0U, cc, expected; ++ int ret, err; ++ ++ ret = dev->dma_ops->read_csts(dev, &csts); ++ if (shutdown_ret || ret || (csts & NVME_CSTS_CFS) || ++ (csts & NVME_CSTS_SHST_MASK) != NVME_CSTS_SHST_CMPLT) { ++ pci_clear_master(pdev); ++ return -EBUSY; ++ } ++ dev->ctrl.ctrl_config &= ~(NVME_CC_ENABLE | NVME_CC_SHN_MASK); ++ expected = dev->ctrl.ctrl_config; ++ ret = dev->dma_ops->write_cc(dev, expected); ++ if (!ret) { ++ ret = read_poll_timeout(dev->dma_ops->read_csts, err, ++ err || !(csts & NVME_CSTS_RDY), ++ 1000, 2000000, false, dev, &csts); ++ if (!ret) ++ ret = err; ++ } ++ if (ret) ++ goto block_dma; ++ cc = readl(dev->bar + NVME_REG_CC); ++ if ((csts & NVME_CSTS_CFS) || ++ (cc & (NVME_CC_ENABLE | NVME_CC_SHN_MASK))) ++ ret = -EBUSY; ++block_dma: ++ pci_clear_master(pdev); ++ return ret; ++} ++ ++static int nvme_dev_disable_paired(struct nvme_dev *dev, bool shutdown, bool paired) + { + enum nvme_ctrl_state state = nvme_ctrl_state(&dev->ctrl); + struct pci_dev *pdev = to_pci_dev(dev->dev); + bool dead; ++ int ret = paired ? -EBUSY : 0; + + mutex_lock(&dev->shutdown_lock); + dead = nvme_pci_ctrl_is_dead(dev); +@@ -3298,15 +3391,26 @@ static void nvme_dev_disable(struct nvme_dev *dev, bool shutdown) + * Give the controller a chance to complete all entered requests + * if doing a safe shutdown. + */ +- if (!dead && shutdown) +- nvme_wait_freeze_timeout(&dev->ctrl); ++ if (!dead && shutdown && nvme_wait_freeze_timeout(&dev->ctrl) <= 0 && paired) ++ panic("Apple ANS: outgoing I/O did not drain; refusing kexec"); + } + + nvme_quiesce_io_queues(&dev->ctrl); + ++ if (paired && (dead || !dev->ctrl.queue_count)) ++ panic("Apple ANS: dead outgoing controller; refusing kexec"); + if (!dead && dev->ctrl.queue_count > 0) { ++ if (paired) { ++ nvme_sync_io_queues(&dev->ctrl); ++ ret = apple_ans_nvmmu_drain(dev->dev); ++ if (ret) ++ panic("Apple ANS: outgoing drain failed %d; refusing kexec", ret); ++ } + nvme_delete_io_queues(dev); +- nvme_disable_ctrl(&dev->ctrl, shutdown); ++ dev_info(dev->dev, "Apple ANS: shutdown starting NVMe SHN paired=%u\n", paired); ++ ret = nvme_disable_ctrl(&dev->ctrl, shutdown); ++ if (paired) ++ ret = nvme_apple_ans_handoff_disable(dev, ret); + nvme_poll_irqdisable(&dev->queues[0]); + } + nvme_suspend_io_queues(dev); +@@ -3329,7 +3433,15 @@ static void nvme_dev_disable(struct nvme_dev *dev, bool shutdown) + if (dev->ctrl.admin_q && !blk_queue_dying(dev->ctrl.admin_q)) + nvme_unquiesce_admin_queue(&dev->ctrl); + } ++ if (paired && !ret) ++ ret = apple_ans_handoff_stopped(dev->dev); + mutex_unlock(&dev->shutdown_lock); ++ return ret; ++} ++ ++static void nvme_dev_disable(struct nvme_dev *dev, bool shutdown) ++{ ++ nvme_dev_disable_paired(dev, shutdown, false); + } + + static int nvme_disable_prepare_reset(struct nvme_dev *dev, bool shutdown) +@@ -3920,6 +4032,12 @@ static void nvme_shutdown(struct pci_dev *pdev) + { + struct nvme_dev *dev = pci_get_drvdata(pdev); + ++ if (kexec_in_progress && apple_ans_is_s8003(dev->dev) && ++ dev->dma_ops == &nvme_pci_apple_ans_ops && !apple_ans_handoff_begin(dev->dev)) { ++ if (!nvme_wait_reset(&dev->ctrl) || nvme_dev_disable_paired(dev, true, true)) ++ panic("Apple ANS: refusing unsafe powered kexec handoff"); ++ return; ++ } + nvme_disable_prepare_reset(dev, true); + } + diff --git a/kernel/patches/drivers/pci/0036-PCI-apple-cold-reset-after-an-uncertified-kexec-hand.patch b/kernel/patches/drivers/pci/0036-PCI-apple-cold-reset-after-an-uncertified-kexec-hand.patch new file mode 100644 index 00000000..f82f0350 --- /dev/null +++ b/kernel/patches/drivers/pci/0036-PCI-apple-cold-reset-after-an-uncertified-kexec-hand.patch @@ -0,0 +1,85 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Thu, 12 Nov 2026 00:43:07 +0100 +Subject: [PATCH] PCI: apple: cold-reset after an uncertified kexec handoff + +The warm guard sets panic_timeout to zero. Neither an uncertified ANS +stop nor failed PERST, LTSSM stop or powered-port certificate can safely +continue into the incoming kernel. Flush the reason and request a cold +watchdog reset instead of hanging in panic. Return without further port +writes if restart unexpectedly returns. + +Assisted-by: LLM +--- + drivers/pci/controller/pcie-apple-h9p.c | 9 +++++---- + include/linux/soc/apple/cold-reset.h | 19 +++++++++++++++++++ + 2 files changed, 24 insertions(+), 4 deletions(-) + create mode 100644 include/linux/soc/apple/cold-reset.h + +diff --git a/drivers/pci/controller/pcie-apple-h9p.c b/drivers/pci/controller/pcie-apple-h9p.c +index f0095394a..28305b0ee 100644 +--- a/drivers/pci/controller/pcie-apple-h9p.c ++++ b/drivers/pci/controller/pcie-apple-h9p.c +@@ -49,6 +49,7 @@ + #include + #include + #include ++#include + + #include "pci-host-common.h" + +@@ -3609,7 +3610,7 @@ static void apple_h9p_s8003_shutdown(struct platform_device *pdev) + if (!READ_ONCE(pcie->ans_handoff_stopped) || + (of_property_read_bool(pcie->dev->of_node, "apple,ans-kexec-handoff") && + !apple_s8003_port_powered(pcie))) +- panic("Apple ANS: refusing uncertified powered handoff"); ++ return apple_cold_reset("Apple ANS: refusing uncertified powered handoff"); + for (port = 0; port < H9P_NUM_PORTS; port++) { + void __iomem *cfg = pcie->base_config + port * H9P_CFG_PORT_STRIDE; + +@@ -3621,9 +3622,9 @@ static void apple_h9p_s8003_shutdown(struct platform_device *pdev) + h9p_rmw(pcie->base_rc + S8003_RC_PORT_PWR(port), 0, + S8003_RC_PORT_PWR_REFCLK_NOGATE); + if (gpiod_direction_output(pcie->perst[port], 1)) +- panic("Apple PCIe: PERST failed during handoff"); ++ return apple_cold_reset("Apple PCIe: PERST failed during handoff"); + if (apple_s8003_port_stop(pcie, port)) +- panic("Apple PCIe: LTSSM stop failed during handoff"); ++ return apple_cold_reset("Apple PCIe: LTSSM stop failed during handoff"); + udelay(S8003_PERST_TO_REFCLK_US); + if (!port && pcie->nvmmu.ready) { + h9p_writel_flush(0, pcie->nvmmu.base + H9P_NVMMU_SART_CTRL); +@@ -3635,7 +3636,7 @@ static void apple_h9p_s8003_shutdown(struct platform_device *pdev) + if (!port && of_property_read_bool(pcie->dev->of_node, "apple,ans-kexec-handoff") && + (!apple_s8003_port_powered(pcie) || + gpiod_get_value_cansleep(pcie->perst[0]) != 1)) +- panic("Apple ANS: outgoing powered port certificate failed"); ++ return apple_cold_reset("Apple ANS: outgoing powered port certificate failed"); + pcie->enabled_ports &= ~BIT(port); + } + dev_info(pcie->dev, "kexec: PCIe PERST asserted, LTSSM stopped, ports powered; NVMMU SART disabled\n"); +diff --git a/include/linux/soc/apple/cold-reset.h b/include/linux/soc/apple/cold-reset.h +new file mode 100644 +index 000000000..34467be50 +--- /dev/null ++++ b/include/linux/soc/apple/cold-reset.h +@@ -0,0 +1,19 @@ ++/* SPDX-License-Identifier: GPL-2.0-only */ ++#ifndef _LINUX_SOC_APPLE_COLD_RESET_H ++#define _LINUX_SOC_APPLE_COLD_RESET_H ++ ++#include ++#include ++#include ++ ++/* Shutdown has no safe return to a warm kernel after an unproven DMA stop. */ ++static inline void apple_cold_reset(const char *reason) ++{ ++ pr_emerg("Apple unsafe handover: %s; forcing cold watchdog reset\n", reason); ++ pr_flush(1000, true); ++ kmsg_dump_desc(KMSG_DUMP_PANIC, reason); ++ WRITE_ONCE(reboot_mode, REBOOT_COLD); ++ emergency_restart(); ++} ++ ++#endif diff --git a/kernel/patches/drivers/pci/0037-nvme-pci-cold-reset-after-unsafe-ANS-kexec-shutdown.patch b/kernel/patches/drivers/pci/0037-nvme-pci-cold-reset-after-unsafe-ANS-kexec-shutdown.patch new file mode 100644 index 00000000..5075181b --- /dev/null +++ b/kernel/patches/drivers/pci/0037-nvme-pci-cold-reset-after-unsafe-ANS-kexec-shutdown.patch @@ -0,0 +1,76 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Thu, 12 Nov 2026 02:08:51 +0100 +Subject: [PATCH] nvme-pci: cold-reset after unsafe ANS kexec shutdown + +An unsafe ANS shutdown cannot panic under a zero panic timeout. Use the +shared logged and flushed cold-reset path for each failed certificate. +If reset unexpectedly returns, leave without further device operations. + +Assisted-by: LLM +--- + drivers/nvme/host/pci.c | 29 +++++++++++++++++++++-------- + 1 file changed, 21 insertions(+), 8 deletions(-) + +diff --git a/drivers/nvme/host/pci.c b/drivers/nvme/host/pci.c +index 40b2448d1..2f356a8ce 100644 +--- a/drivers/nvme/host/pci.c ++++ b/drivers/nvme/host/pci.c +@@ -22,6 +22,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -3391,20 +3392,30 @@ static int nvme_dev_disable_paired(struct nvme_dev *dev, bool shutdown, bool pai + * Give the controller a chance to complete all entered requests + * if doing a safe shutdown. + */ +- if (!dead && shutdown && nvme_wait_freeze_timeout(&dev->ctrl) <= 0 && paired) +- panic("Apple ANS: outgoing I/O did not drain; refusing kexec"); ++ if (!dead && shutdown && nvme_wait_freeze_timeout(&dev->ctrl) <= 0 && paired) { ++ apple_cold_reset("Apple ANS: outgoing I/O did not drain"); ++ mutex_unlock(&dev->shutdown_lock); ++ return -EIO; ++ } + } + + nvme_quiesce_io_queues(&dev->ctrl); + +- if (paired && (dead || !dev->ctrl.queue_count)) +- panic("Apple ANS: dead outgoing controller; refusing kexec"); ++ if (paired && (dead || !dev->ctrl.queue_count)) { ++ apple_cold_reset("Apple ANS: dead outgoing controller"); ++ mutex_unlock(&dev->shutdown_lock); ++ return -EIO; ++ } + if (!dead && dev->ctrl.queue_count > 0) { + if (paired) { + nvme_sync_io_queues(&dev->ctrl); + ret = apple_ans_nvmmu_drain(dev->dev); +- if (ret) +- panic("Apple ANS: outgoing drain failed %d; refusing kexec", ret); ++ if (ret) { ++ dev_err(dev->dev, "Apple ANS: outgoing drain failed %d\n", ret); ++ apple_cold_reset("Apple ANS: outgoing drain failed"); ++ mutex_unlock(&dev->shutdown_lock); ++ return -EIO; ++ } + } + nvme_delete_io_queues(dev); + dev_info(dev->dev, "Apple ANS: shutdown starting NVMe SHN paired=%u\n", paired); +@@ -4034,8 +4045,10 @@ static void nvme_shutdown(struct pci_dev *pdev) + + if (kexec_in_progress && apple_ans_is_s8003(dev->dev) && + dev->dma_ops == &nvme_pci_apple_ans_ops && !apple_ans_handoff_begin(dev->dev)) { +- if (!nvme_wait_reset(&dev->ctrl) || nvme_dev_disable_paired(dev, true, true)) +- panic("Apple ANS: refusing unsafe powered kexec handoff"); ++ if (!nvme_wait_reset(&dev->ctrl) || nvme_dev_disable_paired(dev, true, true)) { ++ apple_cold_reset("Apple ANS: refusing unsafe powered kexec handoff"); ++ return; ++ } + return; + } + nvme_disable_prepare_reset(dev, true); diff --git a/kernel/patches/drivers/pci/0038-PCI-apple-use-explicit-returns-after-cold-reset.patch b/kernel/patches/drivers/pci/0038-PCI-apple-use-explicit-returns-after-cold-reset.patch new file mode 100644 index 00000000..5b49b55d --- /dev/null +++ b/kernel/patches/drivers/pci/0038-PCI-apple-use-explicit-returns-after-cold-reset.patch @@ -0,0 +1,62 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Thu, 12 Nov 2026 02:08:51 +0100 +Subject: [PATCH] PCI: apple: use explicit returns after cold reset + +Use standard C void returns while retaining the early exit if the reset +helper unexpectedly returns. Successful handoff ordering is unchanged. + +Assisted-by: LLM +--- + drivers/pci/controller/pcie-apple-h9p.c | 24 ++++++++++++++++-------- + 1 file changed, 16 insertions(+), 8 deletions(-) + +diff --git a/drivers/pci/controller/pcie-apple-h9p.c b/drivers/pci/controller/pcie-apple-h9p.c +index 28305b0ee..5890d632a 100644 +--- a/drivers/pci/controller/pcie-apple-h9p.c ++++ b/drivers/pci/controller/pcie-apple-h9p.c +@@ -3609,8 +3609,10 @@ static void apple_h9p_s8003_shutdown(struct platform_device *pdev) + cancel_delayed_work_sync(&pcie->late_work); + if (!READ_ONCE(pcie->ans_handoff_stopped) || + (of_property_read_bool(pcie->dev->of_node, "apple,ans-kexec-handoff") && +- !apple_s8003_port_powered(pcie))) +- return apple_cold_reset("Apple ANS: refusing uncertified powered handoff"); ++ !apple_s8003_port_powered(pcie))) { ++ apple_cold_reset("Apple ANS: refusing uncertified powered handoff"); ++ return; ++ } + for (port = 0; port < H9P_NUM_PORTS; port++) { + void __iomem *cfg = pcie->base_config + port * H9P_CFG_PORT_STRIDE; + +@@ -3621,10 +3623,14 @@ static void apple_h9p_s8003_shutdown(struct platform_device *pdev) + h9p_rmw(cfg + PCI_PRIMARY_BUS, GENMASK(23, 8), 0); + h9p_rmw(pcie->base_rc + S8003_RC_PORT_PWR(port), 0, + S8003_RC_PORT_PWR_REFCLK_NOGATE); +- if (gpiod_direction_output(pcie->perst[port], 1)) +- return apple_cold_reset("Apple PCIe: PERST failed during handoff"); +- if (apple_s8003_port_stop(pcie, port)) +- return apple_cold_reset("Apple PCIe: LTSSM stop failed during handoff"); ++ if (gpiod_direction_output(pcie->perst[port], 1)) { ++ apple_cold_reset("Apple PCIe: PERST failed during handoff"); ++ return; ++ } ++ if (apple_s8003_port_stop(pcie, port)) { ++ apple_cold_reset("Apple PCIe: LTSSM stop failed during handoff"); ++ return; ++ } + udelay(S8003_PERST_TO_REFCLK_US); + if (!port && pcie->nvmmu.ready) { + h9p_writel_flush(0, pcie->nvmmu.base + H9P_NVMMU_SART_CTRL); +@@ -3635,8 +3641,10 @@ static void apple_h9p_s8003_shutdown(struct platform_device *pdev) + apple_s8003_port_off(pcie, port); + if (!port && of_property_read_bool(pcie->dev->of_node, "apple,ans-kexec-handoff") && + (!apple_s8003_port_powered(pcie) || +- gpiod_get_value_cansleep(pcie->perst[0]) != 1)) +- return apple_cold_reset("Apple ANS: outgoing powered port certificate failed"); ++ gpiod_get_value_cansleep(pcie->perst[0]) != 1)) { ++ apple_cold_reset("Apple ANS: outgoing powered port certificate failed"); ++ return; ++ } + pcie->enabled_ports &= ~BIT(port); + } + dev_info(pcie->dev, "kexec: PCIe PERST asserted, LTSSM stopped, ports powered; NVMMU SART disabled\n"); From 8286fdfd11cc5bb8c2cd41fc273c2e6c91f4e5af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= Date: Thu, 12 Nov 2026 09:00:00 +0100 Subject: [PATCH 10/10] pinctrl/spi/input: Quiesce S8000 peripherals for kexec After a kexec the next kernel found the S8000 GPIO controller with interrupt modes armed by the previous kernel, the PIO SPI controller with queued transfers and an asserted chip select, and the touch controller still running. An orphaned GPIO interrupt fires forever and touch did not come back. Retire inherited GPIO interrupt modes before the chained parent interrupts are enabled, drain and idle the S8000 PIO SPI controller on shutdown and at probe, and stop the Apple Z2 touch controller before its SPI parent shuts down. Assisted-by: LLM --- ...top-the-touch-controller-on-shutdown.patch | 48 +++++++ ...io-retire-inherited-GPIO-interrupt-m.patch | 130 ++++++++++++++++++ ...and-idle-the-S8000-PIO-controller-on.patch | 122 ++++++++++++++++ 3 files changed, 300 insertions(+) create mode 100644 kernel/patches/drivers/input/0030-Input-apple-z2-stop-the-touch-controller-on-shutdown.patch create mode 100644 kernel/patches/drivers/pinctrl/0004-pinctrl-apple-gpio-retire-inherited-GPIO-interrupt-m.patch create mode 100644 kernel/patches/drivers/spi/0009-spi-apple-drain-and-idle-the-S8000-PIO-controller-on.patch diff --git a/kernel/patches/drivers/input/0030-Input-apple-z2-stop-the-touch-controller-on-shutdown.patch b/kernel/patches/drivers/input/0030-Input-apple-z2-stop-the-touch-controller-on-shutdown.patch new file mode 100644 index 00000000..88cd9ce1 --- /dev/null +++ b/kernel/patches/drivers/input/0030-Input-apple-z2-stop-the-touch-controller-on-shutdown.patch @@ -0,0 +1,48 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Sun, 8 Nov 2026 16:00:00 +0100 +Subject: [PATCH] Input: apple-z2: stop the touch controller on shutdown + +A kexec hands the next kernel an S8000 SPI bus that the touch controller +may still be using: its interrupt thread can run and the SPI core can +still hold its messages. + +Add a shutdown callback that stops the controller and waits for its +interrupt, also when the threaded handler has already masked itself. SPI +children shut down before their controller, so the SPI driver can then +drain its queue. + +Assisted-by: LLM +--- + drivers/input/touchscreen/apple_z2.c | 11 +++++++++++ + 1 file changed, 11 insertions(+) + +diff --git a/drivers/input/touchscreen/apple_z2.c b/drivers/input/touchscreen/apple_z2.c +index 92a04edcd..5dcf3579d 100644 +--- a/drivers/input/touchscreen/apple_z2.c ++++ b/drivers/input/touchscreen/apple_z2.c +@@ -4599,6 +4599,16 @@ static struct spi_device_id apple_z2_of_id[] = { + }; + MODULE_DEVICE_TABLE(spi, apple_z2_of_id); + ++/* Children stop before the SPI controller drains its core queue. */ ++static void apple_z2_shutdown(struct spi_device *spi) ++{ ++ struct apple_z2 *z2 = spi_get_drvdata(spi); ++ ++ apple_z2_stop(z2); ++ /* Also wait when the IRQ thread already masked itself. */ ++ synchronize_irq(spi->irq); ++} ++ + static struct spi_driver apple_z2_driver = { + .driver = { + .name = "apple-z2", +@@ -4609,6 +4619,7 @@ static struct spi_driver apple_z2_driver = { + .id_table = apple_z2_of_id, + .probe = apple_z2_probe, + .remove = apple_z2_remove, ++ .shutdown = apple_z2_shutdown, + }; + + module_spi_driver(apple_z2_driver); diff --git a/kernel/patches/drivers/pinctrl/0004-pinctrl-apple-gpio-retire-inherited-GPIO-interrupt-m.patch b/kernel/patches/drivers/pinctrl/0004-pinctrl-apple-gpio-retire-inherited-GPIO-interrupt-m.patch new file mode 100644 index 00000000..dc5b66a6 --- /dev/null +++ b/kernel/patches/drivers/pinctrl/0004-pinctrl-apple-gpio-retire-inherited-GPIO-interrupt-m.patch @@ -0,0 +1,130 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Sun, 8 Nov 2026 16:00:00 +0100 +Subject: [PATCH] pinctrl: apple-gpio: retire inherited GPIO interrupt modes + after kexec + +After a kexec, or after a panic that ran no shutdown callbacks, GPIO +pins can still be in an interrupt mode set up by the previous kernel. +Once the chained parent interrupts are enabled, such a pin raises an +interrupt that nobody maps or acknowledges, and it keeps firing. + +On apple,s8000-pinctrl, switch every GPIO interrupt mode off at probe, +before the parent interrupts are enabled, and keep outputs, mux, pulls, +data and grouping. Consumers arm their own pins again later. In the +handler, a pending pin without a mapping is masked and its group latch +cleared, with a rate-limited warning. + +Assisted-by: LLM +--- + drivers/pinctrl/pinctrl-apple-gpio.c | 62 +++++++++++++++++++++++++++- + 1 file changed, 60 insertions(+), 2 deletions(-) + +diff --git a/drivers/pinctrl/pinctrl-apple-gpio.c b/drivers/pinctrl/pinctrl-apple-gpio.c +index b97023c97..766fcf5bc 100644 +--- a/drivers/pinctrl/pinctrl-apple-gpio.c ++++ b/drivers/pinctrl/pinctrl-apple-gpio.c +@@ -19,6 +19,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -46,6 +47,7 @@ struct apple_gpio_pinctrl { + + void __iomem *base; + struct regmap *map; ++ bool retire_irqs; + + struct pinctrl_desc pinctrl_desc; + struct gpio_chip gpio_chip; +@@ -379,8 +381,33 @@ static void apple_gpio_irq_handler(struct irq_desc *desc) + chained_irq_enter(chip, desc); + for (pinh = 0; pinh < gc->ngpio; pinh += 32) { + pending = readl_relaxed(pctl->base + REG_IRQ(*grpp, pinh)); +- for_each_set_bit(pinl, &pending, 32) +- generic_handle_domain_irq(gc->irq.domain, pinh + pinl); ++ for_each_set_bit(pinl, &pending, 32) { ++ unsigned int pin = pinh + pinl; ++ int ret; ++ ++ ret = pin < gc->ngpio ? ++ generic_handle_domain_irq(gc->irq.domain, pin) : -EINVAL; ++ if (ret && pctl->retire_irqs) { ++ /* An inherited, unmapped source has nobody to ack it. ++ * Mask only this GPIO, then clear this group's latch. ++ */ ++ if (pin < gc->ngpio) { ++ u32 reg = apple_gpio_get_reg(pctl, pin); ++ u32 mode = FIELD_GET(REG_GPIOx_MODE, reg); ++ u32 off = FIELD_PREP(REG_GPIOx_MODE, ++ REG_GPIOx_IN_IRQ_OFF); ++ ++ if (!(reg & REG_GPIOx_PERIPH) && ++ mode >= REG_GPIOx_IN_IRQ_HI && ++ mode <= REG_GPIOx_IN_IRQ_ANY) ++ apple_gpio_set_reg(pctl, pin, REG_GPIOx_MODE, off); ++ } ++ writel(BIT(pinl), pctl->base + REG_IRQ(*grpp, pinh)); ++ dev_warn_ratelimited(pctl->dev, ++ "masked orphan inherited GPIO IRQ group=%u pin=%u\n", ++ *grpp, pin); ++ } ++ } + } + chained_irq_exit(chip, desc); + } +@@ -403,6 +430,35 @@ static const struct irq_chip apple_gpio_irqchip = { + + /* Probe & register */ + ++static void apple_gpio_quiesce_irqs(struct apple_gpio_pinctrl *pctl) ++{ ++ struct gpio_chip *gc = &pctl->gpio_chip; ++ unsigned int pin, group, mode, masked = 0; ++ u32 reg; ++ ++ if (!pctl->retire_irqs) ++ return; ++ /* Before chained parent IRQs are enabled, retire old GPIO IRQ modes. ++ * Preserve outputs, mux, pulls, data and grouping. Consumers rearm their ++ * own pins later; panic entry need not have run any shutdown callbacks. ++ */ ++ for (pin = 0; pin < gc->ngpio; pin++) { ++ reg = apple_gpio_get_reg(pctl, pin); ++ mode = FIELD_GET(REG_GPIOx_MODE, reg); ++ if (!(reg & REG_GPIOx_PERIPH) && mode >= REG_GPIOx_IN_IRQ_HI && ++ mode <= REG_GPIOx_IN_IRQ_ANY) { ++ apple_gpio_set_reg(pctl, pin, REG_GPIOx_MODE, ++ FIELD_PREP(REG_GPIOx_MODE, REG_GPIOx_IN_IRQ_OFF)); ++ masked++; ++ } ++ } ++ for (group = 0; group < gc->irq.num_parents; group++) ++ for (pin = 0; pin < gc->ngpio; pin += 32) ++ writel(GENMASK(min(32U, gc->ngpio - pin) - 1, 0), ++ pctl->base + REG_IRQ(group, pin)); ++ dev_info(pctl->dev, "inherited GPIO IRQ baseline: masked %u inherited inputs\n", masked); ++} ++ + static int apple_gpio_register(struct apple_gpio_pinctrl *pctl) + { + struct gpio_irq_chip *girq = &pctl->gpio_chip.irq; +@@ -452,6 +508,7 @@ static int apple_gpio_register(struct apple_gpio_pinctrl *pctl) + girq->handler = handle_level_irq; + } + ++ apple_gpio_quiesce_irqs(pctl); + ret = devm_gpiochip_add_data(pctl->dev, &pctl->gpio_chip, pctl); + + out_free_irq_data: +@@ -485,6 +542,7 @@ static int apple_gpio_pinctrl_probe(struct platform_device *pdev) + if (!pctl) + return -ENOMEM; + pctl->dev = &pdev->dev; ++ pctl->retire_irqs = of_device_is_compatible(pdev->dev.of_node, "apple,s8000-pinctrl"); + pctl->soc = of_device_get_match_data(&pdev->dev); + pctl->gpio_chip.irq.num_parents = nirqs; + dev_set_drvdata(&pdev->dev, pctl); diff --git a/kernel/patches/drivers/spi/0009-spi-apple-drain-and-idle-the-S8000-PIO-controller-on.patch b/kernel/patches/drivers/spi/0009-spi-apple-drain-and-idle-the-S8000-PIO-controller-on.patch new file mode 100644 index 00000000..daa284cf --- /dev/null +++ b/kernel/patches/drivers/spi/0009-spi-apple-drain-and-idle-the-S8000-PIO-controller-on.patch @@ -0,0 +1,122 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Sun, 8 Nov 2026 16:00:00 +0100 +Subject: [PATCH] spi: apple: drain and idle the S8000 PIO controller on + shutdown + +On S8000 the SPI controller runs in PIO mode. A kexec left it with +queued transfers, counters and an asserted chip select, and the next +kernel probed it in that state. + +On shutdown, suspend the SPI core queue, deassert the GPIO chip selects +and write the PIO registers back to idle while the controller is +powered. Do the same at probe, so that a controller inherited in any +state starts idle. Other SoCs use different DMA and interrupt registers +and keep their current behaviour. + +Assisted-by: LLM +--- + drivers/spi/spi-apple.c | 48 +++++++++++++++++++++++++++++++++++++++++ + 1 file changed, 48 insertions(+) + +diff --git a/drivers/spi/spi-apple.c b/drivers/spi/spi-apple.c +index dc5154e1b..ed9f67a59 100644 +--- a/drivers/spi/spi-apple.c ++++ b/drivers/spi/spi-apple.c +@@ -15,6 +15,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -141,6 +142,7 @@ struct apple_spi { + struct dmaengine_result dma_result; + const struct apple_spi_hw *hw; + u32 word_delay; ++ struct spi_controller *ctlr; + }; + + static inline void reg_write(struct apple_spi *spi, int offset, u32 value) +@@ -231,6 +233,19 @@ static void apple_spi_init(struct apple_spi *spi) + reg_write(spi, APPLE_SPI_DELAY_POST, 0); + } + ++/* Lab 0015: normalize only the S8000 PIO register set, while powered. */ ++static int apple_spi_s8000_idle(struct apple_spi *spi) ++{ ++ reg_write(spi, APPLE_SPI_CTRL, 0); ++ reg_write(spi, APPLE_SPI_CFG, spi->hw->cfg_base); ++ reg_write(spi, APPLE_SPI_RXCNT, 0); ++ reg_write(spi, APPLE_SPI_TXCNT, 0); ++ reg_write(spi, APPLE_SPI_PIN, APPLE_SPI_PIN_CS); ++ apple_spi_t8010_clear_status(spi); ++ /* Complete posted writes before publishing children or leaving Linux. */ ++ return readl(spi->regs + APPLE_SPI_CTRL) ? -EIO : 0; ++} ++ + static int apple_spi_prepare_message(struct spi_controller *ctlr, + struct spi_message *msg) + { +@@ -1120,6 +1135,7 @@ static int apple_spi_probe(struct platform_device *pdev) + + spi = spi_controller_get_devdata(ctlr); + platform_set_drvdata(pdev, spi); ++ spi->ctlr = ctlr; + init_completion(&spi->done); + init_completion(&spi->dma_done); + spi->hw = of_device_get_match_data(&pdev->dev); +@@ -1214,6 +1230,11 @@ static int apple_spi_probe(struct platform_device *pdev) + } + + apple_spi_init(spi); ++ if (spi->hw->pio_only) { ++ ret = apple_spi_s8000_idle(spi); ++ if (ret) ++ return ret; ++ } + + ret = devm_spi_register_controller(&pdev->dev, ctlr); + if (ret < 0) +@@ -1223,6 +1244,32 @@ static int apple_spi_probe(struct platform_device *pdev) + return 0; + } + ++static void apple_spi_shutdown(struct platform_device *pdev) ++{ ++ struct apple_spi *spi = platform_get_drvdata(pdev); ++ struct spi_controller *ctlr = spi->ctlr; ++ unsigned int cs; ++ int ret; ++ ++ /* Other SoCs use different DMA/IRQ registers and keep their policy. */ ++ if (!spi->hw->pio_only) ++ return; ++ /* Children have shut down; drain the core queue before MMIO. */ ++ ret = spi_controller_suspend(ctlr); ++ if (!ret) ++ ret = pm_runtime_resume_and_get(&pdev->dev); ++ if (ret >= 0) { ++ if (ctlr->cs_gpiods) ++ for (cs = 0; cs < ctlr->num_chipselect; cs++) ++ gpiod_set_value_cansleep(ctlr->cs_gpiods[cs], 0); ++ ret = apple_spi_s8000_idle(spi); ++ } ++ if (ret < 0 && kexec_in_progress) ++ panic("S8000 SPI shutdown failed before kexec: %d", ret); ++ /* Keep runtime power pinned until the jump. */ ++ dev_info(&pdev->dev, "S8000 SPI shutdown result %d\n", ret); ++} ++ + static const struct apple_spi_hw apple_spi_hw_s8000 = { + .legacy_status = true, + .pio_only = true, +@@ -1260,6 +1307,7 @@ MODULE_DEVICE_TABLE(of, apple_spi_of_match); + + static struct platform_driver apple_spi_driver = { + .probe = apple_spi_probe, ++ .shutdown = apple_spi_shutdown, + .driver = { + .name = "apple-spi", + .of_match_table = apple_spi_of_match,