start of tape loading. hw works and have a generation script

This commit is contained in:
Joppe Blondel
2026-10-03 16:29:41 +02:00
parent 47fffc2e71
commit 5897e55094
16 changed files with 784 additions and 106 deletions
+16 -12
View File
@@ -7,25 +7,29 @@ directory instead of accessing the UART hardware directly.
The current allocation is:
- `1000-180C`: Tiny BASIC
- `180D-7FFF`: available for more applications (`1900` is the next convenient
- `1400-1C0C`: Tiny BASIC
- `1C0D-2FFF`: available
- `3000-301D`: tape loader
- `301E-7FFF`: available for more applications (`3100` is the next convenient
aligned origin)
The resident BIOS and monitor own high RAM at `C000-FFFF`. Tiny BASIC owns
`8000-BFFF` for its workspace and stack. A future application may reuse Tiny
BASIC's lower-RAM allocation because ROM applications run one at a time, but it
must leave `C000-FFFF` untouched.
`8000-BFFF` for its workspace and stack. Applications may reuse this lower-RAM
allocation because they run one at a time, but they must leave `C000-FFFF`
untouched.
Applications using the interrupt-driven console must leave interrupt mode 2,
the `I` register, and the BIOS UART/CTC configuration intact. They may return
normally if they preserve the caller's stack, or jump to `BIOS_MONITOR` for a
non-returning monitor warm start after replacing the stack pointer.
Use `BIOS_BANK` and `BIOS_ROM_DISABLE` for the write-only banking latch rather
than writing port `00` directly. The BIOS keeps the ROM-disable state when the
RAM bank selection changes. A ROM application must not expect a normal `RET`
after it maps out the ROM containing its return address.
Use `BIOS_BANK`, `BIOS_ROM_DISABLE`, and `BIOS_ROM_ENABLE` for the write-only
banking latch rather than writing port `00` directly. The BIOS keeps the
ROM-disable state when the RAM bank selection changes. A ROM application must
not expect a normal `RET` after it maps out the ROM containing its return
address.
To add an application, add its assembler target and ROM offset to the Makefile,
overlay its binary in the `monitor.prog.bin` recipe, and add a monitor command
or use `G address` to launch it.
Application origins and the resident-memory layout are defined centrally in
`rom_layout.mk`. The build turns that file into C and assembly includes. To add
an application, add its address there, add its build and overlay rules to the
Makefile, and add a monitor command or use `G address` to launch it.
+199
View File
@@ -0,0 +1,199 @@
Name casette;
PartNo 00;
Date 2026-10-03;
Revision 01;
Designer joppe;
Company unknown;
Assembly None;
Location unknown;
Device g22v10;
/* ------------------------------------------------------------
Constants
------------------------------------------------------------ */
ar = 'b'0;
sp = 'b'0;
/* ------------------------------------------------------------
Pin assignments
Physical signals IOSEL, RD and WR are active-low.
Their polarity is expressed in the equations with !.
------------------------------------------------------------ */
/* inputs */
PIN 1 = CLK;
PIN 2 = A1;
PIN 3 = A2;
PIN 4 = IOSEL;
PIN 5 = RD;
PIN 6 = A0;
PIN 7 = COMP;
PIN 8 = WR;
/* data bus */
PIN 14 = D0;
PIN 15 = D1;
PIN 16 = D2;
PIN 17 = D3;
PIN 18 = D4;
PIN 19 = D5;
PIN 20 = D6;
PIN 21 = D7;
/* registered output */
PIN 22 = LED;
/* ------------------------------------------------------------
Read register at offset 0
Conditions:
/IOSEL = 0
/RD = 0
/WR = 1
A2:A0 = 000
Result:
D0 = COMP
D7..D1 = 0
------------------------------------------------------------ */
D0 = COMP;
D1 = 'b'0;
D2 = 'b'0;
D3 = 'b'0;
D4 = 'b'0;
D5 = 'b'0;
D6 = 'b'0;
D7 = 'b'0;
D0.OE =
!IOSEL
& !RD
& WR
& !A2
& !A1
& !A0;
D1.OE =
!IOSEL
& !RD
& WR
& !A2
& !A1
& !A0;
D2.OE =
!IOSEL
& !RD
& WR
& !A2
& !A1
& !A0;
D3.OE =
!IOSEL
& !RD
& WR
& !A2
& !A1
& !A0;
D4.OE =
!IOSEL
& !RD
& WR
& !A2
& !A1
& !A0;
D5.OE =
!IOSEL
& !RD
& WR
& !A2
& !A1
& !A0;
D6.OE =
!IOSEL
& !RD
& WR
& !A2
& !A1
& !A0;
D7.OE =
!IOSEL
& !RD
& WR
& !A2
& !A1
& !A0;
/* ------------------------------------------------------------
Writable LED register
Write conditions:
/IOSEL = 0
/RD = 1
/WR = 0
A2:A0 = 000
During such a write:
LED.D = D0
Otherwise:
LED.D = LED
Pin 1 (CLK) clocks the registered macrocell.
------------------------------------------------------------ */
LED.D =
(
!IOSEL
& RD
& !WR
& !A2
& !A1
& !A0
& D0
)
#
(
(
IOSEL
# !RD
# WR
# A2
# A1
# A0
)
& LED
);
LED.ar = ar;
LED.sp = sp;
+54
View File
@@ -0,0 +1,54 @@

CUPL(WM) 5.0a Serial# MW-10400000
Device g22v10 Library DLIB-h-40-1
Created Sat Oct 03 16:22:02 2026
Name casette
Partno 00
Revision 01
Date 2026-10-03
Designer joppe
Company unknown
Assembly None
Location unknown
*QP24
*QF5892
*G1
*F0
*L00416 00000000000000000000000011111111
*L00448 11111111111111111111111111111111
*L00480 11111111101110111011011110111111
*L00512 10111111110111111111111011110111
*L00544 11111111111111111111111111111111
*L00576 11101111111110111111111111111111
*L00608 11111111111111101111111111111111
*L00640 11110111111111111111111111100111
*L00672 11111111111111111111111111111111
*L00704 11110110111111111111111111111111
*L00736 11111111111111111110111111111111
*L00768 01111111111111111111111100000000
*L00896 00000000000000000000000000001111
*L00928 10111011101110111011111101111111
*L00960 11111111000000000000000000000000
*L01472 00000000000000000000000011111011
*L01504 10111011101110111111011111111111
*L01536 11110000000000000000000000000000
*L02144 00000000000011111011101110111011
*L02176 10111111011111111111111100000000
*L02880 00000000000000000000000011111011
*L02912 10111011101110111111011111111111
*L02944 11110000000000000000000000000000
*L03648 00001111101110111011101110111111
*L03680 01111111111111110000000000000000
*L04288 00000000000000000000000011111011
*L04320 10111011101110111111011111111111
*L04352 11110000000000000000000000000000
*L04864 00000000000000000000111110111011
*L04896 10111011101111110111111111111111
*L05344 00000000000000000000000011111011
*L05376 10111011101110111111011111111111
*L05408 11111111111111111111111111110111
*L05440 11111111111111110000000000000000
*L05792 00000000000000000010111111111111
*L05824 11110011000000110000000000000000
*C5BE4
*6A87
+189
View File
@@ -0,0 +1,189 @@
#!/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
import math
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")
self.sample_rate = sample_rate
self.baud = baud
self.f_low = f_low
self.f_high = f_high
self.amplitude = amplitude
self.samples_per_half_bit = sample_rate // (2 * baud)
self.phase = 0.0
self.samples = bytearray()
def _tone(self, frequency: float, sample_count: int) -> None:
phase_step = 2.0 * math.pi * frequency / self.sample_rate
scale = int(32767 * self.amplitude)
phase = self.phase
out = self.samples
for _ in range(sample_count):
sample = int(round(scale * math.sin(phase)))
out.extend(struct.pack("<h", sample))
phase += phase_step
if phase >= 2.0 * math.pi:
phase -= 2.0 * math.pi
self.phase = phase
def leader(self, seconds: float) -> None:
self._tone(self.f_high, round(seconds * self.sample_rate))
def bit(self, value: int) -> None:
# Manchester-FSK:
# 0 = low -> high
# 1 = high -> low
if value:
first, second = self.f_high, self.f_low
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)
def byte(self, value: int) -> None:
# MSB first
for bit_no in range(7, -1, -1):
self.bit((value >> bit_no) & 1)
def bytes(self, data: bytes) -> None:
for value in data:
self.byte(value)
def make_block(sequence: int, payload: bytes) -> bytes:
header = struct.pack("<HH", sequence & 0xFFFF, len(payload))
crc = crc16_ccitt_false(header + payload)
return SYNC + header + payload + struct.pack("<H", crc)
def make_eof_block(sequence: int) -> bytes:
header = struct.pack("<HH", sequence & 0xFFFF, 0)
crc = crc16_ccitt_false(header)
return SYNC + header + struct.pack("<H", crc)
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()
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=600)
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("--sample-rate", type=int, default=48000)
parser.add_argument("--leader", type=float, default=1.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,
)
if __name__ == "__main__":
main()
+50
View File
@@ -0,0 +1,50 @@
.include "bios.inc"
.area TAPELOAD (ABS)
.org TAPE_LOADER_ROM_ADDRESS
COMP_PORT = 0x18
WORK_BASE = 0x0B000
LOAD_START = 0x08000
LOAD_LIMIT_HI = 0x0B0
STACK_TOP = 0x0BFFF
OLD_SP = WORK_BASE+0x00
start:
ld (OLD_SP), sp
ld sp, #STACK_TOP
di
poll_loop:
; Read from the hardware port COMP_PORT
in a, (COMP_PORT)
out (COMP_PORT), a
; Save the raw port status in register B
ld b, a
; Print a carriage return '\r' (ASCII 0x0D)
ld a, #0x0D
call BIOS_PUTC
; Restore the port status and isolate the relevant bit
; (Assuming bit 0 determines 0 or 1; adjust 'and' mask if it is a different bit)
ld a, b
and #0x01
; Convert the binary 0 or 1 into ASCII '0' (0x30) or '1' (0x31)
add a, #0x30
; Print the '0' or '1' character
call BIOS_PUTC
; Infinite loop: poll continuously
jr poll_loop
ld sp, (OLD_SP)
ei
ret
@@ -42,7 +42,7 @@
; Adapted to work with GNU z80asm
; Added interrupt handling
; Notes for this monitor port:
; Converted to sdasz80 syntax and linked as a ROM application at 1000.
; Converted to sdasz80 syntax and linked as a ROM application at 2200.
; Fixed-page RST calls were converted to local CALLs.
; UART access uses the resident BIOS ABI at C050/C053.
@@ -102,7 +102,7 @@ STKLMT = 0xB078 ;LOWEST ALLOWED STACK ADDRESS
;*************************************************************
.area TINYBASIC (ABS)
.org 0x1000
.org TINYBASIC_ROM_ADDRESS
main:
START:
LD SP,#STACK ;*** COLD START ***