Files
z80_monitor/apps/casette/tape_loader.asm
T
2026-10-03 17:30:43 +02:00

1055 lines
18 KiB
NASM

.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