mirror of
https://github.com/badvision/jace.git
synced 2024-06-10 07:29:30 +00:00
330 lines
11 KiB
Java
330 lines
11 KiB
Java
/*
|
|
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2.1 of the License, or (at your option) any later version.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this library; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
* MA 02110-1301 USA
|
|
*/
|
|
package jace.hardware;
|
|
|
|
import jace.EmulatorUILogic;
|
|
import jace.apple2e.MOS65C02;
|
|
import jace.config.ConfigurableField;
|
|
import jace.config.Name;
|
|
import jace.core.Card;
|
|
import jace.core.Computer;
|
|
import jace.core.Motherboard;
|
|
import jace.core.PagedMemory;
|
|
import jace.core.RAMEvent;
|
|
import jace.core.RAMEvent.TYPE;
|
|
import jace.core.Utility;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.util.Calendar;
|
|
import java.util.Stack;
|
|
import java.util.logging.Level;
|
|
import java.util.logging.Logger;
|
|
import javafx.scene.control.Label;
|
|
|
|
/**
|
|
* Implementation of the Thunderclock Plus with some limitations:
|
|
*
|
|
* The apple cannot set time. The firmware will act like it is working but
|
|
* nothing will actually happen when a time set command is sent.
|
|
*
|
|
* Though the interrupt features are implemented, they have not been tested.
|
|
*
|
|
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
|
|
*/
|
|
@Name("ThunderClock Plus")
|
|
public class CardThunderclock extends Card {
|
|
|
|
Label clockIcon;
|
|
Label clockFixIcon;
|
|
long lastShownIcon = -1;
|
|
// Only mention that the clock is read if it hasn't been checked for over 30 seconds
|
|
// This is to avoid showing it all the time in programs that poll it constantly
|
|
long MIN_WAIT = 30000;
|
|
@ConfigurableField(category = "OS", name = "Patch Prodos Year", description = "If enabled, the Prodos clock driver will be patched to use the current year.")
|
|
public boolean attemptYearPatch = true;
|
|
|
|
public CardThunderclock(Computer computer) {
|
|
super(computer);
|
|
try {
|
|
loadRom("jace/data/thunderclock_plus.rom");
|
|
} catch (IOException ex) {
|
|
Logger.getLogger(CardDiskII.class.getName()).log(Level.SEVERE, null, ex);
|
|
}
|
|
clockIcon = Utility.loadIconLabel("clock.png");
|
|
}
|
|
|
|
// Raw format: 40 bits, in BCD form (it actually streams out in the reverse order of this, bit 0 first)
|
|
// The data format is fully elaborated in the datasheet of the calendar/clock chip: NEC uPD1990AC
|
|
// month (1-12) -- hex
|
|
// day of week (0-6)
|
|
// day of month, tens digit (0-3)
|
|
// day of month, ones digit (0-9)
|
|
// hour, tens digit (0-2)
|
|
// hour, ones digit (0-9)
|
|
// minute, tens digit (0-5)
|
|
// minute, ones digit (0-9)
|
|
// second, tens digit (0-5)
|
|
// second, ones digit (0-9)
|
|
@Override
|
|
public void reset() {
|
|
irqAsserted = false;
|
|
irqEnabled = false;
|
|
ticks = 0;
|
|
timerRate = 0;
|
|
}
|
|
public boolean strobe = false;
|
|
public boolean clock = false;
|
|
public boolean shiftMode = false;
|
|
public boolean irqEnabled = false;
|
|
public boolean irqAsserted = false;
|
|
public boolean timerEnabled = false;
|
|
public int timerRate = 0;
|
|
|
|
@Override
|
|
protected void handleIOAccess(int register, TYPE type, int value, RAMEvent e) {
|
|
// Data is read via bit-banging the status register
|
|
// Nibbles are sent lowest significant bit first.
|
|
// Commands are sent to the register followed by a strobe pulse on bit 2 on and off
|
|
// So senting the time read command would be a string of bytes: 0x018, 0x01c and then 0x018 again
|
|
//
|
|
// Time read is signaled by 0x018 followed by a register shift command 0x08
|
|
// When register shift is active, a clock signal is used to move to the next bit.
|
|
//
|
|
// A bit is placed in data-in (bit 0)
|
|
// Then the clock is raised (bit 1 set) and then lowered (bit 1 unset)
|
|
// After this, the next time the register is read it will have the next bit
|
|
// of the register in the hibit (bit 7)
|
|
//
|
|
// Reg 0: Command register
|
|
// data in = 0x01
|
|
// clock = 0x02
|
|
// strobe = 0x04
|
|
// register hold = 0x0
|
|
// register shift = 0x08
|
|
// time set = 0x010
|
|
// time read = 0x018
|
|
// Timer modes = 0x020 (64hz), 0x028 (256hz), 0x030 (2048hz)
|
|
// Interrupt enable = 0x040 (IRQ assert is read as 0x020 in the status register)
|
|
// data out = 0x080
|
|
if (type.isRead() && register == 0) {
|
|
e.setNewValue((peekBit()) | (irqAsserted ? 0x020 : 0));
|
|
return;
|
|
}
|
|
|
|
if (register == 8) {
|
|
irqAsserted = false;
|
|
return;
|
|
} else if (register != 0) {
|
|
return;
|
|
}
|
|
|
|
boolean isClock = (value & 0x02) != 0;
|
|
boolean isStrobe = (value & 0x04) != 0;
|
|
boolean isShift = (value & 0x08) != 0;
|
|
boolean isRead = (value & 0x18) != 0;
|
|
|
|
if (!isClock && clock) {
|
|
if (buffer != null) {
|
|
buffer.pop();
|
|
}
|
|
}
|
|
|
|
if (!isStrobe && strobe) {
|
|
shiftMode = isShift;
|
|
if (isRead) {
|
|
if (attemptYearPatch) {
|
|
performProdosPatch(computer);
|
|
}
|
|
getTime();
|
|
clockIcon.setText("Slot " + getSlot());
|
|
long now = System.currentTimeMillis();
|
|
if ((now - lastShownIcon) > MIN_WAIT) {
|
|
EmulatorUILogic.addIndicator(this, clockIcon, 3000);
|
|
}
|
|
lastShownIcon = now;
|
|
}
|
|
shiftMode = isShift;
|
|
}
|
|
|
|
timerEnabled = (value & 0x020) != 0;
|
|
ticks = 0;
|
|
if (timerEnabled) {
|
|
switch (value & 0x038) {
|
|
case 0x020:
|
|
timerRate = (int) (Motherboard.SPEED / 64);
|
|
break;
|
|
case 0x028:
|
|
timerRate = (int) (Motherboard.SPEED / 256);
|
|
break;
|
|
case 0x030:
|
|
timerRate = (int) (Motherboard.SPEED / 2048);
|
|
break;
|
|
default:
|
|
timerEnabled = false;
|
|
timerRate = 0;
|
|
}
|
|
} else {
|
|
timerRate = 0;
|
|
}
|
|
|
|
irqEnabled = (value & 0x040) != 0;
|
|
clock = isClock;
|
|
strobe = isStrobe;
|
|
}
|
|
|
|
@Override
|
|
protected void handleFirmwareAccess(int register, TYPE type, int value, RAMEvent e) {
|
|
// Firmware ROM is used -- only I/O port was needed for proper emulation
|
|
}
|
|
|
|
@Override
|
|
protected void handleC8FirmwareAccess(int register, TYPE type, int value, RAMEvent e) {
|
|
// C8 access is used to read the clock directly
|
|
}
|
|
|
|
@Override
|
|
protected String getDeviceName() {
|
|
return "Thunderclock Plus";
|
|
}
|
|
|
|
int ticks = 0;
|
|
@Override
|
|
public void tick() {
|
|
if (timerEnabled) {
|
|
ticks++;
|
|
if (ticks >= timerRate) {
|
|
ticks = 0;
|
|
irqAsserted = true;
|
|
if (irqEnabled) {
|
|
computer.getCpu().generateInterrupt();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void getTime() {
|
|
Calendar cal = Calendar.getInstance();
|
|
cal.setTimeInMillis(System.currentTimeMillis());
|
|
clearBuffer();
|
|
pushNibble(cal.get(Calendar.MONTH) + 1);
|
|
pushNibble(cal.get(Calendar.DAY_OF_WEEK) - Calendar.SUNDAY);
|
|
pushNibble(cal.get(Calendar.DAY_OF_MONTH) / 10);
|
|
pushNibble(cal.get(Calendar.DAY_OF_MONTH) % 10);
|
|
pushNibble(cal.get(Calendar.HOUR_OF_DAY) / 10);
|
|
pushNibble(cal.get(Calendar.HOUR_OF_DAY) % 10);
|
|
pushNibble(cal.get(Calendar.MINUTE) / 10);
|
|
pushNibble(cal.get(Calendar.MINUTE) % 10);
|
|
pushNibble(cal.get(Calendar.SECOND) / 10);
|
|
pushNibble(cal.get(Calendar.SECOND) % 10);
|
|
}
|
|
Stack<Boolean> buffer;
|
|
|
|
private void clearBuffer() {
|
|
if (buffer == null) {
|
|
buffer = new Stack<>();
|
|
} else {
|
|
buffer.clear();
|
|
}
|
|
}
|
|
|
|
private void pushNibble(int value) {
|
|
for (int i = 0; i < 4; i++) {
|
|
boolean val = (value & 8) != 0;
|
|
buffer.push(val);
|
|
value <<= 1;
|
|
}
|
|
}
|
|
|
|
private int peekBit() {
|
|
if (buffer == null || buffer.isEmpty()) {
|
|
return 0;
|
|
}
|
|
return buffer.peek() ? 0x080 : 0;
|
|
}
|
|
|
|
public void loadRom(String path) throws IOException {
|
|
InputStream romFile = CardThunderclock.class.getClassLoader().getResourceAsStream(path);
|
|
final int cxRomLength = 0x0100;
|
|
final int c8RomLength = 0x0700;
|
|
byte[] romxData = new byte[cxRomLength];
|
|
byte[] rom8Data = new byte[c8RomLength];
|
|
try {
|
|
if (romFile.read(romxData) != cxRomLength) {
|
|
throw new IOException("Bad Thunderclock rom size");
|
|
}
|
|
getCxRom().loadData(romxData);
|
|
romFile.close();
|
|
romFile = CardThunderclock.class.getClassLoader().getResourceAsStream(path);
|
|
if (romFile.read(rom8Data) != c8RomLength) {
|
|
throw new IOException("Bad Thunderclock rom size");
|
|
}
|
|
getC8Rom().loadData(rom8Data);
|
|
romFile.close();
|
|
} catch (IOException ex) {
|
|
throw ex;
|
|
}
|
|
}
|
|
static byte[] DRIVER_PATTERN = {
|
|
(byte) 0x00, (byte) 0x01f, (byte) 0x03b, (byte) 0x05a,
|
|
(byte) 0x078, (byte) 0x097, (byte) 0x0b5, (byte) 0x0d3,
|
|
(byte) 0x0f2
|
|
};
|
|
static int DRIVER_OFFSET = -26;
|
|
static int patchLoc = -1;
|
|
|
|
/**
|
|
* Scan active memory for the Prodos clock driver and patch the internal
|
|
* code to use a fixed value for the present year. This means Prodos will
|
|
* always tell time correctly.
|
|
*/
|
|
public static void performProdosPatch(Computer computer) {
|
|
PagedMemory ram = computer.getMemory().activeRead;
|
|
if (patchLoc > 0) {
|
|
// We've already patched, just validate
|
|
if (ram.readByte(patchLoc) == (byte) MOS65C02.OPCODE.LDA_IMM.getCode()) {
|
|
return;
|
|
}
|
|
}
|
|
int match = 0;
|
|
int matchStart = 0;
|
|
for (int addr = 0x08000; addr < 0x010000; addr++) {
|
|
if (ram.readByte(addr) == DRIVER_PATTERN[match]) {
|
|
match++;
|
|
if (match == DRIVER_PATTERN.length) {
|
|
break;
|
|
}
|
|
} else {
|
|
match = 0;
|
|
matchStart = addr;
|
|
}
|
|
}
|
|
if (match != DRIVER_PATTERN.length) {
|
|
return;
|
|
}
|
|
patchLoc = matchStart + DRIVER_OFFSET;
|
|
ram.writeByte(patchLoc, (byte) MOS65C02.OPCODE.LDA_IMM.getCode());
|
|
int year = Calendar.getInstance().get(Calendar.YEAR) % 100;
|
|
ram.writeByte(patchLoc + 1, (byte) year);
|
|
ram.writeByte(patchLoc + 2, (byte) MOS65C02.OPCODE.NOP.getCode());
|
|
ram.writeByte(patchLoc + 3, (byte) MOS65C02.OPCODE.NOP.getCode());
|
|
Label clockFixIcon = Utility.loadIconLabel("clock_fix.png");
|
|
clockFixIcon.setText("Fixed");
|
|
EmulatorUILogic.addIndicator(CardThunderclock.class, clockFixIcon, 4000);
|
|
}
|
|
} |