Files
SplitBit-Emulator/Source/Emulator/utility.c
T
AnachronautandClaude Opus 5 fed1453e6e Controllers: four pads that say what is held
The console says WHICH KEY WENT DOWN, which is the right shape for typing
and the wrong one for playing. A game wants to know what is being held,
this frame, possibly several things at once, and a stream of presses
cannot say that: a key that is down and staying down sends nothing at all.
Lunar Porter's thrust is a burn per press for exactly that reason.

So a pad is its own device on ports 0x60 to 0x6F, reporting a LEVEL. One
read gives every button at once, holding is the natural thing to express,
two directions together cost nothing, and reading does not consume it - a
game may ask twice in a frame and be told the same thing both times.

Four of them, because a party is four. They cost a port each and nothing
at all when unused. The directions are the low nibble so "which way" is an
AND with 0x0F; the buttons are the high nibble for the same reason. 0x64
says which are really there, so a game can ask for a controller rather
than sitting silent while somebody presses things at it. They never
interrupt: a game polls once a frame because that is when it draws.

KEY-UP ON THE CONSOLE WAS THE OTHER WAY TO DO THIS AND WAS REJECTED. A
terminal hands over characters and can never report a key coming up
however it is asked, so it would have been a thing that worked behind a
window and silently did not down a wire. A separate device can honestly
say it is not there.

Voyager drives pad nought from the keyboard as well as from any real
controller, OR-ed rather than chosen between, so a game written for a pad
is playable on a machine with none and unplugging one mid-game does not
leave somebody holding nothing.

And a recorded path, which is what makes any of it testable: --pad names a
file of one byte a frame, and the manifest has an eighth column for it.
A BYTE A FRAME AND NOT A BYTE A READ - a level asked twice in one frame
has to answer the same both times, and a file that advanced per read would
depend on how the program happened to be written. Voyager's own tests run
headless with nobody holding anything, so without this the device would be
exercised only by somebody playing: the state the console's line editing
was in when it broke twice in two days.

0x50 is the timer, not free. The block this went in was chosen after
looking rather than before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-02 21:44:55 -04:00

185 lines
8.5 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(" -P, --pad FILE Hold a controller from a file, one byte a frame. Given\n");
printf(" again for the next pad. A byte is the buttons held:\n");
printf(" 1 right, 2 left, 4 down, 8 up, 16 A, 32 B, 64 start,\n");
printf(" 128 select.\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'},
{"pad", required_argument, 0, 'P'},
{"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:P:", 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 'P':
// Fills the pads in turn, the same way --disk fills the drives, so the first
// one named is pad nought and the machine has as many as were asked for.
if (options->padCount < PAD_DRIVE_COUNT) {
options->pads[options->padCount++] = 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)]);
}