support custom m68k targets: select the codegen by CpuType and allow a per-target link.ld

This commit is contained in:
Irmen de Jong committed 2026-09-27 11:50:33 +02:00
1 parent 5222562493
commit ec4b47ac0d
10 files changed
+169 -76

No files matched your search

@@ -2,6 +2,7 @@ package prog8.code.target
import prog8.code.core.*
import prog8.code.target.encodings.Encoder
import prog8.code.target.zp.M68kZeropage
import java.nio.file.Path
@@ -88,20 +89,6 @@ class Amiga1200Target: ICompilationTarget,
address in 0xe80000u..0xefffffu // Zorro II autoconfig space
override fun initializeMemoryAreas(compilerOptions: CompilationOptions) {
zeropage = Amiga1200Zeropage(compilerOptions)
zeropage = M68kZeropage(compilerOptions)
}
}
private class Amiga1200Zeropage(options: CompilationOptions): Zeropage(options) {
override val SCRATCH_B1: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
override val SCRATCH_REG: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
override val SCRATCH_W1: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
override val SCRATCH_W2: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
override val SCRATCH_PTR: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
}
@@ -2,6 +2,7 @@ package prog8.code.target
import prog8.code.core.*
import prog8.code.target.encodings.Encoder
import prog8.code.target.zp.M68kZeropage
import java.nio.file.Path
@@ -128,20 +129,6 @@ class Amiga500Target: ICompilationTarget,
address in 0xe80000u..0xefffffu // Zorro II autoconfig space
override fun initializeMemoryAreas(compilerOptions: CompilationOptions) {
zeropage = AmigaZeropage(compilerOptions)
zeropage = M68kZeropage(compilerOptions)
}
}
private class AmigaZeropage(options: CompilationOptions): Zeropage(options) {
override val SCRATCH_B1: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
override val SCRATCH_REG: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
override val SCRATCH_W1: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
override val SCRATCH_W2: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
override val SCRATCH_PTR: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
}
@@ -4,6 +4,7 @@ import prog8.code.core.*
import prog8.code.source.ImportFileSystem.expandTilde
import prog8.code.target.encodings.Encoder
import prog8.code.target.zp.ConfigurableZeropage
import prog8.code.target.zp.M68kZeropage
import java.io.IOException
import java.nio.file.Path
import java.util.*
@@ -109,9 +110,12 @@ class ConfigFileTarget(
CpuType.valueOf("CPU$cpuString")
}
val ioAddresses = parseAddressRanges("io_regions", props)
val zpFullsafe = parseAddressRanges("zp_fullsafe", props)
val zpKernalsafe = parseAddressRanges("zp_kernalsafe", props)
val zpBasicsafe = parseAddressRanges("zp_basicsafe", props)
// The m68k has no zero page, so the zeropage settings below don't apply to an m68k
// target and are not required in its config file.
fun zpSetting(name: String) = if(cpuType.is68k) 0u else props.getInteger(name)
val zpFullsafe = if(cpuType.is68k) emptyList() else parseAddressRanges("zp_fullsafe", props)
val zpKernalsafe = if(cpuType.is68k) emptyList() else parseAddressRanges("zp_kernalsafe", props)
val zpBasicsafe = if(cpuType.is68k) emptyList() else parseAddressRanges("zp_basicsafe", props)
val libraryPath = expandTilde(Path(props.getString("library")))
if(!libraryPath.isDirectory())
@@ -153,12 +157,12 @@ class ConfigFileTarget(
if(assemblerOptionsStr=="") emptyList() else assemblerOptionsStr.split(" "),
varsAddress,
ioAddresses,
props.getInteger("zp_scratch_b1"),
props.getInteger("zp_scratch_reg"),
props.getInteger("zp_scratch_w1"),
props.getInteger("zp_scratch_w2"),
props.getInteger("zp_scratch_ptr"),
props.getInteger("virtual_registers"),
zpSetting("zp_scratch_b1"),
zpSetting("zp_scratch_reg"),
zpSetting("zp_scratch_w1"),
zpSetting("zp_scratch_w2"),
zpSetting("zp_scratch_ptr"),
zpSetting("virtual_registers"),
zpFullsafe,
zpKernalsafe,
zpBasicsafe,
@@ -185,7 +189,8 @@ class ConfigFileTarget(
override fun isIOAddress(address: UInt): Boolean = ioAddresses.any { address in it }
override fun initializeMemoryAreas(compilerOptions: CompilationOptions) {
zeropage = ConfigurableZeropage(
// the m68k has no zero page, so the zp scratch addresses in the config file don't apply there
zeropage = if(cpu.is68k) M68kZeropage(compilerOptions) else ConfigurableZeropage(
zpScratchB1, zpScratchReg, zpScratchW1, zpScratchW2, zpScratchPtr,
virtualregistersStart,
zpBasicsafe,
@@ -2,6 +2,7 @@ package prog8.code.target
import prog8.code.core.*
import prog8.code.target.encodings.Encoder
import prog8.code.target.zp.M68kZeropage
import java.nio.file.Path
/**
@@ -110,16 +111,3 @@ class Qemu68kTarget: ICompilationTarget,
}
}
private class M68kZeropage(options: CompilationOptions): Zeropage(options) {
override val SCRATCH_B1: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
override val SCRATCH_REG: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
override val SCRATCH_W1: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
override val SCRATCH_W2: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
override val SCRATCH_PTR: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
}
@@ -0,0 +1,21 @@
package prog8.code.target.zp
import prog8.code.core.CompilationOptions
import prog8.code.core.Zeropage
/**
* The m68k has no zero page, so there are no scratch locations to hand out.
* Any attempt to use one is a bug in the code generator.
*/
class M68kZeropage(options: CompilationOptions): Zeropage(options) {
override val SCRATCH_B1: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
override val SCRATCH_REG: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
override val SCRATCH_W1: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
override val SCRATCH_W2: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
override val SCRATCH_PTR: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
}
+10
View File
@@ -53,6 +53,16 @@ Passes the linker script (see below).
## The linker script (`link.ld`)
The compiler resolves the linker script for a target in this order:
1. `link.ld` in the target's library directory (the `library` setting). This is how a custom
target, defined outside of the compiler, supplies its own memory layout.
2. `/prog8lib/<targetname>/link.ld`, for targets built into the compiler.
3. `/prog8lib/qemu68k/link.ld` as the fallback, which is the script shown below.
Only the `ELF` output type links at all. The `AMIGAHUNK` output type (used by the built-in
Amiga targets) assembles straight to a hunk executable and ignores the linker script.
```
ENTRY(_start)
SECTIONS
@@ -1,14 +1,13 @@
package prog8.codegen.m68k
import prog8.code.assembly.IAssemblyProgram
import prog8.code.core.CompilationOptions
import prog8.code.core.CpuType
import prog8.code.core.IErrorReporter
import prog8.code.core.OutputType
import prog8.code.core.*
import prog8.code.target.Amiga1200Target
import prog8.code.target.Amiga500Target
import prog8.code.target.Qemu68kTarget
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.readText
class AssemblyProgramM68k(override val name: String, private val outputDir: Path) : IAssemblyProgram {
@@ -20,6 +19,20 @@ class AssemblyProgramM68k(override val name: String, private val outputDir: Path
fun elfFile(): Path = outputDir.resolve("$name.elf")
/**
* A target supplies its own memory layout as a "link.ld" file. A target defined by a
* config file (i.e. living outside of the compiler) keeps it in its library directory, so
* that is looked at first. Targets built into the compiler are looked up as a
* /prog8lib/<targetname>/link.ld resource. Anything else falls back to the qemu68k layout.
*/
private fun resolveLinkerScript(target: ICompilationTarget): String {
target.libraryPath?.resolve("link.ld")?.takeIf { Files.isReadable(it) }?.let { return it.readText() }
val targetResource = "/prog8lib/${target.name}/link.ld"
AssemblyProgramM68k::class.java.getResource(targetResource)?.let { return it.readText() }
return AssemblyProgramM68k::class.java.getResource("/prog8lib/${Qemu68kTarget.NAME}/link.ld")?.readText()
?: error("cannot find $targetResource resource, and no qemu68k fallback either")
}
private fun runProcess(command: List<String>, quiet: Boolean, tool: String? = null): Boolean {
val proc = ProcessBuilder(command).redirectErrorStream(true)
if (quiet)
@@ -85,9 +98,7 @@ class AssemblyProgramM68k(override val name: String, private val outputDir: Path
// Step 2: write linker script and link to ELF executable
val linkScript = outputDir.resolve("$name.link.ld")
val elfFile = elfFile()
val resourceUrl = AssemblyProgramM68k::class.java.getResource("/prog8lib/qemu68k/link.ld")
?: error("cannot find /prog8lib/qemu68k/link.ld resource")
Files.writeString(linkScript, resourceUrl.readText())
Files.writeString(linkScript, resolveLinkerScript(options.compTarget))
val linkCmd = listOf(
"vlink",
+7 -13
View File
@@ -13,10 +13,12 @@ import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.types.choice
import com.github.ajalt.clikt.parameters.types.int
import prog8.ast.AstException
import prog8.code.core.toHex
import prog8.code.source.ImportFileSystem
import prog8.code.source.ImportFileSystem.expandTilde
import prog8.code.target.*
import prog8.code.target.CompilationTargets
import prog8.code.target.Cx16Target
import prog8.code.target.VMTarget
import prog8.code.target.getCompilationTargetByName
import prog8.compiler.*
import prog8.intermediate.IRFileReader
import java.io.*
@@ -202,11 +204,6 @@ private fun compileMain(args: Array<String>): Boolean {
presenter.printErrorLine("Invalid -varsaddress value: $varsAddressStr")
return false
}
val maxCliAddress = if(compilationTarget in setOf(Amiga500Target.NAME, Amiga1200Target.NAME, Qemu68kTarget.NAME, VMTarget.NAME)) 0xFFFFFFFFu else 0xFFFFu
if(varsAddress!=null && varsAddress > maxCliAddress) {
presenter.printErrorLine("vars address must be valid integer 0..${maxCliAddress.toHex()}")
return false
}
if(varsGolden==true && varsHighBank!=null) {
presenter.printErrorLine("Either use -varsgolden or -varshigh, not both.")
@@ -217,12 +214,9 @@ private fun compileMain(args: Array<String>): Boolean {
return false
}
if(compilationTarget in setOf(Amiga500Target.NAME, Amiga1200Target.NAME, Qemu68kTarget.NAME)) {
if(varsGolden || varsHighBank!=null || varsAddress!=null) {
presenter.printErrorLine("The -varsgolden/-varshigh/-varsaddress options are not available on the m68k target")
return false
}
}
// NOTE: whether the target supports these options, and the valid address range, are both
// checked in the compiler where the compilation target is resolved: it knows the actual
// CpuType and pointer size, which also covers custom targets defined by a config file.
if(startVm==true) {
runVm(moduleFiles.first(), quietAll==true, vmTrace==true)
+1 -3
View File
@@ -777,9 +777,7 @@ private fun createAssemblyAndAssemble(program: PtProgram,
else
prog8.codegen.cpu6502.AsmGen6502(lastGeneratedLabelSequenceNr+1, asm6502CallIds)
}
compilerOptions.compTarget.name == Qemu68kTarget.NAME -> prog8.codegen.m68k.M68kCodeGenerator(retainSSAforIR)
compilerOptions.compTarget.name == Amiga500Target.NAME -> prog8.codegen.m68k.M68kCodeGenerator(retainSSAforIR)
compilerOptions.compTarget.name == Amiga1200Target.NAME -> prog8.codegen.m68k.M68kCodeGenerator(retainSSAforIR)
compilerOptions.compTarget.cpu.is68k -> prog8.codegen.m68k.M68kCodeGenerator(retainSSAforIR)
compilerOptions.compTarget.name == VMTarget.NAME -> VmCodeGen(retainSSAforIR, irCallIds)
else -> throw NotImplementedError("no code generator for cpu ${compilerOptions.compTarget.cpu}")
}
+92
View File
@@ -79,6 +79,98 @@ and ``none`` (no launcher). If omitted, the default is ``basic``.
The optional ``pointer_size`` property sets the size of pointers in bytes. It defaults to ``2`` for compatibility
with existing configuration files. The supported values are ``2`` and ``4``.
.. _custom_target_m68k:
Custom targets for the 68000 CPU
--------------------------------
.. index:: single: Custom Targets; 68000
The compiler can also generate m68k code for a custom target machine, not just for the built-in
``amiga500``, ``amiga1200`` and ``qemu68k`` targets. The code generator is selected from the ``cpu``
property rather than from the target name, so declaring an m68k CPU is all that is needed to
route a custom target to the m68k backend.
Set the ``cpu`` property to the name of the CpuType enum value, ``M68000`` or ``M68020``::
cpu = M68020
The exact spelling matters, because the value is the name of the enum value: write ``M68020`` and
not ``68020``. The 6502 CPUs are spelled ``6502``, ``65C02`` and ``virtual``, and those are tried
both with and without a ``CPU`` prefix, so ``6502`` and ``CPU6502`` both work.
An m68k target needs a much smaller configuration file than a 6502 one, because the m68k has no
zero page and uses real CPU registers instead of the compiler's virtual registers. This means the
zeropage related properties are not applicable, and you can simply leave them out of the file:
* ``zp_scratch_ptr``, ``zp_scratch_b1``, ``zp_scratch_reg``, ``zp_scratch_w1``, ``zp_scratch_w2``
* ``zp_fullsafe``, ``zp_kernalsafe``, ``zp_basicsafe``
* ``virtual_registers``
They are only ignored for an m68k CPU; a 6502 target still requires them, and a typo there is still
reported as a missing property. Set ``pointer_size = 4``, since m68k pointers are 32 bit.
Use ``output_type = ELF``. This is the only output type that links, and therefore the only one a
custom m68k target can use; the ``AMIGAHUNK`` output type assembles straight to an Amiga hunk
executable and is only available for the built-in Amiga targets. Two other things are worth knowing
when targeting bare metal: the FPU is enabled for the assembler regardless of the target, and the
assembler CPU is only upgraded to 68020 for ``amiga500`` with floating point enabled.
.. _custom_target_m68k_linker:
The linker script
^^^^^^^^^^^^^^^^^
An ``ELF`` output is produced in two steps: the compiler assembles to an object file, and then
links it into an ELF executable. The layout of that executable comes from a linker script, which
the compiler looks up in this order:
#. ``link.ld`` in the target's library directory (the directory named by the ``library`` property).
This is how a custom target, which lives outside of the compiler, supplies its own memory layout.
#. ``/prog8lib/<targetname>/link.ld``, for targets that are built into the compiler.
#. ``/prog8lib/qemu68k/link.ld`` as a fallback, so a target without a script of its own still links.
A minimal script for a custom target looks like this::
ENTRY(prog8_program_start)
SECTIONS
{
. = 0x40000;
.text : { *(.text) }
.data : {
. = ALIGN(4);
*(.data)
. = ALIGN(4);
prog8_bss_section_start = .;
*(.bss)
*(COMMON)
. = ALIGN(2);
prog8_program_end = .;
}
}
``ENTRY`` should point at ``prog8_program_start``, the label the compiler emits for the start of
the program. The ``.data`` and ``.bss`` sections are deliberately merged into a single output
section so that the linker places them in one load segment: if ``.bss`` were its own output section,
the linker would compute that segment's address as "end of the previous segment + alignment" instead
of honouring the address in the script, and ``prog8_program_end`` would no longer point at the true
end of the loaded image. For the same reason ``prog8_program_end`` is defined by the linker script
rather than by the generated assembly. Startup code that needs to clear the bss, such as the
``qemu68k`` bootinfo support, references that symbol, so leave it in.
The complete script used by the ``qemu68k`` target, together with a description of the linking
steps, is in :source:`the m68k linking documentation <codeGenM68k/docs/linking.md>`.
Source level restrictions
^^^^^^^^^^^^^^^^^^^^^^^^^
The m68k code generator rejects a number of directives and options that only make sense for a
6502 target: the ``%launcher``, ``%address``, ``%memtop`` and ``%varsaddress`` directives, register
annotations on ordinary subroutine parameters (they are only used in ``asmsub``), split word arrays
written as ``&>``, and specific block addresses. The ``-varsgolden``, ``-varshigh`` and
``-varsaddress`` command line options are rejected as well, so where the variables are placed is
decided by the linker script instead.
Memory Model
============