Self written tape stuff

This commit is contained in:
Joppe Blondel
2026-10-04 13:53:22 +02:00
parent 37183d92cd
commit 5348660aa7
8 changed files with 279 additions and 1654 deletions
+1
View File
@@ -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 | | `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 | | `C068` | `BIOS_ROM_DISABLE` | Disable ROM while retaining the selected RAM mapping |
| `C06B` | `BIOS_ROM_ENABLE` | Enable 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 The service wrappers preserve working registers except for documented return
values. The monitor uses the corresponding C API declared in `bios.h`. values. The monitor uses the corresponding C API declared in `bios.h`.
+45 -131
View File
@@ -1,22 +1,4 @@
#!/usr/bin/env python3 #!/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 from __future__ import annotations
import argparse import argparse
@@ -25,29 +7,16 @@ import struct
import wave import wave
from pathlib import Path 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: class FSKWriter:
def __init__(self, sample_rate: int, baud: int, f_low: float, f_high: float, amplitude: float): def __init__(self, sample_rate: int, c_low: int, c_high: 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.sample_rate = sample_rate
self.baud = baud self.c_low = c_low
self.c_high = c_high
self.f_low = f_low self.f_low = f_low
self.f_high = f_high self.f_high = f_high
self.amplitude = amplitude 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.phase = 0.0
self.samples = bytearray() self.samples = bytearray()
@@ -57,7 +26,7 @@ class FSKWriter:
phase = self.phase phase = self.phase
out = self.samples out = self.samples
for _ in range(sample_count): for _ in range(int(sample_count)):
sample = int(round(scale * math.sin(phase))) sample = int(round(scale * math.sin(phase)))
out.extend(struct.pack("<h", sample)) out.extend(struct.pack("<h", sample))
phase += phase_step phase += phase_step
@@ -66,20 +35,19 @@ class FSKWriter:
self.phase = phase self.phase = phase
def leader(self, seconds: float) -> None: def leader(self) -> None:
self._tone(self.f_high, round(seconds * self.sample_rate)) 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: def bit(self, value: int) -> None:
# Manchester-FSK:
# 0 = low -> high
# 1 = high -> low
if value: if value:
first, second = self.f_high, self.f_low self._tone(self.f_high, self.samples_per_high)
else: else:
first, second = self.f_low, self.f_high self._tone(self.f_low, self.samples_per_low)
self._tone(first, self.samples_per_half_bit)
self._tone(second, self.samples_per_half_bit)
def byte(self, value: int) -> None: def byte(self, value: int) -> None:
# MSB first # MSB first
@@ -90,100 +58,46 @@ class FSKWriter:
for value in data: for value in data:
self.byte(value) 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: def chsum(self, data: bytes) -> int:
header = struct.pack("<HH", sequence & 0xFFFF, len(payload)) cs = 0
crc = crc16_ccitt_false(header + payload) for d in data:
return SYNC + header + payload + struct.pack("<H", crc) cs += int(d)
return cs
def make_eof_block(sequence: int) -> bytes: def main() -> None:
header = struct.pack("<HH", sequence & 0xFFFF, 0) parser = argparse.ArgumentParser(description="Encode a binary file as two tone FSK WAV")
crc = crc16_ccitt_false(header) parser.add_argument("input", type=Path, help="Binary input file")
return SYNC + header + struct.pack("<H", crc) parser.add_argument("output", type=Path, help="output WAVl default <input>.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( tones = FSKWriter(args.sample_rate, args.low_cycles, args.high_cycles, args.low, args.high, args.amplitude)
input_path: Path, tones.block(0, args.input.name.encode())
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: with wave.open(str(output_path), "wb") as wav:
wav.setnchannels(1) wav.setnchannels(1)
wav.setsampwidth(2) wav.setsampwidth(2)
wav.setframerate(sample_rate) wav.setframerate(args.sample_rate)
wav.writeframes(writer.samples) wav.writeframes(tones.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,
)
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+211 -1521
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
+21 -2
View File
@@ -19,6 +19,7 @@ BANK_MODE_CLEAR_MASK = 0xF9 ; preserve ROM-disable and auxiliary outputs
.globl _bios_putc .globl _bios_putc
.globl _bios_try_getc .globl _bios_try_getc
.globl _bios_getc .globl _bios_getc
.globl _bios_flush
.globl _bios_init .globl _bios_init
.globl _bios_bank .globl _bios_bank
.globl _bios_rom_disable .globl _bios_rom_disable
@@ -79,7 +80,7 @@ boot_copy:
; Stable BIOS jump table for ROM-resident applications. ; Stable BIOS jump table for ROM-resident applications.
; C050 putc(A), C053 try_getc(), C056 getc(), C059 io_out(A,L), ; 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, ; 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 .org BIOS_PUTC
jp bios_service_putc jp bios_service_putc
.org BIOS_TRY_GETC .org BIOS_TRY_GETC
@@ -100,11 +101,13 @@ boot_copy:
jp _bios_rom_disable jp _bios_rom_disable
.org BIOS_ROM_ENABLE .org BIOS_ROM_ENABLE
jp _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 ; 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 ; 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. ; 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:: _bank_control_shadow::
.db BOOT_BANK_MAPPING .db BOOT_BANK_MAPPING
@@ -258,6 +261,22 @@ bios_service_getc:
pop bc pop bc
ret 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: bios_service_io_out:
push af push af
push bc push bc
+1
View File
@@ -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_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_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_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