.include "bios.inc" .area TAPELOAD (ABS) .org TAPE_LOADER_ROM_ADDRESS ; ===================================================================== ; Cassette training diagnostic ; ; CPU: Z80 @ 1 MHz ; Comparator: port 0x18 ; ; Comparator register: ; D0 = comparator output ; D7..D1 = 0 ; ; This program DOES NOT decode data yet. ; ; It: ; 1. Finds a stable leader ; 2. Measures leader timing ; 3. Waits for 3 consecutive slower-carrier periods ; 4. Captures 128 raw comparator half-periods ; 5. Prints the raw values ; 6. Classifies them as H/L ; 7. Prints carrier run lengths ; ; All sampling happens with interrupts disabled. ; Printing happens only before or after sampling. ; ===================================================================== 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 RETURN_CODE = WORK_BASE + 0x08 ; byte CAPTURE_BUF = 0xB100 CAPTURE_COUNT = 128 ; ===================================================================== ; Entry ; ===================================================================== tape_loader: ; ------------------------------------------------------------- ; Save caller stack. ; ------------------------------------------------------------- ld (OLD_SP),sp ; ------------------------------------------------------------- ; Save interrupt state. ; ; LD A,I copies IFF2 into P/V. ; ------------------------------------------------------------- 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 ; ------------------------------------------------------------- ; Use our own stack. ; ------------------------------------------------------------- di ld sp,#STACK_TOP ; ------------------------------------------------------------- ; Startup message. ; ; BIOS printing requires interrupts. ; print_with_irqs returns with interrupts disabled. ; ------------------------------------------------------------- ld hl,#msg_start call print_with_irqs ; ===================================================================== ; Find stable leader ; ; The first measurement becomes our reference. ; ; Require another 31 measurements within +/-2 counts of that reference. ; ; At 1 MHz we only get a few loop counts per carrier half-period, so ; allowing +/-2 is intentionally generous. ; ; We also keep track of the maximum observed leader count. ; ===================================================================== find_leader: call measure_halfperiod ld (LEADER_REF),a ld (LEADER_MAX),a ; HL = running sum. ld l,a ld h,#0 ; We already have one sample. ; Need 31 more for 32 total. ld b,#31 leader_stability_loop: call measure_halfperiod ; E = measurement ld e,a ; ------------------------------------------------------------- ; Calculate: ; ; abs(sample - reference) ; ; without destroying E. ; ------------------------------------------------------------- ld a,(LEADER_REF) cp e jr nc,leader_ref_ge_sample ; sample > 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: ; A still contains reference. sub e leader_diff_ready: ; Difference 0, 1, or 2 accepted. ; ; >= 3 means this isn't a stable leader. ; Start over. cp #3 jr nc,find_leader ; ------------------------------------------------------------- ; Update maximum leader period. ; ------------------------------------------------------------- ld a,(LEADER_MAX) cp e jr nc,leader_max_ok ld a,e ld (LEADER_MAX),a leader_max_ok: ; ------------------------------------------------------------- ; Add sample to 16-bit sum. ; ------------------------------------------------------------- ld d,#0 add hl,de djnz leader_stability_loop ; ===================================================================== ; Calculate leader average. ; ; 32 samples -> divide by 32. ; ===================================================================== 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 jr z,find_leader ld (LEADER_AVG),a ; ===================================================================== ; Initial carrier threshold. ; ; Everything up to LEADER_MAX is treated as the HIGH-frequency carrier. ; ; To decide that the LOW-frequency carrier has appeared, require: ; ; measurement >= LEADER_MAX + 1 ; ; AND require this to happen three times consecutively. ; ; Example from your previous capture: ; ; leader average ~= 02 ; leader max ~= 03 or 04 ; ; A single 04 can therefore no longer falsely trigger acquisition. ; ===================================================================== ld a,(LEADER_MAX) ld (TONE_THRESHOLD),a inc a jr nz,low_trigger_ok ; Protect against overflow, although this should never happen. ld a,#0xFF low_trigger_ok: ld (LOW_TRIGGER),a ; ===================================================================== ; Wait for the beginning of the slower carrier. ; ; We require THREE consecutive periods >= LOW_TRIGGER. ; ; This is the important change from the previous diagnostic. ; ===================================================================== wait_for_low_restart: ld b,#3 wait_for_low: call measure_halfperiod ld e,a ld a,(LOW_TRIGGER) ; We want: ; ; E >= LOW_TRIGGER ; ; CP does A - E. ; ; A < E -> carry ; A = E -> zero cp e jr z,low_candidate jr c,low_candidate ; Too short: still leader/noise. ; Restart the consecutive counter. jr wait_for_low_restart low_candidate: djnz wait_for_low ; ===================================================================== ; We have now observed three consecutive slow periods. ; ; Capture 128 FRESH measurements. ; ; We deliberately do not print anything here because the tape stream ; has no gaps. ; ===================================================================== ld hl,#CAPTURE_BUF ld b,#CAPTURE_COUNT capture_loop: call measure_halfperiod ld (hl),a inc hl djnz capture_loop ; ===================================================================== ; Capture complete. ; ; Timing no longer matters, so enable interrupts for BIOS output. ; ===================================================================== ei ; ===================================================================== ; Print calibration summary. ; ===================================================================== ld hl,#msg_leader_avg 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_threshold call print_string ld a,(TONE_THRESHOLD) call print_hex8 call print_crlf call print_crlf ; ===================================================================== ; Print raw half-period measurements. ; ; 16 per line. ; ===================================================================== ld hl,#msg_raw call print_string call print_crlf ld hl,#CAPTURE_BUF ld b,#CAPTURE_COUNT ld c,#0 print_raw_loop: ld a,(hl) call print_hex8 ld a,#' ' call BIOS_PUTC inc hl inc c ; New line every 16 samples. ld a,c and #0x0F jr nz,raw_no_newline call print_crlf raw_no_newline: djnz print_raw_loop call print_crlf ; ===================================================================== ; Print H/L classification. ; ; H: ; sample <= TONE_THRESHOLD ; ; L: ; sample > TONE_THRESHOLD ; ; 64 characters per line. ; ===================================================================== ld hl,#msg_class call print_string call print_crlf ld hl,#CAPTURE_BUF ld b,#CAPTURE_COUNT ld c,#0 print_class_loop: ld a,(hl) call classify_sample or a jr z,class_print_low ld a,#'H' jr class_print_char class_print_low: ld a,#'L' class_print_char: call BIOS_PUTC inc hl inc c ; New line every 64 classifications. ld a,c and #0x3F jr nz,class_no_newline call print_crlf class_no_newline: djnz print_class_loop call print_crlf ; ===================================================================== ; Print carrier run lengths. ; ; Example: ; ; L06 H0D L07 H0C ... ; ; This is the most interesting output for the 0x55 preamble. ; ; For nominal 2 kHz / 4 kHz at 600 baud we might expect something ; roughly around: ; ; LOW run: 6-7 comparator half-periods ; HIGH run: 12-14 comparator half-periods ; ; Exact values don't matter. ; Consistency is what matters. ; ===================================================================== ld hl,#msg_runs call print_string call print_crlf ld hl,#CAPTURE_BUF ; ------------------------------------------------------------- ; Establish first carrier. ; ------------------------------------------------------------- ld a,(hl) call classify_sample ; D = current carrier ; ; 0 = LOW ; 1 = HIGH ld d,a ; C = current run length ld c,#1 inc hl ; 127 measurements remain. ld b,#127 run_scan_loop: ld a,(hl) ; classify_sample preserves BC and DE. call classify_sample cp d jr nz,run_changed ; Same carrier. inc c jr run_scan_next run_changed: ; ------------------------------------------------------------- ; A = new carrier. ; ; Save it while printing the old run. ; ------------------------------------------------------------- ld e,a ld a,d call print_tone_letter ld a,c call print_hex8 ld a,#' ' call BIOS_PUTC ; Start new run. ld d,e ld c,#1 run_scan_next: inc hl djnz run_scan_loop ; ------------------------------------------------------------- ; Print final run. ; ------------------------------------------------------------- ld a,d call print_tone_letter ld a,c call print_hex8 call print_crlf call print_crlf ld hl,#msg_done call print_string xor a ld (RETURN_CODE),a ; ===================================================================== ; Return to monitor / BIOS caller. ; ===================================================================== training_exit: di ld sp,(OLD_SP) ld a,(RETURN_CODE) ld c,a ; ------------------------------------------------------------- ; Restore caller interrupt state. ; ------------------------------------------------------------- ld a,(OLD_IFF) or a ld a,c jr z,training_return_di ; EI becomes active after the following instruction. ; RET serves as that instruction. ei ret training_return_di: ret ; ===================================================================== ; measure_halfperiod ; ; 1. Wait for a comparator edge. ; 2. Starting exactly from that edge, count polling iterations until ; the next comparator edge. ; ; Returns: ; ; A = count, 1..255 ; ; If the interval is very long the counter saturates at FF instead of ; wrapping. ; ; ; At 1 MHz your observed leader currently gives roughly: ; ; 02 / 03 counts ; ; which is perfectly usable as long as the two carrier clusters remain ; distinct. ; ===================================================================== measure_halfperiod: in a,(#COMP_PORT) ld d,a ; --------------------------------------------------------------------- ; Wait for 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 to following edge. ; --------------------------------------------------------------------- mhp_measure_loop: inc c in a,(#COMP_PORT) cp d jr nz,mhp_done ; Saturate at FF. ld a,c cp #0xFF jr nz,mhp_measure_loop ; --------------------------------------------------------------------- ; Saturated: ; just wait for the edge without incrementing further. ; --------------------------------------------------------------------- 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 half-period ; ; Output: ; ; A = 1 HIGH-frequency / short-period carrier ; A = 0 LOW-frequency / long-period carrier ; ; Preserves: ; ; BC ; DE ; HL ; ===================================================================== classify_sample: push bc ld c,a ld a,(TONE_THRESHOLD) cp c ; threshold < sample -> LOW-frequency carrier jr c,class_sample_low ld a,#1 pop bc ret class_sample_low: xor a pop bc ret ; ===================================================================== ; print_tone_letter ; ; Input: ; ; A = 0 -> L ; A = 1 -> H ; ===================================================================== print_tone_letter: or a jr z,print_tone_low ld a,#'H' jp BIOS_PUTC print_tone_low: ld a,#'L' jp BIOS_PUTC ; ===================================================================== ; 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 ; ; A -> 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 ; ===================================================================== ; ROM strings ; ===================================================================== msg_start: .ascii "TAPE TRAINING - PLAY TAPE" .db 13,10,0 msg_leader_avg: .ascii "LEADER AVG = " .db 0 msg_leader_max: .ascii "LEADER MAX = " .db 0 msg_low_trigger: .ascii "LOW TRIGGER = " .db 0 msg_threshold: .ascii "H/L THRESHOLD = " .db 0 msg_raw: .ascii "RAW HALF-PERIOD COUNTS:" .db 0 msg_class: .ascii "CLASSIFICATION:" .db 0 msg_runs: .ascii "RUN LENGTHS:" .db 0 msg_done: .ascii "TRAINING CAPTURE DONE" .db 13,10,0