diff --git a/CHANGELOG.md b/CHANGELOG.md index fcea4ce0..9f5684c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Current version +* A very incomplete support for the Z80 microprocessor. + +* A very incomplete support for NEC PC-88. + ## 0.3.0 * Finally faster than C. diff --git a/README.md b/README.md index 39d8878f..b448ceae 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,8 @@ Therefore, no attribution is needed if you are developing and distributing Millf ## Planned features -* more targets: Oric computers, PC-Engine/Turbografx-16, Atari Lynx +* Z80 support, targetting PC-88, ZX Spectrum, later also maybe Armstrad CPC, MSX, TI-83 + +* more 6502 targets: Oric computers, PC-Engine/Turbografx-16, Atari Lynx * support for 65816, SuperFamicom/SNES and Apple IIgs diff --git a/docs/api/command-line.md b/docs/api/command-line.md index 8fb588b4..c4237be1 100644 --- a/docs/api/command-line.md +++ b/docs/api/command-line.md @@ -57,12 +57,12 @@ Default: native if targeting 65816, no otherwise. * `-fjmp-fix`, `-fno-jmp-fix` – Whether should prevent indirect JMP bug on page boundary. `.ini` equivalent: `prevent_jmp_indirect_bug`. -Default: no if targeting a 65C02-compatible architecture, yes otherwise. +Default: no if targeting a 65C02-compatible architecture or a non-6502 architecture, yes otherwise. * `-fzp-register`, `-fno-zp-register` – Whether should reserve 2 bytes of zero page as a pseudoregister. Increases language features. `.ini` equivalent: `zeropage_register`. -Default: yes. +Default: yes if targeting a 6502-based architecture, no otherwise. * `-fdecimal-mode`, `-fno-decimal-mode` – Whether decimal mode should be available. `.ini` equivalent: `decimal_mode`. diff --git a/docs/api/target-platforms.md b/docs/api/target-platforms.md index 1f1a0fb4..155b16df 100644 --- a/docs/api/target-platforms.md +++ b/docs/api/target-platforms.md @@ -45,6 +45,8 @@ Read [the NES programming guide](./famicom-programming-guide.md) for more info. * `apple2` – Apple II+/IIe/Enhanced IIe +* `pc88` – NEC PC-88 (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. @@ -115,6 +117,8 @@ Every platform is defined in an `.ini` file with an appropriate name. * `huc6280` (Hudson HuC6280; experimental) * `65816` (WDC 65816/65802; experimental; currently only programs that use only 16-bit addressing are supported) + + * `z80` (Zilog Z80; experimental and very incomplete) * `modules` – comma-separated list of modules that will be automatically imported @@ -141,7 +145,10 @@ Every platform is defined in an `.ini` file with an appropriate name. #### `[allocation]` section -* `zp_pointers` – either a list of comma separated zeropage addresses that can be used by the program as zeropage pointers, or `all` for all. Each value should be the address of the first of two free bytes in the zeropage. +* `zp_pointers` – +either a list of comma separated zeropage addresses that can be used by the program as zeropage pointers, or `all` for all. +Each value should be the address of the first of two free bytes in the zeropage. +Only used for 6502-based targets. * `segments` – a comma-separated list of segment names. A segment named `default` is always required. @@ -192,6 +199,8 @@ Default: `after_code`. * `::` - inclusive range of bytes in a given segment + * `d88` - a D88 floppy disk image for PC-88 + * `extension` – target file extension, with or without the dot * `bbc_inf` – should the `.inf` file with file metadata for BBC Micro be created diff --git a/docs/index.md b/docs/index.md index a21740b0..ffc31207 100644 --- a/docs/index.md +++ b/docs/index.md @@ -21,7 +21,7 @@ * [Functions](lang/functions.md) -* [Inline assembly syntax](lang/assembly.md) +* [Inline 6502 assembly syntax](lang/assembly.md) * [Important guidelines regarding reentrancy](lang/reentrancy.md) diff --git a/docs/lang/assembly.md b/docs/lang/assembly.md index af110d20..2c7027c7 100644 --- a/docs/lang/assembly.md +++ b/docs/lang/assembly.md @@ -1,6 +1,6 @@ [< back to index](../index.md) -# Using assembly within Millfork programs +# Using 6502 assembly within Millfork programs There are two ways to include raw assembly code in your Millfork programs: diff --git a/include/pc88.ini b/include/pc88.ini new file mode 100644 index 00000000..1a086142 --- /dev/null +++ b/include/pc88.ini @@ -0,0 +1,18 @@ +;a single-load PC-88 program +[compilation] +arch=z80 +modules=default_panic + +[allocation] +; TODO: find a more optimal start address +segment_default_start=$9000 +segment_default_codeend=$bfff +segment_default_datastart=after_code +segment_default_end=$efff + +[output] +style=single +format=d88 +extension=d88 + + diff --git a/src/main/scala/millfork/CompilationOptions.scala b/src/main/scala/millfork/CompilationOptions.scala index 59450c16..a6bc6441 100644 --- a/src/main/scala/millfork/CompilationOptions.scala +++ b/src/main/scala/millfork/CompilationOptions.scala @@ -192,6 +192,7 @@ object Cpu extends Enumeration { case "strictricoh" => StrictRicoh case "strict2a03" => StrictRicoh case "strict2a07" => StrictRicoh + case "z80" => Z80 case _ => ErrorReporting.fatal("Unknown CPU achitecture: " + name) } } diff --git a/src/main/scala/millfork/Main.scala b/src/main/scala/millfork/Main.scala index 6c8786ff..05e705f0 100644 --- a/src/main/scala/millfork/Main.scala +++ b/src/main/scala/millfork/Main.scala @@ -6,14 +6,15 @@ import java.util.Locale import millfork.assembly.mos.AssemblyLine import millfork.assembly.mos.opt._ +import millfork.assembly.z80.opt.Z80OptimizationPresets import millfork.buildinfo.BuildInfo import millfork.cli.{CliParser, CliStatus} import millfork.compiler.mos.MosCompiler import millfork.env.Environment import millfork.error.ErrorReporting import millfork.node.StandardCallGraph -import millfork.output.{AbstractAssembler, AssemblerOutput, MosAssembler, MosInliningCalculator} -import millfork.parser.MosSourceLoadingQueue +import millfork.output._ +import millfork.parser.{MosSourceLoadingQueue, ZSourceLoadingQueue} /** * @author Karol Stasiak @@ -94,6 +95,7 @@ object Main { val result: AssemblerOutput = CpuFamily.forType(platform.cpu) match { case CpuFamily.M6502 => assembleForMos(c, platform, options) + case CpuFamily.I80 => assembleForI80(c, platform, options) } if (c.outputAssembly) { @@ -190,6 +192,39 @@ object Main { result } + private def assembleForI80(c: Context, platform: Platform, options: CompilationOptions): AssemblerOutput = { + val optLevel = c.optimizationLevel.getOrElse(0) + val unoptimized = new ZSourceLoadingQueue( + initialFilenames = c.inputFileNames, + includePath = c.includePath, + options = options).run() + + val program = if (optLevel > 0) { + OptimizationPresets.NodeOpt.foldLeft(unoptimized)((p, opt) => p.applyNodeOptimization(opt, options)) + } else { + unoptimized + } + val callGraph = new StandardCallGraph(program) + + val env = new Environment(None, "") + env.collectDeclarations(program, options) + + val assemblyOptimizations = optLevel match { + case 0 => Nil + case _ => Z80OptimizationPresets.Good + } + + // compile + val assembler = new Z80Assembler(program, env, platform) + val result = assembler.assemble(callGraph, assemblyOptimizations, options) + ErrorReporting.assertNoErrors("Codegen failed") + ErrorReporting.debug(f"Unoptimized code size: ${assembler.unoptimizedCodeSize}%5d B") + ErrorReporting.debug(f"Optimized code size: ${assembler.optimizedCodeSize}%5d B") + ErrorReporting.debug(f"Gain: ${(100L * (assembler.unoptimizedCodeSize - assembler.optimizedCodeSize) / assembler.unoptimizedCodeSize.toDouble).round}%5d%%") + ErrorReporting.debug(f"Initialized variables: ${assembler.initializedVariablesSize}%5d B") + result + } + private def parser = new CliParser[Context] { fluff("Main options:", "") diff --git a/src/main/scala/millfork/Platform.scala b/src/main/scala/millfork/Platform.scala index 541a7c21..09850856 100644 --- a/src/main/scala/millfork/Platform.scala +++ b/src/main/scala/millfork/Platform.scala @@ -143,7 +143,7 @@ object Platform { val codeAllocators = banks.map(b => b -> new UpwardByteAllocator(bankStarts(b), bankCodeEnds(b))) val variableAllocators = banks.map(b => b -> new VariableAllocator( - if (b == "default") freePointers else Nil, bankDataStarts(b) match { + if (b == "default" && CpuFamily.forType(cpu) == CpuFamily.M6502) freePointers else Nil, bankDataStarts(b) match { case None => new AfterCodeByteAllocator(bankEnds(b)) case Some(start) => new UpwardByteAllocator(start, bankEnds(b)) })) @@ -155,6 +155,7 @@ object Platform { case "endaddr" => EndAddressOutput case "pagecount" => PageCountOutput case "allocated" => AllocatedDataOutput + case "d88" => D88Output 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/D88Output.scala b/src/main/scala/millfork/output/D88Output.scala new file mode 100644 index 00000000..ff463aa6 --- /dev/null +++ b/src/main/scala/millfork/output/D88Output.scala @@ -0,0 +1,119 @@ +package millfork.output + +import scala.collection.mutable + +/** + * @author Karol Stasiak + */ +object D88Output extends OutputPackager { + + private def bootloader(targetAddress: Int, sectorCount: Int): Array[Byte] = Array( + 0x21, targetAddress, targetAddress >> 8, 0x01, 0x00, sectorCount, 0x11, 0x02, 0x00, 0xCD, 0x0F, 0xC0, 0xC3, targetAddress, targetAddress >> 8, 0x3E, + 0x02, 0xCD, 0x41, 0xC0, 0x3E, 0x11, 0x93, 0xB8, 0x38, 0x01, 0x78, 0xCD, 0x52, 0xC0, 0x79, 0xCD, + 0x52, 0xC0, 0x7A, 0xCD, 0x52, 0xC0, 0x7B, 0xCD, 0x52, 0xC0, 0x3E, 0x12, 0xCD, 0x41, 0xC0, 0xCD, + 0x71, 0xC0, 0x1C, 0x7B, 0xFE, 0x11, 0x28, 0x03, 0x10, 0xF5, 0xC9, 0x14, 0x1E, 0x01, 0x10, 0xCF, + 0xC9, 0xF5, 0x3E, 0x0F, 0xD3, 0xFF, 0xDB, 0xFE, 0xE6, 0x02, 0x28, 0xFA, 0x3E, 0x0E, 0xD3, 0xFF, + 0x18, 0x07, 0xF5, 0xDB, 0xFE, 0xE6, 0x02, 0x28, 0xFA, 0xF1, 0xD3, 0xFD, 0x3E, 0x09, 0xD3, 0xFF, + 0xDB, 0xFE, 0xE6, 0x04, 0x28, 0xFA, 0x3E, 0x08, 0xD3, 0xFF, 0xDB, 0xFE, 0xE6, 0x04, 0x20, 0xFA, + 0xC9, 0xC5, 0xD5, 0x01, 0xFC, 0x00, 0x11, 0x0C, 0x0A, 0x3E, 0x0B, 0xD3, 0xFF, 0xDB, 0xFE, 0x0F, + 0x30, 0xFB, 0x7A, 0xD3, 0xFF, 0xED, 0xA2, 0x3E, 0x0D, 0xD3, 0xFF, 0xDB, 0xFE, 0x0F, 0x38, 0xFB, + 0xED, 0xA2, 0x7B, 0xD3, 0xFF, 0xC2, 0x79, 0xC0, 0xD1, 0xC1, 0xC9, 0xC9 + ).map(_.toByte) + + + override def packageOutput(mem: CompiledMemory, bank: String): Array[Byte] = { + val b = mem.banks(bank) + val start = b.start + val header = new D88Header + val trackList = new D88TrackList + val sectors = mutable.ListBuffer[D88Sector]() + var trackOffset = 688 + var cylinder = 0 + var head = 0 + var sector = 1 + var trackCount = 0 + + @inline def addSector(data: Array[Byte]): Unit = { + val s = D88Sector(cylinder, head, sector, data) + if (sector == 1) { + trackList.offsets(trackCount) = trackOffset + trackCount += 1 + } + sectors += s + trackOffset += s.length + sector += 1 + if (sector == 17) { + sector = 1 + head += 1 + if (head == 2) { + cylinder += 1 + head = 0 + } + } + } + + val size = b.end + 1 - b.start + val sizeInPages = size.|(0xff).+(1).>>(8) + addSector(bootloader(b.start, sizeInPages)) + for (page <- 0 until sizeInPages) { + addSector(b.output.slice(b.start + (page << 8), b.start + (page << 8) + 0x100)) + } + header.totalSize = trackOffset + sectors.map(_.toArray).foldLeft(header.toArray ++ trackList.toArray)(_ ++ _) + } +} + +sealed trait D88Part { + def toArray: Array[Byte] +} + +class D88Header extends D88Part { + var totalSize: Long = 0 + + def toArray: Array[Byte] = { + Array( + 'M', 'I', 'L', 'L', 'F', 'O', 'R', 'K', + 0, 0, 0, 0, 0, 0, 0, 0, + 0, // NUL + 0, 0, 0, 0, 0, 0, 0, 0, 0, // reserved + 0, // not write protected + 0, // 2D + totalSize, + totalSize >> 8, + totalSize >> 16, + totalSize >> 24 + ).map(_.toByte) + } +} + +class D88TrackList extends D88Part { + val offsets: Array[Int] = Array.fill[Int](164)(0) + + def toArray: Array[Byte] = { + offsets.flatMap(i => Array[Byte]( + i.toByte, + i.>>(8).toByte, + i.>>(16).toByte, + i.>>(24).toByte + )) + } +} + +case class D88Sector(cylinder: Int, head: Int, sector: Int, data: Array[Byte]) extends D88Part { + def length: Int = 16 + 256 + + override def toArray: Array[Byte] = { + val header = Array( + cylinder, head, sector, + 1, // 256B + 16, 0, // sectors per track + 0, // double density + 0, // not deleted + 0, // no crc error + 0, 0, 0, 0, 0, // reserved + 0, 1 // data size + ).map(_.toByte) + val padding = Array.fill[Byte]((256 - data.length.&(0xff)).&(0xff))(-1) + header ++ data ++ padding + } +} \ No newline at end of file diff --git a/src/main/scala/millfork/parser/ZSourceLoadingQueue.scala b/src/main/scala/millfork/parser/ZSourceLoadingQueue.scala new file mode 100644 index 00000000..054caa23 --- /dev/null +++ b/src/main/scala/millfork/parser/ZSourceLoadingQueue.scala @@ -0,0 +1,20 @@ +package millfork.parser + +import millfork.assembly.mos.AssemblyLine +import millfork.assembly.z80.ZLine +import millfork.{CompilationFlag, CompilationOptions} + +/** + * @author Karol Stasiak + */ +class ZSourceLoadingQueue(initialFilenames: List[String], + includePath: List[String], + options: CompilationOptions) extends AbstractSourceLoadingQueue[ZLine](initialFilenames, includePath, options) { + + override def createParser(filename: String, src: String, parentDir: String): MfParser[ZLine] = Z80Parser(filename, src, parentDir, options) + + def enqueueStandardModules(): Unit = { + // TODO + } + +}