Three blocks move and nothing else changes. Branches take 0x60, subroutines take 0x70, and the ALU moves up into the 0x10 block the two of them used to share. Order within each block is preserved exactly - this relocates them, it does not rethink them. WHAT IT BUYS IS AN EMPTY 0x00 TO 0x0F. Program Memory that was never written, or a load that stopped part way and left zeroes in its tail, used to read as a long run of ADDs: the machine carried on through them, arrived somewhere unpredictable, and whatever broke there was a long way from the byte that caused it. Now it faults where it is met: Fault: 0x00 at Program Address 0x0004 is not an instruction. That is the address of the byte after the last real instruction, which is the difference between a diagnosis and a search. Reserving the whole nibble rather than just 0x00 means a run into blank memory faults wherever it starts rather than only when it lands on the right byte. runOffTest records it, and the block is left empty for whatever turns out to want it. The other half is room: branches and subroutines had filled 0x10 to 0x1F between them, so a service return that keeps Q and DP3 had nowhere to sit next to its family. It has 0x76 waiting now. Five places wrote an opcode down that the scripted remap did not reach, and four of them were found by tests rather than by looking: - secondPass.c lists which opcodes take an address, and firstPass.c knows SWI by number. Missing those made XOR read as a branch. - Asm.asm knows SWI by number too, being the other assembler. Missing it made the native and host assemblers disagree byte for byte, which is exactly the check that exists to catch a thing known in two places. - loaderTest.asm carries a hand written payload, and its RETI was 0x19. To the assembler those are numbers and to the program they are data, so nothing but running it could notice. It says so in a comment now. - The Assembler Manual prints the bytes hello.asm assembles to, and two of them were branches. The monitor's recorded disassembly moved by exactly the bytes it should: 18 became 72 wherever SWI appears, with SETD and INIB untouched and every disassembled line still reading the same.
172 lines
4.8 KiB
C
172 lines
4.8 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[] = {
|
|
// ---- Nothing at all in 0x00 to 0x0F ----
|
|
//
|
|
// Kept empty on purpose. Program Memory that has never been written, or a load that
|
|
// stopped part way and left zeroes in its tail, used to read as a long run of
|
|
// additions and then do something unpredictable a long way from the cause. An
|
|
// unassigned byte faults where it is met, with the address, which is the difference
|
|
// between a diagnosis and a search.
|
|
//
|
|
// Arithmetic and Logic Operations:
|
|
{0x10, "ADD"},
|
|
{0x11, "SUB"},
|
|
{0x12, "AND"},
|
|
{0x13, "OR"},
|
|
{0x14, "XOR"},
|
|
{0x15, "NOTA"},
|
|
{0x16, "NOTB"},
|
|
{0x17, "SHL"},
|
|
{0x18, "SHR"},
|
|
// Branch Operations:
|
|
{0x60, "BRI"},
|
|
{0x61, "BRQ"},
|
|
{0x62, "BRA"},
|
|
{0x63, "BRB"},
|
|
{0x64, "BRC"},
|
|
{0x65, "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.
|
|
{0x66, "BNQ"},
|
|
{0x67, "BNA"},
|
|
{0x68, "BNB"},
|
|
{0x69, "BNC"},
|
|
// Subroutine Operations:
|
|
//
|
|
// A block of their own since the branches and these outgrew one nibble between them.
|
|
// Each raw form sits immediately below the ordinary one it cannot be mixed with: RCAL
|
|
// under CALL, RRET under RET, because the frames differ and returning through the
|
|
// wrong one takes the machine somewhere nobody named.
|
|
{0x70, "RCAL"},
|
|
{0x71, "CALL"},
|
|
{0x72, "SWI"},
|
|
{0x73, "RETI"},
|
|
{0x74, "RRET"},
|
|
{0x75, "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 0x65: // 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;
|
|
}
|
|
|
|
|