From 55c73c2c1fcf59b3c0895d683a641c1e6b8a6cd0 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Thu, 29 Oct 2026 09:33:19 +1100 Subject: [PATCH 1/4] thermal: power allocator: run fans on active trip points The power allocator only budgets power actors on its passive trips. A cooling device without power callbacks, such as a fan, mapped to an active trip of a zone the power allocator governs was never set, so a device tree fan curve did nothing on kernels that make it the default governor. Run those cooling devices the way the bang-bang governor does: when the zone reaches an active trip, each such instance asks for its upper state, and when the zone falls below the trip minus its hysteresis it asks for nothing. A cooling device mapped to several trips takes the deepest state asked for, so a ladder of active trips gives a stepped fan curve. Instances that have no target yet (a cooling device bound after the zone passed the trip, or after resume) are set from the zone temperature on the next update. Power actors are left to the power allocator as before, and zones without active trips are unchanged. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/thermal/gov_power_allocator.c | 57 +++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/drivers/thermal/gov_power_allocator.c b/drivers/thermal/gov_power_allocator.c index aaa2192dd5be9..ce9cd7f2298b5 100644 --- a/drivers/thermal/gov_power_allocator.c +++ b/drivers/thermal/gov_power_allocator.c @@ -793,6 +793,60 @@ static void power_allocator_unbind(struct thermal_zone_device *tz) tz->governor_data = NULL; } +/* + * Cooling devices that are not power actors, such as fans, have no power to + * allocate, so the governor runs those on active trip points the way the + * bang-bang governor does: an instance asks for its upper state once the zone + * reaches the trip and for nothing once it drops below the trip's hysteresis. + * A cooling device bound to several trips takes the deepest state asked for, + * so a ladder of active trips gives a stepped fan curve. + */ +static void power_allocator_set_active(struct thermal_instance *instance, + bool on) +{ + instance->target = on ? instance->upper : THERMAL_NO_TARGET; + instance->initialized = true; + thermal_cdev_update_nocheck(instance->cdev); +} + +static void power_allocator_trip_crossed(struct thermal_zone_device *tz, + const struct thermal_trip *trip, + bool upward) +{ + const struct thermal_trip_desc *td = trip_to_trip_desc(trip); + struct thermal_instance *instance; + + lockdep_assert_held(&tz->lock); + + if (trip->type != THERMAL_TRIP_ACTIVE) + return; + + list_for_each_entry(instance, &td->thermal_instances, trip_node) + if (!power_actor_is_valid(instance)) + power_allocator_set_active(instance, upward); +} + +/* + * Set up the active trip instances that have no target yet: after a cooling + * device binds to a trip the zone already sits above, and after resume. + */ +static void power_allocator_init_active(struct thermal_zone_device *tz) +{ + const struct thermal_trip_desc *td; + struct thermal_instance *instance; + + for_each_trip_desc(tz, td) { + if (td->trip.type != THERMAL_TRIP_ACTIVE || + td->trip.temperature == THERMAL_TEMP_INVALID) + continue; + + list_for_each_entry(instance, &td->thermal_instances, trip_node) + if (!instance->initialized && !power_actor_is_valid(instance)) + power_allocator_set_active(instance, + tz->temperature >= td->threshold); + } +} + static void power_allocator_manage(struct thermal_zone_device *tz) { struct power_allocator_params *params = tz->governor_data; @@ -800,6 +854,8 @@ static void power_allocator_manage(struct thermal_zone_device *tz) lockdep_assert_held(&tz->lock); + power_allocator_init_active(tz); + if (trip && tz->temperature < trip->temperature) { reset_pid_controller(params); allow_maximum_power(tz); @@ -818,6 +874,7 @@ static struct thermal_governor thermal_gov_power_allocator = { .name = "power_allocator", .bind_to_tz = power_allocator_bind, .unbind_from_tz = power_allocator_unbind, + .trip_crossed = power_allocator_trip_crossed, .manage = power_allocator_manage, .update_tz = power_allocator_update_tz, }; From 860960cc47be1670890f9c839ff7690388f8f842 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Thu, 29 Oct 2026 09:39:01 +1100 Subject: [PATCH 2/4] dt-bindings: hwmon: apple,smc-hwmon: Allow fans as cooling devices With #cooling-cells = <3> the SMC hwmon node provides its fans as cooling devices, named by their index among the fan-* nodes as in <&smc_hwmon fan-index lower upper>. State 0 leaves a fan to the SMC; the other states run it at speeds spread from its minimum to its maximum. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../devicetree/bindings/hwmon/apple,smc-hwmon.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Documentation/devicetree/bindings/hwmon/apple,smc-hwmon.yaml b/Documentation/devicetree/bindings/hwmon/apple,smc-hwmon.yaml index df155f9c34a65..4e0b4268f3ffa 100644 --- a/Documentation/devicetree/bindings/hwmon/apple,smc-hwmon.yaml +++ b/Documentation/devicetree/bindings/hwmon/apple,smc-hwmon.yaml @@ -56,6 +56,15 @@ properties: The cell is the zero-based index of a temperature channel, counting the temperature-* child nodes in order. + "#cooling-cells": + const: 3 + description: + Makes the fans cooling devices for thermal zones. The first cell is the + zero-based index of a fan, counting the fan-* child nodes in order; the + other two are the lower and upper cooling state. State 0 leaves the fan + to the SMC; states 1 to 8 run it at speeds spread evenly from its + minimum to its maximum. A fan needs all four of its speed and mode keys. + patternProperties: "^current-[A-Za-z0-9]{4}$": $ref: "#/$defs/sensor" From 93c3026d2e0eb2fc7a7a69cb37ae4e737cd978d5 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Thu, 29 Oct 2026 09:39:01 +1100 Subject: [PATCH 3/4] hwmon: macsmc: Register fans as thermal cooling devices On the M4 Pro MacBook Pro (J616s) the SMC keeps both fans in its own mode 3. With no operating system asking it for cooling, it leaves them off, or switches them on late and ramps them slowly, while the dies heat up: four busy CPU cores took the CPU die from 52 C to 95 C in two minutes with both fans at 0 rpm. Linux has to ask for the cooling. With #cooling-cells = <3> on the hwmon node, every fan registers a cooling device, named by its index among the fan-* nodes. State 0 leaves the fan to the SMC; states 1 to 8 run it at speeds spread evenly from its minimum to its maximum, through the same manual mode and Ftst unlock as the fan_control target, which they do not need. A fan runs at the higher of the userspace target and its cooling state, so userspace cannot take it below what the thermal zones ask for, and it goes back to the SMC once neither asks for anything. Unlocking takes the SMC seconds, so states are applied from a work item. Any failure leaves the fan with the SMC. Suspend, shutdown and removal stop the work and hand every fan back; resume applies the cooling states again. Without #cooling-cells nothing changes. KUnit covers the state to speed mapping, cooling without fan_control, cooling together with a userspace target, the stopped state and a failed unlock. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/hwmon/macsmc-hwmon-test.c | 104 ++++++++++++- drivers/hwmon/macsmc-hwmon.c | 248 ++++++++++++++++++++++++++---- 2 files changed, 324 insertions(+), 28 deletions(-) diff --git a/drivers/hwmon/macsmc-hwmon-test.c b/drivers/hwmon/macsmc-hwmon-test.c index 596e904a6f3e8..312e752565e04 100644 --- a/drivers/hwmon/macsmc-hwmon-test.c +++ b/drivers/hwmon/macsmc-hwmon-test.c @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-2.0-only OR MIT /* * KUnit tests for the Apple SMC hwmon driver: channel numbering of the - * temperature sensors, which thermal zones depend on, and manual fan control. + * temperature sensors, which thermal zones depend on, and manual fan control + * by userspace and by the thermal framework. * * Included by macsmc-hwmon.c. */ @@ -217,6 +218,9 @@ static int macsmc_fan_test_init(struct kunit *test) t->hwmon.fan.count = 2; t->hwmon.fan.unlock_key = SMC_KEY(Ftst); mutex_init(&t->hwmon.fan.lock); + INIT_WORK(&t->hwmon.fan.cool_work, macsmc_hwmon_fans_cool_work); + for (i = 0; i < 2; i++) + t->fans[i].hwmon = &t->hwmon; test->priv = t; fan_control = true; kunit_activate_static_stub(test, apple_smc_read, fan_test_read); @@ -326,6 +330,99 @@ static void macsmc_fan_test_legacy(struct kunit *test) KUNIT_EXPECT_EQ(test, t->refused, 0); } +/* Cooling states spread over [min, max]; 0 asks for nothing. */ +static void macsmc_fan_test_state_rpm(struct kunit *test) +{ + KUNIT_EXPECT_EQ(test, macsmc_hwmon_fan_state_rpm(1350, 5777, 0), 0); + KUNIT_EXPECT_EQ(test, macsmc_hwmon_fan_state_rpm(1350, 5777, 1), 1350); + KUNIT_EXPECT_EQ(test, macsmc_hwmon_fan_state_rpm(1350, 5777, 4), 3247); + KUNIT_EXPECT_EQ(test, macsmc_hwmon_fan_state_rpm(1350, 5777, + MACSMC_FAN_COOLING_STATES), 5777); + /* Never past the maximum, whatever the state. */ + KUNIT_EXPECT_EQ(test, macsmc_hwmon_fan_state_rpm(1350, 5777, + MACSMC_FAN_COOLING_STATES + 5), 5777); +} + +/* Cooling needs no fan_control: unlock for a state, release at state 0. */ +static void macsmc_fan_test_cooling(struct kunit *test) +{ + struct macsmc_fan_test *t = test->priv; + + fan_control = false; + t->fans[0].cool_state = 1; + macsmc_hwmon_fans_apply(&t->hwmon); + KUNIT_EXPECT_EQ(test, t->ftst, 1); + KUNIT_EXPECT_EQ(test, t->mode[0], MACSMC_FAN_MODE_MANUAL); + KUNIT_EXPECT_EQ(test, t->target[0], FAN_TEST_F1350); + /* The other fan asks for nothing and stays with the firmware. */ + KUNIT_EXPECT_EQ(test, t->mode[1], MACSMC_FAN_MODE_AUTO); + KUNIT_EXPECT_FALSE(test, t->fans[1].manual); + + t->fans[0].cool_state = MACSMC_FAN_COOLING_STATES; + macsmc_hwmon_fans_apply(&t->hwmon); + KUNIT_EXPECT_EQ(test, t->target[0], FAN_TEST_F5777); + KUNIT_EXPECT_EQ(test, t->ftst_writes, 1); + + t->fans[0].cool_state = 0; + macsmc_hwmon_fans_apply(&t->hwmon); + fan_test_released(test, t); +} + +/* A fan runs at the higher of the userspace target and the cooling state. */ +static void macsmc_fan_test_cooling_and_user(struct kunit *test) +{ + struct macsmc_fan_test *t = test->priv; + + KUNIT_ASSERT_EQ(test, macsmc_hwmon_set_fan(&t->hwmon, 0, 2500), 0); + t->fans[0].cool_state = 1; + macsmc_hwmon_fans_apply(&t->hwmon); + KUNIT_EXPECT_EQ(test, t->target[0], FAN_TEST_F2500); + + t->fans[0].cool_state = MACSMC_FAN_COOLING_STATES; + macsmc_hwmon_fans_apply(&t->hwmon); + KUNIT_EXPECT_EQ(test, t->target[0], FAN_TEST_F5777); + /* Userspace cannot take the fan below what cooling asks for. */ + KUNIT_ASSERT_EQ(test, macsmc_hwmon_set_fan(&t->hwmon, 0, 1350), 0); + KUNIT_EXPECT_EQ(test, t->target[0], FAN_TEST_F5777); + KUNIT_ASSERT_EQ(test, macsmc_hwmon_set_fan(&t->hwmon, 0, 0), 0); + KUNIT_EXPECT_EQ(test, t->mode[0], MACSMC_FAN_MODE_MANUAL); + + t->fans[0].cool_state = 0; + macsmc_hwmon_fans_apply(&t->hwmon); + fan_test_released(test, t); +} + +/* Once stopped (suspend, shutdown, removal), no state takes a fan back. */ +static void macsmc_fan_test_cooling_stopped(struct kunit *test) +{ + struct macsmc_fan_test *t = test->priv; + + t->fans[0].cool_state = 4; + t->fans[1].cool_state = 4; + macsmc_hwmon_fans_apply(&t->hwmon); + KUNIT_EXPECT_EQ(test, t->hwmon.fan.manual, 2); + + macsmc_hwmon_fans_stop(&t->hwmon); + fan_test_released(test, t); + + macsmc_hwmon_fans_apply(&t->hwmon); + fan_test_released(test, t); + KUNIT_EXPECT_EQ(test, t->ftst_writes, 2); +} + +/* A failed unlock leaves the fan with the firmware. */ +static void macsmc_fan_test_cooling_unlock_fails(struct kunit *test) +{ + struct macsmc_fan_test *t = test->priv; + + t->fail_key = SMC_KEY(Ftst); + t->fans[0].cool_state = MACSMC_FAN_COOLING_STATES; + macsmc_hwmon_fans_apply(&t->hwmon); + KUNIT_EXPECT_EQ(test, t->mode[0], MACSMC_FAN_MODE_SYSTEM); + KUNIT_EXPECT_EQ(test, t->hwmon.fan.manual, 0); + KUNIT_EXPECT_FALSE(test, t->hwmon.fan.unlocked); +} + static struct kunit_case macsmc_fan_test_cases[] = { KUNIT_CASE(macsmc_fan_test_unlock_and_release), KUNIT_CASE(macsmc_fan_test_clamp), @@ -333,6 +430,11 @@ static struct kunit_case macsmc_fan_test_cases[] = { KUNIT_CASE(macsmc_fan_test_two_fans), KUNIT_CASE(macsmc_fan_test_no_unlock_key), KUNIT_CASE(macsmc_fan_test_legacy), + KUNIT_CASE(macsmc_fan_test_state_rpm), + KUNIT_CASE(macsmc_fan_test_cooling), + KUNIT_CASE(macsmc_fan_test_cooling_and_user), + KUNIT_CASE(macsmc_fan_test_cooling_stopped), + KUNIT_CASE(macsmc_fan_test_cooling_unlock_fails), {} }; diff --git a/drivers/hwmon/macsmc-hwmon.c b/drivers/hwmon/macsmc-hwmon.c index 76f07add003d3..c0ca04e88d3d2 100644 --- a/drivers/hwmon/macsmc-hwmon.c +++ b/drivers/hwmon/macsmc-hwmon.c @@ -36,6 +36,7 @@ #include #include #include +#include #define MAX_LABEL_LENGTH 32 @@ -184,6 +185,13 @@ struct macsmc_hwmon_fan { char label[MAX_LABEL_LENGTH]; u32 attrs; bool manual; + /* Index of the fan node among the fan-* nodes: its cooling device id. */ + u32 index; + struct macsmc_hwmon *hwmon; + /* Speed asked for through the fan_control target, 0 for none. */ + long user_rpm; + /* Cooling state asked for by the thermal framework, 0 for none. */ + unsigned long cool_state; }; struct macsmc_hwmon_sensors { @@ -198,6 +206,13 @@ struct macsmc_hwmon_sensors { /* The firmware's own fan policy; it refuses a manual mode request. */ #define MACSMC_FAN_MODE_SYSTEM 3 +/* + * Cooling states of a fan with #cooling-cells: 0 leaves the fan to the + * firmware, 1 to MACSMC_FAN_COOLING_STATES run it at speeds spread evenly + * from its minimum to its maximum. + */ +#define MACSMC_FAN_COOLING_STATES 8 + /* How long the firmware gets to leave or re-enter its system mode */ #define MACSMC_FAN_MODE_TIMEOUT_MS 10000 #define MACSMC_FAN_MODE_POLL_MS 100 @@ -216,6 +231,10 @@ struct macsmc_hwmon_fans { bool unlocked; /* Fans in manual mode */ u32 manual; + /* Applies the cooling states, which can take seconds to unlock. */ + struct work_struct cool_work; + /* Suspended, shut down or going away: every fan stays with the SMC. */ + bool stopped; }; struct macsmc_hwmon { @@ -616,43 +635,52 @@ static int macsmc_hwmon_fan_manual(struct macsmc_hwmon *hwmon, return 0; } -static int macsmc_hwmon_set_fan(struct macsmc_hwmon *hwmon, int channel, long val) +/* The speed of a cooling state of a fan running from @min to @max. */ +static long macsmc_hwmon_fan_state_rpm(long min, long max, unsigned long state) { - struct macsmc_hwmon_fan *fan = &hwmon->fan.fans[channel]; - long min, max; - int ret; - - if (!fan_control || fan->mode.macsmc_key == 0) - return -EOPNOTSUPP; - if (val < 0) - return -EINVAL; + if (!state) + return 0; + state = min(state, MACSMC_FAN_COOLING_STATES); + return min + DIV_ROUND_CLOSEST((max - min) * (long)(state - 1), + MACSMC_FAN_COOLING_STATES - 1); +} - guard(mutex)(&hwmon->fan.lock); +/* + * Run a fan at the higher of the userspace target and the speed of its + * cooling state, or hand it to the firmware when neither asks for anything. + * Any failure leaves the fan with the firmware. Called with the fan lock held. + */ +static int macsmc_hwmon_fan_apply(struct macsmc_hwmon *hwmon, + struct macsmc_hwmon_fan *fan) +{ + unsigned long state = READ_ONCE(fan->cool_state); + long min, max, rpm; + int ret; - /* 0 hands the fan back to the firmware. */ - if (!val) + if (hwmon->fan.stopped || (!fan->user_rpm && !state)) return macsmc_hwmon_fan_auto(hwmon, fan); /* * The SMC does no sanity checks on requested fan speeds, so we need to. */ ret = macsmc_hwmon_read_key(hwmon->smc, &fan->min, 1, &min); - if (ret) - return ret; - - ret = macsmc_hwmon_read_key(hwmon->smc, &fan->max, 1, &max); - if (ret) + if (!ret) + ret = macsmc_hwmon_read_key(hwmon->smc, &fan->max, 1, &max); + if (!ret && (min <= 0 || max < min)) + ret = -EIO; + if (ret) { + macsmc_hwmon_fan_auto(hwmon, fan); return ret; + } - if (min <= 0 || max < min) - return -EIO; - val = clamp(val, min, max); + rpm = max(fan->user_rpm, macsmc_hwmon_fan_state_rpm(min, max, state)); + rpm = clamp(rpm, min, max); ret = macsmc_hwmon_fan_manual(hwmon, fan); if (ret) return ret; - ret = macsmc_hwmon_write_key(hwmon->smc, &fan->set, val); + ret = macsmc_hwmon_write_key(hwmon->smc, &fan->set, rpm); if (ret < 0) { macsmc_hwmon_fan_auto(hwmon, fan); return ret; @@ -661,6 +689,27 @@ static int macsmc_hwmon_set_fan(struct macsmc_hwmon *hwmon, int channel, long va return 0; } +static int macsmc_hwmon_set_fan(struct macsmc_hwmon *hwmon, int channel, long val) +{ + struct macsmc_hwmon_fan *fan = &hwmon->fan.fans[channel]; + int ret; + + if (!fan_control || fan->mode.macsmc_key == 0) + return -EOPNOTSUPP; + if (val < 0) + return -EINVAL; + + guard(mutex)(&hwmon->fan.lock); + + /* 0 drops the request; the fan goes back to the firmware unless cooling. */ + fan->user_rpm = val; + ret = macsmc_hwmon_fan_apply(hwmon, fan); + if (ret) + fan->user_rpm = 0; + + return ret; +} + static int macsmc_hwmon_write_fan(struct device *dev, u32 attr, int channel, long val) { @@ -677,11 +726,129 @@ static void macsmc_hwmon_fans_restore(struct macsmc_hwmon *hwmon) macsmc_hwmon_fan_auto(hwmon, &hwmon->fan.fans[i]); } +/* Bring every fan in line with its requests, unless stopped. */ +static void macsmc_hwmon_fans_apply(struct macsmc_hwmon *hwmon) +{ + u32 i; + int ret; + + guard(mutex)(&hwmon->fan.lock); + for (i = 0; i < hwmon->fan.count; i++) { + struct macsmc_hwmon_fan *fan = &hwmon->fan.fans[i]; + + ret = macsmc_hwmon_fan_apply(hwmon, fan); + if (ret) + dev_warn_ratelimited(hwmon->dev, + "%s left to the SMC: cannot run cooling state %lu (%d)\n", + fan->label, READ_ONCE(fan->cool_state), ret); + } +} + +static void macsmc_hwmon_fans_cool_work(struct work_struct *work) +{ + macsmc_hwmon_fans_apply(container_of(work, struct macsmc_hwmon, fan.cool_work)); +} + +/* + * Stop applying cooling states and hand every fan back to the firmware, for + * suspend, shutdown and removal. + */ +static void macsmc_hwmon_fans_stop(struct macsmc_hwmon *hwmon) +{ + scoped_guard(mutex, &hwmon->fan.lock) + hwmon->fan.stopped = true; + cancel_work_sync(&hwmon->fan.cool_work); + macsmc_hwmon_fans_restore(hwmon); +} + static void macsmc_hwmon_fans_restore_action(void *data) { - macsmc_hwmon_fans_restore(data); + macsmc_hwmon_fans_stop(data); } +#if IS_ENABLED(CONFIG_THERMAL_OF) +static int macsmc_hwmon_fan_get_max_state(struct thermal_cooling_device *cdev, + unsigned long *state) +{ + *state = MACSMC_FAN_COOLING_STATES; + return 0; +} + +static int macsmc_hwmon_fan_get_cur_state(struct thermal_cooling_device *cdev, + unsigned long *state) +{ + struct macsmc_hwmon_fan *fan = cdev->devdata; + + *state = READ_ONCE(fan->cool_state); + return 0; +} + +/* + * Unlocking the fans takes the firmware seconds, so the state is applied + * from a work item rather than from the thermal zone update. + */ +static int macsmc_hwmon_fan_set_cur_state(struct thermal_cooling_device *cdev, + unsigned long state) +{ + struct macsmc_hwmon_fan *fan = cdev->devdata; + + if (state > MACSMC_FAN_COOLING_STATES) + return -EINVAL; + + WRITE_ONCE(fan->cool_state, state); + queue_work(system_freezable_wq, &fan->hwmon->fan.cool_work); + return 0; +} + +static const struct thermal_cooling_device_ops macsmc_hwmon_fan_cooling_ops = { + .get_max_state = macsmc_hwmon_fan_get_max_state, + .get_cur_state = macsmc_hwmon_fan_get_cur_state, + .set_cur_state = macsmc_hwmon_fan_set_cur_state, +}; + +/* + * With #cooling-cells = <3> on the hwmon node, every fan is a cooling device + * for the thermal zones that map it as <&hwmon fan-index lower upper>, the + * index counting the fan-* nodes in order. Without it the fans stay with the + * firmware as before. + */ +static void macsmc_hwmon_fans_register_cooling(struct macsmc_hwmon *hwmon) +{ + const u32 needed = HWMON_F_MIN | HWMON_F_MAX | HWMON_F_TARGET; + u32 cells, i; + + if (!hwmon->fan.count || + of_property_read_u32(hwmon->dev->of_node, "#cooling-cells", &cells)) + return; + if (cells != 3) { + dev_warn(hwmon->dev, "#cooling-cells must be 3; fans left to the SMC\n"); + return; + } + + for (i = 0; i < hwmon->fan.count; i++) { + struct macsmc_hwmon_fan *fan = &hwmon->fan.fans[i]; + struct thermal_cooling_device *cdev; + + if ((fan->attrs & needed) != needed || !fan->mode.macsmc_key) { + dev_warn(hwmon->dev, "%s has no speed or mode keys; not a cooling device\n", + fan->label); + continue; + } + + cdev = devm_thermal_of_cooling_device_register(hwmon->dev, fan->index, + "macsmc-fan", fan, + &macsmc_hwmon_fan_cooling_ops); + if (IS_ERR(cdev)) + dev_warn(hwmon->dev, "%s: cannot register a cooling device (%pe)\n", + fan->label, cdev); + } +} +#else +static void macsmc_hwmon_fans_register_cooling(struct macsmc_hwmon *hwmon) +{ +} +#endif + static int macsmc_hwmon_read(struct device *dev, enum hwmon_sensor_types type, u32 attr, int channel, long *val) { @@ -1120,9 +1287,13 @@ static int macsmc_hwmon_populate_sensors(struct macsmc_hwmon *hwmon, if (!hwmon->fan.fans) return -ENOMEM; + n_fan = 0; for_each_child_of_node_with_prefix(hwmon_node, key_node, "fan-") { - if (!macsmc_hwmon_create_fan(hwmon->dev, hwmon->smc, key_node, - &hwmon->fan.fans[hwmon->fan.count])) + struct macsmc_hwmon_fan *fan = &hwmon->fan.fans[hwmon->fan.count]; + + fan->hwmon = hwmon; + fan->index = n_fan++; + if (!macsmc_hwmon_create_fan(hwmon->dev, hwmon->smc, key_node, fan)) hwmon->fan.count++; } } @@ -1513,6 +1684,7 @@ static int macsmc_hwmon_probe(struct platform_device *pdev) ret = devm_mutex_init(&pdev->dev, &hwmon->fan.lock); if (ret) return ret; + INIT_WORK(&hwmon->fan.cool_work, macsmc_hwmon_fans_cool_work); ret = macsmc_hwmon_populate_sensors(hwmon, hwmon->dev->of_node); if (ret) { @@ -1576,6 +1748,12 @@ static int macsmc_hwmon_probe(struct platform_device *pdev) return ret; } + /* + * Released before the restore action above, which stops the cooling + * work and hands the fans back. + */ + macsmc_hwmon_fans_register_cooling(hwmon); + dev_dbg(hwmon->dev, "Registered SMC hwmon device. Sensors:\n"); dev_dbg(hwmon->dev, "Current: %d, Fans: %d, Power: %d, Temperature: %d, Voltage: %d", @@ -1597,16 +1775,32 @@ static void macsmc_hwmon_shutdown(struct platform_device *pdev) struct macsmc_hwmon *hwmon = platform_get_drvdata(pdev); if (hwmon) - macsmc_hwmon_fans_restore(hwmon); + macsmc_hwmon_fans_stop(hwmon); } static int macsmc_hwmon_suspend(struct device *dev) { - macsmc_hwmon_fans_restore(dev_get_drvdata(dev)); + macsmc_hwmon_fans_stop(dev_get_drvdata(dev)); + return 0; +} + +/* Cooling states asked for before suspend apply again; the zones update them. */ +static int macsmc_hwmon_resume(struct device *dev) +{ + struct macsmc_hwmon *hwmon = dev_get_drvdata(dev); + + scoped_guard(mutex, &hwmon->fan.lock) { + hwmon->fan.stopped = false; + /* The userspace targets were dropped when the fans went back. */ + for (u32 i = 0; i < hwmon->fan.count; i++) + hwmon->fan.fans[i].user_rpm = 0; + } + queue_work(system_freezable_wq, &hwmon->fan.cool_work); return 0; } -static DEFINE_SIMPLE_DEV_PM_OPS(macsmc_hwmon_pm_ops, macsmc_hwmon_suspend, NULL); +static DEFINE_SIMPLE_DEV_PM_OPS(macsmc_hwmon_pm_ops, macsmc_hwmon_suspend, + macsmc_hwmon_resume); static struct platform_driver macsmc_hwmon_driver = { .probe = macsmc_hwmon_probe, From 478fc7de86c9e6a06898a5546f6bd05052e83fd2 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Thu, 29 Oct 2026 09:39:01 +1100 Subject: [PATCH 4/4] arm64: dts: apple: t6040-j616s: Run the fans from the die temperatures Make both J616s fans cooling devices of the CPU die zone and the seven GPU zones. Each zone gets a ladder of active trips: 75 C runs the fans at their minimum, 80 C, 85 C and 90 C step them up, and 95 C runs them at full speed. A fan takes the highest state any zone asks for and returns to the SMC once every zone is 10 C below the first trip. The first trip sits above the 70-75 C the CPU die touches for a moment during light desktop bursts, so the fans do not start for those. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/t6040-j616s.dts | 86 +++++++++++++++++++++-- 1 file changed, 80 insertions(+), 6 deletions(-) diff --git a/arch/arm64/boot/dts/apple/t6040-j616s.dts b/arch/arm64/boot/dts/apple/t6040-j616s.dts index 6c28c95f8155d..83b29a6c3f866 100644 --- a/arch/arm64/boot/dts/apple/t6040-j616s.dts +++ b/arch/arm64/boot/dts/apple/t6040-j616s.dts @@ -314,16 +314,50 @@ * failure of that loop; they never act in normal use. * * The GPU has no cooling device of its own: its firmware sets the GPU - * performance state and starts limiting at 101 C. The SMC runs the - * fans in its own mode (3), only at their minimum speed in every load - * measured under Linux, and refuses manual fan modes from the OS. So - * when a GPU die sensor reaches the GPU firmware's limit, the seven + * performance state and starts limiting at 101 C. So when a GPU die + * sensor reaches the GPU firmware's limit, the seven * hottest ones (Tg05, Tg0S, Tg0Y, Tg0k, Tg0z, Tg1V, Tg1l) also shed * CPU power through the CPU clusters, from the same 101 C, leaving the * shared heat budget to the GPU. They shut down at 118 C. While the * GPU is powered off its sensors report placeholder values from -4 to * 4 C. + * + * The fans: left to themselves (SMC mode 3, with no OS asking for + * cooling) they stay off, or switch on late and ramp slowly, while the + * dies heat up under load. So the CPU die zone and the GPU zones also + * run both fans through a ladder of active trips; a fan takes the + * highest state any zone asks for and goes back to the SMC once no + * zone asks for anything. States 1-8 of a fan run it from its minimum + * (1350 rpm) to its maximum (5777 rpm) in even steps. */ +#define J616S_FAN_TRIP(label, node, temp, hyst) \ + label: node { \ + temperature = ; \ + hysteresis = ; \ + type = "active"; \ + }; + +#define J616S_FAN_TRIPS(zone) \ + J616S_FAN_TRIP(zone##_fan1, trip-fan1, 75000, 10000) \ + J616S_FAN_TRIP(zone##_fan2, trip-fan2, 80000, 5000) \ + J616S_FAN_TRIP(zone##_fan3, trip-fan3, 85000, 5000) \ + J616S_FAN_TRIP(zone##_fan4, trip-fan4, 90000, 5000) \ + J616S_FAN_TRIP(zone##_fan5, trip-fan5, 95000, 5000) + +#define J616S_FAN_MAP(trip_label, node, state) \ + node { \ + trip = <&trip_label>; \ + cooling-device = <&smc_hwmon 0 state state>, \ + <&smc_hwmon 1 state state>; \ + }; + +#define J616S_FAN_MAPS(zone) \ + J616S_FAN_MAP(zone##_fan1, map-fan1, 1) \ + J616S_FAN_MAP(zone##_fan2, map-fan2, 2) \ + J616S_FAN_MAP(zone##_fan3, map-fan3, 3) \ + J616S_FAN_MAP(zone##_fan4, map-fan4, 5) \ + J616S_FAN_MAP(zone##_fan5, map-fan5, 8) + thermal-zones { cpu-die-thermal { polling-delay-passive = <1000>; @@ -331,6 +365,8 @@ thermal-sensors = <&smc_hwmon 0>; /* TCMz */ trips { + J616S_FAN_TRIPS(cpu_die) + cpu_die_passive: cpu-die-passive { temperature = <113000>; hysteresis = <5000>; @@ -345,6 +381,8 @@ }; cooling-maps { + J616S_FAN_MAPS(cpu_die) + map-e { trip = <&cpu_die_passive>; cooling-device = @@ -890,6 +928,8 @@ thermal-sensors = <&smc_hwmon 17>; /* Tg05 */ trips { + J616S_FAN_TRIPS(gpu0) + gpu0_passive: trip-passive { temperature = <101000>; hysteresis = <5000>; @@ -904,6 +944,8 @@ }; cooling-maps { + J616S_FAN_MAPS(gpu0) + map-e { trip = <&gpu0_passive>; cooling-device = @@ -941,6 +983,8 @@ thermal-sensors = <&smc_hwmon 18>; /* Tg0S */ trips { + J616S_FAN_TRIPS(gpu1) + gpu1_passive: trip-passive { temperature = <101000>; hysteresis = <5000>; @@ -955,6 +999,8 @@ }; cooling-maps { + J616S_FAN_MAPS(gpu1) + map-e { trip = <&gpu1_passive>; cooling-device = @@ -992,6 +1038,8 @@ thermal-sensors = <&smc_hwmon 19>; /* Tg0Y */ trips { + J616S_FAN_TRIPS(gpu2) + gpu2_passive: trip-passive { temperature = <101000>; hysteresis = <5000>; @@ -1006,6 +1054,8 @@ }; cooling-maps { + J616S_FAN_MAPS(gpu2) + map-e { trip = <&gpu2_passive>; cooling-device = @@ -1043,6 +1093,8 @@ thermal-sensors = <&smc_hwmon 20>; /* Tg0k */ trips { + J616S_FAN_TRIPS(gpu3) + gpu3_passive: trip-passive { temperature = <101000>; hysteresis = <5000>; @@ -1057,6 +1109,8 @@ }; cooling-maps { + J616S_FAN_MAPS(gpu3) + map-e { trip = <&gpu3_passive>; cooling-device = @@ -1094,6 +1148,8 @@ thermal-sensors = <&smc_hwmon 21>; /* Tg0z */ trips { + J616S_FAN_TRIPS(gpu4) + gpu4_passive: trip-passive { temperature = <101000>; hysteresis = <5000>; @@ -1108,6 +1164,8 @@ }; cooling-maps { + J616S_FAN_MAPS(gpu4) + map-e { trip = <&gpu4_passive>; cooling-device = @@ -1145,6 +1203,8 @@ thermal-sensors = <&smc_hwmon 22>; /* Tg1V */ trips { + J616S_FAN_TRIPS(gpu5) + gpu5_passive: trip-passive { temperature = <101000>; hysteresis = <5000>; @@ -1159,6 +1219,8 @@ }; cooling-maps { + J616S_FAN_MAPS(gpu5) + map-e { trip = <&gpu5_passive>; cooling-device = @@ -1196,6 +1258,8 @@ thermal-sensors = <&smc_hwmon 23>; /* Tg1l */ trips { + J616S_FAN_TRIPS(gpu6) + gpu6_passive: trip-passive { temperature = <101000>; hysteresis = <5000>; @@ -1210,6 +1274,8 @@ }; cooling-maps { + J616S_FAN_MAPS(gpu6) + map-e { trip = <&gpu6_passive>; cooling-device = @@ -3789,13 +3855,21 @@ * Fan speeds, limits, targets and modes, and the common SMC sensors; every * key exists on this machine's SMC. Both fans list their target (F0Tg, * F1Tg) and mode (F0Md, F1Md) keys, so with the driver's fan_control - * parameter fanN_target becomes writable; by default the SMC drives the - * fans and the attributes are read-only. + * parameter fanN_target becomes writable; by default the attributes are + * read-only, and the thermal zones above drive the fans. */ #include "hwmon-common.dtsi" #include "hwmon-fan-dual.dtsi" #include "hwmon-laptop.dtsi" +/* + * Both fans are cooling devices of the CPU die and GPU zones, as + * <&smc_hwmon fan-index lower upper>. + */ +&smc_hwmon { + #cooling-cells = <3>; +}; + &pcie_dart { status = "okay"; };