Note: Some machine code syntax in the emulator is not evaluated as it will be in the final machine. In other words: save your assembly code, not your machine code!
= DUO ASSEMBLY GUIDE =This description gives the details of DUO assembly syntax and commands.
Each command begins with an opcode or a function name. Opcodes have 3 letters. Examples of opcodes include SVM and GVL. Function names consist of the letter F followed by a number between 128 and 255 (inclusive). Examples of function names include F130 and F219.
There are between 1 and 3 arguments (inclusive) after each opcode or function name. Spaces separate opcodes, function names, and arguments in a command. Newlines separate commands. An example command is shown below:
BON 10101010 4 259
Arguments can be given in a variety of forms:
There are two general classes of opcodes: those which can modify RAM or return a value (called yielding commands), and those which do not (called unyielding commands).
An example of an unyielding command is SVM (set VRAM). This command accepts a 2 byte VRAM address and 1 byte to write in that address. This effectively displays a group of 8 pixels on the display. To try this command for yourself, copy the code below, paste it in the "assembly code" box, press "compile", then press "execute". You should see 4 white dots on the display.
SVM 8 5 01010101
The VRAM address given is 8 5. To find which byte is being modified, we multiply the first number (8) by 256 and add the second number (5). The result of 8 * 256 + 5 is 2053. This means that the 2054th byte in VRAM is being modified (addresses always start at 0). Each display line contains 256 pixels, which are represented by 32 bytes in VRAM. 2053 / 32 = 64 remainder 5, so the command above alters the 6th byte of the 65th row on the display.
An example of a yielding command is NOT. If the command is embedded, it accepts only one argument: a byte which it will invert and return to the parent command. An example of such a usage is shown below.
SVM 0 0 [NOT 01101110]
The embedded command NOT 01101110 inverts the byte 01101110 and returns 10010001 to the parent command. This effectively makes the parent command SVM 0 0 10010001; If you compile and execute the code above, you will find that it displays three small dots instead of two large dots.
When the NOT command is not embedded, it may accept an extra last argument. This extra argument is a RAM address; it determines where in RAM to store the output of the command. Examine the example below:
The first command inverts the byte 11000011 and stores the result in RAM address 7. The second command calls the data at that address by using brackets ([7]), then displays that byte on the screen. This command is effectively the same as SVM 0 0 00111100. The result is that a single blob is displayed instead of two smaller blobs.
On this page you will find a list of all the commands in DUO assembly. Notice how they are categorized by whether commands are unyielding or yielding.
In addition to NOT, there are many yielding commands which perform a calculation on a set of data. The behavior of these commands is demonstrated below.
The most basic yielding command is GVL (get value). It works like the commands above, except it does not process the input data in any way.
The RRM (return RAM) command returns the data at a given address in RAM. This command is strange because it has only one functional version, despite the fact that it is yielding. Observe the example below:
"Normal" commands, such as NOT and SUB, may either return a value to GVL or may directly write their output to RAM. RRM, on the other hand, will cause the computer to self destruct when you try to use it to directly store data in RAM. This is because all ALU operations occur in 1 clock step, and it is impossible to write from RAM into RAM in one step.
It is noteworthy that the two commands below are functionally equivalent:
Both [RRM 1] and [1] return the data stored in RAM address 1. The fundamental differences:
After pixels have been drawn with SVM, they may be read with the yielding command GVM (get VRAM):
A special register called the boolean bit register is reserved for conditionally inhibiting commands. When an opcode or function name is preceded by a question mark (?), it will not be executed if the boolean bit is 0. The unyielding command SBB (set boolean bit) is used for altering the boolean bit register. It accepts only 1 argument: the bit to store in the register. The example below shows how commands may be conditionally inhibited. Pound symbols (#) are used to denote comments.
All commands and long-term data are stored in 64 kilobytes of main memory. Unlike RAM, this memory is preserved after power to the machine is cut off. Using SMM (set main memory) and GMM (get main memory), a program can modify any byte in main memory.
The computer has a counter which determines the address of the current command to be executed in main memory. The value of this counter may be directly set by GO2 (goto). This command effectively skips program execution to a specified location.
Using main memory addresses directly is a somewhat cumbersome way to program, since they may change if the program is modified. To remedy this problem, the assembler supports the usage of labels. The program below is functionally identical to the one above:
Label names may not contain any spaces, must contain letters, and may not be a keyword recognized by the assembler. A label's address in main memory is declared by placing it on its own line. Main memory addresses are 2 bytes long; to access the more significant byte of a label's address, a plus sign (+) is placed before the label name. When there is no plus sign, the less significant byte is accessed.
The computer contains another 2 byte counter, called the timer, which provides a steady pace for programs when necessary. It increments at a constant rate of 64 times per second. The STM (set timer) and GTM (get timer) commands are used to get and set the value of the timer:
This program will make a line, wait 2 seconds, then make the line longer. The program works by entering a loop in which it constantly checks to see if the timer has exceeded 128 beats. Once this is true, the loop terminates and a command is executed to extend the line.
The timer is particularly useful when creating music to play through the computer's speakers. Unfortunately, the SNT (set note) command does not work in this JavaScript emulator.
The GIN (get input) command lets the program read keyboard strokes. Whenever the user presses or releases a key, the 2 byte input register is modified. The GIN command accesses the last value stored in this register:
When the program is running, it will display the contents of the input register each time the user presses or releases a key.
One of the most distinctive properties of the DUO Ultimate is that its machine code supports user defined functions. The FN1 (function definition 1) and FN2 (function definition 2) commands are used to define a function. FN1 defines how many arguments the function accepts. These arguments are stored in RAM addresses 0 through 2. Note that arguments are right justified, meaning that the last argument is stored in address 2, the second to last argument is stored in address 1, and the third to last argument is stored in address 0. FN2 defines the main memory address of the function's code block. A function call is terminated when a GVL command is called WITHOUT a RAM destination. This command also determines the return value of the function. The example below shows how a function can be defined and used:
The function defined here has the number 175; it is called with the term F175. The arguments after F175 are stored in RAM addresses 1 and 2, and command execution skips to the function's code block. The command SVM [1] [2] 11011011 uses the arguments stored in RAM to display pixels in a given location. The GVL 0 command terminates the function by returning 0. Command execution then skips back to where it was previously.
When a function call is embedded in a command, the return value of the function is used as one of the parent command's arguments. The example below shows this property in action.
The function defined above will add 3 to the given argument and return the value. Note how F226 is embedded in the SVM commands.
Throughout this text I have been keeping a secret about RAM. The truth is that all RAM addresses less than 128 are local and those greater than 127 are global. If data is stored in a local address, it is ONLY visible to the function which stored it. Data stored in global addresses, on the other hand, are visible to ANY function. The example below demonstrates how this property works.
The end behavior of the program looks something like this:
The left column represents data stored in address 20 (a local variable), and the right column shows data stored in address 200 (a global variable). Both variables begin with a value of 00000001. During the function call, both variables are set to a value of 11111111. When the function terminates, the global variable retains the value 11111111, but the local variable assumes its previous value of 00000001.
If you still have questions about DUO assembly, please email me at esperantanaso at gmail.