virtual machine

This commit is contained in:
Irmen de Jong 2019-09-15 05:04:57 +02:00
parent 789b1f69a6
commit d0dfb24172
20 changed files with 784 additions and 121 deletions

2
.gitignore vendored
View File

@ -5,4 +5,6 @@
build/
.idea/workspace.xml
.idea/dictionaries/
.idea/inspectionProfiles/
.attach_pid*

View File

@ -4,7 +4,7 @@
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<MarkdownNavigatorCodeStyleSettings>
<option name="RIGHT_MARGIN" value="72" />
<option name="RIGHT_MARGIN" value="80" />
</MarkdownNavigatorCodeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>

View File

@ -0,0 +1,360 @@
package razorvine.examplemachine
import razorvine.ksim65.Cpu6502
import java.awt.*
import java.awt.image.BufferedImage
import java.util.ArrayDeque
import javax.imageio.ImageIO
import javax.swing.event.MouseInputListener
import razorvine.ksim65.IHostInterface
import java.awt.event.*
import javax.swing.*
/**
* Define a monochrome screen that can display 640x480 pixels
* and/or 80x30 characters (these are 8x16 pixels).
*/
object ScreenDefs {
const val SCREEN_WIDTH_CHARS = 80
const val SCREEN_HEIGHT_CHARS = 30
const val SCREEN_WIDTH = SCREEN_WIDTH_CHARS * 8
const val SCREEN_HEIGHT = SCREEN_HEIGHT_CHARS * 16
const val DISPLAY_PIXEL_SCALING: Double = 1.5
val BG_COLOR = Color(0, 10, 20)
val FG_COLOR = Color(200, 255, 230)
val BORDER_COLOR = Color(20, 30, 40)
val Characters = loadCharacters()
private fun loadCharacters(): Array<BufferedImage> {
val img = ImageIO.read(javaClass.getResource("/charset/unscii8x16.png"))
val charactersImage = BufferedImage(img.width, img.height, BufferedImage.TYPE_INT_ARGB)
charactersImage.createGraphics().drawImage(img, 0, 0, null)
val black = Color(0, 0, 0).rgb
val foreground = FG_COLOR.rgb
val nopixel = Color(0, 0, 0, 0).rgb
for (y in 0 until charactersImage.height) {
for (x in 0 until charactersImage.width) {
val col = charactersImage.getRGB(x, y)
if (col == black)
charactersImage.setRGB(x, y, nopixel)
else
charactersImage.setRGB(x, y, foreground)
}
}
val numColumns = charactersImage.width / 8
val charImages = (0..255).map {
val charX = it % numColumns
val charY = it / numColumns
charactersImage.getSubimage(charX * 8, charY * 16, 8, 16)
}
return charImages.toTypedArray()
}
}
private class BitmapScreenPanel : JPanel() {
private val image = BufferedImage(ScreenDefs.SCREEN_WIDTH, ScreenDefs.SCREEN_HEIGHT, BufferedImage.TYPE_INT_ARGB)
private val g2d = image.graphics as Graphics2D
private var cursorX: Int = 0
private var cursorY: Int = 0
init {
val size = Dimension(
(image.width * ScreenDefs.DISPLAY_PIXEL_SCALING).toInt(),
(image.height * ScreenDefs.DISPLAY_PIXEL_SCALING).toInt()
)
minimumSize = size
maximumSize = size
preferredSize = size
clearScreen()
isFocusable = true
requestFocusInWindow()
}
override fun paint(graphics: Graphics?) {
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
)
}
fun clearScreen() {
g2d.background = ScreenDefs.BG_COLOR
g2d.clearRect(0, 0, ScreenDefs.SCREEN_WIDTH, ScreenDefs.SCREEN_HEIGHT)
cursorX = 0
cursorY = 0
}
fun setPixel(x: Int, y: Int, onOff: Boolean) {
if (onOff)
image.setRGB(x, y, ScreenDefs.FG_COLOR.rgb)
else
image.setRGB(x, y, ScreenDefs.BG_COLOR.rgb)
}
fun getPixel(x: Int, y: Int) = image.getRGB(x, y) != ScreenDefs.BG_COLOR.rgb
fun setChar(x: Int, y: Int, character: Char) {
g2d.clearRect(8 * x, 16 * y, 8, 16)
val coloredImage = ScreenDefs.Characters[character.toInt()]
g2d.drawImage(coloredImage, 8 * x, 16 * y, null)
}
fun scrollUp() {
g2d.copyArea(0, 16, ScreenDefs.SCREEN_WIDTH, ScreenDefs.SCREEN_HEIGHT - 16, 0, -16)
g2d.background = ScreenDefs.BG_COLOR
g2d.clearRect(0, ScreenDefs.SCREEN_HEIGHT - 16, ScreenDefs.SCREEN_WIDTH, 16)
}
fun mousePixelPosition(): Point? {
val pos = mousePosition ?: return null
return Point(
(pos.x / ScreenDefs.DISPLAY_PIXEL_SCALING).toInt(),
(pos.y / ScreenDefs.DISPLAY_PIXEL_SCALING).toInt()
)
}
}
class DebugWindow(val vm: VirtualMachine) : JFrame("debugger"), ActionListener {
val cyclesTf = JTextField("00000000000000")
val regAtf = JTextField("000")
val regXtf = JTextField("000")
val regYtf = JTextField("000")
val regPCtf = JTextField("00000")
val regSPtf = JTextField("000")
val regPtf = JTextField("000000000")
val opcodeTf = JTextField("000")
val mnemonicTf = JTextField("brk ")
private val pauseBt = JButton("Pause").also { it.actionCommand = "pause" }
init {
isFocusable = true
val cpuPanel = JPanel(GridBagLayout())
cpuPanel.border = BorderFactory.createTitledBorder("CPU: ${vm.cpu.name}")
val gc = GridBagConstraints()
gc.insets = Insets(4, 4, 4, 4)
gc.anchor = GridBagConstraints.EAST
gc.gridx = 0
gc.gridy = 0
val cyclesLb = JLabel("cycles")
val regAlb = JLabel("A")
val regXlb = JLabel("X")
val regYlb = JLabel("Y")
val regSPlb = JLabel("SP")
val regPClb = JLabel("PC")
val dummyLb = JLabel("")
val regPlb = JLabel("Status")
val opcodeLb = JLabel("opcode")
val mnemonicLb = JLabel("mnemonic")
listOf(cyclesLb, regAlb, regXlb, regYlb, regSPlb, regPClb, dummyLb, regPlb, opcodeLb, mnemonicLb).forEach {
cpuPanel.add(it, gc)
gc.gridy++
}
gc.anchor = GridBagConstraints.WEST
gc.gridx = 1
gc.gridy = 0
val bitsLb = JTextField("NV-BDIZC")
listOf(cyclesTf, regAtf, regXtf, regYtf, regSPtf, regPCtf, bitsLb, regPtf, opcodeTf, mnemonicTf).forEach {
it.font = Font(Font.MONOSPACED, Font.PLAIN, 16)
it.isEditable = false
it.columns = it.text.length
cpuPanel.add(it, gc)
gc.gridy++
}
add(cpuPanel, BorderLayout.NORTH)
val buttonPanel = JPanel(FlowLayout())
buttonPanel.border = BorderFactory.createTitledBorder("Control")
val resetBt = JButton("Reset").also { it.actionCommand = "reset" }
val cycleBt = JButton("Cycle").also { it.actionCommand = "cycle" }
listOf(resetBt, cycleBt, pauseBt).forEach {
it.addActionListener(this)
buttonPanel.add(it)
}
add(buttonPanel, BorderLayout.CENTER)
pack()
}
override fun actionPerformed(e: ActionEvent) {
when {
e.actionCommand == "reset" -> {
vm.bus.reset()
updateCpu(vm.cpu)
}
e.actionCommand == "cycle" -> {
vm.bus.clock()
updateCpu(vm.cpu)
}
e.actionCommand == "pause" -> {
vm.paused = true
pauseBt.actionCommand = "continue"
pauseBt.text = "Cont."
}
e.actionCommand == "continue" -> {
vm.paused = false
pauseBt.actionCommand = "pause"
pauseBt.text = "Pause"
}
}
}
fun updateCpu(cpu: Cpu6502) {
cyclesTf.text = cpu.totalCycles.toString()
regAtf.text = cpu.hexB(cpu.regA)
regXtf.text = cpu.hexB(cpu.regX)
regYtf.text = cpu.hexB(cpu.regY)
regPtf.text = cpu.regP.asByte().toString(2).padStart(8, '0')
regPCtf.text = cpu.hexW(cpu.regPC)
regSPtf.text = cpu.hexB(cpu.regSP)
opcodeTf.text = cpu.hexB(cpu.currentOpcode)
mnemonicTf.text = cpu.currentMnemonic
}
}
class MainWindow(title: String) : JFrame(title), KeyListener, MouseInputListener, IHostInterface {
private val canvas = BitmapScreenPanel()
val keyboardBuffer = ArrayDeque<Char>()
var mousePos = Point(0, 0)
var leftButton = false
var rightButton = false
var middleButton = false
init {
val borderWidth = 16
layout = GridBagLayout()
defaultCloseOperation = EXIT_ON_CLOSE
isResizable = false
isFocusable = true
// the borders (top, left, right, bottom)
val borderTop = JPanel().apply {
preferredSize = Dimension(
(ScreenDefs.DISPLAY_PIXEL_SCALING * (ScreenDefs.SCREEN_WIDTH + 2 * borderWidth)).toInt(),
(ScreenDefs.DISPLAY_PIXEL_SCALING * borderWidth).toInt()
)
background = ScreenDefs.BORDER_COLOR
}
val borderBottom = JPanel().apply {
preferredSize = Dimension(
(ScreenDefs.DISPLAY_PIXEL_SCALING * (ScreenDefs.SCREEN_WIDTH + 2 * borderWidth)).toInt(),
(ScreenDefs.DISPLAY_PIXEL_SCALING * borderWidth).toInt()
)
background = ScreenDefs.BORDER_COLOR
}
val borderLeft = JPanel().apply {
preferredSize = Dimension(
(ScreenDefs.DISPLAY_PIXEL_SCALING * borderWidth).toInt(),
(ScreenDefs.DISPLAY_PIXEL_SCALING * ScreenDefs.SCREEN_HEIGHT).toInt()
)
background = ScreenDefs.BORDER_COLOR
}
val borderRight = JPanel().apply {
preferredSize = Dimension(
(ScreenDefs.DISPLAY_PIXEL_SCALING * borderWidth).toInt(),
(ScreenDefs.DISPLAY_PIXEL_SCALING * ScreenDefs.SCREEN_HEIGHT).toInt()
)
background = ScreenDefs.BORDER_COLOR
}
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)
addMouseMotionListener(this)
addMouseListener(this)
pack()
requestFocusInWindow()
setLocationRelativeTo(null)
isVisible = true
}
fun start() {
// repaint the screen's back buffer ~60 times per second
val repaintTimer = Timer(1000 / 60) { repaint() }
repaintTimer.initialDelay = 0
repaintTimer.start()
}
// keyboard events:
override fun keyTyped(p0: KeyEvent) {
println(p0)
println("[${p0.keyChar}]")
keyboardBuffer.add(p0.keyChar)
while (keyboardBuffer.size > 8)
keyboardBuffer.pop()
}
override fun keyPressed(p0: KeyEvent) {}
override fun keyReleased(p0: KeyEvent) {}
// mouse events:
override fun mousePressed(p0: MouseEvent) {}
override fun mouseReleased(p0: MouseEvent) {}
override fun mouseEntered(p0: MouseEvent) {}
override fun mouseExited(p0: MouseEvent) {}
override fun mouseDragged(p0: MouseEvent) {}
override fun mouseClicked(p0: MouseEvent) {
val pos = canvas.mousePixelPosition()
if (pos == null)
return
else {
mousePos = pos
leftButton = SwingUtilities.isLeftMouseButton(p0)
rightButton = SwingUtilities.isRightMouseButton(p0)
middleButton = SwingUtilities.isMiddleMouseButton(p0)
}
}
override fun mouseMoved(p0: MouseEvent) {
val pos = canvas.mousePixelPosition()
if (pos == null)
return
else
mousePos = pos
}
// the overrides required for IHostDisplay:
override fun clearScreen() = canvas.clearScreen()
override fun setPixel(x: Int, y: Int) = canvas.setPixel(x, y, true)
override fun clearPixel(x: Int, y: Int) = canvas.setPixel(x, y, false)
override fun getPixel(x: Int, y: Int) = canvas.getPixel(x, y)
override fun setChar(x: Int, y: Int, character: Char) = canvas.setChar(x, y, character)
override fun scrollUp() = canvas.scrollUp()
override fun mouse(): IHostInterface.MouseInfo {
return IHostInterface.MouseInfo(mousePos.x, mousePos.y, leftButton, rightButton, middleButton)
}
override fun keyboard(): Char? {
return if (keyboardBuffer.isEmpty())
null
else
keyboardBuffer.pop()
}
}

View File

@ -0,0 +1,84 @@
package razorvine.examplemachine
import kotlin.concurrent.scheduleAtFixedRate
import java.time.LocalDateTime
import razorvine.ksim65.Bus
import razorvine.ksim65.Cpu6502
import razorvine.ksim65.Version
import razorvine.ksim65.components.*
import razorvine.ksim65.components.Timer
/**
* A virtual computer constructed from the various virtual components
*/
class VirtualMachine(title: String) {
val bus = Bus()
val cpu = Cpu6502(false)
val ram = Ram(0x0000, 0xffff)
private val rtc = RealTimeClock(0xd100, 0xd108)
private val timer = Timer(0xd200, 0xd203, cpu)
private val hostDisplay = MainWindow(title)
private val debugWindow = DebugWindow(this)
private val display = Display(0xd000, 0xd00a, hostDisplay,
ScreenDefs.SCREEN_WIDTH_CHARS, ScreenDefs.SCREEN_HEIGHT_CHARS,
ScreenDefs.SCREEN_WIDTH, ScreenDefs.SCREEN_HEIGHT)
private val mouse = Mouse(0xd300, 0xd304, hostDisplay)
private val keyboard = Keyboard(0xd400, 0xd400, hostDisplay)
init {
ram[Cpu6502.RESET_vector] = 0x00
ram[Cpu6502.RESET_vector + 1] = 0x10
bus += rtc
bus += timer
bus += display
bus += mouse
bus += keyboard
bus += ram
bus += cpu
bus.reset()
hostDisplay.start()
debugWindow.setLocation(hostDisplay.location.x+hostDisplay.width, hostDisplay.location.y)
debugWindow.isVisible = true
}
var paused = false
fun clock() {
if(!paused) {
bus.clock()
debugWindow.updateCpu(cpu)
}
}
}
fun main(args: Array<String>) {
val machine = VirtualMachine("KSim65 demo virtual machine - using ksim65 v${Version.version}")
val v = 0xd000
machine.bus[v + 0x08] = 20
machine.bus[v + 0x09] = 2
val text = ">> Hello this is an example text! 1234567890 <<\n" +
"next line 1\n" +
"next line 2\n" +
"next line 3\rnext line 4\rnext line 5\n" +
"a mistakk\be\n\n\n\n\n\n\n\n\n\n"
text.forEach {
machine.bus[v + 0x0a] = it.toShort()
}
repeat(20) {
Thread.sleep(100)
"time: ${LocalDateTime.now()}\n".forEach { c ->
machine.bus[v + 0x0a] = c.toShort()
}
}
val timer = java.util.Timer("clock", true)
timer.scheduleAtFixedRate(1, 1) {
machine.clock()
}
}

View File

@ -27,6 +27,12 @@ class Bus {
memComponents.forEach { it.clock() }
}
operator fun plusAssign(memcomponent: MemMappedComponent) = add(memcomponent)
operator fun plusAssign(component: BusComponent) = add(component)
operator fun get(address: Address): UByte = read(address)
operator fun set(address: Address, data: UByte) = write(address, data)
fun add(component: BusComponent) {
components.add(component)
component.bus = this
@ -54,6 +60,7 @@ class Bus {
* Any memory mapped component that listens to the address, will receive the data.
*/
fun write(address: Address, data: UByte) {
require(data in 0..255) { "data must be a byte 0..255" }
memComponents.forEach {
if (address >= it.startAddress && address <= it.endAddress)
it[address] = data

View File

@ -11,6 +11,7 @@ import razorvine.ksim65.components.UByte
* TODO: add the optional additional cycles to certain instructions and addressing modes
*/
open class Cpu6502(private val stopOnBrk: Boolean = false) : BusComponent() {
open val name = "6502"
var tracing: ((state:String) -> Unit)? = null
var totalCycles: Long = 0
protected set
@ -102,6 +103,8 @@ open class Cpu6502(private val stopOnBrk: Boolean = false) : BusComponent() {
protected set
protected lateinit var currentInstruction: Instruction
val currentMnemonic: String
get() = currentInstruction.mnemonic
// has an interrupt been requested?
protected var pendingInterrupt: Pair<Boolean, BusComponent>? = null
@ -249,6 +252,9 @@ open class Cpu6502(private val stopOnBrk: Boolean = false) : BusComponent() {
return result
}
/**
* Reset the cpu
*/
override fun reset() {
regSP = 0xfd
regPC = readWord(RESET_vector)
@ -267,6 +273,9 @@ open class Cpu6502(private val stopOnBrk: Boolean = false) : BusComponent() {
currentInstruction = instructions[0]
}
/**
* Process once clock cycle in the cpu
*/
override fun clock() {
if (instrCycles == 0) {
if (pendingInterrupt != null) {
@ -309,11 +318,13 @@ open class Cpu6502(private val stopOnBrk: Boolean = false) : BusComponent() {
totalCycles++
}
/**
* Execute one single complete instruction
*/
open fun step() {
// step a whole instruction
while (instrCycles > 0) clock() // remaining instruction subcycles from the previous instruction
clock() // the actual instruction execution cycle
while (instrCycles > 0) clock() // instruction subcycles
while (instrCycles > 0) clock()
clock()
while (instrCycles > 0) clock()
}
fun nmi(source: BusComponent) {

View File

@ -7,6 +7,7 @@ import razorvine.ksim65.components.Address
* TODO: add the optional additional cycles to certain instructions and addressing modes
*/
class Cpu65C02(stopOnBrk: Boolean = false) : Cpu6502(stopOnBrk) {
override val name = "65C02"
enum class Wait {
Normal,
@ -23,6 +24,9 @@ class Cpu65C02(stopOnBrk: Boolean = false) : Cpu6502(stopOnBrk) {
const val resetCycles = Cpu6502.resetCycles
}
/**
* Process once clock cycle in the cpu
*/
override fun clock() {
when (waiting) {
Wait.Normal -> super.clock()
@ -42,13 +46,15 @@ class Cpu65C02(stopOnBrk: Boolean = false) : Cpu6502(stopOnBrk) {
}
}
/**
* Execute one single complete instruction
*/
override fun step() {
// step a whole instruction
if (waiting == Wait.Normal) {
while (instrCycles > 0) clock() // remaining instruction subcycles from the previous instruction
clock() // the actual instruction execution cycle
while (instrCycles > 0) clock()
clock()
if (waiting == Wait.Normal)
while (instrCycles > 0) clock() // instruction subcycles
while (instrCycles > 0) clock()
else {
totalCycles += instrCycles
instrCycles = 0

View File

@ -0,0 +1,18 @@
package razorvine.ksim65
/**
* The virtual machine uses this to interface with the host system,
* to connect to the "real" devices.
*/
interface IHostInterface {
fun clearScreen()
fun getPixel(x: Int, y: Int): Boolean
fun setPixel(x: Int, y: Int)
fun clearPixel(x: Int, y: Int)
fun setChar(x: Int, y: Int, character: Char)
fun scrollUp()
fun mouse(): MouseInfo
fun keyboard(): Char?
class MouseInfo(val x: Int, val y: Int, val left: Boolean, val right: Boolean, val middle: Boolean)
}

View File

@ -12,7 +12,14 @@ typealias Address = Int
abstract class BusComponent {
lateinit var bus: Bus
/**
* One clock cycle on the bus
*/
abstract fun clock()
/**
* Reset all devices on the bus
*/
abstract fun reset()
}
@ -53,4 +60,8 @@ abstract class MemMappedComponent(val startAddress: Address, val endAddress: Add
abstract class MemoryComponent(startAddress: Address, endAddress: Address) :
MemMappedComponent(startAddress, endAddress) {
abstract fun copyOfMem(): Array<UByte>
init {
require(startAddress and 0xff == 0 && endAddress and 0xff == 0xff) {"address range must span complete page(s)"}
}
}

View File

@ -0,0 +1,167 @@
package razorvine.ksim65.components
import razorvine.ksim65.IHostInterface
import kotlin.math.min
/**
* Text mode and graphics (bitmap) mode display.
* Note that the character matrix and pixel matrix are NOT memory mapped,
* this display device is controlled by sending char/pixel commands to it.
*
* Requires a host display to actually view the stuff, obviously.
*
* reg. value
* ---- -----
* 00 char X position
* 01 char Y position
* 02 r/w character at cX,cY (doesn't change cursor position)
* 03 pixel X pos (lsb)
* 04 pixel X pos (msb)
* 05 pixel Y pos (lsb)
* 06 pixel Y pos (msb)
* 07 read or write pixel value at pX, pY
* 08 cursor X position (r/w)
* 09 cursor Y position (r/w)
* 0a read or write character at cursor pos, updates cursor position, scrolls up if necessary
* control characters: 0x08=backspace, 0x09=tab, 0x0a=newline, 0x0c=formfeed(clear screen), 0x0d=carriagereturn
*
* TODO: cursor blinking, blink speed (0=off)
*/
class Display(
startAddress: Address, endAddress: Address,
private val host: IHostInterface,
private val charWidth: Int,
private val charHeight: Int,
private val pixelWidth: Int,
private val pixelHeight: Int
) : MemMappedComponent(startAddress, endAddress) {
init {
require(endAddress - startAddress + 1 == 11) { "display needs exactly 11 memory bytes" }
}
private var cursorX = 0
private var cursorY = 0
private var charposX = 0
private var charposY = 0
private var pixelX = 0
private var pixelY = 0
private val charMatrix = Array<ShortArray>(charHeight) { ShortArray(charWidth) } // matrix[y][x] to access
override fun clock() {
// if the system clock is synced to the display refresh,
// you *could* add a Vertical Blank interrupt here.
}
override fun reset() {
charMatrix.forEach { it.fill(' '.toShort()) }
cursorX = 0
cursorY = 0
charposX = 0
charposY = 0
pixelX = 0
pixelY = 0
host.clearScreen()
}
override operator fun get(address: Address): UByte {
return when(address-startAddress) {
0x02 -> {
if(charposY in 0 until charHeight && charposX in 0 until charWidth) {
charMatrix[charposY][charposX]
} else 0xff
}
0x07 -> if(host.getPixel(pixelX, pixelY)) 1 else 0
0x08 -> cursorX.toShort()
0x09 -> cursorY.toShort()
0x0a -> {
if(cursorY in 0 until charHeight && cursorX in 0 until charWidth) {
charMatrix[cursorY][cursorX]
} else 0xff
}
else -> return 0xff
}
}
override operator fun set(address: Address, data: UByte) {
when(address-startAddress) {
0x00 -> charposX = data.toInt()
0x01 -> charposY = data.toInt()
0x02 -> {
if(charposY in 0 until charHeight && charposX in 0 until charWidth) {
charMatrix[charposY][charposX] = data
host.setChar(charposX, charposY, data.toChar())
}
}
0x03 -> pixelX = (pixelX and 0xff00) or data.toInt()
0x04 -> pixelX = (pixelX and 0x00ff) or (data.toInt() shl 8)
0x05 -> pixelY = (pixelY and 0xff00) or data.toInt()
0x06 -> pixelY = (pixelY and 0x00ff) or (data.toInt() shl 8)
0x07 -> {
if(data==0.toShort()) host.clearPixel(pixelX, pixelY)
else host.setPixel(pixelX, pixelY)
}
0x08 -> cursorX = min(data.toInt() and 65535, charWidth-1)
0x09 -> cursorY = min(data.toInt() and 65535, charHeight-1)
0x0a -> {
if(cursorY in 0 until charHeight && cursorX in 0 until charWidth) {
when(data.toInt()) {
0x08 -> {
// backspace
cursorX--
if(cursorX<0) {
if(cursorY>0) {
cursorY--
cursorX = charWidth - 1
}
}
charMatrix[cursorY][cursorX] = ' '.toShort()
host.setChar(cursorX, cursorY, ' ')
}
0x09 -> {
// tab
cursorX = (cursorX and 248) + 8
if(cursorX >= charWidth) {
cursorX = 0
cursorDown()
}
}
0x0a -> {
// newline
cursorX = 0
cursorDown()
}
0x0c -> reset() // clear screen
0x0d -> cursorX = 0 // carriage return
else -> {
// set character on screen
charMatrix[cursorY][cursorX] = data
host.setChar(cursorX, cursorY, data.toChar())
cursorX++
if(cursorX >= charWidth) {
cursorX = 0
cursorDown()
}
}
}
}
}
}
}
private fun cursorDown() {
cursorY++
while(cursorY >= charHeight) {
// scroll up 1 line
for(y in 0 .. charHeight-2) {
charMatrix[y+1].copyInto(charMatrix[y])
}
for(x in 0 until charWidth) {
charMatrix[charHeight-1][x] = ' '.toShort()
}
cursorY--
host.scrollUp()
}
}
}

View File

@ -0,0 +1,34 @@
package razorvine.ksim65.components
import razorvine.ksim65.IHostInterface
/**
* Simple keyboard for text entry.
* The keyboard device itself takes care of decoding the keys,
* this device simply produces the actual keys pressed.
* There's NO support right now to detect keydown/keyup events or the
* state of the shift/control/function keys.
*
* reg. value
* ---- ---------
* 00 character from keyboard, 0 = no character/key pressed
*/
class Keyboard(startAddress: Address, endAddress: Address, private val host: IHostInterface) :
MemMappedComponent(startAddress, endAddress) {
init {
require(endAddress - startAddress + 1 == 1) { "keyboard needs exactly 1 memory byte" }
}
override fun clock() {}
override fun reset() {}
override operator fun get(address: Address): UByte {
return when(address-startAddress) {
0x00 -> host.keyboard()?.toShort() ?: 0
else -> 0xff
}
}
override operator fun set(address: Address, data: UByte) { /* read-only device */ }
}

View File

@ -0,0 +1,44 @@
package razorvine.ksim65.components
import razorvine.ksim65.IHostInterface
/**
* An analog mouse or paddle input device, with 2 buttons.
*
* reg. value
* ---- ---------
* 00 mouse pixel pos X (lsb)
* 01 mouse pixel pos X (msb)
* 02 mouse pixel pos Y (lsb)
* 03 mouse pixel pos Y (msb)
* 04 buttons, bit 0 = left button, bit 1 = right button, bit 2 = middle button
*/
class Mouse(startAddress: Address, endAddress: Address, private val host: IHostInterface) :
MemMappedComponent(startAddress, endAddress) {
init {
require(endAddress - startAddress + 1 == 5) { "mouse needs exactly 5 memory bytes" }
}
override fun clock() {}
override fun reset() {}
override operator fun get(address: Address): UByte {
val mouse = host.mouse()
return when (address - startAddress) {
0x00 -> (mouse.x and 0xff).toShort()
0x01 -> (mouse.x ushr 8).toShort()
0x02 -> (mouse.y and 0xff).toShort()
0x03 -> (mouse.y ushr 8).toShort()
0x04 -> {
val b1 = if (mouse.left) 0b00000001 else 0
val b2 = if (mouse.right) 0b00000010 else 0
val b3 = if (mouse.middle) 0b00000100 else 0
return (b1 or b2 or b3).toShort()
}
else -> 0xff
}
}
override operator fun set(address: Address, data: UByte) { /* read-only device */ }
}

View File

@ -3,7 +3,7 @@ package razorvine.ksim65.components
/**
* A simple parallel output device (basically, prints bytes as characters to the console)
*
* byte value
* reg. value
* ---- ---------
* 00 data (the 8 parallel bits)
* 01 control latch (set bit 0 to write the data byte)
@ -22,7 +22,7 @@ class ParallelPort(startAddress: Address, endAddress: Address) : MemMappedCompon
return if (address == startAddress)
dataByte
else
0
0xff
}
override operator fun set(address: Address, data: UByte) {

View File

@ -8,7 +8,7 @@ import java.time.LocalTime
* A real-time time of day clock.
* (System timers are elsewhere)
*
* byte value
* reg. value
* ---- ----------
* 00 year (lsb)
* 01 year (msb)
@ -36,28 +36,28 @@ class RealTimeClock(startAddress: Address, endAddress: Address) : MemMappedCompo
override operator fun get(address: Address): UByte {
return when (address - startAddress) {
0 -> {
0x00 -> {
val year = LocalDate.now().year
(year and 255).toShort()
}
1 -> {
0x01 -> {
val year = LocalDate.now().year
(year ushr 8).toShort()
}
2 -> LocalDate.now().monthValue.toShort()
3 -> LocalDate.now().dayOfMonth.toShort()
4 -> LocalTime.now().hour.toShort()
5 -> LocalTime.now().minute.toShort()
6 -> LocalTime.now().second.toShort()
7 -> {
0x02 -> LocalDate.now().monthValue.toShort()
0x03 -> LocalDate.now().dayOfMonth.toShort()
0x04 -> LocalTime.now().hour.toShort()
0x05 -> LocalTime.now().minute.toShort()
0x06 -> LocalTime.now().second.toShort()
0x07 -> {
val ms = LocalTime.now().nano / 1000
(ms and 255).toShort()
}
8 -> {
0x08 -> {
val ms = LocalTime.now().nano / 1000
(ms ushr 8).toShort()
}
else -> 0
else -> 0xff
}
}

View File

@ -3,15 +3,14 @@ package razorvine.ksim65.components
import razorvine.ksim65.Cpu6502
/**
* A programmable timer. Causes an IRQ or NMI at specified 24-bits intervals.
* A programmable timer. Causes an IRQ or NMI at specified 24-bits clock cycle intervals.
*
* byte value
* reg. value
* ---- --------------
* 00 control register bit 0=enable bit 1=nmi (instead of irq)
* 01 24 bits interval value, bits 0-7 (lo)
* 02 24 bits interval value, bits 8-15 (mid)
* 03 24 bits interval value, bits 16-23 (hi)
*
*/
class Timer(startAddress: Address, endAddress: Address, val cpu: Cpu6502) : MemMappedComponent(startAddress, endAddress) {
private var counter: Int = 0
@ -51,40 +50,34 @@ class Timer(startAddress: Address, endAddress: Address, val cpu: Cpu6502) : MemM
}
override operator fun get(address: Address): UByte {
when (address - startAddress) {
0 -> {
return when (address - startAddress) {
0x00 -> {
var data = 0
if (enabled) data = data or 0b00000001
if (nmi) data = data or 0b00000010
return data.toShort()
data.toShort()
}
1 -> {
return (counter and 0xff).toShort()
}
2 -> {
return ((counter ushr 8) and 0xff).toShort()
}
3 -> {
return ((counter ushr 16) and 0xff).toShort()
}
else -> return 0
0x01 -> (counter and 0xff).toShort()
0x02 -> ((counter ushr 8) and 0xff).toShort()
0x03 -> ((counter ushr 16) and 0xff).toShort()
else -> 0xff
}
}
override operator fun set(address: Address, data: UByte) {
when (address - startAddress) {
0 -> {
0x00 -> {
val i = data.toInt()
enabled = (i and 0b00000001) != 0
nmi = (i and 0b00000010) != 0
}
1 -> {
0x01 -> {
interval = (interval and 0x7fffff00) or data.toInt()
}
2 -> {
0x02 -> {
interval = (interval and 0x7fff00ff) or (data.toInt() shl 8)
}
3 -> {
0x03 -> {
interval = (interval and 0x7f00ffff) or (data.toInt() shl 16)
}
}

View File

@ -1,75 +0,0 @@
package testmain
import razorvine.ksim65.Bus
import razorvine.ksim65.Cpu6502
import razorvine.ksim65.Version
import razorvine.ksim65.components.*
fun main(args: Array<String>) {
println(Version.copyright)
startSimulator(args)
}
private fun startSimulator(args: Array<String>) {
// create a computer system.
// note that the order in which components are added to the bus, is important:
// it determines the priority of reads and writes.
val cpu = Cpu6502(true)
val ram = Ram(0, 0xffff)
ram[Cpu6502.RESET_vector] = 0x00
ram[Cpu6502.RESET_vector + 1] = 0x10
ram[Cpu6502.IRQ_vector] = 0x00
ram[Cpu6502.IRQ_vector + 1] = 0x20
ram[Cpu6502.NMI_vector] = 0x00
ram[Cpu6502.NMI_vector + 1] = 0x30
// // read the RTC and write the date+time to $2000
// for(b in listOf(0xa0, 0x00, 0xb9, 0x00, 0xd1, 0x99, 0x00, 0x20, 0xc8, 0xc0, 0x09, 0xd0, 0xf5, 0x00).withIndex()) {
// ram[0x1000+b.index] = b.value.toShort()
// }
// set the timer to $22aa00 and enable it on regular irq
for(b in listOf(0xa9, 0x00, 0x8d, 0x00, 0xd2, 0xa9, 0x00, 0x8d, 0x01, 0xd2, 0xa9, 0xaa, 0x8d, 0x02,
0xd2, 0xa9, 0x22, 0x8d, 0x03, 0xd2, 0xa9, 0x01, 0x8d, 0x00, 0xd2, 0x4c, 0x19, 0x10).withIndex()) {
ram[0x1000+b.index] = b.value.toShort()
}
// load the irq routine that prints 'irq!' to the parallel port
for(b in listOf(0x48, 0xa9, 0x09, 0x8d, 0x00, 0xd0, 0xee, 0x01, 0xd0, 0xa9, 0x12, 0x8d, 0x00, 0xd0,
0xee, 0x01, 0xd0, 0xa9, 0x11, 0x8d, 0x00, 0xd0, 0xee, 0x01, 0xd0, 0xa9, 0x21, 0x8d, 0x00, 0xd0,
0xee, 0x01, 0xd0, 0x68, 0x40).withIndex()) {
ram[0x2000+b.index] = b.value.toShort()
}
val parallel = ParallelPort(0xd000, 0xd001)
val clock = RealTimeClock(0xd100, 0xd108)
val timer = Timer(0xd200, 0xd203, cpu)
val bus = Bus()
bus.add(cpu)
bus.add(parallel)
bus.add(clock)
bus.add(timer)
bus.add(ram)
bus.reset()
cpu.regP.I = false // enable interrupts
// TODO
// try {
// while (true) {
// bus.clock()
// }
// } catch (ix: Cpu6502.InstructionError) {
// println("Hmmm... $ix")
// }
ram.hexDump(0x1000, 0x1020)
val dis = cpu.disassemble(ram, 0x1000, 0x1020)
println(dis.joinToString("\n"))
ram.hexDump(0x2000, 0x2008)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -48,7 +48,7 @@ class Test6502CpuBasics {
@Test
fun testCpuPerformance6502() {
val cpu = Cpu6502(true)
val ram = Ram(0x1000, 0x2000)
val ram = Ram(0x1000, 0x1fff)
// load a simple program that loops a few instructions
for(b in listOf(0xa9, 0x63, 0xaa, 0x86, 0x22, 0x8e, 0x22, 0x22, 0x91, 0x22, 0x6d, 0x33, 0x33, 0xcd, 0x55, 0x55, 0xd0, 0xee, 0xf0, 0xec).withIndex()) {
ram[0x1000+b.index] = b.value.toShort()
@ -79,7 +79,7 @@ class Test6502CpuBasics {
@Test
fun testCpuPerformance65C02() {
val cpu = Cpu65C02(true)
val ram = Ram(0x0000, 0x2000)
val ram = Ram(0x0000, 0x1fff)
// load a simple program that loops a few instructions
for(b in listOf(0xa9, 0x63, 0xaa, 0x86, 0x22, 0x8e, 0x22, 0x22, 0x91, 0x22, 0x6d, 0x33, 0x33, 0xcd, 0x55, 0x55,
0xff, 0xff, 0x79, 0x9e, 0x56, 0x34, 0xd0, 0xe8, 0xf0, 0xe6).withIndex()) {

View File

@ -28,7 +28,7 @@ class TestDisassembler {
@Test
fun testDisassembleRockwell65C02() {
val cpu = Cpu65C02()
val memory = Ram(0, 0x1000)
val memory = Ram(0, 0x0fff)
val source = javaClass.classLoader.getResource("disassem_r65c02.bin")!!
memory.load(source, 0x0200)
val resultLines = cpu.disassemble(memory, 0x0200, 0x0250)
@ -73,7 +73,7 @@ ${'$'}0250 00 brk""", result)
@Test
fun testDisassembleWDC65C02() {
val cpu = Cpu65C02()
val memory = Ram(0, 0x1000)
val memory = Ram(0, 0x0fff)
val source = javaClass.classLoader.getResource("disassem_wdc65c02.bin")!!
memory.load(source, 0x200)
val resultLines = cpu.disassemble(memory, 0x0200, 0x0215)