9-bit Opcode, 8-bit Data, Stack-Based Micro Controller
Copyright 2012, Sinclair R.F., Inc.

This document describes the 9-bit opcode, 8-bit data, stack-based microcontroller.

Contents

Opcodes
Macros

Directory Contents

This directory contains the assembler and the Verilog and VHDL templates for the processor. While the assember can be run by itself, it is more typically run within the "../../ssbcc" script as part of making a compute computer core.

The "core.v" and "core.vhd" files in this directory are not complete modules. They cannot be compiled.

Introduction

This processor is a minimalist FPGA-based microcontroller. It provides 8-bit data manipulation operations and function calls. There are no condition registers as the results of tests are on the data stack. The instruction space, data stack size, return stack size, and existence and sizes of RAMs and ROMs are controlled by a configuration file so that the processor can be sized for the problem at hand. The configuration file also describes the input and output ports and include 0  to 8 bit port widths and strobes. A complete processor can be implemented using just the configuration file and the assembly file.

A 9-bit opcode was chosen because (1) it works well with Altera, Lattice, and Xilinx SRAM widths and (2) it allows pushing 8-bit data onto the stack with a single instruction.

An 8-bit data width was chosen because that's a practical minimal size. It is also common to many hardware interfaces such as I2C devices.

The machine has single-cycle instruction execution for all instructions. However, some operations, such as a jump, require pushing the 8 lsb of the target address onto the stack in one instruction, executing the jump with the remaining 5 msb of the target address, and then a nop during the following instruction.

Only one data stack shift can be achieved per instruction cycle. This means that all operations that consume the top two elements of the stack, such as a store instruction, can only remove one element from the stack and must be immediately followed by a "drop" instruction to remove what had been the second element on the data stack.

This architecture accommodates up to a 13-bit instruction address space by first pushing the 8 least significant bits of the destination address onto the stack and then encoding the 5 most significant bits in the jump instruction. This is the largest practicable instruction address width because (1) one bit of the opcode is required to indicate pushing an 8-bit value onto the stack, (2) one bit of the remaining 8 bits is required to indicate a jump instructions, (3) one bit is required to indicate whether the jump is always performed or is conditionally performed, and (4) one more bit is required to indicate whether the jump is a jump or a call. This consumes 4 bits and leaves 5 bits of additional address space for the jump instruction.

This architecture also supports 0 to 4 pages of RAM and ROM with a combined limit of 4 pages. Each page of RAM or ROM can hold up to 256 bytes of data. This architecture provides up to 1 kB of RAM and ROM for the micro controller.

Design

The processor is a mix of instructions that operate on the top element of the data stack (such as left or right shifts); operations that combine the top two elements of the data stack such as addition and subtraction and bit wise logical operations; operations that manipulate the data stack such as drop, nip, and 8-bit pushes; operations that move data between the data stack and the return stack; jumps and calls and their conditional variants; memory operations; and I/O operations.

This section describes these. The next section defines these instructions in detail.

OPCODES

This section documents the opcodes.

Alphabetic listing: &, +, -, -1<>, -1=, 0<>, 0=, 0>>, 1+>, 1->, 1>>, <<0, <<1, <<msb, >r, FE=, FF=, ^, call, callc, dis, drop, dup, ena, fetch, fetch+, fetch-, inport, jump, jumpc, lsb>>, msb>>, nip, nop, or, outport, over, push, r>, r@, return, store, store+, store-, swap

Opcode Mapping

Opcode    876543   210    Description
nop 000000000 no operation
<<0 000000001 left shift 1 bit and bring in a 0
<<1 000000010 left shift 1 bit and bring in a 1
<<msb 000000011 left shift 1 bit and rotate the msb into the lsb
0>> 000000100 right shift 1 bit and bring in a 0
1>> 000000101 right shift 1 bit and bring in a 1
msb>> 000000110 right shift 1 bit and keep the msb the same
lsb>> 000000111 right shift 1 bit and rotate the lsb into the msb
dup 000001000 push a duplicate of the top of the data stack onto the data stack
r@ 000001001 push a duplicate of the top of the return stack onto the data stack
over 000001010 push a duplicate of the next-to-top of the data stack onto the data stack
swap 000010010 swap the top and the next-to-top of the data stack
+ 000011000 pop the stack and replace the top with N+T
- 000011100 pop the stack and replace the top with N-T
dis 000011000 disable interrupts
ena 000011001 enable interrupts
0= 000100000 replace the top of the stack with "0xFF" if it is "0x00" (i.e., it is zero), otherwise replace it with "0x00"
0<> 000100001 replace the top of the stack with "0xFF" if it is not "0x00" (i.e., it is non-zero), otherwise replace it with "0x00"
-1= 000100010 replace the top of the stack with "0xFF" if it is "0xFF" (i.e., it is all ones), otherwise replace it with "0x00"
-1<> 000100011 replace the top of the stack with "0xFF" if it is not "0xFF" (i.e., it is not all ones), otherwise replace it with "0x00"
return 000101000 return from a function call
inport 000110000 replace the top of the stack with the contents of the specified input port
outport 000111000 write the next-to-top of the data stack to the output port specified by the top of the data stack
>r 001000000 Pop the top of the data stack and push it onto the return stack
r> 001001001 Pop the top of the return stack and push it onto the data stack
& 001010000 pop the stack and replace the top with N & T
or 001010001 pop the stack and replace the top with N | T
^ 001010010 pop the stack and replace the top with N ^ T
nip 001010011 pop the next-to-top from the data stack
drop 001010100 drop the top value from the stack
1+ 001011000 Add 1 to T
1- 001011100 Subtract 1 from T
store 0011000bb Store N in the T'th entry in bank "bb", drop the top of the data stack
fetch 0011010bb Exchange the top of the stack with the T'th value from bank "bb"
store+ 0011100bb Store N in the T'th entry in bank "bb", nip the data stack, and increment T
store- 0011101bb Store N in the T'th entry in bank "bb", nip the data stack, and decrement T
fetch+ 0011110bb Push the T'th entry from bank "bb" into the data stack as N and increment T
fetch- 0011111bb Push the T'th entry from bank "bb" into the data stack as N and decrement T
jump 0100xxxxx Jump to the address "x_xxxx_TTTT_TTTT"
jumpc 0101xxxxx Conditionally jump to the address "x_xxxx_TTTT_TTTT"
call 0110xxxxx Call the function at address "x_xxxx_TTTT_TTTT"
callc 0111xxxxx Conditionally call the function at address "x_xxxx_TTTT_TTTT"
push 1xxxxxxxx Push the 8-bit value "xxxx_xxxx" onto the data stack.

Instruction: &

Desription: Pop the data stack and replace the top with the bitwise and of the previous top and next-to-top.

Operation:

PC ← PC+1
R and return unchanged
T ← T & N
N ← stack--

Instruction: +

Desription: Pop the data stack and replace the top with the 8 sum of the previous top and next-to-top.

Operation:

PC ← PC+1
R and return unchanged
T ← N + T
N ← stack--

Instruction: -

Desription: Pop the data stack and replace the top with the 8 difference of the previous top and next-to-top.

Operation:

PC ← PC+1
R and return unchanged
T ← N - T
N ← stack--

Instruction: -1<>

Desription: Set the top of the stack to all ones if the previous value was not all ones, otherwise set it to all zeros.

Operation:

PC ← PC+1
R and return unchanged
T ← 0xFF if T!=0xFF, 0x00 otherwise
N and stack unchanged

Instruction: -1=

Desription: Set the top of the stack to all ones if the previous value was all ones, otherwise set it to all zeros.

Operation:

PC ← PC+1
R and return unchanged
T ← 0xFF if T=0xFF, 0x00 otherwise
N and stack unchanged

Instruction: 0<>

Desription: Set the top of the stack to all ones if the previous value was not all zeros, otherwise set it to all zeros.

Operation:

PC ← PC+1
R and return unchanged
T ← 0xFF if T!=0x00, 0x00 otherwise
N and stack unchanged

Instruction: 0=

Desription: Set the top of the stack to all ones if the previous value was all zeros, otherwise set it to all zeros.

Operation:

PC ← PC+1
R and return unchanged
T ← 0xFF if T=0x00, 0x00 otherwise
N and stack unchanged

Instruction: 0>>

Desription: Right shift the top of the stack one bit, replacing the left-most bit with a zero.

Operation:

PC ← PC+1
R and return unchanged
T ← { 0, T[7], T[6], ..., T[1] }
N and stack unchanged

Instruction: 1+

Desription: Add 1 to T.

Operation:

PC ← PC+1
R and return unchanged
T ← T+1
N and stack unchanged

Instruction: 1-

Desription: Subtract 1 from T.

Operation:

PC ← PC+1
R and return unchanged
T ← T-1
N and stack unchanged

Instruction: 1>>

Desription: Right shift the top of the stack one bit, replacing the left-most bit with a zero.

Operation:

PC ← PC+1
R and return unchanged
T ← { 1, T[7], T[6], ..., T[1] }
N and stack unchanged

Instruction: <<0

Desription: Left shift the top of the stack one bit, replacing the right-most bit with a zero. Operation:

PC ← PC+1
R and return unchanged
T ← { T[6], T[5], ..., T[0], 0 }
N and stack unchanged

Instruction: <<1

Desription: Left shift the top of the stack one bit, replacing the right-most bit with a one.
Operation:

PC ← PC+1
R and return unchanged
T ← { T[6], T[5], ..., T[0], 1 }
N and stack unchanged

Instruction: <<msb

Desription: Left shift the top of the stack one bit, leaving the right-most bit unchanged.

Operation:

PC ← PC+1
R and return unchanged
T ← { T[6], T[5], ..., T[0], T[7] }
N and stack unchanged

Instruction: >r

Desription: Pop the data stack and push its previous value onto the return stack.

Operation:

PC ← PC+1
R ← T
++return ← R
T ← N
N ← stack--

Instruction: ^

Desription: Pop the data stack and replace the top with the bitwise exclusive or of the previous top and next-to-top.

Operation:

PC ← PC+1
R and return unchanged
T ← T ^ N
N ← stack--

Instruction: call

Desription: Call the function at the address constructed from the opcode and T. Discard T and push the PC onto the return stack.

Operation:

PC ← { O[4], ..., O[0], T[7], T[6], ..., T[0] }
R ← PC+1
++return ← R
T ← N
N ← stack--

Special:

Interrupts are disabled during the clock cycle immediately following a call instruction.

The assembler normally places a "nop" instruction immediately after the "call" instruction.

Instruction: callc

Desription: Conditionally call the function at the address constructed from the opcode and T. Discard T and conditionally push the next PC onto the return stack.

Operation:

if N != 0 then
  PC ← { O[4], ..., O[0], T[7], T[6], ..., T[0] }
  R ← PC
  ++return ← R
else
  PC ← PC+1
  R and return unchanged
endif
T ← N
N ← stack--

Special:

Interrupts are disabled during the clock cycle immediately following a callc instruction.

The assembler normally places a "drop" instruction immediately after the "callc" instruction.

Instruction: dis

Desription: Disable interrupts.

Operation:

PC ← PC+1
R, return, T, N, and stack unchanged

Instruction: drop

Desription: Pop the data stack, discarding the value that had been on the top.

Operation:

PC ← PC+1
R and return unchanged
T ← N
N ← stack--

Instruction: dup

Desription: Push the top of the data stack onto the data stack.

Operation:

PC ← PC+1
R and return unchanged
T ← T
N ← T
++stack ← N

Instruction: ena

Desription: Enable interrupts.

Operation:

PC ← PC+1
R, return, T, N, and stack unchanged

Instruction: fetch

Desription: Replace the top of the data stack with an 8 bit value from memory. The memory bank is specified by the two least-significant bits of the opcode. The index within the memory bank is specified by the previous value of the top of the stack.

Operation:

PC ← PC+1
R and return unchanged
T ← bb[T] where "bb" is the bank
N and stack unchanged
Special: See memory for instructions on using the fetch and vectorized fetch macros.

Instruction: fetch+

Desription: Push the T'th entry from bank "bb" onto the data stack as N and increment the top of the data stack.

Operation:

PC ← PC+1
R and return unchanged
T ← T+1
N ← bb[T] where "bb" is the bank
++stack
Special: See memory for instructions on using the fetch and vectorized fetch macros.

Instruction: fetch-

Desription: Push the T'th entry from bank "bb" onto the data stack as N and decrement the top of the data stack.

Operation:

PC ← PC+1
R and return unchanged
T ← T-1
N ← bb[T] where "bb" is the bank
++stack
Special: See memory for instructions on using the fetch and vectorized fetch macros.

Instruction: inport

Desription: Replace the top of the data stack with the 8 value from the port specified by the previous value of the top of the data stack.

Operation:

PC ← PC+1
R and return unchanged
T ← input_port[T]
N and stack unchanged

Special:

The recommended procedure to read from an inport port is to use the ".inport" macro.

Instruction: jump

Desription: Jump to the address constructed from the opcode and T. Discard T.

Operation:

PC ← { O[4], ..., O[0], T[7], T[6], ..., T[0] }
R and return unchanged
T ← N
N ← stack--

Special:

Interrupts are disabled during the clock cycle immediately following a jump instruction.

The assembler normally places a "nop" instruction immediately after the "jump" instruction.

Instruction: jumpc

Desription: Jump to the address constructed from the opcode and T if N is non-zero. Discard S and N.

Operation:

if N != 0 then
  PC ← { O[4], ..., O[0], T[7], T[6], ..., T[0] }
else
  PC ← PC+1
end if
R and return unchanged
T ← N
N ← stack--

Special:

Interrupts are disabled during the clock cycle immediately following a jumpc instruction.

The assembler normally places a "drop" instruction immediately after the "jump" instruction so that the conditional is dropped from the data stack.

Instruction: lsb>>

Desription: Right shift the top of the stack one bit, replacing the left-most bit with the previous value of the right-most bit.

Operation:

PC ← PC+1
R and return unchanged
T ← { T[0], T[7], T[6], ..., T[1] }
N and stack unchanged

Instruction: msb>>

Desription: Right shift the top of the stack one bit, preserving the value of the left-most bit.

Operation:

PC ← PC+1
R and return unchanged
T ← { T[7], T[7], T[6], ..., T[1] }
N and stack unchanged

Instruction: nip

Desription: Discard the next-to-top value on the data stack.

Operation:

PC ← PC+1
R and unchanged
T ← T
N ← stack--

Instruction: nop

Desription: No operation.

Operation:

PC &leftarrow PC + 1
R, return, T, N, and stack unchanged

Instruction: or

Desription: Pop the data stack and replace the top with the bitwise or of the previous top and next-to-top.

Operation:

PC ← PC+1
R and return unchanged
T ← T or N
N ← stack--

Instruction: outport

Desription: Pop the data stack and write the previous next-to-top to the port specified by the previous top.
Operation:

PC ← PC+1
R and return unchanged
T ← N
N ← stack--
outport[T] ← N

Special:

This instruction must be following by a "drop" in order to discard the value from the data stack that had been written to the data port. The recommended procedure to write to an output port is to use the ".outport" macro.

Instruction: over

Desription: Push the next-to-top of the data stack onto the data stack.

Operation:

PC ← PC+1
R and return unchanged
T ← N
N ← T
++stack ← N

Instruction: push

Description: Push the specified 8-bit value onto the 8-bit stack.

Operation:

PC ← PC+1
R and return unchanged
T ← opcode[7:0]
N ← T
++stack ← N

Instruction: r>

Desription: Pop the return stack and push its previous value onto the data stack.

Operation:

PC ← PC+1
R ← return--
T ← R
N ← T
++stack ← N

Instruction: r@

Desription: Push the top of the return stack onto the data stack.

Operation:

PC ← PC+1
R and return unchanged
T ← R
N ← T
++stack ← N

Instruction: return

Description: Popd the top of the return stack into the PC.

Operation:

PC ← R
R ← return--
T, N, and stack unchanged

Special: This instruction must be followed by a "nop" instruction.

Instruction: store

Desription: Drop the top of the data stack and store the previous next-to-top of the data stack at the memory location specified by the top of the data stack. The memory bank is specified by the two least significant bits of the opcode.

Operation:

PC ← PC+1
R and return unchanged
T ← N
N ← stack--
bb[T] ← N where "bb" is the bank

Special: See memory for instructions on using the store and vectorized store macros.

Instruction: store+

Desription: Nip the data stack and store the previous next-to-top of the data stack at the memory location specified by the top of the data stack. Increment the top of the data stack The memory bank is specified by the two least significant bits of the opcode.

Operation:

PC ← PC+1
R and return unchanged
T ← T+1
N ← stack--
bb[T] ← N where "bb" is the bank

Special: See memory for instructions on using the store and vectorized store macros.

Instruction: store-

Desription: Nip the data stack and store the previous next-to-top of the data stack at the memory location specified by the top of the data stack. Decrement the top of the data stack The memory bank is specified by the two least significant bits of the opcode.

Operation:

PC ← PC+1
R and return unchanged
T ← T-1
N ← stack--
bb[T] ← N where "bb" is the bank

Special: See memory for instructions on using the store and vectorized store macros.

Instruction: swap

Desription: Swap the top two values on the data stack.

Operation:

PC ← PC+1
R and return unchanged
T ← N
N ← T
stack unchanged

Assembler

This section describes the contents of an assembly language file and the instruction format.

The following is a simple, 10 instruction sequence, demonstrating a loop:

  ; consume 256*6+3 clock cycles
  0 :l00 1 - dup .jumpc(l00) drop .return

This looks a lot like Forth code in that the operations are single words and are strung together on a single line. Unlike traditional assembly languages, there are no source and destination registers, so most of the operations for this stack-based processor simply manipulate the stack. This can make it easier to see the body of the assembly code since an instruction sequence can occupy a single line of the file instead of tens of lines of vertical space. The exceptions to the single-operand format are labels, such as the ":l00" which are declared with a single ":" immediately followed by the name of the label with no intervening spaces; jump instructions such as the 3 instruction, "push jumpc drop", sequence created by the ".jumpc" macro; and the 2 operand, "return nop", sequence created by the ".return" macro. The ".jump", ".jumpc", ".call", and ".callc", macros are pre-defined in the assembler and ensure that the correct sequence of operands is generated for the jump, conditional jump, function call, and conditional function call instructions. Similarly, the ".return" macro is pre-defined in the assember and ensures that the correct sequence of operations is done for returning from a called function.

Memory does not have to be declared for the processor. For example, the LED flashing examples required no variable or constant storage, and therefore do not declare or consume resources required for memory. Variable declarations are done within pages declared using the ".memory" and ".variable" macros as follows:

  .memory RAM myRAM
  .variable save_count
  .variable old_count 0x0a
  .variable out_string .length 16

Here, the ".memory RAM" macro declares the start of a page of RAM. The RAM will be allocated as prescribed in the processor description file. Here, the variable "save_count" will be at memory address "0x00" and will occupy a single, uninitialized slot of memory. The variable "old_count" will also occupy a single slot of memory at address "0x01" and will be initialized to the hex value "0x0a". Note that if the processor is reset that this value will not be re-initialized. Finally, the variable "out_string" will start at address "0x02" and will occupy 16 bytes of memory.

A ROM is declared similarly. For example,

  .memory ROM myROM
  .variable hex_to_ascii '0' '1' '2' '3' '4' '5' '6' '7' ; first 8 characters
                         '8' '9' 'A' 'B' 'C' 'D' 'E' 'F' ; second 8 characters

declares a page of ROM with the 16 element array hex_to_ascii initialized with the values required to convert a 4-bit value to the corresponding hex ascii character. This also illustrates how the initialization sequence (and length determination) can be continued on multiple lines. If "outbyte" is a function that outputs a single byte to a port, then the hex value of a one-byte value can be output using the following sequence:

  dup 0>> 0>> 0>> 0>> hex_to_ascii + .fetch(myROM) .call(outbyte)
  0x0F and hex_to_ascii + .fetch(myROM) .call(outbyte)

The first line extracts the most significant nibble of the byte by right shifting it 4 times while filling the left with zeros, adding that value to the address "hex_to_ascii" to get the corresponding ascii character, fetching that value from the ROM named "myROM", and then calling the function that consumes that value on the top of the stack while sending it to the output port (this takes 11 instructions). The second line extracts the least significant nibble using an "and" instructions and then proceeds similarly (this takes 8 instructions). The ".fetch" macro generates the "fetch" instruction using the 3 bit value of "myROM" as part of the memory address generation.

The ".store" macro is similar to the ".fetch" macro except that the assembler will generate an error message if a "store" operation is attempted to a ROM page.

Two additional variants of the ".fetch" and ".store" macros are provided. The first, ".fetch(save_count)" will generate the 2 instruction sequence consisting of (1) the instruction to push the 8 bit address of "save_count" onto the stack and (2) the "fetch" instruction with the 3 bit page number associated with "save_count". This helps ensure the correct page is used when accessing "save_count". The instruction "store(save_count)" is similar. The second variant of these is for indexed fetches and stores. For example, the preceding example to convert the single-byte value to hex could be written as

  dup 0>> 0>> 0>> 0>> .fetchindexed(hex_to_ascii) .call(outbyte)
  0x0F and .fetchindexed(hex_to_ascii) .call(outbyte)

Here, the macro ".fetchindexed" consumes the top of the data stack as an index into the array variable "hex_to_ascii" and pushes the indexed value onto the top of the data stack.

The "store" instruction must be followed by a drop instruction since it consumes the top two values in the data stack. The ".store" and ".storeindexed" macros generate this drop function automatically. Thus, ".store(myRAM)" generates the 2 instruction sequence "store drop", ".store(save_count)" generates the 3 instruction sequence "save_count store drop", and ".storeindexed(out_string)" generates the 4 instruction sequence "out_string + store drop", all with the proviso that the "store" instructions include the 3 bit address "myRAM".

Program Structure

Directives

Alphebetic listing: .abbr, .constant, .function, .include, .interrupt, .main, .memory, and .variable.

.abbr

TODO

.constant

TODO

.function

TODO

.include

TODO

.interrupt

TODO

.main

TODO

.memory

TODO

.variable

TODO

Macros

Alphebetic listing: .call, .callc, .fetch, .fetch+, .fetch-, .fetchindexed, .fetchvalue, .fetchvector, .inport, .jump, .jumpc, .outport, .return, .store, .store+, .store-, .storeindexed, .storevalue, and .storevector.

.call

TODO

.callc

TODO

.fetch

TODO

.fetch-

TODO

.fetch+

TODO

.fetchindexed

TODO

.fetchvalue

TODO

.fetchvector

TODO

.inport

TODO

.jump

Description: Generate the 3 instruction sequence associated with a jump instruction.

Operation(1): .jump(label) generates the following 3 instructions:
  1 — push the 8 lsb of the label address onto the data stack
  2 — jump with the 5 msb of the label address encoded in the jump instruction
  3 — no operation

Operation(2): .jump(label,op) where "op" is an instruction generates the following 3 instructions:
  1 — push the 8 lsb of the label address onto the data stack
  2 — jump with the 5 msb of the label address encoded in the jump instruction
  3 — op

Note that Operation(1) is a special case of Operation(2) with "op" being the nop instruction.

.jumpc

Description: Generate the 3 instruction sequence associated with a jumpc instruction.

Operation(1): .jumpc(label) generates the following 3 instructions:
  1 — push the 8 lsb of the label address onto the data stack
  2 — jump with the 5 msb of the label address encoded in the jump instruction
  3 — drop

Operation(2): .jumpc(label,op) where "op" is an instruction generates the following 3 instructions:
  1 — push the 8 lsb of the label address onto the data stack
  2 — jump with the 5 msb of the label address encoded in the jumpc instruction
  3 — op

Note that Operation(1) is a special case of Operation(2) with "op" being the drop instruction.

.outport

TODO

.return

TODO

.store

TODO

.store-

TODO

.store+

TODO

.storeindexed

TODO

.storevalue

TODO

.storevector

TODO