Block device peripheral and SBFS file system implemented.
This commit is contained in:
@@ -29,6 +29,13 @@ Instruction instruction_set[] = {
|
||||
{0x13, "BRB"},
|
||||
{0x14, "BRC"},
|
||||
{0x15, "BRD"},
|
||||
// The same four conditions the other way round. A quarter of the conditional
|
||||
// branches in the corpus were a branch over an unconditional one before these
|
||||
// existed, each of them needing a label invented only to be jumped past.
|
||||
{0x1A, "BNQ"},
|
||||
{0x1B, "BNA"},
|
||||
{0x1C, "BNB"},
|
||||
{0x1D, "BNC"},
|
||||
{0x17, "CALL"},
|
||||
{0x18, "SWI"},
|
||||
{0x19, "RETI"},
|
||||
|
||||
@@ -335,7 +335,8 @@ static void checkOperands(intermediateElement *intermediateArray, int arraySize,
|
||||
// the branch block takes an address: RET has none, and BRD gets its destination
|
||||
// from a Data Pointer instead of from the program.
|
||||
if (opcode == 0x10 || opcode == 0x11 || opcode == 0x12 ||
|
||||
opcode == 0x13 || opcode == 0x14 || opcode == 0x17) {
|
||||
opcode == 0x13 || opcode == 0x14 || opcode == 0x17 ||
|
||||
opcode == 0x1A || opcode == 0x1B || opcode == 0x1C || opcode == 0x1D) {
|
||||
// Branches and CALL take a two byte address, which only a label can supply.
|
||||
if (nextType != LABEL) problem = "Branch without label.";
|
||||
} else if ((opcode & 0xF0) == 0xD0 || (opcode & 0xF0) == 0xE0) {
|
||||
|
||||
@@ -0,0 +1,555 @@
|
||||
// SplitDisk.c
|
||||
// Makes and edits SplitBit disk images from the host.
|
||||
//
|
||||
// Until SplitBit can write its own filesystem there has to be some way to get a program
|
||||
// onto a disk, and this is it. It is not a shortcut around the machine: it speaks exactly
|
||||
// the format SplitBit will speak, so an image this makes is one the machine can read and
|
||||
// an image the machine writes is one this can read back.
|
||||
//
|
||||
// Written by Anachronaut
|
||||
|
||||
#include "sbfs.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
// Numbers on a SplitBit disk are most significant byte first, the same as everywhere
|
||||
// else on the machine.
|
||||
static uint16_t readWord(const uint8_t *at) {
|
||||
return (uint16_t)((at[0] << 8) | at[1]);
|
||||
}
|
||||
|
||||
static void writeWord(uint8_t *at, uint16_t value) {
|
||||
at[0] = (uint8_t)(value >> 8);
|
||||
at[1] = (uint8_t)(value & 0xFF);
|
||||
}
|
||||
|
||||
static FILE *openImage(const char *path, const char *mode) {
|
||||
FILE *image = fopen(path, mode);
|
||||
if (image == NULL) {
|
||||
fprintf(stderr, "Error: Couldn't open the disk image \"%s\".\n", path);
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
static int readBlock(FILE *image, uint16_t block, uint8_t *into) {
|
||||
if (fseek(image, (long)block * SBFS_BLOCK_BYTES, SEEK_SET) != 0
|
||||
|| fread(into, 1, SBFS_BLOCK_BYTES, image) != SBFS_BLOCK_BYTES) {
|
||||
fprintf(stderr, "Error: Couldn't read block %u.\n", block);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int writeBlock(FILE *image, uint16_t block, const uint8_t *from) {
|
||||
if (fseek(image, (long)block * SBFS_BLOCK_BYTES, SEEK_SET) != 0
|
||||
|| fwrite(from, 1, SBFS_BLOCK_BYTES, image) != SBFS_BLOCK_BYTES) {
|
||||
fprintf(stderr, "Error: Couldn't write block %u.\n", block);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
uint16_t diskBlocks;
|
||||
uint16_t directoryStart;
|
||||
uint16_t directoryBlocks;
|
||||
uint16_t freeBlocks;
|
||||
} Superblock;
|
||||
|
||||
// Reads block 0 and checks it really is one of ours. Without the magic a blank image and
|
||||
// a formatted one with no files would be the same thing.
|
||||
static int readSuperblock(FILE *image, Superblock *super) {
|
||||
uint8_t block[SBFS_BLOCK_BYTES];
|
||||
if (readBlock(image, 0, block)) {
|
||||
return 1;
|
||||
}
|
||||
if (memcmp(block, SBFS_MAGIC, SBFS_MAGIC_BYTES) != 0) {
|
||||
fprintf(stderr, "Error: That is not a SplitBit disk. Format it first.\n");
|
||||
return 1;
|
||||
}
|
||||
if (block[SBFS_SUPER_VERSION] != SBFS_VERSION) {
|
||||
fprintf(stderr, "Error: That disk is version %u, and this understands version %u.\n",
|
||||
block[SBFS_SUPER_VERSION], SBFS_VERSION);
|
||||
return 1;
|
||||
}
|
||||
super->diskBlocks = readWord(block + SBFS_SUPER_DISK);
|
||||
super->directoryStart = readWord(block + SBFS_SUPER_DIRSTART);
|
||||
super->directoryBlocks = readWord(block + SBFS_SUPER_DIRBLOCKS);
|
||||
super->freeBlocks = readWord(block + SBFS_SUPER_FREE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int writeSuperblock(FILE *image, const Superblock *super) {
|
||||
uint8_t block[SBFS_BLOCK_BYTES];
|
||||
memset(block, 0, sizeof(block));
|
||||
memcpy(block, SBFS_MAGIC, SBFS_MAGIC_BYTES);
|
||||
block[SBFS_SUPER_VERSION] = SBFS_VERSION;
|
||||
writeWord(block + SBFS_SUPER_DISK, super->diskBlocks);
|
||||
writeWord(block + SBFS_SUPER_DIRSTART, super->directoryStart);
|
||||
writeWord(block + SBFS_SUPER_DIRBLOCKS, super->directoryBlocks);
|
||||
writeWord(block + SBFS_SUPER_FREE, super->freeBlocks);
|
||||
return writeBlock(image, 0, block);
|
||||
}
|
||||
|
||||
// The whole directory, held in memory while a command works on it. It is small enough
|
||||
// that reading it once and writing it back once is simpler than picking at blocks, and it
|
||||
// is what the machine will do too once it has the Data Memory to spare.
|
||||
typedef struct {
|
||||
uint8_t *bytes;
|
||||
int entries;
|
||||
} Directory;
|
||||
|
||||
static int readDirectory(FILE *image, const Superblock *super, Directory *directory) {
|
||||
directory->entries = super->directoryBlocks * SBFS_ENTRIES_PER_BLOCK;
|
||||
directory->bytes = malloc((size_t)super->directoryBlocks * SBFS_BLOCK_BYTES);
|
||||
if (directory->bytes == NULL) {
|
||||
fprintf(stderr, "Error: Out of memory reading the directory.\n");
|
||||
return 1;
|
||||
}
|
||||
for (uint16_t i = 0; i < super->directoryBlocks; i++) {
|
||||
if (readBlock(image, (uint16_t)(super->directoryStart + i),
|
||||
directory->bytes + (size_t)i * SBFS_BLOCK_BYTES)) {
|
||||
free(directory->bytes);
|
||||
directory->bytes = NULL;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int writeDirectory(FILE *image, const Superblock *super, const Directory *directory) {
|
||||
for (uint16_t i = 0; i < super->directoryBlocks; i++) {
|
||||
if (writeBlock(image, (uint16_t)(super->directoryStart + i),
|
||||
directory->bytes + (size_t)i * SBFS_BLOCK_BYTES)) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint8_t *entryAt(const Directory *directory, int index) {
|
||||
return directory->bytes + (size_t)index * SBFS_ENTRY_BYTES;
|
||||
}
|
||||
|
||||
static int entryInUse(const uint8_t *entry) {
|
||||
return (entry[SBFS_ENTRY_FLAGS] & SBFS_FLAG_IN_USE) != 0;
|
||||
}
|
||||
|
||||
static uint32_t entrySize(const uint8_t *entry) {
|
||||
return (uint32_t)readWord(entry + SBFS_ENTRY_BLOCKS) * SBFS_BLOCK_BYTES
|
||||
+ entry[SBFS_ENTRY_TAIL];
|
||||
}
|
||||
|
||||
static uint16_t entryBlocksUsed(const uint8_t *entry) {
|
||||
return (uint16_t)SBFS_BLOCKS_USED(readWord(entry + SBFS_ENTRY_BLOCKS),
|
||||
entry[SBFS_ENTRY_TAIL]);
|
||||
}
|
||||
|
||||
// Names are compared as written, the way labels are, and are padded rather than
|
||||
// terminated, so a name that fills the field has no terminator to find.
|
||||
static void entryName(const uint8_t *entry, char *into) {
|
||||
memcpy(into, entry + SBFS_ENTRY_NAME, SBFS_NAME_BYTES);
|
||||
into[SBFS_NAME_BYTES] = '\0';
|
||||
}
|
||||
|
||||
static int findByName(const Directory *directory, const char *name) {
|
||||
char held[SBFS_NAME_BYTES + 1];
|
||||
for (int i = 0; i < directory->entries; i++) {
|
||||
const uint8_t *entry = entryAt(directory, i);
|
||||
if (!entryInUse(entry)) {
|
||||
continue;
|
||||
}
|
||||
entryName(entry, held);
|
||||
if (strcmp(held, name) == 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Where the first free run of the wanted length begins, or -1 if there is not one.
|
||||
//
|
||||
// There is no allocation bitmap, and that is the design rather than an omission: with
|
||||
// files laid down contiguously, every block is either inside some entry's range or it is
|
||||
// not, so the directory already is the allocation map. A bitmap would be a second copy of
|
||||
// a fact that is already written down, and a second copy is a thing that can disagree.
|
||||
static long findFreeRun(const Directory *directory, const Superblock *super, uint16_t wanted) {
|
||||
if (wanted == 0) {
|
||||
// An empty file occupies nothing, so it has no start to speak of. Block 0 is the
|
||||
// superblock and can never hold file data, which makes it the honest way to say
|
||||
// "nowhere" without inventing a place that belongs to somebody else.
|
||||
(void)directory;
|
||||
return 0;
|
||||
}
|
||||
uint32_t firstData = (uint32_t)super->directoryStart + super->directoryBlocks;
|
||||
for (uint32_t candidate = firstData; candidate + wanted <= super->diskBlocks; candidate++) {
|
||||
uint32_t clash = 0;
|
||||
for (int i = 0; i < directory->entries && !clash; i++) {
|
||||
const uint8_t *entry = entryAt(directory, i);
|
||||
if (!entryInUse(entry)) {
|
||||
continue;
|
||||
}
|
||||
uint32_t start = readWord(entry + SBFS_ENTRY_START);
|
||||
uint32_t used = entryBlocksUsed(entry);
|
||||
if (used == 0) {
|
||||
continue;
|
||||
}
|
||||
if (candidate < start + used && start < candidate + wanted) {
|
||||
// Overlaps this file, so start looking again past the end of it.
|
||||
clash = start + used;
|
||||
}
|
||||
}
|
||||
if (clash) {
|
||||
candidate = clash - 1; // The loop's increment takes it to clash.
|
||||
continue;
|
||||
}
|
||||
return (long)candidate;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static uint16_t countFree(const Directory *directory, const Superblock *super) {
|
||||
uint32_t used = 0;
|
||||
for (int i = 0; i < directory->entries; i++) {
|
||||
const uint8_t *entry = entryAt(directory, i);
|
||||
if (entryInUse(entry)) {
|
||||
used += entryBlocksUsed(entry);
|
||||
}
|
||||
}
|
||||
// Block 0 and everything up to the end of the directory is not available.
|
||||
uint32_t overhead = (uint32_t)super->directoryStart + super->directoryBlocks;
|
||||
return (uint16_t)(super->diskBlocks - overhead - used);
|
||||
}
|
||||
|
||||
// ---- Commands ----
|
||||
|
||||
static int commandFormat(const char *path, uint16_t blocks, uint16_t directoryBlocks) {
|
||||
if (blocks <= 1u + directoryBlocks) {
|
||||
fprintf(stderr, "Error: A disk of %u blocks has no room for a superblock and a"
|
||||
" directory of %u.\n", blocks, directoryBlocks);
|
||||
return 1;
|
||||
}
|
||||
// Quietly, because a disk that is not there yet is the ordinary case for format and
|
||||
// not something to complain about on the way past.
|
||||
FILE *image = fopen(path, "r+b");
|
||||
if (image == NULL) {
|
||||
image = openImage(path, "w+b");
|
||||
if (image == NULL) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
uint8_t empty[SBFS_BLOCK_BYTES];
|
||||
memset(empty, 0, sizeof(empty));
|
||||
for (uint16_t i = 0; i < blocks; i++) {
|
||||
if (writeBlock(image, i, empty)) {
|
||||
fclose(image);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
Superblock super;
|
||||
super.diskBlocks = blocks;
|
||||
super.directoryStart = SBFS_FIRST_DIRECTORY_BLOCK;
|
||||
super.directoryBlocks = directoryBlocks;
|
||||
super.freeBlocks = (uint16_t)(blocks - 1 - directoryBlocks);
|
||||
if (writeSuperblock(image, &super)) {
|
||||
fclose(image);
|
||||
return 1;
|
||||
}
|
||||
fclose(image);
|
||||
printf("Formatted %s: %u blocks, %u of directory, %u free.\n",
|
||||
path, blocks, directoryBlocks, super.freeBlocks);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int commandList(const char *path) {
|
||||
FILE *image = openImage(path, "rb");
|
||||
if (image == NULL) {
|
||||
return 1;
|
||||
}
|
||||
Superblock super;
|
||||
Directory directory;
|
||||
if (readSuperblock(image, &super) || readDirectory(image, &super, &directory)) {
|
||||
fclose(image);
|
||||
return 1;
|
||||
}
|
||||
printf("%s: %u blocks, %u of directory, %u entries.\n",
|
||||
path, super.diskBlocks, super.directoryBlocks, directory.entries);
|
||||
printf("%-22s %8s %7s %7s\n", "NAME", "BYTES", "START", "BLOCKS");
|
||||
char name[SBFS_NAME_BYTES + 1];
|
||||
int shown = 0;
|
||||
for (int i = 0; i < directory.entries; i++) {
|
||||
const uint8_t *entry = entryAt(&directory, i);
|
||||
if (!entryInUse(entry)) {
|
||||
continue;
|
||||
}
|
||||
entryName(entry, name);
|
||||
printf("%-22s %8u %7u %7u\n", name, entrySize(entry),
|
||||
readWord(entry + SBFS_ENTRY_START), entryBlocksUsed(entry));
|
||||
shown++;
|
||||
}
|
||||
// The count in the superblock is a cache, so say what the directory actually adds up
|
||||
// to as well. If the two ever disagree, the directory is the one to believe.
|
||||
uint16_t counted = countFree(&directory, &super);
|
||||
printf("%d file%s, %u blocks free", shown, shown == 1 ? "" : "s", counted);
|
||||
if (counted != super.freeBlocks) {
|
||||
printf(" (the superblock says %u, which is stale)", super.freeBlocks);
|
||||
}
|
||||
printf(".\n");
|
||||
free(directory.bytes);
|
||||
fclose(image);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int commandPut(const char *path, const char *hostFile, const char *asName) {
|
||||
FILE *source = fopen(hostFile, "rb");
|
||||
if (source == NULL) {
|
||||
fprintf(stderr, "Error: Couldn't open \"%s\".\n", hostFile);
|
||||
return 1;
|
||||
}
|
||||
fseek(source, 0, SEEK_END);
|
||||
long size = ftell(source);
|
||||
rewind(source);
|
||||
if (size < 0) {
|
||||
fprintf(stderr, "Error: Couldn't measure \"%s\".\n", hostFile);
|
||||
fclose(source);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (strlen(asName) > SBFS_NAME_BYTES) {
|
||||
fprintf(stderr, "Error: \"%s\" is %zu characters, and a name may be %d.\n"
|
||||
" Give a shorter one as the last argument.\n",
|
||||
asName, strlen(asName), SBFS_NAME_BYTES);
|
||||
fclose(source);
|
||||
return 1;
|
||||
}
|
||||
|
||||
FILE *image = openImage(path, "r+b");
|
||||
if (image == NULL) {
|
||||
fclose(source);
|
||||
return 1;
|
||||
}
|
||||
Superblock super;
|
||||
Directory directory;
|
||||
if (readSuperblock(image, &super) || readDirectory(image, &super, &directory)) {
|
||||
fclose(source);
|
||||
fclose(image);
|
||||
return 1;
|
||||
}
|
||||
if (findByName(&directory, asName) >= 0) {
|
||||
fprintf(stderr, "Error: \"%s\" is already on the disk. Delete it first.\n", asName);
|
||||
goto failed;
|
||||
}
|
||||
int slot = -1;
|
||||
for (int i = 0; i < directory.entries && slot < 0; i++) {
|
||||
if (!entryInUse(entryAt(&directory, i))) {
|
||||
slot = i;
|
||||
}
|
||||
}
|
||||
if (slot < 0) {
|
||||
fprintf(stderr, "Error: The directory is full: %d entries, all taken.\n", directory.entries);
|
||||
goto failed;
|
||||
}
|
||||
|
||||
uint16_t whole = (uint16_t)SBFS_WHOLE_BLOCKS(size);
|
||||
uint8_t tail = (uint8_t)SBFS_TAIL_BYTES(size);
|
||||
uint16_t needed = (uint16_t)SBFS_BLOCKS_USED(whole, tail);
|
||||
long start = findFreeRun(&directory, &super, needed);
|
||||
if (start < 0) {
|
||||
fprintf(stderr, "Error: No run of %u free blocks. There may be room on the disk"
|
||||
" without there being room in one piece.\n", needed);
|
||||
goto failed;
|
||||
}
|
||||
|
||||
uint8_t block[SBFS_BLOCK_BYTES];
|
||||
for (uint16_t i = 0; i < needed; i++) {
|
||||
memset(block, 0, sizeof(block));
|
||||
size_t got = fread(block, 1, SBFS_BLOCK_BYTES, source);
|
||||
if (got == 0 && i < needed) {
|
||||
fprintf(stderr, "Error: \"%s\" ended sooner than its size said.\n", hostFile);
|
||||
goto failed;
|
||||
}
|
||||
if (writeBlock(image, (uint16_t)(start + i), block)) {
|
||||
goto failed;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t *entry = entryAt(&directory, slot);
|
||||
memset(entry, 0, SBFS_ENTRY_BYTES);
|
||||
entry[SBFS_ENTRY_FLAGS] = SBFS_FLAG_IN_USE;
|
||||
writeWord(entry + SBFS_ENTRY_START, (uint16_t)start);
|
||||
writeWord(entry + SBFS_ENTRY_BLOCKS, whole);
|
||||
entry[SBFS_ENTRY_TAIL] = tail;
|
||||
memcpy(entry + SBFS_ENTRY_NAME, asName, strlen(asName));
|
||||
|
||||
super.freeBlocks = countFree(&directory, &super);
|
||||
if (writeDirectory(image, &super, &directory) || writeSuperblock(image, &super)) {
|
||||
goto failed;
|
||||
}
|
||||
printf("Put %s on as \"%s\": %ld bytes at block %ld.\n", hostFile, asName, size, start);
|
||||
free(directory.bytes);
|
||||
fclose(source);
|
||||
fclose(image);
|
||||
return 0;
|
||||
|
||||
failed:
|
||||
free(directory.bytes);
|
||||
fclose(source);
|
||||
fclose(image);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int commandGet(const char *path, const char *name, const char *hostFile) {
|
||||
FILE *image = openImage(path, "rb");
|
||||
if (image == NULL) {
|
||||
return 1;
|
||||
}
|
||||
Superblock super;
|
||||
Directory directory;
|
||||
if (readSuperblock(image, &super) || readDirectory(image, &super, &directory)) {
|
||||
fclose(image);
|
||||
return 1;
|
||||
}
|
||||
int slot = findByName(&directory, name);
|
||||
if (slot < 0) {
|
||||
fprintf(stderr, "Error: There is no \"%s\" on that disk.\n", name);
|
||||
free(directory.bytes);
|
||||
fclose(image);
|
||||
return 1;
|
||||
}
|
||||
const uint8_t *entry = entryAt(&directory, slot);
|
||||
uint32_t size = entrySize(entry);
|
||||
uint16_t start = readWord(entry + SBFS_ENTRY_START);
|
||||
uint16_t used = entryBlocksUsed(entry);
|
||||
|
||||
FILE *out = fopen(hostFile, "wb");
|
||||
if (out == NULL) {
|
||||
fprintf(stderr, "Error: Couldn't write \"%s\".\n", hostFile);
|
||||
free(directory.bytes);
|
||||
fclose(image);
|
||||
return 1;
|
||||
}
|
||||
uint8_t block[SBFS_BLOCK_BYTES];
|
||||
uint32_t left = size;
|
||||
for (uint16_t i = 0; i < used; i++) {
|
||||
if (readBlock(image, (uint16_t)(start + i), block)) {
|
||||
fclose(out);
|
||||
free(directory.bytes);
|
||||
fclose(image);
|
||||
return 1;
|
||||
}
|
||||
uint32_t take = (left < SBFS_BLOCK_BYTES) ? left : SBFS_BLOCK_BYTES;
|
||||
fwrite(block, 1, take, out);
|
||||
left -= take;
|
||||
}
|
||||
fclose(out);
|
||||
printf("Got \"%s\" off as %s: %u bytes.\n", name, hostFile, size);
|
||||
free(directory.bytes);
|
||||
fclose(image);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int commandDelete(const char *path, const char *name) {
|
||||
FILE *image = openImage(path, "r+b");
|
||||
if (image == NULL) {
|
||||
return 1;
|
||||
}
|
||||
Superblock super;
|
||||
Directory directory;
|
||||
if (readSuperblock(image, &super) || readDirectory(image, &super, &directory)) {
|
||||
fclose(image);
|
||||
return 1;
|
||||
}
|
||||
int slot = findByName(&directory, name);
|
||||
if (slot < 0) {
|
||||
fprintf(stderr, "Error: There is no \"%s\" on that disk.\n", name);
|
||||
free(directory.bytes);
|
||||
fclose(image);
|
||||
return 1;
|
||||
}
|
||||
// A deleted entry and a never used one are the same thing: the in use bit goes down
|
||||
// and its blocks are free again. The blocks themselves are left as they were, which
|
||||
// is worth knowing if anything is ever meant to be private.
|
||||
memset(entryAt(&directory, slot), 0, SBFS_ENTRY_BYTES);
|
||||
super.freeBlocks = countFree(&directory, &super);
|
||||
int failed = writeDirectory(image, &super, &directory) || writeSuperblock(image, &super);
|
||||
if (!failed) {
|
||||
printf("Deleted \"%s\". %u blocks free.\n", name, super.freeBlocks);
|
||||
}
|
||||
free(directory.bytes);
|
||||
fclose(image);
|
||||
return failed;
|
||||
}
|
||||
|
||||
static void printUsage(const char *program) {
|
||||
printf("Usage: %s <command> <image> [arguments]\n", program);
|
||||
printf("\n");
|
||||
printf("Commands:\n");
|
||||
printf(" format <image> [blocks] [dirblocks] Lay down a fresh filesystem.\n");
|
||||
printf(" list <image> Show what is on the disk.\n");
|
||||
printf(" put <image> <file> [name] Put a host file onto it.\n");
|
||||
printf(" get <image> <name> [file] Take one off it.\n");
|
||||
printf(" delete <image> <name> Remove one.\n");
|
||||
printf("\n");
|
||||
printf("Blocks are %d bytes. A name may be %d characters. Without one, put uses the\n",
|
||||
SBFS_BLOCK_BYTES, SBFS_NAME_BYTES);
|
||||
printf("file's own name, which is often too long, and it will say so.\n");
|
||||
}
|
||||
|
||||
// The part of a path after the last separator, so that put can default to a file's own
|
||||
// name rather than the whole path it was found at.
|
||||
static const char *baseName(const char *path) {
|
||||
const char *slash = strrchr(path, '/');
|
||||
return slash ? slash + 1 : path;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
if (argc < 2 || strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0) {
|
||||
printUsage(argv[0]);
|
||||
return argc < 2 ? 1 : 0;
|
||||
}
|
||||
const char *command = argv[1];
|
||||
if (argc < 3) {
|
||||
fprintf(stderr, "Error: %s needs a disk image.\n", command);
|
||||
return 1;
|
||||
}
|
||||
const char *path = argv[2];
|
||||
|
||||
if (strcmp(command, "format") == 0) {
|
||||
long blocks = (argc > 3) ? strtol(argv[3], NULL, 0) : 512;
|
||||
long directoryBlocks = (argc > 4) ? strtol(argv[4], NULL, 0) : SBFS_DEFAULT_DIRECTORY_BLOCKS;
|
||||
if (blocks < 2 || blocks > 0xFFFF || directoryBlocks < 1 || directoryBlocks > 0xFFFF) {
|
||||
fprintf(stderr, "Error: A disk is between 2 and 65535 blocks, with at least"
|
||||
" one of directory.\n");
|
||||
return 1;
|
||||
}
|
||||
return commandFormat(path, (uint16_t)blocks, (uint16_t)directoryBlocks);
|
||||
}
|
||||
if (strcmp(command, "list") == 0) {
|
||||
return commandList(path);
|
||||
}
|
||||
if (strcmp(command, "put") == 0) {
|
||||
if (argc < 4) {
|
||||
fprintf(stderr, "Error: put needs a file to put on.\n");
|
||||
return 1;
|
||||
}
|
||||
return commandPut(path, argv[3], (argc > 4) ? argv[4] : baseName(argv[3]));
|
||||
}
|
||||
if (strcmp(command, "get") == 0) {
|
||||
if (argc < 4) {
|
||||
fprintf(stderr, "Error: get needs the name of a file on the disk.\n");
|
||||
return 1;
|
||||
}
|
||||
return commandGet(path, argv[3], (argc > 4) ? argv[4] : argv[3]);
|
||||
}
|
||||
if (strcmp(command, "delete") == 0) {
|
||||
if (argc < 4) {
|
||||
fprintf(stderr, "Error: delete needs the name of a file on the disk.\n");
|
||||
return 1;
|
||||
}
|
||||
return commandDelete(path, argv[3]);
|
||||
}
|
||||
fprintf(stderr, "Error: There is no \"%s\" command.\n", command);
|
||||
printUsage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// sbex.h
|
||||
// The SplitBit loadable program format, version one.
|
||||
//
|
||||
// A program that is not the one the machine booted from has to say where it wants to
|
||||
// live, because nothing relocates it. This is a header saying that, in front of the
|
||||
// bytes themselves. It is the same idea as the load address on the front of a C64 .PRG,
|
||||
// with room for the machine to ask a few more questions later.
|
||||
//
|
||||
// Two things read this: whatever builds one on the host, and the loader running on
|
||||
// SplitBit. As with the filesystem, nothing is shared between them but the specification.
|
||||
//
|
||||
// All multi byte numbers are most significant byte first.
|
||||
//
|
||||
// 0 4 "SBEX"
|
||||
// 4 1 Version
|
||||
// 5 1 Reserved
|
||||
// 6 2 Where the code goes in Program Memory
|
||||
// 8 2 Where to start running, an address in Program Memory
|
||||
// 10 2 How many bytes of code there are
|
||||
// 12 2 Where the data goes in Data Memory
|
||||
// 14 2 How many bytes of data there are
|
||||
// 16 The code, then the data
|
||||
//
|
||||
// Sixteen bytes, so the code begins at a round offset and finding it is one step rather
|
||||
// than an arithmetic. Nothing here relocates anything: the addresses are where the
|
||||
// program was built to live, and putting it anywhere else would leave every branch and
|
||||
// every SETD inside it pointing at the wrong place.
|
||||
//
|
||||
// Written by Anachronaut
|
||||
|
||||
#ifndef SBEX_H
|
||||
#define SBEX_H
|
||||
|
||||
#define SBEX_MAGIC "SBEX"
|
||||
#define SBEX_MAGIC_BYTES 4
|
||||
#define SBEX_VERSION 1
|
||||
#define SBEX_HEADER_BYTES 16
|
||||
|
||||
#define SBEX_VERSION_AT 4
|
||||
#define SBEX_CODE_AT 6
|
||||
#define SBEX_ENTRY_AT 8
|
||||
#define SBEX_CODE_LEN_AT 10
|
||||
#define SBEX_DATA_AT 12
|
||||
#define SBEX_DATA_LEN_AT 14
|
||||
|
||||
#endif // SBEX_H
|
||||
@@ -0,0 +1,79 @@
|
||||
// sbfs.h
|
||||
// The SplitBit Filesystem, version one.
|
||||
//
|
||||
// This is the host side's copy of the format. The other implementation is SplitBit
|
||||
// assembly running on the machine itself, so nothing can be shared between them except
|
||||
// the specification: the two have to be kept honest by a document rather than by a
|
||||
// header. Everything here follows that document exactly, and anything that changes here
|
||||
// has to change there in the same breath.
|
||||
//
|
||||
// All multi byte numbers are most significant byte first, the same as every other number
|
||||
// SplitBit stores: addresses, the SPBT binary header, and the vector table.
|
||||
//
|
||||
// Written by Anachronaut
|
||||
|
||||
#ifndef SBFS_H
|
||||
#define SBFS_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define SBFS_MAGIC "SBFS"
|
||||
#define SBFS_MAGIC_BYTES 4
|
||||
#define SBFS_VERSION 1
|
||||
|
||||
#define SBFS_BLOCK_BYTES 256
|
||||
|
||||
// ---- Block 0, the superblock ----
|
||||
//
|
||||
// 0 4 "SBFS"
|
||||
// 4 1 Version
|
||||
// 5 1 Reserved
|
||||
// 6 2 Blocks on the disk
|
||||
// 8 2 First directory block
|
||||
// 10 2 Blocks the directory occupies
|
||||
// 12 2 Free blocks, a cache rather than the authority
|
||||
// 14 Reserved to the end of the block
|
||||
|
||||
#define SBFS_SUPER_VERSION 4
|
||||
#define SBFS_SUPER_DISK 6
|
||||
#define SBFS_SUPER_DIRSTART 8
|
||||
#define SBFS_SUPER_DIRBLOCKS 10
|
||||
#define SBFS_SUPER_FREE 12
|
||||
|
||||
// ---- Directory entries ----
|
||||
//
|
||||
// 0 1 Flags
|
||||
// 1 2 First block of the file's data
|
||||
// 3 2 Whole blocks the file occupies
|
||||
// 5 1 Bytes in the trailing part block, or zero if there is not one
|
||||
// 6 22 Name, padded with zeroes
|
||||
// 28 4 Reserved
|
||||
//
|
||||
// Thirty two divides two hundred and fifty six, so an entry never straddles a block and
|
||||
// reading one never means handling a split.
|
||||
|
||||
#define SBFS_ENTRY_BYTES 32
|
||||
#define SBFS_ENTRIES_PER_BLOCK (SBFS_BLOCK_BYTES / SBFS_ENTRY_BYTES)
|
||||
|
||||
#define SBFS_ENTRY_FLAGS 0
|
||||
#define SBFS_ENTRY_START 1
|
||||
#define SBFS_ENTRY_BLOCKS 3
|
||||
#define SBFS_ENTRY_TAIL 5
|
||||
#define SBFS_ENTRY_NAME 6
|
||||
#define SBFS_NAME_BYTES 22
|
||||
|
||||
#define SBFS_FLAG_IN_USE 0x01
|
||||
|
||||
// The directory begins at block 1 and is this many blocks unless told otherwise, which
|
||||
// is sixty four files. The superblock carries the real number, so this is only what a
|
||||
// freshly formatted disk gets.
|
||||
#define SBFS_FIRST_DIRECTORY_BLOCK 1
|
||||
#define SBFS_DEFAULT_DIRECTORY_BLOCKS 8
|
||||
|
||||
// A file of n bytes occupies n / 256 whole blocks and, if anything is left over, one more
|
||||
// for the tail. Zero means zero in both, so an empty file occupies nothing at all.
|
||||
#define SBFS_WHOLE_BLOCKS(bytes) ((bytes) / SBFS_BLOCK_BYTES)
|
||||
#define SBFS_TAIL_BYTES(bytes) ((bytes) % SBFS_BLOCK_BYTES)
|
||||
#define SBFS_BLOCKS_USED(blocks, tail) ((blocks) + ((tail) ? 1 : 0))
|
||||
|
||||
#endif // SBFS_H
|
||||
Executable
+58
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Wraps an assembled SplitBit binary into a loadable program.
|
||||
|
||||
The assembler emits a boot image: a Program Segment that loads at zero and a Data Segment
|
||||
that does the same. A program meant to be loaded somewhere else has to say where it goes,
|
||||
which is what the SBEX header in front of it is for.
|
||||
|
||||
A program says where it lives by reserving the front of each segment, so the addresses
|
||||
given here have to match the reserves in its source. Nothing checks that for you, and
|
||||
nothing relocates anything if you get it wrong.
|
||||
"""
|
||||
import struct
|
||||
import sys
|
||||
|
||||
|
||||
def segments(raw):
|
||||
at = 4 + 1 + 4 # magic, version, feature flags
|
||||
assert raw[:4] == b"SPBT", "not a SplitBit binary"
|
||||
assert raw[at:at + 3] == b"PRG"
|
||||
plen = struct.unpack(">H", raw[at + 3:at + 5])[0]
|
||||
program = raw[at + 5:at + 5 + plen]
|
||||
at = at + 5 + plen
|
||||
assert raw[at:at + 3] == b"DAT"
|
||||
dlen = struct.unpack(">H", raw[at + 3:at + 5])[0]
|
||||
return program, raw[at + 5:at + 5 + dlen]
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 6:
|
||||
sys.exit("usage: wrap.py <binary> <output> <code address> <data address> <entry>")
|
||||
binary, output = sys.argv[1], sys.argv[2]
|
||||
codeAt, dataAt, entry = (int(a, 0) for a in sys.argv[3:6])
|
||||
|
||||
program, data = segments(open(binary, "rb").read())
|
||||
|
||||
# Everything below the address a segment is placed at is the padding the reserve put
|
||||
# there, and is not part of the program.
|
||||
code = program[codeAt:]
|
||||
values = data[dataAt:]
|
||||
|
||||
header = bytearray(16)
|
||||
header[0:4] = b"SBEX"
|
||||
header[4] = 1
|
||||
struct.pack_into(">H", header, 6, codeAt)
|
||||
struct.pack_into(">H", header, 8, entry)
|
||||
struct.pack_into(">H", header, 10, len(code))
|
||||
struct.pack_into(">H", header, 12, dataAt)
|
||||
struct.pack_into(">H", header, 14, len(values))
|
||||
|
||||
with open(output, "wb") as out:
|
||||
out.write(header)
|
||||
out.write(code)
|
||||
out.write(values)
|
||||
print("%s: %d bytes of code at 0x%04X, %d of data at 0x%04X, entry 0x%04X"
|
||||
% (output, len(code), codeAt, len(values), dataAt, entry))
|
||||
|
||||
|
||||
main()
|
||||
@@ -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