Java tutorial
/* * Created: 31.03.2012 * * Copyright (C) 2012 Victor Antonovich (v.antonovich@gmail.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * 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, see <http://www.gnu.org/licenses/>. */ package su.comp.bk.arch; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import org.apache.commons.lang.ArrayUtils; import android.content.res.Resources; import android.os.Bundle; import android.util.Log; import su.comp.bk.R; import su.comp.bk.arch.cpu.Cpu; import su.comp.bk.arch.io.AudioOutput; import su.comp.bk.arch.io.Device; import su.comp.bk.arch.io.FloppyController; import su.comp.bk.arch.io.KeyboardController; import su.comp.bk.arch.io.MemoryManager; import su.comp.bk.arch.io.PeripheralPort; import su.comp.bk.arch.io.Sel1RegisterSystemBits; import su.comp.bk.arch.io.SystemTimer; import su.comp.bk.arch.io.Timer; import su.comp.bk.arch.io.VideoController; import su.comp.bk.arch.io.VideoControllerManager; import su.comp.bk.arch.memory.Memory; import su.comp.bk.arch.memory.PagedMemory; import su.comp.bk.arch.memory.RandomAccessMemory; import su.comp.bk.arch.memory.RandomAccessMemory.Type; import su.comp.bk.arch.memory.ReadOnlyMemory; /** * BK001x computer implementation. */ public class Computer implements Runnable { private static final String TAG = Computer.class.getName(); // State save/restore: Computer uptime (in nanoseconds) private static final String STATE_UPTIME = Computer.class.getName() + "#uptime"; /** Bus error constant */ public final static int BUS_ERROR = -1; // Computer configuration private Configuration configuration; // Memory table mapped by 8KB blocks private final Memory[] memoryTable = new Memory[8]; // CPU implementation reference private final Cpu cpu; // Video controller reference private VideoController videoController; // Keyboard controller reference private KeyboardController keyboardController; // Periferal port reference private PeripheralPort periferalPort; // Audio output reference private AudioOutput audioOutput; // FLoppy controller reference (<code>null</code> if no floppy controller attached) private FloppyController floppyController; /** I/O registers space min start address */ public final static int IO_REGISTERS_MIN_ADDRESS = 0170000; /** I/O registers space max start address */ public final static int IO_REGISTERS_MAX_ADDRESS = 0177600; // I/O devices list private final List<Device> deviceList = new ArrayList<Device>(); // I/O registers space addresses mapped to devices private final List<?>[] deviceTable = new List[2048]; // Devices start address (depends from connected RAM/ROM) private int devicesStartAddress = IO_REGISTERS_MIN_ADDRESS; private boolean isRunning = false; private boolean isPaused = true; private Thread clockThread; /** Amount of nanoseconds in one millisecond */ public static final long NANOSECS_IN_MSEC = 1000000L; /** BK0010 clock frequency (in kHz) */ public final static int CLOCK_FREQUENCY_BK0010 = 3000; /** BK0011 clock frequency (in kHz) */ public final static int CLOCK_FREQUENCY_BK0011 = 4000; // Computer clock frequency (in kHz) private int clockFrequency; /** Uptime sync threshold (in nanoseconds) */ public static final long SYNC_UPTIME_THRESHOLD = (10L * NANOSECS_IN_MSEC); // Uptime sync threshold (in CPU clock ticks, depends from CPU clock frequency) private long syncUptimeThresholdCpuTicks; // Last uptime sync timestamp (in nanoseconds, absolute value) private long lastUptimeSyncTimestamp; // Last CPU time sync timestamp (in ticks) private long lastCpuTimeSyncTimestamp; // Computer uptime (in nanoseconds) private long uptime; public enum Configuration { /** BK0010 - monitor only */ BK_0010_MONITOR, /** BK0010 - monitor and Basic */ BK_0010_BASIC, /** BK0010 - monitor, Focal and tests */ BK_0010_MSTD, /** BK0010 with connected floppy drive controller (?) */ BK_0010_KNGMD(true), /** BK0011M - MSTD block attached */ BK_0011M_MSTD(false, true), /** BK0011M with connected floppy drive controller (?) */ BK_0011M_KNGMD(true, true); private final boolean isFloppyControllerPresent; private final boolean isMemoryManagerPresent; private Configuration() { this(false, false); } private Configuration(boolean isFloppyControllerPresent) { this(isFloppyControllerPresent, false); } private Configuration(boolean isFloppyControllerPresent, boolean isMemoryManagerPresent) { this.isFloppyControllerPresent = isFloppyControllerPresent; this.isMemoryManagerPresent = isMemoryManagerPresent; } /** * Check is {@link FloppyController} present in this configuration. * @return <code>true</code> if floppy controller present, <code>false</code> otherwise */ public boolean isFloppyControllerPresent() { return isFloppyControllerPresent; } /** * Check is BK-0011 {@link MemoryManager} present in configuration. * @return <code>true</code> if memory manager present, <code>false</code> otherwise */ public boolean isMemoryManagerPresent() { return isMemoryManagerPresent; } } public Computer() { this.cpu = new Cpu(this); } /** * Configure this computer. * @param resources Android resources object reference * @param config computer configuration as {@link Configuration} value * @throws Exception in case of error while configuring */ public void configure(Resources resources, Configuration config) throws Exception { setConfiguration(config); // Apply shared configuration addDevice(new Sel1RegisterSystemBits(!config.isMemoryManagerPresent() ? 0100000 : 0140000)); keyboardController = new KeyboardController(this); addDevice(keyboardController); periferalPort = new PeripheralPort(this); addDevice(periferalPort); addDevice(new Timer()); // Apply computer specific configuration if (!config.isMemoryManagerPresent()) { // BK-0010 configurations setClockFrequency(CLOCK_FREQUENCY_BK0010); // Set RAM configuration RandomAccessMemory workMemory = new RandomAccessMemory("WorkMemory", 0, 020000); addMemory(workMemory); RandomAccessMemory videoMemory = new RandomAccessMemory("VideoMemory", 040000, 020000); addMemory(videoMemory); // Add video controller videoController = new VideoController(videoMemory); addDevice(videoController); // Set ROM configuration addReadOnlyMemory(resources, R.raw.monit10, "Monitor10", 0100000); switch (config) { case BK_0010_BASIC: addReadOnlyMemory(resources, R.raw.basic10_1, "Basic10:1", 0120000); addReadOnlyMemory(resources, R.raw.basic10_2, "Basic10:2", 0140000); addReadOnlyMemory(resources, R.raw.basic10_3, "Basic10:3", 0160000); break; case BK_0010_MSTD: addReadOnlyMemory(resources, R.raw.focal, "Focal", 0120000); addReadOnlyMemory(resources, R.raw.tests, "MSTD10", 0160000); break; case BK_0010_KNGMD: addMemory(new RandomAccessMemory("ExtMemory", 0120000, 020000, Type.K537RU10)); addReadOnlyMemory(resources, R.raw.disk_327, "FloppyBios", 0160000); floppyController = new FloppyController(this); addDevice(floppyController); break; default: break; } } else { // BK-0011 configurations setClockFrequency(CLOCK_FREQUENCY_BK0011); // Set RAM configuration PagedMemory firstPagedMemory = new PagedMemory("PagedMemory0", 040000, 020000, MemoryManager.NUM_RAM_PAGES); PagedMemory secondPagedMemory = new PagedMemory("PagedMemory1", 0100000, 020000, MemoryManager.NUM_RAM_PAGES + MemoryManager.NUM_ROM_PAGES); for (int memoryPageIndex = 0; memoryPageIndex < MemoryManager.NUM_RAM_PAGES; memoryPageIndex++) { Memory memoryPage = new RandomAccessMemory("MemoryPage" + memoryPageIndex, 0, 020000); firstPagedMemory.setPage(memoryPageIndex, memoryPage); secondPagedMemory.setPage(memoryPageIndex, memoryPage); } addMemory(firstPagedMemory.getPage(6)); // Static RAM page at address 0 addMemory(firstPagedMemory); // First paged memory space at address 040000 addMemory(secondPagedMemory); // Second paged memory space at address 0100000 // Set ROM configuration secondPagedMemory.setPage(MemoryManager.NUM_RAM_PAGES, new ReadOnlyMemory("Basic11M:0", 0, loadReadOnlyMemoryData(resources, R.raw.basic11m_0))); secondPagedMemory.setPage(MemoryManager.NUM_RAM_PAGES + 1, new ReadOnlyMemory("Basic11M:1/ExtBOS11M", 0, loadReadOnlyMemoryData(resources, R.raw.basic11m_1, R.raw.ext11m))); addReadOnlyMemory(resources, R.raw.bos11m, "BOS11M", 0140000); switch (config) { case BK_0011M_MSTD: addReadOnlyMemory(resources, R.raw.mstd11m, "MSTD11M", 0160000); break; case BK_0011M_KNGMD: addReadOnlyMemory(resources, R.raw.disk_327, "FloppyBios", 0160000); floppyController = new FloppyController(this); addDevice(floppyController); break; default: break; } // Configure memory manager addDevice(new MemoryManager(firstPagedMemory, secondPagedMemory)); // Add video controller with palette/screen manager PagedMemory pagedVideoMemory = new PagedMemory("PagedVideoMemory", 0, 020000, 2); pagedVideoMemory.setPage(0, firstPagedMemory.getPage(1)); pagedVideoMemory.setPage(1, firstPagedMemory.getPage(7)); videoController = new VideoController(pagedVideoMemory); addDevice(videoController); addDevice(new VideoControllerManager(videoController, pagedVideoMemory)); // Add system timer addDevice(new SystemTimer(this)); } // Add audio output audioOutput = new AudioOutput(this, config.isMemoryManagerPresent()); addDevice(audioOutput); } /** * Set computer configuration. * @param config computer configuration as {@link Configuration} value */ public void setConfiguration(Configuration config) { this.configuration = config; } /** * Get computer configuration. * @return computer configuration as {@link Configuration} value */ public Configuration getConfiguration() { return this.configuration; } private List<Memory> getStatefulMemoryList() { List<Memory> statefulMemoryList = new ArrayList<Memory>(); for (int memoryBlockIdx = 0; memoryBlockIdx < memoryTable.length; memoryBlockIdx++) { Memory memoryBlock = memoryTable[memoryBlockIdx]; if (!(memoryBlock instanceof ReadOnlyMemory)) { if (memoryBlock instanceof RandomAccessMemory) { if (!statefulMemoryList.contains(memoryBlock)) { statefulMemoryList.add(memoryBlock); } } else if (memoryBlock instanceof PagedMemory) { PagedMemory pagedMemory = (PagedMemory) memoryBlock; for (Memory memoryPage : pagedMemory.getPages()) { if (memoryPage != null && !(memoryPage instanceof ReadOnlyMemory) && !statefulMemoryList.contains(memoryPage)) { statefulMemoryList.add(memoryPage); } } statefulMemoryList.add(pagedMemory); } } } return statefulMemoryList; } /** * Save computer state. * @param resources Android {@link Resources} object reference * @param outState {@link Bundle} to save state */ public synchronized void saveState(Resources resources, Bundle outState) { // Save computer configuration outState.putString(Configuration.class.getName(), getConfiguration().name()); // Save computer uptime outState.putLong(STATE_UPTIME, getUptime()); // Save RAM data for (Memory memory : getStatefulMemoryList()) { memory.saveState(outState); } // Save CPU state getCpu().saveState(outState); // Save device states for (Device device : deviceList) { device.saveState(outState); } } /** * Restore computer state. * @param resources Android {@link Resources} object reference * @param inState {@link Bundle} to restore state * @throws Exception in case of error while state restoring */ public synchronized void restoreState(Resources resources, Bundle inState) throws Exception { // Restore computer configuration Configuration config = Configuration.valueOf(inState.getString(Configuration.class.getName())); configure(resources, config); // Restore computer uptime setUptime(inState.getLong(STATE_UPTIME)); // Initialize CPU and devices cpu.initDevices(); // Restore RAM data for (Memory memory : getStatefulMemoryList()) { memory.restoreState(inState); } // Restore CPU state getCpu().restoreState(inState); // Restore device states for (Device device : deviceList) { device.restoreState(inState); } } /** * Get clock frequency (in kHz) * @return clock frequency */ public int getClockFrequency() { return clockFrequency; } /** * Set clock frequency (in kHz) * @param clockFrequency clock frequency to set */ public void setClockFrequency(int clockFrequency) { this.clockFrequency = clockFrequency; this.syncUptimeThresholdCpuTicks = nanosToCpuTime(SYNC_UPTIME_THRESHOLD); } private void addReadOnlyMemory(Resources resources, int romDataResId, String romId, int address) throws IOException { byte[] romData = loadReadOnlyMemoryData(resources, romDataResId); addMemory(new ReadOnlyMemory(romId, address, romData)); } /** * Load ROM data from raw resources. * @param resources {@link Resources} reference * @param romDataResIds ROM raw resource IDs * @return loaded ROM data * @throws IOException in case of ROM data loading error */ private byte[] loadReadOnlyMemoryData(Resources resources, int... romDataResIds) throws IOException { byte[] romData = null; for (int romDataResId : romDataResIds) { romData = ArrayUtils.addAll(romData, loadRawResourceData(resources, romDataResId)); } return romData; } /** * Load data of raw resource. * @param resources {@link Resources} reference * @param resourceId raw resource ID * @return read raw resource data * @throws IOException in case of loading error */ private byte[] loadRawResourceData(Resources resources, int resourceId) throws IOException { InputStream resourceDataStream = resources.openRawResource(resourceId); byte[] resourceData = new byte[resourceDataStream.available()]; resourceDataStream.read(resourceData); return resourceData; } /** * Get {@link Cpu} reference. * @return this <code>Computer</code> CPU object reference */ public Cpu getCpu() { return cpu; } /** * Get {@link VideoController} reference. * @return video controller reference */ public VideoController getVideoController() { return videoController; } /** * Get {@link KeyboardController} reference. * @return keyboard controller reference */ public KeyboardController getKeyboardController() { return keyboardController; } /** * Get {@link PeripheralPort} reference. * @return peripheral port reference */ public PeripheralPort getPeripheralPort() { return periferalPort; } /** * Get {@link FloppyController} reference. * @return floppyController reference or <code>null</code> if not attached */ public FloppyController getFloppyController() { return floppyController; } /** * Set computer uptime (in nanoseconds). * @param uptime computer uptime to set (in nanoseconds) */ public void setUptime(long uptime) { this.uptime = uptime; } /** * Get this computer uptime (in nanoseconds). * @return computer uptime (in nanoseconds) */ public long getUptime() { return uptime; } /** * Add memory (RAM/ROM) to address space. * @param memory {@link Memory} to add */ public void addMemory(Memory memory) { int memoryStartBlock = memory.getStartAddress() >> 13; int memoryBlocksCount = memory.getSize() >> 12; // ((size << 1) >> 13) if ((memory.getSize() & 07777) != 0) { memoryBlocksCount++; } for (int memoryBlockIdx = 0; memoryBlockIdx < memoryBlocksCount; memoryBlockIdx++) { memoryTable[memoryStartBlock + memoryBlockIdx] = memory; } // Correct devices start address, if needed int memoryEndAddress = memory.getStartAddress() + (memory.getSize() << 1); if (getDevicesStartAddress() < memoryEndAddress) { setDevicesStartAddress(Math.min(memoryEndAddress, IO_REGISTERS_MAX_ADDRESS)); } } /** * Add I/O device to address space. * @param device {@link Device} to add */ public void addDevice(Device device) { deviceList.add(device); int[] deviceAddresses = device.getAddresses(); for (int deviceAddress : deviceAddresses) { int deviceTableIndex = (deviceAddress - IO_REGISTERS_MIN_ADDRESS) >> 1; @SuppressWarnings("unchecked") List<Device> addressDevices = (List<Device>) deviceTable[deviceTableIndex]; if (addressDevices == null) { addressDevices = new ArrayList<Device>(1); deviceTable[deviceTableIndex] = addressDevices; } addressDevices.add(device); } } /** * Check is given address is mapped to ROM area. * @param address address to check * @return <code>true</code> if given address is mapped to ROM area, * <code>false</code> otherwise */ public boolean isReadOnlyMemoryAddress(int address) { return (address >= 0) && (getMemory(address) instanceof ReadOnlyMemory); } private Memory getMemory(int address) { Memory memory = memoryTable[address >> 13]; return (memory != null && memory.isRelatedAddress(address)) ? memory : null; } /** * Get I/O devices start address. * @return I/O devices start address value */ public int getDevicesStartAddress() { return devicesStartAddress; } /** * Set I/O devices start address. * @param devicesStartAddress I/O devices start address value to set */ public void setDevicesStartAddress(int devicesStartAddress) { this.devicesStartAddress = devicesStartAddress; } @SuppressWarnings("unchecked") private List<Device> getDevices(int address) { return (List<Device>) deviceTable[(address - IO_REGISTERS_MIN_ADDRESS) >> 1]; } /** * Initialize bus devices state (on power-on cycle or RESET opcode). */ public void initDevices() { for (Device device : deviceList) { device.init(getCpu().getTime()); } } /** * Reset computer state. */ public synchronized void reset() { getCpu().reset(); } /** * Read byte or word from memory or I/O device mapped to given address. * @param isByteMode <code>true</code> to read byte, <code>false</code> to read word * @param address address or memory location to read * @return read memory or I/O device data or <code>BUS_ERROR</code> in case if given memory * location is not mapped */ public int readMemory(boolean isByteMode, int address) { int readValue = BUS_ERROR; // First check for I/O registers if (address >= getDevicesStartAddress()) { List<Device> subdevices = getDevices(address); if (subdevices != null) { long cpuClock = getCpu().getTime(); readValue = 0; for (Device subdevice : subdevices) { // Read subdevice state value in word mode int subdeviceState = subdevice.read(cpuClock, address & 0177776); // For byte mode read and odd address - extract high byte if (isByteMode && (address & 1) != 0) { subdeviceState >>= 8; } // Concatenate this subdevice state value with values of other subdevices readValue |= (subdeviceState & (isByteMode ? 0377 : 0177777)); } } } else { // Check for memory at given address Memory memory = getMemory(address); if (memory != null) { readValue = memory.read(isByteMode, address); } } return readValue; } /** * Write byte or word to memory or I/O device mapped to given address. * @param isByteMode <code>true</code> to write byte, <code>false</code> to write word * @param address address or memory location to write * @param value value (byte/word) to write to given address * @return <code>true</code> if data successfully written or <code>false</code> * in case if given memory location is not mapped */ public boolean writeMemory(boolean isByteMode, int address, int value) { boolean isWritten = false; // First check for I/O registers if (address >= getDevicesStartAddress()) { List<Device> devices = getDevices(address); if (devices != null) { long cpuClock = getCpu().getTime(); for (Device device : devices) { if (device.write(cpuClock, isByteMode, address, value)) { isWritten = true; } } } } else { // Check for memory at given address Memory memory = getMemory(address); if (memory != null) { isWritten = memory.write(isByteMode, address, value); } } return isWritten; } /** * Start computer. */ public synchronized void start() { if (!isRunning) { Log.d(TAG, "starting computer"); this.clockThread = new Thread(this, "ComputerClockThread"); isRunning = true; clockThread.start(); // Waiting to emulation thread start try { this.wait(); } catch (InterruptedException e) { } audioOutput.start(); } else { throw new IllegalStateException("Computer is already running!"); } } /** * Stop computer. */ public void stop() { if (isRunning) { Log.d(TAG, "stopping computer"); audioOutput.stop(); synchronized (this) { isRunning = false; this.notifyAll(); } while (clockThread.isAlive()) { try { this.clockThread.join(); } catch (InterruptedException e) { } } } else { throw new IllegalStateException("Computer is already stopped!"); } } /** * Pause computer. */ public synchronized void pause() { Log.d(TAG, "pausing computer"); isPaused = true; this.notifyAll(); audioOutput.pause(); } /** * Resume computer. */ public synchronized void resume() { Log.d(TAG, "resuming computer"); lastUptimeSyncTimestamp = System.nanoTime(); lastCpuTimeSyncTimestamp = cpu.getTime(); isPaused = false; audioOutput.resume(); this.notifyAll(); } /** * Release computer resources. */ public void release() { Log.d(TAG, "releasing computer"); audioOutput.release(); if (floppyController != null) { floppyController.unmountDiskImages(); } } /** * Get effective emulation clock frequency. * @return effective emulation clock frequency (in kHz) */ public float getEffectiveClockFrequency() { return (float) getCpu().getTime() * NANOSECS_IN_MSEC / getUptime(); } /** * Get CPU time (converted from clock ticks to nanoseconds). * @return current CPU time in nanoseconds */ public long getCpuTimeNanos() { return cpuTimeToNanos(cpu.getTime()); } /** * Get CPU time (converted from clock ticks to nanoseconds). * @param cpuTime CPU time (in clock ticks) to convert * @return CPU time in nanoseconds */ public long cpuTimeToNanos(long cpuTime) { return cpuTime * NANOSECS_IN_MSEC / clockFrequency; } /** * Get number of CPU clock ticks for given time in nanoseconds. * @param nanosecs time (in nanoseconds) to convert to CPU ticks * @return CPU ticks for given time */ public long nanosToCpuTime(long nanosecs) { return nanosecs * clockFrequency / NANOSECS_IN_MSEC; } /** * Check is time to sync CPU time with computer uptime. */ public void checkSyncUptime() { long cpuTimeUptimeDifference = cpu.getTime() - lastCpuTimeSyncTimestamp; if (cpuTimeUptimeDifference >= syncUptimeThresholdCpuTicks) { doSyncUptime(); doTimerTasks(); } } /** * Do timer tasks. */ public void doTimerTasks() { for (Device device : deviceList) { device.timer(uptime); } } /** * Sync CPU time with computer uptime. */ public void doSyncUptime() { long timestamp = System.nanoTime(); uptime += timestamp - lastUptimeSyncTimestamp; lastUptimeSyncTimestamp = timestamp; lastCpuTimeSyncTimestamp = cpu.getTime(); long uptimeCpuTimeDifference = getCpuTimeNanos() - uptime; uptimeCpuTimeDifference = (uptimeCpuTimeDifference > 0) ? uptimeCpuTimeDifference : 1L; long uptimeCpuTimeDifferenceMillis = uptimeCpuTimeDifference / NANOSECS_IN_MSEC; int uptimeCpuTimeDifferenceNanos = (int) (uptimeCpuTimeDifference % NANOSECS_IN_MSEC); try { this.wait(uptimeCpuTimeDifferenceMillis, uptimeCpuTimeDifferenceNanos); } catch (InterruptedException e) { } } @Override public void run() { Log.d(TAG, "computer started"); synchronized (this) { this.notifyAll(); while (isRunning) { if (isPaused) { try { Log.d(TAG, "computer paused"); this.wait(); } catch (InterruptedException e) { } Log.d(TAG, "computer resumed"); } else { cpu.executeNextOperation(); checkSyncUptime(); } } } Log.d(TAG, "computer stopped"); } }