Serial Interpreted Microcontroller Programming Language
This draft post attempts to introduce SIMPL - a simple interpreted language for microcontrollers.
SIMPL is a small language resource that resides in flash memory on the target microcontroller. It contains an interpreter that allows the user to have control and interactive access to the memory and peripherals of the mcu without placing an unnecessary burden on its limited resources.
SIMPL can be used as a development tool to exercise new hardware, or as a compact and efficient programming environment that may be ported to virtually any microcontroller or FPGA soft core, that supports a C compiler.
SIMPL can be implemented in Arduino C++ code, or in standard C code. It compiles to under 10Kbytes on most systems so can reside alongside the user application program.
SIMPL defines a 16 bit integer virtual machine. With this VM present on the target microcontroller, the user need not get involved with the machine specifics of a particular microcontroller.
The Advantage of SIMPL
SIMPL is a minimalist interpreted language for direct control of microcontroller hardware, via a communications link. This may take the form of wired serial connection, a wireless or Bluetooth Low Energy link.
SIMPL reduces a program to a sequence of ASCII characters, which may be entered interactively from the keyboard. It offers a means of executing a series of subroutines (stored in Flash) from a sequence of characters stored in RAM.
SIMPL will run on virtually any microcontroller, or FPGA that supports a C compiler. It's instruction set is small, just 50 commands, and designed to be human readable. Snippets of SIMPL code are compact and can be embedded into communications packets or even web URLs (such as REST), for commanding remote devices.
SIMPL reduces program code down to a few lower case alphabetical and punctuation characters - which each character representing a function or block of functions, which are stored in the flash memory of the microcontroller. These operations can be further reduced to a concatenated block of code that is represented by a single upper case character, however these operations are stored in RAM, and can be updated and modified at run time.
In this way, a microcontroller device, containing the SIMPL interpreter in flash, will execute commands sent to it via a communications link - such as serial, or BLE UART link, or wireless packet protocol conveyed over low power wireless - for example RFM12B or RFM69 and Jeenodes. It can also be reprogrammed remotely.
A Brief Outline.
SIMPL uses a very small interpreter, that sequentially scans through characters stored in a buffer, decodes the character, and executes a small block of code from flash, that is associated with each character.
The interpreter looks at each character in turn, checking if it is a number (0-9), a lowercase character (a-z), a punctuation character, or an uppercase character (A-Z). Depending on the type of character encountered, the interpreter follows a different strategy. Non-printable ASCII characters (less than 32 or greater than 128) are currently ignored by the interpreter.
Numbers 0-9 are decoded into a 16-bit integer variable called x.
Lowercase and punctuation characters, known as primitives, and are decoded in a series of Switch-Case statements to execute small blocks of code.
Uppercase characters, known as Definitions, force the interpreter to jump to a given address in RAM, and execute the string of characters found there, rather than the input buffer.
SIMPL uses a very simple, 16 bit virtual machine model for its operation. This model fits nicely with C code and 16 bit integer maths.
It contains two 16 bit registers, x and y into which the operands are loaded. These registers are used as a pair of operands for all arithmetic, comparison and logic operations, with the result being placed in x. In this respect, x may be considered to be analogous to the accumulator in other systems.
x is also used as the data source and destination register for any memory or I/O operation. The y register is used to supply the address for memory operations.
In addition to x and y, there is a loop counter variable k. This is decremented each time a loop is executed and is used to force the interpreter to exit the loop when k reaches zero.
Implementation in C and Arduino
Whilst the SIMPL interpreter is written in C code ( and C++ on Arduino) for portability, it could be ported to run under the native machine language of any specific processor, if much greater execution speed were needed. However with mcu clock speeds now 10 to 100 times faster than early 8 bit processors, resorting to machine code may seldom be needed.
However, there is a case to warrant the use of direct register manipulation - especially for the digitalRead and digitalWrite commands as used in Arduino. These can be sped up by a factor of 10 using direct port manipulation.
Instruction Set
Alpha Primitives
a
b
c
d Define a digital port pin
e
f for-next structure
g
h Set an output pin High
i Read an input pin
j
k The loop counter operand
l Set an output pin Low
m A blocking delay in milliseconds
n
o Write to an output pin
p print x to the serial device as an integer number
q
r
s Get an ADC sample
t print the current microsecond count (for timing functions)
u A blocking delay in microseconds
v
w While structure
x The primary data (Accumulator) operand
y The secondary (Address) operand
z Sleep - a non-blocking pause for x zeconds - whilst still monitoring UART and I/O
Maths Group
+ Add x and y and put the result in x
- Subtract x from y and put the result in x
* Multiply x and y and put the result in 32 bit pair [y:x]
/ Divide y by x and put the result in x, and remainder in y
% Calculate y modulo x and put the result in x
space Move the number just entered to y (allows for 2nd variable)
Comparison Group
> If y greater than x, put result in x (equiv to x = y-x)
< If y less than x, put result in x (equiv to x = y-x)
= If y equal to x, put result in x (equiv to x = y-x)
Logical Group
& Bitwise AND of y and x
| Bitwise OR of y and x
^ Bitwise XOR of y and x
~ Bitwise Complement of x
Memory Group
@ Fetch the data into x from the memory address defined by y (PEEK)
! Store the data in x to the memory address defined by y (POKE)
Definition Group
: Start a colon definition
; End a colon definition
Block Group
{.................} Loop the code contained within the braces whilst loop variable k is +ve
(................ .) Conditionally execute (or skip) the code contained within the round brackets
[,,,,,,,,,,,,,,,,,,] Create an array of comma separated elements within memory (enumerate and store)
_Print String_ Print the string contained within the underscores
"Store String" Store the string between the quotes in memory
.A. Print the characters assigned to A (list the word A)
'A' Retrieve the string from memory stored at address A
Control and Miscellaneous
? Print out the contents of the program stored in user RAM (LIST)
, A separator between elements stored within memory - store and increment
\ Escape from interpreter - stop all commands
£ Restart interpreter
$ Load x with the ASCII value of next character - do not interpret/execute
# Send a number as a bit pattern to a device, such as port register or shift register
Timing, Sleeping, Pauses and Delays
Much of what a microcontroller does is to synchronise events with time. This might be as simple as flashing a LED once per second, or toggling a square wave from a port pin at a given frequency. Arduino has been equipped with the millis() and micros() functions, and the delay() and delayMicroseconds() functions to make time synchronised coding simpler.
The delay functions are blocking - in that the microcontroller cannot execute any other code until the delay period has elapsed. This is OK for short periods, up to about a second, but it prevents the system from responding to external events, such as a change on an input pin, or the User trying to stop the program. For this reason, SIMPL has been given a non-blocking delay command z, which allows it to pause for a while, whilst still monitoring the UART Rx buffer for certain characters - such as the \ escape character, or for a change in status of any input lines, which might signify an external event.
z - is often associated with sleeping, so appropriate for a command that effectively allows the microcontroller to take a nap, until something happens to wake it up.
I also propose at this point the zecond - that is a non-blocking nap for a second. This allows for naps of 1 zecond up to 18.2 hours to be defined.
Waiting for External Events
This is a common use for While statements for in C.
while(some condition is met)
{ Execute this block of code, and continue to test condition}
This simple construct needs to be efficiently coded into SIMPL.
For example - wait for a value to be exceeded on an ADC input, say 600 on ADC 5.
600 5s>(............)
600 is first loaded into x, and the space character transfers it to y. Then we read ADC 5 (into x) and compare it with the value in y. If the result in x is positive - we execute the code in contained between the round brackets. This code could just be an idle loop or a pause - non-blocking delay. After this code is executed, we need to test again.
To code this into SIMPL we need to have a mechanism to repeatedly perform the test, and a means to execute an idle loop.
We can make this distinction using the two flavours of brackets. The code to test for the condition is enclosed in round brackets, and the code for the tasks to be done whilst idling is enclosed in the curly brackets (braces). This makes our code as close as possible to the C construct.
We then have the general form, where w defines the while statement
w(...condition..){...idle loop.....}
or as in the example
w(600 5s>){loop here until condition returns zero or less}
This is a kind of conditional form of the general loop, which decrements the loop counter k each time around the loop, until zero is reached.
Can we use the same constructs to implement for-next loops?
General form in C
for(i=0; i<=10; i++)
Here we have a loop variable i, which is initialised to zero, tested against an upper limit (to execute a conditional block of code) and then incremented (or modified) each time around the loop.
Let' use f to designate a for loop. We need to supply 3 parameters
initial value, test condition, modifier
We could do this by setting up the initial value and modifier outside of the test condition - by using x and y
Lets run from 0 to 100 with a modifier of +1
0 1f(100<){ loop}
A better method might be to borrow the loop counter k, so that 3 parameters can be entered into the conditional statement.
Using the space as a stack pushdown, the sequence
10 11 12
puts 10 into k, 11 into y and 12 into x.
The Decimalisation of Time- A SIMPL means to handle regular events.
This comes as a result of the need to control equipment at specific times during the day, and on specific days of the week.
It allows very simple code to be used to determine the time and day, and decide whether an action routine should be executed.
By
reducing the time and the day of the week to a 16 bit integer, simple
comparison operations can be done to decide the program flow.
In
applications such as central heating control, there is no need to
resolve to the nearest second, when 10 second accuracy is perfectly
acceptable. It also allows the table that schedules operations to be
condensed into an array of 16 bit integers.
Let's invent a new measure of time - the decond. A decond is 10 seconds and there are 6 deconds in a minute.
1 hour = 360 deconds
1 day = 8640 deconds
1 week = 60480 deconds (less than 65536 - so can be expressed as16 bit unsigned integer).
As
there are less than 10000 deconds in a day, the most significant digit
of the 16 bit number can represent the day of the week between 0
(Sunday) and 6 (Saturday)
In
addition, the Arduino millis() function can be used for timing, and
incrementing the decond counter. 10000 millis in a decond, and 60000
millis in a minute. Both of these will nicely fit into a 16bit unsigned
integer.
More Applications for SIMPL.
A few notable things have happened since I was last using SIMPL to control stepper motors and LED arrays using shift registers. I have developed a new compact RFM12/69 wireless I/O breakout board for the Raspberry Pi in association with my friends at Open Energy Monitor and I have invested in a couple of FPGA dev boards - launched via Kickstarter.
Both of these developments can benefit from using the SIMPL language to exercise them and try out ideas on new hardware.
The RFM-Pi board is a compact I/O board (33 x 28mm) fitted with an RFM12/69 wieless module and ATmega328P, running Arduino with the Jeenodes wireless protocol. The pcb plugs into the I/O header of the Model B+ and Pi 2, where it picks up 3V3 power, Rx and Tx from the Pi's UART and a GPIO line for reset. This is a very simple way of providing Jeenodes/RFM12 wireless communications for the Pi, so that it can be used as a basestation for the series of wireless energy monitoring products developed by OpenEnergyMonitor.
However, in the latest version of the RFM-Pi, I chose to make available most of the I/O pins of the ATmega328 - so that in effect, this board may also be used as a I/O expander for the Pi - providing up to 8 analogue input channels and 5V tolerant I/O pins.It can be used as an Arduino compatible wireless I/O extender for the Raspberry Pi. It breaks out 12 digital I/O pins and 8 analogue pins from the ATmega328 SMD package, giving almost complete Arduino functionality in a small form factor.
Additionally, the board may be used as a plug in module, providing RFM12 wireless and Arduino functionality for some other project. It can act as an analogue sensor interface, or hardware controller and be controlled remotely from distances in excess of 100m using the Jeenodes wireless library. It is compatible with the other products in the Open Energy Monitor Range - and could form the basis of a new form of sensor. It requires a 3V3 supply - which could be provided from a coin cell or from a larger lithium cell- such as these 2250mAh common size 18650CA cells.
By running the SIMPL interpreter in the background, the RFM-Pi board can execute SIMPL commands via a 250m range wireless link.
SIMPL is also great way of getting the Pi to drive the I/O board - by a series of easy serial commands. This would open up the Pi to driving LED arrays, stepper motors and indeed, anything that the Arduino can do - just with a few high level commands. The Arduino has 5V tolerance, so best to have it take the knocks when experimenting with I/O - rather than frying a more expensive Pi.
A SIMPL Compiler?
Having "lifted the hood" again on SIMPL, I have been thinking about the possibility of a SIMPL compiler. There might be a way in which C code source could be compiled into an interpreted SIMPL program. A machine compiler would help with more complex programs and reduce human errors in coding.
SIMPL has a number of primitive operators, that are mapped into machine language, using an interpreter written in C. Lower case alpha and punctuation characters limit the primitives to just 58 instructions. This ensures that a microcontroller that runs SIMPL - has a memorable and easily managed reduced instruction set. For future implementations it means that a machine could run SIMPL from a 6 bit instruction.
This would allow more complex SIMPL programs to be machine generated from C source code, and reduced to an interpreted text file to be run on any micro. It's kind of like the Java Bytecode idea where common code can run on a variety of different platforms - provided that they have the interpreter. Imagine what sort of a SIMPL program could be conveyed in a 140 character text message - sent to a remote device by SMS.
Better Branching
For this to happen, SIMPL needs some further augmentation to allow easier branching and decision making - in what I call "If this - then that" or IFTTT- popularised by a recent new IoT service.
Ideally, and in its simplest form, a parameter needs to be tested against a number (using subtraction) and then branch if the result is positive, negative or zero. The branch would be to either execute the next character (code block) or skip it. This would allow the popular constructs of If-Then-Else and While loops to be generated.
I have implemented a simple < or > test, but it's a little clunky and also needs an equality test as to be included as well.
The round brackets (.....) represent a block inline of code that should be executed on the outcome of a test or skipped. This would allow While and For constructs to be programmed.
The square brackets [....] could be used for a switch-case construct to be generated.
Whilst I am running out of lower case characters for the primitive commands - only e,f,g, t and v remain, there is loads more scope for expansion by using some of the remaining printable punctuation characters - a total of 32. Some such as +, -, *, / ,>, <, {, }, :, ;,@, !, _ and ? have already been defined leaving about 18 for later. Logical operators such as & and | (or) are an obvious extension leaving a few like £, $, %, ^, ~, # to acquire some meaning - probably similar to their meaning in a C context.
So in theory, SIMPL can have a maximum of (26+32) primitive commands plus 26 user defined words - as defined by uppercase alpha characters.
Better Arithmetic
SIMPL currently has an x variable and a y variable that can be set to the previously entered value of x using the @ (fetch) and ! (store) operators. This is a bit clunky and a better way of handling multiple variables would probably be by using a stack. I want to try to keep the interpreter simple, and not implement the full stack logic. As most microcontrollers only combine 2 variables in their math operations, just x and y will be sufficient to handle this.
In FORTH, numbers are automatically put on the stack, but in SIMPL, they always go into x, unless stored to y using the ! operator. A neat idea would be to use the space character (instead of !) to inform the interpreter that another number is expected immediately, and so that x should be stored to y, so that x is free to accept the second number.
This would make the math operations a whole lot more readable - just by separating the variables with a single space.
123 456+p would add 123 and 456 and print it out.
As well as the math operators, the logical operators would also benefit from this increased flexible notation. The space operator implies - push the top of the stack down to make room for another parameter - allowing constructs such as
123 456+ 23-p so the interim sum of 123+456 is pushed down by the 2nd space, allowing room for the 23 and the subtraction operation. This makes it a lot more FORTH like and very much improved readability for numerical operations involving two variables.
Rule: A space following a number implies that the number is automatically copied from x (top) to the y (next) variable.
More Operators
& Bitwise AND
| Bitwise OR
^ Bitwise XOR
~ NOT (invert)
$ String operator
% modulo operator
(...) Conditionally execute this block of code
[...] Use x to select an option from the list contained within the square brackets
£ Invoke SIMPL interpreter
#
, separate a list of integers
. Pop x and print it (FORTH compatibility)
? Print out the current set of definitions
\
"
'
: Start of colon definition
; End of colon definition
_ Text string identifier
1 comment:
Hello Ken.
I had no idea that you were so involved in pushing ones and zeroes around until I clicked on this link.
Subject: The arduino ignition controller for the Lister engine; have you done any recent work on it?
I am not a programmer but I have done some Arduino stuff, most of it related to woodgas datalogging with lots of learning from the GEK.
Now I am attempting to make an electronic ignition controller for an old ATV called a Cushman Trackster. I am attempting to wade through the comprehensive ECU code produced by Josh Stewart (noisymime on github), but it is slow going. I don't have enough fingers to keep track of all the switching back and forth between files. I am writing to ask if you have updated the Lister code to change from a simple delay that is not rpm sensitive to an RPM calculation, which is.
Another subject:
I enjoyed your comments about the PDP-8. We used one in a COM machine (Gould-Beta-Com) back then. We also used Data General Minicomputers, and the Lockheed Mac-16. I remember well the task of having to flip those bit switches, inputting about 25 or so characters just to get the thing to listen to the ASR-33 Teletype.
Pete Stanaitis
Post a Comment