This library provides a Forth/PostScript style shell for Arduino sketches. Shell is a virtual stack machine with a byte token threaded instruction set. The tokens, characters, are chosen so that it is possible to write small scripts directly without a token compiler. As Forth scripts are written in Reverse Polish Notation (RPN), and executed in the given order.
Shell has built-in instruction level trace to aid script debugging and performance tuning. It prints the instruction cycle count, script address, opcode (or full operation name), stack depth and contents. Typical output in the Serial Monitor above.
Shell can be configured to trace full operation names or tokens.
The classical Arduino Blink sketch in the shell script language is
13O{13H1000D13L1000DT}w
And with some extra spacing to make the operations easier to read.
13 O { 13 H 1000 D 13 L 1000 D T } w
And with full instruction names and some formatting:
13 output
{
13 high 1000 delay
13 low 1000 delay
true
} while
A further compressed version (shorter):
13 output
{
1000 13 over over high delay
low delay
true
} while
13O{1000,13ooHDLDT}w
And a slightly faster version:
1000 13 dup output
{
over over high delay
over over low delay
true
} while
1000,13uO{ooHDooLDT}w
And an even shorter version:
13 output
{
13 digitalToggle 1000 delay
true
} while
13O{13X1000DT}w
Scripts can be defined and executed from both Random Access Memory
(SRAM), Electrically Erasable Programmable Read-Only Memory (EEPROM),
and Program Memory (PROGMEM). The supporting data type script_t
and
macro SCRIPT
can be used to define an application dictionary with
program memory based scripts. All named scripts are persistent.
Variables may be written to EEPROM with the operation z
. They are
automatically restored at startup together with defined scripts.
Download and unzip the Arduino-Shell library into your sketchbook libraries directory. Rename from Arduino-Shell-master to Arduino-Shell.
The Shell library and examples should be found in the Arduino IDE File>Examples menu.
Opcode | Parameters | Description | Forth |
---|---|---|---|
, | -- | no operation | |
+ | x y -- x+y | addition | + |
- | x y -- x-y | subtraction | - |
* | x y -- x*y | multiplication | * |
/ | x y -- x/y | division | / |
% | x y -- x%y | modulo | MOD |
# | x y -- x!=y | not equal | |
= | x y -- x==y | equal | = |
< | x y -- x<y | less than | < |
> | x y -- x>y | greater than | > |
~ | x -- ~x | bitwise not | NOT |
& | x y -- x&y | bitwise and | AND |
| | x y -- x|y | bitwise or | OR |
^ | x y -- x^y | bitwise xor | XOR |
@ | addr -- val | read variable | @ |
! | val addr -- | write variable | ! |
. | x -- | print number followed by one space | . |
? | addr -- | print variable | ? |
; | addr block -- | copy block and assign variable (function) | |
\ | x1..xn n -- x1..xn | n > 0: mark stack frame with n-elements | |
\ | x1..xn y1..ym n -- y1..ym | n < 0: remove stack frame with n-elements | |
$ | n -- addr | address of n-element in frame | |
a | -- bytes entries | allocated eeprom | |
b | base -- | number print base | BASE |
c | xn..x1 n -- | drop n stack elements | |
d | x -- | drop | DROP |
e | flag if-block else-block -- | execute block on flag | IF ELSE THEN |
f | addr -- | forget variable | |
g | xn..x1 n -- xn-1..x1 xn | rotate n-elements | ROLL |
h | x y z -- (x*y)/z | scale | */ |
i | flag block -- | execute block if flag is true | IF THEN |
j | xn..x1 -- xn..x1 n | stack depth | DEPTH |
k | -- char | read character from input stream | KEY |
l | low high block( index -- ) -- | execute block from low to high | DO LOOP |
m | -- | write new line to output stream | CR |
n | x -- -x | negate | NEGATE |
o | x y -- x y x | over | OVER |
p | xn..x1 n -- xn..x1 xn | pick | PICK |
q | x -- [x x] or 0 | duplicate if not zero | ?DUP |
r | x y z --- y z x | rotate | ROT |
s | x y -- y x | swap | SWAP |
t | addr -- bool | write variable name to output stream | .NAME |
u | x -- x x | duplicate | DUP |
v | char -- | write character to output stream | EMIT |
w | block( -- flag) -- | execute block while flag is true | BEGIN UNTIL |
x | block -- | execute block | EXECUTE |
y | -- | yield for multi-tasking scheduler | |
z | addr -- | write variable to eeprom memory | |
A | pin -- sample | analogRead(pin) | |
C | xn..x1 -- | clear | ABORT |
D | ms -- | delay | |
E | period addr -- bool | check if timer variable has expired | |
F | -- false | false | FALSE |
H | pin -- | digitalWrite(pin, HIGH) | |
I | pin -- | pinMode(pin, INPUT) | |
K | -- [char true] or false | non-blocking read character from input stream | ?KEY |
L | pin -- | digitalWrite(pin, LOW) | |
M | -- ms | millis() | |
N | -- | no operation | |
O | pin -- | pinMode(pin, OUTPUT) | |
P | value pin -- | analogWrite(pin, value) | |
R | pin -- bool | digitalRead(pin) | |
S | -- | print stack contents | .S |
T | -- true | true | TRUE |
U | pin -- | pinMode(pin, INPUT_PULLUP) | |
W | value pin -- | digitalWrite(pin, value) | |
X | pin -- | digitalToggle(pin) | |
Y | -- | list dictionaries | WORDS |
Z | -- | toggle trace mode |
The shell script language allows several special forms such as literal values, blocks, control structures and argument frames.
Boolean values are true(-1) and false(0). Mapping to boolean may be done with testing non-zero (0#). Defining true(-1) allows Boolean values and relation operations to be directly combined with bitwise operations.
10 0 # . m
T F ~ = . m
The instructions to push the boolean values are T and F.
Integer numbers (decimal, binary and hexadecimal) may be used directly in scripts. When the script is executed the value of the number is pushed on the parameter stack. The statement:
println((3 + (-5)) * 6);
may be written as the following script expression:
3 -5 + 6 * . m
and compressed to:
3,-5+6*.m
Binary literal numbers are prefixed with 0b
, and hexadecimal with
0x
as in C.
10 . 0b10 . 0x10 .
Quote (apostrophe) a character to push it on the parameter stack.
'A .
Variables are defined with :name
. The operator will push the
address of the variable on the parameter stack. It may be accessed
using the operators fetch @
and store !
.
42:x!
:x@
The operator ?
can be used to print the value of a variable.
:x?
It is short hand for:
:x@.
Code blocks have the following form { code-block }
. When the script
is executed the address of the block is pushed on the parameter
stack. The block can be executed with the instruction x.
{ code-block } x
The operation ;
will copy a block to the eeprom and assign a
variable. Used in the form:
:fun { code-block };
:fun@x
The short hand for executing a function is `name
.
`fun
The instruction f may be used to forget variable(s). The eeprom allocation point is reset accordingly.
:fun,f
Control structures follow the same format at PostScript. They are also in Reverse Polish Notation (RPN). The blocks are pushed on the parameter stack before the control structure instruction. Below are the control structures with full instruction names.
bool { if-block } if
bool { if-block } { else-block } ifelse
low high { index loop-block } loop
{ while-block bool } while
The loop-block
will recieve the current index on top of parameter
stack. The while-block
should push a non-zero value to continue the
while loop otherwise zero (false) to terminate the loop.
The instructions are i for if
, e for ifelse
, l for loop
and w for while
.
Output strings have the following form ( output-string )
. When executed the
string within the parenthesis is written to the output stream. The
instruction m will print a new-line (corresponds to forth cr).
A stack marker has the following form [ code-block ]
. When executed the
number of stack elements generated by the code block is pushed on the
parameter stack.
A frame marker has the following form n\ ... -n\
where n is the
number of elements (parameters and locals) in the frame. Positive n
marks the frame and negative n removes the frame stack elements
leaving any return values. Elements within the frame can be accessed
with m$
where m is the element index (1..n). The element address
is pushed on the parameter stack and the value may be accessed with
fetch @
and store !
.
Swap could be defined as:
:swap {2\2$@1$@-2\};
which will mark a frame with two arguments, copy the second and then the first argument, and last remove the frame, leaving the two return values.
Shell allows application extension with a virtual member function,
trap()
. The function is called when the current instruction could not
be handled. The trap()
function may parse any number of
instructions. Underscore _
is used as the escape operation code.
Turn board LED, pin 13, on/off with 1000 ms period.
13 output
{
13 high 1000 delay
13 low 1000 delay
true
} while
Script:
13O{13H1000D13L1000DT}w
Toggle board LED, pin 13, on/off with 1000 ms period.
13 output
{
13 digitalToggle 1000 delay
true
} while
Script:
13O{13X1000DT}w
Turn board LED, pin 13, on/off without using delay. Use timer expired instruction.
13 output
{
1000 `timer ?expired
{ 13 digitalToggle } if
true
} while
Script:
13O{1000:timer,E{13X}iT}w
Turn board LED, pin 13, on/off with 1000 ms period if pin 2 is low.
2 inputPullup
13 output
{
2 digitalRead not
{
13 high 1000 delay
13 low 1000 delay
} if
true
} while
Script:
2U13O{2R~{13H1000D13L1000D}iT}w
Read analog pins and print value in format "An = value".
0 4 { ." A" dup . ." =" analogRead . cr } loop
Script:
0,4{(A)u.(= )A.m}l
Read analog pins (0..4) and print value continuously with 100 ms delay.
{
0 4 { analogRead . } loop
cr 100 delay true
} while
Script:
{0,4{A.}lm100DT}w
Read analog pin 0, turn board LED on if value is within 100..200 else off.
13 output
{
0 analogRead
dup 100 < swap 200 > or not
13 digitalWrite
true
} while
Script:
13O{0Au100<s200>|~13WT}w
Check that a given parameter is within a range low to high.
: within ( x low high -- bool )
rot swap over swap > swap rot < or not ;
10 5 100 within .
-10 5 100 within .
110 5 100 within .
Script:
:within{rsos>sr<|~};
10,5,100`within.
-10,5,100`within.
110,5,100`within.
Check that a given parameter is within a range low to high. Use a stack frame for the three parameters.
: within { x low high -- bool }
x @ high @ > x @ low @ < or not ;
10 5 100 within .
-10 5 100 within .
110 5 100 within .
Script:
:within{3\1$@3$@>1$@2$@<|~-3\};
10,5,100`within.
-10,5,100`within.
110,5,100`within.
Calculate factorial number of given parameter.
: fac ( n -- n! )
1 swap
{
dup 0>
{ swap over * swap 1- true }
{ drop false }
ifelse
} while ;
5 fac .
Script:
:fac{1s{u0>{so*s1-T}{dF}e}w};
5`fac.
Calculate factorial number of given parameter.
: fac ( n -- n! | n > 0 )
1 2 rot { * } loop ;
5 fac .
Script:
:fac{1,2r{*}l};
5`fac.
Calculate factorial number of given parameter.
: fac ( n -- n! )
dup 0>
{ dup 1- fac * }
{ drop 1 }
ifelse ;
5 fac .
Script:
:fac{u0>{u1-`fac*}{d1}e};
5`fac.
Sum a vector of integers on stack. Use that stack marker to get number of elements in vector.
[ 1 2 3 4 5 6 7 8 ] 0 1 rot { drop + } loop
Script:
[1,2,3,4,5,6,7,8]0,1r{d+}l