
/*********************************************************************
 *                
 * Filename:      superio.c
 * Description:   kernel module that configures the PC97338
 *                Super I/O chip present in some ThinkPads
 * Author:        Thomas Hood
 * Reference:     National Semiconductor PC87338/PC97338 data sheet.  November, 1998.
 * Created:       19 July 1999 
 *
 * 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/ioport.h>
#include <linux/proc_fs.h>
#include <linux/devfs_fs_kernel.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include "thinkpad_common.h"
#include "superio.h"


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


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

extern struct proc_dir_entry *thinkpad_ppde;

#ifdef CONFIG_DEVFS_FS
static int superio_open(
	struct inode * pinodeThe,
	struct file * pfileThe
);
static int superio_release(
	struct inode * pinodeThe,
	struct file * pfileThe 
);
static int superio_ioctl(
	struct inode * pinodeThe,
	struct file * pfileThe,
	unsigned int uintIoctlNum,
	unsigned long ulongIoctlArg
);
#endif

/****** variables ******/
static const char _szMyName[] = "superio";
static const char _szImName[] = "superio_do";
static const char _szMyVersion[] = "4.0";
static const char _szProcfile[] = "driver/thinkpad/superio";
static word _wPortIndex, _wPortData;
static struct resource *_presourceSuperio;

MODULE_AUTHOR( "Thomas Hood" );
MODULE_DESCRIPTION( "Driver for the Super I/O chip on IBM ThinkPads" );
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,4,10)
MODULE_LICENSE( "GPL" );
#endif

#ifdef CONFIG_DEVFS_FS
static struct file_operations _fileopsSuperio = {
	.ioctl   = superio_ioctl,
	.open    = superio_open,
	.release = superio_release,
	.owner   = THIS_MODULE
};

static devfs_handle_t _devfs_handleSuperio;
#endif

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

/*
 * We use _p variant of outb just for safety
 * getregb and putregb use the global variables 
 * _wPortIndex and _wPortData
 */
static byte getregb( byte bWhere )
{
	byte bGot;
	unsigned long flags;

	save_flags( flags ); cli();
	outb_p( bWhere, _wPortIndex );
	bGot = inb( _wPortData );
	restore_flags( flags );

	return bGot;
}


static void putregb( byte bWhat, byte bWhere )
{
	unsigned long flags;

	save_flags( flags ); cli();
	outb_p( bWhere, _wPortIndex );
	outb( bWhat, _wPortData );
	restore_flags( flags );

	return;
}


static int get_leg_regs( superio_outparm_t *poutparmThe )
{
	byte bTmp, bID, bRev;

	poutparmThe->dwParm1 = 0;
	poutparmThe->dwParm2 = 0;

	bTmp = getregb( 8 ); /* chip ID and rev */
	bID = bTmp & ~7;
	bRev = bTmp & 7;
	poutparmThe->wParm0 = (((word)bID)<<8) | bRev;

	bTmp = getregb( 0 ); /* enable */
	poutparmThe->dwParm1 |= (dword)bTmp;

	bTmp = getregb( 1 ); /* address */
	poutparmThe->dwParm1 |= ((dword)bTmp) << 8;

	bTmp = getregb( 2 ); /* power and test */
	poutparmThe->dwParm1 |= ((dword)bTmp) << 16;

	bTmp = getregb( 3 ); /* function control */
	poutparmThe->dwParm1 |= ((dword)bTmp) << 24;

	bTmp = getregb( 4 ); /* printer control */
	poutparmThe->dwParm2 |= (dword)bTmp;

#if 0
	bTmp = getregb( 5 ); /* not used */
	poutparmThe->dwParm2 |= ((dword)bTmp) << 8;
#endif

	bTmp = getregb( 6 ); /* power management */
	poutparmThe->dwParm2 |= ((dword)bTmp) << 16;

	bTmp = getregb( 7 ); /* tape, ser, par config */
	poutparmThe->dwParm2 |= ((dword)bTmp) << 24;

	return 0;
}


/*
 * This should always be done by read-modify-write.
 */
static int set_leg_regs( superio_inparm_t *pinparmThe )
{
	byte bTmp;

	bTmp = pinparmThe->dwParm1 & 0xFF;
	putregb( bTmp, 0 ); /* enable */

	bTmp = (pinparmThe->dwParm1 >> 8) & 0xFF;
	putregb( bTmp, 1 ); /* address */

	bTmp = (pinparmThe->dwParm1 >> 16) & 0xFF;
	putregb( bTmp, 2 ); /* power and test */

	bTmp = (pinparmThe->dwParm1 >> 24) & 0xFF;
	putregb( bTmp, 3 ); /* function control */

	bTmp = pinparmThe->dwParm2 & 0xFF;
	putregb( bTmp, 4 ); /* printer control */

#if 0
	bTmp = (pinparmThe->dwParm2 >> 8) & 0xFF;
	putregb( bTmp, 5 ); /* not used */
#endif

	bTmp = (pinparmThe->dwParm2 >> 16) & 0xFF;
	putregb( bTmp, 6 ); /* power management */

	bTmp = (pinparmThe->dwParm2 >> 24) & 0xFF;
	putregb( bTmp, 7 ); /* tape, ser, par config */

	return 0;
}


static int get_pnp_regs( dword *pdwRegs )
{

	*pdwRegs =   (dword)getregb( 0x1b )       ;
	*pdwRegs |= ((dword)getregb( 0x1c )) << 8 ;
	*pdwRegs |= ((dword)getregb( 0x41 )) << 16;
	*pdwRegs |= ((dword)getregb( 0x4F )) << 24;

	return 0;
}


static flag_t is_in_pnp_mode( word wPortIndex, word wPortData )
{
	byte bGot;
	unsigned long flags;

	save_flags( flags ); cli();
	outb_p( 0x1b, wPortIndex );
	bGot = inb( wPortData );
	restore_flags( flags );

	return (flag_t)( (bGot >> 3) & 1 );
}


static byte irq_whose_pnp_code_is( byte bCode )
{

	switch ( bCode ) {
		case 0: return 0; /* "disable" */
		case 1: return 0xFF;
		case 2: return 0xFF;
		case 3: return 3;
		case 4: return 4;
		case 5: return 5;
		case 6: return 6;
		case 7: return 7;
		case 8: return 0xFF; /* "invalid" */
		case 9: return 9;
		case 10: return 10;
		case 11: return 11;
		case 12: return 12;
		case 13: return 0xFF; /* "invalid" */
		case 14: return 0xFF; /* "invalid" */
		case 15: return 15;
	}

	return 0xfe; /* we shouldn't reach here */
}


/*
 * returns -EINVAL if can't encode IRQ 
 */
static int int_pnp_code_for_irq( byte bIRQ )
{

	switch ( bIRQ ) {
		case 0: return 0; /* "disable" */
		case 3: return 3;
		case 4: return 4;
		case 5: return 5;
		case 6: return 6;
		case 7: return 7;
		case 9: return 9;
		case 10: return 10;
		case 11: return 11;
		case 12: return 12;
		case 15: return 15;
		default: return -EINVAL;
	}

	return -EINVAL;
}


static flag_t disable_par_tmp( void )
{
	byte bFER;
	flag_t fWasEnabled;

	/*** disable ***/
	bFER = getregb( 0 ); /* FER */
	fWasEnabled = bFER & 1;
	putregb( bFER & ~1, 0 );

	return fWasEnabled;
}


static flag_t disable_ser_tmp( byte bWhich )
{
	byte bFER, bMaskFER;
	flag_t fWasEnabled;
	
	switch ( bWhich ) {
		case 1: bMaskFER=2; break;
		case 2: bMaskFER=4; break;
		default: bMaskFER=0; break;
	}

	/*** disable ***/
	bFER = getregb( 0 ); /* FER */
	fWasEnabled = (bFER & bMaskFER) ? 1 : 0;
	putregb( bFER & ~bMaskFER, 0 );

	return fWasEnabled;
}


static int get_fdd_irq( byte *pbIRQ )
{
	byte bTmp;

	bTmp = getregb( 0x41 ) & 0x0F;

	*pbIRQ = irq_whose_pnp_code_is( bTmp );

	return 0;
}


static int get_fdd_base_address( word *pwAdrs)
{
	
	*pwAdrs = ((word)getregb( 0x48 ) & ~1) << 2; /* low */
	*pwAdrs |= (((word)getregb( 0x49 ) & ~3) << 8); /* high */

	return 0;
}


static int ablify_par( flag_t fEnable )
{
	byte bFER;
	
	bFER = getregb( 0 ); /* FER */
	if ( fEnable ) 
		putregb( bFER | 1, 0 );
	else
		putregb( bFER & ~1, 0 );

	return 0;
}



static int get_par_irq_legacymode( byte *pbIRQ )
{
	byte bTmp;

	bTmp = getregb( 0x1b ) & 7;
	switch ( bTmp ) {
		case 0: *pbIRQ = 0xFF; break;
		case 1: *pbIRQ = 7; break;
		case 2: *pbIRQ = 9; break;
		case 3: *pbIRQ = 10; break;
		case 4: *pbIRQ = 11; break;
		case 5: *pbIRQ = 14; break;
		case 6: *pbIRQ = 15; break;
		case 7: *pbIRQ = 5; break;
	}

	return 0;
}


static int set_par_irq_legacymode( byte bIRQ )
{
	byte bCode, bPPCR0;
	flag_t fWasEnabled;

	/*** compute code ***/
	switch ( bIRQ ) {
		case 7: bCode = 1; break;
		case 9: bCode = 2; break;
		case 10: bCode = 3; break;
		case 11: bCode = 4; break;
		case 14: bCode = 5; break;
		case 15: bCode = 6; break;
		case 5: bCode = 7; break;
		default:
			return -EINVAL;
	}
	
	fWasEnabled = disable_par_tmp();

	/*** set ***/
	bPPCR0 = getregb( 0x1b );
	putregb( (bPPCR0 & ~7) | (bCode & 7), 0x1b );

	ablify_par( fWasEnabled );

	return 0;
}


static int get_par_irq_pnpmode( byte *pbIRQ )
{
	byte bTmp;

	bTmp = getregb( 0x1b ) >> 4;

	*pbIRQ = irq_whose_pnp_code_is( bTmp );

	return 0;
}


static int set_par_irq_pnpmode( byte bIRQ )
{
	int intCode;
	byte bPPCR0, bCode;
	flag_t fWasEnabled;

	/*** compute code ***/
	intCode = int_pnp_code_for_irq( bIRQ );
	if ( intCode == -EINVAL ) return -EINVAL;
	bCode = (byte)intCode;
	
	fWasEnabled = disable_par_tmp();

	/*** set ***/
	bPPCR0 = getregb( 0x1b );
	putregb( (bPPCR0 & 0x0F) | (bCode << 4), 0x1b );

	ablify_par( fWasEnabled );

	return 0;
}


static int get_par_base_address( word *pwAdrs)
{
	
	*pwAdrs = ((word)getregb( 0x42 )) << 2;
	*pwAdrs |= (((word)getregb( 0x43 ) & ~3) << 8);

	return 0;
}


/*
 * When we write we take care not to alter the "reserved" bits
 */
static int set_par_base_address( word wAdrs )
{
	byte bTmp;
	flag_t fWasEnabled;
	
	fWasEnabled = disable_par_tmp();
	
	/*** set ***/
	putregb( (byte)(wAdrs>>2), 0x42 ); /* low */

	bTmp = getregb( 0x43 ) & 3;
	putregb( ((byte)(wAdrs>>8)&~3) | bTmp, 0x43 ); /* high */

	ablify_par( fWasEnabled );

	return 0;
}


static int get_par_mode( superio_par_mode_t *pmode )
{
	byte bPCR, bPTR;
	
	bPCR = getregb( 4 );
	bPTR = getregb( 2 );

	if ( (bPCR & 5) == 0  &&  (bPTR & 0x80) == 0 ) {
		*pmode = SUPERIO_PAR_MODE_COMPATIBLE;
		return 0;
	} else if ( (bPCR & 5) == 0  &&  (bPTR & 0x80) ) {
		*pmode = SUPERIO_PAR_MODE_EXTENDED;
		return 0;
	} else if ( (bPCR & 5) == 1  &&  (bPTR & 0x80) == 0  &&  (bPCR & 2) == 0 ) {
		*pmode = SUPERIO_PAR_MODE_EPP_1_7;
		return 0;
	} else if ( (bPCR & 5) == 1  &&  (bPTR & 0x80) == 0  &&  (bPCR & 2) ) {
		*pmode = SUPERIO_PAR_MODE_EPP_1_9;
		return 0;
	} else if ( (bPCR & 5) == 4 ) {
		*pmode = SUPERIO_PAR_MODE_ECP;
		return 0;
	}

	printk(KERN_INFO
		"%s: Parallel port mode not recognized: PCR = 0x%x  PTR = 0x%x\n",
		_szMyName,
		bPCR, bPTR
	);
	*pmode = SUPERIO_PAR_MODE_UNRECOGNIZED;

	return 0;
}


static int set_par_mode( superio_par_mode_t mode )
{
	byte bPCR, bPTR;
	
	bPCR = getregb( 4 );
	bPTR = getregb( 2 );

	switch( mode ) {
		case SUPERIO_PAR_MODE_COMPATIBLE:
			bPCR &= ~5;
			bPTR &= ~0x80;
			break;
		case SUPERIO_PAR_MODE_EXTENDED:
			bPCR &= ~5;
			bPTR |= 0x80;
			break;
		case SUPERIO_PAR_MODE_EPP_1_7:
			bPCR &= ~4;  bPCR |= 1;
			bPCR &= ~2; /* EPP version 1.7 */
			bPTR &= ~0x80;
			break;
		case SUPERIO_PAR_MODE_EPP_1_9:
			bPCR &= ~4;  bPCR |= 1;
			bPCR |= 2; /* EPP version 1.9 */
			bPTR &= ~0x80;
			break;
		case SUPERIO_PAR_MODE_ECP:
			bPCR &= ~1;  bPCR |= 4;
			break;
		case SUPERIO_PAR_MODE_UNRECOGNIZED:
			return -EINVAL;
	}

	putregb( bPCR, 4 );
	putregb( bPTR, 2 );

	return 0;
}


static int ablify_ser( byte bWhich, flag_t fEnable )
{
	byte bFER, bMaskFER;
	
	switch ( bWhich ) {
		case 1: bMaskFER=2; break;
		case 2: bMaskFER=4; break;
		default:
			return -EINVAL;
	}

	bFER = getregb( 0 ); /* FER */
	if ( fEnable ) 
		putregb( (bFER | bMaskFER), 0 );
	else
		putregb( (bFER & ~bMaskFER), 0 );

	return 0;
}


static int get_ser_irq( byte bWhich, byte *pbIRQ )
{
	byte bTmp, bCode;
	
	bTmp = getregb( 0x1c );

	switch ( bWhich ) {
		case 1: bCode = bTmp & 0xF; break;
		case 2: bCode = bTmp >> 4; break;
		default:
			return -EINVAL;
	}

	*pbIRQ = irq_whose_pnp_code_is( bCode );

	return 0;
}


static int set_ser_irq( byte bWhich, byte bIRQ )
{
	int intCode;
	byte bPPCR1, bCode;
	flag_t fWasEnabled;

	intCode = int_pnp_code_for_irq( bIRQ );
	if ( intCode == -EINVAL ) return -EINVAL;
	bCode = (byte)intCode;
	
	fWasEnabled = disable_ser_tmp( bWhich );

	/*** set ***/
	bPPCR1 = getregb( 0x1c );
	switch( bWhich ) {
		case 1: putregb( (bPPCR1 & 0xF0) | bCode, 0x1c ); break;
		case 2: putregb( (bPPCR1 & 0x0F) | (bCode << 4), 0x1c ); break;
		default:
			return -EINVAL;
	}

	ablify_ser( bWhich, fWasEnabled );

	return 0;
}


static int get_ser_base_address(
	byte bWhich,
	word *pwAdrs
) {
	byte bIndexL, bIndexH;
	
	switch ( bWhich ) {
		case 1: bIndexL = 0x44; bIndexH = 0x45; break;
		case 2: bIndexL = 0x46; bIndexH = 0x47; break;
		default:
			return -EINVAL;
	}
	
	*pwAdrs = ((word)getregb( bIndexL ) & ~1) << 2;
	*pwAdrs |= (((word)getregb( bIndexH ) & ~3) << 8);

	return 0;
}


/*
 * When we write we take care not to alter the "reserved" bits
 */
static int set_ser_base_address(
	byte bWhich,
	word wAdrs
) {
	byte bIndexL, bIndexH, bTmp;
	flag_t fWasEnabled;
	
	switch ( bWhich ) {
		case 1: bIndexL = 0x44; bIndexH = 0x45; break;
		case 2: bIndexL = 0x46; bIndexH = 0x47; break;
		default:
			return -EINVAL;
	}
	
	fWasEnabled = disable_ser_tmp( bWhich );

	/*** set ***/
	bTmp = getregb( bIndexL ) & 1;
	putregb( ((byte)(wAdrs>>2)&~1) | bTmp, bIndexL );

	bTmp = getregb( bIndexH ) & 3;
	putregb( ((byte)(wAdrs>>8)&~3) | bTmp, bIndexH );

	ablify_ser( bWhich, fWasEnabled );

	return 0;
}


static int superio_read_proc(
	char *pchBuf, 
	char **ppchStart, 
	off_t off,
	int intCount, 
	int *pintEof, 
	void *data
) {
	return snprintf(
		pchBuf, intCount,
		"%s version %s controlling Super I/O chip at ioports 0x%x, 0x%x\n",
		_szMyName, _szMyVersion,
		_wPortIndex, _wPortData
	);
}

static int superio_do_func(
	superio_inparm_t *pinparmThe,
	superio_outparm_t *poutparmThe,
	flag_t fHasWritePerm
) {

	switch ( pinparmThe->wFunc ) {
		case SUPERIO_FUNC_LEG_REGS_GET:
			return get_leg_regs( poutparmThe ); 
		case SUPERIO_FUNC_LEG_REGS_SET:
			if ( !fHasWritePerm ) return -EACCES;
			return set_leg_regs( pinparmThe );
		case SUPERIO_FUNC_PNP_REGS_GET:
			return get_pnp_regs( &(poutparmThe->dwParm1) );
		case SUPERIO_FUNC_FDD_IRQ_GET:
			return get_fdd_irq( (byte *)&(poutparmThe->dwParm1) );
		case SUPERIO_FUNC_FDD_BASE_GET:
			return get_fdd_base_address( (word *)&(poutparmThe->dwParm1) );
		case SUPERIO_FUNC_PAR_IRQ_GET:
			if ( is_in_pnp_mode( _wPortIndex, _wPortData ) ) {
				return get_par_irq_pnpmode( (byte *)&(poutparmThe->dwParm1) );
			} else {
				printk(KERN_INFO
					"%s: Super I/O chip is in legacy mode!\n",
					_szMyName
				);
				return get_par_irq_legacymode( (byte *)&(poutparmThe->dwParm1) );
			}
		case SUPERIO_FUNC_PAR_IRQ_SET:
			if ( !fHasWritePerm ) return -EACCES;
			set_par_irq_legacymode( (byte)(pinparmThe->dwParm1) );
			return set_par_irq_pnpmode( (byte)(pinparmThe->dwParm1) );
		case SUPERIO_FUNC_PAR_BASE_GET:
			return get_par_base_address( (word *)&(poutparmThe->dwParm1) );
		case SUPERIO_FUNC_PAR_BASE_SET:
			if ( !fHasWritePerm ) return -EACCES;
			return set_par_base_address( (word)(pinparmThe->dwParm1) );
		case SUPERIO_FUNC_PAR_MODE_GET:
			return get_par_mode( (superio_par_mode_t *)&(poutparmThe->dwParm1) );
		case SUPERIO_FUNC_PAR_MODE_SET:
			if ( !fHasWritePerm ) return -EACCES;
			return set_par_mode( (superio_par_mode_t)(pinparmThe->dwParm1) );

		case SUPERIO_FUNC_PAR_ABLIFY:
			if ( !fHasWritePerm ) return -EACCES;
			return ablify_par( (flag_t)(pinparmThe->dwParm1) );
		case SUPERIO_FUNC_SER_IRQ_GET:
			return get_ser_irq( (byte)pinparmThe->wParm0, (byte *)&(poutparmThe->dwParm1) );
		case SUPERIO_FUNC_SER_IRQ_SET:
			if ( !fHasWritePerm ) return -EACCES;
			return set_ser_irq( (byte)pinparmThe->wParm0, (byte)(pinparmThe->dwParm1) );
		case SUPERIO_FUNC_SER_BASE_GET:
			return get_ser_base_address( pinparmThe->wParm0, (word *)&(poutparmThe->dwParm1) );
		case SUPERIO_FUNC_SER_BASE_SET:
			if ( !fHasWritePerm ) return -EACCES;
			return set_ser_base_address( pinparmThe->wParm0, (word)(pinparmThe->dwParm1) );
		case SUPERIO_FUNC_SER_ABLIFY:
			if ( !fHasWritePerm ) return -EACCES;
			return ablify_ser( pinparmThe->wParm0, (flag_t)(pinparmThe->dwParm1) );
		default:
			return -EINVAL;
	}
}


int superio_do(
	unsigned long ulongIoctlArg,
	flag_t fCallerHasWritePerm
) {
	superio_inparm_t inparmThe;
	superio_outparm_t outparmThe;
	unsigned long ulRtnCopy;
	int intRtnDo;

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

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

	if ( intRtnDo == -EACCES ) {
		printk(KERN_ERR
			"%s: caller does not have permission to do this to the Super IO device\n",
			_szMyName
		);
		return intRtnDo;
	}

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

	return intRtnDo; /* Should be either 0 or -EACCES or -EINVAL */
}

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

	return 0;
}


static int superio_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 superio_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_SUPERIO_REQUEST: 
			return superio_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) */


/*
 * We write to nonsense port 0x80 in order to
 * protect against false positives due to
 * ISA bus float
 */
static flag_t a_super_io_chip_appears_to_be_at( word wPort )
{
	byte bSave, bID;
	unsigned long flags;

	save_flags( flags ); cli();

	bSave = inb( wPort );

	/*** Check for a register ***/
	outb( 0x0a, wPort );
	outb( ~0x0a, 0x080 ); 
	if ( inb( wPort ) != 0x0a ) { goto No; }
	outb( 0x11, wPort );
	outb( ~0x11, 0x080 ); 
	if ( inb( wPort ) != 0x11 ) { goto No; }
	outb( 0x44, wPort );
	outb( ~0x44, 0x080 ); 
	if ( inb( wPort ) != 0x44 ) { goto No; }

	/* There's a register */

	/*** Check for chip ID ***/
	outb( 0x08, wPort );
	bID = inb( wPort + 1 );
	if ( (bID & ~3) != 0xb0 ) { goto No; }

/* Yes: */
	/* No need to restore index port's contents */
	restore_flags( flags );
	return 1;

No:
	outb( bSave, wPort ); /* restore the port's contents */
	restore_flags( flags );
	return 0;
}


static word locate_chip( void )
{

	if ( a_super_io_chip_appears_to_be_at( 0x26e ) ) return 0x26e;
	if ( a_super_io_chip_appears_to_be_at( 0x2e ) ) return 0x2e;
	return 0;
}


static flag_t see_index_ID( word wPort )
{
	if ( inb( wPort ) == 0x88 ) if ( inb( wPort ) == 0 ) return 1;
	return 0;
}


static int __init superio_init( void )
{
	word wPortBase;
	flag_t fVirgin;

	fVirgin = see_index_ID( 0x2e );

	wPortBase = locate_chip();

	if ( wPortBase == 0 ) {
		printk(KERN_ERR
			"%s: Super I/O chip not found. :-(\n",
			_szMyName
		);
		return -ETHINKPAD_HW_NOT_FOUND;
	}

	/* chip found at wPortBase */

	if ( !is_in_pnp_mode( wPortBase, wPortBase+1 ) ) {
		printk(KERN_ERR
			"%s: Super I/O chip found at port base 0x%x is not in plug-n-play mode. :-(\n",
			_szMyName, wPortBase
		);
		return -ETHINKPAD_HW_NOT_FOUND;
	}

	printk(KERN_INFO
		"%s: Super I/O chip (%svirgin) %sin plug-n-play mode found at port base 0x%x. :-)\n",
		_szMyName,
		fVirgin ? "" : "non-",
		is_in_pnp_mode( wPortBase, wPortBase+1 ) ? "" : "not ",
		wPortBase
	);

	/* At this point we assume we have found a working chip at wPortBase */

	/*** request the io ports ***/
	_presourceSuperio = request_region( wPortBase, 2, "superio" );
	if ( _presourceSuperio == NULL ) {
/*
 * The region is claimed by the programmable interrupt controller "pic1"
 * as part of the range 0x20--0x3f.
 */
#if 0
		printk(KERN_ERR
			"%s: I/O ports for Super I/O chip not available -- aborting.\n",
			_szMyName
		);
		return -ETHINKPAD_HW_NOT_FOUND;
#else
		printk(KERN_INFO
			"%s: I/O ports for Super I/O chip not available, but ignoring this.\n",
			_szMyName
		);
#endif
	}

	_wPortIndex = wPortBase;
	_wPortData = wPortBase + 1;

	if ( !thinkpad_ppde || !create_proc_read_entry(
		_szProcfile,
		S_IFREG | S_IRUGO,
		NULL,
		superio_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_handleSuperio = devfs_register(
		NULL /* dir */,
		"thinkpad/superio",
		DEVFS_FL_DEFAULT | DEVFS_FL_AUTO_DEVNUM,
		0 /* major */, 0 /* minor */,
		S_IFCHR | S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH,
		&_fileopsSuperio,
		NULL /* info */
	);
	if ( _devfs_handleSuperio == NULL ) {
		printk(KERN_ERR "%s: Could not devfs_register(). Aborting.\n", _szMyName );
		return -EIO;
	}
	/* devfs entry created */
#endif

	inter_module_register( _szImName, THIS_MODULE, &superio_do );

	return 0;
}


static void __exit superio_exit( void )
{

	inter_module_unregister( _szImName );

#ifdef CONFIG_DEVFS_FS
	devfs_unregister( _devfs_handleSuperio );
#endif

 	remove_proc_entry( _szProcfile, NULL );

	if ( _presourceSuperio != NULL ) release_resource( _presourceSuperio );

	return;
}

module_init(superio_init);
module_exit(superio_exit);
