* [PATCH] hwmon: (applesmc) Expose the SMC battery charge limit
@ 2026-09-23 14:41 Michal Szpakowski
2026-09-24 7:58 ` Lukas Wunner
0 siblings, 1 reply; 3+ messages in thread
From: Michal Szpakowski @ 2026-09-23 14:41 UTC (permalink / raw)
To: linux-hwmon
Cc: Henrik Rydberg, Guenter Roeck, Jean Delvare, linux-acpi, linux-kernel
Intel Macs keep a battery charge limit inside the SMC, in the key BCLM
(one byte, percent). The SMC enforces it on its own: charging stops at
the limit and the value survives reboots and operating systems, so the
only thing an OS has to do is write it. On macOS the tool "bclm" does
that; on Linux there has been no way, because applesmc exposes keys
read-only through key_at_index.
Expose it as the standard charge_control_end_threshold property on the
battery through a power supply extension, so UPower and the desktops
that read that property pick it up without knowing anything about
Apple. The battery of an Intel Mac is an ACPI Smart Battery handled by
the sbs driver, which the ACPI battery hooks do not cover, so the
supply is found by walking the registered supplies; should it register
after applesmc, its first property-change notification attaches the
extension. Nothing happens on machines whose SMC lacks the key. Writes
are read back, because the SMC silently drops values it does not
accept; 100 disables the cap.
Tested on a MacBookPro13,1 (2016, 13", no Touch Bar): with the limit at
80 and the charger attached, charging ran at a steady 1.55 A and stopped
at 79% of charge_full with status "Full" and current 0, and the limit
read back 80 after a reboot.
Signed-off-by: Michal Szpakowski <michi.szpakowski@gmail.com>
---
drivers/hwmon/Kconfig | 4 +++-
drivers/hwmon/applesmc.c | 134 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 137 insertions(+), 1 deletion(-)
--- a/drivers/hwmon/Kconfig
+++ b/drivers/hwmon/Kconfig
@@ -368,13 +368,15 @@
config SENSORS_APPLESMC
tristate "Apple SMC (Motion sensor, light sensor, keyboard backlight)"
depends on INPUT && X86
+ depends on POWER_SUPPLY
select NEW_LEDS
select LEDS_CLASS
help
This driver provides support for the Apple System Management
Controller, which provides an accelerometer (Apple Sudden Motion
Sensor), light sensors, temperature sensors, keyboard backlight
- control and fan control.
+ control, fan control and, on machines whose SMC has the BCLM key,
+ a battery charge limit exposed as charge_control_end_threshold.
Only Intel-based Apple's computers are supported (MacBook Pro,
MacBook, MacMini).
--- a/drivers/hwmon/applesmc.c
+++ b/drivers/hwmon/applesmc.c
@@ -33,6 +33,7 @@
#include <linux/workqueue.h>
#include <linux/err.h>
#include <linux/bits.h>
+#include <linux/power_supply.h>
/* data port used by Apple SMC */
#define APPLESMC_DATA_PORT 0x300
@@ -64,6 +65,8 @@
#define CLAMSHELL_KEY "MSLD" /* r-o ui8 (unused) */
+#define CHARGE_LIMIT_KEY "BCLM" /* r/w ui8, percent */
+
#define MOTION_SENSOR_X_KEY "MO_X" /* r-o sp78 (2 bytes) */
#define MOTION_SENSOR_Y_KEY "MO_Y" /* r-o sp78 (2 bytes) */
#define MOTION_SENSOR_Z_KEY "MO_Z" /* r-o sp78 (2 bytes) */
@@ -130,6 +133,7 @@
int num_light_sensors; /* number of light sensors */
bool has_accelerometer; /* has motion sensor */
bool has_key_backlight; /* has keyboard backlight */
+ bool has_charge_limit; /* has battery charge limit */
bool init_complete; /* true when fully initialized */
struct applesmc_entry *cache; /* cached key entries */
const char **index; /* temperature key index */
@@ -621,6 +625,9 @@
ret = applesmc_has_key(BACKLIGHT_KEY, &s->has_key_backlight);
if (ret)
return ret;
+ ret = applesmc_has_key(CHARGE_LIMIT_KEY, &s->has_charge_limit);
+ if (ret)
+ return ret;
s->num_light_sensors = left_light_sensor + right_light_sensor;
s->init_complete = true;
@@ -669,6 +676,155 @@
}
/* Device model stuff */
+/*
+ * Battery charge limit
+ *
+ * The SMC key BCLM holds the maximum charge level in percent and the SMC
+ * enforces it by itself: charging stops there and the value is kept across
+ * reboots; 100 means no limit. It is the setting the macOS tool "bclm"
+ * writes. Expose it as charge_control_end_threshold on the battery through
+ * a power supply extension. The battery of an Intel Mac is an ACPI Smart
+ * Battery (sbs), which the ACPI battery hooks do not cover, so the battery
+ * is found by walking the registered supplies; if it is not there yet when
+ * this driver loads, the first property-change notification from it does
+ * the job.
+ */
+static const enum power_supply_property applesmc_battery_props[] = {
+ POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD,
+};
+
+static int applesmc_battery_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;
+
+ if (psp != POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD)
+ return -EINVAL;
+
+ ret = applesmc_read_key(CHARGE_LIMIT_KEY, &limit, 1);
+ if (ret)
+ return ret;
+
+ val->intval = limit;
+ return 0;
+}
+
+static int applesmc_battery_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, readback;
+ int ret;
+
+ if (psp != POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD)
+ return -EINVAL;
+ if (val->intval < 1 || val->intval > 100)
+ return -EINVAL;
+
+ limit = val->intval;
+ ret = applesmc_write_key(CHARGE_LIMIT_KEY, &limit, 1);
+ if (ret)
+ return ret;
+
+ /* The SMC silently ignores values it does not accept. */
+ ret = applesmc_read_key(CHARGE_LIMIT_KEY, &readback, 1);
+ if (ret)
+ return ret;
+ if (readback != limit)
+ return -EINVAL;
+
+ return 0;
+}
+
+static int applesmc_battery_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 struct power_supply_ext applesmc_battery_ext = {
+ .name = "applesmc-charge-limit",
+ .properties = applesmc_battery_props,
+ .num_properties = ARRAY_SIZE(applesmc_battery_props),
+ .get_property = applesmc_battery_get_property,
+ .set_property = applesmc_battery_set_property,
+ .property_is_writeable = applesmc_battery_property_is_writeable,
+};
+
+static struct power_supply *applesmc_battery; /* the extended supply */
+static DEFINE_MUTEX(applesmc_battery_mutex);
+
+static int applesmc_battery_extend(struct power_supply *psy, void *data)
+{
+ int ret;
+
+ if (psy->desc->type != POWER_SUPPLY_TYPE_BATTERY)
+ return 0;
+
+ ret = power_supply_register_extension(psy, &applesmc_battery_ext,
+ &pdev->dev, NULL);
+ if (ret)
+ return ret;
+
+ get_device(&psy->dev);
+ applesmc_battery = psy;
+ return 1; /* one battery is enough, stop walking */
+}
+
+static void applesmc_battery_attach(struct work_struct *work)
+{
+ mutex_lock(&applesmc_battery_mutex);
+ if (!applesmc_battery)
+ power_supply_for_each_psy(NULL, applesmc_battery_extend);
+ mutex_unlock(&applesmc_battery_mutex);
+}
+
+static DECLARE_WORK(applesmc_battery_work, applesmc_battery_attach);
+
+static int applesmc_battery_notify(struct notifier_block *nb,
+ unsigned long event, void *data)
+{
+ if (event == PSY_EVENT_PROP_CHANGED && !applesmc_battery)
+ schedule_work(&applesmc_battery_work);
+ return NOTIFY_OK;
+}
+
+static struct notifier_block applesmc_battery_nb = {
+ .notifier_call = applesmc_battery_notify,
+};
+
+static void applesmc_battery_init(void)
+{
+ if (!smcreg.has_charge_limit)
+ return;
+ power_supply_reg_notifier(&applesmc_battery_nb);
+ applesmc_battery_attach(NULL);
+}
+
+static void applesmc_battery_exit(void)
+{
+ if (!smcreg.has_charge_limit)
+ return;
+ power_supply_unreg_notifier(&applesmc_battery_nb);
+ cancel_work_sync(&applesmc_battery_work);
+ mutex_lock(&applesmc_battery_mutex);
+ if (applesmc_battery) {
+ power_supply_unregister_extension(applesmc_battery,
+ &applesmc_battery_ext);
+ power_supply_put(applesmc_battery);
+ applesmc_battery = NULL;
+ }
+ mutex_unlock(&applesmc_battery_mutex);
+}
+
static int applesmc_probe(struct platform_device *dev)
{
int ret;
@@ -1369,6 +1525,8 @@
goto out_light_ledclass;
}
+ applesmc_battery_init();
+
return 0;
out_light_ledclass:
@@ -1398,6 +1556,7 @@
static void __exit applesmc_exit(void)
{
+ applesmc_battery_exit();
hwmon_device_unregister(hwmon_dev);
applesmc_release_key_backlight();
applesmc_release_light_sensor();
^ permalink raw reply [flat|nested] 3+ messages in thread* Re: [PATCH] hwmon: (applesmc) Expose the SMC battery charge limit
2026-09-23 14:41 [PATCH] hwmon: (applesmc) Expose the SMC battery charge limit Michal Szpakowski
@ 2026-09-24 7:58 ` Lukas Wunner
2026-09-24 9:45 ` Michal Szpakowski
0 siblings, 1 reply; 3+ messages in thread
From: Lukas Wunner @ 2026-09-24 7:58 UTC (permalink / raw)
To: Michal Szpakowski, Jordan Brough
Cc: linux-hwmon, Henrik Rydberg, Guenter Roeck, Jean Delvare,
linux-acpi, linux-kernel
[+cc Jordan]
On Wed, Sep 23, 2026 at 04:41:17PM +0200, Michal Szpakowski wrote:
> Intel Macs keep a battery charge limit inside the SMC, in the key BCLM
> (one byte, percent). The SMC enforces it on its own: charging stops at
> the limit and the value survives reboots and operating systems, so the
> only thing an OS has to do is write it. On macOS the tool "bclm" does
> that; on Linux there has been no way, because applesmc exposes keys
> read-only through key_at_index.
>
> Expose it as the standard charge_control_end_threshold property on the
> battery through a power supply extension, so UPower and the desktops
> that read that property pick it up without knowing anything about
> Apple. The battery of an Intel Mac is an ACPI Smart Battery handled by
> the sbs driver, which the ACPI battery hooks do not cover, so the
> supply is found by walking the registered supplies; should it register
> after applesmc, its first property-change notification attaches the
> extension. Nothing happens on machines whose SMC lacks the key. Writes
> are read back, because the SMC silently drops values it does not
> accept; 100 disables the cap.
>
> Tested on a MacBookPro13,1 (2016, 13", no Touch Bar): with the limit at
> 80 and the charger attached, charging ran at a steady 1.55 A and stopped
> at 79% of charge_full with status "Full" and current 0, and the limit
> read back 80 after a reboot.
Jordan Brough is working on similar patches, perhaps you two can
work together to make sure there's no breakage on either of your
laptops an no conflict between your patches:
https://lore.kernel.org/r/20260918175052.85461-1-jordan@brough.org
Thanks,
Lukas
^ permalink raw reply [flat|nested] 3+ messages in thread
* Re: [PATCH] hwmon: (applesmc) Expose the SMC battery charge limit
2026-09-24 7:58 ` Lukas Wunner
@ 2026-09-24 9:45 ` Michal Szpakowski
0 siblings, 0 replies; 3+ messages in thread
From: Michal Szpakowski @ 2026-09-24 9:45 UTC (permalink / raw)
To: Lukas Wunner, Jordan Brough
Cc: linux-hwmon, Guenter Roeck, Jean Delvare, Henrik Rydberg,
linux-acpi, linux-kernel
On Thu, Sep 24, 2026 at 09:58:10AM +0200, Lukas Wunner wrote:
> Jordan Brough is working on similar patches, perhaps you two can
> work together to make sure there's no breakage on either of your
> laptops an no conflict between your patches:
>
> https://lore.kernel.org/r/20260918175052.85461-1-jordan@brough.org
Thanks, I had missed that series. Jordan's approach is the better one:
extending the battery hooks to SBS batteries is the proper fix for the
problem I worked around by walking the supplies, and the BFCL/LED
handling covers hardware I do not have. Please consider my patch
withdrawn in favour of his; I am happy to test v3 and add a Tested-by.
Two things from a MacBookPro13,1 (2016, 13", USB-C, no MagSafe) that
may be useful for it, Jordan:
1. This SMC has no BFCL key at all. A dump of all 798 keys has BCLM
but no BFCL (the BF* keys here are BFCT, BFLO and BFWC), and asking
the driver directly, through a debug attribute that calls
applesmc_get_entry_by_key():
BFCL (no such key)
BCLM type=ui8 len=1 flags=0xd0 R W
With v2's set_property, the unconditional applesmc_write_key(BFCL)
after the BCLM write therefore fails with -EINVAL, so userspace gets
an error although the limit was in fact applied. Gating the LED
write on applesmc_has_key(BFCL) at init, like the BCLM check, would
fix that.
2. The SMC silently drops writes it does not like: applesmc_write_key()
returns 0 and the key keeps its old value (seen on this machine with
the CLWK key; BCLM accepted everything I tried between 50 and 100).
Reading BCLM back after the write and returning -EINVAL on a
mismatch costs one read and makes the failure visible.
For what it is worth, BCLM=80 on this machine stops charging at 79% of
charge_full with status "Full", the value survives reboots, and UPower
1.91 picks the property up as charge-end-threshold once it re-reads the
battery.
Michal
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-09-24 9:46 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-23 14:41 [PATCH] hwmon: (applesmc) Expose the SMC battery charge limit Michal Szpakowski
2026-09-24 7:58 ` Lukas Wunner
2026-09-24 9:45 ` Michal Szpakowski
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®