-- DiskHeadDorado.mesa -- Last Edited by: Taft, May 22, 1983 1:55 pm DIRECTORY DeviceCleanup USING [Await, Item, Reason], DoradoInputOutput USING [IOAddress, DMuxAddr, Output, ResetDisk, RWMufMan], Environment USING [Base], HeadStartChain USING [Start], Inline USING [BITAND, BITOR, LowHalf], PilotDisk USING [Handle, Label], SA4000Face, SA4000FaceExtras USING [www]; DiskHeadDorado: PROGRAM IMPORTS DeviceCleanup, DoradoInputOutput, RemainingHeads: HeadStartChain, Inline EXPORTS HeadStartChain, SA4000Face SHARES SA4000Face = BEGIN OPEN SA4000Face; -- Data structures shared with disk microcode CSBPtr: TYPE = LONG POINTER TO CSB; CSB: TYPE = MACHINE DEPENDENT RECORD [ head (0): IOCBQueueHead, interruptMask (1): WORD, drive (2): Drive, -- drive currently selected cylinder (3): CARDINAL, -- negative => need to restore disk -- Remainder of CSB is not used by the microcode tail (4): IOCBLongPtr]; IOCBQueueHead: TYPE = MACHINE DEPENDENT RECORD [ SELECT OVERLAID * FROM untagged => [iocb: IOCBShortPtr], tagged => [highBits: [0..77777B], tag: {idle, active}], ENDCASE]; nilIOCBQueueHead: IOCBQueueHead = [untagged[iocb: nilIOCBShortPtr]]; -- IOCB must be odd word aligned IOCBShortPtr: TYPE = Environment.Base RELATIVE POINTER TO IOCB; IOCBLongPtr: TYPE = LONG POINTER TO IOCB; IOCB: TYPE = MACHINE DEPENDENT RECORD [ next (0B): IOCBShortPtr, seal (1B): CARDINAL, drive (2B): Drive, pageCount (3B): CARDINAL, command (4B): DiskCommand, diskAddress (5B): DiskAddress, diskHeader (7B): DiskAddress, headerPtr (11B): LONG POINTER TO DiskAddress, headerECC (13B): LONG CARDINAL, headerStatus (15B): DiskStatus, labelPtr (16B): LONG POINTER TO PilotDisk.Label, labelECC (20B): LONG CARDINAL, labelStatus (22B): DiskStatus, dataPtr (23B): LONG POINTER, dataECC (25B): LONG CARDINAL, dataStatus (27B): DiskStatus, diskLabel (30B): PilotDisk.Label]; nilIOCBShortPtr: IOCBShortPtr = LOOPHOLE[0]; DoradoOperationPtr: TYPE = LONG POINTER TO DoradoOperation; DoradoOperation: TYPE = MACHINE DEPENDENT RECORD [ operation (0): Operation, -- Guarantee required odd word alignment, since Operation is 16-word aligned iocb (15B): IOCB]; DiskStatus: TYPE = MACHINE DEPENDENT RECORD [ seekInc (0: 0..0): BOOLEAN, headOvfl (0: 1..1): BOOLEAN, devCheck (0: 2..2): BOOLEAN, notSelected (0: 3..3): BOOLEAN, notOnLine (0: 4..4): BOOLEAN, notReady (0: 5..5): BOOLEAN, sectorOvfl (0: 6..6): BOOLEAN, fifoUnderflow (0: 7..7): BOOLEAN, fifoOverflow (0: 8..8): BOOLEAN, checkErr (0: 9..9): BOOLEAN, readOnly (0: 10..10): BOOLEAN, cylOffset (0: 11..11): BOOLEAN, iobParityErr (0: 12..12): BOOLEAN, fifoParityErr (0: 13..13): BOOLEAN, eccErr (0: 14..14): BOOLEAN, sectorSearchErr (0: 15..15): BOOLEAN]; nullDiskStatus: DiskStatus = LOOPHOLE[0]; errorStatusMask: DiskStatus = [ -- These bits constitute errors: seekInc: TRUE, headOvfl: TRUE, devCheck: TRUE, notSelected: TRUE, notOnLine: TRUE, notReady: TRUE, sectorOvfl: TRUE, fifoUnderflow: TRUE, fifoOverflow: TRUE, checkErr: TRUE, iobParityErr: TRUE, fifoParityErr: TRUE, eccErr: TRUE, sectorSearchErr: TRUE, -- These are just status bits that do not necessarily constitute errors: readOnly: FALSE, cylOffset: FALSE]; sealValid: CARDINAL = 125377B; sealNil: CARDINAL = 0; cylinderRestore: CARDINAL = 177777B; cylinderUnknown: CARDINAL = 77777B; -- Disk hardware definitions -- Output to DiskControl register, and used as diskCommand in IOCBs diskControl: DoradoInputOutput.IOAddress = 10B; DiskCommand: TYPE = MACHINE DEPENDENT RECORD [ incrementDataPtr (0: 0..0): BOOLEAN _ FALSE, -- in IOCB only; not defined in hardware unused1 (0: 1..4): Filler, clearEnableRun (0: 5..5): BOOLEAN _ FALSE, debugMode (0: 6..6): BOOLEAN _ FALSE, blockTilIndex (0: 7..7): BOOLEAN _ FALSE, header (0: 8..9): Action _ none, label (0: 10..11): Action _ none, data (0: 12..13): Action _ none, unused2 (0: 14..15): Action _ none]; Action: TYPE = MACHINE DEPENDENT {none, write, check, read}; -- Output to DiskMuff register diskMuff: DoradoInputOutput.IOAddress = 11B; MuffCommand: TYPE = MACHINE DEPENDENT RECORD [ unused (0: 0..1): Filler, clearCompareErr (0: 2..2): BOOLEAN _ FALSE, setChecksumErr (0: 3..3): BOOLEAN _ FALSE, clearIndexTW (0: 4..4): BOOLEAN _ FALSE, clearSectorTW (0: 5..5): BOOLEAN _ FALSE, clearSeekTagTW (0: 6..6): BOOLEAN _ FALSE, clearErrors (0: 7..7): BOOLEAN _ FALSE, muffAddr (0: 8..15): MufflerAddress _ tempSense]; -- Input from DiskMuff register MuffInput: TYPE = MACHINE DEPENDENT RECORD [ unused (0: 0..14): Filler, bit (0: 15..15): BOOLEAN]; -- Output to DiskTag register diskTag: DoradoInputOutput.IOAddress = 14B; TagCommand: TYPE = MACHINE DEPENDENT RECORD [ driveTag (0: 0..0): BOOLEAN _ FALSE, cylinderTag (0: 1..1): BOOLEAN _ FALSE, headTag (0: 2..2): BOOLEAN _ FALSE, controlTag (0: 3..3): BOOLEAN _ FALSE, bus (0: 4..15): SELECT OVERLAID * FROM drive => [ unused (0: 4..5): Filler, subSectorCount (0: 6..9): [0..17B] _ 0, -- sub-sectors/sector - 1 loadSubSector (0: 10..10): BOOLEAN _ FALSE, select (0: 11..11): BOOLEAN _ FALSE, driveNumber (0: 12..15): Drive], cylinder => [ cylinder (0: 4..15): [0..7777B]], head => [ unused (0: 4..7): Filler, offset (0: 8..8): BOOLEAN _ FALSE, offsetDirection (0: 9..9): {out(0), in(1)} _ out, headNumber (0: 10..15): [0..77B]], control => [ syncPattern (0: 4..4): {s201, s001} _ s201, -- s001 is Alto-compatible unused (0: 5..5): Filler, strobeLate (0: 6..6): BOOLEAN _ FALSE, strobeEarly (0: 7..7): BOOLEAN _ FALSE, write (0: 8..8): BOOLEAN _ FALSE, read (0: 9..9): BOOLEAN _ FALSE, addressMark (0: 10..10): BOOLEAN _ FALSE, headReset (0: 11..11): BOOLEAN _ FALSE, deviceCheckReset (0: 12..12): BOOLEAN _ FALSE, headSelect (0: 13..13): BOOLEAN _ FALSE, rezero (0: 14..14): BOOLEAN _ FALSE, headAdvance (0: 15..15): BOOLEAN _ FALSE], ENDCASE]; -- Muffler addresses for status bits (DskEth-relative) MufflerAddress: TYPE = MACHINE DEPENDENT { tempSense(0), indexTW(1), sectorTW(2), seekTagTW(3), rdFifoTW(4), wrFifoTW(5), readData(6), writeData(7), enableRun(10B), debugMode(11B), notRdOnlyBlock(12B), notWriteBlock(13B), notCheckBlock(14B), active(15B), select0(16B), select1(17B), seekInc(20B), headOvfl(21B), devCheck(22B), notSelected(23B), notOnLine(24B), notReady(25B), sectorOvfl(26B), fifoUnderflow(27B), fifoOverflow(30B), readDataErr(31B), readOnly(32B), cylOffset(33B), iobParityErr(34B), fifoParityErr(35B), writeError(36B), readError(37B), -- others of interest only to Midas (177B)}; diskDMuxAddr: [0..7777B] = 2000B; -- DMux address of first disk muffler Filler: TYPE = [0..1] _ 0; -- Data structures private to the software DiskHandle: TYPE = MACHINE DEPENDENT RECORD [ unused (0: 0..7): Filler, drive (0: 8..15): Drive]; nullDiskHandle: DiskHandle = LOOPHOLE [177777B]; Drive: TYPE = [0..drives); drives: CARDINAL = 16; -- maximum number of drives per controller -- The "system" drive is drive 0, which is always assumed to be a T-80 that is -- addressed in a funny way. t80 and t300 refer to drives other than drive 0, -- which are addressed in the normal way. Model: TYPE = {nonexistent, system, t80, t300}; modelCylinders: ARRAY Model OF CARDINAL = [ nonexistent: 0, system: 815*5, t80: 815, t300: 815]; modelHeads: ARRAY Model OF CARDINAL = [ nonexistent: 0, system: 1, t80: 5, t300: 19]; modelSectors: ARRAY Model OF CARDINAL = [ nonexistent: 0, system: 28, t80: 28, t300: 28]; -- Public variables globalStateSize: PUBLIC CARDINAL _ 0; -- an IOCB for label fixup is not required for the Dorado implementation. nullDeviceHandle: PUBLIC DeviceHandle _ DeviceFromDiskHandle[nullDiskHandle]; operationSize: PUBLIC CARDINAL _ SIZE[DoradoOperation]; -- Private variables csb: CSBPtr = LOOPHOLE[LONG[177520B]]; totalErrors: CARDINAL _ 0; -- = total errors reported by Poll modelTable: ARRAY [0..drives) OF Model _ ALL [nonexistent]; -- Public procedures -- Assumes Initialize is called before GetDeviceAttributes GetDeviceAttributes: PUBLIC PROCEDURE [device: DeviceHandle] RETURNS [cylinders, movingHeads, fixedHeads, sectorsPerTrack: CARDINAL] = BEGIN diskHandle: DiskHandle = DiskFromDeviceHandle[device]; model: Model = modelTable[diskHandle.drive]; RETURN [ cylinders: modelCylinders[model], movingHeads: modelHeads[model], fixedHeads: 0, sectorsPerTrack: modelSectors[model]]; END; -- Assumes Initialize is called before GetNextDevice GetNextDevice: PUBLIC PROCEDURE [device: DeviceHandle] RETURNS [DeviceHandle] = BEGIN disk: DiskHandle = DiskFromDeviceHandle[device]; IF disk=nullDiskHandle THEN -- drive 0 presumed to exist always RETURN [DeviceFromDiskHandle[[drive: 0]]]; FOR drive: Drive IN (disk.drive..drives) DO IF modelTable[drive]#nonexistent THEN RETURN [DeviceFromDiskHandle[[drive: drive]]]; ENDLOOP; RETURN [nullDeviceHandle]; END; -- Assumes that the disk hardware and microcode are quiescent Initialize: PUBLIC PROCEDURE [t: WORD, globalState: GlobalStatePtr] = BEGIN OPEN DoradoInputOutput; ManualStrobe: PROCEDURE [tag, data: TagCommand] = BEGIN Output[data, diskTag]; Output[Inline.BITOR[tag, data], diskTag]; Output[data, diskTag]; -- No need to wait: drive is advertised to return status within 200 ns -- of its being selected. END; AutoStrobe: PROCEDURE [data: TagCommand] = BEGIN Output[data, diskTag]; -- Need to wait at least 1.2 microseconds for strobe sequence to complete, -- plus an unspecified amount of time (probably less than a microsecond) -- for the drive to return up-to-date status. THROUGH [0..10) DO NULL; ENDLOOP; END; ReadMuffler: PROCEDURE [addr: MufflerAddress] RETURNS [status: BOOLEAN] = BEGIN -- Reads through system DMux rather than through DskEth muffler interface, because -- it's impossible for the disk task to dismiss wakeups without clobbering the -- muffler address. RETURN [ DoradoInputOutput.RWMufMan[ [useDMD: FALSE, dMuxAddr: DMuxFromMufAddr[addr]]].dMuxData#0]; END; ClassifyDrive: PROCEDURE [drive: Drive] RETURNS [model: Model] = BEGIN -- Select the drive. If it doesn't become selected then it doesn't exist. ManualStrobe[tag: [driveTag: TRUE, bus: drive[driveNumber: 0]], data: [bus: drive[select: TRUE, driveNumber: drive]]]; IF ReadMuffler[notSelected] THEN RETURN [nonexistent] ELSE BEGIN -- Try to select a head which does not exist on a T80. -- This causes a headOvfl error on a T80, not on a T300 AutoStrobe[[controlTag: TRUE, bus: control[deviceCheckReset: TRUE]]]; AutoStrobe[[headTag: TRUE, bus: head[headNumber: modelHeads[t80]]]]; model _ IF ReadMuffler[headOvfl] THEN t80 ELSE t300; AutoStrobe[[controlTag: TRUE, bus: control[deviceCheckReset: TRUE]]]; END; END; lastDrive: Drive; ResetDisk[disable]; -- disable disk task so it won't run and confuse matters csb^ _ [ head: nilIOCBQueueHead, interruptMask: t, drive: 0, cylinder: cylinderUnknown, tail: NIL]; modelTable[0] _ system; -- drive 0 presumed to exist always -- Drives 0-2 are connected directly to the controller, and drive 3 also if -- no multiplexor is present. -- If a multiplexor is present, it is connected in place of drive 3. -- Drive 17B is presumed not to exist. Try selecting drive 17B. If this succeeds -- then what really happened is that no multiplexor is present and drive 3 -- got selected instead. If this fails then either there is a multiplexor -- or there is no multiplexor and no drive 3 is present; it is safe to test each -- of drives 4-16 since in this case they will select only if there is a multiplexor. lastDrive _ (IF ClassifyDrive[LAST[Drive]]=nonexistent THEN LAST[Drive]-1 ELSE 3); FOR drive: Drive IN [1..lastDrive] DO modelTable[drive] _ ClassifyDrive[drive]; ENDLOOP; ResetDisk[normal]; -- put disk microcode back in normal state END; InitializeCleanup: PUBLIC PROCEDURE = BEGIN OPEN DeviceCleanup; item: Item; reason: Reason; savedCSB: CSB; DO reason _ Await[@item]; SELECT reason FROM turnOff, kill => BEGIN UNTIL csb.head.tag = idle DO ENDLOOP; savedCSB _ csb^; csb.head.iocb _ nilIOCBShortPtr; END; turnOn => BEGIN csb^ _ savedCSB; csb.cylinder _ cylinderUnknown; END; ENDCASE ENDLOOP END; Initiate: PUBLIC PROCEDURE [operationPtr: OperationPtr] = BEGIN doradoOperationPtr: DoradoOperationPtr = LOOPHOLE[operationPtr]; iocb: IOCBLongPtr = @doradoOperationPtr.iocb; iocbShort: IOCBShortPtr = LOOPHOLE[Inline.LowHalf[iocb]]; diskCommand: DiskCommand _ SELECT operationPtr.command FROM vv => [incrementDataPtr: FALSE, header: check, label: check, data: none], vvr => [incrementDataPtr: FALSE, header: check, label: check, data: read], vvw => [incrementDataPtr: FALSE, header: check, label: check, data: write], vvv => [incrementDataPtr: FALSE, header: check, label: check, data: check], vw => [incrementDataPtr: FALSE, header: check, label: write, data: none], vww => [incrementDataPtr: FALSE, header: check, label: write, data: write], vr => [incrementDataPtr: FALSE, header: check, label: read, data: none], vrr => [incrementDataPtr: FALSE, header: check, label: read, data: read], vrw => [incrementDataPtr: FALSE, header: check, label: read, data: write], vrv => [incrementDataPtr: FALSE, header: check, label: read, data: check], rv => [incrementDataPtr: FALSE, header: read, label: check, data: none], rvr => [incrementDataPtr: FALSE, header: read, label: check, data: read], rvw => [incrementDataPtr: FALSE, header: read, label: check, data: write], rvv => [incrementDataPtr: FALSE, header: read, label: check, data: check], rw => [incrementDataPtr: FALSE, header: read, label: write, data: none], rww => [incrementDataPtr: FALSE, header: read, label: write, data: write], rr => [incrementDataPtr: FALSE, header: read, label: read, data: none], rrr => [incrementDataPtr: FALSE, header: read, label: read, data: read], rrw => [incrementDataPtr: FALSE, header: read, label: read, data: write], rrv => [incrementDataPtr: FALSE, header: read, label: read, data: check], w => [incrementDataPtr: FALSE, header: write, label: none, data: none], SA4000FaceExtras.www => [incrementDataPtr: FALSE, header: write, label: write, data: write], ENDCASE => ERROR; diskCommand.incrementDataPtr _ operationPtr.incrementDataPtr; iocb^ _ [ next: nilIOCBShortPtr, seal: sealValid, drive: DiskFromDeviceHandle[operationPtr.device].drive, pageCount: operationPtr.pageCount, command: diskCommand, diskAddress: operationPtr.clientHeader, diskHeader: operationPtr.clientHeader, headerPtr: @iocb.diskHeader, headerECC: 0, headerStatus: nullDiskStatus, labelPtr: IF diskCommand.label=read THEN operationPtr.labelPtr ELSE @iocb.diskLabel, labelECC: 0, labelStatus: nullDiskStatus, dataPtr: operationPtr.dataPtr, dataECC: 0, dataStatus: nullDiskStatus, diskLabel: operationPtr.labelPtr^]; -- Chain this IOCB onto CSB for processing by microcode. IF csb.head.iocb # nilIOCBShortPtr THEN csb.tail.next _ iocbShort; IF csb.head.iocb = nilIOCBShortPtr AND iocb.seal = sealValid THEN csb.head.iocb _ iocbShort; csb.tail _ iocb; END; Poll: PUBLIC PROCEDURE [operationPtr: OperationPtr] RETURNS [status: Status] = BEGIN doradoOperationPtr: DoradoOperationPtr = LOOPHOLE[operationPtr]; iocb: IOCBLongPtr = @doradoOperationPtr.iocb; iocbShort: IOCBShortPtr = LOOPHOLE[Inline.LowHalf[iocb]]; -- Capture in-progress/done state here and use it for all subsequent decisions. -- If the command finishes during the code below, we will still report inProgress -- to the client, who will Poll again and get the real ending state. status _ IF iocb.seal=sealValid THEN inProgress ELSE goodCompletion; -- Copy things back to client that he might want to monitor while the command -- is still in progress. operationPtr.clientHeader _ iocb.diskAddress; operationPtr.pageCount _ iocb.pageCount; IF status#inProgress THEN BEGIN -- Command has completed: successfully if pageCount=0, unsuccessfully otherwise. IF iocb.pageCount=0 THEN BEGIN IF iocb.diskAddress.head = modelHeads[modelTable[iocb.drive]] THEN -- Successful transfer happened to end at a cylinder boundary. -- Increment disk address to next cylinder. BEGIN iocb.diskAddress.head _ 0; iocb.diskAddress.cylinder _ iocb.diskAddress.cylinder+1; END END ELSE BEGIN -- Error occurred. Must carefully examine DiskStatus for each block to determine -- what happened and to decide what Status to report to the client. combinedStatus: DiskStatus = Inline.BITOR[iocb.headerStatus, Inline.BITOR[iocb.labelStatus, iocb.dataStatus]]; SELECT TRUE FROM -- First check for problems that might have caused the operation to -- malfunction and generated other errors as secondary effects. combinedStatus.notSelected OR combinedStatus.notOnLine OR combinedStatus.notReady => status _ notReady; combinedStatus.seekInc => status _ seekTimeout; combinedStatus.sectorSearchErr => status _ sectorTimeout; -- Check for head overflow: means transfer crossed a cylinder boundary. combinedStatus.headOvfl => IF iocb.diskAddress.head = modelHeads[modelTable[iocb.drive]] THEN BEGIN iocb.diskAddress.head _ 0; iocb.diskAddress.cylinder _ iocb.diskAddress.cylinder+1; GOTO restart; END ELSE status _ hardwareError; -- Look for header problems: iocb.headerStatus.checkErr => -- Header check error: must discriminate possible causes status _ SELECT TRUE FROM iocb.diskHeader.cylinder#iocb.diskAddress.cylinder => wrongCylinder, iocb.diskHeader.head#iocb.diskAddress.head => wrongHead, ENDCASE => wrongSector; -- Would like to distinguish other header errors (e.g., header checksum), -- but there is no Status value with which to report them!! Inline.BITAND[iocb.headerStatus, errorStatusMask]#0 => status _ hardwareError; -- Look for label problems: iocb.labelStatus.sectorOvfl OR iocb.labelStatus.eccErr => status _ labelError; iocb.labelStatus.checkErr => status _ labelCheck; Inline.BITAND[iocb.labelStatus, errorStatusMask]#0 => status _ hardwareError; -- Look for data problems: iocb.dataStatus.sectorOvfl OR iocb.dataStatus.eccErr => status _ dataError; ENDCASE => status _ hardwareError; -- Microcode has been deferring ever since the error occurred. -- Now zap all deferred operations and allow new ones to be Initiated. csb.head.iocb _ nilIOCBShortPtr; totalErrors _ totalErrors+1; EXITS restart => BEGIN iocb.headerStatus _ iocb.labelStatus _ iocb.dataStatus _ nullDiskStatus; iocb.seal _ sealValid; csb.head.iocb _ iocbShort; status _ inProgress; END; END; -- Copy updated information back to client. -- We already did clientHeader and pageCount. -- Note: if reading label, it was read directly to client so don't clobber it here. IF iocb.command.label#read THEN operationPtr.labelPtr^ _ iocb.diskLabel; operationPtr.dataPtr _ iocb.dataPtr; operationPtr.diskHeader _ iocb.diskHeader; END; END; Recalibrate: PUBLIC PROCEDURE [device: DeviceHandle] = BEGIN csb.cylinder _ cylinderRestore; END; Reset: PUBLIC PROCEDURE [device: DeviceHandle] = BEGIN -- WHAT SHOULD THIS DO? END; Start: PUBLIC PROCEDURE = -- exported to HeadStartChain BEGIN RemainingHeads.Start[]; END; -- Private procedures DiskFromDeviceHandle: PROCEDURE [device: DeviceHandle] RETURNS [DiskHandle] = INLINE BEGIN RETURN[LOOPHOLE[device]]; END; DeviceFromDiskHandle: PROCEDURE [disk: DiskHandle] RETURNS [DeviceHandle] = INLINE BEGIN RETURN[LOOPHOLE[disk]]; END; DMuxFromMufAddr: PROCEDURE [mufAdr: MufflerAddress] RETURNS [DoradoInputOutput.DMuxAddr] = INLINE BEGIN RETURN[diskDMuxAddr+LOOPHOLE[mufAdr, CARDINAL]]; END; END. LOG Time: August 8, 1979 1:09 AM By: Redell Action: Add log to file from Jarvis Time: August 17, 1979 1:27 PM By: Gobbel Action: Pilot formatting Time: August 22, 1979 12:40 PM By: Gobbel Action: Bring up to date with revised device face Time: October 2, 1979 11:38 PM By: Redell Action: Further cleanup, esp for bootchains (partial label verification) Time: October 8, 1979 9:22 AM By: McJones Action: Restart controller bug, add hardwareState Time: October 10, 1979 3:53 PM By: McJones Action: INLINE procedures for speed; labelCheck is cue to call StartLabelFix Time: October 30, 1979 11:04 AM By: McJones AR2643: Label fix didn't reset iocb.label from user's label Time: November 1, 1979 1:20 PM By: Frandeen Action: Fix recalibrate after header error; bug in SynchronizeWithController Time: November 7, 1979 1:17 PM By: McJones AR2739: Label fix up confused by spurious labelCheck status returned by read label command on sector with missing (label?) synch byte Time: November 9, 1979 8:59 AM By: Frandeen Action: Implement runs of pages. Change interface to Poll. Change LabelFix procedures. Change interface to StartB since we no longer need an extra IOCB for label fixup. Time: December 13, 1979 11:29 AM By: Gobbel Action: Change name from SA4000D0Head to SA4000HeadD0 Time: January 30, 1980 12:48 PM By: McJones Action: Update to match new face; add StartChain logic Time: June 25, 1980 10:34 AM By: McJones Action: 48-bit processor ids Time: July 24, 1980 11:23 AM By: McJones Action: Label fixup must update iocb.operation.clientHeader December 6, 1980 2:56 PM Taft Convert for Dorado February 13, 1983 1:24 pm Taft Add support for multiple drives and T-300s May 22, 1983 1:53 pm Taft Add www command