/* 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"; } }