TRS-80 DOS - LDOS 5.3.1 for the Model I - FDUBL/CMD Disassembled

Page Customization

Summary:

LDOS 5.3.1 FDUBL/CMD Disassembly - Double-Density Floppy Driver Installer (Model I)

FDUBL/CMD is the LDOS 5.3.1 utility that installs a resident double-density floppy disk driver on a TRS-80 Model I fitted with a double-density adapter board. The Model I Expansion Interface carries a Western Digital 1771 floppy disk controller, which can only record in single density (FM). A doubler board adds a Western Digital 1791 controller alongside it and provides a latch that decides which of the two chips answers at the memory-mapped controller addresses 37ECH to 37EFH. FDUBL supplies the software half of that arrangement: a replacement floppy driver that sets the density latch before every controller operation, understands both recording densities, and falls back to the other density automatically when a sector cannot be found.

The utility supports the two incompatible doubler designs sold for the Model I. The default is the Radio Shack Double-Density Adapter Kit (catalogue number 26-1143), whose control latch is driven by the three high-order bits of a write to the controller's sector register at 37EEH; those bits feed a one-of-eight decoder on the adapter board that latches both the density selection and the write-precompensation enable. The alternative, selected with the PERCOM parameter, is the Percom Doubler, whose latch is set by writing an otherwise meaningless command byte of 0FEH or 0FFH to the controller's command register at 37ECH and then cancelling it with a Force Interrupt. The Percom, LNW and Omikron boards do not implement write precompensation, so when the Percom variant is built FDUBL patches its own precompensation routine out of the driver entirely.

FDUBL is a transient command: it runs at 5200H, builds the requested variant of a driver module in its own image, relocates that module into high memory below HIGH$ (4049H), and lowers HIGH$ to protect it. The module carries the standard LDOS resident-module header, so it is named and discoverable through the module lookup service (RST 28H request code 0BCH, serviced by SYS10). Its name is $FDD. Once installed, FDUBL walks the Drive Control Table at 4700H and rewrites the driver address inside each drive's JP vector so that every disk operation on every fitted drive is routed through the new module instead of through the resident single-density driver at 45FBH in SYS0.

The driver module itself is a modified copy of the resident SYS0 floppy driver. Track positioning, drive selection and Type I command issue are still performed by the resident routines at 45E0H, 461CH, 462AH, 462DH and 4659H, and the controller status is still translated to an LDOS error number by the resident decoder at 46F4H. What FDUBL adds is the density latch write before each operation, a write-precompensation decision on the Radio Shack board, an unrolled data-request polling loop fast enough to keep up with the doubled MFM data rate, a density-dependent choice of the deleted-data-mark write command (0ABH on the 1771 in FM, 0A9H on the 1791 in MFM), and an error recovery path that toggles the density flag and re-seeds the drive geometry so a single-density diskette can be read on a double-density system and the other way round.

The distributed file carries the applied patch FDUBL1. The patch record set replaces the instruction at 5265H and adds a nine-byte routine at 5574H whose only job is to write 2CH into the relative-jump displacement inside the module header at 540DH. That displacement is the driver's entry point. In the Radio Shack build the header sits at 540CH and the original displacement of 41H correctly reaches the driver dispatcher at 544FH; in the Percom build the header is moved up to 5421H, 21 bytes higher, so the displacement has to be reduced by the same 21 bytes to reach the same dispatcher. Without the patch the Percom variant entered its driver 21 bytes past the dispatcher, in the middle of the operation-decoding chain. FDUBL1 is therefore the fix that makes Percom mode work at all.

Program Structure

The file loads as a single contiguous image from 5200H to 557CH, with a transfer address of 521EH. Everything from 5415H upward is either part of the resident module or install-time source material for it; everything below 5415H is install-time code, tables and messages that are discarded once the module is in place.

Address RangeContents
5200H-521DH
30 bytes
Relocation address table. Fifteen 2-byte entries, each holding the address of an instruction inside the driver module whose 16-bit operand must be adjusted when the module is moved to high memory. The Percom build uses only the first fourteen.
521EH-530AH
237 bytes
Install-time code. Banner display, parameter scan, optional construction of the Percom variant, duplicate-installation check, operand relocation, the move into high memory and the Drive Control Table hook-up.
530BH-5332H
40 bytes
Exit paths. Success return, the JCL-aware abort test, and the two error reports.
5333H-5353H
33 bytes
@PARAM keyword table for the PERCOM and TANDY parameters.
5354H-535CH
9 bytes
Install-time control variables: the TANDY flag, the module length, start address, end address and relocation entry count.
535DH-540BH
175 bytes
Message text: the two error messages and the sign-on banner.
540CH-5414H
9 bytes
Resident module header for the module named $FDD. This is the base of the Radio Shack build.
5415H-5420H
12 bytes
Radio Shack write-precompensation routine. Present in the Radio Shack build only; in the Percom build the module header is copied over the bytes above it and this routine falls outside the module.
5421H-5429H
9 bytes
Radio Shack build: the tail of the precompensation routine and the start of the seek hand-off. Percom build: the relocated copy of the module header, and the base of the module.
542AH-5557H
302 bytes
The driver body proper. Density toggle and geometry reseed, the operation dispatcher, read/write sector setup, the inline parameter block reader, and the command issue and data transfer engine.
5558H-5563H
12 bytes
Density latch routine. Holds the Radio Shack version as assembled; the Percom version is copied over it at install time. This is the last byte of the Radio Shack module.
5564H-5573H
16 bytes
Source copy of the Percom density latch routine. Install-time only; it is copied down to 5558H and the original is discarded.
5574H-557CH
9 bytes
The FDUBL1 patch routine, which sets the module header's entry displacement for the Percom build. Install-time only.

Variables and Self-Modified Cells

FDUBL modifies its own image extensively while it decides which variant to build. Every cell below is written by install-time code before the module is relocated, so the value present in the disk file is the Radio Shack default and the value the driver actually runs with may be different.

AddressPurpose
523CH-523DH
2 bytes
PERCOM parameter flag. This is the operand of the LD HL,0000H instruction at 523BH. @PARAM writes 0FFFFH here when PERCOM or P is supplied on the command line. Self-modifying code.
5354H-5355H
2 bytes
TANDY parameter flag. @PARAM writes 0FFFFH here when TANDY or T is supplied. It is only ever tested to reject the combination of both parameters.
5356H-5357H
2 bytes
Module length in bytes. Assembled as 0158H for the Radio Shack build; overwritten with 0147H for the Percom build. Used as the LDDR byte count when the module is copied to high memory.
5358H-5359H
2 bytes
Module start address. Assembled as 540CH for the Radio Shack build; overwritten with 5421H for the Percom build. Held for reference only; the copy is driven from the end address.
535AH-535BH
2 bytes
Module end address, the last byte to be copied. Assembled as 5563H for the Radio Shack build; overwritten with 5567H for the Percom build. Used both as the LDDR source pointer and as the base from which the relocation offset is computed.
535CH
1 byte
Relocation entry count. Assembled as 0FH (15) for the Radio Shack build; decremented to 0EH (14) for the Percom build, because the precompensation routine that the fifteenth entry refers to is not part of the Percom module.
540DH
1 byte
The relative-jump displacement inside the module header, which is the driver's entry point. Assembled as 41H, correct for a module based at 540CH. The FDUBL1 patch routine writes 2CH here for the Percom build, whose module is based at 5421H. Self-modifying code.
540EH-540FH
2 bytes
Resident module chain link word, stored one byte below the address of the next module header. Assembled as 0000H and filled in by the module lookup chain, which is walked from HIGH$ at 4049H.
542BH-542CH
2 bytes
The operand of the JP 462DH at 542AH, the hand-off into the resident seek routine. Rewritten to 462AH for the Percom build so that the resident routine is entered one step earlier, at its controller-ready wait. Self-modifying code.
545DH
1 byte
The displacement of the JR Z,5415H at 545CH, which sends a seek operation to the precompensation routine. Rewritten to 0CCH for the Percom build so the jump instead reaches 542AH and skips precompensation entirely. Self-modifying code.
54C3H-54C4H
2 bytes
The operand of the CALL Z,5415H at 54C2H, the implied seek performed before a read or write. Rewritten to 462AH for the Percom build. Self-modifying code.
550AH-550BH
2 bytes
The two-byte body of the sector data transfer loop, planted at run time by the inline parameter block reader at 54B1H. Holds 1AH 02H (LD A,(DE) then LD (BC),A) for a read, 12H 0AH (LD (DE),A then LD A,(BC)) for a write, and 1AH 7EH (read the data register and discard it) for a verify. Self-modifying code.
46F6H
1 byte
Not part of FDUBL. This is the base error number inside the resident controller status decoder in SYS0, and FDUBL writes it directly through the same inline parameter block reader. 01H for a read or verify, 09H for a write.

Major Routines

AddressName and Purpose
521EHProgram Entry
Entry from DOS with Register Pair HL pointing at the command line tail. Saves the DOS stack pointer into the exit code, displays the banner, and scans the command line through @PARAM. Exits through 530BH.
5243HPercom Variant Construction
Reached only when PERCOM was supplied. Rejects PERCOM together with TANDY, copies the Percom density latch routine over the Radio Shack one, rewrites three operands and one jump displacement to remove write precompensation, and re-bases the module from 540CH to 5421H.
5286HDuplicate Installation Check
Walks all eight Drive Control Table entries, follows each drive's driver vector, and compares the five bytes at driver+4 against the name-length byte and name of the $FDD module. A match means the driver is already resident and the utility aborts.
52B2HOperand Relocation
Computes the distance the module is about to move and walks the relocation address table at 5200H, adding that distance to the 16-bit operand of each listed instruction.
52DCHModule Installation
Copies the module down into high memory ending at HIGH$, lowers HIGH$ by the module length, and points the driver vector of every fitted drive at the relocated module while marking the drive double-density capable.
530BHExit
Restores the DOS stack pointer, returns with Register Pair HL holding 0000H for success or 0FFFFH for failure, and diverts to @ABORT only when a JCL job is in effect.
531DHBoth Doublers Specified Error
Displays "Can't specify both Percom and Tandy!" and exits with an error. Overlaps the routine below it.
5321HDriver Already Resident Error
Displays "Driver is already resident!" and exits with an error. Shares its final two instructions with the routine above it.
5415HWrite Precompensation and Seek
Radio Shack build only. Enables write precompensation on the inner half of the disk and hands over to the resident seek routine. Removed from the Percom build.
542DHDensity Toggle and Geometry Reseed
Error recovery. Inverts the drive's density flag, reloads the drive geometry bytes for the new density, sets the hardware density latch, and optionally restores the head to track zero.
544FHDriver Entry, Operation Dispatcher
The address the module header's relative jump reaches and therefore the entry point of the whole driver. Register B holds the LDOS disk operation code; the routine dispatches to the appropriate handler.
5477HRead, Write and Verify Sector Setup
Sets the density latch, chooses the controller command byte and the retry count for the operation, refuses a write to a write-protected drive, and installs the transfer loop body.
54B1HInline Parameter Block Reader
Called with three parameter bytes following the CALL. Plants the first byte in the resident status decoder at 46F6H and the following word in the transfer loop at 550AH, then falls through into the command issue routine rather than returning.
54BFHCommand Issue and Data Transfer
Performs the implied seek, writes the track and command registers, runs the unrolled data request loop that moves 256 bytes to or from the controller data register, and handles status, retries and the density fallback.
5558HDensity Latch
Sets the hardware density latch from the drive's density flag. Assembled as the Radio Shack version, which writes the controller sector register at 37EEH; overwritten by the Percom version, which writes the controller command register at 37ECH.

Cross-Reference Notes

FDUBL calls into the resident SYS0 core for every service it does not implement itself. The install-time code uses @DSPLY (4467H), @PARAM (4476H), @LOGOT (447BH), @ERROR (4409H) and @ABORT (4030H), reads and rewrites HIGH$ (4049H), tests SFLAG$ (430FH) bit 5 to decide whether a JCL job is running, and rewrites the Drive Control Table at 4700H.

The resident driver module continues to depend on six SYS0 entry points. 45E0H selects the drive and writes the drive-select latch at 37E1H. 461CH waits for the controller to become not-busy, re-asserting the drive-select latch on every pass so the motor stays selected. 462AH and 462DH are the two entry points of the resident seek routine, the first including the not-busy wait and the second beginning at the track register write. 4659H issues a Type I command with the drive's stepping rate merged in, and 4662H is its second entry for a command byte that is already assembled. 46FDH is the resident interrupt-suppression cell that the SYSTEM SMOOTH option patches. 46F4H is the resident routine that converts a controller status byte to an LDOS error number, and 46F6H is the base error number inside it, which FDUBL writes directly.

Nothing calls FDUBL. Once installed, the $FDD module is reached only through the Drive Control Table vectors that FDUBL rewrote, and it can be located by name through the resident module lookup service, RST 28H request code 0BCH, which is serviced by SYS10.

Disassembly:

5200H - Relocation Address Table

Fifteen 2-byte entries. This is data, not code. Each entry holds the address of a 3-byte or 4-byte instruction inside the driver module whose 16-bit operand refers to another address inside the module. Before the module is copied to high memory the routine at 52B2H walks this table and adds the relocation distance to each of those operands. An entry points at the byte immediately BEFORE the operand, because the relocation loop performs an INC HL after loading the entry, so an entry of 547AH refers to the operand at 547BH-547CH. The last entry is used only by the Radio Shack build; the Percom build decrements the count at 535CH from 15 to 14 to drop it, because the instruction it refers to has already been rewritten to point at a resident SYS0 address that must not be relocated.

5200
DEFW 547AH 7A 54
Relocation entry 1. Points at the CALL instruction at 547AH, whose operand at 547BH-547CH holds 5558H, the address of the density latch routine inside the module.
5202
DEFW 548EH 8E 54
Relocation entry 2. Points at the CALL instruction at 548EH, whose operand at 548FH-5490H holds 54B1H, the address of the inline parameter block reader inside the module.
5204
DEFW 54ABH AB 54
Relocation entry 3. Points at the CALL instruction at 54ABH, whose operand at 54ACH-54ADH holds 54B1H, the inline parameter block reader used for the write case.
5206
DEFW 54BBH BB 54
Relocation entry 4. Points at the store instruction at 54BBH, whose operand at 54BCH-54BDH holds 550AH, the address of the self-modified two-byte data transfer loop body inside the module.
5208
DEFW 5551H 51 55
Relocation entry 5. Points at the conditional jump at 5551H, whose operand at 5552H-5553H holds 554EH, the start of the read-path data request wait loop inside the module.
520A
DEFW 54F8H F8 54
Relocation entry 6. Points at the conditional jump at 54F8H, whose operand at 54F9H-54FAH holds 54F3H, the start of the write-path data request wait loop inside the module.
520C
DEFW 550FH 0F 55
Relocation entry 7. Points at the conditional jump at 550FH, whose operand at 5510H-5511H holds 550AH, the data transfer loop body inside the module.
520E
DEFW 5514H 14 55
Relocation entry 8. Points at the conditional jump at 5514H, whose operand at 5515H-5516H holds 550AH, the data transfer loop body inside the module. This is the second of the two long-form jumps in the unrolled transfer loop.
5210
DEFW 54CCH CC 54
Relocation entry 9. Points one byte into the 4-byte instruction at 54CBH, so that the operand it addresses at 54CDH-54CEH is the 550BH held by that instruction, the second byte of the data transfer loop body inside the module.
5212
DEFW 5457H 57 54
Relocation entry 10. Points at the conditional jump at 5457H, whose operand at 5458H-5459H holds 5477H, the read/write/verify sector setup routine inside the module.
5214
DEFW 5540H 40 55
Relocation entry 11. Points at the conditional call at 5540H, whose operand at 5541H-5542H holds 542DH, the density toggle and geometry reseed routine inside the module.
5216
DEFW 5447H 47 54
Relocation entry 12. Points at the CALL instruction at 5447H, whose operand at 5448H-5449H holds 5558H, the density latch routine inside the module.
5218
DEFW 5494H 94 54
Relocation entry 13. Points at the CALL instruction at 5494H, whose operand at 5495H-5496H holds 54B1H, the inline parameter block reader used for the verify case.
521A
DEFW 5546H 46 55
Relocation entry 14. Points at the conditional jump at 5546H, whose operand at 5547H-5548H holds 54BFH, the retry entry of the command issue routine inside the module.
521C
DEFW 54C2H C2 54
Relocation entry 15, used by the Radio Shack build only. Points at the conditional call at 54C2H, whose operand at 54C3H-54C4H holds 5415H, the write precompensation routine inside the module. In the Percom build the install-time code at 5262H has already overwritten that operand with the resident SYS0 address 462AH, which must not be relocated, so the count at 535CH is reduced to 14 and this entry is never read.

521EH - Program Entry, Sign-On Banner and Parameter Scan

The transfer address of the file. LDOS enters here with Register Pair HL pointing at the first character of the command line following the command name. The routine preserves the DOS stack pointer for the exit path, displays the sign-on banner, and hands the command line to @PARAM to pick up the PERCOM and TANDY keywords.

521E
LD (530FH),SP ED 73 0F 53
Self-Modifying Code
Store the stack pointer as supplied by DOS into 530FH-5310H. That pair of bytes is the operand of the LD SP instruction at 530EH in the exit routine, so this instruction plants the DOS stack pointer that the exit path will restore. The listing shows the initial value 0000H; the runtime value is whatever SP held when DOS transferred control here.
5222
PUSH HL E5
Save Register Pair HL onto the stack. HL holds the address of the command line tail supplied by DOS, and the banner display below will destroy it.
5223
LD HL,539EH 21 9E 53
Point Register Pair HL to 539EH, the start of the sign-on banner text "FDUBL 5.3.1 - Driver for the Percom and Tandy type doubler" followed by a line feed, the MISOSYS copyright line, and the 0DH terminator at 540BH.
5226
GOSUB to the resident @DSPLY vector at 4467H to display the string addressed by Register Pair HL, which holds 539EH, the sign-on banner. @DSPLY sends the string to the video display and stops at the 0DH terminator at 540BH.
5229
POP HL E1
Restore Register Pair HL from the stack, recovering the address of the command line tail that DOS supplied at entry.
522A
LD DE,5333H 11 33 53
Point Register Pair DE to 5333H, the start of the @PARAM keyword table. That table names PERCOM and its abbreviation P, whose value is deposited at 523CH, and TANDY and its abbreviation T, whose value is deposited at 5354H.
522D
GOSUB to the resident @PARAM vector at 4476H to parse the command line addressed by Register Pair HL against the keyword table addressed by Register Pair DE, which holds 5333H. @PARAM stores 0FFFFH into the cell named by each keyword that is present and returns with the Z FLAG set when every parameter on the line was recognised.
5230
LD A,2CH 3E 2C
Load Register A with 2CH, decimal 44, the LDOS error number for a parameter error. This is loaded before the test below so that the error path can be reached with a single conditional jump.
If the NZ FLAG (Not Zero) has been set, @PARAM did not recognise something on the command line. JUMP to 532CH to report error 2CH, held in Register A, through @ERROR and return to DOS with an error.
5235
LD HL,(4049H) 2A 49 40
Fetch the current value of HIGH$ from 4049H into Register Pair HL. HIGH$ is the resident word holding the highest address available to programs, which is also the head of the resident module chain.
5238
LD (540EH),HL 22 0E 54
Store the current HIGH$ value, held in Register Pair HL, into 540EH-540FH, the link word of the $FDD module header. The module lookup service reaches the next header in the chain by loading this word and incrementing it, so seeding it with the present HIGH$ links this module to whatever was already resident above it.
523B
LD HL,0000H 21 00 00
Self-Modifying Code
Load Register Pair HL with the contents of 523CH-523DH, which is this instruction's own operand and is the cell named by the PERCOM keyword in the @PARAM table at 5333H. The listing shows the assembled value 0000H, meaning PERCOM was not requested; @PARAM writes 0FFFFH here when PERCOM or P appears on the command line.
523E
LD A,H 7C
Load Register A with Register H, the high byte of the PERCOM flag just fetched from 523CH-523DH into Register Pair HL.
523F
AND L A5
AND Register A, holding the high byte of the PERCOM flag, with Register L, holding its low byte. Register A becomes 0FFH only when both halves of the flag at 523CH-523DH are 0FFH.
5240
INC A 3C
INCrement Register A by 1. Register A holds 0FFH when PERCOM was requested, so incrementing it produces zero and sets the Z FLAG; for any other value of the flag at 523CH-523DH the result is non-zero.
If the NZ FLAG (Not Zero) has been set, the PERCOM flag at 523CH-523DH was not 0FFFFH and the Radio Shack Double-Density Adapter is the target. JUMP to 5286H to skip the Percom variant construction entirely and go straight to the duplicate installation check. Falling through means PERCOM was requested.

5243H - Percom Variant Construction

Reached only when PERCOM or P appeared on the command line. The Percom Doubler sets its density latch through the controller command register at 37ECH rather than through the sector register at 37EEH, and it provides no write precompensation, so this routine rebuilds the driver: it copies the Percom density latch routine over the Radio Shack one, redirects the two references to the precompensation routine at 5415H to the resident seek routine instead, moves the module header up 21 bytes so that the precompensation routine falls outside the module, and adjusts the length, base, end and relocation count accordingly.

5243
LD HL,5354H 21 54 53
Point Register Pair HL to 5354H, the two-byte cell named by the TANDY keyword in the @PARAM table at 5333H. Register A holds zero at this point, because the INC A at 5240H produced zero on the path that reaches here.
5246
OR (HL) B6
OR Register A, which holds zero, with the byte at 5354H, the low half of the TANDY flag. Register A now holds that byte.
5247
INC HL 23
INCrement Register Pair HL by 1, advancing it from 5354H to 5355H, the high half of the TANDY flag.
5248
OR (HL) B6
OR Register A, which holds the low half of the TANDY flag from 5354H, with the byte at 5355H, its high half. Register A is non-zero if either half of the TANDY flag was set by @PARAM.
If the NZ FLAG (Not Zero) has been set, the TANDY flag at 5354H-5355H is non-zero and both doubler types were named on the same command line. JUMP to 531DH to display "Can't specify both Percom and Tandy!" and return to DOS with an error.
524C
LD HL,5564H 21 64 55
Point Register Pair HL to 5564H, the source copy of the Percom density latch routine, which writes the controller command register at 37ECH.
524F
LD DE,5558H 11 58 55
Point Register Pair DE to 5558H, the assembled Radio Shack density latch routine, which writes the controller sector register at 37EEH. This is the routine that the module calls at 5447H and 547AH, so overwriting it in place switches the whole driver to the Percom mechanism without changing any call site.
5252
LD BC,0010H 01 10 00
Load Register Pair BC with 0010H, decimal 16, the length in bytes of the Percom density latch routine at 5564H-5573H. The Radio Shack routine it replaces is only 12 bytes, so the copy extends 4 bytes past 5563H and pushes the end of the module to 5567H.
5255
LDIR ED B0
Block move. Source is Register Pair HL, holding 5564H, the Percom density latch routine. Destination is Register Pair DE, holding 5558H, the Radio Shack density latch routine. Byte count is Register Pair BC, holding 0010H. The direction is incrementing. This substitutes the Percom density mechanism for the Radio Shack one in the driver body.
5257
LD A,0CCH 3E CC
Load Register A with 0CCH, the replacement displacement for the relative jump at 545CH. As assembled that jump is JR Z,5415H, sending a seek operation to the write precompensation routine; 0CCH is minus 52, which redirects it to 542AH, the plain hand-off into the resident seek routine.
5259
LD (545DH),A 32 5D 54
Self-Modifying Code
Store Register A, holding 0CCH, into 545DH, which is the displacement byte of the JR Z instruction at 545CH in the operation dispatcher. The assembled value there is 0B7H, reaching 5415H. After this store a seek operation jumps to 542AH instead, so the Percom driver never touches write precompensation.
525C
LD HL,462AH 21 2A 46
Load Register Pair HL with 462AH, the first entry point of the resident SYS0 seek routine. That entry waits for the controller to become not-busy and then falls into the track positioning code at 462DH.
525F
LD (542BH),HL 22 2B 54
Self-Modifying Code
Store Register Pair HL, holding 462AH, into 542BH-542CH, the operand of the JP instruction at 542AH. As assembled that instruction is JP 462DH, entered after the precompensation routine has already performed the not-busy wait. With precompensation removed the wait has to be reinstated, so the target becomes 462AH.
5262
LD (54C3H),HL 22 C3 54
Self-Modifying Code
Store Register Pair HL, holding 462AH, into 54C3H-54C4H, the operand of the CALL Z instruction at 54C2H that performs the implied seek before a read or write. As assembled that call reaches the precompensation routine at 5415H; it now calls the resident seek routine at 462AH directly. This is also why the fifteenth relocation table entry is dropped: the operand no longer refers to an address inside the module.
GOSUB to 5574H, the routine added by the FDUBL1 patch, which writes 2CH into the module header's relative jump displacement at 540DH and returns with Register Pair HL holding 5414H. As distributed without the patch this instruction was LD HL,5414H alone, which left the displacement at its Radio Shack value of 41H and made the relocated Percom module enter its driver 21 bytes past the dispatcher.
5268
LD DE,5429H 11 29 54
Point Register Pair DE to 5429H, the last byte of the destination for the module header. The header is 9 bytes long, so it will occupy 5421H-5429H after the move.
526B
LD BC,0009H 01 09 00
Load Register Pair BC with 0009H, the length of the resident module header: the 18H signature byte, the entry displacement, the two-byte chain link word, the name-length byte and the four characters of the name $FDD.
526E
LDDR ED B8
Block move. Source is Register Pair HL, holding 5414H, the last byte of the module header in its assembled position at 540CH-5414H. Destination is Register Pair DE, holding 5429H. Byte count is Register Pair BC, holding 0009H. The direction is decrementing, which is what allows the source and destination to overlap safely. The header ends up at 5421H-5429H, on top of the tail of the write precompensation routine, which the Percom build has no use for.
5270
LD HL,0147H 21 47 01
Load Register Pair HL with 0147H, decimal 327, the length of the Percom module. That is the span 5421H to 5567H inclusive: the relocated header, the driver body, and the 16-byte Percom density latch routine that now ends at 5567H.
5273
LD (5356H),HL 22 56 53
Store Register Pair HL, holding 0147H, into 5356H-5357H, the module length cell. The assembled value there is 0158H, the length of the Radio Shack module. This cell becomes the byte count for the LDDR that copies the module into high memory.
5276
EX DE,HL EB
Exchange Register Pair DE with Register Pair HL, moving the module length 0147H into Register Pair DE so that Register Pair HL can be loaded with the module base address for the addition below.
5277
LD HL,5421H 21 21 54
Load Register Pair HL with 5421H, the new base of the module, which is where the LDDR at 526EH has just placed the module header.
527A
LD (5358H),HL 22 58 53
Store Register Pair HL, holding 5421H, into 5358H-5359H, the module start address cell. The assembled value there is 540CH, the base of the Radio Shack module.
527D
ADD HL,DE 19
ADD Register Pair DE, holding the module length 0147H, to Register Pair HL, holding the module base 5421H. Register Pair HL becomes 5568H, one past the end of the module.
527E
DEC HL 2B
DECrement Register Pair HL by 1, from 5568H to 5567H, the address of the last byte of the Percom module.
527F
LD (535AH),HL 22 5A 53
Store Register Pair HL, holding 5567H, into 535AH-535BH, the module end address cell. The assembled value there is 5563H, the last byte of the Radio Shack module. This cell serves twice: as the source pointer for the LDDR that copies the module to high memory, and as the base from which the relocation distance is computed.
5282
LD HL,535CH 21 5C 53
Point Register Pair HL to 535CH, the relocation entry count cell, which holds 0FH as assembled.
5285
DEC (HL) 35
DECrement the byte at 535CH, the relocation entry count, from 0FH to 0EH. This drops the fifteenth table entry at 521CH, which refers to the operand at 54C3H-54C4H. That operand was overwritten at 5262H with the resident address 462AH, which lies outside the module and must not be relocated.

5286H - Duplicate Installation Check

Walks all eight Drive Control Table entries at 4700H. Each entry begins with a 3-byte JP instruction whose operand is the address of the driver serving that drive. This loop follows that operand, steps four bytes into the driver to reach where a resident module header keeps its name-length byte, and compares the five bytes found there against the pattern at 5410H, which is the name-length byte 04H followed by the four characters $FDD. A match means an FDUBL driver is already installed on that drive and the utility refuses to install a second one.

5286
LD HL,4701H 21 01 47
Point Register Pair HL to 4701H, the operand of the JP instruction that opens the Drive Control Table entry for drive 0 at 4700H. On a fitted drive that JP holds the address of the driver serving the drive; on an unfitted unit the entry starts with a RET instead of a JP.
5289
PUSH HL E5
Loop Start
Save Register Pair HL onto the stack. Register Pair HL holds the address of the current Drive Control Table entry's driver operand, and the comparison below destroys it. Each pass of this loop examines one of the eight table entries.
528A
LD A,(HL) 7E
Load Register A with the byte at the address in Register Pair HL, the low half of the current drive's driver address inside its Drive Control Table JP instruction.
528B
INC HL 23
INCrement Register Pair HL by 1 so it addresses the high half of the current drive's driver address.
528C
LD H,(HL) 66
Load Register H with the byte at the address in Register Pair HL, the high half of the current drive's driver address.
528D
LD L,A 6F
Load Register L with Register A, which holds the low half of the driver address saved at 528AH. Register Pair HL now holds the address of the driver currently serving this drive.
528E
INC HL 23
INCrement Register Pair HL by 1, stepping from the driver base towards the offset where a resident module header keeps its name-length byte.
528F
INC HL 23
INCrement Register Pair HL by 1 again, the second of four steps that carry Register Pair HL from the driver base to driver base plus 4.
5290
INC HL 23
INCrement Register Pair HL by 1 again, the third of four steps.
5291
INC HL 23
INCrement Register Pair HL by 1 again. Register Pair HL now addresses driver base plus 4, which in a resident module header is the name-length byte, immediately following the 18H signature, the entry displacement and the two-byte chain link word.
5292
LD DE,5410H 11 10 54
Point Register Pair DE to 5410H, the name-length byte of this program's own module header, which holds 04H and is followed at 5411H-5414H by the four characters $FDD.
5295
LD A,(DE) 1A
Load Register A with the byte at the address in Register Pair DE, which holds 5410H, so Register A receives 04H, the length of the module name $FDD.
5296
CP (HL) BE
Compare Register A, holding the name-length byte 04H from 5410H, against the byte at the address in Register Pair HL, which is driver base plus 4 for the current drive. If they are equal the Z FLAG is set; otherwise the NZ FLAG is set.
If the NZ FLAG (Not Zero) has been set, the byte at driver base plus 4 for this drive is not 04H, so the driver serving it is not a $FDD module. JUMP to 52A8H to move on to the next Drive Control Table entry.
5299
INC HL 23
INCrement Register Pair HL by 1, advancing from the name-length byte at driver base plus 4 to the first character of the driver's name.
529A
INC DE 13
INCrement Register Pair DE by 1, advancing from 5410H to 5411H, the first character of the name $FDD in this program's own header.
529B
LD B,A 47
Load Register B with Register A, which holds 04H, the name length just matched. Register B becomes the loop counter for the four-character name comparison below.
529C
LD A,(DE) 1A
Loop Start
Load Register A with the byte at the address in Register Pair DE, one character of the name $FDD taken from 5411H-5414H.
529D
CP (HL) BE
Compare Register A, holding the current character of $FDD, against the byte at the address in Register Pair HL, the corresponding character of the name recorded in the installed driver's header. If they are equal the Z FLAG is set; otherwise the NZ FLAG is set.
If the NZ FLAG (Not Zero) has been set, the names differ. JUMP to 52A8H to move on to the next Drive Control Table entry.
52A0
INC DE 13
INCrement Register Pair DE by 1, advancing to the next character of $FDD within 5411H-5414H.
52A1
INC HL 23
INCrement Register Pair HL by 1, advancing to the next character of the installed driver's recorded name.
DECrement Register B, the count of name characters still to compare, and LOOP BACK to 529CH if it has not reached zero. Loop End
Falling through means all four characters matched and a $FDD module is already serving this drive.
52A4
POP HL E1
Restore Register Pair HL from the stack, discarding the saved Drive Control Table pointer and rebalancing the stack before the error exit.
JUMP to 5321H to display "Driver is already resident!" and return to DOS with an error, because a driver named $FDD is already hooked into this Drive Control Table entry.
52A8
POP HL E1
Restore Register Pair HL from the stack, recovering the address of the current Drive Control Table entry's driver operand that was saved at 5289H.
52A9
LD DE,000AH 11 0A 00
Load Register Pair DE with 000AH, decimal 10, the size in bytes of one Drive Control Table entry.
52AC
ADD HL,DE 19
ADD Register Pair DE, holding 000AH, to Register Pair HL, holding the current entry's driver operand address. Register Pair HL now addresses the driver operand of the next Drive Control Table entry.
52AD
LD A,51H 3E 51
Load Register A with 51H, the low byte of 4751H, which is the address the pointer reaches after all eight Drive Control Table entries have been examined: 4701H plus eight times 10 bytes.
52AF
CP L BD
Compare Register A, holding 51H, against Register L, the low byte of the pointer in Register Pair HL. If they are equal the Z FLAG is set, meaning all eight entries have been checked; otherwise the NZ FLAG is set.
If the NZ FLAG (Not Zero) has been set, Drive Control Table entries remain. LOOP BACK to 5289H to examine the next one. Loop End

52B2H - Relocation of the Module's Address Operands

Computes how far the module is about to move and applies that distance to every operand named in the relocation address table at 5200H. The module will be copied so that its last byte lands on HIGH$, so the distance is HIGH$ minus the module's present end address, held at 535AH.

52B2
LD IX,5200H DD 21 00 52
Point Register Pair IX to 5200H, the first entry of the relocation address table. Register Pair IX will step through the table two bytes at a time.
52B6
LD HL,(4049H) 2A 49 40
Fetch HIGH$ from 4049H into Register Pair HL, the highest address currently available to programs. This is where the last byte of the module will come to rest.
52B9
LD DE,(535AH) ED 5B 5A 53
Fetch the module end address from 535AH-535BH into Register Pair DE. That cell holds 5563H for the Radio Shack build and 5567H for the Percom build, written there at 527FH.
52BD
OR A B7
OR Register A with itself. This leaves Register A unchanged but clears the CARRY FLAG so that the subtraction below is exact.
52BE
SBC HL,DE ED 52
SUBtract Register Pair DE, holding the module's present end address from 535AH, from Register Pair HL, holding HIGH$ from 4049H. Register Pair HL becomes the distance the module is about to move.
52C0
LD B,H 44
Load Register B with Register H, the high byte of the relocation distance just computed in Register Pair HL.
52C1
LD C,L 4D
Load Register C with Register L, the low byte of the relocation distance. Register Pair BC now holds the distance and remains untouched for the rest of the loop.
52C2
LD A,(535CH) 3A 5C 53
Load Register A with the relocation entry count from 535CH, which holds 0FH for the Radio Shack build and 0EH for the Percom build after the DEC at 5285H. Register A is the loop counter.
52C5
LD L,(IX+00H) DD 6E 00
Loop Start
Load Register L with the byte at the address in Register Pair IX, which addresses the current relocation table entry within 5200H-521DH. This is the low half of the address of the instruction to be fixed up.
52C8
LD H,(IX+01H) DD 66 01
Load Register H with the byte one past the address in Register Pair IX, the high half of the current relocation table entry. Register Pair HL now holds the address recorded in the table, for example 547AH for the first entry.
52CB
INC HL 23
INCrement Register Pair HL by 1, stepping from the recorded instruction address to the low byte of that instruction's 16-bit operand.
52CC
LD E,(HL) 5E
Load Register E with the byte at the address in Register Pair HL, the low half of the operand being relocated.
52CD
INC HL 23
INCrement Register Pair HL by 1 so it addresses the high byte of the operand being relocated.
52CE
LD D,(HL) 56
Load Register D with the byte at the address in Register Pair HL, the high half of the operand. Register Pair DE now holds the original operand value, an address inside the module as assembled.
52CF
EX DE,HL EB
Exchange Register Pair DE with Register Pair HL. Register Pair HL now holds the operand value and Register Pair DE holds the address of the operand's high byte, so the addition below can be performed on the value.
52D0
ADD HL,BC 09
ADD Register Pair BC, holding the relocation distance computed at 52BEH, to Register Pair HL, holding the original operand value. Register Pair HL becomes the address that operand will need once the module has been moved.
52D1
EX DE,HL EB
Exchange Register Pair DE with Register Pair HL again. Register Pair HL is restored to the address of the operand's high byte, and Register Pair DE now carries the relocated value.
52D2
LD (HL),D 72
Store Register D, the high half of the relocated operand value, into the byte addressed by Register Pair HL, the high byte of the operand.
52D3
DEC HL 2B
DECrement Register Pair HL by 1 so it addresses the low byte of the operand.
52D4
LD (HL),E 73
Store Register E, the low half of the relocated operand value, into the byte addressed by Register Pair HL. The instruction named by this relocation table entry now refers to the module's future address.
52D5
INC IX DD 23
INCrement Register Pair IX by 1, the first of two steps that advance it to the next 2-byte relocation table entry.
52D7
INC IX DD 23
INCrement Register Pair IX by 1 again. Register Pair IX now addresses the next entry within the relocation table at 5200H-521DH.
52D9
DEC A 3D
DECrement Register A by 1. Register A holds the number of relocation table entries still to process, loaded from 535CH at 52C2H.
If the NZ FLAG (Not Zero) has been set, entries remain in the relocation table. LOOP BACK to 52C5H to relocate the next operand. Loop End

52DCH - Module Installation and Drive Control Table Hook-Up

Copies the finished module into high memory so that its last byte occupies HIGH$, lowers HIGH$ by the module length to protect it, and then rewrites the driver address inside the Drive Control Table JP vector of every fitted drive so that all disk operations are routed through the new module.

52DC
LD DE,(4049H) ED 5B 49 40
Fetch HIGH$ from 4049H into Register Pair DE, the destination for the last byte of the module. Register Pair DE is the destination pointer for the decrementing block move below.
52E0
LD HL,(535AH) 2A 5A 53
Fetch the module end address from 535AH-535BH into Register Pair HL: 5563H for the Radio Shack build, 5567H for the Percom build. This is the source pointer for the block move.
52E3
LD BC,(5356H) ED 4B 56 53
Fetch the module length from 5356H-5357H into Register Pair BC: 0158H for the Radio Shack build, 0147H for the Percom build. This is the byte count for the block move.
52E7
LDDR ED B8
Block move. Source is Register Pair HL, the module's last byte in low memory. Destination is Register Pair DE, HIGH$. Byte count is Register Pair BC, the module length. The direction is decrementing, so the module is copied from its top downwards and comes to rest occupying HIGH$ minus the length plus one through HIGH$. On completion Register Pair DE holds HIGH$ minus the length, one byte below the relocated module.
52E9
EX DE,HL EB
Exchange Register Pair DE with Register Pair HL, moving the post-move destination pointer, which is HIGH$ minus the module length, into Register Pair HL.
52EA
LD (4049H),HL 22 49 40
Store Register Pair HL, holding HIGH$ minus the module length, into HIGH$ at 4049H. This lowers the top of available memory below the relocated module so that nothing loaded afterwards can overwrite it, and makes the module the new head of the resident module chain that the lookup service walks.
52ED
INC HL 23
INCrement Register Pair HL by 1, from the new HIGH$ value to the first byte of the relocated module, which is the 18H signature byte of the $FDD header and also the driver's entry point.
52EE
LD IY,4700H FD 21 00 47
Point Register Pair IY to 4700H, the base of the Drive Control Table, which holds eight 10-byte entries covering logical units 0 through 7.
52F2
LD B,08H 06 08
Load Register B with 08H, the number of Drive Control Table entries to visit. Register B is the loop counter.
52F4
LD DE,000AH 11 0A 00
Load Register Pair DE with 000AH, decimal 10, the size of one Drive Control Table entry, used to step Register Pair IY from one entry to the next.
52F7
BIT 4,(IY+04H) FD CB 04 66
Loop Start
Test bit 4 of the byte at offset 04H in the Drive Control Table entry addressed by Register Pair IY. That byte carries the drive's secondary specification, including the double-sided flag in bit 5 and the physical unit number in the low nibble. Bit 4 marks a unit that this driver must not take over. If the bit is clear the Z FLAG is set; if it is set the NZ FLAG is set.
If the NZ FLAG (Not Zero) has been set, bit 4 of the byte at offset 04H is set for this unit and its driver vector is left alone. JUMP to 5307H to step to the next Drive Control Table entry.
52FD
LD (IY+01H),L FD 75 01
Store Register L, the low half of the relocated module's base address held in Register Pair HL, into offset 01H of the Drive Control Table entry addressed by Register Pair IY. Offsets 00H through 02H of an entry form a JP instruction, so offsets 01H and 02H are its target address; writing them redirects every operation on this drive to the new driver.
5300
LD (IY+02H),H FD 74 02
Store Register H, the high half of the relocated module's base address held in Register Pair HL, into offset 02H of the Drive Control Table entry addressed by Register Pair IY, completing the JP target. The drive's JP now reaches the 18H signature byte of the $FDD header, which is a relative jump to the driver's operation dispatcher.
5303
SET 6,(IY+04H) FD CB 04 F6
Set bit 6 of the byte at offset 04H in the Drive Control Table entry addressed by Register Pair IY, marking this unit as being served by the double-density driver.
5307
ADD IY,DE FD 19
ADD Register Pair DE, holding 000AH, to Register Pair IY, advancing it from the current Drive Control Table entry to the next one.
DECrement Register B, the count of Drive Control Table entries still to visit, and LOOP BACK to 52F7H if it has not reached zero. Loop End
The driver is now installed on every eligible unit.

530BH - Exit

The common exit for both the success and the failure paths. Every LDOS library command returns with the error code in Register Pair HL, zero for success and 0FFFFH for failure, and diverts to @ABORT only when a JCL job is running, which is signalled by bit 5 of SFLAG$ at 430FH.

530B
LD HL,0000H 21 00 00
Load Register Pair HL with 0000H, the success return code. The failure paths at 5327H bypass this instruction by jumping directly to 530EH with 0FFFFH already in Register Pair HL.
530E
LD SP,0000H 31 00 00
Self-Modifying Code
Load the stack pointer from this instruction's own operand at 530FH-5310H. The listing shows the assembled value 0000H; at run time the instruction at 521EH has already stored the stack pointer that DOS supplied at entry, so this restores the DOS stack regardless of what the program left on it.
5311
LD A,H 7C
Load Register A with Register H, the high half of the return code in Register Pair HL, which is 00H on success and 0FFH on failure.
5312
OR L B5
OR Register A, holding the high half of the return code, with Register L, its low half. Register A is zero only when the whole return code in Register Pair HL is zero.
5313
RET Z C8
If the Z FLAG (Zero) has been set, the return code in Register Pair HL is 0000H and the installation succeeded. Return to DOS.
5314
LD A,(430FH) 3A 0F 43
Load Register A with SFLAG$ from 430FH, the resident system flag byte. Bit 5 of that byte is clear at command level and set while a JCL job is in control.
5317
BIT 5,A CB 6F
Test bit 5 of Register A, holding SFLAG$ from 430FH. If the bit is clear the Z FLAG is set, meaning no JCL job is running; if it is set the NZ FLAG is set.
5319
RET Z C8
If the Z FLAG (Zero) has been set, bit 5 of SFLAG$ at 430FH is clear and no JCL job is in effect. Return to DOS with the failure code 0FFFFH still in Register Pair HL, so the command simply reports its error at command level.
JUMP to the resident @ABORT vector at 4030H. Bit 5 of SFLAG$ at 430FH is set, so a JCL job is running and a failed command must abort the job rather than return quietly.

531DH - Error Message Display and Error Exit

Two error reports sharing one message display sequence. The entry at 531DH loads the address of the "Can't specify both Percom and Tandy!" message and then executes an LD IX instruction whose second byte begins a second, independent LD HL instruction; the entry at 5321H starts on that second byte instead and therefore loads the address of the "Driver is already resident!" message. Both then fall into the same @LOGOT call and error exit.

531D
LD HL,535DH 21 5D 53
Point Register Pair HL to 535DH, the text "Can't specify both Percom and Tandy!" which is terminated by the 0DH at 5381H. Reached from 5249H when both the PERCOM and the TANDY keywords appeared on the command line.
5320
LD IX,5382H DD 21 82 53
Loading Register Pair IX with 5382H has no effect on anything that follows; the instruction exists so that its second, third and fourth bytes can serve as a complete LD HL instruction when entered one byte later. This is the linear reading of the bytes, taken when execution arrives from 531DH.

The four bytes at 5320H-5323H are executed two different ways. Arriving from 531DH they are read as the LD IX instruction above and the message address already in Register Pair HL survives. Arriving from 52A5H, which jumps to 5321H, execution starts on the second byte and reads them as the LD HL instruction below, which replaces the message address. The row that follows shows that second interpretation.

5321
LD HL,5382H 21 82 53
Point Register Pair HL to 5382H, the text "Driver is already resident!" which is terminated by the 0DH at 539DH. This is the entry taken from 52A5H when the duplicate installation check found a $FDD module already hooked into a Drive Control Table entry.
GOSUB to the resident @LOGOT vector at 447BH to display the message addressed by Register Pair HL, which holds either 535DH or 5382H depending on which of the two entries above was taken. @LOGOT sends the 0DH-terminated string to the video display.
5327
LD HL,0FFFFH 21 FF FF
Load Register Pair HL with 0FFFFH, the error return code required by the library command return convention.
JUMP to 530EH, the exit routine, entering past the LD HL,0000H at 530BH so that the error code 0FFFFH in Register Pair HL is preserved.
532C
OR 0C0H F6 C0
OR Register A, holding the error number 2CH loaded at 5230H, with 0C0H. Bit 6 asks @ERROR to display the message only, without the extended explanation, and bit 7 asks it to return to the caller instead of aborting. Register A becomes 0ECH.
GOSUB to the resident @ERROR vector at 4409H to report the error whose number and option bits are in Register A, which holds 0ECH: error 2CH, a parameter error, displayed as a message and returned from rather than aborted.
JUMP to 5327H to load Register Pair HL with the error return code 0FFFFH and leave through the common exit.

5333H - @PARAM Keyword Table

The parameter table handed to @PARAM at 522DH. This is data, not code. Each entry is a 6-character keyword padded with blanks followed by the 2-byte address of the cell that receives the parameter's value, and the table is terminated by a zero byte. PERCOM and its abbreviation P both deposit into 523CH, the operand of the LD HL instruction at 523BH. TANDY and its abbreviation T both deposit into 5354H. @PARAM stores 0FFFFH in the named cell when the keyword is present.

5333
DEFM 'PERCOM' 50 45 52 43 4F 4D
Keyword 1. The full spelling of the parameter that selects the Percom Doubler variant of the driver.
5339
DEFW 523CH 3C 52
Value cell for the PERCOM keyword. 523CH-523DH is the operand of the LD HL instruction at 523BH, which the code at 523EH tests to decide whether to build the Percom variant.
533B
DEFM 'P ' 50 20 20 20 20 20
Keyword 2. The single-letter abbreviation for PERCOM, blank-padded to the fixed 6-character keyword width.
5341
DEFW 523CH 3C 52
Value cell for the P abbreviation, the same 523CH-523DH used by the full PERCOM spelling.
5343
DEFM 'TANDY ' 54 41 4E 44 59 20
Keyword 3. The full spelling of the parameter naming the Radio Shack Double-Density Adapter, blank-padded to 6 characters. This is the default variant, so the keyword changes nothing on its own.
5349
DEFW 5354H 54 53
Value cell for the TANDY keyword. 5354H-5355H is tested only at 5243H, to reject a command line that names both doubler types.
534B
DEFM 'T ' 54 20 20 20 20 20
Keyword 4. The single-letter abbreviation for TANDY, blank-padded to 6 characters.
5351
DEFW 5354H 54 53
Value cell for the T abbreviation, the same 5354H-5355H used by the full TANDY spelling.
5353
DEFB 00H 00
End of table marker. A zero in the first character position tells @PARAM that there are no further keywords.

5354H - Install-Time Control Variables

Five cells that describe the module about to be installed. This is data, not code. The values shown are those assembled into the file, which describe the Radio Shack build; the Percom construction routine at 5243H overwrites four of the five.

5354
DEFW 0000H 00 00
TANDY parameter flag, the cell named by the TANDY and T keywords in the table at 5343H and 534BH. @PARAM writes 0FFFFH here when either appears on the command line. It is read only at 5243H-5248H, to reject PERCOM and TANDY together.
5356
DEFW 0158H 58 01
Module length in bytes. 0158H is decimal 344, the span 540CH to 5563H of the Radio Shack module. Rewritten to 0147H at 5273H for the Percom build. Read at 52E3H as the byte count for the block move into high memory.
5358
DEFW 540CH 0C 54
Module start address, the module header of the Radio Shack build. Rewritten to 5421H at 527AH for the Percom build. Held for reference; the installation is driven from the end address below.
535A
DEFW 5563H 63 55
Module end address, the last byte of the Radio Shack module, which is the RET of the density latch routine. Rewritten to 5567H at 527FH for the Percom build, whose density latch routine is four bytes longer. Read at 52B9H to compute the relocation distance and at 52E0H as the block move source.
535C
DEFB 0FH 0F
Relocation entry count, the number of entries of the table at 5200H that are to be processed. Decremented to 0EH at 5285H for the Percom build. Read at 52C2H as the relocation loop counter.

535DH - Message Text

The two error messages and the sign-on banner. This is data, not code. Each message ends with the 0DH terminator that @DSPLY and @LOGOT stop on; the banner contains an embedded 0AH line feed to separate its two lines.

535D
DEFM "Can't specify both Percom and Tandy!" 43 61 6E 27 74 20 73 70 65 63 69 66 79 20 62 6F 74 68 20 50 65 72 63 6F 6D 20 61 6E 64 20 54 61 6E 64 79 21
Error message displayed from 5324H when the entry at 531DH was taken, which happens when the command line named both the PERCOM and the TANDY parameters.
5381
DEFB 0DH 0D
Carriage return terminator for the message beginning at 535DH.
5382
DEFM 'Driver is already resident!' 44 72 69 76 65 72 20 69 73 20 61 6C 72 65 61 64 79 20 72 65 73 69 64 65 6E 74 21
Error message displayed from 5324H when the entry at 5321H was taken, which happens when the duplicate installation check at 5286H found a module named $FDD already serving one of the drives.
539D
DEFB 0DH 0D
Carriage return terminator for the message beginning at 5382H.
539E
DEFM 'FDUBL 5.3.1 - Driver for the Percom and Tandy type doubler' 46 44 55 42 4C 20 35 2E 33 2E 31 20 2D 20 44 72 69 76 65 72 20 66 6F 72 20 74 68 65 20 50 65 72 63 6F 6D 20 61 6E 64 20 54 61 6E 64 79 20 74 79 70 65 20 64 6F 75 62 6C 65 72
First line of the sign-on banner, displayed from 5226H at the very start of the program.
53D8
DEFB 0AH 0A
Line feed separating the two lines of the sign-on banner. This is not a terminator, so @DSPLY continues with the copyright line below.
53D9
DEFM 'Copyright 1991, MISOSYS, Inc., All rights reserved' 43 6F 70 79 72 69 67 68 74 20 31 39 39 31 2C 20 4D 49 53 4F 53 59 53 2C 20 49 6E 63 2E 2C 20 41 6C 6C 20 72 69 67 68 74 73 20 72 65 73 65 72 76 65 64
Second line of the sign-on banner. This is the displayed copyright line and is distinct from the copyright record carried in the file's load header, which is never executed or displayed.
540B
DEFB 0DH 0D
Carriage return terminator for the sign-on banner beginning at 539EH.

540CH - Resident Module Header

The standard LDOS resident module header, which makes the driver discoverable by name through the module lookup service, RST 28H request code 0BCH. This is data with one executable byte pair: the 18H signature is also the opcode of a relative jump, so the address a Drive Control Table vector points at is simultaneously the header and the driver's entry point. In the Radio Shack build the header stays here at 540CH-5414H; in the Percom build the LDDR at 526EH copies these nine bytes up to 5421H-5429H and the module starts there instead.

540C
DEFB 18H 18
Module signature. The lookup service recognises a resident module header by this byte, and the same byte is the opcode of the relative jump that carries a caller from the module base to the driver's operation dispatcher.
540D
DEFB 41H 41
Self-Modifying Code
The displacement of the relative jump begun by the signature byte. 41H is decimal 65, which from a header based at 540CH reaches 544FH, the driver's operation dispatcher. The routine at 5574H added by the FDUBL1 patch overwrites this with 2CH for the Percom build, whose header sits 21 bytes higher at 5421H and therefore needs a displacement 21 smaller to arrive at the same dispatcher.
540E
DEFW 0000H 00 00
Resident module chain link word. The instruction at 5238H writes the value of HIGH$ read from 4049H here before installation, so this module links to whatever module was already resident above it. The lookup service loads this word and increments it to reach the next header in the chain, and treats 0FFFFH as the end of the chain.
5410
DEFB 04H 04
Name length. The low nibble gives the number of characters in the module name that follows. This byte and the four that follow it are also the pattern that the duplicate installation check at 5292H compares against every installed driver.
5411
DEFM '$FDD' 24 46 44 44
Module name. $FDD is the name under which the double-density floppy driver can be found by the resident module lookup service. In a resident module header the bytes following the name are the module's control block, so the driver code beginning at 5415H, or at 542AH in the Percom build, occupies that position.

5415H - Write Precompensation and Seek (Radio Shack build)

Radio Shack Double-Density Adapter builds only. Write precompensation shifts the timing of recorded bit cells slightly to counteract the crowding that occurs on the inner, shorter tracks of a diskette, and the Radio Shack board implements it while the Percom, LNW and Omikron boards do not. The routine enables precompensation once the head has passed the midpoint of the disk and then hands over to the resident SYS0 seek routine. Its control byte reaches the adapter through the controller's sector register at 37EEH: on the Radio Shack board the three high-order bits of a write to that address drive a one-of-eight decoder that latches the density selection and the precompensation enable, and because a genuine sector number never has those bits set, ordinary sector writes pass through untouched. In the Percom build this routine is not part of the module; the header is copied over 5421H-5429H, the jump at 545CH is redirected to 542AH and the call at 54C2H is redirected to the resident routine at 462AH.

5415
GOSUB to the resident controller-ready wait at 461CH. That routine reads the controller status register at 37ECH, returns as soon as the busy bit is clear, and on every pass rewrites the drive-select latch at 37E1H from the saved copy at 4309H so the selected drive stays selected while the wait runs.
5418
LD A,(IY+06H) FD 7E 06
Load Register A with the byte at offset 06H of the Drive Control Table entry addressed by Register Pair IY, which is the highest cylinder number this drive supports. Register Pair IY was pointed at the drive's table entry by the resident disk primitive that invoked this driver.
541B
ADD A,07H C6 07
ADD 07H to Register A, which holds the highest cylinder number from offset 06H of the Drive Control Table entry. The bias is applied before the halving below so the midpoint rounds towards the outside of the disk.
541D
RRA 1F
Rotate Register A right through the CARRY FLAG, which the ADD above left clear for any drive with fewer than 249 cylinders. This divides the biased highest-cylinder number in Register A by two, giving the cylinder at which write precompensation should begin.
541E
CP (IY+05H) FD BE 05
Compare Register A, holding the precompensation threshold cylinder, against the byte at offset 05H of the Drive Control Table entry addressed by Register Pair IY, which is the cylinder the head is currently on. If the threshold is below the current cylinder the CARRY FLAG is set, meaning the head is on the inner half of the disk; otherwise the NO CARRY FLAG is set.
5421
LD A,0C0H 3E C0
Load Register A with 0C0H, the adapter control byte whose three high-order bits select the precompensation latch with precompensation off. In the Percom build this byte and the eight that follow it are overwritten by the relocated module header, and the code below is never reached.
If the NO CARRY FLAG has been set, the current cylinder at offset 05H of the Drive Control Table entry is at or below the threshold computed above, so the head is on the outer half of the disk and precompensation stays off. JUMP to 5427H with 0C0H in Register A.
5425
OR 20H F6 20
OR Register A, holding the control byte 0C0H, with 20H, producing 0E0H. This is the same latch selection with the precompensation enable bit set, because the head is on the inner half of the disk where bit crowding is worst.
5427
LD (37EEH),A 32 EE 37
Store Register A, holding 0C0H for precompensation off or 0E0H for precompensation on, into the controller sector register at 37EEH. On the Radio Shack adapter the three high-order bits of this write are decoded by the board rather than treated as a sector number, so the write sets the precompensation latch.
542A
JUMP to the resident seek routine at 462DH, which writes the current cylinder from offset 05H of the Drive Control Table entry to the controller track register at 37EDH, resolves the caller's through-numbered sector in Register E into a side and a per-side sector by comparing it against the sectors per track from offset 07H, records the side in bit 4 of offset 03H, writes the resolved sector to the controller sector register at 37EEH and the destination cylinder from Register D to the data register at 37EFH, and issues the Seek command. The not-busy wait that the resident routine's other entry at 462AH performs was already done at 5415H. Self-Modifying Code
In the Percom build the install-time code at 525FH rewrites the operand at 542BH-542CH to 462AH, so the resident wait is performed here instead and the precompensation routine above is bypassed entirely. This instruction is also the target the patched jump at 545CH reaches in the Percom build.

542DH - Density Toggle and Geometry Reseed

Error recovery, called from 5540H when a sector operation fails with a record-not-found status. The most common cause on a double-density system is a diskette recorded in the other density, so this routine inverts the drive's density flag, reloads the two Drive Control Table geometry bytes with the sector and granule layout that matches the new density, and sets the hardware density latch. On alternate retries it also restores the head to track zero.

542D
PUSH HL E5
Save Register Pair HL onto the stack. Register Pair HL holds 37ECH, the controller status register address used by the transfer loop, and this routine needs Register Pair HL for the geometry constants.
542E
LD A,(IY+03H) FD 7E 03
Load Register A with the byte at offset 03H of the Drive Control Table entry addressed by Register Pair IY, the drive specification byte. Bit 6 of that byte is the double-density flag, bit 7 marks the drive write protected, bit 4 is the side select flag that the resident routine at 45E0H presents on bit 3 of the drive-select latch at 37E1H, and bits 0 and 1 hold the stepping rate.
5431
XOR 40H EE 40
XOR Register A, holding the drive specification byte from offset 03H, with 40H. This inverts bit 6, the double-density flag, leaving every other bit of the specification untouched.
5433
LD (IY+03H),A FD 77 03
Store Register A, the drive specification byte with its density flag inverted, back into offset 03H of the Drive Control Table entry addressed by Register Pair IY. Every later reference to the drive's density, including the latch routine at 5558H, now sees the opposite density.
5436
LD HL,2409H 21 09 24
Load Register Pair HL with 2409H, the single-density geometry pair. Register L holds 09H, which becomes offset 07H of the Drive Control Table entry and gives a highest sector number of 9, so 10 sectors per track. Register H holds 24H, which becomes offset 08H and encodes the granules per track and sectors per granule for single density.
5439
BIT 6,A CB 77
Test bit 6 of Register A, the newly inverted density flag from offset 03H of the Drive Control Table entry. If the bit is clear the Z FLAG is set, meaning the drive is now single density; if it is set the NZ FLAG is set, meaning double density.
If the Z FLAG (Zero) has been set, bit 6 of the drive specification byte is clear and the drive has just been switched to single density. JUMP to 5440H with the single-density geometry 2409H already in Register Pair HL.
543D
LD HL,4511H 21 11 45
Load Register Pair HL with 4511H, the double-density geometry pair. Register L holds 11H, which becomes offset 07H of the Drive Control Table entry and gives a highest sector number of 17, so 18 sectors per track. Register H holds 45H, which becomes offset 08H and encodes the granules per track and sectors per granule for double density.
5440
LD (IY+07H),L FD 75 07
Store Register L, holding 09H for single density or 11H for double density, into offset 07H of the Drive Control Table entry addressed by Register Pair IY, the byte whose low five bits give the highest sector number on a track.
5443
LD (IY+08H),H FD 74 08
Store Register H, holding 24H for single density or 45H for double density, into offset 08H of the Drive Control Table entry addressed by Register Pair IY, the byte that gives the granules per track in its top three bits and the sectors per granule in its low five bits.
5446
POP HL E1
Restore Register Pair HL from the stack, recovering 37ECH, the controller status register address the caller was using.
GOSUB to 5558H, the density latch routine, to make the hardware agree with the density flag just inverted at offset 03H of the Drive Control Table entry. In the Radio Shack build that routine writes the sector register at 37EEH; in the Percom build the install-time LDIR at 5255H has replaced it with the version that writes the command register at 37ECH.
544A
BIT 0,B CB 40
Test bit 0 of Register B, which holds the number of retries still remaining for the failing operation. If the bit is clear the Z FLAG is set; if it is set the NZ FLAG is set. Testing the low bit makes the head restore happen on alternate retries only, so the driver first tries the other density on the current track before spending the time to seek back to track zero.
544C
RET Z C8
If the Z FLAG (Zero) has been set, the retry count in Register B is even. Return to the caller at 5543H with the density already switched and no head movement.
JUMP to 546BH, the restore handler in the operation dispatcher, which zeroes the current cylinder at offset 05H of the Drive Control Table entry and issues a Restore command to bring the head back to track zero. That handler ends in a jump to the resident Type I command routine, which returns directly to this routine's caller.

544FH - Driver Entry and Disk Operation Dispatcher

The driver's entry point. Every Drive Control Table vector that FDUBL rewrote points at the module header, whose relative jump lands here. Register B holds the LDOS disk operation code and Register Pair IY addresses the Drive Control Table entry for the drive concerned. The structure follows the resident SYS0 dispatcher at 45FBH, with the seek and read/write cases redirected into this module and the remaining cases handed straight back to the resident code.

544F
LD A,B 78
Load Register A with Register B, which holds the disk operation code supplied by the resident disk primitive that called this driver: 01H select, 06H seek, 07H reselect, 09H read sector, 0AH verify sector, 0DH write sector, 0EH write protected sector, 0FH write track.
5450
OR A B7
OR Register A, holding the operation code, with itself to set the flags. If the code is zero the Z FLAG is set.
5451
RET Z C8
If the Z FLAG (Zero) has been set, the operation code in Register A is zero, which is the no-operation case. Return to the caller with nothing done.
5452
CP 07H FE 07
Compare Register A, holding the operation code, against 07H, the reselect operation. If they are equal the Z FLAG is set; if the code is below 07H the CARRY FLAG is set; if it is above, the NO CARRY FLAG is set.
5454
If the Z FLAG (Zero) has been set, the operation code in Register A is 07H, reselect. JUMP to the resident controller-ready wait at 461CH, which re-asserts the drive-select latch at 37E1H from the saved copy at 4309H until the controller is no longer busy and then returns to this driver's caller. No density work is needed for a reselect.
If the NO CARRY FLAG has been set, the operation code in Register A is above 07H, so it is a read, verify or write of a sector or a track. JUMP to 5477H, this module's own read/write setup, rather than to the resident engine at 466BH.
545A
CP 06H FE 06
Compare Register A, holding the operation code, against 06H, the seek operation. If they are equal the Z FLAG is set; otherwise the NZ FLAG is set.
If the Z FLAG (Zero) has been set, the operation code in Register A is 06H, a seek. JUMP to 5415H to set write precompensation for the destination cylinder before handing over to the resident seek routine. Self-Modifying Code
The displacement byte at 545DH is rewritten to 0CCH at 5259H for the Percom build, which redirects this jump to 542AH so that precompensation is skipped entirely.
545E
DEC A 3D
DECrement Register A by 1. Register A holds an operation code below 06H, so this both tests for the select operation 01H and reduces the remaining step codes by one for the comparison below.
If the Z FLAG (Zero) has been set, the operation code was 01H, drive select. JUMP to 5474H, which hands the request to the resident drive-select routine.
5461
INC (IY+05H) FD 34 05
INCrement the byte at offset 05H of the Drive Control Table entry addressed by Register Pair IY, the drive's current cylinder number. The remaining operation codes are the step-in commands, which move the head one cylinder towards the centre of the disk, so the recorded position is advanced to match.
5464
CP 04H FE 04
Compare Register A, holding the operation code less one, against 04H. Equality identifies the original code 05H, the step-in operation, and sets the Z FLAG; otherwise the NZ FLAG is set.
5466
LD B,58H 06 58
Load Register B with 58H, the WD1771 Step In command with the head-load and track-register-update bits set. The resident Type I command routine merges the drive's stepping rate from bits 0 and 1 of offset 03H of the Drive Control Table entry into this value before issuing it.
5468
If the Z FLAG (Zero) has been set, the operation was a step in. JUMP to the resident Type I command routine at 4659H, which selects the drive through 45E0H, merges the stepping rate from bits 0 and 1 of offset 03H of the Drive Control Table entry with the command in Register B, writes the result to the controller command register at 37ECH, allows a settle delay and returns with Register A zeroed. On the Model I WD1771 those two bits give 3ms, 6ms, 10ms and 15ms.
546B
LD (IY+05H),00H FD 36 05 00
Store zero into offset 05H of the Drive Control Table entry addressed by Register Pair IY, the drive's current cylinder number, because the Restore command issued below drives the head all the way back to track zero. Also entered from 544DH after a density change.
546F
LD B,08H 06 08
Load Register B with 08H, the WD1771 Restore command with the head-load bit set, which steps the head outward until the track zero sensor is reached.
5471
JUMP to the resident Type I command routine at 4659H to issue the Restore command held in Register B, merged with the drive's stepping rate from offset 03H of the Drive Control Table entry, to the controller command register at 37ECH. That routine returns directly to this driver's caller.
5474
JUMP to the resident drive-select routine at 45E0H, which waits for the controller to fall idle, builds the drive-select value by rotating the side select flag from bit 4 of offset 03H of the Drive Control Table entry into bit 3 and merging the unit bits from offset 04H, saves the result at 4309H and writes it to the drive-select latch at 37E1H, then waits for the motor to reach speed when bit 7 of Register A asks for it. It returns directly to this driver's caller.

5477H - Read, Verify and Write Sector Setup

Reached from 5457H for every operation code above 07H. Sets the hardware density latch for the drive, chooses the controller command byte and the retry count appropriate to the operation, refuses a write to a write protected drive, and installs the correct two-byte body into the data transfer loop by way of the inline parameter block reader at 54B1H. The three parameter blocks embedded in this routine are read as data by that routine, never executed.

5477
GOSUB to the resident controller-ready wait at 461CH, which returns once the busy bit of the controller status register at 37ECH is clear, re-asserting the drive-select latch at 37E1H from the saved copy at 4309H on every pass.
GOSUB to 5558H, the density latch routine, so that the controller responding at 37ECH through 37EFH is the one matching bit 6 of offset 03H of the Drive Control Table entry addressed by Register Pair IY. This must happen before any command is issued.
547D
LD A,B 78
Load Register A with Register B, which still holds the disk operation code: 09H read sector, 0AH verify sector, 0DH write sector, 0EH write sector with a deleted data address mark, 0FH write track.
547E
BIT 2,B CB 50
Test bit 2 of Register B, the operation code. That bit is set for every write code, 0DH, 0EH and 0FH, and clear for the read and verify codes 09H and 0AH. If the bit is clear the Z FLAG is set; if it is set the NZ FLAG is set.
5480
LD BC,06A8H 01 A8 06
Load Register Pair BC with 06A8H, the write case parameters. Register B becomes 06H, six retry attempts, and Register C becomes 0A8H, the Write Sector command for a single record with a normal data address mark.
If the NZ FLAG (Not Zero) has been set, bit 2 of the operation code in Register B was set and this is a write. JUMP to 549AH with the write parameters in Register Pair BC, skipping the read and verify cases below. The transfer loop body for a write is installed later, at 54ABH.
5485
LD BC,0488H 01 88 04
Load Register Pair BC with 0488H, the read and verify case parameters. Register B becomes 04H, four retry attempts, and Register C becomes 88H, the Read Sector command for a single record in IBM format.
5488
CP 0AH FE 0A
Compare Register A, holding the operation code copied from Register B at 547DH, against 0AH, the verify sector operation. If they are equal the Z FLAG is set; otherwise the NZ FLAG is set.
If the Z FLAG (Zero) has been set, the operation code is 0AH, verify. JUMP to 5494H to install the verify form of the transfer loop, keeping the four retries already in Register B.
548C
LD B,0AH 06 0A
Load Register B with 0AH, decimal 10, the retry count for a genuine read. A read is given more attempts than a verify because a read failure loses data while a verify failure only reports it.
GOSUB to 54B1H, the inline parameter block reader, which takes the three bytes at 5491H-5493H as its arguments and does not return to the instruction following them. It plants 01H as the base error number in the resident status decoder cell at 46F6H and 1AH 02H as the transfer loop body at 550AH-550BH, then falls through into the command issue routine at 54BFH.
5491
DEFB 01H, 1AH, 02H 01 1A 02
Inline parameter block for the read case. This is data, not code. 01H is the base error number written to 46F6H inside the resident controller status decoder. 1AH and 02H are the opcodes LD A,(DE) and LD (BC),A, which as the transfer loop body read a byte from the controller data register addressed by Register Pair DE and store it into the caller's buffer addressed by Register Pair BC.
5494
GOSUB to 54B1H, the inline parameter block reader, taking the three bytes at 5497H-5499H as its arguments. As above it does not return to the following instruction but falls through into the command issue routine at 54BFH.
5497
DEFB 01H, 1AH, 7EH 01 1A 7E
Inline parameter block for the verify case. This is data, not code. 01H is the base error number written to 46F6H. 1AH and 7EH are the opcodes LD A,(DE) and LD A,(HL), so the transfer loop reads each byte from the controller data register addressed by Register Pair DE and immediately discards it by overwriting Register A from the status register addressed by Register Pair HL. Nothing is stored, which is exactly what a verify requires.
549A
CP 0EH FE 0E
Compare Register A, holding the operation code, against 0EH, the write with a deleted data address mark that LDOS uses for directory sectors. If the code is below 0EH the CARRY FLAG is set; if it equals 0EH the Z FLAG is set; if it is above, meaning 0FH write track, the NO CARRY FLAG is set with the NZ FLAG.
If the CARRY FLAG has been set, the operation code in Register A is below 0EH, so it is an ordinary read, verify or write and the command already in Register C is correct. JUMP to 54A4H.
549E
LD C,0F4H 0E F4
Load Register C with 0F4H, the Write Track command that formats a whole track. This is the command for operation code 0FH.
If the NZ FLAG (Not Zero) has been set, the comparison at 549AH found the operation code above 0EH, so it is 0FH and the Write Track command just loaded into Register C is correct. JUMP to 54A4H.
54A2
LD C,0A9H 0E A9
Load Register C with 0A9H, the Write Sector command with the deleted data address mark bit set as the WD1791 encodes it. Operation code 0EH is how LDOS writes a directory sector. The controller in circuit for a double-density operation is the 1791 on the doubler board, which selects a deleted mark with bit 0 alone; the check at 54DEH converts this to the WD1771 encoding when the drive is running in single density.
54A4
BIT 7,(IY+03H) FD CB 03 7E
Test bit 7 of the byte at offset 03H of the Drive Control Table entry addressed by Register Pair IY, the drive specification byte. That bit marks the drive as write protected. If the bit is clear the Z FLAG is set; if it is set the NZ FLAG is set. The test is made for every operation, but only matters on the write path because a read never reaches the return below with a non-zero result of consequence.
54A8
LD A,0FH 3E 0F
Load Register A with 0FH, decimal 15, the LDOS error number for a write protected diskette. This is loaded unconditionally so the return below needs no second instruction.
54AA
RET NZ C0
If the NZ FLAG (Not Zero) has been set, bit 7 of offset 03H of the Drive Control Table entry is set and the drive is write protected. Return to the caller with error 0FH in Register A.
GOSUB to 54B1H, the inline parameter block reader, taking the three bytes at 54AEH-54B0H as its arguments. This is the write case, reached either by falling through from the read and verify path or directly from the jump at 5483H. As before it does not return but falls into the command issue routine at 54BFH.
54AE
DEFB 09H, 12H, 0AH 09 12 0A
Inline parameter block for the write case. This is data, not code. 09H is the base error number written to 46F6H inside the resident controller status decoder, which is eight higher than the read base so that the same status bit reports a write error rather than a read error. 12H and 0AH are the opcodes LD (DE),A and LD A,(BC), so the transfer loop first stores the byte already in Register A into the controller data register addressed by Register Pair DE and then fetches the next byte from the caller's buffer addressed by Register Pair BC. Fetching one byte ahead of the controller is what allows the loop to keep up with the doubled data rate.

54B1H - Inline Parameter Block Reader

Called with three parameter bytes placed immediately after the CALL. The routine takes the return address off the stack, uses it to read those three bytes, and then discards it, so control never comes back to the caller and instead falls through into the command issue routine below. The first parameter byte sets the base error number inside the resident controller status decoder in SYS0, and the following word is planted as the two-byte body of this module's data transfer loop.

54B1
EX (SP),HL E3
Exchange Register Pair HL with the word on top of the stack. That word is the return address pushed by the CALL, which is the address of the first parameter byte, so Register Pair HL now addresses the parameter block and the caller's Register Pair HL is held on the stack.
54B2
LD A,(HL) 7E
Load Register A with the byte at the address in Register Pair HL, the first parameter: 01H for a read or verify, 09H for a write.
54B3
INC HL 23
INCrement Register Pair HL by 1 so it addresses the second parameter byte.
54B4
Self-Modifying Code
Store Register A, holding the base error number, into 46F6H. That address is not part of this module: it is the operand of the LD A instruction at 46F5H inside the resident controller status decoder in SYS0. That decoder adds the position of the lowest set bit of the controller status byte to this base to produce the LDOS error number, so writing 01H here makes it report read errors and writing 09H makes it report write errors.
54B7
LD A,(HL) 7E
Load Register A with the byte at the address in Register Pair HL, the second parameter byte, which becomes the first opcode of the transfer loop body.
54B8
INC HL 23
INCrement Register Pair HL by 1 so it addresses the third parameter byte.
54B9
LD H,(HL) 66
Load Register H with the byte at the address in Register Pair HL, the third parameter byte, which becomes the second opcode of the transfer loop body.
54BA
LD L,A 6F
Load Register L with Register A, holding the second parameter byte saved at 54B7H. Register Pair HL now holds the two transfer loop opcodes with the first in Register L and the second in Register H, which is the order a 16-bit store writes them to memory.
54BB
LD (550AH),HL 22 0A 55
Self-Modifying Code
Store Register Pair HL, holding the two transfer opcodes, into 550AH-550BH, the body of the data transfer loop. The listing shows those two bytes as 00H; at run time they hold LD A,(DE) and LD (BC),A for a read, LD A,(DE) and LD A,(HL) for a verify, or LD (DE),A and LD A,(BC) for a write.
54BE
POP HL E1
Restore Register Pair HL from the stack, recovering the caller's value that EX (SP),HL placed there at 54B1H. The return address is not restored to the stack, so this routine cannot return; execution falls through into the command issue routine below and the three parameter bytes following the CALL are never executed.

54BFH - Command Issue, Data Transfer and Error Recovery

The heart of the driver, entered by falling through from the parameter block reader and re-entered at the same address for every retry. It performs the implied seek, loads the controller track register, adjusts the deleted data address mark command for the density in use, issues the command, runs the data transfer loop, and then examines the resulting status. A lost data indication retries immediately; a record not found or CRC failure toggles the density before retrying; anything else is converted to an LDOS error number by the resident decoder. The transfer loop itself is written out five times over so that the driver can keep pace with the doubled data rate of a double-density track, where a data request arrives roughly every 16 microseconds.

54BF
PUSH BC C5
Loop Start
Save Register Pair BC onto the stack. Register B holds the retry count and Register C the controller command byte, and both are destroyed by the transfer below. Every retry re-enters at this instruction.
54C0
BIT 4,C CB 61
Test bit 4 of Register C, the controller command byte. That bit is clear in the Read Sector command 88H and the Write Sector commands 0A8H and 0A9H, and set in the Write Track command 0F4H. If the bit is clear the Z FLAG is set; if it is set the NZ FLAG is set. Formatting a track needs no implied seek because the caller has already positioned the head.
If the Z FLAG (Zero) has been set, the command in Register C is a sector read or write. GOSUB to 5415H to set write precompensation for the current cylinder and then perform the implied seek through the resident routine at 462DH. Self-Modifying Code
The operand at 54C3H-54C4H is rewritten to 462AH at 5262H for the Percom build, so that the seek is performed by the resident routine alone and no precompensation is attempted.
54C5
PUSH IX DD E5
Save Register Pair IX onto the stack. The caller's Register Pair IX holds the file control block address for the operation in progress, and this routine needs Register Pair IX as the entry pointer into the transfer loop.
54C7
PUSH DE D5
Save Register Pair DE onto the stack. Register D holds the destination cylinder and Register E the sector number supplied by the caller, and Register Pair DE is about to be reloaded with the controller data register address.
54C8
EX AF,AF' 08
Exchange Register Pair AF with the alternate Register Pair AF. The alternate set becomes active so the caller's flags and Register A can be preserved by the PUSH below and so that the transfer loop has a scratch accumulator of its own.
54C9
PUSH AF F5
Save Register Pair AF onto the stack. Following the exchange above this saves the caller's original Register A and flags.
54CA
PUSH HL E5
Save Register Pair HL onto the stack. Register Pair HL holds whatever the caller left in it and is about to be loaded with a controller register address.
54CB
LD IX,550BH DD 21 0B 55
Load Register Pair IX with 550BH, the second byte of the transfer loop body. The read path jumps to this address through Register Pair IX after it has already fetched the first data byte, so that the loop is entered at its store step rather than at its fetch step.
54CF
LD HL,37EDH 21 ED 37
Point Register Pair HL to 37EDH, the controller track register. It will be decremented one address to 37ECH, the command and status register, after the track has been written, which saves loading Register Pair HL twice.
54D2
GOSUB to the resident controller-ready wait at 461CH to be certain the controller has finished the implied seek, re-asserting the drive-select latch at 37E1H from the saved copy at 4309H on every pass.
54D5
GOSUB to 46FDH, a two-byte resident cell holding a NOP followed by a RET. It is a patch point rather than a routine: the SYSTEM SMOOTH option replaces the NOP with a DI so that interrupts are suppressed across the sector transfer, which removes the flicker interrupts cause but costs the clock its ticks. Calling it here gives the double-density driver the same behaviour the resident driver has.
54D8
LD (HL),D 72
Store Register D, the destination cylinder supplied by the caller, into the byte addressed by Register Pair HL, which holds 37EDH, the controller track register. The controller compares this against the track address recorded in each sector header it passes.
54D9
DEC HL 2B
DECrement Register Pair HL by 1, from 37EDH to 37ECH, the controller command and status register. Register Pair HL keeps that value for the rest of the transfer, where it is the address the data request polling loop reads.
54DA
LD DE,37EFH 11 EF 37
Point Register Pair DE to 37EFH, the controller data register. This is the address the transfer loop body reads from on a read and writes to on a write.
54DD
LD A,C 79
Load Register A with Register C, the controller command byte chosen at 5480H, 5485H, 549EH or 54A2H.
54DE
CP 0A9H FE A9
Compare Register A, holding the controller command byte, against 0A9H, the Write Sector command with a deleted data address mark as the WD1791 on the doubler board encodes it. If they are equal the Z FLAG is set; otherwise the NZ FLAG is set.
If the NZ FLAG (Not Zero) has been set, the command in Register A is not the deleted data address mark write and needs no adjustment. JUMP to 54EAH to issue it as it stands.
54E2
BIT 6,(IY+03H) FD CB 03 76
Test bit 6 of the byte at offset 03H of the Drive Control Table entry addressed by Register Pair IY, the drive's double-density flag. If the bit is clear the Z FLAG is set, meaning the drive is running single density and the original WD1771 in the Expansion Interface is the controller in circuit; if it is set the NZ FLAG is set, meaning the WD1791 on the doubler board is answering.
If the NZ FLAG (Not Zero) has been set, bit 6 of offset 03H of the Drive Control Table entry is set and the WD1791 is in circuit, which encodes a deleted data address mark exactly as the 0A9H already in Register A does. JUMP to 54EAH to issue it unchanged.
54E8
LD A,0ABH 3E AB
Load Register A with 0ABH, the Write Sector command with a deleted data address mark as the WD1771 encodes it. The 1771 uses two bits to select the address mark where the 1791 uses one, so the same intent needs a different command byte when the drive is running single density.
54EA
GOSUB to the resident command issue entry at 4662H, which writes Register A, holding the fully assembled controller command, to the command register at 37ECH, runs a short settle delay and returns with Register A zeroed. This is the second entry of the Type I command routine, taken because the command byte is already complete and no stepping rate is to be merged in.
54ED
BIT 5,C CB 69
Test bit 5 of Register C, the controller command byte. That bit is clear in the Read Sector command 88H and set in every Write Sector and Write Track command. If the bit is clear the Z FLAG is set; if it is set the NZ FLAG is set.
54EF
POP BC C1
Restore Register Pair BC from the stack, recovering the caller's buffer address, which was pushed at 54BFH after the retry count and command byte had been consumed. Register Pair BC is the buffer pointer the transfer loop walks.
54F0
PUSH BC C5
Save Register Pair BC onto the stack again, so that the buffer address remains available for a retry that has to start the transfer over from the beginning.
If the Z FLAG (Zero) has been set, bit 5 of the command in Register C is clear and this is a read or verify. JUMP to 554EH, the read path's first data request wait.
54F3
EI FB
Loop Start
Enable interrupts. The write path waits here for the controller to ask for its first byte, and briefly admitting interrupts on each pass keeps the real time clock running while the head rotates to the target sector.
54F4
LD A,76H 3E 76
Load Register A with 76H, the mask of controller status bits worth waiting on: bit 1 data request, bit 2 lost data, bit 4 record not found, bit 5 write fault and bit 6 write protect.
54F6
DI F3
Disable interrupts before the status test, so that once the controller signals a data request the transfer begins immediately and no interrupt can cost the driver a byte.
54F7
AND (HL) A6
AND Register A, holding the mask 76H, with the byte at the address in Register Pair HL, which holds 37ECH, the controller status register. The result is non-zero once the controller either asks for data or reports one of the failures in the mask.
If the Z FLAG (Zero) has been set, none of the masked bits of the status register at 37ECH is set and the controller is still searching for the sector. LOOP BACK to 54F3H to wait again. Loop End
54FB
LD A,(BC) 0A
Load Register A with the byte at the address in Register Pair BC, the first byte of the caller's buffer, ready to be handed to the controller.
54FC
LD (DE),A 12
Store Register A, holding the first buffer byte, into the byte addressed by Register Pair DE, which holds 37EFH, the controller data register. This satisfies the first data request.
54FD
INC BC 03
INCrement Register Pair BC by 1, advancing the caller's buffer pointer past the byte just written.
54FE
LD A,(BC) 0A
Load Register A with the byte at the address in Register Pair BC, the second buffer byte. The write loop always holds the next byte ready before the controller asks for it.
54FF
INC BC 03
INCrement Register Pair BC by 1, advancing the caller's buffer pointer past the byte just prefetched.
5500
EX AF,AF' 08
Exchange Register Pair AF with the alternate Register Pair AF, parking the prefetched buffer byte in the alternate accumulator so that the status test below can use Register A without losing it.
5501
LD A,(HL) 7E
Load Register A with the byte at the address in Register Pair HL, which holds 37ECH, the controller status register.
5502
AND 0FCH E6 FC
AND Register A, holding the controller status, with 0FCH, clearing bit 1, the data request, and bit 0, the busy indication.
5504
OR 01H F6 01
OR Register A with 01H, setting the busy bit. Register A now holds the status as it should read while the controller is busy and has no outstanding data request, which is the pattern the comparison below waits to stop matching.
5506
CP (HL) BE
Loop Start
Compare Register A, holding the expected busy-with-no-request pattern, against the byte at the address in Register Pair HL, which holds 37ECH, the live controller status. While they match the controller has neither asked for the next byte nor finished, and the Z FLAG is set.
If the Z FLAG (Zero) has been set, the status register at 37ECH still matches the expected pattern. LOOP BACK to 5506H to test again. Loop End
Falling through means the controller has raised a data request or changed state.
5509
EX AF,AF' 08
Exchange Register Pair AF with the alternate Register Pair AF, bringing the prefetched buffer byte back into Register A ready for the transfer loop below.
550A
NOP 00
Self-Modifying Code
First opcode of the data transfer loop body, planted by the parameter block reader at 54BBH. The listing shows the assembled placeholder 00H. At run time it is 1AH, LD A,(DE), reading the controller data register at 37EFH for a read or verify, or 12H, LD (DE),A, writing the prefetched buffer byte to it for a write.
550B
NOP 00
Self-Modifying Code
Second opcode of the data transfer loop body, planted by the same store at 54BBH. At run time it is 02H, LD (BC),A, storing the byte just read into the caller's buffer for a read; 7EH, LD A,(HL), discarding it for a verify; or 0AH, LD A,(BC), prefetching the next buffer byte for a write. This is the address the read path enters through Register Pair IX, having already fetched its first byte at 5554H.
550C
INC BC 03
INCrement Register Pair BC by 1, advancing the caller's buffer pointer to the next byte.
550D
BIT 1,(HL) CB 4E
Loop Start
Test bit 1, the data request, of the byte at the address in Register Pair HL, which holds 37ECH, the controller status register. If the controller wants another byte the NZ FLAG is set. The five copies of this test that follow are an unrolled loop: at double density a data request arrives about every 16 microseconds, so testing repeatedly without the cost of a loop-back on every pass is what allows the driver to keep up.
If the NZ FLAG (Not Zero) has been set, the controller has raised a data request. LOOP BACK to 550AH to transfer the next byte. The first two tests use the long jump form, which costs 10 cycles whether or not it is taken, while the relative form used further down costs 12 when taken and 7 when not.
5512
BIT 1,(HL) CB 4E
Test bit 1, the data request, of the byte at 37ECH addressed by Register Pair HL. Second of the five unrolled tests.
If the NZ FLAG (Not Zero) has been set, the controller has raised a data request. LOOP BACK to 550AH to transfer the next byte.
5517
BIT 1,(HL) CB 4E
Test bit 1, the data request, of the byte at 37ECH addressed by Register Pair HL. Third of the five unrolled tests.
If the NZ FLAG (Not Zero) has been set, the controller has raised a data request. LOOP BACK to 550AH to transfer the next byte.
551B
BIT 1,(HL) CB 4E
Test bit 1, the data request, of the byte at 37ECH addressed by Register Pair HL. Fourth of the five unrolled tests.
If the NZ FLAG (Not Zero) has been set, the controller has raised a data request. LOOP BACK to 550AH to transfer the next byte.
551F
BIT 1,(HL) CB 4E
Test bit 1, the data request, of the byte at 37ECH addressed by Register Pair HL. Fifth and last of the unrolled tests.
If the NZ FLAG (Not Zero) has been set, the controller has raised a data request. LOOP BACK to 550AH to transfer the next byte.
5523
BIT 0,(HL) CB 46
Test bit 0, the busy indication, of the byte at the address in Register Pair HL, which holds 37ECH, the controller status register. If the controller is still executing the command the NZ FLAG is set; if it has finished the Z FLAG is set.
If the NZ FLAG (Not Zero) has been set, the controller is still busy even though it has not asked for data in five consecutive tests. LOOP BACK to 550DH to resume polling. Loop End
Falling through means the command has completed and the status register at 37ECH holds the final result.
5527
EI FB
Enable interrupts. The transfer is over, so the real time clock and any interrupt-driven tasks can run again.
5528
LD A,(HL) 7E
Load Register A with the byte at the address in Register Pair HL, which holds 37ECH, the controller status register. This is the final status of the completed command.
5529
POP HL E1
Restore Register Pair HL from the stack, recovering the value the caller had in it before 54CAH.
552A
EX AF,AF' 08
Exchange Register Pair AF with the alternate Register Pair AF, parking the final controller status in the alternate accumulator so that the caller's saved Register A and flags can be popped into the main set.
552B
POP AF F1
Restore Register Pair AF from the stack, recovering the caller's original Register A and flags saved at 54C9H.
552C
EX AF,AF' 08
Exchange Register Pair AF with the alternate Register Pair AF again, bringing the final controller status back into Register A and leaving the caller's Register A and flags parked in the alternate set where they belong.
552D
AND 7CH E6 7C
AND Register A, holding the final controller status from 37ECH, with 7CH, keeping only the error bits: bit 2 lost data, bit 3 CRC error, bit 4 record not found, bit 5 write fault and bit 6 write protect. The data request and busy bits are discarded because the command has finished.
552F
POP DE D1
Restore Register Pair DE from the stack, recovering the caller's destination cylinder in Register D and sector number in Register E, saved at 54C7H.
5530
POP IX DD E1
Restore Register Pair IX from the stack, recovering the caller's file control block address saved at 54C5H.
5532
POP BC C1
Restore Register Pair BC from the stack, recovering the retry count in Register B and the controller command byte in Register C from the copy pushed at 54F0H.
5533
RET Z C8
If the Z FLAG (Zero) has been set, the masked status in Register A is zero and the operation succeeded. Return to the caller with Register A holding zero, which is the driver's success indication.
5534
BIT 2,A CB 57
Test bit 2 of Register A, the masked controller status, which is the lost data indication. Lost data means the processor did not service a data request in time, which is a timing failure rather than a media failure. If the bit is clear the Z FLAG is set; if it is set the NZ FLAG is set.
If the NZ FLAG (Not Zero) has been set, bit 2 of the status is set and data was lost. LOOP BACK to 54BFH to repeat the whole operation without consuming a retry, because nothing is wrong with the diskette.
5538
PUSH AF F5
Save Register Pair AF onto the stack, preserving the masked controller status in Register A across the recovery attempt below.
5539
AND 18H E6 18
AND Register A, holding the masked controller status, with 18H, keeping only bit 3, the CRC error, and bit 4, the record not found indication. These are the two failures that a wrong density setting produces.
If the Z FLAG (Zero) has been set, neither a CRC error nor a record not found is present, so the failure is a write fault or a write protect violation and no retry will help. JUMP to 554AH to recover the saved status and convert it to an LDOS error number.
553D
BIT 4,A CB 67
Test bit 4 of Register A, which now holds the status masked to the CRC and record not found bits. Record not found is the signature of a diskette recorded in the other density, because the controller cannot read the sector headers at all. If the bit is clear the Z FLAG is set; if it is set the NZ FLAG is set.
553F
PUSH BC C5
Save Register Pair BC onto the stack, preserving the retry count in Register B and the controller command in Register C across the density change below.
If the NZ FLAG (Not Zero) has been set, the failure was a record not found. GOSUB to 542DH to invert the density flag at offset 03H of the Drive Control Table entry addressed by Register Pair IY, reload the geometry bytes at offsets 07H and 08H to match, set the hardware density latch, and on alternate retries restore the head to track zero. A CRC error alone is retried at the same density.
5543
POP BC C1
Restore Register Pair BC from the stack, recovering the retry count in Register B and the controller command in Register C.
5544
POP AF F1
Restore Register Pair AF from the stack, recovering the full masked controller status saved at 5538H in case the retries run out and it has to be reported.
5545
DEC B 05
DECrement Register B by 1, the count of retry attempts still available: ten for a read, six for a write and four for a verify, as set at 548CH, 5480H and 5485H.
If the NZ FLAG (Not Zero) has been set, retries remain. LOOP BACK to 54BFH to reissue the whole operation, at whatever density the recovery above left set. Loop End
5549
LD B,0F1H 06 F1
The retries are exhausted and the controller status is already in Register A, having been popped at 5544H. Loading Register B here is of no consequence; the instruction exists so that its second byte, 0F1H, can be executed on its own as a POP AF by the jump from 553BH, which arrives with the status still on the stack. This is the standard idiom for skipping one byte.
554A
POP AF F1
Alternate entry, taken from 553BH when the failure was neither a CRC error nor a record not found. Restore Register Pair AF from the stack, recovering the masked controller status pushed at 5538H into Register A.
554B
JUMP to the resident controller status decoder at 46F4H with the masked status in Register A. That routine copies the status into Register B at 46F4H, loads Register A with the base error number that the parameter block reader planted in the operand at 46F6H, and rotates Register B right until a set bit falls into the CARRY FLAG, incrementing Register A once per rotation. It returns to this driver's caller with Register A holding the LDOS error number for the failure.
554E
LD A,(HL) 7E
Loop Start
Load Register A with the byte at the address in Register Pair HL, which holds 37ECH, the controller status register. This is the read path's wait for the first byte of the sector.
554F
AND 03H E6 03
AND Register A, holding the controller status, with 03H, keeping bit 1, the data request, and bit 0, the busy indication.
If the Parity Odd condition holds, exactly one of the two remaining bits is set, which means the controller is busy but has no data ready. LOOP BACK to 554EH to test again. Loop End
Parity even ends the wait, and covers both outcomes worth acting on: both bits set, meaning a byte is waiting, and both bits clear, meaning the command finished without ever producing one.
5554
LD A,(DE) 1A
Load Register A with the byte at the address in Register Pair DE, which holds 37EFH, the controller data register. This is the first byte of the sector, fetched here so that the transfer loop can be entered at its store step.
5555
DI F3
Disable interrupts for the duration of the transfer, so that no interrupt can delay the driver past the controller's next data request.
5556
JP (IX) DD E9
JUMP to the address in Register Pair IX, which holds 550BH, the second opcode of the transfer loop body. Entering there rather than at 550AH skips the fetch step, because the first byte of the sector is already in Register A.

5558H - Density Latch, Radio Shack Double-Density Adapter

Sets the hardware density selection from the drive's density flag, and is the last routine of the Radio Shack module. On the Radio Shack Double-Density Adapter the three high-order bits of a write to the controller's sector register at 37EEH are decoded by a one-of-eight decoder on the adapter board and used to latch the density selection and the write-precompensation enable; because a real sector number never has those bits set, the ordinary sector writes the resident driver performs are unaffected. When the Percom variant is built, the install-time block move at 5255H overwrites these twelve bytes with the sixteen-byte Percom version from 5564H, so every caller reaches the right mechanism without any call site changing.

5558
LD A,(IY+03H) FD 7E 03
Load Register A with the byte at offset 03H of the Drive Control Table entry addressed by Register Pair IY, the drive specification byte whose bit 6 is the double-density flag.
555B
AND 40H E6 40
AND Register A, holding the drive specification byte, with 40H, isolating bit 6. Register A becomes 40H when the drive is double density and zero when it is single density.
555D
RRCA 0F
Rotate Register A right, moving the isolated density flag from bit 6 to bit 5. Register A becomes 20H for double density and zero for single density.
555E
XOR 0A0H EE A0
XOR Register A, holding 20H or zero, with 0A0H. Register A becomes 80H when the drive is double density and 0A0H when it is single density. Both values select the adapter's density latch through their three high-order bits, and the difference between them is the density itself.
5560
LD (37EEH),A 32 EE 37
Store Register A, holding 80H for double density or 0A0H for single density, into the controller sector register at 37EEH. On the Radio Shack adapter this write is intercepted by the board's decoder and latches the density, which decides whether the original WD1771 or the WD1791 on the adapter answers at 37ECH through 37EFH.
5563
RET C9
Return to the caller, either 5447H after a density toggle or 547AH at the start of a sector operation. This is the last byte of the Radio Shack module, which is why 535AH is assembled with 5563H.

5564H - Density Latch, Percom Doubler (install-time source)

The Percom Doubler carries no decoder on the sector register. Its density latch is set instead by writing a command byte to the controller command register at 37ECH that no Western Digital controller implements, 0FEH or 0FFH, which the board recognises and consumes; a Force Interrupt is then issued so that the controller does not sit waiting on the meaningless command. These sixteen bytes are never executed where they are assembled: the install-time block move at 5255H copies them over the Radio Shack routine at 5558H, and this copy falls outside the module and is discarded.

5564
LD A,(IY+03H) FD 7E 03
Load Register A with the byte at offset 03H of the Drive Control Table entry addressed by Register Pair IY, the drive specification byte whose bit 6 is the double-density flag.
5567
RLCA 07
Rotate Register A left, moving the density flag from bit 6 to bit 7. This is the first of the two rotations that bring it down to bit 0.
5568
RLCA 07
Rotate Register A left again, moving the density flag from bit 7 round to bit 0. Register A now has its lowest bit set when the drive is double density.
5569
OR 0FEH F6 FE
OR Register A with 0FEH, forcing every bit except bit 0 to one. Register A becomes 0FFH for double density and 0FEH for single density, the two values the Percom Doubler recognises as density latch commands.
556B
LD (37ECH),A 32 EC 37
Store Register A, holding 0FEH or 0FFH, into the controller command register at 37ECH. The Percom board decodes this as a density selection rather than as a controller command, which is possible because no genuine Western Digital command uses those two encodings.
556E
LD A,0D0H 3E D0
Load Register A with 0D0H, the Force Interrupt command with all four interrupt condition bits clear, which terminates whatever the controller is doing without raising an interrupt.
5570
LD (37ECH),A 32 EC 37
Store Register A, holding the Force Interrupt command 0D0H, into the controller command register at 37ECH. This clears any state the controller may have entered from seeing the density byte and leaves it idle and ready for a real command.
5573
RET C9
Return to the caller. After the block move at 5255H this RET sits at 5567H, which is why the Percom build records 5567H as its module end address at 535AH.

5574H - FDUBL1 Patch Routine

Added to the file by the applied patch FDUBL1, whose patch records replace the instruction at 5265H with a call to this routine and append these nine bytes. Its only purpose is to correct the module header's entry displacement for the Percom build. It runs at install time and is not part of either module.

5574
LD A,2CH 3E 2C
Load Register A with 2CH, decimal 44, the relative jump displacement that reaches the operation dispatcher at 544FH from a module header based at 5421H. The assembled value of 41H, decimal 65, is correct only for the Radio Shack header based at 540CH, 21 bytes lower.
5576
LD (540DH),A 32 0D 54
Self-Modifying Code
Store Register A, holding 2CH, into 540DH, the displacement byte of the relative jump in the module header. The header is copied to 5421H by the LDDR at 526EH immediately after this routine returns, so the corrected displacement travels with it. Without this store the relocated Percom module entered its driver at what would be 5464H in the assembled listing, in the middle of the dispatcher's step-code comparison, which is the defect FDUBL1 repairs.
5579
LD HL,5414H 21 14 54
Point Register Pair HL to 5414H, the last byte of the module header, which is the final D of the name $FDD. This is the value the instruction at 5265H loaded directly before the patch was applied, and it is the source pointer the decrementing block move at 526EH requires.
557C
RET C9
Return to 5268H with Register Pair HL holding 5414H, ready for the header to be moved up to 5421H-5429H.