======================================== Gigatron Control Language (GCL) and vCPU ======================================== GCL is a compiled low-level language for writing simple games and applications for the Gigatron TTL microcomputer, without bothering the programmer with the harsh timing requirements of the hardware platform. vCPU is the interpreted 16-bit virtual processor running in the dead time of the video/sound loop. The two are closely tied together, and we sometimes we mix them up. Technically still, "GCL" is the source language or notation, while "vCPU" is the virtual CPU, or interpreter, that is executing compiled GCL instructions. vCPU's advantages over native 8-bit Gigatron code are: 1. you don't need to think about video timing with everything you do 2. operations are 16-bits, and 3. programs can run from RAM. Its disadvantage is that vCPU is slower than native code. The main menu, `Snake', `Mandelbrot', `Tiny BASIC v2' and `WozMon' are pure GCL programs. These programs still use generic support ROM functions implemented as native code: the so-called SYS functions. `Racer', `Pictures' and `Loader' are hybrid programs, meaning that they have application-specific SYS functions and/or data in ROM. `Bricks' and `Tetronis' are vCPU programs written in a vCPU assembler, not using GCL at all. --------------- 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} Compiled to vCPU instructions this program looks like this: RAM address | encoding | | vCPU instruction | | | operand | | | | GCL notation | | | | | V V V V V 0200 cd 29 DEF $022b [def 0202 11 48 44 LDWI $4448 $4448 0205 2b 30 STW $30 D= [do 0207 21 32 LDW $32 C 0209 35 53 0e BGE $0210 [if<0 020c 59 0f LDI $0f 15 020e 90 10 BRA $0212 else 0210 59 05 LDI 5 5] 0212 f0 30 POKE $30 D. 0214 21 32 LDW $32 C 0216 99 32 ADDW $32 C+ 0218 2b 32 STW $32 C= 021a 21 30 LDW $30 D 021c e3 01 ADDI 1 1+ 021e 2b 30 STW $30 D= 0220 11 a8 bb LDWI $bba8 -$4458 0223 99 30 ADDW $30 D+ 0225 35 53 28 BGE $022a if<0 0228 90 05 BRA $0207 loop] 022a ff RET ret] 022b 2b 34 STW $34 Plot= [do 022d 59 00 LDI 0 0 022f 2b 36 STW $36 A= 0231 59 01 LDI 1 1 0233 2b 38 STW $38 B= [do 0235 21 36 LDW $36 A 0237 99 38 ADDW $38 B+ 0239 2b 32 STW $32 C= 023b 21 38 LDW $38 B 023d 2b 36 STW $36 A= 023f 21 32 LDW $32 C 0241 2b 38 STW $38 B= 0243 35 56 46 BLE $0248 if>0 0246 90 33 BRA $0235 loop] 0248 cf 34 CALL $34 Plot! 024a 90 2b BRA $022d loop] As you can see, there is a near perfect one-to-one relation between GCL words and vCPU instructions. BTW: To make a visual distinction between 16-bits code and native code, we always captalize vCPU instructions while we write 8-bits Gigatron code in lower case. ---------- 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-files.txt ----------------- Programming model ----------------- The 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 registers 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. vPC gets incremented by 2 BEFORE fetching the next instruction, except after CALL and RET. 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 saved on the stack with PUSH and restored with POP. 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. Variables normally reside in the zero page. Arbitrary memory can be addressed as bytes and words using 16-bit pointers. --------------------- Notes on GCL notation --------------------- GCL draws most of its inspiration from two notable mini-languages: SWEET16 (Apple II) and FALSE (Amiga). There is a little bit of a FORTH influence as well. There are also similarities with UNIX dc(1) and Altair VTL-2, although those are coincidental. GCL programs are written as a long sequence of words without much structure enforced by the compiler. Most words map directly to a single vCPU instruction and therefore also encode an embedded operand (e.g. `1' means `LDI 1' and `2+' means `ADDI 2'). 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 predefined by the system can be referenced by prefixing a backslash, e.g. '\fontData'. This can be used anywhere where a constant is expected. It also may be preceded by a sign. 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. '_C' indicates a named constant, or label, from the system's symbol table -------------------- 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 Advanced -------- X, Load low/high byte of variable X X. Store vAC as byte into the low/high byte of variable X X++ Increment low/high byte of variable X i++ Increment byte at address i/i+1 System ------ *=ii Set compilation address (default is $200) _C=* Label definition. Refer to its value as \_C _C Treat system symbol `C' as a GCL variable (e.g. _sysFn=) Memory ------ X, X. Read/write unsigned byte at memory address pointed at by X X; X: Read/write word at memory address pointed at by X peek deek Read byte/word from memory address pointed at by vAC i, i. Read/write unsigned byte at zero page address i i; i: Read/write word at zero page address i i?? Table lookup from ROM address vAC+i (ROM page must be prepared) Structured programming ---------------------- [...] Code block, used with `if', `else', `do', `loop' def Define function or inline data: load next vPC in vAC, then jump to end of the current block 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' (which may be in an outer block) if>0loop Optimization for `if>0' + `loop' (works with all conditions) Subroutines ----------- X! Jump to function pointed at 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 at by sysFn, not exceeding i cycles call Jump to function pointed at by vAC Stack variables --------------- i-- i++ Subtract/add constant value from/to stack pointer %i %i= Load/store stack variable at relative byte offset i Data ---- #i #ii Inline high byte value (most often used with labels) `Hello`world! Inline ASCII test. Replace backquotes with spaces ` A single inline backquote (ASCII 96) ##ii ##_C Inline word value, little-endian #@_C PC relative offset (6502-style) Directives ---------- zpReset=i Start allocating GCL variables from address i (default=$30) execution=ii Start execution at address ii (default is first segment >= $200) Versioning ---------- gcl0x gcl1 GCL version. `x' denotes experimental/extended versions We foresee three versions of GCL: gcl0x, gcl1 and gcl2. gcl0x is what we used to make the built-in applications of ROM v1. It is still evolving, sometimes in backward incompatible ways. gcl1 will be the final update in notation once we've settled on what GCL should really look like. gcl0x has some inconsistencies in notation that are confusing. Some aren't easy to resolve while maintaining its spirit. We won't take this step easily. gcl2 will add a macro system. The parenthesis are reserved for that. ----------------- Program structure ----------------- 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} ---------------------------------- 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 ----------------- The current GCL version is 'gcl0x'. The 'x' suffix intends to warn that incompatible changes happen without notice or version number change. Once we believe the notation is stable and some weird quirks removed, we plan to rename it 'gcl1'. Having stated that, GCL was primarily the bootstrapping notation for writing the first vCPU applications in ROMv1. It has brought us all the way to Tiny BASIC. In the meantime a more traditional assembler has emerged, and we have a C compiler. So we might not develop GCL much further, as it has served its purpose. Some gcl1 changes we're still pondering about: Consistency Especially surrounding ,.:; and \ (backslash) Symptoms: - i: and i= do the same thing (store vAC on zero-page) - X: is a DOKE to the address in var X, but there is no notation to POKE/DOKE using a zero page address i - ii: sets the compilation address, it isn't a DOKE - Therefore we can't create code in the zero page _C=123 Constant naming &X &X Get an variable's address. Should treat < and > as part of the name instead of the prefix part of the operator *i Treat as the name of a GCL variable with address i i-- i++ Replace with something that associates with stack: %++i %--i ? () Compile time expressions? Macros? (128+1) -> 129. Can could escape to Python for evaluation. E.g. Color=(Blue+1) See GitHub issue #80 for a more detailed discussion. https://github.com/kervinck/gigatron-rom/issues/80 ------------------- Deprecated features ------------------- i= Alias for i: (was often used as \symbol=) i# Original notation for #i X<++ X>++ Original notation for X++ i<++ i>++ Original notation for i++ ii: Original notation for *=ii i% i%= Original notation for %i %i= i! Original notation for i!! (will only be depricated in gcl1) i? Original notation for i?? (will only be depricated in gcl1) ---------------------- 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. Encoding #C Description ----- --------- -- ----------- ST $5E DD 16 Store byte in zero page ([D]=vAC&256) STW $2B DD 20 Store word in zero page ([D],[D+1]=vAC&255,vAC>>8) STLW $EC DD 26 Store word in stack frame ([vSP+D],[vSP+D+1]=vAC&255,vAC>>8) LD $1A DD 18 Load byte from zero page (vAC=[D]) LDI $59 DD 16 Load immediate small positive constant (vAC=D) LDWI $11 LL HH 20 Load immediate word constant (vAC=$HHLL) LDW $21 DD 20 Word load from zero page (vAC=[D]+256*[D+1]) LDLW $EE DD 26 Load word from stack frame (vAC=[vSP+D]+256*[vSP+D+1]) ADDW $99 DD 28 Word addition with zero page (vAC+=[D]+256*[D+1]) SUBW $B8 DD 28 Word subtraction with zero page (vAC-=[D]+256*[D+1]) ADDI $E3 DD 28 Add small positive constant (vAC+=D) SUBI $E6 DD 28 Subtract small positive constant (vAC-=D) LSLW $E9 28 Shift left ('ADDW vAC' will not work!) (vAC<<=1) INC $93 DD 16 Increment zero page byte ([D]++) ANDI $82 DD 16 Logical-AND with small constant (vAC&=D) ANDW $F8 DD 28 Word logical-AND with zero page (vAC&=[D]+256*[D+1]) ORI $88 DD 14 Logical-OR with small constant (vAC|=D) ORW $FA DD 28 Word logical-OR with zero page (vAC|=[D]+256*[D+1]) XORI $8C DD 14 Logical-XOR with small constant (vAC^=D) XORW $FC DD 26 Word logical-XOR with zero page (vAC^=[D]+256*[D+1]) PEEK $AD 26 Read byte from memory (vAC=[vAC]) DEEK $F6 28 Read word from memory (vAC=[vAC]+256*[vAC+1]) POKE $F0 DD 28 Write byte in memory ([[D+1],[D]]=vAC&255) DOKE $F3 DD 28 Write word in memory ([[D+1],[D]],[[D+1],[D]+1]=vAC&255,vAC>>8) LUP $7F DD 26 ROM lookup, needs trampoline in target page (vAC=ROM[vAC+D]) BRA $90 DD 14 Branch unconditionally (vPC=(vPC&0xff00)+D) BCC $35 CC DD 28 Test vAC and branch conditionally. CC can be EQ=$3F, NE=$72, LT=$50, GT=$4D, LE=$56, GE=$53 CALL $CF DD 26 Goto address but remember vPC (vLR,vPC=vPC+2,[D]+256*[D+1]-2) RET $FF 16 Leaf return (vPC=vLR-2) PUSH $75 26 Push vLR on stack ([vSP-2],v[vSP-1],vSP=vLR&255,vLR>>8,vLR-2) POP $63 26 Pop address from stack (vLR,vSP=[vSP]+256*[vSP+1],vSP+2) ALLOC $DF DD 14 Create or destroy stack frame (vSP+=D) SYS $B4 DD 20+ Native function call using at most 2*T cycles, D=270-max(14,T) HALT $B4 $80 inf Halt vCPU execution DEF $CD DD 26 Define data or code (vAC,vPC=vPC+2,(vPC&0xff00)+D) #C is the number of clocks or cycles, counted back-to-back. So this number includes advancing vPC, fetch, dispatch and execute. The formulas are pseudo-formal. Better take them with a grain of salt. Still: 'DD' or 'D' are intended to represent a single byte. 'LL HH' is intended to represent a 16-bit word in little-endian order. '[X]' is intended to represent a RAM location, as in the native instruction set. 'vAC=D' is intended to mean that the (16-bits) vAC gets the (unsigned) 8-bit value D, and with that clearing its high 8 bits in the process. ---------------------------------------- Experimental vCPU instructions in DEVROM ---------------------------------------- See also this thread: https://forum.gigatron.io/viewtopic.php?f=4&t=136 Mnem. Encoding #C Description ----- --------- -- ----------- CALLI $85 LL HH 28 Goto immediate address and remember vPC (vLR,vPC=vPC+3,$HHLL-2) CMPHS $1f DD 28 Adjust high byte for signed compare (vACH=XXX) CMPHU $97 DD 28 Adjust high byte for unsigned compare (vACH=XXX) Changed cycle times ------------------- LD $1A DD 22 (was 18) INC $93 DD 20 (was 16) ANDI $82 DD 22 (was 16) DEF $CD DD 24 (was 26) ------------------------------------------- Mapping from vCPU instructions to GCL words ------------------------------------------- Summary of prefixes: Operators Word variables: X *i X X= X, X. X; X: X& X| X^ X+ X- Byte variables: X >*i Addresses or constants &X i i& i| i^ i+ i- Instruction Variable word Constant word Keyword Comment ----------- ------------- ------------- ------- ------- LDI ii i LDWI ii LDW X was i; and \C; (keep?) STW X= i: i= deprecated PEEK peek alias for _vAC, LDW+PEEK X, POKE X. DEEK deek alias for _vAC; LDW+DEEK X; DOKE X: LD X <*i >*i was i, (keep?) ST X= <*i= >*i= was i. (keep?) INC X++ <*i++ >*i++ change to i++ LDLW %i was i% STLW %i= was i%= ALLOC i-- i++ XXX should become i%- i%+ CALL X! *i! call alias for _vAC! DEF def POP pop PUSH push RET ret BRA else loop BCC if... ANDW X& *i& ORW X| *i| XORW X^ *i^ ADDW X+ *i+ SUBW X- *i- ANDI &X& i& ORI &X| i| XORI &X^ i^ ADDI &X+ i+ SUBI &X- i- LSLW i<< alias for i*LSLW LSRW i>> XXX if we can add LSRW to vCPU SYS i!! was i! LUP i? ------------------------ Idioms and coding tricks ------------------------ 255& Clear vAC high byte 255| 255^ Clear vAC low byte (there is no 'ANDWI' instruction) \vACH, Move vAC high byte to low (vAC>>=8) \vACH. 255| 255^ Move vAC low byte to high (vAC<<=8) 255^ 1+ Negate vAC low byte >_vAC++ Increment vAC high byte 128- 128- Decrement vAC high byte 128^ 128- Sign extend vAC (provided the high byte is zero) 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 1| else a b-] Checking if the operands are of opposite sign first [if>=0 ...] makes the comparison safe from overflow. [def ... ret] fAddr= Defining function whose address is saved into var fAddr fAddr! Calling said function with CALL (v4 method) [def _fSym=* ... ret] Defining a function whose address is given by symbol fSym \fSym! Calling said function with CALLI (v5 and above) -------------- SYS extensions -------------- 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 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 Added in ROM v4: 0b09 SYS_ExpanderControl_v4_40 Set register in I/O and RAM expander 0b0c SYS_Run6502_v4_80 Execute MOS 6502 code until BRK 0b0f SYS_ResetWaveforms_v4_50 Setup sound tables in page 7 0b12 SYS_ShuffleNoise_v4_46 Shuffle noise table in page 7 0b15 SYS_SpiExchangeBytes_v4_134 Send and receive bytes Added in ROM v5a: 00ef SYS_ReadRomDir_v5_80 Navigate ROM directory Added in ROM v6: 009e SYS_Multiply_s16_v6_66 16 bits multiplication (from at67) 00a1 SYS_Divide_s16_v6_80 15 bits unsigned division (from at67) 00e6 SYS_ScanMemory_v6_50 Search byte in memory region 00e9 SYS_CopyMemory_v6_80 Copy memory region 00ec SYS_CopyMemoryExt_v6_100 Copy memory across RAM extension banks 0b18 SYS_ReceiveSerial1_v6_32 Receive byte on input port (for the loader) Application specific SYS calls in ROM v1 that are -not- part of the ABI: SYS_Read3_40 (Pictures) SYS_LoaderProcessInput_48 (Loader, renamed SYS_ReceiveSerial1) SYS_LoaderNextByteIn_32 (Loader) SYS_LoaderPayloadCopy_34 (Loader) SYS_RacerUpdateVideoX_40 (Racer) SYS_RacerUpdateVideoY_40 (Racer) Retro-actively retired from ABI: SYS_Reset_36 Soft reset (use vReset instead) For details on arguments, side-effects and return values, please refer to the comments in the ROM source files (Core/ROMxxx.py). -- End of document --