CosmOS pre-alpha and launchable application versions of old programs.
This commit is contained in:
@@ -119,6 +119,45 @@ void writeDependencyFile(const char *dependencyPath, const char *outputPath) {
|
||||
fclose(file);
|
||||
}
|
||||
|
||||
// A program that bases one segment and not the other is a mistake the assembler is the
|
||||
// last place to catch. Nothing relocates, so the unbased half keeps the addresses it was
|
||||
// given, which are addresses from zero up, and the loader puts it there: on top of
|
||||
// whatever the system keeps at the bottom of memory. It does not fail at load time and it
|
||||
// does not fail at the jump. It fails later, somewhere else, as corruption.
|
||||
//
|
||||
// Only a segment with something in it can land on anything, so an empty one says nothing.
|
||||
// A base of zero that was actually asked for is left alone, which is how a program says
|
||||
// it meant it.
|
||||
void checkSegmentBases(const char *fileName) {
|
||||
if (!programIsLoadable()) {
|
||||
return; // A boot image. Both segments begin at zero because that is where they go.
|
||||
}
|
||||
|
||||
const char *segmentName[3];
|
||||
segmentName[PROGRAM] = "Program";
|
||||
segmentName[DATA] = "Data";
|
||||
int segmentEnd[3];
|
||||
segmentEnd[PROGRAM] = programLength;
|
||||
segmentEnd[DATA] = dataLength;
|
||||
|
||||
const int segments[2] = { PROGRAM, DATA };
|
||||
for (int i = 0; i < 2; i++) {
|
||||
int mine = segments[i], other = segments[1 - i];
|
||||
int content = segmentEnd[mine] - segmentBase(mine);
|
||||
if (segmentBaseWasGiven(mine) || content <= 0 || !segmentBaseWasGiven(other)) {
|
||||
continue;
|
||||
}
|
||||
fprintf(stderr, RED "Error: The %s Segment is based at 0x%04X, but the %s Segment\n"
|
||||
" has %d byte%s at 0x0000 and was never given a #Base.\n"
|
||||
" Half a program loaded at zero lands on whatever is already there.\n" RESET,
|
||||
segmentName[other], segmentBase(other), segmentName[mine],
|
||||
content, content == 1 ? "" : "s");
|
||||
printf(" File: %s\n", fileName);
|
||||
printf(" Say \"#Base 0x0000\" in the %s Segment if that is what you meant.\n", segmentName[mine]);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
static struct option long_options[] = {
|
||||
{"output", required_argument, 0, 'o'},
|
||||
@@ -185,7 +224,15 @@ int main(int argc, char *argv[]) {
|
||||
// the buffers are filled, because SWI needs the number its vector was given.
|
||||
populateVectorTable(intermediateArray, index);
|
||||
fillInVectorReferences(intermediateArray, index);
|
||||
// The buffers are filled from wherever each segment is based, so that a byte's place
|
||||
// in the buffer is the address it will have. For a boot image both bases are zero and
|
||||
// this changes nothing.
|
||||
programLength = segmentBase(PROGRAM);
|
||||
dataLength = segmentBase(DATA);
|
||||
populateOutputBuffers(intermediateArray, index, Program, &programLength, Data, &dataLength);
|
||||
// After the buffers, because how much a segment actually holds is not known until it
|
||||
// has been filled, and an empty segment is not a mistake.
|
||||
checkSegmentBases(fileName);
|
||||
if (!outputFileName) {
|
||||
outputFileName = createOutputFileName(fileName);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,30 @@
|
||||
|
||||
int debug = 0;
|
||||
|
||||
static uint16_t segmentBases[3];
|
||||
static int basesGiven = 0;
|
||||
// Per segment, because a base of zero that was asked for and a base of zero that was
|
||||
// never mentioned are different things, and only the second one is likely a mistake.
|
||||
static int baseGiven[3];
|
||||
|
||||
void setSegmentBase(int segment, uint16_t base) {
|
||||
segmentBases[segment] = base;
|
||||
baseGiven[segment] = 1;
|
||||
basesGiven = 1;
|
||||
}
|
||||
|
||||
uint16_t segmentBase(int segment) {
|
||||
return segmentBases[segment];
|
||||
}
|
||||
|
||||
int segmentBaseWasGiven(int segment) {
|
||||
return baseGiven[segment];
|
||||
}
|
||||
|
||||
int programIsLoadable(void) {
|
||||
return basesGiven;
|
||||
}
|
||||
|
||||
void toUppercase(char *str) {
|
||||
for (int i = 0; str[i]; i++) {
|
||||
str[i] = toupper(str[i]);
|
||||
@@ -37,6 +61,10 @@ int checkIfKeyword(intermediateElement *currentElement) {
|
||||
// the number that follows. The file that needs the boundary is then the
|
||||
// file that asks for it, rather than relying on whatever came before.
|
||||
return KEYWORD_ALIGN;
|
||||
} else if (strcmp(currentElement->token, "#Base") == 0) {
|
||||
// Says where this segment is loaded, which makes the program a loadable one
|
||||
// rather than a boot image.
|
||||
return KEYWORD_BASE;
|
||||
} else if (strcmp(currentElement->token, "#Reserve") == 0) {
|
||||
// Puts down the number of zero bytes that follows, so that a label can
|
||||
// stand for a region rather than just its first byte.
|
||||
@@ -172,7 +200,9 @@ int checkIfLiteralValue(intermediateElement *currentElement) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint16_t readCount(intermediateElement *currentElement, const char *what) {
|
||||
// Shared by readCount and readAddress, which differ only in whether zero is an answer.
|
||||
// A count of nothing is a typo; an address of zero is the bottom of memory.
|
||||
static uint16_t readNumber(intermediateElement *currentElement, const char *what, long least) {
|
||||
const char *token = currentElement->token;
|
||||
int base;
|
||||
const char *baseName;
|
||||
@@ -202,14 +232,22 @@ uint16_t readCount(intermediateElement *currentElement, const char *what) {
|
||||
}
|
||||
}
|
||||
long value = strtol(digits, NULL, base);
|
||||
if (value < 1 || value > 0xFFFF) {
|
||||
fprintf(stderr, RED "Error: %s was given \"%s\". It has to be at least 1 and no more than 0xFFFF.\n" RESET, what, token);
|
||||
if (value < least || value > 0xFFFF) {
|
||||
fprintf(stderr, RED "Error: %s was given \"%s\". It has to be at least %ld and no more than 0xFFFF.\n" RESET, what, token, least);
|
||||
printf(" File: %s at line %d.\n", currentElement->fileName, currentElement->lineNumber);
|
||||
exit(1);
|
||||
}
|
||||
return (uint16_t)value;
|
||||
}
|
||||
|
||||
uint16_t readCount(intermediateElement *currentElement, const char *what) {
|
||||
return readNumber(currentElement, what, 1);
|
||||
}
|
||||
|
||||
uint16_t readAddress(intermediateElement *currentElement, const char *what) {
|
||||
return readNumber(currentElement, what, 0);
|
||||
}
|
||||
|
||||
int checkIfLabel(intermediateElement *currentElement) {
|
||||
char *token = currentElement->token;
|
||||
int length = strlen(token);
|
||||
@@ -261,7 +299,15 @@ int readToken(intermediateElement *currentElement, FILE *file, int *lineNumber)
|
||||
if (i < (int)(sizeof(buffer) - 1)) {
|
||||
buffer[i++] = c;
|
||||
} else {
|
||||
fprintf(stderr, "Error: String literal too long.\n");
|
||||
// Say the limit and where it was met. A string long enough to reach this
|
||||
// is usually several lines of help text, and "too long" on its own leaves
|
||||
// somebody counting characters to find out by how much.
|
||||
fprintf(stderr, RED "Error: String literal longer than %d characters.\n"
|
||||
" Every string carries its own zero byte, so two written in a row"
|
||||
" are two strings\n rather than one long one. Give each its own"
|
||||
" label and print them one after another.\n" RESET,
|
||||
(int)(sizeof(buffer) - 1));
|
||||
printf(" File: %s at line %d.\n", currentElement->fileName, *lineNumber);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
#define KEYWORD_VECTORS 4
|
||||
#define KEYWORD_ALIGN 5
|
||||
#define KEYWORD_RESERVE 6
|
||||
#define KEYWORD_BASE 7
|
||||
|
||||
// Destination values.
|
||||
#define NOWHERE 0
|
||||
@@ -86,4 +87,31 @@ int readToken(intermediateElement *currentElement, FILE *file, int *lineNumber);
|
||||
// ever emitted as a byte, so there is no reason to hold them to a byte's range.
|
||||
uint16_t readCount(intermediateElement *currentElement, const char *what);
|
||||
|
||||
// The same, but zero is allowed. #Base takes one of these: a segment deliberately based
|
||||
// at the bottom of memory is a thing a program is entitled to say, and saying it out loud
|
||||
// is how it is told apart from a segment nobody based at all.
|
||||
uint16_t readAddress(intermediateElement *currentElement, const char *what);
|
||||
|
||||
// ---- Where a segment is based ----
|
||||
//
|
||||
// A program that says nothing about this is a boot image: both its segments begin at
|
||||
// zero, and the machine loads them there. A program that gives either segment a base is
|
||||
// meant to be loaded somewhere else, so it is written out as a loadable program instead,
|
||||
// with its addresses in front of it and none of the space below them in the file.
|
||||
//
|
||||
// Nothing relocates anything, so the base a program is assembled for has to be the one it
|
||||
// is loaded at.
|
||||
|
||||
void setSegmentBase(int segment, uint16_t base);
|
||||
|
||||
uint16_t segmentBase(int segment);
|
||||
|
||||
// Whether this particular segment was given one. A segment left at zero because nobody
|
||||
// said otherwise cannot be told from one deliberately based at zero by its value alone,
|
||||
// and the difference is what the mismatch check below is about.
|
||||
int segmentBaseWasGiven(int segment);
|
||||
|
||||
// Whether either segment was given one, which is what decides the kind of file written.
|
||||
int programIsLoadable(void);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -76,6 +76,7 @@ Instruction instruction_set[] = {
|
||||
{0x4A, "LDD"},
|
||||
{0x4B, "STD"},
|
||||
{0x4C, "MVSD"},
|
||||
{0x4D, "MVDS"},
|
||||
// Output Operations:
|
||||
{0xD0, "OUTQ"},
|
||||
{0xD1, "OUTA"},
|
||||
@@ -121,6 +122,7 @@ int dataPointerOperands(uint8_t opcode) {
|
||||
case 0x48: // DPUP
|
||||
case 0x49: // DPDN
|
||||
case 0x4C: // MVSD
|
||||
case 0x4D: // MVDS
|
||||
return 1;
|
||||
default:
|
||||
return 0;
|
||||
|
||||
@@ -155,6 +155,9 @@ int loadFile(intermediateElement **intermediateArray, char *fileName, int *inter
|
||||
// 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;
|
||||
// A base has to come before anything else in its segment, so this remembers whether
|
||||
// that segment has had anything put in it yet.
|
||||
static int segmentUsed[3] = {0, 0, 0};
|
||||
int lineNumber = 1; // Line numbers start at 1.
|
||||
// Open the file.
|
||||
FILE *file = fopen(fileName, "r");
|
||||
@@ -229,6 +232,34 @@ int loadFile(intermediateElement **intermediateArray, char *fileName, int *inter
|
||||
// Set the state to DATA so we mark additional tokens for inclusion into Data Memory.
|
||||
status = DATA;
|
||||
break;
|
||||
case KEYWORD_BASE: {
|
||||
if (status != PROGRAM && status != DATA) {
|
||||
fprintf(stderr, RED "Error: #Base outside the Program or Data Segment.\n There is no segment for it to be the base of.\n" RESET);
|
||||
printf(" File: %s at line %d.\n", fileName, lineNumber);
|
||||
exit(1);
|
||||
}
|
||||
if (segmentUsed[status]) {
|
||||
fprintf(stderr, RED "Error: #Base after something is already in the segment.\n"
|
||||
" A base says where the whole segment begins, so it has to come first.\n" RESET);
|
||||
printf(" File: %s at line %d.\n", fileName, lineNumber);
|
||||
exit(1);
|
||||
}
|
||||
(*intermediateIndex)++;
|
||||
if (!readToken(&(*intermediateArray)[*intermediateIndex], file, &lineNumber)) {
|
||||
fprintf(stderr, RED "Error: #Base without an address.\n" RESET);
|
||||
exit(1);
|
||||
}
|
||||
(*intermediateArray)[*intermediateIndex].fileName = fileName;
|
||||
(*intermediateArray)[*intermediateIndex].lineNumber = lineNumber;
|
||||
setSegmentBase(status, readAddress(&(*intermediateArray)[*intermediateIndex], "#Base"));
|
||||
(*intermediateArray)[*intermediateIndex].type = KEYWORD;
|
||||
(*intermediateArray)[*intermediateIndex].byteLength = 0;
|
||||
(*intermediateArray)[*intermediateIndex].destination = NOWHERE;
|
||||
(*intermediateArray)[*intermediateIndex - 1].destination = NOWHERE;
|
||||
(*intermediateArray)[*intermediateIndex - 1].byteLength = 0;
|
||||
(*intermediateIndex)++;
|
||||
continue;
|
||||
}
|
||||
case KEYWORD_ALIGN:
|
||||
case KEYWORD_RESERVE: {
|
||||
// Both take a count, and both only make sense somewhere that has a
|
||||
@@ -288,8 +319,11 @@ int loadFile(intermediateElement **intermediateArray, char *fileName, int *inter
|
||||
printf(" File: %s at line %d.\n", fileName, lineNumber);
|
||||
exit(1);
|
||||
}
|
||||
// Next, check if it's a literal value.
|
||||
} else if (checkIfLiteralValue(&(*intermediateArray)[*intermediateIndex])) {
|
||||
// Next, check if it's a literal value. A string is never one, however it begins:
|
||||
// the quotes are gone by now, so a string starting with a zero looks exactly like
|
||||
// a malformed literal and used to be rejected as one.
|
||||
} else if ((*intermediateArray)[*intermediateIndex].type != STRING
|
||||
&& checkIfLiteralValue(&(*intermediateArray)[*intermediateIndex])) {
|
||||
// We should check to make sure we have a destination for it.
|
||||
if (status == NOWHERE) {
|
||||
fprintf(stderr, RED "Error: Attempting to write a value to nowhere!\n Did you forget to use the #Program or #Data keyword?\n" RESET);
|
||||
@@ -320,6 +354,10 @@ int loadFile(intermediateElement **intermediateArray, char *fileName, int *inter
|
||||
}
|
||||
}
|
||||
(*intermediateArray)[*intermediateIndex].destination = status;
|
||||
if ((status == PROGRAM || status == DATA)
|
||||
&& (*intermediateArray)[*intermediateIndex].type != KEYWORD) {
|
||||
segmentUsed[status] = 1;
|
||||
}
|
||||
(*intermediateIndex)++;
|
||||
}
|
||||
return 0;
|
||||
|
||||
+129
-12
@@ -14,6 +14,7 @@
|
||||
#include "secondPass.h"
|
||||
#include "Assm-util.h"
|
||||
#include "assembly.h"
|
||||
#include "sbex.h"
|
||||
|
||||
int debugSecondPass = 0;
|
||||
|
||||
@@ -62,8 +63,11 @@ void addLabel(char *labelName, uint16_t address, int type, const char *fileName,
|
||||
}
|
||||
|
||||
void populateLabelTable(intermediateElement *intermediateArray, int arraySize) {
|
||||
int programCount = 0;
|
||||
int dataCount = 0;
|
||||
// Counting starts at the base, so a label in a program built to live somewhere else
|
||||
// already holds the address it will have once it is there. Nothing relocates
|
||||
// anything, which is exactly why this has to be right at assembly time.
|
||||
int programCount = segmentBase(PROGRAM);
|
||||
int dataCount = segmentBase(DATA);
|
||||
// Loop through the array, if there's a label definition, add it to the label list.
|
||||
for (int i = 0; i < arraySize ; i++) {
|
||||
// How many zeroes an #Align comes to depends on where the cursor has reached,
|
||||
@@ -159,6 +163,16 @@ static void vectorError(const char *message, intermediateElement *element) {
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Whether the token at b is written on the same line as the one at a, and so belongs to
|
||||
// the same entry. A line is what tells a name with a handler apart from a name on its own.
|
||||
static int sameLine(intermediateElement *intermediateArray, int a, int b) {
|
||||
if (a < 0 || b < 0) {
|
||||
return 0;
|
||||
}
|
||||
return intermediateArray[a].lineNumber == intermediateArray[b].lineNumber
|
||||
&& intermediateArray[a].fileName == intermediateArray[b].fileName;
|
||||
}
|
||||
|
||||
// The next token belonging to the Vector Segment, or -1 if the segment has run out.
|
||||
static int nextVectorToken(intermediateElement *intermediateArray, int arraySize, int from) {
|
||||
for (int i = from; i < arraySize; i++) {
|
||||
@@ -169,12 +183,28 @@ static int nextVectorToken(intermediateElement *intermediateArray, int arraySize
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void addVector(char *name, uint8_t index, uint16_t base, uint16_t handler, intermediateElement *element) {
|
||||
// A declared vector has a name and a number but no handler, so nothing goes into the
|
||||
// table for it. It exists so that a program can name a service it calls without claiming
|
||||
// to implement it, which is what lets one file be included by both sides.
|
||||
// Where a vector of this name already is, or -1. A name can be met twice: once where it
|
||||
// is declared and once where somebody supplies its handler.
|
||||
static int findVector(const char *name) {
|
||||
for (int i = 0; i < vectorArrayCount; i++) {
|
||||
if (vectorArray[i].name && strcmp(vectorArray[i].name, name) == 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void addVector(char *name, uint8_t index, uint16_t base, uint16_t handler,
|
||||
int declaredOnly, intermediateElement *element) {
|
||||
if (vectorArrayCount >= MAX_VECTORS) {
|
||||
vectorError("Too many vectors defined.", element);
|
||||
}
|
||||
for (int i = 0; i < vectorArrayCount; i++) {
|
||||
if (vectorArray[i].index == index && vectorArray[i].base == base) {
|
||||
if (vectorArray[i].index == index && vectorArray[i].base == base
|
||||
&& !vectorArray[i].declaredOnly && !declaredOnly) {
|
||||
fprintf(stderr, RED "Error: That vector already has a handler.\n" RESET);
|
||||
printf("File: %s at line %d.\n", element->fileName, element->lineNumber);
|
||||
exit(1);
|
||||
@@ -189,6 +219,7 @@ static void addVector(char *name, uint8_t index, uint16_t base, uint16_t handler
|
||||
vectorArray[vectorArrayCount].index = index;
|
||||
vectorArray[vectorArrayCount].base = base;
|
||||
vectorArray[vectorArrayCount].handler = handler;
|
||||
vectorArray[vectorArrayCount].declaredOnly = declaredOnly;
|
||||
vectorArrayCount++;
|
||||
}
|
||||
|
||||
@@ -228,7 +259,26 @@ void populateVectorTable(intermediateElement *intermediateArray, int arraySize)
|
||||
int handlerToken = nextVectorToken(intermediateArray, arraySize, portToken + 1);
|
||||
uint16_t handler = resolveHandler(intermediateArray, handlerToken, "Device");
|
||||
addVector(NULL, intermediateArray[portToken].byteValue, HARDWARE_VECTOR_BASE,
|
||||
handler, &intermediateArray[i]);
|
||||
handler, 0, &intermediateArray[i]);
|
||||
i = nextVectorToken(intermediateArray, arraySize, handlerToken + 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
int handlerToken = nextVectorToken(intermediateArray, arraySize, i + 1);
|
||||
int hasHandler = sameLine(intermediateArray, i, handlerToken);
|
||||
int already = findVector(token);
|
||||
|
||||
if (already >= 0) {
|
||||
// Met before. A handler now is somebody implementing what was declared
|
||||
// earlier, which is how one shared file can serve both sides.
|
||||
if (!hasHandler) {
|
||||
vectorError("That vector is declared more than once.", &intermediateArray[i]);
|
||||
}
|
||||
if (!vectorArray[already].declaredOnly) {
|
||||
vectorError("That vector already has a handler.", &intermediateArray[i]);
|
||||
}
|
||||
vectorArray[already].handler = resolveHandler(intermediateArray, handlerToken, token);
|
||||
vectorArray[already].declaredOnly = 0;
|
||||
i = nextVectorToken(intermediateArray, arraySize, handlerToken + 1);
|
||||
continue;
|
||||
}
|
||||
@@ -250,9 +300,15 @@ void populateVectorTable(intermediateElement *intermediateArray, int arraySize)
|
||||
nextFreeVector++;
|
||||
}
|
||||
|
||||
int handlerToken = nextVectorToken(intermediateArray, arraySize, i + 1);
|
||||
if (!hasHandler) {
|
||||
// Nothing follows it on the line, so this says what the vector is called and
|
||||
// what number it has, and leaves implementing it to somebody else.
|
||||
addVector(token, index, SOFTWARE_VECTOR_BASE, 0, 1, &intermediateArray[i]);
|
||||
i = handlerToken;
|
||||
continue;
|
||||
}
|
||||
uint16_t handler = resolveHandler(intermediateArray, handlerToken, token);
|
||||
addVector(token, index, SOFTWARE_VECTOR_BASE, handler, &intermediateArray[i]);
|
||||
addVector(token, index, SOFTWARE_VECTOR_BASE, handler, 0, &intermediateArray[i]);
|
||||
i = nextVectorToken(intermediateArray, arraySize, handlerToken + 1);
|
||||
}
|
||||
}
|
||||
@@ -440,6 +496,48 @@ void populateOutputBuffers(intermediateElement *intermediateArray, int arraySize
|
||||
}
|
||||
}
|
||||
|
||||
// A loadable program: sixteen bytes saying where it belongs, then the code and the data.
|
||||
// The space below each base is not written out, because nothing needs to carry it: the
|
||||
// header says where the bytes go and the loader puts them there.
|
||||
static void writeLoadable(const char *outputFileName, uint8_t *Program, int programCount,
|
||||
uint8_t *Data, int dataCount) {
|
||||
uint16_t codeBase = segmentBase(PROGRAM);
|
||||
uint16_t dataBase = segmentBase(DATA);
|
||||
int codeLength = programCount - codeBase;
|
||||
int dataLength = dataCount - dataBase;
|
||||
if (codeLength < 0) codeLength = 0;
|
||||
if (dataLength < 0) dataLength = 0;
|
||||
|
||||
FILE *outputFile = fopen(outputFileName, "wb");
|
||||
if (!outputFile) {
|
||||
fprintf(stderr, RED "Error: Could not open file \"%s\" for writing.\n" RESET, outputFileName);
|
||||
exit(1);
|
||||
}
|
||||
uint8_t header[SBEX_HEADER_BYTES];
|
||||
memset(header, 0, sizeof(header));
|
||||
memcpy(header, SBEX_MAGIC, SBEX_MAGIC_BYTES);
|
||||
header[SBEX_VERSION_AT] = SBEX_VERSION;
|
||||
header[SBEX_CODE_AT] = (uint8_t)(codeBase >> 8);
|
||||
header[SBEX_CODE_AT + 1] = (uint8_t)(codeBase & 0xFF);
|
||||
// Where it starts is where it begins. A program that wants otherwise puts a branch
|
||||
// at its first instruction, which costs three bytes and needs no format for it.
|
||||
header[SBEX_ENTRY_AT] = (uint8_t)(codeBase >> 8);
|
||||
header[SBEX_ENTRY_AT + 1] = (uint8_t)(codeBase & 0xFF);
|
||||
header[SBEX_CODE_LEN_AT] = (uint8_t)(codeLength >> 8);
|
||||
header[SBEX_CODE_LEN_AT + 1] = (uint8_t)(codeLength & 0xFF);
|
||||
header[SBEX_DATA_AT] = (uint8_t)(dataBase >> 8);
|
||||
header[SBEX_DATA_AT + 1] = (uint8_t)(dataBase & 0xFF);
|
||||
header[SBEX_DATA_LEN_AT] = (uint8_t)(dataLength >> 8);
|
||||
header[SBEX_DATA_LEN_AT + 1] = (uint8_t)(dataLength & 0xFF);
|
||||
fwrite(header, 1, sizeof(header), outputFile);
|
||||
fwrite(Program + codeBase, 1, (size_t)codeLength, outputFile);
|
||||
fwrite(Data + dataBase, 1, (size_t)dataLength, outputFile);
|
||||
fclose(outputFile);
|
||||
printf("Successfully wrote SplitBit loadable program to \"%s\".\n", outputFileName);
|
||||
printf(GREEN " Code: %d bytes at 0x%04X.\n Data: %d bytes at 0x%04X.\n Total size: %d bytes.\n" RESET,
|
||||
codeLength, codeBase, dataLength, dataBase, SBEX_HEADER_BYTES + codeLength + dataLength);
|
||||
}
|
||||
|
||||
void writeOutputFile(const char *outputFileName, uint8_t *Program, int programCount, uint8_t *Data, int dataCount) {
|
||||
FILE *outputFile = fopen(outputFileName, "wb");
|
||||
if (!outputFile) {
|
||||
@@ -487,16 +585,35 @@ void writeOutputFile(const char *outputFileName, uint8_t *Program, int programCo
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// A program with a base is one meant to be loaded, so it is written out with its
|
||||
// addresses in front of it and nothing below them. A boot image carries the padding
|
||||
// because the machine loads it at zero; a loadable one would only be carrying space
|
||||
// it does not use.
|
||||
if (programIsLoadable()) {
|
||||
fclose(outputFile);
|
||||
writeLoadable(outputFileName, Program, programCount, Data, dataCount);
|
||||
return;
|
||||
}
|
||||
|
||||
// The Vector Segment, only if the program named any. Leaving it out entirely is
|
||||
// what lets a binary written before vectors existed still load: the reader treats
|
||||
// the end of the file as an empty table rather than a missing one.
|
||||
int installed = 0;
|
||||
for (int i = 0; i < vectorArrayCount; i++) {
|
||||
if (!vectorArray[i].declaredOnly) {
|
||||
installed++;
|
||||
}
|
||||
}
|
||||
int vectorBytes = 0;
|
||||
if (vectorArrayCount > 0) {
|
||||
if (installed > 0) {
|
||||
fwrite("VEC", sizeof(char), SEGMENT_MARKER_LENGTH, outputFile);
|
||||
vectorBytes = vectorArrayCount * VECTOR_ENTRY_FILE_BYTES;
|
||||
vectorBytes = installed * VECTOR_ENTRY_FILE_BYTES;
|
||||
fputc((vectorBytes >> 8) & 0xFF, outputFile);
|
||||
fputc(vectorBytes & 0xFF, outputFile);
|
||||
for (int i = 0; i < vectorArrayCount; i++) {
|
||||
if (vectorArray[i].declaredOnly) {
|
||||
continue;
|
||||
}
|
||||
uint16_t slot = vectorArray[i].base + (uint16_t)vectorArray[i].index * VECTOR_ENTRY_BYTES;
|
||||
fputc((slot >> 8) & 0xFF, outputFile);
|
||||
fputc(slot & 0xFF, outputFile);
|
||||
@@ -508,10 +625,10 @@ 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" RESET, programCount, dataCount);
|
||||
if (vectorArrayCount > 0) {
|
||||
printf(GREEN " Vectors: %d.\n" RESET, vectorArrayCount);
|
||||
if (installed > 0) {
|
||||
printf(GREEN " Vectors: %d.\n" RESET, installed);
|
||||
}
|
||||
printf(GREEN " Total size: %d bytes.\n" RESET,
|
||||
(programCount + dataCount + SPLITBIT_HEADER_BYTES
|
||||
+ (vectorArrayCount > 0 ? SEGMENT_MARKER_LENGTH + SEGMENT_LENGTH_BYTES + vectorBytes : 0)));
|
||||
+ (installed > 0 ? SEGMENT_MARKER_LENGTH + SEGMENT_LENGTH_BYTES + vectorBytes : 0)));
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ typedef struct {
|
||||
uint8_t index; // Which vector in its table.
|
||||
uint16_t base; // Which table: software or hardware.
|
||||
uint16_t handler; // Where the handler ended up.
|
||||
int declaredOnly; // Named and numbered, with nobody implementing it here.
|
||||
} VectorEntry;
|
||||
|
||||
void freeLabelList();
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
// sbex.h
|
||||
// The SplitBit loadable program format, version one.
|
||||
//
|
||||
// A program that is not the one the machine booted from has to say where it wants to
|
||||
// live, because nothing relocates it. This is a header saying that, in front of the
|
||||
// bytes themselves. It is the same idea as the load address on the front of a C64 .PRG,
|
||||
// with room for the machine to ask a few more questions later.
|
||||
//
|
||||
// Two things read this: whatever builds one on the host, and the loader running on
|
||||
// SplitBit. As with the filesystem, nothing is shared between them but the specification.
|
||||
//
|
||||
// All multi byte numbers are most significant byte first.
|
||||
//
|
||||
// 0 4 "SBEX"
|
||||
// 4 1 Version
|
||||
// 5 1 Reserved
|
||||
// 6 2 Where the code goes in Program Memory
|
||||
// 8 2 Where to start running, an address in Program Memory
|
||||
// 10 2 How many bytes of code there are
|
||||
// 12 2 Where the data goes in Data Memory
|
||||
// 14 2 How many bytes of data there are
|
||||
// 16 The code, then the data
|
||||
//
|
||||
// Sixteen bytes, so the code begins at a round offset and finding it is one step rather
|
||||
// than an arithmetic. Nothing here relocates anything: the addresses are where the
|
||||
// program was built to live, and putting it anywhere else would leave every branch and
|
||||
// every SETD inside it pointing at the wrong place.
|
||||
//
|
||||
// Written by Anachronaut
|
||||
|
||||
#ifndef SBEX_H
|
||||
#define SBEX_H
|
||||
|
||||
#define SBEX_MAGIC "SBEX"
|
||||
#define SBEX_MAGIC_BYTES 4
|
||||
#define SBEX_VERSION 1
|
||||
#define SBEX_HEADER_BYTES 16
|
||||
|
||||
#define SBEX_VERSION_AT 4
|
||||
#define SBEX_CODE_AT 6
|
||||
#define SBEX_ENTRY_AT 8
|
||||
#define SBEX_CODE_LEN_AT 10
|
||||
#define SBEX_DATA_AT 12
|
||||
#define SBEX_DATA_LEN_AT 14
|
||||
|
||||
#endif // SBEX_H
|
||||
@@ -1,58 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Wraps an assembled SplitBit binary into a loadable program.
|
||||
|
||||
The assembler emits a boot image: a Program Segment that loads at zero and a Data Segment
|
||||
that does the same. A program meant to be loaded somewhere else has to say where it goes,
|
||||
which is what the SBEX header in front of it is for.
|
||||
|
||||
A program says where it lives by reserving the front of each segment, so the addresses
|
||||
given here have to match the reserves in its source. Nothing checks that for you, and
|
||||
nothing relocates anything if you get it wrong.
|
||||
"""
|
||||
import struct
|
||||
import sys
|
||||
|
||||
|
||||
def segments(raw):
|
||||
at = 4 + 1 + 4 # magic, version, feature flags
|
||||
assert raw[:4] == b"SPBT", "not a SplitBit binary"
|
||||
assert raw[at:at + 3] == b"PRG"
|
||||
plen = struct.unpack(">H", raw[at + 3:at + 5])[0]
|
||||
program = raw[at + 5:at + 5 + plen]
|
||||
at = at + 5 + plen
|
||||
assert raw[at:at + 3] == b"DAT"
|
||||
dlen = struct.unpack(">H", raw[at + 3:at + 5])[0]
|
||||
return program, raw[at + 5:at + 5 + dlen]
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 6:
|
||||
sys.exit("usage: wrap.py <binary> <output> <code address> <data address> <entry>")
|
||||
binary, output = sys.argv[1], sys.argv[2]
|
||||
codeAt, dataAt, entry = (int(a, 0) for a in sys.argv[3:6])
|
||||
|
||||
program, data = segments(open(binary, "rb").read())
|
||||
|
||||
# Everything below the address a segment is placed at is the padding the reserve put
|
||||
# there, and is not part of the program.
|
||||
code = program[codeAt:]
|
||||
values = data[dataAt:]
|
||||
|
||||
header = bytearray(16)
|
||||
header[0:4] = b"SBEX"
|
||||
header[4] = 1
|
||||
struct.pack_into(">H", header, 6, codeAt)
|
||||
struct.pack_into(">H", header, 8, entry)
|
||||
struct.pack_into(">H", header, 10, len(code))
|
||||
struct.pack_into(">H", header, 12, dataAt)
|
||||
struct.pack_into(">H", header, 14, len(values))
|
||||
|
||||
with open(output, "wb") as out:
|
||||
out.write(header)
|
||||
out.write(code)
|
||||
out.write(values)
|
||||
print("%s: %d bytes of code at 0x%04X, %d of data at 0x%04X, entry 0x%04X"
|
||||
% (output, len(code), codeAt, len(values), dataAt, entry))
|
||||
|
||||
|
||||
main()
|
||||
@@ -592,6 +592,23 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
|
||||
*target = cpu->StackPointer;
|
||||
}
|
||||
break;
|
||||
case 0x4D: {
|
||||
// MVDS - Copy the selected Data Pointer into the Stack Pointer.
|
||||
//
|
||||
// This one is dangerous and is meant to be used rarely. Moving the Stack
|
||||
// under a running program abandons every return address on it, so a RET
|
||||
// after this goes wherever the new Stack happens to say.
|
||||
//
|
||||
// It exists because a system that runs other programs has no other way to
|
||||
// get its Stack back. A program that gives up part way through leaves
|
||||
// whatever it pushed behind, and the interrupt frame that carried the
|
||||
// request to stop is on there too. Without this the Stack only ever grows
|
||||
// downward, one abandoned program at a time, and a shell cannot outlive
|
||||
// many of them.
|
||||
uint16_t *source = selectDataPointer(cpu);
|
||||
cpu->StackPointer = *source;
|
||||
}
|
||||
break;
|
||||
//
|
||||
// Dx - Output Operations:
|
||||
//
|
||||
|
||||
@@ -111,7 +111,10 @@ int main (int argc, char *argv[]) {
|
||||
if (options.debug) {
|
||||
// Wait before advancing, not after, so that a keypress is what moves the
|
||||
// machine on rather than something that happens once it already has.
|
||||
getchar();
|
||||
// Through the console rather than getchar, so that everything reading standard
|
||||
// input reads it the same way and the console's pushback stays the only place
|
||||
// a byte can be sitting.
|
||||
consoleReadByte();
|
||||
}
|
||||
int cycles;
|
||||
if (options.debug) {
|
||||
|
||||
+179
-3
@@ -7,7 +7,167 @@
|
||||
#include "../Assembler/assembly.h" // For the fault vector numbers.
|
||||
#include "controller.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <signal.h>
|
||||
#include <errno.h>
|
||||
#include <termios.h>
|
||||
#include <unistd.h>
|
||||
#include <poll.h>
|
||||
|
||||
// ---- The console ----
|
||||
//
|
||||
// The console owns its own reading rather than going through getchar. stdio keeps a
|
||||
// buffer, and the status port asks the operating system what is waiting; those two
|
||||
// disagree the moment stdio has read ahead, and the status port would then swear nothing
|
||||
// was there while a read returned instantly. One byte of pushback here is enough, because
|
||||
// nothing needs to look further ahead than the byte it is about to take.
|
||||
|
||||
static int consoleKeyMode = 0;
|
||||
static int consoleEnded = 0;
|
||||
static int consolePushback = -1; // A byte already taken from the host, or -1.
|
||||
static struct termios consoleSavedTerminal;
|
||||
static int consoleTerminalSaved = 0;
|
||||
|
||||
void consoleRestore(void) {
|
||||
if (consoleTerminalSaved) {
|
||||
tcsetattr(STDIN_FILENO, TCSANOW, &consoleSavedTerminal);
|
||||
consoleTerminalSaved = 0;
|
||||
}
|
||||
consoleKeyMode = 0;
|
||||
}
|
||||
|
||||
// Restores the terminal and then dies the way it would have died anyway, so that the
|
||||
// shell sees the signal it was expecting rather than a machine that exited quietly.
|
||||
static void consoleSignalHandler(int signalNumber) {
|
||||
consoleRestore();
|
||||
signal(signalNumber, SIG_DFL);
|
||||
raise(signalNumber);
|
||||
}
|
||||
|
||||
static void consoleSetMode(uint8_t mode) {
|
||||
int wantKeys = (mode & CONSOLE_MODE_KEY) != 0;
|
||||
if (wantKeys == consoleKeyMode) {
|
||||
return;
|
||||
}
|
||||
if (!wantKeys) {
|
||||
consoleRestore();
|
||||
return;
|
||||
}
|
||||
// Nothing to configure when input is not a terminal, but the mode is still recorded:
|
||||
// a program asking the status port what mode it is in should be told what it asked
|
||||
// for, whether or not there was a terminal to carry it out on.
|
||||
consoleKeyMode = 1;
|
||||
if (!isatty(STDIN_FILENO)) {
|
||||
return;
|
||||
}
|
||||
if (!consoleTerminalSaved) {
|
||||
if (tcgetattr(STDIN_FILENO, &consoleSavedTerminal) != 0) {
|
||||
return;
|
||||
}
|
||||
consoleTerminalSaved = 1;
|
||||
// Registered on the first use rather than at startup, so a run that never asks
|
||||
// for key mode installs nothing at all.
|
||||
atexit(consoleRestore);
|
||||
signal(SIGINT, consoleSignalHandler);
|
||||
signal(SIGTERM, consoleSignalHandler);
|
||||
}
|
||||
struct termios raw = consoleSavedTerminal;
|
||||
raw.c_lflag &= (tcflag_t)~(ICANON | ECHO);
|
||||
raw.c_cc[VMIN] = 1;
|
||||
raw.c_cc[VTIME] = 0;
|
||||
tcsetattr(STDIN_FILENO, TCSANOW, &raw);
|
||||
}
|
||||
|
||||
// Everything already written is put where it can be seen before the machine asks the host
|
||||
// anything. Standard output is line buffered on a terminal, so a prompt with no newline
|
||||
// after it - "> " is exactly that, and exactly why this matters - would sit in the buffer
|
||||
// while the machine waited for an answer to a question nobody had been shown.
|
||||
//
|
||||
// getchar used to do this by accident, because reading through stdio flushes the line
|
||||
// buffered streams first. Reading with read() does not, so what was a side effect of the
|
||||
// old way is done deliberately here.
|
||||
static void consoleShowWhatIsWritten(void) {
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
uint8_t consoleReadByte(void) {
|
||||
if (consolePushback >= 0) {
|
||||
uint8_t byte = (uint8_t)consolePushback;
|
||||
consolePushback = -1;
|
||||
return byte;
|
||||
}
|
||||
consoleShowWhatIsWritten();
|
||||
unsigned char byte;
|
||||
for (;;) {
|
||||
ssize_t got = read(STDIN_FILENO, &byte, 1);
|
||||
if (got == 1) {
|
||||
return byte;
|
||||
}
|
||||
if (got == 0) {
|
||||
// End of input. Still 0xFF, which is what getchar's EOF became when this was
|
||||
// the only answer available, so nothing written against the old behaviour
|
||||
// changes. The ENDED bit is the new way to know it was not a real byte.
|
||||
consoleEnded = 1;
|
||||
return 0xFF;
|
||||
}
|
||||
if (errno != EINTR) {
|
||||
consoleEnded = 1;
|
||||
return 0xFF;
|
||||
}
|
||||
// Interrupted before anything arrived, so ask again.
|
||||
}
|
||||
}
|
||||
|
||||
// Asking the host whether anything is waiting, and TAKING IT IF THERE IS. The byte goes
|
||||
// into the pushback and the next read of the data port hands it over, so nothing is lost
|
||||
// and no program can tell that it was fetched early.
|
||||
//
|
||||
// Fetching it early is what makes the answer worth having. The operating system will say a
|
||||
// pipe is readable when what is waiting is the end of it, so asking without reading can
|
||||
// only report that SOMETHING is there. Reading settles which: a byte, or the end. Without
|
||||
// this, ENDED could not go up until a program had already read the 0xFF that stands for
|
||||
// it, and every program would have to swallow one imaginary byte to find out there were
|
||||
// none.
|
||||
static void consoleFetch(void) {
|
||||
if (consolePushback >= 0 || consoleEnded) {
|
||||
return;
|
||||
}
|
||||
// Flushed here too. A program that draws something and then polls rather than reads is
|
||||
// just as entitled to have the drawing appear, and it never reaches the read that
|
||||
// would otherwise have flushed for it.
|
||||
consoleShowWhatIsWritten();
|
||||
struct pollfd waiting = { .fd = STDIN_FILENO, .events = POLLIN, .revents = 0 };
|
||||
if (poll(&waiting, 1, 0) <= 0 || (waiting.revents & (POLLIN | POLLHUP)) == 0) {
|
||||
return;
|
||||
}
|
||||
unsigned char byte;
|
||||
ssize_t got = read(STDIN_FILENO, &byte, 1);
|
||||
if (got == 1) {
|
||||
consolePushback = byte;
|
||||
} else if (got == 0) {
|
||||
consoleEnded = 1;
|
||||
}
|
||||
// A read that failed for any other reason is left alone: the next attempt asks again,
|
||||
// and an interrupted poll is not news.
|
||||
}
|
||||
|
||||
static uint8_t consoleStatus(void) {
|
||||
uint8_t status = consoleKeyMode ? CONSOLE_STATUS_KEYMODE : 0;
|
||||
consoleFetch();
|
||||
if (consoleEnded) {
|
||||
// READY IS NOT SET HERE, although a read would answer immediately. The bit means
|
||||
// "there is a byte to be had", and at the end of input there is not; what a read
|
||||
// returns then is 0xFF standing in for nothing. A program looping while READY
|
||||
// stops on its own at the end, which is the behaviour worth having, and one that
|
||||
// wants to know why asks ENDED.
|
||||
return status | CONSOLE_STATUS_ENDED;
|
||||
}
|
||||
if (consolePushback >= 0) {
|
||||
status |= CONSOLE_STATUS_READY;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
// One bit per port, so a device can ask for attention without anything having to poll
|
||||
// it. Eight ports to the byte, low bit first.
|
||||
@@ -233,6 +393,11 @@ static const DeviceRecord *deviceOnPort(uint8_t port) {
|
||||
if (port >= CONTROLLER_PORT_BASE && port <= CONTROLLER_PORT_TOP) {
|
||||
return &controllerRecord;
|
||||
}
|
||||
if (port > PORT_CONSOLE && port <= PORT_CONSOLE_TOP) {
|
||||
// The status and control ports are the same device as the data port, which is the
|
||||
// one in the table and the one that would raise a line if the console ever did.
|
||||
return deviceOnPort(PORT_CONSOLE);
|
||||
}
|
||||
if (port > PORT_DISK && port <= PORT_DISK_TOP) {
|
||||
// The base port is in the table proper, since that is the one that owns the
|
||||
// memory and raises the line. The rest of the block reports the same device.
|
||||
@@ -269,12 +434,17 @@ uint8_t OutputHandler(uint8_t DataByte, uint8_t Address) {
|
||||
}
|
||||
// This function sends the DataByte to the appropriate place based on the Port Address.
|
||||
switch(Address) {
|
||||
case PORT_CONSOLE:
|
||||
case CONSOLE_DATA:
|
||||
// If data is sent here, it should be written to STDOUT.
|
||||
// For now, I'll implement this so it simply writes each byte out as it comes in.
|
||||
// Later, I'll want to use a buffer for this for performance, probably.
|
||||
putchar(DataByte);
|
||||
break;
|
||||
case CONSOLE_CONTROL: consoleSetMode(DataByte); break;
|
||||
case CONSOLE_STATUS:
|
||||
// Read only. A device saying how it is does not take instructions through the
|
||||
// same hole, so a write here is ignored rather than meaning something.
|
||||
break;
|
||||
case DISK_BLOCK_HIGH: diskBlock = (uint16_t)(DataByte << 8) | (diskBlock & 0x00FF); break;
|
||||
case DISK_BLOCK_LOW: diskBlock = (diskBlock & 0xFF00) | DataByte; break;
|
||||
case DISK_COMMAND: diskCommand(DataByte); break;
|
||||
@@ -319,9 +489,15 @@ uint8_t InputHandler(uint8_t Address) {
|
||||
return controllerRead(Address);
|
||||
}
|
||||
switch(Address) {
|
||||
case PORT_CONSOLE:
|
||||
case CONSOLE_DATA:
|
||||
// If data is sent here, it should be read from STDIN.
|
||||
return getchar();
|
||||
return consoleReadByte();
|
||||
break;
|
||||
case CONSOLE_STATUS: return consoleStatus();
|
||||
case CONSOLE_CONTROL:
|
||||
// Write only. Reading it gives zero rather than the mode, because the mode is
|
||||
// a bit in the status port and one fact wants one place to live.
|
||||
return 0;
|
||||
break;
|
||||
case DISK_BLOCK_HIGH: return (uint8_t)(diskBlock >> 8);
|
||||
case DISK_BLOCK_LOW: return (uint8_t)(diskBlock & 0xFF);
|
||||
|
||||
+57
-1
@@ -14,7 +14,15 @@
|
||||
// Which port a device answers on is a property of the machine rather than of any
|
||||
// program, so the numbers live here and everything else refers to them by name.
|
||||
|
||||
#define PORT_CONSOLE 0x00
|
||||
// The console answers on three ports. The data port is the machine's oldest promise and
|
||||
// does not change: writing sends a byte, reading takes one and waits for it. The other two
|
||||
// are additions, so a program written before they existed cannot notice them.
|
||||
#define PORT_CONSOLE 0x00
|
||||
#define PORT_CONSOLE_TOP 0x02
|
||||
#define CONSOLE_DATA 0x00
|
||||
#define CONSOLE_STATUS 0x01
|
||||
#define CONSOLE_CONTROL 0x02
|
||||
|
||||
#define PORT_TEST 0x10
|
||||
#define PORT_REFUSE 0x11
|
||||
#define PORT_MEMORY 0x12
|
||||
@@ -30,6 +38,54 @@
|
||||
#define DISK_STATUS 0x23
|
||||
#define PORT_REGISTRY 0xFF
|
||||
|
||||
// ---- The console ----
|
||||
//
|
||||
// Two modes, chosen by the program through the control port. The console starts in LINE
|
||||
// mode, which is what the machine has always done: the terminal holds what is typed until
|
||||
// Return, and does the echoing and the backspacing on the way. Reading the data port waits
|
||||
// for a whole line to be finished somewhere else and then hands it over a byte at a time.
|
||||
//
|
||||
// KEY mode turns that off. Keys arrive as they are pressed, and nothing echoes them, so a
|
||||
// program that wants them seen has to send them back out itself. That is not a choice this
|
||||
// machine is making; it is what asking the terminal to stop holding a line means, and the
|
||||
// editing goes away with it. A program that wants keys is expected to want that.
|
||||
//
|
||||
// READING THE DATA PORT WAITS IN BOTH MODES. The status port is how a program declines to
|
||||
// wait, and keeping that in one place means the data port means one thing everywhere. A
|
||||
// read that sometimes blocked and sometimes did not, depending on state set somewhere
|
||||
// else, is the kind of thing that works until it does not.
|
||||
//
|
||||
// KEY MODE ONLY REACHES THE TERMINAL when there is one. With input coming from a pipe
|
||||
// there is nothing to put into another mode, and the status port answers by asking the
|
||||
// operating system whether anything is waiting, which is true of a pipe with bytes in it.
|
||||
|
||||
#define CONSOLE_MODE_LINE 0x00
|
||||
#define CONSOLE_MODE_KEY 0x01
|
||||
|
||||
// Set when there is a byte to be had. NOT set at the end of input, although a read would
|
||||
// answer at once there: what it answers is 0xFF standing in for nothing, and calling that
|
||||
// ready would make a loop that reads while READY spin on imaginary bytes forever. A loop
|
||||
// like that now stops when the input does, which is what anybody writing one intends.
|
||||
#define CONSOLE_STATUS_READY 0x01
|
||||
// Set once input has run out for good. The data port still answers 0xFF, which is what it
|
||||
// always did and what every program written before this expects, but 0xFF is also an
|
||||
// ordinary byte and this bit is the only thing that can tell the difference.
|
||||
#define CONSOLE_STATUS_ENDED 0x02
|
||||
// Which mode the console is in, so that a program can put it back the way it found it
|
||||
// rather than assuming it knows.
|
||||
#define CONSOLE_STATUS_KEYMODE 0x04
|
||||
|
||||
// Puts the terminal back the way it was found. Registered with atexit and called from the
|
||||
// signal handlers, because a machine that stops in key mode and does not undo it leaves
|
||||
// the shell that started it unusable, which is a far worse failure than anything the
|
||||
// program was doing.
|
||||
void consoleRestore(void);
|
||||
|
||||
// One byte from the console, waiting if it has to. Everything that reads standard input
|
||||
// goes through here: the emulator owns one byte of pushback, and stdio holding a buffer
|
||||
// of its own behind that would make the status port lie about what is waiting.
|
||||
uint8_t consoleReadByte(void);
|
||||
|
||||
// ---- Device classes ----
|
||||
//
|
||||
// What kind of thing is plugged into a port. Class 0 is not a device: reading an
|
||||
|
||||
Reference in New Issue
Block a user