Documentation‎ > ‎Tutorials‎ > ‎

Using spin2cpp to Convert Spin Files

This tutorial is based on spin2cpp version 1.04 and propeller-elf-g++ version propellergcc_v0_3_5_1996 4.6.1. Earlier versions of both programs will likely work, but is not guaranteed.


Introduction

spin2cpp is a command line tool to convert .spin files for the Parallax Propeller to C++ or C. spin2cpp has two modes of operation: as a Spin compiler, and as a source code conversion tool. In either case, spin2cpp will convert the given spin file and all it's dependencies.

Using spin2cpp as a Spin compiler allows Spin (which is normally interpreted) to be compiled into PASM code instead, for a much higher execution speed (at the cost of a larger code footprint).

spin2cpp was originally designed for use with the propeller-elf-g++ C++ compiler, but the most recent versions feature limited support for plain C output which can be compiled with Catalina or propeller-elf-gcc. In addition, there is preliminary support for Propeller 2 code.

Generally you will want to use PropGCC's CMM, LMM or XMMC memory models with the output, although a small Spin object might be able to fit into one COG with the -mcog memory model.

spin2cpp help message

For your information:
Spin to C++ converter version 1.04
Usage: spin2cpp [options] file.spin
Options:
  --binary:  create binary file for download
  --ccode:   output (static) C code instead of C++
  --dat:     output binary blob of DAT section only
  --elf:     create executable ELF file
  --files:   print list of .cpp files to stdout
  --gas:     create inline assembly out of DAT area;
             with --dat, create gas .S file from DAT area
  --main:    include C++ main() function
  --nopre:   do not run preprocessor on the .spin file
  --p2:      define the target to be the Propeller 2
  -Dname=val: define a preprocessor symbol


Using spin2cpp as a compiler

Using spin2cpp as a compiler allows you to take a spin program and compile it to run as assembly, rather than interpreted bytecodes. Compiling has a number of benefits and drawbacks:
1. The compiled code will run faster than the Spin code (typically 5x or more).
2. The compiled code will be larger than the Spin code (typically 3x or more).
3. You can run code from an external memory (not an option for standard Spin programs).

The simplest way to compile a program is with the --elf option. For example, to compile the Spin program test.spin and all of its sub-objects, just do:
user@desktop:~$ spin2cpp --elf -Os -o test.elf test.spin
user@desktop:~$ propeller-load -r -t test.elf
This will compile test.spin into an executable file called a.out, optimizing for size (-Os). Then, using propeller-load, we can load this "compiled spin" file onto the Propeller.

In order for this to work, propeller-elf-gcc must be in your PATH (i.e. the directory containing propeller-elf-gcc.exe must be in the PATH environment variable). You can test this with the following command: 

Found propeller-elf-gcc:
user@desktop:~$ propeller-elf-gcc
propeller-elf-gcc: fatal error: no input files
compilation terminated.

Did not find propeller-elf-gcc:
user@desktop2:~$ propeller-elf-gcc
-bash: propeller-elf-gcc: command not found
Your output may vary, but should be similar.

Optimization is always a good idea (GCC produces poor code without it). However, the -Os optimization for size, which is usually the preferred one for PropellerGCC, may sometimes not work well. This is because some Spin objects share variables between COGs, and the GCC optimizer doesn't know about this and may optimize away references that it really needs. Try -Os first, and if it doesn't work use -O1.

To get more fine grained control over how spin2cpp compiles your program, see the section below on Spin file annotation.

The following C options are passed through to the GCC compiler from the spin2cpp command line:
  • anything starting with -O ( for optimization, such as -Os, -O0, or -O3)
  • anything starting with -m ( for memory model selection, such as -mcmm or -mlmm)
  • anything starting with -f (for machine independent flags, link)
  • -o outputname (for specifying the elf filename)
  • -D define (for specifying CPP preprocessor #defines)


Using spin2cpp as a conversion tool


Sometimes you may want to convert the Spin file to C or C++, for example to use it in another project. C++ output is the default, so you don't have to do anything special for that. Just:
spin2cpp hello.spin
will produce hello.cpp and hello.h. To generate plain C code, use --ccode to produce hello.c.

spin2cpp also has a number of options for converting the Propeller assembly (PASM) to GNU assembler syntax (GAS) that PropellerGCC can assemble itself or to a binary blob.

Binary Blob:
  • Assembly code is unchanged and will almost certainly run without modification
  • Nothing special needs to be done with the blob to allow linker garbage collection on the project.
  • Constants are compiled into the blob
  • To make any changes, you will have to re-run spin2cpp on the original source file with the changes.
GAS:
  • Very similar syntax to PASM, with the addition of some PropllerGCC psuedo-commands and assembler directives
  • Allows you to make changes to your PASM code, without having to re-run spin2cpp
  • It's slightly more difficult to allow for linker garbage collection with GAS sections.

To make a binary blob for

If you specify --gas on the command line while converting to C or C++, instead of a "dat" array spin2cpp will output inline GAS assembly. This makes the resulting C/C++ somewhat closer to readable, since the PASM part is now inline assembly. If you specify --gas --dat then the DAT section is output as a GAS .S file. You must link in this .S file with program yourself. This requires a bit more effort, including modifying any makefiles that your project may use.

For ordinary use we would *not* recommend using --gas, since the assembler built in to spin2cpp actually works quite well.


Spin File Annotation

spin2cpp recognizes a special annotation comment that allows for you to provide information about the code, without affect the operation of the Spin program. These annotation comments take the form of:

{++TEXT}

where text is replaced with a keyword or code block. While you are not required to annotate your Spin files this way, it allows spin2cpp to generate better code. We recommend always annotating your source files before conversion.

To mark a variable as volatile you use the {++volatile} annotation:
VAR
  long {++volatile} Results
You should mark {++volatile} any variables that are shared between cogs. If you are unsure, you can mark all variables but be aware that this will negatively impact the optimization options that PropellerGCC has.

You can also provide the C/C++ version of a function:
{ indicate to spin2cpp that it should not output Spin methods }
{++!nospin}

{ the Spin version of the methods }
PUB Square(x)
  return x*x

{++
//
// and here are the C versions of the methods
//
int32_t test95::Square(int32_t x)
{
  return x*x;
}
}


spin2cpp pretty much passes through anything inside a {++...} comment. You can also embed C functions by adding a {++ ... } block at the end of the Spin file.

To mark an entire DAT section to go into the hub (relevant for running programs in XMM mode):
DAT {++HUBDATA}
   myvariable long 0


Example Conversion

For this example, we will be using the source code that ships on the Parallax Quickstart board. You can download a copy of the original code here.
Parallax Quickstart board
To start, we have to remove all the spaces from the filenames. This gives us the following two files:

Touch_Buttons.spin
CON
  BUTTON_PINS   = $FF           ' The QuickStart's touch buttons are on the eight LSBs
  SAMPLES       = 32            ' Require 32 high redings to return true

VAR
  long  Results

PUB Start(Rate)
  Results := Rate
  cognew(@Entry, @Results)      ' Launch a new cog to read samples

PUB State | Accumulator
  Accumulator := Results        ' Sample multiple times and return true
  repeat constant(SAMPLES - 1)  '  if every sample was highw
    Accumulator &= Results
  return Accumulator

DAT
                        org
Entry                       
              rdlong    WaitTime, par
              mov       outa, #BUTTON_PINS              ' set TestPins high, but keep as inputs

              mov       Wait, cnt                       ' preset the counter
              add       Wait, WaitTime
Loop
              or        dira, #BUTTON_PINS              ' set TestPins as outputs (high)
              andn      dira, #BUTTON_PINS              ' set TestPins as inputs (floating)
              mov       Reading, #BUTTON_PINS           ' create a mask of applicable pins
              waitcnt   Wait, WaitTime                  ' wait for the voltage to decay
              andn      Reading, ina                    ' clear decayed pins from the mask
              wrlong    Reading, par                    ' write the result to RAM
              jmp       #Loop

Reading       res       1
WaitTime      res       1
Wait          res       1

Touch_Buttons_LED_Demo_v1.0.spin
CON
  _CLKMODE = XTAL1 + PLL16X
  _CLKFREQ = 80_000_000

OBJ
  Buttons          : "Touch_Buttons"

PUB Main
  Buttons.start(_CLKFREQ / 100)                         ' Launch the touch buttons driver sampling 100 times a second
  dira[23..16]~~                                        ' Set the LEDs as outputs
  repeat
    outa[23..16] := Buttons.State                       ' Light the LEDs when touching the corresponding buttons 

Next, we can convert and download the files:
user@desktop:~$ spin2cpp --elf -Os -o quickstart.elf Touch_Buttons_LED_Demo_v1.0.spin
user@desktop:~$ propeller-load -r quickstart.elf 
Propeller Version 1 on /dev/ttyUSB0
Loading quickstart.elf to hub memory
2240 bytes sent                  
Verifying RAM ... OK

Once we play around with the hardware, we can see that this compiled spin code executes exactly the same before.

Next, let's convert the Spin source code to C++ source code:
user@desktop:~$ spin2cpp --gas --main Touch_Buttons_LED_Demo_v1.0.spin
user@desktop:~$ propeller-elf-g++ -Os -o quickstart.elf *.cpp
user@desktop:~$ propeller-load -r quickstart.elf 
Propeller Version 1 on /dev/ttyUSB0
Loading quickstart.elf to hub memory
2252 bytes sent                  
Verifying RAM ... OK

Notice that this conversion passed --gas and --main to spin2cpp, and ultimately produced a file 12 bytes larger.

Next, let's take a look at the generated source code:

Touch_Buttons.h
#ifndef Touch_Buttons_Class_Defined__
#define Touch_Buttons_Class_Defined__

#include <stdint.h>

class Touch_Buttons {
public:
  static const int Button_pins = 255;
  static const int Samples = 32;
  int32_t	Start(int32_t Rate);
  int32_t	State(void);
private:
  int32_t	Results;
};

#endif

Touch_Buttons.cpp
#include <propeller.h>
#include "Touch_Buttons.h"

#ifdef __GNUC__
#define INLINE__ static inline
#define PostEffect__(X, Y) __extension__({ int32_t tmp__ = (X); (X) = (Y); tmp__; })
#else
#define INLINE__ static
static int32_t tmp__;
#define PostEffect__(X, Y) (tmp__ = (X), (X) = (Y), tmp__)
#endif

extern uint8_t _load_start_Touch_Buttons_cog[];
__asm__(
"		.section .Touch_Buttons.cog, \"ax\"\n"
"		.equ	Button_pins, $ff\n"
"		.equ	Samples, $20\n"
"		.compress off\n"
"..start\n"
"		.org	0\n"
"Entry\n"
"		rdlong	Waittime, PAR\n"
"		mov	OUTA, #Button_pins\n"
"		mov	Wait, CNT\n"
"		add	Wait, Waittime\n"
"Loop\n"
"		or	DIRA, #Button_pins\n"
"		andn	DIRA, #Button_pins\n"
"		mov	Reading, #Button_pins\n"
"		waitcnt	Wait, Waittime\n"
"		andn	Reading, INA\n"
"		wrlong	Reading, PAR\n"
"		jmp	#Loop\n"
"Reading\n"
"		.res	1\n"
"Waittime\n"
"		.res	1\n"
"Wait\n"
"		.res	1\n"
"    .compress default\n"

);
int32_t Touch_Buttons::Start(int32_t Rate)
{
  Results = Rate;
  cognew((int32_t)(&(*(int32_t *)&_load_start_Touch_Buttons_cog[0])), (int32_t)(&Results));
  return 0;
}

int32_t Touch_Buttons::State(void)
{
  int32_t	Accumulator;
  Accumulator = Results;
  {
    int32_t _idx__0000;
    for(_idx__0000 = 1; _idx__0000 <= 31; (_idx__0000 = (_idx__0000 + 1))) {
      Accumulator = (Accumulator & Results);
    }
  }
  return Accumulator;
}

Touch_Buttons_LED_Demo_v1.0.h
#ifndef Touch_Buttons_LED_Demo_v1_0_Class_Defined__
#define Touch_Buttons_LED_Demo_v1_0_Class_Defined__

#include <stdint.h>
#include "Touch_Buttons.h"

class Touch_Buttons_LED_Demo_v1_0 {
public:
  static const int _Clkmode = 1032;
  static const int _Clkfreq = 80000000;
  Touch_Buttons	Buttons;
  int32_t	Main(void);
private:
};

#endif

Touch_Buttons_LED_Demo_v1.0.cpp
#include <propeller.h>
#include "Touch_Buttons_LED_Demo_v1.0.h"

#ifdef __GNUC__
#define INLINE__ static inline
#define PostEffect__(X, Y) __extension__({ int32_t tmp__ = (X); (X) = (Y); tmp__; })
#else
#define INLINE__ static
static int32_t tmp__;
#define PostEffect__(X, Y) (tmp__ = (X), (X) = (Y), tmp__)
#endif

int32_t Touch_Buttons_LED_Demo_v1_0::Main(void)
{
  Buttons.Start((_Clkfreq / 100));
  DIRA = ((DIRA & 0xff00ffff) | 0xff0000);
  while (1) {
    OUTA = ((OUTA & 0xff00ffff) | ((Buttons.State() & 0xff) << 16));
  }
  return 0;
}


Touch_Buttons_LED_Demo_v1_0 MainObj__;

int main() {
  return MainObj__.Main();
}


Have you noticed the latent bug in our program? Yes, it is a missing volatile. We need to add {++ volatile} to the spin source file for the Results variable. That looks like this:
Spin:
VAR
  long {++ volatile} Results

And generates
C++:
private:
  int32_t	 volatile Results;

Once we download and run the program we see that it now takes 2284 bytes. As it turns out, in this case we don't need the volatile declaration: the program compiles and runs just fine. However, that is a feature of this particular program with this particular version of PropellerGCC. It is undefined behavior, and should be avoided.

Generated Code Notes

DAT section merges
Spin2cpp will merge all the DAT sections in the spin file into a since composite uint8_t byte array named "dat". It then accesses specific "variables" in the DAT section by using the byte offset of the variable. For example: The spin code:
Dat
  Power
  LeftPower     word      0

Pub Left(Value)
  LeftPower~                                            ' Clear left motor power value

Dat {++ volatile}
              org
Entry
              mov       dira, OutputMask        ' set outputs
              mov       ctra, CTRA_Setting
              mov       ctrb, CTRB_Setting
              mov       frqa, #1                ' frq is added to phs every clock cycle
              mov       frqb, #1                ' so it takes -phs clockcycles to generate a falling edge
... (more assembly code) ...

becomes 
uint8_t  volatile  volatile eddie_motor_driver::dat[] = {
  0x00, 0x00, 0x00, 0x00, 0x13, 0xec, 0xbf, 0xa0, 0x14, 0xf0, 0xbf, 0xa0, 0x15, 0xf2, 0xbf, 0xa0, 
  0x01, 0xf4, 0xff, 0xa0, 0x01, 0xf6, 0xff, 0xa0, 0xf1, 0x35, 0xbc, 0xa0, 0x11, 0x34, 0xbc, 0x80, 
  0xf0, 0x37, 0xbc, 0xa0, 0x1b, 0x32, 0xbc, 0x04, 0x02, 0x36, 0xfc, 0x80, 0x1b, 0x30, 0xbc, 0x04, 
  0x12, 0x30, 0xbc, 0x4c, 0x12, 0x32, 0xbc, 0x4c, 0x11, 0x34, 0xbc, 0xf8, 0x18, 0xf8, 0xbf, 0xa4, 
  0x19, 0xfa, 0xbf, 0xa4, 0x07, 0x00, 0x7c, 0x5c, 0x40, 0x1f, 0x00, 0x00, 0x60, 0x1d, 0x00, 0x00, 
  0x00, 0x00, 0xf8, 0x01, 0x14, 0x00, 0x00, 0x10, 0x17, 0x00, 0x00, 0x10, 0xb0, 0x0e, 0x00, 0x00, 
  0x58, 0x07, 0x00, 0x00, 
};

int32_t eddie_motor_driver::Left(int32_t Value)
{
  ((uint16_t *)&dat[2])[0] = 0;
    ...

You can get around this by generating assembly code (--GAS), getting a reference to the labels in the code, then modifying those labels.



Spin source that uses "abort"
Some spin code will use the abort keyword to unwind the stack with an error code. This works fine in Spin: it simply skips back until it finds a function call prefaced by a \ (back slash), and continues execution from there. Spin2cpp does the best that it can and replicates that operation by using a jump/GOTO type statement. This works fine for exact emulations of the spin code, but ignores the standard C++ destructor contract: destructors are not called.

You should remove (and fix) all calls to abort in your Spin code before converting if you plan on modifying the generated C++ code.

spin2cpp will generate the following for all spin files that use abort:
extern "C" {
#include <setjmp.h>
}
typedef struct { jmp_buf jmp; int32_t val; } AbortHook__;
AbortHook__ *abortChain__ __attribute__((common));


stdint.h usage (int32_t and friends)
"int32_t" is the C99 standard way of specifying a 32 bit integer. spin2cpp uses the stdint.h sizes for it's variables to reduce the chance of bugs creeping in. Although, on the current Propeller an int is 32 bits, a char is 8 bits, and so on, this may not necessarily hold for all future uses of the code. The explicit sizing of variables, while syntactically ugly, does guarantee that no matter what platform you run the code on it will always remain a set number of bits. 

loops with "Yield()"
Empty repeat loops are often used to wait for hardware or another COG to do something. The GCC optimizer can be pretty aggressive about moving and deleting code, so spin2cpp puts the call to Yield() in to prevent the compiler from moving anything past the loop. The __asm__ volatile with "memory" as a clobber indicates that any memory might change during the loop, and again is to keep the optimizer from making assumptions. Yield() won't actually add any code, it's just a note to the compiler that the outside world may be doing things that we don't know about. In pure C/C++ code you'd normally mark variables as volatile for this, but spin2cpp isn't able to do enough analysis to tell which variables might be volatile.

If you manually edit the generated C/C++ files after conversion, you can remove the Yield() if the variable that the loop waits on is volatile.

The PostEffect define
This define is needed for post effect operations that Spin has, but C++ does not. For example,
x := y~~ + 1
tells the Spin compiler that y be added to 1 and the result assigned to x, and the y variable is set to all bits 1 ($FFFFFFFF). C/C++ does not have an equivlent operator, so spin2cpp generates something like
 X = (PostEffect__(Y, -1)) + 1;

Cleaning up the Automatically Generated Output
If your code (in the current compilation unit) does not use any of the boilerplate code that spin2cpp produces (such as Yield() or PostEffect()) then you can safely remove it. If you take out too much the compiler will be sure to let you know.

You can also safely rename variables. The easiest way to do this is by using an the "rename refactor" command in an IDE such as Eclipse or Netbeans. If you choose to do it manually, be sure to change every instance of the name. If you forget any, the compiler will likely let you know.

You must be careful about changing constants. Currently, spin2cpp does not make a complete and clean conversion of constants. For example (from the Quickstart touch LED code):

Spin:
CON
  BUTTON_PINS   = $FF           ' The QuickStart's touch buttons are on the eight LSBs
  SAMPLES       = 32            ' Require 32 high redings to return true

                ...
DAT
                ...
              or        dira, #BUTTON_PINS              ' set TestPins as outputs (high)
              andn      dira, #BUTTON_PINS              ' set TestPins as inputs (floating)
              mov       Reading, #BUTTON_PINS           ' create a mask of applicable pins
                ...

C++:
extern uint8_t _load_start_Touch_Buttons_cog[];
__asm__(
"		.section .Touch_Buttons.cog, \"ax\"\n"
"		.equ	Button_pins, $ff\n"
"		.equ	Samples, $20\n"
     ...
"		or	DIRA, #Button_pins\n"
"		andn	DIRA, #Button_pins\n"
"		mov	Reading, #Button_pins\n"
     ...
);


class Touch_Buttons {
public:
  static const int Button_pins = 255;
  static const int Samples = 32;
     ...
}

Notice that, in the C++ version, the code the constants in two places: a GAS .equ directive, and a static const int. If you change one and not the other then it is likely that your code will not work. This problem applies even when you compile your code to a "pre-assembled" DAT section.

Another fairly safe change is changing the stdint.h variables to the normal int, char, and so on. Just make sure to match the size and type. Commonly used mappings are:
 stdint.h        traditional 
 int32_t int 
 uint32_t  unsigned int 
 int16_t short 
 uint16_t unsigned short 
 int8_t char
 uint8_t unsigned char 


Known Bugs

Spin2cpp does not handle cognew of Spin methods, only ASM blocks. The solution for now is to make a launcher method and use cogstart on that method:
myObjSpin MainObj__;

void Launcher(void * arg){
	MainObj__.InfiniteLoop();	
}


main(){
...
// cognew(InfiniteLoop(), (int32_t)(&SpinStack));
   int stacksize = sizeof(_thread_state_t)+sizeof(int)*3;
   int *stack = (int*) malloc(stacksize);
   cogstart(Launcher, NULL, stack, stacksize);
...
}


Propeller 2 Support

The adventurous will note that version 1.03 and higher has a -p2 flag for Propeller2 support, thanks to work contributed by David Betz (thanks!). This is still quite incomplete -- the spin2cpp assembler doesn't yet have all the P2 opcodes, and there are a lot of missing features, not to mention that the hardware differences between P1 and P2 mean that many Spin programs would need changes to work on P2.

If the P1 Spin code uses any register accesses then it'll probably need changing. So in practice the -p2 flag is useful only for higher level functions; low level drivers will probably need to be ported by hand.



Comments