Index | Thread | Search

From:
Mark Kettenis <mark.kettenis@xs4all.nl>
Subject:
Re: sambat(4) - Samsung laptop battery monitor
To:
Marcus Glocker <marcus@nazgul.ch>
Cc:
tech@openbsd.org
Date:
Sun, 17 May 2026 22:33:50 +0200

Download raw body.

Thread
> Date: Sun, 17 May 2026 20:31:43 +0200
> From: Marcus Glocker <marcus@nazgul.ch>
> 
> On the Samsung Galaxy Book4 Edge qcpas(4) never was returning the
> battery status.  Now I found out that on this laptop the ADSP doesn't
> host an charger_pd (protection domain), instead we need to read the
> battery status values directly from the SAM060B EC over I2C.
> 
> To attach the SAM060B EC we obviously require a new node entry for our
> DTB, so this diff includes the arm64-qcom-dtb-firmware port update as
> well.
> 
> This gives me working apm finally:
> 
> x1e$ dmesg | grep sambat  
> sambat0 at iic1 addr 0x64: EC flags 0x05
> 
> x1e$ sysctl hw.sensors.sambat0
> hw.sensors.sambat0.volt0=17.57 VDC (battery voltage)
> hw.sensors.sambat0.volt1=46.24 VDC (battery design voltage)

This is an odd value.  I'd expect the battery design voltage to be
similar as the actual battery voltage.

> hw.sensors.sambat0.current0=0.42 A (battery current)
> hw.sensors.sambat0.amphour0=3.40 Ah (battery remaining)
> hw.sensors.sambat0.amphour1=3.53 Ah (battery full charge)
> hw.sensors.sambat0.amphour2=45.75 Ah (battery design capacity)

The design capacity is odd as well.  The combination of design voltage
and design capacity suggests the capacity of the battery is 2kWh.
Please be careful...

I don't see any obvious bugs in the driver code though, at least when
using the ACPI code as a reference.

Looking at the ACPI it should be possible to read the discharge cycle
count by reading the value of "register" 0xd0.

> hw.sensors.sambat0.raw0=0 (battery state)
> hw.sensors.sambat0.percent0=96.29% (battery charge)
> 
> x1e$ apm
> Battery state: high, 96% remaining, unknown life estimate
> AC adapter state: connected
> Performance adjustment mode: manual (4012 MHz)
> 
> Ok?
> 
> 
> Index: sys/arch/arm64/conf/GENERIC
> ===================================================================
> RCS file: /cvs/src/sys/arch/arm64/conf/GENERIC,v
> diff -u -p -u -p -r1.315 GENERIC
> --- sys/arch/arm64/conf/GENERIC	14 May 2026 16:20:27 -0000	1.315
> +++ sys/arch/arm64/conf/GENERIC	17 May 2026 18:15:09 -0000
> @@ -627,6 +627,7 @@ tascodec*	at iic?			# TAS2770 audio code
>  tcpci*		at iic?			# USB Type-C controller
>  tipd*		at iic?			# TPS6598x Type-C controller
>  pijuice*	at iic?			# PiJuice HAT
> +sambat*		at iic?			# Samsung SAM060B battery monitor
>  
>  # GPIO "pin bus" drivers
>  gpioiic*	at gpio?		# I2C bus bit-banging
> Index: sys/dev/i2c/files.i2c
> ===================================================================
> RCS file: /cvs/src/sys/dev/i2c/files.i2c,v
> diff -u -p -u -p -r1.74 files.i2c
> --- sys/dev/i2c/files.i2c	23 Nov 2025 21:28:15 -0000	1.74
> +++ sys/dev/i2c/files.i2c	17 May 2026 18:15:10 -0000
> @@ -274,6 +274,11 @@ device	pijuice
>  attach	pijuice at i2c
>  file	dev/i2c/pijuice.c			pijuice
>  
> +# Samsung SAM060B battery monitor
> +device	sambat
> +attach	sambat at i2c
> +file	dev/i2c/sambat.c			sambat
> +
>  # Consumer Control Keyboards
>  device	icc: hid, hidcc, wskbddev
>  attach	icc at ihidbus
> Index: sys/dev/i2c/sambat.c
> ===================================================================
> RCS file: sys/dev/i2c/sambat.c
> diff -N sys/dev/i2c/sambat.c
> --- /dev/null	1 Jan 1970 00:00:00 -0000
> +++ sys/dev/i2c/sambat.c	17 May 2026 18:15:10 -0000
> @@ -0,0 +1,459 @@
> +/*	$OpenBSD$ */
> +
> +/*
> + * Copyright (c) 2026 Marcus Glocker <mglocker@openbsd.org>
> + *
> + * Permission to use, copy, modify, and distribute this software for any
> + * purpose with or without fee is hereby granted, provided that the above
> + * copyright notice and this permission notice appear in all copies.
> + *
> + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
> + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
> + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
> + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
> + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
> + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
> + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
> + */
> +
> +/*
> + * Battery monitor for Samsung laptops with the "SAM060B" embedded
> + * controller (ENE KB9058 silicon with Samsung firmware).  First
> + * confirmed on the Galaxy Book4 Edge; the same EC personality is
> + * likely shared across the Galaxy Book line.
> + *
> + * The host talks to the EC via a vendor "Mbox" command protocol on
> + * I2C, reverse-engineered for Linux by the Saddytech project; this
> + * driver is an independent reimplementation against that reference:
> + *   https://github.com/Saddytech/Galaxy-Book4-Edge-linux
> + *
> + * EC register layout (from the DSDT _SB.ECTC region the Windows
> + * driver writes to):
> + *   0x80  bit0 = battery present, bit2 = AC online
> + *   0x84  bit0 = discharging, bit1 = charging, bit3 = full
> + *   0xA0  upper big-endian word = remaining capacity (mAh)
> + *   0xA4  upper BE = voltage (mV), lower BE = signed current (mA)
> + *   0xB0  upper BE = design capacity (mAh), lower BE = full-charge (mAh)
> + *   0xB4  lower BE = design voltage (mV)
> + *
> + * Each EC register read requires three Mbox transactions:
> + *   write target address (cmd 0xF480 = reg)
> + *   write execute       (cmd 0xFF10 = 0x88 "read")
> + *   read result         (cmd 0xF480)
> + */
> +
> +#include <sys/param.h>
> +#include <sys/systm.h>
> +#include <sys/device.h>
> +#include <sys/sensors.h>
> +
> +#include <machine/apmvar.h>
> +
> +#include <dev/i2c/i2cvar.h>
> +
> +#include "apm.h"
> +
> +/* Mbox framing. */
> +#define SAM_MBOX_WRITE_PREFIX	0x40
> +#define SAM_MBOX_READ_PREFIX	0x30
> +#define SAM_MBOX_READ_SUCCESS	0x50
> +
> +/* Mbox command address (the "hi"/"lo" pair). */
> +#define SAM_CMD_TARGET_HI	0xf4
> +#define SAM_CMD_TARGET_LO	0x80
> +#define SAM_CMD_EXEC_HI		0xff
> +#define SAM_CMD_EXEC_LO		0x10
> +#define SAM_EXEC_READ		0x88
> +
> +/* EC register offsets. */
> +#define SAM_REG_FLAGS		0x80
> +#define SAM_REG_B1ST		0x84
> +#define SAM_REG_B1RR		0xa0
> +#define SAM_REG_B1PV		0xa4
> +#define SAM_REG_B1AF		0xb0
> +#define SAM_REG_B1VL		0xb4
> +
> +#define SAM_FLAG_B1EX		(1 << 0)
> +#define SAM_FLAG_ACEX		(1 << 2)
> +
> +#define SAM_B1ST_DISCHARGE	(1 << 0)
> +#define SAM_B1ST_CHARGE		(1 << 1)
> +#define SAM_B1ST_FULL		(1 << 3)
> +
> +#define SAM_REFRESH_INTERVAL	30	/* seconds */
> +
> +/* Sensor indices. */
> +enum sambat_sensors {
> +	SAMBAT_SENSOR_CHARGE,		/* percent */
> +	SAMBAT_SENSOR_VOLT_NOW,		/* current voltage */
> +	SAMBAT_SENSOR_VOLT_DESIGN,	/* design voltage */
> +	SAMBAT_SENSOR_CURRENT,		/* signed current */
> +	SAMBAT_SENSOR_CHARGE_NOW,	/* remaining capacity (Ah) */
> +	SAMBAT_SENSOR_CHARGE_FULL,	/* full-charge capacity */
> +	SAMBAT_SENSOR_CHARGE_DESIGN,	/* design capacity */
> +	SAMBAT_SENSOR_STATE,		/* drive state */

Maybe you meant battery state here?

> +	SAMBAT_NSENSORS
> +};
> +
> +struct sambat_softc {
> +	struct device		sc_dev;
> +	i2c_tag_t		sc_tag;
> +	int			sc_addr;
> +
> +	struct ksensor		sc_sensor[SAMBAT_NSENSORS];
> +	struct ksensordev	sc_sensordev;
> +
> +	/* Latest decoded readings. */
> +	int			sc_have_data;
> +	int			sc_present;
> +	int			sc_ac_online;
> +	uint8_t			sc_b1st;
> +	uint16_t		sc_remaining_mah;
> +	uint16_t		sc_voltage_mv;
> +	int16_t			sc_current_ma;
> +	uint16_t		sc_design_mah;
> +	uint16_t		sc_fullchg_mah;
> +	uint16_t		sc_design_mv;
> +};
> +
> +struct sambat_softc *sambat_sc;
> +
> +int	sambat_match(struct device *, void *, void *);
> +void	sambat_attach(struct device *, struct device *, void *);
> +
> +int	sambat_mbox_write(struct sambat_softc *, uint8_t, uint8_t, uint8_t);
> +int	sambat_mbox_read(struct sambat_softc *, uint8_t, uint8_t, uint8_t *);
> +int	sambat_ec_read_byte(struct sambat_softc *, uint8_t, uint8_t *);
> +int	sambat_ec_read_block(struct sambat_softc *, uint8_t, uint8_t *, int);
> +
> +void	sambat_refresh(void *);
> +int	sambat_apminfo(struct apm_power_info *);
> +
> +const struct cfattach sambat_ca = {
> +	sizeof(struct sambat_softc), sambat_match, sambat_attach
> +};
> +
> +struct cfdriver sambat_cd = {
> +	NULL, "sambat", DV_DULL
> +};
> +
> +int
> +sambat_match(struct device *parent, void *match, void *aux)
> +{
> +	struct i2c_attach_args *ia = aux;
> +
> +	if (strcmp(ia->ia_name, "samsung,galaxybook-battery") == 0)
> +		return 1;
> +
> +	return 0;
> +}
> +
> +void
> +sambat_attach(struct device *parent, struct device *self, void *aux)
> +{
> +	struct sambat_softc *sc = (struct sambat_softc *)self;
> +	struct i2c_attach_args *ia = aux;
> +	uint8_t probe;
> +	int i;
> +
> +	sambat_sc = sc;
> +	sc->sc_tag = ia->ia_tag;
> +	sc->sc_addr = ia->ia_addr;
> +
> +	/*
> +	 * Probe the EC with a single byte read to confirm we're
> +	 * actually talking to it before exposing sensors.
> +	 */
> +	if (sambat_ec_read_byte(sc, SAM_REG_FLAGS, &probe) != 0) {
> +		printf(": EC probe failed\n");
> +		return;
> +	}
> +
> +	/* Sensor framework setup. */
> +	strlcpy(sc->sc_sensor[SAMBAT_SENSOR_CHARGE].desc,
> +	    "battery charge", sizeof(sc->sc_sensor[0].desc));
> +	sc->sc_sensor[SAMBAT_SENSOR_CHARGE].type = SENSOR_PERCENT;
> +
> +	strlcpy(sc->sc_sensor[SAMBAT_SENSOR_VOLT_NOW].desc,
> +	    "battery voltage", sizeof(sc->sc_sensor[0].desc));
> +	sc->sc_sensor[SAMBAT_SENSOR_VOLT_NOW].type = SENSOR_VOLTS_DC;
> +
> +	strlcpy(sc->sc_sensor[SAMBAT_SENSOR_VOLT_DESIGN].desc,
> +	    "battery design voltage", sizeof(sc->sc_sensor[0].desc));
> +	sc->sc_sensor[SAMBAT_SENSOR_VOLT_DESIGN].type = SENSOR_VOLTS_DC;
> +
> +	strlcpy(sc->sc_sensor[SAMBAT_SENSOR_CURRENT].desc,
> +	    "battery current", sizeof(sc->sc_sensor[0].desc));
> +	sc->sc_sensor[SAMBAT_SENSOR_CURRENT].type = SENSOR_AMPS;
> +
> +	strlcpy(sc->sc_sensor[SAMBAT_SENSOR_CHARGE_NOW].desc,
> +	    "battery remaining", sizeof(sc->sc_sensor[0].desc));
> +	sc->sc_sensor[SAMBAT_SENSOR_CHARGE_NOW].type = SENSOR_AMPHOUR;
> +
> +	strlcpy(sc->sc_sensor[SAMBAT_SENSOR_CHARGE_FULL].desc,
> +	    "battery full charge", sizeof(sc->sc_sensor[0].desc));
> +	sc->sc_sensor[SAMBAT_SENSOR_CHARGE_FULL].type = SENSOR_AMPHOUR;
> +
> +	strlcpy(sc->sc_sensor[SAMBAT_SENSOR_CHARGE_DESIGN].desc,
> +	    "battery design capacity", sizeof(sc->sc_sensor[0].desc));
> +	sc->sc_sensor[SAMBAT_SENSOR_CHARGE_DESIGN].type = SENSOR_AMPHOUR;
> +
> +	strlcpy(sc->sc_sensor[SAMBAT_SENSOR_STATE].desc,
> +	    "battery state", sizeof(sc->sc_sensor[0].desc));
> +	sc->sc_sensor[SAMBAT_SENSOR_STATE].type = SENSOR_INTEGER;
> +
> +	strlcpy(sc->sc_sensordev.xname, sc->sc_dev.dv_xname,
> +	    sizeof(sc->sc_sensordev.xname));
> +	for (i = 0; i < SAMBAT_NSENSORS; i++) {
> +		sc->sc_sensor[i].flags |= SENSOR_FINVALID;
> +		sensor_attach(&sc->sc_sensordev, &sc->sc_sensor[i]);
> +	}
> +	sensordev_install(&sc->sc_sensordev);
> +
> +	if (sensor_task_register(sc, sambat_refresh,
> +	    SAM_REFRESH_INTERVAL) == NULL) {
> +		printf(": can't register update task\n");
> +		return;
> +	}
> +
> +	printf(": EC flags 0x%02x\n", probe);
> +
> +#if NAPM > 0
> +	apm_setinfohook(sambat_apminfo);
> +#endif
> +}
> +
> +/*
> + * Mbox primitives.  All Mbox traffic uses the same I2C slave address
> + * (sc->sc_addr); the "hi"/"lo" pair selects the Mbox register inside
> + * the EC.  Write: 5-byte transfer.  Read: 4-byte write followed by
> + * 2-byte read (first byte must be SAM_MBOX_READ_SUCCESS = 0x50).
> + */
> +int
> +sambat_mbox_write(struct sambat_softc *sc, uint8_t hi, uint8_t lo,
> +    uint8_t data)
> +{
> +	uint8_t buf[5];
> +	int error;
> +
> +	buf[0] = SAM_MBOX_WRITE_PREFIX;
> +	buf[1] = 0x00;
> +	buf[2] = hi;
> +	buf[3] = lo;
> +	buf[4] = data;
> +
> +	iic_acquire_bus(sc->sc_tag, I2C_F_POLL);
> +	error = iic_exec(sc->sc_tag, I2C_OP_WRITE_WITH_STOP, sc->sc_addr,
> +	    NULL, 0, buf, sizeof(buf), I2C_F_POLL);
> +	iic_release_bus(sc->sc_tag, I2C_F_POLL);
> +
> +	if (error)
> +		return error;
> +
> +	/*
> +	 * Short settle delay between Mbox commands.  The Linux driver
> +	 * uses 5ms; the EC tolerates much less in practice and we
> +	 * share the I2C bus with the keyboard, so keep this small.
> +	 */
> +	delay(500);
> +	return 0;
> +}
> +
> +int
> +sambat_mbox_read(struct sambat_softc *sc, uint8_t hi, uint8_t lo,
> +    uint8_t *out)
> +{
> +	uint8_t cmd[4];
> +	uint8_t rsp[2];
> +	int error;
> +
> +	cmd[0] = SAM_MBOX_READ_PREFIX;
> +	cmd[1] = 0x00;
> +	cmd[2] = hi;
> +	cmd[3] = lo;
> +
> +	iic_acquire_bus(sc->sc_tag, I2C_F_POLL);
> +	error = iic_exec(sc->sc_tag, I2C_OP_READ_WITH_STOP, sc->sc_addr,
> +	    cmd, sizeof(cmd), rsp, sizeof(rsp), I2C_F_POLL);
> +	iic_release_bus(sc->sc_tag, I2C_F_POLL);
> +
> +	if (error)
> +		return error;
> +	if (rsp[0] != SAM_MBOX_READ_SUCCESS)
> +		return EIO;
> +
> +	*out = rsp[1];
> +	return 0;
> +}
> +
> +int
> +sambat_ec_read_byte(struct sambat_softc *sc, uint8_t reg, uint8_t *out)
> +{
> +	int error;
> +
> +	error = sambat_mbox_write(sc, SAM_CMD_TARGET_HI, SAM_CMD_TARGET_LO,
> +	    reg);
> +	if (error)
> +		return error;
> +	error = sambat_mbox_write(sc, SAM_CMD_EXEC_HI, SAM_CMD_EXEC_LO,
> +	    SAM_EXEC_READ);
> +	if (error)
> +		return error;
> +	return sambat_mbox_read(sc, SAM_CMD_TARGET_HI, SAM_CMD_TARGET_LO,
> +	    out);
> +}
> +
> +int
> +sambat_ec_read_block(struct sambat_softc *sc, uint8_t reg, uint8_t *buf,
> +    int n)
> +{
> +	int i, error;
> +
> +	for (i = 0; i < n; i++) {
> +		error = sambat_ec_read_byte(sc, reg + i, &buf[i]);
> +		if (error)
> +			return error;
> +	}
> +	return 0;
> +}
> +
> +void
> +sambat_refresh(void *arg)
> +{
> +	struct sambat_softc *sc = arg;
> +	uint8_t flags, b1st;
> +	uint8_t rr[4], pv[4], af[4], vl[4];
> +	uint16_t remaining, voltage, design_c, fullchg_c, design_v;
> +	int16_t cur_ma;
> +	int i;
> +
> +	for (i = 0; i < SAMBAT_NSENSORS; i++)
> +		sc->sc_sensor[i].flags |= SENSOR_FINVALID;
> +
> +	if (sambat_ec_read_byte(sc, SAM_REG_FLAGS, &flags) != 0 ||
> +	    sambat_ec_read_byte(sc, SAM_REG_B1ST, &b1st) != 0 ||
> +	    sambat_ec_read_block(sc, SAM_REG_B1RR, rr, sizeof(rr)) != 0 ||
> +	    sambat_ec_read_block(sc, SAM_REG_B1PV, pv, sizeof(pv)) != 0 ||
> +	    sambat_ec_read_block(sc, SAM_REG_B1AF, af, sizeof(af)) != 0 ||
> +	    sambat_ec_read_block(sc, SAM_REG_B1VL, vl, sizeof(vl)) != 0)
> +		return;
> +
> +	/*
> +	 * Each 4-byte EC field carries two big-endian 16-bit values.
> +	 * The DSDT ByteSwap16(reg >> 16) and ByteSwap16(reg & 0xffff)
> +	 * idioms extract the upper and lower BE words respectively.
> +	 */
> +	remaining = ((uint16_t)rr[2] << 8) | rr[3];
> +	voltage   = ((uint16_t)pv[2] << 8) | pv[3];
> +	cur_ma    = (int16_t)(((uint16_t)pv[0] << 8) | pv[1]);
> +	design_c  = ((uint16_t)af[2] << 8) | af[3];
> +	fullchg_c = ((uint16_t)af[0] << 8) | af[1];
> +	design_v  = ((uint16_t)vl[0] << 8) | vl[1];
> +
> +	if (remaining == 0xffff)
> +		remaining = 0;
> +	if (design_c == 0xffff)
> +		design_c = 0;
> +	if (fullchg_c == 0xffff)
> +		fullchg_c = 0;
> +
> +	sc->sc_present       = !!(flags & SAM_FLAG_B1EX);
> +	sc->sc_ac_online     = !!(flags & SAM_FLAG_ACEX);
> +	sc->sc_b1st          = b1st;
> +	sc->sc_remaining_mah = remaining;
> +	sc->sc_voltage_mv    = voltage;
> +	sc->sc_current_ma    = cur_ma;
> +	sc->sc_design_mah    = design_c;
> +	sc->sc_fullchg_mah   = fullchg_c ? fullchg_c : design_c;
> +	sc->sc_design_mv     = design_v;
> +	sc->sc_have_data     = 1;
> +
> +	if (sc->sc_fullchg_mah > 0) {
> +		/* SENSOR_PERCENT: value = percent * 1000 (100% = 100000). */
> +		sc->sc_sensor[SAMBAT_SENSOR_CHARGE].value =
> +		    (1000ULL * 100 * remaining) / sc->sc_fullchg_mah;
> +		sc->sc_sensor[SAMBAT_SENSOR_CHARGE].flags &= ~SENSOR_FINVALID;
> +	}
> +	sc->sc_sensor[SAMBAT_SENSOR_VOLT_NOW].value =
> +	    (uint64_t)voltage * 1000;	/* mV -> uV */
> +	sc->sc_sensor[SAMBAT_SENSOR_VOLT_NOW].flags &= ~SENSOR_FINVALID;
> +
> +	sc->sc_sensor[SAMBAT_SENSOR_VOLT_DESIGN].value =
> +	    (uint64_t)design_v * 1000;
> +	sc->sc_sensor[SAMBAT_SENSOR_VOLT_DESIGN].flags &= ~SENSOR_FINVALID;
> +
> +	sc->sc_sensor[SAMBAT_SENSOR_CURRENT].value =
> +	    (int64_t)cur_ma * 1000;	/* mA -> uA */
> +	sc->sc_sensor[SAMBAT_SENSOR_CURRENT].flags &= ~SENSOR_FINVALID;
> +
> +	sc->sc_sensor[SAMBAT_SENSOR_CHARGE_NOW].value =
> +	    (uint64_t)remaining * 1000;	/* mAh -> uAh */
> +	sc->sc_sensor[SAMBAT_SENSOR_CHARGE_NOW].flags &= ~SENSOR_FINVALID;
> +
> +	sc->sc_sensor[SAMBAT_SENSOR_CHARGE_FULL].value =
> +	    (uint64_t)sc->sc_fullchg_mah * 1000;
> +	sc->sc_sensor[SAMBAT_SENSOR_CHARGE_FULL].flags &= ~SENSOR_FINVALID;
> +
> +	sc->sc_sensor[SAMBAT_SENSOR_CHARGE_DESIGN].value =
> +	    (uint64_t)design_c * 1000;
> +	sc->sc_sensor[SAMBAT_SENSOR_CHARGE_DESIGN].flags &= ~SENSOR_FINVALID;
> +
> +	sc->sc_sensor[SAMBAT_SENSOR_STATE].value = b1st;
> +	sc->sc_sensor[SAMBAT_SENSOR_STATE].flags &= ~SENSOR_FINVALID;
> +}
> +
> +#if NAPM > 0
> +int
> +sambat_apminfo(struct apm_power_info *info)
> +{
> +	struct sambat_softc *sc = sambat_sc;
> +
> +	info->battery_state = APM_BATT_UNKNOWN;
> +	info->ac_state = APM_AC_UNKNOWN;
> +	info->battery_life = 0;
> +	info->minutes_left = -1;
> +
> +	if (!sc->sc_have_data)
> +		return 0;
> +
> +	if (sc->sc_ac_online)
> +		info->ac_state = APM_AC_ON;
> +	else
> +		info->ac_state = APM_AC_OFF;
> +
> +	if (!sc->sc_present) {
> +		info->battery_state = APM_BATTERY_ABSENT;
> +		return 0;
> +	}
> +
> +	if (sc->sc_fullchg_mah > 0)
> +		info->battery_life =
> +		    (100 * sc->sc_remaining_mah) / sc->sc_fullchg_mah;
> +
> +	if (sc->sc_b1st & SAM_B1ST_FULL) {
> +		info->battery_state = APM_BATT_HIGH;
> +	} else if (sc->sc_b1st & SAM_B1ST_CHARGE) {
> +		info->battery_state = APM_BATT_CHARGING;
> +	} else {
> +		if (info->battery_life > 50)
> +			info->battery_state = APM_BATT_HIGH;
> +		else if (info->battery_life > 25)
> +			info->battery_state = APM_BATT_LOW;
> +		else
> +			info->battery_state = APM_BATT_CRITICAL;
> +	}
> +
> +	/*
> +	 * Estimate minutes-left from instantaneous current (mA) and
> +	 * remaining capacity (mAh).  Only meaningful while discharging.
> +	 */
> +	if ((sc->sc_b1st & SAM_B1ST_CHARGE) == 0 && sc->sc_current_ma < 0) {
> +		int draw = -sc->sc_current_ma;
> +		if (draw > 0)
> +			info->minutes_left =
> +			    (60 * sc->sc_remaining_mah) / draw;
> +	}
> +
> +	return 0;
> +}
> +#endif
> Index: share/man/man4/sambat.4
> ===================================================================
> RCS file: share/man/man4/sambat.4
> diff -N share/man/man4/sambat.4
> --- /dev/null	1 Jan 1970 00:00:00 -0000
> +++ share/man/man4/sambat.4	17 May 2026 16:59:54 -0000
> @@ -0,0 +1,59 @@
> +.\"	$OpenBSD$
> +.\"
> +.\" Copyright (c) 2026 Marcus Glocker <mglocker@openbsd.org>
> +.\"
> +.\" Permission to use, copy, modify, and distribute this software for any
> +.\" purpose with or without fee is hereby granted, provided that the above
> +.\" copyright notice and this permission notice appear in all copies.
> +.\"
> +.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
> +.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
> +.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
> +.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
> +.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
> +.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
> +.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
> +.\"
> +.Dd $Mdocdate: May 17 2026 $
> +.Dt SAMBAT 4
> +.Os
> +.Sh NAME
> +.Nm sambat
> +.Nd Samsung laptop battery monitor
> +.Sh SYNOPSIS
> +.Cd "sambat* at iic?"
> +.Sh DESCRIPTION
> +The
> +.Nm
> +driver provides support for the embedded controller found in Samsung
> +laptops based on the Qualcomm Snapdragon X1E80100, such as the Galaxy
> +Book4 Edge.
> +The controller, identified as
> +.Dq SAM060B
> +in ACPI, is an ENE KB9058 micro controller running Samsung firmware,
> +and provides battery and AC adapter status over the I2C bus using a
> +vendor
> +.Dq Mbox
> +command protocol.
> +.Pp
> +Battery values are exposed through the
> +.Xr sysctl 8
> +sensor framework and the
> +.Xr apm 8
> +power management interface.
> +.Sh SEE ALSO
> +.Xr iic 4 ,
> +.Xr intro 4 ,
> +.Xr apm 8 ,
> +.Xr sysctl 8
> +.Sh HISTORY
> +The
> +.Nm
> +driver first appeared in
> +.Ox 8.0 .
> +.Sh AUTHORS
> +.An -nosplit
> +The
> +.Nm
> +driver was written by
> +.An Marcus Glocker Aq Mt mglocker@openbsd.org .
> Index: sysutils/firmware/arm64-qcom-dtb/Makefile
> ===================================================================
> RCS file: /cvs/ports/sysutils/firmware/arm64-qcom-dtb/Makefile,v
> diff -u -p -u -p -r1.28 Makefile
> --- sysutils/firmware/arm64-qcom-dtb/Makefile	29 Mar 2026 20:53:10 -0000	1.28
> +++ sysutils/firmware/arm64-qcom-dtb/Makefile	17 May 2026 17:16:57 -0000
> @@ -1,6 +1,6 @@
>  FW_DRIVER=	arm64-qcom-dtb
>  FW_VER=		2.7
> -REVISION=	0
> +REVISION=	1
>  
>  DISTNAME=	devicetree-rebasing-6.17-dts
>  
> Index: sysutils/firmware/arm64-qcom-dtb/patches/patch-src_arm64_qcom_x1e80100-samsung-galaxy-book4-edge_dts
> ===================================================================
> RCS file: /cvs/ports/sysutils/firmware/arm64-qcom-dtb/patches/patch-src_arm64_qcom_x1e80100-samsung-galaxy-book4-edge_dts,v
> diff -u -p -u -p -r1.2 patch-src_arm64_qcom_x1e80100-samsung-galaxy-book4-edge_dts
> --- sysutils/firmware/arm64-qcom-dtb/patches/patch-src_arm64_qcom_x1e80100-samsung-galaxy-book4-edge_dts	8 Sep 2024 18:53:25 -0000	1.2
> +++ sysutils/firmware/arm64-qcom-dtb/patches/patch-src_arm64_qcom_x1e80100-samsung-galaxy-book4-edge_dts	17 May 2026 17:16:57 -0000
> @@ -1,7 +1,7 @@
>  Index: src/arm64/qcom/x1e80100-samsung-galaxy-book4-edge.dts
>  --- src/arm64/qcom/x1e80100-samsung-galaxy-book4-edge.dts.orig
>  +++ src/arm64/qcom/x1e80100-samsung-galaxy-book4-edge.dts
> -@@ -0,0 +1,957 @@
> +@@ -0,0 +1,977 @@
>  +// SPDX-License-Identifier: BSD-3-Clause
>  +/*
>  + * Copyright (c) 2023 Qualcomm Innovation Center, Inc. All rights reserved.
> @@ -549,6 +549,26 @@ Index: src/arm64/qcom/x1e80100-samsung-g
>  +		pinctrl-names = "default";
>  +
>  +		wakeup-source;
> ++	};
> ++};
> ++
> ++/*
> ++ * Samsung Galaxy Book4 Edge embedded controller (ENE KB9058, Samsung
> ++ * SAM060B in ACPI).  Provides battery and AC adapter status via a
> ++ * vendor "Mbox" protocol on I2C.  Reverse-engineered Linux driver:
> ++ *   https://github.com/Saddytech/Galaxy-Book4-Edge-linux
> ++ *
> ++ * The DSDT _SB.ECTC._CRS lists this device on two channels:
> ++ *   - ACPI I2C1 (MMIO 0xb80000, FDT &i2c0) at addr 0x64 -- Mbox
> ++ *     command set used by the battery driver.
> ++ *   - ACPI I2C6 (MMIO 0xb94000, FDT &i2c5) at addr 0x62 -- "SABI"
> ++ *     command set used by fan/keyboard backlight tools.
> ++ * Only the Mbox channel (0x64 on &i2c0) is needed for battery status.
> ++ */
> ++&i2c0 {
> ++	ec: ec@64 {
> ++		compatible = "samsung,galaxybook-battery";
> ++		reg = <0x64>;
>  +	};
>  +};
>  +
> 
>