/*	$OpenBSD: apm.c,v 1.61 2004/03/11 17:20:33 millert Exp $	*/

/*-
 * Copyright (c) 1998-2001 Michael Shalayeff. All rights reserved.
 * Copyright (c) 1995 John T. Kohl.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. All advertising materials mentioning features or use of this software
 *    must display the following acknowledgement:
 *	This product includes software developed by the University of
 *	California, Berkeley and its contributors.
 * 4. Neither the name of the University nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF MIND, USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 */

#include "apm.h"

#if NAPM > 1
#error only one APM device may be configured
#endif

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/signalvar.h>
#include <sys/kernel.h>
#include <sys/kthread.h>
#include <sys/lock.h>
#include <sys/proc.h>
#include <sys/user.h>
#include <sys/malloc.h>
#include <sys/device.h>
#include <sys/fcntl.h>
#include <sys/ioctl.h>
#include <sys/event.h>
#include <sys/mount.h>	/* for vfs_syncwait() proto */

#include <machine/conf.h>
#include <machine/cpu.h>
#include <machine/cpufunc.h>
#include <machine/gdt.h>
#include <machine/psl.h>

#include <dev/isa/isareg.h>
#include <i386/isa/isa_machdep.h>
#include <i386/isa/nvram.h>
#include <dev/isa/isavar.h>

#include <machine/biosvar.h>
#include <machine/apmvar.h>

#if defined(APMDEBUG)
#define DPRINTF(x)	printf x
#else
#define	DPRINTF(x)	/**/
#endif

#define	APM_LOCK(sc)	lockmgr(&(sc)->sc_lock, LK_EXCLUSIVE, NULL, curproc)
#define	APM_UNLOCK(sc)	lockmgr(&(sc)->sc_lock, LK_RELEASE, NULL, curproc)

struct cfdriver apm_cd = {
	NULL, "apm", DV_DULL
};

struct apm_softc {
	struct device sc_dev;
	struct klist sc_note;
	int	sc_flags;
	int	batt_life;
	struct proc *sc_thread;
	struct lock sc_lock;
};
#define	SCFLAG_OREAD	0x0000001
#define	SCFLAG_OWRITE	0x0000002
#define	SCFLAG_OPEN	(SCFLAG_OREAD|SCFLAG_OWRITE)

int apmprobe(struct device *, void *, void *);
void apmattach(struct device *, struct device *, void *);

struct cfattach apm_ca = {
	sizeof(struct apm_softc), apmprobe, apmattach
};

void filt_apmrdetach(struct knote *kn);
int filt_apmread(struct knote *kn, long hint);

struct filterops apmread_filtops =
	{ 1, NULL, filt_apmrdetach, filt_apmread};

/* battery percentage at where we get verbose in our warnings.  This
   value can be changed using sysctl(8), value machdep.apmwarn.
   Setting it to zero kills all warnings */
int cpu_apmwarn = 10;

#define	APM_RESUME_HOLDOFF	3

/*
 * Flags to control kernel display
 *	SCFLAG_NOPRINT:		do not output APM power messages due to
 *				a power change event.
 *
 *	SCFLAG_PCTPRINT:	do not output APM power messages due to
 *				to a power change event unless the battery
 *				percentage changes.
 */
#define SCFLAG_NOPRINT	0x0008000
#define SCFLAG_PCTPRINT	0x0004000
#define SCFLAG_PRINT	(SCFLAG_NOPRINT|SCFLAG_PCTPRINT)

#define	APMUNIT(dev)	(minor(dev)&0xf0)
#define	APMDEV(dev)	(minor(dev)&0x0f)
#define APMDEV_NORMAL	0
#define APMDEV_CTL	8

int apm_standbys;
int apm_userstandbys;
int apm_suspends;
int apm_resumes;
int apm_battlow;
int apm_evindex;
int apm_error;
int apm_op_inprog;

u_int apm_flags;
u_char apm_majver;
u_char apm_minver;
int apm_dobusy = 1;
int apm_doidle = 1;
int apm_bebatt = 0;

struct {
	u_int32_t entry;
	u_int16_t seg;
	u_int16_t pad;
} apm_ep;

struct apmregs {
	u_int32_t	ax;
	u_int32_t	bx;
	u_int32_t	cx;
	u_int32_t	dx;
};

int  apmcall(u_int, u_int, struct apmregs *);
void apm_power_print(struct apm_softc *, struct apmregs *);
int  apm_handle_event(struct apm_softc *, struct apmregs *);
void apm_set_ver(struct apm_softc *);
int  apm_periodic_check(struct apm_softc *);
void apm_thread_create(void *v);
void apm_thread(void *);
void apm_disconnect(struct apm_softc *);
void apm_perror(const char *, struct apmregs *);
void apm_powmgt_enable(int onoff);
void apm_powmgt_engage(int onoff, u_int devid);
/* void apm_devpowmgt_enable(int onoff, u_int devid); */
int  apm_record_event(struct apm_softc *sc, u_int type);
const char *apm_err_translate(int code);

#define	apm_get_powstat(r) apmcall(APM_POWER_STATUS, APM_DEV_ALLDEVS, r)
void	apm_standby(void);
void	apm_suspend(void);
void	apm_resume(struct apm_softc *, struct apmregs *);

static int __inline
apm_get_event(struct apmregs *r)
{
	int rv;
	bzero(r, sizeof(*r));
	rv = apmcall(APM_GET_PM_EVENT, 0, r);
	return rv;
}

const char *
apm_err_translate(code)
	int code;
{
	switch(code) {
	case APM_ERR_PM_DISABLED:
		return "power management disabled";
	case APM_ERR_REALALREADY:
		return "real mode interface already connected";
	case APM_ERR_NOTCONN:
		return "interface not connected";
	case APM_ERR_16ALREADY:
		return "16-bit interface already connected";
	case APM_ERR_16NOTSUPP:
		return "16-bit interface not supported";
	case APM_ERR_32ALREADY:
		return "32-bit interface already connected";
	case APM_ERR_32NOTSUPP:
		return "32-bit interface not supported";
	case APM_ERR_UNRECOG_DEV:
		return "unrecognized device ID";
	case APM_ERR_ERANGE:
		return "parameter out of range";
	case APM_ERR_NOTENGAGED:
		return "interface not engaged";
	case APM_ERR_UNABLE:
		return "unable to enter requested state";
	case APM_ERR_NOEVENTS:
		return "No pending events";
	case APM_ERR_NOT_PRESENT:
		return "No APM present";
	default:
		return "unknown error code?";
	}
}

int apmerrors = 0;

void
apm_perror(str, regs)
	const char *str;
	struct apmregs *regs;
{
	printf("apm0: APM %s: %s (%d)\n", str,
	    apm_err_translate(APM_ERR_CODE(regs)),
	    APM_ERR_CODE(regs));
	delay(1000000);

	apmerrors++;
}

void
apm_power_print (sc, regs)
	struct apm_softc *sc;
	struct apmregs *regs;
{
#if !defined(APM_NOPRINT)
	sc->batt_life = BATT_LIFE(regs);
	if (BATT_LIFE(regs) != APM_BATT_LIFE_UNKNOWN) {
		printf("%s: battery life expectancy %d%%\n",
		    sc->sc_dev.dv_xname,
		    BATT_LIFE(regs));
	}
	printf("%s: AC ", sc->sc_dev.dv_xname);
	switch (AC_STATE(regs)) {
	case APM_AC_OFF:
		printf("off,");
		break;
	case APM_AC_ON:
		printf("on,");
		break;
	case APM_AC_BACKUP:
		printf("backup power,");
		break;
	default:
	case APM_AC_UNKNOWN:
		printf("unknown,");
		break;
	}
	if (apm_minver == 0) {
		printf(" battery is ");
		switch (BATT_STATE(regs)) {
		case APM_BATT_HIGH:
			printf("high");
			break;
		case APM_BATT_LOW:
			printf("low");
			break;
		case APM_BATT_CRITICAL:
			printf("CRITICAL");
			break;
		case APM_BATT_CHARGING:
			printf("charging");
			break;
		case APM_BATT_UNKNOWN:
			printf("unknown");
			break;
		default:
			printf("undecoded (%x)", BATT_STATE(regs));
			break;
		}
	} else if (apm_minver >= 1) {
		if (BATT_FLAGS(regs) & APM_BATT_FLAG_NOBATTERY)
			printf(" no battery");
		else {
			printf(" battery charge ");
			if (BATT_FLAGS(regs) & APM_BATT_FLAG_HIGH)
				printf("high");
			else if (BATT_FLAGS(regs) & APM_BATT_FLAG_LOW)
				printf("low");
			else if (BATT_FLAGS(regs) & APM_BATT_FLAG_CRITICAL)
				printf("critical");
			else
				printf("unknown");
			if (BATT_FLAGS(regs) & APM_BATT_FLAG_CHARGING)
				printf(", charging");
			if (BATT_REM_VALID(regs)) {
				int life = BATT_REMAINING(regs);
				if (apm_bebatt)
					life = swap16(life);
				printf(", estimated %d:%02d hours",
				    life / 60, life % 60);
			}
		}
	}

	printf("\n");
#endif
}

void
apm_suspend()
{
	dopowerhooks(PWR_SUSPEND);

	if (cold)
		vfs_syncwait(0);

	(void)apm_set_powstate(APM_DEV_ALLDEVS, APM_SYS_SUSPEND);
}

void
apm_standby()
{
	dopowerhooks(PWR_STANDBY);

	if (cold)
		vfs_syncwait(0);

	(void)apm_set_powstate(APM_DEV_ALLDEVS, APM_SYS_STANDBY);
}

void
apm_resume(sc, regs)
	struct apm_softc *sc;
	struct apmregs *regs;
{
	apm_resumes = APM_RESUME_HOLDOFF;

	/* they say that some machines may require reinitializing the clock */
	initrtclock();

	inittodr(time.tv_sec);
	/* lower bit in cx means pccard was powered down */
	dopowerhooks(PWR_RESUME);
	apm_record_event(sc, regs->bx);
	
	/* acknowledge any rtc interrupt we may have missed */
	rtcdrain(NULL);
}

int
apm_record_event(sc, type)
	struct apm_softc *sc;
	u_int type;
{
	if (!apm_error && (sc->sc_flags & SCFLAG_OPEN) == 0) {
		DPRINTF(("apm_record_event: no user waiting\n"));
		apm_error++;
		return 1;
	}

	apm_evindex++;
	KNOTE(&sc->sc_note, APM_EVENT_COMPOSE(type, apm_evindex));

	return (0);
}

int
apm_handle_event(sc, regs)
	struct apm_softc *sc;
	struct apmregs *regs;
{
	struct apmregs nregs;
	int ret = 0;

	switch(regs->bx) {
	case APM_NOEVENT:
		ret++;
		break;

	case APM_USER_STANDBY_REQ:
		if (apm_resumes || apm_op_inprog)
			break;
		DPRINTF(("user wants STANDBY--fat chance\n"));
		apm_op_inprog++;
		if (apm_record_event(sc, regs->bx)) {
			DPRINTF(("standby ourselves\n"));
			apm_userstandbys++;
		}
		break;
	case APM_STANDBY_REQ:
		if (apm_resumes || apm_op_inprog)
			break;
		DPRINTF(("standby requested\n"));
		if (apm_standbys || apm_suspends) {
			DPRINTF(("premature standby\n"));
			apm_error++;
			ret++;
		}
		apm_op_inprog++;
		if (apm_record_event(sc, regs->bx)) {
			DPRINTF(("standby ourselves\n"));
			apm_standbys++;
		}
		break;
	case APM_USER_SUSPEND_REQ:
		if (apm_resumes || apm_op_inprog)
			break;
		DPRINTF(("user wants suspend--fat chance!\n"));
		apm_op_inprog++;
		if (apm_record_event(sc, regs->bx)) {
			DPRINTF(("suspend ourselves\n"));
			apm_suspends++;
		}
		break;
	case APM_SUSPEND_REQ:
		if (apm_resumes || apm_op_inprog)
			break;
		DPRINTF(("suspend requested\n"));
		if (apm_standbys || apm_suspends) {
			DPRINTF(("premature suspend\n"));
			apm_error++;
			ret++;
		}
		apm_op_inprog++;
		if (apm_record_event(sc, regs->bx)) {
			DPRINTF(("suspend ourselves\n"));
			apm_suspends++;
		}
		break;
	case APM_POWER_CHANGE:
		DPRINTF(("power status change\n"));
		if (apm_get_powstat(&nregs) == 0 &&
		    BATT_LIFE(&nregs) != APM_BATT_LIFE_UNKNOWN &&
		    BATT_LIFE(&nregs) < cpu_apmwarn &&
		    (sc->sc_flags & SCFLAG_PRINT) != SCFLAG_NOPRINT &&
		    ((sc->sc_flags & SCFLAG_PRINT) != SCFLAG_PCTPRINT ||
		     sc->batt_life != BATT_LIFE(&nregs)))
			apm_power_print(sc, &nregs);
		apm_record_event(sc, regs->bx);
		break;
	case APM_NORMAL_RESUME:
		DPRINTF(("system resumed\n"));
		apm_resume(sc, regs);
		break;
	case APM_CRIT_RESUME:
		DPRINTF(("system resumed without us!\n"));
		apm_resume(sc, regs);
		break;
	case APM_SYS_STANDBY_RESUME:
		DPRINTF(("system standby resume\n"));
		apm_resume(sc, regs);
		break;
	case APM_UPDATE_TIME:
		DPRINTF(("update time, please\n"));
		inittodr(time.tv_sec);
		apm_record_event(sc, regs->bx);
		break;
	case APM_CRIT_SUSPEND_REQ:
		DPRINTF(("suspend required immediately\n"));
		apm_record_event(sc, regs->bx);
		apm_suspend();
		break;
	case APM_BATTERY_LOW:
		DPRINTF(("Battery low!\n"));
		apm_battlow++;
		apm_record_event(sc, regs->bx);
		break;
	case APM_CAPABILITY_CHANGE:
		DPRINTF(("capability change\n"));
		if (apm_minver < 2) {
			DPRINTF(("adult event\n"));
		} else {
			if (apmcall(APM_GET_CAPABILITIES, APM_DEV_APM_BIOS,
			    &nregs) != 0) {
				apm_perror("get capabilities", &nregs);
			} else {
				apm_get_powstat(&nregs);
			}
		}
		break;
	default: {
#ifdef APMDEBUG
		char *p;
		switch (regs->bx >> 8) {
		case 0:	p = "reserved system";	break;
		case 1:	p = "reserved device";	break;
		case 2:	p = "OEM defined";	break;
		default:p = "reserved";		break;
		}
#endif
		DPRINTF(("apm_handle_event: %s event, code %d\n", p, regs->bx));
	    }
	}

	return ret;
}

int
apm_periodic_check(sc)
	struct apm_softc *sc;
{
	struct apmregs regs;
	int ret = 0;

	if (apm_op_inprog)
		apm_set_powstate(APM_DEV_ALLDEVS, APM_LASTREQ_INPROG);

	while (1) {
		if (apm_get_event(&regs) != 0) {
			/* i think some bioses combine the error codes */
			if (!(APM_ERR_CODE(&regs) & APM_ERR_NOEVENTS))
				apm_perror("get event", &regs);
			break;
		}

		if (apm_handle_event(sc, &regs))
			break;
	}

	if (apm_error || APM_ERR_CODE(&regs) == APM_ERR_NOTCONN)
		ret = -1;

	if (apm_suspends /*|| (apm_battlow && apm_userstandbys)*/) {
		apm_op_inprog = 0;
		apm_suspend();
	} else if (apm_standbys || apm_userstandbys) {
		apm_op_inprog = 0;
		apm_standby();
	}
	apm_suspends = apm_standbys = apm_battlow = apm_userstandbys = 0;
	apm_error = 0;

	if (apm_resumes)
		apm_resumes--;
	return (ret);
}

void
apm_powmgt_enable(onoff)
	int onoff;
{
	struct apmregs regs;
	bzero(&regs, sizeof(regs));
	regs.cx = onoff ? APM_MGT_ENABLE : APM_MGT_DISABLE;
	if (apmcall(APM_PWR_MGT_ENABLE,
	    (apm_minver? APM_DEV_APM_BIOS : APM_MGT_ALL), &regs) != 0)
		apm_perror("power management enable", &regs);
}

void
apm_powmgt_engage(onoff, dev)
	int onoff;
	u_int dev;
{
	struct apmregs regs;
	if (apm_minver == 0)
		return;
	bzero(&regs, sizeof(regs));
	regs.cx = onoff ? APM_MGT_ENGAGE : APM_MGT_DISENGAGE;
	if (apmcall(APM_PWR_MGT_ENGAGE, dev, &regs) != 0)
		printf("apm0: APM engage (device %x): %s (%d)\n",
		    dev, apm_err_translate(APM_ERR_CODE(&regs)),
		    APM_ERR_CODE(&regs));
}

#ifdef notused
void
apm_devpowmgt_enable(onoff, dev)
	int onoff;
	u_int dev;
{
	struct apmregs regs;
	if (apm_minver == 0)
		return;
	/* enable is auto BIOS management.
	 * disable is program control.
	 */
	bzero(&regs, sizeof(regs));
	regs.cx = onoff ? APM_MGT_ENABLE : APM_MGT_DISABLE;
	if (apmcall(APM_DEVICE_MGMT_ENABLE, dev, &regs) != 0)
		printf("APM device engage (device %x): %s (%d)\n",
		    dev, apm_err_translate(APM_ERR_CODE(&regs)),
		    APM_ERR_CODE(&regs));
}
#endif

int
apm_set_powstate(dev, state)
	u_int dev, state;
{
	struct apmregs regs;
	if (!apm_cd.cd_ndevs || (apm_minver == 0 && state > APM_SYS_OFF))
		return EINVAL;
	bzero(&regs, sizeof(regs));
	regs.cx = state;
	if (apmcall(APM_SET_PWR_STATE, dev, &regs) != 0) {
		apm_perror("set power state", &regs);
		if (APM_ERR_CODE(&regs) == APM_ERR_UNRECOG_DEV)
			return ENXIO;
		else
			return EIO;
	}
	return 0;
}

void
apm_cpu_busy()
{
	struct apmregs regs;
	if (!apm_cd.cd_ndevs || !apm_doidle)
		return;
	bzero(&regs, sizeof(regs));
	if ((apm_flags & APM_IDLE_SLOWS) &&
		apmcall(APM_CPU_BUSY, 0, &regs) != 0) {

#ifdef DIAGNOSTIC
		apm_perror("set CPU busy", &regs);
#endif
	}
}

void
apm_cpu_idle()
{
	struct apmregs regs;
	if (!apm_cd.cd_ndevs || !apm_doidle)
		return;

	bzero(&regs, sizeof(regs));
	if (apmcall(APM_CPU_IDLE, 0, &regs) != 0) {

#ifdef DIAGNOSTIC
		apm_perror("set CPU idle", &regs);
#endif
	}
}

void
apm_set_ver(self)
	struct apm_softc *self;
{
	struct apmregs regs;
	int rv = 0;

	bzero(&regs, sizeof(regs));
	regs.cx = APM_VERSION;

	if (APM_MAJOR(apm_flags) == 1 && APM_MINOR(apm_flags) == 2 &&
	    (rv = apmcall(APM_DRIVER_VERSION, APM_DEV_APM_BIOS, &regs)) == 0) {
		apm_majver = APM_CONN_MAJOR(&regs);
		apm_minver = APM_CONN_MINOR(&regs);
	} else {
#ifdef APMDEBUG
		if (rv)
			apm_perror("set version 1.2", &regs);
#endif
		/* try downgrading to 1.1 */
		bzero(&regs, sizeof(regs));
		regs.cx = 0x0101;

		if (apmcall(APM_DRIVER_VERSION, APM_DEV_APM_BIOS, &regs) == 0) {
			apm_majver = 1;
			apm_minver = 1;
		} else {
#ifdef APMDEBUG
			apm_perror("set version 1.1", &regs);
#endif
			/* stay w/ flags then */
			apm_majver = APM_MAJOR(apm_flags);
			apm_minver = APM_MINOR(apm_flags);

			/* fix version for some endianess-challenged compaqs */
			if (!apm_majver) {
				apm_majver = 1;
				apm_minver = 0;
			}
		}
	}
	printf(": Power Management spec V%d.%d", apm_majver, apm_minver);
	if (apm_flags & APM_IDLE_SLOWS) {
		DPRINTF((" (slowidle)"));
	} else
		apm_dobusy = 0;
#ifdef DIAGNOSTIC
	if (apm_flags & APM_BIOS_PM_DISABLED)
		printf(" (BIOS mgmt disabled)");
	if (apm_flags & APM_BIOS_PM_DISENGAGED)
		printf(" (BIOS managing devices)");
#endif
	printf("\n");
}

void
apm_disconnect(sc)
	struct apm_softc *sc;
{
	struct apmregs regs;
	bzero(&regs, sizeof(regs));
	if (apmcall(APM_SYSTEM_DEFAULTS,
	    (apm_minver == 1 ? APM_DEV_ALLDEVS : APM_DEFAULTS_ALL), &regs))
		apm_perror("system defaults failed", &regs);

	if (apmcall(APM_DISCONNECT, APM_DEV_APM_BIOS, &regs))
		apm_perror("disconnect failed", &regs);
	else
		printf("%s: disconnected\n", sc->sc_dev.dv_xname);
}

int
apmprobe(parent, match, aux)
	struct device *parent;
	void *match, *aux;
{
	struct bios_attach_args *ba = aux;
	bios_apminfo_t *ap = ba->bios_apmp;
	bus_space_handle_t ch, dh;

	if (apm_cd.cd_ndevs ||
	    strcmp(ba->bios_dev, "apm") ||
	    !(ba->bios_apmp->apm_detail & APM_32BIT_SUPPORTED)) {
		DPRINTF(("%s: %x\n", ba->bios_dev, ba->bios_apmp->apm_detail));
		return 0;
	}

	/* addresses check
	   since pc* console and vga* probes much later
	   we cannot check for video memory being mapped
	   for apm stuff w/ bus_space_map() */
	if (ap->apm_code_len == 0 ||
	    (ap->apm_code32_base < IOM_BEGIN &&
	     ap->apm_code32_base + ap->apm_code_len > IOM_BEGIN) ||
	    (ap->apm_code16_base < IOM_BEGIN &&
	     ap->apm_code16_base + ap->apm_code16_len > IOM_BEGIN) ||
	    (ap->apm_data_base < IOM_BEGIN &&
	     ap->apm_data_base + ap->apm_data_len > IOM_BEGIN))
		return 0;

	if (bus_space_map(ba->bios_memt, ap->apm_code32_base,
	    ap->apm_code_len, 1, &ch) != 0) {
		DPRINTF(("apm0: can't map code\n"));
		return 0;
	}
	bus_space_unmap(ba->bios_memt, ch, ap->apm_code_len);

	if (bus_space_map(ba->bios_memt, ap->apm_data_base,
	    ap->apm_data_len, 1, &dh) != 0) {
		DPRINTF(("apm0: can't map data\n"));
		return 0;
	}
	bus_space_unmap(ba->bios_memt, dh, ap->apm_data_len);

	return 1;
}

void
apmattach(parent, self, aux)
	struct device *parent, *self;
	void *aux;
{
	extern union descriptor *dynamic_gdt;
	struct bios_attach_args *ba = aux;
	bios_apminfo_t *ap = ba->bios_apmp;
	struct apm_softc *sc = (void *)self;
	struct apmregs regs;
	u_int cbase, clen, l;
	bus_space_handle_t ch16, ch32, dh;

	apm_flags = ap->apm_detail;
	/*
	 * set up GDT descriptors for APM
	 */
	if (apm_flags & APM_32BIT_SUPPORTED) {

		/* truncate segments' limits to a page */
		ap->apm_code_len -= (ap->apm_code32_base +
		    ap->apm_code_len + 1) & 0xfff;
		ap->apm_code16_len -= (ap->apm_code16_base +
		    ap->apm_code16_len + 1) & 0xfff;
		ap->apm_data_len -= (ap->apm_data_base +
		    ap->apm_data_len + 1) & 0xfff;

		/* adjust version */
		if ((sc->sc_dev.dv_cfdata->cf_flags & APM_VERMASK) &&
		    (apm_flags & APM_VERMASK) !=
		    (sc->sc_dev.dv_cfdata->cf_flags & APM_VERMASK))
			apm_flags = (apm_flags & ~APM_VERMASK) |
			    (sc->sc_dev.dv_cfdata->cf_flags & APM_VERMASK);
		if (sc->sc_dev.dv_cfdata->cf_flags & APM_NOCLI) {
			extern int apm_cli; /* from apmcall.S */
			apm_cli = 0;
		}
		if (sc->sc_dev.dv_cfdata->cf_flags & APM_BEBATT)
			apm_bebatt = 1;
		apm_ep.seg = GSEL(GAPM32CODE_SEL,SEL_KPL);
		apm_ep.entry = ap->apm_entry;
		cbase = min(ap->apm_code32_base, ap->apm_code16_base);
		clen = max(ap->apm_code32_base + ap->apm_code_len,
			   ap->apm_code16_base + ap->apm_code16_len) - cbase;
		if ((cbase <= ap->apm_data_base &&
		     cbase + clen >= ap->apm_data_base) ||
		    (ap->apm_data_base <= cbase &&
		     ap->apm_data_base + ap->apm_data_len >= cbase)) {
			l = max(ap->apm_data_base + ap->apm_data_len + 1,
				cbase + clen + 1) -
			    min(ap->apm_data_base, cbase);
			bus_space_map(ba->bios_memt,
				min(ap->apm_data_base, cbase),
				l, 1, &dh);
			ch16 = dh;
			if (ap->apm_data_base < cbase)
				ch16 += cbase - ap->apm_data_base;
			else
				dh += ap->apm_data_base - cbase;
		} else {

			bus_space_map(ba->bios_memt, cbase, clen + 1, 1, &ch16);
			bus_space_map(ba->bios_memt, ap->apm_data_base,
			    ap->apm_data_len + 1, 1, &dh);
		}
		ch32 = ch16;
		if (ap->apm_code16_base == cbase)
			ch32 += ap->apm_code32_base - cbase;
		else
			ch16 += ap->apm_code16_base - cbase;

		setsegment(&dynamic_gdt[GAPM32CODE_SEL].sd, (void *)ch32,
			   ap->apm_code_len, SDT_MEMERA, SEL_KPL, 1, 0);
		setsegment(&dynamic_gdt[GAPM16CODE_SEL].sd, (void *)ch16,
			   ap->apm_code16_len, SDT_MEMERA, SEL_KPL, 0, 0);
		setsegment(&dynamic_gdt[GAPMDATA_SEL].sd, (void *)dh,
			   ap->apm_data_len, SDT_MEMRWA, SEL_KPL, 1, 0);
		DPRINTF((": flags %x code 32:%x/%x[%x] 16:%x/%x[%x] "
		       "data %x/%x/%x ep %x (%x:%x)\n%s", apm_flags,
		    ap->apm_code32_base, ch32, ap->apm_code_len,
		    ap->apm_code16_base, ch16, ap->apm_code16_len,
		    ap->apm_data_base, dh, ap->apm_data_len,
		    ap->apm_entry, apm_ep.seg, ap->apm_entry+ch32,
		    sc->sc_dev.dv_xname));

		apm_set_ver(sc);

		if (apm_flags & APM_BIOS_PM_DISABLED)
			apm_powmgt_enable(1);
		/*
		 * Engage cooperative power mgt (we get to do it)
		 * on all devices (v1.1).
		 */
		apm_powmgt_engage(1, APM_DEV_ALLDEVS);

		bzero(&regs, sizeof(regs));
		if (apm_get_powstat(&regs) == 0) {
			apm_power_print(sc, &regs);
		} else
			apm_perror("get power status", &regs);
		apm_cpu_busy();

		lockinit(&sc->sc_lock, PWAIT, "apmlk", 0, 0);

		/*
		 * Do a check once, ignoring any errors. This avoids
		 * gratuitous APM disconnects on laptops where the first
		 * event in the queue (after a boot) is non-recognizable.
		 * The IBM ThinkPad 770Z is one of those.
		 */
		apm_periodic_check(sc);

		if (apm_periodic_check(sc) == -1) {
			apm_disconnect(sc);
			apm_dobusy = apm_doidle = 0;
		} else
			kthread_create_deferred(apm_thread_create, sc);
	} else {
		dynamic_gdt[GAPM32CODE_SEL] = dynamic_gdt[GNULL_SEL];
		dynamic_gdt[GAPM16CODE_SEL] = dynamic_gdt[GNULL_SEL];
		dynamic_gdt[GAPMDATA_SEL] = dynamic_gdt[GNULL_SEL];
	}
}

void
apm_thread_create(v)
	void *v;
{
	struct apm_softc *sc = v;
	if (kthread_create(apm_thread, sc, &sc->sc_thread,
	    "%s", sc->sc_dev.dv_xname)) {
		apm_disconnect(sc);
		printf("%s: failed to create kernel thread, disabled",
		    sc->sc_dev.dv_xname);
	}
}

void
apm_thread(v)
	void *v;
{
	struct apm_softc *sc = v;

	for (;;) {
		APM_LOCK(sc);
		(void) apm_periodic_check(sc);
		APM_UNLOCK(sc);
		tsleep(&lbolt, PWAIT, "apmev", 0);
	}
}

int
apmopen(dev, flag, mode, p)
	dev_t dev;
	int flag, mode;
	struct proc *p;
{
	struct apm_softc *sc;
	int error = 0;

	/* apm0 only */
	if (!apm_cd.cd_ndevs || APMUNIT(dev) != 0 ||
	    !(sc = apm_cd.cd_devs[APMUNIT(dev)]))
		return ENXIO;

	DPRINTF(("apmopen: dev %d pid %d flag %x mode %x\n",
	    APMDEV(dev), p->p_pid, flag, mode));

	APM_LOCK(sc);
	switch (APMDEV(dev)) {
	case APMDEV_CTL:
		if (!(flag & FWRITE)) {
			error = EINVAL;
			break;
		}
		if (sc->sc_flags & SCFLAG_OWRITE) {
			error = EBUSY;
			break;
		}
		sc->sc_flags |= SCFLAG_OWRITE;
		break;
	case APMDEV_NORMAL:
		if (!(flag & FREAD) || (flag & FWRITE)) {
			error = EINVAL;
			break;
		}
		sc->sc_flags |= SCFLAG_OREAD;
		break;
	default:
		error = ENXIO;
		break;
	}
	APM_UNLOCK(sc);
	return error;
}

int
apmclose(dev, flag, mode, p)
	dev_t dev;
	int flag, mode;
	struct proc *p;
{
	struct apm_softc *sc;

	/* apm0 only */
	if (!apm_cd.cd_ndevs || APMUNIT(dev) != 0 ||
	    !(sc = apm_cd.cd_devs[APMUNIT(dev)]))
		return ENXIO;

	DPRINTF(("apmclose: pid %d flag %x mode %x\n", p->p_pid, flag, mode));

	APM_LOCK(sc);
	switch (APMDEV(dev)) {
	case APMDEV_CTL:
		sc->sc_flags &= ~SCFLAG_OWRITE;
		break;
	case APMDEV_NORMAL:
		sc->sc_flags &= ~SCFLAG_OREAD;
		break;
	}
	APM_UNLOCK(sc);
	return 0;
}

int
apmioctl(dev, cmd, data, flag, p)
	dev_t dev;
	u_long cmd;
	caddr_t data;
	int flag;
	struct proc *p;
{
	struct apm_softc *sc;
	struct apmregs regs;
	int error = 0;

	/* apm0 only */
	if (!apm_cd.cd_ndevs || APMUNIT(dev) != 0 ||
	    !(sc = apm_cd.cd_devs[APMUNIT(dev)]))
		return ENXIO;

	APM_LOCK(sc);
	switch (cmd) {
		/* some ioctl names from linux */
	case APM_IOC_STANDBY:
		if ((flag & FWRITE) == 0)
			error = EBADF;
		else
			apm_userstandbys++;
		break;
	case APM_IOC_SUSPEND:
		if ((flag & FWRITE) == 0)
			error = EBADF;
		else
			apm_suspends++;
		break;
	case APM_IOC_PRN_CTL:
		if ((flag & FWRITE) == 0)
			error = EBADF;
		else {
			int flag = *(int *)data;
			DPRINTF(( "APM_IOC_PRN_CTL: %d\n", flag ));
			switch (flag) {
			case APM_PRINT_ON:	/* enable printing */
				sc->sc_flags &= ~SCFLAG_PRINT;
				break;
			case APM_PRINT_OFF: /* disable printing */
				sc->sc_flags &= ~SCFLAG_PRINT;
				sc->sc_flags |= SCFLAG_NOPRINT;
				break;
			case APM_PRINT_PCT: /* disable some printing */
				sc->sc_flags &= ~SCFLAG_PRINT;
				sc->sc_flags |= SCFLAG_PCTPRINT;
				break;
			default:
				error = EINVAL;
				break;
			}
		}
		break;
	case APM_IOC_DEV_CTL:
		if ((flag & FWRITE) == 0)
			error = EBADF;
		else {
			struct apm_ctl *actl = (struct apm_ctl *)data;

			bzero(&regs, sizeof(regs));
			if (!apmcall(APM_GET_POWER_STATE, actl->dev, &regs))
				printf("%s: dev %04x state %04x\n",
				    sc->sc_dev.dv_xname, dev, regs.cx);

			error = apm_set_powstate(actl->dev, actl->mode);
		}
		break;
	case APM_IOC_GETPOWER:
		if (apm_get_powstat(&regs) == 0) {
			struct apm_power_info *powerp =
			    (struct apm_power_info *)data;

			bzero(powerp, sizeof(*powerp));
			if (BATT_LIFE(&regs) != APM_BATT_LIFE_UNKNOWN)
				powerp->battery_life = BATT_LIFE(&regs);
			powerp->ac_state = AC_STATE(&regs);
			switch (apm_minver) {
			case 0:
				if (!(BATT_FLAGS(&regs) & APM_BATT_FLAG_NOBATTERY))
					powerp->battery_state = BATT_STATE(&regs);
				break;
			case 1:
			default:
				if (BATT_FLAGS(&regs) & APM_BATT_FLAG_HIGH)
					powerp->battery_state = APM_BATT_HIGH;
				else if (BATT_FLAGS(&regs) & APM_BATT_FLAG_LOW)
					powerp->battery_state = APM_BATT_LOW;
				else if (BATT_FLAGS(&regs) & APM_BATT_FLAG_CRITICAL)
					powerp->battery_state = APM_BATT_CRITICAL;
				else if (BATT_FLAGS(&regs) & APM_BATT_FLAG_CHARGING)
					powerp->battery_state = APM_BATT_CHARGING;
				else if (BATT_FLAGS(&regs) & APM_BATT_FLAG_NOBATTERY)
					powerp->battery_state = APM_BATTERY_ABSENT;
				else
					powerp->battery_state = APM_BATT_UNKNOWN;
				if (BATT_REM_VALID(&regs)) {
					powerp->minutes_left = BATT_REMAINING(&regs);
					if (apm_bebatt)
						powerp->minutes_left =
						    swap16(powerp->minutes_left);
				}
			}
		} else {
			apm_perror("ioctl get power status", &regs);
			error = EIO;
		}
		break;

	default:
		error = ENOTTY;
	}

	APM_UNLOCK(sc);
	return error;
}

void
filt_apmrdetach(kn)
	struct knote *kn;
{
	struct apm_softc *sc = (struct apm_softc *)kn->kn_hook;

	APM_LOCK(sc);
	SLIST_REMOVE(&sc->sc_note, kn, knote, kn_selnext);
	APM_UNLOCK(sc);
}

int
filt_apmread(kn, hint)
	struct knote *kn;
	long hint;
{
	/* XXX weird kqueue_scan() semantics */
	if (hint && !kn->kn_data)
		kn->kn_data = (int)hint;

	return (1);
}

int
apmkqfilter(dev, kn)
	dev_t dev;
	struct knote *kn;
{
	struct apm_softc *sc;

	/* apm0 only */
	if (!apm_cd.cd_ndevs || APMUNIT(dev) != 0 ||
	    !(sc = apm_cd.cd_devs[APMUNIT(dev)]))
		return ENXIO;

	switch (kn->kn_filter) {
	case EVFILT_READ:
		kn->kn_fop = &apmread_filtops;
		break;
	default:
		return (1);
	}

	kn->kn_hook = (caddr_t)sc;

	APM_LOCK(sc);
	SLIST_INSERT_HEAD(&sc->sc_note, kn, kn_selnext);
	APM_UNLOCK(sc);

	return (0);
}
