From cf6a8c37cd8466407913f7b05bac0e6edfffb319 Mon Sep 17 00:00:00 2001 From: Shawn Guo Date: Mon, 10 Aug 2026 11:02:45 +0800 Subject: [PATCH 01/12] PENDING: arm64: dts: qcom: nord-rrd: Enable USB_1 support Enable Nord SoC USB_1 controller with the eUSB2 repeater and the onboard Genesys Logic GL3590 4-port USB 3.2 Gen1 hub as a peer-hub pair (usb5e3,610 HS die + usb5e3,625 SS die). Signed-off-by: Shawn Guo --- arch/arm64/boot/dts/qcom/nord-rrd.dts | 57 +++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/arch/arm64/boot/dts/qcom/nord-rrd.dts b/arch/arm64/boot/dts/qcom/nord-rrd.dts index ddc534e2214b4..ff9ef5d821c4f 100644 --- a/arch/arm64/boot/dts/qcom/nord-rrd.dts +++ b/arch/arm64/boot/dts/qcom/nord-rrd.dts @@ -59,6 +59,14 @@ regulator-max-microvolt = <3300000>; regulator-always-on; }; + + vreg_hub_1p2: regulator-hub-1p2 { + compatible = "regulator-fixed"; + regulator-name = "vreg_hub_1p2"; + regulator-min-microvolt = <1200000>; + regulator-max-microvolt = <1200000>; + regulator-always-on; + }; }; &apps_rsc { @@ -519,6 +527,14 @@ vdd1v8-supply = <&vreg_l3l_1p8>; vdd3v3-supply = <&vreg_pre_reg_3p3>; }; + + eusb2_redriver_usb1: redriver@4f { + compatible = "nxp,ptn3222"; + reg = <0x4f>; + #phy-cells = <0>; + vdd1v8-supply = <&vreg_l3l_1p8>; + vdd3v3-supply = <&vreg_pre_reg_3p3>; + }; }; &qupv3_0 { @@ -626,3 +642,44 @@ status = "okay"; }; + +&usb_1 { + dr_mode = "host"; + + #address-cells = <1>; + #size-cells = <0>; + + status = "okay"; + + hub_hs: hub@1 { + compatible = "usb5e3,610"; + reg = <1>; + reset-gpios = <&tlmm 170 GPIO_ACTIVE_LOW>; + vdd-supply = <&vreg_pre_reg_3p3>; + peer-hub = <&hub_ss>; + }; + + hub_ss: hub@2 { + compatible = "usb5e3,625"; + reg = <2>; + vdd-supply = <&vreg_pre_reg_3p3>; + vdd12-supply = <&vreg_hub_1p2>; + peer-hub = <&hub_hs>; + }; +}; + +&usb_1_hsphy { + vdd-supply = <&vreg_l1e_0p9>; + vdda12-supply = <&vreg_l2i_1p2>; + + phys = <&eusb2_redriver_usb1>; + + status = "okay"; +}; + +&usb_1_qmpphy { + vdda-phy-supply = <&vreg_l1h_0p9>; + vdda-pll-supply = <&vreg_l2h_1p2>; + + status = "okay"; +}; From df7781cf587337e2ad6ef810e8b2e0df85402db2 Mon Sep 17 00:00:00 2001 From: Shawn Guo Date: Sat, 15 Aug 2026 21:25:34 +0800 Subject: [PATCH 02/12] FROMLIST: dt-bindings: remoteproc: qcom,nord-pas: Add qcom,cluster-root property Document the qcom,cluster-root phandle property used to describe HPASS's ADSP0/1/2 cluster relationship. Every member of a cluster carries the property, including the instance owning the shared resources, which references itself; instances referencing the same node form one cluster that boots in order and is torn down as a single unit. The relationship is described with a phandle rather than by containment in a parent node, as ti,k3-r5f-rproc and xlnx,zynqmp-r5fss do, because the hardware does not express it by containment either: the three QDSP6SS blocks are register-independent, with no shared register window for a container node to describe. Also add the qcom,nord-adsp1-pas and qcom,nord-adsp2-pas compatible strings used by the non-root cluster members. Assisted-by: LLM Link: https://lore.kernel.org/r/20260815132541.1575121-2-shengchao.guo@oss.qualcomm.com Signed-off-by: Shawn Guo --- .../bindings/remoteproc/qcom,nord-pas.yaml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Documentation/devicetree/bindings/remoteproc/qcom,nord-pas.yaml b/Documentation/devicetree/bindings/remoteproc/qcom,nord-pas.yaml index e90d2953ba692..58d88cb07e525 100644 --- a/Documentation/devicetree/bindings/remoteproc/qcom,nord-pas.yaml +++ b/Documentation/devicetree/bindings/remoteproc/qcom,nord-pas.yaml @@ -17,6 +17,8 @@ properties: compatible: enum: - qcom,nord-adsp-pas + - qcom,nord-adsp1-pas + - qcom,nord-adsp2-pas - qcom,nord-cdsp0-pas - qcom,nord-cdsp1-pas - qcom,nord-cdsp2-pas @@ -89,6 +91,19 @@ properties: maxItems: 1 description: The names of the state bits used for SMP2P output + qcom,cluster-root: + $ref: /schemas/types.yaml#/definitions/phandle + description: + Reference to the Peripheral Authentication Service instance that owns the + resources shared across this DSP's cluster. HPASS shares clock/reset/NoC + resources between its ADSP0/1/2 QDSP6 instances, and the owning instance + (ADSP0) has to boot first to initialize them before ADSP1/ADSP2 can cold + boot. Every member of a cluster carries this property, the owning instance + included, which references itself; instances whose property references the + same node form one cluster. A cluster is always brought down as a single + unit; any member crashing or being stopped forces every other member to + crash or stop with it. + required: - compatible - reg @@ -101,6 +116,8 @@ allOf: compatible: enum: - qcom,nord-adsp-pas + - qcom,nord-adsp1-pas + - qcom,nord-adsp2-pas then: properties: power-domains: From b3870a28e2e54bf134582892c59a39086f7f4f19 Mon Sep 17 00:00:00 2001 From: Shawn Guo Date: Sat, 15 Aug 2026 21:25:35 +0800 Subject: [PATCH 03/12] FROMLIST: remoteproc: Add cluster field to struct rproc Some Qualcomm SoCs group multiple remote processors into a cluster (e.g. Nord's ADSP0/1/2, which share HPASS-domain PLLs, AG_NOC, RSCp and other resources owned by ADSP0). Firmware on these clusters cannot tolerate the peer-shutdown notifications that qcom_sysmon and qcom_glink_ssr send between independently started/stopped remote processors. Add an opaque cluster identifier to struct rproc so that those notification paths, which live in different subsystems (remoteproc and rpmsg), can recognize siblings in the same cluster and suppress notifications between them, without introducing a module dependency between the two subsystems. Assisted-by: LLM Link: https://lore.kernel.org/r/20260815132541.1575121-3-shengchao.guo@oss.qualcomm.com Signed-off-by: Shawn Guo --- include/linux/remoteproc.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/include/linux/remoteproc.h b/include/linux/remoteproc.h index 7c1546d480082..67169ade60ce4 100644 --- a/include/linux/remoteproc.h +++ b/include/linux/remoteproc.h @@ -278,6 +278,8 @@ enum rproc_features { * @cdev: character device of the rproc * @cdev_put_on_release: flag to indicate if remoteproc should be shutdown on @char_dev release * @features: indicate remoteproc features + * @cluster: opaque identifier shared by remoteprocs whose SSR notifications + * should be coalesced (set by the owning rproc driver), or NULL */ struct rproc { struct list_head node; @@ -319,6 +321,7 @@ struct rproc { struct cdev cdev; bool cdev_put_on_release; DECLARE_BITMAP(features, RPROC_MAX_FEATURES); + void *cluster; }; /** From 6a6f48c3e10e16c9dd325f86e313147e3ef7259f Mon Sep 17 00:00:00 2001 From: Shawn Guo Date: Sat, 15 Aug 2026 21:25:36 +0800 Subject: [PATCH 04/12] FROMLIST: remoteproc: qcom: sysmon: Suppress notify between cluster siblings Firmware on clustered remote processors (e.g. Nord's HPASS ADSP0/1/2) cannot handle the SSCTL peer-shutdown notification sysmon sends whenever a sibling in the same cluster starts or stops, and the QMI request instead times out. Skip sysmon_notify() when the notifying and the notified instance share the cluster identifier their owning rproc driver put in rproc->cluster. The suppression is done on the receiving side, so that a cluster member keeps notifying, and being notified by, remote processors outside its cluster. Instances whose rproc has no cluster set are completely unaffected. Assisted-by: LLM Link: https://lore.kernel.org/r/20260815132541.1575121-4-shengchao.guo@oss.qualcomm.com Signed-off-by: Shawn Guo --- drivers/remoteproc/qcom_sysmon.c | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/drivers/remoteproc/qcom_sysmon.c b/drivers/remoteproc/qcom_sysmon.c index a0830a48b1f40..4af05f940a491 100644 --- a/drivers/remoteproc/qcom_sysmon.c +++ b/drivers/remoteproc/qcom_sysmon.c @@ -67,6 +67,7 @@ static const char * const sysmon_state_string[] = { struct sysmon_event { const char *subsys_name; u32 ssr_event; + void *cluster; }; static DEFINE_MUTEX(sysmon_lock); @@ -473,7 +474,8 @@ static int sysmon_prepare(struct rproc_subdev *subdev) subdev); struct sysmon_event event = { .subsys_name = sysmon->name, - .ssr_event = SSCTL_SSR_EVENT_BEFORE_POWERUP + .ssr_event = SSCTL_SSR_EVENT_BEFORE_POWERUP, + .cluster = sysmon->rproc->cluster, }; mutex_lock(&sysmon->state_lock); @@ -500,7 +502,8 @@ static int sysmon_start(struct rproc_subdev *subdev) struct qcom_sysmon *target; struct sysmon_event event = { .subsys_name = sysmon->name, - .ssr_event = SSCTL_SSR_EVENT_AFTER_POWERUP + .ssr_event = SSCTL_SSR_EVENT_AFTER_POWERUP, + .cluster = sysmon->rproc->cluster, }; reinit_completion(&sysmon->ssctl_comp); @@ -536,7 +539,8 @@ static void sysmon_stop(struct rproc_subdev *subdev, bool crashed) struct qcom_sysmon *sysmon = container_of(subdev, struct qcom_sysmon, subdev); struct sysmon_event event = { .subsys_name = sysmon->name, - .ssr_event = SSCTL_SSR_EVENT_BEFORE_SHUTDOWN + .ssr_event = SSCTL_SSR_EVENT_BEFORE_SHUTDOWN, + .cluster = sysmon->rproc->cluster, }; sysmon->shutdown_acked = false; @@ -567,7 +571,8 @@ static void sysmon_unprepare(struct rproc_subdev *subdev) subdev); struct sysmon_event event = { .subsys_name = sysmon->name, - .ssr_event = SSCTL_SSR_EVENT_AFTER_SHUTDOWN + .ssr_event = SSCTL_SSR_EVENT_AFTER_SHUTDOWN, + .cluster = sysmon->rproc->cluster, }; mutex_lock(&sysmon->state_lock); @@ -588,6 +593,11 @@ static int sysmon_notify(struct notifier_block *nb, unsigned long event, struct qcom_sysmon *sysmon = container_of(nb, struct qcom_sysmon, nb); struct sysmon_event *sysmon_event = data; + /* Cluster siblings' firmware can't handle peer SSR notify; skip it */ + if (sysmon->rproc->cluster && + sysmon->rproc->cluster == sysmon_event->cluster) + return NOTIFY_DONE; + /* Skip non-running rprocs and the originating instance */ if (sysmon->state != SSCTL_SSR_EVENT_AFTER_POWERUP || !strcmp(sysmon_event->subsys_name, sysmon->name)) { From 34753aa896730abc8f3fddee02a87b6cf2917c31 Mon Sep 17 00:00:00 2001 From: Shawn Guo Date: Sat, 15 Aug 2026 21:25:37 +0800 Subject: [PATCH 05/12] FROMLIST: rpmsg: qcom_glink_ssr: Suppress cleanup notify between cluster siblings Like qcom_sysmon's SSCTL notify, clustered remote processors (e.g. Nord's HPASS ADSP0/1/2) cannot handle the glink_ssr DO_CLEANUP broadcast sent to siblings whenever one of them starts or stops, leaving qcom_glink_ssr_notifier_call() to time out waiting for CLEANUP_DONE. Pass the stopping remoteproc's cluster identifier through qcom_glink_ssr_notify(), and plumb each edge's own cluster identifier down from the remoteproc that registers it, so that qcom_glink_ssr_notifier_call() can skip the DO_CLEANUP send when the sender and the receiving edge share a cluster. As in qcom_sysmon, the suppression is done on the receiving side, so that a cluster member keeps sending DO_CLEANUP to, and receiving it from, remote processors outside its cluster. Edges with no cluster set are unaffected. The receiving edge's identifier is carried in struct qcom_glink rather than looked up from the rproc that owns it, so that glink_ssr does not have to reach into struct remoteproc: RPMSG_QCOM_GLINK does not depend on REMOTEPROC, and the identifier is deliberately opaque to keep the two subsystems independent of each other. Assisted-by: LLM Link: https://lore.kernel.org/r/20260815132541.1575121-5-shengchao.guo@oss.qualcomm.com Signed-off-by: Shawn Guo --- drivers/remoteproc/qcom_common.c | 7 +++--- drivers/remoteproc/qcom_common.h | 2 +- drivers/rpmsg/qcom_glink_native.c | 20 ++++++++++++++++- drivers/rpmsg/qcom_glink_native.h | 5 ++++- drivers/rpmsg/qcom_glink_rpm.c | 2 +- drivers/rpmsg/qcom_glink_smem.c | 5 +++-- drivers/rpmsg/qcom_glink_ssr.c | 36 ++++++++++++++++++++++++++----- include/linux/rpmsg/qcom_glink.h | 10 +++++---- 8 files changed, 69 insertions(+), 18 deletions(-) diff --git a/drivers/remoteproc/qcom_common.c b/drivers/remoteproc/qcom_common.c index 1f406bfcb1f59..ec151cfd853a9 100644 --- a/drivers/remoteproc/qcom_common.c +++ b/drivers/remoteproc/qcom_common.c @@ -217,7 +217,8 @@ static int glink_subdev_start(struct rproc_subdev *subdev) { struct qcom_rproc_glink *glink = to_glink_subdev(subdev); - glink->edge = qcom_glink_smem_register(glink->dev, glink->node); + glink->edge = qcom_glink_smem_register(&glink->rproc->dev, glink->node, + glink->rproc->cluster); return PTR_ERR_OR_ZERO(glink->edge); } @@ -237,7 +238,7 @@ static void glink_subdev_unprepare(struct rproc_subdev *subdev) { struct qcom_rproc_glink *glink = to_glink_subdev(subdev); - qcom_glink_ssr_notify(glink->ssr_name); + qcom_glink_ssr_notify(glink->ssr_name, glink->rproc->cluster); } /** @@ -259,7 +260,7 @@ void qcom_add_glink_subdev(struct rproc *rproc, struct qcom_rproc_glink *glink, if (!glink->ssr_name) return; - glink->dev = dev; + glink->rproc = rproc; glink->subdev.start = glink_subdev_start; glink->subdev.stop = glink_subdev_stop; glink->subdev.unprepare = glink_subdev_unprepare; diff --git a/drivers/remoteproc/qcom_common.h b/drivers/remoteproc/qcom_common.h index b0e7e336d363e..7136494cbfbaa 100644 --- a/drivers/remoteproc/qcom_common.h +++ b/drivers/remoteproc/qcom_common.h @@ -14,7 +14,7 @@ struct qcom_rproc_glink { const char *ssr_name; - struct device *dev; + struct rproc *rproc; struct device_node *node; struct qcom_glink_smem *edge; }; diff --git a/drivers/rpmsg/qcom_glink_native.c b/drivers/rpmsg/qcom_glink_native.c index d9d4468e4cbdf..a15d7e6a52423 100644 --- a/drivers/rpmsg/qcom_glink_native.c +++ b/drivers/rpmsg/qcom_glink_native.c @@ -122,6 +122,8 @@ struct qcom_glink { unsigned long features; bool intentless; + /* Cluster identifier of the remote processor behind this edge, or NULL */ + void *cluster; wait_queue_head_t tx_avail_notify; bool sent_read_notify; @@ -1892,7 +1894,8 @@ struct qcom_glink *qcom_glink_native_probe(struct device *dev, unsigned long features, struct qcom_glink_pipe *rx, struct qcom_glink_pipe *tx, - bool intentless) + bool intentless, + void *cluster) { int ret; struct qcom_glink *glink; @@ -1907,6 +1910,7 @@ struct qcom_glink *qcom_glink_native_probe(struct device *dev, glink->features = features; glink->intentless = intentless; + glink->cluster = cluster; spin_lock_init(&glink->tx_lock); spin_lock_init(&glink->rx_lock); @@ -1940,6 +1944,20 @@ struct qcom_glink *qcom_glink_native_probe(struct device *dev, } EXPORT_SYMBOL_GPL(qcom_glink_native_probe); +/** + * qcom_glink_ept_cluster() - cluster identifier of an endpoint's edge + * @ept: endpoint to query + * + * Return: the opaque cluster identifier of the remote processor @ept talks to, + * or NULL if it is not part of a cluster. + */ +void *qcom_glink_ept_cluster(struct rpmsg_endpoint *ept) +{ + struct glink_channel *channel = to_glink_channel(ept); + + return channel->glink->cluster; +} + static int qcom_glink_remove_device(struct device *dev, void *data) { device_unregister(dev); diff --git a/drivers/rpmsg/qcom_glink_native.h b/drivers/rpmsg/qcom_glink_native.h index 8dbec24de23e0..8544f4f5c5450 100644 --- a/drivers/rpmsg/qcom_glink_native.h +++ b/drivers/rpmsg/qcom_glink_native.h @@ -29,12 +29,15 @@ struct qcom_glink_pipe { struct device; struct qcom_glink; +struct rpmsg_endpoint; struct qcom_glink *qcom_glink_native_probe(struct device *dev, unsigned long features, struct qcom_glink_pipe *rx, struct qcom_glink_pipe *tx, - bool intentless); + bool intentless, + void *cluster); +void *qcom_glink_ept_cluster(struct rpmsg_endpoint *ept); void qcom_glink_native_remove(struct qcom_glink *glink); void qcom_glink_native_rx(struct qcom_glink *glink); diff --git a/drivers/rpmsg/qcom_glink_rpm.c b/drivers/rpmsg/qcom_glink_rpm.c index e3ba2c63a5fce..9c1ff05d4a10c 100644 --- a/drivers/rpmsg/qcom_glink_rpm.c +++ b/drivers/rpmsg/qcom_glink_rpm.c @@ -346,7 +346,7 @@ static int glink_rpm_probe(struct platform_device *pdev) 0, &rpm->rx_pipe.native, &rpm->tx_pipe.native, - true); + true, NULL); if (IS_ERR(glink)) { mbox_free_channel(rpm->mbox_chan); return PTR_ERR(glink); diff --git a/drivers/rpmsg/qcom_glink_smem.c b/drivers/rpmsg/qcom_glink_smem.c index 62adc4db23170..f7874bbcf7fcf 100644 --- a/drivers/rpmsg/qcom_glink_smem.c +++ b/drivers/rpmsg/qcom_glink_smem.c @@ -218,7 +218,8 @@ static void qcom_glink_smem_release(struct device *dev) } struct qcom_glink_smem *qcom_glink_smem_register(struct device *parent, - struct device_node *node) + struct device_node *node, + void *cluster) { struct glink_smem_pipe *rx_pipe; struct glink_smem_pipe *tx_pipe; @@ -338,7 +339,7 @@ struct qcom_glink_smem *qcom_glink_smem_register(struct device *parent, glink = qcom_glink_native_probe(dev, GLINK_FEATURE_INTENT_REUSE, &rx_pipe->native, &tx_pipe->native, - false); + false, cluster); if (IS_ERR(glink)) { ret = PTR_ERR(glink); goto err_free_mbox; diff --git a/drivers/rpmsg/qcom_glink_ssr.c b/drivers/rpmsg/qcom_glink_ssr.c index e71d3716c55c5..74c671a19af03 100644 --- a/drivers/rpmsg/qcom_glink_ssr.c +++ b/drivers/rpmsg/qcom_glink_ssr.c @@ -11,6 +11,8 @@ #include #include +#include "qcom_glink_native.h" + /** * struct do_cleanup_msg - The data structure for an SSR do_cleanup message * @version: The G-Link SSR protocol version @@ -55,16 +57,34 @@ struct glink_ssr { struct completion completion; }; +/** + * struct glink_ssr_notify_data - payload of an SSR notification + * @ssr_name: name of the remoteproc that has been stopped + * @cluster: cluster identifier of the remoteproc that has been stopped, + * or NULL if it is not part of a cluster + */ +struct glink_ssr_notify_data { + const char *ssr_name; + void *cluster; +}; + /* Notifier list for all registered glink_ssr instances */ static BLOCKING_NOTIFIER_HEAD(ssr_notifiers); /** * qcom_glink_ssr_notify() - notify GLINK SSR about stopped remoteproc * @ssr_name: name of the remoteproc that has been stopped + * @cluster: cluster identifier of the remoteproc that has been stopped, + * or NULL if it is not part of a cluster */ -void qcom_glink_ssr_notify(const char *ssr_name) +void qcom_glink_ssr_notify(const char *ssr_name, void *cluster) { - blocking_notifier_call_chain(&ssr_notifiers, 0, (void *)ssr_name); + struct glink_ssr_notify_data data = { + .ssr_name = ssr_name, + .cluster = cluster, + }; + + blocking_notifier_call_chain(&ssr_notifiers, 0, &data); } EXPORT_SYMBOL_GPL(qcom_glink_ssr_notify); @@ -100,18 +120,24 @@ static int qcom_glink_ssr_notifier_call(struct notifier_block *nb, void *data) { struct glink_ssr *ssr = container_of(nb, struct glink_ssr, nb); + struct glink_ssr_notify_data *notify_data = data; struct do_cleanup_msg msg; - char *ssr_name = data; + void *cluster; int ret; + /* Cluster siblings' firmware can't handle peer SSR notify; skip it */ + cluster = qcom_glink_ept_cluster(ssr->ept); + if (cluster && cluster == notify_data->cluster) + return NOTIFY_DONE; + ssr->seq_num++; reinit_completion(&ssr->completion); memset(&msg, 0, sizeof(msg)); msg.command = cpu_to_le32(GLINK_SSR_DO_CLEANUP); msg.seq_num = cpu_to_le32(ssr->seq_num); - msg.name_len = cpu_to_le32(strlen(ssr_name)); - strscpy(msg.name, ssr_name, sizeof(msg.name)); + msg.name_len = cpu_to_le32(strlen(notify_data->ssr_name)); + strscpy(msg.name, notify_data->ssr_name, sizeof(msg.name)); ret = rpmsg_send(ssr->ept, &msg, sizeof(msg)); if (ret < 0) diff --git a/include/linux/rpmsg/qcom_glink.h b/include/linux/rpmsg/qcom_glink.h index bfbd48f435fa5..6591d016e8cbb 100644 --- a/include/linux/rpmsg/qcom_glink.h +++ b/include/linux/rpmsg/qcom_glink.h @@ -8,22 +8,24 @@ struct qcom_glink_smem; #if IS_ENABLED(CONFIG_RPMSG_QCOM_GLINK) -void qcom_glink_ssr_notify(const char *ssr_name); +void qcom_glink_ssr_notify(const char *ssr_name, void *cluster); #else -static inline void qcom_glink_ssr_notify(const char *ssr_name) {} +static inline void qcom_glink_ssr_notify(const char *ssr_name, void *cluster) {} #endif #if IS_ENABLED(CONFIG_RPMSG_QCOM_GLINK_SMEM) struct qcom_glink_smem *qcom_glink_smem_register(struct device *parent, - struct device_node *node); + struct device_node *node, + void *cluster); void qcom_glink_smem_unregister(struct qcom_glink_smem *glink); #else static inline struct qcom_glink_smem * qcom_glink_smem_register(struct device *parent, - struct device_node *node) + struct device_node *node, + void *cluster) { return NULL; } From 8263c9600ff6eda9b098b4b06aae67d2f267a57a Mon Sep 17 00:00:00 2001 From: Shawn Guo Date: Sat, 15 Aug 2026 21:25:38 +0800 Subject: [PATCH 06/12] FROMLIST: remoteproc: qcom: pas: Track HPASS ADSP cluster membership On Nord, ADSP0/1/2 share HPASS-domain resources (PLLs, AG_NOC, RSCp, CESTA, THROTTLE, QTMR) owned by ADSP0, and their firmware cannot tolerate one member being started or stopped independently of the others. Modeling that requires each PAS instance to know which other instances it is grouped with. Add a small registry of clusters, keyed by the device_node every member's "qcom,cluster-root" phandle points at, and join it at probe. Membership is answerable from the node being probed alone: every member carries the property, and the one referencing itself is the root, so there is no need to search the device tree for nodes referencing a given instance. A dependent member is rejected at probe if its root is disabled in DT, since its boot could never be sequenced after a root that will not bind. Publish the cluster in rproc->cluster, which is all sysmon and glink_ssr need to stop notifying siblings of each other's SSR events. The cluster is left only after rproc_del() has stopped this member, so that the suppression is still in effect for that stop, and is unwound on every probe error path, since a member freed while still linked into the cluster would be walked by its siblings. Instances without "qcom,cluster-root" are completely unaffected: qcom_pas.cluster stays NULL for them. Assisted-by: LLM Link: https://lore.kernel.org/r/20260815132541.1575121-6-shengchao.guo@oss.qualcomm.com Signed-off-by: Shawn Guo --- drivers/remoteproc/qcom_q6v5_pas.c | 152 +++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/drivers/remoteproc/qcom_q6v5_pas.c b/drivers/remoteproc/qcom_q6v5_pas.c index 37084a038c519..cc851502cf93e 100644 --- a/drivers/remoteproc/qcom_q6v5_pas.c +++ b/drivers/remoteproc/qcom_q6v5_pas.c @@ -66,6 +66,8 @@ struct qcom_pas_data { bool needs_tzmem; }; +struct qcom_pas_cluster; + struct qcom_pas { struct device *dev; struct rproc *rproc; @@ -124,8 +126,148 @@ struct qcom_pas { struct qcom_scm_pas_context *dtb_pas_ctx; struct qmi_tmd_client *tmd_inst; + + struct qcom_pas_cluster *cluster; + struct list_head cluster_node; + bool is_cluster_root; +}; + +/** + * struct qcom_pas_cluster - state shared by clustered PAS instances + * @node: device_node of the cluster root, the key into the global list + * @list: linkage in qcom_pas_cluster_list + * @lock: protects @members and @root + * @members: list of struct qcom_pas, linked via cluster_node + * @root: the member whose "qcom,cluster-root" points at itself + * @refcount: number of members currently attached to this cluster + * + * PAS instances whose "qcom,cluster-root" phandle points at the same node + * share one of these. + */ +struct qcom_pas_cluster { + struct device_node *node; + struct list_head list; + + struct mutex lock; + struct list_head members; + struct qcom_pas *root; + + int refcount; }; +static LIST_HEAD(qcom_pas_cluster_list); +static DEFINE_MUTEX(qcom_pas_cluster_list_lock); + +static struct qcom_pas_cluster *qcom_pas_cluster_get(struct device_node *node) +{ + struct qcom_pas_cluster *cluster; + + mutex_lock(&qcom_pas_cluster_list_lock); + + list_for_each_entry(cluster, &qcom_pas_cluster_list, list) { + if (cluster->node == node) { + cluster->refcount++; + goto out; + } + } + + cluster = kzalloc(sizeof(*cluster), GFP_KERNEL); + if (!cluster) + goto out; + + cluster->node = of_node_get(node); + mutex_init(&cluster->lock); + INIT_LIST_HEAD(&cluster->members); + cluster->refcount = 1; + list_add_tail(&cluster->list, &qcom_pas_cluster_list); + +out: + mutex_unlock(&qcom_pas_cluster_list_lock); + return cluster; +} + +static void qcom_pas_cluster_put(struct qcom_pas_cluster *cluster) +{ + mutex_lock(&qcom_pas_cluster_list_lock); + if (--cluster->refcount == 0) { + list_del(&cluster->list); + mutex_unlock(&qcom_pas_cluster_list_lock); + of_node_put(cluster->node); + kfree(cluster); + return; + } + mutex_unlock(&qcom_pas_cluster_list_lock); +} + +/** + * qcom_pas_cluster_init() - join the cluster referenced by @np, if any + * @pas: PAS instance being probed + * @np: of_node of @pas's platform device + * + * Devices without a "qcom,cluster-root" property are not part of a cluster; + * @pas->cluster is left NULL and this is a no-op (e.g. cdsp0-3). Cluster + * members all carry the property, the root included, whose phandle points + * back at itself. + * + * Return: 0 on success, negative errno on failure. + */ +static int qcom_pas_cluster_init(struct qcom_pas *pas, struct device_node *np) +{ + struct device_node *root_node; + bool is_root; + + root_node = of_parse_phandle(np, "qcom,cluster-root", 0); + if (!root_node) + return 0; + + is_root = root_node == np; + + /* + * A non-root member is useless without its root: it can never be + * booted, since its boot has to be sequenced after the root's. Reject + * it here rather than at first boot, so that a DT enabling a dependent + * DSP but not the one owning the shared resources fails loudly and + * early. + */ + if (!is_root && !of_device_is_available(root_node)) { + dev_err(pas->dev, "cluster root %pOF is not enabled\n", root_node); + of_node_put(root_node); + return -ENODEV; + } + + pas->cluster = qcom_pas_cluster_get(root_node); + of_node_put(root_node); + if (!pas->cluster) + return -ENOMEM; + + pas->is_cluster_root = is_root; + + mutex_lock(&pas->cluster->lock); + list_add_tail(&pas->cluster_node, &pas->cluster->members); + if (is_root) + pas->cluster->root = pas; + mutex_unlock(&pas->cluster->lock); + + return 0; +} + +static void qcom_pas_cluster_exit(struct qcom_pas *pas) +{ + struct qcom_pas_cluster *cluster = pas->cluster; + + if (!cluster) + return; + + mutex_lock(&cluster->lock); + list_del(&pas->cluster_node); + if (cluster->root == pas) + cluster->root = NULL; + mutex_unlock(&cluster->lock); + + qcom_pas_cluster_put(cluster); + pas->cluster = NULL; +} + static void qcom_pas_segment_dump(struct rproc *rproc, struct rproc_dump_segment *segment, void *dest, size_t offset, size_t size) @@ -939,6 +1081,12 @@ static int qcom_pas_probe(struct platform_device *pdev) pas = rproc->priv; pas->dev = &pdev->dev; pas->rproc = rproc; + + ret = qcom_pas_cluster_init(pas, pdev->dev.of_node); + if (ret) + return ret; + rproc->cluster = pas->cluster; + pas->minidump_id = desc->minidump_id; pas->pas_id = desc->pas_id; pas->lite_pas_id = desc->lite_pas_id; @@ -1046,6 +1194,7 @@ static int qcom_pas_probe(struct platform_device *pdev) qcom_pas_unassign_memory_region(pas); free_rproc: device_init_wakeup(pas->dev, false); + qcom_pas_cluster_exit(pas); return ret; } @@ -1059,6 +1208,9 @@ static void qcom_pas_remove(struct platform_device *pdev) rproc_del(pas->rproc); + pas->rproc->cluster = NULL; + qcom_pas_cluster_exit(pas); + qcom_q6v5_deinit(&pas->q6v5); qcom_pas_unassign_memory_region(pas); qcom_remove_glink_subdev(pas->rproc, &pas->glink_subdev); From b5db813970fdc300e1b3033cc756535cb3fc3bff Mon Sep 17 00:00:00 2001 From: Shawn Guo Date: Sat, 15 Aug 2026 21:25:39 +0800 Subject: [PATCH 07/12] FROMLIST: remoteproc: qcom: pas: Sequence HPASS ADSP cluster boot root-first The cluster root owns the HPASS resources shared with its siblings and initializes them as part of its own boot, so ADSP1/ADSP2 can only cold boot once ADSP0 is up. Booting a dependent member first leaves it without the clocks, resets and NoC configuration it needs. Gate a dependent member's qcom_pas_start() on the root having started. The root being enabled in DT does not imply it has already bound, as probe order between the two is not guaranteed, so fail the boot if the root has not bound rather than dereferencing it. If the root has bound but is not running yet, wait briefly on a completion the root signals from its own qcom_pas_start(), so that a member racing the root through a restart does not fail outright. The completion starts out signaled, since a member may be attached to already-running firmware at probe, and is only cleared once the root actually goes down. A dependent member's boot racing the root's stop can therefore still observe a stale completion, in which case it falls back to failing on the root's state. Assisted-by: LLM Link: https://lore.kernel.org/r/20260815132541.1575121-7-shengchao.guo@oss.qualcomm.com Signed-off-by: Shawn Guo --- drivers/remoteproc/qcom_q6v5_pas.c | 76 ++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/drivers/remoteproc/qcom_q6v5_pas.c b/drivers/remoteproc/qcom_q6v5_pas.c index cc851502cf93e..85ffd09c7ea1f 100644 --- a/drivers/remoteproc/qcom_q6v5_pas.c +++ b/drivers/remoteproc/qcom_q6v5_pas.c @@ -139,6 +139,7 @@ struct qcom_pas { * @lock: protects @members and @root * @members: list of struct qcom_pas, linked via cluster_node * @root: the member whose "qcom,cluster-root" points at itself + * @root_booted: signaled once @root has started, cleared when it goes down * @refcount: number of members currently attached to this cluster * * PAS instances whose "qcom,cluster-root" phandle points at the same node @@ -151,10 +152,18 @@ struct qcom_pas_cluster { struct mutex lock; struct list_head members; struct qcom_pas *root; + struct completion root_booted; int refcount; }; +/* + * How long a dependent member waits for its root to finish booting before + * giving up, e.g. when it is racing the root through the crash-restart + * cascade. + */ +#define QCOM_PAS_CLUSTER_ROOT_BOOT_TIMEOUT (1 * HZ) + static LIST_HEAD(qcom_pas_cluster_list); static DEFINE_MUTEX(qcom_pas_cluster_list_lock); @@ -178,6 +187,13 @@ static struct qcom_pas_cluster *qcom_pas_cluster_get(struct device_node *node) cluster->node = of_node_get(node); mutex_init(&cluster->lock); INIT_LIST_HEAD(&cluster->members); + init_completion(&cluster->root_booted); + /* + * Cluster members may be attached to already-running firmware at + * probe, so the root is presumed up until its own qcom_pas_stop() + * reinit_completion()s this the first time it actually goes down. + */ + complete_all(&cluster->root_booted); cluster->refcount = 1; list_add_tail(&cluster->list, &qcom_pas_cluster_list); @@ -268,6 +284,51 @@ static void qcom_pas_cluster_exit(struct qcom_pas *pas) pas->cluster = NULL; } +/** + * qcom_pas_cluster_wait_for_root() - gate a dependent member's boot on its root + * @pas: the non-root cluster member being started + * + * The cluster root owns the resources its siblings need, and initializes them + * as part of its own boot, so a dependent member can only be started once the + * root is up. + * + * Return: 0 if the root is up, negative errno otherwise. + */ +static int qcom_pas_cluster_wait_for_root(struct qcom_pas *pas) +{ + struct qcom_pas_cluster *cluster = pas->cluster; + struct rproc *root; + + /* + * The root's PAS instance is enabled in DT (checked in + * qcom_pas_cluster_init()) but has not necessarily bound yet, and may + * have unbound again. Without it there is nothing to sequence this + * member's boot against. + * + * Note that nothing refcounts members across unbind, so a root freed + * under a concurrent sibling boot remains unhandled. + */ + mutex_lock(&cluster->lock); + root = cluster->root ? cluster->root->rproc : NULL; + mutex_unlock(&cluster->lock); + + if (!root) { + dev_err(pas->dev, "cluster root not bound\n"); + return -ENODEV; + } + + if (root->state == RPROC_RUNNING || root->state == RPROC_ATTACHED) + return 0; + + if (!wait_for_completion_timeout(&cluster->root_booted, + QCOM_PAS_CLUSTER_ROOT_BOOT_TIMEOUT)) { + dev_err(pas->dev, "cluster root not started\n"); + return -ENODEV; + } + + return 0; +} + static void qcom_pas_segment_dump(struct rproc *rproc, struct rproc_dump_segment *segment, void *dest, size_t offset, size_t size) @@ -446,6 +507,12 @@ static int qcom_pas_start(struct rproc *rproc) struct qcom_pas *pas = rproc->priv; int ret; + if (pas->cluster && !pas->is_cluster_root) { + ret = qcom_pas_cluster_wait_for_root(pas); + if (ret) + return ret; + } + ret = qcom_q6v5_prepare(&pas->q6v5); if (ret) return ret; @@ -516,6 +583,9 @@ static int qcom_pas_start(struct rproc *rproc) if (pas->dtb_pas_id) qcom_scm_pas_metadata_release(pas->dtb_pas_ctx); + if (pas->cluster && pas->is_cluster_root) + complete_all(&pas->cluster->root_booted); + /* firmware is used to pass reference from qcom_pas_start(), drop it now */ pas->firmware = NULL; @@ -571,6 +641,12 @@ static int qcom_pas_stop(struct rproc *rproc) int handover; int ret; + if (pas->cluster && pas->is_cluster_root) { + mutex_lock(&pas->cluster->lock); + reinit_completion(&pas->cluster->root_booted); + mutex_unlock(&pas->cluster->lock); + } + ret = qcom_q6v5_request_stop(&pas->q6v5, pas->sysmon); if (ret == -ETIMEDOUT) dev_err(pas->dev, "timed out on wait\n"); From df79775bc0b7642b98c40ad29bb7d37f4b69acf3 Mon Sep 17 00:00:00 2001 From: Shawn Guo Date: Sat, 15 Aug 2026 21:25:40 +0800 Subject: [PATCH 08/12] FROMLIST: remoteproc: qcom: pas: Enforce coupled stop/crash for HPASS ADSP clusters The firmware of a Nord HPASS ADSP cluster cannot tolerate one member being stopped or restarted independently of the others: doing so leaves sysmon/glink_ssr notify requests to siblings timing out, and the whole cluster unable to recover without manually stopping all three and restarting them in dependency order. Coordinate an explicit stop or crash of any member across the whole cluster in two phases, since hardware testing showed that whichever member's PAS/TZ hardware power-off happens chronologically last always fails its own graceful shutdown ack (both the sysmon SSCTL QMI ack and the SMEM-bit/IRQ ack), regardless of whether that member is the cluster root. Phase 1 fans the stop out to every participant concurrently via a per-member work item and waits, at a barrier, for every participant to finish requesting and awaiting its own graceful ack while the whole cluster is still fully powered. Only once every participant has cleared the barrier does phase 2 let each of them proceed to its actual PAS/TZ hardware power-off, so no member is ever asking firmware to ack a shutdown after a sibling's hardware is already gone. The fan-out has to be deferred to a work item rather than calling rproc_shutdown() inline, because qcom_pas_stop() runs with the remoteproc core holding the stopping member's own rproc->lock, and reaching for a sibling's rproc->lock from inside that critical section would risk an ABBA deadlock against a concurrent operation on that sibling. Membership in a round is decided once, by qcom_pas_cluster_member_stops(), and recorded per member, so that the participant count and the two counters it seeds can never diverge. The crashed member that triggered the round is a participant even though its rproc->state is still RPROC_CRASHED -- the core only sets RPROC_OFFLINE after ops->stop() returns -- while a sibling crashing concurrently is deliberately excluded, being already torn down by its own recovery. On a crash, cascade the stop the same way but also auto-restart the whole cluster root-first once every participant has fully powered off, reusing the root_booted completion to keep non-root members' qcom_pas_start() from racing ahead of the root's boot. An explicit stop leaves the ordered restart to users or higher-level policy. Devices without "qcom,cluster-root" are completely unaffected, since qcom_pas.cluster stays NULL for them. Assisted-by: LLM Link: https://lore.kernel.org/r/20260815132541.1575121-8-shengchao.guo@oss.qualcomm.com Signed-off-by: Shawn Guo --- drivers/remoteproc/qcom_q6v5_pas.c | 252 +++++++++++++++++++++++++++++ 1 file changed, 252 insertions(+) diff --git a/drivers/remoteproc/qcom_q6v5_pas.c b/drivers/remoteproc/qcom_q6v5_pas.c index 85ffd09c7ea1f..b4af74bee221d 100644 --- a/drivers/remoteproc/qcom_q6v5_pas.c +++ b/drivers/remoteproc/qcom_q6v5_pas.c @@ -28,6 +28,7 @@ #include #include #include +#include #include "qcom_common.h" #include "qcom_pil_info.h" @@ -129,7 +130,9 @@ struct qcom_pas { struct qcom_pas_cluster *cluster; struct list_head cluster_node; + struct work_struct stop_work; bool is_cluster_root; + bool in_cluster_stop; }; /** @@ -140,6 +143,13 @@ struct qcom_pas { * @members: list of struct qcom_pas, linked via cluster_node * @root: the member whose "qcom,cluster-root" points at itself * @root_booted: signaled once @root has started, cleared when it goes down + * @cascade_work: deferred work that restarts the cluster after a crash + * @cascade_origin: member whose stop or crash triggered the current round + * @cascade_crashed: true if @cascade_origin crashed, rather than being stopped + * @stop_in_progress: re-entrancy guard: a coordinated stop is in flight + * @stop_pending: participants still owing a phase-1 graceful-ack attempt + * @stop_barrier: released once @stop_pending reaches 0 + * @stop_done_pending: participants still owing a phase-2 hardware power-off * @refcount: number of members currently attached to this cluster * * PAS instances whose "qcom,cluster-root" phandle points at the same node @@ -154,6 +164,15 @@ struct qcom_pas_cluster { struct qcom_pas *root; struct completion root_booted; + struct work_struct cascade_work; + struct qcom_pas *cascade_origin; + bool cascade_crashed; + bool stop_in_progress; + + int stop_pending; + struct completion stop_barrier; + int stop_done_pending; + int refcount; }; @@ -164,9 +183,78 @@ struct qcom_pas_cluster { */ #define QCOM_PAS_CLUSTER_ROOT_BOOT_TIMEOUT (1 * HZ) +/* + * How long a member waits at the phase-1 barrier for the rest of the cluster, + * in case a participant never reaches it at all (e.g. rproc_stop() bails out + * before calling ops->stop). + */ +#define QCOM_PAS_CLUSTER_STOP_TIMEOUT (20 * HZ) + static LIST_HEAD(qcom_pas_cluster_list); static DEFINE_MUTEX(qcom_pas_cluster_list_lock); +static void qcom_pas_stop_work_fn(struct work_struct *work) +{ + struct qcom_pas *pas = container_of(work, struct qcom_pas, stop_work); + + rproc_shutdown(pas->rproc); +} + +/** + * qcom_pas_cluster_cascade_work() - restart a crashed cluster, root first + * @work: the cluster's cascade_work + * + * Only ever scheduled from qcom_pas_cluster_stop_complete(), i.e. only after + * every participant has finished its own phase-2 hardware power-off, and only + * when the round that just finished was a crash; explicit stops never + * auto-restart. + */ +static void qcom_pas_cluster_cascade_work(struct work_struct *work) +{ + struct qcom_pas_cluster *cluster = container_of(work, struct qcom_pas_cluster, + cascade_work); + struct qcom_pas *pas, *origin, *root; + + mutex_lock(&cluster->lock); + origin = cluster->cascade_origin; + root = cluster->root; + mutex_unlock(&cluster->lock); + + /* + * Membership is stable here: qcom_pas_cluster_exit() always + * cancel_work_sync()s this work before touching cluster->members, so no + * member can join or leave while this work item is running. + * + * If @origin is itself the root, its own crash-recovery thread + * (rproc_boot_recovery()) is already booting it directly -- never call + * rproc_boot() on @origin from here. The root has to be booted before + * any other member, since a dependent member's qcom_pas_start() blocks + * on cluster->root_booted: were we to boot it first from this + * single-threaded work item, it would wait out its timeout on a root + * boot this same thread has not issued yet. + */ + if (root && root != origin) { + int ret; + + ret = rproc_boot(root->rproc); + if (ret) { + dev_err(root->dev, "failed to restart cluster root: %d\n", ret); + return; + } + } + + list_for_each_entry(pas, &cluster->members, cluster_node) { + int ret; + + if (pas == origin || pas == root) + continue; + + ret = rproc_boot(pas->rproc); + if (ret) + dev_err(pas->dev, "failed to restart cluster sibling: %d\n", ret); + } +} + static struct qcom_pas_cluster *qcom_pas_cluster_get(struct device_node *node) { struct qcom_pas_cluster *cluster; @@ -188,6 +276,8 @@ static struct qcom_pas_cluster *qcom_pas_cluster_get(struct device_node *node) mutex_init(&cluster->lock); INIT_LIST_HEAD(&cluster->members); init_completion(&cluster->root_booted); + init_completion(&cluster->stop_barrier); + INIT_WORK(&cluster->cascade_work, qcom_pas_cluster_cascade_work); /* * Cluster members may be attached to already-running firmware at * probe, so the root is presumed up until its own qcom_pas_stop() @@ -257,6 +347,7 @@ static int qcom_pas_cluster_init(struct qcom_pas *pas, struct device_node *np) return -ENOMEM; pas->is_cluster_root = is_root; + INIT_WORK(&pas->stop_work, qcom_pas_stop_work_fn); mutex_lock(&pas->cluster->lock); list_add_tail(&pas->cluster_node, &pas->cluster->members); @@ -274,6 +365,9 @@ static void qcom_pas_cluster_exit(struct qcom_pas *pas) if (!cluster) return; + cancel_work_sync(&pas->stop_work); + cancel_work_sync(&cluster->cascade_work); + mutex_lock(&cluster->lock); list_del(&pas->cluster_node); if (cluster->root == pas) @@ -329,6 +423,153 @@ static int qcom_pas_cluster_wait_for_root(struct qcom_pas *pas) return 0; } +/* + * A member takes part in a coordinated stop if its hardware is still powered: + * either it is running or attached, or it is the crashed member that triggered + * the round, whose rproc->state is RPROC_CRASHED and only becomes + * RPROC_OFFLINE once its ops->stop() has returned. + * + * A sibling that crashes concurrently is deliberately not a participant: it is + * already being torn down by its own recovery, and pulling it into this round + * would leave the two rounds fighting over the same counters. + */ +static bool qcom_pas_cluster_member_stops(struct qcom_pas *member, + struct qcom_pas *origin) +{ + if (member == origin) + return true; + + return member->rproc->state == RPROC_RUNNING || + member->rproc->state == RPROC_ATTACHED; +} + +/** + * qcom_pas_cluster_trigger_stop() - begin a coordinated cluster stop + * @pas: the member that is being stopped or has crashed + * @crashed: true if @pas crashed, rather than being stopped explicitly + * + * Marks every member whose hardware is still powered as a participant of this + * round and fires off each *other* participant's own full stop concurrently + * via its stop_work, instead of one after another. This lets every + * participant's phase-1 graceful-ack attempt (see qcom_pas_stop()) run while + * all of them are still fully powered, so nobody is asking firmware to ack a + * shutdown after a sibling's hardware is already gone. + * + * A no-op if a coordinated stop is already in flight, e.g. when @pas is a + * sibling whose own stop was itself triggered by this same round: @pas is + * already marked as a participant and just goes on to take part in it. + * + * Must not call rproc_shutdown()/rproc_boot() directly from here: this runs + * from inside qcom_pas_stop(), which the remoteproc core calls with @pas's own + * rproc->lock held, and taking a sibling's rproc->lock synchronously from + * within that critical section would risk an ABBA deadlock against a + * concurrent operation on the sibling. + */ +static void qcom_pas_cluster_trigger_stop(struct qcom_pas *pas, bool crashed) +{ + struct qcom_pas_cluster *cluster = pas->cluster; + struct qcom_pas *member; + int active = 0; + + mutex_lock(&cluster->lock); + if (cluster->stop_in_progress) { + mutex_unlock(&cluster->lock); + return; + } + + cluster->stop_in_progress = true; + cluster->cascade_origin = pas; + cluster->cascade_crashed = crashed; + + list_for_each_entry(member, &cluster->members, cluster_node) { + member->in_cluster_stop = qcom_pas_cluster_member_stops(member, pas); + if (member->in_cluster_stop) + active++; + } + + cluster->stop_pending = active; + cluster->stop_done_pending = active; + reinit_completion(&cluster->stop_barrier); + + /* + * Fan out while still holding the lock, so that membership cannot + * change between counting the participants and scheduling them, and so + * that a stop_work running immediately blocks in + * qcom_pas_cluster_stop_barrier() until the counters above are in + * place. schedule_work() does not sleep, so it is safe from here. + */ + list_for_each_entry(member, &cluster->members, cluster_node) { + if (member == pas || !member->in_cluster_stop) + continue; + + schedule_work(&member->stop_work); + } + mutex_unlock(&cluster->lock); +} + +/** + * qcom_pas_cluster_stop_barrier() - wait for the whole cluster's phase-1 ack + * @pas: the member calling this from inside its own qcom_pas_stop() + * + * Blocks this member's own hardware power-off until every other participant + * has also finished its phase-1 graceful-ack attempt (see qcom_pas_stop()). + * Bounded by QCOM_PAS_CLUSTER_STOP_TIMEOUT, proceeding to phase 2 anyway + * rather than hanging forever. + * + * A member that is not a participant of the current round, having already been + * powered off before it started, has no ack to contribute and must not touch + * the counters. + */ +static void qcom_pas_cluster_stop_barrier(struct qcom_pas *pas) +{ + struct qcom_pas_cluster *cluster = pas->cluster; + + mutex_lock(&cluster->lock); + if (!pas->in_cluster_stop) { + mutex_unlock(&cluster->lock); + return; + } + if (--cluster->stop_pending == 0) + complete_all(&cluster->stop_barrier); + mutex_unlock(&cluster->lock); + + if (!wait_for_completion_timeout(&cluster->stop_barrier, + QCOM_PAS_CLUSTER_STOP_TIMEOUT)) + dev_warn(pas->dev, "timed out waiting for cluster stop barrier\n"); +} + +/** + * qcom_pas_cluster_stop_complete() - record this member's phase-2 completion + * @pas: the member calling this from inside its own qcom_pas_stop() + * + * The last participant to call this, i.e. the last to finish powering off its + * own hardware, ends the round and, if it was triggered by a crash, schedules + * the root-first restart cascade. + */ +static void qcom_pas_cluster_stop_complete(struct qcom_pas *pas) +{ + struct qcom_pas_cluster *cluster = pas->cluster; + bool crashed; + + mutex_lock(&cluster->lock); + if (!pas->in_cluster_stop) { + mutex_unlock(&cluster->lock); + return; + } + pas->in_cluster_stop = false; + + if (--cluster->stop_done_pending != 0) { + mutex_unlock(&cluster->lock); + return; + } + cluster->stop_in_progress = false; + crashed = cluster->cascade_crashed; + mutex_unlock(&cluster->lock); + + if (crashed) + schedule_work(&cluster->cascade_work); +} + static void qcom_pas_segment_dump(struct rproc *rproc, struct rproc_dump_segment *segment, void *dest, size_t offset, size_t size) @@ -647,10 +888,18 @@ static int qcom_pas_stop(struct rproc *rproc) mutex_unlock(&pas->cluster->lock); } + if (pas->cluster) + qcom_pas_cluster_trigger_stop(pas, rproc->state == RPROC_CRASHED); + + /* Phase 1: request and await this member's own graceful ack */ ret = qcom_q6v5_request_stop(&pas->q6v5, pas->sysmon); if (ret == -ETIMEDOUT) dev_err(pas->dev, "timed out on wait\n"); + if (pas->cluster) + qcom_pas_cluster_stop_barrier(pas); + + /* Phase 2: the whole cluster has acked, power the hardware off */ ret = qcom_scm_pas_shutdown(pas->pas_id); if (ret && pas->decrypt_shutdown) ret = qcom_pas_shutdown_poll_decrypt(pas); @@ -681,6 +930,9 @@ static int qcom_pas_stop(struct rproc *rproc) if (pas->smem_host_id) ret = qcom_smem_bust_hwspin_lock_by_host(pas->smem_host_id); + if (pas->cluster) + qcom_pas_cluster_stop_complete(pas); + return ret; } From 0c7bd6f7823232b46b8d76f97e673de41d534d0d Mon Sep 17 00:00:00 2001 From: Shawn Guo Date: Sat, 15 Aug 2026 21:25:41 +0800 Subject: [PATCH 09/12] FROMLIST: remoteproc: qcom: pas: Add Nord ADSP1/2 support Add support for additional Hexagon ADSP cores on Nord (adsp1/adsp2), which sit alongside the existing adsp0 as HPASS cluster members. Link: https://lore.kernel.org/r/20260815132541.1575121-9-shengchao.guo@oss.qualcomm.com Signed-off-by: Shawn Guo --- drivers/remoteproc/qcom_q6v5_pas.c | 44 ++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/drivers/remoteproc/qcom_q6v5_pas.c b/drivers/remoteproc/qcom_q6v5_pas.c index b4af74bee221d..3010ae256db8b 100644 --- a/drivers/remoteproc/qcom_q6v5_pas.c +++ b/drivers/remoteproc/qcom_q6v5_pas.c @@ -2032,6 +2032,48 @@ static const struct qcom_pas_data nord_adsp_resource = { .smem_host_id = 2, }; +static const struct qcom_pas_data nord_adsp1_resource = { + .crash_reason_smem = 663, + .firmware_name = "adsp1.mbn", + .dtb_firmware_name = "adsp1_dtb.mbn", + .pas_id = 53, + .dtb_pas_id = 55, + .minidump_id = 21, + .auto_boot = true, + .early_boot = true, + .proxy_pd_names = (char*[]){ + "cx", + "mx", + NULL + }, + .load_state = "adsp1", + .ssr_name = "lpass1", + .sysmon_name = "adsp1", + .ssctl_id = 0x1d, + .smem_host_id = 66, +}; + +static const struct qcom_pas_data nord_adsp2_resource = { + .crash_reason_smem = 664, + .firmware_name = "adsp2.mbn", + .dtb_firmware_name = "adsp2_dtb.mbn", + .pas_id = 54, + .dtb_pas_id = 56, + .minidump_id = 22, + .auto_boot = true, + .early_boot = true, + .proxy_pd_names = (char*[]){ + "cx", + "mx", + NULL + }, + .load_state = "adsp2", + .ssr_name = "lpass2", + .sysmon_name = "adsp2", + .ssctl_id = 0x1e, + .smem_host_id = 130, +}; + static const struct qcom_pas_data nord_cdsp0_resource = { .crash_reason_smem = 601, .firmware_name = "cdsp.mbn", @@ -2378,6 +2420,8 @@ static const struct of_device_id qcom_pas_of_match[] = { { .compatible = "qcom,milos-mpss-pas", .data = &sm8450_mpss_resource }, { .compatible = "qcom,milos-wpss-pas", .data = &sc7280_wpss_resource }, { .compatible = "qcom,nord-adsp-pas", .data = &nord_adsp_resource }, + { .compatible = "qcom,nord-adsp1-pas", .data = &nord_adsp1_resource }, + { .compatible = "qcom,nord-adsp2-pas", .data = &nord_adsp2_resource }, { .compatible = "qcom,nord-cdsp0-pas", .data = &nord_cdsp0_resource }, { .compatible = "qcom,nord-cdsp1-pas", .data = &nord_cdsp1_resource }, { .compatible = "qcom,nord-cdsp2-pas", .data = &nord_cdsp2_resource }, From aa1ebb24defffde22faf46620aaffa758a0c1225 Mon Sep 17 00:00:00 2001 From: Shawn Guo Date: Tue, 28 Jul 2026 18:00:03 +0800 Subject: [PATCH 10/12] PENDING: arm64: dts: qcom: nord: Add remoteproc for ADSP1/2 Add device tree nodes to support ADSP1/2 which share HPASS clock/reset/NoC resources with ADSP0 and must boot after it. Give all three a qcom,cluster-root property so that they form one cluster which boots ADSP0 first and crashes or stops as a single unit. ADSP1/2 reference ADSP0, which owns those shared resources, and ADSP0 references itself, so that every member of the cluster declares its own membership rather than leaving the owner's implicit. Signed-off-by: Shawn Guo --- arch/arm64/boot/dts/qcom/nord-embedded.dtsi | 22 ++ arch/arm64/boot/dts/qcom/nord.dtsi | 226 ++++++++++++++++++++ 2 files changed, 248 insertions(+) diff --git a/arch/arm64/boot/dts/qcom/nord-embedded.dtsi b/arch/arm64/boot/dts/qcom/nord-embedded.dtsi index 0d1c561b1f9e0..cdb48310a54a5 100644 --- a/arch/arm64/boot/dts/qcom/nord-embedded.dtsi +++ b/arch/arm64/boot/dts/qcom/nord-embedded.dtsi @@ -577,6 +577,28 @@ "mx"; }; +&remoteproc_adsp1 { + clocks = <&rpmhcc RPMH_CXO_CLK>; + clock-names = "xo"; + interconnects = <&hpass_ag_noc MASTER_HPASS_PROC_1 QCOM_ICC_TAG_ALWAYS + &mc_virt SLAVE_EBI1 QCOM_ICC_TAG_ALWAYS>; + power-domains = <&rpmhpd RPMHPD_CX>, + <&rpmhpd RPMHPD_MX>; + power-domain-names = "cx", + "mx"; +}; + +&remoteproc_adsp2 { + clocks = <&rpmhcc RPMH_CXO_CLK>; + clock-names = "xo"; + interconnects = <&hpass_ag_noc MASTER_HPASS_PROC_2 QCOM_ICC_TAG_ALWAYS + &mc_virt SLAVE_EBI1 QCOM_ICC_TAG_ALWAYS>; + power-domains = <&rpmhpd RPMHPD_CX>, + <&rpmhpd RPMHPD_MX>; + power-domain-names = "cx", + "mx"; +}; + &remoteproc_cdsp0 { clocks = <&rpmhcc RPMH_CXO_CLK>; clock-names = "xo"; diff --git a/arch/arm64/boot/dts/qcom/nord.dtsi b/arch/arm64/boot/dts/qcom/nord.dtsi index 3d472d2434b64..9ba96d479a39d 100644 --- a/arch/arm64/boot/dts/qcom/nord.dtsi +++ b/arch/arm64/boot/dts/qcom/nord.dtsi @@ -991,6 +991,52 @@ }; }; + smp2p-adsp1 { + compatible = "qcom,smp2p"; + interrupts-extended = <&ipcc IPCC_MPROC_ADSP1 + IPCC_MPROC_SIGNAL_SMP2P + IRQ_TYPE_EDGE_RISING>; + mboxes = <&ipcc IPCC_MPROC_ADSP1 + IPCC_MPROC_SIGNAL_SMP2P>; + qcom,smem = <617>, <616>; + qcom,local-pid = <0>; + qcom,remote-pid = <66>; + + smp2p_adsp1_out: master-kernel { + qcom,entry-name = "master-kernel"; + #qcom,smem-state-cells = <1>; + }; + + smp2p_adsp1_in: slave-kernel { + qcom,entry-name = "slave-kernel"; + interrupt-controller; + #interrupt-cells = <2>; + }; + }; + + smp2p-adsp2 { + compatible = "qcom,smp2p"; + interrupts-extended = <&ipcc IPCC_MPROC_ADSP2 + IPCC_MPROC_SIGNAL_SMP2P + IRQ_TYPE_EDGE_RISING>; + mboxes = <&ipcc IPCC_MPROC_ADSP2 + IPCC_MPROC_SIGNAL_SMP2P>; + qcom,smem = <617>, <616>; + qcom,local-pid = <0>; + qcom,remote-pid = <130>; + + smp2p_adsp2_out: master-kernel { + qcom,entry-name = "master-kernel"; + #qcom,smem-state-cells = <1>; + }; + + smp2p_adsp2_in: slave-kernel { + qcom,entry-name = "slave-kernel"; + interrupt-controller; + #interrupt-cells = <2>; + }; + }; + smp2p-cdsp0 { compatible = "qcom,smp2p"; qcom,smem = <94>, <432>; @@ -1798,6 +1844,8 @@ memory-region = <&hpass_dsp0_mem>, <&hpass_dsp0_dtb_mem>; + qcom,cluster-root = <&remoteproc_adsp>; + qcom,qmp = <&aoss_qmp>; qcom,smem-states = <&smp2p_adsp_out 0>; @@ -1865,6 +1913,184 @@ }; }; + remoteproc_adsp1: remoteproc@6000000 { + compatible = "qcom,nord-adsp1-pas"; + reg = <0x0 0x06000000 0x0 0x10000>; + + interrupts-extended = <&intc GIC_SPI 155 IRQ_TYPE_EDGE_RISING>, + <&smp2p_adsp1_in 0 IRQ_TYPE_EDGE_RISING>, + <&smp2p_adsp1_in 1 IRQ_TYPE_EDGE_RISING>, + <&smp2p_adsp1_in 2 IRQ_TYPE_EDGE_RISING>, + <&smp2p_adsp1_in 3 IRQ_TYPE_EDGE_RISING>, + <&smp2p_adsp1_in 7 IRQ_TYPE_EDGE_RISING>; + interrupt-names = "wdog", + "fatal", + "ready", + "handover", + "stop-ack", + "shutdown-ack"; + + memory-region = <&hpass_dsp1_mem>, + <&hpass_dsp1_dtb_mem>; + + qcom,cluster-root = <&remoteproc_adsp>; + + qcom,qmp = <&aoss_qmp>; + + qcom,smem-states = <&smp2p_adsp1_out 0>; + qcom,smem-state-names = "stop"; + + status = "disabled"; + + glink-edge { + interrupts-extended = <&ipcc IPCC_MPROC_ADSP1 + IPCC_MPROC_SIGNAL_GLINK_QMP + IRQ_TYPE_EDGE_RISING>; + mboxes = <&ipcc IPCC_MPROC_ADSP1 + IPCC_MPROC_SIGNAL_GLINK_QMP>; + qcom,remote-pid = <66>; + label = "adsp1"; + + fastrpc { + compatible = "qcom,nord-fastrpc", + "qcom,kaanapali-fastrpc"; + qcom,glink-channels = "fastrpcglink-apps-dsp"; + label = "adsp1"; + #address-cells = <1>; + #size-cells = <0>; + + compute-cb@11 { + compatible = "qcom,fastrpc-compute-cb"; + reg = <11>; + iommus = <&apps_smmu_0 0x0a0b 0x0040>, + <&apps_smmu_0 0x0a4b 0x0040>, + <&apps_smmu_0 0x0a6b 0x0000>, + <&apps_smmu_0 0x0a8b 0x0000>; + dma-coherent; + }; + + compute-cb@12 { + compatible = "qcom,fastrpc-compute-cb"; + reg = <12>; + iommus = <&apps_smmu_0 0x0a0c 0x0080>, + <&apps_smmu_0 0x0a4c 0x0020>, + <&apps_smmu_0 0x0a6c 0x0020>, + <&apps_smmu_0 0x0a8c 0x0080>; + dma-coherent; + }; + + compute-cb@13 { + compatible = "qcom,fastrpc-compute-cb"; + reg = <13>; + iommus = <&apps_smmu_0 0x0a0d 0x0080>, + <&apps_smmu_0 0x0a4d 0x0020>, + <&apps_smmu_0 0x0a6d 0x0020>, + <&apps_smmu_0 0x0a8d 0x0080>; + dma-coherent; + }; + + compute-cb@14 { + compatible = "qcom,fastrpc-compute-cb"; + reg = <14>; + iommus = <&apps_smmu_0 0x0a0e 0x0080>, + <&apps_smmu_0 0x0a4e 0x0020>, + <&apps_smmu_0 0x0a6e 0x0020>, + <&apps_smmu_0 0x0a8e 0x0080>; + dma-coherent; + }; + }; + }; + }; + + remoteproc_adsp2: remoteproc@6100000 { + compatible = "qcom,nord-adsp2-pas"; + reg = <0x0 0x06100000 0x0 0x10000>; + + interrupts-extended = <&intc GIC_SPI 178 IRQ_TYPE_EDGE_RISING>, + <&smp2p_adsp2_in 0 IRQ_TYPE_EDGE_RISING>, + <&smp2p_adsp2_in 1 IRQ_TYPE_EDGE_RISING>, + <&smp2p_adsp2_in 2 IRQ_TYPE_EDGE_RISING>, + <&smp2p_adsp2_in 3 IRQ_TYPE_EDGE_RISING>, + <&smp2p_adsp2_in 7 IRQ_TYPE_EDGE_RISING>; + interrupt-names = "wdog", + "fatal", + "ready", + "handover", + "stop-ack", + "shutdown-ack"; + + memory-region = <&hpass_dsp2_mem>, + <&hpass_dsp2_dtb_mem>; + + qcom,cluster-root = <&remoteproc_adsp>; + + qcom,qmp = <&aoss_qmp>; + + qcom,smem-states = <&smp2p_adsp2_out 0>; + qcom,smem-state-names = "stop"; + + status = "disabled"; + + glink-edge { + interrupts-extended = <&ipcc IPCC_MPROC_ADSP2 + IPCC_MPROC_SIGNAL_GLINK_QMP + IRQ_TYPE_EDGE_RISING>; + mboxes = <&ipcc IPCC_MPROC_ADSP2 + IPCC_MPROC_SIGNAL_GLINK_QMP>; + qcom,remote-pid = <130>; + label = "adsp2"; + + fastrpc { + compatible = "qcom,nord-fastrpc", + "qcom,kaanapali-fastrpc"; + qcom,glink-channels = "fastrpcglink-apps-dsp"; + label = "adsp2"; + #address-cells = <1>; + #size-cells = <0>; + + compute-cb@3 { + compatible = "qcom,fastrpc-compute-cb"; + reg = <3>; + iommus = <&apps_smmu_0 0x0a33 0x0040>, + <&apps_smmu_0 0x0a53 0x0000>, + <&apps_smmu_0 0x0a73 0x0040>, + <&apps_smmu_0 0x0a93 0x0000>; + dma-coherent; + }; + + compute-cb@4 { + compatible = "qcom,fastrpc-compute-cb"; + reg = <4>; + iommus = <&apps_smmu_0 0x0a34 0x0040>, + <&apps_smmu_0 0x0a54 0x0000>, + <&apps_smmu_0 0x0a74 0x0040>, + <&apps_smmu_0 0x0a94 0x0000>; + dma-coherent; + }; + + compute-cb@5 { + compatible = "qcom,fastrpc-compute-cb"; + reg = <5>; + iommus = <&apps_smmu_0 0x0a35 0x0000>, + <&apps_smmu_0 0x0a55 0x0020>, + <&apps_smmu_0 0x0a75 0x0020>, + <&apps_smmu_0 0x0a95 0x0000>; + dma-coherent; + }; + + compute-cb@6 { + compatible = "qcom,fastrpc-compute-cb"; + reg = <6>; + iommus = <&apps_smmu_0 0x0a36 0x0000>, + <&apps_smmu_0 0x0a56 0x0020>, + <&apps_smmu_0 0x0a76 0x0020>, + <&apps_smmu_0 0x0a96 0x0000>; + dma-coherent; + }; + }; + }; + }; + ipcc: mailbox@9007000 { compatible = "qcom,nord-ipcc", "qcom,ipcc"; From 3ca3f414281e0bbe08e2967200d755561223fa76 Mon Sep 17 00:00:00 2001 From: Shawn Guo Date: Tue, 28 Jul 2026 18:01:10 +0800 Subject: [PATCH 11/12] PENDING: arm64: dts: qcom: nord-rrd: Enable ADSP1/2 remoteproc Enable ADSP1 and ADSP2 remoteproc on the Nord RRD board. Signed-off-by: Shawn Guo --- arch/arm64/boot/dts/qcom/nord-rrd.dts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/arch/arm64/boot/dts/qcom/nord-rrd.dts b/arch/arm64/boot/dts/qcom/nord-rrd.dts index ff9ef5d821c4f..12047eaf081b1 100644 --- a/arch/arm64/boot/dts/qcom/nord-rrd.dts +++ b/arch/arm64/boot/dts/qcom/nord-rrd.dts @@ -562,6 +562,20 @@ status = "okay"; }; +&remoteproc_adsp1 { + firmware-name = "qcom/nord/adsp1.mbn", + "qcom/nord/adsp1_dtbs.elf"; + + status = "okay"; +}; + +&remoteproc_adsp2 { + firmware-name = "qcom/nord/adsp2.mbn", + "qcom/nord/adsp2_dtbs.elf"; + + status = "okay"; +}; + &remoteproc_cdsp0 { firmware-name = "qcom/nord/cdsp.mbn", "qcom/nord/cdsp_dtbs.elf"; From a5391f8c6d81c6cc0d043cfd80cda8953ead09a6 Mon Sep 17 00:00:00 2001 From: Shawn Guo Date: Sat, 8 Aug 2026 20:56:19 +0800 Subject: [PATCH 12/12] PENDING: arm64: dts: qcom: nord-ride-sx: Enable ADSP1/2 remoteproc Enable ADSP1 and ADSP2 remoteproc on the Nord Ride SX board. Signed-off-by: Shawn Guo --- arch/arm64/boot/dts/qcom/nord-ride-sx.dts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/arch/arm64/boot/dts/qcom/nord-ride-sx.dts b/arch/arm64/boot/dts/qcom/nord-ride-sx.dts index b8839e1b86421..922749cc7ad2a 100644 --- a/arch/arm64/boot/dts/qcom/nord-ride-sx.dts +++ b/arch/arm64/boot/dts/qcom/nord-ride-sx.dts @@ -594,6 +594,20 @@ status = "okay"; }; +&remoteproc_adsp1 { + firmware-name = "qcom/nord/adsp1.mbn", + "qcom/nord/adsp1_dtbs.elf"; + + status = "okay"; +}; + +&remoteproc_adsp2 { + firmware-name = "qcom/nord/adsp2.mbn", + "qcom/nord/adsp2_dtbs.elf"; + + status = "okay"; +}; + &remoteproc_cdsp0 { firmware-name = "qcom/nord/cdsp.mbn", "qcom/nord/cdsp_dtbs.elf";