Close

Microbasic CPU instructions and their execution

A project log for CPU running Basic

Celebrating 50 years of Tiny Basic by implementing a custom micro-coded 16/32-bit CPU that executes it directly (up to 100MHz)

zpekiczpekic 11/12/2025 at 06:210 Comments

Microbasic CPU executes the full set of TBIL (Tiny Basic Intermediate Language) operation codes, which are described here. It is a physical implementation of a virtual machine required by Tiny Basic to interpret Basic, unlike implementations on classic microprocessors which implement the TB virtual machine in their own instructions (for example 6502 or 6800)

It is helpful to visualize the instruction set:

Note: for extended Tiny Basic, op codes 0x25 (FOR), 0x26 (NEXT) and 0x1E (NC - next statement after colon) are also used. 

Execution of each instruction begins with straightforward fetch cycle:

fetch:    traceString 51;                                            // CR, LF, then trace Basic line number (in hex, for speed)
    traceString 2;                                             // trace IL_PC and future opcode
    IL_OP <= from_interpreter, IL_PC <= inc, traceSDepth;    // load opcode, advance IL_PC, indent by stack depth IL code if tracing is on
    T <= zero, alu <= reset0, if IL_A_VALID then fork else INTERNAL_ERR;    // jump to entry point implementing the opcode (or break if we went into the weeds)

IL_OP is the 8-bit instruction register which is loaded from external IL ROM (name of this MUX source is "from_interpreter"), then the IL_PC is incremented. After that, microinstruction control unit executes a "fork" instruction, which is loads the microinstruction program counter with the address of the starting routine for that instruction. Hardware that supports this:

Looking at the content of map ROM, it is easy to recognize the start addresses of the instructions. For example, comparing first 16 words with the first row of the instruction set table it is clear that SX starts at microcode ROM location 0x00E, and that location 0x00D is occupied by the "bad op code" routine which will generate an internal error. 

constant mb_mapper: mb_mapper_memory := (
-- L0381@0011 0011.  0011.map 0x00;
0 => X"0011",
1 => X"000E",
2 => X"000E",
3 => X"000E",
4 => X"000E",
5 => X"000E",
6 => X"000E",
7 => X"000E",
-- L0386@0013 0013.  0013.map 0x08;
8 => X"0013",
-- L0392@0015 0015.  0015.map 0x09;
9 => X"0015",
-- L0400@0019 0019.  0019.map 0x0A;
10 => X"0019",
-- L0408@001D 001D.  001D.map 0x0B;
11 => X"001D",
-- L0416@0021 0021.  0021.map 0x0C;
12 => X"0021",
13 => X"000D",
14 => X"000D",
15 => X"000D",
...

 This mapping ROM is produced automatically by the MCC compiler based on .map statements in the microcode source. The first map matches all instruction opcode patterns, meaning the whole mapping ROM will be first filled with entry to go into the badop, and then subsequent .map statements will override based on the pattern they specify. This makes implementation of op-codes for even complex processors easy (including the ones which have "prefixes" such as Z80, in which case either the map ROM must be expanded (fast solution, but needs more ROM) or condition implemented to recognize the prefix in each "overloaded" instruction call (slower)

    .map 0b????????;                     // opcode sink of last resort
badop:    goto INTERNAL_ERR;
    
    ////////////////////////////////////////////////////////////////////////////////
    .map 0b00000???;                     // SX (Stack exchange, 0x00 .. 0x07)
    ////////////////////////////////////////////////////////////////////////////////
    traceString 15;                      // trace mnemonic
    ExpStack <= startSwap;               // R <= ExpStack(0), S <= ExpStack(0 + param) 
    ExpStack <= endSwap, goto fetch;     // ExpStack(0) <= S, ExpStack(0 + param) <= R

    .map 0x00;                           // SX 0 does nothing, so map to just skip
    traceString 15;                      // trace mnemonic
    goto fetch;

Execution of each IL instruction is traced on dedicated trace serial output, if enabled. If not enabled, each traceXX subroutine call takes 1 cycle, which is ok penalty to be able to visualize how TBIL executes Basic command line and/or program. 

The number after traceString is index into a table which is like a string format:

traceString 51 - prints Basic line number and IL PC

traceString 2 - prints depth of IL stack (indents)

traceString 15 etc... prints SX (mnemonic for the executed instruction)

For example, this is how "print 17/5" is executed in direct mode:

As expected in direct mode (Basic line number is 0000), it starts and ends with GL (get line) instruction which accepts input characters (from "console", which is USB2UART to host computer) until ENTER is pressed. 

Discussions