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