We are currently working on new rules for what content should and shouldn't be allowed on this website, and are looking for feedback! See Esolang:2026 topicality proposal to view and give feedback on the current draft.
User:Enny/ENNCPU
| Paradigm(s) | imperative |
|---|---|
| Designed by | Juice F. Ennedy |
| Appeared in | 2025 |
| Memory system | register, cell |
| Computational class | Turing-complete |
| Reference implementation | https://gitlab.com/kneelian/enncpu/ |
| Influenced by | ez80, ARM7 |
| File extension(s) | .enn |
ENNCPU is an instruction set architecture (ISA) for a fantasy 24-bit CPU.
Design
ENNCPU is meant to be a somewhat serious ISA inspired by real CPUs and designs of the eighties and nineties, meant to represent a CPU that would power high-end workstations in an alternative computing history. It is not very esoteric, as it is not particularly difficult to program in, but it nevertheless has some unusual and otherwise rare design choices. General outline of important or interesting features:
- Eight general-purpose 24-bit registers named
A, B, C, D, E, F, G, H - Shadow registry i.e. secondary register file that can be swapped out at need (cf. z80, ez80)
- Special-purpose registers, including a stack pointer, inaccessible for general computation
- 24-bit address space (cf. ez80), and all GP registers can be used as address registers
- Two permission levels (kernel and usermode)
- Simple MMU managing mapping and permissions for 4K pages
- Load-store architecture (general RISC ideas) with pre/post-increment/decrement (syntax inspired by m68k)
- Big-endian memory access and instructions (cf. SPARC)
- Two-operand instructions (cf. x86) where the destination is one of the sources
- Fixed-width 16-bit instructions (cf. Arm Thumb 1)
- 24-bit floating point instructions operating on the same registers as normal arithmetic (cf. RISC-V Zfinx ext.)
- Packed 3x8-bit operations providing SIMD in a single GP register (cf. RISC-V P ext.)
- Predication for all instructions (cf. Arm6/7/11), determined by single bit
- Size of immediate arguments limited to 6 or rarely 8 bits (cf. small immediates in Arm)
- Non-predicated conditional jumps
JxZy, JxNZybased on whether a register is (non)zero (cf. Aarch64CBZ, CBNZ)
ENNCPU is trivially the same computation class as an average general-purpose CPU; if we say that x86 or the ez80 are Turing-complete, then so is ENNCPU (despite memory limitations).
The exact instruction set of ENNCPU is not yet finalised and is subject to change
Example code: Fibonacci generator
Here is an example of a short ENNCPU program that generates the first 38 members of the Fibonacci sequence (up to register limits):
.ORG 0x0000
.SEC %PGA
@START
ADRL A, @STK ; 6800
ADRM A, @STK ; 7004 / loads addr. of @STK into A
WSP A ; 2c11 / writes A to SP
@FIB
MOV A, #0 ; 6800
MOV B, #1 ; 6a01
MOV H, #37 ; 6725
POPS D
@FIB_LOOP
PSHS A ; 2c02
MOV C, B ; 0391
ADD B, A ; 0208
MOV A, C ; 0382
SUB H, #1 ; 23c1
JMNZO H, @FIB_LOOP ; 55e5
PSHS D ; 2cc2
RET ; 2e00
; at this point the stack contains the
; Fibonacci numbers in reverse order
%PGA
.ORG 0x0400
@STK
Let's break it down.
Sections and the Preprocessor
The above code was written for the ASSENNBLER, the reference assembler for ENNASM that produces ENNCPU object code. As the object code can be pretty sparse (kilobytes or megabytes between sections), the ASSENNBLER provides an option to assemble "sections" of code into a chunk, starting at a given address and incrementing automatically, independently. This is done through the following preprocessor idiom:
.ORG #0xZZZZZZ .SEC %SECTION_NAME ... ... ... %SECTION_NAME
The .ORG directive tells the preprocessor to make its internal address counter start from a new address, and the .SEC %X ... %X 'harness' places invisible start and end labels that the preprocessor uses to assemble the rest of the code. This harness also reserves 8 bytes of bookkeeping information at the start of every section, to help the loader know where to load the given code.
This is technically not obligatory to include, but given as the reference implementation relies on this idiom and the 8 bytes of bookkeeping, it is practically unavoidable.
Setting up the Stack
The following three instructions set up the stack pointer:
... ADRL A, @STK ADRM A, @STK WSP A ... .ORG 0x0400 @STK
As ENNCPU uses a fixed-width, 16-bit instruction format, immediates are limited in size. Therefore, the address of the designated stack area (and, really, pretty much any other construct) has to be built up in thirds. As we know the stack area is located in the first 64K (specifically, at 0x400, as the preprocessor directive shows), we can avoid loading the top byte: the upper bytes of the register are already zeroed out by ADRL. The ASSENNBLER takes care to replace instances of label names in these kinds of instructions with the actual label address or label offset, depending on the exact instruction type.
The instruction WSP Rs simply moves the value of the register into the stack pointer; consider it an equivalent of Arm's mov sp, x0 in this case.
Labels
Like in all assemblers, labels do not actually exist: they're tools of convenience to help the programmer write jumps and memory accesses more fluently. In ENNASM, labels are prefixed with @ and are an ASCII string up to 16 characters long. There are no format restrictions (e.g. a label can start with a number), but every label must be unique; unlike in some assemblers, new instances of labels do not overwrite previous instances (you can't do things like b.gt 1f that you can in Arm).
@FIB ...
Loading Immediates
The instruction mnemonics ADRL Rd, #imm8 and MOV Rd, #imm8 actually alias the same underlying instruction.
@FIB MOV A, #0 MOV B, #1 MOV H, #37
Using the Stack
ENNCPU has a rich array of memory access operations, which include pushes and pops using the internal SP register. All memory operations can be in one of three widths: byte access (8-bit), word access (16-bit), and sesqui access (24-bit). The first operation here pops the return address (more on this later) into a temporary storage register, D. After this, we see the Fibonacci loop pushing the value stored in reg. A at the start of every iteration. As we have already prepared the register with 0 before the start of the loop, it first pushes 0 (the first Fibonacci number).
POPS D @FIB_LOOP PSHS A ...
Register Arithmetic
What follows is a pretty simple sequence of moving to a temporary, summing two registers, and moving that temporary back into the main registers. As ENNCPU is a two-operand architecture much like x86, all arithmetic operations store the result in the first operand register listed: the instruction ADD B, A will produce a value that is the sum of these two registers, and then store it in B at the end of its execution. When there is an immediate (#imm6 for arithmetic instructions), the same principles of storage apply.
... MOV C, B ADD B, A MOV A, C SUB H, #1
Jumps
ENNCPU has a variety of jump types. Aside from regular offset jumps (adding or subtracting from the instruction pointer) (JxO), it can also do quick jumps to the first 512 bytes (occasionally called 'page A'/PGA in examples and comments) (JxA), or jump to an address stored in a register (JxR). It furthermore includes conditional jumps (like Arm64's cbz and cbnz), whose execution depends on the value of the first register argument (JxZy and JxNZy). All regular jumps start with the mnemonic prefix JM Finally, all ENNCPU jumps can also involve linking to ease subroutine/procedure implementation: linking jumps push the return address, i.e. IP + 2, to the stack before jumping, and their mnemonics start with the prefix JL.
At the end of a subroutine, it's common to see a simple RET, representing popping the return address from the top of the stack and jumping to it. Improper stack management can lead to false returns to nonsense memory addresses.
... JMNZO H, @FIB_LOOP PSHS D RET
This short sequence of code tells us that execution will jump to @FIB_LOOP only if H is nonzero (i.e. the loop counter is still true), and otherwise (i.e. when the loop ends), we push the return address that we have previously stashed in D, and then return. Of course, it is possible to also just do JMR D, as it stores the return address, but this is semantically more opaque, and might be less optimised in the future.
Example code: bigint integer square root
Here is another code example, taken from a wider int48 library. The bigint/int48 library implements 48-bit integers using a pair of 24-bit registers. For one-operand functions, the input is in {A, B} = {47:24, 23:0} (i.e. big-endian format), and for two-operand functions, the second operand is in {C, D} in the same format. Much like regular register operations, this library assumes that the first source operand will also be the destination.
This bit of code implements the integer square root function for such integers:
@ISQRT_I48
MOV C, #0
MOV D, #0
SET D, #23
@ISQRT_LOOP
MOV E, C
BOR E, D
MOV F, E
MULA F, E
MOV H, E
MULB H, E
CEQ H, A
CLE.P F, B
OR CLT H, A
MOV.P C, E
LSHR D, #1
JMNZO D, @ISQRT_LOOP
MOV B, C
MOV A, #0
RET
Let's break down the new concepts.
Multiplication
Unlike most other arithmetic instructions, ENNCPU multiplication comes in two parts: the MULA Rd, Rs instruction provides the low 24 bits of a 24 x 24 multiplication, and MULB Rd, Rs gives the high 24 bits. This instruction pair cannot take immediate arguments. A sequence of MULA and MULB thus provides the full possible range of multiplication products split in two registers; this is exactly how it's used here:
MOV F, E
MULA F, E
MOV H, E
MULB H, E
Thus, H:F now contain the square of E.
Bitwise operations
ENNCPU has a robust set of bitwise operations. Apart from the expected BOR (i.e. bitwise-or), it also features a bidirectional barrel shifter (LSHL Rd, Rs/#imm6 for leftward shifts, and LSHR Rd, Rs/#imm6 for rightward shifts), and per-bit toggles: SET Rd, #imm6 will toggle only the specified bit, counting from the right edge with the LSB being #0 and the MSB #23. Immediates larger than 23 are ignored.
Conditional execution
Leaning on the design choices present in 32-bit versions of the Arm architecture, almost every instruction can be predicated. Unlike Arm predication, and due to encoding limitations (as all instructions are 16-bit), there is only one predication bit and one predication type, corresponding to a boolean internal flag register. To compensate for this, ENNCPU has a range of conditional instructions that set or clear the flag depending on the outcome.
Predicated instructions are suffixed with .P in the assembly, and are thus clearly visible; the dot is not optional. Pretty much any instruction (other than debug instructions and the error instructions) can be predicated, including conditional instructions themselves.
Conditional instructions come in two groups: one large group that sets the flag register if they evaluate to true and clears it if they evaluate to false, and one smaller group that only sets the flag register if they evaluate to true but preserves it if they evaluate to false; this second group is prefixed by a separate OR Cxx in the assembly stream, although they nevertheless still assemble to one 16-bit instruction. These two types, alongside predication of conditional instructions, allow chaining of conditionals in a way that can form more complex predicates. In the above code, we have the following section:
...
CEQ H, A
CLE.P F, B
OR CLT H, A
MOV.P C, E
...
In less technical terms, this translates to if( ((H == A) and (F <= B)) or (H < A) ) { C = E; }, and shows both predicated conditionals (i.e. if CEQ H, A evaluates to true, then also check CLE F, B) and or-conditionals (i.e. regardless of whether the first two checks passed, set the flag to true if CLT H, A evaluates to true). At the end, we have a predicated move, which will execute only if the previous condition block evaluates to true.
ENNCPU predicates are not consumed after a predicated instruction passes: you can chain any number of predicated instructions off of one conditional check.
Example code: Mandelbrot set fragments
We won't cover the whole Mandelbrot set subroutine here as it's somewhat long (~200 lines of code), but there are a few more things I want to draw attention to.
Preprocessor literals and memory operations
The ENNASM preprocessor supports embedding literals in the object code bytestream. As far as numerical types are concerned, there are preprocessor directives for .INT8, .INT16, .INT24 and .FP24. To operate on these, you put a label before them, load the label's address, and then do memory accesses. In the Mandelbrot set program, the following occurs:
... ADRL A, @VARS ADRM A, @VARS LDRS D, A+ LDRS H, A+ ... LDRS E, A+ LDRS H, A ... @VARS .FP24 1.0 .FP24 -2.0 .FP24 1.0 .FP24 -1.0 ...
Here, after the address has been loaded into A using the previously encountered ADRL/ADRM Rd, #imm8, this address is used to load a sesqui-word (24 bits) into D using the instruction LDRS Rd, Rs. Other load sizes available are byte loads (through LDRB Rd, Rs) and word loads (through LDRW Rd, Rs). Although not present in this program, stores are correspondingly done using the STRB/STRW/STRS Rs, Rd instructions. Since floating points are, as the name shows, 24 bits wide, they have to be moved using sesqui loads and stores.
The plus suffix after the address register in LDRS D, A+ means that the instruction does a post-increment, i.e. after loading from the address, an offset equivalent to the size of the loaded data (here this is 3 because a sesqui is three bytes). All four combinations of pre- and post-increment and -decrement are available, using plus and minus suffixes and prefixes. The syntax of these addressing modes is inspired by m68k equivalents.
ENNCPU does not support address displacement; if you need to use the address register as a base, you have to do integer arithmetic on it before memory operations.
Shadow registry
A powerful feature of the x80 and descendant ISAs is the idea of the shadow registry. This is a parallel register file that can partially or entirely be swapped into the active set to provide a sort of extended registry without expending encoding space. In the Mandelbrot program, the shadow registry is used to store some constants that get accessed frequently:
... SHDW B SHDW D SHDW H ... LITE B ...
Aside from SHDW Rs, which copies the register's contents into the corresponding shadow register, and LITE Rd, which copies the contents of the corresponding shadow register onto the active register, there is also SWPR Rd, which exchanges values between shadow and light registry without overwriting anything (compare z80's EX AF, AF'), and SWAP which exchanges the entire registry (compare z80's EXX).
Floating point support
ENNCPU provides some hardware support for floating point numbers, in a fp24 format. Floating point operations operate on the same registry integer and other arithmetic does: there is no separate float register file (similar to the RISC-V Zfinx extension) The floating point format used by ENNCPU is a truncation of IEEE-754 32-bit floats, cutting off the low 8 bits of the mantissa. This gives them one sign bit, 8 bits of exponent, and 15+1 mantissa bits. It is possible to bitwise convert to floats used on, for example, Arm from the ENNCPU format simply by doing std::bit_cast<float>( u32(Rd) << 8 ); on the register containing the desired floating point value.
Floating point operations do not support any immediates at all; all operations must be done between two registers. To load values into registers for arithmetic or comparisons, you must either load them from memory, or must first move an integer value into a register and perform a float cast. The first was already shown above; the second is found elsewhere in the code:
... MOVL H, #0x72 MOVM H, #0x01 ; 370 FCNV H, H ...
That is, the FCNV Rd, Rs instruction takes the unsigned integer value stored in Rs and casts it to fp24. If you need a negative value, a further negation step using FNEG Rd, Rs is also necessary.
ENNCPU supports a range of floating point operations, which you can see in more detail in the ISA overview elsewhere on this page. In the Mandelbrot program, the following arithmetic instructions are used, in order of appearance:
FSUB D, H FDIV D, H FMUL G, D FADD G, H FCGT H, A
Notably, ENNCPU does not have a floating point equality conditional: all fp24 checks must be done either through a greater-than or smaller-than (FCGT Rd, Rs and FCLT Rd, Rs). This was chosen due to the low precision of the fp24 type, and programmers are instead advised to load a satisfactorily small epsilon value into a register and check for equality by a sequence of FSUB A, B and FCLT A, E (assuming E holds the epsilon).
Also appearing in the code are the floating point constant instructions, in the format of FC1/2/PI/... Rd that load a constant into a register. These are inspired by the x87 FLDPI and related instructions. These can sometimes be used to bypass loading from memory or moving an immediate through the int-to-float conversion pipeline. The only such instruction used in the Mandelbrot program is FC2 A. Others are described in the ISA overview.
Example code: Hello World!
For interfacing with the outside world, ENNCPU can either do syscalls that execute specific functions in its handler (platform-specific and more involved, but ultimately more powerful), or rely on the emulator's debugging facilities (debug instructions, pretty simple). Let's print "Hello World!" using debug instructions:
.ORG 0x0000 .SEC %PGA ADRL A, @STRING ADRM A, @STRING @LOOP LDRB B, A+ DBGC B JMNZO B, @LOOP MOV B, '\n' DBGC B ERR @STRING .ASCIZ Hello World! %PGA
The instruction DBGC Rs prints out the register content as a character/u8 to the debug console. It has siblings in DBGB/DBGW/DBGS/DBGF Rs that print out the value in the register as one of a few number formats.
The .ASCIZ directive embeds a string as a stream of bytes into the binary, terminated by a zero byte. In the current implementation of the assennbler, strings do not have to be put in quotes, and due to a logic bug in source parsing commas must be 'escaped' by a sequence of two commas: .ASCIZ This is 100% software-rendered,, from a string. Whitespace is collapsed in string literals (any sequence of spaces is treated as a single space), and trailing whitespace is ignored.
The MOV Rd, #imm8 instruction can also use single quotes to denote character literals; apart from the regular character set, ENNASM also includes special handling for newlines and tabs, escaped by a backslash.
Defines and Macros
ENNASM supports some relatively simple preprocessor defines and macros. These function as a simple string replacement: there are no functional macros. Defines replace a string with another string and can be useful to rename registers or input constants; macros insert a block of code with the appropriate numbered registers into the instruction stream. Both of these facilities operate before the assembler even sees the instructions, so disambiguations and aliases still work fine.
Defines are done with $def X Y, after which all instances of X will be replaced by Y. A define is removed using $undef X, after which replacement will not be done.
Macro blocks are opened with $macro NAME NR and closed with $endm. The name of a macro must be an alphanumeric string; the number count shows how many registers the macro takes. Instead of the register names [A, H], macro registers are instead named m0, m1, … m9 for however many registers the macro takes, allowing up to 10 register names. Macro bodies can also contain real register names, in case that a static register is required. A macro pseudo-instruction can be used like any other built-in instruction mnemonic, with the caveat that the name must be prefixed with an underscore. A simple example with defines and macros:
$def X A $macro int48add 3 ADD m0, m1 CGT m1, m0 ADD.P m2, #1 $endm .ORG 0x000 .SEC %PGA SUB A, #1 SUB B, #1 _int48add X, B, C ; expands to ADD A, B / ... $undef X %PGA
Due to the fact that macro expansion is simply string replacement, macro arguments can also be immediates or even instruction mnemonics. A good example of this is the following RGB888 to RGB565 macro I wrote:
; m0 is the target reg ; m1 - m3 are the colours ; m4 is a temp register $macro 888_TO_565 5 MOV m0, #0 MOVL m0, #m1 LSHR m0, #3 BOR m4, m0 MOVL m0, #m2 LSHR m0, #2 LSHL m4, #6 BOR m4, m0 MOVL m0, #m3 LSHR m0, #3 LSHL m4, #5 BOR m4, m0 ENDW m0, m4 $endm ; ... _888_TO_565 A, 0xff, 0x00, 0xff, B ; magenta
Once defined, a macro cannot be undefined.
General ABI Principles
To write larger programs in ENNASM, some degree of modularity is required. Most code written by me follows these principles:
- Shadow registers are all considered volatile (but the callee must swap back before returning)
- Light registers are callee-saved, except for arguments which are volatile
- Code reached from interrupts cannot use the shadow registry at all
The first two points ensure that no registers will get overwritten unpredictably by a called function, while the third ensures that interrupts cannot corrupt a function's state. Functions that do not use the shadow registry are thus interrupt-callable, assuming they also take adequate care of spilling the remaining registers onto the stack. Popping wantonly off the stack and grabbing the caller's own stack contents is, of course, ill-advised.
Syscalls and Interfacing
ENNCPU supports a unified interface for requesting functions from outside the "computation sandbox". These are called syscalls and are done using the instruction SYSCI #imm8. Unlike basically all other instructions, syscalls take arguments in fixed registers [ABCD], and write back to them without further CPU interaction. Syscall #0 is guaranteed to never work.
Currently, the only syscall implemented is SYSCI #1, which is the syscode for interfacing with DEVICEs. The CPU harness holds a vector of attached, enabled DEVICEs, starting at zero. Currently, ENNCPU can have up to 65535 DEVICEs (highly unlikely it will ever need more), and all DEVICEs implement a common interface and a set of unique functions. The arguments to SYSCI #1 are:
A : device number B : function number C : argument 1 D : argument 2
All DEVICEs guarantee that they will implement function #1 (A == 0x01), which is IDENTIFY(), in pseuco-C++ looking like:
void IDENTIFY(CPU* cpu)
{
cpu->ACTIVE_SET.A = 0x00xxxxxx; // name pt. 1
cpu->ACTIVE_SET.B = 0x00yyyyyy; // name pt. 2
cpu->ACTIVE_SET.C = 0x0000zzzz; // number of functions
return;
}
That is, after the syscall resolves, registers A and B will store up to 6 bytes corresponding to the DEVICE's name or identifier, and register C will store the number of functions the DEVICE implements. Functions on every DEVICE start at 1 and are numbered sequentially without any gaps; function #0 is guaranteed to return cleanly without change of CPU state. All implemented functions will also write back to register A their specific signature for executing correctly, which can be up to 6 bytes long.
The only DEVICE currently implemented is the DSKENN, the disk device, implementing three functions:
01 : IDENTIFY() 02 : TAKE_SECTOR(u32 which, u32 whence) -> A := 0x14 // write to disk 03 : GIVE_SECTOR(u32 which, u32 where) -> A := 0x24 // read from disk
The DSKENN is automatically instantiated when the emulator is provided a floppy disk image as an optional commandline argument; it currently handles only 1.44MB floppy disk images with 512B sectors, regardless of the filesystem implemented on them.
The function GIVE_SECTOR() will copy a sector of 512 bytes from the disk (indicated by the argument C = which) to the CPU's memory (at the address indicated by argument D = where), notifying the CPU with the signature 0x24. While DSKENN also exposes TAKE_SECTOR() which will correctly write back its signature (notifying the CPU that there was no error), DSKENN is currently a read-only interface.
Example use of a syscall to call the DSKENN DEVICE to give sector zero (boot sector) of a floppy image:
.ORG 0x0000
.SEC %PROG
MOV A, #0
MOV B, #1
SYSCI #1
ADRL C, @DSKENN_ID
ADRM C, @DSKENN_ID
LDRS D, C+
LDRS E, C+
CNE A, D
OR CNE B, E
FAR JMO.P @KILL
MOV A, #0
MOV B, #3 ; GIVE_SECTOR()
MOV C, #0 ; zeroth
ADRL D, @DESTINATION
ADRM D, @DESTINATION
SYSCI #1
CNE A, #0x24
FAR JMO.P @KILL
; reaching this point we can be sure
; that @DESTINATION holds 512 bytes of
; floppy disk contents
@KILL
ERR
@DSKENN_ID
.ASCII DSK
.ASCII ENN
%PROG
.ORG 0x0400
@DESTINATION
Hacks and Tricks
It's obvious that ENNASM is a program of questionable code quality and robustness; some things that might seem obvious to you are not really idiomatic or even easy to do in it. In the future, these shortcomings might even be fully fixed, but for the time being they remain an unavoidable part of the assembler. Nevertheless, there are ways to compensate for these limitations.
Stack manipulation
The stack pointer is not directly modifiable, but sometimes it is necessary to discard state off it. To avoid popping into a register (for example, if all registers are currently live), there exist the zero-argument instructions JUNKB, JUNKW, JUNKS, that discard the top value off the stack. Furthermore, there also exist zero-argument instructions to swap the top two elements of the stack without popping, these being SWVB, SWVW, SWVS. In case data needs to remain on the stack but is still needed, the one-argument PEEKB/PEEKW/PEEKS Rd will do the same as a pop but without advancing the SP.
Loading large immediates
As was mentioned before, ENNCPU immediates are generally limited to 6 or 8 bits, and ENNASM follows this limitation by generally disallowing immediates larger than an instruction's allowed width. Nevertheless, one immediate type can be used to bypass this: labels. As a label resolves to a number that is potentially up to 24 bits long, one can use the following trick to load a large immediate into a register:
.ORG 0x028A @FB_WIDTH ; ... MOVL A, @FB_WIDTH MOVM A, @FB_WIDTH
Since the .ORG directive and labels do not actually insert any code, and MOVx and ADRx are aliases of each other, you can use the label resolution scheme to load a register-sized immediate without having to manually break it down or use magic numbers in code.
Saving predication state
In many cases, predication state might be worthwhile to preserve. While it's not recommended to rely on predication state remaining the same across a subroutine call, saving the predication state is unavoidable when, for example, an interrupt is raised and execution is redirected. The instruction pair RPS/WPS does read the internal state register, but this contains a lot of different flags, and extracting the predication bit from it can get messy. Keeping in mind that predication state is preserved until a conditional instruction is hit, one can simply use the existing predication state to store something to the stack:
PSHS A MOV.P A, #1 PSHB A ; ... POPB A CEQ A, #1 POPS A
Instruction Set Overview
| Instruction | Full Name | Pseudo-C++ Equivalent |
|---|---|---|
𝑖𝑛𝑠𝑛.P ... |
predicated (any insn.) | if(FLAG) { /* ... */ }
|
ERR |
error | quit();
|
MOV Rd, Rs |
move register | Rd = Rs;
|
MOVL Rd, #imm8 |
move into low | Rd = (arg & 0xff);
|
ADRL Rd, @label
| ||
MOVM Rd, #imm8 |
move into mid | Rd &= 0xff00ff; Rd |= ((arg & 0xff) << 8); |
ADRM Rd, @label
| ||
MOVH Rd, #imm8 |
move into high | Rd &= 0x00ffff; Rd |= ((arg & 0xff) << 16); |
ADRH Rd, @label
| ||
GETL Rd, Rs |
get low | Rd = (Rs & 0x0000ff);
|
GETM Rd, Rs |
get mid | Rd = (Rs & 0x00ff00) >> 8;
|
GETH Rd, Rs |
get high | Rd = (Rs & 0xff0000) >> 16;
|
HALFA Rd, Rs |
get lower 12b | Rd = Rs & 0xfff;
|
HALFB Rd, Rs |
get upper 12b | Rd = (Rs >> 12) & 0xfff;
|
XCG Rd, Rs |
exchange registers | t = Rd; Rd = Rs; Rs = t;
|
INV Rd, Rs |
invert register | Rd = ~Rs;
|
SWAP |
swap with shadow registry | CURR_REG[] = SHADOW_REG[]; SHADOW_REG[] = OLD_REG[]; |
SWPR Rd |
swap single register | CURR_REG[Rd] = SHADOW_REG[Rd]; SHADOW_REG[Rd] = OLD_REG[Rd]; |
SHDW Rd |
store Rd in shadow |
SHADOW_REG[Rd] = CURR_REG[Rd];
|
LITE Rd |
get Rd from shadow |
CURR_REG[Rd] = SHADOW_REG[Rd];
|
RSP Rd |
read stack pointer | Rd = SP;
|
WSP Rd |
write stack pointer | SP = Rd;
|
ADD Rd, Rs/#imm6 |
add | Rd += Rs;Rd += imm6;
|
SUB Rd, Rs/#imm6 |
subtract | Rd -= Rs;Rd -= imm6;
|
MULA Rd, Rs |
multiply low | u64 temp = u64(Rd) * Rs; Rd = temp & 0x00ffffff; |
MULB Rd, Rs |
multiply high | u64 temp = u64(Rd) * Rs; Rd = (temp >> 24) & 0x00ffffff; |
DIV Rd, Rs |
integer divide | Rd /= Rs;
|
MOV Rd, Rs |
modulo/remainder | Rd %= Rs;
|
ENDW Rd, Rs |
reverse word endianness | Rd = (Rs_low << 8) | (Rs_mid >> 8);
|
ENDS Rd, Rs |
reverse sesqui endianness | Rd = (Rs_low << 16) | Rs_mid | (Rs_hi >> 16);
|
CEQ Rd, Rs/#imm6 |
check equal | FLAG = (Rd == Rs);
|
OR CEQ Rd, Rs/#imm6 |
if(Rd == Rs) { FLAG = true; }
| |
CNE Rd, Rs/#imm6 |
check nonequal | FLAG = (Rd != Rs);
|
OR CNE Rd, Rs/#imm6 |
if(Rd != Rs) { FLAG = true; }
| |
CGT Rd, Rs/#imm6 |
conditional greater | FLAG = (Rd > Rs);
|
OR CGT Rd, Rs/#imm6 |
if(Rd > Rs) { FLAG = true; }
| |
CGE Rd, Rs/#imm6 |
conditional greater-or-equal | FLAG = (Rd >= Rs);
|
OR CGE Rd, Rs/#imm6 |
if(Rd >= Rs) { FLAG = true; }
| |
CLT Rd, Rs/#imm6 |
conditional less than | FLAG = (Rd < Rs);
|
OR CLT Rd, Rs/#imm6 |
if(Rd < Rs) { FLAG = true; }
| |
CLE Rd, Rs/#imm6 |
conditional less-or-equal than | FLAG = (Rd <= Rs);
|
OR CLE Rd, Rs/#imm6 |
if(Rd <= Rs) { FLAG = true; }
| |
CAND Rd, Rs |
conditional AND | FLAG = (Rd && Rs);
|
COR RD, Rs |
conditional OR | FLAG = (Rd || Rs);
|
CNAND Rd, Rs |
conditional NAND | FLAG = !(Rd && Rs);
|
CNOR Rd, Rs |
conditional NOR | FLAG = !(Rd || Rs);
|
BAND Rd, Rs/#imm6 |
bitwise AND | Rd = (Rd & Rs);
|
BOR Rd, Rs/#imm6 |
bitwise OR | Rd |= Rs;
|
BXOR Rd, Rs |
bitwise XOR | Rd ^= Rs;
|
BNOR Rd, Rs |
bitwise NOR | Rd = ~(Rd | Rs);
|
SET Rd, #imm6 |
set bit | Rd |= (1 << arg);
|
CLR Rd, #imm6 |
clear bit | Rd &= ~(1 << arg);
|
TGL Rd, #imm6 |
toggle bit | Rd ^= (1 << arg);
|
CBIT Rd, #imm6 |
test bit | FLAG = (Rd & (1 << arg));
|
POPC Rd, Rs |
popcount | Rd = std::popcnt(Rs);
|
PARI Rd, Rs |
bit parity | Rd = std::popcnt(Rs) % 2;
|
LEAD Rd, Rs |
count leading zeros | Rd = std::countl_zero(Rs);
|
TAIL Rd, Rs |
count tailing zeros | ... |
PSHI #imm8 |
push immediate | SP -= 1; PUSH8(imm8);
|
PSHB Rs |
push byte | SP -= 1; PUSH8(Rs);
|
POPB Rd |
pop byte | Rd = POP8(); SP += 1;
|
PSHW Rs |
push word | SP -= 2; PUSH16(Rs);
|
POPW Rd |
pop word | Rd = POP16(); SP += 2;
|
PSHS Rs |
push sesqui | SP -= 3; PUSH24(Rs);
|
POPS Rd |
pop sesqui | Rd = POP24(); SP += 3;
|
SVWB |
swerve bytes | t1 = POP8(); t2 = POP8(); PUSH8(t1); PUSH8(t2);
|
SWVW |
swerve words | ... |
SWVS |
swerve sesquis | |
LDRB Rd, Rs |
load byte | Rd = READ8(Rs);
|
LDRW Rd, Rs |
load word | Rd = READ16(Rs);
|
LDRS Rd, Rs |
load sesqui | Rd = READ24(Rs);
|
STRB Rs, Rd |
store byte | STORE8(Rd, Rs & 0xff);
|
STRW Rs, Rd |
store word | STORE16(Rd, Rs & 0xffff);
|
STRS Rs, Rd |
store sesqui | STORE24(Rd, Rs & 0xffffff);
|
LDRx Rd, -Rs |
load with predecrement | Rs -= x; Rd = READx(Rs);
|
LDRx Rd, +Rs |
load with preincrement | Rs += x; Rd = READx(Rs);
|
LDRx Rd, Rs- |
load with postdecrement | Rd = READx(Rs); Rs -= x;
|
LDRx Rd, Rs+ |
load with postincrement | Rd = READx(Rs); Rs += x;
|
STRx Rs, ±Rd |
store with preinc/dec. | ...
|
STRx Rs, Rd± |
store with postinc/dec. | ...
|
PSH2W Rs1Rs2 |
push two words | SP -= 2; PUSH16(Rs1); SP -= 2; PUSH16(Rs2); |
PSH2S Rs1Rs2 |
push two sesquis | SP -= 3; PUSH24(Rd2); SP -= 3; PUSH24(Rd1); |
POP2W Rd1Rd2 |
pop two words | Rd1 = POP16(); SP += 2; Rd2 = POP16(); SP += 2; |
POP2S Rd1Rd2 |
pop two sesquis | Rd1 = POP24(); SP += 3 Rd2 = POP24(); SP += 3; |
LD2S Rd1Rd2, Rs |
load two sesquis | Rd1 = READ24(Rs); Rd2 = READ24(Rs + 3); |
LD2S Rd1Rd2, -Rs |
load with predecrement | Rs -= 6; Rd1 = READ24(Rs); Rd2 = READ24(Rs + 3); |
LD2S Rd1Rd2, Rs+ |
load with postincrement | Rd1 = READ24(Rs); Rd2 = READ24(Rs + 3); Rs += 6; |
STR2S Rs1Rs2, Rd |
store two sesquis | STORE24(Rd, Rs1); STORE24(Rd+3, Rs2); |
STR2S Rs1Rs2, -Rd |
... | ... |
STR2S Rs1Rs2, Rd+ |
... | ... |
FCNV Rd, Rs |
convert to float | Rd = f24(Rs);
|
FCST Rd, Rs |
cast from float | Rd = u24(Rs);
|
FC0 Rd |
fp24 constant zero | Rd = 0.0;
|
FC1 Rd |
fp24 constant one | Rd = 1.0;
|
FC2 Rd |
fp24 constant two | Rd = 2.0;
|
FCSQ2 Rd |
fp24 constant √2 | Rd = 1.414213562;
|
FCPHI Rd |
fp24 constant φ | Rd = 1.6180339;
|
FCPI Rd |
fp24 constant π | Rd = 3.1415926;
|
FTAU Rd |
fp24 constant τ | Rd = 6.283185;
|
FCE Rd |
fp24 constant e | Rd = 2.718282;
|
FNEG Rd, Rs |
floating point negate | Rd = -f24(Rs);
|
FABS Rd, Rs |
floating point abs. value | Rd = std::abs(Rs);
|
FREC Rd, Rs |
floating point reciprocal | Rd = 1.0/Rs;
|
FADD Rd, Rs |
floating point add | Rd = f24(Rd) + f24(Rs);
|
FSUB Rd, Rs |
floating point sub | Rd = f24(Rd) - f24(Rs);
|
FMUL Rd, Rs |
floating point mul | Rd = f24(Rd) * f24(Rs);
|
FDIV Rd, Rs |
floating point div | Rd = f24(Rd) / f24(Rs);
|
FMOD Rd, Rs |
floating point remainder | Rd = f24(Rd) % f24(Rs);
|
FABS Rd, Rs |
floating point absolute value | Rd = std::abs(f24(Rs));
|
FSQT Rd, Rs |
floating point square root | Rd = std::sqrt(f24(Rs));
|
FLOG Rd, Rs |
floating point logarithm | Rd = std::log(f24(Rs));
|
FSIN Rd, Rs |
floating point sine | ... |
FCOS Rd, Rs |
floating point cosine | |
FTAN Rd, Rs |
floating point tangent | |
FASIN Rd, Rs |
floating point arcsine | |
FCGT Rd, Rs |
floating point greater than | FLAG = f24(Rd) > f24(Rs);
|
FCLT Rd, Rs |
floating point smaller than | ... |
RET |
return from jump-link | IP = POP24();
|
JMA @label/#imm9 |
jump absolute (to first 512B) | IP = label_addr;
|
JLA @label/#imm9 |
jump-link to first 512B | PUSH24(IP); IP = label_addr; |
JxO @label/±#imm5 |
jump with offset | IP += i6(arg);
|
JxR Rs |
jump to register | IP = Rs;
|
JxZO Rt, @label/±#imm5 |
jump with offset if zero | if(Rt == 0) { IP += i6(arg); }
|
JxZR Rt, Rs |
jump to register if zero | if(Rt == 0) { IP = Rs; }
|
JxNZO Rt, @label/±#imm5 |
jump with offset if nonzero | if(Rt) { IP += i6(arg); }
|
JxNZR Rt, Rs |
jump to register if nonzero | if(Rt) { IP = Rs; }
|
FAR JMO @label/±#imm13 |
far jump with offset | IP += i14(arg);
|
FAR JLO @label/±#imm13 |
far jump-link with offset | PUSH24(IP); IP += i14(arg); |