Close

Extending Tiny Basic (to be more like another Tiny Basic)

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/27/2025 at 00:040 Comments

While in it original form it can already be useful (esp. for embedded apps), the original Tiny Basic interpreter is very rudimentary and is lacking many features. During the same time, another Tiny Basic version emerged. It was a classic interpreter (no intermediary code) but had a bit bigger feature support. 

With some minor tweaks, I was able to largely close the gap. What is missing:

On the flip-side:

Here is the list of new capabilities and how they were implemented. Depending on the feature, changes were needed in any of the code layers (interpreter or microcode), or in hardware itself (Basic CPU)


Basic feature
InterpreterMicrocodeCPU
NEWadded to parser and execute as CLEAR--
FOR v=from TO end [STEP step]added to parser right after LET (for speed, more frequently used statements should be parsed out first). Interpreter just gets the variable name (must be A..Z, array elements not allowed), from and end value. If STEP is not given, default of 1 is pushed on stack and then then new instruction FS is executed.FS first checks if Vars_Next is populated. If yes, it means that this an iteration, therefore var = var + step, var > end must be executed. If no, means FOR must be set up with var = start, var > end. If FOR loop must be terminated, there are two cases:
(1) pointer to NEXT exists, just go there and find first instruction after
(2) pointer to NEXT is not set, so search for matching NEXT and then continue with case (1)
Added CPU instruction 0x25 (FS) - there is a Vars_For field for each variable
NEXT vadded to parser after FOR. Interpreter checks for presence of variable name A..Z (implicit NEXT with no variable name is not allowed) and then executes FE instruction.FE first ensures FOR has been executed for this variable (if not, that is clearly an error), and then puts the pointer in Basic text of this NEXT statement in the Next field. Branching back to FOR is easy because Vars_For contains the line number. Added CPU instruction 0x26 (FE), there is a Vars_Next field for each variable
INPUT "prompt"Check for double quote before expression, and if found print out verbatim, then continue--
multiple LET v=expr1, v=expr, v=expr...Modified LET to check for presence of comma after each variable assignment, and loop if present. --
@(index) arrayAdded in LET command (left side) and expression evaluation (right side). This way it appears there is one array that can be used on both sides of expressions. New USR operations added:
@(index) on left side (assign): USR(30, PrgEnd + 2* index, value)
@(index) on right side (get value): USR(31, PrgEnd + 2* index)
new operation in register T to evaluate address from index
SIZE read-only variableadded parsing and evaluated as USR(29,...)SIZE = Core_End - PrgEnd. value of PrgEnd is evaluated at each warm start, which Core_End (last address in RAM) is currently hard coded. new operation on register T
ABS() functionadded parsing and execute using already existing code path for RND()--
% (modulo operator)added parsing and execute  through new USR(27, .., ..) call

:T2	BC T3, "%"		//factors separated by modulo
	JS FACT
	LN 27			//a % b = USR(27, a, b)
	SX 1
	SX 5
	SX 1
	SX 4			//rearrange stack so that 0x001B (USR code) is at the bottom
	SX 1			//swap TOS and NOS
	SX 3
	SX 1
	SX 2
	US
	BR T0
:T3   	RT
added USR(27, ...,...) which in turn uses existing div / mod routinelast step of div also corrects the sign of remainder to be same to dividend
THEN shortcut (If THEN is followed by integer, it is assumed to be a GOTO (e.g. IF a>b THEN 320))slight parser modification in IF statement--
PEEK(address) functioninvoke USR(20, address, 0)

:F2   	BC F2ABS, "PEEK"	// peek function
	LN 20			// peek is usr(20, param, param), always PEEK8
	JS FUNC
	BR F2CONT
--
POKE address, byteinvoke USR(24, address, byte) but discard the return value

// POKE
:POKE	BC RETN, "POKE"
	LN 24		//POKE = usr(24, address, byte)
	JS EXPR		//address
	BC *, ","	//expect comma
	JS EXPR		//byte value
	US
	SP		//drop usr() return value
	NX
--

The CPU and the microcode both support the full functionality, it is just a matter of which version of interpreter is presented to the CPU:

In the project, both are present and can be selected by flip of the switch. This can be done safely when the interpreter is in command mode, waiting for input (executing GL instruction). To differentiate, different prompt characters are used ( > original, vs. : extended)

I implemented FS (FOR start) and FE (NEXT) to leave on top of evaluation stack the line number of next instruction to be executed, which can be:

Statement \ CaseLoopDon't loop
FORFirst statement after FORFirst statement after NEXT
NEXTFOR statementFirst statement after NEXT

This way, FOR/NEXT branching boils down to executing a GO instruction. Which already has the GOTO cache hooked up, so FOR / NEXT loops can benefit from caching too.

Comparing the benchmark results for Basic code on original interpreter vs. the extended, the extended runs 3% faster:

Extended, at 100MHz:

Original, also at 100MHz:

Discussions