THE MACHINE SUPPLIES BLOCKS AND SAYS WHAT A DRIVE IS. It says nothing about filesystems, which is what leaves room for a system that would rather have its own - and is why the volatile bit is a fact about the hardware rather than a promise about SBFS. 0x26 what the selected drive is: bit 0, contents do not survive 0x27, 0x28 how many blocks it has --ram-disk N a drive of N blocks with memory behind it A drive of memory selects, reads, writes and has a size like any other, and a program cannot tell the difference except by how fast it was. The one thing it cannot work out for itself is that the contents are volatile, because an empty disk and a volatile disk look identical from outside. THAT BIT IS THE DIFFERENCE BETWEEN A DRIVE A SYSTEM MAY FORMAT ON SIGHT AND ONE IT MUST NOT. CosmOS formats a volatile drive it cannot read, because there was never anything on it to lose, and leaves every other unreadable drive alone - an unformatted floppy is not an invitation, it is a blank floppy. Removing that check formats somebody's blank disk, which is checked rather than asserted: cosmosBlankDisk boots with one and requires it to be refused. So CosmOS grew a format. The size comes from the drive rather than from a superblock, since a superblock states a size too and that is no use on a disk which has not got one yet. Sixteen directory blocks, 128 names, chosen rather than worked out: a scratch disk runs out of names long before room, and this machine cannot divide. The RAM disk is no faster on this emulator by default, and that is honest rather than disappointing: the emulated disk has no seek time unless asked for one. With --disk-cycles 10000 the same copy is 7.94M cycles against 8.70M, the difference being every write. run.sh takes "ram:2048" where an image name goes, which needs no removing between runs because there is nothing to remove. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
173 lines
7.8 KiB
C
173 lines
7.8 KiB
C
// utility.c
|
|
// Utilities for the SplitBit CPU Emulator
|
|
// Written by Anachronaut
|
|
// 10/15/2024
|
|
|
|
#include "utility.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <getopt.h>
|
|
#include "../Assembler/assembly.h"
|
|
|
|
void printHelp(const char *programName) {
|
|
printf("Usage: %s [OPTIONS] [boot image]\n", programName);
|
|
printf("\n");
|
|
printf("Named an image, it is placed into memory and started, which is what a\n");
|
|
printf("debugger does and how the test suite runs. Given only a disk, the machine\n");
|
|
printf("starts the way hardware would: the built in ROM is shadowed into Program\n");
|
|
printf("Memory, and it reads the disk for everything else.\n");
|
|
printf("\n");
|
|
printf("Options:\n");
|
|
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(" -L, --disk-cycles N How many cycles a block read or write takes. Zero, the\n");
|
|
printf(" default, finishes before the next instruction starts.\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(" -S, --screen FILE Save a picture of the screen, as a PPM, when the machine\n");
|
|
printf(" stops. Works with or without a window, which is how the\n");
|
|
printf(" tests look at a screen on a host that has no display.\n");
|
|
printf(" -K, --keyboard FILE Feed the console from this file as though it were a\n");
|
|
printf(" keyboard rather than a terminal. Which means the console does\n");
|
|
printf(" its own line editing, the way it must when a window is open\n");
|
|
printf(" and there is no terminal behind it to do it.\n");
|
|
printf(" -N, --sound FILE Save every sample the machine made, as raw signed 16 bit\n");
|
|
printf(" at 48kHz. What --screen is for a picture: the only way to\n");
|
|
printf(" check a sound on a machine with no speaker.\n");
|
|
printf(" -h, --help Display this help message.\n");
|
|
}
|
|
|
|
uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options) {
|
|
static struct option long_options[] = {
|
|
{"debug", no_argument, 0, 'd'},
|
|
{"cycles", required_argument, 0, 'c'},
|
|
{"fast", no_argument, 0, 'f'},
|
|
{"disk", required_argument, 0, 'D'},
|
|
{"ram-disk", required_argument, 0, 'R'},
|
|
{"write-protect", no_argument, 0, 'W'},
|
|
{"disk-cycles", required_argument, 0, 'L'},
|
|
{"screen", required_argument, 0, 'S'},
|
|
{"keyboard", required_argument, 0, 'K'},
|
|
{"sound", required_argument, 0, 'N'},
|
|
{"help", no_argument, 0, 'h'},
|
|
{0, 0, 0, 0 }
|
|
};
|
|
int opt;
|
|
int option_index = 0;
|
|
|
|
// ---- Everything off, in one line rather than nine ----
|
|
//
|
|
// This was a list of assignments, one per field, and a list beside a struct drifts from
|
|
// the struct: adding `disks` and `diskCount` left them holding whatever was on the stack,
|
|
// so a machine given one disk was told it already had four drives. The same struct
|
|
// growing a field once before left Voyager linking against an object that disagreed
|
|
// about its size.
|
|
//
|
|
// Every default here is nought or nothing, and a default that is not can be written
|
|
// below this line where it will be read as the exception it is.
|
|
*options = (EmulatorOptions){0};
|
|
|
|
// Parse options
|
|
while ((opt = getopt_long(argc, argv, "dc:fhD:WL:S:K:N:R:", long_options, &option_index)) != -1) {
|
|
switch (opt) {
|
|
case 'd':
|
|
options->debug = 1;
|
|
break;
|
|
case 'c': {
|
|
// The count has to be a plain positive number. Anything else is
|
|
// almost certainly a mistyped command line rather than a request
|
|
// to run zero cycles.
|
|
char *end;
|
|
long value = strtol(optarg, &end, 10);
|
|
if (*end != '\0' || value <= 0) {
|
|
fprintf(stderr, "Error: --cycles needs a positive number, not \"%s\".\n", optarg);
|
|
return OPTIONS_ERROR;
|
|
}
|
|
options->cycles = (unsigned long)value;
|
|
}
|
|
break;
|
|
case 'f':
|
|
options->fast = 1;
|
|
break;
|
|
case 'D':
|
|
// Each one is the next drive. The first is also left in `disk`, because a
|
|
// machine with one disk is what almost every caller means and reading it
|
|
// that way keeps them all unchanged.
|
|
if (options->diskCount >= DISK_DRIVE_COUNT) {
|
|
fprintf(stderr, "Error: This machine has %d drives.\n",
|
|
DISK_DRIVE_COUNT);
|
|
return 1;
|
|
}
|
|
options->disks[options->diskCount++] = optarg;
|
|
if (options->disk == NULL) {
|
|
options->disk = optarg;
|
|
}
|
|
break;
|
|
case 'R': {
|
|
char *end;
|
|
const unsigned long blocks = strtoul(optarg, &end, 10);
|
|
if (*optarg == '\0' || *end != '\0' || blocks == 0) {
|
|
fprintf(stderr, "Error: --ram-disk wants a number of blocks.\n");
|
|
return 1;
|
|
}
|
|
options->ramDisk = blocks;
|
|
}
|
|
break;
|
|
case 'W':
|
|
options->writeProtect = 1;
|
|
break;
|
|
case 'L':
|
|
options->diskCycles = strtoul(optarg, NULL, 0);
|
|
break;
|
|
case 'S':
|
|
options->screen = optarg;
|
|
break;
|
|
case 'K':
|
|
options->keyboard = optarg;
|
|
break;
|
|
case 'N':
|
|
options->sound = optarg;
|
|
break;
|
|
case 'h':
|
|
printHelp(argv[0]);
|
|
return OPTIONS_HELP;
|
|
default:
|
|
printHelp(argv[0]);
|
|
return OPTIONS_ERROR;
|
|
}
|
|
}
|
|
return OPTIONS_OK;
|
|
}
|
|
|
|
// Writes a byte out as eight binary digits, most significant first. printf's %b is
|
|
// a recent addition to C and not available everywhere, so this does it by hand.
|
|
// The buffer must have room for nine characters.
|
|
static void formatBinary(uint8_t value, char *out) {
|
|
for (int i = 0; i < 8; i++) {
|
|
out[i] = (value & (0x80 >> i)) ? '1' : '0';
|
|
}
|
|
out[8] = '\0';
|
|
}
|
|
|
|
void printRegisters(CPURegisters *cpu, uint8_t *Program, uint8_t *Data) {
|
|
char status[9];
|
|
formatBinary(cpu->Status, status);
|
|
printf("***** CPU Registers *****\n");
|
|
printf("A: 0x%02X\tB: 0x%02X\tQ: 0x%02X\tStatus: 0b%s\n", cpu->A, cpu->B, cpu->Q, status);
|
|
printf("Program Counter: 0x%04X Current Instruction: 0x%02X (%s)\n", cpu->ProgramCounter, Program[cpu->ProgramCounter],getMnemonic(Program[cpu->ProgramCounter]));
|
|
for (int i = 0; i < DATA_POINTERS; i++) {
|
|
printf(" Data Pointer %d: 0x%04X Current Data Value: 0x%02X%s\n",
|
|
i, cpu->DataPointer[i], Data[cpu->DataPointer[i]],
|
|
i >= PRESERVED_DATA_POINTERS ? " (volatile)" : "");
|
|
}
|
|
// The two casts keep these inside Data Memory. The Stack Pointer starts at the
|
|
// very top, so without them the display would read off the end of the array
|
|
// before a single byte has been pushed.
|
|
printf(" Stack Pointer: 0x%04X Current Value: (0x%02X) (0x%02X)\n", cpu->StackPointer,
|
|
Data[(uint16_t)(cpu->StackPointer + 1)], Data[(uint16_t)(cpu->StackPointer + 2)]);
|
|
}
|
|
|