Block device peripheral and SBFS file system implemented.

This commit is contained in:
Anachronaut
2026-08-16 14:03:29 -04:00
parent 04dfcd707b
commit eff6902bcf
36 changed files with 3251 additions and 31 deletions
+7
View File
@@ -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"},
+2 -1
View File
@@ -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) {
+555
View File
@@ -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;
}
+46
View File
@@ -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
+79
View File
@@ -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
+58
View File
@@ -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()
+32
View File
@@ -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
+5
View File
@@ -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
View File
@@ -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);
+48
View File
@@ -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
+14 -1
View File
@@ -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;
+2
View File
@@ -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);