This document is a summary of my current thoughts for Nybbler, a simple single-stack machine. The primary goal of Nybbler is to be extensible in math stack depth and word size.

Comments, questions, and suggestions are welcome.


Contents


The Math Stack

The description here assumes the math stack is four deep. The registers in the math stack are given names that show off my devotion to classic HP calculators: X, Y, Z, and T. However, the actual depth of the math stack is expected to vary from implementation to implementation. There are four types of registers in the Nybbler math stack:

The Instruction Register

The instruction set is oriented around four-bit opcodes. When a word containing instructions is read from memory, it is deposited in the instruction register (IR). The machine executes the instruction encoded by the most-significant nybble of the IR. After an instruction is executed, the IR is shifted left by four bits. Some opcodes take a four-bit operand, so the IR sometimes needs to be shifted left by eight bits; however, it is also possible to implement this by replacing the most-significant nybble of the IR with a NOP when it is shifted left (this replaces the consumed operand with a NOP, ensuring the operand will not be mistaken for an opcode). This document assumes the NOP option is used.

Implementations vary the word size in units of four. Since some instructions require the IR be capable of holding two nybbles, the smallest possible word size is eight bits. I suspect the smallest useful word size will be twelve bits.

The IR is composed of three types of nybbles:

Address Registers

The Nybbler has sixteen address registers that may be used by software to hold temporary variables. All memory accesses (including program fetches) use an address taken from one of the address registers. Address registers may be loaded from the stack and may supply a value to the stack. Address registers may also supply an operand to the ALU, which is intended to allow the program counter to be incremented.

Constants

The Nybbler is also capable of supplying sixteen constants to be pushed onto the math stack. The constants are TBD and are expected to vary from implementation to implementation. Nevertheless, an implementation is expected to supply at least the following constants:

Instruction Set

The table below summarizes the Nybbler instruction set. In the table, n denotes a nybble operand.

ValueNameOperationNotes
0 nNEXT IR<-MEM(REG(n)); REG(n)<-REG(n)+1 Obviously, some sort of temporary register will be needed to hold the operand while the register is incremented.

The value of this opcode must be zero to cause instruction flow to proceed to the next instruction. This usage means register 0 contains the program counter.

TBD + X<-X+Y; Y<-Z; Z<-T; IR<-IR<<4 Two's complement addition.
+1+ X<-X+Y+1; Y<-Z; Z<-T; IR<-IR<<4 Add with a carry in. NOT +1+ constitutes subtraction.
& X<-X&Y; Y<-Z; Z<-T; IR<-IR<<4 Bitwise logical AND
NOT X<-~X; IR<-IR<<4 Bitwise logical NOT
DOWN TEMP<-X; X<-Y; Y<-Z; Z<-T; T<-TEMP; IR<-IR<<4 Rolls the stack down, essentially implementing DROP. The stack is automatically lifted when registers, memory, and constants are loaded; DUP is therefore a sequence like !F @F @F and requires a spare address register.

It is envisioned that the stack registers will be moved simultaneously; a TEMP register should not actually be needed.

SWAP X<->Y; IR<-IR<<4 The exchange is envisioned as happening simultaneously.
NOP IR<-IR<<4 This instruction is primarily used to squash operands. If the implementation is capable of shifting the IR by 8 bits, the opcode may be put to some other use.
IF if( Y=0 ) IR<-0; IR<-IR<<4 Conditional branch. Tests the flag in the Y register, replacing all opcodes following the IF with 0NEXT if the flag is false. If the flag is true, opcodes following IF survive the test and are executed. A conditional branch is performed by loading the target address in X then executing IF 0JMP. If the flag is true, the 0JMP is executed and control transfers to the address found in X. Otherwise, the 0JMP is replaced with 0NEXT and the next word is executed.
nJMP X<->REG(n); IR<-IR<<8 Jumps are performed by exchanging X with the program counter; after the instruction is executed, X will have a return address. This allows a single instruction to serve as JMP, CALL, and RETURN:

  • JMP is executed by discarding the return address.
  • CALL is executed by saving the return address, perhaps on a return stack managed by software.
  • RETURN is executing by loading the return address into X from the software-managed return stack. The return address placed into X by the instruction is discarded (unless you need a COME FROM address...).

Since JMP does not clear IR, implementations with a sufficiently large word size may be able to save the return address on a software-managed stack before the jump takes effect.

nCONST T<-Z; Z<-Y; Y<-X; X<-CONST(n); IR<-IR<<8 Pushes a constant onto the math stack.

Large literals are loaded by starting with a suitable constant (either 0 or -1) and shifting the remainder of the literal into X a nybble at a time via nLIT.

Implementations with sufficiently large word sizes and math stack depths may be able to support loading constants through the program counter via a sequence such as 0@ @0 1 + !0 (word size of at least 36 bits and two extra math stack words).

n@ T<-Z; Z<-Y; Y<-X; X<-MEM(REG(n)); IR<-IR<<8 Fetches the memory word at the address in register n and pushes it onto the stack.
n! MEM(REG(n))<-X; X<-Y; Y<-Z; Z<-T; IR<-IR<<8 Stores X into memory at the address in register n and drops the stack.
@n T<-Z; Z<-Y; Y<-X; X<-REG(n); IR<-IR<<8 Pushes a copy of register n onto the stack.
!n REG(n)<-X; X<-Y; Y<-Z; IR<-IR<<8 Stores X into register n and drops the stack.