Block device peripheral and SBFS file system implemented.
This commit is contained in:
@@ -270,6 +270,38 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
||||
// CALL - Push the Program Counter to the Stack, and perform an immediate branch.
|
||||
genericCall(cpu);
|
||||
break;
|
||||
case 0x1A:
|
||||
// BNQ - Branch if Q is not 0.
|
||||
if(cpu->Q != 0) {
|
||||
genericBranch(cpu);
|
||||
} else {
|
||||
cpu->ProgramCounter+=2;
|
||||
}
|
||||
break;
|
||||
case 0x1B:
|
||||
// BNA - Branch if A is not 0.
|
||||
if(cpu->A != 0) {
|
||||
genericBranch(cpu);
|
||||
} else {
|
||||
cpu->ProgramCounter+=2;
|
||||
}
|
||||
break;
|
||||
case 0x1C:
|
||||
// BNB - Branch if B is not 0.
|
||||
if(cpu->B != 0) {
|
||||
genericBranch(cpu);
|
||||
} else {
|
||||
cpu->ProgramCounter+=2;
|
||||
}
|
||||
break;
|
||||
case 0x1D:
|
||||
// BNC - Branch if the Carry Flag is clear.
|
||||
if (!(cpu->Status & STATUS_CARRY)) {
|
||||
genericBranch(cpu);
|
||||
} else {
|
||||
cpu->ProgramCounter+=2;
|
||||
}
|
||||
break;
|
||||
case 0x18: {
|
||||
// SWI - Software Interrupt. The byte after the opcode names the vector.
|
||||
// Never masked: this is an instruction the program deliberately ran, not
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <stdlib.h>
|
||||
#include "cpu.h"
|
||||
#include "controller.h"
|
||||
#include "io.h"
|
||||
#include "utility.h"
|
||||
#include <string.h>
|
||||
#include <getopt.h>
|
||||
@@ -90,6 +91,9 @@ int main (int argc, char *argv[]) {
|
||||
fprintf(stderr, "Error: Couldn't read file: %s\n", programFile);
|
||||
return 1;
|
||||
}
|
||||
if (options.disk != NULL && attachDisk(options.disk, options.writeProtect)) {
|
||||
return 1;
|
||||
}
|
||||
CPURegisters cpu;
|
||||
// The controller has to know where the memories are before anything can reach
|
||||
// them through it. Banks 0 and 1 are those two arrays.
|
||||
@@ -136,6 +140,7 @@ int main (int argc, char *argv[]) {
|
||||
printf("Cycle: %lu\n", cycleCount);
|
||||
}
|
||||
}
|
||||
detachDisk();
|
||||
if (limitReached) {
|
||||
printf("Execution stopped after %lu cycles. (cycle limit reached)\n", cycleCount);
|
||||
} else if (cpu.Status & STATUS_FAULT) {
|
||||
|
||||
+126
-4
@@ -63,6 +63,109 @@ uint8_t refusingPort(void) {
|
||||
return refusedPort;
|
||||
}
|
||||
|
||||
// ---- The disk ----
|
||||
//
|
||||
// A block device and nothing more. It knows numbered blocks and has never heard of a
|
||||
// file, which is the whole point: a filesystem is software this machine will run, not
|
||||
// something the host does on its behalf. A disk that understood filenames would be the
|
||||
// emulator doing the work and the machine pretending it had.
|
||||
|
||||
static FILE *diskImage = NULL;
|
||||
static uint32_t diskBlockCount = 0;
|
||||
static uint8_t diskBuffer[DISK_BLOCK_BYTES];
|
||||
static uint16_t diskBlock = 0;
|
||||
static uint8_t diskStatus = 0;
|
||||
static uint8_t diskProtected = 0;
|
||||
|
||||
uint8_t attachDisk(const char *path, uint8_t writeProtect) {
|
||||
diskProtected = writeProtect ? 1 : 0;
|
||||
diskImage = fopen(path, "r+b");
|
||||
if (diskImage == NULL) {
|
||||
// It may be there and simply not writable, which is a read only disk rather than
|
||||
// a missing one. Try that before deciding to make a new one.
|
||||
diskImage = fopen(path, "rb");
|
||||
if (diskImage != NULL) {
|
||||
diskProtected = 1;
|
||||
}
|
||||
}
|
||||
if (diskImage == NULL) {
|
||||
// Nothing there, so make one. A fresh image is zeroes, which is what an unwritten
|
||||
// block should read as.
|
||||
diskImage = fopen(path, "w+b");
|
||||
if (diskImage == NULL) {
|
||||
fprintf(stderr, "Error: Couldn't open or create the disk image: %s\n", path);
|
||||
return 1;
|
||||
}
|
||||
static const uint8_t empty[DISK_BLOCK_BYTES] = {0};
|
||||
for (uint32_t i = 0; i < DISK_DEFAULT_BLOCKS; i++) {
|
||||
if (fwrite(empty, 1, DISK_BLOCK_BYTES, diskImage) != DISK_BLOCK_BYTES) {
|
||||
fprintf(stderr, "Error: Couldn't write the disk image: %s\n", path);
|
||||
fclose(diskImage);
|
||||
diskImage = NULL;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fseek(diskImage, 0, SEEK_END) != 0) {
|
||||
fprintf(stderr, "Error: Couldn't measure the disk image: %s\n", path);
|
||||
fclose(diskImage);
|
||||
diskImage = NULL;
|
||||
return 1;
|
||||
}
|
||||
long size = ftell(diskImage);
|
||||
// A part written block at the end is not a block, so it is not counted.
|
||||
diskBlockCount = (size > 0) ? (uint32_t)(size / DISK_BLOCK_BYTES) : 0;
|
||||
// The protect bit is a standing property, so it reads true before anything has been
|
||||
// asked of the disk rather than only after a write has been turned away.
|
||||
diskStatus = diskProtected ? DISK_STATUS_PROTECTED : 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void detachDisk(void) {
|
||||
if (diskImage != NULL) {
|
||||
fclose(diskImage);
|
||||
diskImage = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
// Reads or writes the block the block registers name. The line goes up either way: the
|
||||
// operation finished, and whether it worked is what Status is for.
|
||||
static void diskCommand(uint8_t command) {
|
||||
// The protect bit describes the disk rather than the operation, so it survives.
|
||||
diskStatus = diskProtected ? DISK_STATUS_PROTECTED : 0;
|
||||
if (command == DISK_COMMAND_WRITE && diskProtected) {
|
||||
diskStatus |= DISK_STATUS_ERROR;
|
||||
raiseInterrupt(PORT_DISK);
|
||||
return;
|
||||
}
|
||||
if (diskImage == NULL || diskBlock >= diskBlockCount) {
|
||||
diskStatus |= DISK_STATUS_ERROR;
|
||||
raiseInterrupt(PORT_DISK);
|
||||
return;
|
||||
}
|
||||
long offset = (long)diskBlock * DISK_BLOCK_BYTES;
|
||||
if (fseek(diskImage, offset, SEEK_SET) != 0) {
|
||||
diskStatus |= DISK_STATUS_ERROR;
|
||||
raiseInterrupt(PORT_DISK);
|
||||
return;
|
||||
}
|
||||
size_t moved = 0;
|
||||
if (command == DISK_COMMAND_READ) {
|
||||
moved = fread(diskBuffer, 1, DISK_BLOCK_BYTES, diskImage);
|
||||
} else if (command == DISK_COMMAND_WRITE) {
|
||||
moved = fwrite(diskBuffer, 1, DISK_BLOCK_BYTES, diskImage);
|
||||
fflush(diskImage);
|
||||
} else {
|
||||
diskStatus |= DISK_STATUS_ERROR;
|
||||
raiseInterrupt(PORT_DISK);
|
||||
return;
|
||||
}
|
||||
if (moved != DISK_BLOCK_BYTES) {
|
||||
diskStatus |= DISK_STATUS_ERROR;
|
||||
}
|
||||
raiseInterrupt(PORT_DISK);
|
||||
}
|
||||
|
||||
// ---- A device that brings memory ----
|
||||
//
|
||||
// The simplest thing that owns a bank. Writing to its port fills its memory with the
|
||||
@@ -75,11 +178,18 @@ uint8_t refusingPort(void) {
|
||||
static uint8_t deviceMemoryBlock[DEVICE_MEMORY_BYTES];
|
||||
|
||||
uint8_t *deviceMemory(uint8_t port, uint32_t *capacity) {
|
||||
if (port != PORT_MEMORY) {
|
||||
return NULL;
|
||||
if (port == PORT_MEMORY) {
|
||||
*capacity = DEVICE_MEMORY_BYTES;
|
||||
return deviceMemoryBlock;
|
||||
}
|
||||
*capacity = DEVICE_MEMORY_BYTES;
|
||||
return deviceMemoryBlock;
|
||||
if (port == PORT_DISK) {
|
||||
// The disk's buffer is one block. Reading fills it and writing takes what is in
|
||||
// it, and the only way to reach it is to register it as a bank and go through the
|
||||
// controller.
|
||||
*capacity = DISK_BLOCK_BYTES;
|
||||
return diskBuffer;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// ---- The bus registry ----
|
||||
@@ -104,6 +214,7 @@ static const DeviceRecord deviceTable[] = {
|
||||
{ PORT_TEST, DEVICE_TEST, 0 },
|
||||
{ PORT_REFUSE, DEVICE_REFUSE, 0 },
|
||||
{ PORT_MEMORY, DEVICE_MEMORY, DEVICE_FLAG_HAS_MEMORY },
|
||||
{ PORT_DISK, DEVICE_DISK, DEVICE_FLAG_HAS_MEMORY },
|
||||
{ PORT_REGISTRY, DEVICE_REGISTRY, 0 },
|
||||
};
|
||||
static const int deviceCount = (int)(sizeof(deviceTable) / sizeof(deviceTable[0]));
|
||||
@@ -122,6 +233,11 @@ static const DeviceRecord *deviceOnPort(uint8_t port) {
|
||||
if (port >= CONTROLLER_PORT_BASE && port <= CONTROLLER_PORT_TOP) {
|
||||
return &controllerRecord;
|
||||
}
|
||||
if (port > PORT_DISK && port <= PORT_DISK_TOP) {
|
||||
// The base port is in the table proper, since that is the one that owns the
|
||||
// memory and raises the line. The rest of the block reports the same device.
|
||||
return deviceOnPort(PORT_DISK);
|
||||
}
|
||||
for (int i = 0; i < deviceCount; i++) {
|
||||
if (deviceTable[i].port == port) {
|
||||
return &deviceTable[i];
|
||||
@@ -159,6 +275,9 @@ uint8_t OutputHandler(uint8_t DataByte, uint8_t Address) {
|
||||
// Later, I'll want to use a buffer for this for performance, probably.
|
||||
putchar(DataByte);
|
||||
break;
|
||||
case DISK_BLOCK_HIGH: diskBlock = (uint16_t)(DataByte << 8) | (diskBlock & 0x00FF); break;
|
||||
case DISK_BLOCK_LOW: diskBlock = (diskBlock & 0xFF00) | DataByte; break;
|
||||
case DISK_COMMAND: diskCommand(DataByte); break;
|
||||
case PORT_MEMORY:
|
||||
// Fills the memory this device owns with the byte written. Nothing is
|
||||
// reachable from here: to get at it, register it as a bank and go through
|
||||
@@ -204,6 +323,9 @@ uint8_t InputHandler(uint8_t Address) {
|
||||
// If data is sent here, it should be read from STDIN.
|
||||
return getchar();
|
||||
break;
|
||||
case DISK_BLOCK_HIGH: return (uint8_t)(diskBlock >> 8);
|
||||
case DISK_BLOCK_LOW: return (uint8_t)(diskBlock & 0xFF);
|
||||
case DISK_STATUS: return diskStatus;
|
||||
case PORT_REFUSE:
|
||||
// Refuses reads as well, so both directions are covered.
|
||||
refuseAccess(VECTOR_GUARD_VIOLATION);
|
||||
|
||||
@@ -18,6 +18,16 @@
|
||||
#define PORT_TEST 0x10
|
||||
#define PORT_REFUSE 0x11
|
||||
#define PORT_MEMORY 0x12
|
||||
|
||||
// The disk answers on a block of four ports and interrupts on the first of them. A device
|
||||
// that spans more than one port raises its line on its base, which is the rule the
|
||||
// machine has not needed until now: the controller spans sixteen and never interrupts.
|
||||
#define PORT_DISK 0x20
|
||||
#define PORT_DISK_TOP 0x23
|
||||
#define DISK_BLOCK_HIGH 0x20
|
||||
#define DISK_BLOCK_LOW 0x21
|
||||
#define DISK_COMMAND 0x22
|
||||
#define DISK_STATUS 0x23
|
||||
#define PORT_REGISTRY 0xFF
|
||||
|
||||
// ---- Device classes ----
|
||||
@@ -35,11 +45,49 @@
|
||||
#define DEVICE_TEST 0x10
|
||||
#define DEVICE_REFUSE 0x11
|
||||
#define DEVICE_MEMORY 0x12
|
||||
#define DEVICE_DISK 0x13
|
||||
|
||||
// What a device brings besides itself. This means memory that somebody has to register
|
||||
// with the controller, so the controller's own bank 2 does not count: it is already there.
|
||||
#define DEVICE_FLAG_HAS_MEMORY 0x01
|
||||
|
||||
// ---- The disk ----
|
||||
//
|
||||
// Blocks are a page each, so a block number is the whole of a 16 bit address and the
|
||||
// arithmetic never needs a multiply. Sixteen megabytes is absurd for this machine, which
|
||||
// is the point: there is room for anything a filesystem might want to grow into later.
|
||||
|
||||
#define DISK_BLOCK_BYTES 256
|
||||
|
||||
// A fresh image is made the size of Program and Data together, which is a round number
|
||||
// for this machine and small enough to read in a hex editor while it is being built.
|
||||
#define DISK_DEFAULT_BLOCKS 512
|
||||
|
||||
#define DISK_COMMAND_READ 0x01
|
||||
#define DISK_COMMAND_WRITE 0x02
|
||||
|
||||
// Set while an operation is still going. It always reads clear here, because the host
|
||||
// finishes before the next instruction does, but a machine with a slower disk would set
|
||||
// it and a program that ignores it would break there. Honour it anyway.
|
||||
#define DISK_STATUS_BUSY 0x01
|
||||
// Set when the disk cannot be written at all. Unlike the two bits above it, this is not
|
||||
// about the last operation: it is a standing property of the medium, readable before
|
||||
// anything is attempted. A write protected disk is barred here, in the device, rather
|
||||
// than by anything in the filesystem, so writing blocks directly cannot get around it.
|
||||
#define DISK_STATUS_PROTECTED 0x04
|
||||
|
||||
// Set when the last operation did not work: no image, or a block that is not on it.
|
||||
// A disk that cannot read a block is an ordinary thing that happens to working programs,
|
||||
// so it says so rather than stopping the machine.
|
||||
#define DISK_STATUS_ERROR 0x02
|
||||
|
||||
// Attaches an image, making one if it is not there. A disk is read only if the host will
|
||||
// not let the file be written, or if writeProtect asks for it, which is the emulated
|
||||
// equivalent of the tab on the side of a floppy. Returns 1 if it could not attach.
|
||||
uint8_t attachDisk(const char *path, uint8_t writeProtect);
|
||||
|
||||
void detachDisk(void);
|
||||
|
||||
// How many bytes a device's entry in the registry runs to. Reading past the end gives
|
||||
// zero, so the record can grow later without anything already written having to change.
|
||||
#define DEVICE_RECORD_BYTES 2
|
||||
|
||||
@@ -17,6 +17,9 @@ void printHelp(const char *programName) {
|
||||
printf(" -d, --debug Enable debug mode.\n");
|
||||
printf(" -c, --cycles N Stop after N cycles instead of running until the program halts.\n");
|
||||
printf(" -f, --fast Run as fast as possible, ignoring the emulated cycle rate.\n");
|
||||
printf(" -D, --disk FILE Attach a disk image, making one if it is not there.\n");
|
||||
printf(" -W, --write-protect Attach the disk read only. A disk the host will not let\n");
|
||||
printf(" you write is read only whether you ask for this or not.\n");
|
||||
printf(" -h, --help Display this help message.\n");
|
||||
}
|
||||
|
||||
@@ -25,6 +28,8 @@ uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options) {
|
||||
{"debug", no_argument, 0, 'd'},
|
||||
{"cycles", required_argument, 0, 'c'},
|
||||
{"fast", no_argument, 0, 'f'},
|
||||
{"disk", required_argument, 0, 'D'},
|
||||
{"write-protect", no_argument, 0, 'W'},
|
||||
{"help", no_argument, 0, 'h'},
|
||||
{0, 0, 0, 0 }
|
||||
};
|
||||
@@ -34,9 +39,11 @@ uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options) {
|
||||
options->debug = 0;
|
||||
options->fast = 0;
|
||||
options->cycles = 0;
|
||||
options->disk = NULL;
|
||||
options->writeProtect = 0;
|
||||
|
||||
// Parse options
|
||||
while ((opt = getopt_long(argc, argv, "dc:fh", long_options, &option_index)) != -1) {
|
||||
while ((opt = getopt_long(argc, argv, "dc:fhD:W", long_options, &option_index)) != -1) {
|
||||
switch (opt) {
|
||||
case 'd':
|
||||
options->debug = 1;
|
||||
@@ -57,6 +64,12 @@ uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options) {
|
||||
case 'f':
|
||||
options->fast = 1;
|
||||
break;
|
||||
case 'D':
|
||||
options->disk = optarg;
|
||||
break;
|
||||
case 'W':
|
||||
options->writeProtect = 1;
|
||||
break;
|
||||
case 'h':
|
||||
printHelp(argv[0]);
|
||||
return OPTIONS_HELP;
|
||||
|
||||
@@ -19,6 +19,8 @@ typedef struct {
|
||||
uint8_t debug; // Step one instruction at a time, printing the registers.
|
||||
uint8_t fast; // Ignore the cycle rate and run as fast as the host allows.
|
||||
unsigned long cycles; // Stop after this many cycles. Zero means run until the program halts.
|
||||
const char *disk; // Disk image to attach, or NULL for a machine with no disk.
|
||||
uint8_t writeProtect; // Attach the disk read only, the way a tab on a floppy would.
|
||||
} EmulatorOptions;
|
||||
|
||||
uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options);
|
||||
|
||||
Reference in New Issue
Block a user