45M is a tiny virtual console. It can execute sequences of bytes that have a special meaning. These sequences are called "ROMs" and the bytes "machine code". To make writing ROMs easier for Humans, 45M also has an assembly language. The language exposes special words that translate directly to machine words. These are called mnemonics. In this book, we will learn about the mnemonics to build simple games with 45M.
If you want to follow along, you can open the 45M editor, or have a text file open. Every 45M program fit in a single file. Now let's start writing some 45M.
All 45M ROMs must start with `0 0` or other numbers. We will learn why soon.
0 0 LIT 1 INC
There is a lot going on in the above. Let's work through it.
First, LIT is an instruction that tells 45M to push the next number into the parameter stack.
Then, INC is another instruction that increases by one the top of the parameter stack.
If you haven't done so, execute the above program. You should see that the pstack now has 02 stored in it.
Now that we've seen the stack, let's see how we can manipulate it.
LIT lets you push the next number to the stack. LIT2 lets you push the next two numbers:
LIT2 5 5
The above is the same as LIT 5 LIT 5, but saves one byte.
You can read about the other instructions in the glossary. Let's look at DUP. It says that the purpose is the "Duplicate the top of the stack". There's another column called "Effect" which says:
n -- n n
This is a way to indicate the effect of DUP on the stack. DUP requires a number to be on the stack. This number is the "n" represented on the left of "--". After its execution, the stack now has "n" twice.
Learning how to read stack effects is a great way to quickly see what parameters an instruction needs on the stack, and how it will modify the stack.
All numbers are expressed in hexadecimal.
LIT f \ push the decimal value 15 to the stack
It's possible to represent a number in binary.
LIT #11111111 \ push the decimal value 15 to the stack
Let's add two numbers together:
LIT2 1 1 ADD
Now, let's multiply the result by two:
LIT 2 MUL
How about we divide it by 4?
LIT 4 DIV
If you ever need a random number, you can use the RND instruction which will place on the stack a random number in the 0 255 range.
When your program gets assembled, every mnemonic gets mapped to a corresponding machine code value, which fits in a byte. Since every instruction fits in a single byte, we can refer to a particular place in your ROM by its offset. In 45M, this offset is called an address.
Imagine the following program:
0 0 LIT2 5 5 ADD
The address of the LIT2 instruction is 2. The address of the ADD instruction is 5. Let's now look at the following program:
0 0 start: LIT2 5 5 ADD
In the above code, we added a label called start. It could have been called anything else. When 45M's preprocessor finds a label, it remembers its name and location, so that we can use this location later on.
Let's look at the following program:
0 0 LIT @start JMP start: LIT2 5 5 ADD BRK LIT 2
Let's try to understand what happens. When executed, the Instruction Pointer (IP) starts at address 2. It finds LIT, so it pushes the number that follows it into the stack. In this case, @start represents the memory address of the start label, which is 5. Our stack now has a 5 in it. Then it finds a JMP instruction. The JMP instruction tells the IP to take the value of the top of the stack, so 5, and to carry on. Our program then executes the LIT2 instruction, then the ADD one, and finally BRK. BRK is a special instruction that stops the execution of the program. Meaning that the remaining LIT 2 instruction won't be executed.
There are different ways to jump. For example, JMR will store the position of the next instruction in a special stack called the return stack. This allows to resume the execution with the RET instruction.
The screen on 45M is 16x16 pixels, meaning that a screen position can be encoded in one byte. The address of the top left pixel is 00, and the address of the bottom right if ff. The high nibble represents the rows, and the low nibble the columns. For example, the pixel at the 4th row and 10th column is 4a.
Pixels are 1 byte values that encode a color. The format used is RGB332. Let's look at the following example:
1 1 1 0 0 0 0 0
We know that a byte can be represented with 8 bits. In RGB 332, the highest 3 bits encode the R (red). In the above, the first three bits are set to 1, so the resulting color will be red.
1 1 1 0 0 1 0 1
As you can see, colors can be mixed by turning on or off the relevant bit.
Let's now draw a blue pixel on the screen:
0 0 ZER LIT2 #00000011 0 SET
Let's unpack. ZER is an instruction that pushes 0 to the pstack. It's the equivalent of LIT 0, but saves one byte. Then, we push the color blue, followed by 0. This 0 represents the layer. 0 is background, 1 is foreground. Finally, SET draws the pixel on the screen.
Like most consoles, 45M has a controller. A very basic one! It has your usual up left down right keys, as well as A and B which are mapped the the X and C key of the keyboard, respectively.
You can push to the stack the value of the current key being pressed with the KEY instruction. The value has the following format:
0 0 0 B A R L D U
B: the B button A: the A button R: the right arrow L: the left arrow D: the down arrow U: the up arrow
Remember, each ROM starts with two values: 0 0. These values are actually memory addresses. Let's take the following example:
@frame 0 BRK frame: ZER LIT2 #00000011 0 SET
Let's start with memory address 0. It's the screen vector. The value at this address will be called 60 times per second. In the example above, we have set it to the frame label.
Now let's continue with memory address 1. The controller vector:
@frame @key BRK frame: ZER LIT2 #00000011 0 SET key: KEY
The key vector will be executed anytime a key from the controller has been pressed.
snake_color= ff target_color= LIT #0010101 load= ZER LDB store= ZER STB @game @kpress LIT2 55 0 STB LIT2 54 1 STB LIT2 54 2 STB BRK game: FRM LIT 4 MOD IFB LIT2 0 1 CLS LITa @target $target_color KUP SET LIT @move JMR $load KUP LDS LIT @hit JCR LIT @draw JMR BRK move: LITa @len DEC domove: PSH RSI DEC LDB RSI STB PUL DEC DUP ZER NEQ LIT @domove JCN POP LITa @dir CTL @up @down @left @right 0 0 RET up: $load CFZ AND NOT LIT @lost JCN $load TEN SUB $store RET down: $load CFZ AND LIT f0 EQU LIT @lost JCN $load TEN ADD $store RET left: $load CZF AND NOT LIT @lost JCN $load DEC $store RET right: $load CZF AND LIT f EQU LIT @lost JCN $load INC $store RET draw: LITa @len dodraw: PSH RSI DEC LDB LIT2 $snake_color 1 SET PUL DEC DUP LIT 1 NEQ LIT @dodraw JCN POP $load KUP LDS LIT $snake_color EQU LIT @lost JCR $load LIT2 $snake_color 1 SET RET hit: LITa @len DEC LDB LITa @len STB LITa @len INC LIT @len STA RND LIT @target STA RET lost: LIT2 0 0 STA BRK kpress: KEY LIT @dir STA BRK dir: 8 len: 3 target: dd
|LIT||:: n||Push following number to the stack, increase IP by 2|
|LIT2||:: n1 n2||Push n1 and n2 to the stack, increase IP by 3|
|LITa||:: a||Push the value at address a to stack, move IP by 2|
|POP||n||Pop top of the stack|
|DUP||n -- n n||Duplicate top of the stack|
|SWP||n1 n2 -- n2 n1||Swap top of the stack|
|ROT||n1 n2 n3 -- n2 n3 n1||Rotate top of the stack|
|OVR||n1 n2 -- n1 n2 n1||Copy n1 to top of the stack|
|PSH||n --||Take value off pstack and push it to rstack|
|PUL||-- n||Pull value off rstack and push it to pstack|
|RSI||-- n||Copy top of rstack without affecting it|
|RSJ||-- n||Copy second item of rstack without affecting it|
|STA||n a||Store n to address|
|LDA||a -- n||Load from address|
|LDS||n l -- n||Load pixel from screen layer l|
|STB||n a||Store n in buffer at address a|
|LDB||a -- n||Load from buffer address a|
|ADD||n1 n2 -- n1+n2||Add top two stack values|
|SUB||n1 n2 -- n1-n2||Substract top stack value from second|
|INC||n -- n+1||Increment top of the stack|
|DEC||n -- n-1||Decrement top of the stack|
|MUL||n1 n2 -- n1*n2||Multiply top two stack values|
|DIV||n1 n2 -- n1/n2||Divide top two stack values|
|MOD||n1 n2 -- n1%n2||Divide n1 by n2 and push remainder|
|RND||-- n||Push random number|
|EQU||n1 n2 -- f||Check if n1 and n2 are equal|
|NEQ||n1 n2 -- f||Check if n1 and n2 are different|
|AND||n1 n2 -- n1&n2||Push n1 & n2 to the stack|
|INV||n -- ~n||Invert the bits of n|
|NOT||n -- !n||Push 1 if n is 0, 0 otherwise|
|RET||Pop rstack into IP|
|JMP||a||Set the IP to a|
|JMR||a||Push IP+1 to rstack, then JMP|
|IFB||f||BRK if f is 0, otherwise continue|
|JCN||f a||Set IP to a if f is not 0|
|JCR||f a||Push IP+1 to rstack, then JCN|
|KEY||-- n||Push the key being pressed|
|CTL||k -- :: u d l r a b||Switch on k, set IP to adr or move IP by 8|
|CLS||c l||Set screen layer to color|
|SET||o c l||Set pixel at offset o to color a on layer l|
|HLN||o w c l||Draw hline at offset o with width of w, color c on layer l|
|VLN||o h c l||Draw vline at offset o with height of h, color c on layer l|
|FRM||-- n||Push current frame to stack|
|SPR||a o c l||Draw 2 byte sprite from address a to offset o|
|ZER||-- 0||Push zero|
|TEN||-- 0x10||Push 10|
|KUP||-- 0b00000001||Push controller value for UP|
|KDO||-- 0b00000010||Push controller value for DOWN|
|KLE||-- 0b00000100||Push controller value for LEFT|
|KRI||-- 0b00001000||Push controller value for RIGHT|
|KEA||-- 0b00010000||Push controller value for A|
|KEB||-- 0b00100000||Push controller value for B|
|CZF||-- 0f||Push 0f|
|CFZ||-- f0||Push f0|
|CFF||-- ff||Push ff|