ksim65/src/main/kotlin/razorvine/c64emu/Cia.kt

416 lines
18 KiB
Kotlin

package razorvine.c64emu
import razorvine.ksim65.Cpu6502
import razorvine.ksim65.components.Address
import razorvine.ksim65.components.MemMappedComponent
import razorvine.ksim65.components.UByte
import java.awt.event.KeyEvent
/**
* Minimal simulation of the MOS 6526 CIA chip.
* Depending on what CIA it is (1 or 2), some registers do different things on the C64.
* This implementation provides a working keyboard matrix, joystick in port#2 (cia 1),
* time of day clock, and the essentials of the timer A and B.
*/
class Cia(val number: Int, startAddress: Address, endAddress: Address, val cpu: Cpu6502) : MemMappedComponent(startAddress, endAddress) {
private var ramBuffer = Array<UByte>(endAddress-startAddress+1) { 0 }
private var regPRA = 0xff
// joystick in port 2 configuration (only works on cia#1)
private var joy2up = false
private var joy2down = false
private var joy2left = false
private var joy2right = false
private var joy2fire = false
class TimeOfDay {
private var updatedAt = 0L
private var startedAt = 0L
private var userStartTime = 0.0
private var running = false
var hours = 0
var minutes = 0
var seconds = 0
var tenths = 0
private var latchedTime = 0L
private var latched = false
fun start() {
if (!running) {
updatedAt = System.currentTimeMillis()
startedAt = updatedAt
userStartTime = hours*3600+minutes*60+seconds+tenths/10.0
running = true
}
latch(false)
}
fun stop() {
running = false
}
fun latch(latched: Boolean) {
this.latched = latched
update()
}
fun update() {
if (!running || latched) return
latchedTime = System.currentTimeMillis()
if (updatedAt != latchedTime) {
updatedAt = latchedTime
var elapsedSeconds = (latchedTime.toDouble()-startedAt)/1000.0+userStartTime
if(elapsedSeconds>60*3600)
elapsedSeconds=0.0 // TOD counds max 60 hours
hours = (elapsedSeconds/3600).toInt()
elapsedSeconds -= hours*3600
minutes = (elapsedSeconds/60).toInt()
elapsedSeconds -= minutes*60
seconds = (elapsedSeconds).toInt()
elapsedSeconds -= seconds
tenths = (elapsedSeconds*10).toInt()
}
}
}
private var totalCycles = 0
private var tod = TimeOfDay()
private var timerAset = 0
private var timerBset = 0
private var timerAactual = 0
private var timerBactual = 0
private var timerAinterruptEnabled = false
private var timerBinterruptEnabled = false
private data class HostKeyPress(val code: Int, val rightSide: Boolean = false, val numpad: Boolean = false)
private val hostKeyPresses = mutableSetOf<HostKeyPress>()
init {
require(endAddress-startAddress+1 == 256) { "cia requires exactly 256 memory bytes (16*16 mirrored)" }
}
override fun clock() {
totalCycles++
if (totalCycles%20000 == 0) {
// TOD resolution is 0.1 second, no need to update it in every bus cycle
tod.update()
}
if (ramBuffer[0x0e].toInt() and 1 != 0) {
// timer A is enabled, assume system cycles counting for now
timerAactual--
if (timerAactual == 0 && timerAinterruptEnabled) {
if (number == 1) cpu.irqAsserted = true
else if (number == 2) cpu.nmiAsserted = true
}
if (timerAactual < 0) timerAactual = if (ramBuffer[0x0e].toInt() and 0b00001000 != 0) 0 else timerAset
}
if (ramBuffer[0x0f].toInt() and 1 != 0) {
// timer B is enabled
val regCRB = ramBuffer[0x0f].toInt()
if (regCRB and 0b01000000 != 0) {
// timer B counts timer A underruns
if (timerAactual == 0) timerBactual--
} else {
// timer B counts just the system cycles
timerBactual--
}
if (timerBactual == 0 && timerBinterruptEnabled) {
if (number == 1) cpu.irqAsserted = true
else if (number == 2) cpu.nmiAsserted = true
}
if (timerBactual < 0) timerBactual = if (regCRB and 0b00001000 != 0) 0 else timerBset
}
}
override fun reset() {
hostKeyPresses.clear()
tod.stop()
ramBuffer.fill(0)
timerAactual = 0
timerAset = 0
timerBactual = 0
timerBset = 0
joy2up = false
joy2down = false
joy2left = false
joy2right = false
joy2fire = false
}
override operator fun get(offset: Int): UByte {
fun scanColumn(vararg keys: HostKeyPress): UByte {
var bits = 0b10000000
var presses = 0
for (key in keys) {
if (key in hostKeyPresses) presses = presses or bits
bits = bits ushr 1
}
return (presses.inv() and 255).toShort()
}
val register = offset and 15
if(number==1) {
// first CIA has keyboard matrix
if(register==0x00) {
// reading $dc00 is joystick in port #2
return (0b01100000
or (if(joy2up) 0 else 0b00000001)
or (if(joy2down) 0 else 0b00000010)
or (if(joy2left) 0 else 0b00000100)
or (if(joy2right) 0 else 0b00001000)
or (if(joy2fire) 0 else 0b00010000)).toShort()
} else if(register==0x01) {
// register 1 on CIA#1 is the keyboard data port (and joystick #1...)
// if bit is cleared in PRA, contains keys pressed in that column of the matrix
// NOTE: we do not emulate a joystick in port #1 as this conflicts with the keyboard.
// just use the joystick in port #2 for now...
return when (regPRA) {
0b00000000 -> {
// check if any keys are pressed at all (by checking all columns at once)
if (hostKeyPresses.isEmpty()) 0xff.toShort() else 0x00.toShort()
}
0b11111110 -> {
// read column 0
scanColumn(HostKeyPress(KeyEvent.VK_DOWN), HostKeyPress(KeyEvent.VK_F5), HostKeyPress(KeyEvent.VK_F3),
HostKeyPress(KeyEvent.VK_F1), HostKeyPress(KeyEvent.VK_F7), HostKeyPress(KeyEvent.VK_RIGHT),
HostKeyPress(KeyEvent.VK_ENTER), HostKeyPress(KeyEvent.VK_BACK_SPACE))
}
0b11111101 -> {
// read column 1
scanColumn(HostKeyPress(KeyEvent.VK_SHIFT), // left shift
HostKeyPress(KeyEvent.VK_E), HostKeyPress(KeyEvent.VK_S), HostKeyPress(KeyEvent.VK_Z),
HostKeyPress(KeyEvent.VK_4), HostKeyPress(KeyEvent.VK_A), HostKeyPress(KeyEvent.VK_W),
HostKeyPress(KeyEvent.VK_3))
}
0b11111011 -> {
// read column 2
scanColumn(HostKeyPress(KeyEvent.VK_X), HostKeyPress(KeyEvent.VK_T), HostKeyPress(KeyEvent.VK_F),
HostKeyPress(KeyEvent.VK_C), HostKeyPress(KeyEvent.VK_6), HostKeyPress(KeyEvent.VK_D),
HostKeyPress(KeyEvent.VK_R), HostKeyPress(KeyEvent.VK_5))
}
0b11110111 -> {
// read column 3
scanColumn(HostKeyPress(KeyEvent.VK_V), HostKeyPress(KeyEvent.VK_U), HostKeyPress(KeyEvent.VK_H),
HostKeyPress(KeyEvent.VK_B), HostKeyPress(KeyEvent.VK_8), HostKeyPress(KeyEvent.VK_G),
HostKeyPress(KeyEvent.VK_Y), HostKeyPress(KeyEvent.VK_7))
}
0b11101111 -> {
// read column 4
scanColumn(HostKeyPress(KeyEvent.VK_N), HostKeyPress(KeyEvent.VK_O), HostKeyPress(KeyEvent.VK_K),
HostKeyPress(KeyEvent.VK_M), HostKeyPress(KeyEvent.VK_0), HostKeyPress(KeyEvent.VK_J),
HostKeyPress(KeyEvent.VK_I), HostKeyPress(KeyEvent.VK_9))
}
0b11011111 -> {
// read column 5
scanColumn(HostKeyPress(KeyEvent.VK_COMMA), HostKeyPress(KeyEvent.VK_AT), HostKeyPress(KeyEvent.VK_COLON),
HostKeyPress(KeyEvent.VK_PERIOD), HostKeyPress(KeyEvent.VK_MINUS), HostKeyPress(KeyEvent.VK_L),
HostKeyPress(KeyEvent.VK_P), HostKeyPress(KeyEvent.VK_PLUS))
}
0b10111111 -> {
// read column 6
scanColumn(HostKeyPress(KeyEvent.VK_SLASH), HostKeyPress(KeyEvent.VK_CIRCUMFLEX), HostKeyPress(KeyEvent.VK_EQUALS),
HostKeyPress(KeyEvent.VK_SHIFT, rightSide = true), // right shift
HostKeyPress(KeyEvent.VK_HOME), HostKeyPress(KeyEvent.VK_SEMICOLON), HostKeyPress(KeyEvent.VK_ASTERISK),
HostKeyPress(KeyEvent.VK_DEAD_TILDE) // pound sign
)
}
0b01111111 -> {
// read column 7
scanColumn(HostKeyPress(KeyEvent.VK_ESCAPE), HostKeyPress(KeyEvent.VK_Q), HostKeyPress(KeyEvent.VK_ALT),
HostKeyPress(KeyEvent.VK_SPACE), HostKeyPress(KeyEvent.VK_2), HostKeyPress(KeyEvent.VK_CONTROL),
HostKeyPress(KeyEvent.VK_BACK_QUOTE), HostKeyPress(KeyEvent.VK_1))
}
else -> {
// invalid column selection
0xff
}
}
}
}
return when (register) {
0x04 -> (timerAactual and 0xff).toShort()
0x05 -> (timerAactual ushr 8).toShort()
0x06 -> (timerBactual and 0xff).toShort()
0x07 -> (timerBactual ushr 8).toShort()
0x08 -> {
tod.start()
(tod.tenths and 0x0f).toShort()
}
0x09 -> toBCD(tod.seconds)
0x0a -> toBCD(tod.minutes)
0x0b -> {
tod.latch(true)
toBCD(tod.hours)
}
else -> ramBuffer[register]
}
}
override operator fun set(offset: Int, data: UByte) {
val register = offset and 15
if (number == 1 && register == 0x00) {
// PRA data port A (select keyboard matrix column)
regPRA = data.toInt()
}
if (register != 0x0d) ramBuffer[register] = data
when (register) {
0x04 -> {
if (ramBuffer[0x0e].toInt() and 0b10000000 == 0) {
timerAset = (timerAset and 0xff00) or data.toInt()
timerAactual = timerAset
}
}
0x05 -> {
if (ramBuffer[0x0e].toInt() and 0b10000000 == 0) {
timerAset = (timerAset and 0x00ff) or (data.toInt() shl 8)
timerAactual = timerAset
}
}
0x06 -> {
if (ramBuffer[0x0f].toInt() and 0b10000000 == 0) {
timerBset = (timerBset and 0xff00) or data.toInt()
timerBactual = timerBset
}
}
0x07 -> {
if (ramBuffer[0x0f].toInt() and 0b10000000 == 0) {
timerBset = (timerBset and 0x00ff) or (data.toInt() shl 8)
timerBactual = timerBset
}
}
0x08 -> tod.tenths = data.toInt() and 0x0f
0x09 -> tod.seconds = fromBCD(data)
0x0a -> tod.minutes = fromBCD(data)
0x0b -> {
tod.stop()
tod.hours = fromBCD(data)
}
0x0d -> {
if (data.toInt() and 0b10000000 != 0) {
// set ICR bits
val newICR = ramBuffer[0x0d].toInt() or (data.toInt() and 0b01111111)
timerAinterruptEnabled = newICR and 1 != 0
timerBinterruptEnabled = newICR and 2 != 0
ramBuffer[0x0d] = newICR.toShort()
} else {
// clear ICR bits
val newICR = ramBuffer[0x0d].toInt() and (data.toInt() and 0b01111111).inv()
timerAinterruptEnabled = newICR and 1 != 0
timerBinterruptEnabled = newICR and 2 != 0
ramBuffer[0x0d] = newICR.toShort()
}
}
}
}
private fun toBCD(data: Int): UByte {
val tens = data/10
val ones = data-tens*10
return ((tens shl 4) or ones).toShort()
}
private fun fromBCD(bcd: UByte): Int {
val ibcd = bcd.toInt()
val tens = ibcd ushr 4
val ones = ibcd and 0x0f
return tens*10+ones
}
fun hostKeyPressed(event: KeyEvent) {
val rightSide = event.keyLocation == KeyEvent.KEY_LOCATION_RIGHT
val numpad = event.keyLocation == KeyEvent.KEY_LOCATION_NUMPAD
val shift = HostKeyPress(KeyEvent.VK_SHIFT, rightSide = true, numpad = false)
fun register(eventId: Int, vararg keys: HostKeyPress) {
if (eventId == KeyEvent.KEY_PRESSED) keys.forEach { hostKeyPresses.add(it) }
else keys.forEach { hostKeyPresses.remove(it) }
}
fun unregister(keyCode: Int) {
hostKeyPresses.removeAll { it.code == keyCode }
}
// to avoid some 'stuck' keys, if we receive a shift/control/alt RELEASE, we wipe the keyboard buffer
// (this can happen because we're changing the key code for some pressed keys below,
// and a released key doesn't always match the pressed key code anymore then)
if (event.id == KeyEvent.KEY_RELEASED && event.keyCode in listOf(KeyEvent.VK_SHIFT, KeyEvent.VK_CONTROL, KeyEvent.VK_ALT,
KeyEvent.VK_ALT_GRAPH)
) hostKeyPresses.clear()
// try to remap the keys a bit so a modern PC keyboard maps better to the keys of the C64
when (event.keyChar) {
'@' -> {
unregister(KeyEvent.VK_SHIFT)
register(event.id, HostKeyPress(KeyEvent.VK_AT))
}
'^' -> {
unregister(KeyEvent.VK_SHIFT)
register(event.id, HostKeyPress(KeyEvent.VK_CIRCUMFLEX))
}
'*' -> {
unregister(KeyEvent.VK_SHIFT)
register(event.id, HostKeyPress(KeyEvent.VK_ASTERISK))
}
'+' -> {
unregister(KeyEvent.VK_SHIFT)
register(event.id, HostKeyPress(KeyEvent.VK_PLUS))
}
':' -> {
unregister(KeyEvent.VK_SHIFT)
register(event.id, HostKeyPress(KeyEvent.VK_COLON))
}
'~' -> {
unregister(KeyEvent.VK_SHIFT)
register(event.id, HostKeyPress(KeyEvent.VK_DEAD_TILDE))
}
'&' -> register(event.id, shift, HostKeyPress(KeyEvent.VK_6))
'(' -> register(event.id, shift, HostKeyPress(KeyEvent.VK_8))
')' -> register(event.id, shift, HostKeyPress(KeyEvent.VK_9))
'[' -> register(event.id, shift, HostKeyPress(KeyEvent.VK_COLON))
']' -> register(event.id, shift, HostKeyPress(KeyEvent.VK_SEMICOLON))
'"' -> register(event.id, HostKeyPress(KeyEvent.VK_2))
'\'' -> register(event.id, shift, HostKeyPress(KeyEvent.VK_7))
else -> // F2 = shift+F1
// F4 = shift+F3
// F6 = shift+F5
// F8 = shift+F7
// insert = shift+backspace(del)
// up = shift+down
// left = shift+right
// just map the key as usual
// both alts map to the 'commodore key' (same left alt)
// both controls and the tab key map to the 'single left control'
when (event.keyCode) {
KeyEvent.VK_CONTROL, KeyEvent.VK_TAB -> register(event.id, HostKeyPress(KeyEvent.VK_CONTROL))
KeyEvent.VK_ALT, KeyEvent.VK_ALT_GRAPH -> register(event.id, HostKeyPress(KeyEvent.VK_ALT))
KeyEvent.VK_F2 -> register(event.id, shift, HostKeyPress(KeyEvent.VK_F1))
KeyEvent.VK_F4 -> register(event.id, shift, HostKeyPress(KeyEvent.VK_F3))
KeyEvent.VK_F6 -> register(event.id, shift, HostKeyPress(KeyEvent.VK_F5))
KeyEvent.VK_F8 -> register(event.id, shift, HostKeyPress(KeyEvent.VK_F7))
KeyEvent.VK_INSERT -> register(event.id, shift, HostKeyPress(KeyEvent.VK_BACK_SPACE))
KeyEvent.VK_UP -> register(event.id, shift, HostKeyPress(KeyEvent.VK_DOWN))
KeyEvent.VK_LEFT -> register(event.id, shift, HostKeyPress(KeyEvent.VK_RIGHT))
else -> register(event.id, HostKeyPress(event.keyCode, rightSide, numpad))
}
}
}
fun setJoystick2(up: Boolean, down: Boolean, left: Boolean, right: Boolean, fire: Boolean) {
if(number!=1)
throw NotImplementedError("joystick port 2 is connected to cia#1")
joy2up = up
joy2down = down
joy2left = left
joy2right = right
joy2fire = fire
}
}