Diagram
This is a screenshot of the CPU as designed in Logisim-Evolution, it is running snake
What is Y84? Why does it exist?
Y84 is a project I developed for my computer architecture class. The project was to design a CPU with at least 4 registers and at least 4 unique instructions, including reading/writing memory. You also had to create an associated assembler, example program, and manual. Extra credit was available for going above and beyond, adding things like branching, additional instructions, pipelining, etc. I went a little overboard with my submission and created y84. I realized that creating snake was just barely achievable with everything I had learned in this course, so I went all in and made it happen.
Y84 is a project made up of 2 components: A CPU designed in Logisim-Evolution, and an assembler that converts .y files into image files that can be loaded by Logisim-Evolution
There are definitely some silly design choices, some on purpose (using << in every single instruction) and others because of my limited knowledge at the time (dedicated instructions for specific external components) but overall I'm still very proud of this project.
You can run this yourself in Logisim-Evolution by loading the .circ file found here. This will simulate the actual logic gates.
What is this demo?
I wanted to learn Svelte5 and SvelteKit and try my hand at making a website from scratch, so I decided to make an emulator of my cpu running in JavaScript. I also figured it would just be a neat project to show off, and much of this page was inspired by Khan Academy's code editor, where I learned to program for the first time 10 years ago. Khan Academy
Y84 Manual
Specs
- 16-bit addressed general purpose memory (2 bytes represent a unique address)
- 16-bit addressed instruction memory
- 16 16-bit registers
- 12 general purpose registers (R0, R1, …, R11)
- 3 with designated purposes (VR, SP, LR)
- 1 zero register (ZR)
- 3 “external” components:
- 8x8 LED screen
- Terminal
- Keyboard
Register Descriptions
- VR (R12): Volatile Register for temporary data or 12-bit immediate values.
- SP (R13): Stack Pointer, initialize with -1 for top-of-memory stack.
- LR (R14): Link Register for next instruction address when branching with link.
- ZR (R15): Zero Register, read-only, always returns 0.
Instruction Structure
Each instruction is 16 bits, with three main structures:
- 4-bit opcode, imm12
- 4-bit opcode, Rt, imm8
- 4-bit opcode, Rt, Rm, Rn
Registers use 4 bits (total 16 registers).
Instruction Types (first 2 bits)
- 00: Arithmetic operation
- 01: Store immediate to register or interact with an external component
- 10: Branching instruction
- 11: Store/load to memory
Arithmetic Operation (00)
- 0000 -> signed addition
- 0001 -> signed addition
- 0010 -> signed multiplication
- 0011 -> unsigned division
Storing Immediate / External Interaction (01)
- 0100 Rt, imm8 -> Store sign-extended 8-bit immediate to register.
- 0101 Rt, imm8 -> Shift Rt left by 8 bits and bitwise OR the immediate to it.
- 0110 Rt, imm8 -> Interact with components:
- imm8 = 1 => Store keyboard key to Rt (0 if empty buffer).
- imm8 = 2 => Store a random number to Rt.
- imm8 = 4 => Print Rt to terminal (ASCII).
- imm8 = 8 => Flip pixel at Rt on screen (mod 64).
- 0111 imm12 -> Store 0-extended (unsigned) 12-bit immediate to VR (for const mem addresses).
Branching (10)
- 1000 Rt, imm8 -> Branch with signed offset if Rt = 0 (use ZR for unconditional branch)
- 1001 Rt, ----, Rm -> Branch to value of Rm if Rt = 0 (use ZR for unconditional branch)
- 1010 Rt, imm8 -> Branch with signed offset if Rt > 0 (use ZR for unconditional branch with link)
- 1011 Rt, ----, Rm -> Branch to value of Rm if Rt > 0 (use ZR for unconditional branch with link)
Store / Load Memory (11)
- 1100 Rt, Rm, Rn -> Load Rt's value from address at Rm + Rn.
- 1110 Rt, Rm, Rn -> Store Rt's value to address at Rm + Rn.
Language Manual
Assembler Details
This language, named y, uses a Python (on this site, JS) assembler:
./ycc [target] [--debug]
File Requirements
- Target files must have a .y extension and output instruction/data files.
- Data files don't generate with empty data or if the segment is all 0.
Syntax Overview
Comments and Labels
Comments use //
, blank lines are also ignored
Labels are any line ending with a colon EX:
label:
Data Segment
The data segment must be at the top of the file, with every line containing an =
sign
num = 20, 30
There are no directives, so each of these are treated as 16-bit
Strings store ASCII values sequentially as 16-bit characters.
str = "Hello, World!\n\0"
You can also define a chunk of data (filled with 0)
values = [200]
And set specific values within them
values[3] = 50
Integer Literals
If you put parentheses in place of an integer constant, you can put any valid Python expression that results in a number in its place. For example:
num = (2 ** 10)
However, single-quoted strings get replaced with their ASCII value (assuming they contain one character):
characters = ('\n'), ('a'), ('\0')
Aliases
This was a quality-of-life feature I added. You can assign additional names to registers within the data segment with the following syntax:
input := R1
This makes it so that "input" in the text segment will be interpreted as R1. You can tell this is an alias because it uses :=
, and the "source" is a register.
Text Segment
Every instruction is comprised of two things: the "source" and "destination," separated by <<
. It's formatted a bit unconventionally, but here are some examples:
top:
R2 << R1 + R0
R5 << R6 * R7
VR << R4[R2] // load mem
R4[R0] << VR // store mem
PC << R6 ?=0 top // branch if R6 = 0
PC << R7 ?>0 top // branch if R7 > 0
PC@ << printf // branch with link
PC << LR // "return"
You can tell this is the text segment because <<
is used in place of =
.
There exist some "shortcuts" thanks to the zero register:
PC << uncondBranch
// Is encoded the same way as
PC << ZR ?=0 uncondBranch
Or my personal favorite:
R4 << R3
\/
R4 << ZR + R3
It's important to note that an immediate can only be used alone; you cannot use them within a larger instruction. For example, the following code is invalid:
R4 << R3 + 10
Instead, you'd have to write it like this:
VR << 10
R4 << R3 + VR
Additional Examples
num = 10
// Examples of external components in use
R0 << cin // get key in buffer (0 if empty)
R1 << rand // get random 16-bit num
cout << R0 // print R0 to terminal
screen << R1 // flip R1th pixel
VR << &num // addresses must go to VR
R10 << VR[ZR] // ZR provides a convenient offset
R0 << 9 // set param
PC@ << proc // dest of PC@ is how you do "BL"
cout << R10 // printf("\n");
PC << PC // halt (branches with offset 0)
proc: // prints R0 as a single decimal digit
VR << ('0')
VR << VR + R0
cout << VR
PC << LR // return
Example Program: Snake
Snake was my favorite idea for an example program because I knew that with my knowledge it was just barely possible and that it could be a good example of utilizing every component. It's controlled with WASD, using the keyboard, and the apple goes to a random location every time. This implementation is optimized for 1kHz.
When you die, there's an animation that retracts the snake and then shows a frown if you scored <15 points and a smile if you scored ≥15 points. Your score also gets printed to the terminal in base 10. On death, you can then press any key to play the game again.
Quality-of-Life Features
- Not allowing you to go backward into yourself.
- Looping around when you hit a wall.
- Giving you an extra "move" when you collect the apple.
More Technical Details
The game uses two "arrays" of length 64, one of dynamic size used as a ring buffer that stores the positions of each node of the snake, and one that allows for fast checking of if a spot is dangerous or not.
The snake is modeled as a queue. When it moves to a new spot, it flips the spot at the tail and then flips the new spot at the head, replacing the value where its tail was stored with the new head value. It also updates the collision field as needed.
Capturing the apple may take more time because the game must shift over a bunch of the snake's values by one. This depends on the time at which you capture the apple. It can be anywhere from 1 move to n moves (where n is the length). On average, you move n/2 data when collecting the apple. Regular moves, however, shouldn't take any longer if you're length 1 or length 60.
Example Program: Calculator
Enter two base 10 integers, separated by *, for a printed result. (The cpu is 16-bit, so overflow may occur)