Long standing assembler bugs fixed, new path system. Make compatibility update.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
; A Fibonacci number generating program that uses two bytes to store the value.
|
||||
|
||||
#Include Libraries/print.asm
|
||||
#Include print.asm
|
||||
|
||||
#Program
|
||||
start:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
; A Fibonacci number generating program that uses four bytes to store the value.
|
||||
|
||||
#Include Libraries/print.asm
|
||||
#Include print.asm
|
||||
|
||||
#Program
|
||||
start:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
; A Fibonacci number generating program that uses only one byte to store the value.
|
||||
|
||||
#Include Libraries/print.asm
|
||||
#Include print.asm
|
||||
|
||||
#Program
|
||||
start:
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
; The initial pattern is a glider. ANSI terminal control codes redraw the field
|
||||
; in place. Press Ctrl-C to stop the emulator.
|
||||
|
||||
#Include Libraries/print.asm
|
||||
#Include print.asm
|
||||
|
||||
#Program
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
; This program asks the user for input, then prints whatever they input back to the console again.
|
||||
; It stores the input string in a buffer in the Data Memory.
|
||||
|
||||
#Include Libraries/print.asm
|
||||
#Include print.asm
|
||||
|
||||
#Program
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
# SplitBit Programs Makefile
|
||||
# Anachronaut
|
||||
#
|
||||
# Builds every SplitBit program into build/, and keeps track of which libraries
|
||||
# each one includes so that editing a library reassembles whatever depends on it.
|
||||
#
|
||||
# make Assemble everything.
|
||||
# make clean Throw away build/.
|
||||
# make run-hello Assemble and run one program.
|
||||
|
||||
ASM ?= ../Assembler
|
||||
EMU ?= ../SplitBit
|
||||
BUILD ?= build
|
||||
|
||||
# Libraries are included by bare name, so the assembler is told where to find them.
|
||||
INCLUDES = -I Libraries
|
||||
|
||||
# The programs worth building. Files in Libraries/ are left out because they have no
|
||||
# entry point of their own, and the ones in testPrograms/ are covered by 'make test'
|
||||
# in the parent directory.
|
||||
PROGRAMS = \
|
||||
hello.asm \
|
||||
printHello.asm \
|
||||
inputTest.asm \
|
||||
replCalculator.asm \
|
||||
Fibonacci/8bitFibonacci.asm \
|
||||
Fibonacci/16bitFibonacci.asm \
|
||||
Fibonacci/32bitFibonacci.asm \
|
||||
primeSieve/8bitSieve.asm \
|
||||
primeSieve/16bitSegmentedSieve.asm \
|
||||
gameOfLife/16x16Life.asm
|
||||
|
||||
BINARIES = $(PROGRAMS:%.asm=$(BUILD)/%.bin)
|
||||
DEPENDENCIES = $(BINARIES:.bin=.d)
|
||||
|
||||
all: $(BINARIES)
|
||||
|
||||
# -M writes out which source files went into the binary, in the form of a make rule.
|
||||
$(BUILD)/%.bin: %.asm
|
||||
@mkdir -p $(@D)
|
||||
$(ASM) $(INCLUDES) -M $(@:.bin=.d) -o $@ $<
|
||||
|
||||
# Assemble and run a single program, as in 'make run-hello'.
|
||||
run-%: $(BUILD)/%.bin
|
||||
$(EMU) $<
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILD)
|
||||
|
||||
# Pull in the dependency rules written by -M above, so that touching a library
|
||||
# reassembles every program that includes it.
|
||||
-include $(DEPENDENCIES)
|
||||
|
||||
.PHONY: all clean
|
||||
@@ -9,7 +9,7 @@
|
||||
;
|
||||
; Output is hexadecimal (0002 through FFFD), separated by spaces.
|
||||
|
||||
#Include Libraries/print.asm
|
||||
#Include print.asm
|
||||
|
||||
#Program
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
; This is an implementation of The Sieve of Eratosthenes that finds all the primes between 2 and 255.
|
||||
|
||||
#Include ../Libraries/print.asm
|
||||
#Include print.asm
|
||||
|
||||
#Program
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
; 10/27/2024
|
||||
|
||||
; Include the subroutine file.
|
||||
#Include Libraries/print.asm
|
||||
#Include print.asm
|
||||
|
||||
#Program
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
; Whitespace is optional. Supported operators are + - * & | and ^.
|
||||
; Arithmetic wraps to eight bits. Enter Q to quit.
|
||||
|
||||
#Include Libraries/print.asm
|
||||
#Include print.asm
|
||||
|
||||
#Program
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
; This program asks the user for input, then prints whatever they input back to the console again.
|
||||
; It stores the input string in a buffer in the Data Memory.
|
||||
|
||||
#Include ../Libraries/print.asm
|
||||
#Include print.asm
|
||||
|
||||
#Program
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
; A test for the new math subroutines.
|
||||
|
||||
#Include ../Libraries/print.asm
|
||||
#Include ../Libraries/int8.asm
|
||||
#Include ../Libraries/int16.asm
|
||||
#Include print.asm
|
||||
#Include int8.asm
|
||||
#Include int16.asm
|
||||
|
||||
#Program
|
||||
start:
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
; Tests LDD and STD, the instructions that move a Data Pointer through Data Memory.
|
||||
;
|
||||
; STD writes a pointer into memory, LDD reads one back out. Together they let a
|
||||
; program build and walk a table of addresses, which is the reason the CPU has
|
||||
; more than one Data Pointer to walk it with.
|
||||
;
|
||||
; Correct output is:
|
||||
; Hello
|
||||
; World
|
||||
; H
|
||||
|
||||
#Program
|
||||
|
||||
start:
|
||||
; Build a two entry address table at runtime.
|
||||
SETD.0 Slot0
|
||||
SETD.1 Hello
|
||||
STD.1.0 ; Write the address in DP1 to the memory addressed by DP0.
|
||||
SETD.0 Slot1
|
||||
SETD.1 World
|
||||
STD.1.0
|
||||
|
||||
; Walk the table, following each entry in turn.
|
||||
SETD.0 Slot0
|
||||
LDD.1.0 ; DP1 becomes the address stored at DP0.
|
||||
CALL printDP1
|
||||
DPUP.0 0d02 ; Step DP0 over the two byte entry.
|
||||
LDD.1.0
|
||||
CALL printDP1
|
||||
|
||||
; A pointer can also follow itself, which is what LDD with one pointer means.
|
||||
SETD.0 Slot0
|
||||
LDD.0.0 ; DP0 becomes the address it was pointing at.
|
||||
LDA.0
|
||||
OUTA 0x00
|
||||
INIA 0x0A
|
||||
OUTA 0x00
|
||||
HALT
|
||||
|
||||
printDP1:
|
||||
; Print the string addressed by DP1. DP1 is preserved across the CALL, so the
|
||||
; caller gets it back untouched.
|
||||
LDA.1
|
||||
BRA printDone
|
||||
OUTA 0x00
|
||||
INCD.1
|
||||
BRI printDP1
|
||||
printDone:
|
||||
INIA 0x0A
|
||||
OUTA 0x00
|
||||
RET
|
||||
|
||||
#Data
|
||||
|
||||
Hello:
|
||||
"Hello"
|
||||
|
||||
World:
|
||||
"World"
|
||||
|
||||
; The table itself. Two entries, two bytes each, filled in at run time.
|
||||
Slot0:
|
||||
0x00 0x00
|
||||
Slot1:
|
||||
0x00 0x00
|
||||
@@ -1,6 +1,6 @@
|
||||
; Tests for the printing subroutines provided by print.asm
|
||||
|
||||
#Include ../Libraries/print.asm
|
||||
#Include print.asm
|
||||
#Include printDigitTest.asm
|
||||
#Include printDecimalTest.asm
|
||||
#Include printHexTest.asm
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
; Tests address tables written down by the assembler.
|
||||
;
|
||||
; Naming a label in the Data Segment places its two byte address there. That is
|
||||
; what lets a program lay down a table of addresses ahead of time and walk it
|
||||
; with LDD, rather than having to build the table at run time with STD.
|
||||
;
|
||||
; The label after the table also checks that the assembler counts those two
|
||||
; bytes when it works out the addresses of everything that follows.
|
||||
;
|
||||
; Correct output is:
|
||||
; one
|
||||
; two
|
||||
; three
|
||||
; AFTER
|
||||
|
||||
#Program
|
||||
|
||||
start:
|
||||
SETD.0 Table
|
||||
INIB 0d3 ; Three entries in the table.
|
||||
|
||||
nextEntry:
|
||||
LDD.1.0 ; DP1 becomes the address held in this table slot.
|
||||
CALL printDP1
|
||||
DPUP.0 0d02 ; Step DP0 over the two byte slot.
|
||||
DECB
|
||||
BRB tableDone
|
||||
BRI nextEntry
|
||||
|
||||
tableDone:
|
||||
; Anything placed after the table has to be where the assembler said it was.
|
||||
SETD.2 After
|
||||
CALL printDP2
|
||||
HALT
|
||||
|
||||
printDP1:
|
||||
LDA.1
|
||||
BRA print1Done
|
||||
OUTA 0x00
|
||||
INCD.1
|
||||
BRI printDP1
|
||||
print1Done:
|
||||
INIA 0x0A
|
||||
OUTA 0x00
|
||||
RET
|
||||
|
||||
printDP2:
|
||||
LDA.2
|
||||
BRA print2Done
|
||||
OUTA 0x00
|
||||
INCD.2
|
||||
BRI printDP2
|
||||
print2Done:
|
||||
INIA 0x0A
|
||||
OUTA 0x00
|
||||
RET
|
||||
|
||||
#Data
|
||||
|
||||
One:
|
||||
"one"
|
||||
Two:
|
||||
"two"
|
||||
Three:
|
||||
"three"
|
||||
|
||||
; The table. Each of these names becomes the two byte address of that string.
|
||||
Table:
|
||||
One
|
||||
Two
|
||||
Three
|
||||
|
||||
After:
|
||||
"AFTER"
|
||||
@@ -44,10 +44,35 @@ make
|
||||
|
||||
### Usage:
|
||||
```
|
||||
./Assembler [assembly file]
|
||||
./Assembler [options] [assembly file]
|
||||
```
|
||||
#### Options:
|
||||
- -o \<file\>: Write the binary to this path.
|
||||
- -I \<dir\>: Look in this directory for included files. May be given more than once.
|
||||
- -M \<file\>: Write out which source files the binary depends on, as a make rule.
|
||||
- -h, --help: Show help and usage information.
|
||||
|
||||
#### Notes:
|
||||
- The assembled binaries are saved with the same name as the assembly source file they're assembled from, with a .bin extension, in the same directory that you call the assembler from.
|
||||
- Without -o, the assembled binary is saved with the same name as the assembly source file, with a .bin extension, in the directory that you call the assembler from.
|
||||
- Included files are looked for beside the file that includes them, and then along the directories given with -I.
|
||||
|
||||
### Building Programs With Make:
|
||||
The assembler is built to work with make. The -o option puts the binary where the build system wants it, and -M writes out which libraries went into it, so that editing a library reassembles everything that includes it.
|
||||
|
||||
Programs/makefile does this for the programs in this repository:
|
||||
```
|
||||
cd Programs
|
||||
make
|
||||
```
|
||||
|
||||
The rule it uses is small enough to copy into your own projects:
|
||||
```
|
||||
$(BUILD)/%.bin: %.asm
|
||||
@mkdir -p $(@D)
|
||||
$(ASM) -I Libraries -M $(@:.bin=.d) -o $@ $<
|
||||
|
||||
-include $(BINARIES:.bin=.d)
|
||||
```
|
||||
|
||||
### Tests:
|
||||
The test suite assembles and runs every program in Programs/ and compares the results against recorded output.
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <libgen.h>
|
||||
#include <unistd.h>
|
||||
#include <getopt.h>
|
||||
#include "Assm-util.h"
|
||||
#include "firstPass.h"
|
||||
#include "secondPass.h"
|
||||
@@ -82,11 +84,83 @@ void assemblerCleanup(intermediateElement *intermediateArray, int arraySize, cha
|
||||
free(outputFileName);
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
if (argc < 2) {
|
||||
fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
|
||||
void printUsage(const char *programName) {
|
||||
printf("Usage: %s [OPTIONS] <sourcefile>\n", programName);
|
||||
printf("\n");
|
||||
printf("Options:\n");
|
||||
printf(" -o <file> Write the binary to this path instead of alongside the source.\n");
|
||||
printf(" -I <dir> Look in this directory for included files. May be given more than once.\n");
|
||||
printf(" -M <file> Write the source files this binary depends on, as a make rule.\n");
|
||||
printf(" -h, --help Display this help message.\n");
|
||||
}
|
||||
|
||||
// Writes a make rule naming every source file that went into the binary, so that a
|
||||
// build system knows to reassemble when any of them changes. The empty rules after it
|
||||
// are so that deleting a library does not leave make with a prerequisite it cannot
|
||||
// build; without them the build stops instead of just reassembling.
|
||||
void writeDependencyFile(const char *dependencyPath, const char *outputPath) {
|
||||
FILE *file = fopen(dependencyPath, "w");
|
||||
if (!file) {
|
||||
fprintf(stderr, RED "Error: Could not open file \"%s\" for writing.\n" RESET, dependencyPath);
|
||||
exit(1);
|
||||
}
|
||||
fprintf(file, "%s:", outputPath);
|
||||
for (int i = 0; i < sourceFileCount(); i++) {
|
||||
fprintf(file, " %s", sourceFile(i));
|
||||
}
|
||||
fprintf(file, "\n\n");
|
||||
// The source itself is index 0, and always exists, so it needs no empty rule.
|
||||
for (int i = 1; i < sourceFileCount(); i++) {
|
||||
fprintf(file, "%s:\n", sourceFile(i));
|
||||
}
|
||||
fclose(file);
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
static struct option long_options[] = {
|
||||
{"output", required_argument, 0, 'o'},
|
||||
{"include", required_argument, 0, 'I'},
|
||||
{"depend", required_argument, 0, 'M'},
|
||||
{"help", no_argument, 0, 'h'},
|
||||
{0, 0, 0, 0 }
|
||||
};
|
||||
char *outputFileName = NULL;
|
||||
char *dependencyFileName = NULL;
|
||||
int option_index = 0;
|
||||
int opt;
|
||||
|
||||
while ((opt = getopt_long(argc, argv, "o:I:M:h", long_options, &option_index)) != -1) {
|
||||
switch (opt) {
|
||||
case 'o':
|
||||
outputFileName = strdup(optarg);
|
||||
break;
|
||||
case 'I':
|
||||
addIncludeDirectory(optarg);
|
||||
break;
|
||||
case 'M':
|
||||
dependencyFileName = strdup(optarg);
|
||||
break;
|
||||
case 'h':
|
||||
printUsage(argv[0]);
|
||||
return 0;
|
||||
default:
|
||||
printUsage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (optind >= argc) {
|
||||
fprintf(stderr, RED "Error: No source file specified.\n" RESET);
|
||||
printUsage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
char *fileName = argv[optind];
|
||||
optind++;
|
||||
if (optind < argc) {
|
||||
fprintf(stderr, RED "Error: Unexpected argument: %s\n" RESET, argv[optind]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Allocate initial space for the intermediate array.
|
||||
size_t arraySize = 1024;
|
||||
intermediateElement *intermediateArray = malloc(arraySize * sizeof(intermediateElement));
|
||||
@@ -95,14 +169,22 @@ int main(int argc, char *argv[]) {
|
||||
exit(1);
|
||||
}
|
||||
|
||||
char *fileName = argv[1];
|
||||
// The file named on the command line is the first source file, and the include
|
||||
// list owns the copy that everything else points at.
|
||||
char *source = recordSourceFile(strdup(fileName));
|
||||
int index = 0;
|
||||
loadFile(&intermediateArray, fileName, &index, &arraySize);
|
||||
loadFile(&intermediateArray, source, &index, &arraySize);
|
||||
populateLabelTable(intermediateArray, index);
|
||||
fillInLabelAddresses(intermediateArray, index);
|
||||
populateOutputBuffers(intermediateArray, index, Program, &programLength, Data, &dataLength);
|
||||
char *outputFileName = createOutputFileName(fileName);
|
||||
if (!outputFileName) {
|
||||
outputFileName = createOutputFileName(fileName);
|
||||
}
|
||||
writeOutputFile(outputFileName, Program, programLength, Data, dataLength);
|
||||
if (dependencyFileName) {
|
||||
writeDependencyFile(dependencyFileName, outputFileName);
|
||||
free(dependencyFileName);
|
||||
}
|
||||
assemblerCleanup(intermediateArray, index, outputFileName);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -51,13 +51,21 @@ int checkIfInstruction(intermediateElement *currentElement) {
|
||||
strcpy(token, currentElement->token);
|
||||
toUppercase(token);
|
||||
|
||||
// An instruction that works through a Data Pointer may name which one by
|
||||
// hanging a selector off the mnemonic, as in LDA.2. Split that off before
|
||||
// looking the mnemonic up.
|
||||
char *selector = strchr(token, '.');
|
||||
if (selector) {
|
||||
*selector = '\0';
|
||||
selector++;
|
||||
// An instruction that works through a Data Pointer names which one by hanging a
|
||||
// selector off the mnemonic, as in LDA.2. LDD and STD move a pointer through a
|
||||
// pointer, so they take two, as in LDD.1.0. Split those off before looking the
|
||||
// mnemonic up.
|
||||
char *selector[MAX_DATA_POINTER_OPERANDS] = { NULL };
|
||||
int selectorsGiven = 0;
|
||||
char *dot = strchr(token, '.');
|
||||
while (dot) {
|
||||
*dot = '\0';
|
||||
dot++;
|
||||
if (selectorsGiven < MAX_DATA_POINTER_OPERANDS) {
|
||||
selector[selectorsGiven] = dot;
|
||||
}
|
||||
selectorsGiven++;
|
||||
dot = strchr(dot, '.');
|
||||
}
|
||||
|
||||
uint8_t opcode = getOpcode(token);
|
||||
@@ -66,32 +74,38 @@ int checkIfInstruction(intermediateElement *currentElement) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int selectorsWanted = dataPointerOperands(opcode);
|
||||
if (selectorsGiven > selectorsWanted) {
|
||||
if (selectorsWanted == 0) {
|
||||
fprintf(stderr, RED "Error: %s does not work through a Data Pointer, so it cannot take a selector.\n" RESET, token);
|
||||
} else {
|
||||
fprintf(stderr, RED "Error: %s takes %d Data Pointer selector(s), but \"%s\" gives %d.\n" RESET, token, selectorsWanted, currentElement->token, selectorsGiven);
|
||||
}
|
||||
printf(" File: %s at line %d.\n", currentElement->fileName, currentElement->lineNumber);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
currentElement->type = INSTRUCTION;
|
||||
currentElement->byteValue = opcode;
|
||||
currentElement->byteLength = 1;
|
||||
currentElement->dataPointer = 0;
|
||||
// The selectors are emitted whether or not they were written, so the length is
|
||||
// fixed by the instruction. Leaving one off is the same as writing 0.
|
||||
currentElement->byteLength = 1 + selectorsWanted;
|
||||
|
||||
if (instructionTakesDataPointer(opcode)) {
|
||||
// The selector is emitted whether or not it was written, so these are
|
||||
// always two bytes. Leaving it off is the same as writing 0.
|
||||
currentElement->byteLength = 2;
|
||||
if (selector) {
|
||||
for (int i = 0; i < selectorsWanted; i++) {
|
||||
currentElement->dataPointer[i] = 0;
|
||||
if (i < selectorsGiven && selector[i]) {
|
||||
char *end;
|
||||
long value = strtol(selector, &end, 10);
|
||||
if (*selector == '\0' || *end != '\0' || value < 0 || value >= DATA_POINTERS) {
|
||||
long value = strtol(selector[i], &end, 10);
|
||||
if (*selector[i] == '\0' || *end != '\0' || value < 0 || value >= DATA_POINTERS) {
|
||||
fprintf(stderr, RED "Error: \"%s\" does not name a Data Pointer.\n Selectors run from 0 to %d.\n" RESET, currentElement->token, DATA_POINTERS - 1);
|
||||
printf(" File: %s at line %d.\n", currentElement->fileName, currentElement->lineNumber);
|
||||
exit(1);
|
||||
}
|
||||
currentElement->dataPointer = (uint8_t)value;
|
||||
currentElement->dataPointer[i] = (uint8_t)value;
|
||||
}
|
||||
} else if (selector) {
|
||||
fprintf(stderr, RED "Error: %s does not work through a Data Pointer, so it cannot take a selector.\n" RESET, token);
|
||||
printf(" File: %s at line %d.\n", currentElement->fileName, currentElement->lineNumber);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (debug) printf("Token: %s is an instruction using Data Pointer %d.\n", currentElement->token, currentElement->dataPointer);
|
||||
if (debug) printf("Token: %s is an instruction with %d selector(s).\n", currentElement->token, selectorsWanted);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include "Assm-util.h"
|
||||
#include "assembly.h"
|
||||
|
||||
#define MAX_INCLUDES 128
|
||||
|
||||
@@ -48,7 +49,7 @@ typedef struct {
|
||||
char* fileName;
|
||||
int lineNumber;
|
||||
uint8_t byteValue;
|
||||
uint8_t dataPointer; // Which Data Pointer this instruction works through, if it works through one.
|
||||
uint8_t dataPointer[MAX_DATA_POINTER_OPERANDS]; // Which Data Pointers this instruction works through, if any.
|
||||
int byteLength;
|
||||
uint16_t address;
|
||||
int type; // "KEYWORD", "INSTRUCTION" , "LABEL" , "VALUE", "STRING"
|
||||
|
||||
@@ -59,6 +59,8 @@ Instruction instruction_set[] = {
|
||||
{0x47, "SETD"},
|
||||
{0x48, "DPUP"},
|
||||
{0x49, "DPDN"},
|
||||
{0x4A, "LDD"},
|
||||
{0x4B, "STD"},
|
||||
// Output Operations:
|
||||
{0xD0, "OUTQ"},
|
||||
{0xD1, "OUTA"},
|
||||
@@ -82,10 +84,14 @@ const char* getMnemonic(uint8_t opcode) {
|
||||
return "---";
|
||||
}
|
||||
|
||||
int instructionTakesDataPointer(uint8_t opcode) {
|
||||
// These instructions all work through a Data Pointer, and so are followed by a
|
||||
// byte naming which one. Everything else is a single byte opcode as before.
|
||||
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 0x33: // PSHD
|
||||
case 0x36: // POPD
|
||||
case 0x40: // INCD
|
||||
|
||||
@@ -8,10 +8,52 @@
|
||||
#ifndef ASSEMBLY_H
|
||||
#define ASSEMBLY_H
|
||||
|
||||
// ---- The SplitBit binary format ----
|
||||
//
|
||||
// A binary starts with a file header, then the Program Segment, then the Data
|
||||
// Segment. All multi byte numbers are stored most significant byte first.
|
||||
//
|
||||
// Offset Size Field
|
||||
// 0 4 "SPBT", so a file that is not a SplitBit binary is spotted at once
|
||||
// 4 1 Format version
|
||||
// 5 4 Required feature flags
|
||||
// 9 3 "PRG"
|
||||
// 12 2 Program Segment length
|
||||
// 14 N Program Segment
|
||||
// .. 3 "DAT"
|
||||
// .. 2 Data Segment length
|
||||
// .. M Data Segment
|
||||
//
|
||||
// The feature flags are how a binary says it needs something the base machine does
|
||||
// not provide, so that an emulator which cannot provide it refuses to run the binary
|
||||
// rather than quietly doing the wrong thing. No features are defined yet; the field
|
||||
// is here so that adding one later does not need another format version.
|
||||
|
||||
#define SPLITBIT_MAGIC "SPBT"
|
||||
#define SPLITBIT_MAGIC_LENGTH 4
|
||||
#define SPLITBIT_FORMAT_VERSION 1
|
||||
#define SPLITBIT_FLAGS_LENGTH 4
|
||||
#define SEGMENT_MARKER_LENGTH 3
|
||||
#define SEGMENT_LENGTH_BYTES 2
|
||||
|
||||
// Everything the format costs a file, on top of the two segments themselves.
|
||||
#define SPLITBIT_HEADER_BYTES (SPLITBIT_MAGIC_LENGTH + 1 + SPLITBIT_FLAGS_LENGTH \
|
||||
+ 2 * (SEGMENT_MARKER_LENGTH + SEGMENT_LENGTH_BYTES))
|
||||
|
||||
// Features this build of the emulator can provide. A binary asking for anything
|
||||
// outside this set is refused.
|
||||
#define SPLITBIT_FEATURES_SUPPORTED 0x00000000u
|
||||
|
||||
// Features the assembler currently needs to ask for. Nothing, so far.
|
||||
#define SPLITBIT_FEATURES_REQUIRED 0x00000000u
|
||||
|
||||
const char* getMnemonic(uint8_t opcode);
|
||||
|
||||
uint8_t getOpcode(char* mnemonic);
|
||||
|
||||
int instructionTakesDataPointer(uint8_t opcode);
|
||||
// How many Data Pointer selector bytes follow the given opcode. Never more than two.
|
||||
#define MAX_DATA_POINTER_OPERANDS 2
|
||||
|
||||
int dataPointerOperands(uint8_t opcode);
|
||||
|
||||
#endif // CPU_H
|
||||
|
||||
+139
-25
@@ -7,46 +7,153 @@
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <ctype.h>
|
||||
#include <libgen.h>
|
||||
#include <limits.h>
|
||||
#include "firstPass.h"
|
||||
#include "Assm-util.h"
|
||||
#include "assembly.h"
|
||||
|
||||
char *includeList[MAX_INCLUDES];
|
||||
// Each source file that has gone into this assembly. The path is kept as it was
|
||||
// written, because that is what reads well in an error message or a dependency
|
||||
// rule. The canonical form is kept alongside it so that the same file reached by
|
||||
// two different routes is recognised as one file and included only once.
|
||||
typedef struct {
|
||||
char *path;
|
||||
char *canonical;
|
||||
} SourceFile;
|
||||
|
||||
SourceFile includeList[MAX_INCLUDES];
|
||||
int includeCount = 0;
|
||||
|
||||
char *includeDirectories[MAX_INCLUDES];
|
||||
int includeDirectoryCount = 0;
|
||||
|
||||
void freeIncludeList() {
|
||||
for (int i = 0; i < includeCount; i++) {
|
||||
if (includeList[i]) {
|
||||
free(includeList[i]);
|
||||
}
|
||||
free(includeList[i].path);
|
||||
free(includeList[i].canonical);
|
||||
}
|
||||
includeCount = 0;
|
||||
for (int i = 0; i < includeDirectoryCount; i++) {
|
||||
free(includeDirectories[i]);
|
||||
}
|
||||
includeDirectoryCount = 0;
|
||||
}
|
||||
|
||||
int isFileIncluded(const char *fileName) {
|
||||
for (int i = 0; i < includeCount; i++) {
|
||||
if (strcmp(includeList[i], fileName) == 0) {
|
||||
return 1;
|
||||
void addIncludeDirectory(const char *directory) {
|
||||
if (includeDirectoryCount >= MAX_INCLUDES) {
|
||||
fprintf(stderr, RED "Error: Too many include directories.\n" RESET);
|
||||
exit(1);
|
||||
}
|
||||
includeDirectories[includeDirectoryCount] = strdup(directory);
|
||||
includeDirectoryCount++;
|
||||
}
|
||||
|
||||
int sourceFileCount() {
|
||||
return includeCount;
|
||||
}
|
||||
|
||||
const char *sourceFile(int index) {
|
||||
return includeList[index].path;
|
||||
}
|
||||
|
||||
char *recordSourceFile(char *path) {
|
||||
// realpath resolves symlinks and things like "..", so that Libraries/print.asm
|
||||
// and ../Libraries/print.asm are known to be the same file.
|
||||
char *canonical = realpath(path, NULL);
|
||||
if (!canonical) {
|
||||
canonical = strdup(path);
|
||||
}
|
||||
for (int i = 0; i < includeCount; i++) {
|
||||
if (strcmp(includeList[i].canonical, canonical) == 0) {
|
||||
// Already assembled once. Including it again is not an error, it just
|
||||
// does nothing, which is what lets two libraries share a dependency.
|
||||
free(canonical);
|
||||
free(path);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
if (includeCount >= MAX_INCLUDES) {
|
||||
fprintf(stderr, RED "Error: Too many included files.\n" RESET);
|
||||
exit(1);
|
||||
}
|
||||
includeList[includeCount].path = path;
|
||||
includeList[includeCount].canonical = canonical;
|
||||
includeCount++;
|
||||
return path;
|
||||
}
|
||||
|
||||
// Joins a directory and a file name into a newly allocated path.
|
||||
static char *joinPath(const char *directory, const char *name) {
|
||||
if (!directory || directory[0] == '\0' || strcmp(directory, ".") == 0) {
|
||||
return strdup(name);
|
||||
}
|
||||
size_t length = strlen(directory) + 1 + strlen(name) + 1;
|
||||
char *joined = malloc(length);
|
||||
if (!joined) {
|
||||
fprintf(stderr, RED "Error: Memory allocation failed.\n" RESET);
|
||||
exit(1);
|
||||
}
|
||||
snprintf(joined, length, "%s/%s", directory, name);
|
||||
return joined;
|
||||
}
|
||||
|
||||
static int fileExists(const char *path) {
|
||||
FILE *file = fopen(path, "r");
|
||||
if (file) {
|
||||
fclose(file);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void addIncludedFile(const char *fileName) {
|
||||
if (includeCount < MAX_INCLUDES) {
|
||||
includeList[includeCount] = strdup(fileName);
|
||||
includeCount++;
|
||||
} else {
|
||||
fprintf(stderr, RED "Error: Too many included files.\n" RESET);
|
||||
exit(1);
|
||||
// Works out where an #Include actually points. An absolute path is taken as given.
|
||||
// Otherwise the file is looked for beside the file that asked for it, so that a
|
||||
// library including its own siblings needs no help, and then along the include
|
||||
// directories in the order they were given. Returns NULL if it is nowhere.
|
||||
static char *resolveInclude(const char *includingFile, const char *requested) {
|
||||
if (requested[0] == '/') {
|
||||
return fileExists(requested) ? strdup(requested) : NULL;
|
||||
}
|
||||
|
||||
char *copy = strdup(includingFile);
|
||||
char *candidate = joinPath(dirname(copy), requested);
|
||||
free(copy);
|
||||
if (fileExists(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
free(candidate);
|
||||
|
||||
for (int i = 0; i < includeDirectoryCount; i++) {
|
||||
candidate = joinPath(includeDirectories[i], requested);
|
||||
if (fileExists(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
free(candidate);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Says where the assembler looked, so that a missing include is easy to fix.
|
||||
static void reportMissingInclude(const char *includingFile, const char *requested, int lineNumber) {
|
||||
fprintf(stderr, RED "Error: Couldn't find included file \"%s\".\n" RESET, requested);
|
||||
printf(" File: %s at line %d.\n", includingFile, lineNumber);
|
||||
printf(" Looked for it:\n");
|
||||
char *copy = strdup(includingFile);
|
||||
printf(" beside %s\n", includingFile);
|
||||
free(copy);
|
||||
for (int i = 0; i < includeDirectoryCount; i++) {
|
||||
printf(" in %s\n", includeDirectories[i]);
|
||||
}
|
||||
if (includeDirectoryCount == 0) {
|
||||
printf(" and nowhere else, because no include directories were given with -I.\n");
|
||||
}
|
||||
}
|
||||
|
||||
int loadFile(intermediateElement **intermediateArray, char *fileName, int *intermediateIndex, size_t *arraySize){
|
||||
// Initial setup.
|
||||
addIncludedFile(fileName);
|
||||
// fileName is a path already resolved and recorded by the caller, and the copy
|
||||
// the include list owns, so element fileNames can safely point at it.
|
||||
int status = NOWHERE;
|
||||
int lineNumber = 1; // Line numbers start at 1.
|
||||
// Open the file.
|
||||
@@ -73,19 +180,26 @@ int loadFile(intermediateElement **intermediateArray, char *fileName, int *inter
|
||||
int testValue = checkIfKeyword(&(*intermediateArray)[*intermediateIndex]);
|
||||
if(testValue > 0) {
|
||||
switch (testValue){
|
||||
case KEYWORD_INCLUDE:
|
||||
case KEYWORD_INCLUDE: {
|
||||
// We need to load another file and process it before continuing.
|
||||
status = NOWHERE;
|
||||
// Get the filename and load up the file.
|
||||
// Get the filename and work out where it actually is.
|
||||
(*intermediateIndex)++;
|
||||
readToken(&(*intermediateArray)[*intermediateIndex], file, &lineNumber);
|
||||
char *includeFile = (*intermediateArray)[*intermediateIndex].token;
|
||||
if (isFileIncluded(includeFile)) {
|
||||
fprintf(stderr, RED "Error: File %s is included more than once.\n" RESET, includeFile);
|
||||
printf(" File: %s at line %d.\n", fileName, lineNumber);
|
||||
char *requested = (*intermediateArray)[*intermediateIndex].token;
|
||||
char *resolved = resolveInclude(fileName, requested);
|
||||
if (!resolved) {
|
||||
reportMissingInclude(fileName, requested, lineNumber);
|
||||
exit(1);
|
||||
}
|
||||
loadFile(intermediateArray, includeFile, intermediateIndex, arraySize);
|
||||
// recordSourceFile takes the path, and hands back NULL if this file
|
||||
// has already been assembled. Including it twice is harmless, which
|
||||
// is what lets two libraries depend on a third.
|
||||
char *owned = recordSourceFile(resolved);
|
||||
if (owned) {
|
||||
loadFile(intermediateArray, owned, intermediateIndex, arraySize);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case KEYWORD_PROGRAM:
|
||||
// Set the state PROGRAM so we mark additional tokens for inclusion into Program Memory.
|
||||
|
||||
@@ -14,6 +14,18 @@
|
||||
|
||||
void freeIncludeList();
|
||||
|
||||
// Adds a directory to search when an #Include is not found beside the file that
|
||||
// asked for it. Directories are searched in the order they were added.
|
||||
void addIncludeDirectory(const char *directory);
|
||||
|
||||
// Records a source file and returns the copy the assembler will keep, which is what
|
||||
// element fileNames point at. Returns NULL if this file has already been included.
|
||||
char *recordSourceFile(char *path);
|
||||
|
||||
// Every source file that went into this assembly, for writing make dependencies.
|
||||
int sourceFileCount();
|
||||
const char *sourceFile(int index);
|
||||
|
||||
int loadFile(intermediateElement **intermediateArray, char *fileName, int *intermediateIndex, size_t *arraySize);
|
||||
|
||||
#endif // FIRSTPASS_H
|
||||
|
||||
@@ -162,8 +162,8 @@ void populateOutputBuffers(intermediateElement *intermediateArray, int arraySize
|
||||
Program[(*programCount)++] = intermediateArray[i].byteValue;
|
||||
// Instructions that work through a Data Pointer carry a selector
|
||||
// byte naming which one, whether or not the programmer wrote it.
|
||||
if (instructionTakesDataPointer(intermediateArray[i].byteValue)) {
|
||||
Program[(*programCount)++] = intermediateArray[i].dataPointer;
|
||||
for (int d = 0; d < dataPointerOperands(intermediateArray[i].byteValue); d++) {
|
||||
Program[(*programCount)++] = intermediateArray[i].dataPointer[d];
|
||||
}
|
||||
// Make sure any operand bytes this instruction expects are present.
|
||||
checkOperands(intermediateArray, arraySize, i);
|
||||
@@ -192,12 +192,12 @@ void populateOutputBuffers(intermediateElement *intermediateArray, int arraySize
|
||||
Data[(*dataCount)++] = '\0'; // Add null terminator to Data buffer
|
||||
break;
|
||||
case LABEL:
|
||||
// The label table reserved two bytes for this, but there's nothing here
|
||||
// that knows how to emit them, so every later Data label would be shifted
|
||||
// out of place. Refuse it rather than assemble something that looks fine.
|
||||
fprintf(stderr, RED "Error: Label \"%s\" used as a value in the Data Segment.\n Label references are only supported in the Program Segment.\n" RESET, intermediateArray[i].token);
|
||||
printf("File: %s at line %d.\n", intermediateArray[i].fileName, intermediateArray[i].lineNumber);
|
||||
exit(1);
|
||||
// A label named in the Data Segment puts its address there, which is
|
||||
// how a program lays down a table of addresses for LDD to walk.
|
||||
// Two bytes, most significant first, the same order addresses are
|
||||
// stored in everywhere else.
|
||||
Data[(*dataCount)++] = (intermediateArray[i].address >> 8) & 0xFF; // High byte
|
||||
Data[(*dataCount)++] = intermediateArray[i].address & 0xFF; // Low byte
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -211,8 +211,18 @@ void writeOutputFile(const char *outputFileName, uint8_t *Program, int programCo
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Write the file header: the magic, the format version, and the features this
|
||||
// binary needs from the machine. An emulator that cannot provide one of those
|
||||
// features refuses the file rather than running it and going quietly wrong.
|
||||
fwrite(SPLITBIT_MAGIC, sizeof(char), SPLITBIT_MAGIC_LENGTH, outputFile);
|
||||
fputc(SPLITBIT_FORMAT_VERSION, outputFile);
|
||||
uint32_t required = SPLITBIT_FEATURES_REQUIRED;
|
||||
for (int i = SPLITBIT_FLAGS_LENGTH - 1; i >= 0; i--) {
|
||||
fputc((required >> (i * 8)) & 0xFF, outputFile); // Most significant byte first.
|
||||
}
|
||||
|
||||
// Write the "PRG" header for the program segment
|
||||
fwrite("PRG", sizeof(char), 3, outputFile);
|
||||
fwrite("PRG", sizeof(char), SEGMENT_MARKER_LENGTH, outputFile);
|
||||
|
||||
// Write the program segment length as a 2-byte value (big-endian)
|
||||
uint16_t programSize = programCount;
|
||||
@@ -227,7 +237,7 @@ void writeOutputFile(const char *outputFileName, uint8_t *Program, int programCo
|
||||
}
|
||||
|
||||
// Write the "DAT" header for the data segment
|
||||
fwrite("DAT", sizeof(char), 3, outputFile);
|
||||
fwrite("DAT", sizeof(char), SEGMENT_MARKER_LENGTH, outputFile);
|
||||
|
||||
// Write the data segment length as a 2-byte value (big-endian)
|
||||
uint16_t dataSize = dataCount;
|
||||
@@ -243,5 +253,5 @@ void writeOutputFile(const char *outputFileName, uint8_t *Program, int programCo
|
||||
|
||||
fclose(outputFile);
|
||||
printf("Successfully wrote SplitBit binary to \"%s\".\n", outputFileName);
|
||||
printf(GREEN " Program Segment size: %d bytes.\n Data Segment size: %d bytes.\n Total size: %d bytes.\n" RESET, programCount, dataCount, (programCount+dataCount+10));
|
||||
printf(GREEN " Program Segment size: %d bytes.\n Data Segment size: %d bytes.\n Total size: %d bytes.\n" RESET, programCount, dataCount, (programCount + dataCount + SPLITBIT_HEADER_BYTES));
|
||||
}
|
||||
|
||||
+77
-57
@@ -4,47 +4,80 @@
|
||||
// 10/16/2024
|
||||
|
||||
#include "bootstrap.h"
|
||||
#include "../Assembler/assembly.h" // For the binary format, which both tools share.
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
int byte = 0;
|
||||
const uint8_t HEADER_LENGTH = 3; // Number of bytes for the header.
|
||||
const uint8_t LENGTH_SIZE = 2; // Number of bytes for the segment length.
|
||||
|
||||
uint32_t readLength(FILE *file) {
|
||||
uint16_t length = 0;
|
||||
for (int i = 0; i < LENGTH_SIZE; i++) {
|
||||
// Reads a number of the given width, most significant byte first.
|
||||
// Returns 1 if the file runs out before the number does.
|
||||
static uint8_t readNumber(FILE *file, int width, const char *what, uint32_t *result) {
|
||||
uint32_t value = 0;
|
||||
for (int i = 0; i < width; i++) {
|
||||
int byte = fgetc(file);
|
||||
if (byte == EOF) {
|
||||
fprintf(stderr, "Error: Unexpected end of file while reading a segment length.\n");
|
||||
return UINT32_MAX;
|
||||
fprintf(stderr, "Error: Unexpected end of file while reading %s.\n", what);
|
||||
return 1;
|
||||
}
|
||||
length = (length << 8) | (uint8_t)byte;
|
||||
value = (value << 8) | (uint8_t)byte;
|
||||
}
|
||||
return length;
|
||||
*result = value;
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t readHeader(FILE *file, const char *expectedHeader) {
|
||||
char header[HEADER_LENGTH];
|
||||
for (int i = 0; i < HEADER_LENGTH; i++) {
|
||||
// Reads a fixed length marker and checks it against the one expected. The caller's
|
||||
// buffer must have room for length + 1 characters; it always comes back with a null
|
||||
// on the end, so it is safe to print in an error message either way.
|
||||
static uint8_t readMarker(FILE *file, const char *expected, int length, char *found) {
|
||||
memset(found, 0, length + 1);
|
||||
for (int i = 0; i < length; i++) {
|
||||
int byte = fgetc(file);
|
||||
if (byte == EOF) {
|
||||
fprintf(stderr, "Error: Unexpected end of file while reading a header.\n");
|
||||
return UINT32_MAX;
|
||||
return 1;
|
||||
}
|
||||
header[i] = (uint8_t)byte;
|
||||
found[i] = (char)byte;
|
||||
}
|
||||
if (strncmp(header, expectedHeader, 3)) {
|
||||
fprintf(stderr, "Error: Bad header.\n");
|
||||
printf("Header: %s\nExpected Header: %s\n", header, expectedHeader);
|
||||
return UINT32_MAX;
|
||||
return strncmp(found, expected, length) != 0;
|
||||
}
|
||||
|
||||
// Reads the file header: the magic, the format version, and the features the binary
|
||||
// says it needs from the machine.
|
||||
static uint8_t readFileHeader(FILE *file) {
|
||||
char magic[SPLITBIT_MAGIC_LENGTH + 1];
|
||||
if (readMarker(file, SPLITBIT_MAGIC, SPLITBIT_MAGIC_LENGTH, magic)) {
|
||||
fprintf(stderr, "Error: This is not a SplitBit binary.\n");
|
||||
if (strncmp(magic, "PRG", 3) == 0) {
|
||||
fprintf(stderr, " It looks like a binary from before the format carried a version.\n Reassemble it and try again.\n");
|
||||
} else {
|
||||
fprintf(stderr, " Expected the file to begin with \"%s\", found \"%s\".\n", SPLITBIT_MAGIC, magic);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint32_t version;
|
||||
if (readNumber(file, 1, "the format version", &version)) {
|
||||
return 1;
|
||||
}
|
||||
if (version != SPLITBIT_FORMAT_VERSION) {
|
||||
fprintf(stderr, "Error: This binary is in format version %u, and this emulator reads version %u.\n", version, SPLITBIT_FORMAT_VERSION);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint32_t required;
|
||||
if (readNumber(file, SPLITBIT_FLAGS_LENGTH, "the feature flags", &required)) {
|
||||
return 1;
|
||||
}
|
||||
uint32_t missing = required & ~(uint32_t)SPLITBIT_FEATURES_SUPPORTED;
|
||||
if (missing) {
|
||||
fprintf(stderr, "Error: This binary was built for a machine this emulator cannot provide.\n");
|
||||
fprintf(stderr, " It asks for feature bits 0x%08X, which are not implemented here.\n", missing);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t loadSegment(FILE *file, uint8_t *Memory, uint16_t length) {
|
||||
for (int16_t i = 0; i < length; i++) {
|
||||
byte = fgetc(file);
|
||||
static uint8_t loadSegment(FILE *file, uint8_t *Memory, uint32_t length) {
|
||||
for (uint32_t i = 0; i < length; i++) {
|
||||
int byte = fgetc(file);
|
||||
if (byte == EOF) {
|
||||
fprintf(stderr, "Error: Unexpected end of file while reading a segment.\n");
|
||||
return 1;
|
||||
@@ -54,44 +87,31 @@ uint8_t loadSegment(FILE *file, uint8_t *Memory, uint16_t length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Reads one segment: its marker, its length, and then its contents.
|
||||
static uint8_t readSegment(FILE *file, const char *marker, uint8_t *Memory) {
|
||||
char found[SEGMENT_MARKER_LENGTH + 1];
|
||||
if (readMarker(file, marker, SEGMENT_MARKER_LENGTH, found)) {
|
||||
fprintf(stderr, "Error: Expected a \"%s\" segment here, found \"%s\".\n", marker, found);
|
||||
return 1;
|
||||
}
|
||||
uint32_t length;
|
||||
if (readNumber(file, SEGMENT_LENGTH_BYTES, "a segment length", &length)) {
|
||||
return 1;
|
||||
}
|
||||
return loadSegment(file, Memory, length);
|
||||
}
|
||||
|
||||
uint8_t loadFile(char *path, uint8_t *Program, uint8_t *Data) {
|
||||
FILE *file = fopen(path, "rb");
|
||||
if (file == NULL) {
|
||||
fprintf(stderr, "Error: Couldn't open file: %s\n", path);
|
||||
return 1;
|
||||
}
|
||||
// Read the first three bytes and check if they're the PRG header.
|
||||
if (readHeader(file, "PRG") == UINT32_MAX) {
|
||||
// The Program Segment must come first, then the Data Segment. Short circuiting
|
||||
// here means there is one exit, and so only one place that has to close the file.
|
||||
uint8_t failed = readFileHeader(file)
|
||||
|| readSegment(file, "PRG", Program)
|
||||
|| readSegment(file, "DAT", Data);
|
||||
fclose(file);
|
||||
return 1;
|
||||
}
|
||||
// Now we need to get the length of the Program Section.
|
||||
uint32_t length = readLength(file);
|
||||
if (length == UINT32_MAX) {
|
||||
fclose(file);
|
||||
return 1;
|
||||
}
|
||||
// Now we load the Program Segment.
|
||||
if (loadSegment(file, Program, (uint16_t)length)) {
|
||||
fclose(file);
|
||||
return 1;
|
||||
}
|
||||
// Okay, Program is loaded, now do the same thing but for Data.
|
||||
// Check the header.
|
||||
if (readHeader(file, "DAT") == UINT32_MAX) {
|
||||
return 1;
|
||||
}
|
||||
// Get the legnth of the Data Section.
|
||||
length = readLength(file);
|
||||
if (length == UINT32_MAX){
|
||||
fclose(file);
|
||||
return 1;
|
||||
}
|
||||
// Now load the Data Section.
|
||||
if (loadSegment(file, Data, (uint16_t)length)) {
|
||||
fclose(file);
|
||||
return 1;
|
||||
}
|
||||
fclose(file);
|
||||
return 0;
|
||||
return failed;
|
||||
}
|
||||
|
||||
@@ -368,6 +368,28 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
||||
*target -= cpu->Program[cpu->ProgramCounter];
|
||||
}
|
||||
break;
|
||||
case 0x4A: {
|
||||
// LDD - Load the first Data Pointer from the two bytes of Data Memory
|
||||
// addressed by the second. Byte order matches everywhere else an address
|
||||
// is stored: most significant first, then least significant.
|
||||
// The source is taken by value before anything is written, so LDD.0.0
|
||||
// follows the pointer in DP0 rather than tripping over itself.
|
||||
uint16_t *destination = selectDataPointer(cpu);
|
||||
uint16_t source = *selectDataPointer(cpu);
|
||||
*destination = (uint16_t)cpu->Data[source] << 8;
|
||||
*destination |= (uint16_t)cpu->Data[(uint16_t)(source + 1)];
|
||||
}
|
||||
break;
|
||||
case 0x4B: {
|
||||
// STD - Store the first Data Pointer into the two bytes of Data Memory
|
||||
// addressed by the second. The cast on the second address keeps it inside
|
||||
// Data Memory when the pointer sits at the very top of it.
|
||||
uint16_t value = *selectDataPointer(cpu);
|
||||
uint16_t address = *selectDataPointer(cpu);
|
||||
cpu->Data[address] = (value >> 8) & 0xFF;
|
||||
cpu->Data[(uint16_t)(address + 1)] = value & 0xFF;
|
||||
}
|
||||
break;
|
||||
//
|
||||
// Dx - Output Operations:
|
||||
//
|
||||
|
||||
@@ -6,6 +6,11 @@ A semicolon, ';', denotes the start of a comment, anything beyond it on a line i
|
||||
|
||||
Special Keywords are denoted with hash marks, '#'. The Keywords are #Include, #Program, and #Data.
|
||||
|
||||
SplitBit programs must have a Program Segment. You define the start of a program with the #Program Keyword.
|
||||
SplitBit programs may have a Data Segment. You may define the start of the data with the #Data Keyword.
|
||||
|
||||
## Literal Values:
|
||||
|
||||
Literal values may be defined in a few ways. Numerical values must be within the range of a single 8 bit integer.
|
||||
The assembler will accept:
|
||||
- Hexadecimal values prefaced with 0x, eg. 0x00, 0x7F.
|
||||
@@ -14,13 +19,10 @@ The assembler will accept:
|
||||
|
||||
Any token beginning with a '0' is read as a numerical literal, so a malformed one is an error rather than something the assembler tries to interpret as a label. This also means a label cannot begin with a '0'.
|
||||
|
||||
Instructions that read operand bytes out of Program Memory must be followed by those operands. The branch instructions and CALL take a label; SETD takes a label or a pair of literal bytes; INIA, INIB, DPUP, DPDN, and the input and output instructions each take a single literal byte.
|
||||
|
||||
## Labels:
|
||||
|
||||
Labels may be a string of up to 32 alphanumeric characters that must end with a colon, ':'.
|
||||
|
||||
A label may be referenced by name, without the colon, to place its two byte address into the Program Segment. Label references are only valid in the Program Segment.
|
||||
|
||||
```
|
||||
programStart:
|
||||
|
||||
@@ -29,15 +31,108 @@ loopStart:
|
||||
errorHandler01:
|
||||
```
|
||||
|
||||
A label may be referenced by name, without the colon, to place its two byte address wherever the reference appears.
|
||||
|
||||
The #Include Keyword tells the assembler to load another file to be assembled along with the current file. It is more or less equivalent to copying the contents of the included file into the current file being processed. You simply put the path to the file to include in quotes after the keyword.
|
||||
In the Program Segment that is how the branch instructions and SETD are given somewhere to go. In the Data Segment it writes the address down as data, which is how a table of addresses is built for LDD to walk.
|
||||
|
||||
```
|
||||
#Include string.asm
|
||||
#Data
|
||||
|
||||
One:
|
||||
"one"
|
||||
Two:
|
||||
"two"
|
||||
|
||||
Table: ; Two entries, each the two byte address of a string above.
|
||||
One
|
||||
Two
|
||||
```
|
||||
|
||||
SplitBit programs must have a Program Segment. You define the start of a program with the #Program Keyword.
|
||||
SplitBit programs may have a Data Segment. You may define the start of the data with the #Data Keyword.
|
||||
## Naming a Data Pointer:
|
||||
|
||||
The instructions that work through a Data Pointer name which one by hanging a selector off the mnemonic, after a full stop.
|
||||
|
||||
```
|
||||
LDA.2 ; Load A through Data Pointer 2.
|
||||
STA.1 ; Store A through Data Pointer 1.
|
||||
INCD.2 ; Step Data Pointer 2 along.
|
||||
SETD.3 Grid ; Aim Data Pointer 3 at Grid.
|
||||
```
|
||||
|
||||
Leave the selector off and the instruction uses Data Pointer 0, so a program that only needs one pointer never has to write one.
|
||||
|
||||
```
|
||||
LDA ; Exactly the same as LDA.0
|
||||
```
|
||||
|
||||
LDD and STD move a pointer through a pointer, so they take two selectors. The first names the pointer being moved and the second names the pointer that addresses it. Either may be left off, and again means Data Pointer 0.
|
||||
|
||||
```
|
||||
LDD.1.0 ; Data Pointer 1 becomes the address stored at Data Pointer 0.
|
||||
STD.1.0 ; Store Data Pointer 1 into the memory addressed by Data Pointer 0.
|
||||
LDD.2 ; Same as LDD.2.0
|
||||
LDD ; Same as LDD.0.0, which makes DP0 follow the address it holds.
|
||||
```
|
||||
|
||||
Writing a selector on an instruction that does not work through a Data Pointer is an error, as is naming a pointer the machine does not have, or giving an instruction more selectors than it takes.
|
||||
|
||||
## Instruction Operands:
|
||||
|
||||
Instructions that read operand bytes out of Program Memory must be followed by those operands. The branch instructions and CALL take a label; SETD takes a label or a pair of literal bytes; INIA, INIB, DPUP, DPDN, and the input and output instructions each take a single literal byte.
|
||||
|
||||
Data Pointer selectors do not count as operands here, because they are written on the mnemonic rather than after it.
|
||||
|
||||
## Including Other Files:
|
||||
|
||||
The #Include Keyword tells the assembler to load another file to be assembled along with the current file. It is more or less equivalent to copying the contents of the included file into the current file being processed. You simply put the name of the file to include after the keyword.
|
||||
|
||||
```
|
||||
#Include print.asm
|
||||
```
|
||||
|
||||
The assembler looks for that file in two places, in this order:
|
||||
|
||||
1. Beside the file that asked for it. A library including its own siblings needs no help.
|
||||
2. Along the include directories given with -I on the command line, in the order they were given.
|
||||
|
||||
An absolute path is taken as it is written. If the file turns up nowhere, the assembler says so and lists every place it looked.
|
||||
|
||||
Because a library is normally referred to by name alone, a program that uses one has to be told where the libraries live:
|
||||
|
||||
```
|
||||
Assembler -I Libraries primeSieve/8bitSieve.asm
|
||||
```
|
||||
|
||||
Including the same file twice does nothing the second time, so two libraries may both depend on a third without the program that uses them having to know. The assembler compares files by where they really are rather than by how they were spelled, so the same library reached by two different routes is still only assembled once.
|
||||
|
||||
## Running the Assembler:
|
||||
|
||||
```
|
||||
Assembler [options] <sourcefile>
|
||||
```
|
||||
|
||||
| Option | Meaning |
|
||||
| -- | -- |
|
||||
| -o, --output \<file\> | Write the binary to this path. Without it, the binary is named after the source file, with a .bin extension, in the directory the assembler was run from. |
|
||||
| -I, --include \<dir\> | Look in this directory for included files. May be given more than once, and the directories are searched in the order given. |
|
||||
| -M, --depend \<file\> | Write out which source files went into the binary, as a make rule. |
|
||||
| -h, --help | Print the options and stop. |
|
||||
|
||||
The assembler stops at the first error, says which file and line it was in, and exits without writing a binary.
|
||||
|
||||
## Building With Make:
|
||||
|
||||
The -o and -M options are there so that the assembler fits into a build system. -o puts the binary wherever the build wants it, and -M writes down which libraries went into it, so that editing a library reassembles every program that includes it.
|
||||
|
||||
```
|
||||
$(BUILD)/%.bin: %.asm
|
||||
@mkdir -p $(@D)
|
||||
$(ASM) -I Libraries -M $(@:.bin=.d) -o $@ $<
|
||||
|
||||
-include $(BINARIES:.bin=.d)
|
||||
```
|
||||
|
||||
Programs/makefile in this repository builds every program that way, if you would like a longer example to copy.
|
||||
|
||||
## An Example SplitBit Assembly Program:
|
||||
|
||||
@@ -67,4 +162,38 @@ HelloString: ; By uninforced convention, Data Labels start with a capital lette
|
||||
"Hello, World!"
|
||||
```
|
||||
|
||||
## An Example Using More Than One Data Pointer:
|
||||
|
||||
Copying between two places in Data Memory needs two pointers: one to read through and one to write through. With a single pointer this loop has to save and restore it on every pass.
|
||||
|
||||
```
|
||||
; Copy a string from one place in Data Memory to another.
|
||||
|
||||
#Program
|
||||
|
||||
start:
|
||||
SETD.0 Source ; DP0 walks the source.
|
||||
SETD.1 Dest ; DP1 walks the destination.
|
||||
|
||||
copy:
|
||||
LDA.0 ; Read a byte through DP0.
|
||||
BRA copyDone ; A zero byte is the end of the string.
|
||||
STA.1 ; Write it through DP1.
|
||||
INCD.0
|
||||
INCD.1
|
||||
BRI copy
|
||||
|
||||
copyDone:
|
||||
HALT
|
||||
|
||||
#Data
|
||||
|
||||
Source:
|
||||
"Copied through two pointers."
|
||||
|
||||
Dest:
|
||||
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||
```
|
||||
|
||||
Remember that DP0, DP1 and DP2 survive a CALL, so a loop like this one can call a subroutine in the middle without losing either pointer. DP3 does not survive, which is what makes it the pointer a subroutine uses to hand an address back.
|
||||
|
||||
+122
-75
@@ -17,105 +17,120 @@ It has ten registers:
|
||||
|
||||
- The Program Counter is a 16 bit pointer into the Program Memory.
|
||||
- The PC points to the current operation the CPU is executing, it initializes at Program Address 0x0000.
|
||||
- The PC is only modified by the branch instructions and the CALL and RET instructions. It cannot be directly set by the programmer.**
|
||||
- The PC is only modified by the branch instructions and the CALL and RET instructions. It cannot be directly set by the programmer.
|
||||
|
||||
- The Data Pointers (0-3) are 16 bit pointers into the Data Memory.
|
||||
- A DP points to the current bytes of data that the CPU can read or write to, it initializes at Data Address 0x0000.
|
||||
- A DP points to a byte of data that the CPU can read or write, and each one initializes at Data Address 0x0000.
|
||||
- A DP can be set arbitrarily by the programmer to any value.
|
||||
- Every instruction that reads or writes Data Memory names the DP it works through. See Naming a Data Pointer below.
|
||||
- Data Pointers 0, 1 and 2 are preserved through subroutine calls. Data Pointer 3 is not.
|
||||
- Because DP3 is not preserved, a subroutine can use it to pass an address back to the calling routine, in the same way Q passes back a byte. Unlike Q, an address can refer to as much data as you like.
|
||||
|
||||
- The Stack Pointer is a 16 bit pointer into the Data Memory.
|
||||
- The SP points to the current element of the stack, it initializes at location 0xFFFF.
|
||||
- The SP value is only modified by the push and pop instructions and cannot be set by the programmer.
|
||||
- The Stack lives in Data Memory, so a Data Pointer can be aimed at it and used to read what is on it.
|
||||
|
||||
- The Status register is an 8 bit register whose various bits are used as flags. Only three of these flags are used in the current implementation.
|
||||
- The Status register is an 8 bit register whose various bits are used as flags. Only two of these flags are used in the current implementation.
|
||||
- Bit 0 is the Carry/Borrow Flag. Any arithmetic operation either sets or clears it depending on whether or not the result causes Q to overflow/underflow. It is a 1 if a carry/underflow occurred, and a 0 otherwise. If A or B overflows or underflows from the use of an increment or decrement instruction, this flag will also be set. Non-overflowing increments or decrements will also reset it.
|
||||
- Bit 1 is the Stack Collision Flag. It is set if the Data Pointer's value ever meets or exceeds the Stack Pointer's value. This condition also sets the Halt Flag.
|
||||
- Bit 7 is the Halt Flag. It is set by the HALT instruction, or if there is a stack collision.
|
||||
- Bit 7 is the Halt Flag. It is set by the HALT instruction.
|
||||
|
||||
## Naming a Data Pointer:
|
||||
|
||||
Twelve instructions work through a Data Pointer. Each of them carries a selector byte immediately after its opcode, naming which Data Pointer it means. LDD and STD move a pointer through a pointer, so they carry two selectors, the first naming the pointer being moved and the second naming the pointer that addresses it.
|
||||
|
||||
The selector is a full byte, but only enough of it is read to choose among the Data Pointers the machine has. A selector larger than the highest numbered pointer wraps around rather than being rejected, so it is the assembler's job to refuse to write one.
|
||||
|
||||
In assembly the selector is written on the mnemonic itself, as `LDA.2` or `LDD.1.0`. Leaving it off means Data Pointer 0, so a program that only needs one pointer never has to mention them at all. See the Assembler Manual.
|
||||
|
||||
## List of Instructions:
|
||||
|
||||
The Bytes column is the total length of the instruction, counting its opcode, any Data Pointer selectors, and any other operands it reads out of Program Memory.
|
||||
|
||||
### Arithmetic and Logic Operations: 9 Instructions
|
||||
| Hex Code | Mnemonic | Description |
|
||||
| -- | ---- | -- |
|
||||
| 00 | ADD | Adds A, B, and the Carry Flag, the result is stored in Q. |
|
||||
| 01 | SUB | Subtracts B and the Carry Flag from A, the result is stored in Q. |
|
||||
| 02 | AND | Bitwise and of A and B, the result is stored in Q. |
|
||||
| 03 | OR | Bitwise or of A and B, the result is stored in Q. |
|
||||
| 04 | XOR | Bitwise xor of A and B, the result is stored in Q. |
|
||||
| 05 | NOTA | Bitwise inversion of A, the result is stored in Q. |
|
||||
| 06 | NOTB | Bitwise inversion of B, the result is stored in Q. |
|
||||
| 07 | SHL | A and B form a circular shift register. Rotate this register left. |
|
||||
| 08 | SHR | A and B form a circular shift register. Rotate this register right. |
|
||||
| Hex Code | Mnemonic | Bytes | Description |
|
||||
| -- | ---- | -- | -- |
|
||||
| 00 | ADD | 1 | Adds A, B, and the Carry Flag, the result is stored in Q. |
|
||||
| 01 | SUB | 1 | Subtracts B and the Carry Flag from A, the result is stored in Q. |
|
||||
| 02 | AND | 1 | Bitwise and of A and B, the result is stored in Q. |
|
||||
| 03 | OR | 1 | Bitwise or of A and B, the result is stored in Q. |
|
||||
| 04 | XOR | 1 | Bitwise xor of A and B, the result is stored in Q. |
|
||||
| 05 | NOTA | 1 | Bitwise inversion of A, the result is stored in Q. |
|
||||
| 06 | NOTB | 1 | Bitwise inversion of B, the result is stored in Q. |
|
||||
| 07 | SHL | 1 | A and B form a circular shift register. Rotate this register left. |
|
||||
| 08 | SHR | 1 | A and B form a circular shift register. Rotate this register right. |
|
||||
|
||||
### Branch and Subroutine Operations: 7 Instructions
|
||||
| Hex Code | Mnemonic | Description |
|
||||
| -- | ---- | -- |
|
||||
| 10 | BRI | Branch Immediately. Loads the immediate next two bytes of Program Memory into the Program Counter, first the most significant byte, then the least. |
|
||||
| 11 | BRQ | Branch on Q. If Q is zero, loads the immediate next two bytes of Program Memory into the Program Counter. |
|
||||
| 12 | BRA | Branch on A. If A is zero, loads the immediate next two bytes of Program Memory into the Program Counter. |
|
||||
| 13 | BRB | Branch on B. If B is zero, loads the immediate next two bytes of Program Memory into the Program Counter. |
|
||||
| 14 | BRC | Branch if Carry is set. |
|
||||
| 17 | CALL | Call subroutine. Stores all the registers to the Stack, A, B, and the Program Counter, then performs an immediate branch. |
|
||||
| 1F | RET | Restores the saved registers from the Stack, then immediately branches to the Return Address by setting the Program Counter to the next instruction after the last CALL. |
|
||||
|
||||
| Hex Code | Mnemonic | Bytes | Description |
|
||||
| -- | ---- | -- | -- |
|
||||
| 10 | BRI | 3 | Branch Immediately. Loads the immediate next two bytes of Program Memory into the Program Counter, first the most significant byte, then the least. |
|
||||
| 11 | BRQ | 3 | Branch on Q. If Q is zero, loads the immediate next two bytes of Program Memory into the Program Counter. |
|
||||
| 12 | BRA | 3 | Branch on A. If A is zero, loads the immediate next two bytes of Program Memory into the Program Counter. |
|
||||
| 13 | BRB | 3 | Branch on B. If B is zero, loads the immediate next two bytes of Program Memory into the Program Counter. |
|
||||
| 14 | BRC | 3 | Branch if Carry is set. |
|
||||
| 17 | CALL | 3 | Call subroutine. Pushes the Program Counter, Data Pointers 0 through 2, B and A to the Stack, then performs an immediate branch. This costs ten bytes of Stack. |
|
||||
| 1F | RET | 1 | Return from subroutine. Restores A, B, and Data Pointers 0 through 2 from the Stack, then sets the Program Counter to the instruction after the CALL. Data Pointer 3 and Q are left as the subroutine leaves them. |
|
||||
|
||||
### Register Operations: 9 Instructions
|
||||
| Hex Code | Mnemonic | Description |
|
||||
| -- | ---- | -- |
|
||||
| 20 | RSTA | Resets A to 0. |
|
||||
| 21 | RSTB | Resets B to 0. |
|
||||
| 22 | INCA | Adds 1 to A. If it overflows, it sets the Carry Flag, otherwise, it resets it. |
|
||||
| 23 | INCB | Adds 1 to B. If it overflows, it sets the Carry Flag, otherwise, it resets it. |
|
||||
| 24 | DECA | Subtracts 1 from A. If it underflows, it sets the Carry Flag, otherwise, it resets it. |
|
||||
| 25 | DECB | Subtracts 1 from B. If it underflows, it sets the Carry Flag, otherwise, it resets it. |
|
||||
| 26 | INIA | Loads the next byte of Program Memory to A. |
|
||||
| 27 | INIB | Loads the next byte of Program Memory to B. |
|
||||
| 28 | CCF | Clears the Carry Flag. |
|
||||
| Hex Code | Mnemonic | Bytes | Description |
|
||||
| -- | ---- | -- | -- |
|
||||
| 20 | RSTA | 1 | Resets A to 0. |
|
||||
| 21 | RSTB | 1 | Resets B to 0. |
|
||||
| 22 | INCA | 1 | Adds 1 to A. If it overflows, it sets the Carry Flag, otherwise, it resets it. |
|
||||
| 23 | INCB | 1 | Adds 1 to B. If it overflows, it sets the Carry Flag, otherwise, it resets it. |
|
||||
| 24 | DECA | 1 | Subtracts 1 from A. If it underflows, it sets the Carry Flag, otherwise, it resets it. |
|
||||
| 25 | DECB | 1 | Subtracts 1 from B. If it underflows, it sets the Carry Flag, otherwise, it resets it. |
|
||||
| 26 | INIA | 2 | Loads the next byte of Program Memory to A. |
|
||||
| 27 | INIB | 2 | Loads the next byte of Program Memory to B. |
|
||||
| 28 | CCF | 1 | Clears the Carry Flag. |
|
||||
|
||||
### Stack Operations: 7 Instructions
|
||||
| Hex Code | Mnemonic | Description |
|
||||
| -- | ---- | -- |
|
||||
| 30 | PSHQ | Stores Q into Data Memory at the location referenced by the Stack Pointer then decrements the Stack Pointer. |
|
||||
| 31 | PSHA | Stores A into Data Memory at the location referenced by the Stack Pointer then decrements the Stack Pointer. |
|
||||
| 32 | PSHB | Stores B into Data Memory at the location referenced by the Stack Pointer then decrements the Stack Pointer. |
|
||||
| 33 | PSHD | Stores the Data Pointer referenced by the next byte in Program Memory to the stack, with the low byte on top. Decrements the Stack Pointer by two. |
|
||||
| 34 | POPA | Reads the location referenced by the Stack Pointer from Data Memory into A then increments the Stack Pointer. |
|
||||
| 35 | POPB | Reads the location referenced by the Stack Pointer from Data Memory into B then increments the Stack Pointer. |
|
||||
| 36 | POPD | Restores the Data Pointer referenced by the next byte in Program Memory from the stack, increments the Stack Pointer by two. |
|
||||
| Hex Code | Mnemonic | Bytes | Description |
|
||||
| -- | ---- | -- | -- |
|
||||
| 30 | PSHQ | 1 | Stores Q into Data Memory at the location referenced by the Stack Pointer then decrements the Stack Pointer. |
|
||||
| 31 | PSHA | 1 | Stores A into Data Memory at the location referenced by the Stack Pointer then decrements the Stack Pointer. |
|
||||
| 32 | PSHB | 1 | Stores B into Data Memory at the location referenced by the Stack Pointer then decrements the Stack Pointer. |
|
||||
| 33 | PSHD | 2 | Stores the named Data Pointer to the stack, with the low byte on top. Decrements the Stack Pointer by two. |
|
||||
| 34 | POPA | 1 | Reads the location referenced by the Stack Pointer from Data Memory into A then increments the Stack Pointer. |
|
||||
| 35 | POPB | 1 | Reads the location referenced by the Stack Pointer from Data Memory into B then increments the Stack Pointer. |
|
||||
| 36 | POPD | 2 | Restores the named Data Pointer from the stack, increments the Stack Pointer by two. |
|
||||
|
||||
### Data Operations: 10 Instructions
|
||||
| Hex Code | Mnemonic | Description |
|
||||
| -- | ---- | -- |
|
||||
| 40 | INCD | Increments the Data Pointer referenced by the next byte in Program Memory. |
|
||||
| 41 | DECD | Decrements the Data Pointer referenced by the next byte in Program Memory. |
|
||||
| 42 | LDA | Loads the byte referenced from Data Memory by the Data Pointer into A referenced by the next byte in Program Memory. |
|
||||
| 43 | LDB | Loads the byte referenced from Data Memory by the Data Pointer into B referenced by the next byte in Program Memory. |
|
||||
| 44 | STQ | Stores Q into the byte referenced by the Data Pointer in Data Memory referenced by the next byte in Program Memory. |
|
||||
| 45 | STA | Stores A into the byte referenced by the Data Pointer in Data Memory referenced by the next byte in Program Memory. |
|
||||
| 46 | STB | Stores B into the byte referenced by the Data Pointer in Data Memory referenced by the next byte in Program Memory. |
|
||||
| 47 | SETD | Loads the next two bytes of Program Memory into the Data Pointer referenced by the next byte in Program Memory. |
|
||||
| 48 | DPUP | Offset Data Pointer referenced by the next byte in Program Memory up by the value of the immediate byte after of Program Memory. |
|
||||
| 49 | DPDN | Offset Data Pointer referenced by the next byte in Program Memory down by the value of the immediate byte after of Program Memory. |
|
||||
### Data Operations: 12 Instructions
|
||||
| Hex Code | Mnemonic | Bytes | Description |
|
||||
| -- | ---- | -- | -- |
|
||||
| 40 | INCD | 2 | Increments the named Data Pointer. |
|
||||
| 41 | DECD | 2 | Decrements the named Data Pointer. |
|
||||
| 42 | LDA | 2 | Loads the byte addressed by the named Data Pointer into A. |
|
||||
| 43 | LDB | 2 | Loads the byte addressed by the named Data Pointer into B. |
|
||||
| 44 | STQ | 2 | Stores Q into the byte addressed by the named Data Pointer. |
|
||||
| 45 | STA | 2 | Stores A into the byte addressed by the named Data Pointer. |
|
||||
| 46 | STB | 2 | Stores B into the byte addressed by the named Data Pointer. |
|
||||
| 47 | SETD | 4 | Loads the two bytes of Program Memory following the selector into the named Data Pointer, most significant byte first. |
|
||||
| 48 | DPUP | 3 | Offsets the named Data Pointer up by the value of the byte following the selector. |
|
||||
| 49 | DPDN | 3 | Offsets the named Data Pointer down by the value of the byte following the selector. |
|
||||
| 4A | LDD | 3 | Loads the first named Data Pointer from the two bytes of Data Memory addressed by the second, most significant byte first. |
|
||||
| 4B | STD | 3 | Stores the first named Data Pointer into the two bytes of Data Memory addressed by the second, most significant byte first. |
|
||||
|
||||
LDD and STD are how a program follows an address it has stored, rather than one the assembler wrote into the instruction. Together with more than one Data Pointer, they are what makes a table of addresses usable: one pointer walks the table while another follows whatever entry it is on. Naming the same pointer twice, as in `LDD.0.0`, makes that pointer follow the address it is currently holding.
|
||||
|
||||
### Output Operations: 3 Instructions
|
||||
| Hex Code | Mnemonic | Description |
|
||||
| -- | ---- | -- |
|
||||
| D0 | OUTQ | Writes the value of Q to an Output specified by the next byte of Program Memory. |
|
||||
| D1 | OUTA | Writes the value of A to an Output specified by the next byte of Program Memory. |
|
||||
| D2 | OUTB | Writes the value of B to an Output specified by the next byte of Program Memory. |
|
||||
| Hex Code | Mnemonic | Bytes | Description |
|
||||
| -- | ---- | -- | -- |
|
||||
| D0 | OUTQ | 2 | Writes the value of Q to an Output specified by the next byte of Program Memory. |
|
||||
| D1 | OUTA | 2 | Writes the value of A to an Output specified by the next byte of Program Memory. |
|
||||
| D2 | OUTB | 2 | Writes the value of B to an Output specified by the next byte of Program Memory. |
|
||||
|
||||
### Input Operations: 2 Instructions
|
||||
| Hex Code | Mnemonic | Description |
|
||||
| -- | ---- | -- |
|
||||
| E0 | INA | Writes the value of an Input to A. The input port is specified the next byte of Program Memory. |
|
||||
| E1 | INB | Writes the value of an Input to B. The input port is specified the next byte of Program Memory. |
|
||||
| Hex Code | Mnemonic | Bytes | Description |
|
||||
| -- | ---- | -- | -- |
|
||||
| E0 | INA | 2 | Writes the value of an Input to A. The input port is specified by the next byte of Program Memory. |
|
||||
| E1 | INB | 2 | Writes the value of an Input to B. The input port is specified by the next byte of Program Memory. |
|
||||
|
||||
### Special Operations: 2 Instructions
|
||||
| Hex Code | Mnemonic | Description |
|
||||
| -- | ---- | -- |
|
||||
| F0 | NOP | Perform no Operation, increment the Program Counter. |
|
||||
| FF | HALT | Stops CPU Execution. |
|
||||
| Hex Code | Mnemonic | Bytes | Description |
|
||||
| -- | ---- | -- | -- |
|
||||
| F0 | NOP | 1 | Perform no Operation, increment the Program Counter. |
|
||||
| FF | HALT | 1 | Stops CPU Execution. |
|
||||
|
||||
## Input and Output In the Emulator:
|
||||
|
||||
@@ -145,11 +160,43 @@ End:
|
||||
"Hello, World!"
|
||||
```
|
||||
|
||||
Nothing in that program names a Data Pointer, so all of it runs through Data Pointer 0.
|
||||
|
||||
### Structure of a SplitBit Binary File:
|
||||
The Program and Data values are both stored in a single file for loading into the system. The Program Segment must come first, then the Data Segment. The system will look for a three letter header, PRG for program and DAT for data. After the header is a two byte value representing the length of the segment. The length is stored little endian, which is typical for all values in SplitBit. The system loads the memories with the bytes from the file in sequence starting from address 0x0000.
|
||||
|
||||
Here's an example hex dump of the hello world program stored in the proper format:
|
||||
The Program and Data values are both stored in a single file for loading into the system. Every multi byte value in the format is stored most significant byte first, which is the same order the CPU reads addresses out of Program Memory.
|
||||
|
||||
A file begins with a nine byte header:
|
||||
|
||||
| Offset | Size | Field |
|
||||
| -- | -- | -- |
|
||||
| 0 | 4 | The characters `SPBT`, so that a file which is not a SplitBit binary is recognised as such straight away. |
|
||||
| 4 | 1 | The format version. This document describes version 1. |
|
||||
| 5 | 4 | Required feature flags. |
|
||||
|
||||
The feature flags are how a binary states that it needs something the base machine does not provide. An emulator that cannot provide everything a binary asks for refuses to run it, rather than running it and going quietly wrong. No feature bits are defined yet, so the field is currently zero in every binary.
|
||||
|
||||
After the header come the two segments, the Program Segment first and then the Data Segment. Each begins with a three character marker, `PRG` or `DAT`, followed by a two byte length. The system loads each memory with the bytes that follow, in sequence, starting from address 0x0000.
|
||||
|
||||
Here is the hello world program above, assembled and dumped as hex:
|
||||
|
||||
```
|
||||
50 52 47 00 0F 26 12 00 0A D1 00 40 10 00 00 28 0A D1 00 FF 44 41 54 00 0E 48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 21 00
|
||||
53 50 42 54 01 00 00 00 00 50 52 47 00 11 42 00 12 00 0c d1 00 40 00 10 00 00 26 0a d1 00 ff 44
|
||||
41 54 00 0e 48 65 6c 6c 6f 2c 20 57 6f 72 6c 64 21 00
|
||||
```
|
||||
|
||||
Taken apart:
|
||||
|
||||
| Bytes | Meaning |
|
||||
| -- | -- |
|
||||
| `53 50 42 54` | `SPBT` |
|
||||
| `01` | Format version 1 |
|
||||
| `00 00 00 00` | No features required |
|
||||
| `50 52 47` | `PRG` |
|
||||
| `00 11` | The Program Segment is 17 bytes long |
|
||||
| `42 00 12 00 0c d1 00 40 00 10 00 00 26 0a d1 00 ff` | The Program Segment |
|
||||
| `44 41 54` | `DAT` |
|
||||
| `00 0e` | The Data Segment is 14 bytes long |
|
||||
| `48 65 6c 6c 6f 2c 20 57 6f 72 6c 64 21 00` | The Data Segment |
|
||||
|
||||
The `42 00` at the start of the Program Segment is worth a look: `42` is LDA, and the `00` after it is the Data Pointer selector the assembler filled in, because the program did not name one.
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
Hello
|
||||
World
|
||||
H
|
||||
Execution halted after 79 cycles.
|
||||
@@ -0,0 +1,5 @@
|
||||
one
|
||||
two
|
||||
three
|
||||
AFTER
|
||||
Execution halted after 122 cycles.
|
||||
+34
-31
@@ -4,11 +4,10 @@
|
||||
# One test per line, fields separated by '|'. Blank lines and lines starting
|
||||
# with '#' are ignored.
|
||||
#
|
||||
# name | source | assemble-from | mode | stdin | limit
|
||||
# name | source | mode | stdin | limit
|
||||
#
|
||||
# source and assemble-from are both relative to Programs/. assemble-from exists
|
||||
# because #Include paths resolve against the current directory rather than
|
||||
# against the including file, so each program has one directory it builds in.
|
||||
# source is relative to Programs/. Everything assembles from there with
|
||||
# Libraries/ on the include path, and the binary is written into Tests/build.
|
||||
#
|
||||
# Modes:
|
||||
# run Assemble, execute, compare all output against Tests/expected/<name>.out
|
||||
@@ -23,43 +22,47 @@
|
||||
# on what a program prints.
|
||||
|
||||
# ---- Programs that halt on their own ----
|
||||
hello | hello.asm | . | run | - | -
|
||||
printHello | printHello.asm | . | run | - | -
|
||||
8bitFibonacci | Fibonacci/8bitFibonacci.asm | . | run | - | -
|
||||
16bitFibonacci | Fibonacci/16bitFibonacci.asm | . | run | - | -
|
||||
32bitFibonacci | Fibonacci/32bitFibonacci.asm | . | run | - | -
|
||||
8bitSieve | primeSieve/8bitSieve.asm | primeSieve | run | - | -
|
||||
16bitSegmentedSieve | primeSieve/16bitSegmentedSieve.asm | . | run | - | -
|
||||
mathTest | testPrograms/mathTest.asm | testPrograms | run | - | -
|
||||
printTest | testPrograms/printTest.asm | testPrograms | run | - | -
|
||||
int16print | Libraries/int16print.asm | Libraries | run | - | -
|
||||
hello | hello.asm | run | - | -
|
||||
printHello | printHello.asm | run | - | -
|
||||
8bitFibonacci | Fibonacci/8bitFibonacci.asm | run | - | -
|
||||
16bitFibonacci | Fibonacci/16bitFibonacci.asm | run | - | -
|
||||
32bitFibonacci | Fibonacci/32bitFibonacci.asm | run | - | -
|
||||
8bitSieve | primeSieve/8bitSieve.asm | run | - | -
|
||||
16bitSegmentedSieve | primeSieve/16bitSegmentedSieve.asm | run | - | -
|
||||
mathTest | testPrograms/mathTest.asm | run | - | -
|
||||
printTest | testPrograms/printTest.asm | run | - | -
|
||||
int16print | Libraries/int16print.asm | run | - | -
|
||||
|
||||
# ---- The multiple Data Pointer behaviour, which nothing else exercises ----
|
||||
dataPointerTest | testPrograms/dataPointerTest.asm | testPrograms | run | - | -
|
||||
twoPointerCopy | testPrograms/twoPointerCopy.asm | testPrograms | run | - | -
|
||||
dataPointerTest | testPrograms/dataPointerTest.asm | run | - | -
|
||||
twoPointerCopy | testPrograms/twoPointerCopy.asm | run | - | -
|
||||
pointerTableTest | testPrograms/pointerTableTest.asm | run | - | -
|
||||
staticTableTest | testPrograms/staticTableTest.asm | run | - | -
|
||||
|
||||
# ---- Programs driven by console input ----
|
||||
inputTest | inputTest.asm | . | run | inputTest.in | -
|
||||
inputTestOld | testPrograms/inputTest.asm | testPrograms | run | inputTest.in | -
|
||||
replCalculator | replCalculator.asm | . | run | replCalculator.in | -
|
||||
inputTest | inputTest.asm | run | inputTest.in | -
|
||||
inputTestOld | testPrograms/inputTest.asm | run | inputTest.in | -
|
||||
replCalculator | replCalculator.asm | run | replCalculator.in | -
|
||||
|
||||
# ---- Programs that run forever by design, bounded by a cycle count ----
|
||||
# 3,000,000 cycles is about fourteen generations of the glider, which puts
|
||||
# evolveBoard and its pointer juggling through its paces many times over.
|
||||
16x16Life | gameOfLife/16x16Life.asm | . | run | - | 3000000
|
||||
16x16Life | gameOfLife/16x16Life.asm | run | - | 3000000
|
||||
|
||||
# ---- Libraries: no entry point, so only check that they assemble ----
|
||||
lib-int8 | Libraries/int8.asm | . | assemble | - | -
|
||||
lib-int16 | Libraries/int16.asm | . | assemble | - | -
|
||||
lib-int32 | Libraries/int32.asm | . | assemble | - | -
|
||||
lib-math | Libraries/math.asm | . | assemble | - | -
|
||||
lib-int8 | Libraries/int8.asm | assemble | - | -
|
||||
lib-int16 | Libraries/int16.asm | assemble | - | -
|
||||
lib-int32 | Libraries/int32.asm | assemble | - | -
|
||||
lib-math | Libraries/math.asm | assemble | - | -
|
||||
|
||||
# ---- Known breakages, recorded rather than ignored ----
|
||||
# print.asm branches to 'start', which only the including program defines.
|
||||
lib-print | Libraries/print.asm | . | xfail | - | -
|
||||
# These three call print.asm routines but have no #Include line at all.
|
||||
printDecimalTest | testPrograms/printDecimalTest.asm | testPrograms | xfail | - | -
|
||||
printDigitTest | testPrograms/printDigitTest.asm | testPrograms | xfail | - | -
|
||||
printHexTest | testPrograms/printHexTest.asm | testPrograms | xfail | - | -
|
||||
# shiftTest includes "print.asm", which is not beside it.
|
||||
shiftTest | testPrograms/shiftTest.asm | testPrograms | xfail | - | -
|
||||
lib-print | Libraries/print.asm | xfail | - | -
|
||||
# These three call print.asm routines but have no #Include line at all. They are
|
||||
# only ever pulled in by printTest.asm, so they are not standalone programs.
|
||||
printDecimalTest | testPrograms/printDecimalTest.asm | xfail | - | -
|
||||
printDigitTest | testPrograms/printDigitTest.asm | xfail | - | -
|
||||
printHexTest | testPrograms/printHexTest.asm | xfail | - | -
|
||||
# shiftTest calls printDecimal, which print.asm does not have. It looks like the
|
||||
# routine was renamed and this caller was never updated.
|
||||
shiftTest | testPrograms/shiftTest.asm | xfail | - | -
|
||||
|
||||
+12
-12
@@ -49,10 +49,10 @@ for tool in "$ASSEMBLER" "$EMULATOR"; do
|
||||
fi
|
||||
done
|
||||
|
||||
PROGRAMS="$ROOT/Programs"
|
||||
|
||||
rm -rf "$BUILD"
|
||||
mkdir -p "$BUILD" "$EXPECTED"
|
||||
cp -r "$ROOT/Programs/." "$BUILD/"
|
||||
find "$BUILD" -name '*.bin' -delete
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
@@ -104,30 +104,30 @@ check() {
|
||||
}
|
||||
|
||||
assemble() {
|
||||
# assemble <source> <assemble-from>; echoes the built binary path on success
|
||||
local src="$1" dir="$2"
|
||||
local wd="$BUILD/$dir" rel bin
|
||||
rel="$(realpath --relative-to="$wd" "$BUILD/$src")"
|
||||
bin="$wd/$(basename "${src%.asm}").bin"
|
||||
if ( cd "$wd" && "$ASSEMBLER" "$rel" ) >"$BUILD/.assemble.log" 2>&1; then
|
||||
# assemble <name> <source>; echoes the built binary path on success.
|
||||
# Everything builds from Programs/ with Libraries/ on the include path, and the
|
||||
# binary goes to Tests/build, so the source tree is never written to.
|
||||
local name="$1" src="$2"
|
||||
local bin="$BUILD/$name.bin"
|
||||
if ( cd "$PROGRAMS" && "$ASSEMBLER" -I Libraries -o "$bin" "$src" ) >"$BUILD/.assemble.log" 2>&1; then
|
||||
echo "$bin"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
while IFS='|' read -r name src dir mode stdin limit; do
|
||||
while IFS='|' read -r name src mode stdin limit; do
|
||||
name="$(trim "$name")"
|
||||
[ -z "$name" ] && continue
|
||||
case "$name" in \#*) continue ;; esac
|
||||
src="$(trim "$src")"; dir="$(trim "$dir")"
|
||||
src="$(trim "$src")"
|
||||
mode="$(trim "$mode")"; stdin="$(trim "$stdin")"
|
||||
limit="$(trim "$limit")"
|
||||
|
||||
wanted "$name" || continue
|
||||
|
||||
if [ "$mode" = "xfail" ]; then
|
||||
if assemble "$src" "$dir" >/dev/null; then
|
||||
if assemble "$name" "$src" >/dev/null; then
|
||||
FAIL=$((FAIL + 1)); FAILED_NAMES+=("$name")
|
||||
report "FAIL" "$name" "expected assembly to fail, but it succeeded"
|
||||
else
|
||||
@@ -137,7 +137,7 @@ while IFS='|' read -r name src dir mode stdin limit; do
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! BIN="$(assemble "$src" "$dir")"; then
|
||||
if ! BIN="$(assemble "$name" "$src")"; then
|
||||
FAIL=$((FAIL + 1)); FAILED_NAMES+=("$name")
|
||||
report "FAIL" "$name" "assembly failed"
|
||||
head -3 "$BUILD/.assemble.log" | tr -d '\033' | sed 's/\[[0-9;]*m//g' | sed 's/^/ /'
|
||||
|
||||
Reference in New Issue
Block a user