/* drivers.c: interface to PnP capable device drivers */
/*
 * $Header: /root/pnp/module/RCS/drivers.c,v 1.5 1996/07/08 21:40:42 root Exp $
 *
 * $Log: drivers.c,v $
 * Revision 1.5  1996/07/08  21:40:42  root
 * add driver lock/unlock facilities
 *
 * Revision 1.4  1996/06/20  19:14:29  root
 * empty BIOS device possible config block handled correctly
 *
 * Revision 1.3  1996/06/15  12:39:49  root
 * ioctl access added
 *
 * Revision 1.2  1996/06/12  18:56:34  root
 * standard internal i/f & BIOS read/query
 *
 * Revision 1.1  1996/06/09  10:41:29  root
 * card configuration now done
 *
 *
 */

/*
 * (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"

/* driver list */
pnp_driver *pnp_driver_list = NULL;

extern void pnp_inc_mod_usage(void);
extern void pnp_dec_mod_usage(void);

#if 0
static void pnp_dump_possibility(pnp_possibility*);
#endif

/*****************************************************************************/
/* search a device for compatible EISA id descriptors */
static int pnp_find_eisa_id(pnp_device *device, eisa_t id)
{
    u_char *ptr;

    if (id==device->pd_id)
	return 0;

    if (!device->pd_card && !device->pd_card->pc_resources)
	return -ENOENT;

    /* skip over LOGDEV tag */
    ptr = device->pd_card->pc_resources + device->pd_resoffset;
    ptr += (*ptr&0x07) + 1;

    /* read from the resource list till the end tag is found */
    while (1) {
	/* stop when the end tag or a logial device ID is found */
	if ((*ptr&0x78)==(PNPSTAG_END<<3) ||
	    (*ptr&0x78)==(PNPSTAG_LOGICAL_DEV_ID<<3)
	    )
	    break;

	/* if found a compatible device ID, return if it matches */
	if ((*ptr&0x78)==(PNPSTAG_COMPATIBLE_DEV_ID<<3))
	    if (*(eisa_t*)(ptr+1)==id)
		return 0;

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

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

} /* end pnp_find_eisa_id() */

/*****************************************************************************/
/* add a driver to the system (must search uncommitted devices for a match) */
int pnp_register(pnp_driver *driver)
{
    pnp_driver *pdr;
    pnp_device *pd;
    u_int tmp;
    int rt;

    /* make sure the driver list does not contain one of the same ID */
    for (pdr=pnp_driver_list; pdr; pdr=pdr->pdr_next)
	if (driver->pdr_id==pdr->pdr_id)
	    return -EEXIST;

    /* link in */
    driver->pdr_next = pnp_driver_list;
    pnp_driver_list = driver;

    tmp = ((u_char*)&driver->pdr_id)[0]<<8 | ((u_char*)&driver->pdr_id)[1];
    printk("Added PnP driver: %c%c%c%02x%02x\n",
	   ((tmp>>10)&0x1F)+'A'-1,
	   ((tmp>>5)&0x1F)+'A'-1,
	   ((tmp>>0)&0x1F)+'A'-1,
	   ((u_char*)&driver->pdr_id)[2],
	   ((u_char*)&driver->pdr_id)[3]
	   );

    /*=======================================================================*/
    /* scan the device list for a device that this driver can drive */
    tmp = driver->pdr_id;
    for (pd=pnp_device_list; pd; pd=pd->pd_next)
	if (pnp_find_eisa_id(pd,tmp)==0 && !pd->pd_driver) {
	    /* try and attach the device to the driver */
	    pd->pd_driver = driver;
	    rt = (*driver->pdr_attach)(pd,1);
	    if (rt<0)
		pd->pd_driver = NULL;	/* driver refused */
	}

    pnp_inc_mod_usage();

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

/*****************************************************************************/
/* remove a driver from the system (must detach devices) */
int pnp_unregister(pnp_driver *driver)
{
    pnp_driver **ppdr;
    pnp_device *pd;
    int rt;

    /*=======================================================================*/
    /* scan the device list for a device that this driver is driving */
    for (pd=pnp_device_list; pd; pd=pd->pd_next)
	if (pd->pd_driver==driver) {
	    /* detach the device from the driver */
	    rt = (*driver->pdr_attach)(pd,0);
	    if (rt<0)
		return rt;
	    pd->pd_driver = NULL;
	}

    /*=======================================================================*/
    /* remove the driver from the driver list */
    for (ppdr=&pnp_driver_list; *ppdr; ppdr=&(*ppdr)->pdr_next)
	if (*ppdr==driver)
	    break;

    if (!*ppdr)
	return -ENODEV;

    *ppdr = driver->pdr_next;

    pnp_dec_mod_usage();

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

/*****************************************************************************/
/* activate a device & driver */
void pnp_drv_activate(pnp_device *device)
{
    /* switch on the device */
    device->pd_card->pc_interface->pi_set_active(device,1);

    /* activate the driver if present */
    if (device->pd_driver)
	device->pd_driver->pdr_control(device,PNPDC_ACTIVATE);

} /* end pnp_drv_activate() */

/*****************************************************************************/
/* deactivate a device & driver */
void pnp_drv_deactivate(pnp_device *device)
{
    /* deactivate the driver if present */
    if (device->pd_driver)
	device->pd_driver->pdr_control(device,PNPDC_DEACTIVATE);

    /* and now switch off the device */
    device->pd_card->pc_interface->pi_set_active(device,0);

} /* end pnp_drv_deactivate() */

/*****************************************************************************/
/* turn resources into a more amenable form (indexed by dependency block) */
int pnp_get_res_dep_block(pnp_device *device, int dep, pnp_possibility *poss)
{
    u_char in_dep = 0, gotmem = 0, rs = 0;
    u_char irq=0, dma=0, io=0, mem=0;
    u_short len = 0;
    const u_char *res;

    if (!device->pd_card && !device->pd_card->pc_resources)
	return -ENOENT;
    res = device->pd_card->pc_resources + device->pd_resoffset;

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

    /* skip over LOGDEV tag */
    if ((*res&0x78)==(PNPSTAG_LOGICAL_DEV_ID<<3))
	res += (*res&0x07) + 1;

    /*=======================================================================*/
    /* if next tag is end tag - no config */
    if (!(*res&0x80) && (*res>>3)==PNPSTAG_END)
	return -ENXIO;

    /*=======================================================================*/
    /* 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;

	    if (in_dep<=0 || dep==-1) {
		switch (rs&0x7F) {
		 case PNPLTAG_MEM:
		    if (gotmem && poss->pp_memtype)
			return -EIO;
		    gotmem = 1;
		    poss->pp_memtype = 0;
		    if (mem>=4)
			return -EIO;
		    poss->pp_mem[mem].low = (void*)(*(u_short*)(res+1) << 8);
		    poss->pp_mem[mem].high = (void*)(*(u_short*)(res+3) << 8);
		    poss->pp_mem[mem].align = *(u_short*)(res+5);
		    if (poss->pp_mem[mem].align==0)
			poss->pp_mem[mem].align = 0x10000;
		    poss->pp_mem[mem].range = *(u_short*)(res+7) << 8;
		    set_bit(mem,&poss->pp_s.mem);
		    mem++;
		    break;
		 case PNPLTAG_MEM32:
		    if (gotmem && !poss->pp_memtype)
			return -EIO;
		    gotmem = 1;
		    poss->pp_memtype = 1;
		    if (mem>=4)
			return -EIO;
		    poss->pp_mem[mem].low = (void*)(*(u_long*)(res+1));
		    poss->pp_mem[mem].high = (void*)(*(u_long*)(res+5));
		    poss->pp_mem[mem].align = *(u_short*)(res+9);
		    poss->pp_mem[mem].range = *(u_short*)(res+13);
		    set_bit(mem,&poss->pp_s.mem);
		    mem++;
		    break;
		 case PNPLTAG_MEM32_FIXED:
		    if (gotmem && !poss->pp_memtype)
			return -EIO;
		    gotmem = 1;
		    poss->pp_memtype = 1;
		    if (mem>=4)
			return -EIO;
		    poss->pp_mem[mem].low =
			poss->pp_mem[mem].high = (void*)(*(u_long*)(res+1));
		    poss->pp_mem[mem].align = 1;
		    poss->pp_mem[mem].range = *(u_short*)(res+5);
		    set_bit(mem,&poss->pp_s.mem);
		    mem++;
		    break;
		 default:
		    break;
		}
	    }
	}
	else {
	    /*===============================================================*/
	    /* small resource */
	    len = rs & 0x07;
	    rs >>= 3;
	    switch (rs) {
	     case PNPSTAG_LOGICAL_DEV_ID:
		return (in_dep==0 && dep>0) ? -ENXIO : 0;
	     case PNPSTAG_START_DEP:
		in_dep = 1;
		dep--;
		break;
	     case PNPSTAG_END_DEP:
		if (dep>=0)
		    return -ENXIO;
		in_dep = -1;
		break;
	     case PNPSTAG_END:
		return (in_dep==0 && dep>0) ? -ENXIO : 0;
	     default:
		if (in_dep<=0 || dep==-1) {
		    switch (rs) {
		     case PNPSTAG_IRQ:
			if (irq>=2)
			    return -EIO;
			poss->pp_irq[irq].nums[0] = res[0];
			poss->pp_irq[irq].nums[1] = res[1];
			poss->pp_irq[irq].type = len>2 ? res[2] : 1;
			set_bit(irq,&poss->pp_s.irq);
			irq++;
			break;
		     case PNPSTAG_DMA:
			if (dma>=2)
			    return -EIO;
			poss->pp_dma[dma].chans[0] = res[0];
			poss->pp_dma[dma].type = res[1];
			set_bit(dma,&poss->pp_s.dma);
			dma++;
			break;
		     case PNPSTAG_IO:
			if (io>=7)
			    return -EIO;
			poss->pp_io[io].low = *(u_short*)(res+1);
			poss->pp_io[io].high = *(u_short*)(res+3);
			poss->pp_io[io].align = res[5];
			poss->pp_io[io].range = res[6];
			set_bit(io,&poss->pp_s.io);
			io++;
			break;
		     case PNPSTAG_IO_FIXED:
			if (io>=7)
			    return -EIO;
			poss->pp_io[io].low =
			    poss->pp_io[io].high = *(u_short*)(res+0);
			poss->pp_io[io].range = res[2];
			set_bit(io,&poss->pp_s.io);
			io++;
			break;
		     default:
			break;
		    }
		}
		break;
	    }
	} /* end if large tag else... */

	res += len;
    }
    return 0;

} /* end pnp_get_res_dep_block() */

/*****************************************************************************/
/* try reconfiguration of a device */
int pnp_try_reconfigure(pnp_device *device, pnp_config *request)
{
    static const int irqtbit[] = { 1, 3, 0, 2 };
    static const u_char memtmask[] = { 0x01, 0x02, 0x03, 0x08 };
    pnp_config *conf = NULL;
    pnp_possibility *poss = NULL;
    int depnum, rt, loop, loop2;
    u_long irqflags[2] = { 0, 0 };

    /*=======================================================================*/
    /* allocate the holder for a config possibility */
    poss = (pnp_possibility*) kmalloc(sizeof(*poss),GFP_KERNEL);
    if (!poss)
	goto NOMEM;

    /* ask the driver to release all its currently open resources */
    if (device->pd_driver) {
	/* try to lock the driver */
	rt = device->pd_driver->pdr_control(device,PNPDC_LOCK);
	if (rt<0)
	    return rt;

	pnp_drv_deactivate(device);
	device->pd_driver->pdr_control(device,PNPDC_RELEASE_RES);

	/* also get driver's irqflags */
	irqflags[0] = device->pd_driver->pdr_irqflags[0];
	irqflags[1] = device->pd_driver->pdr_irqflags[1];
    } else {
	pnp_drv_deactivate(device);
    }

    /* get a space to hold the potential configuration */
    conf = (pnp_config*) kmalloc(sizeof(*conf),GFP_KERNEL);
    if (!conf)
	goto NOMEM;
    memset(conf,0,sizeof(*conf));

    /*=======================================================================*/
    /* examine each dependency block till one is found */
    depnum = 0;
    while (1) {
     NEXT:
	/* get next dependency block */
	rt = pnp_get_res_dep_block(device,depnum,poss);
	if (rt<0)
	    goto ERROR;
	depnum++;

	/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
	/* make sure request and resource aren't mutually exclusive */
	conf->pc_memtype = poss->pp_memtype;

	/* IRQ first */
	for (loop=0; loop<2; loop++) {
	    /* ignore if not requested */
	    if (!test_bit(loop,&request->pc_s.irq))
		continue;

	    /* reject if requested-off but available */
	    if (request->pc_irq[loop].num==0) {
		if (test_bit(loop,&poss->pp_s.irq))
		    goto NEXT;
		continue;
	    }

	    /* reject if requested but unavailable */
	    if (!test_bit(loop,&poss->pp_s.irq))
		goto NEXT;

	    /* reject if IRQ number is unavailable */
	    if (!test_bit(request->pc_irq[loop].num,poss->pp_irq[loop].nums))
		goto NEXT;

	    /* restrict IRQ number */
	    poss->pp_irq[loop].nums[0] = 0;
	    poss->pp_irq[loop].nums[1] = 0;
	    set_bit(request->pc_irq[loop].num,poss->pp_irq[loop].nums);

	    /* reject if type is unavailable */
	    if (request->pc_irq[loop].type!=0xFF) {
		if (!test_bit(irqtbit[request->pc_irq[loop].type],
			      &poss->pp_irq[loop].type))
		    goto NEXT;

		/* restrict */
		poss->pp_irq[loop].type = 0;
		set_bit(irqtbit[request->pc_irq[loop].type],
			&poss->pp_irq[loop].type);
	    }
	}

	/*-------------------------------------------------------------------*/
	/* DMA */
	for (loop=0; loop<2; loop++) {
	    /* ignore if not requested */
	    if (!test_bit(loop,&request->pc_s.dma))
		continue;

	    /* reject if requested-off but is available */
	    if (request->pc_dma[loop].chan==4) {
		if (test_bit(loop,&poss->pp_s.dma))
		    goto NEXT;
		continue;
	    }

	    /* reject if requested but unavailable */
	    if (!test_bit(loop,&poss->pp_s.dma))
		goto NEXT;

	    /* reject if DMA number is unavailable */
	    if (!test_bit(request->pc_dma[loop].chan,poss->pp_dma[loop].chans))
		goto NEXT;

	    /* restrict DMA number */
	    poss->pp_dma[loop].chans[0] = 0;
	    set_bit(request->pc_dma[loop].chan,poss->pp_dma[loop].chans);
	}

	/*-------------------------------------------------------------------*/
	/* IO */
	for (loop=0; loop<8; loop++) {
	    /* ignore if not requested */
	    if (!test_bit(loop,&request->pc_s.io))
		continue;

	    /* reject if requested-off but is available */
	    if (request->pc_io[loop].base==0) {
		if (test_bit(loop,&poss->pp_s.io))
		    goto NEXT;
		continue;
	    }

	    /* reject if requested but unavailable */
	    if (!test_bit(loop,&poss->pp_s.io))
		goto NEXT;

	    /* reject if IO range is unavailable or misaligned */
	    if (request->pc_io[loop].base<poss->pp_io[loop].low ||
		request->pc_io[loop].base>poss->pp_io[loop].high ||
		((request->pc_io[loop].base - poss->pp_io[loop].low) %
		 poss->pp_io[loop].align != 0)
		)
		goto NEXT;

	    /* restrict IO range */
	    poss->pp_io[loop].low =
		poss->pp_io[loop].high =
		    request->pc_io[loop].base;
	}

	/*-------------------------------------------------------------------*/
	/* MEM */
	for (loop=0; loop<4; loop++) {
	    /* ignore if not requested */
	    if (!test_bit(loop,&request->pc_s.mem))
		continue;

	    /* reject if requested-off but is available */
	    if (request->pc_mem[loop].base==0) {
		if (test_bit(loop,&poss->pp_s.mem))
		    goto NEXT;
		continue;
	    }

	    /* reject if requested but unavailable */
	    if (!test_bit(loop,&poss->pp_s.mem))
		goto NEXT;

	    /* reject if MEM range is unavailable, misaligned or bad type */
	    if (request->pc_mem[loop].base<poss->pp_mem[loop].low ||
		request->pc_mem[loop].base>poss->pp_mem[loop].high ||
		((request->pc_mem[loop].base - poss->pp_mem[loop].low) %
		 poss->pp_mem[loop].align != 0)
		)
		goto NEXT;

	    /* reject if mem type (8/16/32-bit data) not supported */
	    if (request->pc_mem[loop].type!=0xFF) {
		if (!test_bit(request->pc_mem[loop].type,
			      &memtmask[(poss->pp_mem[loop].type>>3)&0x03]))
		    goto NEXT;
		/* restrict */
		poss->pp_mem[loop].type &= 0xE7;
		poss->pp_mem[loop].type |= request->pc_mem[loop].type << 3;
	    }

	    /* restrict MEM range */
	    poss->pp_mem[loop].low =
		poss->pp_mem[loop].high =
		    request->pc_mem[loop].base;
	}

	/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
	/* select kernel resources for IRQs, DMAs and IO ports. Note that this
	 * bit is bypassed if "force" is specified
	 */
	if (request->pc_force)
	    goto FORCE;

	/* IRQs first - note that these can be shared under certain conditions,
	 * just to make life more interesting:-)
	 */
	for (loop=0; loop<2; loop++) {
	    if (!test_bit(loop,&poss->pp_s.irq))
		continue;	/* unused */
	    
	    /* ask the kernel about available IRQ's */
	    for (loop2=0; loop2<16; loop2++) {
		if (!test_bit(loop2,poss->pp_irq[loop].nums))
		    continue;
		if (is_irq_available(loop2,irqflags[loop]))
		    break;
	    }
	    if (loop2>=16)
		goto NEXT;	/* reject */

	    /* set the IRQ */
	    conf->pc_irq[loop].num = loop2;
	    conf->pc_irq[loop].type = 0;	/* only use low,edge for now */
	    set_bit(loop,&conf->pc_s.irq);
	}

	/*-------------------------------------------------------------------*/
	/* DMAs next */
	for (loop=0; loop<2; loop++) {
	    if (!test_bit(loop,&poss->pp_s.dma))
		continue;	/* unused */
	    
	    /* ask the kernel about available DMA's */
	    for (loop2=0; loop2<8; loop2++) {
		if (!test_bit(loop2,poss->pp_dma[loop].chans))
		    continue;
		if (is_dma_available(loop2))
		    break;
	    }
	    if (loop2>=8)
		goto NEXT;	/* reject */

	    /* set the DMA */
	    conf->pc_dma[loop].chan = loop2;
	    set_bit(loop,&conf->pc_s.dma);
	}

	/*-------------------------------------------------------------------*/
	/* IOs next */
	for (loop=0; loop<8; loop++) {
	    if (!test_bit(loop,&poss->pp_s.io))
		continue;	/* unused */
	    
	    /* ask the kernel about IO port ranges being available */
	    for (loop2 = poss->pp_io[loop].low;
		 loop2 <= poss->pp_io[loop].high;
		 loop2 += poss->pp_io[loop].align
		 ) {
		if (check_region(loop2,poss->pp_io[loop].range)==0)
		    break;
	    }
	    if (loop2 > poss->pp_io[loop].high)
		goto NEXT;	/* reject */

	    /* set the IO */
	    conf->pc_io[loop].base = loop2;
	    set_bit(loop,&conf->pc_s.io);
	}
     FORCE:

	/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
	/* tell the driver about the new configuration */
	if (device->pd_driver) {
	    rt = device->pd_driver->pdr_reconfigure(device,poss,conf);
	    if (rt==-EAGAIN)
		continue;
	    if (rt!=0)
		goto ERROR;
	}

	/* tell the device the new configuration */
	device->pd_card->pc_interface->pi_set_config(device,conf);
	goto SUCCESS;

    } /* end while find valid resources */

    goto NXIO;

    /*=======================================================================*/
 SUCCESS:
    rt = 0;
    goto ERROR;

 NOMEM:
    rt = -ENOMEM;
    goto ERROR;

 NXIO:
    rt = -ENXIO;

 ERROR:
    if (poss) kfree(poss);
    if (conf) kfree(conf);
    pnp_drv_activate(device);
    if (device->pd_driver)
	device->pd_driver->pdr_control(device,PNPDC_UNLOCK);
    return rt;
} /* end pnp_try_reconfigure() */

/*****************************************************************************/
#if 0
static void pnp_dump_possibility(pnp_possibility *poss)
{
    int loop, loop2;
    char buff[17];

    printk("dump poss: irq:%02x dma:%02x io:%02x mem:%02x\n",
	   poss->pp_s.irq,
	   poss->pp_s.dma,
	   poss->pp_s.io,
	   poss->pp_s.mem
	   );

    for (loop=0; loop<2; loop++) {
	if (!test_bit(loop,&poss->pp_s.irq))
	    continue;
	for (loop2=0; loop2<16; loop2++)
	    buff[loop2] =
		test_bit(loop2,poss->pp_irq[loop].nums) ?
		    "0123456789ABCDEF"[loop2] : '-';
	buff[16] = 0;
	printk("IRQ[%d]: %s t:%02x\n",loop,buff,poss->pp_irq[loop].type);
    }

    for (loop=0; loop<2; loop++) {
	if (!test_bit(loop,&poss->pp_s.dma))
	    continue;
	for (loop2=0; loop2<8; loop2++)
	    buff[loop2] =
		test_bit(loop2,poss->pp_dma[loop].chans) ? loop2 + '0' : '-';
	buff[8] = 0;
	printk("DMA[%d]: %s t:%02x\n",loop,buff,poss->pp_dma[loop].type);
    }

    for (loop=0; loop<7; loop++) {
	if (!test_bit(loop,&poss->pp_s.io))
	    continue;
	printk("IO[%d]: %02x-%02x al:%02x r:%02x\n",
	       loop,
	       poss->pp_io[loop].low,
	       poss->pp_io[loop].high,
	       poss->pp_io[loop].align,
	       poss->pp_io[loop].range
	       );
    }

    for (loop=0; loop<3; loop++) {
	if (!test_bit(loop,&poss->pp_s.mem))
	    continue;
	printk("MEM[%d]: %08x-%08x al:%08x r:%08x t:%c\n",
	       loop,
	       (u_int)poss->pp_mem[loop].low,
	       (u_int)poss->pp_mem[loop].high,
	       poss->pp_mem[loop].align,
	       poss->pp_mem[loop].range,
	       "bw?l"[(poss->pp_mem[loop].type>>3)&0x03]
	       );
    }

} /* end pnp_dump_possibility() */
#endif
