diff --git a/docs/api/target-platforms.md b/docs/api/target-platforms.md index d945e3ef..328574c6 100644 --- a/docs/api/target-platforms.md +++ b/docs/api/target-platforms.md @@ -56,6 +56,8 @@ Read [the Apple 2 programming guide](./apple2-programming-guide.md) for more inf * `pc88` – NEC PC-88 (very incomplete and not usable for anything yet) +* `zxspectrum` – Sinclair ZX Spectrum 48k (very incomplete and not usable for anything yet) + The primary and most tested platform is Commodore 64. Currently, targets that assume that the program will be loaded from disk or tape are better tested. diff --git a/include/pc88.ini b/include/pc88.ini index 1a086142..c873a62c 100644 --- a/include/pc88.ini +++ b/include/pc88.ini @@ -1,3 +1,4 @@ +;DON'T USE THIS ;a single-load PC-88 program [compilation] arch=z80 diff --git a/include/zxspectrum.ini b/include/zxspectrum.ini new file mode 100644 index 00000000..67f45504 --- /dev/null +++ b/include/zxspectrum.ini @@ -0,0 +1,20 @@ +;DON'T USE THIS +;a single-load ZX Spectrum 48k program +[compilation] +arch=z80 +modules=default_panic + +[allocation] +segments=default,slowram +segment_default_start=$8000 +segment_default_datastart=after_code +segment_default_end=$ffff +segment_slowram_start=$5ccb +segment_slowram_end=$7fff + +[output] +style=single +format=tap +extension=tap + + diff --git a/src/main/scala/millfork/Platform.scala b/src/main/scala/millfork/Platform.scala index 4aec7971..f8ae1d60 100644 --- a/src/main/scala/millfork/Platform.scala +++ b/src/main/scala/millfork/Platform.scala @@ -158,6 +158,7 @@ object Platform { case "pagecount" => PageCountOutput case "allocated" => AllocatedDataOutput case "d88" => D88Output + case "tap" => TapOutput case n => n.split(":").filter(_.nonEmpty) match { case Array(b, s, e) => BankFragmentOutput(b, parseNumber(s), parseNumber(e)) case Array(s, e) => CurrentBankFragmentOutput(parseNumber(s), parseNumber(e)) diff --git a/src/main/scala/millfork/output/TapOutput.scala b/src/main/scala/millfork/output/TapOutput.scala new file mode 100644 index 00000000..b5f6ca82 --- /dev/null +++ b/src/main/scala/millfork/output/TapOutput.scala @@ -0,0 +1,91 @@ +package millfork.output + +import java.nio.charset.StandardCharsets + +/** + * @author Karol Stasiak + */ +object TapOutput extends OutputPackager { + + def isAlphanum(c: Char): Boolean = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') + + override def packageOutput(mem: CompiledMemory, bank: String): Array[Byte] = { + val filteredName: String = mem.programName.filter(isAlphanum) + val b = mem.banks(bank) + val code = b.output.slice(b.start, b.end + 1) + val codeDataBlock = new DataBlock(code) + val codeHeaderBlock = new HeaderBlock(3, "CODE", code.length, b.start, 32768) + val loaderDataBlock = new DataBlock(ZxSpectrumBasic.loader("CODE", filteredName, b.start)) + val loaderHeaderBlock = new HeaderBlock(0, "LOADER", loaderDataBlock.inputData.length, 10, loaderDataBlock.inputData.length) + val result = Array(loaderHeaderBlock, loaderDataBlock, codeHeaderBlock, codeDataBlock).map(_.toArray) + result.flatten + } +} + +abstract class TapBlock { + def rawData: Array[Byte] + + def checksum: Byte = rawData.foldLeft(0)(_ ^ _).toByte + + def toArray: Array[Byte] = Array[Byte]( + rawData.length.+(1).toByte, + rawData.length.+(1).>>(8).toByte + ) ++ rawData :+ checksum +} + +class HeaderBlock(typ: Int, name: String, lengthOfData: Int, param1: Int, param2: Int) extends TapBlock { + val rawData: Array[Byte] = Array[Byte](0, typ.toByte) ++ name.take(10).padTo(10, ' ').getBytes(StandardCharsets.US_ASCII) ++ Array[Byte]( + lengthOfData.toByte, + lengthOfData.>>(8).toByte, + param1.toByte, + param1.>>(8).toByte, + param2.toByte, + param2.>>(8).toByte, + ) +} + +class DataBlock(val inputData: Array[Byte]) extends TapBlock { + val rawData: Array[Byte] = 0xff.toByte +: inputData +} + +object ZxSpectrumBasic { + + class Snippet(val array: Array[Byte]) extends AnyVal + + implicit def _implicit_String_to_Snippet(s: String): Snippet = new Snippet(s.getBytes(StandardCharsets.US_ASCII)) + + private def token(i: Int) = new Snippet(Array(i.toByte)) + + val SCREEN$: Snippet = token(170) + val CODE: Snippet = token(175) + val VAL: Snippet = token(176) + val USR: Snippet = token(192) + val INK: Snippet = token(217) + val PAPER: Snippet = token(218) + val BORDER: Snippet = token(231) + val REM: Snippet = token(234) + val LOAD: Snippet = token(239) + val POKE: Snippet = token(244) + val PRINT: Snippet = token(245) + val RUN: Snippet = token(247) + val RANDOMIZE: Snippet = token(249) + val CLS: Snippet = token(251) + val CLEAR: Snippet = token(253) + + def line(number: Int, tokens: Snippet*): Array[Byte] = { + val content = tokens.flatMap(_.array).toArray + Array[Byte](number.>>(8).toByte, number.toByte, (content.length + 1).toByte, (content.length + 1).>>(8).toByte) ++ content :+ 13.toByte + } + + private def quoted(a: Any): Snippet = "\"" + a + "\"" + + def loader(filename: String, rem: String, loadAddress: Int): Array[Byte] = { + Array( + line(10, REM, rem), + line(20, BORDER, VAL, "\"0\":", INK, VAL, "\"0\":", PAPER, VAL, "\"7\":", CLS), + line(30, CLEAR, VAL, quoted(loadAddress - 1)), + line(40, LOAD, quoted(filename), CODE), + line(50, RANDOMIZE, USR, VAL, quoted(loadAddress)) + ).flatten + } +} \ No newline at end of file