.include "bios.inc" .area TAPELOAD (ABS) .org TAPE_LOADER_ROM_ADDRESS ; ===================================================================== ; Cassette Manchester decoder diagnostic ; ; CPU: Z80 @ 1 MHz ; Comparator port: 0x18 ; ; This stage: ; ; 1. Calibrates HIGH carrier from leader ; 2. Detects LOW carrier at start of 0x55 preamble ; 3. Learns FULL-bit HIGH/LOW run lengths from 0x55 ; 4. Derives HALF/FULL run thresholds ; 5. Manchester-decodes bits ; 6. Searches bit-by-bit for sync word D3 91 ; 7. Captures 30 following bytes ; 8. Prints the decoded bytes ; ; Nothing is written to 0x8000 yet. ; ; Interrupts remain disabled during tape reception. ; ===================================================================== COMP_PORT = 0x18 STACK_TOP = 0xBFFF WORK_BASE = 0xB000 OLD_SP = WORK_BASE + 0x00 ; word OLD_IFF = WORK_BASE + 0x02 ; byte LEADER_REF = WORK_BASE + 0x03 ; byte LEADER_AVG = WORK_BASE + 0x04 ; byte LEADER_MAX = WORK_BASE + 0x05 ; byte TONE_THRESHOLD = WORK_BASE + 0x06 ; byte LOW_TRIGGER = WORK_BASE + 0x07 ; byte CURRENT_TONE = WORK_BASE + 0x08 ; byte OLD_TONE = WORK_BASE + 0x09 ; byte TEMP_TONE = WORK_BASE + 0x0A ; byte RUN_COUNT = WORK_BASE + 0x0B ; byte HIGH_RUN_SUM = WORK_BASE + 0x0C ; word LOW_RUN_SUM = WORK_BASE + 0x0E ; word HIGH_RUN_AVG = WORK_BASE + 0x10 ; byte LOW_RUN_AVG = WORK_BASE + 0x11 ; byte HIGH_RUN_THRESH = WORK_BASE + 0x12 ; byte LOW_RUN_THRESH = WORK_BASE + 0x13 ; byte TRAIN_REMAIN = WORK_BASE + 0x14 ; byte RETURN_CODE = WORK_BASE + 0x15 ; byte ; Buffer contains: ; ; D3 91 + 30 following decoded bytes CAPTURE_BUF = 0xB100 CAPTURE_COUNT = 32 ; ===================================================================== ; Entry ; ===================================================================== tape_loader: ld (OLD_SP),sp ; --------------------------------------------------------------------- ; Save interrupt state. ; --------------------------------------------------------------------- ld a,i jp po,iff_was_off ld a,#1 jr save_iff iff_was_off: xor a save_iff: ld (OLD_IFF),a di ld sp,#STACK_TOP ; --------------------------------------------------------------------- ; Initial message. ; --------------------------------------------------------------------- ld hl,#msg_start call print_with_irqs ; ===================================================================== ; STEP 1 ; ; Find stable leader. ; ; First period is the reference. ; Require another 31 periods within +/-2 counts. ; ===================================================================== find_leader: call measure_halfperiod ld (LEADER_REF),a ld (LEADER_MAX),a ; HL = running sum ld l,a ld h,#0 ; One sample already obtained. ld b,#31 leader_stability_loop: call measure_halfperiod ; E = measurement ld e,a ; --------------------------------------------------------------------- ; abs(measurement - reference) ; --------------------------------------------------------------------- ld a,(LEADER_REF) cp e jr nc,leader_ref_ge_sample ; measurement > reference ld a,e ld c,a ld a,(LEADER_REF) ld d,a ld a,c sub d jr leader_diff_ready leader_ref_ge_sample: sub e leader_diff_ready: ; Difference 0, 1 or 2 accepted. cp #3 jp nc,find_leader ; --------------------------------------------------------------------- ; Update maximum observed leader period. ; --------------------------------------------------------------------- ld a,(LEADER_MAX) cp e jr nc,leader_max_ok ld a,e ld (LEADER_MAX),a leader_max_ok: ; Add E to HL. ld d,#0 add hl,de djnz leader_stability_loop ; ===================================================================== ; Rounded leader average. ; ; 32 samples. ; ; Add 16 before dividing by 32: ; ; average = (sum + 16) / 32 ; ===================================================================== ld de,#16 add hl,de srl h rr l srl h rr l srl h rr l srl h rr l srl h rr l ld a,l or a jp z,find_leader ld (LEADER_AVG),a ; ===================================================================== ; Initial carrier discrimination. ; ; From our real measurements: ; ; HIGH carrier = 03 / 04 ; LOW carrier = 05 / 06 ; ; Therefore: ; ; threshold = rounded leader average + 1 ; low trigger = threshold + 1 ; ; Expected: ; ; LEADER AVG = 03 ; CARRIER THRESHOLD = 04 ; LOW TRIGGER = 05 ; ; Classification: ; ; <= threshold : HIGH ; > threshold : LOW ; ===================================================================== ld a,(LEADER_AVG) inc a jr nz,tone_threshold_ok ld a,#0xFE tone_threshold_ok: ld (TONE_THRESHOLD),a inc a jr nz,low_trigger_ok ld a,#0xFF low_trigger_ok: ld (LOW_TRIGGER),a ; ===================================================================== ; STEP 2 ; ; Wait for THREE consecutive definitely-LOW measurements. ; ; This prevents a single slightly-long leader measurement from ; triggering the preamble detector. ; ===================================================================== wait_for_low_restart: ld b,#3 wait_for_low: call measure_halfperiod ld e,a ld a,(LOW_TRIGGER) cp e ; E >= LOW_TRIGGER ? jr z,low_candidate jr c,low_candidate jp wait_for_low_restart low_candidate: djnz wait_for_low ; ===================================================================== ; We are now somewhere inside the first LOW run of the preamble. ; ; Discard the rest of that partial run. ; ; scan_run returns after detecting the transition to HIGH. ; ===================================================================== xor a ; LOW call scan_run ; Must have changed to HIGH. or a jp z,training_error ld (CURRENT_TONE),a ; ===================================================================== ; STEP 3 ; ; Train using 16 complete carrier runs from the 0x55 preamble. ; ; 0x55 produces alternating FULL runs: ; ; HIGH FULL ; LOW FULL ; HIGH FULL ; LOW FULL ; ... ; ; 16 runs gives: ; ; 8 HIGH ; 8 LOW ; ===================================================================== ld hl,#0x0000 ld (HIGH_RUN_SUM),hl ld (LOW_RUN_SUM),hl ld a,#16 ld (TRAIN_REMAIN),a train_run_loop: ld a,(CURRENT_TONE) ld (OLD_TONE),a call scan_run ; A = new tone. ld (TEMP_TONE),a ; Reject empty run. ld a,(RUN_COUNT) or a jp z,training_error ; DE = run count. ld e,a ld d,#0 ld a,(OLD_TONE) or a jr z,train_low_run ; --------------------------------------------------------------------- ; HIGH full run. ; --------------------------------------------------------------------- ld hl,(HIGH_RUN_SUM) add hl,de ld (HIGH_RUN_SUM),hl jr train_run_added ; --------------------------------------------------------------------- ; LOW full run. ; --------------------------------------------------------------------- train_low_run: ld hl,(LOW_RUN_SUM) add hl,de ld (LOW_RUN_SUM),hl train_run_added: ld a,(TEMP_TONE) ld (CURRENT_TONE),a ld a,(TRAIN_REMAIN) dec a ld (TRAIN_REMAIN),a jp nz,train_run_loop ; ===================================================================== ; Average 8 HIGH and 8 LOW runs. ; ===================================================================== ld hl,(HIGH_RUN_SUM) call divide_hl_by_8 ld a,l or a jp z,training_error ld (HIGH_RUN_AVG),a ld hl,(LOW_RUN_SUM) call divide_hl_by_8 ld a,l or a jp z,training_error ld (LOW_RUN_AVG),a ; ===================================================================== ; Derive HALF/FULL discriminator. ; ; threshold = ceil(3/4 * full_run) ; ; Expected from our current measurements: ; ; HIGH FULL ~= 3 ; HIGH threshold = 3 ; ; LOW FULL ~= 2 ; LOW threshold = 2 ; ; Thus: ; ; HIGH 1/2 -> HALF, 3+ -> FULL ; LOW 1 -> HALF, 2+ -> FULL ; ===================================================================== ld a,(HIGH_RUN_AVG) call make_run_threshold ld (HIGH_RUN_THRESH),a ld a,(LOW_RUN_AVG) call make_run_threshold ld (LOW_RUN_THRESH),a ; ===================================================================== ; We are currently at a Manchester midpoint in the remaining 0x55 ; preamble. ; ; CURRENT_TONE is the carrier immediately following that midpoint. ; ; Search the decoded BIT STREAM for: ; ; D3 91 ; ; Searching bit-by-bit means we do not need to know byte alignment. ; ; HL = 16-bit shift register ; DE = timeout counter ; ===================================================================== ld hl,#0x0000 ld de,#8192 sync_search_loop: call get_bit ; C = decoded bit ld c,a ; Shift previous bits left. add hl,hl ; Append new bit. ld a,l or c ld l,a ; D3 91 ? ld a,h cp #0xD3 jr nz,sync_not_found ld a,l cp #0x91 jp z,sync_found sync_not_found: dec de ; DEC DE doesn't affect flags. ld a,d or e jp nz,sync_search_loop jp sync_error ; ===================================================================== ; Sync found. ; ; We are exactly byte-aligned immediately after the 0x91. ; ; Store D3 91 and then decode another 30 bytes. ; ===================================================================== sync_found: ld hl,#CAPTURE_BUF ld a,#0xD3 ld (hl),a inc hl ld a,#0x91 ld (hl),a inc hl ld b,#30 capture_decoded_bytes: call get_byte ld (hl),a inc hl djnz capture_decoded_bytes ; ===================================================================== ; Reception complete. ; ; We can now enable interrupts and print. ; ===================================================================== ei ld hl,#msg_sync_ok call print_string call print_training_values ld hl,#msg_bytes call print_string call print_crlf ld hl,#CAPTURE_BUF ld b,#CAPTURE_COUNT ld c,#0 print_byte_loop: ld a,(hl) call print_hex8 ld a,#' ' call BIOS_PUTC inc hl inc c ; 16 bytes per line. ld a,c and #0x0F jr nz,print_byte_no_crlf call print_crlf print_byte_no_crlf: djnz print_byte_loop call print_crlf xor a ld (RETURN_CODE),a jp diagnostic_exit ; ===================================================================== ; Error paths ; ===================================================================== training_error: ld a,#1 ld (RETURN_CODE),a ei ld hl,#msg_training_error call print_string call print_training_values jp diagnostic_exit sync_error: ld a,#2 ld (RETURN_CODE),a ei ld hl,#msg_sync_error call print_string call print_training_values jp diagnostic_exit timing_error: ld a,#3 ld (RETURN_CODE),a ei ld hl,#msg_timing_error call print_string call print_training_values ; ===================================================================== ; Exit ; ===================================================================== diagnostic_exit: di ld sp,(OLD_SP) ld a,(RETURN_CODE) ld c,a ; Restore caller's interrupt state. ld a,(OLD_IFF) or a ld a,c jr z,diagnostic_return_di ei ret diagnostic_return_di: ret ; ===================================================================== ; measure_halfperiod ; ; Wait for one comparator edge, then measure the COMPLETE interval ; between that edge and the following edge. ; ; Returns: ; ; A = count 1..255 ; ; Saturates at FF. ; ; NOTE: ; ; Each invocation first synchronizes to an edge and then measures the ; next interval, so sequential calls effectively skip one raw ; half-period between measured intervals. ; ; That's okay because training and decoding use the same mechanism. ; ===================================================================== measure_halfperiod: in a,(#COMP_PORT) ld d,a ; --------------------------------------------------------------------- ; Synchronize to first edge. ; --------------------------------------------------------------------- mhp_wait_first_edge: in a,(#COMP_PORT) cp d jr z,mhp_wait_first_edge ; Start measurement at this edge. ld d,a ld c,#0 ; --------------------------------------------------------------------- ; Measure until next edge. ; --------------------------------------------------------------------- mhp_measure_loop: inc c in a,(#COMP_PORT) cp d jr nz,mhp_done ld a,c cp #0xFF jr nz,mhp_measure_loop ; --------------------------------------------------------------------- ; Saturated. ; --------------------------------------------------------------------- mhp_saturated: in a,(#COMP_PORT) cp d jr z,mhp_saturated ld a,#0xFF ret mhp_done: ld a,c ret ; ===================================================================== ; classify_sample ; ; Input: ; ; A = measured carrier period ; ; Output: ; ; A = 1 HIGH-frequency carrier ; A = 0 LOW-frequency carrier ; ; sample <= TONE_THRESHOLD : HIGH ; sample > TONE_THRESHOLD : LOW ; ; Preserves BC, DE, HL. ; ===================================================================== classify_sample: push bc ld c,a ld a,(TONE_THRESHOLD) cp c ; threshold < sample -> LOW jr c,classify_low ld a,#1 pop bc ret classify_low: xor a pop bc ret ; ===================================================================== ; scan_run ; ; Input: ; ; A = carrier currently present ; ; Output: ; ; RUN_COUNT = measured run length ; A = newly detected carrier ; ; ; IMPORTANT: ; ; The previous scan_run consumed the first measurement belonging to ; this run while detecting the previous transition. ; ; Therefore the run starts at count 1, not 0. ; ; This was the important fix which changed training from: ; ; H=2 L=1 ; ; to: ; ; H=3 L=2 ; ===================================================================== scan_run: ld e,a ; expected tone ; First sample of this run was already consumed by the ; previous transition detection. ld b,#1 scan_run_loop: call measure_halfperiod call classify_sample cp e jr nz,scan_run_changed inc b ; Protect against wraparound. jp z,timing_error jr scan_run_loop scan_run_changed: ; A contains new tone. ld (TEMP_TONE),a ld a,b ld (RUN_COUNT),a ld a,(TEMP_TONE) ret ; ===================================================================== ; divide_hl_by_8 ; ===================================================================== divide_hl_by_8: srl h rr l srl h rr l srl h rr l ret ; ===================================================================== ; make_run_threshold ; ; Input: ; ; A = learned FULL run length ; ; Output: ; ; A = ceil(3/4 * FULL) ; ; Formula: ; ; (3*FULL + 3) / 4 ; ; Examples: ; ; full = 2 -> threshold = 2 ; full = 3 -> threshold = 3 ; full = 4 -> threshold = 3 ; full = 5 -> threshold = 4 ; full = 6 -> threshold = 5 ; ===================================================================== make_run_threshold: ld b,a add a,a ; 2 * full add a,b ; 3 * full add a,#3 srl a srl a or a ret nz ld a,#1 ret ; ===================================================================== ; run_is_full ; ; Input: ; ; A = carrier of run just measured ; ; Uses RUN_COUNT. ; ; Output: ; ; A = 0 HALF ; A = 1 FULL ; ===================================================================== run_is_full: or a jr z,run_check_low ld a,(HIGH_RUN_THRESH) jr run_do_compare run_check_low: ld a,(LOW_RUN_THRESH) run_do_compare: ld b,a ld a,(RUN_COUNT) cp b ; run_count < threshold -> HALF jr c,run_is_half ld a,#1 ret run_is_half: xor a ret ; ===================================================================== ; get_bit ; ; Entry: ; ; positioned immediately after a known Manchester midpoint ; ; CURRENT_TONE = carrier following that midpoint ; ; ; If the next carrier run is FULL: ; ; its ending transition is the next midpoint. ; ; ; If the next carrier run is HALF: ; ; its ending transition is a bit boundary. ; ; Consume another HALF run to reach the midpoint. ; ; ; At a midpoint: ; ; LOW -> HIGH = bit 0 ; HIGH -> LOW = bit 1 ; ; Since TEMP_TONE is the tone AFTER the midpoint: ; ; new HIGH = bit 0 ; new LOW = bit 1 ; ; Returns: ; ; A = 0 or 1 ; ; Preserves BC, DE, HL. ; ===================================================================== get_bit: push bc push de push hl ld a,(CURRENT_TONE) ld (OLD_TONE),a ; --------------------------------------------------------------------- ; Measure first run after previous midpoint. ; --------------------------------------------------------------------- call scan_run ; A = new tone. ld (TEMP_TONE),a ; Was old run FULL or HALF? ld a,(OLD_TONE) call run_is_full or a jr nz,get_bit_at_midpoint ; --------------------------------------------------------------------- ; HALF: ; ; First transition was a bit boundary. ; ; Consume the second HALF run. ; --------------------------------------------------------------------- ld a,(TEMP_TONE) ld (CURRENT_TONE),a call scan_run ld (TEMP_TONE),a ; This second run should also be HALF. ld a,(CURRENT_TONE) call run_is_full or a jp nz,timing_error ; --------------------------------------------------------------------- ; We are now at a Manchester midpoint. ; --------------------------------------------------------------------- get_bit_at_midpoint: ld a,(TEMP_TONE) ld (CURRENT_TONE),a ; New HIGH means LOW -> HIGH = bit 0. or a jr z,get_bit_one xor a jr get_bit_done get_bit_one: ld a,#1 get_bit_done: pop hl pop de pop bc ret ; ===================================================================== ; get_byte ; ; Decode eight bits, MSB first. ; ===================================================================== get_byte: push bc push de ld b,#8 ld e,#0 get_byte_loop: call get_bit ld d,a sla e ld a,d or e ld e,a djnz get_byte_loop ld a,e pop de pop bc ret ; ===================================================================== ; Diagnostic printing ; ===================================================================== print_training_values: ld hl,#msg_leader call print_string ld a,(LEADER_AVG) call print_hex8 call print_crlf ld hl,#msg_leader_max call print_string ld a,(LEADER_MAX) call print_hex8 call print_crlf ld hl,#msg_low_trigger call print_string ld a,(LOW_TRIGGER) call print_hex8 call print_crlf ld hl,#msg_tone_threshold call print_string ld a,(TONE_THRESHOLD) call print_hex8 call print_crlf ld hl,#msg_high_run call print_string ld a,(HIGH_RUN_AVG) call print_hex8 call print_crlf ld hl,#msg_low_run call print_string ld a,(LOW_RUN_AVG) call print_hex8 call print_crlf ld hl,#msg_high_threshold call print_string ld a,(HIGH_RUN_THRESH) call print_hex8 call print_crlf ld hl,#msg_low_threshold call print_string ld a,(LOW_RUN_THRESH) call print_hex8 call print_crlf call print_crlf ret ; ===================================================================== ; Printing helpers ; ===================================================================== print_with_irqs: ei call print_string di ret print_string: ld a,(hl) or a ret z call BIOS_PUTC inc hl jr print_string print_crlf: ld a,#13 call BIOS_PUTC ld a,#10 jp BIOS_PUTC ; ===================================================================== ; print_hex8 ; ; Print A as two hexadecimal characters. ; ===================================================================== print_hex8: push af rrca rrca rrca rrca and #0x0F call print_hex_nibble pop af and #0x0F ; fall through print_hex_nibble: cp #10 jr c,print_hex_digit add a,#('A' - 10) jp BIOS_PUTC print_hex_digit: add a,#'0' jp BIOS_PUTC ; ===================================================================== ; Strings ; ===================================================================== msg_start: .ascii "TAPE DECODE TEST - PLAY TAPE" .db 13,10,0 msg_sync_ok: .ascii "SYNC D3 91 FOUND" .db 13,10,0 msg_training_error: .ascii "TAPE TRAINING ERROR" .db 13,10,0 msg_sync_error: .ascii "TAPE SYNC ERROR" .db 13,10,0 msg_timing_error: .ascii "TAPE MANCHESTER TIMING ERROR" .db 13,10,0 msg_leader: .ascii "LEADER AVG = " .db 0 msg_leader_max: .ascii "LEADER MAX = " .db 0 msg_low_trigger: .ascii "LOW TRIGGER = " .db 0 msg_tone_threshold: .ascii "CARRIER THRESHOLD = " .db 0 msg_high_run: .ascii "HIGH FULL RUN = " .db 0 msg_low_run: .ascii "LOW FULL RUN = " .db 0 msg_high_threshold: .ascii "HIGH RUN THRESH = " .db 0 msg_low_threshold: .ascii "LOW RUN THRESH = " .db 0 msg_bytes: .ascii "DECODED BYTES:" .db 0