/* 
 * File...........: linux/drivers/s390/block/dasd_fba.c
 * Author(s)......: Holger Smolinski <Holger.Smolinski@de.ibm.com>
 * Bugreports.to..: <Linux390@de.ibm.com>
 * (C) IBM Corporation, IBM Deutschland Entwicklung GmbH, 1999,2000
 *          fixed partition handling and HDIO_GETGEO
 */

#include <linux/config.h>
#include <linux/stddef.h>
#include <linux/kernel.h>
#include <asm/debug.h>

#include <linux/slab.h>
#include <linux/hdreg.h>	/* HDIO_GETGEO                      */
#include <linux/blk.h>

#include <asm/ccwcache.h>
#include <asm/idals.h>
#include <asm/ebcdic.h>
#include <asm/io.h>
#include <asm/irq.h>
#include <asm/s390dyn.h>

#include "dasd_int.h"
#include "dasd_fba.h"
#include "dasd_3370_erp.h"
#include "dasd_9336_erp.h"

#ifdef PRINTK_HEADER
#undef PRINTK_HEADER
#endif				/* PRINTK_HEADER */
#define PRINTK_HEADER DASD_NAME"(fba):"

#define DASD_FBA_CCW_WRITE 0x41
#define DASD_FBA_CCW_READ 0x42
#define DASD_FBA_CCW_LOCATE 0x43
#define DASD_FBA_CCW_DEFINE_EXTENT 0x63

dasd_discipline_t dasd_fba_discipline;

typedef struct
    dasd_fba_private_t {
	dasd_fba_characteristics_t rdc_data;
} dasd_fba_private_t;

#ifdef CONFIG_DASD_DYNAMIC
static
devreg_t dasd_fba_known_devices[] = {
	{
	      ci: { hc: {ctype:0x6310, dtype:0x9336}},
	      flag:(DEVREG_MATCH_CU_TYPE |
                    DEVREG_MATCH_DEV_TYPE | DEVREG_TYPE_DEVCHARS),
              oper_func:dasd_oper_handler
        },
	{
                ci: { hc: {ctype:0x3880, dtype:0x3370}},
                flag:(DEVREG_MATCH_CU_TYPE |
                      DEVREG_MATCH_DEV_TYPE | DEVREG_TYPE_DEVCHARS),
                oper_func:dasd_oper_handler
        }
};
#endif
static inline int
define_extent (ccw1_t * ccw, DE_fba_data_t * DE_data, int rw,
	       int blksize, int beg, int nr, ccw_req_t* cqr,
               dasd_device_t* device)
{
        int rc=0;
	memset (DE_data, 0, sizeof (DE_fba_data_t));
	ccw->cmd_code = DASD_FBA_CCW_DEFINE_EXTENT;
	ccw->count = 16;
	if ((rc=dasd_set_normalized_cda (ccw, __pa (DE_data), cqr, device)))
                return rc;
	if (rw == WRITE)
		(DE_data->mask).perm = 0x0;
	else if (rw == READ)
		(DE_data->mask).perm = 0x1;
	else
		DE_data->mask.perm = 0x2;
	DE_data->blk_size = blksize;
	DE_data->ext_loc = beg;
	DE_data->ext_end = nr - 1;
        return rc;
}

static inline void
locate_record (ccw1_t * ccw, LO_fba_data_t * LO_data, int rw, int block_nr,
	       int block_ct, ccw_req_t* cqr, dasd_device_t* device)
{
	memset (LO_data, 0, sizeof (LO_fba_data_t));
	ccw->cmd_code = DASD_FBA_CCW_LOCATE;
	ccw->count = 8;
	dasd_set_normalized_cda (ccw, __pa (LO_data), cqr, device);
	if (rw == WRITE)
		LO_data->operation.cmd = 0x5;
	else if (rw == READ)
		LO_data->operation.cmd = 0x6;
	else
		LO_data->operation.cmd = 0x8;
	LO_data->blk_nr = block_nr;
	LO_data->blk_ct = block_ct;
}

static int
dasd_fba_id_check (s390_dev_info_t * info)
{
	if (info->sid_data.cu_type == 0x3880)
		if (info->sid_data.dev_type == 0x3370)
			return 0;
	if (info->sid_data.cu_type == 0x6310)
		if (info->sid_data.dev_type == 0x9336)
			return 0;
	return -ENODEV;
}

static int
dasd_fba_check_characteristics (struct dasd_device_t *device)
{
	int rc = -ENODEV;
	void *rdc_data;
	dasd_fba_private_t *private;

	if (device == NULL) {
		printk (KERN_WARNING PRINTK_HEADER
			"Null device pointer passed to characteristics checker\n");
                return -ENODEV;
	}
	device->private = kmalloc (sizeof (dasd_fba_private_t), GFP_KERNEL);
	if (device->private == NULL) {
		printk (KERN_WARNING PRINTK_HEADER
			"memory allocation failed for private data\n");
                rc = -ENOMEM;
                goto fail;
	}
	private = (dasd_fba_private_t *) device->private;
	rdc_data = (void *) &(private->rdc_data);
	rc = read_dev_chars (device->devinfo.irq, &rdc_data, 32);
	if (rc) {
		printk (KERN_WARNING PRINTK_HEADER
			"Read device characteristics returned error %d\n", rc);
                goto fail;
	}
	printk (KERN_INFO PRINTK_HEADER
		"%04X on sch %d: %04X/%02X(CU:%04X/%02X) %dMB at(%d B/blk)\n",
		device->devinfo.devno, device->devinfo.irq,
		device->devinfo.sid_data.dev_type,
		device->devinfo.sid_data.dev_model,
		device->devinfo.sid_data.cu_type,
		device->devinfo.sid_data.cu_model,
		(private->rdc_data.blk_bdsa *
		 (private->rdc_data.blk_size >> 9)) >> 11,
		private->rdc_data.blk_size);
        goto out;
 fail:
        if ( rc ) {
                kfree(device->private);
                device->private = NULL;
        }
        
 out:
	return rc;
}

static int
dasd_fba_do_analysis (struct dasd_device_t *device)
{
	int rc = 0;
	int sb;
	dasd_fba_private_t *private = (dasd_fba_private_t *) device->private;
	int bs = private->rdc_data.blk_size;

	memset (&(device->sizes), 0, sizeof (dasd_sizes_t));
	switch (bs) {
	case 512:
	case 1024:
	case 2048:
	case 4096:
		device->sizes.bp_block = bs;
		break;
	default:
		printk (KERN_INFO PRINTK_HEADER
			"/dev/%s (%04X): unknown blocksize %d\n",
			device->name, device->devinfo.devno, bs);
		return -EMEDIUMTYPE;
	}
	device->sizes.s2b_shift = 0;	/* bits to shift 512 to get a block */
	for (sb = 512; sb < bs; sb = sb << 1)
		device->sizes.s2b_shift++;

	device->sizes.blocks = (private->rdc_data.blk_bdsa);
	device->sizes.pt_block = 1;

	return rc;
}

static int
dasd_fba_fill_geometry (struct dasd_device_t *device, struct hd_geometry *geo)
{
	int rc = 0;
	unsigned long sectors = device->sizes.blocks << device->sizes.s2b_shift;
	unsigned long tracks = sectors >> 6;
	unsigned long cyls = tracks >> 4;

	switch (device->sizes.bp_block) {
	case 512:
	case 1024:
	case 2048:
	case 4096:
		break;
	default:
		return -EINVAL;
	}
	geo->cylinders = cyls;
	geo->heads = 16;
	geo->sectors = 128 >> device->sizes.s2b_shift;
	return rc;
}

static dasd_era_t
dasd_fba_examine_error (ccw_req_t * cqr, devstat_t * stat)
{
	dasd_device_t *device = (dasd_device_t *) cqr->device;
	if (stat->cstat == 0x00 &&
	    stat->dstat == (DEV_STAT_CHN_END | DEV_STAT_DEV_END))
		    return dasd_era_none;

	switch (device->devinfo.sid_data.dev_model) {
	case 0x3370:
		return dasd_3370_erp_examine (cqr, stat);
	case 0x9336:
		return dasd_9336_erp_examine (cqr, stat);
	default:
		return dasd_era_recover;
	}
}

static dasd_erp_action_fn_t
dasd_fba_erp_action (ccw_req_t * cqr)
{
	return dasd_default_erp_action;
}

static dasd_erp_postaction_fn_t
dasd_fba_erp_postaction (ccw_req_t * cqr)
{
	if (cqr->function == dasd_default_erp_action)
		return dasd_default_erp_postaction;
	printk (KERN_WARNING PRINTK_HEADER
		"unknown ERP action %p\n", cqr->function);
	return NULL;
}

static ccw_req_t *
dasd_fba_build_cp_from_req (dasd_device_t * device, struct request *req)
{
	ccw_req_t *rw_cp = NULL;
	int rw_cmd;
	int bhct, i = 0;
	long size;
	ccw1_t *ccw;
	DE_fba_data_t *DE_data;
	LO_fba_data_t *LO_data;
	struct buffer_head *bh;
	dasd_fba_private_t *private = (dasd_fba_private_t *) device->private;
	int byt_per_blk = device->sizes.bp_block;
        
	if (req->cmd == READ) {
		rw_cmd = DASD_FBA_CCW_READ;
	} else if (req->cmd == WRITE) {
		rw_cmd = DASD_FBA_CCW_WRITE;
	} else {
		PRINT_ERR ("Unknown command %d\n", req->cmd);
		return NULL;
	}
	/* Build the request */
	/* count hs to prevent errors, when bh smaller than block */
        bh = req -> bh;
	bhct = 0;
        while ( bh != NULL ) {
                if (bh->b_size < byt_per_blk) {
                        BUG();
                }
                bhct += bh->b_size >> (device->sizes.s2b_shift+9);
                bh = bh->b_reqnext;
        }
        
        if (private->rdc_data.mode.bits.data_chain) {
                rw_cp = dasd_alloc_request (dasd_fba_discipline.name,
                                            2 + bhct,
                                            sizeof (DE_fba_data_t) +
                                            sizeof (LO_fba_data_t),
                                            device);
        } else {
                rw_cp = dasd_alloc_request (dasd_fba_discipline.name,
                                            1 + 2 * bhct,
                                            sizeof (DE_fba_data_t) +
                                            bhct * sizeof (LO_fba_data_t),
                                            device);
        }
	if (!rw_cp) {
		return NULL;
	}
	DE_data = rw_cp->data;
	LO_data = rw_cp->data + sizeof (DE_fba_data_t);
	ccw = rw_cp->cpaddr;

	if (define_extent (ccw, DE_data, req->cmd, byt_per_blk,
                           req->sector, req->nr_sectors, rw_cp, device)) {
                goto clear_rw_cp;
        }
	ccw->flags |= CCW_FLAG_CC;
        ccw ++;
        locate_record (ccw, LO_data, req->cmd, 0, 
                       private->rdc_data.mode.bits.data_chain ? bhct : 1, rw_cp, device);
        if (ccw->cda == 0) {
                goto clear_rw_cp;
        }
        ccw->flags |= CCW_FLAG_CC;
        
        bh = req -> bh;
        i = 0;
        while ( bh != NULL ) {
                for (size = 0; size < bh->b_size; size += byt_per_blk) {
                        ccw ++;
                        ccw->cmd_code = rw_cmd;
                        ccw->count = byt_per_blk;
                        if (dasd_set_normalized_cda (ccw,__pa (bh->b_data + size), rw_cp, device)) {
                                goto clear_rw_cp;
                        }
                        if (private->rdc_data.mode.bits.data_chain) {
                                ccw->flags |= CCW_FLAG_DC;
                        } else {
                                ccw->flags |= CCW_FLAG_CC;
                        }
                }
                bh = bh->b_reqnext;
                if ( bh != NULL &&
                     !(private->rdc_data.mode.bits.data_chain)) {
                        ccw++;
                        i++;
                        LO_data++;
                        locate_record (ccw, LO_data, req->cmd, i, 1, rw_cp, device);
                        if (ccw->cda == 0) {
                                goto clear_rw_cp;
                        }
                        ccw->flags |= CCW_FLAG_CC;
                }
        }
	ccw->flags &= ~(CCW_FLAG_DC | CCW_FLAG_CC);

	rw_cp->device = device;
	rw_cp->expires = 5 * TOD_MIN;		/* 5 minutes */
	rw_cp->req = req;
	check_then_set (&rw_cp->status, CQR_STATUS_EMPTY, CQR_STATUS_FILLED);
        goto out;
 clear_rw_cp:
        dasd_free_request (rw_cp, device);
        rw_cp = NULL;
 out:
	return rw_cp;
}

static int
dasd_fba_fill_info (dasd_device_t * device, dasd_information_t * info)
{
	int rc = 0;
	info->label_block = 1;
	info->FBA_layout = 1;
	info->characteristics_size = sizeof (dasd_fba_characteristics_t);
	memcpy (info->characteristics,
		&((dasd_fba_private_t *) device->private)->rdc_data,
		sizeof (dasd_fba_characteristics_t));
	info->confdata_size = 0;
	return rc;
}


static char *
dasd_fba_dump_sense (struct dasd_device_t *device, ccw_req_t * req)
{
	char *page = (char *) get_free_page (GFP_KERNEL);
	int len;
	if (page == NULL) {
		return NULL;
	}
	len = sprintf (page, KERN_WARNING PRINTK_HEADER
		       "device %04X on irq %d: I/O status report:\n",
		       device->devinfo.devno, device->devinfo.irq);

	return page;
}

dasd_discipline_t dasd_fba_discipline = {
        owner: THIS_MODULE,
	name:"FBA ",
	ebcname:"FBA ",
	max_blocks:((PAGE_SIZE >> 1) / sizeof (ccw1_t) - 1),
	id_check:dasd_fba_id_check,
	check_characteristics:dasd_fba_check_characteristics,
	do_analysis:dasd_fba_do_analysis,
	fill_geometry:dasd_fba_fill_geometry,
	start_IO:dasd_start_IO,
	term_IO:dasd_term_IO,
	examine_error:dasd_fba_examine_error,
	erp_action:dasd_fba_erp_action,
	erp_postaction:dasd_fba_erp_postaction,
	build_cp_from_req:dasd_fba_build_cp_from_req,
	dump_sense:dasd_fba_dump_sense,
	int_handler:dasd_int_handler,
	fill_info:dasd_fba_fill_info,
};

int
dasd_fba_init (void)
{
	int rc = 0;
	printk (KERN_INFO PRINTK_HEADER
		"%s discipline initializing\n", dasd_fba_discipline.name);
	ASCEBC (dasd_fba_discipline.ebcname, 4);
	dasd_discipline_add (&dasd_fba_discipline);
#ifdef CONFIG_DASD_DYNAMIC
	{
		int i;
		for (i = 0;
		     i < sizeof (dasd_fba_known_devices) / sizeof (devreg_t);
		     i++) {
			printk (KERN_INFO PRINTK_HEADER
				"We are interested in: Dev %04X/%02X @ CU %04X/%02x\n",
				dasd_fba_known_devices[i].ci.hc.dtype,
				dasd_fba_known_devices[i].ci.hc.dmode,
				dasd_fba_known_devices[i].ci.hc.ctype,
				dasd_fba_known_devices[i].ci.hc.cmode);
			s390_device_register (&dasd_fba_known_devices[i]);
		}
	}
#endif				/* CONFIG_DASD_DYNAMIC */
        return rc;
}

void
dasd_fba_cleanup( void ) {
        printk ( KERN_INFO PRINTK_HEADER
                 "%s discipline cleaning up\n", dasd_fba_discipline.name);
#ifdef CONFIG_DASD_DYNAMIC
        {
	int i;
        for ( i=0; i<sizeof(dasd_fba_known_devices)/sizeof(devreg_t); i++) {
                s390_device_unregister(&dasd_fba_known_devices[i]);
        }
        }
#endif /* CONFIG_DASD_DYNAMIC */
        dasd_discipline_del(&dasd_fba_discipline);
}

#ifdef MODULE
int
init_module (void)
{
	int rc = 0;
	rc = dasd_fba_init ();
	return rc;
}

void
cleanup_module (void)
{
	dasd_fba_cleanup ();
	return;
}
#endif


/*
 * Overrides for Emacs so that we follow Linus's tabbing style.
 * Emacs will notice this stuff at the end of the file and automatically
 * adjust the settings for this buffer only.  This must remain at the end
 * of the file.
 * ---------------------------------------------------------------------------
 * Local variables:
 * c-indent-level: 4 
 * c-brace-imaginary-offset: 0
 * c-brace-offset: -4
 * c-argdecl-indent: 4
 * c-label-offset: -4
 * c-continued-statement-offset: 4
 * c-continued-brace-offset: 0
 * indent-tabs-mode: nil
 * tab-width: 8
 * End:
 */