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.