mirror of
https://github.com/irmen/ksim65.git
synced 2025-01-13 09:31:43 +00:00
added Commodore-64 emulator
This commit is contained in:
parent
8aad9795f7
commit
e13e86f33f
20
README.md
20
README.md
@ -24,12 +24,20 @@ Properties of this simulator:
|
|||||||
- passes several extensive unit test suites that verify instruction and cpu flags behavior
|
- 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)
|
- 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
|
## Documentation
|
||||||
|
|
||||||
Still to be written. For now, use the source ;-)
|
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
ehbasic.png
Normal file
BIN
ehbasic.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 179 KiB |
8791
src/ehbasic/basic-patched.asm
Normal file
8791
src/ehbasic/basic-patched.asm
Normal file
File diff suppressed because it is too large
Load Diff
133
src/ehbasic/main.asm
Normal file
133
src/ehbasic/main.asm
Normal 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
|
247
src/main/kotlin/razorvine/c64emu/GUI.kt
Normal file
247
src/main/kotlin/razorvine/c64emu/GUI.kt
Normal 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) {}
|
||||||
|
}
|
1121
src/main/kotlin/razorvine/c64emu/Petscii.kt
Normal file
1121
src/main/kotlin/razorvine/c64emu/Petscii.kt
Normal file
File diff suppressed because it is too large
Load Diff
61
src/main/kotlin/razorvine/c64emu/VicII.kt
Normal file
61
src/main/kotlin/razorvine/c64emu/VicII.kt
Normal 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)")
|
||||||
|
}
|
||||||
|
}
|
92
src/main/kotlin/razorvine/c64emu/c64Main.kt
Normal file
92
src/main/kotlin/razorvine/c64emu/c64Main.kt
Normal 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()
|
||||||
|
}
|
@ -7,6 +7,7 @@ import java.awt.image.BufferedImage
|
|||||||
import javax.imageio.ImageIO
|
import javax.imageio.ImageIO
|
||||||
import javax.swing.event.MouseInputListener
|
import javax.swing.event.MouseInputListener
|
||||||
import razorvine.ksim65.IHostInterface
|
import razorvine.ksim65.IHostInterface
|
||||||
|
import razorvine.ksim65.IVirtualMachine
|
||||||
import java.awt.event.*
|
import java.awt.event.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.swing.*
|
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 cyclesTf = JTextField("00000000000000")
|
||||||
val speedKhzTf = JTextField("0000000")
|
val speedKhzTf = JTextField("0000000")
|
||||||
val regAtf = JTextField("000")
|
val regAtf = JTextField("000")
|
||||||
@ -160,7 +161,7 @@ class DebugWindow(val vm: VirtualMachine) : JFrame("debugger"), ActionListener {
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
defaultCloseOperation = EXIT_ON_CLOSE
|
defaultCloseOperation = EXIT_ON_CLOSE
|
||||||
preferredSize = Dimension(350, 600)
|
preferredSize = Dimension(350, 500)
|
||||||
val cpuPanel = JPanel(GridBagLayout())
|
val cpuPanel = JPanel(GridBagLayout())
|
||||||
cpuPanel.border = BorderFactory.createTitledBorder("CPU: ${vm.cpu.name}")
|
cpuPanel.border = BorderFactory.createTitledBorder("CPU: ${vm.cpu.name}")
|
||||||
val gc = GridBagConstraints()
|
val gc = GridBagConstraints()
|
||||||
@ -323,12 +324,9 @@ class MainWindow(title: String) : JFrame(title), KeyListener, MouseInputListener
|
|||||||
addMouseMotionListener(this)
|
addMouseMotionListener(this)
|
||||||
addMouseListener(this)
|
addMouseListener(this)
|
||||||
pack()
|
pack()
|
||||||
requestFocusInWindow()
|
|
||||||
setLocationRelativeTo(null)
|
setLocationRelativeTo(null)
|
||||||
setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, mutableSetOf())
|
setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, mutableSetOf())
|
||||||
isVisible = true
|
requestFocusInWindow()
|
||||||
toFront()
|
|
||||||
requestFocus()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
|
@ -33,8 +33,8 @@ class EhBasicMachine(title: String) {
|
|||||||
bus += cpu
|
bus += cpu
|
||||||
bus.reset()
|
bus.reset()
|
||||||
|
|
||||||
|
hostDisplay.isVisible = true
|
||||||
hostDisplay.start()
|
hostDisplay.start()
|
||||||
hostDisplay.requestFocus()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var paused = false
|
var paused = false
|
||||||
|
@ -3,6 +3,7 @@ package razorvine.examplemachine
|
|||||||
import kotlin.concurrent.scheduleAtFixedRate
|
import kotlin.concurrent.scheduleAtFixedRate
|
||||||
import razorvine.ksim65.Bus
|
import razorvine.ksim65.Bus
|
||||||
import razorvine.ksim65.Cpu6502
|
import razorvine.ksim65.Cpu6502
|
||||||
|
import razorvine.ksim65.IVirtualMachine
|
||||||
import razorvine.ksim65.Version
|
import razorvine.ksim65.Version
|
||||||
import razorvine.ksim65.components.*
|
import razorvine.ksim65.components.*
|
||||||
import razorvine.ksim65.components.Timer
|
import razorvine.ksim65.components.Timer
|
||||||
@ -11,9 +12,9 @@ import javax.swing.ImageIcon
|
|||||||
/**
|
/**
|
||||||
* A virtual computer constructed from the various virtual components
|
* A virtual computer constructed from the various virtual components
|
||||||
*/
|
*/
|
||||||
class VirtualMachine(title: String) {
|
class VirtualMachine(title: String) : IVirtualMachine {
|
||||||
val bus = Bus()
|
override val bus = Bus()
|
||||||
val cpu = Cpu6502(false)
|
override val cpu = Cpu6502(false)
|
||||||
val ram = Ram(0x0000, 0xffff)
|
val ram = Ram(0x0000, 0xffff)
|
||||||
private val rtc = RealTimeClock(0xd100, 0xd108)
|
private val rtc = RealTimeClock(0xd100, 0xd108)
|
||||||
private val timer = Timer(0xd200, 0xd203, cpu)
|
private val timer = Timer(0xd200, 0xd203, cpu)
|
||||||
@ -29,6 +30,7 @@ class VirtualMachine(title: String) {
|
|||||||
init {
|
init {
|
||||||
hostDisplay.iconImage = ImageIcon(javaClass.getResource("/icon.png")).image
|
hostDisplay.iconImage = ImageIcon(javaClass.getResource("/icon.png")).image
|
||||||
debugWindow.iconImage = hostDisplay.iconImage
|
debugWindow.iconImage = hostDisplay.iconImage
|
||||||
|
debugWindow.setLocation(hostDisplay.location.x+hostDisplay.width, hostDisplay.location.y)
|
||||||
|
|
||||||
ram[Cpu6502.RESET_vector] = 0x00
|
ram[Cpu6502.RESET_vector] = 0x00
|
||||||
ram[Cpu6502.RESET_vector + 1] = 0x10
|
ram[Cpu6502.RESET_vector + 1] = 0x10
|
||||||
@ -43,16 +45,14 @@ class VirtualMachine(title: String) {
|
|||||||
bus += cpu
|
bus += cpu
|
||||||
bus.reset()
|
bus.reset()
|
||||||
|
|
||||||
hostDisplay.start()
|
|
||||||
|
|
||||||
debugWindow.setLocation(hostDisplay.location.x+hostDisplay.width, hostDisplay.location.y)
|
|
||||||
debugWindow.isVisible = true
|
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()
|
while (cpu.instrCycles > 0) bus.clock()
|
||||||
bus.clock()
|
bus.clock()
|
||||||
while (cpu.instrCycles > 0) bus.clock()
|
while (cpu.instrCycles > 0) bus.clock()
|
||||||
|
9
src/main/kotlin/razorvine/ksim65/IVirtualMachine.kt
Normal file
9
src/main/kotlin/razorvine/ksim65/IVirtualMachine.kt
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package razorvine.ksim65
|
||||||
|
|
||||||
|
interface IVirtualMachine {
|
||||||
|
fun stepInstruction()
|
||||||
|
|
||||||
|
var paused: Boolean
|
||||||
|
val cpu: Cpu6502
|
||||||
|
val bus: Bus
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user