/** * Copyright 2012 Tobias Gierke <tobias.gierke@code-sourcery.de> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package de.codesourcery.jasm16.emulator.devices.impl; import java.io.IOException; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.LockSupport; import de.codesourcery.jasm16.Address; import de.codesourcery.jasm16.Register; import de.codesourcery.jasm16.emulator.ICPU; import de.codesourcery.jasm16.emulator.IEmulator; import de.codesourcery.jasm16.emulator.IEmulatorInvoker; import de.codesourcery.jasm16.emulator.devices.DeviceDescriptor; import de.codesourcery.jasm16.emulator.devices.HardwareInterrupt; import de.codesourcery.jasm16.emulator.devices.IDevice; import de.codesourcery.jasm16.emulator.exceptions.DeviceErrorException; import de.codesourcery.jasm16.emulator.memory.IMemory; import de.codesourcery.jasm16.utils.Misc; /** * Floppy disk drive. * * <p>The Mackapar 3.5" Floppy Drive is compatible with all standard 3.5" 1440 KB * floppy disks. The floppies need to be formatted in 16 bit mode, for a total of * 737,280 words of storage. Data is saved on 80 tracks with 18 sectors per track, * for a total of 1440 sectors containing 512 words each. * The M35FD works is asynchronous, and has a raw read/write speed of 30.7kw/s. * Track seeking time is about 2.4 ms per track.</p> * * @author tobias.gierke@code-sourcery.de */ public class DefaultFloppyDrive implements IDevice { /** * Emulation read speed. */ public static final int SECTORS_PER_SECOND = 60; /** * Emulation seek speed. */ public static final float SEEK_TIME_IN_MS_PER_TRACK = 2.4f; /** * Name: Mackapar 3.5" Floppy Drive (M35FD) * ID: 0x4fd524c5, version: 0x000b * Manufacturer: 0x1eb37e91 (MACKAPAR) */ public static final DeviceDescriptor DESC = new DeviceDescriptor("Mackapar 3.5\" Floppy Drive (M35FD)" , "Floppy disk drive", 0x4fd524c5,0x000b,0x1eb37e91); private volatile boolean interruptsEnabled = false; private volatile int interruptMessage = 0; private final Object WORKER_THREAD_LOCK = new Object(); // @GuardedBy( WORKER_THREAD_LOCK ) private WorkerThread workerThread; // @GuardedBy( WORKER_THREAD_LOCK ) private TimerThread timerThread; private final Object DISK_LOCK = new Object(); // @GuardedBy( DISK_LOCK ) private FloppyDisk disk = null; // @GuardedBy( DISK_LOCK ) private StatusCode status = StatusCode.NO_MEDIA; // @GuardedBy( DISK_LOCK ) private ErrorCode error = ErrorCode.NONE; private volatile IEmulator emulator; private volatile boolean runAtMaxSpeed = false; private final AtomicInteger sectorsLeftThisSecond = new AtomicInteger(SECTORS_PER_SECOND); protected static enum CommandType { TERMINATE, READ, WRITE, DISK_CHANGED; } protected static abstract class DriveCommand { private final CommandType type; protected DriveCommand(CommandType type) { this.type = type; } public final CommandType getType() { return type; } public final boolean hasType(CommandType t) { return t.equals( type ); } } protected static final class ReadCommand extends DriveCommand { private final int sector; private final Address targetMemoryAddress; protected ReadCommand(int sector,Address targetMemoryAddress) { super(CommandType.READ); this.sector=sector; this.targetMemoryAddress = targetMemoryAddress; } public int getSector() { return sector; } public Address getTargetMemoryAddress() { return targetMemoryAddress; } @Override public String toString() { return "READ_SECTOR[ sector = "+sector+" , target memory = "+Misc.toHexString( targetMemoryAddress ); } } protected static final class WriteCommand extends DriveCommand { private final int sector; private final Address sourceMemoryAddress; protected WriteCommand(int sector,Address sourceMemoryAddress) { super(CommandType.WRITE); this.sector=sector; this.sourceMemoryAddress = sourceMemoryAddress; } public int getSector() { return sector; } public Address getSourceMemoryAddress() { return sourceMemoryAddress; } @Override public String toString() { return "WRITE_SECTOR[ sector = "+sector+" , target memory = "+Misc.toHexString( sourceMemoryAddress ); } } protected static final class TerminateCommand extends DriveCommand { protected TerminateCommand() { super(CommandType.TERMINATE); } @Override public String toString() { return "TERMINATE"; } } protected static final class DiskChangedCommand extends DriveCommand { protected DiskChangedCommand() { super(CommandType.DISK_CHANGED); } @Override public String toString() { return "DISK_CHANGED"; } } protected final class TimerThread extends Thread { private volatile boolean terminate = false; public TimerThread() { setDaemon( true ); setName("floppy-timer-thread"); } @Override public void run() { while ( ! terminate ) { sectorsLeftThisSecond.set( SECTORS_PER_SECOND ); try { Thread.sleep( 1000 ); } catch(InterruptedException e) { // can't help it } } } public void terminate() { this.terminate = true; } } public DefaultFloppyDrive(boolean runAtMaxSpeed) { this.runAtMaxSpeed = runAtMaxSpeed; } protected WorkerThread getWorkerThread() { WorkerThread result = null; boolean workerStarted = false; synchronized(WORKER_THREAD_LOCK) { if ( workerThread == null || ! workerThread.isAlive() ) { WorkerThread tmp = new WorkerThread(); tmp.start(); workerThread = tmp; workerStarted = true; } result = workerThread; } if ( workerStarted ) { logDebug("Worker thread started."); } return result; } public FloppyDisk getDisk() { synchronized(DISK_LOCK ) { return disk; } } public void setRunAtMaxSpeed(boolean runAtMaxSpeed) { this.runAtMaxSpeed = runAtMaxSpeed; } public void setDisk(FloppyDisk disk) { if ( disk == null ) { throw new IllegalArgumentException("disk must not be null"); } synchronized(DISK_LOCK ) { this.disk = disk; } logDebug( "Disk inserted: "+disk); if ( emulator != null ) { getWorkerThread().diskChanged(); } } public void eject() { boolean diskChanged = false; synchronized(DISK_LOCK ) { if ( disk != null ) { disk = null; diskChanged = true; } } if ( diskChanged ) { logDebug("Disk ejected"); if ( emulator != null ) { getWorkerThread().diskChanged(); } } } private void updateStatus(StatusCode status,ErrorCode errorCode) { boolean statusChanged; synchronized(DISK_LOCK ) { statusChanged = this.status != status; if ( errorCode != null && errorCode != this.error ) { statusChanged = true; this.error=errorCode; } this.status = status; } if ( statusChanged ) { logDebug("New status: "+status+" / error: "+errorCode); if ( interruptsEnabled ) { emulator.triggerInterrupt( new HardwareInterrupt( this , interruptMessage ) ); } } } protected void stopWorkerThread() { logDebug("Stopping worker thread"); synchronized(WORKER_THREAD_LOCK) { if ( workerThread != null && workerThread.isAlive() ) { try { workerThread.terminate(); } finally { workerThread = null; } } } } protected final class WorkerThread extends Thread { private final Object SLEEP_LOCK = new Object(); private final BlockingQueue<DriveCommand> commandQueue = new ArrayBlockingQueue<DriveCommand>(1); private final CountDownLatch terminated = new CountDownLatch(1); // @GuardedBy( DISK_LOCK ) private int currentHeadPosition = 0; // sector the disk's read/write head is currently at public WorkerThread() { setDaemon(true); setName("floppy-disk-worker-thread"); } public void diskChanged() { sendCommand( new DiskChangedCommand() , true ); } private DriveCommand takeCommand() { while(true) { try { return commandQueue.take(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } @Override public void run() { logDebug("Worker thread is running."); try { while ( true ) { DriveCommand command = null; while( true ) { command = commandQueue.peek(); if ( command != null && ! command.hasType(CommandType.DISK_CHANGED ) ) { logDebug("Worker thread received command "+command); break; } if ( command != null ) { // disk changed , reset head position logDebug("Disk changed, reset head position."); takeCommand(); currentHeadPosition = 0; } setIdleStatus(null); while(true) { try { synchronized(SLEEP_LOCK) { SLEEP_LOCK.wait(1000); // sleep for 1 second } break; } catch(InterruptedException e) { Thread.currentThread().interrupt(); } } } if ( command.hasType( CommandType.TERMINATE ) ) { takeCommand(); break; } ErrorCode newErrorCode=ErrorCode.NONE; try { updateStatus(StatusCode.BUSY,ErrorCode.NONE); newErrorCode = processCommand( command ); } catch(IOException e) { logError("Command "+command+" failed",e); newErrorCode = ErrorCode.BAD_SECTOR; } catch(Exception e) { logError("Command "+command+" failed",e); newErrorCode = ErrorCode.BROKEN; } finally { takeCommand(); setIdleStatus(newErrorCode); } } } finally { try { logDebug("Disk worker thread terminated."); } finally { terminated.countDown(); } } } private void setIdleStatus(ErrorCode errorCode) { synchronized( DISK_LOCK ) { if ( disk == null ) { updateStatus(StatusCode.NO_MEDIA,errorCode); } else { updateStatus( disk.isWriteProtected() ? StatusCode.READY_WP : StatusCode.READY , errorCode ); } } } private ErrorCode processCommand(DriveCommand cmd) throws IOException { logDebug("Executing command "+cmd); switch( cmd.getType() ) { case READ: /* READ */ final ReadCommand readCmd = (ReadCommand) cmd; moveHead( readCmd.getSector() ); // moveHead() calls Object#sleep() , do NOT use in synchronized block synchronized( DISK_LOCK ) { if ( disk != null ) { enforceSpeed(); final IOException outcome = emulator.doWithEmulator( new IEmulatorInvoker<IOException>() { @Override public IOException doWithEmulator(IEmulator emulator, ICPU cpu, IMemory memory) { try { disk.readSector( readCmd.getSector() , memory , readCmd.getTargetMemoryAddress() ); } catch (IOException e) { return e; } return null; } }); if ( outcome != null ) { throw outcome; } return ErrorCode.NONE; } } return ErrorCode.NO_MEDIA; case WRITE: /* WRITE */ final WriteCommand writeCmd = (WriteCommand) cmd; moveHead( writeCmd.getSector() ); // moveHead() calls Object#sleep() , do NOT use in synchronized block boolean writeProtected=false; synchronized( DISK_LOCK ) { if ( disk != null ) { if ( ! disk.isWriteProtected() ) { enforceSpeed(); final IOException outcome = emulator.doWithEmulator( new IEmulatorInvoker<IOException>() { @Override public IOException doWithEmulator(IEmulator emulator, ICPU cpu, IMemory memory) { try { disk.writeSector( writeCmd.getSector() , memory , writeCmd.getSourceMemoryAddress() ); } catch (IOException e) { return e; } return null; } }); if ( outcome != null ) { throw outcome; } return ErrorCode.NONE; } writeProtected = true; } } return writeProtected ? ErrorCode.PROTECTED : ErrorCode.NO_MEDIA; default: throw new RuntimeException("Internal error,unhandled command type "+cmd); } } private void enforceSpeed() { if ( runAtMaxSpeed ) { return; } while( sectorsLeftThisSecond.get() <= 0 ) { try { Thread.sleep(10); } catch(Exception e) { } } sectorsLeftThisSecond.decrementAndGet(); } private void moveHead(int sector) { synchronized( DISK_LOCK ) { if ( currentHeadPosition != sector ) { final int delta = Math.abs( currentHeadPosition - sector ); if ( ! runAtMaxSpeed ) { // 1 ns = 10^-9 seconds // 1 ms = 10^-3 seconds LockSupport.parkNanos( (long) SEEK_TIME_IN_MS_PER_TRACK * delta * 1000000 ); // 2.8ms per track } currentHeadPosition = sector; } } } public boolean readSector(int sector,Address targetMemoryAddress) { return sendCommand( new ReadCommand(sector,targetMemoryAddress ) , false ); } public boolean writeSector(int sector,Address targetMemoryAddress) { return sendCommand( new WriteCommand(sector,targetMemoryAddress ) , false ); } private boolean sendCommand(DriveCommand cmd,boolean wait) { boolean result; while( true ) { try { if ( wait ) { commandQueue.put( cmd ); result = true; } else { result = commandQueue.offer( cmd ); } break; } catch (InterruptedException e1) { Thread.currentThread().interrupt(); } } if ( result ) { synchronized (SLEEP_LOCK) { SLEEP_LOCK.notifyAll(); } } return result; } public void terminate() { sendCommand( new TerminateCommand() , true ); while ( true ) { try { terminated.await(); break; } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } } /** * Drive status codes. * * @author tobias.gierke@code-sourcery.de */ public static enum StatusCode { NO_MEDIA( 0x0000 , "There's no floppy in the drive."), READY( 0x0001 , "The drive is ready to accept commands."), READY_WP( 0x0002 , "Same as ready, except the floppy is write protected."), BUSY( 0x0003 , "The drive is busy either reading or writing a sector."); private final int code; private final String description; private StatusCode(int code, String description) { this.code = code; this.description = description; } public int getCode() { return code; } public String getDescription() { return description; } } /** * Error codes. * * @author tobias.gierke@code-sourcery.de */ public static enum ErrorCode { NONE( 0x0000 ,"There's been no error since the last poll."), BUSY( 0x0001 ,"Drive is busy performing an action"), NO_MEDIA( 0x0002 ,"Attempted to read or write with no floppy inserted."), PROTECTED( 0x0003 ,"Attempted to write to write protected floppy."), EJECT( 0x0004 ,"The floppy was removed while reading or writing."), BAD_SECTOR(0x0005 ,"The requested sector is broken, the data on it is lost."), BROKEN( 0xffff ,"There's been some major software or hardware problem,try turning off and turning on the device again."); private final int code; private final String description; private ErrorCode(int code, String description) { this.code = code; this.description = description; } public int getCode() { return code; } public String getDescription() { return description; } } @Override public void afterAddDevice(IEmulator emulator) { synchronized( WORKER_THREAD_LOCK ) { if ( timerThread == null || ! timerThread.isAlive() ) { timerThread = new TimerThread(); timerThread.start(); } } this.emulator = emulator; } @Override public void beforeRemoveDevice(IEmulator emulator) { try { stopWorkerThread(); } finally { try { synchronized( WORKER_THREAD_LOCK ) { timerThread.terminate(); timerThread = null; } } finally { this.emulator = null; } } } @Override public void reset() { interruptsEnabled = false; interruptMessage = 0; synchronized( DISK_LOCK ) { error = ErrorCode.NONE; if ( disk == null ) { status = StatusCode.NO_MEDIA; } else if ( disk.isWriteProtected() ) { status = StatusCode.READY_WP; } else { status = StatusCode.READY; } } } @Override public boolean supportsMultipleInstances() { return false; } @Override public DeviceDescriptor getDeviceDescriptor() { return DESC; } private void logError(String msg) { if ( emulator != null ) { emulator.getOutput().error( msg ); } } private void logError(String msg,Throwable t) { if ( emulator != null ) { emulator.getOutput().error( msg , t ); } } private void logDebug(String msg) { if ( emulator != null ) { emulator.getOutput().debug( msg ); } } @Override public int handleInterrupt(IEmulator emulator, ICPU cpu, IMemory memory) { final int msg= cpu.getRegisterValue( Register.A ); switch( msg) { case 0: /* 0 Poll device. * Sets B to the current state (see below) and C to the last error * since the last device poll. */ logDebug("Device status polled"); cpu.setRegisterValue(Register.B , status.getCode() ); cpu.setRegisterValue(Register.C , error.getCode() ); synchronized(DISK_LOCK) { error = ErrorCode.NONE; } break; case 1: /* * 1 Set interrupt. Enables interrupts and sets the message to X if X is anything * other than 0, disables interrupts if X is 0. When interrupts are enabled, * the M35FD will trigger an interrupt on the DCPU-16 whenever the state or * error message changes. */ int irqMsg = cpu.getRegisterValue(Register.X); if ( irqMsg == 0 ) { logDebug("Interrupts disabled."); interruptMessage = 0; interruptsEnabled = false; } else { logDebug("Interrupts enabled with message "+irqMsg); interruptMessage = irqMsg ; interruptsEnabled = true; } break; case 2: /* 2 Read sector. Reads sector X to DCPU ram starting at Y. * Sets B to 1 if reading is possible and has been started, anything else if it * fails. Reading is only possible if the state is STATE_READY or * STATE_READY_WP. * Protects against partial reads. */ final Address targetAddress = Address.wordAddress( cpu.getRegisterValue( Register.Y ) ); final int readSector = cpu.getRegisterValue(Register.X); logDebug("Read request for sector #"+readSector+" , store at "+targetAddress); if ( ! isValidSector( readSector ) ) { logError("Invalid sector number "+readSector); throw new DeviceErrorException("Invalid sector number "+readSector,DefaultFloppyDrive.this); } if ( getWorkerThread().readSector( readSector , targetAddress ) ) { cpu.setRegisterValue(Register.B , 1 ); } else { cpu.setRegisterValue(Register.B , 0 ); } break; case 3: /* 3 Write sector. Writes sector X from DCPU ram starting at Y. * Sets B to 1 if writing is possible and has been started, anything else if it * fails. Writing is only possible if the state is STATE_READY. * Protects against partial writes. */ final Address sourceAddress = Address.wordAddress( cpu.getRegisterValue( Register.Y ) ); final int writeSector = cpu.getRegisterValue(Register.X); logDebug("Write request for sector #"+writeSector+" , read from "+sourceAddress); if ( ! isValidSector( writeSector ) ) { logError("Invalid sector number "+writeSector); throw new DeviceErrorException("Invalid sector number "+writeSector,DefaultFloppyDrive.this); } if ( getWorkerThread().writeSector( writeSector , sourceAddress ) ) { cpu.setRegisterValue(Register.B , 1 ); } else { cpu.setRegisterValue(Register.B , 0 ); } break; default: logError("Received unknown interrupt message: "+msg); throw new DeviceErrorException("Received unknown interrupt message: "+msg,DefaultFloppyDrive.this); } return 0; } private boolean isValidSector(int sector) { synchronized (DISK_LOCK) { if ( disk != null ) { return sector < disk.getSectorCount(); } return true; } } }