fpga4fun.comwhere FPGAs are fun

Spoc programmer's model

Spoc has a small instruction set, and a few addressing modes.
That makes Spoc programmer's model easy to learn.
Instruction set

Spoc currently supports 8 instructions:

FunctionNumber of
DO Executes specified operation (+,-,or,and,xor,not)2
SELSelects a register1
NOTLogical not (inverse all the bits)1
JMPJumps to a new memory location1
JSRJumps to a subroutine1
RETReturns from subroutine0


 inc RA2      // increments register RA2
 dec A       // decrements accumulator
 sel WA10      // selects WA10
 do #0xBA00 + WA22 -> A    // adds value 0xBA00 to register WA22, and writes the result to the accumulator
 do A and #0x5555 -> @     // logical AND between accumulator and value goes to memory
 do A xnor #0x5555 -> RA0     // logical XNOR between accumulator and value goes to register RA0, selects RA0
 not RA1      // inverts all the bits in register RA1, selects RA1
 jsr A+RA10      // calculates A+RA10, and jumps to this address (subroutine, returns with RET)
 jmp #loop      // jmp to label "loop"
Addressing modes

Spoc supports 3 addressing modes:

Addressing modeFunctionSymbol
ImmediateReads a constant from code memory#
DirectAccesses a registerWAxx or RAxx
IndirectAccesses a memory location@


 do #1234 -> A    // Immediate addressing: moves value 1234 to accumulator
 do A -> RA4    // Direct addressing: writes accumulator value to register RA4
 do @ -> A    // Indirect addressing: reads memory to accumulator
 do A -> @    // Indirect addressing: writes accumulator to memory

It is possible to use indirect addressing for both source and destination operands.

 do @ -> @
 do @ + #0x22 -> @
 do A or @ -> @

Indirect addressing is related to "selected registers" (see below "Memory and register files" paragraph).
DO instruction
The DO instruction is the most powerful, as it can execute an operation using up to 2 sources and write the result to up to 2 destinations.

To write to 2 destinations, separate them by a comma.
 do #22 -> A, RA0
 do A + #22 -> A, WA1
 do A - @ -> A, WA1
 do A or RA3 -> WA4, A
 do @ and #22 -> A, @
 do WA6 xor #22 -> A, @
When writing to 2 destinations, one is always the accumulator, the other is either a register (RAxx/WAxx), or the memory (@).
Accumulators and data sizes
Each spoc instruction specifies a data size, from a list of possible sizes.
Spoc0 features 4 valid data sizes: 1, 8, 16 and 32 bits. The default is 16 bits (when no data size is specified).
The data sizes are specified by postfixing the instructions (.bit, .byte, .word and .dw).

Spoc has also an accumulator for each valid data size. With Spoc0, that gives us 4 accumulators.
Accumulators are independent (writing to one doesn't affect the others).


 do.bit #1 -> A      // writes 1 to the 1-bit accumulator
 inc.byte A      // increments the 8-bits accumulator
 do.word A + #0x1000-> A      // adds 0x1000 to the 16-bits accumulator
 do.dw #0x12DECF80 -> A      // writes 0x12DECF80 to the 32-bits accumulator
The instruction JMP is used to branch to a new program location.
The instruction JSR is used to branch to a new program location, from which you eventually return using the instruction RET.

These instructions can be execution conditionally (with the help of the C and Z flags, see next paragraph).


 jmp #gothere      // jumps unconditionally
 jsr #gotosubroutine    // jumps unconditionally to subroutine

 jmp.Z=0 #gothere    // jumps conditionally (if flag Z is 0)
The branching instructions can use calculated addresses to branch. For example, it is possible to have tables of subroutines.

Note: the current registers are de-selected after any branching instruction (see later for a discussion on "selected" registers).
Before using JSR instructions, make sure you initialize the SP (stack pointer) register.


  do #0x0C00 -> SP      // stack from 0x0C00 to 0x0FFF, enough for a depth of 64 subroutine calls

  jsr #mysubroutine
The stack is used to store the subroutines return addresses. The stack uses memory and grows upwards (in Spoc0, the SP pointer increments by 16 for each JSR instruction, and decrements by 16 for each RET).
Flags and conditional execution
The Flags are used to conditionally execute other instructions.
Spoc0 uses 2 flags: The Z flag is 0 if the result of the previous operation is 0.
The Z flag is 1 if the result of the previous operation is not 0 (note: many CPUs take the opposite convention...)

The Flags are set by all the executed instructions.


 do #3 -> A
 dec A   // A becomes 0x0002, C is 0, Z is 1
 dec A   // A becomes 0x0001, C is 0, Z is 1
 dec A   // A becomes 0x0000, C is 0, Z is 0
 dec A   // A becomes 0xFFFF, C is 1, Z is 1
 dec A   // A becomes 0xFFFE, C is 0, Z is 1
 dec A   // A becomes 0xFFFD, C is 0, Z is 1
You can execute a DO operation that has no destination. In that case, the operation is executed, the result is lost, but the flags are still updated.


 do #3 -> WA0     // writes 3 to register WA0

 // now we are going to do 3 subtractions, without saving the results. But the flags are still updated. 
 do WA0-#2    // WA0>2, so we get C=0, Z=1
 do WA0-#3    // WA0=3, so we get C=0, Z=0
 do WA0-#4    // WA0<4, so we get C=1, Z=1

 // now run some conditional instructions
 jmp.c=0 #mylabel1     // conditional jump, not executed since C=1
 add.z=0 WA0 + A -> RA2       // conditional addition, not executed since Z=1
 jmp.z=1 #mylabel2     // conditional jump, executed since Z=1
Finally, if a conditional instruction is not executed, the flags are not updated either.


 do #1 -> A    // A is 0x0001, C is 0, Z is 1
 dec.z=0 A      // not executed, and flags not changed
Carry flag as operand
The carry flag can also be used in the source operand (in the source operand, the carry flag can be named "C" or "CY" or "CARRY").


 do CY -> A
 do A + #22 + C -> A
 do A xor CARRY -> RA0

The carry flag value is not signed-extended for arithmetic operation, but it is signed-extended for logical operation (in other words, the carry value is 0 or 1 for arithmetic operations, or 0 and 0xFFFF for logical operations).


 do #0 -> A
 dec A        // A=0xFFFF, C=1
 do CY + #22 -> A       // arithmetic operation, so A=23

 do #0 -> A
 dec A        // A=0xFFFF, C=1
 do CY xor #0x1111 -> A       // logical operation, so A=0xEEEE
Memory and register files
The symbol "@" is used to indicate a memory access.
It is used in conjunction with registers named "RAxx" and "WAxx".

Example of memory read (read from address 0x200):

 do #0x0200 -> RA0
 do @ -> A      // reads memory 0x200, and puts the value in accumulator

Example of memory write (write to address 0x200):

 do #0x0200 -> WA17
 do RA3 -> @       // writes content of RA3 to memory 0x200
The address of a read memory access is given by a "RAxx" register.
The address of a write memory access is given by a "WAxx" register.

The registers are part of a file of registers, so you have many of these registers to work with.
Spoc0 has 32 RA registers, named RA0 to RA31, and 32 WA registers, named WA0 to WA31.

One register of each file is "selected" at a given time of execution. To select a register, you can either use the "sel" instruction, or write to the register.

The value of the selected register is used as memory address when an instruction does a memory access.
During each memory access, the registers are auto-incremented.


 do #0x0200 -> RA5    // writes 0x200 to RA5, and selects it
 do #0x0300 -> WA7    // writes 0x300 to WA7, and selects it

 // RA5 and WA7 are both selected
 do @ -> @    // copies the value from memory location 0x200 to memory location 0x300

 // now RA5=0x210 and WA7=0x310
 do WA7 + #0x20 -> RA6     // RA6 is now selected with the value 0x330
 do @ -> A       // memory location 0x330 is read and copied to the accumulator

 sel RA5    // re-select RA5. Since it was non-persistent, its value is back to 0x0200 (see later for explanation on persistent registers)
The registers RAxx/WAxx can also be used for other purpose than memory accesses. They have almost the same capabilities than the accumulators, so can be incremented, added, xor-ed...
They can also be used for subroutine parameter passing.
Memory spaces
With Spoc0, each register WAxx and RAxx is 16 bits wide (.word), so that it can address 64K.

Spoc uses separate data and code spaces. The data space is "bit-addressable". So you can access any part of it without any alignment constraint. For example, you can write a 32-bits value to address 0xABCD, and then write another at address 0xABC3, and finally read from address 0xABC7 (which reads part of both values).

The code space is normally not accessible directly. It is used to hold the instructions to execute. But there is a hook available to be able to access the code space anyway, so that you can store tables, data, strings...
Spoc0 uses register CS to access the code space.


  do #GreetingString -> CS      // CS points to a string, and selects CS
  do.byte @ -> A     // read the "H" into the accumulator

  data.byte "Hello world!", 13
Another reserved value is "$". It represents the actual PC (program code location, the location of the currently executed instruction). That's a read-only value.


  jmp $     // forever loop (if you want spoc to "die")
The code space is bit-addressable when you use a blockram (like in Spoc0), or byte-addressable if you use a serial Flash. That gives 64Kbits space for Spoc0, and 64Kbytes space when serial flash is used.
Reserved spaces
Spoc0 uses one blockram (4Kbits) for data space, which fills addresses 0x0000 to 0x0FFF.
Above that, the space 0x1000 to 0xFFFF is the "external memory". It should be used to interface external peripherals.

Also, the first 1Kbits of the blockram data space (addresses 0x0000 to 0x03FF) is reserved to hold the values of the registers RAxx/WAxx. You can still use it, if you know what you're doing...

Reserved registers
The registers WA30/31 and RA30/31 are reserved. Don't use them.

For example, in Spoc0, register WA31 is used as the PC... so if you were to use WA31, you would actually jump somewhere...
Persistent registers
When a register is de-selected, and then re-selected at a later time, its value can either be "persistent" (it recovers the value after the last auto-increment), or "not-persistent" (it recovers the value affected to it originally, i.e. before any memory access/auto-increment).

You choose if you want a register to be persistent at the time you select it (by using or not the ".p" postfix).


 do #0x0200 -> RA5    // RA5 is selected (not persistent)
 do @ -> A    // copies the value from memory location 0x200 to accumulator
                  // RA5 value is now 0x210

 do #0x0300 -> RA6.p    // RA6 is selected (persistent)
 do @ -> A    // copies the value from memory location 0x300 to accumulator
                  // RA6 value is now 0x310

 sel RA5.p    // re-selects RA5. Since it was non-persistent, its value is back to 0x0200
                   // note that it is now persistent!

 do @ -> A    // copies the value from memory location 0x200 to accumulator
                  // RA5 value is now 0x210

 sel RA6    // re-selects RA6. Since it was persistent, its value is 0x0310
 do @ -> A    // copies the value from memory location 0x310 to accumulator
                  // RA6 value is now 0x320

 sel RA5    // re-selects RA5 (not persistent). But since it was persistent, its value is 0x0210

 do @ -> A    // copies the value from memory location 0x210 to accumulator
                  // RA5 value is now 0x220