git: 75a1939f6360 - main - aq(4): arm PHY thermal shutdown from the admin poll

From: Nick Price <nprice_at_FreeBSD.org>
Date: Sat, 12 Sep 2026 22:19:51 UTC
The branch main has been updated by nprice:

URL: https://cgit.FreeBSD.org/src/commit/?id=75a1939f6360d4a329891e2075d18eef069db9bb

commit 75a1939f6360d4a329891e2075d18eef069db9bb
Author:     Nick Price <nprice@FreeBSD.org>
AuthorDate: 2026-09-11 03:35:07 +0000
Commit:     Nick Price <nprice@FreeBSD.org>
CommitDate: 2026-09-12 22:19:14 +0000

    aq(4): arm PHY thermal shutdown from the admin poll
    
    move thermal shutdown arming to the admin poll
    
    this gives a more reasonable delay prior to the first attempt, and also
    allows us to retry and make the option runtime-tuneable via a new
    disable_thermal_arm sysctl
    
    Approved by:    adrian (mentor)
    Reviewed by:    adrian
    Differential Revision:  https://reviews.freebsd.org/D59593
    
    Signed-off-by: Nick Price <nprice@FreeBSD.org>
---
 share/man/man4/aq.4    | 21 ++++++++++++++++++++-
 sys/dev/aq/aq2_fw.c    | 35 ++++++++++++++++++++++++++++++++++-
 sys/dev/aq/aq_device.h |  2 ++
 sys/dev/aq/aq_fw.h     |  4 ++--
 sys/dev/aq/aq_fw2x.c   | 22 ++++++++++++++++------
 sys/dev/aq/aq_irq.c    | 30 ++++++++++++++++++++++++++++++
 sys/dev/aq/aq_main.c   | 24 ++++++++++++++----------
 7 files changed, 118 insertions(+), 20 deletions(-)

diff --git a/share/man/man4/aq.4 b/share/man/man4/aq.4
index 50b3fd853e8c..c511070248cd 100644
--- a/share/man/man4/aq.4
+++ b/share/man/man4/aq.4
@@ -3,7 +3,7 @@
 .\"
 .\" SPDX-License-Identifier: BSD-2-Clause
 .\"
-.Dd August 6, 2026
+.Dd September 11, 2026
 .Dt AQ 4
 .Os
 .Sh NAME
@@ -97,6 +97,24 @@ Comparing this against the transitions logged by the driver
 distinguishes a single link event from a flapping link.
 .El
 .Pp
+The following variable controls the PHY thermal shutdown.
+It is present only on adapters whose firmware reports a PHY temperature
+and lets the driver arm the shutdown, and it may also be set as a
+.Xr loader.conf 5
+tunable:
+.Bl -tag -width indent
+.It Va thermal_shutdown.enabled
+Arm the PHY's autonomous thermal shutdown.
+Armed, the PHY drops the link when its die passes the threshold the
+board provisions; the driver reports the trip, waits for the PHY to
+cool, and brings the link back.
+The default, 1, arms the shutdown on every adapter that supports it,
+whether or not its firmware ships it armed; 0 disarms it on those same
+adapters.
+The driver applies the setting from its periodic status poll, normally
+within a second, and again after any reset of the adapter.
+.El
+.Pp
 The following variables tune the driver's own tracing:
 .Bl -tag -width indent
 .It Va debug
@@ -111,4 +129,5 @@ All categories are enabled by default.
 .Sh SEE ALSO
 .Xr arp 4 ,
 .Xr miibus 4 ,
+.Xr loader.conf 5 ,
 .Xr ifconfig 8
diff --git a/sys/dev/aq/aq2_fw.c b/sys/dev/aq/aq2_fw.c
index 3c2ba7ad944c..c373e98ebfe1 100644
--- a/sys/dev/aq/aq2_fw.c
+++ b/sys/dev/aq/aq2_fw.c
@@ -46,6 +46,7 @@ static int aq2_fw_get_link_counters(struct aq_hw *hw, uint32_t *up,
     uint32_t *down);
 static int aq2_fw_get_temp(struct aq_hw *hw, int *temp_mc);
 static int aq2_fw_get_thermal_limit(struct aq_hw *hw, int *limit_mc);
+static int aq2_fw_thermal_arm(struct aq_hw *hw, bool enable);
 static int aq2_fw_get_phy_hot_warning(struct aq_hw *hw, bool *hot);
 
 /* Coherent OUT-window read, bracketed by the transaction id. */
@@ -542,6 +543,38 @@ aq2_fw_get_temp(struct aq_hw *hw, int *temp_mc)
 	return (0);
 }
 
+/* interface-in thermal_shutdown.enable; the F/W boots with it set. */
+static int
+aq2_fw_thermal_arm(struct aq_hw *hw, bool enable)
+{
+	uint32_t v, want;
+	int err, limit_mc;
+
+	/* No threshold means an unpopulated word or a departed adapter. */
+	err = aq2_fw_get_thermal_limit(hw, &limit_mc);
+	if (err != 0)
+		return (err);
+
+	mtx_lock(&hw->fw_mtx);
+	v = AQ_READ_REG(hw, AQ2_FW_INTERFACE_IN_THERMAL_SHUTDOWN_REG);
+	if (enable)
+		want = v | AQ2_FW_INTERFACE_IN_THERMAL_SHUTDOWN_ENABLE;
+	else
+		want = v & ~AQ2_FW_INTERFACE_IN_THERMAL_SHUTDOWN_ENABLE;
+	if (want != v) {
+		AQ_WRITE_REG(hw, AQ2_FW_INTERFACE_IN_THERMAL_SHUTDOWN_REG,
+		    want);
+		err = aq2_fw_wait_shared_ack(hw);
+		/* Unacked: put the word back so the next poll retries. */
+		if (err != 0)
+			AQ_WRITE_REG(hw,
+			    AQ2_FW_INTERFACE_IN_THERMAL_SHUTDOWN_REG, v);
+	}
+	mtx_unlock(&hw->fw_mtx);
+
+	return (err);
+}
+
 /* interface-in thermal_shutdown.shutdown_temperature, whole degC. */
 static int
 aq2_fw_get_thermal_limit(struct aq_hw *hw, int *limit_mc)
@@ -618,7 +651,7 @@ const struct aq_firmware_ops aq2_fw_ops = {
 	.get_phy_fault = aq2_fw_get_phy_fault,
 	.get_phy_hot_warning = aq2_fw_get_phy_hot_warning,
 	.phy_reset = NULL,	/* A2 clears thermal shutdown on its own reset */
-	.thermal_arm = NULL,	/* A2 firmware ships thermal shutdown armed */
+	.thermal_arm = aq2_fw_thermal_arm,
 	.get_thermal_limit = aq2_fw_get_thermal_limit,
 	.led_control = NULL,
 };
diff --git a/sys/dev/aq/aq_device.h b/sys/dev/aq/aq_device.h
index 4f4c3335e4c8..1f71a410b9f0 100644
--- a/sys/dev/aq/aq_device.h
+++ b/sys/dev/aq/aq_device.h
@@ -134,6 +134,8 @@ struct aq_dev {
 	int               thermal_retry_ticks;	/* earliest tick to retry at */
 	int               thermal_temp_mc;	/* temp at the last shutdown/cool */
 	int               thermal_recover_mc;	/* recover once cooled to here */
+	bool              thermal_shutdown_enabled;	/* arm the shutdown */
+	int               thermal_arm_err;	/* last result returned */
 	bool              init_failed;		/* aq_hw_init() left the hw down */
 	int               init_retries;
 	bool              reset_pending;	/* a re-init is already queued */
diff --git a/sys/dev/aq/aq_fw.h b/sys/dev/aq/aq_fw.h
index bdac9f87cb08..334c8c874edf 100644
--- a/sys/dev/aq/aq_fw.h
+++ b/sys/dev/aq/aq_fw.h
@@ -86,8 +86,8 @@ struct aq_firmware_ops
 	/* Resets the PHY (clears a latched thermal shutdown). */
 	int (*phy_reset)(struct aq_hw* hw);
 
-	/* Arms the firmware autonomous thermal shutdown (A2 ships armed). */
-	int (*thermal_arm)(struct aq_hw* hw);
+	/* Arms or disarms the firmware autonomous thermal shutdown. */
+	int (*thermal_arm)(struct aq_hw* hw, bool enable);
 
 	/* Reports the PHY high-temperature shutdown threshold (millidegrees C). */
 	int (*get_thermal_limit)(struct aq_hw* hw, int* limit_mc);
diff --git a/sys/dev/aq/aq_fw2x.c b/sys/dev/aq/aq_fw2x.c
index a4e584da2d65..cfb8048ac81a 100644
--- a/sys/dev/aq/aq_fw2x.c
+++ b/sys/dev/aq/aq_fw2x.c
@@ -650,11 +650,11 @@ aq_fw2x_phy_reset(struct aq_hw* hw)
 	return (err);
 }
 
-/* Arm autonomous thermal shutdown (1E.C478.A), cleared by any PHY reset. */
+/* Arm/disarm thermal shutdown, cleared by any PHY reset. */
 static int
-aq_fw2x_thermal_arm(struct aq_hw* hw)
+aq_fw2x_thermal_arm(struct aq_hw* hw, bool enable)
 {
-	uint16_t ctrl;
+	uint16_t ctrl, want;
 	int err;
 
 	if ((hw->fw_caps & FW2X_CAP_TEMPERATURE) == 0)
@@ -664,11 +664,21 @@ aq_fw2x_thermal_arm(struct aq_hw* hw)
 	aq_fw2x_phy_id_probe(hw);
 	err = aq_fw2x_phy_read(hw, AQ_PHY_MMD_GLOBAL, AQ_PHY_THERMAL_CTRL_REG,
 	    &ctrl);
-	if (err == 0 && ctrl == 0xffff)
+	if (err != 0)
+		goto out;
+	/* All ones is no PHY answering, not a control word. */
+	if (ctrl == 0xffff) {
 		err = ENXIO;
-	if (err == 0 && (ctrl & AQ_PHY_THERMAL_SD_EN) == 0)
+		goto out;
+	}
+	if (enable)
+		want = ctrl | AQ_PHY_THERMAL_SD_EN;
+	else
+		want = ctrl & ~AQ_PHY_THERMAL_SD_EN;
+	if (want != ctrl)
 		err = aq_fw2x_phy_write(hw, AQ_PHY_MMD_GLOBAL,
-		    AQ_PHY_THERMAL_CTRL_REG, ctrl | AQ_PHY_THERMAL_SD_EN);
+		    AQ_PHY_THERMAL_CTRL_REG, want);
+out:
 	mtx_unlock(&hw->fw_mtx);
 
 	return (err);
diff --git a/sys/dev/aq/aq_irq.c b/sys/dev/aq/aq_irq.c
index e52820ad77dc..48cbb6c3f2de 100644
--- a/sys/dev/aq/aq_irq.c
+++ b/sys/dev/aq/aq_irq.c
@@ -178,6 +178,35 @@ aq_thermal_report_hot(struct aq_dev *aq_dev)
 		device_printf(aq_dev->dev, "PHY over-temperature warning\n");
 }
 
+/* reconcile thermal arm with knob value */
+static void
+aq_thermal_apply(struct aq_dev *aq_dev)
+{
+	struct aq_hw *hw = &aq_dev->hw;
+	bool enable;
+	int err;
+
+	/* A queued re-init would undo the write; the poll after it applies. */
+	if (hw->fw_ops->thermal_arm == NULL || aq_dev->reset_pending)
+		return;
+
+	enable = aq_dev->thermal_shutdown_enabled;
+	err = hw->fw_ops->thermal_arm(hw, enable);
+	/* No sensor to arm against; the knob is absent on such an adapter. */
+	if (err == ENOTSUP)
+		return;
+	/* Print only on a change, so a lasting failure is reported once. */
+	if (err == aq_dev->thermal_arm_err)
+		return;
+	aq_dev->thermal_arm_err = err;
+	if (err != 0)
+		device_printf(aq_dev->dev, "could not %s PHY thermal shutdown, "
+		    "error %d\n", enable ? "arm" : "disarm", err);
+	else
+		device_printf(aq_dev->dev, "PHY thermal shutdown %s\n",
+		    enable ? "armed" : "disarmed");
+}
+
 /* Recover after cooldown: A1 needs a PHY reset then re-init, A2 re-inits alone. */
 static void
 aq_thermal_poll(struct aq_dev *aq_dev)
@@ -188,6 +217,7 @@ aq_thermal_poll(struct aq_dev *aq_dev)
 
 	switch (aq_dev->thermal_state) {
 	case AQ_THERMAL_NORMAL:
+		aq_thermal_apply(aq_dev);
 		aq_thermal_report_hot(aq_dev);
 		if (aq_dev->linkup)
 			return;
diff --git a/sys/dev/aq/aq_main.c b/sys/dev/aq/aq_main.c
index 819ab5fb7e5c..f55f958233ce 100644
--- a/sys/dev/aq/aq_main.c
+++ b/sys/dev/aq/aq_main.c
@@ -378,6 +378,7 @@ aq_if_attach_pre(if_ctx_t ctx)
 	hw->fc.fc_rx = 1;
 	hw->fc.fc_tx = 1;
 	softc->linkup = 0U;
+	softc->thermal_shutdown_enabled = true;
 	/* Set here, not in aq_if_init(): a recovery re-init must not reset it. */
 	softc->thermal_retry_ticks = ticks;
 
@@ -778,14 +779,6 @@ aq_if_init(if_ctx_t ctx)
 	softc->init_failed = false;
 	softc->init_retries = 0;
 
-	/* aq_hw_init reloads the PHY, resetting the thermal-shutdown arming. */
-	if (hw->fw_ops->thermal_arm != NULL) {
-		err = hw->fw_ops->thermal_arm(hw);
-		if (err != 0 && err != ENOTSUP)
-			device_printf(softc->dev,
-			    "could not arm PHY thermal shutdown\n");
-	}
-
 	aq_if_media_status(ctx, &ifmr);
 
 	aq_update_vlan_filters(softc);
@@ -1459,7 +1452,7 @@ aq_add_stats_sysctls(struct aq_dev *softc)
 	struct sysctl_oid       *tree = device_get_sysctl_tree(dev);
 	struct sysctl_oid_list  *child = SYSCTL_CHILDREN(tree);
 	struct aq_stats *stats = &softc->curr_stats;
-	struct sysctl_oid       *stat_node, *queue_node;
+	struct sysctl_oid       *stat_node, *queue_node, *thermal_node;
 	struct sysctl_oid_list  *stat_list, *queue_list;
 	uint32_t                link_up, link_down;
 	int                     temp_mc;
@@ -1481,10 +1474,21 @@ aq_add_stats_sysctls(struct aq_dev *softc)
 
 	/* ENOTSUP means no sensor; other errors may just be a cold PHY. */
 	if (softc->hw.fw_ops != NULL && softc->hw.fw_ops->get_temp != NULL &&
-	    softc->hw.fw_ops->get_temp(&softc->hw, &temp_mc) != ENOTSUP)
+	    softc->hw.fw_ops->get_temp(&softc->hw, &temp_mc) != ENOTSUP) {
 		SYSCTL_ADD_PROC(ctx, child, OID_AUTO, "temperature",
 		    CTLTYPE_INT | CTLFLAG_RD | CTLFLAG_MPSAFE, softc, 0,
 		    aq_sysctl_temperature, "IK", "PHY temperature");
+		/* Without the sensor there is no shutdown to arm. */
+		if (softc->hw.fw_ops->thermal_arm != NULL) {
+			thermal_node = SYSCTL_ADD_NODE(ctx, child, OID_AUTO,
+			    "thermal_shutdown", CTLFLAG_RD | CTLFLAG_MPSAFE,
+			    NULL, "PHY thermal shutdown");
+			SYSCTL_ADD_BOOL(ctx, SYSCTL_CHILDREN(thermal_node),
+			    OID_AUTO, "enabled", CTLFLAG_RWTUN,
+			    &softc->thermal_shutdown_enabled, 0,
+			    "Arm the PHY's autonomous thermal shutdown");
+		}
+	}
 
 	/* Only some firmware interface versions count link transitions. */
 	if (softc->hw.fw_ops != NULL &&