/* bios.c: PnP BIOS communication module */
/*
 * $Header: /root/pnp/module/RCS/bios.c,v 1.4 1996/06/12 18:56:34 root Exp $
 *
 * $Log: bios.c,v $
 * Revision 1.4  1996/06/12  18:56:34  root
 * standard internal i/f & BIOS read/query
 *
 * Revision 1.3  1996/05/28  20:37:55  root
 * Read-only index version works
 *
 * Revision 1.2  1996/05/26  19:07:24  root
 * code restructured & modularized
 *
 * Revision 1.1  1996/05/26  17:55:34  root
 * Initial revision
 *
 * Revision 1.1  1996/05/26  15:38:03  root
 * Initial revision
 *
 */

/*
 * (c) Copyright 1996  D.W.Howells <dwh@nexor.co.uk>,
 */

#include <linux/sched.h>
#include <linux/mm.h>
#include <linux/malloc.h>
#include <linux/ioport.h>
#include <linux/delay.h>
#include <linux/ldt.h>
#include <asm/dma.h>
#include <asm/io.h>
#include <asm/bitops.h>
#include "pnp_if.h"

extern int pnp_bios__getnumdev(u_char*,u_int*);
extern int pnp_bios__getconfig(u_char*,csn_t*,u_short*);
extern int pnp_bios__getdevnode(u_char*,u_char*,u_int,u_int);

u_char pnp_bios_16ret32, pnp_bios_16ret32_end;

#define MYSEG(X) (sizeof(*pnpbios_ldt)*X | 0x0004)

/* BIOS PnP extension access data */
u_short pnpbios_entry;		/* 16-bit protected mode code entry offset */
u_int pnpbios_code_seg;		/* 16-bit protected mode code segment base */
u_int pnpbios_data_seg;		/* 16-bit protected mode data segment base */

/* BIOS access details */
u_short pnpbios_cs;		/* BIOS code segment */
u_short pnpbios_ds;		/* BIOS data segment */
u_short pnpbios_ret_cs;		/* 16->32 bit return from BIOS code segment */
u_short pnpbios_kdata_ds;	/* my kernel data, BIOS view */
struct desc_struct *pnpbios_ldt;/* the LDT I use in place the real one */
u_char *pnpbios_buffer;		/* BIOS communication data buffer */
u_int pnpbios_sizedev;		/* size of a BIOS device node */

/* PnP standard i/f */
static int pnp_bios_get_config(pnp_device *, pnp_config *);
static int pnp_bios_set_config(pnp_device *, const pnp_config *);
static int pnp_bios_set_active(pnp_device *, int);
static int pnp_bios_comment(pnp_device *, char *, int);
pnp_interface pnp_bios_interface = {
    "BIOS",
    pnp_bios_get_config,	/* get config */
    pnp_bios_set_config,	/* set config */
    pnp_bios_set_active,	/* activate */
    pnp_bios_comment		/* comment */
};

static char *pnp_find_end_tag(char *ptr, char *end);
static int pnp_read_resource_block(u_char *, pnp_config *);

/*****************************************************************************/
/* search for BIOS for "$PnP" flag */
int pnp_bios_search(void)
{
    u_char *ptr;

    /* look through the BIOS rom for the PnP header */
    for (ptr=(u_char*)0xE0000; ptr<=(u_char*)0xFFFF0; ptr+=0x00010)
	if (ptr[0]=='$' && ptr[1]=='P' && ptr[2]=='n' && ptr[3]=='P')
	    break;

    /* return error if not found extensions */
    if (ptr>=(u_char*)0x100000)
	return -ENOENT;

    printk("found PnP bios extensions v%x.%x @ %08x\n",
	   (ptr[4]>>4)&0xF, ptr[4]&0xF, (u_int)ptr
	   );
    pnpbios_entry = *(u_short*)(ptr+0x11);
    pnpbios_code_seg = *(u_int*)(ptr+0x13);
    pnpbios_data_seg = *(u_int*)(ptr+0x1D);

    printk("BIOS: entry=%04x@%08x data=@%08x\n",
	   pnpbios_entry,pnpbios_code_seg,pnpbios_data_seg
	   );
    return 0;
} /* end pnp_bios_search() */

/*****************************************************************************/
/* construct an LDT under the local process for BIOS access */
int pnp_bios_begin_access(int *taskix, struct desc_struct **curldt)
{
    u_int base, limit, ix;

#ifdef MODULE
    /* find out which entry in the task array is mine */
    for (ix=1; ix<NR_TASKS; ix++)
	if (task[ix]==current)
	    break;
#else
    /* find an unused entry in the task array (don't interfere with any that
     * might be in use)
     */
    for (ix=3; ix<NR_TASKS; ix++)
	if (!task[ix])
	    break;
#endif

    if (ix>=NR_TASKS)
	return -ESRCH;

    *taskix = ix;

    /* allocate a new LDT */
    pnpbios_ldt = (struct desc_struct*) vmalloc(5*LDT_ENTRY_SIZE);
    if (!pnpbios_ldt)
	return -ENOMEM;

    /* allocate a 32K buffer */
    pnpbios_buffer = (u_char*) kmalloc(32768,GFP_KERNEL);
    if (!pnpbios_buffer) {
	vfree(pnpbios_ldt);
	return -ENOMEM;
    }

    /* note where the process's real LDT is */
#ifdef MODULE
    *curldt = current->ldt;
#else
    *curldt = &default_ldt;
#endif

    /*=======================================================================*/
    /* set up my LDT - catch-all first */
    pnpbios_ldt[0].a = 0;
    pnpbios_ldt[0].b = 0;

    /* BIOS code first */
    base = pnpbios_code_seg + 0xC0000000;
    limit = 65536/PAGE_SIZE;
    pnpbios_ldt[1].a = 0x00000000 | ((base&0xFFFF)<<16) | (limit&0xFFFF);
    pnpbios_ldt[1].b = 0x00809a00 | (base&0xFF000000) | ((base>>16)&0xFF) |
	(limit&0x000F0000);
    pnpbios_cs = MYSEG(1);

    /* BIOS data next */
    base = pnpbios_data_seg + 0xC0000000;
    limit = 65536/PAGE_SIZE;
    pnpbios_ldt[2].a = 0x00000000 | ((base&0xFFFF)<<16) | (limit&0xFFFF);
    pnpbios_ldt[2].b = 0x00809200 | (base&0xFF000000) | ((base>>16)&0xFF) |
	(limit&0x000F0000);
    pnpbios_ds = MYSEG(2);

    /* then my kernel code space for 16-bit to 32-bit return */
    base = (u_int)&pnp_bios_16ret32 + 0xC0000000;
    limit = &pnp_bios_16ret32_end - &pnp_bios_16ret32;
    pnpbios_ldt[3].a = 0x00000000 | ((base&0xFFFF)<<16) | (limit&0xFFFF);
    pnpbios_ldt[3].b = 0x00409a00 | (base&0xFF000000) | ((base>>16)&0xFF) |
	(limit&0x000F0000);
    pnpbios_ret_cs = MYSEG(3);

    /* and then my kernel data space */
    base = (u_int)pnpbios_buffer + 0xC0000000;
    limit = 32768/PAGE_SIZE;
    pnpbios_ldt[4].a = 0x00000000 | ((base&0xFFFF)<<16) | (limit&0xFFFF);
    pnpbios_ldt[4].b = 0x00809200 | (base&0xFF000000) | ((base>>16)&0xFF) |
	(limit&0x000F0000);
    pnpbios_kdata_ds = MYSEG(4);

#if 0
    printk("\n");
    printk("LDT[%04x]: %08lx%08lx  LDT[%04x]: %08lx%08lx\n",
	   MYSEG(0),pnpbios_ldt[0].b,pnpbios_ldt[0].a,
	   MYSEG(1),pnpbios_ldt[1].b,pnpbios_ldt[1].a);
    printk("LDT[%04x]: %08lx%08lx  LDT[%04x]: %08lx%08lx\n",
	   MYSEG(2),pnpbios_ldt[2].b,pnpbios_ldt[2].a,
	   MYSEG(3),pnpbios_ldt[3].b,pnpbios_ldt[3].a);
    printk("LDT[%04x]: %08lx%08lx\n",
	   MYSEG(4),pnpbios_ldt[4].b,pnpbios_ldt[4].a);

    printk("pnp_bios_search: addr=%p\n",&pnp_search_bios);
    printk("pnp_bios_getnumdev: addr=%p\n",&pnp_bios_getnumdev);
    printk("pnp_bios_16ret32: addr=%p\n",&pnp_bios_16ret32);
#endif

    return 0;
} /* end pnp_bios_begin_access() */

/*****************************************************************************/
/* tidy up after accessing BIOS */
int pnp_bios_end_access(void)
{
    kfree(pnpbios_buffer);
    vfree(pnpbios_ldt);

    return 0;
} /* end pnp_bios_end_access() */

/*****************************************************************************/
/* get config data from BIOS */
int pnp_bios_access_config(pnp_card *card, u_char *rev, u_short *port,
			   csn_t *topcsn)
{
    int rt1, rt2, ret = 0;
    u_int taskix;
    struct desc_struct *curldt;
    pnp_device *dev, **pdevice;

    *rev = 0;
    *port = 0;
    *topcsn = 0;
    pnpbios_sizedev = 0;

    for (pdevice=&pnp_device_list; *pdevice; pdevice=&(*pdevice)->pd_next) {;}

    /*=======================================================================*/
    /* create a set of segments giving access to BIOS */
    rt1 = pnp_bios_begin_access(&taskix,&curldt);
    if (rt1<0)
	return rt1;

    /*=======================================================================*/
    /* substitute my LDT for the real one */
    set_ldt_desc(gdt+(taskix<<1)+FIRST_LDT_ENTRY, pnpbios_ldt, 5);
    load_ldt(taskix);

    /* access two informative BIOS functions */
    rt1 = pnp_bios__getnumdev(&card->pc_ndev,&pnpbios_sizedev);
    rt2 = pnp_bios__getconfig(rev,topcsn,port);

    /* read all the system device nodes if there are any */
    if (rt1==0 && card->pc_ndev>0) {
	/* get enough space to hold all available device nodes */
	card->pc_resources =
	    (u_char*) kmalloc(card->pc_ndev*pnpbios_sizedev,GFP_KERNEL);
	if (card->pc_resources) {
	    u_char ndev, *ptr, *ptr2;
	    int rt, loop;

	    /* read the system device nodes */
	    ndev=0x00;
	    ptr = card->pc_resources;
	    for (loop=card->pc_ndev; loop>0; loop--) {
		rt = pnp_bios__getdevnode(&ndev,ptr,pnpbios_sizedev,1);
		if (rt<0) {
		    card->pc_ndev--;
		    continue;
		}

		/* create a device structure to honour the node */
		dev = (pnp_device*) kmalloc(sizeof(*dev),GFP_KERNEL);
		if (!dev) {
		    ret = -ENOMEM;
		    break;
		}
		
		dev->pd_next = NULL;
		dev->pd_card = card;
		dev->pd_driver = NULL;
		dev->pd_resoffset = ptr - card->pc_resources;
		dev->pd_id = *(eisa_t*)&ptr[3];
		dev->pd_dev = ptr[2];
		memcpy(dev->pd_ifdata,ptr+7,5);

		*pdevice = dev;
		pdevice = &dev->pd_next;

		/* dispose of unwanted bits in resource list - current config
		 * and device header
		 */
		ptr2 = pnp_find_end_tag(ptr+12, ptr+rt);
		if (!ptr2) {
		    ret = -EIO;
		    break;
		}
		ptr2 += (*ptr2 & 0x07) + 1;

		/* shift down the possible configs and compat dev IDs */
		rt -= ptr2 - ptr;
		memmove(ptr,ptr2,rt);

		/* remove the end tag between configs & compats */
		ptr2 = pnp_find_end_tag(ptr, ptr+rt);
		if (!ptr2) {
		    ret = -EIO;
		    break;
		}
		rt -= ptr - ptr2;
		ptr = ptr2;
		ptr2 += (*ptr2 & 0x07) + 1;

		rt -= ptr2 - ptr;
		memmove(ptr,ptr2,rt);

		ptr += rt;
	    }
	}
	else {
	    ret = -ENOMEM;
	}
    }

    /*=======================================================================*/
    /* restore real LDT of the host process */
    set_ldt_desc(gdt+(taskix<<1)+FIRST_LDT_ENTRY, curldt, LDT_ENTRIES);
    load_ldt(taskix);

    pnp_bios_end_access();

    return ret;

} /* end pnp_bios_access_config() */

/*****************************************************************************/
/* initialize pnp bios part */
int pnp_bios_init(void)
{
    pnp_card *card;
    int rt;
    u_char rev;

    rt = pnp_bios_search();
    if (rt<0)
	return rt;
    
    card = (pnp_card*) kmalloc(sizeof(*card),GFP_KERNEL);
    card->pc_interface = &pnp_bios_interface;
    card->pc_csn = 0;
    card->pc_ndev = 0;
    card->pc_id = PNPDRVID('P','N','P',0x0C0,0);
    card->pc_unique = 0;
    card->pc_resources = NULL;
    card->pc_ressize = 0;

    card->pc_next = pnp_card_list;
    pnp_card_list = card;

    rt = pnp_bios_access_config(card, &rev, &pnp_isa_rdport,
				&pnp_isa_last_csn);
    if (rt<0) {
	printk("BIOS: init failed\n");
	return rt;
    }

    printk("BIOS: revision=%1x.%1x ncsn=%02x rd_port=%04x\n",
	   (rev>>4)&0x0f,rev&0x0F,pnp_isa_last_csn,pnp_isa_rdport);
    printk("BIOS: nsysdev=%02x\n",card->pc_ndev);

    return 0;
} /* end pnp_bios_init() */

/*****************************************************************************/
/* look for an END tag */
static char *pnp_find_end_tag(char *ptr, char *end)
{
    /* read from the resource list till the end tag is found */
    while (ptr<end) {
	if ((*ptr&0x78)==(PNPSTAG_END<<3))
	    return ptr;

	/* skip large descriptors */
	if (*ptr&0x80) {
	    ptr++;
	    if (ptr+2>=end)
		return NULL;
	    ptr += *(u_short*)ptr + 2;
	    continue;
	}

	/* skip small descriptors */
	ptr += (*ptr&0x07) + 1;
    }
    return 0;
} /* end pnp_find_end_tag() */

/*****************************************************************************/
/* turn resources into a more amenable form */
static int pnp_read_resource_block(u_char *res, pnp_config *conf)
{
    static u_char irqtab[5] = { 2, 2, 0, 3, 1 };
    u_char gotmem = 0, rs = 0;
    u_char irq=0, dma=0, io=0, mem=0;
    u_short len = 0, tmp;

    /* indicate no resources yet */
    memset(conf,0,sizeof(*conf));

    /*=======================================================================*/
    /* read from the resource list till the end tag is found */
    while (1) {
	rs = *(res++);
	if (rs&0x80) {
	    /* large resource */
	    len = *(u_short*)res;
	    res += 2;

	    switch (rs&0x7F) {
	     case PNPLTAG_MEM:
		if (gotmem && conf->pc_memtype)
		    return -EIO;
		gotmem = 1;
		conf->pc_memtype = 0;
		if (mem>=4)
		    return -EIO;
		conf->pc_mem[mem].type = (res[0]>>3)&0x03;
		conf->pc_mem[mem].base = (void*)(*(u_short*)(res+1) << 8);
		conf->pc_mem[mem].top = (void*)(*(u_short*)(res+7) << 8);
		(u_int)conf->pc_mem[mem].top += (u_int)conf->pc_mem[mem].base;
		set_bit(mem,&conf->pc_s.mem);
		mem++;
		break;
	     case PNPLTAG_MEM32:
		if (gotmem && !conf->pc_memtype)
		    return -EIO;
		gotmem = 1;
		conf->pc_memtype = 1;
		if (mem>=4)
		    return -EIO;
		conf->pc_mem[mem].type = (res[0]>>3)&0x03;
		conf->pc_mem[mem].base = *(void**)&res[1];
		(u_int)conf->pc_mem[mem].top =
		    (u_int)conf->pc_mem[mem].base + *(u_int*)&res[13];
		set_bit(mem,&conf->pc_s.mem);
		mem++;
		break;
	     case PNPLTAG_MEM32_FIXED:
		if (gotmem && !conf->pc_memtype)
		    return -EIO;
		gotmem = 1;
		conf->pc_memtype = 1;
		if (mem>=4)
		    return -EIO;
		conf->pc_mem[mem].type = (res[0]>>3)&0x03;
		conf->pc_mem[mem].base = *(void**)&res[1];
		(u_int)conf->pc_mem[mem].top =
		    (u_int)conf->pc_mem[mem].base + *(u_int*)&res[5];
		set_bit(mem,&conf->pc_s.mem);
		mem++;
		break;
	     default:
		break;
	    }
	}
	else {
	    /*===============================================================*/
	    /* small resource */
	    len = rs & 0x07;
	    rs >>= 3;
	    switch (rs) {
	     case PNPSTAG_LOGICAL_DEV_ID:
	     case PNPSTAG_END:
		return 0;
	     case PNPSTAG_IRQ:
		if (irq>=2)
		    return -EIO;
		tmp = ~*(u_short*)&res[0];
		conf->pc_irq[irq].num =
		    find_first_zero_bit(&tmp,16) & 0x0f;
		tmp = ~res[2];
		conf->pc_irq[irq].type = len<=2 ? 2 :
		    irqtab[find_first_zero_bit(&tmp,4)+1];

		set_bit(irq,&conf->pc_s.irq);
		irq++;
		break;
	     case PNPSTAG_DMA:
		if (dma>=2)
		    return -EIO;
		tmp = ~res[0];
		conf->pc_dma[dma].chan =
		    find_first_zero_bit(&tmp,8);
		set_bit(dma,&conf->pc_s.dma);
		dma++;
		break;
	     case PNPSTAG_IO:
		if (io>=7)
		    return -EIO;
		conf->pc_io[io].base = *(u_short*)(res+1);
		set_bit(io,&conf->pc_s.io);
		io++;
		break;
	     case PNPSTAG_IO_FIXED:
		if (io>=7)
		    return -EIO;
		conf->pc_io[io].base = *(u_short*)(res+0);
		set_bit(io,&conf->pc_s.io);
		io++;
		break;
	     default:
		break;
	    }
	} /* end if large tag else... */

	res += len;
    }
    return 0;

} /* end pnp_read_resource_block() */

/*****************************************************************************/
/* read the configuration of a BIOS device */
static int pnp_bios_get_config(pnp_device *device, pnp_config *conf)
{
    int rt;
    u_int taskix;
    struct desc_struct *curldt;
    u_char ndev;
    u_char *node;

    /*=======================================================================*/
    /* create a set of segments giving access to BIOS */
    rt = pnp_bios_begin_access(&taskix,&curldt);
    if (rt<0)
	return rt;

    /* get a buffer to hold the device node */
    node = (u_char*) kmalloc(pnpbios_sizedev,GFP_KERNEL);
    if (!node) {
	rt = -ENOMEM;
	goto error;
    }

    /*=======================================================================*/
    /* substitute my LDT for the real one */
    set_ldt_desc(gdt+(taskix<<1)+FIRST_LDT_ENTRY, pnpbios_ldt, 5);
    load_ldt(taskix);

    ndev = device->pd_dev;
    rt = pnp_bios__getdevnode(&ndev,node,pnpbios_sizedev,1);

    /* restore real LDT of the host process */
    set_ldt_desc(gdt+(taskix<<1)+FIRST_LDT_ENTRY, curldt, LDT_ENTRIES);
    load_ldt(taskix);

    /*=======================================================================*/
    if (rt>=0) {
	/* extract the configuration */
	rt = pnp_read_resource_block(node+12,conf);
    }

 error:
    if (!node)
	kfree(node);
    pnp_bios_end_access();

    return rt;

} /* end pnp_bios_get_config() */

/*****************************************************************************/
/* set the configuration of a BIOS device */
static int pnp_bios_set_config(pnp_device *device, const pnp_config *conf)
{
    return -ENOSYS;
} /* end pnp_bios_set_config() */

/*****************************************************************************/
/* (de)activate a BIOS device */
static int pnp_bios_set_active(pnp_device *device, int active)
{
    return -ENOSYS;
} /* end pnp_bios_set_active() */

/*****************************************************************************/
/* comment on a device for /proc/pnp */
static int pnp_bios_comment(pnp_device *device, char *buffer, int size)
{
    return sprintf(buffer,"type=%02x,%02x,%02x attr=%04x ",
		   device->pd_ifdata[0],
		   device->pd_ifdata[1],
		   device->pd_ifdata[2],
		   *(u_short*)(device->pd_ifdata+3)
		   );

} /* end pnp_bios_comment() */
