Model I TRSDOS v2.3 TAPEDISK/CMD Explained

Introduction

This is a disassembly of TRSDOS v2.3's TAPEDISK/CMD File, which is a self-contained cassette loader/monitor for the TRS-80.

Key features:

Command 'C' - Loads a program from cassette using sync detection (A5H, 55H pattern), checksummed data blocks, and auto-execution

Command 'E' - Exits to address 402DH

Command 'F' - Saves memory to cassette with format: F name:SSSS EEEE XXXX

Self-saving - The 'F' command actually saves THIS LOADER ITSELF (5200H-53F1H) to tape!

Visual feedback - Flashes screen corner during load

recovery - Attempts to resync after checksum errors

Disassembly

5200H - Main Entry Point and Command Prompt

This is the main entry point of the cassette loader/monitor. It initializes the stack and prompts the user with a '?' for command input. The three valid commands are: C (load from Cassette), E (Exit), and F (save File to cassette).

5200
LD SP,41FCH 31FC41
Initialize the Stack Pointer to 41FCH, which is just below the program's working area. This ensures stack operations won't overwrite program code or data.
5203
LD A,0DH 3E0D
Load Register A with 0DH (ASCII: CARRIAGE RETURN) to start output on a new line.
5205
GOSUB to ROM routine at 0033H to display the character in Register A (carriage return) at the current cursor position.
5208
LD A,3FH 3E3F
Load Register A with 3FH (ASCII: ?) as the command prompt character.
520A
GOSUB to ROM routine at 0033H to display the '?' prompt on screen.
520D
LD B,3FH 063F
Load Register B with 3FH (63 decimal) as the maximum number of characters the user can input.
520F
LD HL,4318H 211843
Point Register Pair HL to 4318H, which is the input buffer where the user's command line will be stored.
5212
GOSUB to ROM routine at 05D9H to get a line of input from the keyboard. Input is stored at (HL), max length in B. Returns with Carry set if BREAK was pressed.
5215
If the C FLAG (Carry) has been set (user pressed BREAK), LOOP BACK to 5208H to re-display the '?' prompt and wait for input again.

5217H - Command Dispatch

At this point, HL points to the input buffer at 4318H containing the user's command. We now check the first character to determine which command to execute.

5217
LD A,(HL) 7E
Fetch the first character of the user's input from the buffer pointed to by HL and store it into Register A.
5218
CP 43H FE43
Compare the value held in Register A against 43H (ASCII: C). If Register A equals C, the Z FLAG is set; otherwise the NZ FLAG is set.
521A
If the Z FLAG (Zero) has been set (command is 'C'), JUMP to 52CFH to execute the Cassette Load routine.
521D
NOP 00
No Operation. This and the following 4 NOPs are patch space reserved for future modifications or additional commands.
521E
NOP 00
No Operation (patch space).
521F
NOP 00
No Operation (patch space).
5220
NOP 00
No Operation (patch space).
5221
NOP 00
No Operation (patch space).
5222
CP 45H FE45
Compare the value held in Register A against 45H (ASCII: E). If Register A equals E, the Z FLAG is set; otherwise the NZ FLAG is set.
5224
If the Z FLAG (Zero) has been set (command is 'E'), JUMP to 402DH to Exit the program and return to whatever called this loader.
5227
CP 46H FE46
Compare the value held in Register A against 46H (ASCII: F). If Register A equals F, the Z FLAG is set; otherwise the NZ FLAG is set.
5229
If the NZ FLAG (Not Zero) has been set (command is NOT 'F'), JUMP to 52C4H to display the "SN ERROR" (Syntax Error) message.

522CH - Parse 'F' (File Save) Command Arguments

The 'F' command saves memory to cassette tape. The command format is: F filename:SSSS EEEE XXXX where SSSS=start address (hex), EEEE=end address (hex), XXXX=execution address (hex). All addresses are entered as 4 hex digits.

522C
GOSUB to 535EH to skip any whitespace and point HL to the first non-space character (the filename).
522F
LD (5269H),HL 226952
Store the filename pointer (HL) into memory location 5269H. This saves the start of the filename for later use when writing the cassette header.

Now scan forward through the filename until we find the ':' delimiter.

5232
LD A,(HL) 7E
[SCAN LOOP] Fetch the character at the current position pointed to by HL.
5233
INC HL 23
INCrement Register Pair HL to point to the next character.
5234
CP 3AH FE3A
Compare the value held in Register A against 3AH (ASCII: :). If we found the colon delimiter, the Z FLAG will be set.
5236
If the NZ FLAG (Not Zero) has been set (not a colon), LOOP BACK to 5232H to continue scanning for the ':' delimiter.

Found the ':' - now parse the START address (4 hex digits = 2 bytes, high byte first).

5238
GOSUB to 535EH to skip any whitespace after the colon.
523B
GOSUB to 5376H to parse two hex digits and convert them to a binary byte in Register A. This gets the HIGH byte of the start address.
523E
LD (5278H),A 327852
Store the start address HIGH byte into memory location 5278H. NOTE: This is self-modifying code - it becomes the operand of an instruction later.
5241
INC HL 23
INCrement Register Pair HL to point past the two hex digits just parsed.
5242
GOSUB to 5376H to parse the next two hex digits. This gets the LOW byte of the start address.
5245
LD (5277H),A 327752
Store the start address LOW byte into memory location 5277H.

Now parse the END address (4 hex digits = 2 bytes, high byte first).

5248
GOSUB to 535EH to skip any whitespace before the end address.
524B
GOSUB to 5376H to parse two hex digits for the HIGH byte of the end address.
524E
LD (527BH),A 327B52
Store the end address HIGH byte into memory location 527BH.
5251
INC HL 23
INCrement Register Pair HL to point past the two hex digits just parsed.
5252
GOSUB to 5376H to parse the LOW byte of the end address.
5255
LD (527AH),A 327A52
Store the end address LOW byte into memory location 527AH.

Now parse the EXECUTION address (4 hex digits = 2 bytes, high byte first).

5258
GOSUB to 535EH to skip any whitespace before the execution address.
525B
GOSUB to 5376H to parse two hex digits for the HIGH byte of the execution address.
525E
LD (53F3H),A 32F353
Store the execution address HIGH byte into memory location 53F3H for inclusion in the cassette file trailer.
5261
INC HL 23
INCrement Register Pair HL to point past the two hex digits just parsed.
5262
GOSUB to 5376H to parse the LOW byte of the execution address.
5265
LD (53F2H),A 32F253
Store the execution address LOW byte into memory location 53F2H.

5268H - Initialize Cassette Output and Calculate Data Length

All three addresses have been parsed. Now we initialize the cassette output system and calculate how many bytes need to be saved. KEY INSIGHT: This routine saves the loader program ITSELF (5200H-53F1H) to tape!

5268
LD DE,431AH 111A43
Point Register Pair DE to 431AH, which is just past the command character in the input buffer. This may be used for filename handling.
526B
LD B,00H 0600
Set Register B to 00H to initialize the block counter to zero.
526D
LD HL,53F4H 21F453
Point Register Pair HL to 53F4H, which is the cassette output buffer where data is accumulated before being written to tape.
5270
GOSUB to 4420H to initialize the cassette output system (turn on motor, write leader tone, etc.).
5273
If the NZ FLAG (Not Zero) has been set (initialization error), JUMP to 52BCH to handle the error.

Now calculate the length of data to save. The source is THIS PROGRAM from 5200H to 53F1H.

5276
LD DE,5200H 110052
Point Register Pair DE to 5200H - this is the START of THIS PROGRAM. The loader saves itself to cassette!
5279
LD HL,53F1H 21F153
Point Register Pair HL to 53F1H - this is the END of THIS PROGRAM (just before the output buffer at 53F4H).
527C
XOR A AF
Set Register A to ZERO and clear the Carry flag in preparation for the subtraction.
527D
SBC HL,DE ED52
SUBtract with Carry: HL = HL - DE - Carry = 53F1H - 5200H = 01F1H (497 bytes).
527F
INC HL 23
INCrement HL to include the final byte in the count. HL now = 01F2H (498 bytes total).
5280
LD C,00H 0E00
Set Register C to 00H. C is used as both a buffer index and a checksum accumulator.

5282H - Write Data Blocks to Cassette

This loop writes the program data to cassette in 256-byte blocks. H contains the number of full pages (256-byte blocks) to write, L contains the remaining bytes for the final partial block.

5282
DEC H 25
[PAGE LOOP] DECrement the page counter in H. If H was 01H (one full page), it becomes 00H. If it goes negative, we're done with full pages.
5283
If the M FLAG (Minus/Sign) has been set (H went negative), JUMP to 528DH to handle any remaining partial block.
5286
LD B,00H 0600
Set Register B to 00H. When B=0 and used with DJNZ, this means loop 256 times (a full page of bytes).
5288
GOSUB to 5327H to write one block of B bytes (256 bytes when B=0) from memory starting at DE to the cassette output buffer.
528B
LOOP BACK to 5282H to check if there are more full pages to write.

Done with full 256-byte pages. Now check if there's a partial block remaining.

528D
XOR A AF
Set Register A to ZERO for comparison.
528E
CP L BD
Compare A (zero) with L (remaining byte count). If L=0, there are no remaining bytes.
528F
If the Z FLAG (Zero) has been set (no remaining bytes), JUMP to 5295H to write the file trailer.
5291
LD B,L 45
Load Register B with the remaining byte count from L.
5292
GOSUB to 5327H to write the final partial block of B bytes to cassette.

5295H - Write File Trailer and Close Cassette

All program data has been written. Now write the file trailer which contains the EOF marker (02H 02H) followed by the execution address (low byte, high byte).

5295
LD A,02H 3E02
Load Register A with 02H, which is the EOF (End Of File) marker byte.
5297
GOSUB to 5342H to write the first EOF marker byte (02H) to the cassette output buffer.
529A
GOSUB to 5342H to write the second EOF marker byte (02H) to the cassette output buffer.
529D
LD A,(53F2H) 3AF253
Fetch the execution address LOW byte (stored earlier during command parsing) into Register A.
52A0
GOSUB to 5342H to write the execution address LOW byte to the cassette output buffer.
52A3
LD A,(53F3H) 3AF353
Fetch the execution address HIGH byte into Register A.
52A6
GOSUB to 5342H to write the execution address HIGH byte to the cassette output buffer.

Trailer written. Now flush any remaining data in the buffer and close the cassette file.

52A9
XOR A AF
Set Register A to ZERO for comparison.
52AA
CP C B9
Compare A (zero) with C (buffer index). If C=0, the buffer is empty and doesn't need flushing.
52AB
LD DE,(5269H) ED5B6952
Load Register Pair DE with the filename pointer (saved earlier at 522FH) for the finalization routine.
52AF
If the Z FLAG (Zero) has been set (buffer empty), JUMP to 52B6H to skip the buffer flush.
52B1
GOSUB to 4439H to flush the remaining buffer contents to cassette tape.
52B4
If the NZ FLAG (Not Zero) has been set (write error), JUMP to 52BCH to handle the error.
52B6
GOSUB to 4428H to finalize the cassette write (turn off motor, write trailer tone, etc.).
52B9
If the Z FLAG (Zero) has been set (success), JUMP to 5200H to return to the main command prompt.

52BCH - Error Handler

This routine handles errors from cassette operations. It sets the error bit and calls the error reporting routine.

52BC
OR 80H F680
OR Register A with 80H to set bit 7 (the error flag bit) while preserving the original error code in the lower bits.
52BE
GOSUB to 4409H to report display an error message based on the code in A.
52C1
JUMP to 5200H to return to the main command prompt after error handling.

52C4H - Syntax Error Handler

This routine displays "SN ERROR" when an unrecognized command is entered.

52C4
LD HL,53EAH 21EA53
Point Register Pair HL to 53EAH, which contains the message "SN ERROR" (Syntax Error).
52C7
LD B,08H 0608
Set Register B to 08H - the length of the "SN ERROR" message (8 characters).
52C9
GOSUB to 536EH to display B characters starting at (HL) - prints "SN ERROR".
52CC
JUMP to 5200H to return to the main command prompt.

52CFH - Cassette Load Routine ('C' Command)

This is the cassette load routine, invoked by the 'C' command. It reads a program from cassette tape into memory. The routine first synchronizes with the tape by looking for the sync pattern (A5H followed by 55H), then reads data blocks until an EOF marker is found.

52CF
DI F3
Disable Interrupts. CRITICAL: Cassette bit timing is extremely sensitive - any interrupt would corrupt the data being read.
52D0
GOSUB to 53D5H to turn the cassette motor ON by setting bit 2 of port FFH.

Now search for the first sync byte (A5H) in the cassette data stream.

52D3
[SYNC LOOP 1] GOSUB to 53B0H to read one bit from the cassette and shift it into Register A.
52D6
CP A5H FEA5
Compare Register A against A5H (10100101 binary) - the first sync byte pattern.
52D8
If the NZ FLAG (Not Zero) has been set (not A5H), LOOP BACK to 52D3H to keep reading bits until sync is found.

Found A5H. Now look for the second sync byte (55H).

52DA
[SYNC LOOP 2] GOSUB to 53A6H to read a complete byte (8 bits) from cassette into Register A.
52DD
CP 55H FE55
Compare Register A against 55H (01010101 binary) - the second sync byte pattern.
52DF
If the NZ FLAG (Not Zero) has been set (not 55H), LOOP BACK to 52DAH to keep looking for the 55H sync byte.

Both sync bytes found! Now skip past 6 leader bytes before the actual data begins.

52E1
LD B,06H 0606
Set Register B to 06H - skip 6 leader/padding bytes after the sync pattern.
52E3
[SKIP LOOP] GOSUB to 53A6H to read and discard one byte.
52E6
DECrement B and Jump if Not Zero. Loop until all 6 leader bytes have been skipped.

52E8H - Block Type Detection Loop

This loop reads bytes from the cassette looking for block type markers. Type 3CH indicates a data block, type 78H indicates end of file. The bottom-right character of the screen is toggled to provide a visual "loading" indicator.

52E8
LD A,(3C3FH) 3A3F3C
[BLOCK LOOP START] Fetch the character at video RAM location 3C3FH (bottom-right corner of screen).
52EB
XOR 0AH EE0A
XOR Register A with 0AH to toggle certain bits. This creates a flashing effect as the visual "loading" indicator.
52ED
LD (3C3FH),A 323F3C
Store the toggled value back to video RAM, causing the character to change and indicate loading activity.
52F0
[TYPE LOOP] GOSUB to 53A6H to read one byte from cassette - this should be a block type marker.
52F3
CP 78H FE78
Compare Register A against 78H. This is the EOF (End Of File) marker indicating all data has been loaded.
52F5
If the Z FLAG (Zero) has been set (found EOF marker 78H), JUMP to 531AH to finish loading and execute the program.
52F7
CP 3CH FE3C
Compare Register A against 3CH. This is the DATA BLOCK marker indicating a block of program data follows.
52F9
If the NZ FLAG (Not Zero) has been set (neither EOF nor data block), LOOP BACK to 52F0H to keep looking for a valid block type.

52FBH - Load Data Block

Found a data block marker (3CH). Now read the block: first the byte count, then the 2-byte load address, then the actual data bytes, and finally verify the checksum.

52FB
GOSUB to 53A6H to read the block length byte - how many data bytes follow in this block.
52FE
LD B,A 47
Copy the block length into Register B as a loop counter.
52FF
GOSUB to 5365H to read the 2-byte load address (low byte first, then high byte) into Register Pair HL.
5302
ADD A,L 85
Start the checksum calculation: A = A (high byte of address) + L (low byte of address).
5303
LD C,A 4F
Save the running checksum into Register C.

Now read B data bytes and store them starting at the address in HL.

5304
[DATA LOOP] GOSUB to 53A6H to read one data byte from cassette into Register A.
5307
LD (HL),A 77
Store the data byte into memory at the address pointed to by HL.
5308
INC HL 23
INCrement HL to point to the next memory location.
5309
ADD A,C 81
Add the data byte to the running checksum: A = data byte + previous checksum.
530A
LD C,A 4F
Save the updated checksum back into Register C.
530B
DECrement B and Jump if Not Zero. Loop until all B data bytes have been read and stored.

All data bytes loaded. Now verify the checksum.

530D
GOSUB to 53A6H to read the checksum byte from cassette into Register A.
5310
CP C B9
Compare the read checksum (A) with the calculated checksum (C). If they match, the Z FLAG is set.
5311
If the Z FLAG (Zero) has been set (checksums match - block OK), JUMP BACK to 52E8H to read the next block.

Checksum mismatch! Display an error indicator and try to resync.

5313
LD A,43H 3E43
Load Register A with 43H (ASCII: C) as a checksum error indicator character.
5315
LD (3C3EH),A 323E3C
Store 'C' at video RAM location 3C3EH (near bottom-right) to visually indicate a Checksum error occurred.
5318
JUMP to 52F0H to try to resync and find the next valid block (error recovery attempt).

531AH - Load Complete - Execute Program

EOF marker (78H) was found - loading is complete. Read the execution address and jump to it to run the loaded program.

531A
GOSUB to 5365H to read the 2-byte execution address from the cassette file trailer into Register Pair HL.
531D
GOSUB to 53D0H to turn the cassette motor OFF (clear bit 2 of port FFH).
5320
EI FB
Enable Interrupts. Safe to do now since cassette operations are complete.
5321
JP 5200HC30052
JUMP to 5200H to return to the command prompt. NOTE: The execution address in HL is NOT used here - the program returns to the monitor instead of auto-executing. This may be intentional for debugging or the JP (HL) was replaced.
5324
JP 5200HC30052
Duplicate jump to main loop - possibly a patch or alignment.

5327H - Write Data Block to Cassette

This subroutine writes one data block to the cassette output buffer. It writes: block type (01H), block length (B+2), load address (DE), and B bytes of data from memory at (DE). The buffer is automatically flushed to tape when full.

5327
LD A,01H 3E01
Load Register A with 01H - this is the data block type marker for the cassette format.
5329
GOSUB to 5342H to write the block type byte (01H) to the cassette output buffer.
532C
LD A,02H 3E02
Load Register A with 02H as the base for block length calculation.
532E
ADD A,B 80
Add B (data byte count) to A. Result = B + 2, which is the total block length including the 2-byte address.
532F
GOSUB to 5342H to write the block length to the cassette output buffer.
5332
LD A,E 7B
Load Register A with the LOW byte of the load address (from Register E).
5333
GOSUB to 5342H to write the load address LOW byte to the buffer.
5336
LD A,D 7A
Load Register A with the HIGH byte of the load address (from Register D).
5337
GOSUB to 5342H to write the load address HIGH byte to the buffer.

Header written. Now write B bytes of actual data from memory starting at DE.

533A
LD A,(DE) 1A
[DATA LOOP] Fetch the data byte from memory at address DE.
533B
GOSUB to 5342H to write the data byte to the cassette output buffer.
533E
INC DE 13
INCrement DE to point to the next source memory location.
533F
DECrement B and Jump if Not Zero. Loop until all data bytes have been written.
5341
RET C9
RETurn to caller. Block has been written to the output buffer.

5342H - Write Byte to Cassette Output Buffer

This subroutine writes one byte to the cassette output buffer at 53F4H. Register C is used as the buffer index. When C wraps from FFH to 00H (buffer full), the buffer is automatically flushed to tape by calling 4439H.

5342
LD IX,53F4H DD21F453
Load Index Register IX with 53F4H, the base address of the cassette output buffer.
5346
PUSH BC C5
Save BC onto the stack. We need B preserved and will use BC for the index calculation.
5347
LD B,00H 0600
Clear Register B so BC = 00:C = just the buffer index in C.
5349
ADD IX,BC DD09
Add BC to IX. IX now points to 53F4H + C, the next available byte in the buffer.
534B
POP BC C1
Restore BC from the stack (original B value is back).
534C
LD (IX+00H),A DD7700
Store the byte in Register A into the buffer at position IX+0.
534F
INC C 0C
INCrement the buffer index C. If C was FFH, it wraps to 00H and sets the Z FLAG.
5350
RET NZ C0
If the NZ FLAG (Not Zero) is set (buffer not full), RETurn immediately.

Buffer is full (C wrapped to 00H). Flush the 256-byte buffer to cassette tape.

5351
PUSH DE D5
Save DE onto the stack (it contains the source data pointer).
5352
LD DE,(5269H) ED5B6952
Load DE with the filename pointer (stored at 522FH during command parsing) for the write routine.
5356
GOSUB to 4439H to write the full 256-byte buffer to cassette tape.
5359
If the NZ FLAG (Not Zero) has been set (write error), JUMP to 52BCH to handle the error. NOTE: This does not restore DE - error handling restarts the whole process.
535C
POP DE D1
Restore DE from the stack (source data pointer).
535D
RET C9
RETurn to caller. Buffer has been flushed and is ready for more data.

535EH - Skip Whitespace

This utility subroutine advances HL past any space characters (20H) in the input buffer, leaving HL pointing to the first non-space character.

535E
INC HL 23
[SKIP LOOP] INCrement HL to point to the next character position.
535F
LD A,(HL) 7E
Fetch the character at the current position into Register A.
5360
CP 20H FE20
Compare Register A against 20H (ASCII: SPACE).
5362
If the Z FLAG (Zero) has been set (found a space), LOOP BACK to 535EH to skip it and check the next character.
5364
RET C9
RETurn to caller. HL now points to the first non-space character, which is also in Register A.

5365H - Read 16-bit Address from Cassette

This subroutine reads two bytes from cassette and assembles them into a 16-bit address in HL. The bytes are read low-byte first, then high-byte (little-endian order).

5365
GOSUB to 53A6H to read the LOW byte of the address from cassette into Register A.
5368
LD L,A 6F
Store the low byte into Register L.
5369
GOSUB to 53A6H to read the HIGH byte of the address from cassette into Register A.
536C
LD H,A 67
Store the high byte into Register H. HL now contains the complete 16-bit address.
536D
RET C9
RETurn to caller with the address in HL and the high byte still in A.

536EH - Print String

This subroutine prints B characters to the screen starting from the address in HL, using the ROM character output routine at 0033H.

536E
LD A,(HL) 7E
[PRINT LOOP] Fetch the character at address HL into Register A.
536F
GOSUB to ROM routine at 0033H to display the character in Register A at the current cursor position.
5372
INC HL 23
INCrement HL to point to the next character.
5373
DECrement B and Jump if Not Zero. Loop until all B characters have been printed.
5375
RET C9
RETurn to caller.

5376H - Parse Two Hex Digits to Binary

This subroutine parses two ASCII hex digit characters from (HL) and (HL+1) and converts them to a single binary byte in Register A. For example, "4A" becomes 4AH (74 decimal). If an invalid hex digit is found, it aborts to the syntax error handler.

5376
LD A,(HL) 7E
Fetch the FIRST hex digit character from address HL.
5377
GOSUB to 538AH to convert the ASCII hex digit to its 4-bit binary value (0-15).
537A
LD B,A 47
Save the first digit's value into Register B (this will become the high nibble).
537B
INC HL 23
INCrement HL to point to the second hex digit.
537C
LD A,(HL) 7E
Fetch the SECOND hex digit character from address HL.
537D
GOSUB to 538AH to convert the second ASCII hex digit to its 4-bit binary value.

Now combine the two nibbles: shift the first digit left 4 bits, then OR with the second digit.

5380
SLA B CB20
Shift Left Arithmetic: shift B left one bit position (multiply by 2).
5382
SLA B CB20
Shift Left Arithmetic: shift B left again (now multiplied by 4).
5384
SLA B CB20
Shift Left Arithmetic: shift B left again (now multiplied by 8).
5386
SLA B CB20
Shift Left Arithmetic: shift B left once more. B is now the original value × 16 (shifted into high nibble).
5388
OR B B0
OR Register A (low nibble) with Register B (high nibble). A now contains the complete 8-bit value.
5389
RET C9
RETurn to caller with the binary value in Register A.

538AH - Convert Single Hex Digit ASCII to Binary

This subroutine converts a single ASCII hex digit character ('0'-'9' or 'A'-'F') in Register A to its binary value (0-15). If the character is not a valid hex digit, it manipulates the stack to abort to the syntax error handler at 52C4H.

538A
CP 41H FE41
Compare Register A against 41H (ASCII: A). Check if it's a letter A-F.
538C
If the C FLAG (Carry) has been set (A < 'A'), JUMP to 5395H to check if it's a digit 0-9 instead.
538E
CP 47H FE47
Compare Register A against 47H (ASCII: G). Check upper bound of A-F range.
5390
If the NC FLAG (No Carry) has been set (A >= 'G'), JUMP to 53A0H - invalid hex digit, abort to error handler.
5392
SUB 37H D637
SUBtract 37H from A. Converts 'A'(41H) to 10, 'B'(42H) to 11, ... 'F'(46H) to 15.
5394
RET C9
RETurn with the binary value 10-15 in Register A.

Character was less than 'A' - check if it's a decimal digit 0-9.

5395
CP 30H FE30
Compare Register A against 30H (ASCII: 0).
5397
If the C FLAG (Carry) has been set (A < '0'), JUMP to 53A0H - invalid hex digit, abort to error handler.
5399
CP 3AH FE3A
Compare Register A against 3AH (one past ASCII: 9).
539B
If the NC FLAG (No Carry) has been set (A >= ':'), JUMP to 53A0H - invalid hex digit, abort to error handler.
539D
SUB 30H D630
SUBtract 30H from A. Converts '0'(30H) to 0, '1'(31H) to 1, ... '9'(39H) to 9.
539F
RET C9
RETurn with the binary value 0-9 in Register A.

53A0H - Abort to Syntax Error (Stack Manipulation)

This is the error abort routine called when an invalid hex digit is encountered. It uses a clever stack manipulation technique: it pops the immediate return address (discarding it), then replaces the NEXT return address on the stack with the error handler address. When the calling routine eventually returns, it goes to the error handler instead.

53A0
POP BC C1
POP the immediate return address off the stack into BC (discarding it - we won't return to 5376H or wherever called us).
53A1
LD HL,52C4H 21C452
Load HL with 52C4H, which is the address of the syntax error handler ("SN ERROR").
53A4
EX (SP),HL E3
EXchange HL with the value on top of stack. This replaces the return address (to the F command parser) with 52C4H. The original return address goes into HL (discarded).
53A5
RET C9
RETurn - but we return to 52C4H (syntax error handler) instead of the original caller!

53A6H - Read One Byte from Cassette

This subroutine reads 8 bits from the cassette tape and assembles them into a byte in Register A. It calls 53B0H eight times, each call reading one bit and shifting it into position.

53A6
PUSH BC C5
Save BC onto the stack (B will be used as a bit counter).
53A7
LD B,08H 0608
Load Register B with 08H - we need to read 8 bits to make one byte.
53A9
GOSUB to 53B0H to read one bit from cassette and shift it into Register A.
53AC
DECrement B and Jump if Not Zero. Loop until all 8 bits have been read.
53AE
POP BC C1
Restore BC from the stack.
53AF
RET C9
RETurn with the complete byte in Register A.

53B0H - Read One Bit from Cassette

This is the low-level bit-reading routine. It waits for a rising edge on the cassette input (bit 7 of port FFH), then uses precise timing delays to sample the bit value. The alternate register set (EXX, EX AF,AF') is used to preserve the caller's registers during the timing-critical operations.

53B0
EXX D9
EXchange BC, DE, HL with their alternate registers BC', DE', HL'. This preserves the main registers during bit reading.
53B1
EX AF,AF' 08
EXchange AF with alternate AF'. The accumulated byte is preserved in AF' while we work.

Wait for a rising edge on the cassette input signal (transition from 0 to 1).

53B2
IN A,(FFH) DBFF
Read the cassette port FFH looking for an edge. Bit 7 is the cassette data input.
53B4
RLA 17
Rotate Left through Carry. Bit 7 (cassette input) moves into the Carry flag.
53B5
If the NC FLAG (No Carry) is set (cassette input was 0), LOOP BACK to 53B2H to keep waiting for a 1.

Rising edge detected! Now use precise timing delays to sample the bit value.

53B7
LD B,41H 0641
Load B with 41H (65 decimal) for the first timing delay.
53B9
We need a little delay. DECrement B and Jump if Not Zero. This is a tight timing loop - 65 iterations × ~13 T-states ≈ 845 T-states delay.
53BB
GOSUB to 53DAH - this turns OFF the cassette relay briefly (timing adjustment for hardware).
53BE
LD B,76H 0676
Load B with 76H (118 decimal) for the second timing delay.
53C0
[DELAY LOOP 2] DECrement B and Jump if Not Zero. 118 iterations for longer delay.
53C2
IN A,(FFH) DBFF
Read the cassette port again - NOW we sample the actual bit value at the precise timing point.
53C4
LD B,A 47
Save the sampled port value into B.
53C5
EX AF,AF' 08
EXchange AF with AF'. Now A contains the byte being accumulated (restored from alternate).
53C6
RL B CB10
Rotate Left through Carry on B. This moves bit 7 (the sampled cassette data) into the Carry flag.
53C8
RLA 17
Rotate Left through Carry on A. The Carry (cassette bit) shifts into bit 0 of A, and the byte shifts left.
53C9
PUSH AF F5
Save AF (with accumulated byte) onto the stack temporarily.
53CA
GOSUB to 53DAH to reset the cassette port state (timing/hardware adjustment).
53CD
POP AF F1
Restore AF from the stack.
53CE
EXX D9
EXchange back to main register set BC, DE, HL.
53CF
RET C9
RETurn. The new bit has been shifted into Register A.

53D0H - Turn Cassette Motor OFF

This subroutine turns the cassette motor OFF by clearing bit 2 of port FFH. It uses the common port control routine at 53DDH with HL=FB00H to clear the motor bit.

53D0
LD HL,FB00H 2100FB
Load HL with FB00H. H=FBH is the AND mask (11111011 - clears bit 2), L=00H is the OR mask.
53D3
JUMP to 53DDH to apply the mask and update port FFH.

53D5H - Turn Cassette Motor ON

This subroutine turns the cassette motor ON by setting bit 2 of port FFH. It uses the common port control routine at 53DDH with HL=FF04H to set the motor bit.

53D5
LD HL,FF04H 2104FF
Load HL with FF04H. H=FFH is the AND mask (all bits preserved), L=04H is the OR mask (sets bit 2 - motor on).
53D8
JUMP to 53DDH to apply the mask and update port FFH.

53DAH - Reset Cassette Port (Timing Adjustment)

This subroutine is called during bit reading for hardware timing adjustment. It preserves all bits (AND FFH) and ORs with 00H (no change), but the act of writing to the port may reset edge detection hardware.

53DA
LD HL,FF00H 2100FF
Load HL with FF00H. H=FFH is the AND mask (preserve all), L=00H is the OR mask (add nothing).
53DD
JR 53DDH — (fall through)
(Falls through to 53DDH)

53DDH - Cassette Port Control (Common Routine)

This is the common cassette port control routine. It reads the current port state from the shadow register at 403DH, applies an AND mask (H) and OR mask (L), writes to port FFH, and updates the shadow register. Returns with the Z flag set based on bit 2 (motor state).

53DD
LD A,(403DH) 3A3D40
Fetch the current cassette port state from the shadow register at 403DH. The shadow register tracks what was last written to port FFH.
53E0
AND H A4
AND Register A with H (the AND mask). This clears any bits that are 0 in H.
53E1
OR L B5
OR Register A with L (the OR mask). This sets any bits that are 1 in L.
53E2
OUT (FFH),A D3FF
Output the modified value to cassette port FFH. This actually controls the hardware.
53E4
LD (403DH),A 323D40
Store the new value back to the shadow register at 403DH for future reference.
53E7
BIT 2,A CB57
Test bit 2 of Register A (the motor control bit). Sets Z flag if bit 2 is 0 (motor off), clears Z if bit 2 is 1 (motor on).
53E9
RET C9
RETurn to caller. Z flag indicates motor state.

53EAH - Error Message Data

This is the "SN ERROR" message displayed when an unrecognized command is entered or invalid hex digits are encountered.

53EA
DEFM 'SN ERROR' 53 4E 20 45 52 52 4F 52
ASCII string "SN ERROR" (Syntax Error) - 8 bytes.