Return to Menu

= DUO ULTIMATE ASSEMBLER AND EMULATOR =

Assembly code:


Machine code:




Display:

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).

Unyielding commands cannot be embedded and always have the same number of arguments.
Yielding commands may be embedded, in which case they will return a value. If they are unembedded, they may accept a RAM destination address as an extra argument and write their output to that address.


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:

NOT 11000011 7
SVM 0 0 [7]

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.

# Stores 5 in address 0, since 2 + 3 = 5.
ADD 2 3 0
# Stores 9, since 13 - 4 = 9.
SUB 13 4 0
# Stores 1, since 8 is equal to 8.
EQU 8 8 0
# Stores 0, since 3 is not equal to 16.
EQU 3 16 0
# Stores 1, since 5 is unequal to 7.
UNE 5 7 0
# Stores 0, since 24 is not greater than 39.
GRE 24 39 0
# Stores 00000001, the 6th bit in 01000100.
BGT 01001000 5 0
# Stores 00101111, the result when the 3rd bit in 00001111 is changed into a 1.
BON 00001111 2 0
# Stores 0, since 0 AND 1 is 0.
AND 0 1 0
# Stores 00010000, since 00110000 AND 01010000 is 00010000.
AND 00110000 01010000 0
# Stores 1, since 0 OR 1 is 1.
EIT 0 1 0

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.

# Stores 86 in address 0.
GVL 86 0

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:

# Stores 11001100 in RAM address 2.
GVL [NOT 00110011] 2
# Stores 11001100 in RAM address 2.
NOT 00110011 2

# Stores 9 (15 - 6) in RAM address 2.
GVL [SUB 15 6] 2
# Stores 9 (15 - 6) in RAM address 2.
SUB 15 6 2

# Moves the data from address 1 in RAM to address 2.
GVL [RRM 1] 2
# DOES NOT WORK!!!
RRM 1 2

"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:

# Moves the data from address 1 in RAM to address 2.
GVL [RRM 1] 2
# Moves the data from address 1 in RAM to address 2.
GVL [1] 2

Both [RRM 1] and [1] return the data stored in RAM address 1. The fundamental differences:

The computer can interpret [1] much faster, and bytewise it requires less memory.
Only RRM can be used to perform array based RAM operations. For example, [RRM [97]] returns the data in RAM stored in the address which in turn is stored in address 97 in RAM.


After pixels have been drawn with SVM, they may be read with the yielding command GVM (get VRAM):

SVM 0 0 11100101
# Copies the 8 pixels from one place to another.
GVM 0 0 25
SVM 0 5 [25]

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.

SBB 1
# The boolean bit is 1, so the command below IS executed.
?SVM 0 0 10101010
SBB 0
# The boolean bit is now 0, so the command below is NOT executed.
?SVM 0 0 11111111

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.

# Stores 10010010 in address 1373 (5 * 256 + 93) of main memory.
SMM 10010010 5 93
# Recalls the data in address 1373 (5 * 256 + 93) of main memory
# and stores the data in address 0 of RAM.
GMM 5 93 0

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.

# Store 0 in RAM address 0.
GVL 0 0
# Execution skips to the command at address 13 in main memory.
GO2 0 13
# This command is skipped.
ADD [0] 1 0
# This command happens to be at address 13.
ADD [0] 1 0
# The data at RAM address 0 is now 1, since it was only incremented once.

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:

# Store 0 in RAM address 0.
GVL 0 0
# Execution skips to the label.
GO2 +SKIP_LABEL SKIP_LABEL
# This command is skipped.
ADD [0] 1 0
# Execution continues at this label.
SKIP_LABEL
ADD [0] 1 0
# The data at RAM address 0 is now 1, since it was only incremented once.

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:

SVM 0 0 11111111
# Resets the timer.
STM 0 0
LOOP_START
# Stores the less significant byte of the timer in RAM address 0.
GTM 1 0
# If the timer is greater than 128, exit the loop.
SBB [GRE [0] 128]
?GO2 +EXIT_LOOP EXIT_LOOP
# Otherwise, continue looping.
GO2 +LOOP_START LOOP_START
EXIT_LOOP
SVM 0 1 11111111

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:

# Stores the last input in RAM address 0. The first argument determines
# which of the 2 bytes to access.
GIN 0 0
LOOP_START
# Stores the data from the input register in RAM address 1.
GIN 0 1
# If the input register has changed, display what it is now.
SBB [EQU [0] [1]]
?GO2 +LOOP_START LOOP_START
GVL [1] 0
SVM 0 0 [0]
GO2 +LOOP_START LOOP_START

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:

# Function 175 accepts 2 arguments.
FN1 175 2
# Defines the location of function 175's code block.
FN2 175 +FUNCTION_BLOCK FUNCTION_BLOCK
# Calls the function 3 times. As a result, 9 blobs are drawn on the display.
F175 0 5
F175 1 6
F175 2 7
# Effectively stops the program.
END
GO2 +END END

# The function code is defined here.
FUNCTION_BLOCK
# The function's arguments are stored in RAM addresses 1 and 2.
# Recall that arguments are right justified!
SVM [1] [2] 11011011
# This command terminates the function.
GVL 0

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.

# Defines the function.
FN1 226 1
FN2 226 +ADD_3 ADD_3
# Displays the result of calling the function with various arguments.
SVM 0 0 [F226 2]
SVM 1 0 [F226 5]
SVM 2 0 [F226 89]
SVM 3 0 [F226 14]
# Effectively stops the program.
END
GO2 +END END

# The function code is defined here.
ADD_3
# Terminates the function AND sets the return value.
GVL [ADD [2] 3]

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.

# Defines a function.
FN1 139 1
FN2 139 +SET_RAM SET_RAM
# Stores data in two RAM addresses. The first is local (< 128),
# the second is global (> 127).
GVL 00000001 20
GVL 00000001 200
# Displays the data in those addresses.
SVM 0 0 [20]
SVM 0 5 [200]
# Calls the function.
F139 11111111
# Displays the RAM data again.
SVM 2 0 [20]
SVM 2 5 [200]
# Ends the program.
END
GO2 +END END

# The function's code block.
SET_RAM
# Stores new data in the RAM addresses.
GVL [2] 20
GVL [2] 200
# Displays the data in the addresses.
SVM 1 0 [20]
SVM 1 5 [200]
# Ends the function.
GVL 0

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.

Return to Menu

Return to the Ostracod Pond