packages feed

language-Modula2-0.1: examples/Modula-2_Libraries/PMOS/sources/general/floppy.mod

IMPLEMENTATION MODULE Floppy;

	(****************************************************************)
	(*								*)
	(*		Device driver for floppy disk.			*)
	(*								*)
	(*  Programmer:		P. Moylan				*)
	(*  Last edited:	25 November 1994			*)
	(*  Status:		Working					*)
	(*								*)
	(*	Working with 360KB, 1.2MB, and 1.44MB disks, but not	*)
	(*	completely reliably, and more fiddling with delays	*)
	(*	while reading from controller will be needed.		*)
	(*								*)
	(*	The busy waits are inelegant and unreliable, and I	*)
	(*	should look for a better method.			*)
	(*								*)
	(*	There are intermittent failures of the controller,	*)
	(*	i.e. the controller is not always listening when it	*)
	(*	should be, and does not always send a reply when it	*)
	(*	should, which suggests that there might be some time	*)
	(*	delays which should be lengthened; but my tests have	*)
	(*	failed to track down the cause.  Luckily, the problem	*)
	(*	usually disappears on a retry.				*)
	(*								*)
	(*	Still need to do something about:			*)
	(*	    Implementation of verify, format			*)
	(*	    Handling case of 360K disk in 1.2M or 1.44M drive	*)
	(*	    Detection of disk change				*)
	(*								*)
	(****************************************************************)

(************************************************************************)
(*									*)
(* HARDWARE ASSUMPTIONS:						*)
(*									*)
(*  This module is set up to handle the 765 diskette controller, with	*)
(*  DMA transfers to/from main memory using channel 2 of the 8237A DMA	*)
(*  controller.  Up to four disk drives can be handled, but the present	*)
(*  version of the software assumes two drives.  (I dropped support for	*)
(*  the other two because I doubt that anyone will ever need it).	*)
(*									*)
(*  Ports 3F0H-3F7H are reserved for the diskette controller.  Only	*)
(*  four of these ports are actually used: 3F2H for some motor control	*)
(*  bits, 3F4H for the 765 status register, 3F5H for the 765 data	*)
(*  register, and 3F7H for setting the data rate.  Commands to the 765	*)
(*  controller are sent as byte strings to its data register, and	*)
(*  status information comes back through the same port.  Essentially	*)
(*  the only function of the 765 status register is to indicate whether	*)
(*  the data register is ready to send or receive information.		*)
(*									*)
(*  Interrupts from the diskette controller come through request line 6	*)
(*  of the master 8259 interrupt controller (which lives at ports	*)
(*  020H-03FH), which maps them to processor interrupt number 14.	*)
(*									*)
(*  The disks are formatted with 8, 9, 15, or 18 sectors per track, and	*)
(*  there are two heads per cylinder, i.e. both sides of the disk are	*)
(*  used.								*)
(*									*)
(*  All of that information is no doubt confusing to those unfamiliar	*)
(*  with the hardware.  From a software viewpoint, the following	*)
(*  sequence of operations is needed to perform a disk read or write:	*)
(*									*)
(*   1.	Tell the diskette controller to start the motor, if it is not	*)
(*	already running from an earlier operation.  A time delay is	*)
(*	then necessary to let the motor run up to its correct operating	*)
(*	speed.								*)
(*   2.	Load the DMA controller with the memory address to be used for	*)
(*	the data transfer.  (In most computers, the disk controller has	*)
(*	its own built-in DMA hardware.  In this computer, the DMA	*)
(*	controller is physically separate from the disk controller; but	*)
(*	the principle is the same, since we permanently dedicate	*)
(*	channel 2 of the DMA controller to floppy disk operations).	*)
(*   3.	Send the appropriate command bytes to the diskette controller	*)
(*	to seek (i.e. move the read-write head) to the desired track.	*)
(*   4.	Wait for the interrupt which announces that the seek operation	*)
(*	is done.							*)
(*   5.	Send the appropriate command bytes to the diskette controller	*)
(*	ports to start the read or write operation.  The data transfer	*)
(*	between the diskette controller and main memory occurs via the	*)
(*	DMA controller, and does not require any further software	*)
(*	intervention.							*)
(*   6.	When the operation is complete, the diskette controller sends	*)
(*	an interrupt request to the processor.  The interrupt routine	*)
(*	can then send a "stop motor" command to the diskette.		*)
(*									*)
(*  This description is oversimplified in that it ignores error		*)
(*  handling.  In fact a large part of the software in this module is	*)
(*  the code needed to deal with various types of error.		*)
(*									*)
(************************************************************************)

FROM SYSTEM IMPORT
    (* type *)	BYTE, ADDRESS,
    (* proc *)	ADR;

FROM IOErrorCodes IMPORT
    (* type *)	ErrorCode,
    (* proc *)	TranslateErrorCode;

FROM Devices IMPORT
    (* type *)	Device, RequestBlock, RequestBlockPointer, OperationType,
    (* proc *)	InstallDeviceDriver, DeviceName, AcceptRequest, IOrequest;

FROM MaintenancePages IMPORT
    (* type *)	MaintenancePage,
    (* proc *)	CreateMaintenancePage, Associate;

FROM Windows IMPORT
    (* type *)	Window, Colour, FrameType, DividerType,
    (* proc *)	OpenWindow, CloseWindow, WriteChar, WriteString, WriteLn,
		ChangeScrollingRegion, OpenSimpleWindow;

FROM NumericIO IMPORT
    (* proc *)	WriteCard, WriteHexByte, WriteHexWord, WriteAddress;

FROM LowLevel IMPORT
    (* proc *)	IANDB, IORB, INOTB, RS, OutByte, InByte;

FROM DMA IMPORT
    (* proc *)	LoadDMAparameters, CheckDMAAddress;

FROM MiscPMOS IMPORT
    (* proc *)	ShortDelay, ReadCMOS;

FROM Semaphores IMPORT
    (* type *)	Semaphore,
    (* proc *)	CreateSemaphore, DestroySemaphore, Wait, Signal;

FROM Timer IMPORT
    (* proc *)	Sleep, TimedWait;

FROM TaskControl IMPORT
    (* type *)	Lock,
    (* proc *)	CreateTask, CreateInterruptTask, WaitForInterrupt,
		CreateLock, DestroyLock, Obtain, Release;

FROM TerminationControl IMPORT
    (* proc *)	SetTerminationProcedure;

(************************************************************************)
(*			MISCELLANEOUS GLOBAL DATA			*)
(************************************************************************)

CONST testing = TRUE;		(* debugging control *)

VAR Mpage: MaintenancePage;
    log: Window;

(************************************************************************)
(*			LOTS OF USEFUL CONSTANTS			*)
(************************************************************************)

CONST

    (************************************)
    (* Formatting, etc., parameters.	*)
    (************************************)

    NumberOfHeads = 2;
    DataLengthCode = 0FFH;

    (*********************)
    (* Interrupt number. *)
    (*********************)

    DiskInterrupt = 14;

    (*****************)
    (* Port numbers. *)
    (*****************)

    BasePort = 03F0H;
    MotorControlPort = BasePort + 2;
    StatusPort = BasePort + 4;
    DataPort = BasePort + 5;
    DataRatePort = BasePort + 7;

    (********************************************************************)
    (* Flags in controller status register.  The high-order bit is set	*)
    (* if the controller is ready to communicate with the processor, 	*)
    (* and the next bit indicates the direction.  The low-order six	*)
    (* bits, which this software does not use, give more precise	*)
    (* information about why the controller is not ready.		*)
    (********************************************************************)

    StatusMask = 0C0H;
    ReadyToSend = BYTE(0C0H);
    ReadyToListen = BYTE(080H);

    (********************************************************************)
    (* Flags in internal controller status registers SR0, SR1, SR2, and	*)
    (* SR3.  (Note that these are distinct from the main status		*)
    (* register mentioned above).  We list here only those flags	*)
    (* checked by more than one procedure.  Some further flags are	*)
    (* defined locally in some procedures.				*)
    (********************************************************************)

    SeekEnd = 20H;	(* in SR0 *)

(************************************************************************)
(*			    DRIVE INFORMATION				*)
(************************************************************************)

VAR
    (* The variable "floppy" is the means by which this device driver	*)
    (* makes itself known to module Devices.				*)

    floppy: Device;

TYPE DriveNumber = SHORTCARD [0..1];

    (* In this version, we support only two drives.  See comments in	*)
    (* source about how to support four drives if required.		*)

TYPE DriveType = [0..5];

    (* The drive types which the software currently recognises are:	*)
    (*		0	no drive present				*)
    (*		1	double sided, 48 TPI, capacity 360KB		*)
    (*		2	high capacity, 96 TPI, capacity 1.2MB		*)
    (*		3	unknown type					*)
    (*		4	3.5", capacity 1.44MB				*)
    (*		5	unknown type					*)

CONST UnknownDriveType = MAX(DriveType);

    (* The "unknown" category does not mean that all is lost.  The	*)
    (* present version of the software has no provision for handling	*)
    (* unknown types; but in principle such provision could be added.	*)
    (* For example, one could work out how many tracks there were on	*)
    (* an unknown disk by checking which Seek operations work.		*)

TYPE DataRateCode = SHORTCARD [0..2];

    (* The data rate code is 0 for 500KBS, 1 for 300KBS, 2 for 250KBS.	*)
    (* Information in the DriveInfo array is partly an attribute of the	*)
    (* drive, and partly of the medium in the drive.  In this version	*)
    (* of the software, we allow for 360KB and 1.2MB drives, but have	*)
    (* not yet allowed for the case of a 360KB disk in a 1.2MB drive.	*)
    (* The hardware will support that case, but the software is not yet	*)
    (* set up to work out what size disk is present.			*)

VAR DriveInfo: ARRAY DriveNumber OF
		    RECORD
			drivetype: DriveType;
			SectorsPerTrack: CARDINAL;
			NumberOfCylinders: CARDINAL;
			SectorGap: BYTE;
			DataRate: DataRateCode;
			HeadSettlingTime: BYTE;	(* milliseconds *)
		    END (*RECORD*);

(************************************************************************)
(*		INFORMATION ABOUT THE DRIVE MOTORS			*)
(************************************************************************)
(*									*)
(*  The bits in the motor control port register are:			*)
(*	   bit	7	turn on drive motor 3				*)
(*		6	turn on drive motor 2				*)
(*		5	turn on drive motor 1				*)
(*		4	turn on drive motor 0				*)
(*		3	interrupt enable				*)
(*		2	reset (0=reset, 1=no reset)			*)
(*	      1-0	number of drive to select			*)
(*									*)
(*  Note that any number of motors may be running but only one drive	*)
(*  can be selected at any given time.  Because of this, and because	*)
(*  of controller limitations, we can only do I/O on one disk at a	*)
(*  time, but we do have the option of keeping the other motors running	*)
(*  in anticipation of future operations on the other drives.		*)
(*									*)
(************************************************************************)

VAR

    (* The following array is in fact an array of constants, giving the	*)
    (* motor control bits for the four drives.  We set up the values	*)
    (* 10H, 20H, 40H, 80H in the initialisation code.			*)

    MotorControlBit: ARRAY DriveNumber OF BYTE;

    (* SharedMotorData holds data which may be modified by more than	*)
    (* one task.  The lock field is for critical section protection.	*)
    (* The count field is used only during module initialisation, to	*)
    (* count how many motor control tasks are running.  MotorStatus	*)
    (* is the byte we will put out to the motor control port.		*)

    SharedMotorData:	RECORD
			    lock: Lock;
			    count: SHORTCARD;
			    MotorStatus: BYTE;
			END (*RECORD*);

    (* Interaction between the user task and the drive control task is	*)
    (* via several semaphores: MotorStartRequest is for requests to	*)
    (* start the motor, MotorUpToSpeed is for saying when the drive is	*)
    (* at operating speed, and MotorMayBeStopped is for telling the	*)
    (* motor control task that the drive is no longer needed.  There is	*)
    (* a separate motor control task for each motor.			*)

    MotorStartRequest, MotorUpToSpeed, MotorMayBeStopped:
					ARRAY DriveNumber OF Semaphore;

(************************************************************************)
(*		INFORMATION ABOUT THE CONTROLLER STATE			*)
(************************************************************************)

TYPE
    HeadNumber = SHORTCARD [0..1];

VAR
    ShutDownDesired: BOOLEAN;
    OperationDone: Semaphore;
    ResetNeeded: BOOLEAN;
    CalibrationNeeded: ARRAY DriveNumber OF BOOLEAN;
    CurrentCylinder: ARRAY DriveNumber OF CARDINAL;

    (* The controller status record is updated after every interrupt	*)
    (* from the disk controller.  We choose to make it global to the	*)
    (* module, at least in this version, because it is accessed by	*)
    (* several different procedures.					*)

    (* The amount of meaningful information stored here varies with the	*)
    (* operation performed.  The procedure which collects the		*)
    (* information interrogates the disk controller, and accepts as	*)
    (* many status bytes as the controller is prepared to deliver.	*)
    (* If the "valid" field in this record is FALSE, something has gone	*)
    (* wrong with collecting the status information itself.		*)

    (* Most commands produce seven bytes of reply: three bytes of	*)
    (* status information, mostly consisting of error flags, then three	*)
    (* bytes giving the cylinder, head, and sector which was reached at	*)
    (* the end of command execution, and finally a number giving sector	*)
    (* size (the code is log2(#bytes/128).  The Sense Interrupt Status	*)
    (* command produces only two bytes of reply (and these are given	*)
    (* immediately, without producing any interrupt); the first is the	*)
    (* contents of Status Register 0 of the controller, and the second	*)
    (* is the current cylinder number.  The Sense Drive Status command	*)
    (* produces only one byte of reply, as does any invalid command.	*)
    (* The Recalibrate, Specify, and Seek commands do not produce any	*)
    (* reply, but in those cases we can send a Sense Interrupt Status	*)
    (* command to force some status information to be sent.		*)

    (* Incidentally, it often turns out that some of the reply bytes	*)
    (* are worthless, since the controller has the bad habit of just	*)
    (* bouncing back the information about cylinder, etc., which it was	*)
    (* sent, rather than sending data actually read from the disk.	*)
    (* For example, a Seek operation can succeed even if there is no	*)
    (* disk in the drive, which means that it can be a little tricky	*)
    (* working out just why a command failed.  Even the "ready" flags	*)
    (* are useless; contrary to what the controller documentation says,	*)
    (* a drive can appear to be ready even when the door is open and	*)
    (* there is no disk present.					*)

    ControllerStatus:	RECORD
			    valid: BOOLEAN;
			    info: ARRAY [0..6] OF BYTE
			END (*RECORD*);

(************************************************************************)
(*			    DISK MOTOR CONTROL				*)
(************************************************************************)

PROCEDURE MotorControlTask;

    (* Several tasks execute this procedure, one for each drive.  The	*)
    (* client task is expected to perform the operations:		*)
    (*		Signal (MotorStartRequest[drive]);			*)
    (*		Wait (MotorUpToSpeed[drive]);				*)
    (*		do the desired disk operation;				*)
    (*		Signal (MotorMayBeStopped[drive]);			*)
    (* This task responds to the Signal operations by starting and	*)
    (* stopping the drive motor.  The reason for having a separate task	*)
    (* is that we leave the motor running between operations - thereby	*)
    (* avoiding a motor startup delay - as long as new start requests	*)
    (* are coming along frequently enough.  Thus, this task has a job	*)
    (* to do even after the disk operations for the client task have	*)
    (* been completed.							*)

    CONST
	UpToSpeedDelay = 250;  MotorStopDelay = 2000;
	TimeoutCheckInterval = 500;

    VAR drive: DriveNumber;
	TimedOut: BOOLEAN;
	motorlog: Window;

    BEGIN
	(* Preamble: since several tasks are executing this procedure,	*)
	(* we need to work out which drive the current invocation is	*)
	(* supposed to be looking after.				*)

	WITH SharedMotorData DO
	    Obtain (lock);
	    drive := count;  INC (count);
	    Release (lock);
	END (*WITH*);

	IF testing THEN
	    OpenWindow (motorlog, red, white, 4*CARDINAL(drive),
	    		4*CARDINAL(drive)+3, 61, 79, simpleframe, nodivider);
	    Associate (motorlog, Mpage);
	    WriteString (motorlog, "Drive number ");
	    WriteHexByte (motorlog, drive);
	    ChangeScrollingRegion (motorlog, 2, 2);
	END (*IF*);

	(* Each time around the main loop of this procedure, we start	*)
	(* and stop the motor once.  There is an inner loop which keeps	*)
	(* the motor running over several disk operations, as long as	*)
	(* motor start requests arrive sufficiently frequently.		*)

	LOOP	(* until ShutDownDesired *)

	    IF testing THEN
		WriteLn (motorlog);  WriteString (motorlog, "stopped");
	    END (*IF*);

	    (* Wait for a motor start request, periodically checking	*)
	    (* the ShutDownDesired flag.				*)

	    REPEAT
		IF ShutDownDesired THEN EXIT(*LOOP*) END(*IF*);
		TimedWait (MotorStartRequest[drive],
					TimeoutCheckInterval, TimedOut);
	    UNTIL NOT TimedOut;

	    (* A request has arrived.  Start the motor.	*)

	    WITH SharedMotorData DO
		Obtain (lock);
		MotorStatus := IORB(drive, IANDB (0FCH,
			IORB (MotorStatus, MotorControlBit[drive])));
		OutByte (MotorControlPort, MotorStatus);
		Release (lock);
	    END (*WITH*);
	    IF testing THEN
		WriteLn (motorlog);  WriteString (motorlog, "starting");
	    END (*IF*);

	    (* Give the motor time to reach its operating speed.	*)

	    Sleep (UpToSpeedDelay);

	    (* We remain in the following inner loop for as long as	*)
	    (* there is sufficient demand for keeping the motor going.	*)

	    REPEAT	(* until TimedOut *)

		(* Ensure that the drive is selected, and then inform	*)
		(* the client that it is ready to use.			*)

		WITH SharedMotorData DO
		    Obtain (lock);
		    IF IANDB (MotorStatus, 3) <> BYTE(drive) THEN
			MotorStatus := IORB(drive, IANDB (MotorStatus, 0FCH));
			OutByte (MotorControlPort, MotorStatus);
		    END (*IF*);
		    Release (lock);
		END (*WITH*);
		Signal (MotorUpToSpeed[drive]);

		IF testing THEN
		    WriteLn (motorlog);  WriteString (motorlog, "in use");
		END (*IF*);

		(* Wait until the client has finished using the drive.	*)

		Wait (MotorMayBeStopped[drive]);

		IF ShutDownDesired THEN
		    TimedOut := TRUE;
		ELSE

		    (* Here is where we must decide whether to stop the	*)
		    (* motor.  Wait - but not forever - for a new start	*)
		    (* request to arrive.				*)

		    IF testing THEN
			WriteLn(motorlog);WriteString(motorlog, "timing out");
		    END (*IF*);

		    TimedWait (MotorStartRequest[drive], MotorStopDelay,
								TimedOut);

		END (*IF*);

	    UNTIL TimedOut;

	    (* Stop the motor.  At this stage there might well be an	*)
	    (* operation in progress on some other drive, so we must be	*)
	    (* careful not to interfere with the "selected" field of	*)
	    (* the MotorStatus byte.					*)

	    WITH SharedMotorData DO
		Obtain (lock);
		MotorStatus := IANDB (MotorStatus,
				INOTB(MotorControlBit[drive]));
		OutByte (MotorControlPort, MotorStatus);
		Release (lock);
	    END (*WITH*);

	    IF testing THEN
		WriteLn (motorlog);  WriteString (motorlog, "stopped");
	    END (*IF*);

	END (* main LOOP *);

	IF testing THEN
	    CloseWindow (motorlog);
	END (*IF*);

	WITH SharedMotorData DO
	    Obtain (lock);
	    DEC (count);
	    Release (lock);
	END (*WITH*);

    END MotorControlTask;

(************************************************************************)

PROCEDURE CreateMotorControlTasks;

    (* Initialisation procedure: Sets up the motor control tasks, and	*)
    (* the global variables which they use.				*)

    CONST AllowInterrupts = 0CH;

    VAR drive: DriveNumber;

    BEGIN
	ShutDownDesired := FALSE;
	MotorControlBit[0] := 10H;
	MotorControlBit[1] := 20H;

	(* To support four drives, include the statements:
		MotorControlBit[2] := 40H;
		MotorControlBit[3] := 80H;
	*)

	WITH SharedMotorData DO
	    CreateLock (lock);
	    count := 0;
	    MotorStatus := AllowInterrupts;
	END (*WITH*);
	FOR drive := 0 TO MAX(DriveNumber) DO
	    CreateSemaphore (MotorStartRequest[drive], 0);
	    CreateSemaphore (MotorUpToSpeed[drive], 0);
	    CreateSemaphore (MotorMayBeStopped[drive], 0);
	    CreateTask (MotorControlTask, 13, "Floppy motor");
	END (*FOR*);
    END CreateMotorControlTasks;

(************************************************************************)

PROCEDURE ShutDownMotors;

    VAR drive, MotorsRunning: SHORTCARD;

    BEGIN
	ShutDownDesired := TRUE;
	REPEAT
	    Sleep (400);
	    WITH SharedMotorData DO
		Obtain (lock);
		MotorsRunning := count;
		Release (lock);
	    END (*WITH*);
	UNTIL MotorsRunning = 0;
	DestroyLock (SharedMotorData.lock);
	FOR drive := 0 TO MAX(DriveNumber) DO
	    DestroySemaphore (MotorStartRequest[drive]);
	    DestroySemaphore (MotorUpToSpeed[drive]);
	    DestroySemaphore (MotorMayBeStopped[drive]);
	END (*FOR*);
    END ShutDownMotors;

(************************************************************************)
(*			THE BASIC DISK OPERATIONS			*)
(************************************************************************)

PROCEDURE SendCommand (command: BYTE): BOOLEAN;

    (* The basic output operation to the disk controller command port.	*)
    (* It is complicated by the fact that the controller is not always	*)
    (* ready to receive a command, so we might have to busy wait for	*)
    (* the controller's being willing to listen to us.			*)
    (* The function result indicates success; it is FALSE if we were	*)
    (* unable to get the controller to listen.  In that case, this	*)
    (* procedure sets the global flag ResetNeeded.  This procedure also	*)
    (* returns FALSE, without sending anything, if ResetNeeded has	*)
    (* already been set by an earlier entry.				*)

    CONST MaxNumberOfRetries = 10;
	  pauselength = 300;

    VAR patience: [0..MaxNumberOfRetries];

    BEGIN

	(* Exit immediately if a reset is needed.  This can happen if a	*)
	(* command is more than one byte long; if something goes wrong	*)
	(* while sending such a command, we would only make matters	*)
	(* worse by sending the later bytes of that command.		*)

	IF ResetNeeded THEN
	    RETURN FALSE;
	END (*IF*);

	(* There needs to be a brief delay between reading and writing	*)
	(* the controller's data register and a read of its status	*)
	(* register.							*)

	ShortDelay (pauselength);

	(* We see whether the disk controller is idle by reading the	*)
	(* top two bits from its status port.  The idle state is shown	*)
	(* by binary 10 in those bits; the other bits are irrelevant to	*)
	(* us in this case.						*)

	patience := MaxNumberOfRetries;
	WHILE (patience > 0) AND
		(IANDB(InByte(StatusPort), StatusMask) <> ReadyToListen) DO
	    DEC (patience);
	END (*WHILE*);

	(* If the controller is still not idle, give up.	*)

	IF patience = 0 THEN
	    IF testing THEN
		WriteString (log, "SendCommand failure.  Status byte is ");
		WriteHexByte (log, InByte (StatusPort));  WriteLn (log);
	    END (*IF*);
	    ResetNeeded := TRUE;
	    RETURN FALSE;
	END (*IF*);

	(* All OK.  Send the desired command.	*)

	OutByte (DataPort, command);
	RETURN TRUE;

    END SendCommand;

(************************************************************************)

PROCEDURE WaitForCompletion;

    (* This procedure is called after every nontrivial command - that	*)
    (* is, every command which takes some time to complete, as distinct	*)
    (* from the commands which simply request status and which give an	*)
    (* immediate reply - to the disk controller.  We wait for the	*)
    (* interrupt which indicates completion of the operation, and then	*)
    (* if necessary issue a "sense" command to the controller, to make	*)
    (* it return some information about the status of the operation	*)
    (* just completed.  (In some cases, no sense command is needed,	*)
    (* because the controller spontaneously returns the information).	*)
    (* The returned information is left in the global variable called	*)
    (* ControllerStatus.						*)

    CONST SenseInterruptStatus = 8;  BusyBit = 10H;
	TimeoutPeriod = 500;	(* milliseconds *)
	pause1 = 20000;  pause2 = 20000;	(* arbitrary units *)

	(* PAUSELENGTH TESTS WITH 33 MHz PROCESSOR:		*)
	(*	8000 is big enough				*)
	(*	5000 produces intermittent failures		*)
	(*	1000 is too small				*)
	(* Earlier results on a slower processor:		*)
	(*	100 is certainly big enough			*)
	(*	60 failed on some tests				*)
	(*	50 works most of the time			*)
	(*	25 is too small					*)
	(* Have not yet tested the case pause1 <> pause2	*)
	(* The above figures are for the 1.2MB drive.  For my	*)
	(* drive B it seems that a longer pause is needed, so	*)
	(* I've arbitrarily increased the time.			*)

	(* ORIGINAL PAUSELENGTH TESTS WITH 286 PROCESSOR:	*)
	(*	1 was big enough				*)
	(*	0 was far too large!! - error in compiler?	*)

    VAR count: CARDINAL;  TimedOut: BOOLEAN;
	j: CARDINAL;	(* needed only for testing code *)

    BEGIN

	(* Wait for the interrupt.  If the interrupt does not happen	*)
	(* within a reasonable time, return with a "not ready"		*)
	(* indication in the controller status record (since the most	*)
	(* probable reason is that there is no disk in the drive).	*)
	(* If the interrupt occurs, issue a "sense" command if needed.	*)

	TimedWait (OperationDone, TimeoutPeriod, TimedOut);

	IF TimedOut THEN
	    IF testing THEN
		WriteString (log, "WaitForCompletion: timed out.");
		WriteLn (log);
		WriteString (log, "Interrupt masks:   master ");
		WriteHexByte (log, InByte(21H));
		WriteString (log, "   slave ");
		WriteHexByte (log, InByte (0A1H));
		WriteLn (log);
	    END (*IF*);
	    ResetNeeded := TRUE;
	    WITH ControllerStatus DO
		valid := TRUE;  info[0] := 48H;
		info[1] := 0;  info[2] := 0;
	    END (*WITH*);
	    RETURN;
	END (*IF*);

	(* Send a Sense command if needed.  Note that many operations	*)
	(* imply an automatic Sense, in which case it would be wrong to	*)
	(* send the explicit Sense command.  We make the distinction by	*)
	(* checking whether the controller is currently waiting for a	*)
	(* command.							*)

	IF IANDB (InByte(StatusPort),StatusMask) = ReadyToListen THEN
	    IF testing THEN
		WriteString (log, "WaitForCompletion: sending Sense command.");
		WriteLn (log);
	    END (*IF*);
	    IF NOT SendCommand(SenseInterruptStatus) THEN
		IF testing THEN
		    WriteString (log, "WaitForCompletion: Sense command not accepted.");
		    WriteLn (log);
		END (*IF*);
		ControllerStatus.valid := FALSE;
		RETURN;
	    END (*IF*);

	    (* A small delay is needed between writing a command and	*)
	    (* reading the status port.					*)

	    ShortDelay (pause1);

	END (*IF*);

	(* Each time around this loop, we pick up one byte of reply.	*)

	count := 0;
	LOOP

	    (* Read the controller status port to check whether the	*)
	    (* controller is willing to give us some information.	*)

	    IF IANDB (InByte(StatusPort),StatusMask) <> ReadyToSend THEN
		IF testing THEN
		    WriteString (log, "WaitForCompletion: controller not replying.");
		    WriteLn (log);
		    WriteCard (log, count);
		    WriteString (log, " bytes received so far.");
		    WriteLn (log);
		END (*IF*);
		ControllerStatus.valid := FALSE;
		RETURN;
	    END (*IF*);

	    (* OK, the controller is ready, read a byte.	*)

	    ControllerStatus.info[count] := InByte (DataPort);

	    (* A brief pause here, to let the controller get its breath.*)

	    ShortDelay (pause2);

	    (* Check the controller status port again, to see whether	*)
	    (* there is still more information to come.  If not, this	*)
	    (* operation is done and we can return.			*)

	    IF IANDB(InByte(StatusPort),BusyBit) = BYTE(0) THEN
		ControllerStatus.valid := TRUE;
		IF testing THEN
		    WriteString (log, "WaitForCompletion: data bytes are ");
		    FOR j := 0 TO count DO
			WriteChar (log, " ");
			WriteHexByte (log, ControllerStatus.info[j]);
		    END (*FOR*);
		    WriteLn (log);
		END (*IF*);
		RETURN;
	    END (*IF*);

	    (* Yes, more to come.  Go around the loop again, unless	*)
	    (* the controller is putting out more information than it	*)
	    (* should, in which case something is going wrong and the	*)
	    (* controller must be reset.				*)

	    IF count = 6 THEN
		ResetNeeded := TRUE;  ControllerStatus.valid := FALSE;
		RETURN;
	    END (*IF*);

	    INC (count);

	END (*LOOP*);

    END WaitForCompletion;

(************************************************************************)

PROCEDURE ResetController;

    (* This operation is performed when the software and controller	*)
    (* somehow got out of synchronism, so that we need to set the	*)
    (* controller back to a known state.				*)

    CONST TimingParameterSpecification = 3;

    VAR dummy: BOOLEAN;  drive: DriveNumber;

    BEGIN

	(* Pulse the reset bit low, and then let it go high again.	*)

	WITH SharedMotorData DO
	    Obtain (lock);
	    OutByte (MotorControlPort, IANDB(MotorStatus, 0FBH));
	    OutByte (MotorControlPort, MotorStatus);
	    Release (lock);
	END (*WITH*);
	FOR drive := 0 TO MAX(DriveNumber) DO
	    CalibrationNeeded[drive] := TRUE;
	END (*FOR*);
	ResetNeeded := FALSE;
	IF testing THEN
	    WriteString (log, "Reset performed, now waiting for completion.");
	    WriteLn (log);
	END (*IF*);

	(* Wait until the controller has done the operation, and then	*)
	(* check that the reset succeeded.				*)

	WaitForCompletion;
	IF NOT ControllerStatus.valid THEN
	    IF testing THEN
		WriteString (log, "Disk reset failed.");  WriteLn (log);
	    END (*IF*);
	    ResetNeeded := TRUE;

	ELSIF ControllerStatus.info[0] <> BYTE(0C0H) THEN
	    IF testing THEN
		WriteString (log, "Disk reset has failed.  SR0 = ");
		WriteHexByte (log, ControllerStatus.info[0]);  WriteLn (log);
	    END (*IF*);
	    ResetNeeded := TRUE;

	ELSE

	    (* Reset has succeeded.  Now send the controller a command	*)
	    (* which tells it the step rate, head unload time, and head	*)
	    (* load time.						*)

	    dummy := SendCommand (TimingParameterSpecification)
			AND SendCommand (0DFH) AND SendCommand (2);

	END (*IF*);

    END ResetController;

(************************************************************************)

PROCEDURE Recalibrate (drive: DriveNumber): ErrorCode;

    (* Puts the disk head back into a known state, by driving the head	*)
    (* to its outermost position.  This operation is needed whenever	*)
    (* the controller loses track of the head position, which can	*)
    (* happen now and then through cumulative positioning errors.	*)
    (* As usual, the returned value is an error code.			*)
    (* Remark: for an 80-track disk, recalibration can fail because the	*)
    (* hardware doesn't realise that a disk can have that many tracks,	*)
    (* and gives up before stepping the head across the whole 80	*)
    (* tracks.  Rather than check for this special case, which will not	*)
    (* arise very often, we allow the operation to fail.  On a retry,	*)
    (* the operation will succeed because by then the head has already	*)
    (* been moved most of the way to track 0.				*)

    CONST RecalibrateCode = 7;

    VAR success: BOOLEAN;

    BEGIN
	IF testing THEN
	    WriteString (log, "Recalibrating the drive.");  WriteLn (log);
	END (*IF*);

	success := SendCommand (RecalibrateCode) AND SendCommand (drive);
	IF ResetNeeded OR NOT success THEN
	    RETURN ControllerNotListening;
	END (*IF*);

	(* Check the status reply, to see whether the recalibration	*)
	(* has succeeded.  If not, a reset is needed.			*)

	WaitForCompletion;

	IF ControllerStatus.valid
		AND (ControllerStatus.info[0] = BYTE(SeekEnd + drive))
		AND (ControllerStatus.info[1] = BYTE(0))
	  THEN
	    CurrentCylinder[drive] := 0;  CalibrationNeeded[drive] := FALSE;
	    RETURN OK;
	ELSE
	    ResetNeeded := TRUE;
	    RETURN CalibrationFailure;
	END (*IF*);
    END Recalibrate;

(************************************************************************)

PROCEDURE Seek (drive: DriveNumber; cylinder: CARDINAL): ErrorCode;

    (* Drives the disk read/write head to the desired cylinder.  The	*)
    (* value returned is an error code (OK if no errors).		*)

    CONST SeekCode = 0FH;
	StepsPerCylinder = 1;

    VAR success: BOOLEAN;  result: ErrorCode;  position: BYTE;

    BEGIN

	(* Check for some special cases: illegal cylinder number,	*)
	(* recalibration needed, or position already OK.		*)

	IF cylinder >= DriveInfo[drive].NumberOfCylinders THEN
	    RETURN IllegalBlockNumber;
	END (*IF*);
	IF CalibrationNeeded[drive] THEN
	    result := Recalibrate (drive);
	    IF result <> OK THEN
		RETURN result;
	    END (*IF*);
	END (*IF*);
	IF cylinder = CurrentCylinder[drive] THEN
	    IF testing THEN
		WriteString (log, "Seek: already on desired cylinder.");
		WriteLn (log);
	    END (*IF*);
	    RETURN OK;
	END (*IF*);

	position := BYTE(cylinder*StepsPerCylinder);

	(* The command to the disk controller is a sequence of three	*)
	(* bytes, whose meaning is, I hope, obvious from the parameters	*)
	(* mentioned in the next statement.				*)
	(* NOTE: according to the documentation, the second byte of the	*)
	(* command should specify which head as well as which drive,	*)
	(* and the same for the SR0 value when we read back the		*)
	(* interrupt status.  From tests, it appears that we should not	*)
	(* specify the head, and that the "head" bit in SR0 is never	*)
	(* set after a seek.						*)

	success := SendCommand (SeekCode) AND SendCommand (drive)
			AND SendCommand (position);
	IF NOT success THEN
	    RETURN ControllerNotListening;
	END (*IF*);

	WaitForCompletion;
	IF NOT ControllerStatus.valid THEN
	    RETURN ControllerOutOfSync;
	ELSIF (ControllerStatus.info[0] <> BYTE(SeekEnd + drive))
			OR (ControllerStatus.info[1] <> position) THEN
	    IF testing THEN
		WriteString (log, "Seek error, needed reply ");
		WriteHexByte (log, SeekEnd+drive);
		WriteString(log, " ");
		WriteHexByte (log, position);
		WriteString (log, ", obtained reply ");
		WriteHexByte (log, ControllerStatus.info[0]);
		WriteString(log, " ");
		WriteHexByte (log, ControllerStatus.info[1]);
		WriteLn(log);
	    END (*IF*);
	    CalibrationNeeded[drive] := TRUE;
	    RETURN SeekFailure;
	ELSE
	    CurrentCylinder[drive] := cylinder;
	    RETURN OK;
	END (*IF*);

    END Seek;

(************************************************************************)

PROCEDURE DumpIDinformation (drive: DriveNumber;  head: HeadNumber);

    (* Reads current sector information from the disk, which might help	*)
    (* us determine more about the cause of an error.			*)

    (* Remark: my tests so far suggest that we don't get any error	*)
    (* information this way, but we do seem to get back information	*)
    (* about the current track and sector.				*)

    CONST ReadIDCommand = 4AH;

    VAR success: BOOLEAN;

    BEGIN

	success := SendCommand(ReadIDCommand)
			AND SendCommand(BYTE(4*head + drive));

	IF NOT success THEN
	    IF testing THEN
		WriteString (log, "Can't read ID information.");
		WriteLn (log);
	    END (*IF*);
	END (*IF*);

	WaitForCompletion;

	(* The information which we want is dumped to the screen by	*)
	(* procedure WaitForCompletion.					*)

    END DumpIDinformation;

(************************************************************************)

PROCEDURE DoTheOperation (operation: CARDINAL;  drive: DriveNumber;
				head: HeadNumber;
				cylinder, sector: CARDINAL): ErrorCode;

    (* Performs a disk operation (read=1, write=2) assuming that the	*)
    (* motor is running, the read/write head has been driven to the	*)
    (* correct cylinder, and the DMA controller is ready to go.		*)
    (* Notice that there is no specification of how much data to	*)
    (* transfer.  That detail is handled by the DMA controller, which	*)
    (* has already been set up before this procedure was called.	*)

    CONST ReadCommand = 0E6H;  WriteCommand = 0C5H;

    VAR opcode: BYTE;  success: BOOLEAN;
	SectorsTransferred: CARDINAL;

    BEGIN
	OutByte (DataRatePort, DriveInfo[drive].DataRate);
	CASE operation OF
	    1:	opcode := ReadCommand;
	  |
	    2:	opcode := WriteCommand;
	END (*CASE*);

	(* The operation is performed by sending the disk controller a	*)
	(* command string starting with "opcode".  The significance of	*)
	(* the remaining bytes will, I believe, be obvious from the	*)
	(* statement below.						*)

	success := SendCommand(opcode) AND SendCommand(4*head + drive)
			AND SendCommand(BYTE(cylinder)) AND SendCommand(head)
			AND SendCommand(BYTE(sector))
			AND SendCommand(2)	(* sector size code *)
			AND SendCommand(BYTE(DriveInfo[drive].SectorsPerTrack))
			AND SendCommand(DriveInfo[drive].SectorGap)
			AND SendCommand(DataLengthCode);

	IF NOT success THEN
	    RETURN ControllerNotListening;
	END (*IF*);

	WaitForCompletion;
	WITH ControllerStatus DO

	    IF NOT valid THEN
		RETURN ControllerOutOfSync;
	    END(*IF*);

	    IF ORD(IANDB(info[1], 2)) <> 0 THEN
		RETURN WriteProtected;
	    END (*IF*);

	    IF (ORD(IANDB(info[1], 5)) <> 0)
			OR (ORD(IANDB(info[2], 12H)) <> 0) THEN
		CalibrationNeeded[drive] := TRUE;
		RETURN SectorNotFound;
	    END (*IF*);

	    IF (ORD(IANDB(info[1], 20H)) <> 0)
			OR (ORD(IANDB(info[2], 21H)) <> 0) THEN
		RETURN BadData;
	    END (*IF*);

	    IF (info[1] <> BYTE(0)) OR (info[2] <> BYTE(0)) THEN
		RETURN UndiagnosedFailure;
	    END (*IF*);

	    (* In the following test, we mask out the "head" bit of	*)
	    (* status register 0, because it does confusing things at	*)
	    (* the end of a multi-sector read.				*)

	    IF IANDB(info[0], 0FBH) <> BYTE(drive) THEN
		RETURN DriveNotReady;
	    END (*IF*);

	    (* The next little calculation works out how much data was	*)
	    (* actually transferred.  Entries 3,4,5 of the "info" field	*)
	    (* give the ending cylinder, head, and sector respectively.	*)
	    (* Remark: we never use this information except when testing*)

	    SectorsTransferred := DriveInfo[drive].SectorsPerTrack*
				((ORD(info[3])-cylinder)*NumberOfHeads
						+CARDINAL(info[4])-ORD(head))
			+ ORD(info[5]) - sector;
	    IF testing THEN
		WriteCard (log, SectorsTransferred);
		WriteString (log, " sectors were transferred.");
		WriteLn (log);
	    END (*IF*);

	    RETURN OK;

	END (*WITH*);

    END DoTheOperation;

(************************************************************************)
(*			THE DISK INTERRUPT TASK				*)
(************************************************************************)

PROCEDURE DiskInterruptTask;

    (* Responds to the disk interrupts.  The interrupt could occur for	*)
    (* a variety of reasons, depending on what operation was in		*)
    (* progress.  Rather than sort that out here, we simply perform a	*)
    (* semaphore Signal to wake up whichever task was waiting for the	*)
    (* interrupt.							*)

    VAR status: BYTE;

    BEGIN
	LOOP (*FOREVER*)
	    WaitForInterrupt;
	    status := InByte(StatusPort);

	    (* We occasionally get an interrupt with 0 in the status	*)
	    (* port, and I can find nothing in the data sheets to	*)
	    (* explain this - it is perhaps a power-on interrupt.  In	*)
	    (* any case, it is certainly not a "command done" signal,	*)
	    (* therefore we can afford to ignore it.			*)

	    IF status <> BYTE(0) THEN
		Signal (OperationDone);
	    END (*IF*);
	END (*LOOP*);
    END DiskInterruptTask;

(************************************************************************)
(*			DATA TRANSFER PROCEDURES			*)
(************************************************************************)

PROCEDURE DiskOperation (VAR (*INOUT*) details: RequestBlock);

    (* Performs a disk transfer as specified in the request block.	*)

    CONST DMAchannel = 2;  DefaultSectorSize = 512;

    VAR command: CARDINAL;  drive: DriveNumber;  BlockNumber: CARDINAL;
	head: HeadNumber;  cylinder, sector: CARDINAL;
	result: ErrorCode;

    BEGIN
	WITH details DO
	    CASE operation OF
		verify:		command := 0;
		|
		read:		command := 1;
		|
		write:		command := 2;
		|
		physicalread:	ByteCount := DefaultSectorSize;
				command := 1;
		|
		physicalwrite:	ByteCount := DefaultSectorSize;
				command := 2;
		|
		ELSE
			Status := FeatureNotImplemented;
			RETURN;
	    END (*CASE*);
	    drive := SHORTCARD(unit);
	END (*WITH*);

	Signal (MotorStartRequest [drive]);

	(* Convert the block number to the head/cylinder/sector form	*)
	(* which the hardware expects.  The relationship is		*)
	(*  BlockNumber = SectorsPerTrack*(NumberOfHeads*cylinder+head)	*)
	(*				+ sector - 1			*)
	(* Note that sector number starts from 1, while the numbering	*)
	(* of blocks, heads, and cylinders all start from 0.		*)

	BlockNumber := CARDINAL (details.BlockNumber);
	WITH DriveInfo[drive] DO
	    sector := BlockNumber MOD SectorsPerTrack + 1;
	    cylinder := BlockNumber DIV SectorsPerTrack;
	END (*WITH*);
	head := SHORTCARD(cylinder MOD CARDINAL(NumberOfHeads));
	cylinder := cylinder DIV NumberOfHeads;

	Wait (MotorUpToSpeed [drive]);
	result := Seek (drive, cylinder);
	IF testing THEN
	    WriteString (log, "Return from procedure Seek");
	    WriteLn (log);
	END (*IF*);
	IF result = OK THEN
	    IF testing THEN
		WriteString (log, "Successful completion of seek operation");
		WriteLn (log);
	    END (*IF*);

	    (* The seek has apparently been successful.  Set up the	*)
	    (* DMA controller.						*)

	    IF testing THEN
		WriteString (log, "Setting up DMA, buffer address is ");
		WriteAddress (log, details.BufferAddress);
		WriteLn (log);
	    END (*IF*);

	    WITH details DO
		IF NOT CheckDMAAddress (BufferAddress, ByteCount) THEN
		    Signal (MotorMayBeStopped [drive] );
		    Status := BadDMAAddress;
		    RETURN;
		END (*IF*);
		LoadDMAparameters (DMAchannel, command, BufferAddress,
							ByteCount);
	    END (*WITH*);

	    (* For a write operation, wait for the head settling time.	*)
	    (* For a read we do not bother, since there is a chance	*)
	    (* that the head will have settled anyway by the time the	*)
	    (* desired sector comes around, and the rare problems will	*)
	    (* be solved when we re-try the operation.			*)

	    IF command = 2 THEN
		Sleep (CARDINAL(DriveInfo[drive].HeadSettlingTime));
	    END (*IF*);

	    (* All OK so far, perform the actual transfer.	*)

	    result := DoTheOperation (command, drive, head, cylinder, sector);
	    IF result <> OK THEN
		DumpIDinformation (drive, head);
	    END (*IF*);
	ELSE
	    IF testing THEN
		WriteString (log, "Seek failed");
		WriteLn (log);
	    END (*IF*);
	END (*IF*);

	Signal (MotorMayBeStopped [drive] );
	IF ResetNeeded THEN
	    ResetController;
	END (*IF*);
	details.Status := result;

    END DiskOperation;

(************************************************************************)
(*			INTERFACE TO THE FILE SYSTEM			*)
(************************************************************************)

PROCEDURE DiskRequestHandler;

    (* This procedure runs as an independent task.  Each time around	*)
    (* its main loop, it picks up one enqueued I/O request and executes	*)
    (* the request.  The requests are placed on the queue by calls to	*)
    (* module Devices.  On completion of an operation, we inform the	*)
    (* caller by performing a Signal on a user-specified semaphore.	*)

    CONST MaxNumberOfRetries = 3;  StopCode = 0FFFFH;

    VAR reply: ErrorCode;  retries: CARDINAL;
	RequestPointer: RequestBlockPointer;
	string: ARRAY [0..31] OF CHAR;

    BEGIN
	LOOP
	    RequestPointer := AcceptRequest (floppy);
	    WITH RequestPointer^ DO
		IF operation = shutdown THEN
		    Signal (DoneSemaphorePointer^);
		    EXIT (*LOOP*);
		END (*IF*);
	    END (*WITH*);
	    retries := 0;
	    REPEAT
		DiskOperation (RequestPointer^);
		reply := RequestPointer^.Status;
		IF testing AND (reply <> OK) THEN
			WriteString (log, "DISK ERROR: error code ");
			TranslateErrorCode (reply, string);
			WriteString (log, string);  WriteLn (log);
		END (*IF*);
		INC (retries);
	    UNTIL (reply = OK) OR (retries>MaxNumberOfRetries);
	    Signal (RequestPointer^.DoneSemaphorePointer^);
	END (*LOOP*);
    END DiskRequestHandler;

(************************************************************************)
(*			    INITIALISATION				*)
(************************************************************************)

PROCEDURE InitialiseDriveParameters (drive: DriveNumber;  type: DriveType);

    (* Sets up the information we need about the given drive.		*)
    (* Notes:								*)
    (*    1.	The Sector Gap values shown are for read/write/verify.	*)
    (*		When formatting a disk, use 054H for a 1.2MB disk, and	*)
    (*		050H for a 360KB disk.					*)
    (*    2.	I have not yet allowed for the possibility of a 360K	*)
    (*		disk in a 1.2M drive.  In that case, use 023H for the	*)
    (*		sector gap, and a data rate code of 1.			*)

    BEGIN
	WITH DriveInfo[drive] DO
	    drivetype := type;
	    CASE type OF
	 	    0:		(* no drive present *)
			SectorsPerTrack := 9;
			NumberOfCylinders := 0;
			SectorGap := 02AH;  DataRate := 2;
			HeadSettlingTime := 20;
		|
		    1:		(* 360KB drive *)
			SectorsPerTrack := 9;
			NumberOfCylinders := 40;
			SectorGap := 02AH;  DataRate := 2;
			HeadSettlingTime := 20;
		|
		    2:		(* 1.2MB drive *)
			SectorsPerTrack := 15;
			NumberOfCylinders := 80;
			SectorGap := 01BH;  DataRate := 0;
			HeadSettlingTime := 15;
		|
		    4:		(* 1.44MB drive - still have to fix details *)
				(* Note: for a disk formatted at low	*)
				(* density, the parameters for case 1	*)
				(* seem to work.			*)
			SectorsPerTrack := 18;
			NumberOfCylinders := 80;
			SectorGap := 01BH;  DataRate := 0;
			HeadSettlingTime := 15;
		|
		    ELSE	(* unknown type *)
			SectorsPerTrack := 9;
			NumberOfCylinders := 0;
			SectorGap := 02AH;  DataRate := 2;
			HeadSettlingTime := 20;
	    END (*CASE*);
	END (*WITH*);
    END InitialiseDriveParameters;

(************************************************************************)

PROCEDURE SetupDriveInformation;

    (* Sets up the information we need about each drive.  In this	*)
    (* version, we get information from CMOS about the type of the	*)
    (* first two drives, but we can't say much about the other two.	*)

    VAR DriveTypeCode: BYTE;

    BEGIN
	DriveTypeCode := ReadCMOS (10H);

	IF testing THEN
	    WriteString (log, "DriveTypeCode from CMOS is ");
	    WriteHexByte (log, DriveTypeCode);
	    WriteString (log, " hexadecimal.");
	    WriteLn (log);
	END (*IF*);

	InitialiseDriveParameters (0, RS(CARDINAL(DriveTypeCode), 4));
	InitialiseDriveParameters (1, DriveType(IANDB(DriveTypeCode, 0FH)));

	(* The following two calls should be included only if four	*)
	(* drives are to be supported.					*)
(*
	InitialiseDriveParameters (2, UnknownDriveType);
	InitialiseDriveParameters (3, UnknownDriveType);
*)

    END SetupDriveInformation;

(************************************************************************)
(*				SHUTDOWN				*)
(************************************************************************)

PROCEDURE ShutDown;

    (* Brings the tasks in this module to an orderly halt.  This is to	*)
    (* guard against the possibly catastrophic consequences of program	*)
    (* termination while a disk operation is still in progress.		*)

    VAR Sync: Semaphore;  RequestRecord: RequestBlock;
	DontPanic: Window;

    BEGIN
	OpenSimpleWindow (DontPanic, 11, 13, 33, 45);
	WriteString (DontPanic, "DON'T PANIC");
	IF testing THEN
	    WriteString (log, "Starting the shutdown operation.");
	    WriteLn (log);
	END (*IF*);

	(* Send a shutdown request to the task which deals with the	*)
	(* queue of floppy disk operation requests.			*)

	CreateSemaphore (Sync, 0);
	WITH RequestRecord DO
	    operation := shutdown;
	    device := floppy;
	    unit := 0;
	    DoneSemaphorePointer := ADR (Sync);
	END (*WITH*);
	IOrequest (RequestRecord);
	Wait (Sync);
	DestroySemaphore (Sync);

	IF testing THEN
	    WriteString (log, "About to close down the motor control tasks.");
	    WriteLn (log);
	END (*IF*);

	(* Close down the motor control tasks, and destroy the		*)
	(* semaphores belonging to them.				*)

	ShutDownMotors;

	IF testing THEN
	    WriteString (log, "Motor control tasks are now stopped.");
	    WriteLn (log);
	END (*IF*);

	(* Dispose of miscellaneous global variables.	*)

	DestroySemaphore (OperationDone);
	CloseWindow (DontPanic);

    END ShutDown;

(************************************************************************)
(*			MODULE INITIALISATION				*)
(************************************************************************)

BEGIN
    IF testing THEN
	CreateMaintenancePage (Mpage);
	OpenWindow (log, black, green, 0,23, 0,60, simpleframe, doubledivider);
	Associate (log, Mpage);
	WriteString (log,"         Diagnostic output from floppy disk driver");
	ChangeScrollingRegion (log, 3, 22);
    END (*IF*);

    SetupDriveInformation;

    (* Create the motor control tasks, and the information they need.	*)

    CreateMotorControlTasks;

    CreateSemaphore (OperationDone, 0);
    CreateInterruptTask (DiskInterrupt, DiskInterruptTask, "Floppy int");
    IF testing THEN
	WriteString (log, "Have now installed the interrupt task.");
	WriteLn (log);
	WriteString (log, "Interrupt masks:   master ");
	WriteHexByte (log, InByte(21H));
	WriteString (log, "   slave ");
	WriteHexByte (log, InByte (0A1H));
	WriteLn (log);
    END (*IF*);
    ResetController;
    floppy := InstallDeviceDriver (1);
    DeviceName (floppy, 0, "A", 0, "");
    DeviceName (floppy, 1, "B", 0, "");
    CreateTask (DiskRequestHandler, 14, "Floppy request");
    SetTerminationProcedure (ShutDown);

END Floppy.