Sat Apr 27 14:50:18 2024 UTC (35d)
Expose a sysctl interface hw.acpi.thinkpad<M>.bat[<N>].<behavior> to control
some aspects of battery charging behavior on supported systems:

    charge_start
	threshold below which to start charging (in %, 0-99)

    charge_stop
	threshold above which to stop charging (in %, 1-100)

    force_discharge
	discharge while on AC power, e.g., for calibration

    charge_inhibit
	inhibit charging while on AC power

From Malte Dehling


(christos)
diff -r1.56 -r1.57 src/sys/dev/acpi/thinkpad_acpi.c

cvs diff -r1.56 -r1.57 src/sys/dev/acpi/thinkpad_acpi.c (expand / switch to context diff)
--- src/sys/dev/acpi/thinkpad_acpi.c 2024/04/27 14:45:11 1.56
+++ src/sys/dev/acpi/thinkpad_acpi.c 2024/04/27 14:50:18 1.57
@@ -1,4 +1,4 @@
-/* $NetBSD: thinkpad_acpi.c,v 1.56 2024/04/27 14:45:11 christos Exp $ */
+/* $NetBSD: thinkpad_acpi.c,v 1.57 2024/04/27 14:50:18 christos Exp $ */
 
 /*-
  * Copyright (c) 2007 Jared D. McNeill <jmcneill@invisible.ca>
@@ -27,13 +27,14 @@
  */
 
 #include <sys/cdefs.h>
-__KERNEL_RCSID(0, "$NetBSD: thinkpad_acpi.c,v 1.56 2024/04/27 14:45:11 christos Exp $");
+__KERNEL_RCSID(0, "$NetBSD: thinkpad_acpi.c,v 1.57 2024/04/27 14:50:18 christos Exp $");
 
 #include <sys/param.h>
 #include <sys/device.h>
 #include <sys/module.h>
 #include <sys/sdt.h>
 #include <sys/systm.h>
+#include <sys/sysctl.h>
 
 #include <dev/acpi/acpireg.h>
 #include <dev/acpi/acpivar.h>
@@ -49,10 +50,27 @@
 #define	THINKPAD_NFANSENSORS	1
 #define	THINKPAD_NSENSORS	(THINKPAD_NTEMPSENSORS + THINKPAD_NFANSENSORS)
 
+typedef struct tp_sysctl_param {
+	device_t		sp_dev;
+	int			sp_bat;
+} tp_sysctl_param_t;
+
+typedef union tp_batctl {
+	int			have_any;
+	struct {
+	    int			charge_start:1;
+	    int			charge_stop:1;
+	    int			charge_inhibit:1;
+	    int			force_discharge:1;
+	    int			individual_control:1;
+	}			have;
+} tp_batctl_t;
+
 typedef struct thinkpad_softc {
 	device_t		sc_dev;
 	device_t		sc_ecdev;
 	struct acpi_devnode	*sc_node;
+	struct sysctllog	*sc_log;
 	ACPI_HANDLE		sc_powhdl;
 	ACPI_HANDLE		sc_cmoshdl;
 	ACPI_INTEGER		sc_ver;
@@ -90,6 +108,14 @@
 	envsys_data_t		sc_sensor[THINKPAD_NSENSORS];
 
 	int			sc_display_state;
+
+#define THINKPAD_BAT_ANY	0
+#define THINKPAD_BAT_PRIMARY	1
+#define THINKPAD_BAT_SECONDARY	2
+#define THINKPAD_BAT_LAST	3
+
+	tp_batctl_t		sc_batctl;
+	tp_sysctl_param_t	sc_scparam[THINKPAD_BAT_LAST];
 } thinkpad_softc_t;
 
 /* Hotkey events */
@@ -130,6 +156,17 @@
 #define	THINKPAD_DISPLAY_ALL \
 	(THINKPAD_DISPLAY_LCD | THINKPAD_DISPLAY_CRT | THINKPAD_DISPLAY_DVI)
 
+#define THINKPAD_GET_CHARGE_START	"BCTG"
+#define THINKPAD_SET_CHARGE_START	"BCCS"
+#define THINKPAD_GET_CHARGE_STOP	"BCSG"
+#define THINKPAD_SET_CHARGE_STOP	"BCSS"
+#define THINKPAD_GET_FORCE_DISCHARGE	"BDSG"
+#define THINKPAD_SET_FORCE_DISCHARGE	"BDSS"
+#define THINKPAD_GET_CHARGE_INHIBIT	"BICG"
+#define THINKPAD_SET_CHARGE_INHIBIT	"BICS"
+
+#define THINKPAD_CALL_ERROR		0x80000000
+
 #define THINKPAD_BLUETOOTH_HWPRESENT	0x01
 #define THINKPAD_BLUETOOTH_RADIOSSW	0x02
 #define THINKPAD_BLUETOOTH_RESUMECTRL	0x04
@@ -168,6 +205,9 @@
 static uint8_t	thinkpad_brightness_read(thinkpad_softc_t *);
 static void	thinkpad_cmos(thinkpad_softc_t *, uint8_t);
 
+static void	thinkpad_battery_probe_support(device_t);
+static void	thinkpad_battery_sysctl_setup(device_t);
+
 CFATTACH_DECL3_NEW(thinkpad, sizeof(thinkpad_softc_t),
     thinkpad_match, thinkpad_attach, thinkpad_detach, NULL, NULL, NULL,
     0);
@@ -220,6 +260,7 @@
 	int i;
 
 	sc->sc_dev = self;
+	sc->sc_log = NULL;
 	sc->sc_powhdl = NULL;
 	sc->sc_cmoshdl = NULL;
 	sc->sc_node = aa->aa_node;
@@ -371,6 +412,17 @@
 	/* Register temperature and fan sensors with envsys */
 	thinkpad_sensors_init(sc);
 
+	/* Probe supported battery charge/control operations */
+	thinkpad_battery_probe_support(self);
+
+	if (sc->sc_batctl.have_any) {
+		for (i = 0; i < THINKPAD_BAT_LAST; i++) {
+			sc->sc_scparam[i].sp_dev = self;
+			sc->sc_scparam[i].sp_bat = i;
+		}
+		thinkpad_battery_sysctl_setup(self);
+	}
+
 fail:
 	if (!pmf_device_register(self, NULL, thinkpad_resume))
 		aprint_error_dev(self, "couldn't establish power handler\n");
@@ -396,6 +448,9 @@
 	if (sc->sc_sme != NULL)
 		sysmon_envsys_unregister(sc->sc_sme);
 
+	if (sc->sc_log != NULL)
+		sysctl_teardown(&sc->sc_log);
+
 	pmf_device_deregister(self);
 
 	pmf_event_deregister(self, PMFE_DISPLAY_BRIGHTNESS_UP,
@@ -946,6 +1001,290 @@
 	if (ACPI_FAILURE(rv))
 		aprint_error_dev(sc->sc_dev, "couldn't evaluate CMOS: %s\n",
 		    AcpiFormatException(rv));
+}
+
+static uint32_t
+thinkpad_call_method(device_t self, const char *path, uint32_t arg)
+{
+	thinkpad_softc_t *sc = device_private(self);
+	ACPI_HANDLE handle = sc->sc_node->ad_handle;
+	ACPI_OBJECT args[1];
+	ACPI_OBJECT_LIST arg_list;
+	ACPI_OBJECT rval;
+	ACPI_BUFFER buf;
+	ACPI_STATUS rv;
+
+	args[0].Type = ACPI_TYPE_INTEGER;
+	args[0].Integer.Value = arg;
+	arg_list.Pointer = &args[0];
+	arg_list.Count = __arraycount(args);
+
+	memset(&rval, 0, sizeof rval);
+	buf.Pointer = &rval;
+	buf.Length = sizeof rval;
+
+	rv = AcpiEvaluateObjectTyped(handle, path, &arg_list, &buf,
+	    ACPI_TYPE_INTEGER);
+
+	if (ACPI_FAILURE(rv)) {
+		aprint_error_dev(self, "call %s.%s(%x) failed: %s\n",
+		    acpi_name(handle), path, (unsigned)arg,
+		    AcpiFormatException(rv));
+		return THINKPAD_CALL_ERROR;
+	}
+
+	return rval.Integer.Value;
+}
+
+static void
+thinkpad_battery_probe_support(device_t self)
+{
+	thinkpad_softc_t *sc = device_private(self);
+	ACPI_HANDLE hdl = sc->sc_node->ad_handle, tmp;
+	ACPI_STATUS rv;
+	uint32_t val;
+
+	sc->sc_batctl.have_any = 0;
+
+	rv = AcpiGetHandle(hdl, THINKPAD_GET_CHARGE_START, &tmp);
+	if (ACPI_SUCCESS(rv)) {
+		val = thinkpad_call_method(self, THINKPAD_GET_CHARGE_START,
+		    THINKPAD_BAT_PRIMARY);
+		if (!(val & THINKPAD_CALL_ERROR) && (val & 0x100)) {
+			sc->sc_batctl.have.charge_start = 1;
+			if (val & 0x200)
+				sc->sc_batctl.have.individual_control = 1;
+		}
+	}
+
+	rv = AcpiGetHandle(hdl, THINKPAD_GET_CHARGE_STOP, &tmp);
+	if (ACPI_SUCCESS(rv)) {
+		val = thinkpad_call_method(self, THINKPAD_GET_CHARGE_STOP,
+		    THINKPAD_BAT_PRIMARY);
+		if (!(val & THINKPAD_CALL_ERROR) && (val & 0x100))
+			sc->sc_batctl.have.charge_stop = 1;
+	}
+
+	rv = AcpiGetHandle(hdl, THINKPAD_GET_FORCE_DISCHARGE, &tmp);
+	if (ACPI_SUCCESS(rv)) {
+		val = thinkpad_call_method(self, THINKPAD_GET_FORCE_DISCHARGE,
+		    THINKPAD_BAT_PRIMARY);
+		if (!(val & THINKPAD_CALL_ERROR) && (val & 0x100))
+			sc->sc_batctl.have.force_discharge = 1;
+	}
+
+	rv = AcpiGetHandle(hdl, THINKPAD_GET_CHARGE_INHIBIT, &tmp);
+	if (ACPI_SUCCESS(rv)) {
+		val = thinkpad_call_method(self, THINKPAD_GET_CHARGE_INHIBIT,
+		    THINKPAD_BAT_PRIMARY);
+		if (!(val & THINKPAD_CALL_ERROR) && (val & 0x20))
+			sc->sc_batctl.have.charge_inhibit = 1;
+	}
+
+	if (sc->sc_batctl.have_any)
+		aprint_verbose_dev(self, "battery control capabilities: %x\n",
+		    sc->sc_batctl.have_any);
+}
+
+static int
+thinkpad_battery_sysctl_charge_start(SYSCTLFN_ARGS)
+{
+	struct sysctlnode node = *rnode;
+	tp_sysctl_param_t *sp = node.sysctl_data;
+	int charge_start;
+	int err;
+
+	charge_start = thinkpad_call_method(sp->sp_dev,
+	    THINKPAD_GET_CHARGE_START, sp->sp_bat) & 0xff;
+
+	node.sysctl_data = &charge_start;
+	err = sysctl_lookup(SYSCTLFN_CALL(&node));
+	if (err || newp == NULL)
+		return err;
+
+	if (charge_start < 0 || charge_start > 99)
+		return EINVAL;
+
+	if (thinkpad_call_method(sp->sp_dev, THINKPAD_SET_CHARGE_START,
+	    charge_start | sp->sp_bat<<8) & THINKPAD_CALL_ERROR)
+		return EIO;
+
+	return 0;
+}
+
+static int
+thinkpad_battery_sysctl_charge_stop(SYSCTLFN_ARGS)
+{
+	struct sysctlnode node = *rnode;
+	tp_sysctl_param_t *sp = node.sysctl_data;
+	int charge_stop;
+	int err;
+
+	charge_stop = thinkpad_call_method(sp->sp_dev,
+	    THINKPAD_GET_CHARGE_STOP, sp->sp_bat) & 0xff;
+
+	if (charge_stop == 0)
+		charge_stop = 100;
+
+	node.sysctl_data = &charge_stop;
+	err = sysctl_lookup(SYSCTLFN_CALL(&node));
+	if (err || newp == NULL)
+		return err;
+
+	if (charge_stop < 1 || charge_stop > 100)
+		return EINVAL;
+
+	if (charge_stop == 100)
+		charge_stop = 0;
+
+	if (thinkpad_call_method(sp->sp_dev, THINKPAD_SET_CHARGE_STOP,
+	    charge_stop | sp->sp_bat<<8) & THINKPAD_CALL_ERROR)
+		return EIO;
+
+	return 0;
+}
+
+static int
+thinkpad_battery_sysctl_charge_inhibit(SYSCTLFN_ARGS)
+{
+	struct sysctlnode node = *rnode;
+	tp_sysctl_param_t *sp = node.sysctl_data;
+	bool charge_inhibit;
+	int err;
+
+	charge_inhibit = thinkpad_call_method(sp->sp_dev,
+	    THINKPAD_GET_CHARGE_INHIBIT, sp->sp_bat) & 0x01;
+
+	node.sysctl_data = &charge_inhibit;
+	err = sysctl_lookup(SYSCTLFN_CALL(&node));
+	if (err || newp == NULL)
+		return err;
+
+	if (thinkpad_call_method(sp->sp_dev, THINKPAD_SET_CHARGE_INHIBIT,
+	    charge_inhibit | sp->sp_bat<<4 | 0xffff<<8) & THINKPAD_CALL_ERROR)
+		return EIO;
+
+	return 0;
+}
+
+static int
+thinkpad_battery_sysctl_force_discharge(SYSCTLFN_ARGS)
+{
+	struct sysctlnode node = *rnode;
+	tp_sysctl_param_t *sp = node.sysctl_data;
+	bool force_discharge;
+	int err;
+
+	force_discharge = thinkpad_call_method(sp->sp_dev,
+	    THINKPAD_GET_FORCE_DISCHARGE, sp->sp_bat) & 0x01;
+
+	node.sysctl_data = &force_discharge;
+	err = sysctl_lookup(SYSCTLFN_CALL(&node));
+	if (err || newp == NULL)
+		return err;
+
+	if (thinkpad_call_method(sp->sp_dev, THINKPAD_SET_FORCE_DISCHARGE,
+	    force_discharge | sp->sp_bat<<8) & THINKPAD_CALL_ERROR)
+		return EIO;
+
+	return 0;
+}
+
+static void
+thinkpad_battery_sysctl_setup_controls(device_t self,
+    const struct sysctlnode *rnode, int battery)
+{
+	thinkpad_softc_t *sc = device_private(self);
+
+	if (sc->sc_batctl.have.charge_start)
+		(void)sysctl_createv(&sc->sc_log, 0, &rnode, NULL,
+		    CTLFLAG_READWRITE, CTLTYPE_INT, "charge_start",
+		    SYSCTL_DESCR("charge start threshold (0-99)"),
+		    thinkpad_battery_sysctl_charge_start, 0,
+		    (void *)&(sc->sc_scparam[battery]), 0,
+		    CTL_CREATE, CTL_EOL);
+
+	if (sc->sc_batctl.have.charge_stop)
+		(void)sysctl_createv(&sc->sc_log, 0, &rnode, NULL,
+		    CTLFLAG_READWRITE, CTLTYPE_INT, "charge_stop",
+		    SYSCTL_DESCR("charge stop threshold (1-100)"),
+		    thinkpad_battery_sysctl_charge_stop, 0,
+		    (void *)&(sc->sc_scparam[battery]), 0,
+		    CTL_CREATE, CTL_EOL);
+
+	if (sc->sc_batctl.have.charge_inhibit)
+		(void)sysctl_createv(&sc->sc_log, 0, &rnode, NULL,
+		    CTLFLAG_READWRITE, CTLTYPE_BOOL, "charge_inhibit",
+		    SYSCTL_DESCR("charge inhibit"),
+		    thinkpad_battery_sysctl_charge_inhibit, 0,
+		    (void *)&(sc->sc_scparam[battery]), 0,
+		    CTL_CREATE, CTL_EOL);
+
+	if (sc->sc_batctl.have.force_discharge)
+		(void)sysctl_createv(&sc->sc_log, 0, &rnode, NULL,
+		    CTLFLAG_READWRITE, CTLTYPE_BOOL, "force_discharge",
+		    SYSCTL_DESCR("force discharge"),
+		    thinkpad_battery_sysctl_force_discharge, 0,
+		    (void *)&(sc->sc_scparam[battery]), 0,
+		    CTL_CREATE, CTL_EOL);
+}
+
+static void
+thinkpad_battery_sysctl_setup(device_t self)
+{
+	thinkpad_softc_t *sc = device_private(self);
+	const struct sysctlnode *rnode, *cnode;
+	int err;
+
+	err = sysctl_createv(&sc->sc_log, 0, NULL, &rnode,
+	    0, CTLTYPE_NODE, "acpi", NULL,
+	    NULL, 0, NULL, 0, CTL_HW, CTL_CREATE, CTL_EOL);
+	if (err)
+		goto fail;
+
+	err = sysctl_createv(&sc->sc_log, 0, &rnode, &rnode,
+	    0, CTLTYPE_NODE, device_xname(self),
+	    SYSCTL_DESCR("ThinkPad ACPI controls"),
+	    NULL, 0, NULL, 0, CTL_CREATE, CTL_EOL);
+	if (err)
+		goto fail;
+
+	if (sc->sc_batctl.have.individual_control) {
+		err = sysctl_createv(&sc->sc_log, 0, &rnode, &cnode,
+		    0, CTLTYPE_NODE, "bat0",
+		    SYSCTL_DESCR("battery charge controls (primary battery)"),
+		    NULL, 0, NULL, 0, CTL_CREATE, CTL_EOL);
+		if (err)
+			goto fail;
+
+		thinkpad_battery_sysctl_setup_controls(self, cnode,
+		    THINKPAD_BAT_PRIMARY);
+
+		err = sysctl_createv(&sc->sc_log, 0, &rnode, &cnode,
+		    0, CTLTYPE_NODE, "bat1",
+		    SYSCTL_DESCR("battery charge controls (secondary battery)"),
+		    NULL, 0, NULL, 0, CTL_CREATE, CTL_EOL);
+		if (err)
+			goto fail;
+
+		thinkpad_battery_sysctl_setup_controls(self, cnode,
+		    THINKPAD_BAT_SECONDARY);
+	} else {
+		err = sysctl_createv(&sc->sc_log, 0, &rnode, &cnode,
+		    0, CTLTYPE_NODE, "bat",
+		    SYSCTL_DESCR("battery charge controls"),
+		    NULL, 0, NULL, 0, CTL_CREATE, CTL_EOL);
+		if (err)
+			goto fail;
+
+		thinkpad_battery_sysctl_setup_controls(self, cnode,
+		    THINKPAD_BAT_ANY);
+	}
+
+	return;
+
+fail:
+	aprint_error_dev(self, "unable to add sysctl nodes (%d)\n", err);
 }
 
 static bool