diff --git a/projects/ROCKNIX/devices/SM8550/linux/dts/qcom/qcs8550-ayaneo-pocketevo.dts b/projects/ROCKNIX/devices/SM8550/linux/dts/qcom/qcs8550-ayaneo-pocketevo.dts index 6a00e935a7c..d060c4d279d 100644 --- a/projects/ROCKNIX/devices/SM8550/linux/dts/qcom/qcs8550-ayaneo-pocketevo.dts +++ b/projects/ROCKNIX/devices/SM8550/linux/dts/qcom/qcs8550-ayaneo-pocketevo.dts @@ -107,3 +107,14 @@ }; }; }; + +/* + * The tacho interrupt fires about 80 times a second whenever the fan spins and + * repeatedly pulls the SoC back out of deep suspend. Overridden here rather + * than in qcs8550-ayaneo-pocket-common.dtsi, which six other boards include. + * Costs hwmon fan1_input RPM reporting, as on the AYN Odin 3. + */ +&pwm_fan { + /delete-property/ interrupts; + /delete-property/ interrupt-parent; +}; diff --git a/projects/ROCKNIX/devices/SM8550/linux/dts/qcom/qcs8550-ayn-common.dtsi b/projects/ROCKNIX/devices/SM8550/linux/dts/qcom/qcs8550-ayn-common.dtsi index 156dff38b7f..785ebe2e234 100644 --- a/projects/ROCKNIX/devices/SM8550/linux/dts/qcom/qcs8550-ayn-common.dtsi +++ b/projects/ROCKNIX/devices/SM8550/linux/dts/qcom/qcs8550-ayn-common.dtsi @@ -598,6 +598,12 @@ regulator-min-microvolt = <2720000>; regulator-max-microvolt = <3960000>; regulator-initial-mode = ; + + /* Gamepad MCU rail; the rpmh suspend-state patch turns this + * into a SLEEP-set vote so the MCU powers off across suspend. */ + regulator-state-mem { + regulator-off-in-suspend; + }; }; vreg_l2b_3p0: ldo2 { @@ -1096,7 +1102,9 @@ }; &pcieport0 { - wake-gpios = <&tlmm 96 GPIO_ACTIVE_HIGH>; + /* WAKE# is active-low per PCIe CEM; ACTIVE_HIGH reads as permanently + * asserted, so the WLAN endpoint could never signal wake cleanly. */ + wake-gpios = <&tlmm 96 GPIO_ACTIVE_LOW>; reset-gpios = <&tlmm 94 GPIO_ACTIVE_LOW>; wifi@0 { diff --git a/projects/ROCKNIX/devices/SM8550/options b/projects/ROCKNIX/devices/SM8550/options index f9e1fdd77ec..7b0f002e1c4 100644 --- a/projects/ROCKNIX/devices/SM8550/options +++ b/projects/ROCKNIX/devices/SM8550/options @@ -28,7 +28,7 @@ KERNEL_MAKE_EXTRACMD=" $(get_kernel_make_extracmd)" # Kernel cmdline - EXTRA_CMDLINE="quiet rootwait console=tty0 allow_mismatched_32bit_el0 fw_devlink.strict=1 pcie_ports=compat irqaffinity=0-2 cgroup.memory=nokmem,nosocket nosoftlockup usbcore.interrupt_interval_override=045e:028e:2" + EXTRA_CMDLINE="quiet rootwait console=tty0 allow_mismatched_32bit_el0 fw_devlink.strict=1 pcie_ports=compat irqaffinity=0-2 cgroup.memory=nokmem,nosocket nosoftlockup usbcore.interrupt_interval_override=045e:028e:2 ufshcd_core.uic_cmd_timeout=3000 mem_sleep_default=deep" # Bootloader to use (syslinux / u-boot) BOOTLOADER="qcom-abl" diff --git a/projects/ROCKNIX/devices/SM8550/patches/linux/0201-scsi-ufs-drain-relink-completions-out-of-band-pm.patch.disabled b/projects/ROCKNIX/devices/SM8550/patches/linux/0201-scsi-ufs-drain-relink-completions-out-of-band-pm.patch.disabled new file mode 100644 index 00000000000..6a023486de4 --- /dev/null +++ b/projects/ROCKNIX/devices/SM8550/patches/linux/0201-scsi-ufs-drain-relink-completions-out-of-band-pm.patch.disabled @@ -0,0 +1,424 @@ +From: jaewun +Subject: [PATCH] scsi: ufs: drain relink completions during PM + +On this non-MCQ controller the UFS interrupt is a threaded IRQF_ONESHOT +line. If the threaded handler is left pending across a PM transition, +genirq keeps the line masked and ufshcd_intr() is not re-entered. UIC and +UTP completions from the resume relink can then remain latched in +REG_INTERRUPT_STATUS while the relink waits for completion. + +Drain those completions without depending on a new UFS IRQ: + + - Use ufshcd_relinking() to cover both PM relink and the explicit + error-handler reset window. + - Use disable_irq_nosync() while relinking so the PM/EH path does not + wait on the threaded handler it is bypassing. + - Drain UTP/UIC/TMF completions from the hardirq PM path and from a PM + poll timer, and read-clear UIC error-code registers after errors. + - Complete failed DME_LINK_STARTUP UIC commands with + UIC_CMD_RESULT_FAILURE so link startup can retry. + - On non-MCQ reset completion, requeue all outstanding SCSI commands + instead of trusting stale post-reset OCS state. + +Also recover a failed SW clk-gating hibern8 exit inline. That path runs +outside pm_op_in_progress during the freezer phase; scheduling fatal EH there +can leave the freezer waiting on I/O that depends on the same recovery. Keep +clk-gating hibern8 enabled, but suppress the fatal EH guard for that specific +ungate window and run link recovery directly. + +Signed-off-by: jaewun +--- +--- a/drivers/ufs/core/ufshcd.c ++++ b/drivers/ufs/core/ufshcd.c +@@ -348,6 +348,20 @@ + static void ufshcd_hba_vreg_set_lpm(struct ufs_hba *hba); + static void ufshcd_hba_vreg_set_hpm(struct ufs_hba *hba); + ++/* ++ * True while the controller is being relinked during PM or during the explicit ++ * error-handler reset bracket. In these windows the threaded IRQ handler may ++ * not run, so ufshcd_disable_irq() must not synchronize on it and completions ++ * are drained without relying on a new UFS IRQ. A dedicated flag is used rather ++ * than UFSHCD_STATE_RESET so generic resets/probe are unaffected. Lock-free ++ * cross-context reads -> READ_ONCE. ++ */ ++static inline bool ufshcd_relinking(struct ufs_hba *hba) ++{ ++ return READ_ONCE(hba->pm_op_in_progress) || ++ READ_ONCE(hba->relink_poll_active); ++} ++ + void ufshcd_enable_irq(struct ufs_hba *hba) + { + if (!hba->is_irq_enabled) { +@@ -360,7 +374,16 @@ + void ufshcd_disable_irq(struct ufs_hba *hba) + { + if (hba->is_irq_enabled) { +- disable_irq(hba->irq); ++ /* ++ * hba->irq is threaded and IRQF_ONESHOT. During PM/EH relink the ++ * threaded handler is bypassed and completions are serviced directly, ++ * so do not wait for the thread here. The controller reset and direct ++ * completion drain handle any in-flight state. ++ */ ++ if (ufshcd_relinking(hba)) ++ disable_irq_nosync(hba->irq); ++ else ++ disable_irq(hba->irq); + hba->is_irq_enabled = false; + } + } +@@ -1921,11 +1944,33 @@ + hba->clk_gating.is_suspended = true; + if (ufshcd_is_link_hibern8(hba)) { + ret = ufshcd_uic_hibern8_exit(hba); +- if (ret) +- dev_err(hba->dev, "%s: hibern8 exit failed %d\n", ++ if (ret) { ++ /* ++ * This SW clk-gating hibern8 exit runs outside ++ * pm_op_in_progress. During the freezer phase, ++ * scheduling fatal EH can leave I/O waiting on the ++ * same recovery. Run the relink inline and bracket it ++ * with the poller so direct completion draining covers ++ * link startup. ++ */ ++ dev_err(hba->dev, "%s: hibern8 exit failed %d, recovering link\n", + __func__, ret); +- else ++ if (!hba->mcq_enabled) { ++ WRITE_ONCE(hba->relink_poll_active, true); ++ mod_timer(&hba->pm_poll_timer, ++ jiffies + msecs_to_jiffies(2)); ++ } ++ ret = ufshcd_link_recovery(hba); ++ if (!hba->mcq_enabled) { ++ WRITE_ONCE(hba->relink_poll_active, false); ++ timer_delete_sync(&hba->pm_poll_timer); ++ } ++ if (ret) ++ dev_err(hba->dev, "%s: link recovery after hibern8 exit failed %d\n", ++ __func__, ret); ++ } else { + ufshcd_set_link_active(hba); ++ } + } + hba->clk_gating.is_suspended = false; + } +@@ -4382,7 +4427,12 @@ + spin_lock_irqsave(hba->host->host_lock, flags); + hba->active_uic_cmd = NULL; + hba->uic_async_done = NULL; +- if (ret && !hba->pm_op_in_progress) { ++ /* ++ * Suppress fatal link-broken+EH for the SW clk-gating ungate hibern8 ++ * exit. That path runs outside pm_op_in_progress during the freezer ++ * phase; ufshcd_ungate_work handles recovery inline instead. ++ */ ++ if (ret && !hba->pm_op_in_progress && !hba->clk_gating.is_suspended) { + ufshcd_set_link_broken(hba); + ufshcd_schedule_eh_work(hba); + } +@@ -6383,11 +6433,53 @@ + ufs_debugfs_exception_event(hba, status); + } + +-/* Complete requests that have door-bell cleared */ ++/* ++ * Non-MCQ counterpart of ufshcd_mcq_force_compl_one(): invoked via ++ * ufshcd_complete_requests(force_compl=true) from ufshcd_host_reset_and_restore() ++ * (and the error handler) AFTER ufshcd_hba_stop() has reset the controller. The ++ * normal single-doorbell path (ufshcd_transfer_req_compl/ufshcd_poll) only ++ * completes tags whose doorbell bit reads back clear, so a request left ++ * outstanding by a failed UIC/link op is not completed. The in-memory OCS is ++ * stale after reset, so requeue SCSI commands rather than reporting a ++ * possibly-bogus result; reserved device-management tags are completed by ++ * their own paths and skipped (mirrors ufshcd_mcq_force_compl_one()). ++ */ ++static bool ufshcd_force_compl_one(struct request *rq, void *priv) ++{ ++ struct scsi_cmnd *cmd = blk_mq_rq_to_pdu(rq); ++ struct scsi_device *sdev = rq->q->queuedata; ++ struct ufs_hba *hba = shost_priv(sdev->host); ++ unsigned long flags; ++ ++ if (blk_mq_is_reserved_rq(rq)) ++ return true; ++ ++ spin_lock_irqsave(&hba->outstanding_lock, flags); ++ __clear_bit(rq->tag, &hba->outstanding_reqs); ++ spin_unlock_irqrestore(&hba->outstanding_lock, flags); ++ ++ if (!test_bit(SCMD_STATE_COMPLETE, &cmd->state)) { ++ set_host_byte(cmd, DID_REQUEUE); ++ ufshcd_release_scsi_cmd(hba, cmd); ++ scsi_done(cmd); ++ } ++ ++ return true; ++} ++ ++/* Complete requests that have door-bell cleared (or, on force, all outstanding) */ + static void ufshcd_complete_requests(struct ufs_hba *hba, bool force_compl) + { + if (hba->mcq_enabled) + ufshcd_mcq_compl_pending_transfer(hba, force_compl); ++ else if (force_compl) ++ /* ++ * The controller has been reset; complete every outstanding ++ * request (the doorbell no longer reflects them) so no request ++ * remains stuck. ++ */ ++ blk_mq_tagset_busy_iter(&hba->host->tag_set, ++ ufshcd_force_compl_one, NULL); + else + ufshcd_transfer_req_compl(hba); + +@@ -6863,6 +6955,16 @@ + + hba->force_reset = false; + spin_unlock_irqrestore(hba->host->host_lock, flags); ++ /* ++ * Arm the out-of-band completion poller across the error-handler ++ * relink. ufshcd_state == UFSHCD_STATE_RESET here and pm_op_in_progress ++ * is clear, so ufshcd_relinking() gates the drain and nosync IRQ ++ * disable while the reset is in progress. ++ */ ++ if (!hba->mcq_enabled) { ++ WRITE_ONCE(hba->relink_poll_active, true); ++ mod_timer(&hba->pm_poll_timer, jiffies + msecs_to_jiffies(2)); ++ } + err = ufshcd_reset_and_restore(hba); + if (err) + dev_err(hba->dev, "%s: reset and restore failed with err %d\n", +@@ -6889,6 +6991,14 @@ + } + ufshcd_clear_eh_in_progress(hba); + spin_unlock_irqrestore(hba->host->host_lock, flags); ++ /* ++ * Stop the relink poller before unprepare can release the clocks. Clear the ++ * flag first so the callback sees ufshcd_relinking() false and will not ++ * re-arm, then synchronously delete. Done after the host_lock is dropped ++ * (the drain takes host_lock). ++ */ ++ WRITE_ONCE(hba->relink_poll_active, false); ++ timer_delete_sync(&hba->pm_poll_timer); + ufshcd_err_handling_unprepare(hba); + up(&hba->host_sem); + +@@ -7207,6 +7317,112 @@ + return retval; + } + ++/* ++ * ufshcd_pm_drain_completions - service UTP/UIC/TMF completions without the IRQ ++ * @hba: per adapter instance ++ * ++ * The UFS interrupt is a threaded IRQF_ONESHOT line. If its threaded handler is ++ * left pending across a PM transition the genirq core keeps the line masked, so ++ * ufshcd_intr() is not re-invoked and relink completions can remain pending. ++ * Called inline from ufshcd_intr()'s PM fast path (hardirq) AND from the PM poll ++ * timer (softirq), so it must not depend on the UFS IRQ firing. pm_drain_active ++ * serialises the two callers so they cannot both W1C REG_INTERRUPT_STATUS. ++ */ ++static void ufshcd_pm_drain_completions(struct ufs_hba *hba) ++{ ++ int retries = hba->nutrs; ++ ++ if (test_and_set_bit(0, &hba->pm_drain_active)) ++ return; ++ ++ while (retries-- > 0) { ++ unsigned long flags; ++ u32 status, compl_bits; ++ ++ status = ufshcd_readl(hba, REG_INTERRUPT_STATUS) & ++ ufshcd_readl(hba, REG_INTERRUPT_ENABLE); ++ if (!status) ++ break; ++ ++ /* W1C exactly the snapshot we read. */ ++ ufshcd_writel(hba, status, REG_INTERRUPT_STATUS); ++ ++ if (status & UFSHCD_ERROR_MASK) { ++ /* ++ * read-to-clear all UIC error-code registers so a ++ * transient resume-time error cannot storm. ++ */ ++ ufshcd_readl(hba, REG_UIC_ERROR_CODE_PHY_ADAPTER_LAYER); ++ ufshcd_readl(hba, REG_UIC_ERROR_CODE_DATA_LINK_LAYER); ++ ufshcd_readl(hba, REG_UIC_ERROR_CODE_NETWORK_LAYER); ++ ufshcd_readl(hba, REG_UIC_ERROR_CODE_TRANSPORT_LAYER); ++ ufshcd_readl(hba, REG_UIC_ERROR_CODE_DME); ++ ++ /* ++ * If the relink's in-flight UIC command (e.g. ++ * DME_LINKSTARTUP) errored with no accompanying UIC ++ * completion, force-complete it with UIC_CMD_RESULT_FAILURE ++ * so ufshcd_wait_for_uic_cmd() returns promptly and the ++ * link startup can retry. Restricted to DME_LINK_STARTUP ++ * with no uic_async_done in flight so power-mode UIC ++ * commands are untouched. irqsave: this also runs in ++ * softirq (the PM poll timer). ++ */ ++ spin_lock_irqsave(hba->host->host_lock, flags); ++ if (!(status & UFSHCD_UIC_MASK) && !hba->uic_async_done && ++ hba->active_uic_cmd && ++ hba->active_uic_cmd->cmd_active && ++ hba->active_uic_cmd->command == ++ UIC_CMD_DME_LINK_STARTUP) { ++ struct uic_command *cmd = hba->active_uic_cmd; ++ ++ cmd->argument2 = (cmd->argument2 & ++ ~MASK_UIC_COMMAND_RESULT) | ++ UIC_CMD_RESULT_FAILURE; ++ cmd->cmd_active = false; ++ complete(&cmd->done); ++ } ++ spin_unlock_irqrestore(hba->host->host_lock, flags); ++ } ++ ++ compl_bits = status & (UTP_TRANSFER_REQ_COMPL | UFSHCD_UIC_MASK); ++ if (compl_bits & UFSHCD_UIC_MASK) ++ ufshcd_uic_cmd_compl(hba, compl_bits); ++ if (status & UTP_TASK_REQ_COMPL) ++ ufshcd_tmc_handler(hba); ++ if (compl_bits & UTP_TRANSFER_REQ_COMPL) ++ ufshcd_transfer_req_compl(hba); ++ } ++ ++ clear_bit(0, &hba->pm_drain_active); ++} ++ ++/* ++ * ufshcd_pm_poll_timer_fn - out-of-band UFS completion drain during PM resume. ++ * ++ * Armed across the PM relink window. Fires from the arch timer, so it services ++ * completions even if the IRQ line remains oneshot-masked. Self-rearms while ++ * relinking; the PM/EH paths clear the state and delete the timer on exit. ++ */ ++static void ufshcd_pm_poll_timer_fn(struct timer_list *t) ++{ ++ struct ufs_hba *hba = timer_container_of(hba, t, pm_poll_timer); ++ ++ if (hba->mcq_enabled || !ufshcd_relinking(hba)) ++ return; ++ ++ ufshcd_pm_drain_completions(hba); ++ ++ /* ++ * Re-check before re-arming so a concurrent disarm (which clears the ++ * relink state before timer_delete_sync()) cannot lose the race and leave ++ * the timer armed. A stray re-arm would be harmless anyway -- the next tick ++ * sees the state clear and returns without MMIO -- but avoid it. ++ */ ++ if (ufshcd_relinking(hba)) ++ mod_timer(&hba->pm_poll_timer, jiffies + msecs_to_jiffies(2)); ++} ++ + /** + * ufshcd_intr - Main interrupt service routine + * @irq: irq number +@@ -7586,10 +7802,22 @@ + struct ufs_hba *hba = __hba; + u32 intr_status, enabled_intr_status; + ++ /* ++ * On this non-MCQ controller interrupt handling is normally deferred to ++ * the threaded handler, which may not run during PM relink. Service ++ * UTP/UIC/TMF completions inline and read-clear UIC error-code registers. ++ * Do not run ufshcd_check_errors() here: it can schedule error handling ++ * while PM recovery is already in progress. ++ */ ++ if (!hba->mcq_enabled && ufshcd_relinking(hba)) { ++ ufshcd_pm_drain_completions(hba); ++ return IRQ_HANDLED; ++ } ++ + /* + * Handle interrupt in thread if MCQ or ESI is disabled, + * and no active UIC command. + */ + if ((!hba->mcq_enabled || !hba->mcq_esi_enabled) && + !hba->active_uic_cmd) + return IRQ_WAKE_THREAD; +@@ -9729,6 +9957,7 @@ + + static void ufshcd_hba_exit(struct ufs_hba *hba) + { ++ timer_delete_sync(&hba->pm_poll_timer); + if (hba->is_powered) { + ufshcd_pm_qos_exit(hba); + ufshcd_exit_clk_scaling(hba); +@@ -10182,6 +10411,15 @@ + if (ret) + goto out; + ++ /* ++ * Vendor resume is done so the controller clocks/registers are usable. ++ * Arm the completion poller for the relink. If the UFS IRQ remains ++ * oneshot-masked, the timer drains UIC/UTP completions from the arch timer ++ * instead. Disarmed at out: before PM state is released. ++ */ ++ if (!hba->mcq_enabled) ++ mod_timer(&hba->pm_poll_timer, jiffies + msecs_to_jiffies(2)); ++ + /* For DeepSleep, the only supported option is to have the link off */ + WARN_ON(ufshcd_is_ufs_dev_deepsleep(hba) && !ufshcd_is_link_off(hba)); + +@@ -10259,9 +10497,18 @@ + out: + if (ret) + ufshcd_update_evt_hist(hba, UFS_EVT_WL_RES_ERR, (u32)ret); ++ /* ++ * Stop the relink completion poller BEFORE ufshcd_release() (which can ++ * queue clock gating once is_suspended is cleared) so the timer can never ++ * read UFS MMIO after the clocks are gated. Clear pm_op_in_progress first so ++ * a concurrently-firing timer sees the flag false and does not re-arm, then ++ * synchronously delete it. From here normal IRQ delivery resumes: the ++ * now-runnable threaded handler drains any remaining IS and unmasks the line. ++ */ ++ hba->pm_op_in_progress = false; ++ timer_delete_sync(&hba->pm_poll_timer); + hba->clk_gating.is_suspended = false; + ufshcd_release(hba); +- hba->pm_op_in_progress = false; + return ret; + } + +@@ -10884,6 +11131,13 @@ + */ + hba->vcc_off_delay_us = 2000; + ++ /* ++ * Set up the relink completion poll timer before the first goto out_disable ++ * (which reaches ufshcd_hba_exit() -> timer_delete_sync()), so teardown ++ * never operates on an uninitialised timer. ++ */ ++ timer_setup(&hba->pm_poll_timer, ufshcd_pm_poll_timer_fn, 0); ++ + err = ufshcd_hba_init(hba); + if (err) + goto out_error; +--- a/include/ufs/ufshcd.h ++++ b/include/ufs/ufshcd.h +@@ -976,6 +976,18 @@ + enum ufs_pm_level pm_lvl_min; + int pm_op_in_progress; + ++ /* ++ * Out-of-band completion drain for PM and error-handler relink. If the ++ * threaded IRQ remains oneshot-masked, pm_poll_timer drains completions ++ * from the arch timer while pm_op_in_progress or relink_poll_active is ++ * set. pm_drain_active serialises the timer drain against the hardirq ++ * drain. relink_poll_active is set only around the explicit error-handler ++ * reset bracket, not for generic resets. ++ */ ++ struct timer_list pm_poll_timer; ++ unsigned long pm_drain_active; ++ bool relink_poll_active; ++ + /* Auto-Hibernate Idle Timer register value */ + u32 ahit; + diff --git a/projects/ROCKNIX/devices/SM8550/patches/linux/0203-thermal-qcom-tsens-skip-sm8550-uplow-wake-irq.patch b/projects/ROCKNIX/devices/SM8550/patches/linux/0203-thermal-qcom-tsens-skip-sm8550-uplow-wake-irq.patch new file mode 100644 index 00000000000..5b79e679917 --- /dev/null +++ b/projects/ROCKNIX/devices/SM8550/patches/linux/0203-thermal-qcom-tsens-skip-sm8550-uplow-wake-irq.patch @@ -0,0 +1,51 @@ +From: Edouard Durand +Subject: [PATCH] thermal: qcom: tsens: skip SM8550 uplow wake IRQs + +Native s2idle testing on SM8550 devices repeatedly resumes immediately with +pm_wakeup_irq pointing at the SM8550 TSENS uplow/passive threshold IRQ. +The IRQ wake state exposed under /sys/kernel/irq is read-only on the live +kernel, so runtime testing cannot disable this wake source. + +Keep normal TSENS interrupt handling active and keep critical TSENS wake +enabled. Only skip arming the ordinary "uplow" TSENS IRQ as a wake source on +qcom,sm8550. Scoped to the SoC-level compatible (not a specific board) so it +covers every SM8550 board, not just the one it was first observed on. + +Guard the disable side to match so the wake refcount stays balanced and +resume does not warn about an unbalanced disable_irq_wake(). +--- +--- a/drivers/thermal/qcom/tsens.c 2026-08-25 19:01:04.024339245 +0800 ++++ b/drivers/thermal/qcom/tsens.c 2026-08-25 19:01:06.090453327 +0800 +@@ -1289,6 +1289,12 @@ + return 0; + } + ++static bool tsens_uplow_wake_allowed(void) ++{ ++ /* SM8550 resumes immediately from s2idle on the uplow threshold IRQ. */ ++ return !of_machine_is_compatible("qcom,sm8550"); ++} ++ + int tsens_suspend_common(struct tsens_priv *priv) + { + if (!device_may_wakeup(priv->dev)) +@@ -1297,7 +1303,8 @@ + if (priv->feat->combo_int) + enable_irq_wake(priv->combined_irq); + else { +- enable_irq_wake(priv->uplow_irq); ++ if (tsens_uplow_wake_allowed()) ++ enable_irq_wake(priv->uplow_irq); + if (priv->feat->crit_int) + enable_irq_wake(priv->crit_irq); + } +@@ -1316,7 +1323,8 @@ + if (priv->feat->combo_int) + disable_irq_wake(priv->combined_irq); + else { +- disable_irq_wake(priv->uplow_irq); ++ if (tsens_uplow_wake_allowed()) ++ disable_irq_wake(priv->uplow_irq); + if (priv->feat->crit_int) + disable_irq_wake(priv->crit_irq); + } diff --git a/projects/ROCKNIX/devices/SM8550/patches/linux/0207-scsi-ufs-qcom-balance-irq-on-host-reset-error.patch b/projects/ROCKNIX/devices/SM8550/patches/linux/0207-scsi-ufs-qcom-balance-irq-on-host-reset-error.patch new file mode 100644 index 00000000000..625deb187bf --- /dev/null +++ b/projects/ROCKNIX/devices/SM8550/patches/linux/0207-scsi-ufs-qcom-balance-irq-on-host-reset-error.patch @@ -0,0 +1,55 @@ +From: jaewun +Subject: [PATCH] scsi: ufs: qcom: balance the IRQ disable on host_reset error exits + +ufs_qcom_host_reset() snapshots whether the IRQ was enabled, calls +ufshcd_disable_irq(), and only re-enables it on the success path. The two +error returns (reset_control_assert / reset_control_deassert failure) leave +the IRQ disabled, leaking the disable depth and leaving the controller IRQ +masked for the rest of the controller's life. + +The relink path can now mask the IRQ with disable_irq_nosync() while PM is in +progress, so these error exits must preserve IRQ depth as well. Route both +failures through a common exit that re-enables the IRQ when it was enabled on +entry. + +Signed-off-by: jaewun +--- +diff --git a/drivers/ufs/host/ufs-qcom.c b/drivers/ufs/host/ufs-qcom.c +index 375fd24..61a5b3a 100644 +--- a/drivers/ufs/host/ufs-qcom.c ++++ b/drivers/ufs/host/ufs-qcom.c +@@ -449,7 +449,7 @@ static int ufs_qcom_host_reset(struct ufs_hba *hba) + if (ret) { + dev_err(hba->dev, "%s: core_reset assert failed, err = %d\n", + __func__, ret); +- return ret; ++ goto out; + } + + /* +@@ -463,15 +463,23 @@ static int ufs_qcom_host_reset(struct ufs_hba *hba) + if (ret) { + dev_err(hba->dev, "%s: core_reset deassert failed, err = %d\n", + __func__, ret); +- return ret; ++ goto out; + } + + usleep_range(1000, 1100); + ++ ret = 0; ++out: ++ /* ++ * Re-enable the IRQ on the error exits too, otherwise a reset ++ * assert/deassert failure leaks the disable and leaves the controller ++ * IRQ masked. (This path became reachable once ufshcd_disable_irq() stops ++ * synchronizing during PM resume.) ++ */ + if (reenable_intr) + ufshcd_enable_irq(hba); + +- return 0; ++ return ret; + } + + static u32 ufs_qcom_get_hs_gear(struct ufs_hba *hba) diff --git a/projects/ROCKNIX/devices/SM8550/patches/linux/0215-PCI-qcom-skip-l23-ready-after-pme-sm8550.patch b/projects/ROCKNIX/devices/SM8550/patches/linux/0215-PCI-qcom-skip-l23-ready-after-pme-sm8550.patch new file mode 100644 index 00000000000..895926d37fd --- /dev/null +++ b/projects/ROCKNIX/devices/SM8550/patches/linux/0215-PCI-qcom-skip-l23-ready-after-pme-sm8550.patch @@ -0,0 +1,35 @@ +From: Luke Johnson +Subject: [PATCH] PCI: qcom: skip L2/L3-ready LTSSM poll after PME_TurnOff (SM8550) + +On SM8550 the L2/L3-ready poll read_poll_timeout(dw_pcie_get_ltssm) in +dw_pcie_suspend_noirq() accesses a controller LTSSM register after the +PME_Turn_Off broadcast, which the SoC cannot service at that point (the +read wedges/faults), so system suspend never resumes. Confirmed needed +on AYN Thor: with the get_ltssm helper corrected to read PARF_LTSSM, +suspend still crashes at the L2-wait; only skipping the poll lets it +proceed. Set pp->skip_l23_ready so the DWC core waits a fixed delay for +L2/L3 entry instead of polling, matching i.MX6's handling. + +7.2 carries the mainline D3cold series natively (d3cold eligibility +helper, get_ltssm, the skip_l23_ready escape hatch), but nothing sets +the flag for qcom. This lands in the SM8550 device patch dir, so only +SM8550 builds are affected. +--- +--- a/drivers/pci/controller/dwc/pcie-qcom.c 2026-08-25 19:11:23.915612378 +0800 ++++ b/drivers/pci/controller/dwc/pcie-qcom.c 2026-08-25 19:11:26.562970425 +0800 +@@ -2109,6 +2109,15 @@ + + pp->ops = &qcom_pcie_dw_ops; + ++ /* ++ * SM8550: the L2/L3-ready poll in dw_pcie_suspend_noirq() reads a ++ * controller LTSSM register after the PME_Turn_Off broadcast, which ++ * this SoC cannot service at that point; the read wedges suspend with ++ * no resume. Skip the poll and use the fixed delay instead, as other ++ * DWC platforms (e.g. i.MX6) do for the same limitation. ++ */ ++ pp->skip_l23_ready = true; ++ + ret = qcom_pcie_parse_ports(pcie); + if (ret) { + if (ret != -ENODEV) { diff --git a/projects/ROCKNIX/devices/SM8550/patches/linux/0218-regulator-qcom-rpmh-add-suspend-state-support.patch b/projects/ROCKNIX/devices/SM8550/patches/linux/0218-regulator-qcom-rpmh-add-suspend-state-support.patch new file mode 100755 index 00000000000..97e01e91c04 --- /dev/null +++ b/projects/ROCKNIX/devices/SM8550/patches/linux/0218-regulator-qcom-rpmh-add-suspend-state-support.patch @@ -0,0 +1,251 @@ +From: Luke Johnson +Date: Thu, 2 Jul 2026 00:00:00 +1200 +Subject: [PATCH] regulator: qcom-rpmh: add suspend-state (SLEEP/WAKE TCS) support + +The mainline driver only ever votes in RPMH_ACTIVE_ONLY_STATE, so every +rail that is enabled when the system suspends stays at full power through +the platform sleep state. Downstream (qcom,set=<3>) mirrors votes into +the SLEEP set and additionally forces specific rails off in sleep. + +Implement regulator-state-mem support: + + * set_suspend_enable/disable place an enable vote in the SLEEP set and + the current active-set contribution in the WAKE set, so the AOP wake + set replay restores exactly the pre-suspend state before the kernel + resumes. rpmh's is_req_valid() prunes equal sleep/wake pairs, so a + rail with no APPS enable vote degenerates to a guaranteed no-op and + other DRVs' votes can never be overridden. + + * set_suspend_mode does the same for the VRM mode register (LDO LPM in + suspend with HPM restored on wake). + + * Every active-set request is now also mirrored into the SLEEP cache. + rpmh_flush() runs on runtime cluster idle too, not only at system + suspend; without the mirror, an enable that arrives after resume + would only update the wake side and leave a stale sleep=off vote that + cuts the rail on every runtime RSC deep-idle. Equal pairs are pruned, + so the mirror is invisible in the TCSes until a suspend vote is armed. + + * ops->resume re-aligns the SLEEP set with the active set so the + system-suspend configuration does not persist into post-resume + runtime idle (e.g. a codec rail left in LPM under WLAN load). + +Only regulators whose DT node carries regulator-state-mem with +regulator-on/off-in-suspend are affected (the regulator core calls the +suspend ops, and ops->resume, only for those). The companion regulator +core patch maps s2idle (PM_SUSPEND_TO_IDLE) to the same state-mem +constraints, so the ops fire for both suspend modes on this platform. +Active-set votes are never modified. +--- + drivers/regulator/qcom-rpmh-regulator.c | 165 +++++++++++++++++++++++- + 1 file changed, 163 insertions(+), 2 deletions(-) + +--- a/drivers/regulator/qcom-rpmh-regulator.c ++++ b/drivers/regulator/qcom-rpmh-regulator.c +@@ -204,8 +204,21 @@ + else + ret = rpmh_write_async(vreg->dev, RPMH_ACTIVE_ONLY_STATE, cmd, + 1); ++ if (ret < 0) ++ return ret; + +- return ret; ++ /* ++ * Mirror every active-set request into the SLEEP set cache. rpmh ++ * prunes cache entries whose sleep and wake values are equal (an ++ * ACTIVE_ONLY write also updates the cached wake value), so this ++ * changes nothing in the sleep/wake TCSes by itself. It matters ++ * once a suspend-state vote (set_suspend_*) has been placed for a ++ * register: any later active-set change then re-aligns the sleep ++ * vote instead of leaving a stale suspend value that rpmh_flush() ++ * would re-apply on every runtime RSC deep-idle entry. This is ++ * cache-only; no request is sent to the hardware here. ++ */ ++ return rpmh_write(vreg->dev, RPMH_SLEEP_STATE, cmd, 1); + } + + static int _rpmh_regulator_vrm_set_voltage_sel(struct regulator_dev *rdev, +@@ -342,6 +355,141 @@ + } + + /** ++ * rpmh_regulator_send_sleep_wake_request() - cache a SLEEP set vote together ++ * with the matching WAKE set restore value for one register of ++ * an RPMh regulator resource ++ * @vreg: Pointer to the RPMh regulator ++ * @reg_offset: VRM/XOB register offset within the RPMh resource ++ * @sleep_val: Value to apply while the RSC is in its sleep state ++ * @wake_val: Value the wake set replay restores before the CPUs resume ++ * ++ * Both votes are only cached here; rpmh_flush() writes them to the sleep ++ * and wake TCSes when the last CPU powers down. rpmh's is_req_valid() ++ * prunes cache entries whose sleep and wake values are equal, so sending ++ * an identical pair is a guaranteed no-op for this resource. The active ++ * set is never modified by this function. ++ * ++ * Return: 0 on success, or a negative error number on failure ++ */ ++static int rpmh_regulator_send_sleep_wake_request(struct rpmh_vreg *vreg, ++ u32 reg_offset, u32 sleep_val, u32 wake_val) ++{ ++ struct tcs_cmd cmd = { ++ .addr = vreg->addr + reg_offset, ++ .data = sleep_val, ++ }; ++ int ret; ++ ++ ret = rpmh_write(vreg->dev, RPMH_SLEEP_STATE, &cmd, 1); ++ if (ret < 0) ++ return ret; ++ ++ cmd.data = wake_val; ++ ++ return rpmh_write(vreg->dev, RPMH_WAKE_ONLY_STATE, &cmd, 1); ++} ++ ++static int rpmh_regulator_set_suspend_enable(struct regulator_dev *rdev) ++{ ++ struct rpmh_vreg *vreg = rdev_get_drvdata(rdev); ++ ++ return rpmh_regulator_send_sleep_wake_request(vreg, ++ RPMH_REGULATOR_REG_ENABLE, 1, vreg->enabled == 1); ++} ++ ++static int rpmh_regulator_set_suspend_disable(struct regulator_dev *rdev) ++{ ++ struct rpmh_vreg *vreg = rdev_get_drvdata(rdev); ++ ++ /* ++ * WAKE restores the current active-set contribution: 1 only if an ++ * APPS enable vote is in place right now. If no enable request has ++ * ever been sent (enabled == -EINVAL) the pair is 0/0, which rpmh ++ * prunes - the vote of another DRV (AOP, TZ, ...) keeping such a ++ * rail on can never be overridden from here. ++ */ ++ return rpmh_regulator_send_sleep_wake_request(vreg, ++ RPMH_REGULATOR_REG_ENABLE, 0, vreg->enabled == 1); ++} ++ ++/* ++ * Return the PMIC mode value currently requested in the active set, or a ++ * negative error number if no mode request has been sent. ++ */ ++static int rpmh_regulator_vrm_active_pmic_mode(struct rpmh_vreg *vreg) ++{ ++ if (vreg->bypassed) ++ return PMIC4_BOB_MODE_PASS; ++ ++ if (vreg->mode == REGULATOR_MODE_INVALID) ++ return -EINVAL; ++ ++ return vreg->hw_data->pmic_mode_map[vreg->mode]; ++} ++ ++static int rpmh_regulator_vrm_set_suspend_mode(struct regulator_dev *rdev, ++ unsigned int mode) ++{ ++ struct rpmh_vreg *vreg = rdev_get_drvdata(rdev); ++ int sleep_mode, wake_mode; ++ ++ if (mode > REGULATOR_MODE_STANDBY) ++ return -EINVAL; ++ ++ sleep_mode = vreg->hw_data->pmic_mode_map[mode]; ++ if (sleep_mode < 0) ++ return sleep_mode; ++ ++ /* ++ * WAKE must restore the pre-suspend active-set mode. If no mode ++ * request has ever been sent, reuse sleep_mode so that the equal ++ * pair is pruned instead of inventing a wake vote that was not ++ * present before suspend. ++ */ ++ wake_mode = rpmh_regulator_vrm_active_pmic_mode(vreg); ++ if (wake_mode < 0) ++ wake_mode = sleep_mode; ++ ++ /* Never override bypass (pass-through) mode in the sleep set. */ ++ if (vreg->bypassed) ++ sleep_mode = wake_mode; ++ ++ return rpmh_regulator_send_sleep_wake_request(vreg, ++ RPMH_REGULATOR_REG_VRM_MODE, sleep_mode, wake_mode); ++} ++ ++static int rpmh_regulator_resume(struct regulator_dev *rdev) ++{ ++ struct rpmh_vreg *vreg = rdev_get_drvdata(rdev); ++ int mode; ++ int ret; ++ ++ /* ++ * Re-align the SLEEP set with the active set. The rpmh sleep/wake ++ * cache persists after resume, and rpmh_flush() also runs when the ++ * last CPU powers down for runtime cpuidle - without this undo the ++ * system-suspend configuration would be re-applied on every runtime ++ * RSC sleep. Writing SLEEP equal to the current active value makes ++ * is_req_valid() prune the entry at the next flush. ++ */ ++ ret = rpmh_regulator_send_sleep_wake_request(vreg, ++ RPMH_REGULATOR_REG_ENABLE, ++ vreg->enabled == 1, vreg->enabled == 1); ++ if (ret < 0) ++ return ret; ++ ++ if (vreg->hw_data->regulator_type != VRM) ++ return 0; ++ ++ mode = rpmh_regulator_vrm_active_pmic_mode(vreg); ++ if (mode < 0) ++ return 0; ++ ++ return rpmh_regulator_send_sleep_wake_request(vreg, ++ RPMH_REGULATOR_REG_VRM_MODE, mode, mode); ++} ++ ++/** + * rpmh_regulator_vrm_get_optimum_mode() - get the mode based on the load + * @rdev: Regulator device pointer for the rpmh-regulator + * @input_uV: Input voltage +@@ -399,6 +547,10 @@ + .list_voltage = regulator_list_voltage_linear_range, + .set_mode = rpmh_regulator_vrm_set_mode, + .get_mode = rpmh_regulator_vrm_get_mode, ++ .set_suspend_enable = rpmh_regulator_set_suspend_enable, ++ .set_suspend_disable = rpmh_regulator_set_suspend_disable, ++ .set_suspend_mode = rpmh_regulator_vrm_set_suspend_mode, ++ .resume = rpmh_regulator_resume, + }; + + static const struct regulator_ops rpmh_regulator_vrm_drms_ops = { +@@ -411,6 +563,10 @@ + .set_mode = rpmh_regulator_vrm_set_mode, + .get_mode = rpmh_regulator_vrm_get_mode, + .get_optimum_mode = rpmh_regulator_vrm_get_optimum_mode, ++ .set_suspend_enable = rpmh_regulator_set_suspend_enable, ++ .set_suspend_disable = rpmh_regulator_set_suspend_disable, ++ .set_suspend_mode = rpmh_regulator_vrm_set_suspend_mode, ++ .resume = rpmh_regulator_resume, + }; + + static const struct regulator_ops rpmh_regulator_vrm_bypass_ops = { +@@ -424,12 +580,19 @@ + .get_mode = rpmh_regulator_vrm_get_mode, + .set_bypass = rpmh_regulator_vrm_set_bypass, + .get_bypass = rpmh_regulator_vrm_get_bypass, ++ .set_suspend_enable = rpmh_regulator_set_suspend_enable, ++ .set_suspend_disable = rpmh_regulator_set_suspend_disable, ++ .set_suspend_mode = rpmh_regulator_vrm_set_suspend_mode, ++ .resume = rpmh_regulator_resume, + }; + + static const struct regulator_ops rpmh_regulator_xob_ops = { + .enable = rpmh_regulator_enable, + .disable = rpmh_regulator_disable, + .is_enabled = rpmh_regulator_is_enabled, ++ .set_suspend_enable = rpmh_regulator_set_suspend_enable, ++ .set_suspend_disable = rpmh_regulator_set_suspend_disable, ++ .resume = rpmh_regulator_resume, + }; + + /** diff --git a/projects/ROCKNIX/devices/SM8550/patches/linux/0509-usb-typec-ucsi-clear-USB-role.patch b/projects/ROCKNIX/devices/SM8550/patches/linux/0509-usb-typec-ucsi-clear-USB-role.patch new file mode 100644 index 00000000000..ba52de19a72 --- /dev/null +++ b/projects/ROCKNIX/devices/SM8550/patches/linux/0509-usb-typec-ucsi-clear-USB-role.patch @@ -0,0 +1,36 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Anze +Date: Sun, 19 Jul 2026 00:30:00 +0200 +Subject: [PATCH] usb: typec: ucsi: clear USB role when partner lacks USB + data + +Commit 0c8ee850572 ("usb: typec: ucsi: Add UCSI_USB4_IMPLIES_USB quirk +for X1E80100") gated the "partner advertises no USB data -> +USB_ROLE_NONE" decision behind the UCSI_USB4_IMPLIES_USB quirk. On +ports without that quirk (e.g. SM8750 / AYN Odin 3, pmic_glink UCSI) +the USB data role is then never cleared for a charger-only partner, so +it stays attached across system suspend. That keeps the USB power +island up, and the deep-suspend firmware power-collapse hard-hangs the +SoC when the cable is unplugged while suspended: dead screen, the SoC +runs hot, only a forced reset recovers (7.0.x is fine, 7.1.x hangs). + +Restore the pre-0c8ee850572 behaviour: clear the USB role whenever the +partner advertises no USB data. + +Signed-off-by: Anze +--- +--- a/drivers/usb/typec/ucsi/ucsi.c ++++ b/drivers/usb/typec/ucsi/ucsi.c +@@ -1196,10 +1196,8 @@ + typec_partner_set_usb_mode(con->partner, USB_MODE_USB4); + } + +- if ((!UCSI_CONSTAT(con, PARTNER_FLAG_USB)) && +- ((con->ucsi->quirks & UCSI_USB4_IMPLIES_USB) && +- (!(UCSI_CONSTAT(con, PARTNER_FLAG_USB4_GEN3) || +- UCSI_CONSTAT(con, PARTNER_FLAG_USB4_GEN4))))) ++ /* Only notify USB controller if partner supports USB data */ ++ if (!(UCSI_CONSTAT(con, PARTNER_FLAG_USB))) + u_role = USB_ROLE_NONE; + + ret = usb_role_switch_set_role(con->usb_role_sw, u_role); diff --git a/projects/ROCKNIX/devices/SM8550/patches/linux/1004-input-rsinput-suspend-resume-gamepad-mcu.patch b/projects/ROCKNIX/devices/SM8550/patches/linux/1004-input-rsinput-suspend-resume-gamepad-mcu.patch new file mode 100644 index 00000000000..83a293a8542 --- /dev/null +++ b/projects/ROCKNIX/devices/SM8550/patches/linux/1004-input-rsinput-suspend-resume-gamepad-mcu.patch @@ -0,0 +1,64 @@ +From: jaewun +Subject: [PATCH] input: rsinput: suspend and resume the gamepad MCU across system sleep + +The gamepad MCU streams input reports continuously over the UART. Across a +system-suspend teardown the MCU keeps sending, so the geni-serial RX interrupt +storms while the port is stopping, the primary handler returns IRQ_NONE, and the +kernel's spurious-IRQ detector permanently disables the UART IRQ -- the gamepad +is then dead after resume until the IRQ is re-requested. + +Add PM ops: on suspend quiesce the MCU (drop its enable rail and assert reset, +mirroring rsinput_remove) so the line goes silent before the UART suspends; on +resume re-power and re-initialise it. + +Signed-off-by: jaewun +--- +--- a/drivers/input/joystick/rsinput.c ++++ b/drivers/input/joystick/rsinput.c +@@ -612,6 +612,38 @@ + regulator_disable(drv->vdd); + } + ++static int rsinput_suspend(struct device *dev) ++{ ++ struct rsinput_driver *drv = dev_get_drvdata(dev); ++ ++ /* ++ * The MCU streams gamepad reports continuously over the UART. If it keeps ++ * sending across the system-suspend teardown, the geni-serial RX interrupt ++ * storms while the port is being stopped, the primary handler returns ++ * IRQ_NONE, and the kernel's spurious-IRQ detector permanently disables the ++ * UART IRQ -- leaving the gamepad dead after resume until the IRQ is ++ * re-requested. Quiesce the MCU (drop its enable rail and hold it in reset, ++ * mirroring rsinput_remove) so the line goes silent before the UART ++ * suspends; rsinput_resume() re-powers and re-initialises it. ++ */ ++ if (drv->enable_gpio) ++ gpiod_set_value_cansleep(drv->enable_gpio, 0); ++ ++ if (drv->reset_gpio) ++ gpiod_set_value_cansleep(drv->reset_gpio, 0); ++ ++ return 0; ++} ++ ++static int rsinput_resume(struct device *dev) ++{ ++ struct rsinput_driver *drv = dev_get_drvdata(dev); ++ ++ return rsinput_init_commands(drv); ++} ++ ++static DEFINE_SIMPLE_DEV_PM_OPS(rsinput_pm_ops, rsinput_suspend, rsinput_resume); ++ + static const struct of_device_id rsinput_of_match[] = { + { .compatible = "gamepad,rsinput" }, + { /* sentinel */ } +@@ -624,6 +656,7 @@ + .driver = { + .name = "rsinput", + .of_match_table = rsinput_of_match, ++ .pm = pm_sleep_ptr(&rsinput_pm_ops), + }, + }; + diff --git a/projects/ROCKNIX/devices/SM8550/patches/linux/1006-tty-serial-qcom-geni-mask-non-console-irq-on-suspend.patch b/projects/ROCKNIX/devices/SM8550/patches/linux/1006-tty-serial-qcom-geni-mask-non-console-irq-on-suspend.patch new file mode 100644 index 00000000000..6738c87c8db --- /dev/null +++ b/projects/ROCKNIX/devices/SM8550/patches/linux/1006-tty-serial-qcom-geni-mask-non-console-irq-on-suspend.patch @@ -0,0 +1,69 @@ +From: jaewun +Subject: [PATCH] tty: serial: qcom-geni: mask non-console IRQ across system suspend + +A peripheral that streams continuously over a non-console geni UART -- the AYN +gamepad MCU on 89c000.serial (irq 190) -- keeps transmitting across the +system-suspend teardown. uart_suspend_port() sets uport->suspended = 1 before +the line is quiet, and qcom_geni_serial_isr() then returns IRQ_NONE for every +subsequent interrupt WITHOUT clearing the GENI IRQ status. The level-triggered +line immediately re-asserts and storms; the kernel's spurious-interrupt detector +fires ("irq 190: nobody cared") and permanently disables the IRQ. That kills the +gamepad after resume, and the wedged geni state cascades into a hard suspend hang +on the following cycle ("first suspend works, the next one wedges"). + +The existing rsinput_suspend() MCU quiesce (1005) cannot prevent this on Thor: +the gamepad node has no reset-gpios, so only enable-gpios is toggled and the MCU +keeps its UART TX alive; the live trace confirms the storm still happens. + +Fix it at the controller: mask the IRQ at the GIC in qcom_geni_serial_suspend() +for non-console ports, before uart_suspend_port() runs. At that point +uport->suspended is still 0, so the ISR still services and clears the IRQ; +disable_irq() -> synchronize_irq() therefore drains the last in-flight handler +and returns, and the masked line delivers no further hardirqs, so the storm can +never reach the spurious detector. Balance it with enable_irq() in +qcom_geni_serial_resume(), after uart_resume_port() has re-initialised the port. + +The console UART (uart_console()) is deliberately left untouched. The non-console +geni UARTs here are not wakeup sources (no wakeup-source in DT, device_may_wakeup() +is false), so the enable_irq_wake() early-return in uart_suspend_port() is not +taken; the disable/enable pair is self-balanced regardless. + +Signed-off-by: jaewun +--- +diff --git a/drivers/tty/serial/qcom_geni_serial.c b/drivers/tty/serial/qcom_geni_serial.c +--- a/drivers/tty/serial/qcom_geni_serial.c ++++ b/drivers/tty/serial/qcom_geni_serial.c +@@ -1956,6 +1956,23 @@ + struct qcom_geni_private_data *private_data = uport->private_data; + + /* ++ * A peer that keeps streaming into a non-console geni UART across the ++ * suspend teardown (e.g. the gamepad MCU on irq 190) storms the RX IRQ ++ * once uart_suspend_port() sets uport->suspended: the ISR then returns ++ * IRQ_NONE without clearing the line, the level IRQ re-asserts, and the ++ * spurious-IRQ detector ("nobody cared") permanently disables it -- which ++ * kills the gamepad after resume and wedges the next suspend. Mask it ++ * here, while the ISR still services and acks the IRQ, so synchronize_irq() ++ * drains the last handler cleanly and the masked line delivers no further ++ * hardirqs. These geni UARTs are not wakeup sources, so the ++ * device_may_wakeup() early-return in uart_suspend_port() is not taken and ++ * this disable_irq() is balanced by enable_irq() in ++ * qcom_geni_serial_resume(). ++ */ ++ if (!uart_console(uport)) ++ disable_irq(uport->irq); ++ ++ /* + * This is done so we can hit the lowest possible state in suspend + * even with no_console_suspend + */ +@@ -1974,6 +1991,9 @@ + struct qcom_geni_private_data *private_data = uport->private_data; + + ret = uart_resume_port(private_data->drv, uport); ++ /* Balance the disable_irq() taken in qcom_geni_serial_suspend(). */ ++ if (!uart_console(uport)) ++ enable_irq(uport->irq); + if (uart_console(uport)) { + geni_icc_set_tag(&port->se, QCOM_ICC_TAG_ALWAYS); + geni_icc_set_bw(&port->se); diff --git a/projects/ROCKNIX/devices/SM8550/patches/linux/1007-scsi-ufs-qcom-propagate-hibern8-exit-failure-clk-scale.patch b/projects/ROCKNIX/devices/SM8550/patches/linux/1007-scsi-ufs-qcom-propagate-hibern8-exit-failure-clk-scale.patch new file mode 100644 index 00000000000..912fb147c80 --- /dev/null +++ b/projects/ROCKNIX/devices/SM8550/patches/linux/1007-scsi-ufs-qcom-propagate-hibern8-exit-failure-clk-scale.patch @@ -0,0 +1,34 @@ +From: jaewun +Subject: [PATCH] scsi: ufs: qcom: propagate hibern8 exit failure from clk_scale_notify + +ufs_qcom_clk_scale_notify() wakes the link with ufshcd_uic_hibern8_exit() +after a POST_CHANGE clock scale but drops its return value and always returns 0. +Every other error in this function (hibern8 enter, the pre/post change helpers) +is captured and returned; only this success-path link-wake was silently ignored. + +If the link fails to exit hibern8, the function still reports success to the +clock-scaling core. ufshcd_scale_clks() consumes a non-zero POST_CHANGE return: +on failure it rolls back the OPP/clock frequency, leaves target_freq unchanged, +and lets devfreq retry on the next poll. Returning 0 bypasses that rollback. + +Capture and return the error so the failure is propagated to the +clock-scaling/devfreq accounting and rollback path instead of being masked as +success. Link recovery is still handled by the existing UIC error path; this +change only fixes the clk-scaling return value. + +Signed-off-by: jaewun +--- +diff --git a/drivers/ufs/host/ufs-qcom.c b/drivers/ufs/host/ufs-qcom.c +--- a/drivers/ufs/host/ufs-qcom.c ++++ b/drivers/ufs/host/ufs-qcom.c +@@ -1779,8 +1779,8 @@ + } + + ufs_qcom_icc_update_bw(host); +- ufshcd_uic_hibern8_exit(hba); ++ err = ufshcd_uic_hibern8_exit(hba); + } + +- return 0; ++ return err; + } diff --git a/projects/ROCKNIX/devices/SM8550/patches/linux/1008-scsi-ufs-qcom-auto-hibern8-clk-gating-collision.patch b/projects/ROCKNIX/devices/SM8550/patches/linux/1008-scsi-ufs-qcom-auto-hibern8-clk-gating-collision.patch new file mode 100644 index 00000000000..a8c47fcb8c5 --- /dev/null +++ b/projects/ROCKNIX/devices/SM8550/patches/linux/1008-scsi-ufs-qcom-auto-hibern8-clk-gating-collision.patch @@ -0,0 +1,38 @@ +From: jaewun +Subject: [PATCH] scsi: ufs: qcom: disable HW auto-hibern8 with clk-gating hibern8 + +The qcom host sets UFSHCD_CAP_HIBERN8_WITH_CLK_GATING, so ufshcd_gate_work() +parks the link with a SW DME_HIBERNATE_ENTER before gating clocks. The core also +defaults HW auto-hibern8 on (ahit = 150 ms). On idle the HW can park the M-PHY +first; ufshcd_gate_work() then issues a redundant SW hibern8-enter on the +already-parked link. That can time out waiting for power-mode-change +completion and mark the link broken. + +HW auto-hibern8 and SW clk-gating hibern8 are mutually exclusive ways to park +the link. Keep the SW clk-gating path, which runs in ufshcd_gate_work() with +the IRQ still enabled, and disable HW auto-hibern8 via +UFSHCD_QUIRK_BROKEN_AUTO_HIBERN8. + +Signed-off-by: jaewun +--- +--- a/drivers/ufs/host/ufs-qcom.c ++++ b/drivers/ufs/host/ufs-qcom.c +@@ -1157,6 +1157,18 @@ + + if (drvdata && drvdata->quirks) + hba->quirks |= drvdata->quirks; ++ ++ /* ++ * This host uses UFSHCD_CAP_HIBERN8_WITH_CLK_GATING, i.e. ufshcd_gate_work() ++ * issues a SW DME_HIBERNATE_ENTER to park the link before gating clocks. ++ * Leaving HW auto-hibern8 enabled too makes the HW park the link first; ++ * gate_work then issues a redundant SW hibern8-enter on the already-parked ++ * link, which can time out waiting for power-mode-change completion. ++ * The two mechanisms are mutually exclusive; keep the SW clk-gating ++ * path and disable HW auto-hibern8. ++ */ ++ if (hba->caps & UFSHCD_CAP_HIBERN8_WITH_CLK_GATING) ++ hba->quirks |= UFSHCD_QUIRK_BROKEN_AUTO_HIBERN8; + } + + static void ufs_qcom_set_phy_gear(struct ufs_qcom_host *host) diff --git a/projects/ROCKNIX/devices/SM8550/patches/linux/1009-scsi-ufs-recover-hibern8-enter-clk-gating.patch b/projects/ROCKNIX/devices/SM8550/patches/linux/1009-scsi-ufs-recover-hibern8-enter-clk-gating.patch new file mode 100644 index 00000000000..ee1d1c5c0f5 --- /dev/null +++ b/projects/ROCKNIX/devices/SM8550/patches/linux/1009-scsi-ufs-recover-hibern8-enter-clk-gating.patch @@ -0,0 +1,146 @@ +From: gh123man +Subject: [PATCH] scsi: ufs: recover clock-gating hibern8-enter failures inline + +ufshcd_gate_work() only restores the software clock-gating state to CLKS_ON +when DME_HIBER_ENTER times out. It does not repair the UFS link. This is +especially destructive while system PM is in progress: pm_op_in_progress +intentionally suppresses the generic fatal error handler, so the controller +is left operational in software but unable to complete device commands. +Subsequent filesystem I/O repeatedly times out and a following suspend can +block forever in the filesystem sync, after userspace has already blanked +the display. + +Recover a failed clock-gating hibern8 enter inline. Keep +clk_gating.is_suspended set around the UIC command and recovery so the +generic UIC error path cannot race a second error-handler reset when the +failure occurs outside system PM. On this kernel the UFS interrupt is +serviced entirely in hardirq context, so link recovery completes through +the normal handler and needs no out-of-band poller. + +After the first failure, pin runtime UFS clock gating off for the remainder +of the boot. This is the same active-request pin used by the clkgate_enable +sysfs control. System PM remains enabled and can still transition the UFS +device and link for suspend, while the failed idle Hibern8 path is not +retried. + +Capture the clock-gating and PM state when the worker is admitted, +immediately before DME_HIBER_ENTER, and after a failure. Emit the +snapshots only on the failure path. This makes it possible to distinguish +a controller failure from a race where system suspend requests clocks back +on after the gate worker has passed its initial eligibility check. + +Signed-off-by: gh123man +--- +--- a/drivers/ufs/core/ufshcd.c 2026-08-25 21:37:00.258096745 +0800 ++++ b/drivers/ufs/core/ufshcd.c 2026-08-25 21:37:15.670766966 +0800 +@@ -2029,6 +2029,9 @@ + { + struct ufs_hba *hba = container_of(work, struct ufs_hba, + clk_gating.gate_work.work); ++ enum clk_gating_state admitted_state; ++ int admitted_active_reqs; ++ bool admitted_pm; + int ret; + + scoped_guard(spinlock_irqsave, &hba->clk_gating.lock) { +@@ -2048,6 +2051,15 @@ + + if (hba->clk_gating.active_reqs) + return; ++ ++ /* ++ * Preserve the state that admitted this worker. A suspend request ++ * can race after this lock is dropped and change the gating state ++ * before the Hibern8 command is sent. ++ */ ++ admitted_state = hba->clk_gating.state; ++ admitted_active_reqs = hba->clk_gating.active_reqs; ++ admitted_pm = READ_ONCE(hba->pm_op_in_progress); + } + + scoped_guard(spinlock_irqsave, hba->host->host_lock) { +@@ -2058,16 +2070,82 @@ + + /* put the link into hibern8 mode before turning off clocks */ + if (ufshcd_can_hibern8_during_gating(hba)) { ++ enum clk_gating_state h8_start_state; ++ int h8_start_active_reqs; ++ bool h8_start_pm; ++ ++ /* Snapshot any state transition since the worker was admitted. */ ++ scoped_guard(spinlock_irqsave, &hba->clk_gating.lock) { ++ h8_start_state = hba->clk_gating.state; ++ h8_start_active_reqs = hba->clk_gating.active_reqs; ++ } ++ h8_start_pm = READ_ONCE(hba->pm_op_in_progress); ++ ++ /* ++ * Suppress the generic fatal UIC error path while this worker owns ++ * recovery. System PM already does this through pm_op_in_progress, ++ * but an ordinary idle-gating failure needs the same exclusion. ++ */ ++ hba->clk_gating.is_suspended = true; + ret = ufshcd_uic_hibern8_enter(hba); + if (ret) { +- hba->clk_gating.state = CLKS_ON; +- dev_err(hba->dev, "%s: hibern8 enter failed %d\n", ++ enum clk_gating_state failed_state; ++ int failed_active_reqs; ++ bool failed_enabled; ++ bool failed_suspended; ++ ++ scoped_guard(spinlock_irqsave, &hba->clk_gating.lock) { ++ failed_state = hba->clk_gating.state; ++ failed_active_reqs = hba->clk_gating.active_reqs; ++ failed_enabled = hba->clk_gating.is_enabled; ++ failed_suspended = hba->clk_gating.is_suspended; ++ hba->clk_gating.state = CLKS_ON; ++ } ++ dev_err(hba->dev, ++ "%s: hibern8 enter failed %d, recovering link\n", + __func__, ret); ++ dev_err(hba->dev, ++ "%s: gate race snapshot: admitted state=%d active=%d pm=%d; h8-start state=%d active=%d pm=%d; failed state=%d active=%d enabled=%d suspended=%d pm=%d outstanding=%#lx gate-work=%u ungate-work=%u\n", ++ __func__, admitted_state, admitted_active_reqs, ++ admitted_pm, ++ h8_start_state, h8_start_active_reqs, ++ h8_start_pm, ++ failed_state, failed_active_reqs, ++ failed_enabled, failed_suspended, ++ READ_ONCE(hba->pm_op_in_progress), ++ READ_ONCE(hba->outstanding_reqs), ++ work_busy(&hba->clk_gating.gate_work.work), ++ work_busy(&hba->clk_gating.ungate_work)); + trace_ufshcd_clk_gating(hba, + hba->clk_gating.state); ++ ++ ret = ufshcd_link_recovery(hba); ++ ++ /* ++ * Do not retry a destructive idle-Hibern8 transition this boot. ++ * Pinning active_reqs is how clkgate_enable=0 keeps normal ++ * hold/release pairs balanced while clock gating is disabled. ++ */ ++ scoped_guard(spinlock_irqsave, &hba->clk_gating.lock) { ++ if (hba->clk_gating.is_enabled) { ++ hba->clk_gating.active_reqs++; ++ hba->clk_gating.is_enabled = false; ++ } ++ } ++ hba->clk_gating.is_suspended = false; ++ ++ if (ret) ++ dev_err(hba->dev, ++ "%s: link recovery after hibern8 enter failed %d\n", ++ __func__, ret); ++ else ++ dev_warn(hba->dev, ++ "%s: link recovered; runtime clock gating disabled\n", ++ __func__); + return; + } + ufshcd_set_link_hibern8(hba); ++ hba->clk_gating.is_suspended = false; + } + + ufshcd_disable_irq(hba); diff --git a/projects/ROCKNIX/devices/SM8550/patches/linux/1010-scsi-ufs-qcom-keep-mphy-powered-on-hibern8-park.patch b/projects/ROCKNIX/devices/SM8550/patches/linux/1010-scsi-ufs-qcom-keep-mphy-powered-on-hibern8-park.patch new file mode 100644 index 00000000000..17733b2c550 --- /dev/null +++ b/projects/ROCKNIX/devices/SM8550/patches/linux/1010-scsi-ufs-qcom-keep-mphy-powered-on-hibern8-park.patch @@ -0,0 +1,80 @@ +From: jaewun +Subject: [PATCH] scsi: ufs: qcom: keep M-PHY powered across hibern8 parking on no_phy_retention + +The SM8550 qcom host is flagged no_phy_retention: phy_power_off() can drop +calibrated M-PHY state, and recalibration is done by full link startup through +ufs_qcom_power_up_sequence(). + +ufs_qcom_setup_clocks(on=false) is also used for SW clk-gating and runtime PM +cases where the UFS link is only parked in HIBERN8. The matching resume path +issues DME_HIBERNATE_EXIT, which assumes retained PHY state rather than running +a full link startup. + +On no_phy_retention hosts, keep the PHY powered when the link is merely parked +in HIBERN8: skip phy_power_off() on the way down and the matching phy_power_on() +on the way up. True LINK_OFF system suspend is unchanged and still powers the +PHY off before recalibrating on resume. + +Signed-off-by: jaewun + +--- a/drivers/ufs/host/ufs-qcom.c ++++ b/drivers/ufs/host/ufs-qcom.c +@@ -1253,7 +1253,9 @@ + enum ufs_notify_change_status status) + { + struct ufs_qcom_host *host = ufshcd_get_variant(hba); ++ const struct ufs_qcom_drvdata *drvdata; + struct phy *phy; ++ bool keep_phy; + int err; + + /* +@@ -1264,7 +1266,16 @@ + if (!host) + return 0; + ++ drvdata = of_device_get_match_data(hba->dev); + phy = host->generic_phy; ++ ++ /* ++ * no_phy_retention hosts can lose calibrated M-PHY state on phy_power_off(). ++ * Keep the PHY powered when the link is only parked in HIBERN8; true ++ * LINK_OFF still powers the PHY off and recalibrates on resume. ++ */ ++ keep_phy = drvdata && drvdata->no_phy_retention && ++ ufs_qcom_is_link_hibern8(hba); + + switch (status) { + case PRE_CHANGE: +@@ -1283,19 +1294,23 @@ + ufs_qcom_dev_ref_clk_ctrl(host, false); + } + +- err = phy_power_off(phy); +- if (err) { +- dev_err(hba->dev, "phy power off failed, ret=%d\n", err); +- return err; ++ if (!keep_phy) { ++ err = phy_power_off(phy); ++ if (err) { ++ dev_err(hba->dev, "phy power off failed, ret=%d\n", err); ++ return err; ++ } + } + } + break; + case POST_CHANGE: + if (on) { +- err = phy_power_on(phy); +- if (err) { +- dev_err(hba->dev, "phy power on failed, ret = %d\n", err); +- return err; ++ if (!keep_phy) { ++ err = phy_power_on(phy); ++ if (err) { ++ dev_err(hba->dev, "phy power on failed, ret = %d\n", err); ++ return err; ++ } + } + + /* enable the device ref clock for HS mode*/ diff --git a/projects/ROCKNIX/devices/SM8550/patches/linux/1011-scsi-ufs-hold-clk-gating-across-system-pm.patch b/projects/ROCKNIX/devices/SM8550/patches/linux/1011-scsi-ufs-hold-clk-gating-across-system-pm.patch new file mode 100644 index 00000000000..b54dc5d6bb8 --- /dev/null +++ b/projects/ROCKNIX/devices/SM8550/patches/linux/1011-scsi-ufs-hold-clk-gating-across-system-pm.patch @@ -0,0 +1,96 @@ +From: gh123man +Subject: [PATCH] scsi: ufs: hold clock gating across system PM + +Hardware diagnostics captured a software clock-gating worker entering +DME_HIBER_ENTER with REQ_CLKS_OFF, no active requests, and no PM operation +in progress. While that command was still running, system suspend reached +UFS: pm_op_in_progress became set, ufshcd_hold() changed the state to +REQ_CLKS_ON, and the ungate worker became pending behind the running gate +worker. The UIC command then timed out while the PM path waited for that +worker. + +Serialize these paths at the existing system-PM prepare/complete boundary. +When the well-known LUN is not already runtime suspended in the exact +system suspend state, take a clock-gating hold after runtime resume in +ufshcd_suspend_prepare(). This cancels or drains any pending gate worker +while normal completion handling is available. Retain the hold across all +device suspend and resume callbacks, then release it from +ufshcd_resume_complete(). + +The normal UFS suspend path keeps its own balanced hold, so this extra pin +does not prevent explicit system power transitions. It only prevents the +asynchronous idle-gating Hibern8 path from starting inside the system-PM +window. Devices already runtime suspended in the desired system state +need no extra hold and remain untouched. + +Signed-off-by: gh123man +--- +--- a/drivers/ufs/core/ufshcd.c ++++ b/drivers/ufs/core/ufshcd.c +@@ -11439,6 +11439,10 @@ void ufshcd_resume_complete(struct device *dev) + { + struct ufs_hba *hba = dev_get_drvdata(dev); + ++ if (hba->complete_clkgate_hold) { ++ hba->complete_clkgate_hold = false; ++ ufshcd_release(hba); ++ } + if (hba->complete_put) { + ufshcd_rpm_put(hba); + hba->complete_put = false; +@@ -11470,5 +11474,6 @@ int __ufshcd_suspend_prepare(struct device *dev, bool rpm_ok_for_spm) + { + struct ufs_hba *hba = dev_get_drvdata(dev); ++ bool rpm_ready_for_spm; + int ret; + + /* +@@ -11480,17 +11485,28 @@ int __ufshcd_suspend_prepare(struct device *dev, bool rpm_ok_for_spm) + if (hba->ufs_device_wlun) { + /* Prevent runtime suspend */ + ufshcd_rpm_get_noresume(hba); ++ rpm_ready_for_spm = rpm_ok_for_spm && ufshcd_rpm_ok_for_spm(hba); + /* + * Check if already runtime suspended in same state as system + * suspend would be. + */ +- if (!rpm_ok_for_spm || !ufshcd_rpm_ok_for_spm(hba)) { ++ if (!rpm_ready_for_spm) { + /* RPM state is not ok for SPM, so runtime resume */ + ret = ufshcd_rpm_resume(hba); + if (ret < 0 && ret != -EACCES) { + ufshcd_rpm_put(hba); + return ret; + } ++ ++ /* ++ * Serialize system PM against the asynchronous clock-gating ++ * worker. ufshcd_hold() cancels or drains a pending gate while ++ * normal IRQ completion is still available. Keeping this hold ++ * until .complete prevents a new idle Hibern8 command from racing ++ * any suspend or resume phase. ++ */ ++ ufshcd_hold(hba); ++ hba->complete_clkgate_hold = true; + } + hba->complete_put = true; + } +--- a/include/ufs/ufshcd.h ++++ b/include/ufs/ufshcd.h +@@ -929,6 +929,8 @@ struct ufs_hba { + * @nr_queues: number of Queues of different queue types + * @complete_put: whether or not to call ufshcd_rpm_put() from inside + * ufshcd_resume_complete() ++ * @complete_clkgate_hold: whether ufshcd_resume_complete() must release ++ * clock-gating hold acquired by ufshcd_suspend_prepare() + * @mcq_sup: is mcq supported by UFSHC + * @mcq_enabled: is mcq ready to accept requests + * @mcq_esi_enabled: is mcq ESI configured +@@ -1110,6 +1112,7 @@ struct ufs_hba { + unsigned int nr_hw_queues; + unsigned int nr_queues[HCTX_MAX_TYPES]; + bool complete_put; ++ bool complete_clkgate_hold; + bool scsi_host_added; + bool mcq_sup; + bool lsdb_sup; diff --git a/projects/ROCKNIX/devices/SM8550/patches/linux/1015-ufs-qcom-disable-rx-linecfg-after-link-startup.patch b/projects/ROCKNIX/devices/SM8550/patches/linux/1015-ufs-qcom-disable-rx-linecfg-after-link-startup.patch new file mode 100644 index 00000000000..6984f9f16c4 --- /dev/null +++ b/projects/ROCKNIX/devices/SM8550/patches/linux/1015-ufs-qcom-disable-rx-linecfg-after-link-startup.patch @@ -0,0 +1,160 @@ +From: jaewun +Subject: [PATCH] phy: qcom-qmp-ufs: allow UFS hosts to control RX LineCfg + +Qualcomm downstream UFS hosts keep PHY RX LineCfg enabled for link startup and +disable it immediately afterward. The downstream comment says some UFS devices +send incorrect LineCfg during power-mode-change, which can put the host PHY in +an invalid state. + +Add a QMP UFS helper for the host driver to toggle the PCS LINECFG_DISABLE bit +and call it from the existing Qualcomm link-startup PRE/POST hooks. + +This keeps RX LineCfg enabled while the link starts, then disables host RX +LineCfg before post-startup power-mode-change traffic. + +Signed-off-by: jaewun + +--- a/drivers/phy/qualcomm/phy-qcom-qmp-ufs.c ++++ b/drivers/phy/qualcomm/phy-qcom-qmp-ufs.c +@@ -14,5 +14,6 @@ + #include + #include ++#include + #include + #include + #include +@@ -40,12 +41,16 @@ + #define NUM_OVERLAY 2 + ++/* QPHY LINECFG_DISABLE bit */ ++#define RX_LINECFG_DISABLE BIT(1) ++ + /* set of registers with offsets different per-PHY */ + enum qphy_reg_layout { + /* PCS registers */ + QPHY_SW_RESET, + QPHY_START_CTRL, + QPHY_PCS_READY_STATUS, + QPHY_PCS_POWER_DOWN_CONTROL, ++ QPHY_LINECFG_DISABLE, + /* Keep last to ensure regs_layout arrays are properly initialized */ + QPHY_LAYOUT_SIZE + }; +@@ -66,6 +71,7 @@ static const unsigned int ufsphy_v4_regs_layout[QPHY_LAYOUT_SIZE] = { + [QPHY_PCS_READY_STATUS] = QPHY_V4_PCS_UFS_READY_STATUS, + [QPHY_SW_RESET] = QPHY_V4_PCS_UFS_SW_RESET, + [QPHY_PCS_POWER_DOWN_CONTROL] = QPHY_V4_PCS_UFS_POWER_DOWN_CONTROL, ++ [QPHY_LINECFG_DISABLE] = QPHY_V4_PCS_UFS_LINECFG_DISABLE, + }; + + static const unsigned int ufsphy_v5_regs_layout[QPHY_LAYOUT_SIZE] = { +@@ -81,6 +87,7 @@ static const unsigned int ufsphy_v6_regs_layout[QPHY_LAYOUT_SIZE] = { + [QPHY_PCS_READY_STATUS] = QPHY_V6_PCS_UFS_READY_STATUS, + [QPHY_SW_RESET] = QPHY_V6_PCS_UFS_SW_RESET, + [QPHY_PCS_POWER_DOWN_CONTROL] = QPHY_V6_PCS_UFS_POWER_DOWN_CONTROL, ++ [QPHY_LINECFG_DISABLE] = QPHY_V6_PCS_UFS_LINECFG_DISABLE, + }; + + static const struct qmp_phy_init_tbl milos_ufsphy_serdes[] = { +@@ -1920,6 +1927,39 @@ static void qmp_ufs_init(struct qmp_ufs *qmp) + qmp_ufs_init_all(qmp, &cfg->tbls_hs_b); + } + ++int qcom_qmp_ufs_ctrl_rx_linecfg(struct phy *phy, bool enable) ++{ ++ struct qmp_ufs *qmp; ++ const struct qmp_phy_cfg *cfg; ++ u32 offset; ++ u32 before; ++ u32 after; ++ ++ if (!phy) ++ return -ENODEV; ++ ++ qmp = phy_get_drvdata(phy); ++ if (!qmp || !qmp->pcs) ++ return -ENODEV; ++ ++ cfg = qmp->cfg; ++ offset = cfg->regs[QPHY_LINECFG_DISABLE]; ++ if (!offset) ++ return -EOPNOTSUPP; ++ ++ before = readl(qmp->pcs + offset); ++ after = enable ? before & ~RX_LINECFG_DISABLE : ++ before | RX_LINECFG_DISABLE; ++ writel(after, qmp->pcs + offset); ++ after = readl(qmp->pcs + offset); ++ ++ dev_dbg(qmp->dev, "UFS PHY RX LineCfg %s: reg=0x%x -> 0x%x\n", ++ enable ? "enabled" : "disabled", before, after); ++ ++ return 0; ++} ++EXPORT_SYMBOL_GPL(qcom_qmp_ufs_ctrl_rx_linecfg); ++ + static int qmp_ufs_power_on(struct phy *phy) + { + struct qmp_ufs *qmp = phy_get_drvdata(phy); +--- /dev/null ++++ b/include/linux/phy/phy-qcom-qmp-ufs.h +@@ -0,0 +1,20 @@ ++/* SPDX-License-Identifier: GPL-2.0 */ ++#ifndef __PHY_QCOM_QMP_UFS_H__ ++#define __PHY_QCOM_QMP_UFS_H__ ++ ++#include ++#include ++#include ++ ++struct phy; ++ ++#if IS_ENABLED(CONFIG_PHY_QCOM_QMP_UFS) ++int qcom_qmp_ufs_ctrl_rx_linecfg(struct phy *phy, bool enable); ++#else ++static inline int qcom_qmp_ufs_ctrl_rx_linecfg(struct phy *phy, bool enable) ++{ ++ return -EOPNOTSUPP; ++} ++#endif ++ ++#endif +--- a/drivers/ufs/host/ufs-qcom.c 2026-08-25 21:46:04.636073768 +0800 ++++ b/drivers/ufs/host/ufs-qcom.c 2026-08-25 21:46:06.557198106 +0800 +@@ -13,6 +13,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -715,10 +716,17 @@ + static int ufs_qcom_link_startup_notify(struct ufs_hba *hba, + enum ufs_notify_change_status status) + { ++ struct ufs_qcom_host *host = ufshcd_get_variant(hba); + int err = 0; ++ int linecfg_err; + + switch (status) { + case PRE_CHANGE: ++ linecfg_err = qcom_qmp_ufs_ctrl_rx_linecfg(host->generic_phy, true); ++ if (linecfg_err && linecfg_err != -EOPNOTSUPP) ++ dev_warn(hba->dev, "failed to enable RX LineCfg: %d\n", ++ linecfg_err); ++ + if (ufs_qcom_cfg_timers(hba, false, ULONG_MAX)) { + dev_err(hba->dev, "%s: ufs_qcom_cfg_timers() failed\n", + __func__); +@@ -739,6 +747,10 @@ + + break; + case POST_CHANGE: ++ linecfg_err = qcom_qmp_ufs_ctrl_rx_linecfg(host->generic_phy, false); ++ if (linecfg_err && linecfg_err != -EOPNOTSUPP) ++ dev_warn(hba->dev, "failed to disable RX LineCfg: %d\n", ++ linecfg_err); + ufs_qcom_link_startup_post_change(hba); + break; + default: diff --git a/projects/ROCKNIX/devices/SM8550/patches/linux/1016-leds-qcom-lpg-restore-led-state-across-suspend.patch b/projects/ROCKNIX/devices/SM8550/patches/linux/1016-leds-qcom-lpg-restore-led-state-across-suspend.patch new file mode 100644 index 00000000000..e9e9a84fc29 --- /dev/null +++ b/projects/ROCKNIX/devices/SM8550/patches/linux/1016-leds-qcom-lpg-restore-led-state-across-suspend.patch @@ -0,0 +1,101 @@ +From: Edouard Durand +Subject: [PATCH] leds: qcom-lpg: keep LED state across suspend, off in noirq + +The LPG lives on the PMIC, so a lit LED stays lit for the whole sleep +once the SoC is down. Turn every LED off in the suspend noirq phase, +the last step before the platform enters sleep, and restore the +programmed state in resume noirq, before any other device resumes. A +sleep indication armed by userspace then survives the whole suspend +entry, the LED is dark while sleeping, and the same indication is the +first sign of life on wake, well before userspace thaws. + +The noirq phase is deliberate: the LED class helper suspends LEDs in +the normal device phase, where the slower devices suspending after it +would leave the LED dark seconds before the system actually sleeps. + +Signed-off-by: Edouard Durand +--- +--- a/drivers/leds/rgb/leds-qcom-lpg.c 2026-08-25 21:19:43.621064255 +0800 ++++ b/drivers/leds/rgb/leds-qcom-lpg.c 2026-08-25 21:19:47.434754677 +0800 +@@ -162,6 +162,8 @@ + struct led_classdev cdev; + struct led_classdev_mc mcdev; + ++ struct list_head node; ++ + unsigned int num_channels; + struct lpg_channel *channels[] __counted_by(num_channels); + }; +@@ -193,6 +195,8 @@ + + struct mutex lock; + ++ struct list_head leds; ++ + struct pwm_chip *pwm; + + const struct lpg_data *data; +@@ -1464,6 +1468,8 @@ + ret = devm_led_classdev_register_ext(lpg->dev, &led->cdev, &init_data); + if (ret) + dev_err_probe(lpg->dev, ret, "unable to register %s\n", cdev->name); ++ else ++ list_add(&led->node, &lpg->leds); + + return ret; + } +@@ -1594,6 +1600,37 @@ + return 0; + } + ++static struct led_classdev *lpg_led_cdev(struct lpg_led *led) ++{ ++ return led->num_channels > 1 ? &led->mcdev.led_cdev : &led->cdev; ++} ++ ++static int lpg_suspend_noirq(struct device *dev) ++{ ++ struct lpg *lpg = dev_get_drvdata(dev); ++ struct lpg_led *led; ++ ++ list_for_each_entry(led, &lpg->leds, node) ++ led_classdev_suspend(lpg_led_cdev(led)); ++ ++ return 0; ++} ++ ++static int lpg_resume_noirq(struct device *dev) ++{ ++ struct lpg *lpg = dev_get_drvdata(dev); ++ struct lpg_led *led; ++ ++ list_for_each_entry(led, &lpg->leds, node) ++ led_classdev_resume(lpg_led_cdev(led)); ++ ++ return 0; ++} ++ ++static const struct dev_pm_ops lpg_pm_ops = { ++ NOIRQ_SYSTEM_SLEEP_PM_OPS(lpg_suspend_noirq, lpg_resume_noirq) ++}; ++ + static int lpg_probe(struct platform_device *pdev) + { + const struct lpg_data *data; +@@ -1613,6 +1650,8 @@ + lpg->data = data; + lpg->dev = &pdev->dev; + mutex_init(&lpg->lock); ++ INIT_LIST_HEAD(&lpg->leds); ++ platform_set_drvdata(pdev, lpg); + + lpg->map = dev_get_regmap(pdev->dev.parent, NULL); + if (!lpg->map) +@@ -1841,6 +1880,7 @@ + .probe = lpg_probe, + .driver = { + .name = "qcom-spmi-lpg", ++ .pm = pm_sleep_ptr(&lpg_pm_ops), + .of_match_table = lpg_of_table, + }, + }; diff --git a/projects/ROCKNIX/devices/SM8550/patches/linux/1017-wifi-ath12k-let-a-pending-firmware-restart-finish-be.patch b/projects/ROCKNIX/devices/SM8550/patches/linux/1017-wifi-ath12k-let-a-pending-firmware-restart-finish-be.patch new file mode 100755 index 00000000000..b2550e9e944 --- /dev/null +++ b/projects/ROCKNIX/devices/SM8550/patches/linux/1017-wifi-ath12k-let-a-pending-firmware-restart-finish-be.patch @@ -0,0 +1,58 @@ +From 4dfe10c05da623862733003de47f7dec9062d789 Mon Sep 17 00:00:00 2001 +From: Edouard Durand +Date: Thu, 13 Aug 2026 10:10:47 +0800 +Subject: [PATCH] wifi: ath12k: let a pending firmware restart finish before + suspending + +ath12k reloads the firmware on every system resume: resume_early() powers the +device up and the QMI FW_READY that follows queues restart_work, which tears +the data path down and rebuilds it through ath12k_core_reconfigure_on_crash(). +ath12k_core_resume() waits ATH12K_RESET_TIMEOUT_HZ for it, and when that wait +is satisfied the worker really has finished, because the completion is its last +statement. + +Two windows escape that. A resume whose wait expires returns -ETIMEDOUT and +leaves the worker running past the thaw, and a reset_work recovery queues the +same worker with no wait in front of it at all. In both, the next suspend can +reach ath12k_core_suspend_late() while the worker is midway through the +firmware download and the QMI handshake, and ath12k_hif_power_down() then stops +MHI underneath it. + +srng, dp and wmi have been torn down but not rebuilt at that point, so nothing +guarantees what the device comes back as, and an rfkill cycle would not put it +right because nothing on that path re-runs probe. + +This is a race found by reading the code, not one reproduced on demand. On a +WCN7850 handheld that suspends and resumes constantly it has not been observed +to fire, but the second window needs no unusual timing to open: a firmware +crash can queue that worker at any moment, including while a suspend is on its +way down, and no amount of care in userspace can see that coming. + +Flush restart_work before the suspend path touches the device. It costs a +bounded wait on a path that already tolerates ATH12K_RESET_TIMEOUT_HZ. + +Signed-off-by: Edouard Durand +--- + drivers/net/wireless/ath/ath12k/core.c | 6 ++++++ + 1 file changed, 6 insertions(+) + +diff --git a/drivers/net/wireless/ath/ath12k/core.c b/drivers/net/wireless/ath/ath12k/core.c +index 980a12f..34d0a9e 100644 +--- a/drivers/net/wireless/ath/ath12k/core.c ++++ b/drivers/net/wireless/ath/ath12k/core.c +@@ -121,6 +121,12 @@ int ath12k_core_suspend(struct ath12k_base *ab) + struct ath12k *ar; + int ret, i; + ++ /* A firmware reload from a resume whose wait expired, or from a crash ++ * recovery, can still be running here. Powering the device down ++ * halfway through it leaves state only a driver reload rebuilds. ++ */ ++ flush_work(&ab->restart_work); ++ + ret = ath12k_core_continue_suspend_resume(ab); + if (ret <= 0) + return ret; +-- +2.49.0.windows.1 + diff --git a/projects/ROCKNIX/devices/SM8550/patches/linux/1018-wifi-ath12k-warn-when-the-system-sleeps-with-the-dev.patch b/projects/ROCKNIX/devices/SM8550/patches/linux/1018-wifi-ath12k-warn-when-the-system-sleeps-with-the-dev.patch new file mode 100755 index 00000000000..cbdb33795a6 --- /dev/null +++ b/projects/ROCKNIX/devices/SM8550/patches/linux/1018-wifi-ath12k-warn-when-the-system-sleeps-with-the-dev.patch @@ -0,0 +1,56 @@ +From f510e6ea4c3dda504cbdcda9bf5828a6b5088ca5 Mon Sep 17 00:00:00 2001 +From: Edouard Durand +Date: Thu, 13 Aug 2026 10:10:47 +0800 +Subject: [PATCH] wifi: ath12k: warn when the system sleeps with the device + still up + +The suspend callbacks do their work only when the hardware has reached +ATH12K_HW_STATE_OFF. Any other state is taken to mean wowlan armed the firmware +and the device must stay powered, so the callbacks return success and skip +everything. + +That assumption is unchecked. If the radio is brought up again while the system +is on its way down, or a recovery is still in flight, the state is not OFF and +no wowlan is armed either: the SoC then sleeps with a powered, un-suspended +chip, and on resume the driver skips the power up and carries on against a +device that has meanwhile been through a PCIe link down. The symptoms surface +much later and look nothing like a suspend bug. + +Log it, with the state that was caught, since ON, RESTARTING and WEDGED each +mean something different. This changes no behaviour. + +Signed-off-by: Edouard Durand +--- + drivers/net/wireless/ath/ath12k/core.c | 12 +++++++++++- + 1 file changed, 11 insertions(+), 1 deletion(-) + +diff --git a/drivers/net/wireless/ath/ath12k/core.c b/drivers/net/wireless/ath/ath12k/core.c +index 34d0a9e..fa62f63 100644 +--- a/drivers/net/wireless/ath/ath12k/core.c ++++ b/drivers/net/wireless/ath/ath12k/core.c +@@ -167,11 +167,21 @@ EXPORT_SYMBOL(ath12k_core_suspend); + + int ath12k_core_suspend_late(struct ath12k_base *ab) + { ++ struct ath12k *ar = ab->pdevs[0].ar; + int ret; + + ret = ath12k_core_continue_suspend_resume(ab); +- if (ret <= 0) ++ if (ret <= 0) { ++ /* Skipping the power down is only correct when wowlan is keeping ++ * the firmware alive; otherwise the SoC sleeps with a running ++ * chip that nothing will resynchronise on resume. ++ */ ++ if (!ret && !device_may_wakeup(ab->dev)) ++ ath12k_warn(ab, "suspending with the device still up, hw state %d, no wowlan armed\n", ++ (ar && ar->ah) ? ar->ah->state : -1); ++ + return ret; ++ } + + ath12k_acpi_stop(ab); + +-- +2.49.0.windows.1 + diff --git a/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/030-suspend_mode b/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/030-suspend_mode index 4f773ea5546..e240aca9d91 100755 --- a/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/030-suspend_mode +++ b/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/030-suspend_mode @@ -1,14 +1,55 @@ #!/bin/sh # SPDX-License-Identifier: GPL-2.0 # Copyright (C) 2023 JELOS (https://github.com/JustEnoughLinuxOS) +# Copyright (C) 2026 ROCKNIX (https://github.com/ROCKNIX) -### Sleep is currently broken, so we'll disable it. +. /etc/profile.d/001-functions -/usr/bin/suspendmode off +# Deep suspend is only enabled on devices where it has been validated. +# Everything else keeps the previous behavior (suspend disabled) until +# their remaining issues are resolved. +case "${QUIRK_DEVICE}" in + "Retroid Pocket 6"*|"AYN Odin 2"*|"AYN-Odin2"|"AYN Thor"|"AYN-Thor") + # Default to mem suspend and keep systemd on that state. + mkdir -p /storage/.config/sleep.conf.d + cat </storage/.config/sleep.conf.d/zz-sm8550-deeponly.conf +[Sleep] +SuspendState= +SuspendState=mem +EOF + + # bin/power-handler owns the power key here: it has to see the press + # before anything suspends, so that a device driving an external display + # blanks its own panel instead of taking the output down with the SoC. + # It calls systemctl suspend itself for every other case. The lid stays + # with logind. + cat <~/.config/logind.conf.d/login.conf +[Login] +HandlePowerKey=ignore +HandleSuspendKey=suspend +HandleLidSwitch=suspend +HandleLidSwitchExternalPower=suspend +HandleLidSwitchDocked=ignore +EOF + + MYSLEEPMODE=$(get_setting system.suspendmode) + if [ -z "${MYSLEEPMODE}" ] || [ "${MYSLEEPMODE}" = "off" ] + then + /usr/bin/suspendmode mem + fi + ;; + *) + ### Suspend is not validated on this device yet, keep it disabled. + rm -f /storage/.config/sleep.conf.d/zz-sm8550-deeponly.conf -### Ignore power button presses for now, until we can finish up fixing sleep. -cat <~/.config/logind.conf.d/login.conf + /usr/bin/suspendmode off + + cat <~/.config/logind.conf.d/login.conf [Login] HandlePowerKey=ignore HandleSuspendKey=ignore EOF + ;; +esac + +systemctl try-restart systemd-logind.service 2>/dev/null || true diff --git a/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/031-audio-mute-recovery b/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/031-audio-mute-recovery new file mode 100644 index 00000000000..9f0d0b55ca0 --- /dev/null +++ b/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/031-audio-mute-recovery @@ -0,0 +1,30 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0 +# Copyright (C) 2026 ROCKNIX (https://github.com/ROCKNIX) + +# Clear a stale speaker mute left over from an interrupted suspend. +# +# The suspend-grace pre hook mutes the sink and relies on the post hook to +# unmute, tracked by a flag in /run. wireplumber persists the mute per route +# on disk, so a power loss while suspended keeps the mute across reboots +# with the flag gone; the device then boots silent with no UI to recover it. +# Those hooks are the only sink-mute writers on this platform, so any mute +# found on the internal speaker at boot is stale by definition. +# +# The sink registers a few seconds after wireplumber starts; retry in the +# background so autostart is not held up. Named sink, not @DEFAULT_SINK@: +# booting docked makes the display sink the default, and the stale mute +# still sits on the speaker. + +. /etc/profile + +( + for _ in $(seq 30); do + SINK=$(pactl list short sinks 2>/dev/null | awk '/Speaker/ {print $2; exit}') + if [ -n "${SINK}" ]; then + pactl set-sink-mute "${SINK}" false >/dev/null 2>&1 + exit 0 + fi + sleep 2 + done +) & diff --git a/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/bin/power-handler b/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/bin/power-handler new file mode 100755 index 00000000000..76d44892d86 --- /dev/null +++ b/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/bin/power-handler @@ -0,0 +1,138 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# Copyright (C) 2026 ROCKNIX (https://github.com/ROCKNIX) + +# Power key policy for SM8550 devices that have real suspend. +# +# rocknix-fake-suspend exists for devices that cannot sleep: it owns a state +# machine with timed shutdown, delay flags and lid handling, and it assumes +# nothing else will suspend underneath it. Running a device that *can* sleep +# through that machine leaves two owners of the display, and a real suspend +# arriving while it holds the screen blanked resumes to a dead panel. +# +# So the key is taken here instead: +# +# suspend not validated here -> hand straight back to fake suspend +# screen already blanked -> unblank, whatever put it there +# external display attached -> blank the internal panel, never suspend +# otherwise -> real suspend +# +# The "never suspend on an external display" rule is the one fake suspend +# already applies; keeping it here is what stops a docked device powering +# its TV output down mid-session. The blank state is ours and lives in one +# flag, which sleep.d tears down before any real suspend so the two paths +# can never overlap. + +. /etc/profile + +case $(get_setting system.loglevel) in +verbose) DEBUG=true ;; +*) DEBUG=false ;; +esac + +BLANK_FLAG="/run/power-blank-active.flag" + +# Devices where deep suspend is not validated keep the old behaviour exactly: +# 030-suspend_mode leaves system.suspendmode off for them, and fake suspend is +# still the right owner. Delegating rather than reimplementing keeps those +# devices byte-identical to a tree without this handler. +# +# "release" is ours and fake suspend knows nothing about it, so it must never +# be forwarded. +if [ "$1" != "release" ] && [ "$(get_setting system.suspendmode)" = "off" ]; then + ${DEBUG} && log $0 "Suspend not enabled here, deferring to fake suspend." + exec /usr/bin/rocknix-fake-suspend "$@" +fi + +# Blanking and the "is a display attached" question both come from +# external-display. It blanks internal panels only: a DPMS off on an active DP output makes +# msm_dp_display_disable() report the audio jack as unplugged, and the HPD +# plug events that would undo it are discarded while the display is off, so +# dock audio stays dead until a physical replug. +EXTERNAL_DISPLAY="/usr/bin/external-display" + +# External display dependency. The helper itself is not part of this series, +# and where it is missing every question below answers "not attached", so the +# handler suspends exactly as it would on a device with no external output. +# To drop external display handling altogether, remove this wrapper and the +# two branches that call it: the blank instead of suspend in "power", and the +# release in "release". +external_display() { + [ -x "${EXTERNAL_DISPLAY}" ] || return 1 + "${EXTERNAL_DISPLAY}" "$@" +} + +# Deliberately no sink muting. wireplumber persists mute per route, so a sink +# muted here that then disappears (external display audio across a dock change) +# stays muted on disk forever, with nothing to unmute it. A docked device with +# its screen off is still driving a TV, so its audio should keep playing. +# +# Deliberately no LED handling on the blank path. A dark power LED means +# asleep on this platform, and a docked device with its screen off is awake +# and charging. The suspend path below is the one exception: sleep.d does not +# light the sleep indication until after the WiFi teardown, seconds after the +# press, so the press looks dead and invites a second press that wakes the +# device. Light it here, at press time; 000-led repeats the same writes later +# and the post hooks restore as usual. +POWER_LED="/sys/class/leds/power-led" + +case "$1" in + power) + # The blank is ours, so a press always ends it first. This also covers the + # display being blanked while docked and then undocked: without it the + # screen stays dark with nothing explaining why, and the next press would + # suspend a device the user thought was already asleep. + if [ -e "${BLANK_FLAG}" ]; then + ${DEBUG} && log $0 "Waking from blank." + rm -f "${BLANK_FLAG}" + external_display unblank + exit 0 + fi + + # Never suspend while an external display is attached: the output would go + # down with the SoC and the timed-shutdown path could power off a device + # that is driving a TV. The key becomes a screen toggle instead. + if external_display connected; then + ${DEBUG} && log $0 "External display attached, blanking instead of suspending." + touch "${BLANK_FLAG}" + external_display blank + exit 0 + fi + + ${DEBUG} && log $0 "Suspending." + if [ -d "${POWER_LED}" ]; then + echo none >"${POWER_LED}/trigger" + echo 0 0 255 >"${POWER_LED}/multi_intensity" + echo 255 >"${POWER_LED}/brightness" + fi + # Dark screen with the blue LED, at press time. Safe on an aborted + # suspend: resume re-powers the panel and the post hooks restore + # brightness, and the same restore runs on a failed sleep. + for BL in /sys/class/backlight/*/brightness; do + [ -f "${BL}" ] && echo 0 >"${BL}" + done + systemctl suspend + ;; + + release) + # Called from sleep.d/pre. A real suspend must never begin while we hold + # the panel blanked: "output ... power off" is compositor state that + # survives suspend, so resume would restore brightness onto a panel still + # switched off and the device would look dead, recoverable only over ssh. + if [ -e "${BLANK_FLAG}" ]; then + ${DEBUG} && log $0 "Releasing blank before suspend." + rm -f "${BLANK_FLAG}" + external_display unblank + fi + ;; + + lid) + # logind still owns the lid on this platform (HandleLidSwitch=suspend in + # 030-suspend_mode), so acting here as well would race its handler. Note + # that this means closing the lid on a docked device does suspend it, + # unlike the power key above. + ${DEBUG} && log $0 "Lid event left to logind: $2" + ;; +esac + +exit 0 diff --git a/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/sleep.d/post/000-suspend-grace b/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/sleep.d/post/000-suspend-grace new file mode 100755 index 00000000000..637960a4609 --- /dev/null +++ b/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/sleep.d/post/000-suspend-grace @@ -0,0 +1,20 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0 +# Copyright (C) 2026 ROCKNIX (https://github.com/ROCKNIX) + +# Record the resume point for the matching pre hook. CLOCK_MONOTONIC +# freezes during suspend, so the difference between two readings is time +# awake. /proc/uptime would not do: it advances while asleep. + +awk '/^now at/ {print int($3/1e9); exit}' /proc/timer_list >/run/.last_resume_monotonic + +# Undo the mute the pre hook took while it held the suspend for WiFi. Only +# what it muted: without the flag this would silently unmute a device the +# user muted. The flag is cleared only after pactl succeeds, so a sound +# server that is not yet responsive after thaw gets a retry on the next +# cycle instead of leaving the device muted. pactl needs XDG_RUNTIME_DIR, +# which the profile sets and this hook otherwise has no reason to load. +if [ -e /run/.grace-muted ]; then + . /etc/profile + pactl set-sink-mute @DEFAULT_SINK@ false >/dev/null 2>&1 && rm -f /run/.grace-muted +fi diff --git a/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/sleep.d/post/001-led b/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/sleep.d/post/001-led deleted file mode 100644 index a1c37fda5d4..00000000000 --- a/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/sleep.d/post/001-led +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/sh -# SPDX-License-Identifier: GPL-2.0 -# Copyright (C) 2024 ROCNIX (https://github.com/ROCKNIX) - -# Restore LED state that was saved prior to device sleep - -. /etc/profile - -### Get the current LED state and store it -SLEEP_LED_STATE=$(get_setting "sleep.led.color") -if [ ! -n "${SLEEP_LED_STATE}" ]; then - SLEEP_LED_STATE="battery" - set_setting "sleep.led.color" "battery" -fi - -### Turn on LEDS -/usr/bin/ledcontrol ${SLEEP_LED_STATE} diff --git a/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/sleep.d/post/001-usb-role b/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/sleep.d/post/001-usb-role new file mode 100755 index 00000000000..7afedc04d9a --- /dev/null +++ b/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/sleep.d/post/001-usb-role @@ -0,0 +1,15 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0 +# Copyright (C) 2026 ROCKNIX (https://github.com/ROCKNIX) + +# Restore the USB role saved by the pre hook's suspend-time toggle. + +for r in /sys/class/usb_role/*-role-switch/role; do + [ -f "$r" ] || continue + s="/run/.usb-role.$(basename "$(dirname "$r")")" + if [ -f "$s" ]; then + echo "$(cat "$s")" > "$r" 2>/dev/null || true + rm -f "$s" + fi +done +exit 0 diff --git a/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/sleep.d/post/099-led-restore b/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/sleep.d/post/099-led-restore new file mode 100755 index 00000000000..f7b10e5880a --- /dev/null +++ b/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/sleep.d/post/099-led-restore @@ -0,0 +1,23 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0 +# Copyright (C) 2026 ROCKNIX (https://github.com/ROCKNIX) + +# The wake is complete: end the blue indication and restore the LED state +# saved before sleep. ledcontrol restarts batteryledstatus when the saved +# state is battery. + +. /etc/profile + +POWER_LED="/sys/class/leds/power-led" + +SLEEP_LED_STATE=$(get_setting "sleep.led.color") +if [ ! -n "${SLEEP_LED_STATE}" ]; then + SLEEP_LED_STATE="battery" + set_setting "sleep.led.color" "battery" +fi + +if [ -d "${POWER_LED}" ]; then + echo 0 >"${POWER_LED}/brightness" +fi + +/usr/bin/ledcontrol ${SLEEP_LED_STATE} diff --git a/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/sleep.d/pre/000-blank-release b/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/sleep.d/pre/000-blank-release new file mode 100755 index 00000000000..fdcb2950d37 --- /dev/null +++ b/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/sleep.d/pre/000-blank-release @@ -0,0 +1,16 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0 +# Copyright (C) 2026 ROCKNIX (https://github.com/ROCKNIX) + +# Release the power-handler screen blank before a real suspend begins. +# +# The blank is a compositor state that survives suspend, so resuming with it +# still held restores brightness onto a panel that is switched off: the device +# looks dead and only ssh gets it back. The handler itself refuses to suspend +# while blanked, but a suspend from the ES menu or from systemd does not go +# through it. Runs before the other pre hooks so they see a normal display. + +HANDLER="/usr/lib/autostart/quirks/platforms/SM8550/bin/power-handler" +[ -x "${HANDLER}" ] && "${HANDLER}" release + +exit 0 diff --git a/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/sleep.d/pre/000-led b/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/sleep.d/pre/000-led new file mode 100755 index 00000000000..fa5eb248a66 --- /dev/null +++ b/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/sleep.d/pre/000-led @@ -0,0 +1,41 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0 +# Copyright (C) 2026 ROCKNIX (https://github.com/ROCKNIX) + +# Save the LED state, turn all LEDS off for sleep, then light the power LED +# solid blue for the whole suspend transition, including the grace hold. The +# LED core turns it off as the devices suspend and restores it in early +# resume, so the same blue is also the first sign of wake, before the screen. +# batteryledstatus is stopped first: it repaints the LED with charge colors +# and would override the indication. + +. /etc/profile + +POWER_LED="/sys/class/leds/power-led" + +PRE_SLEEP_LED_STATE=$(get_setting "led.color") +if [ ! -n "${PRE_SLEEP_LED_STATE}" ]; then + PRE_SLEEP_LED_STATE="battery" + set_setting "led.color" "battery" +fi +set_setting sleep.led.color "${PRE_SLEEP_LED_STATE}" + +systemctl stop batteryledstatus 2>/dev/null + +# "poweroff" darkens the LEDs without touching the saved colour; "off" would +# persist led.color=off, and a suspend that dies before the post hooks run +# would then bring the device back with its LED permanently off. +/usr/bin/ledcontrol poweroff + +if [ -d "${POWER_LED}" ]; then + echo none >"${POWER_LED}/trigger" + echo 0 0 255 >"${POWER_LED}/multi_intensity" + echo 255 >"${POWER_LED}/brightness" +fi + +# Blank the panel on the way down: the kernel resume re-powers it at its +# last level, which would flash the screen before the post hooks restore +# the proper brightness. +for BL in /sys/class/backlight/*/brightness; do + [ -f "${BL}" ] && echo 0 >"${BL}" +done diff --git a/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/sleep.d/pre/001-led b/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/sleep.d/pre/001-led deleted file mode 100644 index cbdead0c6a5..00000000000 --- a/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/sleep.d/pre/001-led +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh -# SPDX-License-Identifier: GPL-2.0 -# Copyright (C) 2024 ROCNIX (https://github.com/ROCKNIX) - -# Store current LED state and turn of LEDS as they prevent the device from sleeping while on - -. /etc/profile - -### Get the current LED state and store it -PRE_SLEEP_LED_STATE=$(get_setting "led.color") -if [ ! -n "${PRE_SLEEP_LED_STATE}" ]; then - PRE_SLEEP_LED_STATE="battery" - set_setting "led.color" "battery" -fi - -set_setting sleep.led.color "${PRE_SLEEP_LED_STATE}" - -### Turn off LEDS -/usr/bin/ledcontrol "off" diff --git a/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/sleep.d/pre/001-suspend-grace b/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/sleep.d/pre/001-suspend-grace new file mode 100755 index 00000000000..e8fa834c838 --- /dev/null +++ b/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/sleep.d/pre/001-suspend-grace @@ -0,0 +1,59 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0 +# Copyright (C) 2026 ROCKNIX (https://github.com/ROCKNIX) + +# Never suspend again hard on the heels of a resume. Every resume reloads +# the WCN7850 firmware, and suspending during that bring-up wedges the chip +# beyond anything but a full power-off. The radio is already rfkill-blocked +# by the time platform hooks run, so the interface state cannot be probed +# from here; what proved reliable in testing is a bounded hold: a suspend +# arriving within WINDOW_SECONDS of the last resume waits out the remainder +# behind a blanked backlight, so the button press still looks instant. + +. /etc/profile + +[ "$(get_setting wifi.enabled)" = "1" ] || exit 0 + +WINDOW_SECONDS=15 +MARKER="/run/.last_resume_monotonic" + +[ -r "${MARKER}" ] || exit 0 +LAST="$(cat ${MARKER})" +case "${LAST}" in ''|*[!0-9]*) exit 0 ;; esac + +monotonic() { + awk '/^now at/ {print int($3/1e9); exit}' /proc/timer_list +} + +DEADLINE=$((LAST + WINDOW_SECONDS)) +NOW="$(monotonic)" +[ "${NOW}" -ge "${DEADLINE}" ] && exit 0 + +for BL in /sys/class/backlight/*/brightness; do + [ -f "${BL}" ] && echo 0 >"${BL}" +done + +# The screen goes dark on the button press but audio keeps playing for as +# long as this hold lasts, which is how the delay gets noticed. Flag the +# mute only when this hook actually changed the state, so the post hook +# never unmutes a mute the user set themselves. +# +# Not while an external display is attached: the default sink is then the +# display's, wireplumber persists mute per route, and a sink muted here that +# is gone by resume (undocked while asleep) stays muted on disk forever with +# nothing left to unmute it. A few seconds of audio is the cheaper trade. +# +# External display dependency. The helper is not part of this series; where +# it is missing this reads as "not attached" and the mute happens just as it +# would on a device with no external output. To drop the check, reduce the +# condition to the pactl test alone. +if ! { [ -x /usr/bin/external-display ] && /usr/bin/external-display connected; } && \ + pactl get-sink-mute @DEFAULT_SINK@ 2>/dev/null | grep -q 'no$'; then + pactl set-sink-mute @DEFAULT_SINK@ true >/dev/null 2>&1 && touch /run/.grace-muted +fi + +log $0 "WiFi bring-up still settling, holding suspend for $((DEADLINE - NOW))s." + +while [ "$(monotonic)" -lt "${DEADLINE}" ]; do + sleep 1 +done diff --git a/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/sleep.d/pre/003-usb-role b/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/sleep.d/pre/003-usb-role new file mode 100755 index 00000000000..1a37de09638 --- /dev/null +++ b/projects/ROCKNIX/packages/hardware/quirks/platforms/SM8550/sleep.d/pre/003-usb-role @@ -0,0 +1,24 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0 +# Copyright (C) 2026 ROCKNIX (https://github.com/ROCKNIX) + +# Toggle the USB role across suspend: left untouched, the dwc3/type-c +# stack renegotiates on resume and holds dwc3+ufshcd resume ~10s each. +# TCPM/pmic_glink reverts the written value within ~300ms - the forced +# re-resolution is the operative part, so verify + re-assert once and +# log reverts to kmsg. The post hook restores the saved role. + +for r in /sys/class/usb_role/*-role-switch/role; do + [ -f "$r" ] || continue + cat "$r" > "/run/.usb-role.$(basename "$(dirname "$r")")" + echo none > "$r" 2>/dev/null || true + sleep 0.3 + if [ "$(cat "$r")" != "none" ]; then + echo "sleep: usb role reverted by typec stack; re-asserting" > /dev/kmsg + echo none > "$r" 2>/dev/null || true + sleep 0.2 + [ "$(cat "$r")" != "none" ] && \ + echo "sleep: usb role re-assert lost; suspending with role=$(cat "$r")" > /dev/kmsg + fi +done +exit 0 diff --git a/projects/ROCKNIX/packages/rocknix/sources/scripts/wifi-resume b/projects/ROCKNIX/packages/rocknix/sources/scripts/wifi-resume new file mode 100755 index 00000000000..470e39e8e63 --- /dev/null +++ b/projects/ROCKNIX/packages/rocknix/sources/scripts/wifi-resume @@ -0,0 +1,57 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0 +# Copyright (C) 2026 ROCKNIX (https://github.com/ROCKNIX) + +# Bring the radio back after a resume, once NetworkManager is actually awake. +# +# NetworkManager learns of the wake only after the sleep hooks finish, and it +# reports STATE=asleep until then. Unblocking the radio inside that gap lets +# iwd associate behind NM's back; NM then tears the fresh association down +# ("deauthenticating by local choice") and strands the chip in a state where +# scans fail and association never starts again. Waiting for NM to leave the +# asleep state closes the gap instead of betting on how long it lasts. + +. /etc/profile.d/001-functions + +WAIT_SECONDS=30 + +nm_state() { nmcli -t -f STATE general 2>/dev/null; } + +WAITED=0 +TIMED_OUT=0 +while :; do + STATE="$(nm_state)" + # Anything other than asleep means NM has processed the wake. An empty + # reply means NM is not answering yet, which is also not ready. + [ -n "${STATE}" ] && [ "${STATE}" != "asleep" ] && break + if [ "${WAITED}" -ge "$((WAIT_SECONDS * 5))" ]; then + TIMED_OUT=1 + break + fi + WAITED=$((WAITED + 1)) + sleep 0.2 +done + +if [ "${TIMED_OUT}" = "1" ]; then + # NM asleep at timeout means a new suspend owns the radio: unblocking now + # would hand the freezer a chip mid-bring-up, the wedge this exists to + # prevent. Stay blocked; the next resume runs a fresh instance. + if [ "${STATE}" = "asleep" ]; then + log $0 "NetworkManager still asleep after ${WAIT_SECONDS}s, leaving WIFI blocked." + exit 0 + fi + log $0 "NetworkManager unresponsive after ${WAIT_SECONDS}s, enabling anyway." +else + log $0 "NetworkManager awake after $((WAITED * 200))ms, enabling WIFI." +fi +/usr/bin/wifictl enable + +WLAN="$(ls /sys/class/net 2>/dev/null | grep -m1 ^wlan)" +[ -z "${WLAN}" ] && exit 0 + +# The firmware reloads on unblock and iwd's first scans fail against it, after +# which its retry backoff wastes a minute. Kick one once the chip has settled. +sleep 4 +iwctl station "${WLAN}" scan >/dev/null 2>&1 ||: + +exit 0 diff --git a/projects/ROCKNIX/packages/sysutils/sleep/sources/sleep.sh b/projects/ROCKNIX/packages/sysutils/sleep/sources/sleep.sh index 301637e25e4..d6cd1c2cefd 100755 --- a/projects/ROCKNIX/packages/sysutils/sleep/sources/sleep.sh +++ b/projects/ROCKNIX/packages/sysutils/sleep/sources/sleep.sh @@ -77,10 +77,31 @@ modules() { quirks() { for QUIRK in /usr/lib/autostart/quirks/platforms/"${HW_DEVICE}"/sleep.d/${1}/* \ /usr/lib/autostart/quirks/devices/"${QUIRK_DEVICE}"/sleep.d/${1}/*; do + [ -x "${QUIRK}" ] || continue "${QUIRK}" >${EVENTLOG} 2>&1 done } +# rfkill returns as soon as the block is queued and the driver tears down +# asynchronously, so the freezer otherwise races it. ath12k cares: its resume +# path only does its work when the radio was already down. Skipped where no +# wlan rfkill switch is registered, since the block did nothing to wait for. +wifi_wait_down() { + local dev flags tries + + dev=$(ls /sys/class/net 2>/dev/null | grep -m1 ^wlan) + [ -z "${dev}" ] && return 0 + grep -qx wlan /sys/class/rfkill/*/type 2>/dev/null || return 0 + + for tries in $(seq 1 30); do + flags=$(cat "/sys/class/net/${dev}/flags" 2>/dev/null) || return 0 + [ -z "${flags}" ] && return 0 + (( flags & 1 )) || return 0 + sleep .1 + done + log $0 "WIFI interface ${dev} still up after 3s, suspending anyway." +} + case $1 in pre) if [ "$(get_setting wifi.enabled)" == "1" ]; then @@ -90,7 +111,8 @@ case $1 in wifictl pin >${EVENTLOG} 2>&1 log $0 "Disabling WIFI." - nohup wifictl disable >${EVENTLOG} 2>&1 + wifictl disable >${EVENTLOG} 2>&1 + wifi_wait_down fi headphones stop @@ -110,8 +132,14 @@ case $1 in bluetooth start if [ "$(get_setting wifi.enabled)" == "1" ]; then - log $0 "Enabling WIFI." - nohup wifictl enable >${EVENTLOG} 2>&1 + # NetworkManager only learns the system is awake after these hooks + # finish, so the radio has to stay blocked until it does. Detached + # because that wait must not hold up the rest of the resume. + log $0 "Enabling WIFI once NetworkManager reports it is awake." + # A helper from an earlier resume can still be waiting, and its unit name + # would make this systemd-run fail silently, leaving the radio blocked. + systemctl stop wifi-resume.service >${EVENTLOG} 2>&1 + systemd-run --no-block --collect --unit=wifi-resume /usr/bin/wifi-resume >${EVENTLOG} 2>&1 fi DEVICE_VOLUME=$(get_setting "audio.volume" 2>/dev/null) diff --git a/projects/ROCKNIX/packages/sysutils/system-utils/sources/scripts/input_sense b/projects/ROCKNIX/packages/sysutils/system-utils/sources/scripts/input_sense index 019f1f173fc..9473e77019e 100755 --- a/projects/ROCKNIX/packages/sysutils/system-utils/sources/scripts/input_sense +++ b/projects/ROCKNIX/packages/sysutils/system-utils/sources/scripts/input_sense @@ -24,6 +24,14 @@ case $(get_setting system.loglevel) in ;; esac +### Power and lid keys go to fake suspend unless the platform ships its own +### handler. rocknix-fake-suspend exists for devices that cannot sleep and it +### assumes nothing else will suspend underneath it, so a platform with working +### hardware suspend needs its own policy rather than that state machine. +POWER_HANDLER="/usr/bin/rocknix-fake-suspend" +[ -x "/usr/lib/autostart/quirks/platforms/${HW_DEVICE}/bin/power-handler" ] && \ + POWER_HANDLER="/usr/lib/autostart/quirks/platforms/${HW_DEVICE}/bin/power-handler" + ### Define matching values from evtest, but allow them to be overridden in system.cfg. KEY_VOLUME_UP=$(get_setting key.volume.up) [ -z "${KEY_VOLUME_UP}" ] && KEY_VOLUME_UP='KEY_VOLUME*UP' @@ -386,15 +394,15 @@ set +e ;; (${FUNCTION_POWER_PRESSED_EVENT}) ${DEBUG} && log $0 "${FUNCTION_POWER_PRESSED_EVENT}: Pressed" - /usr/bin/rocknix-fake-suspend power & + ${POWER_HANDLER} power & ;; (${FUNCTION_LID_CLOSE_EVENT}) ${DEBUG} && log $0 "${FUNCTION_LID_CLOSE_EVENT}: Close" - /usr/bin/rocknix-fake-suspend lid close & + ${POWER_HANDLER} lid close & ;; (${FUNCTION_LID_OPEN_EVENT}) ${DEBUG} && log $0 "${FUNCTION_LID_OPEN_EVENT}: Open" - /usr/bin/rocknix-fake-suspend lid open & + ${POWER_HANDLER} lid open & ;; esac