This is small 16-bit CPU, written in Verilog 2001.
It doesn't serve any specific purpose and was made just for fun.
RCPU has 3 addressable 16-bit registers: A, B and C. Also it has 32-bit Program Counter (PC), 4-bit Flag Register (F), 16-bit Stack Pointer (SP), 16-bit Frame Pointer (FP) and some other internal registers about which you shouldn't worry.
- Register : Simply use value from A, B or C registers. (In binary, it's
001
-011
.000
- simply use 0) - Immediate Small : 8-bit constant is inside instruction code (See I Type instructions)
- Immediate Big : 16-bit constant after opcode (
100
) - Absolute : Use 16-bit value at the 32-bit address, specified after opcode (
101
) - Addressed : Use 16-bit value at the 32-bit address, specified by A register and page register (
110
) - Stack : Use 16-bit value at the 32-bit address, specified by sum of 16-bit value after opcode and in FP register (
111
)
Addresses are little-endian, words and instructions consist of 2 bytes. Reading from odd address simply swaps bytes
Address | Size (bits) | Direction | Name | Description |
---|---|---|---|---|
FFFF0000 |
8 | Write | LCD_DATA |
LCD D0-D7 pins |
FFFF0001 |
8 | Write | LCD_CTRL |
LCD control pins |
FFFF0002 |
8 | Read | SWITCH |
DIP switch state |
FFFF1000 |
16 | Read/Write | PAGE_REG |
High 16 bits for addressed memory mode |
FFFF101E |
16 | Read/Write | SP |
Other way to access SP |
FFFFF000 |
8 | Write | BPX_EN |
Breakpoint enable bit flags |
FFFFF002 FFFFF006 FFFFF00A FFFFF00E |
16 | Write | BPX_LOW |
Breakpoint #X activation address low 16 bits |
FFFFF004 FFFFF008 FFFFF00C FFFFF010 |
16 | Write | BPX_HIGH |
Breakpoint #X activation high 16 bits |
FFFFF012 |
16 | Write | BPA_LOW |
Breakpoint handler address low 16 bits |
FFFFF014 |
16 | Write | BPA_HIGH |
Breakpoint handler address high 16 bits |
FFFFFFF6 |
8 | Write | IR_EN |
Infrared interrupt enable |
FFFFFFF8 |
16 | Write | IR_LOW |
Infrared interrupt address low 16 bits |
FFFFFFFA |
16 | Write | IR_HIGH |
Infrared interrupt address high 16 bits |
FFFFFFF7 |
8 | Write | KEY_EN |
Keyboard interrupt enable |
FFFFFFFC |
16 | Write | KEY_LOW |
Keyboard interrupt address low 16 bits |
FFFFFFFE |
16 | Write | KEY_HIGH |
Keyboard interrupt address high 16 bits |
PAGE_REG
and SP
could also be accessed with @0
and @15
respectively
C | N | Z | V |
---|---|---|---|
3 | 2 | 1 | 0 |
- Carry flag - set if previous operation resulted unsigned overflow.
- Negative flag - set if previous operation result is negative number.
- Zero flag - set if previous operation result is 0.
- Overflow flag - set if previous operation resulted signed overflow.
R - register
M - memory address
I - immediate value
F - fast memory address
A0, A1, A2 - arguments of instruction
0000 |
Source 1 | Opcode | Source 2 | Destination |
---|---|---|---|---|
4 bits | 3 bits | 4 bits | 2 bits | 3 bits |
Flags: CNZV
Opcode | Syntax | Description | Formal Actions |
---|---|---|---|
0000 |
ADD RMI, R, RM |
Addition | A3 <= A1 + A2 |
0001 |
ADC RMI, R, RM |
Addition with carry | A3 <= A1 + A2 + C |
0010 |
SUB RMI, R, RM |
Subtraction | A3 <= A1 - A2 |
0011 |
SBC RMI, R, RM |
Subtraction with carry | A3 <= A1 - A2 - C |
0100 |
MUL RMI, R, RM |
Multiplication (32-bit) | {A, A3} <= A1 * A2 |
0101 |
MLL RMI, R, RM |
Multiplication (16-bit) | A3 <= A1 * A2 |
0110 |
SGN RMI, R, RM |
Set sign of value | A3 <= {A1[15], A2[14:0] |
0111 |
RAS RMI, R, RM |
Right arithmetic shift | A3 <= A1 >>> A2 |
1000 |
LSH RMI, R, RM |
Left logical shift | A3 <= A1 << A2 |
1001 |
RSH RMI, R, RM |
Right logical shift | A3 <= A1 >> A2 |
1010 |
LRT RMI, R, RM |
Left cyclic shift | A3 <= A1 <cyclic< A2 |
1011 |
RRT RMI, R, RM |
Right cyclic shift | A3 <= A1 >cyclic> A2 |
1100 |
AND RMI, R, RM |
Bitwise and | A3 <= A1 & A2 |
1101 |
OR RMI, R, RM |
Bitwise or | A3 <= A1 | A2 |
1110 |
XOR RMI, R, RM |
Bitwise xor | A3 <= A1 ^ A2 |
1111 |
NOT RMI, RM |
Bitwise not | A2 <= ~A1 |
1000 |
Source | H/L | Destination | H/L | Unused |
---|---|---|---|---|---|
4 bits | 3 bits | 1 bit | 3 bits | 1 bit | 4 bits |
Flags: CNZV
Syntax | Description | Formal Actions
-----------------------------|-------------|--------------------
MOV8
RMI[L/H], RM[L/H]
| Move 1 byte | A2[H/L] <= A1[H/L]
L/H bits specify which byte should be used, low if 0, high if 1. In case of addressing modes 101
-111
if L/H is set, flip last bit of address
11 |
Opcode | Address |
---|---|---|
2 bits | 1 bit | 13 bits |
Flags: ----
Opcode | Syntax | Description | Formal Actions |
---|---|---|---|
0 |
JMP M |
Jump to given address(relative) | PC <= PC + {A1, 1'b0} |
1 |
JMPL M |
Long jump to given address | PC <= {PC[31:28], A, A1, 1'b0} |
Before jumping PC increments at fetching cycle, so actual jump address is PC + A1 + 1
1001 |
Source | Unused |
---|---|---|
4 bits | 3 bits | 9 bits |
Flags: ----
Syntax | Description | Formal Actions |
---|---|---|
JMR RM |
Jump to given address | PC <= {PC[31:17], A1, 1'b0} |
01 |
Opcode | Source 1 | Opcode(continue) | Immediate |
---|---|---|---|---|
2 bits | 2 bits | 3 bits | 1 bit | 8 bits |
Flags: CNZV
Opcode | Syntax | Description | Formal Actions |
---|---|---|---|
00,0 |
ADDI RM, I, RM |
Add immediate value | A3 <= A1 + A2 |
01,0 |
ADCI RM, I, RM |
Add immediate value with carry | A3 <= A1 + A2 + C |
10,0 |
SUBI RM, I, RM |
Subtract immediate value | A3 <= A1 - A2 |
11,0 |
SBCI RM, I, RM |
Subtract immediate value with carry | A3 <= A1 - A2 - C |
00,1 |
ANDI RM, I, RM |
Bitwise and with immediate value | A3 <= A1 & A2 |
01,1 |
ORI RM, I, RM |
Bitwise or with immediate value | A3 <= A1 | A2 |
10,1 |
XORI RM, I, RM |
Bitwise xor with immediate value | A3 <= A1 ^ A2 |
11,1 |
LDI I, RM |
Load immediate value | A3 <= A2 |
Restriction: First and third argument (destination) should use the same addressing mode! If A1 == 0, then use SP
0001 |
Source 1 | Opcode | Destination | Immediate |
---|---|---|---|---|
4 bits | 3 bits | 2 bits | 3 bits | 4 bits |
Flags: CNZV
Opcode | Syntax | Description | Formal Actions |
---|---|---|---|
00 |
LSHI RMI, I, RM |
Left logical shift at immediate value | A3 <= A1 << A2 |
01 |
RSHI RMI, I, RM |
Right logical shift at immediate value | A3 <= A1 >> A2 |
10 |
LRTI RMI, I, RM |
Left cyclic shift at immediate value | A3 <= A1 <cyclic< A2 |
11 |
RRTI RMI, I, RM |
Right cyclic shift at immediate value | A3 <= A1 >cyclic> A2 |
0010 |
Opcode | Flag | 0 |
Immediate (address shift) |
---|---|---|---|---|
4 bits | 1 bit | 2 bits | 1 bit | 8 bits |
Flags: ----
Opcode | Syntax | Description | Formal Actions |
---|---|---|---|
0 |
JFC M, I |
If flag is clear, jump to address | if(!F[A2]) PC <= PC + {A1, 1'b0} |
1 |
JFS M, I |
If flag is set, jump to address | if(F[A2]) PC <= PC + {A1, 1'b0} |
Before jumping PC increments at fetching cycle, so actual jump address is PC + A1 + 2
JFS
instruction is not implemented yet!
0010 |
Source/Destination | 1 |
Opcode | Unused | Fast memory address |
---|---|---|---|---|---|
4 bits | 3 bits | 1 bit | 1 bit | 3 bits | 4 bits |
Flags: CNZV (if LOAD)
Opcode | Syntax | Description | Formal Actions |
---|---|---|---|
0 |
LOAD F, RM |
Load value from fast memory | fmem[A2] <= A1 |
1 |
SAVE RMI, F |
Save value to fast memory | A1 <= fmem[A2] |
Fast memory is a register based memory, address to which is specified by @address
syntax, which can be used only
in LS-type commands. It is also mapped to FFFF1000-FFFF101F addresses of normal memory space
0011 |
Source/Destination | Opcode | SVPC add |
---|---|---|---|
4 bits | 3 bits | 2 bits | 7 bits |
Flags: CNZV (if POP)
Opcode | Syntax | Description | Formal Actions |
---|---|---|---|
00 |
PUSH RMI |
Push value to stack | mem[SP] <= A1; SP <= SP - 2 |
01 |
POP RM |
Pop value from stack | SP <= SP + 2; A1 <= mem[SP] |
10 |
SVPC |
Push PC and FP to stack | mem[SP:SP-3] <= PC + {A2, 1'b0}; mem[SP-4:SP-5] <= FP; SP <= SP - 6 |
11 |
RET |
Load PC and FP from stack | SP <= SP + 6; PC <= mem[SP:SP-3] ; FP <= mem[SP-4:SP-5] |
If in POP instruction 0 is used as destination, then load to flag register |
Macro | Actual commands |
---|---|
NOP |
ADD 0, 0, 0 |
MOV RMI, RMI |
ADD A1, 0, A2 |
JVC M |
JFC A1 0 |
JVS M |
JFS A1 0 |
JNE M |
JFC A1 1 |
JEQ M |
JFS A1 1 |
JGE M |
JFC A1 2 |
JLT M |
JFS A1 2 |
JCC M |
JFC A1 3 |
JCS M |
JFS A1 3 |
CALL M |
SVPC; JMP A1 |
HALT |
JMP <current address> |
DW I |
<raw data in A1> |
Function calls are done by instructions SVPC
and JMP <function>
or macro CALL <function>
.
Instruction SVPC
saves PC and FP to stack (in order of "PC.h, PC.l, FP", so addresses are "SP, SP-2, SP-4").
Before function call arguments should be pushed to stack in C-language style (from last to first).
In the function body arguments are accessed by [8]
, [10]
, [12]
, etc. and local variables by [0]
, [-2]
, [-4]
, etc.
Function result should be placed in A or A:B or in memory address, specified by A:B register pair.
Function returns by RET
instruction (SP should point to the same address as at the start of function).
After function call SP should be incremented by arguments size (usually by ADDI
instruction).
All output ports return 0 on reading, while writing to input ports does nothing.
LCD_DATA
and LCD_CTRL
are ports for controlling character LCD display.
SWITCH
is a port for reading signals from DIP switch, located on PCB (returns numbers from 0 to 15)
PAGE_REG
is a port which provides high 16 bits for address, when addressed mode is used (because A register has only 16 bits, not 32). Also it is in fast memory at address @0
INT_EN
, INT_LOW
and INT_HIGH
are ports for controlling keyboard interrupts.
BPX_EN
, BPX_LOW
, BPX_HIGH
, BPA_LOW
and BPA_HIGH
are ports for controlling breakpoint interrupts.
There also is port SP
, which is projection of SP register on memory
There are infrared remote and keyboard interrupts which are rising only if corresponding interrupt enable register (FFFFFFF6/7
respectively) is set.
Register is set after writing to it non-zero value and reset after writing 0. (But as all output ports it returns 0 on reading)
When keyboard key is pressed, control jumps to address, stored in interrupt address register (Addresses FFFFFFF8-B
and FFFFFFFC-F
), pushes key scan code (or IR key number) and saves ABC, PC and FP registers to stack (in order of "C, B, A, PC.h, PC.l, FP", so addresses are from SP to SP-11).
So inside of interrupt looks like a simple function (except for need to explicitly pop registers ABC at the end of interrupt).
Breakpoints are memory addresses which rise an interrupt when CPU try to access them.
There can be 4 hardware interrupts, which are specified and enabled by FFFFF000
-FFFFF017
ports.
Address where control jumps is specified by FFFFF018
-FFFFF01B
ports.
Interrupt argument is breakpoint number, everything else is the same, as in keyboard interrupts.
User macros are only assembler structures, so they don't appear in final result. They serve only as syntax sugar and can be replaced by normal assembler language instructions. User macros are simply macros defined by user, or inline functions, if you wish. Syntax is:
#defmacro <name> [<parameter types list>]
<instructions>
enddef
Parameter types are simply comma separated list of types, which are the same, as specified in instruction set tables, so all possible types are r
, rm
, rmi
, i
.
In macro body parameters can be used as an argument for instruction (if it supports type of that parameter) by using $ sign and number of the parameter, as in $1
for first argument.
After definition user macros can be used as normal macros, but with $ sign before its name.
Code example:
#defmacro add32 [rmi, rmi, r, r, rm, rm] ; $add32 (int32 a, int32 b, int32 out)
add $2, $4, $6
adc $1, $3, $5
enddef
main:
MOV 1234h, A
MOV 5678h, B
$add32 4321h, 8765h, A, B, A, B ; results by 5555DDDD in A:B pair
PS/2 controller which I used in this project because mine was awful:
http://www.eecg.toronto.edu/%7Ejayar/ece241_08F/AudioVideoCores/ps2/ps2.html
My assembler for this CPU (with features described in Some other stuff section):
https://github.com/silent-observer/RASM