mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Jordan Brough <jordan@brough.org>
To: Guenter Roeck <linux@roeck-us.net>, Henrik Rydberg <rydberg@bitmath.org>
Cc: Jordan Brough <jordan@brough.org>,
	Michal Szpakowski <michi.szpakowski@gmail.com>,
	linux-hwmon@vger.kernel.org, linux-kernel@vger.kernel.org,
	"Rafael J. Wysocki" <rafael@kernel.org>
Subject: [PATCH v3 3/3] hwmon: (applesmc) add charge_control_end_threshold support
Date: Wed, 30 Sep 2026 16:26:35 -0600	[thread overview]
Message-ID: <20260930222650.1883805-4-jordan@brough.org> (raw)
In-Reply-To: <20260930222650.1883805-1-jordan@brough.org>

Intel MacBooks keep the battery charge limit in the SMC key BCLM, and
some also have BFCL, which sets the threshold for the MagSafe LED.

Expose BCLM as charge_control_end_threshold through the power_supply
extension API, attached to the battery with the ACPI battery hooks.

Below 100%, BFCL is set 3 points below BCLM so that the LED turns green
before charging stops. On a MacBookAir6,2 the LED turns green about 0.75
points above BFCL and charging stops about 0.25 points below BCLM, so
this leaves about 2 points; other models were not measured. BFCL is only
written if the SMC has the key, and a failure to write it is only logged.

The SMC ignores BCLM values it does not accept but still reports
success, so read the value back and return -EINVAL if it was not applied.

Limits below 20 are rejected. 20 is known to work and 10 is not accepted
by the SMC on the hardware tested; the exact floor in between was not
determined.

Serialize the writes with applesmc_bat_lock, and notify userspace of
changes with power_supply_changed().

Tested-by: Michal Szpakowski <michi.szpakowski@gmail.com>
Signed-off-by: Jordan Brough <jordan@brough.org>
---
 drivers/hwmon/Kconfig    |   1 +
 drivers/hwmon/applesmc.c | 146 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 147 insertions(+)

diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig
index fecff8610ea8..d627b4cf11e5 100644
--- a/drivers/hwmon/Kconfig
+++ b/drivers/hwmon/Kconfig
@@ -380,6 +380,7 @@ config SENSORS_FAM15H_POWER
 config SENSORS_APPLESMC
 	tristate "Apple SMC (Motion sensor, light sensor, keyboard backlight)"
 	depends on INPUT && X86
+	depends on POWER_SUPPLY || POWER_SUPPLY=n
 	select NEW_LEDS
 	select LEDS_CLASS
 	help
diff --git a/drivers/hwmon/applesmc.c b/drivers/hwmon/applesmc.c
index d0baa10502f7..b6d9ca302730 100644
--- a/drivers/hwmon/applesmc.c
+++ b/drivers/hwmon/applesmc.c
@@ -33,6 +33,8 @@
 #include <linux/workqueue.h>
 #include <linux/err.h>
 #include <linux/bits.h>
+#include <linux/power_supply.h>
+#include <acpi/battery.h>
 #include <asm/barrier.h>
 
 /* data port used by Apple SMC */
@@ -76,6 +78,20 @@
 
 #define TEMP_SENSOR_TYPE	"sp78"
 
+/*
+ * BCLM caps charging at a percentage. BFCL only sets when the charging LED
+ * switches from orange to green.
+ */
+#define BATTERY_CHARGE_LIMIT_KEY	"BCLM" /* r/w ui8 */
+#define BATTERY_CHARGE_LIMIT_LED_KEY	"BFCL" /* r/w ui8 */
+
+/*
+ * Points kept between BCLM and BFCL so the LED turns green before charging
+ * stops. Measured on a MacBookAir6,2, where a margin of 1 only just ties;
+ * 3 leaves headroom for other models.
+ */
+#define APPLESMC_BATTERY_CHARGE_LIMIT_LED_MARGIN 3
+
 /* List of keys used to read/write fan speeds */
 static const char *const fan_speed_fmt[] = {
 	"F%dAc",		/* actual speed */
@@ -131,6 +147,8 @@ static struct applesmc_registers {
 	int num_light_sensors;		/* number of light sensors */
 	bool has_accelerometer;		/* has motion sensor */
 	bool has_key_backlight;		/* has keyboard backlight */
+	bool has_battery_charge_limit;	/* has BCLM battery charge limit */
+	bool has_battery_charge_limit_led; /* has BFCL MagSafe LED charge limit */
 	bool init_complete;		/* true when fully initialized */
 	struct applesmc_entry *cache;	/* cached key entries */
 	const char **index;		/* temperature key index */
@@ -633,6 +651,12 @@ static int applesmc_init_smcreg_try(void)
 	if (ret)
 		return ret;
 	ret = applesmc_has_key(BACKLIGHT_KEY, &s->has_key_backlight);
+	if (ret)
+		return ret;
+	ret = applesmc_has_key(BATTERY_CHARGE_LIMIT_KEY, &s->has_battery_charge_limit);
+	if (ret)
+		return ret;
+	ret = applesmc_has_key(BATTERY_CHARGE_LIMIT_LED_KEY, &s->has_battery_charge_limit_led);
 	if (ret)
 		return ret;
 
@@ -724,6 +748,120 @@ static struct platform_driver applesmc_driver = {
 	},
 };
 
+static DEFINE_MUTEX(applesmc_bat_lock);
+
+static int applesmc_bat_get_property(struct power_supply *psy,
+				     const struct power_supply_ext *ext,
+				     void *data,
+				     enum power_supply_property psp,
+				     union power_supply_propval *val)
+{
+	u8 limit;
+	int ret;
+
+	switch (psp) {
+	case POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD:
+		scoped_guard(mutex, &applesmc_bat_lock)
+			ret = applesmc_read_key(BATTERY_CHARGE_LIMIT_KEY, &limit, 1);
+		if (ret)
+			return ret;
+		val->intval = limit;
+		return 0;
+	default:
+		return -EINVAL;
+	}
+}
+
+static int applesmc_bat_set_property(struct power_supply *psy,
+				     const struct power_supply_ext *ext,
+				     void *data,
+				     enum power_supply_property psp,
+				     const union power_supply_propval *val)
+{
+	u8 limit, led_limit, readback;
+	int ret;
+
+	switch (psp) {
+	case POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD:
+		/* 20 is known to work and 10 is not accepted; the floor is unknown. */
+		if (val->intval < 20 || val->intval > 100)
+			return -EINVAL;
+
+		limit = val->intval;
+		/* At 100% charging is never cut short, so no margin is needed. */
+		led_limit = limit < 100 ?
+			limit - APPLESMC_BATTERY_CHARGE_LIMIT_LED_MARGIN : limit;
+
+		scoped_guard(mutex, &applesmc_bat_lock) {
+			ret = applesmc_write_key(BATTERY_CHARGE_LIMIT_KEY, &limit, 1);
+			if (ret)
+				return ret;
+
+			/*
+			 * The SMC silently ignores values it does not accept and
+			 * still reports success, so read the limit back.
+			 */
+			ret = applesmc_read_key(BATTERY_CHARGE_LIMIT_KEY, &readback, 1);
+			if (ret)
+				return ret;
+			if (readback != limit)
+				return -EINVAL;
+
+			if (smcreg.has_battery_charge_limit_led) {
+				ret = applesmc_write_key(BATTERY_CHARGE_LIMIT_LED_KEY,
+							 &led_limit, 1);
+				/* BCLM is already applied, so this is not an error. */
+				if (ret)
+					pr_warn("failed to update MagSafe LED threshold (BFCL): %d\n",
+						ret);
+			}
+		}
+
+		power_supply_changed(psy);
+		return 0;
+	default:
+		return -EINVAL;
+	}
+}
+
+static int applesmc_bat_property_is_writeable(struct power_supply *psy,
+					      const struct power_supply_ext *ext,
+					      void *data,
+					      enum power_supply_property psp)
+{
+	return psp == POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD;
+}
+
+static const enum power_supply_property applesmc_bat_props[] = {
+	POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD,
+};
+
+static const struct power_supply_ext applesmc_bat_ext = {
+	.name			= "applesmc-charge-control",
+	.properties		= applesmc_bat_props,
+	.num_properties		= ARRAY_SIZE(applesmc_bat_props),
+	.get_property		= applesmc_bat_get_property,
+	.set_property		= applesmc_bat_set_property,
+	.property_is_writeable	= applesmc_bat_property_is_writeable,
+};
+
+static int applesmc_bat_add(struct power_supply *battery, struct acpi_battery_hook *hook)
+{
+	return power_supply_register_extension(battery, &applesmc_bat_ext, &pdev->dev, NULL);
+}
+
+static int applesmc_bat_remove(struct power_supply *battery, struct acpi_battery_hook *hook)
+{
+	power_supply_unregister_extension(battery, &applesmc_bat_ext);
+	return 0;
+}
+
+static struct acpi_battery_hook applesmc_bat_hook = {
+	.name = "Apple SMC Battery Charge Control",
+	.add_battery = applesmc_bat_add,
+	.remove_battery = applesmc_bat_remove,
+};
+
 /*
  * applesmc_calibrate - Set our "resting" values.  Callers must
  * hold applesmc_lock.
@@ -1563,6 +1701,10 @@ static int __init applesmc_init(void)
 		goto out_light_ledclass;
 	}
 
+	if (smcreg.has_battery_charge_limit &&
+	    IS_REACHABLE(CONFIG_ACPI_BATTERY_HOOKS))
+		acpi_battery_hook_register(&applesmc_bat_hook);
+
 	return 0;
 
 out_light_ledclass:
@@ -1589,6 +1731,10 @@ static int __init applesmc_init(void)
 
 static void __exit applesmc_exit(void)
 {
+	if (smcreg.has_battery_charge_limit &&
+	    IS_REACHABLE(CONFIG_ACPI_BATTERY_HOOKS))
+		acpi_battery_hook_unregister(&applesmc_bat_hook);
+
 	hwmon_device_unregister(hwmon_dev);
 	applesmc_release_key_backlight();
 	applesmc_release_light_sensor();
-- 
2.56.0


      parent reply	other threads:[~2026-09-30 22:27 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-30 22:26 [PATCH v3 0/3] " Jordan Brough
2026-09-30 22:26 ` [PATCH v3 1/3] ACPI: battery: add acpi_ prefix to the battery hook API Jordan Brough
2026-09-30 23:19   ` Armin Wolf
2026-10-01  0:21   ` Jonathan Woithe
2026-10-01  1:14   ` Derek J. Clark
2026-10-01 13:19   ` Mark Pearson
2026-09-30 22:26 ` [PATCH v3 2/3] ACPI: battery: add unified battery hook mechanism for ACPI and SBS batteries Jordan Brough
2026-09-30 22:26 ` Jordan Brough [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260930222650.1883805-4-jordan@brough.org \
    --to=jordan@brough.org \
    --cc=linux-hwmon@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux@roeck-us.net \
    --cc=michi.szpakowski@gmail.com \
    --cc=rafael@kernel.org \
    --cc=rydberg@bitmath.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®