diff --git a/README.md b/README.md index 63536ad..2ea9f11 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,7 @@ Assembly applications include `include/bios.inc` and call these fixed entries: | `C065` | `BIOS_BANK` | `A=0`: split lower 48K/upper 16K; otherwise select upper 64K | | `C068` | `BIOS_ROM_DISABLE` | Disable ROM while retaining the selected RAM mapping | | `C06B` | `BIOS_ROM_ENABLE` | Enable ROM while retaining the selected RAM mapping | +| `C06E` | `BIOS_FLUSH` | Wait until the UART output queue is empty | The service wrappers preserve working registers except for documented return values. The monitor uses the corresponding C API declared in `bios.h`. diff --git a/apps/casette/encode.py b/apps/casette/encode.py index 57286ce..a0d8fa7 100644 --- a/apps/casette/encode.py +++ b/apps/casette/encode.py @@ -1,22 +1,4 @@ #!/usr/bin/env python3 -""" -z80_tape_encode.py - -Encode a binary file as a mono PCM WAV using robust two-tone Manchester FSK. - -Defaults: -- 48 kHz sample rate -- 600 bit/s -- 2 kHz / 4 kHz tones -- Manchester FSK: - 0 -> low tone for half a bit, then high tone - 1 -> high tone for half a bit, then low tone -- 1 second 4 kHz leader -- 64 bytes of 0x55 preamble -- 256-byte CRC-protected blocks -- CRC-16/CCITT-FALSE -""" - from __future__ import annotations import argparse @@ -25,29 +7,16 @@ import struct import wave from pathlib import Path -SYNC = b"\xD3\x91" -PREAMBLE_BYTE = 0x55 - - -def crc16_ccitt_false(data: bytes) -> int: - crc = 0xFFFF - for byte in data: - crc ^= byte << 8 - for _ in range(8): - crc = ((crc << 1) ^ 0x1021) & 0xFFFF if (crc & 0x8000) else (crc << 1) & 0xFFFF - return crc - - class FSKWriter: - def __init__(self, sample_rate: int, baud: int, f_low: float, f_high: float, amplitude: float): - if sample_rate % (2 * baud) != 0: - raise ValueError("sample_rate must be divisible by 2*baud") + def __init__(self, sample_rate: int, c_low: int, c_high: int, f_low: float, f_high: float, amplitude: float): self.sample_rate = sample_rate - self.baud = baud + self.c_low = c_low + self.c_high = c_high self.f_low = f_low self.f_high = f_high self.amplitude = amplitude - self.samples_per_half_bit = sample_rate // (2 * baud) + self.samples_per_low = (sample_rate * c_low) // f_low + self.samples_per_high = (sample_rate * c_high) // f_high self.phase = 0.0 self.samples = bytearray() @@ -57,7 +26,7 @@ class FSKWriter: phase = self.phase out = self.samples - for _ in range(sample_count): + for _ in range(int(sample_count)): sample = int(round(scale * math.sin(phase))) out.extend(struct.pack(" None: - self._tone(self.f_high, round(seconds * self.sample_rate)) + def leader(self) -> None: + self._tone(self.f_high, (200*self.sample_rate)//self.f_high) + self._tone(self.f_low, (200*self.sample_rate)//self.f_low) + for _ in range(8): + self.bit(1) + self.bit(0) + self.byte(0x0f) def bit(self, value: int) -> None: - # Manchester-FSK: - # 0 = low -> high - # 1 = high -> low if value: - first, second = self.f_high, self.f_low + self._tone(self.f_high, self.samples_per_high) else: - first, second = self.f_low, self.f_high - - self._tone(first, self.samples_per_half_bit) - self._tone(second, self.samples_per_half_bit) + self._tone(self.f_low, self.samples_per_low) def byte(self, value: int) -> None: # MSB first @@ -90,100 +58,46 @@ class FSKWriter: for value in data: self.byte(value) + def block(self, nr: int, data: bytes) -> None: + self.leader() + self.byte(nr) + l = len(data) + self.byte(l>>8) + self.byte(l&0xff) + self.bytes(data) + self.byte(self.chsum(data)) -def make_block(sequence: int, payload: bytes) -> bytes: - header = struct.pack(" int: + cs = 0 + for d in data: + cs += int(d) + return cs -def make_eof_block(sequence: int) -> bytes: - header = struct.pack(" None: + parser = argparse.ArgumentParser(description="Encode a binary file as two tone FSK WAV") + parser.add_argument("input", type=Path, help="Binary input file") + parser.add_argument("output", type=Path, help="output WAVl default .wav") + parser.add_argument("--sample-rate", type=int, default=48000) + parser.add_argument("--low", type=float, default=2000.0, help="low tone in Hz") + parser.add_argument("--high", type=float, default=4000.0, help="high tone in Hz") + parser.add_argument("--low-cycles", type=int, default=2, help="low tone cycle count for a 0") + parser.add_argument("--high-cycles", type=int, default=4, help="high tone cycle count for a 1") + parser.add_argument("--amplitude", type=float, default=0.75) + parser.add_argument("--block-size", type=int, default=64) + args = parser.parse_args() + output_path = args.output or args.input.with_name(args.input.stem + ".wav") -def encode( - input_path: Path, - output_path: Path, - *, - sample_rate: int = 48_000, - baud: int = 600, - f_low: float = 2_000.0, - f_high: float = 4_000.0, - leader_seconds: float = 1.0, - preamble_bytes: int = 64, - block_size: int = 256, - amplitude: float = 0.75, -) -> None: - data = input_path.read_bytes() + tones = FSKWriter(args.sample_rate, args.low_cycles, args.high_cycles, args.low, args.high, args.amplitude) + tones.block(0, args.input.name.encode()) - if not 0 < amplitude <= 1.0: - raise ValueError("amplitude must be > 0 and <= 1") - if not 1 <= block_size <= 0xFFFF: - raise ValueError("block_size must be in 1..65535") - - writer = FSKWriter(sample_rate, baud, f_low, f_high, amplitude) - - writer.leader(leader_seconds) - writer.bytes(bytes([PREAMBLE_BYTE]) * preamble_bytes) - - sequence = 0 - for start in range(0, len(data), block_size): - payload = data[start:start + block_size] - writer.bytes(make_block(sequence, payload)) - sequence = (sequence + 1) & 0xFFFF - - writer.bytes(make_eof_block(sequence)) with wave.open(str(output_path), "wb") as wav: wav.setnchannels(1) wav.setsampwidth(2) - wav.setframerate(sample_rate) - wav.writeframes(writer.samples) - - duration = len(writer.samples) / 2 / sample_rate - - print(f"Input: {input_path}") - print(f"Output: {output_path}") - print(f"Input bytes: {len(data)}") - print(f"Bit rate: {baud} bit/s") - print(f"Tones: {f_low:g} Hz / {f_high:g} Hz") - print(f"Sample rate: {sample_rate} Hz") - print(f"Half-bit: {writer.samples_per_half_bit} samples") - print(f"Block size: {block_size} bytes") - print(f"WAV duration: {duration:.2f} s") - - -def main() -> None: - parser = argparse.ArgumentParser(description="Encode a binary file as Manchester two-tone FSK WAV.") - parser.add_argument("input", type=Path, help="binary input file") - parser.add_argument("output", type=Path, nargs="?", help="output WAV; default: INPUT.wav") - parser.add_argument("--baud", type=int, default=150) - parser.add_argument("--low", type=float, default=1000.0, help="low tone in Hz") - parser.add_argument("--high", type=float, default=2000.0, help="high tone in Hz") - parser.add_argument("--sample-rate", type=int, default=48000) - parser.add_argument("--leader", type=float, default=4.0, help="leader seconds") - parser.add_argument("--preamble", type=int, default=64, help="number of 0x55 preamble bytes") - parser.add_argument("--block-size", type=int, default=64) - parser.add_argument("--amplitude", type=float, default=0.75) - - args = parser.parse_args() - output = args.output or args.input.with_name(args.input.name + ".wav") - - encode( - args.input, - output, - sample_rate=args.sample_rate, - baud=args.baud, - f_low=args.low, - f_high=args.high, - leader_seconds=args.leader, - preamble_bytes=args.preamble, - block_size=args.block_size, - amplitude=args.amplitude, - ) - + wav.setframerate(args.sample_rate) + wav.writeframes(tones.samples) if __name__ == "__main__": main() diff --git a/apps/casette/tape_loader.asm b/apps/casette/tape_loader.asm index 9117e61..f89ee11 100644 --- a/apps/casette/tape_loader.asm +++ b/apps/casette/tape_loader.asm @@ -3,1612 +3,302 @@ .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 +TAPE_PORT = 0x18 STACK_TOP = 0xBFFF - WORK_BASE = 0xB000 OLD_SP = WORK_BASE + 0x00 ; word OLD_IFF = WORK_BASE + 0x02 ; byte +SHORT_LEN = WORK_BASE + 0x03 ; byte +LONG_LEN = WORK_BASE + 0x04 ; byte +TRESHOLD = WORK_BASE + 0x05 ; byte +SHORT_COUNT = WORK_BASE + 0x06 ; byte +LONG_COUNT = WORK_BASE + 0x07 ; 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 - - -; ===================================================================== +; #################################################3 ; Entry -; ===================================================================== - +; #################################################3 tape_loader: - ld (OLD_SP),sp - - -; --------------------------------------------------------------------- -; Save interrupt state. -; --------------------------------------------------------------------- - + ; 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. -; --------------------------------------------------------------------- - + ; 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 - - -; ===================================================================== + ; Calibrate receiver + call calibrate + + ; Print parameters + ei + ld hl,#msg_short_len + call print_string + ld a,(SHORT_LEN) + call print_hex8 + call print_crlf + + ld hl,#msg_long_len + call print_string + ld a,(LONG_LEN) + call print_hex8 + call print_crlf + + ld hl,#msg_treshold + call print_string + ld a,(TRESHOLD) + call print_hex8 + call print_crlf + + ld hl,#msg_short_count + call print_string + ld a,(SHORT_COUNT) + call print_hex8 + call print_crlf + + ld hl,#msg_long_count + call print_string + ld a,(LONG_COUNT) + call print_hex8 + call print_crlf + + call BIOS_FLUSH + di + +; #################################################3 ; Exit -; ===================================================================== - -diagnostic_exit: - +; #################################################3 +exit: di - - ld sp,(OLD_SP) - - - ld a,(RETURN_CODE) + ; Return code 0 + ld a,#0 ld c,a - - ; Restore caller's interrupt state. - ld a,(OLD_IFF) - or a - - ld a,c - - jr z,diagnostic_return_di - - + jr z,return_di ei ret - - -diagnostic_return_di: - +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 -; ===================================================================== - +; #################################################3 +; Wait for next edge +; D holds current comparator state +; Returns B = loop count +; #################################################3 +wait_edge: + ld b,#0 +1$: + inc b + in a,(TAPE_PORT) + cp a,d + jr z,1$ + ld d,a + ret + +; #################################################3 +; Wait for a full cycle +; D holds the current comparator state +; Returns B = loop count +; #################################################3 +wait_cycle: + ld b,#0 +1$: + inc b + in a,(TAPE_PORT) + cp a,d + jp z,1$ + ld d,a +2$: + inc b + in a,(TAPE_PORT) + cp a,d + jp z,2$ + ld d,a + ret + +; #################################################3 +; Average 16 cycle intervals +; Returns A = average loop count +; preserves bc +; #################################################3 +average16: + push bc + ld c,#16 + ld e,#0 + +1$: + call wait_cycle + ; Add loop count to e and take moving average + ld a,e + add a,b + srl a + ld e,a + dec c + jr nz,1$ + + ld a,e + pop bc + ret + +; #################################################3 +; #################################################3 +calibrate: + ; establish comparator state + in a,(TAPE_PORT) + ld d,a + ; Wait for the first edge + call wait_edge + + ; Measure shoft pulses + call average16 + ld (SHORT_LEN), a + + ; Threshold for finding long pulse -> short*1.5 + ; stored in c + ld c,a + srl a + add a,c + ld c,a + + ; Wait for the first long pulse +1$: + call average16 + cp a,c + jr c,1$ + + ; Measure long pulses + call average16 + ld (LONG_LEN), a + + ; Calculate period treshold + ld b,a + ld a,(SHORT_LEN) + ; Average between the two lengths + add a,b + srl a + ld (TRESHOLD),a + + ; Wait for first short for training + ld c,a +3$: + call wait_cycle + ld a,b + cp a,c + jr nc,3$ + + ; Count short cycles + ld e,#1 +4$: + call wait_cycle + ld a,b + cp a,c + jr nc,5$ + inc e + jr 4$ +5$: + ld a,e + ld (SHORT_COUNT),a + + ; Count long cycles + ld e,#1 +6$: + call wait_cycle + ld a,b + cp a,c + jr c,7$ + inc e + jr 6$ +7$: + ld a,e + ld (LONG_COUNT),a + + ret + +; #################################################3 +; IO Helper functions +; #################################################3 print_with_irqs: - ei - call print_string - + call BIOS_FLUSH 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 - - + ; 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 - -; ===================================================================== +; #################################################3 ; Strings -; ===================================================================== - +; #################################################3 msg_start: - .ascii "TAPE DECODE TEST - PLAY TAPE" .db 13,10,0 +msg_short_len: + .ascii "Short len: " + .db 0 -msg_sync_ok: +msg_long_len: + .ascii "Long len: " + .db 0 - .ascii "SYNC D3 91 FOUND" - .db 13,10,0 +msg_treshold: + .ascii "Treshold: " + .db 0 +msg_short_count: + .ascii "Short count: " + .db 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 \ No newline at end of file +msg_long_count: + .ascii "Long count: " + .db 0 diff --git a/apps/casette/test2.wav b/apps/casette/test2.wav new file mode 100644 index 0000000..9037cd5 Binary files /dev/null and b/apps/casette/test2.wav differ diff --git a/apps/casette/tone_2khz.wav b/apps/casette/tone_2khz.wav new file mode 100644 index 0000000..48a57ff Binary files /dev/null and b/apps/casette/tone_2khz.wav differ diff --git a/apps/casette/tone_4khz.wav b/apps/casette/tone_4khz.wav new file mode 100644 index 0000000..98bc5ab Binary files /dev/null and b/apps/casette/tone_4khz.wav differ diff --git a/boot.asm b/boot.asm index f9f316e..934dcf4 100644 --- a/boot.asm +++ b/boot.asm @@ -19,6 +19,7 @@ BANK_MODE_CLEAR_MASK = 0xF9 ; preserve ROM-disable and auxiliary outputs .globl _bios_putc .globl _bios_try_getc .globl _bios_getc + .globl _bios_flush .globl _bios_init .globl _bios_bank .globl _bios_rom_disable @@ -79,7 +80,7 @@ boot_copy: ; Stable BIOS jump table for ROM-resident applications. ; C050 putc(A), C053 try_getc(), C056 getc(), C059 io_out(A,L), ; C05C io_in(A), C05F delay_ms(HL), C062 monitor warm start, -; C065 bank(A), C068 disable ROM, C06B enable ROM. +; C065 bank(A), C068 disable ROM, C06B enable ROM, C06E flush output. .org BIOS_PUTC jp bios_service_putc .org BIOS_TRY_GETC @@ -100,11 +101,13 @@ boot_copy: jp _bios_rom_disable .org BIOS_ROM_ENABLE jp _bios_rom_enable +.org BIOS_FLUSH + jp bios_service_flush ; Port 00 is write-only. Keep its last BIOS-managed value outside the C data ; areas so a monitor warm start does not reset it. The bootstrap establishes ; this same value in the hardware before the resident image is copied. -.org RESIDENT_RAM_BASE + 0x70 +.org RESIDENT_RAM_BASE + 0x73 _bank_control_shadow:: .db BOOT_BANK_MAPPING @@ -258,6 +261,22 @@ bios_service_getc: pop bc ret +bios_service_flush: + push af + push bc + push de + push hl + push ix + push iy + call _bios_flush + pop iy + pop ix + pop hl + pop de + pop bc + pop af + ret + bios_service_io_out: push af push bc diff --git a/include/bios.inc b/include/bios.inc index f651be8..8598e7c 100644 --- a/include/bios.inc +++ b/include/bios.inc @@ -11,3 +11,4 @@ BIOS_MONITOR = RESIDENT_RAM_BASE + 0x62 ; warm-start the monitor; does not r BIOS_BANK = RESIDENT_RAM_BASE + 0x65 ; A=0 split 48K/16K, otherwise upper 64K BIOS_ROM_DISABLE = RESIDENT_RAM_BASE + 0x68 ; disable ROM while preserving the bank selection BIOS_ROM_ENABLE = RESIDENT_RAM_BASE + 0x6B ; enable ROM while preserving the bank selection +BIOS_FLUSH = RESIDENT_RAM_BASE + 0x6E ; wait until all queued UART output is sent