I might make some mistakes here, and since the DCPU-16 specs aren't final yet, some of the information might become outdated. If you notice any errors, or there's something that I've made too complicated (which I unfortunately have a tendency to do), please inform me and I will do my best to correct it.
UPDATE 1: I have fixed some mistakes, and written a section on math operators (+ example).
UPDATE 2: I have decided to move the tutorial to the 0x10c Wiki instead. This way, those of you who are already knowledgeable in DCPU-16 assembly can help me improve this tutorial by directly editing that article instead. I will probably still be updatign this post from time to time, though.
UPDATE 3: New sections on conditionals, labels, jumping and subroutines.
UPDATE 4: New section on the stack, as well as updated sections ("introduction" and "subroutines"). Updates may come less frequently due to much school work etc., but I assure you, they will come, just don't expect this to be finished immediately.
First of all, you need an assembler to create machine code, and an emulator to run the program on (until the game is released to the public). You can find a list of such tools here, but I've been using DCPU-16 Studio myself, and this is the tool I am going to describe how to use in this tutorial. If you do not want to download any programs, you can also try this online DCPU-16 emulator.
Before you start this tutorial, I will assume that you have some basic knowledge of binary and hexadecimal numbers. If you do not know what these two are, I will not go into detail in this tutorial, but I would recommend you to google it beforehand, or look at this article in the 0x10c wiki.
DCPU-16 is built up of 16-bit unsigned words, which is a 16-digit binary number (or 4-digit hexadecimal, or a whole number between 0 and 65,535). It consists of 65,536 such words of RAM, and 11 so-called "registers".
There are 8 regular registers, and 3 special registers. The 8 regular registers (named A, B, C, X, Y, Z, I and J) can be thought of as "temporary memory locations" where you can store words while you do stuff to them, for instance, if you want to compute the sum of two numbers. The program counter (PC) tells the CPU where in the CPU it is, and we can use it to jump back and forth in our program. I will come back to the other two, the stack pointer (SP) and the overflow register (O), later, but for now, you don't have to worry about them.
The rest of the RAM in the DCPU-16 is used to both store instructions that tells the DCPU-16 what to do, to store data, such as texts or images, and to communicate with other devices, such as the keyboard, the display and the ship itself. Generally, all memory is treated alike, so there are no restrictions as to where you can store data and where you can store instructions, with exception of communication with other devices, which happens within special ranges of memory.
Each of the 65,536 words in the RAM are numbered from 0 to 65,535 (or 0xFFFF in hexadecimal notation). The "number" that represents the location of a specific word in memory, is often called an address.
A simple adder
Let's start with a simple program that adds two numbers, say, 72 and 54:
;; Basic DCPU adder ;; by Fred SET A, 72 ; Store 72 in A SET B, 54 ; Store 54 in B ADD A, B ; Add B to A :end SET PC, end
In DCPU-16 Studio, you can generate machine code by selecting "Assemble" or by pressing F9.
When you do this, the program is assembled into machine code and loaded into the CPU memory. Each instruction takes up one word, or sometimes two or even three words, depending on its type. Then it starts the program at address 0.
Let's go through it step by step:
;; Basic DCPU adder ;; by Fred
This is a comment. Comments in DCPU-16 begin with a semi-colon. Every time DCPU-16 see a semi-colon, it will ignore the rest of the line. This is very useful to make your code more readable to yourself and others.
SET A, 72
This is the SET instruction, which stores a value somewhere. It takes two arguments: the first is where you want to store it, and the second is what you want to store. In this case, it takes the number 72 and stores it in register A.
In DCPU-Studio, you can select "Single Step" or by pressing F7 to run a single instruction at a time. This way, you can see exactly what your program does. If you run this instruction, you should see that the A field under "Registers" will turn yellow, which tells you that it has changed, and its value will become 72.
SET B, 54
This is the same as the above: it takes the number 54 and stores it in register B.
ADD A, B
This is the ADD instruction, which adds one number to another. In other words, it takes the value of A and the value of B, adds them together, and stores the result in A.
In this case, it takes 72 (the value of A) and 54 (the value of B) and adds them together, which yields 126. Then it stores this number in A, so that A is now 126.
:end SET PC, end
These two lines cause the program to go into an infinite loop. The reason why we do this is that otherwise, the program would continue to run in RAM after it has finished, can cause unexpected behavior. Loops are explained in the Labels and jumping section below.
More math! Yay!
Let's have a quick look at the other math operators in DCPU-16. Note that all the math operators will store the result in the first argument you pass to them (which is A in the examples).
Basic math operators
ADD A, B ; adds B to A SUB A, B ; subtracts B from A MUL A, B ; multiplies A by B DIV A, B ; divides A by B MOD A, B ; divides A by B, and give the remainder
ADD, SUB and MUL work exactly as you would expect them to: they add, subtract or multiply A and B, and store the result in A.
DIV divides A and B and stores the result in A. But, you might say, what about fractions? Since DCPU-16 can only operate on whole numbers, what would happen if we tried to compute 8 / 3? The answer is that it rounds down, and effectively discards the fractional part of the result, so that dividing 8 by 3 would yield 2 as the result (8 / 3 = 2.66..., rounded down).
But this causes another problem: since DIV discards the fractional part of the number, how can we find it? The answer to this is the MOD instruction, which in mathematics often is denoted with as A % B. It divides A by B, but instead of taking the answer and rounding it down, it takes the remainder - that is, what it "left over" after dividing. For instance, if you try to compute 8 % 3, you would get 2 (since 8 / 3 = 2, and 2 * 3 = 6, this means that the remainder that got lost during dividing is 8 - 6 = 2).
Binary math operators
In addition to the five basic math operators, there are also five binary math operators:
SHL A, B ; shift A left by B bits SHR A, B ; shift A right by B bits AND A, B ; "and" of A and B BOR A, B ; "or" of A and B XOR A, B ; "exclusive or" of A and B
To understand these operators, we need to look at the numbers in their binary form.
The SHL and SHR instructions takes the value of A and shifts it B bits to the left or right. For instance:
SET A, 10 ; A = 0000 1010 SHL A, 1 ; A = 0001 0100, 1 bit left SHL A, 1 ; A = 0010 1000, 1 bit left SHR A, 1 ; A = 0001 0100, 1 bit right SHR A, 2 ; A = 0000 0101, 2 bits right
SHL A, B is equvivalent to multiplying A by 2B, while SHR A, B is equivalent to dividing by 2B.
The AND ("and") instruction gives us all the bits that are 1 in both A and B:
SET A, 10 ; A = 1010 (10) SET B, 3 ; B = 0011 (3) AND A, B ; A = 0010 (2)
The BOR ("or") instruction gives us all the bits that are 1 in either A or B:
SET A, 10 ; A = 1010 (10) SET B, 3 ; B = 0011 (3) BOR A, B ; A = 1011 (11)
The XOR ("exclusive or") instruction gives us all the bits that are 1 in either A or B, and 0 in the other:
SET A, 10 ; A = 1010 (10) SET B, 3 ; B = 0011 (3) XOR A, B ; A = 1001 (9)
Example: Odd or even?
In this program, we will set A and B to two numbers, and see if the sum of them is odd or even. If it is odd, the C register will be 1, otherwise it will be 0.
; Set A and B to our numbers ; You may change these and see ; how the outcome will change SET A, 33 SET B, 12 ; Compute A % 2, which is 0 if ; even and 1 if odd MOD A, 2 ; Do the same to B MOD B, 2 ; Copy the value of A to C SET C, A ; Since the sum of two numbers ; is only odd if one number is ; even and the other is odd, ; we can use XOR here: ; Get the exclusive or of C ; (which is now the same as A) ; and B XOR C, B ; Finish :end SET PC, end
The stack acts as a sort of temporary memory storage for your program. It begins at the end of your program's RAM (at address 0xFFFF), and goes backwards from there.
You can think of the DCPU-16 stack as analogous to a stack of plates, where each plate represents a 16-bit word:
- We can "push" plates to the top of the stack.
- We can "pop" plates off the top of the stack.
- We can "peek" at the plate at the top, without removing it.
These are also the three operations we can do to the DCPU-16 stack.
The first operation, PUSH, adds a word to the top of the stack:
; stack is now: (empty) SET PUSH, 1 ; stack is now:  SET PUSH, 2 ; stack is now: , 1 SET PUSH, 3 ; stack is now: , 2, 1
The second operation, PEEK, gives us the word currently on the top of the stack:
; stack is now: , 2, 1 SET I, PEEK ; I is now 3 ; stack is now: , 2, 1
The third operation, POP, gives us the word currently on the top of the stack, and then removes it:
; stack is now: , 2, 1 SET A, POP ; A is now 3 ; stack is now: , 1 SET B, POP ; B is now 2 ; stack is now:  SET C, POP ; C is now 1 ; stack is now: (empty)
Conditionals and program jumping
So far, the programs we have looked at have only one execution path: from start to end. But what if you wanted your program to behave differently based on different kinds of inputs? This is where the conditional instructions come in handy:
IFE A, B ; if A and B are equal... IFN A, B ; if A and B are not equal... IFG A, B ; if A is greater than B... IFB A, B ; if A AND B (bitwise AND) is nonzero...
These will tell the CPU to perform the next instruction if and only if the condition is true, otherwise it will skip it.
Labels and jumping
In the first program, you saw this piece of code at the end:
:end SET PC, end
Here, :end is a label. A label is a name that you can give to a specific instruction. When you use this name in your assembly code, it will be replaced with the address of the next instruction after the label (in this case, "SET PC, end").
The program counter (PC), as aforementioned, tells the CPU where in the program the CPU is, or more specifically, the address of the next instruction. We can alter the value of this to skip to another part of the program.
In the piece of code above, the program will set the program counter to the instruction itself. Now the CPU runs this instruction again, and it continues to do this indefinitely.
We can combine the conditionals and the label jumping to create a loop that actually ends, like this piece of code:
SET I, 0 ; Initialize the value of I to 0 before we run our loop :loop ; Label that marks the start of the loop IFE I, 10 ; If I has reached 10, we want to exit the loop SET PC, loop_end ; so we jump to the label 'loop_end' ; we can do something in the loop here if we want to ADD I, 1 ; Make sure to increase the value of I, so that it ; will actually reach 10 sometime SET PC, loop ; Return to the label 'loop' :loop_end ; Label that marks where the program will continue after the loop SET PC, loop_end ; Infinite loop to end the program
We can also use the conditional instructions and jumping to create more complex conditionals:
SET A, 4 ; Set our input IFE A, 0 ; If A == 0, we want to jump to label 'a_is_zero' SET PC, a_is_zero IFE A, 1 ; If A == 1, we want to jump to label 'a_is_one' SET PC, a_is_one IFE A, 2 ; If A == 2, we want to jump to label 'a_is_two' SET PC, a_is_two SET PC, end ; If A is anything else, we can put what we want to do ; here :a_is_zero ; What we want to do if A == 0 ADD B, 1 SET PC, end :a_is_one ; What we want to do if A == 1 ADD B, 2 SET PC, end :a_is_two ; What we want to do if A == 2 ADD B, 4 :end SET PC, end ; Finish the program
A subroutine is a piece of code which will be run, and then return to where it was called from. In other words, it's a jump that will return when it has finished.
A subroutine is defined like this:
:my_subroutine ; Subroutine name ; what to do SET PC, POP ; Return to where the subroutine was called
And it is called like this:
JSR my_subroutine ; Call subroutine 'my_subroutine'
When a subroutine, the current position of the program counter (PC) is pushed to the stack, and is then changed to the address of the first instruction in the subroutine. When the subroutine is complete, it will call
SET PC, POP
to get the address of the instruction from where it was called, and return execution from there.
For example, a program that calculates A2 - 1:
SET A, 4 ; Our input JSR square ; Call the 'square' subroutine SUB A, 1 ; Subtract 1 :end SET PC, end :square ; Subroutine name MUL A, A ; Multiply A by itself (squaring) SET PC, POP ; Return
To be continued...