
/*********************************************************************
 *                
 * Filename:      thinkpadpm.c
 * Description:   thinkpadpm.o is a kernel module that serves as
 *                an interface between the thinkpad module and the
 *                kernel power management drivers
 * Author:        Thomas Hood
 * Created:       27 January 2001
 *
 * Please report bugs to the author ASAP.
 * 
 *     Copyright (c) 1999 J.D. Thomas Hood, All rights reserved
 *     
 *     This program is free software; you can redistribute it and/or 
 *     modify it under the terms of the GNU General Public License as 
 *     published by the Free Software Foundation; either version 2 of 
 *     the License, or (at your option) any later version.
 * 
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *     GNU General Public License for more details.
 * 
 *     To receive a copy of the GNU General Public License, please write
 *     to the Free Software Foundation, Inc., 59 Temple Place, Suite 330,
 *     Boston, MA 02111-1307 USA
 *     
 ********************************************************************/

#include "thinkpad_driver.h"

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/proc_fs.h>
#include <linux/devfs_fs_kernel.h>
#include <asm/io.h>
#include <asm/uaccess.h>
#include <linux/apm_bios.h>
#include <linux/pm.h>
#include "thinkpad_common.h"
#include "thinkpadpm.h"


/****** definitions ******/


/****** declarations ******/

extern struct proc_dir_entry *thinkpad_ppde;

#ifdef CONFIG_DEVFS_FS
static int thinkpadpm_open(
	struct inode * pinodeThe,
	struct file * pfileThe
);
static int thinkpadpm_release(
	struct inode * pinodeThe,
	struct file * pfileThe 
);
static int thinkpadpm_ioctl(
	struct inode * pinodeThe,
	struct file * pfileThe,
	unsigned int uintIoctlNum,
	unsigned long ulongIoctlArg
);
#endif

/****** variables ******/

static const char _szMyName[] = "thinkpadpm";
static const char _szImName[] = "thinkpadpm_do";
static const char _szMyVersion[] = "4.0";
static const char _szProcfile[] = "driver/thinkpad/thinkpadpm";

MODULE_AUTHOR( "Thomas Hood" );
MODULE_DESCRIPTION( "Driver for managing power on IBM ThinkPads" );
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,4,10)
MODULE_LICENSE( "GPL" );
#endif

#ifdef CONFIG_DEVFS_FS
static struct file_operations _fileopsThinkpadpm = {
	.ioctl   = thinkpadpm_ioctl,
	.open    = thinkpadpm_open,
	.release = thinkpadpm_release,
	.owner   = THIS_MODULE
};

static devfs_handle_t _devfs_handleThinkpadpm;
#endif

/*
 * -----------------------------------------------------------------------
 * THE FOLLOWING SECTION WAS COPIED VERBATIM OUT OF THE APM DRIVER v. 1.15
 * ... because the apm driver doesn't export these functions
 * -----------------------------------------------------------------------
 */
#include <linux/apm_bios.h>
#include <linux/pm.h>

/*
 * Define to make the APM BIOS calls zero all data segment registers (so
 * that an incorrect BIOS implementation will cause a kernel panic if it
 * tries to write to arbitrary memory).
 */
#define APM_ZERO_SEGS

/*
 * Save a segment register away
 */
#define savesegment(seg, where) \
		__asm__ __volatile__("movl %%" #seg ",%0" : "=m" (where))

static struct {
	unsigned long	offset;
	unsigned short	segment;
}				apm_bios_entry;

/*
 * These are the actual BIOS calls.  Depending on APM_ZERO_SEGS and
 * CONFIG_APM_ALLOW_INTS, we are being really paranoid here!  Not only
 * are interrupts disabled, but all the segment registers (except SS)
 * are saved and zeroed this means that if the BIOS tries to reference
 * any data without explicitly loading the segment registers, the kernel
 * will fault immediately rather than have some unforeseen circumstances
 * for the rest of the kernel.  And it will be very obvious!  :-) Doing
 * this depends on CS referring to the same physical memory as DS so that
 * DS can be zeroed before the call. Unfortunately, we can't do anything
 * about the stack segment/pointer.  Also, we tell the compiler that
 * everything could change.
 *
 * Also, we KNOW that for the non error case of apm_bios_call, there
 * is no useful data returned in the low order 8 bits of eax.
 */
#define APM_DO_CLI	\
	if (apm_info.allow_ints) \
		__sti(); \
	else \
		__cli();

#ifdef APM_ZERO_SEGS
#	define APM_DECL_SEGS \
		unsigned int saved_fs; unsigned int saved_gs;
#	define APM_DO_SAVE_SEGS \
		savesegment(fs, saved_fs); savesegment(gs, saved_gs)
#	define APM_DO_ZERO_SEGS \
		"pushl %%ds\n\t" \
		"pushl %%es\n\t" \
		"xorl %%edx, %%edx\n\t" \
		"mov %%dx, %%ds\n\t" \
		"mov %%dx, %%es\n\t" \
		"mov %%dx, %%fs\n\t" \
		"mov %%dx, %%gs\n\t"
#	define APM_DO_POP_SEGS \
		"popl %%es\n\t" \
		"popl %%ds\n\t"
#	define APM_DO_RESTORE_SEGS \
		loadsegment(fs, saved_fs); loadsegment(gs, saved_gs)
#else
#	define APM_DECL_SEGS
#	define APM_DO_SAVE_SEGS
#	define APM_DO_ZERO_SEGS
#	define APM_DO_POP_SEGS
#	define APM_DO_RESTORE_SEGS
#endif

/**
 *	apm_bios_call	-	Make an APM BIOS 32bit call
 *	@func: APM function to execute
 *	@ebx_in: EBX register for call entry
 *	@ecx_in: ECX register for call entry
 *	@eax: EAX register return
 *	@ebx: EBX register return
 *	@ecx: ECX register return
 *	@edx: EDX register return
 *	@esi: ESI register return
 *
 *	Make an APM call using the 32bit protected mode interface. The
 *	caller is responsible for knowing if APM BIOS is configured and
 *	enabled. This call can disable interrupts for a long period of
 *	time on some laptops.  The return value is in AH and the carry
 *	flag is loaded into AL.  If there is an error, then the error
 *	code is returned in AH (bits 8-15 of eax) and this function
 *	returns non-zero.
 */

static u8 apm_bios_call(u32 func, u32 ebx_in, u32 ecx_in,
	u32 *eax, u32 *ebx, u32 *ecx, u32 *edx, u32 *esi)
{
	APM_DECL_SEGS
	unsigned long	flags;

	__save_flags(flags);
	APM_DO_CLI;
	APM_DO_SAVE_SEGS;
	/*
	 * N.B. We do NOT need a cld after the BIOS call
	 * because we always save and restore the flags.
	 */
	__asm__ __volatile__(APM_DO_ZERO_SEGS
		"pushl %%edi\n\t"
		"pushl %%ebp\n\t"
		"lcall %%cs:" SYMBOL_NAME_STR(apm_bios_entry) "\n\t"
		"setc %%al\n\t"
		"popl %%ebp\n\t"
		"popl %%edi\n\t"
		APM_DO_POP_SEGS
		: "=a" (*eax), "=b" (*ebx), "=c" (*ecx), "=d" (*edx),
		  "=S" (*esi)
		: "a" (func), "b" (ebx_in), "c" (ecx_in)
		: "memory", "cc");
	APM_DO_RESTORE_SEGS;
	__restore_flags(flags);
	return *eax & 0xff;
}

/**
 *	apm_bios_call_simple	-	make a simple APM BIOS 32bit call
 *	@func: APM function to invoke
 *	@ebx_in: EBX register value for BIOS call
 *	@ecx_in: ECX register value for BIOS call
 *	@eax: EAX register on return from the BIOS call
 *
 *	Make a BIOS call that does only returns one value, or just status.
 *	If there is an error, then the error code is returned in AH
 *	(bits 8-15 of eax) and this function returns non-zero. This is
 *	used for simpler BIOS operations. This call may hold interrupts
 *	off for a long time on some laptops.
 */

static u8 apm_bios_call_simple(u32 func, u32 ebx_in, u32 ecx_in, u32 *eax)
{
	u8		error;
	APM_DECL_SEGS
	unsigned long	flags;

	__save_flags(flags);
	APM_DO_CLI;
	APM_DO_SAVE_SEGS;
	{
		int	cx, dx, si;

		/*
		 * N.B. We do NOT need a cld after the BIOS call
		 * because we always save and restore the flags.
		 */
		__asm__ __volatile__(APM_DO_ZERO_SEGS
			"pushl %%edi\n\t"
			"pushl %%ebp\n\t"
			"lcall %%cs:" SYMBOL_NAME_STR(apm_bios_entry) "\n\t"
			"setc %%bl\n\t"
			"popl %%ebp\n\t"
			"popl %%edi\n\t"
			APM_DO_POP_SEGS
			: "=a" (*eax), "=b" (error), "=c" (cx), "=d" (dx),
			  "=S" (si)
			: "a" (func), "b" (ebx_in), "c" (ecx_in)
			: "memory", "cc");
	}
	APM_DO_RESTORE_SEGS;
	__restore_flags(flags);
	return error;
}

/**
 *	set_power_state	-	set the power management state
 *	@what: which items to transition
 *	@state: state to transition to
 *
 *	Request an APM change of state for one or more system devices. The
 *	processor state must be transitioned last of all. what holds the
 *	class of device in the upper byte and the device number (0xFF for
 *	all) for the object to be transitioned.
 *
 *	The state holds the state to transition to, which may in fact
 *	be an acceptance of a BIOS requested state change.
 */

static int set_power_state(u_short what, u_short state)
{
	u32	eax;

	if (apm_bios_call_simple(APM_FUNC_SET_STATE, what, state, &eax))
		return (eax >> 8) & 0xff;
	return APM_SUCCESS;
}

/* ----------------------------------------------------------------------- */
/* END OF SECTION COPIED VERBATIM OUT OF THE APM DRIVER                    */
/* ----------------------------------------------------------------------- */

static int get_power_state(u_short what, u_short *state)
{
	u32	eax;
	u32	ecx;
	u32	dummy;

	if (apm_bios_call(APM_FUNC_GET_STATE, what, 0, &eax, &dummy, &ecx,
			&dummy, &dummy))
		return (eax >> 8) & 0xff;
	*state = ecx;

	return APM_SUCCESS;
}

/* ----------------------------------------------------------------------- */


/****** functions *******/

static int thinkpadpm_read_proc(
	char *pchBuf, 
	char **ppchStart, 
	off_t off,
	int intCount, 
	int *pintEof, 
	void *data
) {
	return snprintf(
		pchBuf, intCount,
		"%s version %s accessing APM BIOS at 0x%x:0x%lx\n",
		_szMyName, _szMyVersion,
		apm_bios_entry.segment,
		apm_bios_entry.offset
	);
}

static int get_state( word wWhat, word *pwState )
{
	int intRtn;

#ifdef DEBUG
	printk(KERN_INFO
		"%s: Getting power state for device 0x%x\n",
		_szMyName, wWhat
	);
#endif
	*pwState = 0x9999; /* ... so we can tell when *pwState wasn't changed */
	intRtn = get_power_state( wWhat, pwState );
	if ( intRtn ) {
		printk(KERN_INFO
			"%s: Power state of device 0x%x could not be got; 0x%x was returned as the state -- APM error 0x%x\n",
			_szMyName, wWhat, *pwState, intRtn
		);
	} else {
#ifdef DEBUG
		printk(KERN_INFO
			"%s: Power state of device 0x%x is 0x%x\n",
			_szMyName, wWhat, *pwState
		);
#endif
	}

	return intRtn;
}

static int set_state( word wWhat, word wState )
{
	int intRtn;

#ifdef DEBUG
	printk(KERN_INFO
		"%s: Setting power state for device 0x%x to 0x%x\n",
		_szMyName, wWhat, wState
	);
#endif
	intRtn = set_power_state( wWhat, wState );
#ifdef DEBUG
	if ( intRtn ) {
		printk(KERN_INFO
			"%s: Power state of device 0x%x could not be set -- APM error 0x%x\n",
			_szMyName, wWhat, intRtn
		);
	}
#endif

	return intRtn;
}

static int thinkpadpm_do_func(
	thinkpadpm_inparm_t *pinparmThe,
	thinkpadpm_outparm_t *poutparmThe,
	flag_t fHasWritePerm
) {

	switch ( pinparmThe->wFunc ) {
		case THINKPADPM_FUNC_STATE_GET:
			return get_state( pinparmThe->wParm0, (word *)&(poutparmThe->dwParm1) ); 
		case THINKPADPM_FUNC_STATE_SET:
			if ( !fHasWritePerm ) return -EACCES;
			return set_state( pinparmThe->wParm0, (word)pinparmThe->dwParm1 );
		default:
			return -EINVAL;
	}
}


int thinkpadpm_do( 
	unsigned long ulongIoctlArg,
	flag_t fCallerHasWritePerm
) {
	thinkpadpm_inparm_t inparmThe;
	thinkpadpm_outparm_t outparmThe;
	unsigned long ulRtnCopy;
	int intRtnDo;

	ulRtnCopy = copy_from_user(
		&inparmThe,
		(byte *)&(((thinkpadpm_ioparm_t *)ulongIoctlArg)->in),
		sizeof( inparmThe )
	);
	if ( ulRtnCopy ) return -EFAULT;

	intRtnDo = thinkpadpm_do_func( &inparmThe, &outparmThe, fCallerHasWritePerm );

	if ( intRtnDo == -EACCES ) {
		printk(KERN_ERR
			"%s: Caller does not have permission to do this power management act\n",
			_szMyName
		);
		return -EACCES;
	}

	outparmThe.wRc = intRtnDo;

	ulRtnCopy = copy_to_user(
		(byte *)&(((thinkpadpm_ioparm_t *)ulongIoctlArg)->out),
		&outparmThe,
		sizeof( outparmThe )
	);
	if ( ulRtnCopy ) return -EFAULT;

	return outparmThe.wRc ? -ETHINKPAD_SUBDRIVER : 0;
}

#ifdef CONFIG_DEVFS_FS
static int thinkpadpm_open(
	struct inode * pinodeThe,
	struct file * pfileThe
) {

	return 0;
}


static int thinkpadpm_release(
	struct inode * pinodeThe,
	struct file * pfileThe
) {

	return 0;
}


static flag_t caller_has_w( struct file * pfileThe )
{
	return ((pfileThe->f_mode) & FMODE_WRITE) ? 1 : 0;
}


static int thinkpadpm_ioctl(
	struct inode * pinodeThe,
	struct file * pfileThe,
	unsigned int uintIoctlNum,
	unsigned long ulongIoctlArg
) {

#ifdef DEBUG_VERBOSE
	printk( "%s: Doing ioctl number 0x%x\n", _szMyName, uintIoctlNum );
#endif

	switch ( uintIoctlNum ) {
		/* should  return  at the end of each case block */

		case IOCTL_THINKPADPM_REQUEST: 
			return thinkpadpm_do(
				ulongIoctlArg, 
				caller_has_w( pfileThe )
			);

		default:
			/* ioctl number not recognized -- do nothing */
			return -ENOTTY;

	} /* switch */
			
	return -ETHINKPAD_PROGRAMMING; /* We should never arrive here */
}
#endif /* defined(CONFIG_DEVFS_FS) */

static int __init thinkpadpm_init( void )
{

	/*** Set up APM BIOS interface ***/
	if ( !pm_active ) {
		printk(KERN_INFO "thinkpadpm: Power management not active. :-(\n");
		return -ENODEV;
	}
	if (apm_info.disabled) {
		printk(KERN_NOTICE "thinkpadpm: APM is disabled.\n");
		return -ENODEV;
	}
	if (apm_info.bios.version == 0) {
		printk(KERN_INFO "thinkpadpm: No APM BIOS. :-(\n");
		return -ENODEV;
	}
	if ((apm_info.bios.flags & APM_32_BIT_SUPPORT) == 0) {
		printk(KERN_INFO "thinkpadpm: No 32 bit APM BIOS support. :-(\n");
		return -ENODEV;
	}
	apm_bios_entry.segment = APM_CS;
	apm_bios_entry.offset = apm_info.bios.offset;
	printk(KERN_INFO
		"thinkpadpm: Found APM BIOS version %d.%d flags 0x%02x entry 0x%x:0x%lx. :-)\n",
		((apm_info.bios.version >> 8) & 0xff),
		(apm_info.bios.version & 0xff),
		apm_info.bios.flags,
		apm_bios_entry.segment,
		apm_bios_entry.offset
	);

	if ( !thinkpad_ppde || !create_proc_read_entry(
		_szProcfile,
		S_IFREG | S_IRUGO,
		NULL,
		thinkpadpm_read_proc,
		NULL
	) ) {
		printk(KERN_ERR "%s: Could not create_proc_read_entry() /proc/%s\n", _szMyName, _szProcfile );
	}
	/* proc entry created */

#ifdef CONFIG_DEVFS_FS
	_devfs_handleThinkpadpm = devfs_register(
		NULL /* dir */,
		"thinkpad/thinkpadpm",
		DEVFS_FL_DEFAULT | DEVFS_FL_AUTO_DEVNUM,
		0 /* major */, 0 /* minor */,
		S_IFCHR | S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH,
		&_fileopsThinkpadpm,
		NULL /* info */
	);
	if ( _devfs_handleThinkpadpm == NULL ) {
		printk(KERN_ERR "%s: Could not devfs_register(). Aborting.\n", _szMyName );
		return -EIO;
	}
	/* dev entry created */
#endif

	inter_module_register( _szImName, THIS_MODULE, &thinkpadpm_do );

	return 0;
}


static void __exit thinkpadpm_exit( void )
{

	inter_module_unregister( _szImName );

#ifdef CONFIG_DEVFS_FS
	devfs_unregister( _devfs_handleThinkpadpm );
#endif

	remove_proc_entry( _szProcfile, NULL );

	return;
}

module_init(thinkpadpm_init);
module_exit(thinkpadpm_exit);
