diff --git a/kernel/config/armada-kernel.config.overrides b/kernel/config/armada-kernel.config.overrides index 01f66c5..c7e73cf 100644 --- a/kernel/config/armada-kernel.config.overrides +++ b/kernel/config/armada-kernel.config.overrides @@ -351,3 +351,14 @@ CONFIG_NETFILTER_XT_MATCH_MULTIPORT=m CONFIG_IP_ADVANCED_ROUTER=y CONFIG_IP_MULTIPLE_TABLES=y CONFIG_WIREGUARD=m + +# ============== Suspend/resume debugging ============== +# Without these, a device that will not stay in deep suspend is undiagnosable: +# pm_wakeup_irq names the IRQ that pulled the SoC out, and pm_test exercises the +# device suspend callbacks without powering down. Found the SM8550 wake sources +# with them; keeping them on so the next regression does not need a rebuild. +CONFIG_PM_DEBUG=y +CONFIG_PM_ADVANCED_DEBUG=y +CONFIG_PM_SLEEP_DEBUG=y +# /sys/kernel/debug/irq/irqs/ — "wdepth" shows which IRQs are armed for wake. +CONFIG_GENERIC_IRQ_DEBUGFS=y diff --git a/kernel/dts/qcs8550-ayaneo-pocketevo.dts b/kernel/dts/qcs8550-ayaneo-pocketevo.dts index 6a00e93..f3bc6dc 100644 --- a/kernel/dts/qcs8550-ayaneo-pocketevo.dts +++ b/kernel/dts/qcs8550-ayaneo-pocketevo.dts @@ -107,3 +107,22 @@ }; }; }; + +/* + * Drop the fan tachometer interrupt. + * + * qcs8550-ayaneo-pocket-common.dtsi gives pwm-fan an edge-falling TLMM + * interrupt on GPIO 64 for the tacho. Measured on a Pocket EVO it fires about + * 80 times a second whenever the fan spins, and the SoC leaves deep suspend + * after 2-8s; with the fan stopped the same suspend holds for 13-92s. The AYN + * Odin 3, which suspends for hours, declares no tacho interrupt at all. + * + * Overridden here rather than in the shared DTSI because that file is included + * by six other boards whose fan wiring has not been checked. + * + * Trade-off: hwmon fan1_input stops reporting RPM, matching the Odin 3. + */ +&pwm_fan { + /delete-property/ interrupts; + /delete-property/ interrupt-parent; +}; diff --git a/kernel/patches/0201-scsi-ufs-drain-relink-completions-out-of-band-pm.patch b/kernel/patches/0201-scsi-ufs-drain-relink-completions-out-of-band-pm.patch new file mode 100644 index 0000000..6a02348 --- /dev/null +++ b/kernel/patches/0201-scsi-ufs-drain-relink-completions-out-of-band-pm.patch @@ -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/kernel/patches/0203-thermal-qcom-tsens-skip-sm8550-uplow-wake-irq.patch b/kernel/patches/0203-thermal-qcom-tsens-skip-sm8550-uplow-wake-irq.patch new file mode 100644 index 0000000..3942457 --- /dev/null +++ b/kernel/patches/0203-thermal-qcom-tsens-skip-sm8550-uplow-wake-irq.patch @@ -0,0 +1,57 @@ +From 2b4adfaf66434bddbde753c3ae7229d8c73a0ea5 Mon Sep 17 00:00:00 2001 +From: jaewun +Date: Mon, 11 May 2026 01:20:00 +1200 +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 enable_irq_wake() for the ordinary "uplow" TSENS IRQs 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. +--- + drivers/thermal/qcom/tsens.c | 15 ++++++++++++++- + 1 file changed, 14 insertions(+), 1 deletion(-) + +diff --git a/drivers/thermal/qcom/tsens.c b/drivers/thermal/qcom/tsens.c +index fbdab9d00000..fbdab9d00001 100644 +--- a/drivers/thermal/qcom/tsens.c ++++ b/drivers/thermal/qcom/tsens.c +@@ -1174,6 +1174,15 @@ static const struct thermal_zone_device_ops tsens_of_ops = { + .set_trips = tsens_set_trips, + }; + ++static bool tsens_irq_wake_enabled(const char *irqname) ++{ ++ if (!of_machine_is_compatible("qcom,sm8550")) ++ return true; ++ ++ /* Keep critical thermal wake; only passive threshold-window IRQs are noisy. */ ++ return strcmp(irqname, "uplow") != 0; ++} ++ + static int tsens_register_irq(struct tsens_priv *priv, char *irqname, + irq_handler_t thread_fn) + { +@@ -1203,11 +1212,15 @@ static int tsens_register_irq(struct tsens_priv *priv, char *irqname, + + if (ret) + dev_err(&pdev->dev, "%s: failed to get irq\n", + __func__); +- else ++ else if (tsens_irq_wake_enabled(irqname)) + enable_irq_wake(irq); ++ else ++ dev_info(&pdev->dev, ++ "leaving TSENS %s IRQ %d as non-wakeup on SM8550\n", ++ irqname, irq); + } + + put_device(&pdev->dev); + return ret; + } +-- +2.50.0 diff --git a/kernel/patches/0207-scsi-ufs-qcom-balance-irq-on-host-reset-error.patch b/kernel/patches/0207-scsi-ufs-qcom-balance-irq-on-host-reset-error.patch new file mode 100644 index 0000000..625deb1 --- /dev/null +++ b/kernel/patches/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/kernel/patches/1006-tty-serial-qcom-geni-mask-non-console-irq-on-suspend.patch b/kernel/patches/1006-tty-serial-qcom-geni-mask-non-console-irq-on-suspend.patch new file mode 100644 index 0000000..6738c87 --- /dev/null +++ b/kernel/patches/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/kernel/patches/1007-scsi-ufs-qcom-propagate-hibern8-exit-failure-clk-scale.patch b/kernel/patches/1007-scsi-ufs-qcom-propagate-hibern8-exit-failure-clk-scale.patch new file mode 100644 index 0000000..912fb14 --- /dev/null +++ b/kernel/patches/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/kernel/patches/1008-scsi-ufs-qcom-auto-hibern8-clk-gating-collision.patch b/kernel/patches/1008-scsi-ufs-qcom-auto-hibern8-clk-gating-collision.patch new file mode 100644 index 0000000..a8c47fc --- /dev/null +++ b/kernel/patches/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/kernel/patches/1010-scsi-ufs-qcom-keep-mphy-powered-on-hibern8-park.patch b/kernel/patches/1010-scsi-ufs-qcom-keep-mphy-powered-on-hibern8-park.patch new file mode 100644 index 0000000..17733b2 --- /dev/null +++ b/kernel/patches/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/kernel/patches/1015-ufs-qcom-disable-rx-linecfg-after-link-startup.patch b/kernel/patches/1015-ufs-qcom-disable-rx-linecfg-after-link-startup.patch new file mode 100644 index 0000000..6074339 --- /dev/null +++ b/kernel/patches/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 ++++ b/drivers/ufs/host/ufs-qcom.c +@@ -13,6 +13,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -715,9 +716,16 @@ 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__); +@@ -738,5 +746,11 @@ static int ufs_qcom_link_startup_notify(struct ufs_hba *hba, + err = ufshcd_disable_host_tx_lcc(hba); + + 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); ++ break; + default: + break; diff --git a/kernel/patches/series b/kernel/patches/series index 7d679f9..6125f2c 100644 --- a/kernel/patches/series +++ b/kernel/patches/series @@ -149,3 +149,24 @@ v2_20260420_neil_armstrong_arm64_qcom_sm8650_misc_enhancements.patch 0074-soundwire-qcom-PCM-data-port-format-enable.patch 0612-ROCKNIX-odin3-q6apm-start-mi2s-port-at-prepare.patch 0613-ASoC-q6apm-scope-prepare-start-to-MI2S.patch + +# ============== SM8550 deep suspend (ROCKNIX PR #2954, shuuri-labs/jaewun) ============== +# Wake sources and resume races that stop SM8550 staying in deep suspend, in the +# upstream PR's numeric order. Two of that PR's patches are deliberately absent: +# 0502 (qcom-ipcc IRQF_NO_SUSPEND) is already carried here as 0504, and 1004 +# (rsinput MCU suspend/resume) is for a gamepad driver the Pocket EVO does not +# use, and 1007/1015 do not apply to 7.1.5 (commented out below). Deep suspend +# was validated on 7.1.5 without them. +# +# This set is version-fragile: three of the eight broke when the kernel moved +# from 7.0.11 to 7.1.5, and 0201 in particular tracks ufshcd_intr(), which 7.2 +# changes again by reverting the threaded IRQ handler. Expect to re-derive +# rather than rebase on the next bump. +0201-scsi-ufs-drain-relink-completions-out-of-band-pm.patch +0203-thermal-qcom-tsens-skip-sm8550-uplow-wake-irq.patch +0207-scsi-ufs-qcom-balance-irq-on-host-reset-error.patch +1006-tty-serial-qcom-geni-mask-non-console-irq-on-suspend.patch +# 1007-scsi-ufs-qcom-propagate-hibern8-exit-failure-clk-scale.patch # does not apply to 7.1.5 +1008-scsi-ufs-qcom-auto-hibern8-clk-gating-collision.patch +1010-scsi-ufs-qcom-keep-mphy-powered-on-hibern8-park.patch +# 1015-ufs-qcom-disable-rx-linecfg-after-link-startup.patch # does not apply to 7.1.5