Here are the details of the input format accepted by the cas assembler, and the instructions available in the Crazy Small CPU version 2.
Assuming that you have an input file called myprog.s, you can assemble this by running the command:
./cas myprog.s
This will produce the ROM files:
- botrom.rom and toprom.rom, used by Logisim and the csim simulator
- botrom.img and toprom.img, to be burned to real EEPROMs
All instructions have an optional 8-bit operand which can be:
- a decimal constant
- an octal constant, starting with 0
- a hex constant, starting with 0x
- a label
If the operand is omitted, then 255 is the implicit operand. Operand values range from 0 to 255 decimal.
Some instructions use all eight bits of the operand, shown as addr below. Some instructions use the lower four bits of the operand, shown as const below.
The "Flags" column indicates if the value in the Flags register is altered by the instruction. This also indicates if the ALU result is written into RAM at the explicit (or implicit) address operand.
Mnemonic | Flags | Comment |
---|---|---|
ADDMB addr | Y | Store A + B into RAM and also into B |
ADDM addr | Y | Store A + B into RAM |
ANDM addr | Y | Store A & B into RAM |
CLC | Y | Clear carry flag, set zero flag |
DAB const | Display A and B, load A & B with constant | |
DADDM addr | Y | Store BCD A + B into RAM |
DIVM addr | Y | Store A / B into RAM |
DMAB addr | Display A and B, load A & B from RAM | |
DSUBM addr | Y | Store BCD A - B into RAM |
HMULM addr | Y | Store A * B (high nibble) into RAM |
JCC addr | Jump if C clear | |
JCS addr | Jump if C set | |
JEQ addr | Jump if equal to zero | |
JGE addr | Jump if >= zero | |
JGT addr | Jump if greater than 0 | |
JLE addr | Jump if <= zero | |
JLT addr | Jump if less than zero | |
JMP addr | Jump always | |
JNC addr | Jump if N clear | |
JNE addr | Jump if not equal to 0 | |
JNS addr | Jump if N set | |
JVC addr | Jump if V clear | |
JVS addr | Jump if V set | |
JZC addr | Jump if Z clear | |
JZS addr | Jump if Z set | |
LCA const | Load constant into A | |
LCB const | Load constant into B | |
LMA addr | Load A from RAM | |
LMB addr | Load B from RAM | |
LMULM addr | Y | Store A * B (low nibble) into RAM |
MODM addr | Y | Store A % B into RAM |
NOP | No operation | |
ORM addr | Y | Store A OR B into RAM |
SMA addr | Y | Store A into RAM |
SMBA addr | Y | Store A into RAM and B |
SMB addr | Y | Store B into RAM |
SMIA addr | Y | Store A + 1 into RAM |
SUBM addr | Y | Store A - B into RAM |
TAB | Y | Transfer A to B |
TBA | Y | Transfer B to A |
TBF | Y | Transfer B to flags |
XORM addr | Y | Store A ^ B into RAM |
ZEROM addr | Y | Store zero into RAM |
In the assembly input, a comment starts at a hash character (#) and continues to the end of the line. All of this is ignored by the cas assembler. Blank input lines are ignored by the assembler. The assembler supports backslash '\' characters to continue a single line over multiple lines.
The cas assembler provides labels for you to name:
- constants
- RAM locations
- jump destination points
To associate a label with a value, write your label at the beginning of the line, followed by a colon, then the EQU pseudo-op and the value. Example:
num1: EQU 34 # Associate num1 as location 34
five: EQU 5 # Associate five as the constant 5
Later on in your program, you can write
LCA 5 # Load 5 into A
LCB five # Also load 5, into B
SMB num1 # Store B's value in location num1
Any label which is defined without an EQU gets the value of the program counter at that point and can be used as a jump destination point. For example:
loop1:
loop2: LCA five
. . .
. . .
JMP loop2
. . .
JMP loop1
As there is no HALT instruction, you should end your programs with an infinite loop, e.g.
end: JMP end
The csim simulator recognises a jump to the current program counter value and exits the simulation.
Any ALU operation that is written to RAM sets the value of the Flags register. This register holds four bits:
- N: set if the ALU result is negative
- Z: set if the ALU result is zero
- V: set if there was an overflow
- C: set if there was a carry
4-bit binary nibble values are treated as signed values in the range -8 to +7 decimal. All ALU operations may set the negative (N) and zero (Z) flags. The overflow (V) flag is set when the signed A and B values have one sign (positive or negative) and the result has a different sign.
4-bit BCD nibble values are treated as unsigned values in the range 0 to 9 decimal. The BCD operations do not set the negative (N) or overflow (V) flag. If you try to do a BCD operation when one or the inputs is out of the 0 to 9 range, the result is zero.
The carry (C) flag is set when the result does not fit into four bits. ALU operations that may set the carry (C) flag are:
- addition, subtraction and multiplication of any type
- incrementing A's value
Finally, the TBF instruction sets the four NZVC flag bits to be the value of the B register.
The Flags register retains its value until it is overwritten. The conditional jump instructions can be used to jump to an instruction location based on the various combinations of the Flags register.
At each instruction location, there can be up to sixteen instructions. Each one is chosen based on the sixteen possible combinations of NZVC bits in the Flags register.
The conditional jump instructions automatically fill these sixteen positions with JMP or NOP instructions to obtain the desired behaviour. For example, if you write the instruction
JCS endloop # End loop when carry is set
then eight of the positions at this location will be set to JMP instructions for the positions that indicate a set carry (C) flag. The other eight positions will be filled with NOP instructions.
The assembler gives the programmer explict control over this choice as well. At each instruction location, you can define which instructions to insert for one, many or all sixteen of the NZVC Flags combinations.
Each assembly line can contain multiple instructions. Instructions are separated by a vertical bar. By default, the first instruction on the line is placed into all sixteen instruction positions.
Other instructions are placed in increasing positions, up to the sixteenth position. For example, the instruction line:
LCA 0 | LCB 1 | JMP 3 | ADDM 8
stores the LCA 0
instruction into all sixteen positions, and then
overwrites position 1 with LCB 1
, position 2 with JMP 3
and
position 3 with ADDM 8
.
You can also prefix an instruction with a word which is a combination
of the NZVCnzvc
letters. Uppercase letters require that flag to be set.
Lowercase letters require that flag to be clear. If this 'flags' word
appears, then the instruction is placed in those positions that correspond
to the set (or cleared) flags.
Here is a full example. Consider the instruction line:
LCA 2 | Nz LCA 3 | LCA 4
The cas assembler will place the instructions in these sixteen positions:
Position | Flags | Instruction |
---|---|---|
0 | nzvc | LCA 2 (default) |
1 | nzvC | LCA 2 (default) |
2 | nzVc | LCA 4 (position 2) |
3 | nzVC | LCA 2 (default) |
4 | nZvc | LCA 2 (default) |
5 | nZvC | LCA 2 (default) |
6 | nZVc | LCA 2 (default) |
7 | nZVC | LCA 2 (default) |
8 | Nzvc | LCA 3 (Nz) |
9 | NzvC | LCA 3 (Nz) |
10 | NzVc | LCA 3 (Nz) |
11 | NzVC | LCA 3 (Nz) |
12 | NZvc | LCA 2 (default) |
13 | NZvC | LCA 2 (default) |
14 | NZVc | LCA 2 (default) |
15 | NZVC | LCA 2 (default) |
Here is an example of manually choosing the instructions to perform based on the Flags values. We want to print the 4-bit nibble at RAM location 10 out as an ASCII character, i.e. '0' .. '9' or 'A' .. 'F'. This requires the code to map nibble values 0x0 .. 0x9 to the ASCII values 0x30 to 0x39, and the nibble values 0xa to 0xf to the ASCII values 0x41 to 0x46.
I've numbered each line, and each line is described after the code.
1. CLC # Clear carry before the add
2. LCA 7
3. LMB 10
4. ADDMB # B=B+7, flags set
5. LMB 10 | zC NOP # Reload the digit if 0-9
6. LCA 3 | zC LCA 4
7. DAB
-
Clear the carry so the ADDMB won't be affected by a carry in.
-
Load constant 7 into A. Why 7? Read on.
-
Load the nibble to print from location 10.
-
Add 7 to this nibble and store the result back in B. If the nibble was 0xa (10), then this becomes 17, except that this won't fit in the B register, so it becomes 0x1: the low nibble of ASCII 'A' (0x41). This will have set the carry (C) flag: in fact, any nibble value from 9 up to 15 will set the carry flag.
That's a problem though, as we don't want to convert the nibble value 9 into an uppercase character. Luckily, 9 + 7 = 16, which sets the carry flag but also turns into 0 and sets the zero (Z) flag.
So, nibble values 10 and upwards, when added to 7, set the carry flag. Value 9 also does this, but it sets the zero flag as well.
-
If the carry flag is set but not the zero flag (C), do nothing. We keep B+7 in the B register. For all other flag combinations, reload the original nibble value into the B register.
At this point, we either have flags zC and B+7 in the B register because the nibble value was 10 .. 15, or we have the original nibble value in the B register because the nibble value was 0 .. 9.
-
For nibble values 10 and upwards (zC), set A to 0x4: we now have the correct ASCII character 'A' .. 'F' in the A/B registers. For all other nibble values, set A to 0x3: we now have the correct ASCII digits '0' .. '9' in the A/B registers.
-
Print out the ASCII character in the A/B registers to the UART.
Even though the CPU architecture is Harvard style, there is a way to perform function calls as long as there are only sixteen callers or less to the function.
It works as follows. Each function caller loads a "caller id" into either the A or B register before jumping to the function. The function saves the caller id. Just before the end of the function, the function loads the stored caller id into the Flags register with an instruction sequence ending with a TBF instruction.
The Flags register now holds one of the sixteen caller ids. We can now use a 2-dimensional instruction to jump back to the instruction following the original function call. Here is an example of two function calls followed by the function itself.
caller: EQU 0 # Id of the function caller
# Print digit 2 and newline
LCA 0 # Caller id
LCB 2
JMP printdigit
enddigit2:
# Print digit 4 and newline
LCA 1 # Caller id
LCB 4
JMP printdigit
enddigit4:
. . .
# Function to print a digit followed by a newline
printdigit: SMA caller # Save caller id
LCA 3
DAB 0 # Print the digit, load 0 into A
LCB 10
DMAB caller # Print newline, get caller id
TBF # Transfer caller to flags
# Jump based on flags value
JMP enddigit2 | JMP enddigit4