/*
JPC: An x86 PC Hardware Emulator for a pure Java Virtual Machine
Release Version 2.4
A project from the Physics Dept, The University of Oxford
Copyright (C) 2007-2010 The University of Oxford
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2 as published by
the Free Software Foundation.
This program 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 General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Details (including contact information) can be found at:
jpc.sourceforge.net
or the developer website
sourceforge.net/projects/jpc/
Conceived and Developed by:
Rhys Newman, Ian Preston, Chris Dennis
End of licence header
*/
package org.jpc.emulator.motherboard;
import org.jpc.emulator.*;
import org.jpc.emulator.peripheral.FloppyController;
import org.jpc.support.*;
import java.io.*;
import java.util.Calendar;
/**
* Emulation of an MC146818 Real-time Clock.
* @see <a href="http://courses.ece.uiuc.edu/ece391/references/mc146818.pdf">
* MC146818A - Datasheet</a>
* @author Chris Dennis
*/
public class RTC extends AbstractHardwareComponent implements IODevice
{
private static final int RTC_SECONDS = 0;
private static final int RTC_SECONDS_ALARM = 1;
private static final int RTC_MINUTES = 2;
private static final int RTC_MINUTES_ALARM = 3;
private static final int RTC_HOURS = 4;
private static final int RTC_HOURS_ALARM = 5;
private static final int RTC_ALARM_DONT_CARE = 0xc0;
private static final int RTC_DAY_OF_WEEK = 6;
private static final int RTC_DAY_OF_MONTH = 7;
private static final int RTC_MONTH = 8;
private static final int RTC_YEAR = 9;
private static final int RTC_REG_EQUIPMENT_BYTE = 0x14;
private static final int RTC_REG_IBM_CENTURY_BYTE = 0x32;
private static final int RTC_REG_IBM_PS2_CENTURY_BYTE = 0x37;
private static final int RTC_REG_A = 10;
private static final int RTC_REG_B = 11;
private static final int RTC_REG_C = 12;
private static final int RTC_REG_D = 13;
private static final int REG_A_UIP = 0x80;
private static final int REG_B_SET = 0x80;
private static final int REG_B_PIE = 0x40;
private static final int REG_B_AIE = 0x20;
private static final int REG_B_UIE = 0x10;
private static final int REG_CSUM_HIGH = 0x2e;
private static final int REG_CSUM_LOW = 0x2f;
private byte[] cmosData; //rowa
private byte cmosIndex; //rw
private int irq; //r
private Calendar currentTime; //rw
/* periodic timer */
private Timer periodicTimer;
private long nextPeriodicTime; //rw
/* second update */
private Timer secondTimer;
private Timer delayedSecondTimer;
private long nextSecondTime; //ri
private PeriodicCallback periodicCallback;
private SecondCallback secondCallback;
private DelayedSecondCallback delayedSecondCallback;
private InterruptController irqDevice;
private Clock timeSource;
private int ioPortBase;
private DriveSet.BootType bootType;
private boolean ioportRegistered;
private boolean drivesInited;
private boolean floppiesInited;
private final Calendar startTime;
/**
* Construct a new RTC which will register at ioports <code>ioPort</code>
* and <code>ioPort+1</code>. Interrupt requests will be sent on the
* supplied channel number.
* @param ioPort ioport base address.
* @param irq interrupt channel number.
*/
public RTC(int ioPort, int irq, Calendar start)
{
this.startTime = start;
bootType = null;
ioportRegistered = false;
drivesInited = false;
floppiesInited = false;
ioPortBase = ioPort;
this.irq = irq;
cmosData = new byte[128];
cmosData[RTC_REG_A] = 0x26;
cmosData[RTC_REG_B] = 0x02;
cmosData[RTC_REG_C] = 0x00;
cmosData[RTC_REG_D] = (byte) 0x80;
periodicCallback = new PeriodicCallback();
secondCallback = new SecondCallback();
delayedSecondCallback = new DelayedSecondCallback();
}
/**
* Construct a new RTC which will register at ioports <code>ioPort</code>
* and <code>ioPort+1</code>. Interrupt requests will be sent on the
* supplied channel number.
* @param ioPort ioport base address.
* @param irq interrupt channel number.
*/
public RTC(int ioPort, int irq)
{
this(ioPort, irq, Calendar.getInstance());
}
public void saveState(DataOutput output) throws IOException
{
output.writeInt(cmosData.length);
output.write(cmosData);
output.writeByte(cmosIndex);
output.writeInt(irq);
//calendar
output.writeLong(nextSecondTime);
output.writeInt(ioPortBase);
output.writeInt(bootType.ordinal());
output.writeBoolean(ioportRegistered);
output.writeBoolean(drivesInited);
output.writeBoolean(floppiesInited);
//timers
periodicTimer.saveState(output);
secondTimer.saveState(output);
delayedSecondTimer.saveState(output);
}
public void loadState(DataInput input) throws IOException
{
ioportRegistered = false;
int len = input.readInt();
input.readFully(cmosData, 0, len);
cmosIndex = input.readByte();
irq = input.readInt();
//calendar
nextSecondTime = input.readLong();
ioPortBase = input.readInt();
bootType = DriveSet.BootType.values()[input.readInt()];
ioportRegistered = input.readBoolean();
drivesInited = input.readBoolean();
floppiesInited = input.readBoolean();
//timers
periodicTimer = timeSource.newTimer(periodicCallback);
secondTimer = timeSource.newTimer(secondCallback);
delayedSecondTimer = timeSource.newTimer(delayedSecondCallback);
periodicTimer.loadState(input);
secondTimer.loadState(input);
delayedSecondTimer.loadState(input);
}
private static final long scale64(long input, int multiply, int divide)
{
//return (BigInteger.valueOf(input).multiply(BigInteger.valueOf(multiply)).divide(BigInteger.valueOf(divide))).longValue();
long rl = (0xffffffffl & input) * multiply;
long rh = (input >>> 32) * multiply;
rh += (rl >> 32);
long resultHigh = 0xffffffffl & (rh / divide);
long resultLow = 0xffffffffl & ((((rh % divide) << 32) + (rl & 0xffffffffl)) / divide);
return (resultHigh << 32) | resultLow;
}
private void init()
{
//Calendar now = Calendar.getInstance();
this.setTime(startTime);
int val = this.toBCD(startTime.get(Calendar.YEAR) / 100);
cmosData[RTC_REG_IBM_CENTURY_BYTE] = (byte) val;
cmosData[RTC_REG_IBM_PS2_CENTURY_BYTE] = (byte) val;
/* memory size */
val = 640; /* base memory in K */
cmosData[0x15] = (byte) val;
cmosData[0x16] = (byte) (val >>> 8);
int ramSize = PC.SYS_RAM_SIZE;
val = (ramSize / 1024) - 1024;
if (val > 65535) val = 65535;
cmosData[0x17] = (byte) val;
cmosData[0x18] = (byte) (val >>> 8);
cmosData[0x30] = (byte) val;
cmosData[0x31] = (byte) (val >>> 8);
if (ramSize > (16 * 1024 * 1024))
val = (ramSize / 65536) - ((16 * 1024 * 1024) / 65536);
else
val = 0;
if (val > 65535) val = 65535;
cmosData[0x34] = (byte) val;
cmosData[0x35] = (byte) (val >>> 8);
switch (bootType) {
case FLOPPY:
cmosData[0x3d] = (byte) 0x01; /* floppy boot */
break;
default:
case HARD_DRIVE:
cmosData[0x3d] = (byte) 0x02; /* hard drive boot */
break;
case CDROM:
cmosData[0x3d] = (byte) 0x03; /* CD-ROM boot */
break;
}
cmosData[0x2d] = 0x20;
int sum = 0;
for (int i=0x10; i<=0x2d; i++)
sum += cmosData[i] & 0xff;
cmosData[REG_CSUM_HIGH] = (byte) (sum >> 8);
cmosData[REG_CSUM_LOW] = (byte) sum;
}
private void cmosInitHD(DriveSet drives)
{
BlockDevice drive0 = drives.getHardDrive(0);
BlockDevice drive1 = drives.getHardDrive(1);
cmosData[0x12] = (byte) (((drive0 != null) ? 0xf0 : 0) | ((drive1 != null) ? 0x0f : 0));
if (drive0 != null) {
cmosData[0x19] = (byte) 47;
cmosData[0x1b] = (byte) drive0.getCylinders();
cmosData[0x1b + 1] = (byte) (drive0.getCylinders() >>> 8);
cmosData[0x1b + 2] = (byte) drive0.getHeads();
cmosData[0x1b + 3] = (byte) 0xff;
cmosData[0x1b + 4] = (byte) 0xff;
cmosData[0x1b + 5] = (byte) (0xc0 | ((drive0.getHeads() > 8) ? 0x8 : 0));
cmosData[0x1b + 6] = (byte) drive0.getCylinders();
cmosData[0x1b + 7] = (byte) (drive0.getCylinders() >>> 8);
cmosData[0x1b + 8] = (byte) drive0.getSectors();
}
if (drive1 != null) {
cmosData[0x1a] = (byte) 47;
cmosData[0x24] = (byte) drive1.getCylinders();
cmosData[0x24 + 1] = (byte) (drive1.getCylinders() >>> 8);
cmosData[0x24 + 2] = (byte) drive1.getHeads();
cmosData[0x24 + 3] = (byte) 0xff;
cmosData[0x24 + 4] = (byte) 0xff;
cmosData[0x24 + 5] = (byte) (0xc0 | ((drive1.getHeads() > 8) ? 0x8 : 0));
cmosData[0x24 + 6] = (byte) drive1.getCylinders();
cmosData[0x24 + 7] = (byte) (drive1.getCylinders() >>> 8);
cmosData[0x24 + 8] = (byte) drive1.getSectors();
}
int value = 0;
for (int i = 0; i < 4; i++)
if (drives.getHardDrive(i) != null) {
int translation;
if ((drives.getHardDrive(i).getCylinders() <= 1024) &&
(drives.getHardDrive(i).getHeads() <= 16) &&
(drives.getHardDrive(i).getSectors() <= 63))
/* No Translation. */
translation = 0;
else
/* LBA Translation */
translation = 1;
value |= translation << (i * 2);
}
cmosData[0x39] = (byte) value;
}
private void cmosInitFloppy(FloppyController fdc)
{
int val = (cmosGetFDType(fdc, 0) << 4) | cmosGetFDType(fdc, 1);
cmosData[0x10] = (byte) val;
int num = 0;
val = 0;
if (fdc.getDriveType(0) != FloppyController.DriveType.DRIVE_NONE)
num++;
if (fdc.getDriveType(1) != FloppyController.DriveType.DRIVE_NONE)
num++;
switch (num) {
case 0:
break;
case 1:
val |= 0x01;
break;
case 2:
val |= 0x41;
break;
}
val |= 0x02; // Have FPU
val |= 0x04; // Have PS2 Mouse
cmosData[RTC_REG_EQUIPMENT_BYTE] = (byte) val;
}
private int cmosGetFDType(FloppyController fdc, int drive)
{
switch (fdc.getDriveType(drive)) {
case DRIVE_144:
return 4;
case DRIVE_288:
return 5;
case DRIVE_120:
return 2;
default:
return 0;
}
}
public int[] ioPortsRequested()
{
int base = ioPortBase;
return new int[]{base, base + 1};
}
public int ioPortRead8(int address)
{
return 0xff & cmosIOPortRead(address);
}
public int ioPortRead16(int address)
{
return (0xff & ioPortRead8(address)) | (0xff00 & (ioPortRead8(address + 1) << 8));
}
public int ioPortRead32(int address)
{
return (0xffff & ioPortRead16(address)) | (0xffff0000 & (ioPortRead16(address + 2) << 16));
}
public void ioPortWrite8(int address, int data)
{
cmosIOPortWrite(address, 0xff & data);
}
public void ioPortWrite16(int address, int data)
{
this.ioPortWrite8(address, data);
this.ioPortWrite8(address + 1, data >> 8);
}
public void ioPortWrite32(int address, int data)
{
this.ioPortWrite16(address, data);
this.ioPortWrite16(address + 2, data >> 16);
}
private void periodicUpdate()
{
this.timerUpdate(nextPeriodicTime);
cmosData[RTC_REG_C] |= 0xc0;
irqDevice.setIRQ(irq, 1);
}
private void secondUpdate()
{
if ((cmosData[RTC_REG_A] & 0x70) != 0x20) {
nextSecondTime += timeSource.getTickRate();
secondTimer.setExpiry(nextSecondTime);
} else {
this.nextSecond();
if (0 == (cmosData[RTC_REG_B] & REG_B_SET)) /* update in progress bit */
cmosData[RTC_REG_A] |= REG_A_UIP;
/* should be 244us = 8 / 32768 second, but currently the timers do not have the necessary resolution. */
long delay = (timeSource.getTickRate() * 1) / 100;
if (delay < 1)
delay = 1;
delayedSecondTimer.setExpiry(nextSecondTime + delay);
}
}
private void delayedSecondUpdate()
{
if (0 == (cmosData[RTC_REG_B] & REG_B_SET))
this.timeToMemory();
/* check alarm */
if (0 != (cmosData[RTC_REG_B] & REG_B_AIE))
if (((cmosData[RTC_SECONDS_ALARM] & 0xc0) == 0xc0 ||
cmosData[RTC_SECONDS_ALARM] == currentTime.get(Calendar.SECOND)) &&
((cmosData[RTC_MINUTES_ALARM] & 0xc0) == 0xc0 ||
cmosData[RTC_MINUTES_ALARM] == currentTime.get(Calendar.MINUTE)) &&
((cmosData[RTC_HOURS_ALARM] & 0xc0) == 0xc0 ||
cmosData[RTC_HOURS_ALARM] == currentTime.get(Calendar.HOUR_OF_DAY))) {
cmosData[RTC_REG_C] |= 0xa0;
irqDevice.setIRQ(irq, 1);
}
/* update ended interrupt */
if (0 != (cmosData[RTC_REG_B] & REG_B_UIE)) {
cmosData[RTC_REG_C] |= 0x90;
irqDevice.setIRQ(irq, 1);
}
/* clear update in progress bit */
cmosData[RTC_REG_A] &= ~REG_A_UIP;
nextSecondTime += timeSource.getTickRate();
secondTimer.setExpiry(nextSecondTime);
}
private void timerUpdate(long currentTime)
{
int periodCode = cmosData[RTC_REG_A] & 0x0f;
if ((periodCode != 0) && (0 != (cmosData[RTC_REG_B] & REG_B_PIE))) {
if (periodCode <= 2)
periodCode += 7;
/* period in 32 kHz cycles */
int period = 1 << (periodCode - 1);
/* compute 32 kHz clock */
long currentClock = scale64(currentTime, 32768, (int) timeSource.getTickRate());
long nextIRQClock = (currentClock & ~(period - 1)) + period;
nextPeriodicTime = scale64(nextIRQClock, (int) timeSource.getTickRate(), 32768) + 1;
periodicTimer.setExpiry(nextPeriodicTime);
} else
periodicTimer.disable();
}
private void nextSecond()
{
//currentTime = Calendar.getInstance();
currentTime.add(Calendar.SECOND, 1);
}
private void cmosIOPortWrite(int address, int data)
{
if ((address & 1) == 0)
cmosIndex = (byte) (data & 0x7f);
else
switch (this.cmosIndex) {
case RTC_SECONDS_ALARM:
case RTC_MINUTES_ALARM:
case RTC_HOURS_ALARM:
/* XXX: not supported */
cmosData[this.cmosIndex] = (byte) data;
break;
case RTC_SECONDS:
case RTC_MINUTES:
case RTC_HOURS:
case RTC_DAY_OF_WEEK:
case RTC_DAY_OF_MONTH:
case RTC_MONTH:
case RTC_YEAR:
cmosData[this.cmosIndex] = (byte) data;
/* if in set mode, do not update the time */
if (0 == (cmosData[RTC_REG_B] & REG_B_SET))
this.memoryToTime();
break;
case RTC_REG_A:
/* UIP bit is read only */
cmosData[RTC_REG_A] = (byte) ((data & ~REG_A_UIP) | (cmosData[RTC_REG_A] & REG_A_UIP));
this.timerUpdate(timeSource.getEmulatedNanos());
break;
case RTC_REG_B:
if (0 != (data & REG_B_SET)) {
/* set mode: reset UIP mode */
cmosData[RTC_REG_A] &= ~REG_A_UIP;
data &= ~REG_B_UIE;
} else
/* if disabling set mode, update the time */
if (0 != (cmosData[RTC_REG_B] & REG_B_SET))
this.memoryToTime();
cmosData[RTC_REG_B] = (byte) data;
this.timerUpdate(timeSource.getEmulatedNanos());
break;
case RTC_REG_C:
case RTC_REG_D:
/* cannot write to them */
break;
default:
cmosData[this.cmosIndex] = (byte) data;
break;
}
}
private int cmosIOPortRead(int address)
{
if ((address & 1) == 0)
return 0xff;
else
switch (this.cmosIndex) {
case RTC_REG_C:
int ret = cmosData[RTC_REG_C];
irqDevice.setIRQ(irq, 0);
cmosData[RTC_REG_C] = (byte) 0x00;
return ret;
default:
return cmosData[this.cmosIndex];
}
}
private void setTime(Calendar date)
{
this.currentTime = Calendar.getInstance(date.getTimeZone());
this.currentTime.setTime(date.getTime());
this.timeToMemory();
}
private void memoryToTime()
{
currentTime.set(Calendar.SECOND, this.fromBCD(cmosData[RTC_SECONDS]));
currentTime.set(Calendar.MINUTE, this.fromBCD(cmosData[RTC_MINUTES]));
currentTime.set(Calendar.HOUR_OF_DAY, this.fromBCD(cmosData[RTC_HOURS] & 0x7f));
if (0 == (cmosData[RTC_REG_B] & 0x02) && 0 != (cmosData[RTC_HOURS] & 0x80))
currentTime.add(Calendar.HOUR_OF_DAY, 12);
currentTime.set(Calendar.DAY_OF_WEEK, this.fromBCD(cmosData[RTC_DAY_OF_WEEK]));
currentTime.set(Calendar.DAY_OF_MONTH, this.fromBCD(cmosData[RTC_DAY_OF_MONTH]));
currentTime.set(Calendar.MONTH, this.fromBCD(cmosData[RTC_MONTH]) - 1);
currentTime.set(Calendar.YEAR, this.fromBCD(cmosData[RTC_YEAR]) + 2000); //is this offset correct?
}
private void timeToMemory()
{
cmosData[RTC_SECONDS] = (byte) this.toBCD(currentTime.get(Calendar.SECOND));
cmosData[RTC_MINUTES] = (byte) this.toBCD(currentTime.get(Calendar.MINUTE));
if (0 != (cmosData[RTC_REG_B] & 0x02)) /* 24 hour format */
cmosData[RTC_HOURS] = (byte) this.toBCD(currentTime.get(Calendar.HOUR_OF_DAY));
else { /* 12 hour format */
cmosData[RTC_HOURS] = (byte) this.toBCD(currentTime.get(Calendar.HOUR));
if (currentTime.get(Calendar.AM_PM) == Calendar.PM)
cmosData[RTC_HOURS] |= 0x80;
}
cmosData[RTC_DAY_OF_WEEK] = (byte) this.toBCD(currentTime.get(Calendar.DAY_OF_WEEK));
cmosData[RTC_DAY_OF_MONTH] = (byte) this.toBCD(currentTime.get(Calendar.DAY_OF_MONTH));
cmosData[RTC_MONTH] = (byte) this.toBCD(currentTime.get(Calendar.MONTH) + 1);
cmosData[RTC_YEAR] = (byte) this.toBCD(currentTime.get(Calendar.YEAR) % 100);
}
private int toBCD(int a) //Binary Coded Decimal
{
if (0 != (cmosData[RTC_REG_B] & 0x04))
return a;
else
return ((a / 10) << 4) | (a % 10);
}
private int fromBCD(int a) //Binary Coded Decimal
{
if (0 != (cmosData[RTC_REG_B] & 0x04))
return a;
else
return ((a >> 4) * 10) + (a & 0x0f);
}
public boolean initialised()
{
return ((irqDevice != null) && (timeSource != null) && ioportRegistered && drivesInited && floppiesInited && (bootType != null));
}
public void reset()
{
irqDevice = null;
timeSource = null;
ioportRegistered = false;
drivesInited = false;
floppiesInited = false;
bootType = null;
cmosData = new byte[128];
cmosData[RTC_REG_A] = 0x26;
cmosData[RTC_REG_B] = 0x02;
cmosData[RTC_REG_C] = 0x00;
cmosData[RTC_REG_D] = (byte) 0x80;
periodicCallback = new PeriodicCallback();
secondCallback = new SecondCallback();
delayedSecondCallback = new DelayedSecondCallback();
}
private class PeriodicCallback implements TimerResponsive
{
public void callback()
{
RTC.this.periodicUpdate();
}
public int getType() {
return 3;
}
}
private class SecondCallback implements TimerResponsive
{
public void callback()
{
RTC.this.secondUpdate();
}
public int getType() {
return 4;
}
}
private class DelayedSecondCallback implements TimerResponsive
{
public void callback()
{
RTC.this.delayedSecondUpdate();
}
public int getType() {
return 5;
}
}
public boolean updated()
{
return (irqDevice.updated() && timeSource.updated() && ioportRegistered);
}
public void updateComponent(HardwareComponent component)
{
if ((component instanceof IOPortHandler) && component.updated()) {
((IOPortHandler) component).registerIOPortCapable(this);
ioportRegistered = true;
}
}
public void acceptComponent(HardwareComponent component)
{
if ((component instanceof InterruptController) && component.initialised())
irqDevice = (InterruptController) component;
if ((component instanceof Clock) && component.initialised())
timeSource = (Clock) component;
if ((component instanceof IOPortHandler) && component.initialised()) {
((IOPortHandler) component).registerIOPortCapable(this);
ioportRegistered = true;
}
if ((component instanceof DriveSet) && component.initialised()) {
this.cmosInitHD((DriveSet) component);
drivesInited = true;
}
if ((component instanceof FloppyController) && component.initialised()) {
this.cmosInitFloppy((FloppyController) component);
floppiesInited = true;
}
if (component instanceof DriveSet)
bootType = ((DriveSet) component).getBootType();
if (this.initialised()) {
init();
periodicTimer = timeSource.newTimer(periodicCallback);
secondTimer = timeSource.newTimer(secondCallback);
delayedSecondTimer = timeSource.newTimer(delayedSecondCallback);
nextSecondTime = timeSource.getEmulatedNanos() /*+ 1000000000L;/*/ + (99 * timeSource.getTickRate()) / 100;
delayedSecondTimer.setExpiry(nextSecondTime);
}
}
public byte[] getCMOS()
{
byte[] res = new byte[128];
System.arraycopy(cmosData, 0, res, 0, 128);
return res;
}
public String toString()
{
return "MC146818 RealTime Clock";
}
}