added Commodore-64 emulator

This commit is contained in:
Irmen de Jong 2019-09-19 21:29:33 +02:00
parent 8aad9795f7
commit e13e86f33f
13 changed files with 10482 additions and 22 deletions

View File

@ -24,12 +24,20 @@ Properties of this simulator:
- passes several extensive unit test suites that verify instruction and cpu flags behavior
- maximum simulated performance is a 6502 running at ~100 Mhz (on my machine)
## Virtual machine examples
Two virtual example machines are included.
The default one starts with ``gradle run`` or run the ``ksim64vm`` command.
There's another one ``ehBasicMain`` that is configured to run the "enhanced 6502 basic" ROM.
## Documentation
Still to be written. For now, use the source ;-)
## Virtual machine examples
Three virtual example machines are included.
The default one starts with ``gradle run`` or run the ``ksim64vm`` command.
There's another one ``ehBasicMain`` that is configured to run the "enhanced 6502 basic" ROM:
![ehBasic](ehbasic.png)
Finally there is a fairly functional C64 emulator running the actual roms (not included,
but can be easily found elsewhere for example with the Vice emulator):
![C64 emulation](c64.png)

BIN
c64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

BIN
ehbasic.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

File diff suppressed because it is too large Load Diff

133
src/ehbasic/main.asm Normal file
View File

@ -0,0 +1,133 @@
; minimal monitor for EhBASIC and 6502 simulator V1.05
; tabs converted to space, tabwidth=6
; To run EhBASIC on the simulator load and assemble [F7] this file, start the simulator
; running [F6] then start the code with the RESET [CTRL][SHIFT]R. Just selecting RUN
; will do nothing, you'll still have to do a reset to run the code.
.include "basic-patched.asm"
; put the IRQ and MNI code in RAM so that it can be changed
IRQ_vec = VEC_SV+2 ; IRQ code vector
NMI_vec = IRQ_vec+$0A ; NMI code vector
; setup for the 6502 simulator environment
IO_AREA = $F000 ; set I/O area for this monitor
ACIAsimwr = $d00a ; simulated ACIA write port (display char out)
ACIAsimrd = $d400 ; simulated ACIA read port (keyboard input char)
; now the code. all this does is set up the vectors and interrupt code
; and wait for the user to select [C]old or [W]arm start. nothing else
; fits in less than 128 bytes
*= $FF80 ; pretend this is in a 1/8K ROM
; reset vector points here
RES_vec
CLD ; clear decimal mode
LDX #$FF ; empty stack
TXS ; set the stack
; set up vectors and interrupt code, copy them to page 2
LDY #END_CODE-LAB_vec ; set index/count
LAB_stlp
LDA LAB_vec-1,Y ; get byte from interrupt code
STA VEC_IN-1,Y ; save to RAM
DEY ; decrement index/count
BNE LAB_stlp ; loop if more to do
; now do the signon message, Y = $00 here
LAB_signon
LDA LAB_mess,Y ; get byte from sign on message
BEQ LAB_nokey ; exit loop if done
JSR V_OUTP ; output character
INY ; increment index
BNE LAB_signon ; loop, branch always
LAB_nokey
JSR V_INPT ; call scan input device
BCC LAB_nokey ; loop if no key
AND #$DF ; mask xx0x xxxx, ensure upper case
CMP #'W' ; compare with [W]arm start
BEQ LAB_dowarm ; branch if [W]arm start
CMP #'C' ; compare with [C]old start
BNE RES_vec ; loop if not [C]old start
JMP LAB_COLD ; do EhBASIC cold start
LAB_dowarm
JMP LAB_WARM ; do EhBASIC warm start
; byte out to simulated ACIA
ACIAout
STA ACIAsimwr ; save byte to simulated ACIA
RTS
; byte in from simulated ACIA
ACIAin
LDA ACIAsimrd ; get byte from simulated ACIA
BEQ LAB_nobyw ; branch if no byte waiting
SEC ; flag byte received
RTS
LAB_nobyw
CLC ; flag no byte received
no_load ; empty load vector for EhBASIC
no_save ; empty save vector for EhBASIC
RTS
; vector tables
LAB_vec
.word ACIAin ; byte in from simulated ACIA
.word ACIAout ; byte out to simulated ACIA
.word no_load ; null load vector for EhBASIC
.word no_save ; null save vector for EhBASIC
; EhBASIC IRQ support
IRQ_CODE
PHA ; save A
LDA IrqBase ; get the IRQ flag byte
LSR ; shift the set b7 to b6, and on down ...
ORA IrqBase ; OR the original back in
STA IrqBase ; save the new IRQ flag byte
PLA ; restore A
RTI
; EhBASIC NMI support
NMI_CODE
PHA ; save A
LDA NmiBase ; get the NMI flag byte
LSR ; shift the set b7 to b6, and on down ...
ORA NmiBase ; OR the original back in
STA NmiBase ; save the new NMI flag byte
PLA ; restore A
RTI
END_CODE
LAB_mess
.text $0D,$0A,"6502 EhBASIC [C]old/[W]arm ?",$00
; sign on string
; system vectors
*= $FFFA
.word NMI_vec ; NMI vector
.word RES_vec ; RESET vector
.word IRQ_vec ; IRQ vector

View File

@ -0,0 +1,247 @@
package razorvine.c64emu
import razorvine.ksim65.components.MemoryComponent
import java.awt.*
import java.awt.image.BufferedImage
import java.awt.event.*
import java.awt.image.ByteLookupTable
import java.awt.image.LookupOp
import java.io.CharConversionException
import java.util.*
import javax.swing.*
import javax.swing.Timer
/**
* Define the C64 character screen matrix: 320x200 pixels,
* 40x25 characters (of 8x8 pixels), and a colored border.
*/
object ScreenDefs {
const val SCREEN_WIDTH_CHARS = 40
const val SCREEN_HEIGHT_CHARS = 25
const val SCREEN_WIDTH = SCREEN_WIDTH_CHARS * 8
const val SCREEN_HEIGHT = SCREEN_HEIGHT_CHARS * 8
const val DISPLAY_PIXEL_SCALING: Double = 2.0
val colorPalette = listOf( // this is Pepto's Commodore-64 palette http://www.pepto.de/projects/colorvic/
Color(0x000000), // 0 = black
Color(0xFFFFFF), // 1 = white
Color(0x813338), // 2 = red
Color(0x75cec8), // 3 = cyan
Color(0x8e3c97), // 4 = purple
Color(0x56ac4d), // 5 = green
Color(0x2e2c9b), // 6 = blue
Color(0xedf171), // 7 = yellow
Color(0x8e5029), // 8 = orange
Color(0x553800), // 9 = brown
Color(0xc46c71), // 10 = light red
Color(0x4a4a4a), // 11 = dark grey
Color(0x7b7b7b), // 12 = medium grey
Color(0xa9ff9f), // 13 = light green
Color(0x706deb), // 14 = light blue
Color(0xb2b2b2) // 15 = light grey
)
}
private class BitmapScreenPanel(val chargenData: ByteArray, val ram: MemoryComponent) : JPanel() {
private val image = BufferedImage(ScreenDefs.SCREEN_WIDTH, ScreenDefs.SCREEN_HEIGHT, BufferedImage.TYPE_INT_ARGB)
private val g2d = image.graphics as Graphics2D
private val normalCharacters = loadCharacters(false)
init {
val size = Dimension(
(image.width * ScreenDefs.DISPLAY_PIXEL_SCALING).toInt(),
(image.height * ScreenDefs.DISPLAY_PIXEL_SCALING).toInt()
)
minimumSize = size
maximumSize = size
preferredSize = size
isFocusable = true
requestFocusInWindow()
}
private fun loadCharacters(shifted: Boolean): Array<BufferedImage> {
val chars = Array(256) { BufferedImage(8, 8, BufferedImage.TYPE_BYTE_BINARY) }
val offset = if(shifted) 256*8 else 0
// val color = ScreenDefs.colorPalette[14].rgb
for(char in 0..255) {
for(line in 0..7) {
val charbyte = chargenData[offset + char*8 + line].toInt()
for(x in 0..7) {
if(charbyte and (0b10000000 ushr x) !=0 )
chars[char].setRGB(x, line, 0xffffff)
}
}
}
return chars
}
override fun paint(graphics: Graphics?) {
redrawCharacters()
val g2d = graphics as Graphics2D?
g2d!!.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF)
g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE)
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC)
g2d.drawImage(
image, 0, 0, (image.width * ScreenDefs.DISPLAY_PIXEL_SCALING).toInt(),
(image.height * ScreenDefs.DISPLAY_PIXEL_SCALING).toInt(), null
)
}
private fun redrawCharacters() {
val screen = 0x0400
val colors = 0xd800
g2d.background = ScreenDefs.colorPalette[ram[0xd021].toInt()]
g2d.clearRect(0, 0, ScreenDefs.SCREEN_WIDTH, ScreenDefs.SCREEN_HEIGHT)
for(y in 0 until ScreenDefs.SCREEN_HEIGHT_CHARS) {
for(x in 0 until ScreenDefs.SCREEN_WIDTH_CHARS) {
val char = ram[screen + x + y*ScreenDefs.SCREEN_WIDTH_CHARS].toInt()
val color = ram[colors + x + y*ScreenDefs.SCREEN_WIDTH_CHARS].toInt()
drawColoredChar(x, y, char, color and 15)
}
}
}
private val coloredCharacters = mutableMapOf<Pair<Int, Int>, BufferedImage>()
private fun drawColoredChar(x: Int, y: Int, char: Int, color: Int) {
var cached = coloredCharacters[Pair(char, color)]
if(cached==null) {
cached = normalCharacters.get(char)
val colored = g2d.deviceConfiguration.createCompatibleImage(8, 8, BufferedImage.BITMASK)
val sourceRaster = cached.raster
val coloredRaster = colored.raster
val pixelArray = IntArray(4)
val javaColor = ScreenDefs.colorPalette[color]
val coloredPixel = listOf(javaColor.red, javaColor.green, javaColor.blue, javaColor.alpha).toIntArray()
for(y in 0..7) {
for(x in 0..7) {
val source = sourceRaster.getPixel(x, y, pixelArray)
if(source[0]!=0) {
coloredRaster.setPixel(x, y, coloredPixel)
}
}
}
coloredCharacters[Pair(char, color)] = colored
cached = colored
}
g2d.drawImage(cached, x * 8, y * 8, null)
}
}
class MainWindow(title: String, chargenData: ByteArray, val ram: MemoryComponent) : JFrame(title), KeyListener {
private val canvas = BitmapScreenPanel(chargenData, ram)
val keyboardBuffer = ArrayDeque<Char>()
private var borderTop: JPanel
private var borderBottom: JPanel
private var borderLeft: JPanel
private var borderRight: JPanel
init {
val borderWidth = 24
layout = GridBagLayout()
defaultCloseOperation = EXIT_ON_CLOSE
isResizable = false
isFocusable = true
// the borders (top, left, right, bottom)
borderTop = JPanel().apply {
preferredSize = Dimension(
(ScreenDefs.DISPLAY_PIXEL_SCALING * (ScreenDefs.SCREEN_WIDTH + 2 * borderWidth)).toInt(),
(ScreenDefs.DISPLAY_PIXEL_SCALING * borderWidth).toInt()
)
background = ScreenDefs.colorPalette[14]
}
borderBottom = JPanel().apply {
preferredSize = Dimension(
(ScreenDefs.DISPLAY_PIXEL_SCALING * (ScreenDefs.SCREEN_WIDTH + 2 * borderWidth)).toInt(),
(ScreenDefs.DISPLAY_PIXEL_SCALING * borderWidth).toInt()
)
background = ScreenDefs.colorPalette[14]
}
borderLeft = JPanel().apply {
preferredSize = Dimension(
(ScreenDefs.DISPLAY_PIXEL_SCALING * borderWidth).toInt(),
(ScreenDefs.DISPLAY_PIXEL_SCALING * ScreenDefs.SCREEN_HEIGHT).toInt()
)
background = ScreenDefs.colorPalette[14]
}
borderRight = JPanel().apply {
preferredSize = Dimension(
(ScreenDefs.DISPLAY_PIXEL_SCALING * borderWidth).toInt(),
(ScreenDefs.DISPLAY_PIXEL_SCALING * ScreenDefs.SCREEN_HEIGHT).toInt()
)
background = ScreenDefs.colorPalette[14]
}
var c = GridBagConstraints()
c.gridx = 0; c.gridy = 1; c.gridwidth = 3
add(borderTop, c)
c = GridBagConstraints()
c.gridx = 0; c.gridy = 2
add(borderLeft, c)
c = GridBagConstraints()
c.gridx = 2; c.gridy = 2
add(borderRight, c)
c = GridBagConstraints()
c.gridx = 0; c.gridy = 3; c.gridwidth = 3
add(borderBottom, c)
// the screen canvas(bitmap)
c = GridBagConstraints()
c.gridx = 1; c.gridy = 2
add(canvas, c)
addKeyListener(this)
pack()
setLocationRelativeTo(null)
setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, mutableSetOf())
setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, mutableSetOf())
requestFocusInWindow()
}
fun start() {
// repaint the screen's back buffer ~60 times per second
val repaintTimer = Timer(1000 / 60) {
repaint()
borderTop.background = ScreenDefs.colorPalette[ram[0xd020].toInt() and 15]
borderBottom.background = ScreenDefs.colorPalette[ram[0xd020].toInt() and 15]
borderLeft.background = ScreenDefs.colorPalette[ram[0xd020].toInt() and 15]
borderRight.background = ScreenDefs.colorPalette[ram[0xd020].toInt() and 15]
if(keyboardBuffer.isNotEmpty()) {
// inject keystrokes directly into the c64's keyboard buffer (translate to petscii first)
var kbbLen = ram[0xc6]
while(kbbLen<=10 && keyboardBuffer.isNotEmpty()) {
try {
val char = keyboardBuffer.pop()
// print("CHAR: '$char' ${char.toShort()} -> ")
val petscii = when(char) {
'\n' -> 13 // enter
'\b' -> 20 // backspace ('delete')
else -> Petscii.encodePetscii(char.toString(), true)[0]
}
// println("$petscii")
ram[0x277 + kbbLen] = petscii
kbbLen++
} catch(ccx: CharConversionException) {
// ignore character
// println("ignored")
}
}
ram[0xc6] = kbbLen
}
}
repaintTimer.initialDelay = 0
repaintTimer.start()
}
// keyboard events:
override fun keyTyped(event: KeyEvent) {
// println("KEY TYPED: $event '${event.keyChar}'")
keyboardBuffer.add(event.keyChar)
while (keyboardBuffer.size > 8)
keyboardBuffer.pop()
}
override fun keyPressed(event: KeyEvent) {}
override fun keyReleased(event: KeyEvent) {}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,61 @@
package razorvine.c64emu
import razorvine.ksim65.components.Address
import razorvine.ksim65.components.MemMappedComponent
import razorvine.ksim65.components.UByte
class VicII(startAddress: Address, endAddress: Address): MemMappedComponent(startAddress, endAddress) {
private var ramBuffer = Array<UByte>(endAddress - startAddress + 1) { 0 }
private var rasterIrqLine = 0
var currentRasterLine = 1
private var totalClocks = 0L
private var interruptStatusRegisterD019 = 0
override fun clock() {
totalClocks++
if(totalClocks % 63L == 0L) {
currentRasterLine++
if(currentRasterLine >= 312)
currentRasterLine = 0
interruptStatusRegisterD019 = if(currentRasterLine == rasterIrqLine) {
// signal that current raster line is equal to the desired IRQ raster line
interruptStatusRegisterD019 or 0b00000001
} else
interruptStatusRegisterD019 and 0b11111110
}
}
override fun reset() {
rasterIrqLine = 0
currentRasterLine = 1
totalClocks = 0L
interruptStatusRegisterD019 = 0
}
override fun get(address: Address): UByte {
val register = (address - startAddress) and 63
// println("VIC GET ${register.toString(16)}")
return when(register) {
0x11 -> (0b00011011 or ((currentRasterLine and 0b100000000) ushr 1)).toShort()
0x12 -> {
// println(" read raster: $currentRasterLine")
(currentRasterLine and 255).toShort()
}
0x19 -> interruptStatusRegisterD019.toShort()
else -> ramBuffer[register]
}
}
override fun set(address: Address, data: UByte) {
val register = (address - startAddress) and 63
ramBuffer[register] = data
when(register) {
0x11 -> {
val rasterHigh = (data.toInt() ushr 7) shl 8
rasterIrqLine = (rasterIrqLine and 0x00ff) or rasterHigh
}
0x12 -> rasterIrqLine = (rasterIrqLine and 0xff00) or data.toInt()
}
// println("VIC SET ${register.toString(16)} = ${data.toString(16)} (rasterIrqLine= $rasterIrqLine)")
}
}

View File

@ -0,0 +1,92 @@
package razorvine.c64emu
import razorvine.examplemachine.DebugWindow
import kotlin.concurrent.scheduleAtFixedRate
import razorvine.ksim65.Bus
import razorvine.ksim65.Cpu6502
import razorvine.ksim65.IVirtualMachine
import razorvine.ksim65.Version
import razorvine.ksim65.components.*
import java.io.File
import java.nio.file.Paths
import javax.swing.ImageIcon
/**
* The virtual representation of the Commodore-64
*/
class C64Machine(title: String) : IVirtualMachine {
private val romsPath = Paths.get(expandUser("~/.vice/C64"))
val chargenData = romsPath.resolve("chargen").toFile().readBytes()
val basicData = romsPath.resolve("basic").toFile().readBytes()
val kernalData = romsPath.resolve("kernal").toFile().readBytes()
override val bus = Bus()
override val cpu = Cpu6502(false)
val ram = Ram(0x0000, 0xffff)
val vic = VicII(0xd000, 0xd3ff)
val basicRom = Rom(0xa000, 0xbfff).also { it.load(basicData) }
val kernalRom = Rom(0xe000, 0xffff).also { it.load(kernalData) }
private val debugWindow = DebugWindow(this)
private val hostDisplay = MainWindow(title, chargenData, ram)
init {
hostDisplay.iconImage = ImageIcon(javaClass.getResource("/icon.png")).image
debugWindow.iconImage = hostDisplay.iconImage
debugWindow.setLocation(hostDisplay.location.x+hostDisplay.width, hostDisplay.location.y)
bus += basicRom
bus += kernalRom
bus += vic
bus += ram
bus += cpu
bus.reset()
debugWindow.isVisible = true
hostDisplay.isVisible = true
hostDisplay.start()
}
private fun expandUser(path: String): String {
if(path.startsWith("~" + File.separator)) {
return System.getProperty("user.home") + path.substring(1);
} else {
throw UnsupportedOperationException("home dir expansion not implemented for other users")
}
}
override var paused = false
override fun stepInstruction() {
while (cpu.instrCycles > 0) bus.clock()
bus.clock()
while (cpu.instrCycles > 0) bus.clock()
}
fun start() {
val timer = java.util.Timer("clock", true)
val startTime = System.currentTimeMillis()
timer.scheduleAtFixedRate(1, 1) {
if(!paused) {
repeat(400) {
stepInstruction()
if(vic.currentRasterLine == 255) {
// we force an irq here ourselves rather than fully emulating the VIC-II's raster IRQ
// or the CIA timer IRQ/NMI.
cpu.irq()
}
}
debugWindow.updateCpu(cpu, bus)
val duration = System.currentTimeMillis() - startTime
val speedKhz = cpu.totalCycles.toDouble() / duration
debugWindow.speedKhzTf.text = "%.1f".format(speedKhz)
}
}
}
}
fun main(args: Array<String>) {
val machine = C64Machine("virtual Commodore-64 - using KSim65 v${Version.version}")
machine.start()
}

View File

@ -7,6 +7,7 @@ import java.awt.image.BufferedImage
import javax.imageio.ImageIO
import javax.swing.event.MouseInputListener
import razorvine.ksim65.IHostInterface
import razorvine.ksim65.IVirtualMachine
import java.awt.event.*
import java.util.*
import javax.swing.*
@ -146,7 +147,7 @@ private class BitmapScreenPanel : JPanel() {
}
}
class DebugWindow(val vm: VirtualMachine) : JFrame("debugger"), ActionListener {
class DebugWindow(val vm: IVirtualMachine) : JFrame("debugger"), ActionListener {
val cyclesTf = JTextField("00000000000000")
val speedKhzTf = JTextField("0000000")
val regAtf = JTextField("000")
@ -160,7 +161,7 @@ class DebugWindow(val vm: VirtualMachine) : JFrame("debugger"), ActionListener {
init {
defaultCloseOperation = EXIT_ON_CLOSE
preferredSize = Dimension(350, 600)
preferredSize = Dimension(350, 500)
val cpuPanel = JPanel(GridBagLayout())
cpuPanel.border = BorderFactory.createTitledBorder("CPU: ${vm.cpu.name}")
val gc = GridBagConstraints()
@ -323,12 +324,9 @@ class MainWindow(title: String) : JFrame(title), KeyListener, MouseInputListener
addMouseMotionListener(this)
addMouseListener(this)
pack()
requestFocusInWindow()
setLocationRelativeTo(null)
setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, mutableSetOf())
isVisible = true
toFront()
requestFocus()
requestFocusInWindow()
}
fun start() {

View File

@ -33,8 +33,8 @@ class EhBasicMachine(title: String) {
bus += cpu
bus.reset()
hostDisplay.isVisible = true
hostDisplay.start()
hostDisplay.requestFocus()
}
var paused = false

View File

@ -3,6 +3,7 @@ package razorvine.examplemachine
import kotlin.concurrent.scheduleAtFixedRate
import razorvine.ksim65.Bus
import razorvine.ksim65.Cpu6502
import razorvine.ksim65.IVirtualMachine
import razorvine.ksim65.Version
import razorvine.ksim65.components.*
import razorvine.ksim65.components.Timer
@ -11,9 +12,9 @@ import javax.swing.ImageIcon
/**
* A virtual computer constructed from the various virtual components
*/
class VirtualMachine(title: String) {
val bus = Bus()
val cpu = Cpu6502(false)
class VirtualMachine(title: String) : IVirtualMachine {
override val bus = Bus()
override val cpu = Cpu6502(false)
val ram = Ram(0x0000, 0xffff)
private val rtc = RealTimeClock(0xd100, 0xd108)
private val timer = Timer(0xd200, 0xd203, cpu)
@ -29,6 +30,7 @@ class VirtualMachine(title: String) {
init {
hostDisplay.iconImage = ImageIcon(javaClass.getResource("/icon.png")).image
debugWindow.iconImage = hostDisplay.iconImage
debugWindow.setLocation(hostDisplay.location.x+hostDisplay.width, hostDisplay.location.y)
ram[Cpu6502.RESET_vector] = 0x00
ram[Cpu6502.RESET_vector + 1] = 0x10
@ -43,16 +45,14 @@ class VirtualMachine(title: String) {
bus += cpu
bus.reset()
hostDisplay.start()
debugWindow.setLocation(hostDisplay.location.x+hostDisplay.width, hostDisplay.location.y)
debugWindow.isVisible = true
hostDisplay.requestFocus()
hostDisplay.isVisible = true
hostDisplay.start()
}
var paused = false
override var paused = false
fun stepInstruction() {
override fun stepInstruction() {
while (cpu.instrCycles > 0) bus.clock()
bus.clock()
while (cpu.instrCycles > 0) bus.clock()

View File

@ -0,0 +1,9 @@
package razorvine.ksim65
interface IVirtualMachine {
fun stepInstruction()
var paused: Boolean
val cpu: Cpu6502
val bus: Bus
}