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> .
|INC||0x0001||Increment <0> in place|
|DEC||0x0002||Decrement <0> in place|
|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 |
|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, DAT_ADDR_SWAP|USER_BANK|UI_INC, POP_TO_DAT|UI_INC, WRITE_RAM|UI_INC, UI_DONE])
The assembly mnemonics for operands uses the whole name from the table above. I think this makes it a little easier to read.