TinyBasicLike, a target-independent Basic interpreter
(Last modified 29 October 2023)

Edited 30 Jul 2023: It would help to include the .zip file.  Sorry about that.  (Thanks, Maya!)

Edited 29 Oct 2023: Moved link to source files to the TinyBasicLike core webpage.


This project began after a typical Hackaday rabbit-hole journey.  I somehow ended up at BleuLlama's TinyBasicPlus website, which featured a small Basic interpreter built around the earliest Palo Alto Tiny Basic.  I downloaded the source code and started looking through it.

I remember the excitement, decades ago, when Dr. Dobb's Journal (DDJ) released Tiny BASIC.  The idea of having a real computer language running on a small computer I had built fascinated me.  That dream took me into embedded firmware design and to what became my career.  The download of TinyBasicPlus brought me full circle, as it contains source for a Tiny Basic interpreter, written in C, suitable for running on small micros.


History

TinyBasic Plus (TBP) was created by Scott Lawrence, based on work done by Gordon Brandly and modified by Mike Field, Scott Lawrence, and Brian O'Dell (according to notes in the TBP source).

Brandly's original work started with Palo Alto Tiny BASIC, which was published in the May 1976 issue of DDJ.  He ported that code to the Motorola 68000 and released it in the February 1985 issue of DDJ as TBI68K.  Mike Field then ported Brandly's code to the Arduino, writing his version in C.  Scott Lawrence in turn added his work to Field's code and released his version as TinyBasic Plus, which is the code I began with.

It seems Lawrence's work started in 2012, culminating in his release of version v0.15 in June 2018, which is what I used.

I didn't want to call my project TinyBasic Plus, as that is Scott's work.  But I did want to retain the feel of Tiny Basic's history.  So I decided to call my project TinyBasicLike, since it's kind of like TinyBasic.


TinyBasicLike

The most important aspect of my project is target-independence.  I wanted to isolate everything that makes up the core of TinyBasicLike (TBL) from the code needed by whatever platform runs it.  This will let me quickly create a new target version of TBL with zero impact on the core code.

I divided TBL into two C files.  The file TinyBasicLike.c contains the core code, including the interpreter and support functions.  The core file is compiled with a second file that contains all target-specific code; the result is a TBL version matched to the selected target.

Writing the core file required me to strip all target-specific elements from the original source file, typically low-level Arduino code.  Whatever the core needed to access that was specific to the target was replaced with a required C function.  This set of target functions will be supplied in the associated target source file.

For example, when the core needs to output a character to the console, it invokes the function t_OutChar().  The console has no clue where this is character is going or what hardware is involved, it knows only to call that function to output a character.  Similarly, the core calls t_GetChar() when it needs a character from the console.

Design of the core

I stayed with the design of the original core.  This is a true text-based interpreter; there is no intermediate language or tokenization involved.  A keyword or command is read by stepping across the characters in the input text line.  When a keyword or command is found, control passes to the appropriate function, which continues to step across the text line as it gathers arguments.  After converting the characters to the appropriate data, the function performs the requested action, then returns to the main loop and the next set of characters is collected and executed.

I was impressed with the size of the execution loop in the core.  This is pages of labels, code, and goto statements, easily the largest single function I've ever worked on.  I like this design.  The use of gotos allows blocks of code dedicated to certain functions to be accessed by other blocks easily.  For example, the block of code for saving the current file to storage simply sets a couple of flags, then jumps to the code (via goto) that lists the current file.  Saving and listing are identical functions, only the target output device (file or console) changes.  Simple and elegant.

Supported keywords are stored in a table and scanned by the core when a text line is processed.  Similar tables exist in the core for functions, logical operators, relational operators, and shift operators.  The core expects the target to supply another table containing the names of all supported I/O ports.  Note that the core knows nothing about what these ports do, the core only uses this table to resolve a port name it finds in a program.

I've tried to make as many elements as possible of the core design target-independent.  For example, the core is not inherently 16-bit or 32-bit.  The size of a data element is defined by the target source file and is known to the core as DATA_SIZE.  I have a 32-bit version of TBL for the STM32F4.  I also built a version for my desktop Linux box that is 64-bit, because yes, I wanted to make a 64-bit Tiny Basic.

Similarly, the amount of RAM available to the core for your program is defined by the target file, which declares the RAM buffer and which the core references as an extern.

Core modifications

The TBP I started with followed most of the design for the Palo Alto Tiny Basic.  For example, the original code supported 26 single-character variables (A-Z); each variable is one DATA_SIZE unit in size.  There is no support for string or floating-point variables, though you can print fixed strings from your program.  Programs are written with line numbers, not free text.  Editing a program means retyping existing lines or entering just a line number to delete an existing line.

Since I was writing something that wasn't truly Tiny Basic, only sort of like it, I made a few changes to the core design.  I borrowed a concept from Dartmouth BASIC and made variables X, Y, and Z each into 20x20 arrays.  For example, your program can refer to X, X(399), or X(19,19).  The variables X, X(0), and X(0,0) all refer to the same address in memory and can be used interchangeably.

I added operators for left shift (<<) and right shift (>>)

 I added the ADDR() function, which returns the address of a variable or array element.

I added down-counting timers (DCTs), maintained by the target.  You can use TIMERRATE to assign a tic rate to the target's DCT system; rate is assigned in usecs.  ADDTIMER adds a named variable to the target's list of timer variables.  DELTIMER removes a named variable from the target's list.  Your program uses a DCT by writing a value to the associated variable.  The target decrements the value in that variable at the set rate until the value reaches zero.  Your program can simply test the variable's value and take action when the value hits zero, signaling the required delay has elapsed.

LIST can list all program lines, a block of lines, or a single line.  SAVE can save all program lines, a block of lines, or a single line to a named file.

LOAD reads a named file into program memory, overwriting any existing program in memory; I added an AreYouSure warning before the overwrite happens.  I also added MERGE, which will merge a file from storage with an existing program.

The changes to SAVE, LOAD, and MERGE allow you to create libraries of code, then add them as blocks to a new program.

Added PRINTX to print a value in hex, PRINTA to print a value as its corresponding ASCII character, ? as a synonym for PRINT, ?X as a synonym for PRINTX, and ?A as a synonym for PRINTA.

INPUT now accepts hex numbers (start with 0x) and characters (in form 'c').

INPUT now displays the base variable name when it prompts for input; for example:  A ?

INPUT can handle a mix of simple variables, 1D and 2D arrays; for example:  INPUT B, X(20), Z(4,5)

Rewrote the RND() function using custom 64-bit code but reduced (if needed) to the value of DATA_SIZE.

Overloaded the assignment operator (=) to allow initializing arrays.  It now writes the first value to an array element, then writes successive comma-delimited values to successive array elements; for example: X(8) = 8, 9, 10.  If you include a comma but no value, the corresponding cell is not modified.  For example, X(8) = 8, , 10 would not change X(9).

Added AND, OR, and XOR logical operators.

Added the WORDS keywords (borrowed from Forth).  This prints a list of all keywords, functions, operators, and target I/O ports.

Added support for automatically starting a file upon power-up or reset.  The file must be named autorun.bas (case-sensitive).

Added support for typing ctrl-C on the console for interrupting program execution and returning to the core main loop.

The target design

Isolating the target code means you can focus on porting to your target hardware without worrying about messing up any core functionality. The target must supply a small set of functions to the core, hiding all target-specific limits and setup.  All such target-specific functions are named t_xxx(), where xxx describes the function to perform.  For example, the core will invoke the function t_ColdBoot() immediately after reset to allow the target to execute any startup actions.

This collection of functions is all of the target-specific code and can be quite small.  For example, my target_linux.c file, which builds a 64-bit version of TBL for the Linux desktop, is only 10K.  My target file for the STM32F4 is over 30K, but that's because I included support for ALL possible USARTs and I also included code to support program storage in flash.

Speaking of flash, I designed a small flash-based storage system for the STM32F4.  I didn't want to deal with a USB drive.  Normally, flash storage would be done with an SD card and ChaN's FatFS.  I love ChaN's system, but I really wanted the absolute smallest amount of hardware on my target.  So I used the STM32F407's flash sector 3 (16KB) as a single flash drive.  The design holds eight 2KB flash files, accessible by file name.  A 2K byte file may not sound like much, but that's actually quite a bit of code.  Feel free to modify this if you like, perhaps by using one of the really large flash sectors instead.

In order to minimize the number of flash erases, I designed the flash storage to read and modify a 16KB block of RAM.  The flash sector is copied into this RAM block on startup, and all subsequent file reads and writes use this RAM buffer, not the actual flash.  To force writing the RAM buffer contents back to flash, I used TBL's BYE keyword.  This normally logs out of the Tiny Basic system, but there is nothing to log out to in the embedded world.  In this case, the core calls t_Shutdown, which lets the target erase the flash sector, then write the RAM buffer to it, assuming the RAM buffer doesn't match existing flash.

Part of the target source code exposes I/O ports to the core through a required table named ports_tab[].  This table lists all I/O ports you want the core to access, using names of your choice.  The core will search this table for a named port; if the port is found, the core will record the index of the ports_tab[] entry.  Later, the core will call t_WritePort() or t_ReadPort(), passing the saved index into the routine so the target code knows which port address to access.

There is a set of target functions that deal with file I/O.  t_GetFirstFileName causes the target to return the name of the first file in the storage buffer.  t_GetNextFileName causes the target to return the next file name in order, or NULL if there are no more files in storage.  These routines serve as a crude directory list utility.  Interestingly, there are no similar functions in the Linux world.  Linux is POSIX compliant and the notion of a directory is not.  So the Linux version of TBL has no file support but the STM32F4 version does.

The STM32F4 Discovery board, which I used as my target hardware, has a blue USER button.  My target code polls the state of the GPIO pin (GPIOA-0) to see if the button is pressed.  If it is, the target code returns TRUE when the core checks to see if the user has entered ctrl-C on the console.  This means you can break a running program using either the console keyboard or the Discovery USER button.

Here is the full list of target-specific functions used by the TBL core:

Name Purpose Notes
t_ColdBoot Target performs system init following power-on

t_WarmBoot Target performs system recovery following crash

t_Shutdown Target performs system prep for power-off

t_OutChar Send char to active output stream
Blocks until able to send char
t_GetChar Get char from active input stream
Blocks until char is available
t_ConsoleCharAvailable Check for available char from console
Ignores active stream, returns TRUE if char available
t_GetCharFromConsole Get char from console
Ignores active stream, blocks until char available
t_SetTimerRate Sets tic rate for the down-counting timers (DCTs)
Rate selected in usecs
t_AddTimer Add a variable to the table of DCTs

t_DeleteTimer Remove a variable from the table of DCTs

t_SetOutputStream Select the active output stream
Used for console and for file writes
t_SetInputStream Select the active input stream
Used for console and for file reads
t_FileExistsQ Tests to see if a named file exists

t_OpenFile Opens a named file for read or write

t_CloseFile Closes an open file
NOT guaranteed to flush pending writes!
t_GetFirstFileName Returns name of first file in directory or on device

t_GetNextFileName Returns name of next file in director or on device

t_DeleteFile Deletes a named file

t_ReadPort Target returns value from selected I/O port

t_WritePort Target writes value to selected I/O port
Writes can be 8-, 16- or 32-bit wide
t_Test Generic test function
Called by core, performs target-specific action (usually used for debug)




TBL on the STM32F4 Discovery

So what does all this look like on a real embedded micro?  The file target_STM32F4.c is a TBL implementation for use on the STM32F4 Discovery board.  I chose this board because I had a couple laying around and had existing startup and system init code already done.

My target file assigns a 16K RAM buffer to hold the user's Basic program plus all variables.  It also allocates flash sector 3 (16KB, starting at 0x0800c000) as file storage.  It declares DATA_SIZE to be a uint32_t, creating a 32-bit TBL.  It provides addresses for all ports associated with GPIOA (USER button) and GPIOD (LEDs).  It uses USART1 on pins PB6 (Tx) and PB7 (Rx) as a serial console at 19.2 KBaud.

I've included a suitable makefile (target_STM32F4.mak) for building the final code.  You will have to edit the locations for certain files and directories to match your own STM32F4 directory layout.  I've also provided a working target_STM32F4.elf that you can push into your Discovery board and play around with TBL.

Naturally, I had to make a blinky.bas program.  This code uses a DCT to set the blink rate and duty cycle for blinking the orange LED on the Discovery board.  The program is an endless loop; enter ctrl-C on the console serial port or press the USER button on the Discovery board to halt the program.  If you save this program to the Discovery board's flash as autorun.bas, the orange LED will begin blinking when you power-up the board.  Here is the listing, captured from my console screen (gtkTerm on my Linux desktop):

>
>list
10 \      blinky.bas    blink orange LED on Discovery board
20 \
30 \ Orange LED is on PD13; need to make GPIOD:13 an output.
40 GPIOD_MODE = GPIOD_MODE OR 1<<26
50 \
100 \ Set up variable T as a down-counting timer with 1 msec tic rate.
110 TIMERRATE 1000
120 ADDTIMER T
150 PRINT : PRINT "Hit Ctrl-C to end program..."
200 \ Turn on orange LED for a while.
210 GPIOD_OSET = 1<<13
220 T = 250
230 IF T > 0 GOTO 230
300 \ Turn off orange LED for a while.
310 GPIOD_ORESET = 1<<13
320 T = 750
330 IF T > 0 GOTO 330
400 GOTO 200
OK
>

As you can see, accessing bits in the STM32F4 port registers is straightforward.  This opens the possibility for much more complex I/O operations, such as generating PWM outputs or complex waveforms.

I was curious about execution speed, so I wrote a program that simply counts up from 0 for one second, then reports the count.  This yielded about 6700 empty loops per second, or about six lines of TBL program code per millisecond.  Not blazing fast, considering it's running at 168 MHz, but speed has never been the hallmark of a true interpreter.  Still, I find it a lot of fun to hack out a working program on a bare-metal platform in a high(ish) level language.

The TBL core and whatever target code you add can give you a quick jump on board bring-up for new hardware.  It can also serve as a great entry point for exploring a new board or for working out the kinks in a new project.

That's a wrap

This has been the most fun I've had in embedded development in quite a while.  It has been a pleasure to work with such well-designed code, and I appreciate the effort that the previous contributors put into their work.  I also appreciate their making this code available for people like me to play with.

I have left their licensing and acknowledgements intact in my TinyBasicLike.c source file, while adding my own list of contributions.  If you use or modify any code in either of my C source files, please retain all original licensing and acknowledgements.

Note that this code has been lightly tested and I guarantee you will find bugs.  You would be daft to rely on this code for anything serious.  You've been warned; don't blame me if things go belly-up using this code or any code derived from it.  I'm releasing it so you can tinker and have fun, just like I did.

You can find the source files in the TinyBasicLike core webpage.

Please drop me an email with any comments or suggestions.


Home