Home | History | Annotate | Line # | Download | only in dev
ccd.c revision 1.1
      1 /*
      2  * Copyright (c) 1988 University of Utah.
      3  * Copyright (c) 1990 The Regents of the University of California.
      4  * All rights reserved.
      5  *
      6  * This code is derived from software contributed to Berkeley by
      7  * the Systems Programming Group of the University of Utah Computer
      8  * Science Department.
      9  *
     10  * Redistribution and use in source and binary forms, with or without
     11  * modification, are permitted provided that the following conditions
     12  * are met:
     13  * 1. Redistributions of source code must retain the above copyright
     14  *    notice, this list of conditions and the following disclaimer.
     15  * 2. Redistributions in binary form must reproduce the above copyright
     16  *    notice, this list of conditions and the following disclaimer in the
     17  *    documentation and/or other materials provided with the distribution.
     18  * 3. All advertising materials mentioning features or use of this software
     19  *    must display the following acknowledgement:
     20  *	This product includes software developed by the University of
     21  *	California, Berkeley and its contributors.
     22  * 4. Neither the name of the University nor the names of its contributors
     23  *    may be used to endorse or promote products derived from this software
     24  *    without specific prior written permission.
     25  *
     26  * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
     27  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
     28  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
     29  * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
     30  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
     31  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
     32  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
     33  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
     34  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
     35  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
     36  * SUCH DAMAGE.
     37  *
     38  *	from: Utah Hdr: ccd.c 1.6 90/11/28
     39  *	from: @(#)cd.c	7.4 (Berkeley) 5/7/91
     40  *	$Id: ccd.c,v 1.1 1994/06/24 14:11:02 hpeyerl Exp $
     41  */
     42 
     43 /*
     44  * "Concatenated" disk driver.
     45  */
     46 #include "ccd.h"
     47 #if NCCD > 0
     48 
     49 #include <sys/param.h>
     50 #include <sys/systm.h>
     51 #include <sys/errno.h>
     52 #include <sys/dkstat.h>
     53 #include <sys/buf.h>
     54 #include <sys/malloc.h>
     55 #include <sys/conf.h>
     56 
     57 #include <dev/ccdvar.h>
     58 
     59 #ifdef DEBUG
     60 int ccddebug = 0x00;
     61 #define CDB_FOLLOW	0x01
     62 #define CDB_INIT	0x02
     63 #define CDB_IO		0x04
     64 #endif
     65 
     66 struct	buf ccdbuf[NCCD];
     67 struct	buf *ccdbuffer();
     68 char	*ccddevtostr();
     69 int	ccdiodone();
     70 
     71 #define	ccdunit(x)	((minor(x) >> 3) & 0x7)	/* for consistency */
     72 
     73 #define	getcbuf()	\
     74 	((struct buf *)malloc(sizeof(struct buf), M_DEVBUF, M_WAITOK))
     75 #define putcbuf(bp)	\
     76 	free((caddr_t)(bp), M_DEVBUF)
     77 
     78 struct ccd_softc {
     79 	int		 sc_flags;		/* flags */
     80 	size_t		 sc_size;		/* size of ccd */
     81 	int		 sc_ileave;		/* interleave */
     82 	int		 sc_nccdisks;		/* number of components */
     83 	struct ccdcinfo	 sc_cinfo[NCCDISKS];	/* component info */
     84 	struct ccdiinfo	 *sc_itable;		/* interleave table */
     85 	int		 sc_usecnt;		/* number of requests active */
     86 	struct buf	 *sc_bp;		/* "current" request */
     87 	int		 sc_dk;			/* disk index */
     88 } ccd_softc[NCCD];
     89 
     90 /* sc_flags */
     91 #define	CDF_ALIVE	0x01
     92 #define CDF_INITED	0x02
     93 
     94 /*
     95  * ccdattach() is called at boot time in new systems.  We do
     96  * nothing here since old systems will not call this.
     97  */
     98 
     99 void
    100 ccdattach(n)
    101         int n;
    102 {
    103 }
    104 
    105 ccdinit(ccd)
    106 	struct ccddevice *ccd;
    107 {
    108 	register struct ccd_softc *cs = &ccd_softc[ccd->ccd_unit];
    109 	register struct ccdcinfo *ci;
    110 	register size_t size;
    111 	register int ix;
    112 	size_t minsize;
    113 	dev_t dev;
    114 
    115 #ifdef DEBUG
    116 	if (ccddebug & (CDB_FOLLOW|CDB_INIT))
    117 		printf("ccdinit: unit %d\n", ccd->ccd_unit);
    118 #endif
    119 	cs->sc_dk = ccd->ccd_dk;
    120 	cs->sc_size = 0;
    121 	cs->sc_ileave = ccd->ccd_interleave;
    122 	cs->sc_nccdisks = 0;
    123 	/*
    124 	 * Verify that each component piece exists and record
    125 	 * relevant information about it.
    126 	 */
    127 	minsize = 0;
    128 	for (ix = 0; ix < NCCDISKS; ix++) {
    129 		if ((dev = ccd->ccd_dev[ix]) == NODEV)
    130 			break;
    131 		ci = &cs->sc_cinfo[ix];
    132 		ci->ci_dev = dev;
    133 		/*
    134 		 * Calculate size (truncated to interleave boundary
    135 		 * if necessary.
    136 		 */
    137 		if (bdevsw[major(dev)].d_psize) {
    138 			size = (size_t) (*bdevsw[major(dev)].d_psize)(dev);
    139 			if ((int)size < 0)
    140 				size = 0;
    141 		} else
    142 			size = 0;
    143 		if (cs->sc_ileave > 1)
    144 			size -= size % cs->sc_ileave;
    145 		if (size == 0) {
    146 			printf("ccd%d: not configured (component %s missing)\n",
    147 			       ccd->ccd_unit, ccddevtostr(ci->ci_dev));
    148 			return(0);
    149 		}
    150 		if (minsize == 0 || size < minsize)
    151 			minsize = size;
    152 		ci->ci_size = size;
    153 		cs->sc_size += size;
    154 		cs->sc_nccdisks++;
    155 	}
    156 	/*
    157 	 * If uniform interleave is desired set all sizes to that of
    158 	 * the smallest component.
    159 	 */
    160 	if (ccd->ccd_flags & CDF_UNIFORM) {
    161 		for (ci = cs->sc_cinfo;
    162 		     ci < &cs->sc_cinfo[cs->sc_nccdisks]; ci++)
    163 			ci->ci_size = minsize;
    164 		cs->sc_size = cs->sc_nccdisks * minsize;
    165 	}
    166 	/*
    167 	 * Construct the interleave table
    168 	 */
    169 	if (!ccdinterleave(cs))
    170 		return(0);
    171 	if (ccd->ccd_dk >= 0)
    172 		dk_wpms[ccd->ccd_dk] = 32 * (60 * DEV_BSIZE / 2);	/* XXX */
    173 	printf("ccd%d: %d components ", ccd->ccd_unit, cs->sc_nccdisks);
    174 	for (ix = 0; ix < cs->sc_nccdisks; ix++)
    175 		printf("%c%s%c",
    176 		       ix == 0 ? '(' : ' ',
    177 		       ccddevtostr(cs->sc_cinfo[ix].ci_dev),
    178 		       ix == cs->sc_nccdisks - 1 ? ')' : ',');
    179 	printf(", %d blocks ", cs->sc_size);
    180 	if (cs->sc_ileave)
    181 		printf("interleaved at %d blocks\n", cs->sc_ileave);
    182 	else
    183 		printf("concatenated\n");
    184 	cs->sc_flags = CDF_ALIVE | CDF_INITED;
    185 	return(1);
    186 }
    187 
    188 /*
    189  * XXX not really ccd specific.
    190  */
    191 char *
    192 ccddevtostr(dev)
    193 	dev_t dev;
    194 {
    195 	static char dbuf[5];
    196 
    197 	dbuf[1] = 'd';
    198 	switch (major(dev)) {
    199 	case 2:
    200 		dbuf[0] = 'r';
    201 		break;
    202 	case 4:
    203 		dbuf[0] = 's';
    204 		break;
    205 	case 5:
    206 		dbuf[0] = 'c';
    207 		break;
    208 	default:
    209 		dbuf[0] = dbuf[1] = '?';
    210 		break;
    211 	}
    212 	dbuf[2] = (minor(dev) >> 3) + '0';
    213 	dbuf[3] = (minor(dev) & 7) + 'a';
    214 	dbuf[4] = '\0';
    215 	return (dbuf);
    216 }
    217 
    218 ccdinterleave(cs)
    219 	register struct ccd_softc *cs;
    220 {
    221 	register struct ccdcinfo *ci, *smallci;
    222 	register struct ccdiinfo *ii;
    223 	register daddr_t bn, lbn;
    224 	register int ix;
    225 	u_long size;
    226 
    227 #ifdef DEBUG
    228 	if (ccddebug & CDB_INIT)
    229 		printf("ccdinterleave(%x): ileave %d\n", cs, cs->sc_ileave);
    230 #endif
    231 	/*
    232 	 * Allocate an interleave table.
    233 	 * Chances are this is too big, but we don't care.
    234 	 */
    235 	size = (cs->sc_nccdisks + 1) * sizeof(struct ccdiinfo);
    236 	cs->sc_itable = (struct ccdiinfo *)malloc(size, M_DEVBUF, M_WAITOK);
    237 	bzero((caddr_t)cs->sc_itable, size);
    238 	/*
    239 	 * Trivial case: no interleave (actually interleave of disk size).
    240 	 * Each table entry represent a single component in its entirety.
    241 	 */
    242 	if (cs->sc_ileave == 0) {
    243 		bn = 0;
    244 		ii = cs->sc_itable;
    245 		for (ix = 0; ix < cs->sc_nccdisks; ix++) {
    246 			ii->ii_ndisk = 1;
    247 			ii->ii_startblk = bn;
    248 			ii->ii_startoff = 0;
    249 			ii->ii_index[0] = ix;
    250 			bn += cs->sc_cinfo[ix].ci_size;
    251 			ii++;
    252 		}
    253 		ii->ii_ndisk = 0;
    254 #ifdef DEBUG
    255 		if (ccddebug & CDB_INIT)
    256 			printiinfo(cs->sc_itable);
    257 #endif
    258 		return(1);
    259 	}
    260 	/*
    261 	 * The following isn't fast or pretty; it doesn't have to be.
    262 	 */
    263 	size = 0;
    264 	bn = lbn = 0;
    265 	for (ii = cs->sc_itable; ; ii++) {
    266 		/*
    267 		 * Locate the smallest of the remaining components
    268 		 */
    269 		smallci = NULL;
    270 		for (ci = cs->sc_cinfo;
    271 		     ci < &cs->sc_cinfo[cs->sc_nccdisks]; ci++)
    272 			if (ci->ci_size > size &&
    273 			    (smallci == NULL ||
    274 			     ci->ci_size < smallci->ci_size))
    275 				smallci = ci;
    276 		/*
    277 		 * Nobody left, all done
    278 		 */
    279 		if (smallci == NULL) {
    280 			ii->ii_ndisk = 0;
    281 			break;
    282 		}
    283 		/*
    284 		 * Record starting logical block and component offset
    285 		 */
    286 		ii->ii_startblk = bn / cs->sc_ileave;
    287 		ii->ii_startoff = lbn;
    288 		/*
    289 		 * Determine how many disks take part in this interleave
    290 		 * and record their indices.
    291 		 */
    292 		ix = 0;
    293 		for (ci = cs->sc_cinfo;
    294 		     ci < &cs->sc_cinfo[cs->sc_nccdisks]; ci++)
    295 			if (ci->ci_size >= smallci->ci_size)
    296 				ii->ii_index[ix++] = ci - cs->sc_cinfo;
    297 		ii->ii_ndisk = ix;
    298 		bn += ix * (smallci->ci_size - size);
    299 		lbn = smallci->ci_size / cs->sc_ileave;
    300 		size = smallci->ci_size;
    301 	}
    302 #ifdef DEBUG
    303 	if (ccddebug & CDB_INIT)
    304 		printiinfo(cs->sc_itable);
    305 #endif
    306 	return(1);
    307 }
    308 
    309 #ifdef DEBUG
    310 printiinfo(ii)
    311 	struct ccdiinfo *ii;
    312 {
    313 	register int ix, i;
    314 
    315 	for (ix = 0; ii->ii_ndisk; ix++, ii++) {
    316 		printf(" itab[%d]: #dk %d sblk %d soff %d",
    317 		       ix, ii->ii_ndisk, ii->ii_startblk, ii->ii_startoff);
    318 		for (i = 0; i < ii->ii_ndisk; i++)
    319 			printf(" %d", ii->ii_index[i]);
    320 		printf("\n");
    321 	}
    322 }
    323 #endif
    324 
    325 ccdopen(dev, flags)
    326 	dev_t dev;
    327 {
    328 	int unit = ccdunit(dev);
    329 	register struct ccd_softc *cs = &ccd_softc[unit];
    330 
    331 #ifdef DEBUG
    332 	if (ccddebug & CDB_FOLLOW)
    333 		printf("ccdopen(%x, %x)\n", dev, flags);
    334 #endif
    335 	if (unit >= NCCD || (cs->sc_flags & CDF_ALIVE) == 0)
    336 		return(ENXIO);
    337 	return(0);
    338 }
    339 
    340 void
    341 ccdstrategy(bp)
    342 	register struct buf *bp;
    343 {
    344 	register int unit = ccdunit(bp->b_dev);
    345 	register struct ccd_softc *cs = &ccd_softc[unit];
    346 	register daddr_t bn;
    347 	register int sz, s;
    348 
    349 #ifdef DEBUG
    350 	if (ccddebug & CDB_FOLLOW)
    351 		printf("ccdstrategy(%x): unit %d\n", bp, unit);
    352 #endif
    353 	if ((cs->sc_flags & CDF_INITED) == 0) {
    354 		bp->b_error = ENXIO;
    355 		bp->b_flags |= B_ERROR;
    356 		goto done;
    357 	}
    358 	bn = bp->b_blkno;
    359 	sz = howmany(bp->b_bcount, DEV_BSIZE);
    360 	if (bn < 0 || bn + sz > cs->sc_size) {
    361 		sz = cs->sc_size - bn;
    362 		if (sz == 0) {
    363 			bp->b_resid = bp->b_bcount;
    364 			goto done;
    365 		}
    366 		if (sz < 0) {
    367 			bp->b_error = EINVAL;
    368 			bp->b_flags |= B_ERROR;
    369 			goto done;
    370 		}
    371 		bp->b_bcount = dbtob(sz);
    372 	}
    373 	bp->b_resid = bp->b_bcount;
    374 	/*
    375 	 * "Start" the unit.
    376 	 * XXX: the use of sc_bp is just to retain the "traditional"
    377 	 * interface to the start routine.
    378 	 */
    379 	s = splbio();
    380 	cs->sc_bp = bp;
    381 	ccdstart(unit);
    382 	splx(s);
    383 	return;
    384 done:
    385 	biodone(bp);
    386 }
    387 
    388 ccdstart(unit)
    389 	int unit;
    390 {
    391 	register struct ccd_softc *cs = &ccd_softc[unit];
    392 	register struct buf *bp = cs->sc_bp;
    393 	register long bcount, rcount;
    394 	struct buf *cbp;
    395 	caddr_t addr;
    396 	daddr_t bn;
    397 
    398 #ifdef DEBUG
    399 	if (ccddebug & CDB_FOLLOW)
    400 		printf("ccdstart(%d)\n", unit);
    401 #endif
    402 	/*
    403 	 * Instumentation (not real meaningful)
    404 	 */
    405 	cs->sc_usecnt++;
    406 	if (cs->sc_dk >= 0) {
    407 		dk_busy |= 1 << cs->sc_dk;
    408 		dk_xfer[cs->sc_dk]++;
    409 		dk_wds[cs->sc_dk] += bp->b_bcount >> 6;
    410 	}
    411 	/*
    412 	 * Allocate component buffers and fire off the requests
    413 	 */
    414 	bn = bp->b_blkno;
    415 	addr = bp->b_un.b_addr;
    416 	for (bcount = bp->b_bcount; bcount > 0; bcount -= rcount) {
    417 		cbp = ccdbuffer(cs, bp, bn, addr, bcount);
    418 		rcount = cbp->b_bcount;
    419 		(*bdevsw[major(cbp->b_dev)].d_strategy)(cbp);
    420 		bn += btodb(rcount);
    421 		addr += rcount;
    422 	}
    423 }
    424 
    425 /*
    426  * Build a component buffer header.
    427  */
    428 struct buf *
    429 ccdbuffer(cs, bp, bn, addr, bcount)
    430 	register struct ccd_softc *cs;
    431 	struct buf *bp;
    432 	daddr_t bn;
    433 	caddr_t addr;
    434 	long bcount;
    435 {
    436 	register struct ccdcinfo *ci;
    437 	register struct buf *cbp;
    438 	register daddr_t cbn, cboff;
    439 
    440 #ifdef DEBUG
    441 	if (ccddebug & CDB_IO)
    442 		printf("ccdbuffer(%x, %x, %d, %x, %d)\n",
    443 		       cs, bp, bn, addr, bcount);
    444 #endif
    445 	/*
    446 	 * Determine which component bn falls in.
    447 	 */
    448 	cbn = bn;
    449 	cboff = 0;
    450 	/*
    451 	 * Serially concatenated
    452 	 */
    453 	if (cs->sc_ileave == 0) {
    454 		register daddr_t sblk;
    455 
    456 		sblk = 0;
    457 		for (ci = cs->sc_cinfo; cbn >= sblk + ci->ci_size; ci++)
    458 			sblk += ci->ci_size;
    459 		cbn -= sblk;
    460 	}
    461 	/*
    462 	 * Interleaved
    463 	 */
    464 	else {
    465 		register struct ccdiinfo *ii;
    466 		int ccdisk, off;
    467 
    468 		cboff = cbn % cs->sc_ileave;
    469 		cbn /= cs->sc_ileave;
    470 		for (ii = cs->sc_itable; ii->ii_ndisk; ii++)
    471 			if (ii->ii_startblk > cbn)
    472 				break;
    473 		ii--;
    474 		off = cbn - ii->ii_startblk;
    475 		if (ii->ii_ndisk == 1) {
    476 			ccdisk = ii->ii_index[0];
    477 			cbn = ii->ii_startoff + off;
    478 		} else {
    479 			ccdisk = ii->ii_index[off % ii->ii_ndisk];
    480 			cbn = ii->ii_startoff + off / ii->ii_ndisk;
    481 		}
    482 		cbn *= cs->sc_ileave;
    483 		ci = &cs->sc_cinfo[ccdisk];
    484 	}
    485 	/*
    486 	 * Fill in the component buf structure.
    487 	 */
    488 	cbp = getcbuf();
    489 	cbp->b_flags = bp->b_flags | B_CALL;
    490 	cbp->b_iodone = ccdiodone;
    491 	cbp->b_proc = bp->b_proc;
    492 	cbp->b_dev = ci->ci_dev;
    493 	cbp->b_blkno = cbn + cboff;
    494 	cbp->b_un.b_addr = addr;
    495 	cbp->b_vp = 0;
    496 	if (cs->sc_ileave == 0)
    497 		cbp->b_bcount = dbtob(ci->ci_size - cbn);
    498 	else
    499 		cbp->b_bcount = dbtob(cs->sc_ileave - cboff);
    500 	if (cbp->b_bcount > bcount)
    501 		cbp->b_bcount = bcount;
    502 	/*
    503 	 * XXX: context for ccdiodone
    504 	 */
    505 	cbp->b_saveaddr = (caddr_t)bp;
    506 	cbp->b_pfcent = ((cs - ccd_softc) << 16) | (ci - cs->sc_cinfo);
    507 #ifdef DEBUG
    508 	if (ccddebug & CDB_IO)
    509 		printf(" dev %x(u%d): cbp %x bn %d addr %x bcnt %d\n",
    510 		       ci->ci_dev, ci-cs->sc_cinfo, cbp, cbp->b_blkno,
    511 		       cbp->b_un.b_addr, cbp->b_bcount);
    512 #endif
    513 	return(cbp);
    514 }
    515 
    516 ccdintr(unit)
    517 	int unit;
    518 {
    519 	register struct ccd_softc *cs = &ccd_softc[unit];
    520 	register struct buf *bp = cs->sc_bp;
    521 
    522 #ifdef DEBUG
    523 	if (ccddebug & CDB_FOLLOW)
    524 		printf("ccdintr(%d): bp %x\n", unit, bp);
    525 #endif
    526 	/*
    527 	 * Request is done for better or worse, wakeup the top half.
    528 	 */
    529 	if (--cs->sc_usecnt == 0 && cs->sc_dk >= 0)
    530 		dk_busy &= ~(1 << cs->sc_dk);
    531 	if (bp->b_flags & B_ERROR)
    532 		bp->b_resid = bp->b_bcount;
    533 	biodone(bp);
    534 }
    535 
    536 /*
    537  * Called by biodone at interrupt time.
    538  * Mark the component as done and if all components are done,
    539  * take a ccd interrupt.
    540  */
    541 ccdiodone(cbp)
    542 	register struct buf *cbp;
    543 {
    544 	register struct buf *bp = (struct buf *)cbp->b_saveaddr;/* XXX */
    545 	register int unit = (cbp->b_pfcent >> 16) & 0xFFFF;	/* XXX */
    546 	int count, s;
    547 
    548 	s = splbio();
    549 #ifdef DEBUG
    550 	if (ccddebug & CDB_FOLLOW)
    551 		printf("ccdiodone(%x)\n", cbp);
    552 	if (ccddebug & CDB_IO) {
    553 		printf("ccdiodone: bp %x bcount %d resid %d\n",
    554 		       bp, bp->b_bcount, bp->b_resid);
    555 		printf(" dev %x(u%d), cbp %x bn %d addr %x bcnt %d\n",
    556 		       cbp->b_dev, cbp->b_pfcent & 0xFFFF, cbp,
    557 		       cbp->b_blkno, cbp->b_un.b_addr, cbp->b_bcount);
    558 	}
    559 #endif
    560 
    561 	if (cbp->b_flags & B_ERROR) {
    562 		bp->b_flags |= B_ERROR;
    563 		bp->b_error = biowait(cbp);
    564 #ifdef DEBUG
    565 		printf("ccd%d: error %d on component %d\n",
    566 		       unit, bp->b_error, cbp->b_pfcent & 0xFFFF);
    567 #endif
    568 	}
    569 	count = cbp->b_bcount;
    570 	putcbuf(cbp);
    571 
    572 	/*
    573 	 * If all done, "interrupt".
    574 	 * Again, sc_bp is only used to preserve the traditional interface.
    575 	 */
    576 	bp->b_resid -= count;
    577 	if (bp->b_resid < 0)
    578 		panic("ccdiodone: count");
    579 	if (bp->b_resid == 0) {
    580 		ccd_softc[unit].sc_bp = bp;
    581 		ccdintr(unit);
    582 	}
    583 	splx(s);
    584 }
    585 
    586 ccdioctl(dev, cmd, data, flag, p)
    587 	dev_t dev;
    588 	int cmd;
    589 	caddr_t data;
    590 	int flag;
    591 	struct proc *p;
    592 {
    593 	return(EINVAL);
    594 }
    595 
    596 ccdsize(dev)
    597 	dev_t dev;
    598 {
    599 	int unit = ccdunit(dev);
    600 	register struct ccd_softc *cs = &ccd_softc[unit];
    601 
    602 	if (unit >= NCCD || (cs->sc_flags & CDF_INITED) == 0)
    603 		return(-1);
    604 	return(cs->sc_size);
    605 }
    606 
    607 ccddump(dev)
    608 {
    609 	return(ENXIO);
    610 }
    611 #endif
    612