HALT is terminal - stepCPU returns at once when the Halt Flag is up, so a halted machine does not execute, service devices, or take an interrupt - and that has to stay true, because every test ends with a halt and "halted" is how a program says it has finished. The consequence was that SplitBit had no way to wait at all. Every wait was a spin, and a spin is bus traffic: 11.5% of Type over a 14K file on a disk of ten thousand cycles, after read-ahead had already hidden three quarters of the latency. WAIT is 0xFE, one byte, no operands, sitting under HALT where the instruction that almost stops the machine belongs. Three decisions in it: - A line already standing means there is nothing to wait for, so WAIT does nothing. That is what makes test-then-wait race-free. - Any line ends the wait, masked or not, so a program can sleep on a device it has no handler for and read its status afterwards. Masking says who answers a request, not whether it happened. - A line that wakes the CPU without being dispatched is taken down by the WAIT. Left standing it would be found by the next WAIT, which would return at once - the program would spin exactly as before while looking as though it slept. Waiting is NOT a Status bit, and that is the trap avoided rather than a gap: Status rides into the interrupt frame and comes back out, so a machine interrupted mid-wait would return from its handler still waiting, and wait again for what it had already been given. An internal field instead. Idle cycles are counted apart from bus cycles and the halt line says so when there are any, which is what makes the difference observable at all - with the line-clearing removed the total moves by ONE cycle, 20,100 against 20,099, and only the idle half changes, halving to 9,976. A test on totals could never have seen it. Tests/terminal.sh asks that question, being the file for things a recorded output cannot see, and fails with the clear removed while "both reads finished" still passes. Three collisions, all found by building it: - 0xFE was the assembler's "not an instruction" sentinel. getOpcode now answers a negative NOT_AN_OPCODE, which is outside the range of every possible answer instead of inside the unused part of it. - 0xFE was also what faultTest and faultResumeTest executed to provoke a fault. They now use 0xFD and say why, because they did not fail when it became an instruction - they HUNG, having started sleeping instead. - Keys.asm has had a label called "wait" for a year, and mnemonics are matched uppercased. What that reported was "Branch without label" at the BRQ thirty lines away. The assembler now refuses a label that is already an instruction, at the label, by name; every instruction added takes a word out of the space of label names, so this will happen again.
158 lines
4.0 KiB
C
158 lines
4.0 KiB
C
// assembly.c
|
|
// These are functions useful for translating assembly mnemonics to hex and vice-versa for the SplitBit CPU.
|
|
// Written by Anachronaut
|
|
// 10/18/2024
|
|
|
|
#include "assembly.h"
|
|
#include <string.h>
|
|
|
|
typedef struct {
|
|
uint8_t opcode;
|
|
const char* mnemonic;
|
|
} Instruction;
|
|
|
|
Instruction instruction_set[] = {
|
|
// Arithmetic and Logic Operations:
|
|
{0x00, "ADD"},
|
|
{0x01, "SUB"},
|
|
{0x02, "AND"},
|
|
{0x03, "OR"},
|
|
{0x04, "XOR"},
|
|
{0x05, "NOTA"},
|
|
{0x06, "NOTB"},
|
|
{0x07, "SHL"},
|
|
{0x08, "SHR"},
|
|
// Branch Operations:
|
|
{0x10, "BRI"},
|
|
{0x11, "BRQ"},
|
|
{0x12, "BRA"},
|
|
{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"},
|
|
{0x16, "RCAL"},
|
|
{0x17, "CALL"},
|
|
{0x18, "SWI"},
|
|
{0x19, "RETI"},
|
|
{0x1E, "RRET"},
|
|
{0x1F, "RET"},
|
|
// Register Operations:
|
|
{0x20, "RSTA"},
|
|
{0x21, "RSTB"},
|
|
{0x22, "INCA"},
|
|
{0x23, "INCB"},
|
|
{0x24, "DECA"},
|
|
{0x25, "DECB"},
|
|
{0x26, "INIA"},
|
|
{0x27, "INIB"},
|
|
{0x28, "CCF"},
|
|
{0x29, "MVQA"},
|
|
{0x2A, "MVQB"},
|
|
{0x2B, "SIF"},
|
|
{0x2C, "CIF"},
|
|
// Stack Operations:
|
|
{0x30, "PSHQ"},
|
|
{0x31, "PSHA"},
|
|
{0x32, "PSHB"},
|
|
{0x33, "PSHD"},
|
|
{0x34, "POPA"},
|
|
{0x35, "POPB"},
|
|
{0x36, "POPD"},
|
|
// Data Operations:
|
|
{0x40, "INCD"},
|
|
{0x41, "DECD"},
|
|
{0x42, "LDA"},
|
|
{0x43, "LDB"},
|
|
{0x44, "STQ"},
|
|
{0x45, "STA"},
|
|
{0x46, "STB"},
|
|
{0x47, "SETD"},
|
|
{0x48, "DPUP"},
|
|
{0x49, "DPDN"},
|
|
{0x4A, "LDD"},
|
|
{0x4B, "STD"},
|
|
{0x4C, "MVSD"},
|
|
{0x4D, "MVDS"},
|
|
{0x4E, "DPUA"},
|
|
{0x4F, "DPDA"},
|
|
{0x50, "DPUW"},
|
|
{0x51, "DPDW"},
|
|
// Output Operations:
|
|
{0xD0, "OUTQ"},
|
|
{0xD1, "OUTA"},
|
|
{0xD2, "OUTB"},
|
|
// Input Operations:
|
|
{0xE0, "INA"},
|
|
{0xE1, "INB"},
|
|
// Special Operations:
|
|
{0xF0, "NOP"},
|
|
{0xFE, "WAIT"},
|
|
{0xFF, "HALT"}
|
|
};
|
|
|
|
int num_instructions = sizeof(instruction_set) / sizeof(Instruction);
|
|
|
|
const char* getMnemonic(uint8_t opcode) {
|
|
for (int i = 0; i < num_instructions; i++) {
|
|
if (instruction_set[i].opcode == opcode) {
|
|
return instruction_set[i].mnemonic;
|
|
}
|
|
}
|
|
return "---";
|
|
}
|
|
|
|
int dataPointerOperands(uint8_t opcode) {
|
|
// How many Data Pointer selector bytes follow this opcode. Most instructions
|
|
// have none. The ones that work through a pointer have one naming which pointer.
|
|
// LDD and STD move a pointer through a pointer, so they name two.
|
|
switch (opcode) {
|
|
case 0x4A: // LDD
|
|
case 0x4B: // STD
|
|
return 2;
|
|
case 0x15: // BRD
|
|
case 0x33: // PSHD
|
|
case 0x36: // POPD
|
|
case 0x40: // INCD
|
|
case 0x41: // DECD
|
|
case 0x42: // LDA
|
|
case 0x43: // LDB
|
|
case 0x44: // STQ
|
|
case 0x45: // STA
|
|
case 0x46: // STB
|
|
case 0x47: // SETD
|
|
case 0x48: // DPUP
|
|
case 0x49: // DPDN
|
|
case 0x4C: // MVSD
|
|
case 0x4D: // MVDS
|
|
case 0x4E: // DPUA
|
|
case 0x4F: // DPDA
|
|
case 0x50: // DPUW
|
|
case 0x51: // DPDW
|
|
return 1;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
int getOpcode(char* mnemonic) {
|
|
for (int i = 0; i < num_instructions; i++) {
|
|
if (strcmp(instruction_set[i].mnemonic, mnemonic) == 0) {
|
|
return instruction_set[i].opcode;
|
|
}
|
|
}
|
|
// NOT_AN_OPCODE, and it is negative on purpose. This used to answer 0xFE on the
|
|
// grounds that 0xFE was unused - which was true until WAIT was given that opcode, at
|
|
// which point the assembler would have read WAIT as a word it did not recognise. A
|
|
// sentinel picked from the unused half of a range stops being a sentinel the moment
|
|
// somebody uses the range, so this one is outside the range altogether.
|
|
return NOT_AN_OPCODE;
|
|
}
|
|
|
|
|