TRS-80 DOS - LDOS 5.3.1 for the Model I - SYS12/SYS Disassembled

Page Customization

Summary:

LDOS 5.3.1 SYS12/SYS Disassembly - Directory Services @DODIR and @RAMDIR (Model I)

SYS12/SYS is the last of the twelve LDOS 5.3.1 system overlays. It contains the two "mini" directory services: @DODIR, which either displays an abbreviated directory on the video screen or copies raw directory records into a caller's buffer, and @RAMDIR, which builds a compact table of fixed-length directory records in memory for a program to browse. The MiniDOS filter uses both, and any application that offers its own directory display can call them. Because nothing in the resident kernel depends on it, SYS12/SYS can be purged from a working system diskette when the two mini directory routines are not needed.

The overlay is loaded to 4E00H by the resident overlay loader at 4BF5H and occupies 4E00H through 50FDH, 766 bytes, in one contiguous piece. It also deposits a two-byte load block at 4BC9H in the resident padding area, and the value it deposits, 02FEH, is exactly that 766-byte length. Entry is at 4E00H with the RST 28H request code still in Register A; the overlay masks it with 70H and services two sub-functions, 10H for @RAMDIR (request code 9EH) and 20H for @DODIR (request code AEH, the code loaded by the resident @DODIR vector at 4463H). Any other value returns immediately. Every path RETurns to the RST 28H dispatcher in SYS0/SYS with Register A holding a standard LDOS error number and the Z flag set on success.

Neither service reads the directory a record at a time from disk. Both walk the Hash Index Table, the second sector of the directory cylinder, which holds one non-zero hash byte for every directory record in use and a zero for every free slot. SYS12/SYS reads that whole sector into 5100H-51FFH, a page above its own image that no part of SYS12/SYS occupies, and then steps a record number through it in slot-major order: the low five bits of a record number are the directory sector and the high three bits are the slot within that sector, so the scan adds 20H each time and increments the sector part when that addition carries. Only slots whose hash byte is non-zero are looked up, and a self-modifying compare remembers which directory sector is already sitting in the resident directory buffer at 4200H so that eight consecutive records cost one disk read.

The free-space arithmetic is shared by three of the five entry paths. The Granule Allocation Table, sector 0 of the directory cylinder, is read into the same 5100H page; the count of cylinders to examine is taken from the configuration byte at GAT offset CCH plus 35, and free granules are counted by rotating each GAT byte right with a 1 shifted in from the left, which both tests bit 0 and guarantees the loop ends when the byte has become 0FFH. Granules are converted to sectors through the drive's geometry bytes, and sectors are converted to kilobytes by adding 2 and dividing by 4, since four 256-byte sectors make one K.

Before touching the directory at all, every entry runs the drive-ready test at 50AEH. That routine seeks the drive to the cylinder its control block says the head is already on, then samples the WD1771 status register three times, waiting for the index pulse to go away, come back and go away again. Seeing a full index-pulse cycle inside twenty clock ticks proves a diskette is actually turning; failing to see one returns error 8, device not available.

Variable and Self-Modifying Code List

Address RangePurpose
4BC9H-4BCAH
2 bytes
Overlay load block deposited in the resident padding area of SYS0/SYS. The word value 02FEH is the length of the SYS12/SYS image, 766 bytes, which is 50FDH minus 4E00H plus one.
4200H-42FFH
256 bytes
SBUFF$, the resident shared directory/sector buffer in SYS0/SYS. DIRRD at 4B10H reads the directory sector holding the wanted record into it, and SYS12/SYS addresses the record as 4200H plus the slot offset.
4480H-4482H
3 bytes
CFCB$, the resident command file control block buffer, borrowed by @DODIR to hold the three-character extension mask copied out of the caller's buffer. The character 24H (a dollar sign) in any position matches anything.
4483H-4492H
16 bytes
CFCB$ plus three, borrowed as the scratch area in which the display path builds each fifteen-character file specification before handing it to @DSPLY.
5100H-51FFH
256 bytes
Scratch page immediately above the SYS12/SYS image, used for the Granule Allocation Table sector on the free-space paths and for the Hash Index Table sector on both directory scans. 51CCH is the low byte of the GAT configuration word (the cylinder count less 35) and 51CDH its high byte, which FORMAT/CMD builds at 6318H to 6335H as the eight inch flag in bit 5, the double density flag in bit 6, the granules per track in bits 2 to 0, bits 7 and 3 always set, and bit 4 set only when the SYSTEM parameter has re-formatted cylinder 0 in single density to make a dual density boot disk. Bit 3 additionally marks a disk in the LDOS 5.3 x.3 extended-dating format and bit 7 exempts it from the DATECONV/CMD system-disk check. 51D0H-51D7H hold the disk name and 51D8H-51DFH the disk date.
4ECAH
1 byte
Self-modifying operand of the CP 0FFH at 4EC9H in the @RAMDIR scan. It caches the directory sector number currently held in SBUFF$ so that the next record in the same sector skips the disk read. Loaded as 0FFH so the first record always reads.
4F5AH
1 byte
Self-modifying operand of the CP 0FFH at 4F59H, the identical directory-sector cache for the @DODIR scan. Loaded as 0FFH.
4F6CH
1 byte
Self-modifying operand of the LD A,00H at 4F6BH, holding the @DODIR function code 0 to 4 taken from Register B at entry. Bit 0 selects buffer output over screen display and bit 1 selects extension filtering.
4FA0H
1 byte
Self-modifying operand of the LD A,00H at 4F9FH, the countdown of file specifications remaining on the current display line. Set to 4 when a display request starts and reloaded with 4 each time a line is completed.
4FB1H
1 byte
Self-modifying operand of the LD A,00H at 4FB0H, the countdown of display lines remaining on the current screen. Set to 0FH when a display request starts and reloaded with 0FH after the operator acknowledges a full screen.
5017H
1 byte
Self-modifying operand of the LD A,00H at 5016H, the ASCII drive digit planted into every file specification the builder produces. Written at 50B1H as the drive number ORed with 30H.

Major Routine List

AddressRoutine
4E00HRequest Code Dispatch
Masks the RST 28H request code with 70H. 20H is @DODIR and jumps to 4EE6H, 10H is @RAMDIR and falls through to 4E0AH, and anything else returns at once.
4E0AH@RAMDIR Entry
Register B is the drive number, Register C selects the request, and Register Pair HL is the caller's buffer. Rejects a drive number above 7 with error 32, swaps the two registers so Register C is the drive, and runs the drive-ready test. Register C of 0FFH asks for free space, 00H asks for the whole directory, anything else asks for one directory record.
4E1DHFree-Space Reply
Calls the capacity calculator at 5025H, then rewrites the caller's four-byte reply as used kilobytes followed by free kilobytes.
4E3DHSingle Directory Record Request
Computes how many directory sectors the drive geometry allows, rejects a record whose sector part is out of range with error 16, reads the record and rejects anything that is not an in-use, primary, visible, non-system entry with error 25.
4E77H@RAMDIR Record Builder
Turns the directory record addressed by Register Pair HL into the 22-byte @RAMDIR record at the buffer addressed by Register Pair DE, and writes a 2BH end marker in the byte after it which the next record overwrites.
4EA6HFull @RAMDIR Scan
Reads the Hash Index Table, walks every record slot in it, and appends a 22-byte record for each qualifying file.
4EE6H@DODIR Entry
Register B is the function code 0 to 4, Register C is the drive number and Register Pair HL is the caller's buffer, whose first three bytes are the extension mask for functions 2 and 3. Function 4 is the disk-information request; a function above 4 is rejected with error 29.
4F1AHDisk Information Reply
Builds the twenty-byte reply: eight bytes of disk name, eight bytes of disk date, the capacity in kilobytes and the free space in kilobytes.
4F2CH@DODIR Record Iterator
Steps the record number to the next Hash Index Table slot and, when the table is exhausted, either writes an 0FFH terminator into the caller's buffer or sends a final carriage return to the screen.
4F4EH@DODIR Hash Index Table Scan
Skips free slots, reads the directory sector when it is not already buffered, and calls the extension filter and then the per-record handler.
4F75HPer-Record Handler
Function code bit 0 set copies eighteen raw directory bytes to the buffer; bit 0 clear builds the file specification and displays it four to a line, fifteen lines to a screen.
4FC4HExtension Filter
Compares the three-character extension of the directory record against the mask in CFCB$, upper-casing the mask characters and treating a dollar sign as a wildcard. Returns NZ to reject the record.
4FE2HFile Specification Builder
Builds a fifteen-byte blank-padded NAME/EXT:d string plus an 03H terminator from a directory record into the buffer addressed by Register Pair DE.
5025HCapacity and Free-Space Calculator
Reads the Granule Allocation Table, works out the total sectors on the diskette from the drive geometry and counts the free granules, and stores the total kilobytes and the free kilobytes as two words at the caller's buffer.
5075HKilobyte Conversion and Store
Multiplies a granule or cylinder count by a sectors-per-unit figure, rounds and divides the sector total by four to give kilobytes, and stores the result as a word. Entered both as a subroutine and by falling through from 5073H.
508BHRead the Granule Allocation Table
Reads sector 0 of the drive's directory cylinder into 5100H, leaving error 20 in Register A for a failure.
509CHRead the Hash Index Table
Reads sector 1 of the drive's directory cylinder into 5100H, leaving error 22 in Register A for a failure.
50AEHDrive Ready Test
Plants the drive digit in the file specification builder, verifies that the unit has a real drive control block, seeks it and waits for a complete WD1771 index-pulse cycle, returning error 8 if none arrives within twenty clock ticks.
50ECHIndex Pulse Sample
Compares the clock tick counter against the deadline, and otherwise re-selects the drive and returns the index bit of the WD1771 status register. On the deadline it discards its caller's return address and forces error 8.

Cross-Reference Notes

SYS12/SYS is called from outside by way of two RST 28H request codes. Code AEH is loaded by the resident @DODIR vector at 4463H in SYS0/SYS, so any program that calls @DODIR reaches this overlay. Code 9EH has no resident vector in the LDOS 5.3.1 Model I supervisor-call table and is issued directly by the caller, which is how @RAMDIR is reached. The MiniDOS filter uses both.

SYS12/SYS calls into the resident core of SYS0/SYS for every service it needs: the file-operation prologue at 49F1H, the drive control table lookups GETDCT at 478FH and DCTBYT at 479CH, the disk primitives SEEK at 475EH and RSELCT at 4759H, the directory helpers DIRCYL at 4B65H, RDSSEC at 4B45H and DIRRD at 4B10H, the multiply helpers MULTEA at 4B6CH and @MULT at 44C1H, and @DSPLY at 4467H. It reads the clock tick counter TIMER$ at 4040H. From the Model I ROM it uses the character display routine at 0033H, the wait-for-key routine at 0049H and the clear-screen routine at 01C9H. It issues no RST 28H requests of its own and never touches the WD1771 registers directly; all controller traffic goes through the SYS0/SYS primitives.

Disassembly:

4BC9H - Overlay Load Block

Every LDOS overlay deposits a two-byte record into the padding area at the end of the resident RST 28H dispatcher in SYS0/SYS. The word records the length of the overlay image just loaded, which for SYS12/SYS is 02FEH, 766 bytes, matching the span 4E00H through 50FDH. The disassembler renders the two bytes as an instruction; they are data and are never executed.

4BC9
DEFW 02FEH FE 02
Overlay length record. The value 02FEH is 766, the number of bytes SYS12/SYS occupies from its load address 4E00H through its last byte at 50FDH. This word is data deposited by the loader, not code.

4E00H - Request Code Dispatch

The overlay is entered here by the self-modified CALL inside the resident RST 28H dispatcher, with Register A still holding the request code the caller placed there. Masking with 70H reduces the code to a sub-function selector: 20H is @DODIR (request code AEH) and 10H is @RAMDIR (request code 9EH).

4E00
AND 70H E6 70
Register A holds the RST 28H request code the caller loaded before the restart, either 9EH for @RAMDIR or 0AEH for @DODIR. Mask off everything except bits 6 to 4, leaving 10H for 9EH and 20H for 0AEH.
4E02
CP 20H FE 20
Compare Register A, now the masked sub-function selector, against 20H, the @DODIR sub-function. If Register A equals 20H the Z FLAG is set; otherwise the NZ FLAG is set.
4E04
If the Z FLAG (Zero) has been set, the request was 0AEH, so JUMP to 4EE6H to run the @DODIR directory display and directory-copy service.
4E07
CP 10H FE 10
Compare Register A, the masked sub-function selector, against 10H, the @RAMDIR sub-function. If Register A equals 10H the Z FLAG is set; otherwise the NZ FLAG is set.
4E09
RET NZ C0
If the NZ FLAG (Not Zero) has been set, the masked code is neither 20H nor 10H, so SYS12/SYS has nothing to do with it. Return to the RST 28H dispatcher in SYS0/SYS. Register A still holds the masked code, which is non-zero, so the caller sees a non-zero status.

4E0AH - @RAMDIR Entry and Parameter Validation

The @RAMDIR service is entered with Register B holding the drive number 0 to 7, Register C selecting which of the three replies is wanted, and Register Pair HL addressing the caller's buffer. Register C of 0FFH asks only for the disk's used and free space, 00H asks for a table covering every file on the disk, and any other value asks for one single directory record.

4E0A
LD A,07H 3E 07
Load Register A with 7, the highest drive number LDOS supports, ready to range-check the caller's drive number.
4E0C
CP B B8
Compare Register A, holding the limit 7, against Register B, holding the caller's drive number. If Register B is greater than 7 the CARRY FLAG is set; if Register B is 7 or less the NO CARRY FLAG is set.
4E0D
LD A,20H 3E 20
Load Register A with 20H, LDOS error number 32, "illegal drive number", ready to be returned. This load does not disturb the flags set by the comparison above.
4E0F
RET C D8
If the CARRY FLAG has been set, the drive number in Register B was above 7, so return to the RST 28H dispatcher with Register A holding error 32 and the NZ condition that marks a failure.
4E10
GOSUB to the resident file-operation prologue at 49F1H in SYS0/SYS. That entry point asserts the "already open" state, saves the caller's registers, records the caller's return address in 430CH and the file control block pointer in 430AH, and arranges a register-restoring return, so the body of the service runs with Register Pair IX addressing the caller's file control block.
4E13
LD A,B 78
Copy Register B, holding the validated drive number, into Register A so that the two parameter registers can be exchanged.
4E14
LD B,C 41
Copy Register C, holding the caller's request selector (0FFH for free space, 00H for the whole directory, otherwise a directory record number less one), into Register B.
4E15
LD C,A 4F
Copy Register A, holding the drive number, into Register C. The two registers are now in the order every SYS0/SYS directory routine expects: Register C is the drive number and Register B is the record selector.
4E16
GOSUB to the drive-ready test at 50AEH, which plants the ASCII drive digit taken from Register C into the file specification builder, checks that a real drive control block exists for that unit, seeks the drive and waits for a complete index-pulse cycle from the WD1771. It returns Z with Register A zero when the drive is turning, or NZ with an error number.
4E19
RET NZ C0
If the NZ FLAG (Not Zero) has been set, the drive named in Register C is not ready, so return to the RST 28H dispatcher with the error number the ready test left in Register A, either 08H for device not available or the difference the missing drive control block produced.
4E1A
INC B 04
INCrement Register B, holding the caller's request selector, by 1. A selector of 0FFH becomes 00H and sets the Z FLAG, which is how the free-space request is recognised.
4E1B
If the NZ FLAG (Not Zero) has been set, the selector in Register B was not 0FFH, so JUMP to 4E3AH to sort out the remaining two cases. Falling through means the caller asked for the free-space reply.

4E1DH - Free-Space Reply

Reached when the caller passed 0FFH in Register C. The calculator at 5025H writes the disk's total capacity in kilobytes into the first word of the caller's buffer and the free space in kilobytes into the second. This section reads both words straight back out, converts the capacity into the space in use by subtracting the free space, and rewrites the buffer as used kilobytes followed by free kilobytes.

4E1D
PUSH HL E5
Save Register Pair HL, which addresses the caller's four-byte reply buffer, on the stack. The calculator below advances Register Pair HL as it stores, so the original address must be kept.
4E1E
GOSUB to the capacity and free-space calculator at 5025H. It reads the Granule Allocation Table of the drive in Register C, computes the total kilobytes on the diskette into the word at the buffer addressed by Register Pair HL and the free kilobytes into the word after it, and returns with Register Pair HL addressing the last byte it stored and Register Pair DE holding the free kilobytes.
4E21
If the NZ FLAG (Not Zero) has been set, the Granule Allocation Table could not be read and Register A holds error 20. JUMP to 4E38H to discard the saved buffer address and return that error.
4E23
LD B,(HL) 46
Register Pair HL addresses the fourth byte of the reply, the high byte of the free kilobytes. Load Register B with it.
4E24
DEC HL 2B
DECrement Register Pair HL by 1 so that it addresses the third byte of the reply, the low byte of the free kilobytes.
4E25
LD C,(HL) 4E
Load Register C with the low byte of the free kilobytes. Register Pair BC now holds the free space in kilobytes as a sixteen-bit value.
4E26
DEC HL 2B
DECrement Register Pair HL by 1 so that it addresses the second byte of the reply, the high byte of the total capacity in kilobytes.
4E27
LD A,(HL) 7E
Load Register A with the high byte of the total capacity in kilobytes, holding it while Register Pair HL is moved back one more byte.
4E28
DEC HL 2B
DECrement Register Pair HL by 1 so that it addresses the first byte of the reply, the low byte of the total capacity in kilobytes.
4E29
LD L,(HL) 6E
Load Register L with the low byte of the total capacity in kilobytes, overwriting the buffer pointer's low half now that the pointer has been fully consumed.
4E2A
LD H,A 67
Load Register H with the high byte of the total capacity held in Register A. Register Pair HL now holds the disk's total capacity in kilobytes.
4E2B
SBC HL,DE ED 52
SUBtract Register Pair DE, which the calculator left holding the free kilobytes, from Register Pair HL, holding the total kilobytes. The calculator ended with an XOR A so the carry is clear and no borrow is introduced. Register Pair HL now holds the kilobytes in use.
4E2D
EX DE,HL EB
Exchange Register Pair DE and Register Pair HL, so that Register Pair DE holds the kilobytes in use ready to be stored and Register Pair HL is free to take the buffer address back.
4E2E
POP HL E1
Restore Register Pair HL from the stack, giving back the address of the first byte of the caller's four-byte reply buffer saved at 4E1DH.
4E2F
LD (HL),E 73
Store Register E, the low byte of the kilobytes in use, into the first byte of the caller's reply buffer, overwriting the total capacity the calculator left there.
4E30
INC HL 23
INCrement Register Pair HL by 1 to address the second byte of the caller's reply buffer.
4E31
LD (HL),D 72
Store Register D, the high byte of the kilobytes in use, into the second byte of the caller's reply buffer.
4E32
INC HL 23
INCrement Register Pair HL by 1 to address the third byte of the caller's reply buffer.
4E33
LD (HL),C 71
Store Register C, the low byte of the free kilobytes recovered at 4E25H, into the third byte of the caller's reply buffer.
4E34
INC HL 23
INCrement Register Pair HL by 1 to address the fourth byte of the caller's reply buffer.
4E35
LD (HL),B 70
Store Register B, the high byte of the free kilobytes recovered at 4E23H, into the fourth byte of the caller's reply buffer. The reply is now used kilobytes followed by free kilobytes.
4E36
XOR A AF
Set Register A to ZERO and clear all flags, which sets the Z FLAG and marks the request as successful with no error number.
4E37
RET C9
Return to the RST 28H dispatcher in SYS0/SYS with Register A zero and the Z FLAG set, and with the caller's buffer holding the used and free kilobytes.
4E38
POP HL E1
Error Path
Reached when the Granule Allocation Table read failed. Discard the buffer address saved at 4E1DH by popping it back into Register Pair HL, balancing the stack.
4E39
RET C9
Return to the RST 28H dispatcher with the error number the calculator left in Register A, 14H for LDOS error 20, "GAT read error", and the NZ condition that marks a failure.

4E3AH - Single Record Request and Directory Range Check

Reached when the caller's selector in Register B was not 0FFH. A selector of zero asks for the whole directory and is sent to 4EA6H; anything else is a single-record request, and the code adds one to it to form the directory record number. Before the record can be read, the number of directory sectors the drive's geometry provides is computed from the drive control table so the record number's sector part can be range checked. The formula is the standard LDOS one: sectors per track multiplied by heads, doubled for a double-sided drive, less the two sectors that hold the Granule Allocation Table and the Hash Index Table.

4E3A
DEC B 05
DECrement Register B by 1, undoing the increment at 4E1AH so that Register B again holds the caller's original selector. The flags now reflect that value.
4E3B
If the Z FLAG (Zero) has been set, the caller's selector in Register B was 00H, meaning the whole directory is wanted. JUMP to 4EA6H to build a table covering every file on the disk.
4E3D
INC B 04
INCrement Register B by 1 again. Register B now holds the caller's selector plus one, and that is the directory record number this service will read; selector 00H is reserved for the full scan and 0FFH for the free-space reply, so the record number is always at least one.
4E3E
LD A,07H 3E 07
Load Register A with 7, the offset within the drive control table entry of the allocation byte whose bits 4 to 0 hold the highest sector number on a track and whose bits 7 to 5 hold the head count field.
4E40
GOSUB to the DCTBYT helper at 479CH in SYS0/SYS, which computes the address of the drive control table entry for the drive number in Register C and returns in Register A the byte at the offset Register A named, here offset 7.
4E43
LD D,A 57
Copy the drive control table allocation byte from Register A into Register D so that both of its fields can be extracted.
4E44
AND 1FH E6 1F
Mask Register A, the allocation byte, down to bits 4 to 0, leaving the highest sector number on a track.
4E46
LD E,A 5F
Copy the highest sector number from Register A into Register E.
4E47
INC E 1C
INCrement Register E by 1, turning the highest sector number into the count of sectors on a track.
4E48
XOR D AA
Exclusive-OR Register A, which holds only the low five bits of the allocation byte, with Register D, which holds the whole allocation byte. The matching low bits cancel, leaving only the head count field in bits 7 to 5 of Register A.
4E49
RLCA 07
Rotate Register A Left: bit 7 of the head count field moves into bit 0 and into the carry. This is the first of three rotations that bring the field down to the bottom of the register.
4E4A
RLCA 07
Rotate Register A Left again, continuing to shift the head count field toward bit 0.
4E4B
RLCA 07
Rotate Register A Left a third time. Register A now holds the head count field as a plain number in bits 2 to 0.
4E4C
INC A 3C
INCrement Register A by 1, since the field is stored one less than the true count. Register A now holds the number of heads.
4E4D
GOSUB to the eight-by-eight multiply helper MULTEA at 4B6CH in SYS0/SYS, which returns in Register A the product of Register A, the head count, and Register E, the sectors per track. Register A now holds the number of sectors on one cylinder.
4E50
LD E,A 5F
Copy the sectors-per-cylinder figure from Register A into Register E to hold it while the drive's sided-ness is fetched.
4E51
LD A,04H 3E 04
Load Register A with 4, the offset within the drive control table entry of the byte whose bit 5 marks a double-sided drive and whose low nibble is the physical unit number.
4E53
GOSUB to DCTBYT at 479CH in SYS0/SYS again, returning in Register A the byte at offset 4 of the drive control table entry for the drive in Register C.
4E56
BIT 5,A CB 6F
Test bit 5 of Register A, the drive control table byte at offset 4, which is set when the drive is double-sided. If the bit is clear the Z FLAG is set; if it is set the NZ FLAG is set.
4E58
LD A,E 7B
Load Register A with the sectors-per-cylinder figure saved in Register E. This load does not disturb the flags left by the bit test.
4E59
If the Z FLAG (Zero) has been set, bit 5 was clear and the drive is single-sided, so JUMP over the doubling to 4E5CH.
4E5B
ADD A,A 87
ADD Register A to itself, doubling the sectors-per-cylinder figure in Register A because a double-sided drive puts two tracks on every cylinder.
4E5C
SUB 02H D6 02
SUBtract 2 from Register A, holding the sectors per cylinder, to discount sector 0 which holds the Granule Allocation Table and sector 1 which holds the Hash Index Table. Register A now holds the number of directory sectors available on the directory cylinder.
4E5E
LD D,A 57
Copy the count of directory sectors from Register A into Register D, ready to compare the caller's record number against it.
4E5F
LD A,B 78
Load Register A with the directory record number formed at 4E3DH from Register B, the caller's selector plus one.
4E60
AND 1FH E6 1F
Mask Register A, the directory record number, down to bits 4 to 0. In an LDOS directory record number the low five bits are the directory sector and the high three bits are the slot within that sector, so this leaves the sector part alone.
4E62
CP D BA
Compare Register A, the directory sector part of the record number, against Register D, the number of directory sectors this drive's geometry provides. If Register A is less than Register D the CARRY FLAG is set; if it is equal or greater the NO CARRY FLAG is set.
4E63
If the CARRY FLAG has been set, the record number lies inside the directory, so JUMP to 4E69H to read it.
4E65
LD A,10H 3E 10
Error Path
Load Register A with 10H, LDOS error number 16, "illegal logical file number", the code returned when the requested directory record lies beyond the last directory sector on this drive.
4E67
OR A B7
OR Register A with itself, which changes nothing but sets the NZ FLAG because Register A holds 10H, marking the return as a failure.
4E68
RET C9
Return to the RST 28H dispatcher in SYS0/SYS with Register A holding error 16 and the NZ condition set.

4E69H - Read and Qualify the Requested Record

The record number in Register B and the drive in Register C are handed to the resident DIRRD routine, which reads the containing directory sector into SBUFF$ at 4200H and points Register Pair HL at the record itself. The attribute byte is then tested: only a record that is in use, primary rather than a secondary extended entry, and neither system nor invisible is reported.

4E69
PUSH HL E5
Save Register Pair HL, which addresses the caller's buffer where the 22-byte reply record is to be built, on the stack while the directory read uses Register Pair HL for its own result.
4E6A
GOSUB to the resident DIRRD routine at 4B10H in SYS0/SYS. Register B holds the directory record number and Register C the drive number; DIRRD reads the directory sector containing that record into the shared buffer SBUFF$ at 4200H and returns Register Pair HL addressing the 32-byte record, with Z on success or NZ and error 17 in Register A on a read failure.
4E6D
POP DE D1
Restore the caller's buffer address from the stack into Register Pair DE, which is where the record builder expects to find its destination.
4E6E
RET NZ C0
If the NZ FLAG (Not Zero) has been set, the directory sector could not be read, so return to the RST 28H dispatcher with the error number DIRRD left in Register A, 11H for LDOS error 17, "directory read error".
4E6F
LD A,(HL) 7E
Register Pair HL addresses offset 0 of the directory record in SBUFF$, the attribute byte. Load Register A with it. Bit 7 marks a secondary extended record, bit 6 a system file, bit 4 an in-use record, bit 3 an invisible file and bits 2 to 0 the access level.
4E70
AND 0D8H E6 D8
Mask the attribute byte in Register A down to bits 7, 6, 4 and 3, the secondary, system, in-use and invisible flags, discarding the access level and the unused bit 5.
4E72
XOR 10H EE 10
Exclusive-OR Register A with 10H so that the result is zero only when the in-use bit was set and the secondary, system and invisible bits were all clear. Zero sets the Z FLAG; anything else sets the NZ FLAG.
4E74
LD A,19H 3E 19
Load Register A with 19H, LDOS error number 25, "file access denied", ready to be returned. This load does not disturb the flags set above.
4E76
RET NZ C0
If the NZ FLAG (Not Zero) has been set, the record is free, or is a secondary extended record, or belongs to a system or invisible file, so it is not reported. Return to the RST 28H dispatcher with error 25 in Register A.

4E77H - @RAMDIR Record Builder

The heart of @RAMDIR. Register Pair HL addresses a qualifying 32-byte directory record in SBUFF$ and Register Pair DE addresses the place in the caller's buffer where the reply record goes. The result is a fixed 22-byte record: fifteen bytes of blank-padded file specification, the access level, the end-of-file byte, the logical record length, the ending record number as a word and the file size in kilobytes as a word. A 2BH marker is written in the byte after it, which the next record of a full scan overwrites and which therefore remains as the end-of-table marker after the last record.

4E77
PUSH HL E5
Save Register Pair HL, addressing offset 0 of the directory record in SBUFF$, on the stack, because the file specification builder advances Register Pair HL across the name and extension fields.
4E78
GOSUB to the file specification builder at 4FE2H, which reads the eight-character name at offset 5 and the three-character extension at offset 0DH of the directory record addressed by Register Pair HL and writes a fifteen-byte blank-padded NAME/EXT:d string, followed by an 03H terminator, into the caller's buffer addressed by Register Pair DE. It returns Register Pair DE addressing that 03H byte.
4E7B
POP HL E1
Restore Register Pair HL from the stack so that it again addresses offset 0 of the directory record in SBUFF$.
4E7C
LD A,(HL) 7E
Load Register A with the attribute byte at offset 0 of the directory record, already known to describe an in-use, primary, visible, non-system file.
4E7D
AND 07H E6 07
Mask the attribute byte in Register A down to bits 2 to 0, the file's access level, which counts up from 0 for full access.
4E7F
LD (DE),A 12
Store the access level from Register A into the caller's buffer at the byte Register Pair DE addresses, which is offset 15 of the reply record and is the 03H terminator the specification builder left there.
4E80
INC DE 13
INCrement Register Pair DE by 1 so that it addresses offset 16 of the reply record.
4E81
INC L 2C
INCrement Register L by 1, moving Register Pair HL from offset 0 to offset 1 of the directory record. Only Register L is stepped because a directory record never straddles the 4200H page boundary.
4E82
INC L 2C
INCrement Register L by 1 again, moving Register Pair HL to offset 2 of the directory record, the remainder of the packed modification date.
4E83
INC L 2C
INCrement Register L by 1 a third time, so Register Pair HL addresses offset 3 of the directory record, the end-of-file byte offset within the last sector.
4E84
LDI ED A0
Block move one byte: copy the end-of-file byte at directory offset 3, addressed by Register Pair HL, to offset 16 of the reply record, addressed by Register Pair DE, then INCrement both pointers and DECrement Register Pair BC. Register Pair HL now addresses directory offset 4 and Register Pair DE reply offset 17.
4E86
LDI ED A0
Block move one more byte: copy the logical record length at directory offset 4 to offset 17 of the reply record and step both pointers again. Register Pair HL now addresses directory offset 5, the start of the file name.
4E88
LD A,L 7D
Load Register A with Register L, the low byte of the address of directory offset 5 within SBUFF$, so that a fixed distance can be added to it.
4E89
ADD A,0FH C6 0F
ADD 15 to Register A, moving the pointer from directory offset 5 to directory offset 20, the low byte of the file's ending record number.
4E8B
LD L,A 6F
Put the adjusted low byte back into Register L, so Register Pair HL now addresses offset 20 of the directory record within SBUFF$.
4E8C
LD A,(HL) 7E
Load Register A with the low byte of the file's ending record number from directory offset 20.
4E8D
LD (DE),A 12
Store that low byte of the ending record number into offset 18 of the reply record, addressed by Register Pair DE.
4E8E
INC HL 23
INCrement Register Pair HL by 1 so that it addresses offset 21 of the directory record, the high byte of the ending record number.
4E8F
INC DE 13
INCrement Register Pair DE by 1 so that it addresses offset 19 of the reply record.
4E90
LD H,(HL) 66
Load Register H with the high byte of the ending record number from directory offset 21, overwriting the buffer page number now that the directory record has been fully read.
4E91
LD L,A 6F
Load Register L with the low byte of the ending record number still held in Register A. Register Pair HL now holds the whole ending record number, the count of 256-byte sectors the file occupies.
4E92
EX DE,HL EB
Exchange Register Pair DE and Register Pair HL, so Register Pair HL addresses offset 19 of the reply record and Register Pair DE holds the ending record number ready to be stored and then scaled.
4E93
LD (HL),D 72
Store Register D, the high byte of the ending record number, into offset 19 of the reply record. Offsets 18 and 19 together now hold the ending record number as a word.
4E94
INC HL 23
INCrement Register Pair HL by 1 so that it addresses offset 20 of the reply record, where the file size in kilobytes goes.
4E95
SRL D CB 3A
Shift Register D, the high byte of the ending record number, Right one place with a zero entering bit 7 and bit 0 falling into the carry. This is the first half of dividing the sector count in Register Pair DE by two.
4E97
RR E CB 1B
Rotate Register E, the low byte of the ending record number, Right through the carry, bringing in the bit that fell out of Register D. Register Pair DE now holds the sector count divided by two.
4E99
SRL D CB 3A
Shift Register D Right again, starting the second division by two of the sector count held in Register Pair DE.
4E9B
RR E CB 1B
Rotate Register E Right through the carry to complete it. Register Pair DE now holds the ending record number divided by four, which is the file size in kilobytes because four 256-byte sectors make one K.
4E9D
LD (HL),E 73
Store Register E, the low byte of the file size in kilobytes, into offset 20 of the reply record addressed by Register Pair HL.
4E9E
INC HL 23
INCrement Register Pair HL by 1 so that it addresses offset 21 of the reply record.
4E9F
LD (HL),D 72
Store Register D, the high byte of the file size in kilobytes, into offset 21 of the reply record. The 22 bytes of the record are now complete.
4EA0
INC HL 23
INCrement Register Pair HL by 1 so that it addresses the byte immediately after the finished 22-byte record.
4EA1
LD (HL),2BH 36 2B
Store 2BH, the character +, into that byte as the end-of-table marker. During a full scan the next record starts here and overwrites it, so the marker survives only after the last record built.
4EA3
EX DE,HL EB
Exchange Register Pair DE and Register Pair HL, so Register Pair DE addresses the marker byte, which is where the next record of a full scan will begin, and Register Pair HL holds the file size in kilobytes, which is discarded.
4EA4
XOR A AF
Set Register A to ZERO and clear all flags, setting the Z FLAG to report success with no error number.
4EA5
RET C9
Return. For a single-record request this returns to the RST 28H dispatcher in SYS0/SYS with the reply built; for the full scan it returns to 4EE3H to fetch the next record.

4EA6H - Full @RAMDIR Directory Scan

Reached when the caller passed 00H in Register C, asking for every file on the disk. The Hash Index Table is read into 5100H and every one of its 256 slots is examined in slot-major order, the record number stepping by 20H so that all eight slots of one directory sector are visited before moving to the next sector. A self-modifying compare at 4EC9H remembers which directory sector already sits in SBUFF$, so a run of records in the same sector costs a single disk read. Each qualifying file adds a 22-byte record to the caller's buffer and the table ends with the 2BH marker left by the last record built.

4EA6
EX DE,HL EB
Exchange Register Pair DE and Register Pair HL, moving the caller's buffer address out of Register Pair HL into Register Pair DE, which is where the record builder at 4E77H writes.
4EA7
GOSUB to the Hash Index Table reader at 509CH, which reads sector 1 of the directory cylinder of the drive in Register C into 5100H through 51FFH. It returns Z on success, or NZ with 16H in Register A, LDOS error 22, "HIT read error".
4EAA
RET NZ C0
If the NZ FLAG (Not Zero) has been set, the Hash Index Table could not be read, so return to the RST 28H dispatcher with error 22 in Register A.
4EAB
Loop Start
JUMP to 4EBEH to test the very first Hash Index Table slot, entering the scan below the record-stepping code so that record number 00H is examined before anything is added to it.
4EAD
POP BC C1
Restore Register Pair BC from the stack, recovering the record number in Register B and the drive number in Register C that were saved at 4EC3H before the record was examined.
4EAE
LD H,51H 26 51
Load Register H with 51H, the page of the Hash Index Table image read in at 4EA7H, so that Register Pair HL will address a byte inside 5100H through 51FFH.
4EB0
LD L,B 68
Load Register L with the record number in Register B. Register Pair HL now addresses that record's hash byte in the Hash Index Table image, since the table holds one byte per record number.
4EB1
LD A,L 7D
Loop Start
Load Register A with Register L, the current record number, ready to advance it to the next slot.
4EB2
ADD A,20H C6 20
ADD 20H to Register A, the record number, moving to the next slot within the same directory sector because bits 7 to 5 of a record number are the slot and bits 4 to 0 are the sector. A carry means the eighth slot has just been passed.
4EB4
LD L,A 6F
Put the advanced record number back into Register L, so Register Pair HL addresses the next hash byte in the Hash Index Table image.
4EB5
If the NO CARRY FLAG has been set, the slot number did not wrap, so JUMP to 4EBEH to examine this record.
4EB7
INC L 2C
INCrement Register L by 1. The slot field has wrapped back to zero, so the directory sector field in bits 4 to 0 is stepped on to the next sector.
4EB8
BIT 5,L CB 6D
Test bit 5 of Register L, the record number. The sector field is five bits wide, so this bit becomes set only when the sector number has run past 1FH and every one of the 256 Hash Index Table slots has been visited. Clear sets the Z FLAG; set sets the NZ FLAG.
4EBA
If the Z FLAG (Zero) has been set, there are still slots left, so JUMP to 4EBEH to examine this record.
4EBC
XOR A AF
Loop End
Set Register A to ZERO and clear all flags, setting the Z FLAG to report that the whole table was built successfully.
4EBD
RET C9
Return to the RST 28H dispatcher in SYS0/SYS. The caller's buffer holds one 22-byte record per qualifying file, ended by the 2BH marker the last record builder left behind.
4EBE
LD A,(HL) 7E
Load Register A with the hash byte for the current record number from the Hash Index Table image at 5100H addressed by Register Pair HL. A non-zero byte is the hash of a file name occupying that record; zero means the slot is free.
4EBF
OR A B7
OR Register A with itself to set the flags from the hash byte. If the byte is zero the Z FLAG is set; otherwise the NZ FLAG is set.
4EC0
If the Z FLAG (Zero) has been set, the slot is free and there is no directory record to read, so LOOP BACK to 4EB1H to step on to the next slot.
4EC2
LD B,L 45
Copy Register L, the record number whose hash byte is non-zero, into Register B, where DIRRD expects the directory record number.
4EC3
PUSH BC C5
Save Register Pair BC, holding the record number in Register B and the drive number in Register C, on the stack so that the loop can recover them at 4EADH after the record has been processed.
4EC4
LD A,L 7D
Load Register A with Register L, the record number, so that its two fields can be separated.
4EC5
AND 0E0H E6 E0
Mask Register A down to bits 7 to 5, the slot number within the directory sector, multiplied in place by 32 because each directory record is 32 bytes long. This is the record's byte offset inside its sector.
4EC7
LD L,A 6F
Put that byte offset into Register L. Together with the page byte set at 4ED6H, Register Pair HL will address the record inside SBUFF$.
4EC8
XOR B A8
Exclusive-OR Register A, which holds only the slot field, with Register B, which holds the whole record number. The matching slot bits cancel, leaving in Register A just the directory sector number in bits 4 to 0.
4EC9
CP 0FFH FE FF
Self-Modifying Code
Compare Register A, the directory sector number wanted, against the operand at 4ECAH. That operand holds the directory sector number already sitting in SBUFF$ from the previous record and is written at 4ECDH. It is loaded as 0FFH, a value no five-bit sector number can take, so the first record always forces a read. If they match the Z FLAG is set; otherwise the NZ FLAG is set.
4ECB
If the Z FLAG (Zero) has been set, the wanted directory sector is already in the buffer at 4200H, so JUMP to 4ED6H and skip the disk read.
4ECD
LD (4ECAH),A 32 CA 4E
Self-Modifying Code
Store the directory sector number from Register A into 4ECAH, the operand of the compare at 4EC9H, recording which sector is about to be read into SBUFF$ so that the following records in the same sector can skip the read.
4ED0
GOSUB to the resident DIRRD routine at 4B10H in SYS0/SYS with Register B holding the directory record number and Register C the drive number, reading the containing directory sector into SBUFF$ at 4200H.
4ED3
If the NZ FLAG (Not Zero) has been set, the directory sector could not be read and Register A holds 11H, LDOS error 17, "directory read error". JUMP to the shared error tail at 4FC2H, which balances the stack and returns that error.
4ED6
LD H,42H 26 42
Load Register H with 42H, the page of the shared directory buffer SBUFF$ at 4200H. With the byte offset already in Register L, Register Pair HL now addresses offset 0 of this record inside the buffered directory sector.
4ED8
LD A,(HL) 7E
Load Register A with the attribute byte at offset 0 of the directory record now addressed by Register Pair HL.
4ED9
AND 0D8H E6 D8
Mask the attribute byte in Register A down to bits 7, 6, 4 and 3, the secondary, system, in-use and invisible flags.
4EDB
XOR 10H EE 10
Exclusive-OR Register A with 10H, so the result is zero only for an in-use primary record belonging to a file that is neither system nor invisible. Zero sets the Z FLAG; anything else sets the NZ FLAG.
4EDD
If the NZ FLAG (Not Zero) has been set, this record does not describe a visible ordinary file, so LOOP BACK to 4EADH to recover the record number and move on without adding anything to the caller's buffer.
4EDF
PUSH HL E5
Save Register Pair HL, addressing offset 0 of the directory record in SBUFF$, on the stack, because the record builder advances it.
4EE0
GOSUB to the record builder at 4E77H, which converts the directory record addressed by Register Pair HL into a 22-byte @RAMDIR record at the buffer position addressed by Register Pair DE, writes the 2BH end marker after it and leaves Register Pair DE addressing that marker, ready for the next record.
4EE3
POP HL E1
Restore Register Pair HL from the stack, balancing the push at 4EDFH. Its value is not used again, because the loop rebuilds Register Pair HL from the record number.
4EE4
Loop End
LOOP BACK to 4EADH to recover the record number and drive number and step on to the next Hash Index Table slot.

4EE6H - @DODIR Entry and Function Dispatch

The @DODIR service is entered with Register B holding a function code 0 to 4, Register C the drive number and Register Pair HL the caller's buffer. Bit 0 of the function code decides whether records go to the caller's buffer or file specifications go to the screen, and bit 1 decides whether the files are filtered by a three-character extension mask taken from the first three bytes of that buffer. Function 4 is a separate request for the disk's name, date, capacity and free space.

4EE6
LD A,07H 3E 07
Load Register A with 7, the highest drive number LDOS supports, ready to range-check the caller's drive number.
4EE8
CP C B9
Compare Register A, holding the limit 7, against Register C, holding the caller's drive number. If Register C is greater than 7 the CARRY FLAG is set; otherwise the NO CARRY FLAG is set.
4EE9
LD A,20H 3E 20
Load Register A with 20H, LDOS error number 32, "illegal drive number", without disturbing the flags set by the comparison above.
4EEB
RET C D8
If the CARRY FLAG has been set, the drive number in Register C was above 7, so return to the RST 28H dispatcher with error 32 in Register A.
4EEC
GOSUB to the resident file-operation prologue at 49F1H in SYS0/SYS, which saves the caller's registers, records the caller's return address in 430CH and the file control block pointer in 430AH, sets Register Pair IX to that control block and arranges a register-restoring return.
4EEF
GOSUB to the drive-ready test at 50AEH, which plants the ASCII drive digit taken from Register C into the file specification builder at 5017H, confirms the unit has a real drive control block, seeks it and waits for a full WD1771 index-pulse cycle.
4EF2
RET NZ C0
If the NZ FLAG (Not Zero) has been set, the drive named in Register C is not ready, so return to the RST 28H dispatcher with the error number the ready test left in Register A.
4EF3
LD A,B 78
Load Register A with Register B, the caller's function code, a value from 0 to 4.
4EF4
LD (4F6CH),A 32 6C 4F
Self-Modifying Code
Store the function code from Register A into 4F6CH, the operand of the LD A,00H at 4F6BH. Every record processed reloads the function code from that operand, so this one write configures the whole scan. Bit 1 selects extension filtering and bit 0 selects buffer output rather than screen display.
4EF7
CP 04H FE 04
Compare Register A, the function code, against 4, the disk-information request. If they are equal the Z FLAG is set; if Register A is below 4 the CARRY FLAG is set as well.
4EF9
If the Z FLAG (Zero) has been set, the caller asked for function 4, so JUMP to 4F1AH to build the disk-information reply instead of scanning the directory.
4EFB
LD A,1DH 3E 1D
Load Register A with 1DH, LDOS error number 29, "record number out of range", the code used to reject a function number above 4. This load does not disturb the flags from the comparison above.
4EFD
RET NC D0
If the NO CARRY FLAG has been set, the function code in Register B was above 4, so return to the RST 28H dispatcher with error 29 in Register A. Functions 0 to 3 leave the carry set and continue.
4EFE
PUSH HL E5
Save Register Pair HL, the caller's buffer address, on the stack. The buffer supplies the extension mask from its first three bytes and also receives the output records, so its address is needed again after the mask is copied out.
4EFF
PUSH BC C5
Save Register Pair BC, holding the function code in Register B and the drive number in Register C, on the stack, because the block move below destroys Register Pair BC.
4F00
LD DE,4480H 11 80 44
Point Register Pair DE to CFCB$ at 4480H, the resident command file control block buffer, which SYS12/SYS borrows to hold the three-character extension mask while the scan runs.
4F03
LD BC,0003H 01 03 00
Load Register Pair BC with 3, the number of mask characters to copy.
4F06
LDIR ED B0
Block move: copy 3 bytes forward from the source addressed by Register Pair HL, the first three bytes of the caller's buffer holding the extension mask, to the destination addressed by Register Pair DE, CFCB$ at 4480H, decrementing Register Pair BC to zero. The mask is only consulted when bit 1 of the function code is set, but it is copied on every call.
4F08
POP BC C1
Restore Register Pair BC from the stack, giving back the function code in Register B and the drive number in Register C.
4F09
LD A,04H 3E 04
Load Register A with 4, the number of file specifications the display path puts on one video line.
4F0B
LD (4FA0H),A 32 A0 4F
Self-Modifying Code
Store 4 from Register A into 4FA0H, the operand of the LD A,00H at 4F9FH, initialising the countdown of file specifications remaining on the current display line.
4F0E
LD A,0FH 3E 0F
Load Register A with 15, the number of directory lines the display path puts on one screen before pausing for the operator.
4F10
LD (4FB1H),A 32 B1 4F
Self-Modifying Code
Store 15 from Register A into 4FB1H, the operand of the LD A,00H at 4FB0H, initialising the countdown of display lines remaining on the current screen.
4F13
GOSUB to the Hash Index Table reader at 509CH, which reads sector 1 of the directory cylinder of the drive in Register C into 5100H through 51FFH, returning NZ with 16H in Register A, LDOS error 22, on failure.
4F16
POP DE D1
Restore the caller's buffer address from the stack into Register Pair DE, which is where the record-copying path writes its output.
4F17
RET NZ C0
If the NZ FLAG (Not Zero) has been set, the Hash Index Table could not be read, so return to the RST 28H dispatcher with error 22 in Register A.
4F18
JUMP to 4F4EH to begin the directory scan at Hash Index Table slot 00H, entering below the record-stepping code so that record number 00H is examined first. Register Pair HL still addresses 5100H, the first byte of the table.

4F1AH - Function 4, Disk Information Reply

Function 4 fills a twenty-byte reply: the eight-character disk name, the eight-character disk date, the disk capacity in kilobytes and the free space in kilobytes. The capacity calculator is run first because it is what brings the Granule Allocation Table into 5100H, and the name and date are then copied straight out of that image from GAT offsets D0H and D8H.

4F1A
PUSH HL E5
Save Register Pair HL, the caller's twenty-byte reply buffer address, on the stack so that it can be recovered after the free-space figures have been stored into its tail.
4F1B
LD DE,0010H 11 10 00
Load Register Pair DE with 16, both the offset within the reply at which the capacity and free-space words go and the number of bytes of disk name and date that precede them.
4F1E
PUSH DE D5
Save that count of 16 on the stack, since Register Pair DE is needed for the address arithmetic and the count is wanted again as the block-move length.
4F1F
ADD HL,DE 19
ADD 16 in Register Pair DE to Register Pair HL, the buffer address, so Register Pair HL addresses offset 16 of the reply, where the calculator is to place the capacity and free-space words.
4F20
GOSUB to the capacity and free-space calculator at 5025H. It reads sector 0 of the directory cylinder, the Granule Allocation Table, into 5100H, then stores the total kilobytes on the diskette at offset 16 of the reply and the free kilobytes at offset 18.
4F23
POP BC C1
Restore the count 16 from the stack into Register Pair BC, where the block move below expects its length.
4F24
POP DE D1
Restore the caller's reply buffer address from the stack into Register Pair DE, the destination of the block move.
4F25
LD HL,51D0H 21 D0 51
Point Register Pair HL to 51D0H, offset D0H of the Granule Allocation Table image the calculator has just read into 5100H. That is the eight-byte disk name, immediately followed at offset D8H by the eight-byte disk date.
4F28
LDIR ED B0
Block move: copy 16 bytes forward from the source addressed by Register Pair HL, the disk name and date in the Granule Allocation Table image, to the destination addressed by Register Pair DE, offsets 0 to 15 of the caller's reply buffer, counting Register Pair BC down to zero. The reply is now name, date, capacity and free space.
4F2A
XOR A AF
Set Register A to ZERO and clear all flags, setting the Z FLAG to report success with no error number.
4F2B
RET C9
Return to the RST 28H dispatcher in SYS0/SYS with the twenty-byte disk-information reply in the caller's buffer.

4F2CH - @DODIR Record Iterator and End of Scan

Every record handler returns here, because the address 4F2CH is pushed as a return address at 4F6AH before the handler is reached. The iterator steps the record number to the next Hash Index Table slot in the same slot-major order the @RAMDIR scan uses. When the table is exhausted, a buffer request is closed with an 0FFH terminator and a display request with a final carriage return.

4F2C
POP BC C1
Loop Start
Restore Register Pair BC from the stack, recovering the record number in Register B and the drive number in Register C that were saved at 4F53H before this record was processed.
4F2D
LD H,51H 26 51
Load Register H with 51H, the page of the Hash Index Table image read in at 4F13H, so Register Pair HL will address a byte inside 5100H through 51FFH.
4F2F
LD L,B 68
Load Register L with the record number in Register B, so Register Pair HL addresses that record's hash byte in the Hash Index Table image.
4F30
LD A,L 7D
Load Register A with Register L, the current record number, ready to advance it.
4F31
ADD A,20H C6 20
ADD 20H to Register A, the record number, moving to the next slot within the same directory sector, since bits 7 to 5 of a record number are the slot and bits 4 to 0 the sector. A carry means the last slot has been passed.
4F33
LD L,A 6F
Put the advanced record number back into Register L, so Register Pair HL addresses the next hash byte in the Hash Index Table image.
4F34
If the NO CARRY FLAG has been set, the slot number did not wrap, so JUMP to 4F4EH to examine this record.
4F36
INC L 2C
INCrement Register L by 1, stepping the directory sector field in bits 4 to 0 on to the next sector now that the slot field has wrapped to zero.
4F37
BIT 5,L CB 6D
Test bit 5 of Register L, the record number. That bit becomes set only when the five-bit sector field has run past 1FH, meaning all 256 Hash Index Table slots have been visited. Clear sets the Z FLAG; set sets the NZ FLAG.
4F39
If the Z FLAG (Zero) has been set, slots remain, so JUMP to 4F4EH to examine this record.
4F3B
LD A,(4F6CH) 3A 6C 4F
Loop End
Load Register A from 4F6CH, the self-modifying operand holding the @DODIR function code written at 4EF4H, to decide how the finished scan should be closed off.
4F3E
AND 01H E6 01
Mask the function code in Register A down to bit 0, which is set for the two functions that copy records into the caller's buffer and clear for the two that display file specifications.
4F40
If the NZ FLAG (Not Zero) has been set, the caller asked for records in a buffer, so JUMP to 4F49H to write the end-of-table marker.
4F42
LD A,0DH 3E 0D
Load Register A with 0DH, the carriage return code, to end the last partly filled line of the directory display.
4F44
GOSUB to ROM routine at 0033H, the Model I character display routine, which prints the character in Register A at the cursor and advances it, here moving the cursor to the start of the next line.
4F47
XOR A AF
Set Register A to ZERO and clear all flags, setting the Z FLAG to report success with no error number.
4F48
RET C9
Return to the RST 28H dispatcher in SYS0/SYS with the directory display complete.
4F49
LD A,0FFH 3E FF
Load Register A with 0FFH, the value that marks the end of the list of directory records in the caller's buffer. No directory attribute byte can be 0FFH, so it is unambiguous.
4F4B
LD (DE),A 12
Store the 0FFH terminator into the caller's buffer at the position Register Pair DE addresses, which is where the next eighteen-byte record would have started.
4F4C
XOR A AF
Set Register A to ZERO and clear all flags, setting the Z FLAG to report success with no error number.
4F4D
RET C9
Return to the RST 28H dispatcher in SYS0/SYS with the caller's buffer holding one eighteen-byte record per qualifying file, ended by the 0FFH marker.

4F4EH - @DODIR Hash Index Table Scan

The body of the @DODIR scan. Free Hash Index Table slots are skipped without any disk activity; for an occupied slot the record number is split into its slot offset and its directory sector, the sector is read only if it is not already the one sitting in SBUFF$, and the record is then passed through the extension filter and on to the per-record handler. The return address 4F2CH is pushed by hand before the handler runs, so that both the filter rejecting a record and the handler finishing normally land back on the iterator.

4F4E
LD A,(HL) 7E
Load Register A with the hash byte for the current record number from the Hash Index Table image at 5100H addressed by Register Pair HL. A non-zero value is the hash of the file name that occupies the record; zero means the slot is free.
4F4F
OR A B7
OR Register A with itself to set the flags from the hash byte. Zero sets the Z FLAG; anything else sets the NZ FLAG.
4F50
If the Z FLAG (Zero) has been set, the slot is free, so LOOP BACK to 4F30H to step on to the next slot without reading anything from the disk.
4F52
LD B,L 45
Copy Register L, the record number whose hash byte is non-zero, into Register B, where DIRRD expects the directory record number.
4F53
PUSH BC C5
Save Register Pair BC, holding the record number in Register B and the drive number in Register C, on the stack so that the iterator can recover them at 4F2CH.
4F54
LD A,L 7D
Load Register A with Register L, the record number, so that its slot and sector fields can be separated.
4F55
AND 0E0H E6 E0
Mask Register A down to bits 7 to 5, the slot number within the directory sector. Because each directory record is 32 bytes long, leaving the field in place gives the record's byte offset inside its sector directly.
4F57
LD L,A 6F
Put that byte offset into Register L, ready to be combined at 4F65H with the page of the shared directory buffer.
4F58
XOR B A8
Exclusive-OR Register A, holding only the slot field, with Register B, holding the whole record number. The slot bits cancel, leaving in Register A just the directory sector number in bits 4 to 0.
4F59
CP 0FFH FE FF
Self-Modifying Code
Compare Register A, the directory sector wanted, against the operand at 4F5AH, which holds the directory sector already read into SBUFF$ and is written at 4F5DH. It is loaded as 0FFH, a value no five-bit sector number can take, so the first record always forces a read. Equal sets the Z FLAG; otherwise the NZ FLAG is set.
4F5B
If the Z FLAG (Zero) has been set, the wanted directory sector is already in the buffer at 4200H, so JUMP to 4F65H and skip the disk read.
4F5D
LD (4F5AH),A 32 5A 4F
Self-Modifying Code
Store the directory sector number from Register A into 4F5AH, the operand of the compare at 4F59H, recording which sector is about to be read so that the remaining records in it can skip the read.
4F60
GOSUB to the resident DIRRD routine at 4B10H in SYS0/SYS with Register B holding the record number and Register C the drive number, reading the containing directory sector into SBUFF$ at 4200H.
4F63
If the NZ FLAG (Not Zero) has been set, the directory sector could not be read and Register A holds 11H, LDOS error 17, "directory read error", so JUMP to the shared error tail at 4FC2H which balances the stack and returns.
4F65
LD H,42H 26 42
Load Register H with 42H, the page of the shared directory buffer SBUFF$ at 4200H. With the slot offset already in Register L, Register Pair HL now addresses offset 0 of this record inside the buffered directory sector.
4F67
LD BC,4F2CH 01 2C 4F
Load Register Pair BC with 4F2CH, the address of the record iterator, so that it can be planted on the stack as a return address.
4F6A
PUSH BC C5
PUSH the address 4F2CH onto the stack as the return address for everything that follows, so that both a rejection by the extension filter and a normal finish by the record handler come back to the iterator.
4F6B
LD A,00H 3E 00
Self-Modifying Code
Load Register A with the operand at 4F6CH, which holds the @DODIR function code written there at 4EF4H. The listing shows the initial value 00H; at run time it is the caller's function code, 0 to 3 on this path.
4F6D
PUSH HL E5
Save Register Pair HL, addressing offset 0 of the directory record in SBUFF$, on the stack because the extension filter advances it across the extension field.
4F6E
PUSH DE D5
Save Register Pair DE, the caller's buffer write position, on the stack because the extension filter uses Register Pair DE to walk the mask in CFCB$.
4F6F
GOSUB to the extension filter at 4FC4H with Register A holding the function code and Register Pair HL addressing the directory record. When bit 1 of the function code is clear it returns Z at once; otherwise it compares the record's three-character extension against the mask in CFCB$ and returns NZ if they differ.
4F72
POP DE D1
Restore Register Pair DE from the stack, giving back the caller's buffer write position.
4F73
POP HL E1
Restore Register Pair HL from the stack, giving back the address of offset 0 of the directory record in SBUFF$.
4F74
RET NZ C0
If the NZ FLAG (Not Zero) has been set, the extension did not match the mask, so return through the address pushed at 4F6AH, which sends control to the iterator at 4F2CH and on to the next record.

4F75H - Per-Record Handler

Bit 0 of the function code chooses between the two things @DODIR can do with a record that has passed the filter. With the bit set, eighteen bytes of the raw directory record are copied to the caller's buffer: the sixteen bytes from the attribute byte through the extension, then the ending record number word, the four password bytes in between being skipped. With the bit clear, the record is turned into a file specification and displayed.

4F75
LD A,(4F6CH) 3A 6C 4F
Self-Modifying Code
Load Register A from 4F6CH, the operand holding the @DODIR function code written at 4EF4H.
4F78
RRCA 0F
Rotate Register A Right: bit 0 of the function code, which selects buffer output over screen display, moves into the carry flag.
4F79
LD A,(HL) 7E
Load Register A with the attribute byte at offset 0 of the directory record addressed by Register Pair HL. This load does not disturb the carry flag set above.
4F7A
If the NO CARRY FLAG has been set, bit 0 of the function code was clear, so JUMP to 4F8FH to display this record's file specification instead of copying it.
4F7C
AND 90H E6 90
Mask the attribute byte in Register A down to bit 7, the secondary extended record flag, and bit 4, the in-use flag. The buffer path is deliberately less selective than the display path, and passes system and invisible files through.
4F7E
XOR 10H EE 10
Exclusive-OR Register A with 10H, so the result is zero only for an in-use primary record. Zero sets the Z FLAG; anything else sets the NZ FLAG.
4F80
RET NZ C0
If the NZ FLAG (Not Zero) has been set, this record is free or is a secondary extended record, so return through the address pushed at 4F6AH to the iterator at 4F2CH without copying anything.
4F81
LD BC,0010H 01 10 00
Load Register Pair BC with 16, the number of bytes to copy from the front of the directory record: the attribute byte, the flags and date bytes, the end-of-file byte, the logical record length, the eight-character name and the three-character extension.
4F84
LDIR ED B0
Block move: copy 16 bytes forward from the source addressed by Register Pair HL, offsets 0 to 0FH of the directory record in SBUFF$, to the destination addressed by Register Pair DE, the caller's buffer, counting Register Pair BC down to zero. Register Pair HL is left addressing offset 10H of the record.
4F86
INC HL 23
INCrement Register Pair HL by 1, stepping over directory offset 10H, the low byte of the update-password field which LDOS 5.3 reuses for the modification time and extended year.
4F87
INC HL 23
INCrement Register Pair HL by 1, stepping over directory offset 11H, the high byte of that same reused field.
4F88
INC HL 23
INCrement Register Pair HL by 1, stepping over directory offset 12H, the low byte of the access-password field which LDOS 5.3 zeroes.
4F89
INC HL 23
INCrement Register Pair HL by 1, stepping over directory offset 13H, the high byte of that field. Register Pair HL now addresses offset 14H, the low byte of the ending record number.
4F8A
LD C,02H 0E 02
Load Register C with 2, the number of remaining bytes to copy. Register B is already zero because the previous block move counted Register Pair BC down to zero, so Register Pair BC holds 2.
4F8C
LDIR ED B0
Block move: copy the two bytes of the ending record number at directory offsets 14H and 15H, addressed by Register Pair HL, into the caller's buffer addressed by Register Pair DE. The record written is eighteen bytes long and Register Pair DE is left ready for the next one.
4F8E
RET C9
Return through the address pushed at 4F6AH, sending control to the iterator at 4F2CH to step on to the next Hash Index Table slot.

4F8FH - Directory Display Path

The abbreviated directory display. Only in-use primary records for files that are neither system nor invisible are shown. Each file specification is built into the scratch area at 4483H and sent to @DSPLY, four to a line and fifteen lines to a screen; when a screen fills, the routine waits for a keypress and clears the screen before going on. The two counters are self-modifying operands rather than variables.

4F8F
AND 0D8H E6 D8
Mask the attribute byte in Register A, loaded at 4F79H, down to bits 7, 6, 4 and 3: the secondary extended, system, in-use and invisible flags.
4F91
XOR 10H EE 10
Exclusive-OR Register A with 10H, so the result is zero only for an in-use primary record belonging to a file that is neither system nor invisible. Zero sets the Z FLAG; anything else sets the NZ FLAG.
4F93
RET NZ C0
If the NZ FLAG (Not Zero) has been set, this record is not one an abbreviated directory shows, so return through the address pushed at 4F6AH to the iterator at 4F2CH.
4F94
LD DE,4483H 11 83 44
Point Register Pair DE to 4483H, three bytes into CFCB$, the scratch area in which the fifteen-character file specification and its 03H terminator are assembled. The three bytes below it hold the extension mask and must not be overwritten.
4F97
PUSH DE D5
Save that scratch address on the stack, because the builder advances Register Pair DE and the address is needed again as the start of the string to display.
4F98
GOSUB to the file specification builder at 4FE2H, which reads the name and extension from the directory record addressed by Register Pair HL and writes a fifteen-byte blank-padded NAME/EXT:d string plus an 03H terminator into the scratch area addressed by Register Pair DE.
4F9B
POP HL E1
Restore the scratch address 4483H from the stack into Register Pair HL, where @DSPLY expects the address of the string to display.
4F9C
GOSUB to the resident @DSPLY vector at 4467H in SYS0/SYS, which sends the string addressed by Register Pair HL to the video display until it reaches the 03H terminator. Four fifteen-character entries fill sixty of the sixty-four columns of a Model I line.
4F9F
LD A,00H 3E 00
Self-Modifying Code
Load Register A with the operand at 4FA0H, the count of file specifications still to go on the current display line. The listing shows the initial value 00H; at run time it is 4 when a display request starts, set at 4F0BH, and is reloaded with 4 at 4FA8H.
4FA1
DEC A 3D
DECrement Register A by 1, counting off the entry just displayed. Reaching zero sets the Z FLAG.
4FA2
LD (4FA0H),A 32 A0 4F
Self-Modifying Code
Store the decremented count from Register A back into 4FA0H, the operand of the load at 4F9FH, so that the next entry sees the new value.
4FA5
RET NZ C0
If the NZ FLAG (Not Zero) has been set, the line still has room, so return through the address pushed at 4F6AH to the iterator at 4F2CH without ending the line.
4FA6
LD A,04H 3E 04
Load Register A with 4, the number of file specifications on a full display line, ready to restart the count.
4FA8
LD (4FA0H),A 32 A0 4F
Self-Modifying Code
Store 4 into 4FA0H, the operand of the load at 4F9FH, restarting the per-line count for the line about to begin.
4FAB
LD A,0DH 3E 0D
Load Register A with 0DH, the carriage return code, to end the completed line of four entries.
4FAD
GOSUB to ROM routine at 0033H, the Model I character display routine, which prints the carriage return in Register A and moves the cursor to the start of the next line.
4FB0
LD A,00H 3E 00
Self-Modifying Code
Load Register A with the operand at 4FB1H, the count of display lines still to go on the current screen. The listing shows the initial value 00H; at run time it is 15, set at 4F10H and reloaded at 4FB9H.
4FB2
DEC A 3D
DECrement Register A by 1, counting off the line just finished. Reaching zero sets the Z FLAG.
4FB3
LD (4FB1H),A 32 B1 4F
Self-Modifying Code
Store the decremented line count from Register A back into 4FB1H, the operand of the load at 4FB0H.
4FB6
RET NZ C0
If the NZ FLAG (Not Zero) has been set, the screen is not yet full, so return through the address pushed at 4F6AH to the iterator at 4F2CH.
4FB7
LD A,0FH 3E 0F
Load Register A with 15, the number of directory lines on a full screen, ready to restart the count.
4FB9
LD (4FB1H),A 32 B1 4F
Self-Modifying Code
Store 15 into 4FB1H, the operand of the load at 4FB0H, restarting the per-screen line count for the screen about to begin.
4FBC
GOSUB to ROM routine at 0049H, the Model I wait-for-keypress routine, which does not return until the operator presses a key. This holds the full screen of directory entries until it has been read.
4FBF
JUMP to ROM routine at 01C9H, the Model I clear-screen routine, which blanks the video RAM at 3C00H and homes the cursor. Because this is a jump rather than a call, the clear-screen routine's own RETurn goes to the address pushed at 4F6AH, sending control to the iterator at 4F2CH.
4FC2
POP BC C1
Error Path
Shared exit for a failed directory read, reached from 4ED3H in the @RAMDIR scan and 4F63H in the @DODIR scan. Discard the record number and drive number saved on the stack at 4EC3H or 4F53H so the stack is balanced.
4FC3
RET C9
Return to the RST 28H dispatcher in SYS0/SYS with the error number DIRRD left in Register A, 11H for LDOS error 17, "directory read error", and the NZ condition set.

4FC4H - Extension Filter

Applied to every record when bit 1 of the @DODIR function code is set. The three characters of the mask copied into CFCB$ at 4F06H are compared against the three extension characters at offset 0DH of the directory record. A mask character of 24H, the dollar sign, matches anything; a mask character of 41H or above has bit 5 cleared so that a lower-case letter typed by the operator matches the upper-case letter stored in the directory.

4FC4
BIT 1,A CB 4F
Test bit 1 of Register A, the @DODIR function code loaded at 4F6BH. That bit is set for functions 2 and 3, the two that filter by extension. Clear sets the Z FLAG; set sets the NZ FLAG.
4FC6
RET Z C8
If the Z FLAG (Zero) has been set, no extension filtering was asked for, so return at once with the Z condition, which the caller reads as "record accepted".
4FC7
LD BC,000DH 01 0D 00
Load Register Pair BC with 0DH, the offset within a directory record of the three-character extension field.
4FCA
ADD HL,BC 09
ADD 0DH in Register Pair BC to Register Pair HL, which addresses offset 0 of the directory record in SBUFF$, so that Register Pair HL addresses the first character of the file's extension.
4FCB
LD B,03H 06 03
Load Register B with 3, the number of extension characters to compare, as the loop counter.
4FCD
LD DE,4480H 11 80 44
Point Register Pair DE to CFCB$ at 4480H, the three-character extension mask copied out of the caller's buffer at 4F06H.
4FD0
LD A,(DE) 1A
Loop Start
Load Register A with the current mask character from CFCB$, addressed by Register Pair DE.
4FD1
CP 24H FE 24
Compare Register A, the mask character, against 24H (ASCII: $), the wildcard character. If Register A equals 24H the Z FLAG is set; otherwise the NZ FLAG is set.
4FD3
If the Z FLAG (Zero) has been set, this mask position is a wildcard, so JUMP to 4FDDH to accept the extension character without comparing it.
4FD5
CP 41H FE 41
Compare Register A, the mask character, against 41H (ASCII: A). If Register A is below 41H the CARRY FLAG is set; if it is 41H or above the NO CARRY FLAG is set.
4FD7
If the CARRY FLAG has been set, the mask character is a digit or punctuation rather than a letter, so JUMP to 4FDBH and compare it exactly as given.
4FD9
RES 5,A CB AF
Reset bit 5 of Register A, the mask character, converting a lower-case letter to upper case so that it will match the upper-case extension characters LDOS stores in the directory.
4FDB
CP (HL) BE
Compare Register A, the upper-cased mask character, against the extension character of the directory record addressed by Register Pair HL. If they are equal the Z FLAG is set; otherwise the NZ FLAG is set.
4FDC
RET NZ C0
If the NZ FLAG (Not Zero) has been set, the extension does not match the mask, so return with the NZ condition, which tells the scan at 4F74H to skip this record.
4FDD
INC HL 23
INCrement Register Pair HL by 1 to address the next character of the directory record's extension.
4FDE
INC DE 13
INCrement Register Pair DE by 1 to address the next character of the mask in CFCB$.
4FDF
Loop End
DECrement Register B, the count of extension characters still to compare, and LOOP BACK to 4FD0H if it has not reached zero.
4FE1
RET C9
Return with the Z condition left by the last successful comparison, telling the scan that the record's extension matched the mask.

4FE2H - File Specification Builder

Shared by both services. Register Pair HL addresses offset 0 of a directory record in SBUFF$ and Register Pair DE the place the string is to go. The result is always exactly fifteen bytes followed by an 03H terminator: the name with its trailing blanks removed, a slash and the extension when there is one, a colon, the drive digit and enough blanks to bring the printable part up to a fixed width. Register C carries a running budget of thirteen characters for the name, slash and extension so that the blank padding always makes the total the same.

4FE2
LD A,L 7D
Load Register A with Register L, the low byte of the address of offset 0 of the directory record within SBUFF$.
4FE3
ADD A,05H C6 05
ADD 5 to Register A, moving the pointer from offset 0 of the directory record to offset 5, the first character of the eight-character file name.
4FE5
LD L,A 6F
Put the adjusted low byte back into Register L, so Register Pair HL addresses the first character of the file name inside the 4200H buffer page.
4FE6
LD C,0DH 0E 0D
Load Register C with 13, the fixed width in characters of the name, optional slash and optional extension taken together. Every character actually written to that part of the string decrements this budget, and whatever is left is made up with blanks.
4FE8
LD B,08H 06 08
Load Register B with 8, the number of characters in the directory record's name field, as the loop counter.
4FEA
LD A,(HL) 7E
Loop Start
Load Register A with the current name character from the directory record addressed by Register Pair HL.
4FEB
INC HL 23
INCrement Register Pair HL by 1 so that it addresses the next character of the name field.
4FEC
CP 20H FE 20
Compare Register A, the name character, against 20H (ASCII: space), which pads a name shorter than eight characters. If it is a blank the Z FLAG is set; otherwise the NZ FLAG is set.
4FEE
If the Z FLAG (Zero) has been set, the name has ended, so JUMP to 4FF7H to skip past the remaining blanks of the name field.
4FF0
LD (DE),A 12
Store the name character from Register A into the output string at the position Register Pair DE addresses.
4FF1
INC DE 13
INCrement Register Pair DE by 1 so that it addresses the next position in the output string.
4FF2
DEC C 0D
DECrement Register C, the thirteen-character budget, by 1 to account for the character just written.
4FF3
Loop End
DECrement Register B, the count of name characters left, and LOOP BACK to 4FEAH if it has not reached zero.
4FF5
All eight name characters were used, so Register Pair HL already addresses offset 0DH of the directory record. JUMP to 4FFBH to deal with the extension.
4FF7
LD A,L 7D
Load Register A with Register L, the low byte of the pointer which is standing just past the blank that ended the name.
4FF8
ADD A,B 80
ADD Register B, the count of name-field characters that had still to be examined including the blank just found, to Register A.
4FF9
DEC A 3D
DECrement Register A by 1, because the pointer had already been stepped past that blank. Register A now holds the low byte of the address of offset 0DH, the first character of the extension.
4FFA
LD L,A 6F
Put the adjusted low byte back into Register L, so Register Pair HL addresses the extension field of the directory record.
4FFB
LD A,(HL) 7E
Load Register A with the first character of the directory record's three-character extension field.
4FFC
CP 20H FE 20
Compare Register A, the first extension character, against 20H (ASCII: space). A blank here means the file has no extension. If it is blank the Z FLAG is set; otherwise the NZ FLAG is set.
4FFE
If the Z FLAG (Zero) has been set, there is no extension, so JUMP to 5012H to append the colon and drive digit directly after the name.
5000
LD A,2FH 3E 2F
Load Register A with 2FH (ASCII: /), the separator LDOS puts between a file name and its extension.
5002
LD (DE),A 12
Store the slash from Register A into the output string at the position Register Pair DE addresses.
5003
INC DE 13
INCrement Register Pair DE by 1 to address the next position in the output string.
5004
DEC C 0D
DECrement Register C, the thirteen-character budget, by 1 to account for the slash.
5005
LD B,03H 06 03
Load Register B with 3, the number of characters in the directory record's extension field, as the loop counter.
5007
LD A,(HL) 7E
Loop Start
Load Register A with the current extension character from the directory record addressed by Register Pair HL.
5008
INC HL 23
INCrement Register Pair HL by 1 so that it addresses the next character of the extension field.
5009
CP 20H FE 20
Compare Register A, the extension character, against 20H (ASCII: space), the pad character for an extension shorter than three characters. If it is blank the Z FLAG is set; otherwise the NZ FLAG is set.
500B
If the Z FLAG (Zero) has been set, the extension has ended, so JUMP to 5012H to append the colon and drive digit.
500D
LD (DE),A 12
Store the extension character from Register A into the output string at the position Register Pair DE addresses.
500E
INC DE 13
INCrement Register Pair DE by 1 to address the next position in the output string.
500F
DEC C 0D
DECrement Register C, the thirteen-character budget, by 1 to account for the extension character just written.
5010
Loop End
DECrement Register B, the count of extension characters left, and LOOP BACK to 5007H if it has not reached zero.
5012
LD A,3AH 3E 3A
Load Register A with 3AH (ASCII: :), the separator that introduces the drive number in an LDOS file specification.
5014
LD (DE),A 12
Store the colon from Register A into the output string at the position Register Pair DE addresses. The colon and the drive digit are outside the thirteen-character budget, which is why every string comes out the same length.
5015
INC DE 13
INCrement Register Pair DE by 1 to address the next position in the output string.
5016
LD A,00H 3E 00
Self-Modifying Code
Load Register A with the operand at 5017H, the ASCII drive digit. The listing shows the initial value 00H; at run time it is the drive number ORed with 30H, written there by the drive-ready test at 50B1H before any directory work begins.
5018
LD (DE),A 12
Store the drive digit from Register A into the output string at the position Register Pair DE addresses.
5019
INC DE 13
INCrement Register Pair DE by 1 to address the next position in the output string.
501A
LD A,20H 3E 20
Loop Start
Load Register A with 20H (ASCII: space), the character used to pad the string out to its fixed width.
501C
LD (DE),A 12
Store a blank from Register A into the output string at the position Register Pair DE addresses.
501D
INC DE 13
INCrement Register Pair DE by 1 to address the next position in the output string.
501E
DEC C 0D
DECrement Register C, the thirteen-character budget, by 1. Because a full name and extension use only twelve of the thirteen, at least one blank is always written.
501F
Loop End
If the NZ FLAG (Not Zero) has been set, the budget in Register C is not yet exhausted, so LOOP BACK to 501AH to write another blank.
5021
LD A,03H 3E 03
Load Register A with 03H, the end-of-text code that terminates a string for @DSPLY and the other LDOS display routines.
5023
LD (DE),A 12
Store the 03H terminator into the output string at the position Register Pair DE addresses. Register Pair DE is deliberately not advanced, so the caller gets back the address of the terminator; the @RAMDIR builder overwrites it with the file's access level.
5024
RET C9
Return with Register Pair DE addressing the 03H terminator, fifteen bytes on from where the string began.

5025H - Capacity and Free-Space Calculator

Reads the Granule Allocation Table for the drive in Register C and writes two words into the buffer addressed by Register Pair HL: the total capacity of the diskette in kilobytes and the free space in kilobytes. The capacity comes from the drive geometry in the drive control table, the free space from counting zero bits in the allocation table itself. The count of cylinders to examine is taken from the configuration byte at Granule Allocation Table offset CCH, which holds the cylinder count less 35.

5025
GOSUB to the Granule Allocation Table reader at 508BH, which reads sector 0 of the directory cylinder of the drive in Register C into 5100H through 51FFH and leaves 14H in Register A, LDOS error 20, if the read fails.
5028
RET NZ C0
If the NZ FLAG (Not Zero) has been set, the Granule Allocation Table could not be read, so return to the caller with error 20 in Register A.
5029
PUSH IY FD E5
Save Register Pair IY on the stack. The resident file-operation prologue left it undisturbed for the caller, and this routine is about to overwrite it with a drive control table address.
502B
GOSUB to GETDCT at 478FH in SYS0/SYS, which sets Register Pair IY to 4700H plus ten times the drive number in Register C, the drive control table entry for this drive, and preserves Register Pair HL.
502E
EX DE,HL EB
Exchange Register Pair DE and Register Pair HL, moving the caller's buffer address out of Register Pair HL into Register Pair DE so that Register Pair HL can be used for the cylinder count.
502F
LD H,00H 26 00
Load Register H with zero, clearing the high half of Register Pair HL so it can hold a cylinder count of up to 255.
5031
LD L,(IY+06H) FD 6E 06
Load Register L with the byte at offset 6 of the drive control table entry addressed by Register Pair IY, the highest cylinder number the drive is configured for.
5034
INC HL 23
INCrement Register Pair HL by 1, turning the highest cylinder number into the number of cylinders, since they are numbered from zero.
5035
LD A,(IY+08H) FD 7E 08
Load Register A with the byte at offset 8 of the drive control table entry, whose bits 7 to 5 hold the granules per track less one and whose bits 4 to 0 hold the sectors per granule less one.
5038
AND 1FH E6 1F
Mask Register A down to bits 4 to 0, leaving the sectors per granule less one.
503A
INC A 3C
INCrement Register A by 1, giving the true number of sectors in a granule.
503B
PUSH AF F5
Save Register A, the sectors per granule, on the stack. It is needed again at 5072H to convert the free granule count into free sectors.
503C
PUSH DE D5
Save Register Pair DE, the caller's buffer address, on the stack, since Register Pair DE is about to be reused by the multiply helper.
503D
LD E,A 5F
Copy the sectors-per-granule figure from Register A into Register E, where the eight-by-eight multiply helper expects one of its operands.
503E
LD A,(IY+08H) FD 7E 08
Load Register A again with the byte at offset 8 of the drive control table entry, this time to extract its upper field.
5041
AND 0E0H E6 E0
Mask Register A down to bits 7 to 5, leaving the granules per track less one still sitting at the top of the register.
5043
RLCA 07
Rotate Register A Left: bit 7 of the granules-per-track field moves into bit 0. This is the first of three rotations that bring the field down to the bottom of the register.
5044
RLCA 07
Rotate Register A Left again, continuing to shift the granules-per-track field toward bit 0.
5045
RLCA 07
Rotate Register A Left a third time. Register A now holds the granules per track less one as a plain number in bits 2 to 0.
5046
INC A 3C
INCrement Register A by 1, giving the true number of granules on a track.
5047
GOSUB to the eight-by-eight multiply helper MULTEA at 4B6CH in SYS0/SYS, which returns in Register A the product of Register A, the granules per track, and Register E, the sectors per granule. Register A now holds the number of sectors on one track.
504A
BIT 5,(IY+04H) FD CB 04 6E
Test bit 5 of the byte at offset 4 of the drive control table entry addressed by Register Pair IY, which is set when the drive is double-sided. Clear sets the Z FLAG; set sets the NZ FLAG.
504E
If the Z FLAG (Zero) has been set, the drive is single-sided and one track is one cylinder, so JUMP over the doubling to 5051H.
5050
ADD A,A 87
ADD Register A to itself, doubling the sectors-per-track figure in Register A to give the sectors per cylinder on a double-sided drive.
5051
POP BC C1
Restore the caller's buffer address, saved at 503CH, from the stack into Register Pair BC, where the store routine below expects to find its destination.
5052
GOSUB to the kilobyte conversion and store routine at 5075H with Register Pair HL holding the number of cylinders, Register A the sectors per cylinder and Register Pair BC the destination address. It multiplies the two, converts the sector total to kilobytes and stores the result as a word at the caller's buffer, returning Register Pair HL addressing the high byte it wrote.
5055
INC HL 23
INCrement Register Pair HL by 1 so that it addresses the third byte of the caller's buffer, where the free-space word will go.
5056
PUSH HL E5
Save that address on the stack; it is recovered at 5071H as the destination for the free-space word.
5057
LD HL,5100H 21 00 51
Point Register Pair HL to 5100H, the first byte of the Granule Allocation Table image read in at 5025H. The table holds one byte per cylinder, each bit standing for one granule, set when the granule is allocated.
505A
LD DE,0000H 11 00 00
Clear Register Pair DE to zero to start the running count of free granules.
505D
LD A,(51CCH) 3A CC 51
Load Register A from 51CCH, offset CCH of the Granule Allocation Table image, the low byte of the disk configuration word. It holds the number of cylinders on the diskette less 35. The adjacent high byte at offset CDH carries the disk-format flags: bit 5 eight inch, bit 6 double density, bits 2 to 0 the granules per track, bit 4 set when cylinder 0 alone is single density on a dual density boot disk, bit 3 marking a disk already in the LDOS 5.3 x.3 extended-dating format, and bit 7 exempting it from the DATECONV/CMD system-disk check.
5060
ADD A,23H C6 23
ADD 35 to Register A, restoring the true number of cylinders, and therefore the number of allocation-table bytes to examine.
5062
LD B,A 47
Copy that cylinder count from Register A into Register B as the outer loop counter, one pass per allocation-table byte.
5063
LD A,(HL) 7E
Loop Start
Load Register A with the allocation byte for the current cylinder from the Granule Allocation Table image addressed by Register Pair HL. Each set bit is an allocated granule; the unused high bits of a partly used byte are set as well, which is what makes the inner loop terminate.
5064
SCF 37
Loop Start
Set the Carry flag, so that the rotate below shifts a one into the top of the allocation byte.
5065
RRA 1F
Rotate Register A Right through the carry: bit 0, the granule being examined, moves into the carry flag and the one just set moves into bit 7. Repeating this both walks the granule bits and drives the byte toward 0FFH.
5066
If the CARRY FLAG has been set, the granule that came out of bit 0 was allocated, so JUMP to 5069H without counting it.
5068
INC DE 13
INCrement Register Pair DE by 1, adding this free granule to the running count.
5069
CP 0FFH FE FF
Compare Register A, the allocation byte being rotated, against 0FFH. Every rotation shifts in another one from the top, so the byte reaches 0FFH exactly when all of its granule bits have been examined. Equal sets the Z FLAG; otherwise the NZ FLAG is set.
506B
Loop End
If the NZ FLAG (Not Zero) has been set, granule bits remain in this cylinder's byte, so LOOP BACK to 5064H to examine the next one.
506D
INC L 2C
INCrement Register L by 1 so that Register Pair HL addresses the allocation byte for the next cylinder. Only Register L is stepped because the table never leaves the 5100H page.
506E
Loop End
DECrement Register B, the count of cylinders still to examine, and LOOP BACK to 5063H if it has not reached zero.
5070
EX DE,HL EB
Exchange Register Pair DE and Register Pair HL, moving the total count of free granules into Register Pair HL where the multiply helper expects it.
5071
POP BC C1
Restore the address of the third byte of the caller's buffer, saved at 5056H, into Register Pair BC as the destination for the free-space word.
5072
POP AF F1
Restore Register A with the sectors-per-granule figure saved at 503BH, the multiplier that turns free granules into free sectors.
5073
POP IY FD E1
Restore Register Pair IY from the stack, giving the caller back the value saved at 5029H. Execution now falls straight into the conversion and store routine below, which therefore serves both as a subroutine and as the tail of this one.

5075H - Kilobyte Conversion and Store

Entered twice for every free-space request: once as a subroutine from 5052H, with Register Pair HL holding the number of cylinders and Register A the sectors per cylinder, and once by falling through from 5073H, with Register Pair HL holding the number of free granules and Register A the sectors per granule. Either way the product is a count of 256-byte sectors, which is converted to kilobytes by adding two and dividing by four, and stored as a word at the address in Register Pair BC.

5075
GOSUB to the resident @MULT vector at 44C1H in SYS0/SYS, which forms the 24-bit product of the sixteen-bit value in Register Pair HL and the eight-bit value in Register A, returning the high sixteen bits in Register Pair HL and the low eight bits in Register A. A diskette never holds more than 65535 sectors, so the whole product is the pair Register L and Register A.
5078
LD H,B 60
Load Register H with Register B, the high byte of the destination address passed in Register Pair BC, beginning to reassemble that address in Register Pair HL.
5079
LD D,L 55
Load Register D with Register L, the high byte of the sector total, moving it into Register Pair DE before Register L is overwritten.
507A
LD L,C 69
Load Register L with Register C, the low byte of the destination address. Register Pair HL now addresses the place in the caller's buffer where the word is to be stored.
507B
LD E,A 5F
Load Register E with Register A, the low byte of the sector total. Register Pair DE now holds the whole count of sectors.
507C
INC DE 13
INCrement Register Pair DE, the sector total, by 1. This is the first half of adding two before the division by four, which rounds the kilobyte figure to the nearest whole K instead of always truncating.
507D
INC DE 13
INCrement Register Pair DE by 1 again, completing the rounding adjustment.
507E
SRL D CB 3A
Shift Register D, the high byte of the adjusted sector total, Right one place with a zero entering bit 7 and bit 0 falling into the carry, beginning the first division by two.
5080
RR E CB 1B
Rotate Register E, the low byte of the adjusted sector total, Right through the carry, bringing in the bit that fell out of Register D and completing the first division by two.
5082
SRL D CB 3A
Shift Register D Right again, beginning the second division by two of the value in Register Pair DE.
5084
RR E CB 1B
Rotate Register E Right through the carry to complete it. Register Pair DE now holds the sector total divided by four, which is the size in kilobytes because four 256-byte sectors make one K.
5086
LD (HL),E 73
Store Register E, the low byte of the kilobyte figure, into the caller's buffer at the address Register Pair HL holds.
5087
INC HL 23
INCrement Register Pair HL by 1 to address the second byte of the word.
5088
LD (HL),D 72
Store Register D, the high byte of the kilobyte figure, into the caller's buffer, completing the word.
5089
XOR A AF
Set Register A to ZERO and clear all flags, setting the Z FLAG to report success and clearing the carry, which the free-space caller relies on when it subtracts with borrow at 4E2BH.
508A
RET C9
Return with Register Pair HL addressing the high byte just stored and Register Pair DE holding the kilobyte figure.

508BH - Read the Granule Allocation Table

Fetches sector 0 of the drive's directory cylinder, the Granule Allocation Table, into the scratch page at 5100H. Register C names the drive. The error code is loaded after the read so that it does not disturb the flags the read routine returned.

508B
PUSH DE D5
Save Register Pair DE, which the callers use for the output buffer address, on the stack across the read.
508C
PUSH HL E5
Save Register Pair HL, which the callers use for the buffer address or for working values, on the stack across the read.
508D
GOSUB to DIRCYL at 4B65H in SYS0/SYS, which reads offset 9 of the drive control table entry for the drive in Register C and returns the directory cylinder number in Register D.
5090
LD HL,5100H 21 00 51
Point Register Pair HL to 5100H, the scratch page immediately above the SYS12/SYS image, as the destination for the sector.
5093
LD E,L 5D
Load Register E with Register L, which is zero because the buffer address is page-aligned. Register E is the sector number, and sector 0 of the directory cylinder is the Granule Allocation Table.
5094
GOSUB to RDSSEC at 4B45H in SYS0/SYS, which reads the sector named by Register D, the directory cylinder, and Register E, sector 0, from the drive in Register C into the buffer addressed by Register Pair HL, retrying once before giving up.
5097
LD A,14H 3E 14
Load Register A with 14H, LDOS error number 20, "GAT read error", the code the caller returns when the read failed. This load does not disturb the flags RDSSEC set.
5099
POP HL E1
Restore Register Pair HL from the stack, giving the caller back the value saved at 508CH.
509A
POP DE D1
Restore Register Pair DE from the stack, giving the caller back the value saved at 508BH.
509B
RET C9
Return with the Z condition and the Granule Allocation Table in 5100H on success, or with the NZ condition and error 20 in Register A on failure.

509CH - Read the Hash Index Table

Fetches sector 1 of the drive's directory cylinder, the Hash Index Table, into the same scratch page at 5100H. The table holds one byte per directory record: the hash of the file name occupying that record, or zero when the record is free. Both directory scans use it to avoid reading directory sectors that hold nothing.

509C
LD HL,5100H 21 00 51
Point Register Pair HL to 5100H, the scratch page above the SYS12/SYS image, as the destination for the sector.
509F
PUSH BC C5
Save Register Pair BC, holding the caller's record number in Register B and the drive number in Register C, on the stack across the read.
50A0
PUSH DE D5
Save Register Pair DE, holding the caller's output buffer address, on the stack across the read.
50A1
GOSUB to DIRCYL at 4B65H in SYS0/SYS, which returns in Register D the directory cylinder number taken from offset 9 of the drive control table entry for the drive in Register C.
50A4
LD E,01H 1E 01
Load Register E with 1, the sector number of the Hash Index Table on the directory cylinder.
50A6
GOSUB to RDSSEC at 4B45H in SYS0/SYS, which reads cylinder Register D, sector Register E, from the drive in Register C into the buffer addressed by Register Pair HL.
50A9
POP DE D1
Restore Register Pair DE from the stack, giving the caller back the output buffer address saved at 50A0H.
50AA
POP BC C1
Restore Register Pair BC from the stack, giving the caller back the record number and drive number saved at 509FH.
50AB
LD A,16H 3E 16
Load Register A with 16H, LDOS error number 22, "HIT read error", the code the caller returns when the read failed. This load does not disturb the flags RDSSEC set.
50AD
RET C9
Return with the Z condition and the Hash Index Table in 5100H on success, or with the NZ condition and error 22 in Register A on failure.

50AEH - Drive Ready Test

Called by both services before any directory work. It plants the ASCII drive digit in the file specification builder, checks that the unit really has a drive control block, seeks the drive to the cylinder its block says the head is already on, and then watches the WD1771 index-pulse status bit for a complete pulse cycle. A diskette that is present and turning produces that cycle within a few clock ticks; if twenty ticks pass without it, error 8 is returned.

50AE
LD A,C 79
Load Register A with Register C, the drive number 0 to 7 the caller asked for.
50AF
OR 30H F6 30
OR the drive number in Register A with 30H, converting the binary number into its ASCII digit.
50B1
LD (5017H),A 32 17 50
Self-Modifying Code
Store the ASCII drive digit from Register A into 5017H, the operand of the LD A,00H at 5016H inside the file specification builder. Every specification the builder produces from now on carries this drive digit after its colon.
50B4
PUSH IY FD E5
Save Register Pair IY on the stack before it is loaded with a drive control table address, so the caller's value survives.
50B6
GOSUB to GETDCT at 478FH in SYS0/SYS, which sets Register Pair IY to 4700H plus ten times the drive number in Register C, the drive control table entry for this unit.
50B9
LD A,(IY+00H) FD 7E 00
Load Register A with the byte at offset 0 of that drive control table entry. A real drive block begins with a three-byte JP 45FBH, so this byte is C3H for units 0 to 3; units 4 to 7 fall on the stub area at 4728H onward where the byte is C9H, a RET.
50BC
SUB 0C3H D6 C3
SUBtract C3H, the opcode of a JP instruction, from Register A. A real drive block leaves Register A zero and sets the Z FLAG; one of the RET stubs for units 4 to 7 leaves 06H and sets the NZ FLAG.
50BE
If the NZ FLAG (Not Zero) has been set, this unit has no drive control block, so JUMP to 50E9H to restore Register Pair IY and return the non-zero difference in Register A as the failure status.
50C0
PUSH HL E5
Save Register Pair HL, holding the caller's buffer address, on the stack across the seek and the index-pulse wait.
50C1
LD D,(IY+05H) FD 56 05
Load Register D with the byte at offset 5 of the drive control table entry, the cylinder the head is currently believed to be on. Seeking to it costs nothing if the belief is right and re-homes the drive if it is not.
50C4
LD E,A 5F
Load Register E with Register A, which the subtraction at 50BCH left holding zero, so the seek names sector 0 of that cylinder.
50C5
GOSUB to the SEEK primitive at 475EH in SYS0/SYS, which loads operation code 06H, records the drive from Register C in LDRV$ at 4308H, fetches the drive control block and runs the WD1771 seek to the cylinder in Register D.
50C8
EI FB
Enable interrupts, which the disk primitive disabled while it drove the WD1771. The wait below depends on the real-time clock interrupt continuing to advance the tick counter at 4040H, so interrupts must be back on before it starts.
50C9
GOSUB to the RSELCT primitive at 4759H in SYS0/SYS, operation code 07H, which re-selects the drive in Register C and returns the WD1771 status register in Register A.
50CC
BIT 4,(IY+04H) FD CB 04 66
Test bit 4 of the byte at offset 4 of the drive control table entry addressed by Register Pair IY. When that bit is set the unit is not a rotating floppy whose index pulse can be watched, so the test below is skipped. Clear sets the Z FLAG; set sets the NZ FLAG.
50D0
If the NZ FLAG (Not Zero) has been set, no index-pulse test is wanted for this unit, so JUMP to 50E7H and report the drive ready.
50D2
LD A,(4040H) 3A 40 40
Load Register A with the real-time clock tick counter TIMER$ at 4040H, which the RST 38H interrupt handler in SYS0/SYS advances on every clock tick.
50D5
ADD A,14H C6 14
ADD 20 to the current tick count in Register A, forming the tick value at which the index-pulse wait is to be abandoned.
50D7
LD D,A 57
Copy that deadline tick value from Register A into Register D, where the sampling routine at 50ECH compares it against the live counter.
50D8
Loop Start
GOSUB to the index-pulse sampler at 50ECH, which returns Z when the WD1771 index bit is clear, NZ when it is set, and abandons the whole test with error 8 if the deadline in Register D has been reached.
50DB
Loop End
If the NZ FLAG (Not Zero) has been set, the index hole is still under the sensor, so LOOP BACK to 50D8H and keep sampling until the pulse ends.
50DD
Loop Start
GOSUB to the index-pulse sampler at 50ECH again, this time waiting for the pulse to arrive.
50E0
Loop End
If the Z FLAG (Zero) has been set, the index bit is still clear, so LOOP BACK to 50DDH until the index hole comes round.
50E2
Loop Start
GOSUB to the index-pulse sampler at 50ECH a third time, waiting for the pulse just seen to pass. Seeing the bit go clear, then set, then clear again proves the diskette is turning rather than that the bit happened to be sitting in one state.
50E5
Loop End
If the NZ FLAG (Not Zero) has been set, the index hole is still under the sensor, so LOOP BACK to 50E2H until the pulse ends.
50E7
XOR A AF
Set Register A to ZERO and clear all flags, setting the Z FLAG to report that the drive is ready with no error number.
50E8
POP HL E1
Restore Register Pair HL from the stack, giving back the caller's buffer address saved at 50C0H. The timeout path at 50FCH also enters here after forcing error 8 into Register A.
50E9
POP IY FD E1
Restore Register Pair IY from the stack, giving the caller back the value saved at 50B4H. The missing-drive path at 50BEH also enters here, with the non-zero difference still in Register A.
50EB
RET C9
Return to the caller with Register A zero and the Z FLAG set when the drive is ready, or with a non-zero error number and the NZ condition when it is not.

50ECH - Index Pulse Sample

One sample of the WD1771 index-pulse status bit, with the deadline test folded in. When the tick counter has reached the deadline in Register D the routine does not return normally at all: it pops its caller's return address off the stack and jumps into the middle of the ready test's exit sequence, so that a stalled drive unwinds cleanly with error 8 no matter which of the three waits was running.

50EC
LD A,(4040H) 3A 40 40
Load Register A with the real-time clock tick counter TIMER$ at 4040H, advanced by the RST 38H interrupt handler in SYS0/SYS on every tick.
50EF
CP D BA
Compare Register A, the live tick count, against Register D, the deadline computed at 50D5H as twenty ticks after the wait began. If they are equal the Z FLAG is set; otherwise the NZ FLAG is set.
50F0
If the Z FLAG (Zero) has been set, twenty ticks have passed without the expected index-pulse transition, so JUMP to 50F8H to abandon the whole test.
50F2
GOSUB to the RSELCT primitive at 4759H in SYS0/SYS, operation code 07H, which re-selects the drive named in Register C and returns the WD1771 status register in Register A.
50F5
BIT 1,A CB 4F
Test bit 1 of Register A, the WD1771 Type I status register just read. That bit is the index-pulse indicator: it is set while the index hole is passing the sensor and clear at all other times. Clear sets the Z FLAG; set sets the NZ FLAG.
50F7
RET C9
Return to the wait loop with the Z condition when the index bit is clear and the NZ condition when it is set.
50F8
POP AF F1
Error Path
Discard the return address that the wait loop's CALL left on the stack, by popping it into Register Pair AF, so that this routine can leave without returning to whichever of the three waits invoked it.
50F9
LD A,08H 3E 08
Load Register A with 8, LDOS error number 8, "device not available", the code that reports a drive with no diskette turning in it.
50FB
OR A B7
OR Register A with itself, which changes nothing but sets the NZ FLAG because Register A holds 8, marking the return as a failure.
50FC
JUMP to 50E8H, the exit sequence of the drive ready test, which restores Register Pair HL and Register Pair IY and returns to the service with error 8 in Register A. This is the last byte of the SYS12/SYS image, which ends at 50FDH.