Ausam/sys/dmr/rh.c
#define AGSM
/*
* Optimised RHP04, RJP04, RM02 and RM03 driver
*
* When instructed correctly (via the ifdefs) this driver will attempt
* to queue io requests by drive, cylinder and sector, and endeavour
* to overlap seeks with data transfers for more than one drive.
* Method is very similar to Childrens Museum RK driver.
*
* Some error correction and recovery is attempted.
* You have been WARNED.
*
* Based on the novel by Peter Ivanov UNSWDCS
*
* Debugged and recoded by Ian Johnstone AGSM
* and
* Chris Maltby DCS.
*
* RM02 features added by Craig McGregor Architecture
*/
#include "../defines.h"
#include "../param.h"
#include "../buf.h"
#include "../conf.h"
#include "../user.h"
#include "../seg.h"
/* (no PRESEEK)
#define PRESEEK /* enable overlapped seeks on multiple drives */
#define SWEEP /* enable checking to force max head travel */
/* (no RJP04)
#define RJP04 /* define for RP04's on the unibus,
ONLY will work for non 11/70 because of rhstart.
undefine for RP04's on RH70 */
/* (RHP04)
#define RM02 /* define if RM02 or RM03 drive */
/* if RM03 - define RJP04 */
/* (no ECC)
#define ECC /* enable data check correction */
/*
#define RHSTATS /* define to enable stats collection */
/* (no NO_OFFLINEQ)
#define NO_OFFLINEQ /* stop qing of requests for offlined drives */
/* (no DROP_OFFLINEQ)
#define DROP_OFFLINEQ /* delete drive request q when offlined */
struct
{
int rhcs1; /* Control and Status register 1 */
int rhwc; /* Word Count register */
int rhba; /* Bus address register */
int rhda; /* Desired disc address - sector/track */
int rhcs2; /* Control and Status register 2 */
int rhds; /* Drive status register */
int rher1; /* Error register 1 */
int rhas; /* Attention summary register */
int rhla; /* Disc position look-ahead register */
int rhdb; /* Data buffer */
int rhmr; /* Maintenance register */
int rhdt; /* Drive type encoding */
int rhsn; /* Drive serial number */
int rhof; /* Offset register - contains fmt16 bit */
int rhdc; /* Desired cylinder address register */
int rhcc; /* Current cylinder address register */
int rher2; /* Error register 2 */
int rher3; /* Error register 3 */
int rhpos; /* Burst error bit position */
int rhpat; /* Burst error bit pattern */
#ifndef RJP04
int rhbae; /* 11/70 bus extension register */
int rhcs3; /* Control and Status register 3 */
#endif RJP04
};
#ifndef RM02
/* rp04 characteristics */
#define NCYLS 411 /* cylinders per pack */
#define NTRKS 19 /* tracks per cylinder */
#define NSECS 22 /* sectors per track */
/* rp04 names */
#define RHOPEN hpopen
#define RHSTRATEGY hpstrategy
#define RHSTART hpstart
#define RHINTR hpintr
#define RHREGS hpregs
#define RHECC hpecc
#define RHREAD hpread
#define RHWRITE hpwrite
#define RHPHYS hpphys
#define RHDINIT hpdinit
#define RHPOWF hppowf
#define RH_SIZES hp_sizes
#define RHQ hpq
#define RHINTLV hpintlv
#define RHINTST hpintst
#define RHTAB hptab
#define RHBUF hpbuf
#else RM02
/* rm02 characteristics */
#define NCYLS 823 /* cylinders per pack */
#define NTRKS 5 /* tracks per cylinder */
#define NSECS 32 /* sectors per track */
/* rm02 names */
#define RHOPEN rmopen
#define RHSTRATEGY rmstrategy
#define RHSTART rmstart
#define RHINTR rmintr
#define RHREGS rmregs
#define RHECC rmecc
#define RHREAD rmread
#define RHWRITE rmwrite
#define RHPHYS rmphys
#define RHDINIT rmdinit
#define RHPOWF rmpowf
#define RH_SIZES rm_sizes
#define RHQ rmq
#define RHINTLV rmintlv
#define RHINTST rmintst
#define RHTAB rmtab
#define RHBUF rmbuf
#endif RM02
#define RHADDR 0176700
#define MAXDRV 8 /* the number of drives that can be attached */
#define TVOL (sizeof RH_SIZES/sizeof RH_SIZES[0]) /* total number of volumes */
#define RHAGE 10 /* number of times this block may be pre-empted for io
before io is forced */
#define RHSPL spl5 /* priority of rh disc */
#ifdef AGSM
#define NDRV 2 /* the number of real rp04/rm02 drives on controller */
#define PRESEEK
#endif AGSM
#ifdef ARCH
#define NDRV 1 /* the number of real rp04/rm02 drives on controller */
#endif ARCH
/*
* The following maps logical volumes of various sizes to locations
* on various drives. This must be altered to add extra drives.
* PLEASE GET THEM CORRECT FIRST TIME.
*/
struct
{
unsigned nblocks;
int cyloff;
int drive;
}
RH_SIZES[]
{
#ifdef AGSM
/* 00: 0->155 (156cyls) drive 0 */ 65208, 0, 0,
/* user1: ADMIN + LIBRARY */
/* 01: 156->219 (64cyls) drive 0 */ 26752, 156, 0,
/* spare or 'swap' or '/tmp' */
/* 02: 220->254 (35cyls) drive 0 */ 14630, 220, 0,
/* system disk */
/* 03: 255->410 (156cyls) drive 0 */ 65208, 255, 0,
/* user0: STAFF + STUDENTS */
/* 10: 0->155 (156cyls) drive 1 */ 65208, 0, 1,
/* srce: SYSTEM SOURCE */
/* 11: 156->219 (64cyls) drive 1 */ 26752, 156, 1,
/* /tmp + 'swap' */
/* 12: 220->254 (35cyls) drive 1 */ 14630, 220, 1,
/* user2: non-agsm users */
/* 13: 255->410 (156cyls) drive 1 */ 65208, 255, 1,
/* spare */
#endif AGSM
#ifdef ARCH
/* 00: 0->324 (325cyls) drive 0 */ 52000, 0, 0,
/* user1: spare + small users */
/* 01: 325->426 (102cyls) drive 0 */ 16320, 325, 0,
/* 'swap' or '/tmp' */
/* 02: 427->497 (71cyls) drive 0 */ 11360, 427, 0,
/* system disk */
/* 03: 498->822 (325cyls) drive 0 */ 52000, 498, 0,
/* user0: users */
#endif ARCH
};
/*
* structure of an rh disc queue
*/
struct rhq
{
char rhq_flags; /* flags for qs */
/* assumed zero - RHOFF==0 */
#define RHOFF 00 /* drive offline */
#ifdef PRESEEK
#define RHSEEK 01 /* seeking flag for q header */
#endif PRESEEK
#define RHREADY 02 /* ready for io flag for q header */
#define RHBUSY 03 /* data transfer in progress */
#define RHRECAL 04 /* recalibrate in progress */
#define RHIDLE 05 /* nought doing */
#ifdef SWEEP
char rhq_dirf; /* current direction of head movement */
#define UP 1 /* the disc cyls are moving up */
#define DOWN 0 /* the disc cyls are moving down */
#endif SWEEP
struct buf *rhq_bufp; /* pointer to first buffer in queue for this drive */
#ifdef RHSTATS
char rhq_nreq; /* number of requests in queue - stats */
char rhq_rmax; /* high water mark for q */
unsigned rh_cyls[NCYLS]; /* accesses per cylinder */
#endif RHSTATS
} RHQ[NDRV];
#ifdef RHSTATS
/*
* gather statistics to decide on optimal interleaving factor.
* When starting an io accumulate in RHINTLV the count of
* each segment to go to io transfer start.
*/
long RHINTLV[NSECS*4]; /* divide disk into NSECS*4 segments */
#endif RHSTATS
int intlv 1; /* interleaving factor for queueing io on same cylinder */
int RHINTST 2; /* DEBUG try this much look-ahead */
/*
* We use a few buffer variables for other purposes here, ie:
* b_scratch to hold unit/cylinder address of this request
* b_resid to hold track/sector address of this request
* av_back to hold request age count
*/
#define rh_cyl b_scratch /* at least an int */
#define rh_sector b_resid.lobyte
#define rh_track b_resid.hibyte
#define rh_trksec b_resid
#define rh_age av_back
struct devtab RHTAB;
#ifndef RAW_BUFFER_POOL
struct buf RHBUF;
#endif RAW_BUFFER_POOL
/* various drive commands utilised herein */
#define GO 01
#define PRESET 020 /* read-in-preset */
#define RECAL 06 /* recalibrate */
#define DCLR 010 /* drive clear */
#define SEEK 04 /* seek */
#define SEARCH 030 /* search */
#define UNLOAD 02 /* unload pack */
/* rhds bits */
#define DRY 0200 /* drive ready */
#define VV 0100 /* volume valid */
#define MOL 010000 /* medium on line */
#define ERR 040000 /* the OR of error bits */
#define ATA 0100000 /* attention bit */
/* rhcs1 bits */
#define IE 0100 /* interrupt enable bit */
#define RDY 0200 /* controller ready */
#define TRE 040000 /* the OR of error bits */
/* rher1 bits */
#define ECH 000100 /* hard ecc error */
#define AOE 001000 /* end of drive error */
#define WLE 004000 /* Write protect error */
#define DTE 010000 /* drive timing error */
#define OPI 020000 /* operation incomplete */
#define UNS 040000 /* drive unsafe */
#define DCK 0100000 /* data check error for RHPs and RJPs (DEC) */
/* rhcs2 bits */
#define CLR 040 /* controller clear */
/* rhof bits */
#define FMT16 010000 /* 16bit format (as opposed to 18-bit format) (DEC) */
RHOPEN(dev)
{
register int n;
/*
** If the requested drive is offline -> error
** Initialize it if necessary. Error if this fails.
*/
if ((n = dev.d_minor) >= TVOL)
{
u.u_error = EOPENFAIL;
return;
}
n = RH_SIZES[n].drive;
RHSPL();
if (RHQ[n].rhq_flags == RHOFF)
{
RHDINIT(n);
if (RHQ[n].rhq_flags == RHOFF)
{
u.u_error = EOPENFAIL;
}
}
spl0();
}
RHSTRATEGY(bp)
register struct buf *bp;
{
register unsigned p1, p2;
int dirf;
p1 = bp->b_dev.d_minor;
/*
* Validate the request
*/
p2 = &RH_SIZES[p1];
#ifdef NO_OFFLINEQ | DROP_OFFLINEQ
if (p2->nblocks <= bp->b_blkno || RHQ[p2->drive] == RHOFF)
#else NO_OFFLINEQ | DROP_OFFLINEQ
if (p2->nblocks <= bp->b_blkno)
#endif NO_OFFLINEQ | DROP_OFFLINEQ
{
bp->b_flags =| B_ERROR;
iodone(bp);
return;
}
#ifdef _1170 & RJP04
if (bp->b_flags & B_PHYS)
{
mapalloc(bp);
}
#endif _1170 & RJP04
bp->av_forw = 0;
bp->rh_age = RHAGE;
bp->b_error = 0; /* clear error count */
bp->rh_cyl = p2->cyloff;
p1 = bp->b_blkno;
bp->rh_sector = lrem(p1,NSECS); /* sector */
p1 = ldiv(p1,NSECS); /* trk and cyl */
bp->rh_track = p1 % NTRKS;
bp->rh_cyl =+ p1/NTRKS;
p2 = &RHQ[p2->drive];
/*
* Now "rh_cyl" contains "cylinder"
* and "rh_trksec" contains the "track and sector"
*/
#ifdef RHSTATS
p2->rh_cyls[bp->rh_cyl]++;
p2->rhq_nreq++;
if (p2->rhq_nreq > p2->rhq_rmax) p2->rhq_rmax = p2->rhq_nreq;
#endif RHSTATS
RHSPL();
if ((p1 = p2->rhq_bufp) == NULL)
{
/* this queue is empty */
p2->rhq_bufp = bp;
RHSTART();
}
else
{
/*
* the queue is not empty, so place in queue so as to
* minimise head movement.
*/
#ifdef SWEEP
dirf = p2->rhq_dirf;
#endif SWEEP
p2 = p1->av_forw;
while (p2)
{
/* skip any overtaken blocks */
if (!(p2->rh_age))
p1 = p2;
p2 = p2->av_forw;
}
for (; p2 = p1->av_forw; p1 = p2)
{
if (p1->rh_cyl<=bp->rh_cyl && bp->rh_cyl<=p2->rh_cyl
|| p1->rh_cyl>=bp->rh_cyl && bp->rh_cyl>=p2->rh_cyl)
{
while (bp->rh_cyl == p2->rh_cyl)
{
/*
** for a cylinder match, do the
** rotational optimisation.
** intlv is presumably the optimal value.
** SEE RHSTATS as to how this could be done
*/
if (p2->rh_sector > p1->rh_sector)
{
if (bp->rh_sector > p1->rh_sector + intlv
&& bp->rh_sector < p2->rh_sector - intlv)
break;
}
else
{
if (bp->rh_sector > p1->rh_sector + intlv
|| bp->rh_sector < p2->rh_sector - intlv)
break;
}
p1 = p2;
if (!(p2 = p1->av_forw)) break;
}
break;
}
#ifdef SWEEP
else
{
if (dirf == UP)
{
if (p2->rh_cyl < p1->rh_cyl)
if (bp->rh_cyl > p1->rh_cyl)
break;
else
dirf = DOWN;
}
else
{
if (p2->rh_cyl > p1->rh_cyl)
if (bp->rh_cyl < p1->rh_cyl)
break;
else
dirf = UP;
}
}
#endif SWEEP
}
bp->av_forw = p2;
p1->av_forw = bp;
while (p2)
{
/* count down overtaken blocks */
p2->rh_age--;
p2 = p2->av_forw;
}
}
spl0();
}
/*
* start seeks as required and possibly one data transfer.
*/
RHSTART()
{
/* called at RHSPL or greater */
register struct buf *bp;
register struct rhq *qp;
register int n;
static int drv, ioage; /* we assume that these are zero initially */
#ifdef PRESEEK
for (n = NDRV; --n >= 0;)
{
qp = &RHQ[n];
if ((bp = qp->rhq_bufp) && (qp->rhq_flags == RHIDLE))
{
/*
** for all available drives start seeking
*/
RHADDR->rhcs2 = n;
if (RHADDR->rhcc == bp->rh_cyl)
{
qp->rhq_flags = RHREADY;
}
else
{
int xx;
#ifdef SWEEP
if (bp->rh_cyl > RHADDR->rhcc)
qp->rhq_dirf = UP;
else if (bp->rh_cyl < RHADDR->rhcc)
qp->rhq_dirf = DOWN;
#endif SWEEP
xx = bp->rh_sector - RHINTST;
if (xx < 0) xx =+ 22;
xx.hibyte = bp->rh_track;
RHADDR->rhda = xx;
RHADDR->rhdc = bp->rh_cyl;
qp->rhq_flags = RHSEEK;
RHADDR->rhcs1.lobyte = IE | SEARCH | GO;
}
}
}
#endif PRESEEK
/*
* check if possible to start an io
*/
for (n = NDRV; --n >= 0;)
if (RHQ[n].rhq_flags == RHBUSY) return;
if (RHADDR->rhcs1 & RDY) /* ensure controller available */
{
/*
** try to start an IO
*/
n = NDRV;
do
{
qp = &RHQ[drv];
if ((bp = qp->rhq_bufp) && qp->rhq_flags == RHREADY)
{
#ifdef RHSTATS
int a;
#endif RHSTATS
qp->rhq_flags = RHBUSY;
RHADDR->rhcs2 = drv;
RHADDR->rhdc = bp->rh_cyl;
#ifdef RHSTATS
a = bp->rh_sector * 4;
a =- RHADDR->rhla >> 4;
if (a <= 3) a =+ NSECS*4;
RHINTLV[a-4]++;
#endif RHSTATS
#ifdef RJP04
rhstart(bp, &RHADDR->rhda, bp->rh_trksec);
#else RJP04
rhstart(bp, &RHADDR->rhda, bp->rh_trksec, &RHADDR->rhbae);
#endif RJP04
if (--ioage <= 0)
{
ioage = RHAGE;
if (++drv >= NDRV)
drv = 0;
}
return;
}
if (++drv >= NDRV) drv=0;
ioage = RHAGE;
} while (--n > 0);
}
if (!(RHADDR->rhcs1 & IE))
for (n=0; n<NDRV; n++)
{
/*
* if the IE bit has not been set,
* then nothing is happening and nothing
* was started, so find a drive which will accept
* a command and set the IE bit with a nop
*/
RHADDR->rhcs2 = n;
if (RHADDR->rhds & DRY)
{
RHADDR->rhcs1 = IE;
return;
}
}
}
RHINTR()
{
/* called at RHSPL or greater */
register int n;
register struct rhq *qp;
register struct buf *bp;
/*
** An error has occured and/or a data transfer has completed.
*/
for (n=0; n<NDRV; n++)
{
RHADDR->rhcs2 = n; /* select drive */
qp = &RHQ[n];
if ((RHADDR->rhds & ATA) || qp->rhq_flags == RHBUSY)
{
bp = qp->rhq_bufp;
if ((RHADDR->rhds & MOL) && !(RHADDR->rhds & VV))
{
/* drive has come online */
#ifndef RM02
printf("HP drive %d turned on\n", n);
#else RM02
printf("RM drive %d turned on\n", n);
#endif RM02
RHDINIT(n);
}
else if (!(RHADDR->rhds & MOL))
{
/* drive down - disable and flag */
#ifndef RM02
printf("HP drive %d turned off\n", n);
#else RM02
printf("RM drive %d turned off\n", n);
#endif RM02
qp->rhq_flags = RHOFF;
#ifdef DROP_OFFLINEQ
dropq:
while (bp)
{
bp->b_flags =| B_ERROR;
iodone(bp);
bp = bp->av_forw;
}
qp->rhq_bufp = 0;
#endif DROP_OFFLINEQ
}
else if (RHADDR->rhds & ERR)
{
/* a drive error */
int er1; /* saves the error register */
RHREGS(qp);
er1 = RHADDR->rher1;
RHADDR->rhcs1.lobyte = IE | DCLR | GO;
while (!(RHADDR->rhds & DRY));
if (er1 & (UNS | DTE | OPI))
{
if (RHADDR->rher1 & UNS)
{
/* Still unsafe, unload for safety */
qp->rhq_flags = RHOFF;
RHADDR->rhcs1.lobyte = IE | UNLOAD | GO;
#ifndef RM02
printf("HP drive %d UNSAFE\n", n);
#else RM02
printf("RM drive %d UNSAFE\n", n);
#endif RM02
printf("\nSTOP and re-START drive to recover\n\n");
#ifdef DROP_OFFLINEQ
goto dropq;
#endif DROP_OFFLINEQ
}
else
{
/* Okay now, recal and go */
qp->rhq_flags = RHRECAL;
RHADDR->rhcs1.lobyte = IE | RECAL | GO;
}
}
#ifdef ECC
else if ((er1 & DCK) && !(er1 & ECH))
{
if (RHECC())
continue; /* IO resumed */
else
RHADDR->rhcs1 = TRE | IE;
goto trcmplt; /* IO done */
}
#endif ECC
switch(qp->rhq_flags)
{
case RHOFF:
case RHREADY:
case RHIDLE:
break;
case RHBUSY:
RHADDR->rhcs1 = TRE | IE;
if (er1 & AOE)
{
/* This might occur if RH_SIZES wrong */
RHADDR->rhcs1 = TRE | IE;
goto trcmplt;
}
else if (er1 & WLE)
{
/* Drive is wrt protected - give up */
bp->b_flags =| B_ERROR;
goto unlink;
}
#ifdef PRESEEK
case RHSEEK:
qp->rhq_flags = RHIDLE;
#else PRESEEK
qp->rhq_flags = RHREADY;
#endif PRESEEK
goto errec;
case RHRECAL:
if (qp->rhq_bufp)
goto errec;
}
}
else
{
/*
* Operation complete. Transfer or seek/recalibrate
*/
if (qp->rhq_flags == RHBUSY)
{
if (RHADDR->rhcs1 & RDY)
{
if (RHADDR->rhcs1 & TRE)
{
RHREGS(qp);
RHADDR->rhcs1 = TRE | IE;
errec:
if (++bp->b_error >= 10)
{
bp->b_flags =| B_ERROR;
goto unlink;
}
}
else
{
/* Transfer complete SUCCESS! */
trcmplt:
bp->b_resid = RHADDR->rhwc;
unlink:
qp->rhq_bufp = bp->av_forw;
iodone(bp);
}
}
else
{
continue;
}
}
#ifdef PRESEEK
if (qp->rhq_flags == RHSEEK)
qp->rhq_flags = RHREADY;
else if (qp->rhq_flags != RHRECAL)
qp->rhq_flags = RHIDLE;
#else PRESEEK
if (qp->rhq_flags != RHRECAL)
qp->rhq_flags = RHREADY;
#endif PRESEEK
}
clear:
RHADDR->rhas = 1 << n;
}
}
RHSTART();
}
RHREGS(qp)
struct rhq *qp;
{
static struct rherrmsgs
{
char *str; /* identify which register */
int *reg; /* address of device register */
}
rherrmsgs[]
{
"ER1", &RHADDR->rher1,
"CS1", &RHADDR->rhcs1,
"DS", &RHADDR->rhds,
"CS2", &RHADDR->rhcs2,
"WC", &RHADDR->rhwc,
"BA", &RHADDR->rhba,
"DC", &RHADDR->rhdc,
"DA", &RHADDR->rhda,
"AS", &RHADDR->rhas,
0
};
register struct rherrmsgs *p = rherrmsgs;
#ifndef RM02
printf("HPERR\t");
#else RM02
printf("RMERR\t");
#endif RM02
do
{
printf("%s=%o, ", p->str, *(p->reg));
p++;
} while (p->str);
printf("FLAGS=%o\n",qp->rhq_flags);
}
/*
* Physical IO:
* truncate transfers at the ends of logical volumes
*/
RHREAD(dev)
int dev;
{
RHPHYS(dev, B_READ);
}
RHWRITE(dev)
int dev;
{
RHPHYS(dev, B_WRITE);
}
RHPHYS(dev, flag)
int dev;
int flag; /* B_READ or B_WRITE */
{
register unsigned a, b, c;
a = RH_SIZES[dev.d_minor & 07].nblocks;
b = u.u_offset >> 9;
if (b >= a)
{
u.u_error = ENXIO;
return;
}
c = u.u_count;
if (ldiv(c+511, 512) + b > a)
c = (a - b) << 9;
a = u.u_count - c;
u.u_count = c;
#ifdef RAW_BUFFER_POOL
physio(&RHSTRATEGY, 0, dev, flag);
#else RAW_BUFFER_POOL
physio(&RHSTRATEGY, &RHBUF, dev, flag);
#endif RAW_BUFFER_POOL
u.u_count =+ a;
}
RHDINIT(n)
register n;
{
/* called at RHSPL or greater */
RHADDR->rhcs2 = n;
if (RHADDR->rhds & MOL) /* this may generate a NED error */
/* RHINTR() will handle it */
{
RHADDR->rhcs1.lobyte = IE | DCLR | GO;
/*
** possibly some risk in these while loops here and
** in previous code in that if the drive is maybe
** MOL & !(DRY) ever....... not very likely and you
** would be buggered if it was.....
*/
while (!(RHADDR->rhds & DRY));
RHADDR->rhcs1.lobyte = IE | PRESET | GO;
while (!(RHADDR->rhds & DRY));
RHADDR->rhof = FMT16;
#ifdef PRESEEK
RHQ[n].rhq_flags = RHIDLE;
#else PRESEEK
RHQ[n].rhq_flags = RHREADY;
#endif PRESEEK
}
}
#ifdef POWER_FAIL
RHPOWF(rdev)
{
/*
* rh disc power fail recovery routine
*
* It is ASSUMED that
*
* . a power fail has occured and the cpu did a reset
* . mem management registers have been restored
* . as have unibus map regs as have
* . the other vital ones.
*/
register struct buf *bp;
register struct rhq *qp;
register d;
long n;
RHADDR->rhcs2=CLR;
for (d=0; d< NDRV; d++)
RHQ[n].rhq_flags = RHOFF;
if (rdev)
{
for (d=0; d<NDRV; d++)
{
/*
* this drive was on when it died
*/
RHADDR->rhcs2 = d;
for (;;)
{
if ((RHADDR->rhcs1 & RDY) && (RHADDR->rhds & MOL) && (RHADDR->rhds & DRY))
{
RHDINIT(d);
break;
}
for (n=2000000; n>0; n--);
#ifndef RM02
printf("HPERR PWR FAIL RECVRY WAITING DRIVE %o\n", d);
#else RM02
printf("RMERR PWR FAIL RECVRY WAITING DRIVE %o\n", d);
#endif RM02
}
}
}
RHSTART();
}
#endif POWER_FAIL
#ifdef ARCH
help();
#endif ARCH