Harvard architecture; a 16-bit address space of 6-bit text bytes and a 16-bit address space of 8-bit data bytes
Three addressing modes: register, immediate, memory location
Register names start with %
6-bit instructions, 37 ones (1.5 nibbles)
0-31 - there are no immediate constants afterward
32-63 - there are immediate constants afterward (each somehow 16 bits)
TODO: figure out how to encode immediate constants
Two stacks:
- register stack - 16 bits per item, stores the data for operations. Starts at the 256th byte, grows upward. Stack underflow not handled
- memory stack - 16 bits per item, stores the function return addresses. Starts at the 1024th byre, grows downward. Stack underflow nor collisions with the register stack not handled
IO:
- multiple serial ports
- 16-bit address space for port numbers
- simple in and out operations
Registers:
-
PC
-16 bits
- program counter, points to a 6-bit byte in thetext
section. Is always the next instruction to be executed, similar to x86 -
FR
-4-16 bits
- flag register with the least significant bits representingCF
ZF
OF
SF
; (up to 16 bits because it has to fit on the stack)- TODO: figure out when it's cleared
-
TOS
-15 or 16 bits
- initial value 256 (assuming 16 bits) - points to two bytes in the data section - the top of the register stack; grows upward (a push increments) -
SP
-15 or 16 bits
- initial value 1024 (assuming 16 bits) - points to two bytes in the data section - the top of the memory stack; grows downward (a push subtracts)- TODO: figure out whether the stack pointers are byte-level
- TODO: figure out whether collisions are prevented
- TODO: figure out how to decrement
tos
below 0 / increment above$2^n$
Opcode structure:
| 0b0 (immediate not there) | 5-bit opcode |
| 0b1 (immediate present) | 5-bit opcode | 0b00 (padding) | 16-bit immediate |
Arithmetic operations (big-endian):
- signed - look at
OF
to determine whether there was overflow; and atSF
to determine the sign of the result (regardless of overflow) - unsigned - look at
CF
to determine whether there was overflow
Only division is a strictly signed operation.
halt
- 000000
Stop the execution
load
- 000001
load %FR
(old assembler's loadf
) - 000010
load [mem]
(old assembler's load $mem
) - 100000
Push a value onto the register stack. Value source:
- no arguments - pop an address from the register stack and use it as the address
%FR
- the flag register itself[mem]
- from the memory location at[mem]
Assumption: the data is always present; the address space is filled up entirely
store
- 000100
Pop the value from the register stack, then store it at the value the top register stack item points to (this whole operation does two consecutive pops from the stack)
store $imm
- 100001
store %FR
(old assembler's storef
) - 000101
Store the immediate value or the word in the flag register at [tos]
(popping the address from the stack)
Assumption: the data is always present; the address space is filled up entirely
swap
- 000110
Swap the values of the two top elements of the register stack
dup
- 000111
Duplicate from the top register stack element, pushing it onto the register stack
dup2
- 001000
Duplicate the second from the top element of the register stack, pushing it onto the register stack
mov $IMM
- 100010
Push the value of IMM
onto the register stack
push
- 001001
push %FR
(old assembler's pushf
) - 001010
Copy the value of the flag register or pop a value from the register stack, push the value onto the memory stack
pop
- 001100
pop %FR
(old assembler's popf
) - 001101
Pop a value from the memory stack and set the flag register to it or push it onto the register stack
add
- 001110
Pop two items from the register stack, add their values, pushing the result onto the register stack.
Reset the flag register, then set the CF
to 1 if the result has an extra carry, OF
if there is a signed overflow (the sign complement of the truncated result is not the same as the non-truncated result), ZF
to 1 if the truncated result is 0 and the SF
to 1 if the sign of the result is negative (ignores the truncated bits, only looks at the most significant bit of truncated result).
Note: the implementation isn't at all working as expected, as the change_flag_result
function takes in the truncated result.
Note: further operations' TODOs say "as if it's addition"; OF
isn't affected how you'd expect it to with addition for them - we compute (most_significant_bit_a
sub
- 001111
Pop two items from the register stack, subtracting the second one (in order of popping) from the first, and push the result (the two's complement) onto the register stack.
Reset the flag register, then set the CF
to 1 if the result is negative, OF
if there is a signed overflow (the sign complement of the truncated result is not the same as the non-truncated result), the ZF
to 1 if the truncated result is 0 and the SF
to 1 if the sign of the result is negative (even if it was truncated).
mul
- 010000
Pop two items from the register stack, multiply them, and push the result onto the register stack. Set the CF
to 1 if there is an unsigned overflow (the upper half of the result isn't 0), the OF
if there is a signed overflow (the sign complement of the truncated result is not the same as the non-truncated result), the ZF
to 1 if the truncated result is 0 and the SF
to 1 if the sign of the result is negative (even if it was truncated).
TODO: figure out why the implementation sets all the other flags as if it's addition
TODO: figure out whether setting the ZF
to 0 only if the second half of the result is 0 is intended
div
- 010001
Pop two items from the register stack, dividing the first one popped by the second, and push the result onto the register stack. Purely unsigned.
Reset the flag register, and set the ZF
to 0 if the result is truly 0 (and not rounded).
If zero division is encountered, set the result, the CF
and OF
to all-1's and the rest of the flags to 0.
TODO: figure out whether the implementation ever sets the CF
to 1
TODO: figure out why the implementation sets all the flags other than CF
as if it's addition
TODO: figure out whether setting ZF
should work like this or like the implementation (only looking at the rounded result)
and
- 010010
Compute binary and
between two popped values from the register stack, push the result onto the register stack.
Set the zero and sign flags depending on whether the value is 0 and whether the first bit is `.
or
- 010011
Compute binary or
between two popped values from the register stack, pushe the result onto the register stack.
Set ZF
and SF
depending on whether the value is 0 and whether the first bit is 1.
xor
- 010100
Compute binary xor
between two popped values from the register stack, push the result onto the register stack.
Set ZF
and SF
depending on whether the value is 0 and whether the first bit is 1.
not
- 010101
Replace the top of the register stack with its' bitwise not.
Reset the flag register, and set the OF
to 1, the ZF
to 1 if the result is 0, flip the value of SF
.
lsh $IMM
- 100011
Pop a value from the register stack, shift it by IMM
to the left (
Reset the flag register, and set the CF
to 1 if any 1's are shifted out and the ZF
if the truncated result is 0.
TODO: figure out why the implementation sets all the other flags as if it's addition
TODO: figure out whether we should set the OF
to the shifted out bit for 1-bit shifts
rsh $IMM
- 100100
Pop a value from the register stack, shift it by IMM
to the right, and push the result onto the register stack.
Reset the flag register, and set the CF
to 1 if any 1's are shifted out and the ZF
if the truncated result is 0.
TODO: figure out why the implementation sets all the other flags as if it's addition
TODO: figure out whether we should set the OF
to the shifted out bit for 1-bit shifts
call $IMM
(call label
) - 100101
call
- 010110
Push the next instruction's absolute location (the current PC
) onto the memory stack, pop a value from the register stack if it's not provided, and set it as the relative address to execute next (add it to the PC
after incrementing it; call 0
pushes the instruction; call -3
is an infinite loop)
ret
- 010111
Pop an address from the memory stack and set it as the absolute address to execute next (set PC
to it instead of incrementing it).
cmpe
(compare equals) - 011000
cmpe $imm
- 100110
Pop a value (or two if the immediate isn't provided) from the register stack, compare them and push the result onto the register stack as a boolean (0xffff
if they are equal, 0x0000
otherwise).
cmpb
(compare bigger) - 011001
cmpb $imm
- 100111
Pop a value (or two if the immediate isn't provided) from the register stack, compare them and push the result onto the register stack as a boolean (0xffff
if the first one popped is larger than the second one popped or the immediate, 0x0000
otherwise).
jmp
- 011010
jmp $imm
- 101000
Pop a value from the register stack if it's not provided, and set it as the relative address to execute next (add it to the PC
after incrementing it; jmp 0
does nothing; jmp -3
is an infinite loop)
jc
- 011011
jc $IMM
- 101001
Pop a value from the register stack. If it's a boolean true (0xffff
):
- pop a value from the register stack if an immediate isn't provided
- set it as the relative address to execute next (add it to the
PC
after incrementing it;jc 0
does nothing;jc -3
loops until all boolean true values are popped)
in $imm
- 101010
Hang the processor, waiting for the single-character user input on the device at the port specified, then push it onto the register stack
TODO: remove this.
out $imm
- 101011
Pop a value from the register stack and output it on the specified port
nop
- 011100
Do not do anything