Permalink
Switch branches/tags
Nothing to show
Find file Copy path
05dc9ec Oct 17, 2018
1 contributor

Users who have contributed to this file

363 lines (309 sloc) 14.9 KB
-------------------------------
Gigatron Control Language (GCL)
-------------------------------
GCL is an interpreted low-level language for writing simple games for
the Gigatron TTL microcomputer, without bothering with the harsh timing
requirements of the hardware platform.
Technically, "GCL" is the source language or notation, while "vCPU" is the
virtual CPU, or interpreter, that is executing compiled GCL instructions.
The two are closely tied together.
---------------
Example program
---------------
gcl1 {GCL version}
{Approximate BASIC equivalent}
{Function to draw binary value as pixels}
[def {10 GOTO 80}
$4448 D= {Middle of screen} {20 D=$4448: REM MIDDLE OF SCREEN}
[do
C [if<0 15 else 5] D. {30 IF C<0 POKE D,15 ELSE POKE D,5}
C C+ C= {40 C=C+C}
D 1+ D= {50 D=D+1}
-$4458 D+ if<0 loop] {60 IF D<$4458 THEN 30}
ret {70 RETURN}
] Plot=
{Compute largest 16-bit Fibonacci number and plot it on screen}
[do
0 A= {80 A=0}
1 B= {90 B=1}
[do
A B+ C= {100 C=A+B}
B A= C B= {110 A=B: B=C}
if>0 loop] {120 IF B>0 THEN 100}
Plot! {130 GOSUB 20}
loop] {140 GOTO 80}
----------
Deployment
----------
Translation from GCL to vCPU is done offline. So the Gigatron runs
vCPU applications. There are other ways to generate vCPU programs,
for example through at67's assembler. There are two ways to get a
vCPU program into the Gigatron. One is by sending it into RAM through
the input port using the Loader application. The other is to store
the object code in the ROM disk portion of the EPROM. The file
format for exchange of vCPU programs is GT1. This format is described
in a different document: GT1-FileFormat.txt
-----------------
Programming model
-----------------
GCL draws most of its inspiration from two notable mini-languages:
SWEET16 (Apple II) and FALSE (Amiga). There is also a little bit
of a FORTH influence. The GCL programming model is "accumulator
oriented": there is no implicit expression stack. vCPU does have a
call stack however. Programs are typically made of many small
function definitions followed by a main loop. The functions usually
operate on global variables, although it is also possible to put
variables on the stack.
The virtual regisers are 16-bits (except vSP) and reside in the
zero page.
vAC is the virtual accumulator, used by instructions as operand
and/or result
vPC is the virtual program counter. Programs run from RAM. When
executing, vPC auto-increments just its low byte, so it wraps
around on the same RAM page. (Code normally doesn't "use" this.)
Function calls can go across pages.
vLR is the link register. It points to the instruction after the
most recent CALL instruction. This is used to return after
making a function call. When nesting functions, vLR should
be pushed on the stack.
vSP is the stack pointer. The stack lives in the zero page top and
grows down.
Program variables are 16-bit words and typically hold a number or
a pointer. Named variables reside in the zero page. Arbitrary
memory can be addressed as bytes using 16-bit pointers.
GCL translation is single-pass and doesn't have a linking stage.
Therefore a typical GCL program has a setup phase followed by a
main loop. In the setup phase, variables are initialized with
values and references to functions or data. When running, function
calling is done through those variables. vCPU is optimized for
function calling in that way. Long functions that don't fit in a
page should be split into multiple functions. When a program gets
bigger than a single segment or page, the setup phase will have to
hop over to the next page. The clearest way to do that is as follows:
{Start compilation at $200}
[def ... ret] Function1=
[def ... ret] Function2=
$300 call
{Continue compilation at $300}
$300:
[def ... ret] Function3=
[def ... ret] Function4=
$400 call
{Continue compilation at $400}
$400:
[def ... ret] Function5=
[do ... loop] {Main loop}
This can be done in a slightly more space-efficient way by using
"ret" and vLR. This saves 1 byte per page, but is cryptic. Also you
can't do any function calls in the setup phase as that uses vLR.
{Start compilation at $200. (vLR=$200)}
[def ... ret] Function1=
[def ... ret] Function2=
\vLR>++ ret {Increment high byte of vLR and go there}
{Continue compilation at $300}
$300:
[def ... ret] Function3=
[def ... ret] Function4=
\vLR>++ ret
{Continue compilation at $400}
$400:
[def ... ret] Function5=
[do ... loop] {Main loop}
-----------------
Notes on notation
-----------------
GCL programs are written as a long sequence of "words" without much
structure enforced by the compiler. Most words map directly to a
single virtual instruction and therefore also encode an embedded
operand (e.g. 1+). Many words operate on both vAC and some variable
or constant. Sequences can be grouped with () {} or [], each of
which has its own meaning. Spaces and newlines simply separate
words. Use indentation for clarity. There is no need for spaces
around the grouping characters []{}().
Constants are decimal, or hexadecimal when preceded with '$'.
Constants can be preceded by '-' or '+' (note: -$1000, not $-0000).
For convenience, symbols defined by the host system can be referenced
by prefixing a backslash, e.g. '\fontData'. This can be used anywhere
where an integer is expected.
Variable names start with an alphanumeric character and are case
sensitive. The overview below uses the following conventions:
'i' indicates an 8-bit integer constant.
'ii' indicates a 16-bit integer constant.
'X' indicates a named variable, allocated globally on the zero page.
--------------------
Meaning of GCL words
--------------------
Basics
------
{...} Comments ignored by machine. Comments can be nested
i ii Load integer constant into vAC, e.g. 1, +1972, -$be05
X= Store vAC into variable X
X Load variable X into vAC
X+ X- Add/subtract variable X to/from vAC
i+ i- Add/subtract small constant to/from vAC
i<< Shift vAC i bits left
i& i| i^ Logical AND/OR/XOR vAC with small constant
X& X| X^ Logical AND/OR/XOR vAC with variable X
X<. X>. Store vAC as byte into the low/high byte of variable X
X<, X>, Read low/high byte of variable X
X<++ X>++ Increment low/high byte of variable X
i-- i++ Subtract/add constant value from/to stack pointer
i% Load stack variable at relative byte offset i into vAC
i%= Store vAC on stack at relative byte offset i
Memory
------
X, Read unsigned byte from memory pointed by X into vAC
X. Write vAC as byte to memory pointed by variable X
X; Read word from memory pointed by X into vAC
X: Write vAC as word to memory pointed by variable X
i, Read unsigned byte at zero page address i into vAC
i. Store vAC as byte into zero page address i
i; Read word at zero page address i into vAC
i: Write vAC as word into zero page address i
i? Table lookup from ROM address vAC+i
peek Read byte from memory pointed by vAC (Same as "\vAC,")
deek Read word from memory pointed by vAC (Same as "\vAC;")
Structured programming
----------------------
[...] Code block, used with "if", "else", "do", "loop"
if>0 Continue executing code if vAC>0, otherwise jump to
end of the block (or past an optional matching "else")
Conditions are "=0" ">0" "<0" "<=0" ">=0" "<>0"
else When encountered, skip rest of code until end of block
do Mark the start of a loop
loop Jump back to matching "do" (May be in an outer block)
if>0loop Optimization for "if>0 loop" (works with all conditions)
def Load next vPC in vAC and jump to end of current block
Subroutines
-----------
X! Jump to function pointed by X, store old vPC in vLR
ret Jump to vLR, to return from a leaf function. Non-leaf
functions should use "pop ret" as return sequence
push Push vLR onto stack, for entering a non-leaf function
pop Remove top of stack and put value in vLR
i! Call native code pointed by sysFn, not exceeding i cycles
call Jump to function pointed by vAC (Same as "\vAC!")
Data
----
i# Raw byte-value i, value will be truncated to lowest 8 bits
Versioning
----------
gcl0x gcl1 GCL version. "x" denotes experimental/extended versions
There are two versions of GCL: gcl0x and gcl1. gcl0x is what we
used to make the built-in applications of ROM v1. gcl1 is the planned
cosmetic upgrade in notation.
----------------------------------
Common pitfalls in GCL programming
----------------------------------
- Forgetting "ret" at the end of a function block
- Calling functions from a leaf function (forgetting "push" and "pop")
- Forgetting that all named variables are global
- Calling a function before the last page when also using vLR hopping in setup
- Misspelling a variable (Tip: inspect the symbol table output)
- Renaming a function, but not where it is called
-----------------
Future extensions
-----------------
GCL "v1" will have differences we're pondering about:
'A Character code?
\12 For raw byte values (better readable than 12#)
<p++ >p++ Notation for p<++ p>++ (Consider < and > as part of the name)
"Text" Text string?
() Macros?
Consistency of , . : ; in combination with \
----------------------
vCPU instruction table
----------------------
The vCPU interpreter has 34 core instructions. Each opcode is just
a jump offset into the interpreter code page to the code that
implements its behavior. Most instructions take a single byte
operand, but some have two and others none.
Mnem. Operands Description
----- -------- -----------
ST $DD Store byte in zero page ([D]=vAC)
STW $DD Store word into zero page ([D]=vAC&255,[D+1]=vAC>>8)
STLW $DD Store word in stack frame (vSP[D],vSP[D+1]=vAC&255,vAC>>8)
LD $DD Load byte from zero page (vAC=[D])
LDI $DD Load immediate small positive constant (vAC=D)
LDWI $DDDD Load immediate arbitrary constant (vAC=D)
LDW $DD Word load from zero page (vAC=[D]+256*[D+1])
LDLW $DD Load word from stack frame (vAC=vSP[D]+256*vSP[D+1])
ADDW $DD Word addition with zero page (vAC+=[D]+256*[D+1])
SUBW $DD Word subtraction with zero page (vAC-=[D]+256*[D+1])
ADDI $DD Add small positive constant (vAC+=D)
SUBI $DD Subtract small positive constant (vAC-=D)
LSLW - Shift left (because 'ADDW vAC' will not work!) (vAC+=vAC)
INC $DD Increment zero page byte ([D]++)
ANDI $DD Logical-AND with constant (vAC&=D)
ANDW $DD Word logical-AND with zero page (vAC&=[D]+256*[D+1])
ORI $DD Logical-OR with constant (vAC|=D)
ORW $DD Word logical-OR with zero page (vAC|=[D]+256*[D+1])
XORI $DD Logical-XOR with constant (vAC^=D)
XORW $DD Word logical-XOR with zero page (vAC^=[D]+256*[D+1])
PEEK - Read byte from memory (vAC=[vAC])
DEEK - Read word from memory (vAC=[vAC]+256*[vAC+1])
POKE $DD Write byte in memory ([[D+1],[D]]=vAC&255)
DOKE $DD Write word in memory ([[D+1],[D]],[[D+1],[D]+1]=vAC&255,vAC>>8)
LUP $DD ROM lookup (vAC=ROM[D,AC])
BRA $DD Branch unconditionally (vPC=(vPC&0xff00)+D)
BCC $CC $DD Test vAC and branch conditionally. CC can be EQ,NE,LT,GT,LE,GE
CALL $DD Goto address but remember vPC (vLR,vPC=vPC+2,[D]+256*[D+1]-2)
RET - Leaf return (vPC=vLR-2)
PUSH - Push vLR on stack ([--vSP]=vLR&255,[--vSP]=vLR>>8)
POP - Pop value from stack (vAC=[vSP]+256*[vSP+1],vSP+=2)
ALLOC $DD Create or destroy stack frame (vSP+=D)
SYS $DD Native function call using at most 2*T cycles, D=270-max(14,T)
DEF $DD Define data or code (vAC,vPC=vPC+2,D+256*(vPC>>8))
-------------
Coding tricks
-------------
$ff& Clear vAC high byte
$ff| $ff^ Clear vAC low byte (there is no 'ANDWI' instruction)
\vACH, Move vAC high byte to low (vAC>>=8)
\vACH. $ff| $ff^ Move vAC low byte to high (vAC<<=8)
\vAC>++ Increment vAC high byte
128- 128- Decrement vAC high byte
a b- [if>=0 ...] if a >= b ... But this breaks in case of overflow!
(For example consider a = 30000 and b = -5000)
a b^ [if<0 b else a b-] Checking if the operands are of opposite sign first
[if>=0 ...] makes the comparison safe from overflow.
-------------
SYS extension
-------------
Addresses of SYS functions that are part of the ABI:
00ad SYS_Exec_88 Load serialized vCPU code from ROM and execute
04a7 SYS_Random_34 Get random number and update entropy
0600 SYS_LSRW1_48 Shift right 1 bit
0619 SYS_LSRW2_52 Shift right 2 bits
0636 SYS_LSRW3_52 Shift right 3 bits
0652 SYS_LSRW4_50 Shift right 4 bits
066d SYS_LSRW5_50 Shift right 5 bits
0687 SYS_LSRW6_48 Shift right 6 bits
04b9 SYS_LSRW7_30 Shift right 7 bits
04c6 SYS_LSRW8_24 Shift right 8 bits
06a0 SYS_LSLW4_46 Shift left 4 bits
04cd SYS_LSLW8_24 Shift left 8 bits
04e1 SYS_VDrawBits_134 Draw 8 vertical pixels
06c0 SYS_Unpack_56 Unpack 3 bytes into 4 pixels
04d4 SYS_Draw4_30 Copy 4 pixels to screen memory
00f4 SYS_Out_22 Write byte to hardware OUT register
00f9 SYS_In_24 Read byte from hardwar IN register
02e9 SYS_NextByteIn_32 Receive next byte and put in load buffer
06e7 SYS_PayloadCopy_34 Copy byte from load buffer to RAM destination
Added in ROM v2:
0b00 SYS_SetMode_v2_80 Set video mode 0..3
0b03 SYS_SetMemory_v2_54 Set 1..256 bytes of memory to value
Added in ROM v3:
0b06 SYS_SendSerial1_v3_80 Send data out over game controller port
0c00 SYS_Sprite6_v3_64 Draw sprite of 6 pixels wide and N pixels high
0c40 SYS_Sprite6x_v3_64 Draw sprite mirrored in X direction
0c80 SYS_Sprite6y_v3_64 Draw sprite upside down
0cc0 SYS_Sprite6xy_v3_64 Draw sprite mirrored and upside down
Application specific SYS calls in ROM v1 that are -not- part of the ABI:
SYS_Read3_40 (Pictures)
SYS_LoaderProcessInput_48 (Loader)
SYS_RacerUpdateVideoX_40 (Racer)
SYS_RacerUpdateVideoY_40 (Racer)
Retro-actively retired from ABI:
SYS_Reset_36 Soft reset (use vReset instead)
-- End of document --