Flexspin was designed to accept the language Spin as documented in the Parallax Propeller Manual. It should be able to compile most Spin programs, as long as there is space. The restriction is an important one; other Spin compilers produce Spin bytecode, a compact form that is interpreted by a program in the Propeller's ROM. Flexspin produces LMM code, which is basically a slightly modified Propeller machine code (slightly modified to run in HUB memory instead of COG). This is much larger than Spin bytecode, but also much, much faster.
Flexspin is able to produce binaries for both the P1 and P2 chips. Any assembly language written in DAT sections (or inside inline ASM blocks) must be for the appropriate chip; it will not be translated.
Flexspin also supports many of the features of the Spin2 language as extensions to Spin 1. It can also accept Spin2 programs as input. Spin1 and Spin2 are not completely compatible. Spin2 features which are not compatible with Spin1 are enabled if the file extension is
flexspin has a pre-processor that understands basic directives like
#ifdef / #ifndef / #else / #endif.
#define FOO hello
Defines a new macro
FOO with the value
hello. Whenever the symbol
FOO appears in the text, the preprocessor will substitute
Note that unlike the C preprocessor, this one cannot accept arguments in macros. Only simple defines are permitted.
If no value is given, e.g.
then the symbol is defined as the string
Introduces a conditional compilation section, which is only compiled if
the symbol after the
#ifdef is in fact defined. For example:
#ifdef __P2__ '' propeller 2 code goes here #else '' propeller 1 code goes here #endif
Introduces a conditional compilation section, which is only compiled if
the symbol after the
#ifndef is not defined. For example:
#ifndef __P2__ #error this code only works on Propeller 2 #endif
Switches the meaning of conditional compilation.
A combination of
A combination of
Prints an error message. Mainly used in conditional compilation to report an unhandled condition. Everything after the
#error directive is printed.
Includes a file.
Prints a warning message.
Removes the definition of a symbol, e.g. to undefine
There are several predefined symbols:
||always defined to 1 (for P1) or 2 (for P2)|
||if compiling for Propeller 2|
||if compiling for Propeller 2|
||if --asm is given (PASM output) (always defined by flexspin)|
||if C++ or C is being output (never in flexspin)|
||if C++ is being output (never in flexspin)|
||a string containing the date when compilation was begun|
||a string giving the current file being compiled|
||the current source line number|
||a string containing the time when compilation was begun|
||a string containing the full version of flexspin in use|
A predefined symbol is also generated for type of output being created:
||if PASM code is being generated|
||if bytecode is being generated|
||if C code is being generated|
||if C++ code is being generated|
There are some built in functions for doing memory allocation. These are intended for C or BASIC, but may be used by Spin programs as well.
The main function is
_gc_alloc_managed(siz), which allocates
siz bytes of memory managed by the garbage collector. It returns 0 if not enough memory is avilable, otherwise returns a pointer to the start of the memory (like C's
malloc). As long as there is some reference in COG or HUB memory to the pointer which got returned, the memory will be considered "in use". If there is no more such reference then the garbage collector will feel free to reclaim it. There's also
_gc_alloc(siz) which is similar but marks the memory so it will never be reclaimed, and
_gc_free(ptr) which explicitly frees a pointer previously allocated by
The size of the heap is determined by a constant
HEAPSIZE declared in the top level object. If none is given then a (small) default value is used.
' put this CON in the top level object to specify how much memory should be provided for ' memory allocation (the "heap"). The default is 4K on P2, 256 bytes on P1 CON HEAPSIZE = 32768 ' or however much memory you want to provide for the allocator ' here's a function to allocate memory ' "siz" is the size in bytes PUB allocmem(size) : ptr ptr := _gc_alloc_managed(size)
The garbage collection functions and heap are only included in programs which explicitly ask for them.
Temporary memory may be allocated on the stack by means of the call
__builtin_alloca(siz), which allocates
siz bytes of memory on the stack. This is like the C
alloca function. Note that the pointer returned by
__builtin_alloca will become invalid as soon as the current function returns, so it should not be placed in any global variable (and definitely should not be returned from the function!)
Interoperation with C and BASIC
C and BASIC files may be included as objects in Spin1 and Spin2 programs. To do this, be sure to include the entire file name (including any extension, like
.bas) in the
Note that C is a case sensitive language, so you must use the exact same name that C uses: if
VGA is a constant defined in a C file
video.c then it must be used always as all upper case, not as
vga. For example:
OBJ screen: "video.c" ... x := screen.VGA ' this is fine x := screen.Vga ' this is an error, the case does not match
Calling C standard library functions
A simple way to include the C standard library is to declare an object using
libc.a. C standard library functions may then be accessed as methods of that object. For example, to call
sprintf you could do:
OBJ c: "libc.a" ... c.sprintf(@buf, string("the value is %x", 10), val)
Extensions to Spin
Flexspin has a number of extensions to the Spin languages (both Spin1 and Spin2). Many Spin2 features may be used in Spin1, and vice-versa. For example in Spin2 functions with no arguments must be called like
foo(), whereas flexspin still accepts the Spin1 version
@@@ operator returns the absolute hub address of a variable. This is the same as
@ in Spin code, but in PASM code
@ returns only the address relative to the start of the
Access to member variables
Flexspin allows (read only) access to member variables and constants using the
. notation. That is, if
S is an object like:
CON rows = 24 VAR long x, y
then one may write
S.x to access member variable
S.rows to access the constant
rows. The original Spin syntax
S#rows is still accepted for accessing constants.
Modifying member variables of another object directly is not permitted, and will produce a syntax error.
Alternate string notation
Flexspin interprets the notation
@"some chars" to mean the same thing as
STRING("some chars"). This provides a shorter way to write some messages.
n of a variable
x may be accessed via the notation
m is the same as
n this may be simplified to
x.[n]. This notation may also be applied to hardware registers.
Boolean short-circuit operators
flexspin has operators
x __andthen__ y and
x __orelse__ y which only evaluate the right hand side (
y) if they need to (like C's
||). Ordinary boolean
OR operators will be optimized to these in the cases where
y do not have side effects.
CASE_FAST is just like
CASE, except that each of the case items must be a constant expression. It is guaranteed to compile to a jump table (regular
CASE may sometimes compile to a sequence of
cognew) functions in Flexspin can start functions from other objects than the current. (In "regular" Spin only functions from the same object may be started this way.)
IF...THEN...ELSE expressions; you can use IF/THEN/ELSE in an expression, like:
r := if a then b else c
which is the same as
if a then r := b else r := c
This may also be written in C / Verilog style as:
r := (a) ? b : c
In the latter form the parentheses around
a are mandatory in Spin1 to avoid confusion with the random number operator
?. In Spin2 the question mark is no longer used for random numbers, so the issue does not arise there.
Default function parameters
flexspin permits function parameters to be given default values by adding
= X after the parameter declaration, where
X is a constant expression. For instance:
VAR long a PUB inc(n=1) a += n PUB main inc(2) ' adds 2 to a inc(1) ' adds 1 to a inc ' same as inc(1)
The default values must, for now, be constant. Perhaps in the future this restriction will be relaxed, but there are some slightly tricky issues involving variable scope that must be resolved first.
Default string parameters
If a default function parameter is declared as a string, and a string literal is passed to it, that string literal is transformed into a string constant. Normally Spin uses just the first character of a string literal when one is seen in an expression (outside of STRING). Basically flexspin inserts a
string operator around the literal in this case. So for example in:
PUB write(msg = string("")) '' do some stuff ... write(string("hello, world")) write("hello, world")
the two calls to
write will do the same thing. In regular Spin, and in flexspin in the case where the default value is not present on a parameter, the second call will actually be interpreted as:
write($68, $65, ..., 0) ' $68 = ASCII value of "h"
which is probably not what was intended.
A function may have an "alias" created for it. That is, if you want to be able to call the same function by two different names
_add, you can do:
PUB _add(x, y) return x+y PUB add = _add
PUB add = _add line says that
add is an alias for
Aliases defined this way are "weak"; that is, they may be overridden by later definitions. They are mostly intended for use in libraries where for some reason (e.g. C standard compatibility) we wish to allow the program to use the same name as a library function without a conflict occuring.
Inline assembly with asm/end
flexspin accepts inline assembly in
PRI sections. Inline assembly starts with
asm and ends with
endasm. The inline assembly is somewhat limited; the only operands permitted are immediate values, hardware registers like
OUTA, local variables (including parameters and result values) of the containing function, or labels of that function. (Spin doesn't support goto and labels, but you can define labels in
asm blocks and jump to them from other
asm blocks that are in the same function. Some other languages supported by flexspin do have labels.) Member variables (declared in the
VAR block) may not be used directly in inline assembly.
Branching inside the function should work, but trying to return from it or to otherwise jump outside the function will almost certainly cause you to come to grief, even if the compiler allows it. Calling subroutines is also not permitted.
All non-branch instructions should work properly in inline assembly, as long as the operands satisfy the constraints above. Conditional execution is allowed, and flag bits may be set and tested.
If you need temporary variables inside some inline assembly, declare them as locals in the enclosing function.
PUB waitcnt2(newcnt, incr) asm waitcnt newcnt, incr endasm return newcnt
waits until CNT reaches "newcnt", and returns "newcnt + incr".
Note that unlike most Spin blocks, the
asm block has to end with
endasm. This is because indentation is not significant inside the assembly code. For example, labels typically start at the leftmost margin.
Spin2 style inline assembly (org/end)
flexspin also accepts Spin2 style inline assembly, marked with
end instead of
endasm. So the above example could be written as:
PUB waitcnt2(newcnt, incr) org waitcnt newcnt, incr end return newcnt
There are two important differences between
(1) Normally inline assembly is treated the same as code generated by the compiler, and subject to optimization. However, code between
end is not optimized. So if your inline assembly is timing sensitive, you should use
org rather than
asm to start it.
(2) Code between
end is executed from the FCACHE area (in COG memory). Ordinary
asm is hubexec.
Differences from PropTool/PNut
Unlike the "official" Spin2 compiler, flexspin does not accept an address for the inline assembly ("org $xxx"), and the assembly is not called as a subroutine (so no
ret statement should be included). There are also some important restrictions in FlexSpin's implementation:
call instructions may not be used in inline assembly
ptra register must not be modified by the inline assembly.
(3) The area set aside for inline assembly is smaller by default in FlexSpin than in Parallax's Spin2. This may be changed with the --fcache parameter to flexspin.
Pointers to methods may be created with
@ and called using the normal calling syntax. For example:
VAR LONG funcptr PUB twice(x) : r r := x + x ... funcptr := @twice y := funcptr(a)
If no parameters are to be passed to the called function, it is still necessary to write
() after it in order to force it to be interpreted as a call. That is,
a = f()
causes an indirect call of the function pointed to by
a = f
copies the pointer in
For functions returning multiple results, a
:N notation is required after the function call, where
N is an integer giving the expected number of results:
x,y := fptr(a):2
:2 indicates that fptr is a pointer to a function which returns 2 results. Putting
:1 for functions which return 0 or 1 results is optional in flexspin.
It is the programmer's responsibility to make sure that the number of results and arguments passed to a method called via a pointer are correct. No type checking is done.
Multiple return values and assignments
flexspin allows multiple return values and assignments. For example, to swap two variables
b you can write:
a,b := b,a
It's also acceptable (and perhaps easier to read) if there are parentheses around some or all of the multiple values:
(a,b) := (b,a)
A function can also return multiple values. For instance, a function to calculate both a quotient and remainder could be written as:
PUB divrem(x,y) return x/y, x//y
PUB divrem(x,y) : q, r q := x/y r := x//y
This could later be used like:
(a,digit) := divrem(a, 10)
It is also allowed to pass the multiple values returned from one function to another. So for example:
' function to double a 64 bit number PUB dbl64(ahi, alo): bhi, blo bhi := ahi blo := alo asm add blo, blo wc addx bhi, bhi endasm ' function to quadruple a 64 bit number PUB quad64(ahi, alo) return dbl64(dbl64(ahi, alo))
The proposed Spin2 syntax for abstract object definitions and object pointers is accepted. A declaration like:
OBJ fds = "FullDuplexSerial"
fds as having the methods of a FullDuplexSerial object, but without any actual variable or storage being instantiated. Symbols declared this way may be used to cast parameters to an object type, for example:
PUB print(f, c) fds[f].dec(c) PUB doprint22 print(@aFullDuplexSerialObj, 22)
PUB FILE and PRI FILE
pri function declaration may include a
file directive which gives the file which contains the actual definition of the function. This looks like:
pub file "utils.spin" myfunc(x, y)
This declares a function
myfunc with two parameters, which will be loaded from the file "utils.spin". The function will be a public function of the object. This provides an easy way to import the same function (e.g. a decimal conversion routine) into many different objects.
pub file and
pri file differ from the
obj directive in that they do not create a new object; the functions defined in the new file are part of the current object.
Note that there is no need for a body to the function (it is an error to give one). The number of parameters and return values, however, should be specified; they must match the number given in the final definition contained in the file.
The function body need not be in Spin. For example, to use the C
atoi function in a Spin object on a Propeller1, you could do:
obj ser: "spin/FullDuplexSerial.spin" pub file "libc/stdlib/atoi.c" atoi(str) pub test ser.start(31, 30, 0, 115_200) x := string("1234") ser.dec(atoi(x))
(For Propeller2 you would have to modify this to use "spin/SmartSerial" and to change the output pins appropriately.)
Beware that functions declared with
file are treated the same as other functions; in particular, note that the first function in the top level object will be used as the starting point for the program, even if that function was declared with
pub file or
pri file. So unlike in C, the declaration of external functions should be placed at the end of the file rather than the beginning (unless for some reason you want the main program to come from another file).
Typed parameters and return values
The "expression" in a default parameter may also be a type name, for example
float, a flag indicating an unsigned type
+long, (or one of the pointer types
@long (pointer to long),
@word (pointer to word),
@float. These do not provide a default value, but do provide a hint to the compiler about what type of value is expected. This isn't terribly useful for Spin, but does make it possible for the compiler to check types and/or convert them if necessary for Spin functions called from C or BASIC.
Variables declared as return values may also be given type hints, which will be used to determine the type of the function when it is accessed from another language.
' negate a floating point value PUB negfloat(x = float) : r = float r := x ^ $80000000
Typed local variables
As in Spin2, local variables may be declared prefixed with
long to give them a specific size.
PUB dostuff(x) : r | byte tempstring ' may use tempstring here as an array
flexspin has some new operators for treating values as unsigned
a +/ b is the unsigned quotient of a and b (treating both as unsigned) a +// b is the unsigned remainder of a and b a +** b gives the upper 32 bits of unsigned multiplication a +< b is an unsigned version of < a +> b is an unsigned version of > a +=< b is an unsigned version of =< a +=> b is an unsigned version of =>
Most of these are also in Spin2.
Additional Spin2 operators
flexspin accepts some other Spin2 operators:
a \ b uses the value of a, but then sets a to b x <=> y returns -1, 0, or 1 if x < y, x == y, or x > y
flexspin implements some new operators:
a +| b is the same as a ZEROX b in Spin2 a -| b is the same as a SIGNX b in Spin2
Array parameters (deprecated)
flexspin allows method parameters to be small arrays; in this case, the caller must supply one argument for each element of the array. For example:
pub selector(n, a) return a[n] pub tryit return selector(n, 1, 2, 3, 4)
This particular example could be achieved via
lookup, but there are other cases where it might be convenient to bundle parameters together in an array.
This feature is still incomplete, and may not work properly for C/C++ output.
New PASM directives
flexspin accepts some Spin2 assembly directives, even in Spin1 mode.
Forces the COG PC to reach a certain value by inserting 0 if necessary. For example,
orgf $100 will insert 0 bytes until the COG program counter reaches $100.
orgf X is basically similar to:
long 0[X - $]
ORGF is valid only in COG space.
Specifies that labels and code after this must be in HUB memory.
orgh ' following code must be in HUB orgh $400 ' following code must be in HUB at a specific address
If an address is given, then the code must not have exceeded that address yet, and 0 bytes will be inserted to force the HUB memory to get up to the given address.
Note that labels normally have two values, their COG memory address (specified by the last ORG) and their hub memory address (specified implicitly by how they are placed in RAM). After
orgh the COG memory address is no longer valid, and the hub memory address may be explicitly given if the
orgh had a value.
New intrinsics for both P1 and P2
Flexspin supports some new builtin functions. These typically start with an underscore to avoid confusion with existing variable names. Note that in Spin2 mode many of these are available without the leading underscore, and in fact it's better to use the non-underscore versions since those are also supported in the official Parallax compiler.
_pinw(p, c) forces
p to be an output and sets it to 0 if
c is 0 or 1 if
c is 1. If c is any other value the result is undefined. Supported for both P1 and P2.
_pinl(p) drives pin
p low, i.e. it forces
p to be an output and sets it to 0. This is supported for both P1 and P2.
_pinh(p) drives pin
p high, i.e. it forces
p to be an output and sets it to 1. This is supported for both P1 and P2.
p to be an output and inverts it. This is supported for both P1 and P2.
_waitx(n) waits for
n cycles, plus the cycle time required for the instruction. This is 2 cycles on P2, and 8 cycles on P1.
In Spin2 mode all of the above are available without the underscore.
Compatibility with other Spin 1 compilers
Most Spin language features are supported. There may be some features that do not work; if you find any, please report them so they can be fixed.
The lexer and parser are different from the Parallax ones, so they may well report errors on code the Parallax compiler accepts.
Timing of produced code is different, of course (in general much faster than the native Spin interpreter). This may affect some objects; sometimes developers left out delay loops in time critical code because the Spin interpreter is so slow they weren't necessary. Watch out for this when porting I2C, SPI and similar functions.
@@ operator always truncates its result to 16 bits; flexspin does not do this. This won't matter in typical use (on the P1 addresses always fit in 16 bits anyway) but may be noticeable for some exotic uses.
The Spin1 CHIPVER command always evaluates as 1, regardless of the actual chip version stored at $FFFF.
Local variables may shadow method names
In FlexSpin local variables may have the same name as methods in the object. In the original Spin and Spin2 compilers this will produce an error. The FlexSpin way is more "traditional" (matches how most compilers/languages work).
Special characters in identifiers
Special characters like
$ may be included in Spin function and variable names
by preceding them with a backquote, e.g. to define a function
pub chr`$(x) return x
In regular Spin opcodes like
TEST are always reserved.
In flexspin, opcodes are only reserved inside
DAT sections, so it is legal to have a function or variable named
flexspin adds some reserved words:
then. Programs which use these reserved words may not work correctly in flexspin, although there has been some effort made to make them work as regular identifiers in many contexts.
Literal strings like
"hello" are treated as lists of characters in regular Spin, unless enclosed in a
STRING declaraction. So for example:
is parsed as
whereas in flexspin it is treated as an array of characters, so it is parsed like:
which will be the same as
The difference is rarely noticeable, because flexspin does convert string literals to lists in many places.
As an extension, flexspin allows you to write:
The parser has the wrong precedence for
@, so a DAT section declaration like:
will produce a single value of
@foo+12, rather than 3 values of
As a workaround, enclose the value in parentheses:
Type narrowing in assignment
A series of assignments like:
a := b := 512
is evaluated by flexspin as
b := 512 a := b
This is sometimes different from how the official Spin1 interpreter does it. In particular, if
b is a byte or word then the result of
b:=512 is truncated to 8 or 16 bits, whereas in the official interpreter the full 32 bit result is assigned to
a. Thus, if
b is a byte then flexspin sets
0 whereas Spin1 sets
Second argument to ORG is ignored
In Spin the
org pseudo-instruction has an optional second parameter giving a limit to the size of the assembly code (like an implied
fit at the end). Flexspin ignores this second parameter.
Spin1 on P2
Many Spin1 programs may be ported from the Propeller 1 to the Propeller 2, but there are some important exceptions:
PASM must be translated to P2ASM. The assembly language for the Propeller 1 and Propeller 2 are different; there are some similarities, but in general any assembly language code will have to be translated to work on the different processor.
WAITPEQ, WAITPNE, and WAITVID are not implemented on P2
The hardware register set is different; the P2 does not have the CTRx, FRQx, PHSx, VCFG, or VSCL registers.
Compatibility with Spin2
flexspin is mostly, but not completely, compatible with the standard Spin2 compiler. Not all Spin2 builtin functions are available on the P1; only the ones listed in the "New intrinsics for both P1 and P2" are available on all platforms. But when compiling for P2 all of the Spin2 builtin functions should be available.
: character is used in multiple conflicting ways in Spin2:
a ? b : 2 x := b():1 x := lookup(b : 1, 2)
The flexspin parser works differently from the PNut parser, and in complicated nested expressions with multiple
: it may not give the same results. Some tips for writing code that will work with both compilers:
- In general try not to nest expressions involving
- Do not use
lookdowntype expressions except as the separator between the selection expression and the list of results
- Make liberal use of parentheses to resolve ambiguity, e.g. write
a ? (b()) : 1or
a ? (b():1) : 2to distinguish between
:1as an indirect method marker nad
:1as the "else" part of the
@ operator always gives an absolute address in flexspin's Spin2 dialect, even inside assembly code. This is different from the standard Spin2 interpreter, where it produces an address relative to the start of the current object. In most contexts the flexspin behavior is more convenient, but it is something to keep in mind.
If you really need a relative offset, declare a label like
entry at the start of your assembly and use
@label - @entry to find the offset of
entry. This will work in all compilers.
No address may be given in an ORG/END pair. If no FCACHE is available (e.g. -O0 is given) then the code is run as hubexec, in which case no self-modifying code or local data is permitted.
The space available for use in ORG/END inline assembly is smaller by default in flexspin (128 longs) than in PNut / Proptool (304 longs). You can change the flexspin value with the
--fcache= flag, e.g.
--fcache=304, but beware that this may cause the COG memory to overflow if the program uses a lot of local variables or pointers.
The location of the clock frequency is at the standard location $10 used by TAQOZ, micropython, and most C compilers, rather than $40 as used by Spin2.
COG memory is also laid out differently. See the general compiler documentation for details of the memory map.
DEBUG statements are accepted only in Spin2 methods, not in PASM (they are ignored in PASM).
Debug statements are output only when the
-g flag is given to flexspin.
Only a subset of the Spin2
DEBUG directives are accepted:
ZSTR, UDEC, UDEC_BYTE, UDEC_WORD, UDEC_LONG, SDEC, SDEC_BYTE, SDEC_WORD, SDEC_LONG, UHEX, UHEX_BYTE, UHEX_WORD, UHEX_LONG
Other debug directives are ignored, with a warning.
DEBUG in flexspin is implemented differently than in PNut, so timing when debug is enabled may be different.
DEBUG statements containing backticks are (partially) translated so as to output the correct strings, but FlexProp does not have any way to interpret these strings so no graphical debug capabiliites exist.
ASMCLK pseudo-instruction is supported as a preprocessor macro in FlexSpin, so only the most common spellings like
asmclk will work (e.g.
aSmClk will not work).
REGEXEC Spin2 instructions are not supported at this time, mainly because they depend on a particular layout of memory in the Spin2 interpreter.