Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add generic adapter for ZX Spectrum #40

Merged
merged 2 commits into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions MACHINES.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,12 +263,21 @@ BASIC programs can be loaded and started by
LOAD ""
RUN

(`J`, `"`, `"`, `ENTER`, ..., `R`, `ENTER`")
(`J`, `"`, `"`, `ENTER`, ..., `R`, `ENTER`)

Machine language programs can be started by
Note: Some programs may start automatically after loading.

Machine language programs can loaded by

LOAD "" CODE

(`J`, `"`, `"`, `EXTEND MODE`, `I`, `ENTER`)

...and started by

PRINT USR 32768

(`P`, `EXTEND MODE`, `L`, `ENTER`)

where `32768` is their entry address.

10 changes: 7 additions & 3 deletions retroload-lib/examples/formats/zxspectrum/Makefile
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
include ../Makefile.inc

all: rl.bas.tap rl.bin.tap rl.bas.tzx rl.bin.tzx
all: rl.bas.tap rl.bin.tap rl.bas.tzx rl.bin.tzx rl.bas rl.bin

# https://github.com/weiju/zxtaputils
%.tzx: %.tap
tzxmerge -o $@ $<

# https://github.com/weiju/zxtaputils
%.bas.tap: %.bas
%.bas.tap: %.txt
bas2tap $< $@

# https://github.com/weiju/zxtaputils
%.bas: %.txt
bas2tap --format plain $< $@

# https://github.com/weiju/zxtaputils
%.bin.tap: %.bin
tapify --objtype code --filename RL --startaddr 32768 $< $@
Expand All @@ -24,4 +28,4 @@ all: rl.bas.tap rl.bin.tap rl.bas.tzx rl.bin.tzx
retroload -o $@ $<

clean:
rm -f *.wav *.tap *.tzx *.bin
rm -f *.wav *.tap *.tzx *.bin *.bas
Binary file not shown.
Binary file modified retroload-lib/examples/formats/zxspectrum/rl.bas
Binary file not shown.
Binary file added retroload-lib/examples/formats/zxspectrum/rl.bin
Binary file not shown.
9 changes: 9 additions & 0 deletions retroload-lib/examples/formats/zxspectrum/rl.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
10 PRINT "-------------------------------"
20 PRINT
30 PRINT "RETROLOAD.COM"
40 PRINT
50 PRINT "EXAMPLE FOR ZX SPECTRUM (BASIC)"
60 PRINT
70 PRINT "LOADED AND EXECUTED!"
80 PRINT
90 PRINT "-------------------------------"
24 changes: 24 additions & 0 deletions retroload-lib/src/Examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,30 @@ const examples: ExampleDefinition[] = [
tests: [], // TODO: use different example filename (RL) and test again on real hardware
},
// ZX Spectrum
{
dir: 'zxspectrum',
file: 'rl.bas',
options: {format: 'zxspectrumgeneric', name: 'RL', zxtype: '0'},
hash: '1284a33c7f22528d35f0472bc20aee75',
instructions: 'LOAD ""\nRUN',
tests: ['2023-11-03 OK (ZX Spectrum+)'],
},
{
dir: 'zxspectrum',
file: 'number_array.dat', // contains a 1-dimensional array with 4 elements: 23, 41, 13, 37
options: {format: 'zxspectrumgeneric', name: 'RL', zxtype: '1'},
hash: '4b14148581938c3d8507e20786efa443',
instructions: 'LOAD "" DATA a()',
tests: ['2023-11-03 OK (ZX Spectrum+)'],
},
{
dir: 'zxspectrum',
file: 'rl.bin',
options: {format: 'zxspectrumgeneric', name: 'RL', load: '8000', zxtype: '3'},
hash: '84f3375e693b3c42bdfb0e46cbc656c0',
instructions: 'LOAD ""CODE\nPRINT USR 32768',
tests: ['2023-11-03 OK (ZX Spectrum+)'],
},
{
dir: 'zxspectrum',
file: 'rl.bas.tap',
Expand Down
9 changes: 9 additions & 0 deletions retroload-lib/src/common/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ export function calculateChecksum8(data: BufferAccess) {
return sum;
}

export function calculateChecksum8Xor(ba: BufferAccess, initial = 0x00) {
let sum = initial;
for (let i = 0; i < ba.length(); i++) {
sum ^= ba.getUint8(i);
}

return sum;
}

export function hex8(value: number): string {
return `0x${value.toString(16).padStart(2, '0')}`;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {Logger} from '../../../common/logging/Logger.js';
import {BlockStartNotFound, DecodingError, EndOfInput} from '../../ConverterExceptions.js';
import {type Position, formatPosition} from '../../../common/Positioning.js';
import {type FrequencyRange, is} from '../../Frequency.js';
import {hex8} from '../../../common/Utils.js';
import {calculateChecksum8Xor, hex8} from '../../../common/Utils.js';
import {SyncFinder} from '../../SyncFinder.js';

const fSyncIntro: FrequencyRange = [680, 930]; // 770 Hz
Expand Down Expand Up @@ -71,7 +71,7 @@ export class Apple2HalfPeriodProcessor {
throw new EndOfInput();
}
const dataBa = BufferAccess.createFromUint8Array(new Uint8Array(bytesRead));
const calculatedChecksum = calculateXorChecksum8(dataBa);
const calculatedChecksum = calculateChecksum8Xor(dataBa);
const checksumCorrect = calculatedChecksum === readChecksum;

if (!checksumCorrect) {
Expand Down Expand Up @@ -151,16 +151,6 @@ export class Apple2HalfPeriodProcessor {
}
}

export function calculateXorChecksum8(data: BufferAccess) {
let value = 0xff;

for (let i = 0; i < data.length(); i++) {
value ^= data.getUint8(i);
}

return value;
}

export class FileDecodingResult {
constructor(
readonly data: BufferAccess,
Expand Down
2 changes: 2 additions & 0 deletions retroload-lib/src/encoding/AdapterProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import Z1013GenericAdapter from './adapter/Z1013GenericAdapter.js';
import Z1013Z13Adapter from './adapter/Z1013Z13Adapter.js';
import Z1013Z80Adapter from './adapter/Z1013Z80Adapter.js';
import Zx81PAdapter from './adapter/Zx81PAdapter.js';
import ZxSpectrumGenericAdapter from './adapter/ZxSpectrumGenericAdapter.js';
import ZxSpectrumTapAdapter from './adapter/ZxSpectrumTapAdapter.js';
import ZxSpectrumTzxAdapter from './adapter/ZxSpectrumTzxAdapter.js';

Expand Down Expand Up @@ -63,6 +64,7 @@ export const adapters: AdapterDefinition[] = [
Z1013Z13Adapter,
Z1013Z80Adapter,
Zx81PAdapter,
ZxSpectrumGenericAdapter,
ZxSpectrumTapAdapter,
ZxSpectrumTzxAdapter,
];
103 changes: 103 additions & 0 deletions retroload-lib/src/encoding/adapter/ZxSpectrumGenericAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import {ZxSpectrumTzxEncoder} from '../encoder/ZxSpectrumTzxEncoder.js';
import {BufferAccess} from '../../common/BufferAccess.js';
import {type ArgumentOptionDefinition, nameOption, type OptionContainer, loadOption} from '../Options.js';
import {type RecorderInterface} from '../recorder/RecorderInterface.js';
import {unidentifiable, type AdapterDefinition} from './AdapterDefinition.js';
import {calculateChecksum8Xor} from '../../common/Utils.js';
import {Logger} from '../../common/logging/Logger.js';

const typeOption: ArgumentOptionDefinition<number> = {
name: 'zxtype',
label: 'File type',
description: 'ZX Spectrum: File type. Known types: 0 = BASIC program, 1 = array of numbers, 2 = array of characters, 3 = binary program (default). Arrays will be assigned the names a() or a$().',
argument: 'type',
required: false,
common: false,
type: 'text',
parse: (v) => v === '' ? typeBinary : parseInt(v, 16),
};

const typeProgram = 0;
const typeNumberArray = 1;
const typeCharArray = 2;
const typeBinary = 3;

/**
* https://faqwiki.zxnet.co.uk/wiki/Spectrum_tape_interface
*/
const definition: AdapterDefinition = {

name: 'ZX Spectrum (Generic data)',

internalName: 'zxspectrumgeneric',

targetName: ZxSpectrumTzxEncoder.getTargetName(),

options: [
nameOption,
typeOption,
loadOption,
],

identify(_filename: string, _ba: BufferAccess) {
return unidentifiable;
},

encode(recorder: RecorderInterface, ba: BufferAccess, options: OptionContainer) {
const name = options.getArgument(nameOption);
const type = options.getArgument(typeOption);

let param1;
let param2;
switch (type) {
case typeProgram:
// Misusing the loadOption in this case for the autostart parameter (BASIC line number).
param1 = options.getArgument(loadOption) ?? 0x8000;
param2 = ba.length();
break;
case typeNumberArray:
param1 = 0x8100; // a()
param2 = 0x8000;
break;
case typeCharArray:
param1 = 0xc100; // a$()
param2 = 0x8000;
break;
case typeBinary:
param1 = options.getArgument(loadOption) ?? 0x8000;
param2 = 0x8000;
break;
default:
Logger.info(`Warning: The specified file type ${type} is not known. Known types: 0 (BASIC program), 1 (number array), 2 (string array), 3 (binary data)`);
param1 = 0x8000;
param2 = 0x8000;
break;
}
// The array names a() and a$() stored at the tape doesn't seem to matter.
// The computer uses the name specified on loading (LOAD "" CODE b()).

const e = new ZxSpectrumTzxEncoder(recorder);
e.begin();

const headerBlockBa = BufferAccess.create(19);
headerBlockBa.writeUint8(0x00); // marker byte
headerBlockBa.writeUint8(type);
headerBlockBa.writeAsciiString(name, 10, 0x20);
headerBlockBa.writeUint16Le(ba.length());
headerBlockBa.writeUint16Le(param1);
headerBlockBa.writeUint16Le(param2);
headerBlockBa.writeUint8(calculateChecksum8Xor(headerBlockBa.slice(0, 18)));

e.recordStandardSpeedDataBlock(headerBlockBa);

const dataBlockBa = BufferAccess.create(ba.length() + 2);
dataBlockBa.writeUint8(0xff); // marker byte
dataBlockBa.writeBa(ba);
dataBlockBa.writeUint8(calculateChecksum8Xor(dataBlockBa));

e.recordStandardSpeedDataBlock(dataBlockBa);

e.end();
},
};
export default definition;