Files
z80_monitor/apps/casette/tape_loader.asm
T

1614 lines
26 KiB
NASM

.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