Codepages
-> -> -> Bytecode format

Cornerstone Bytecode Notes

Warning: These notes are incomplete, and there's a fair amount about the Cornerstone bytecode I haven't fathomed.

Cornerstone is written in interpreted bytecode; on the PC, the interpreter is called MME.EXE. The bytecode is for a stack-based VM, with a 16-bit word.

A bytecode program is stored in two files: a .MME file and a .OBJ file. The .MME file is passed to the interpreter; the .OBJ filename is deduced from it (it's assumed to have the same name, substituting ".obj" for ".mme"). The actual bytecode is in the .OBJ file.

MME File Format

The .MME file has a 96-byte header, of which the following fields appear to be the only ones used by the interpreter. All are little-endian words:

Offset Meaning
0x0E The address of the last code byte in the .OBJ file, divided by 512 and rounded down.
0x10 The address of the entry point in 'far call' format — that is, the high byte is the 1-based module number, and the low byte is the number of the entry in the module header.
0x16 The size in words of the initial RAM image.
0x1A The offset in the .MME file of the module headers table, divided by 256.
0x1E The length of the module headers table, in words.

Immediately following the header is a table of module offsets. Again, all entries are little-endian words:

Offset Meaning
0x60 The number of modules. PC Cornerstone supports up to 24.
0x62 The offset of each module in the .OBJ file, divided by 256.

For all modules except the last, the end address of a module is the start address of the next module - 1. When the module index is read in, the header word 0x0E is used to work out the end address of the last module; this is appended to the table in memory.

The module index is followed by a 'globals' index:

Offset Meaning
+0 The number of program-wide global variables.
+2 The number of module-wide global variables, module 1.
+4 The number of module-wide global variables, module 2.
+6 etc.

And this, in turn, is followed by the initial values of these variables.

    DW  g0,g1,...   ; Initial values of program-wide globals.
    DW  m0g0,m0g1,...   ; Initial values of module 1 globals.
    DW  m1g0,m1g1,...   ; Initial values of module 2 globals.
    etc.

Program-wide globals are accessed with the LOADG, PUTG, STOREG, INCG and DECG instructions. PC Cornerstone supports at most 256 globals.

Module globals are accessed with LOADMG, PUTMG etc. PC Cornerstone supports at most 192 module globals. If these instructions are used with a number higher than 192, they refer to interpreter system variables such as the current date or time.

After the globals table, the loader then seeks to the module headers using header word 0x1A. The original VM does not allow the module headers to occupy more than 2k.

Each module header starts with a word; this is the number of words that follow. The subsequent words are procedure addresses relative to the start of the module.

Offset Meaning
+0 Number of entry points.
+2 Offset of entry point 1.
+4 Offset of entry point 2.
+6 etc.

The module headers are followed by the initial contents of VM RAM (aligned to a 256-byte boundary). The interpreter finds offset of initial RAM by: 256 * (header word 0x1A + 1 + ((header word 0x1E * 2) / 256)). This suggests to me that if the module headers happened to be an exact multiple of 256 bytes in size, the interpreter would require 256 empty bytes before the RAM contents.

VM memory layout

To a first approximation, the virtual machine has 128k of RAM — if you were writing a Cornerstone interpreter on anything other than an extremely constrained machine, you'd just allocate 128k and have done with it.

In a situation where the RAM is constrained, it is composed of two segments of the same size, referred to here as 'low' and 'high'. The 'low' segment starts at 0x00000, and the high segment at 0x10000. For example, if only 96k was available for VM RAM, it would appear to the virtual machine as two 48k segments: 0x00000-0x0BFFF and 0x10000-0x1BFFF. Thus, RAM is only contiguous in a full-128k configuration.

Within the VM bytecode an address in RAM is represented by a 16-bit value:

Frequently, the 16-bit VM address is treated as a pointer to a "Tuple" or a "Vector/Tuple" structure. In this case, I refer to it as a "tuple ID" or "vector ID".

Low RAM is initialised from the .MME file; the header word at 0x16 gives the number of words in the initial image. Assuming that X is the first address after the initial image, the memory at X is initialised as:

Word at X:   0000h
Word at X+2: X + 6
Word at X+6: X + 6
Word at X+8: End of interpreter RAM low segment - (X+8)

For example, if the word at 0x16 is 0x4A48, X will be 2 * 0x4A48 = 0x9490, and memory will be initialised as follows:

0x9490: 0000
0x9492: 9496
0x9496: 9496
0x9498: 6B68

VM RAM is split between data and stack memory. The initial split appears to be the top 1/64th of high memory is given to the stack (so 1k on a full 128k system, 512 bytes on a 64k system, etc.). To avoid confusion with the interpreter's own stack (where local variables live) I'm going to call these areas of memory 'vector data' and 'tuple stack' respectively.

'Vector data' memory starts just after the RAM image loaded from file, and has an allocator system I haven't worked out yet. Presumably the four words initialised above represent an empty arena. Since the arena is visible to the bytecode, all interpreters should use the same allocation system, whatever that is.

Strings in RAM are stored in Pascal format:

    WORD length
    BYTE text[];

File I/O

Cornerstone allows up to 25 open files (or 'channels', as it calls them). One of these will be the .OBJ file; its handle is held in the bottom 5 bits of system variable 0xDB.

Channels can either be disk files, or devices such as a printer. In the PC Cornerstone interpreter, files are allocated channel handles 0-23 and the printer gets handle 24.

One file used internally by Cornerstone is called sysbuf.hld. This is created by the interpreter on startup and appears to be a swapfile of some kind.

OBJ File Format

A .OBJ file is a sequence of modules, each aligned to 256 bytes. Modules are a sequence of procedures.

Code is divided into 256-byte blocks. An instruction cannot be split across two different blocks. The blocks are swapped in and out of memory by the interpreter as required. The last instruction encountered in each block must be NEXTB, to instruct the interpreter to load the next block. NEXTB might be at offset 255, or it might come sooner. For example, assembling the sequence:

    PUSH_L3
    OPEN    8

If the PUSH_L3 happened to end up at offset 253, then there would be no room for the OPEN (a 3-byte opcode) at offset 254. So NEXTB would be inserted at offset 254, and the OPEN would be at offset 0 in the next block.

Each procedure starts with a single byte:

Bits 0-6
give the number of local variables used by this procedure. These are initialised to 0x8001 (FALSE).
Bit 7
is set if initial values for local variables follow.

If bit 7 of the intro byte is set, the procedure header is followed by a list of encoded initial values for local variables. Each entry is 2 or 3 bytes, the first one of which is:

Bits 0-5
Index number of local variable, 0-31
Bit 6
If this bit is 0, the two following bytes are the word value to assign. If it is 1, the one following byte is the value; sign-extend it to a word.
Bit 7
Set if this is the last initial value.

Opcodes

Explanation of the "args" column:

"1-> "
pops one word from the stack, pushes none.
"2->1"
pops two words, pushes one.
" ->1"
pops no words, pushes one.
etc.

Let $1 refer to the top word on the stack, $2 the one below it, and so forth.
$B is a byte following the instruction byte.
$W is a little-endian word following the instruction byte.

Mnemonics are my own invention.

Opcode Args Mnemonic Effect
0x00 ->1 BREAK I have assumed this to be a debug breakpoint. It replaces the 0x00 in memory with a previously stored byte, pushes the current location counter onto the stack, and set the location counter to the value of the stored byte!
0x01 2->1 ADD Pushes $2 + $1
0x02 2->1 SUB Pushes $2 - $1
0x03 2->1 MUL Pushes $2 × $1
0x04 2->1 DIV Divides $1 by $2 and pushes the quotient.
0x05 2->1 MOD Divides $1 by $2 and pushes the remainder.
0x06 1->1 NEG Changes $1 to its two's complement.
0x07 2->1 ASHIFT Arithmetic shift $2 by $1 bits (to the left if $1 is positive, to the right if $1 is negative). When shifting to the right, the sign is preserved:
shift = -$1;
if ($2 == 0x8000) { $2 = 0x4000; --shift; }
$2 = - ((-$2) >> shift);
       
0x08 ->1 INCL Increment local variable $B, and push its new value onto the stack.
0x09 ->1 PUSH8 Push constant 8.
0x0A ->1 PUSH4 Push constant 4.
0x0B ->1 DECL Decrement local variable $B, and push its new value onto the stack.
0x0C ->1 PUSHm1 Push constant -1 (0xFFFF).
0x0D ->1 PUSH3 Push constant 3.
0x0E 2->1 AND Pushes the bitwise AND of $1 and $2.
0x0F 2->1 OR Pushes the bitwise OR of $1 and $2.
0x10 2->1 SHIFT Shift $2 by $1 bits (to the left if $1 is positive, right if $1 is negative). Unlike ASHIFT, this uses the 8086 SAR instruction.
0x11 1->1 VALLOC Allocates a vector in interpreter RAM; $1 = size in words. Pushes the new vector ID.
0x12 ?->1 VALLOCI As VALLOC, but also populates the newly-allocated vector with $2,$3,$4... up to $1 words. Pushes the new vector ID.
0x13 2-> VSIZE Resize a vector? $1 is new size; $2 is vector ID, which cannot be FALSE.
0x14 1->1 TALLOC Allocate a tuple of $1 words on the tuple stack. Returns the new tuple ID. Interpreter terminates if allocation fails.
0x15 ?->1 TALLOCI As TALLOC, but also populates the newly-allocated tuple with $2,$3,$4... up to $1 words. Pushes the new tuple ID.
0x16 2->1 VLOADW Read a word from a vector / tuple. $2 is the vector ID; this is converted to the actual address, then an offset of ($1 * 2) - 2 bytes is added.
0x17 2->1 VLOADB Read a byte from a vector / tuple. $2 is the vector ID; this is converted to the actual address, then an offset of ($1 + 1) bytes is added.
0x18 1->1 VLOADW_ Read a word from a vector / tuple. $1 is the vector ID; this is converted to the actual address, then ($B * 2) is added.
0x19 1->1 VLOADB_ Read a byte from memory. $1 is the vector ID; this is converted to the actual address, then ($B + 2) is added.
0x1A 3-> VPUTW Store word $1 in memory. $3 is the vector ID; this is converted to the actual address, then ($2 * 2) - 2 is added.
0x1B 3-> VPUTB Store the low byte of $1 in memory. $3 is the vector ID; this is converted to the actual address, then ($2 + 1) is added.
0x1C 2-> VPUTW_ Store word $1 in memory. $2 is the vector ID; this is converted to the actual address, then ($B * 2) is added.
0x1D 1->1 VPUTB_ Store the low byte of $1 in memory. $2 is the vector ID; this is converted to the actual address, then ($B + 2) is added.
0x1E 3-> VECSETW Writes $2 copies of word $1 into memory. $3 gives the VM address.
0x1F 3-> VECSETB Writes $2 copies of byte $1 into memory. $3 gives the VM address, which is first incremented by 2.
0x20 3-> VECCPYW Copy $2 words from vector $3 to vector $1.
0x21 3-> VECCPYB Copy $4 bytes from vector $5 (plus ($2+2)) to vector $3 (plus ($1+2)).
0x22 ->1 LOADG Pushes the value of global variable $B.
0x23 ->1 LOADMG Pushes the value of module global variable $B.
0x24 ->1 PUSH2 Push constant 2.
0x25 ->1 PUSHW Push word $W.
0x26 ->1 PUSHB Push byte $B.
0x27 ->1 PUSH_NIL Push FALSE (0x8001).
0x28 ->1 PUSH0 Push constant 0.
0x29 1->2 DUP Duplicate the word at the top of the stack.
0x2A ->1 PUSHm8 Push constant -8 (0xFFF8).
0x2B ->1 PUSH5 Push constant 5.
0x2C ->1 PUSH1 Push constant 1.
0x2D 1-> PUTMG Set module global variable $B to $1.
0x2E ->1 PUSHFF Push constant 0xFF.
0x2F 1-> PULL Discard the top word from the stack.
0x30 JUMP If $B is nonzero, treat it as a signed byte and add it to the program counter. If it is zero, read the next word from the instruction stream and set the program counter to that.
0x31 1-> JUMPZ As JUMP, but only if $1 is zero.
0x32 1-> JUMPNZ As JUMP, but only if $1 is nonzero.
0x33 1-> JUMPF As JUMP, but only if $1 is FALSE (0x8001).
0x34 1-> JUMPNF As JUMP, but only if $1 is not FALSE.
0x35 1-> JUMPG As JUMP, but only if $1 is > 0.
0x36 1-> JUMPLE As JUMP, but only if $1 is <= 0.
0x37 1-> JUMPL As JUMP, but only if $1 is < 0.
0x38 1-> JUMPGE As JUMP, but only if $1 is >= 0.
0x39 2-> JUMPL2 As JUMP, but only if $1 is < $2.
0x3A 2-> JUMPLE2 As JUMP, but only if $1 is <= $2.
0x3B 2-> JUMPGE2 As JUMP, but only if $1 is >= $2.
0x3C 2-> JUMPG2 As JUMP, but only if $1 is > $2.
0x3D 2-> JUMPEQ As JUMP, but only if $1 == $2.
0x3E 1-> JUMPNE As JUMP, but only if $1 <> $2.
0x3F ->? CALL0 Call address $W (in this module) with 0 arguments.
0x40 1->? CALL1 Call address $W (in this module) with 1 argument.
0x41 2->? CALL2 Call address $W (in this module) with 2 arguments.
0x42 3->? CALL3 Call address $W (in this module) with 3 arguments.
0x43 ?->? CALL Call address $1 (in this module) with $B arguments.
0x44 ->? CALLF0 Far call to address $W with 0 arguments. The high byte of $W is the 1-based module number; the low byte is the number of the entry point. The module headers table is used to convert this to the procedure address.
0x45 1->? CALLF1 Far call to address $W with 1 argument.
0x46 2->? CALLF2 Far call to address $W with 2 arguments.
0x47 3->? CALLF3 Far call to address $W with 3 arguments.
0x48 ?->? CALLF Far call to address $1 with $B arguments.
0x49 1->1 RETURN Returns from a procedure call, pushing $1 onto the caller's stack.
0x4A ->1 RFALSE Returns from a procedure call, pushing FALSE (0x8001).
0x4B ->1 RZERO Returns from a procedure call, pushing 0.
0x4C ->1 PUSH6 Push constant 6.
0x4D HALT Terminate execution. $W gives an error code.
0x4E NEXTB The last instruction in a 256-byte code block. May be followed by one or more packing bytes; execution resumes at the next 256-byte boundary. Used to ensure the next block is paged in.
0x4F ->1 PUSH7 Push constant 7.
0x50 3-> PRINTV Print up to 100 bytes to screen. $2 = count of bytes. $3 is the VM address; this is converted to the actual address, then ($1 + 2) is added to it.
0x51 2->1 LOADVB2 Read byte from memory. $2 gives vector ID. After conversion to actual address, ($1 - 1) is added.
0x52 3-> POPVB2 Write byte $1 to memory. $3 gives vector ID. After conversion to actual address, ($2 - 1) is added.
0x53 2->1 ADD_ Same as 0x01
0x54 INCLV Increment the value in local variable $B, treating it as a vector (ie, the VM aborts if the result would be FALSE (0x8001)).
0x55 RET Return from a procedure call, not pushing anything onto the caller's stack.
0x56 1-> PUTL Store $1 in local variable $B.
0x57 ->1 LOADL Push local variable $B onto the stack.
0x58 1->1 STOREL Copy $1 to local variable $B. $1 remains at the top of the stack.
0x59 ->1 BITSVL Read bits of a word within a vector. $W gives the operation details:
Bits 15-12 are a shift. The word read will be shifted right by
      this amount of bits before being masked.
Bits 11- 8 give the number of bits to read, 0-15.
Bits  7- 4 are the number of a local variable which contains a vector ID.
Bits  3- 0 are the number of the word in the vector.
0x5A 1->1 BITSV Read bits of a word within a vector. $W gives the operation details:
Bits 15-12 are a shift. The word read will be shifted right by this amount 
      of bits before being masked.
Bits 11- 8 give the number of bits to read, 0-15.
Bits  7- 0 are the number of the word in the vector.
$1 gives the vector ID.
0x5B 1-> BBSETVL Replace bits of a word within a vector. $W gives the operation details:
Bits 15-12 are a shift. The word written will be shifted left by this amount 
      of bits before being masked.
Bits 11- 8 give the number of bits to use, 0-15.
Bits  7- 4 are the number of a local variable which contains a vector ID.
Bits  3- 0 are the number of the word in the vector.
$1 gives the word to write.
0x5C: 2-> BBSETV Replace bits of a word within a vector. $W gives the operation details:
Bits 15-12 are a shift. The word written will be shifted left by this amount 
       of bits before being masked.
Bits 11- 8 give the number of bits to use, 0-15.
Bits  7- 0 are the number of the word in the vector.
$1 gives the vector ID. $2 gives the word to write.
0x5D: -> BSETVL Set/clear a single bit within a vector. $W gives the operation details:
Bits 15-12 are the bit number, 0-15.
Bit      8 is the value to write. 
Bits  7- 4 are the number of a local variable which contains a vector ID.
Bits  3- 0 are the number of the word in the vector.
0x5E: 1-> BSETV Set/clear a single bit within a vector. $W gives the operation details:
Bits 15-12 = bit number
Bit      8 is the value to write. 
Bits  7- 0 are the number of the word in the vector.
$1 gives the vector ID.
0x5F This is the first byte of a 2-byte opcode.
0x5F 0x00 n/a Not defined.
0x5F 0x01 2->1 XOR $1 becomes $1 XOR $2
0x5F 0x02 1->1 NOT $1 becomes the bitwise complement of $1
0x5F 0x03 2->1 ROTATE As SHIFT, but rotates bits.
0x5F 0x04 3->1 VFIND Search a vector for a word value.
$1 = number of words to search.
$2 = vector ID.
$3 = word to find.
Pushes index of the word found, FALSE if not found.
0x5F 0x05 2->1 STRCHR Search string for a character. $1 = VM address of string to search, $2 = character. Pushes index of character found, FALSE if not found.
0x5F 0x06 1-> PUTG Store $1 in global variable $B.
0x5F 0x07 ?-> PULLN Remove $1 words from the stack (plus $1 itself)
0x5F 0x08 n/a Not defined.
0x5F 0x09 2-> LONGJMPR Jump to a stack frame saved by SETJMP, pushing a single word as return code. $1 = frame pointer to return to, $2 = return code.
0x5F 0x0A 1-> LONGJMP Jump to a stack frame saved by SETJMP. $1 = frame pointer to return to.
0x5F 0x0B ->7 SETJMP Push seven words onto the stack:
  • tuple stack pointer
  • $W (ie, word at instruction address+1)
  • $W (ie, word at instruction address+3)
  • current module
  • current frame pointer
  • callSP
  • current stack pointer
The top word is the stack pointer, for passing to LONGJMPR / LONGJMP.
0x5F 0x0C 2->1 OPEN Open a channel. $2 holds the VM address of the filename (a Pascal-format string). $B holds ?access mode?
Returns channel ID if successful, FALSE if failed.
0x5F 0x0D 1->1 CLOSE Close a channel. $1 gives the channel ID.
Returns 0 if OK, -1 if failed.
0x5F 0x0E 3->1 READ Read $1 words from channel $2 into vector $3, starting at word 1 of the vector.
0x5F 0x0F 3->1 WRITE Write $1 words to channel $2 from vector $3, starting at word 1 of the vector.
0x5F 0x10 4->1 READREC Read $1 words from channel $3 into vector $4, starting at record $2, and putting the data two bytes after the beginning of the vector. System variable 0xCC defines record size.
0x5F 0x11 4->1 WRITEREC Write $1 words to channel $3, starting at record $2. Data come from 2 bytes after the beginning of vector $4. System variable 0xCC defines record size.
0x5F 0x12 -> DISP Display operation. $B specifies operation:
0: Cursor right
1: Cursor left
2: Cursor down
3: Cursor up 
4: If cursor column < 80, print a space
5: Clear from cursor row to bottom
0x5F 0x13 1->1 XDISP Extended display operation. $B specifies operation:
0: Scroll area from cursor row to row specified in system variable 0xC4 
  down by $1 lines. Clears area if $1 = 0.
1: Scroll area from cursor row to system variable 0xC4 up by $1 lines. 
  Clears area if $1 = 0.
2: No effect on PC Cornerstone.
3: No effect on PC Cornerstone.
4: Scroll area between rows specified in system variables 0xC4 and 0xC5 down 
   by $1 lines. Clears area if $1 = 0.
5: Scroll area between rows specified in system variables 0xC4 and 0xC5 up
   by $1 lines. Clears area if $1 = 0.
6: Draw a horizontal line to column $1.
0x5F 0x14 1->1 FSIZE Get size of file attached to channel $1, divided by 256.
0x5F 0x15 1->1 UNLINK Delete the file whose name is $1
0x5F 0x16 ?-> PULLRET If the last return was not RET (ie, a value was returned), remove one word from the stack.
0x5F 0x17 ->1 KBINPUT Poll the keyboard. Returns FALSE if no key hit, otherwise the key. Cursor keys are translated into escape sequences — 0x1B followed by the scancode. ESC itself becomes 0x1B 0x00.
0x5F 0x18
0x5F 0x19
0x5F 0x1A
0x5F 0x1B
0x5F 0x1C 1-> TPULL Increase the tuple stack pointer by $1 words.
0x5F 0x1D
0x5F 0x1E
0x5F 0x1F 5>1 STRICMP Compare two strings, ignoring spaces and upper/lower case.
$1 = length of first string
$2 = offset within first vector
$3 = length of second string
$4 = vector address of first string
$5 = vector address of second string
0x5F 0x20 5>1 STRICMP1 As STRICMP, but vector ID of first string is decremented by one before comparison.
0x5F 0x21 4->1
0x5F 0x22 1->1 Unknown, but appears to take a vector incorporating attributes and screen coordinates.
0x5F 0x23 2->1 STRCMP Compares two strings; $1 and $2 are the VM addresses of the two strings.
0x5F 0x24 2->1 MEMCMP Compares $1 bytes of memory at VM addresses $2 and $3. Pushes -1, 0 or 1.
0x5F 0x25 2->1 MEMCMPO Compares $2 bytes of memory at VM addresses $3 (plus offset $1) and $4. Pushes -1, 0 or 1.
0x5F 0x26 2->1 VECL $2 is a vector ID. $1 is an offset. Converts $2 to an address. Let X be the byte at address + ($1 - 1). Pushes $1 plus the word at offset X from the start of the vector plus 3.
0x5F 0x27 ->1 DECMG Decrement module global variable $B, and push the resulting value onto the stack.
0x5F 0x28 DECG Decrement global variable $B, and push the resulting value onto the stack.
0x5F 0x29 ?-> PULL_ Remove $B words from the stack.
0x5F 0x2A INCG Increment global variable $B, and push the resulting value onto the stack.
0x5F 0x2B ->1 INCMG Increment module global variable $B, and push the resulting value onto the stack.
0x5F 0x2C 1->1 STOREMG As PUTMG, but $1 remains at the top of the stack.
0x5F 0x2D 1->1 STOREG As PUTG, but $1 remains at the top of the stack.
0x5F 0x2E ->1 Return constant value $B.
0x5F 0x2F 1-> PRCHAR Prints $1 as a character. No character set translaton is done; the text is assumed to be in the default codepage (probably 437).
0x5F 0x30
0x5F 0x31
0x5F 0x32
0x5F 0x33
0x5F 0x34
0x5F 0x35
0x5F 0x36
0x5F 0x37 8->1
0x5F 0x38 2->1 RENAME Rename a file. $1 is the old filename, $2 the new.
0x60-0x9F ->1 VREAD__ Read word (n) of a vector.
Bits 0-3 of the opcode give the number of a local variable which holds the 
         vector ID.
Bits 4-5 of the opcode give the number of the word to read from the vector.
0xA0-0xBF ->1 PUSHL_ As PUSHL, but the variable number is given by by the low 5 bits of the opcode.
0xC0-0xDF 1-> PUTL_ As PUTL, but the variable number is given by the low 5 bits of the opcode.
0xE0-0xFF 1->1 STOREL_ As STOREL, but the variable number is given by the low 5 bits of the opcode.

System variables

When module globals are accessed, a variable number of 0xC0 or higher indicates an interpreter system variable:

0xC0:   Controls behaviour of keyboard input -- how, I'm not sure.
0xC1:   Set to FALSE to disable screen output
0xC2:   Not used by PC Cornerstone
0xC3:   Not used by PC Cornerstone
0xC4:   Used to define window(s) on the screen.
0xC5:   Used to define window(s) on the screen.
0xC6:   Extended error code? Set to 7 if READREC fails.
0xC7:   Screen width, columns - 1
0xC8:   Screen height, rows - 1
0xC9:   Cursor column
0xCA:   Cursor row
0xCC:   Record size for READREC / WRITEREC
0xCD:   Current month
0xCE:   Current day 
0xCF:   Current year
0xD0:   Current time, hours
0xD1:   Current time, minutes 
0xD2:   Current time, seconds
0xD3:   Used by opcode 0x5F 0x22.
0xD4:   Control-Break flag (0 if Control-Break has been pressed.)
0xD5:   Screen attributes:
        Bit 0: Reverse video
        Bit 3: Bright
        Bit 5: Blinking 
0xD7:   A 3-letter string, encoded into 16 bits as:
        enc(str[0]) << 11 | enc(str[1]) * 45 | enc(str[2])
        enc(char) is:  0-25 for A-Z
                      26-35 for 0-9
                      36-44 for $ & # @ ! % - _ /  respectively

        This is normally 'DBF' but the /e parameter to MME can be used to
        change it.

0xDA:   Bits 0-4:  Channel handle of OBJ file
        Bits 5-15: Total size of modules (header word 0x0E << 5)
0xDB:   0 if beeper enabled, else disabled.
0xE7:   System statistics: #outc
0xE8:   System statistics: #outs
0xE9:   System statistics: #curpos
0xEA:   System statistics: #disp
0xEB:   System statistics: #xdisp
0xEC:   System statistics: #gets
0xED:   System statistics: #sets
0xEE:   System statistics: #hsets
0xEF:   System statistics: #vsets

John Elliott 2014-04-15