Last Edit: April 19, 2022, 3:01 a.m.

This is a summary of what I've got working with my 16-bit stack-based CPU so far.

I've been designing it using an Alchitry Au FPGA prototype board. It's got an Artix 7 XC7A35T-1C with a handful of support hardware built in.

It has a 32 bit address bus and a 16 bit data bus.

Currently memory is accessed at the word level only.

Here is a list of the current op-codes. Operand parameters are written as pp+N . The top of stack is written as <0> and the next item on the stack <1> .

Code Value Description
NOP 0x0000 No Operation
INC 0x0001 Increment <0> in place
DEC 0x0002 Decrement <0> in place
HALT 0x0003 Stops CPU
LOAD_CONST 0x0004 pushes pp+1 onto stack
ADD 0x0005 Pops two items, and pushes the result
LOAD_MEM 0x0006 Loads the value in the memory address pointed at by pp+1
TO_MEM 0x0007 Pops the stack and writes that value to memory address pointed at by pp+1
MUL 0x0008 Multiply top two items on the stack, removing them, and placing result on top
BSL 0x0009 bit shift left <0> in-place one bit.
BSR 0x000A bit shift right <0> in-place one bit.
JUMP 0x000B Jump to fixed position }}}
POP 0x000C Discard <0>
JUMP_IF_ZERO 0x000D If <0> is 0, jump to pp+1
TO_POINTER 0x000E <0> and <1> . Writes value of <1> to the address pointed at by <0>
LOAD_POINTER 0x000F Pop the stack, read value at that address and push it
SWAP 0x0010 Switches top and second items on stack
DUP 0x0011 Pushes <0> on the stack again, duplicating it
JUMP_IF_NEG 0x0012 Jump to pp+1 if <0> is negative
JUMP_IF_CARRY 0x0013 Jump to pp+1 if there was an overflow
CALL 0x0014 Jump to pp+1 and push the PP frame.
RETURN 0x0015 Pop the PP frame and jump to its pp value.
JUMP_IF_LESS 0x0016 Jump to pp+1 if the top of the stack is less than the next item
JUMP_IF_MORE 0x0017 Jump to pp+1 if the top of the stack is more than the next item
JUMP_IF_SAME 0x0018 Jump to pp+1 if the top of the stack equals the next item.
USER_BANK 0x0019 Set the high word of the memory read/write address to pp+1
LONG_JUMP 0x001A Set the high word of the pp address to pp+1 and jump to pp+2
LOAD_LOCAL 0x001B Push the stack frame + pp+1 onto the stack
TO_LOCAL 0x001C Pop and write to the stack frame + pp+1
JUMP_POINTER 0x001D Pop <0> and set PP to that value
CALL_POINTER 0x001E Pop <0> and push it onto the PP stack
GET_PP 0x001F Push pp onto the stack.

The main stack is not accessible from the main system ram. It is 256 elements deep. If more elements are needed, they should be written to / retrieved from ram instead.

When doing a call or return, the program pointer is automatically stored and restored on its own internal stack separate from the main system stack. The user bank, pp bank, and local stack frame pointer are also all automatically kept track of in their own stacks. These stacks are currently defined as 256 deep so that is the maximum call depth using the built-in functionality.

The LOAD_LOCAL and TO_LOCAL instructions use the stack frame pointer as a storage location for "local" variables. These are kind of like an in-ram stack where the base pointer is maintained and the offset is automatically calculated for you.

The language is microcoded with a lookup table. The first three instructions in the microcode rom load the current pp from ram, look up the value in the jump table rom at that address, and then jump to that position in the microcode rom. There is a python program that compiles the microcode rom and microcode jump table.

The program assembler is written in python. It imports and uses the microcode compiler when determining the op codes for each instruction. This way any changes or improvements in the micro-architecture will automatically proliferate to the assembler.

The microcode rom uses 32 bit wide words. Some of the control signals can be used in parallel and some are exclusive. For example, the ALU has 5 bits dedicated but they are encoded as an integer which is then decoded in the ALU to determine what operation to perform.

# Control Word Bit Definitions
# these can be or'd together when defining the micro instruction steps for each
# main instruction
# hex to binary conversions for each nibble, 1 = 0001, 2 = 0010, 4=0100, 8=1000
UI_DONE       = 0x00000001 #bit0 ends the instruction (resets uop counter)
UI_INC        = 0x00000002 #bit1 increments the microinstruction counter
UI_JMP        = 0x00000004 #bit2 sets the microinstruction counter based on a lookup table for the value on the data bus.
                         #     The table is created by this file.
PP_TO_ADDR    = 0x00000008 #bit3 puts the program pointer on the address bus
PP_INC        = 0x00000010 #bit4 increments the program pointer
PP_PUSH       = 0x00000020 #bit5 pushes a new value on the program pointer stack from the data bus
PP_POP        = 0x00000040 #bit6 pops the last value from the program pointer stack
                         #     If pp_push and pp_pop are both on at the same time, it replaces the current pp
RAM_TO_DAT    = 0x00000080 #bit7 loads data from ram to the data bus
WRITE_RAM     = 0x00000100 #bit8 sets a value in ram from the data bus
DAT_ADDR_SWAP = 0x00000200 #bit9 copies the data bus to the addr bus
PUSH          = 0x00000400 #bit10 pushes the current data bus value onto the main stack
POP_TO_DAT    = 0x00000800 #bit11 pops an item from the stack

#ALU effect select
#ALU control is 5 bits 12-17 (hex from 0x01 to 0x1F bit shifted left by 16)
# this means we have 32 total ALU operations (can't use zero)
INC           = 1 << 12
DEC           = 2 << 12
ADD           = 3 << 12
MUL           = 4 << 12
BSL           = 5 << 12
BSR           = 6 << 12
DUP           = 7 << 12
SWAP          = 8 << 12

#Jump condition selecct
# jump control is 4 bits 18-21 (hex from 0x01 to 0x0F shifted left by 17 bits)
# this means we can hae 15 total jump condtions (can't use zero)
JEZ           = 1 << 18 
JIN           = 2 << 18
JIC           = 3 << 18
JILT          = 4 << 18
JIGT          = 5 << 18
JIE           = 6 << 18

SET_PP_BANK   = 0x00400000 #bit22
SET_USER_BANK = 0x00800000 #bit23
PP_BANK       = 0x01000000 #bit24
USER_BANK     = 0x02000000 #bit25
FRAME_ACCESS  = 0x04000000 #bit26
BIT_27        = 0x08000000 #bit27
BIT_28        = 0x10000000 #bit28
BIT_29        = 0x20000000 #bit29
BIT_30        = 0x40000000 #bit30
HALT          = 0x80000000 #bit31

The first three microinstructions that every operand inherits are as follows.

initial_ops = []
initial_ops.append(PP_TO_ADDR|PP_BANK|UI_INC) # tell ram what address the PP is pointing to  0x0100000a
initial_ops.append(RAM_TO_DAT|PP_INC|UI_INC) # put the value from ram into the data bus      0x00000092
initial_ops.append(UI_JMP) # Follow the ucode offset                                         0x00000004

Each operand consists of one or more control words combined together as follows.

#Last item in stack is pointer.  Second item is value to write
instructions['TO_POINTER']  = (0x000E, [POP_TO_DAT|UI_INC,

The assembly mnemonics for operands uses the whole name from the table above. I think this makes it a little easier to read.

There once was an [X] from [Place B]
Who satisfied [Predicate P]
Then [X] did [Thing A]
In an [Adjective] way
Resulting in [Circumstance C]