Subject: ACPI LCD brightness control
To: None <tech-kern@netbsd.org>
From: Yorick Hardy <yhardy@uj.ac.za>
List: tech-kern
Date: 12/20/2007 10:45:55
This is a multi-part message in MIME format.
--------------090009020302090803080700
Content-Type: text/plain; charset=ISO-8859-1; format=flowed
Content-Transfer-Encoding: 7bit

On my computer the LCD brightness is controlled using ACPI.

I attach a patch for the acpilcd driver which provides sysctl
support to control the brightness (lcd1.patch). The driver
attempts to support part of Appendix B.6 of the ACPI specification
3.0b.

To get it working I had to use lcd2.patch. As far as can tell
the specification does not require a device to provide a HID?
My computer's LCD device does not report a valid HID, so I had to
remove the restriction.

I also attempted to provide some documentation in lcd3.patch.

Any suggestions or corrections?

-- 
Kind regards,

Yorick Hardy

--------------090009020302090803080700
Content-Type: text/plain;
 name="lcd1.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
 filename="lcd1.patch"

--- /dev/null	2007-12-17 11:26:08.000000000 +0200
+++ sys/dev/acpi/acpi_lcd.c	2007-12-17 11:29:50.000000000 +0200
@@ -0,0 +1,337 @@
+#include <sys/cdefs.h>
+
+#include <sys/param.h>
+#include <sys/systm.h>
+#include <sys/device.h>
+#include <sys/proc.h>
+#include <sys/kernel.h>
+#include <sys/callout.h>
+#include <sys/sysctl.h>
+
+#include <machine/bus.h>
+
+#include <dev/acpi/acpica.h>
+#include <dev/acpi/acpivar.h>
+
+struct lcd_acpi_softc {
+	struct device sc_dev;
+	struct sysctllog *sc_log;
+	struct acpi_devnode *sc_node;
+	ACPI_OBJECT *valid_levels;
+	int min_level, max_level;
+};
+
+static int	lcd_acpi_match(struct device *, struct cfdata *, void *);
+static void	lcd_acpi_attach(struct device *, struct device *, void *);
+
+CFATTACH_DECL(acpilcd, sizeof(struct lcd_acpi_softc),
+    lcd_acpi_match, lcd_acpi_attach, NULL, NULL);
+
+
+static ACPI_STATUS
+lcd_walk_cb_match(ACPI_HANDLE hnd, UINT32 v, void *context,
+    void **status)
+{
+	int *found = (int*)context;
+	const char *name = acpi_name(hnd);
+
+	if ((name = strrchr(name, '.')) == NULL)
+		return AE_OK;
+
+	name++;
+
+	if (!strcmp(name, "_BCL"))
+		*found = 1;
+
+	return AE_OK;
+}
+
+static int
+lcd_acpi_match(struct device *parent, struct cfdata *match,
+    void *aux)
+{
+	struct acpi_attach_args *aa = aux;
+	int found = 0;
+	ACPI_STATUS rv;
+
+	if (aa->aa_node->ad_type != ACPI_TYPE_DEVICE)
+		return 0;
+
+	rv = AcpiWalkNamespace(ACPI_TYPE_METHOD,
+	    aa->aa_node->ad_handle, 1, lcd_walk_cb_match, (void*)&found, NULL);
+
+	return found;
+}
+
+static int
+lcd_sysctl_percent(SYSCTLFN_ARGS)
+{
+	struct sysctlnode node;
+	int i, t, error, closest, closestdist, distance;
+	ACPI_STATUS rv;
+	ACPI_INTEGER value;
+	ACPI_OBJECT param;
+	ACPI_OBJECT_LIST params;
+	struct lcd_acpi_softc *sc = rnode->sysctl_data;
+
+	rv = acpi_eval_integer(sc->sc_node->ad_handle, "_BQC", &value);
+	if (ACPI_FAILURE(rv))
+	{
+		aprint_error("%s: Cannot eval integer _BQC (%d)\n",
+		    sc->sc_dev.dv_xname, rv);
+		return EIO;
+	}
+	t = 100 * (value - sc->min_level) / (sc->max_level - sc->min_level);
+
+	node = *rnode;
+	node.sysctl_data = &t;
+	error = sysctl_lookup(SYSCTLFN_CALL(&node));
+
+	if (error || newp == NULL)
+		return error;
+
+	if (t < 0 || t > 100)
+		return EINVAL;
+
+	t = sc->min_level + t * (sc->max_level - sc->min_level) / 100;
+
+	closest = sc->valid_levels->Package.Elements[2].Integer.Value;
+	closestdist = t - sc->valid_levels->Package.Elements[2].Integer.Value;
+	if (closestdist < 0) closestdist = -closestdist;
+
+	for (closest = i = 0; i < sc->valid_levels->Package.Count; i++)
+	{
+		distance = t
+			 - sc->valid_levels->Package.Elements[i].Integer.Value;
+		if (distance < 0)
+			distance = -distance;
+		if (distance < closestdist)
+		{
+			closest = sc->valid_levels->Package.Elements[i].Integer.Value;
+			closestdist = distance;
+		}
+	}
+
+	params.Count = 1;
+	params.Pointer = &param;
+
+	param.Type = ACPI_TYPE_INTEGER;
+	param.Integer.Value = closest;
+
+	rv = AcpiEvaluateObject(sc->sc_node->ad_handle, "_BCM", &params, NULL);
+
+	if (ACPI_FAILURE(rv))
+	{
+		aprint_error("%s: Cannot eval integer _BCM (%d)\n",
+		    sc->sc_dev.dv_xname, rv);
+		return EIO;
+	}
+
+	return 0;
+}
+
+static int
+lcd_sysctl_level(SYSCTLFN_ARGS)
+{
+	struct sysctlnode node;
+	int i, valid, t, error;
+	ACPI_STATUS rv;
+	ACPI_INTEGER value;
+	ACPI_OBJECT param;
+	ACPI_OBJECT_LIST params;
+	struct lcd_acpi_softc *sc = rnode->sysctl_data;
+
+	rv = acpi_eval_integer(sc->sc_node->ad_handle, "_BQC", &value);
+	if (ACPI_FAILURE(rv))
+	{
+		aprint_error("%s: Cannot eval integer _BQC (%d)\n",
+		    sc->sc_dev.dv_xname, rv);
+		return EIO;
+	}
+	t = value;
+
+	node = *rnode;
+	node.sysctl_data = &t;
+	error = sysctl_lookup(SYSCTLFN_CALL(&node));
+	if (error || newp == NULL)
+		return error;
+
+	for (valid = i = 0; i < sc->valid_levels->Package.Count; i++)
+		if (sc->valid_levels->Package.Elements[i].Integer.Value == t)
+			valid = 1;
+
+	if (!valid) return EINVAL;
+
+	params.Count = 1;
+	params.Pointer = &param;
+
+	param.Type = ACPI_TYPE_INTEGER;
+	param.Integer.Value = t;
+
+	rv = AcpiEvaluateObject(sc->sc_node->ad_handle, "_BCM", &params, NULL);
+
+	if (ACPI_FAILURE(rv))
+	{
+		aprint_error("%s: Cannot eval integer _BCM (%d)\n",
+		    sc->sc_dev.dv_xname, rv);
+		return EIO;
+	}
+
+	return 0;
+}
+
+static int
+lcd_sysctl_next(SYSCTLFN_ARGS)
+{
+	struct sysctlnode node;
+	int i, next, error;
+	ACPI_STATUS rv;
+	ACPI_INTEGER val;
+	struct lcd_acpi_softc *sc = rnode->sysctl_data;
+
+	rv = acpi_eval_integer(sc->sc_node->ad_handle, "_BQC", &val);
+	if (ACPI_FAILURE(rv))
+	{
+		aprint_error("%s: Cannot eval integer _BQC (%d)\n",
+		    sc->sc_dev.dv_xname, rv);
+		return EIO;
+	}
+
+	for (next = 0, i = 2; i < sc->valid_levels->Package.Count; i++)
+		if (sc->valid_levels->Package.Elements[i].Integer.Value == val)
+			next = i;
+
+	next++;
+
+	if (next >= sc->valid_levels->Package.Count)
+		next = sc->valid_levels->Package.Count - 1;
+
+	next = sc->valid_levels->Package.Elements[next].Integer.Value;
+
+	node = *rnode;
+	node.sysctl_data = &next;
+
+	error = sysctl_lookup(SYSCTLFN_CALL(&node));
+
+	return error;
+}
+
+static int
+lcd_sysctl_previous(SYSCTLFN_ARGS)
+{
+	struct sysctlnode node;
+	int i, previous, error;
+	ACPI_STATUS rv;
+	ACPI_INTEGER val;
+	struct lcd_acpi_softc *sc = rnode->sysctl_data;
+
+	rv = acpi_eval_integer(sc->sc_node->ad_handle, "_BQC", &val);
+	if (ACPI_FAILURE(rv))
+	{
+		aprint_error("%s: Cannot eval integer _BQC (%d)\n",
+		    sc->sc_dev.dv_xname, rv);
+		return EIO;
+	}
+
+	for (previous = 1, i = 2; i < sc->valid_levels->Package.Count; i++)
+		if (sc->valid_levels->Package.Elements[i].Integer.Value == val)
+			previous = i;
+
+	previous--;
+
+	if (sc->valid_levels->Package.Count >= 3 && previous < 2)
+		previous = 2;
+
+	previous = sc->valid_levels->Package.Elements[previous].Integer.Value;
+
+	node = *rnode;
+	node.sysctl_data = &previous;
+
+	error = sysctl_lookup(SYSCTLFN_CALL(&node));
+
+	return error;
+}
+
+static void
+lcd_acpi_attach(struct device *parent, struct device *self, void *aux)
+{
+	struct lcd_acpi_softc *sc = (struct lcd_acpi_softc *)self;
+	struct acpi_attach_args *aa = aux;
+	const struct sysctlnode *node, *snode;
+	int i;
+	ACPI_STATUS rv;
+	ACPI_BUFFER buf;
+
+	aprint_naive(": ACPI LCD Control\n");
+	aprint_normal(": ACPI LCD Control\n");
+
+	sc->sc_node = aa->aa_node;
+
+	rv = acpi_eval_struct(sc->sc_node->ad_handle, "_BCL", &buf);
+	if (ACPI_FAILURE(rv))
+	{
+		aprint_error("%s: Cannot eval struct _BCL (%d)\n",
+		    sc->sc_dev.dv_xname, rv);
+		return;
+	}
+
+	sc->valid_levels = (ACPI_OBJECT *)buf.Pointer;
+	sc->min_level = sc->valid_levels->Package.Elements[0].Integer.Value;
+	sc->max_level = sc->valid_levels->Package.Elements[0].Integer.Value;
+
+	aprint_normal("%s: supported brightness levels are",
+		       sc->sc_dev.dv_xname);
+
+	for (i = 0; i < sc->valid_levels->Package.Count; i++)
+	{
+		if (sc->valid_levels->Package.Elements[i].Integer.Value < sc->min_level)
+			sc->min_level = sc->valid_levels->Package.Elements[i].Integer.Value;
+		if (sc->valid_levels->Package.Elements[i].Integer.Value > sc->max_level)
+			sc->max_level = sc->valid_levels->Package.Elements[i].Integer.Value;
+		aprint_normal(" %d",
+		     (int) sc->valid_levels->Package.Elements[i].Integer.Value);
+	}
+
+	aprint_normal("\n");
+/*	AcpiOsFree(buf.Pointer);*/
+
+	if (sysctl_createv(&sc->sc_log, 0, NULL, &node, CTLFLAG_PERMANENT,
+	    CTLTYPE_NODE, "hw", NULL, NULL, 0, NULL, 0, CTL_HW, CTL_EOL) != 0)
+		return;
+
+	if (sysctl_createv(&sc->sc_log, 0, &node, &snode, 0,
+	    CTLTYPE_NODE, sc->sc_dev.dv_xname,
+	    SYSCTL_DESCR("ACPI LCD control"),
+	    NULL, 0, NULL, 0, CTL_CREATE, CTL_EOL) != 0)
+		return;
+
+	if (sysctl_createv(&sc->sc_log, 0, &snode, &node, 0,
+	    CTLTYPE_NODE, "brightness",
+	    SYSCTL_DESCR("LCD brightness control"),
+	    NULL, 0, NULL, 0, CTL_CREATE, CTL_EOL) != 0)
+		return;
+
+	if (sysctl_createv(&sc->sc_log, 0, &node, &snode,
+	    CTLFLAG_READWRITE | CTLFLAG_ANYWRITE, CTLTYPE_INT, "level",
+	    SYSCTL_DESCR("LCD brightness level (hardware)"),
+	    lcd_sysctl_level, 0, sc, 0, CTL_CREATE, CTL_EOL) != 0)
+		return;
+
+	if (sysctl_createv(&sc->sc_log, 0, &node, &snode,
+	    CTLFLAG_READWRITE | CTLFLAG_ANYWRITE, CTLTYPE_INT, "percent",
+	    SYSCTL_DESCR("LCD brightness (percent)"),
+	    lcd_sysctl_percent, 0, sc, 0, CTL_CREATE, CTL_EOL) != 0)
+		return;
+
+	if (sysctl_createv(&sc->sc_log, 0, &node, &snode,
+	    CTLFLAG_READONLY, CTLTYPE_INT, "next",
+	    SYSCTL_DESCR("next LCD brightness level"),
+	    lcd_sysctl_next, 0, sc, 0, CTL_CREATE, CTL_EOL) != 0)
+		return;
+
+	if (sysctl_createv(&sc->sc_log, 0, &node, &snode,
+	    CTLFLAG_READONLY, CTLTYPE_INT, "previous",
+	    SYSCTL_DESCR("previous LCD brightness level"),
+	    lcd_sysctl_previous, 0, sc, 0, CTL_CREATE, CTL_EOL) != 0)
+		return;
+}
--- ./sys/dev/acpi/files.acpi.orig	2007-12-13 13:13:58.000000000 +0200
+++ ./sys/dev/acpi/files.acpi	2007-12-18 13:10:59.000000000 +0200
@@ -56,6 +56,11 @@
 attach	acpitz at acpinodebus
 file	dev/acpi/acpi_tz.c		acpitz
 
+# ACPI LCD control
+device	acpilcd
+attach	acpilcd at acpinodebus
+file	dev/acpi/acpi_lcd.c		acpilcd
+
 # Serial interface
 attach	com at acpinodebus with com_acpi
 file	dev/acpi/com_acpi.c		com_acpi

--------------090009020302090803080700
Content-Type: text/plain;
 name="lcd2.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
 filename="lcd2.patch"

--- ./sys/dev/acpi/acpi.c.orig	2007-12-18 13:21:02.000000000 +0200
+++ sys/dev/acpi/acpi.c	2007-12-18 13:21:43.000000000 +0200
@@ -616,16 +616,6 @@
 					continue;
 			}
 
-			/*
-			 * XXX Same problem as above...
-			 *
-			 * Do this check only for devices, as e.g.
-			 * a Thermal Zone doesn't have a HID.
-			 */
-			if (ad->ad_devinfo->Type == ACPI_TYPE_DEVICE &&
-			    (ad->ad_devinfo->Valid & ACPI_VALID_HID) == 0)
-				continue;
-
 			ad->ad_device = config_found_ia(&sc->sc_dev,
 			    "acpinodebus", &aa, acpi_print);
 		}

--------------090009020302090803080700
Content-Type: text/plain;
 name="lcd3.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
 filename="lcd3.patch"

--- share/man/man4/acpi.4.orig	2007-12-17 11:34:38.000000000 +0200
+++ share/man/man4/acpi.4	2007-12-17 11:37:06.000000000 +0200
@@ -43,6 +43,7 @@
 .Cd "acpibat*  at acpi?"
 .Cd "acpibut*  at acpi?"
 .Cd "acpiec*   at acpi?"
+.Cd "acpilcd*  at acpi?"
 .Cd "acpilid*  at acpi?"
 .Cd "acpitz*   at acpi?"
 .Cd "attimer*  at acpi?"
@@ -128,6 +129,9 @@
 .It acpiec
 .Tn ACPI
 Embedded Controllers.
+.It acpilcd
+.Tn ACPI
+LCD control.
 .It acpilid
 .Tn ACPI
 lid switches.
@@ -177,6 +181,7 @@
 .Xr acpibat 4 ,
 .Xr acpibut 4 ,
 .Xr acpiec 4 ,
+.Xr acpilcd 4 ,
 .Xr acpilid 4 ,
 .Xr acpitz 4 ,
 .Xr aiboost 4 ,
--- /dev/null	2007-12-17 12:56:55.000000000 +0200
+++ share/man/man4/acpilcd.4	2007-12-17 12:58:21.000000000 +0200
@@ -0,0 +1,64 @@
+.Dd December 17, 2007
+.Dt ACPILCD 4
+.Os
+.Sh NAME
+.Nm acpilcd
+.Nd ACPI LCD Control
+.Sh SYNOPSIS
+.Cd "acpilcd* at acpi?"
+.Sh DESCRIPTION
+The
+.Nm
+driver supports ACPI LCD control. During the kernel boot the presence of a
+.Nm
+device is indicated by two lines, for example
+.Pp
+.Bd -literal
+acpilcd0 at acpi0 (LCD): ACPI LCD Control
+acpilcd0: supported brightness levels are 70 40 0 10 20 30 40 50 60 70
+.Ed
+.Pp
+where the second line lists the valid brightness levels. The first two
+numbers are the brightness levels when the AC adaptor is connected and
+when running from battery respectively. The remaining values are a list
+of values that can be cycled through when increasing or decreasing the
+brightness. The brightness levels can be set using
+.Xr sysctl 8
+using the node hw.acpilcd0.brightness.
+.Pp
+.Bl -inset -offset 1n -compact
+.It Em hw.acpilcd0.brightness.level
+is the current brightness level and can be set to one of the valid
+brightness levels.
+
+.It Em hw.acpilcd0.brightness.percent
+is the current brightness level as a percentage of the brightness
+available and can be set to any value from 0 to 100 (the driver will
+select the closest brightness level).
+
+.It Em hw.acpilcd0.brightness.next
+is the next brighter level. If no brighter level is available,
+hw.acpilcd0.brightness.next has the value of the current level.
+
+.It Em hw.acpilcd0.brightness.previous
+is the previous darker level. If no darker level is available,
+hw.acpilcd0.brightness.previous has the value of the current level.
+.El
+
+.Sh EXAMPLES
+It is possible to increase the brightness in steps, up to the maximum, using
+.Bd -literal
+sysctl -w \\
+ hw.acpilcd0.brightness.level=`sysctl -n hw.acpilcd0.brightness.next`
+.Ed
+.Pp
+It is possible to decrease the brightness in steps, up to the minimum, using
+.Bd -literal
+sysctl -w \\
+ hw.acpilcd0.brightness.level=`sysctl -n hw.acpilcd0.brightness.previous`
+.Ed
+
+.Sh SEE ALSO
+.Xr acpi 4 ,
+.Xr dmesg 8 ,
+.Xr sysctl 8

--------------090009020302090803080700--