diff --git a/.gitignore b/.gitignore index 378eac2..1d20adc 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ build +*__pycache__** \ No newline at end of file diff --git a/Makefile b/Makefile index 8439170..18bfa67 100644 --- a/Makefile +++ b/Makefile @@ -5,11 +5,6 @@ ASM := zasm ASMFLAGS := -w -b BUILDDIR := build -FILES := bios - -SOURCES := $(addprefix ./, $(addsuffix .asm, $(FILES))) -TARGETS := $(addprefix $(BUILDDIR)/, $(addsuffix .bin, $(FILES))) - TOPTARGETS := all clean $(TOPTARGETS): $(SUBDIRS) @@ -21,9 +16,9 @@ clean: $(BUILDDIR): mkdir -p $(BUILDDIR) -$(BUILDDIR)/%.bin: ./%.asm $(BUILDDIR)/memory.inc +$(BUILDDIR)/bios.bin: bios.asm tape.asm $(BUILDDIR)/memory.inc echo '>$<' - $(ASM) $(ASMFLAGS) $< $@ + $(ASM) $(ASMFLAGS) bios.bin $(BUILDDIR)/bios.bin basic: $(MAKE) -C apps/basic $(or $(MAKECMDGOALS),all) --no-print-directory diff --git a/apps/basic/basic.asm b/apps/basic/basic.asm index 96926dc..6c1907f 100644 --- a/apps/basic/basic.asm +++ b/apps/basic/basic.asm @@ -15,6 +15,7 @@ ; - Unsupported functions cause FCERR, not a reset or REM ; - modern backspace handling ; - Added DROM/EROM/BANK0/BANK1 commands +; - CLOAD ; ;================================================================================== ; The updates to the original BASIC within this file are copyright Grant Searle @@ -423,8 +424,8 @@ WORDTB: .WORD PEND .WORD CONT .WORD LIST .WORD CLEAR - .WORD FCERR - .WORD FCERR + .WORD CLOAD + .WORD FCERR ; csave .WORD NEW ; RESERVED WORD TOKEN VALUES @@ -4543,4 +4544,133 @@ FULLSETTOP: FULLMSG: .BYTE "[FULL MEMORY]",CR,LF,0,0 +CLOAD: ld a,(hl) ; get byte after "CLOAD" + cp ZTIMES ; "*" token? ("CLOAD*") + jp z,FCERR ; yes - array load (ARRLD1) + cp ZPRINT ; "?" ("PRINT token") verify + jp z,FCERR ; yes - flag "verify" + cp '#' ; 'cload#'? + jp z,CLOADB ; yes - load binary + ; just cload - load basic + jp FCERR + +CLOADB: inc hl ; skip # + call GETNUM ; evaluate numeric expression + call DEINT ; convert FPREG to de + ; de contains load address + ; TODO do safety check on address + push hl + push bc + push de + + ld hl,CLSTARTMSG + call PRS + + ; Load block + call CLREADBLOCK + ; hl contains block, check block type + ld a,(hl) + cp 1 ; first block needs to be a header + jr nz,CLNOHDR ; First block is not a header + ; first block is a header, get nr of blocks + inc hl + ld de,(hl) + ; print number of blocks to load + push de + push hl + ex de,hl + call PRNTHL + ld hl,CLBLOCKSMSG + call PRS + pop hl + inc hl + inc hl ; get filename + call PRS ; print filename + ; de contains the nr of blocks to load + ; TODO check de>0 + + pop de +1$: push de ; save repeat counter + call CLREADBLOCK ; hl = new data + ; TODO check block type + inc hl ; goto start of data + pop de ; get repeat counter + push hl ; save stored data + + exx + pop hl ; get stored data back + pop de ; get destination + ld bc,64 ; block size + ldir ; copy data to memory + push de ; store destination address + exx + + dec de + ld a,e + cp 0 + jr nz,1$ ; repeat if not done yet + ld a,d + cp 0 + jr nz,1$ ; repeat if not done yet + + pop de + pop bc + pop hl + ret + +CLREADBLOCK: + call BIOS_CAS_READ_BLOCK + jr nc,1$ + call BIOS_CAS_WAIT_SILENCE + call BIOS_CAS_READ_BLOCK + push hl + jr nc,2$ + ; error, make sure it is silent again + push af + call BIOS_CAS_WAIT_SILENCE + pop af + jr CLERR +1$: push hl ; save hl + call BIOS_CAS_READ_BLOCK_DRY + ; Ignore errors on this block +2$: ; but make sure it is silent again + call BIOS_CAS_WAIT_SILENCE + pop hl + ret + ; Second block failed as well, error +CLERR: cp 1 + jr z,CLECAL + cp 2 + jr z,CLESYN + cp 3 + jr z,CLERED + cp 4 + jr z,CLECHS + jp FCERR ; Unknown error +CLECAL: ld hl, CLECALMSG + jr CLEPRINT +CLESYN: ld hl, CLESYNMSG + jr CLEPRINT +CLERED: ld hl, CLEREDMSG + jr CLEPRINT +CLECHS: ld hl, CLECHSMSG + jr CLEPRINT +CLNOHDR: + ld hl,CLNOHDRMSG +CLEPRINT: + call PRS + pop de + pop bc + pop hl + jp FCERR + +CLSTARTMSG: .db "Play tape...",0ah,0dh,00h +CLECALMSG: .db "Calibration failed",0ah,0dh,00h +CLESYNMSG: .db "Synchronization failed",0ah,0dh,00h +CLEREDMSG: .db "Read failed",0ah,0dh,00h +CLECHSMSG: .db "Checksum error",0ah,0dh,00h +CLNOHDRMSG: .db "No header found", 0ah,0dh,00h +CLBLOCKSMSG: .db " blocks to load", 0ah,0dh,00h +CLCLRF: .db 0ah,0dh,00h + .end diff --git a/bios.asm b/bios.asm index d5a0a7c..e96a8dd 100644 --- a/bios.asm +++ b/bios.asm @@ -26,6 +26,7 @@ TX_BUSY .equ WORKSPACE+006h TX_BUF .equ WORKSPACE+007h RX_BUF .equ TX_BUF+BUF_SIZE SYSREG_VALUE .equ RX_BUF+BUF_SIZE +END_OF_BIOS_WS .equ SYSREG_VALUE+001h ; ------------------------------------------------------- ; entry @@ -99,6 +100,12 @@ boot_copy: jp setrom .ORG BIOS_BANK jp bank +.ORG BIOS_CAS_READ_BLOCK + jp read_block +.ORG BIOS_CAS_WAIT_SILENCE + jp wait_silence +.ORG BIOS_CAS_READ_BLOCK_DRY + jp read_block_dry ; ------------------------------------------------------- ; BIOS ENTRY @@ -137,9 +144,30 @@ init: ; Enter bios jp BASIC_RAM_START +; 100$ +; ld hl,msg_start +; call BIOS_PUTS +; call BIOS_FLUSH +; call read_block +; jr c,101$ ; error +; ; block received correctly +; ld hl,BLOCK_BUFFER +; call BIOS_PUTS +; call BIOS_FLUSH +; jp 100$ +; 101$: +; add '0' ; get error number +; call BIOS_PUTC +; ld a, 0ah +; call BIOS_PUTC +; ld a, 0dh +; call BIOS_PUTC +; call BIOS_FLUSH +; jp 100$ ; Shouldnt come here ; just to be safe an endless loop + ei 0$: call getc call putc @@ -524,3 +552,8 @@ bank: ; Strings ; ------------------------------------------------------- msg_header: .db "BIOS v1.0 - Joppe Blondel",00dh,00ah,000h + +; ------------------------------------------------------- +; Other files +; ------------------------------------------------------- +#include "tape.asm" \ No newline at end of file diff --git a/bios.inc b/bios.inc index e711b26..83ea311 100644 --- a/bios.inc +++ b/bios.inc @@ -5,3 +5,6 @@ BIOS_PUTS .equ 0c059h BIOS_FLUSH .equ 0c05ch BIOS_ROM .equ 0c05fh BIOS_BANK .equ 0c062h +BIOS_CAS_READ_BLOCK .equ 0c065h +BIOS_CAS_WAIT_SILENCE .equ 0c068h +BIOS_CAS_READ_BLOCK_DRY .equ 0c06bh \ No newline at end of file diff --git a/hardware.inc b/hardware.inc index 5c99aba..11b9da7 100644 --- a/hardware.inc +++ b/hardware.inc @@ -4,3 +4,5 @@ ROM_DISABLE_BIT .equ 001h BANK_SELECT_BIT .equ 002h BANK_ENABLE_BIT .equ 004h BANK_MODE_CLEAR_MASK .equ 0f9h + +TAPE_PORT .equ 018h diff --git a/memory.mk b/memory.mk index c7b8651..85502b0 100644 --- a/memory.mk +++ b/memory.mk @@ -11,11 +11,11 @@ SHARED_MEM_END := 0xffff BIOS_STACK_TOP := 0xffff BIOS_ROM_START := 0x0080 -BIOS_ROM_SIZE := 0x0580 +BIOS_ROM_SIZE := 0x0780 -BASIC_ROM_START := 0x0600 +BASIC_ROM_START := 0x0800 BASIC_ROM_SIZE := 0x2000 BIOS_RAM_START := 0xc000 -BIOS_WP_RAM_START := 0xc600 -BASIC_RAM_START := 0xca00 +BIOS_WP_RAM_START := 0xc800 +BASIC_RAM_START := 0xcc00 diff --git a/scripts/encode.py b/scripts/encode.py new file mode 100644 index 0000000..c09ac9a --- /dev/null +++ b/scripts/encode.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import struct +import wave +import os +import math +from pathlib import Path + +BLOCK_SIZE = 64 +TYPE_RAW = 0 +TYPE_HEADER = 1 + +def crc16(data: bytes) -> int: + """Calculate CRC-16/CCITT-FALSE.""" + crc = 0xffff + for value in data: + crc ^= value << 8 + for _ in range(8): + if crc & 0x8000: + crc = (crc << 1) ^ 0x1021 + else: + crc <<= 1 + crc &= 0xffff + return crc + + +class PulseWriter: + def __init__( + self, + sample_rate: int, + t_short_us: float, + t_long_us: float, + amplitude: float, + ) -> None: + self.sample_rate = sample_rate + self.short_samples = self._duration_samples(t_short_us) + self.long_samples = self._duration_samples(t_long_us) + self.high = round(32767 * amplitude) + self.low = -self.high + self.samples = bytearray() + + self.space(0.1) + + def space(self, time: float) -> None: + self._level(0, int(time*self.sample_rate)) + + def _duration_samples(self, duration_us: float) -> int: + sample_count = round(duration_us * self.sample_rate / 1_000_000) + if sample_count < 1: + raise ValueError( + f"pulse duration {duration_us:g} us is shorter than one sample" + ) + return sample_count + + def _level(self, value: int, sample_count: int) -> None: + self.samples.extend(struct.pack(" None: + self._level(self.high, self.short_samples) + self._level(self.low, self.long_samples) + + def long_pulse(self) -> None: + self._level(self.high, self.long_samples) + self._level(self.low, self.short_samples) + + def marker(self) -> None: + self._level(self.high, 2*self.long_samples) + self._level(self.low, 2*self.long_samples) + + def bit(self, value: int) -> None: + if value: + self.long_pulse() + else: + self.short_pulse() + + def byte(self, value: int) -> None: + # Transmit bytes most-significant bit first. + for bit_number in range(7, -1, -1): + self.bit((value >> bit_number) & 1) + + def bytes(self, value: bytes) -> None: + for byte in value: + self.byte(byte) + + def leader(self) -> None: + self.space(0.2) + for _ in range(50): + self._level(self.high, self.short_samples) + self._level(self.low, self.short_samples) + for _ in range(50): + self._level(self.high, self.long_samples) + self._level(self.low, self.long_samples) + self.marker() + for value in (0x55, 0x5A, 0xC3): + self.byte(value) + + def block(self, type:int, data:bytes) -> None: + self.leader() + block_data = struct.pack(" None: + fname = path.name + fsize = os.path.getsize(path) + data = struct.pack(" None: + pulses.block(TYPE_RAW, data) + pulses.block(TYPE_RAW, data) + +def main() -> None: + parser = argparse.ArgumentParser( + description="Generate the pulse-width encoded tape leader" + ) + parser.add_argument("input", type=Path, help="input file to encode") + parser.add_argument("output", type=Path, help="output WAV file") + parser.add_argument("--sample-rate", type=int, default=48_000) + parser.add_argument( + "--t-short", + type=float, + default=250.0, + metavar="MICROSECONDS", + help="short pulse interval in microseconds (default: 250)", + ) + parser.add_argument( + "--t-long", + type=float, + default=500.0, + metavar="MICROSECONDS", + help="long pulse interval in microseconds (default: 500)", + ) + parser.add_argument("--amplitude", type=float, default=0.75) + args = parser.parse_args() + + if args.sample_rate <= 0: + parser.error("--sample-rate must be positive") + if args.t_short <= 0 or args.t_long <= 0: + parser.error("pulse intervals must be positive") + if args.t_short >= args.t_long: + parser.error("--t-short must be less than --t-long") + if not 0.0 <= args.amplitude <= 1.0: + parser.error("--amplitude must be between 0 and 1") + + try: + pulses = PulseWriter( + args.sample_rate, + args.t_short, + args.t_long, + args.amplitude, + ) + except ValueError as error: + parser.error(str(error)) + + # create header block + header(pulses, args.input) + pulses.space(1.0) + with open(args.input, "rb") as f: + while True: + data = f.read(BLOCK_SIZE) + if not len(data): + break + raw(pulses, data) + + pulses.space(1.0) + with wave.open(str(args.output), "wb") as wav: + wav.setnchannels(1) + wav.setsampwidth(2) + wav.setframerate(args.sample_rate) + wav.writeframes(pulses.samples) + + +if __name__ == "__main__": + main() diff --git a/tape.asm b/tape.asm new file mode 100644 index 0000000..fc69a6a --- /dev/null +++ b/tape.asm @@ -0,0 +1,530 @@ +BLOCK_SIZE .equ 1+64+2 ; type + 64 bytes + crc-16 +BLOCK_ERROR_CALIBRATION .equ 1 +BLOCK_ERROR_SYNC .equ 2 +BLOCK_ERROR_READ .equ 3 +BLOCK_ERROR_CHSUM .equ 4 + +TAPE_WORKSPACE .equ END_OF_BIOS_WS +SHORT_AVG .equ TAPE_WORKSPACE + 000h ; byte +SHORT_MIN .equ TAPE_WORKSPACE + 001h ; byte +SHORT_MAX .equ TAPE_WORKSPACE + 002h ; byte +LONG_AVG .equ TAPE_WORKSPACE + 003h ; byte +LONG_MIN .equ TAPE_WORKSPACE + 004h ; byte +LONG_MAX .equ TAPE_WORKSPACE + 005h ; byte +BLOCK_BUFFER .equ TAPE_WORKSPACE + 006h ; BLOCK_SIZE + +; ------------------------------------------------------- +; Measure edge-to-edge pulse +; needs d (comparator state) +; returns b (loop count) +; carry is timeout +; uses a +; loop is 36T extra 22T (0.6 loops) +; ------------------------------------------------------- +measure_pulse: + ld b,0 ; 4T +1$: + ; use b as timeout counter (246 loops) + inc b ; 4T + jr z,9$ ; 12/7T + in a,(TAPE_PORT) ; 11T + cp d ; 4T + jp z,1$ ; 10T + ; loop has 36T + ; edge found + ld d,a ; 4T + or a ; 4T, clear carry + ret ; 10T +9$: + scf + ret + +; ------------------------------------------------------- +; Ignore one pulse and then measure the next +; needs d (comparator state) +; returns b (loop count) +; carry is timeout +; uses a +; loop is 36T extra 26T (0.7 loops) +; ------------------------------------------------------- +sample_pulse: + ld b,0 ; 4T +1$: + ; use b as timeout counter (246 loops) + inc b ; 4T + jr z,9$ ; 12/7T + in a,(TAPE_PORT) ; 11T + cp d ; 4T + jp z,1$ ; 10T + ; loop has 36T + ; edge found + ld d,a ; 4T + ; start timing from now + ld b,0 ; 4T +2$: + ; use b as timeout counter (246 loops) + inc b ; 4T + jr z,9$ ; 12/7T + in a,(TAPE_PORT) ; 11T + cp d ; 4T + jp z,2$ ; 10T + ; loop has 36T + ; edge found + ld d,a ; 4T + or a ; 4T, clear carry + ret +9$: + scf + ret + +; ------------------------------------------------------- +; Average 8 pulse measurements +; needs d (comparator state) +; returns a (average) +; cary is timeout +; uses b,e,h +; ------------------------------------------------------- +average8: + ld e,0 + ld h,8 +1$: + ; synchronize to next pulse and ignore measurement + call sample_pulse + ret c ; return when timeout + call sample_pulse + ret c ; return when timeout + ld a,e ; add loop count to e + add b + ld e,a + dec h + jr nz,1$ ; repeat this 8 times + ld a,e + srl a + srl a + srl a ; divide by 8 + or a ; cear carry + ret + +; ------------------------------------------------------- +; Make a +/-25% window around a +; needs a +; returns b (minimum) c (maximum) +; uses e,h +; NOTE: USES 8 BIT COUNTER +; @1MHZ t_long<1.14ms +; @2MHZ t_long<571us +; @4MHZ t_long<285us +; TODO: Check feasibility of using 16bit accumulator +; ------------------------------------------------------- +make_window: + ld e,a + srl a + srl a + ld h,a ; h = a/4 + ld a,e + sub h + ld b,a ; b = a-25% + ld a,e + add h + ld c,a ; c = a+25% + ret + +; ------------------------------------------------------- +; Calibration +; d needs to be kept from now on +; uses a,b,c,e,h,l +; carry is error +; ------------------------------------------------------- +calibrate: + ld a,(TAPE_PORT) ; get current state + ld d,a + call sample_pulse ; make sure we start in silence + jp nc,cal_err ; should timeout + ; wait for the first edge + ld l,0 +1$: + call sample_pulse + jr nc,2$ + ; use l as timout counter + ; inc l + ; jr nx,1$ + jr 1$ + ; timeout + jp cal_err ; TODO ignored timeout for now +2$: + ; edge found + ; get short leader timing + call average8 + jr c,cal_err ; timeout + ld (SHORT_AVG),a + call make_window + ld a,b + ld (SHORT_MIN),a + ld a,c + ld (SHORT_MAX),a + ; find the long leader + ; need 8 consecutive pulses > short*1.5 + ld a,(SHORT_AVG) + ld c,a + srl a + add c ; a = 1.5*short + ld c,a + ; TODO no overall timeout here +3$: + ld e,4 ; repeat 4 times, each first pulse + ; is ignored +4$: + call sample_pulse + jr c,cal_err + call sample_pulse + jr c,cal_err + ld a,b + ; try again if its a short pulse + cp c + jr c,3$ + ; we found a long pulse + dec e + jr nz,4$ ; repeat this 4 times + ; long leader found + call average8 + jr c,cal_err ; timeout + ld (LONG_AVG),a + call make_window + ld a,b + ld (LONG_MIN),a + ld a,c + ld (LONG_MAX),a + ; A quick sanity check: long>1.5*short + ld a,(SHORT_AVG) + ld c,a + srl a + add c + ld c,a ; c = 1.5*short + ld a,(LONG_AVG) + cp c + jr c,cal_err + ; Find the marker. marker is 2*long + ; threshold is 1.5*long + ld a,(LONG_AVG) + ld c,a + srl a + add c + ld c,a ; c = 1.5*long + ; Find two consecutive marker-width intervals +5$: + ld e,0 +6$: + call measure_pulse + jr c,cal_err ; timeout + ld a,b + cp c + jr c,5$ ; not a marker + inc e ; marker interval found + ld a,e + cp 2 ; we need two of them + jr c,6$ ; not 2 yet, measure next + ; both marker halves found + ; we are at the beginning of the first data pulse + ; next 8 bytes is the training byte 0x55 + call read_byte + jr c,cal_err + cp a,055h + jr nz,cal_err ; training byte not 0x55 + ; calibration complete + or a ; clear carry + ret +cal_err: + scf + ret + +; ------------------------------------------------------- +; Read a single bit +; needs d (comparator state) +; valid: z=1 c=bit +; error: z=0 +; uses a,b,c +; ------------------------------------------------------- +read_bit: + ; entry may be late into first pulse + ; wait for first edge to measure + ld b,0 +1$: + inc b + jr z,bit_bad + in a,(TAPE_PORT) + cp d + jp z,1$ + ld d,a + ; ignored first edge + ; measure second pulse + ld b,0 +2$: + inc b + jr z,bit_bad + in a,(TAPE_PORT) + cp d + jp z,2$ + ld d,a + ; if in short window (second half is short pulse) + ; 1 + ld a,(SHORT_MIN) + ld c,a + ld a,b + cp c + jr c,bit_bad ; pulse no silence + ret + +; ------------------------------------------------------- +; Strings +; ------------------------------------------------------- +msg_start: .db "PLAY TAPE", 0ah,0dh,00h diff --git a/tapes/test.wav b/tapes/test.wav new file mode 100644 index 0000000..77f17b0 Binary files /dev/null and b/tapes/test.wav differ