The researchy nature of this project makes changes more likely throughout the semester. The 20190918 version is to be used for Assignment 1.
Instruction set design is hard. Prof. Dietz has designed dozens of instruction sets in the three decades he's been a professor, and it still isn't easy for him to get things right. Thus, rather than giving you complete freedom to design your own instruction set, we're going to walk through the design logic for a hopefully reasonably well-crafted one that he built specifically for Fall 2019 EE480. However, this design is not complete -- each student must devise their own encoding of the instructions implement their own assembler.
"AXA" is a palindrome: it reads the same forward and backward. That's basically what reversible computing is about: being able to execute forward or backward. However, this symmetry is broken by the fact that programs normally only execute forward. The reverse execution supported here is essentially an error-handling mechanism, allowing backing-up to a given earlier state in the execution of the program. As you'll see, the mechanism used also provides only limited reverse execution; you can't undo an arbitrary number of instructions.
The machine is a Harvard architecture, with separate instruction (.text segment) and data (.data segment) memories. It has sixteen 16-bit registers, 16-bit datapaths, and 16-bit addresses, and each address in either memory holds one 16-bit word. Most of the instructions look fairly normal, but there are a few oddities to support reversibility and use of reverse execution to unwind operations to deal with errors and perhaps even implement transactional memory.
This instruction set is complete enough that I hope to be giving you a compiler (including full C source code) that translates programs written in a significant subset of C into AXA code. It will not be a particularly smart compiler (ok, it's really dumb), but it will show you how AXA can be used for complete programs.
AXA's instruction set uses a simple general-register model encoding each instruction as a single 16-bit word. Although implementing reversibility of some operations isn't very easy, the bulk of the operations are really quite ordinary and very little is strange in forward execution. In any case, the assembly langauge is also straightforward. Most instructions allow specifying a destination register, $d. Many also allow specifying a src, which can be any one of the following. Some instructions treat the i4 form differently; so we also use s16 to refer to any of the forms that directly produces a 16-bit value: $s, @$s, and i4$.
Only the last of these four operand structures is really unusual. In fact, most of the instruction set is inherently reversible and thus does nothing very surprising. The troublesome ones are the ones that do push() operations: lhi, llo, or, and, dup, shr, and land.
Instruction | Description | Functionality | Reverse Functionality (errors!=0) |
---|---|---|---|
xhi $d, i8 | Exclusive OR high immediate | $d^=(i8<<8); ++pc | $d^=(i8<<8); --pc |
xlo $d, i8 | Exclusive OR low immediate | $d^=i8; ++pc | $d^=i8; --pc |
lhi $d, i8 | Load high immediate | push($d); $d=(i8<<8); ++pc | $d=pop(); --pc |
llo $d, i8 | Load low immediate | push($d); $d=signext(i8); ++pc | $d=pop(); --pc |
add $d, src | Add integers | $d+=src; ++pc | $d-=src; --pc |
sub $d, src | Subtract integers | $d-=src; ++pc | $d+=src; --pc |
xor $d, src | Bitwise eXclusive-OR | $d^=src; ++pc | $d^=src; --pc |
ex $d, @$s | Exchange (d!=s) | swap($d, @$s); ++pc | swap($d, @$s); --pc |
rol $d, src | Rotate left | $d=rotateleft($d,src); ++pc | $d=rotateright($d,src); --pc |
shr $d, src | Shift right (arithmetic) | push($d); $d=($d>>src); ++pc | $d=pop(); --pc |
or $d, src | Bitwise OR | push($d); $d|=src; ++pc | $d=pop(); --pc |
and $d, src | Bitwise AND | push($d); $d&=src; ++pc | $d=pop(); --pc |
dup $d, src | Duplicate | push($d); $d=src; ++pc | $d=pop(); --pc |
bz $d, i4 | Branch if zero | if (!$d) pc+=i4 | --pc |
jz $d, s16 | Jump if zero | if (!$d) pc=s16 | --pc |
bnz $d, i4 | Branch if non-zero | if ($d) pc+=i4 | --pc |
jnz $d, s16 | Jump if non-zero | if ($d) pc=s16 | --pc |
bn $d, i4 | Branch if negative | if ($d<0) pc+=i4 | --pc |
jn $d, s16 | Jump if negative | if ($d<0) pc=s16 | --pc |
bnn $d, i4 | Branch if non-negative | if ($d>=0) pc+=i4 | --pc |
jnn $d, s16 | Jump if non-negative | if ($d>=0) pc=s16 | --pc |
jerr $d, mask | Enable checking for mask, fail to $d | check|=mask; ++pc | check&=~mask; errors&=~mask; if (!errors) pc=$d else --pc |
land | Landing for branch | push(lastpc); ++pc | pc=pop() |
com | Commit (disable all active jerr) | check=0; errors=0; ++pc | check=0; errors=0; ++pc |
fail mask | Reverse until jerr that covers mask | errors|=(mask&check); --pc | --pc (really not useful!) |
sys | SYStem (system call; end execution) | halt | --pc |
Determining how to encode the above instructions as bit patterns is a key part of your project. However, there are a few rules:
The AXA processor has two different types of registers: general-purpose registers and the undo stack. Do not confuse the undo stack with a conventional stack -- there is assumed to be one of those too, stored in ordinary data memory.
There are 16 general-purpose registers, some of which have special purposes -- a lot like MIPS. They all have names as well as numbers. Perhaps the best way to give both is the following specification (formatted as an AIK specification):
.const {r0 r1 r2 r3 r4 r5 r6 r7 r8 r9 r10 r11 rt fp sp rv }
Registers $r0 through $r11 (aka, registers $0 through $11) are "user" registers to be used in any way the programmer sees fit. However, it is expected that the assembler or compiler would use registers starting at $12 for "internal" things and starting at $0 for normal coding. The special meanings of the last four registers are not enforced by the hardware, but by convention: