diff --git a/MACHINES.md b/MACHINES.md index 7f82d53..de5efe8 100644 --- a/MACHINES.md +++ b/MACHINES.md @@ -232,6 +232,42 @@ Run machine language program (with entry point `2700`, hexadecimal): EXEC &H2700 +# Sharp MZ-700 + +Volume: 70 % (using cassette adapter side B) + +Load and run machine language program from monitor: + + (PLAY) + L + +To load BASIC programs, a BASIC interpreter (like `1Z-013B.mzf`, `927c33ddd4ae916ca2852207abf0be64`) must be loaded first. + +Within BASIC, BASIC programs can be loaded and run like this: + + (PLAY) + LOAD + RUN + +**BASICODE** + +There is a BASICODE implementation `S-BASIC/VERTALER BASICODE-2.**` (`s-basicode 2.mzf`, `1d9a2a3258b3233e90ae9b9529ec3f02`) that can be used to load and run BASICODE programs. It seems to be a patched version of the standard BASIC. + +First load the BASICODE-BASIC from the monitor: + + (PLAY) + L + +Then from within BASIC the "library routines" need to be initialized using + + LOAD/A + +and afterwards you can load and run BASICODE programs using: + + (PLAY) + LOAD/B + RUN + # TI-99/4A Volume: 60 % (using line cable) diff --git a/retroload-lib/examples/formats/sharpmz/Makefile b/retroload-lib/examples/formats/sharpmz/Makefile new file mode 100644 index 0000000..9f9dc67 --- /dev/null +++ b/retroload-lib/examples/formats/sharpmz/Makefile @@ -0,0 +1,12 @@ +include ../Makefile.inc + +all: rl.bin rl.mzf + +%.mzf: %.mzf.asm %.bin + $(Z80_ASM) $< -o $@ + +%.bin: %.bin.asm + $(Z80_ASM) $< -o $@ + +clean: + rm -f *.wav *.bin diff --git a/retroload-lib/examples/formats/sharpmz/rl.bin b/retroload-lib/examples/formats/sharpmz/rl.bin new file mode 100644 index 0000000..5fa659d Binary files /dev/null and b/retroload-lib/examples/formats/sharpmz/rl.bin differ diff --git a/retroload-lib/examples/formats/sharpmz/rl.bin.asm b/retroload-lib/examples/formats/sharpmz/rl.bin.asm new file mode 100644 index 0000000..c4d9691 --- /dev/null +++ b/retroload-lib/examples/formats/sharpmz/rl.bin.asm @@ -0,0 +1,50 @@ +; retroload.com +; example program for Sharp MZ-700 +; to be built using z80asm +; +; Load: L +; Run: J1200 +; + + OFFSET: equ 0x1200 + + ; monitor rom routines + BELL: equ 0x003e + PRINTS: equ 0x0012 + GETL: equ 0x0003 + + org OFFSET + +; program + ld hl, GREETING + call PRINTSTRING + call BELL + call GETL + jp 0x0000 + +PRINTSTRING: + ; using our own routine because the monitor's MSG routine ignores the carriage returns + ; beginning of string in HL + ld a, (hl) + cp 0 + ret z + push hl + call PRINTS + pop hl + inc hl + jp PRINTSTRING + +GREETING: + dm "\r" + dm "---------------------------------\r" + dm "\r" + dm "RETROLOAD.COM\r" + dm "\r" + dm "EXAMPLE FOR SHARP MZ-700 (BINARY)\r" + dm "\r" + dm "LOADED AND EXECUTED!\r" + dm "\r" + dm "---------------------------------\r" + dm "\r" + dm "PRESS RETURN TO RETURN TO MONITOR\r" + db 0 diff --git a/retroload-lib/examples/formats/sharpmz/rl.mzf b/retroload-lib/examples/formats/sharpmz/rl.mzf new file mode 100644 index 0000000..10b04bc Binary files /dev/null and b/retroload-lib/examples/formats/sharpmz/rl.mzf differ diff --git a/retroload-lib/examples/formats/sharpmz/rl.mzf.asm b/retroload-lib/examples/formats/sharpmz/rl.mzf.asm new file mode 100644 index 0000000..466b30e --- /dev/null +++ b/retroload-lib/examples/formats/sharpmz/rl.mzf.asm @@ -0,0 +1,24 @@ +; retroload.com +; example program for Sharp MZ-700 +; to be built using z80asm +; +; Load: L +; Run: J1200 +; + + OFFSET: equ 0x1200 + + db 0x01 ; Type: MZ-700 binary + dm "RL" + db 0x0d ; string delimiter + dm " " ; pad to 17 bytes including delimiter + dw END - START ; file size + dw OFFSET ; load address + dw OFFSET ; entry address + ds 104, 0x00 ; pad header to 128 bytes + +START: + + incbin "rl.bin" + +END: diff --git a/retroload-lib/src/Examples.ts b/retroload-lib/src/Examples.ts index 0c198ac..ff07d7e 100644 --- a/retroload-lib/src/Examples.ts +++ b/retroload-lib/src/Examples.ts @@ -42,6 +42,7 @@ const examples: ExampleDefinition[] = [ '2023-10-31 (KC 85/4 with BASICODE-3C Andreas and Uwe Zierott, R. Wenzel 1.5; BAC854C.SSS: d48116eaeab75f01b31ba3515fd45615)', '2023-12-10 (C64 C with BASICODE 3 Version D-1 MvD 1988; prg: d7c0990f12499ad2c3d31539e0366712, Basicode 3 Version D-1 (DE).d64: 962b708b1f6b5ac720c25e7848f4d796', '2023-12-10 (MSX Philips VG-8020 with BASICODE-3 (1987)(NOS)(NL).cas: 36018c4014149a46f600381fc4c4dadf', + // Loads on Sharp MZ-700, but there seem to be some glitches with the example. Possibly BASICODE-2 incompatibility. '2025-01-08 (Sharp MZ-700 with S-BASIC/VERTALER BASICODE-2.**; s-basicode 2.mzf: 1d9a2a3258b3233e90ae9b9529ec3f02', ], }, // Atari 800 XL @@ -276,6 +277,23 @@ const examples: ExampleDefinition[] = [ instructions: 'bload"cas:",r', tests: ['2023-12-10 OK (Philips VG-8020)'], }, + // Sharp MZ-700 + { + dir: 'sharpmz', + file: 'rl.bin', + options: {format: 'sharpmzgeneric', shortpilot: true, name: 'RL', load: '1200', entry: '1200', sharpmznorepeat: true, sharpmztype: '1'}, + hash: '08b882304fde8a43bc6c83b84a96d282', + instructions: 'L', + tests: ['2025-01-08 OK (Sharp MZ-700)'], + }, + { + dir: 'sharpmz', + file: 'rl.mzf', + options: {shortpilot: true, sharpmznorepeat: true}, + hash: '08b882304fde8a43bc6c83b84a96d282', + instructions: 'L', + tests: ['2025-01-08 OK (Sharp MZ-700)'], + }, // TA alphatronic PC { dir: 'ta_bas', diff --git a/retroload-lib/src/encoding/Adapters.ts b/retroload-lib/src/encoding/Adapters.ts index 7455c26..02bc934 100644 --- a/retroload-lib/src/encoding/Adapters.ts +++ b/retroload-lib/src/encoding/Adapters.ts @@ -22,6 +22,8 @@ import {Mo5K7Adapter} from './adapter/mo5/Mo5K7Adapter.js'; import {MsxCasAdapter} from './adapter/msx/MsxCasAdapter.js'; import {MsxGenericAdapter} from './adapter/msx/MsxGenericAdapter.js'; import {MsxTsxAdapter} from './adapter/msx/MsxTsxAdapter.js'; +import {SharpMzGenericAdapter} from './adapter/sharpmz/SharpMzGenericAdapter.js'; +import {SharpMzMzfAdapter} from './adapter/sharpmz/SharpMzMzfAdapter.js'; import {TaGenericAdapter} from './adapter/ta/TaGenericAdapter.js'; import {TiFiadAdapter} from './adapter/ti/TiFiadAdapter.js'; import {TiGenericAdapter} from './adapter/ti/TiGenericAdapter.js'; @@ -59,6 +61,8 @@ const adapters: InternalAdapterDefinition[] = [ MsxCasAdapter, MsxGenericAdapter, MsxTsxAdapter, + SharpMzGenericAdapter, + SharpMzMzfAdapter, TaGenericAdapter, TiFiadAdapter, TiGenericAdapter, diff --git a/retroload-lib/src/encoding/Options.ts b/retroload-lib/src/encoding/Options.ts index 017725e..05f1e66 100644 --- a/retroload-lib/src/encoding/Options.ts +++ b/retroload-lib/src/encoding/Options.ts @@ -153,7 +153,7 @@ export const entryOption: ArgumentOptionDefinition = { required: false, type: 'text', parse(v) { - if (v === undefined) { + if (v === undefined || v === '') { return undefined; } const address = parseInt(v, 16); diff --git a/retroload-lib/src/encoding/adapter/sharpmz/SharpMzDefinitions.ts b/retroload-lib/src/encoding/adapter/sharpmz/SharpMzDefinitions.ts new file mode 100644 index 0000000..a907c6b --- /dev/null +++ b/retroload-lib/src/encoding/adapter/sharpmz/SharpMzDefinitions.ts @@ -0,0 +1,9 @@ +import {FlagOptionDefinition} from '../../Options.js'; + +export const sharpmznorepeatOption: FlagOptionDefinition = { + name: 'sharpmznorepeat', + label: 'Don\'t repeat data', + description: 'Don\'t repeat data and header', + common: true, + type: 'bool', +}; diff --git a/retroload-lib/src/encoding/adapter/sharpmz/SharpMzEncoder.ts b/retroload-lib/src/encoding/adapter/sharpmz/SharpMzEncoder.ts new file mode 100644 index 0000000..9fea619 --- /dev/null +++ b/retroload-lib/src/encoding/adapter/sharpmz/SharpMzEncoder.ts @@ -0,0 +1,142 @@ +import {ByteRecorder, recordByteMsbFirst, recordBytes} from '../ByteRecorder.js'; +import {BufferAccess} from '../../../common/BufferAccess.js'; +import {Oscillator} from '../Oscillator.js'; +import {RecorderInterface} from '../../recorder/RecorderInterface.js'; + +const fLongPulse = 1000; +const fShortPulse = 2000; + +/** + * Encoder for Sharp MZ-700 and similar + * + * https://original.sharpmz.org/mz-700/tapeproc.htm + * https://original.sharpmz.org/mz-700/coremain.htm + * + * Note: The "L" mark after checksums in this documentation seems not to be required. + * + * Repeating the data seems to be optional: If the first recording can be loaded, the rest is ignored. + */ +export class SharpMzEncoder implements ByteRecorder { + private readonly oscillator: Oscillator; + + public constructor( + private readonly recorder: RecorderInterface, + ) { + this.oscillator = new Oscillator(this.recorder); + } + + public begin(): void { + this.oscillator.begin(); + } + + public recordHeader(header: BufferAccess, repeat: boolean = false, shortpilot: boolean = false): void { + const checksum = calculateChecksum(header); + const checksumBuffer = BufferAccess.create(2); + checksumBuffer.writeUint16Be(checksum); + + // LGAP + this.oscillator.recordOscillations(fShortPulse, shortpilot ? 5000 : 22000); // "only 10,000 for the MZ-80B" + + // LTM + this.oscillator.recordOscillations(fLongPulse, 40); + this.oscillator.recordOscillations(fShortPulse, 40); + this.oscillator.recordOscillations(fLongPulse, 1); + + // L + this.oscillator.recordOscillations(fLongPulse, 1); + + // HDR + this.recorder.beginAnnotation('Header Data'); + this.recordBytes(header); + this.recorder.endAnnotation(); + + // CHKH + this.recordBytes(checksumBuffer); + + if (repeat) { + // 256S + this.oscillator.recordOscillations(fShortPulse, 256); + + // HDRC + this.recorder.beginAnnotation('Header Data Repeated'); + this.recordBytes(header); + this.recorder.endAnnotation(); + + // CHKH + this.recordBytes(checksumBuffer); + } + } + + public recordData(data: BufferAccess, repeat: boolean = false): void { + const checksum = calculateChecksum(data); + const checksumBuffer = BufferAccess.create(2); + checksumBuffer.writeUint16Be(checksum); + + // SGAP + this.oscillator.recordOscillations(fShortPulse, 11000); + + // STM + this.oscillator.recordOscillations(fLongPulse, 20); + this.oscillator.recordOscillations(fShortPulse, 20); + this.oscillator.recordOscillations(fLongPulse, 1); + + // L + this.oscillator.recordOscillations(fLongPulse, 1); + + // FILE + this.recorder.beginAnnotation('File Data'); + this.recordBytes(data); + this.recorder.endAnnotation(); + + // CHKF + this.recordBytes(checksumBuffer); + + if (repeat) { + // 256S + this.oscillator.recordOscillations(fShortPulse, 256); + + // FILEC + this.recorder.beginAnnotation('File Data Repeated'); + this.recordBytes(data); + this.recorder.endAnnotation(); + + // CHKF + this.recordBytes(checksumBuffer); + } + } + + public recordBytes(data: BufferAccess): void { + recordBytes(this, data); + } + + public end(): void { + this.oscillator.end(); + } + + public recordByte(byte: number): void { + recordByteMsbFirst(this, byte); + this.oscillator.recordOscillations(fLongPulse, 1); + } + + public recordBit(value: number): void { + if (value) { + this.oscillator.recordOscillations(fLongPulse, 1); + } else { + this.oscillator.recordOscillations(fShortPulse, 1); + } + } +} + +/** + * sum of 1-bits in data + */ +function calculateChecksum(data: BufferAccess): number { + let bitSum = 0; + for (const byte of data.bytes()) { + for (let i = 0; i < 8; i++) { + bitSum = (bitSum + ((byte >> i) & 1)) & 0xffff; + } + } + + return bitSum; +} diff --git a/retroload-lib/src/encoding/adapter/sharpmz/SharpMzGenericAdapter.ts b/retroload-lib/src/encoding/adapter/sharpmz/SharpMzGenericAdapter.ts new file mode 100644 index 0000000..5d82968 --- /dev/null +++ b/retroload-lib/src/encoding/adapter/sharpmz/SharpMzGenericAdapter.ts @@ -0,0 +1,83 @@ +import {ArgumentOptionDefinition, OptionContainer, entryOption, loadOption, nameOption, shortpilotOption} from '../../Options.js'; +import {FormatIdentification, InternalAdapterDefinition, unidentifiable} from '../AdapterDefinition.js'; +import {BufferAccess} from '../../../common/BufferAccess.js'; +import {InvalidArgumentError} from '../../../common/Exceptions.js'; +import {Logger} from '../../../common/logging/Logger.js'; +import {RecorderInterface} from '../../recorder/RecorderInterface.js'; +import {SharpMzEncoder} from './SharpMzEncoder.js'; +import {sharpmznorepeatOption} from './SharpMzDefinitions.js'; + +export const maxNameLength = 16; + +const fileTypeMachineCode = 1; + +const sharpmztypeOption: ArgumentOptionDefinition = { + name: 'sharpmztype', + label: 'File type', + description: 'File type. Possible types: 1 = machine code program file, 2 = MZ-80 BASIC program file, 3 = MZ-80 data file, 4 = MZ-700 data file, 5 = MZ-700 BASIC program file', + argument: 'type', + required: false, + common: false, + type: 'text', + parse: (v) => v === '' ? fileTypeMachineCode : parseInt(v, 16), +}; + +/** + * Adapter for generic data for Sharp MZ-700 + */ +export const SharpMzGenericAdapter: InternalAdapterDefinition = { + label: 'Sharp-MZ (Generic binary)', + name: 'sharpmzgeneric', + options: [ + loadOption, + nameOption, + entryOption, + shortpilotOption, + sharpmznorepeatOption, + sharpmztypeOption, + ], + identify, + encode, +}; + +function identify(_filename: string, _ba: BufferAccess): FormatIdentification { + return unidentifiable; +} + +function encode(recorder: RecorderInterface, ba: BufferAccess, options: OptionContainer): void { + const loadAddress = options.getArgument(loadOption); + const entryAddress = options.getArgument(entryOption) ?? 0x0000; + + const name = options.getArgument(nameOption); + if (name.length > maxNameLength) { + throw new InvalidArgumentError(nameOption.name, `Option name is expected to be a string of ${maxNameLength} characters maximum. Example: HELLO`); + } + if (loadAddress === undefined) { + throw new InvalidArgumentError(loadOption.name, 'Option load must be set.'); + } + + const fileType = options.getArgument(sharpmztypeOption); + + const nameUppercase = name.toUpperCase(); // in "SharpSCII" only uppercase letters equal ASCII + + const doRepeat = !options.isFlagSet(sharpmznorepeatOption); + const shortpilot = options.isFlagSet(shortpilotOption); + + const header = BufferAccess.create(128); + header.setUint8(0x00, fileType); + header.setAsciiString(0x01, nameUppercase, 17, 0x20); // pad with spaces + header.setUint8(0x01 + nameUppercase.length, 0x0d); // string delimiter + header.setUint16Le(0x12, ba.length()); + header.setUint16Le(0x14, loadAddress); + header.setUint16Le(0x16, entryAddress); + + Logger.debug('Header:'); + Logger.debug(header.asHexDump()); + + const e = new SharpMzEncoder(recorder); + + e.begin(); + e.recordHeader(header, doRepeat, shortpilot); + e.recordData(ba, doRepeat); + e.end(); +} diff --git a/retroload-lib/src/encoding/adapter/sharpmz/SharpMzMzfAdapter.ts b/retroload-lib/src/encoding/adapter/sharpmz/SharpMzMzfAdapter.ts new file mode 100644 index 0000000..0959689 --- /dev/null +++ b/retroload-lib/src/encoding/adapter/sharpmz/SharpMzMzfAdapter.ts @@ -0,0 +1,59 @@ +import {FormatIdentification, InternalAdapterDefinition} from '../AdapterDefinition.js'; +import {OptionContainer, shortpilotOption} from '../../Options.js'; +import {hex16, hex8} from '../../../common/Utils.js'; +import {BufferAccess} from '../../../common/BufferAccess.js'; +import {Logger} from '../../../common/logging/Logger.js'; +import {RecorderInterface} from '../../recorder/RecorderInterface.js'; +import {SharpMzEncoder} from './SharpMzEncoder.js'; +import {sharpmznorepeatOption} from './SharpMzDefinitions.js'; + +/** + * Adapter for Sharp MZ .MZF files + * + * https://original.sharpmz.org/mz-700/tapeproc.htm + * https://original.sharpmz.org/mz-700/coremain.htm + */ +export const SharpMzMzfAdapter: InternalAdapterDefinition = { + label: 'Sharp MZ .MZF-File', + name: 'sharpmzmzf', + options: [ + sharpmznorepeatOption, + shortpilotOption, + ], + identify, + encode, +}; + +function identify(filename: string, _ba: BufferAccess): FormatIdentification { + return { + filename: (/^.*\.mzf$/iu).exec(filename) !== null, + header: undefined, + }; +} + +function encode(recorder: RecorderInterface, ba: BufferAccess, options: OptionContainer): void { + const header = ba.slice(0, 128); + const data = ba.slice(128); + const fileType = header.getUint8(0); + const fileSize = header.getUint16Le(0x12); + // TODO: File name seems to be delimited by 0x0d and uses "SHARPSCII". We would need to convert it to display it. + // const fileName = header.slice(1, 14).asAsciiString(); + const loadAddress = header.getUint16Le(0x14); + const startAddress = header.getUint16Le(0x16); + // const comment = header.slice(0x18, 104); + Logger.info(`File type: ${hex8(fileType)} File size: ${hex16(fileSize)} Load address: ${hex16(loadAddress)} Start address: ${hex16(startAddress)}`); + // Logger.info(fileName); + if (fileSize !== data.length()) { + Logger.error(`Warning: Actual length of data in MZF file (${data.length()} bytes) is not equal to length specified within header (${fileSize} bytes).`); + } + + const doRepeat = !options.isFlagSet(sharpmznorepeatOption); + const shortpilot = options.isFlagSet(shortpilotOption); + + const e = new SharpMzEncoder(recorder); + + e.begin(); + e.recordHeader(header, doRepeat, shortpilot); + e.recordData(data, doRepeat); + e.end(); +} diff --git a/retroload/src/retroload.ts b/retroload/src/retroload.ts index 6b791fb..c76e9b9 100755 --- a/retroload/src/retroload.ts +++ b/retroload/src/retroload.ts @@ -45,7 +45,7 @@ async function main(): Promise { const infile = program.args[0]; const outfile = typeof options['o'] === 'string' ? options['o'] : undefined; const shouldPlay = outfile === undefined; - const format = typeof options['f'] === 'string' ? options['f'] : undefined; + const format = typeof options['format'] === 'string' ? options['format'] : undefined; Logger.setVerbosity(parseInt(typeof options['loglevel'] === 'string' ? options['loglevel'] : '1', 10)); const data = readFile(infile);