SPASM instructions by category

There are five instruction categories:

Memory accessing instructions

The SPASM computer is a load/store design similar to that adopted by RISC CPUs.  This means that an instruction may either load data from memory into a register or it may store data from a register into memory.  An instruction may not do both of these.  Furthermore, only load and store instructions may access memory.  Any instructions that modify data can only operate on data once it is in a register.

Instruction format

An example instruction appears below.  This is an instruction which loads the word at address var1 into register R1. There are two important points to note about this example instruction that will apply to all instructions.  First, the op-code for the instruction (LW) appears on the left-hand side separated from its arguments.  Second, all data movement is from right to left.  In this example data moves from var1 to R1.  This may not be the natural way that you read an instruction.  However, it does correspond to the assignment statement in high level languages where the destination always appears on the left-hand side of the assignment operator.

Addressing modes

Unfortunately, there is more than one way to access memory in load and store instructions.  Each CPU design, including SPASM, has several addressing modes.  An addressing mode is a way to specify a memory address in an instruction.  A list of SPASM addressing modes appears below.  Note that the first two modes are not really addressing modes because they do not address memory at all.       MOV   R3,R1     ; Move contents of R1 to R3. As you can see from this list the addressing modes become quite complex.  You will find that these become second nature when you use instructions in the laboratory sessions in this unit.
 
It is useful to note that SPASM has very simple addressing modes.  Real CPUs implement many more complex modes.  However, the trend with recently designed CPUs is to reduce the number of modes available.  As an example of a more complex addressing mode, look at the following Intel 80x86 instruction. This instruction loads a value from the memory address calculated by adding the contents of the BX and SI registers, then adding the constant value 7.  Other architectures also introduce auto-increment addressing modes which are similar to indirect addressing except that the register that contains the address is incremented to form a new address.  For example, the following is a PDP-11 instruction for loading a character into register R1. Here, before the instruction is executed R2 contains the address of the character to be loaded.  After the instruction R2 then contains the address of the character following the one that is loaded.  You may see a correspondence with the C/C++ auto-increment operators that are used to process character strings.  In fact, C was designed to take advantage of these PDP-11 instructions.

Load instructions

Now that we have examined addressing modes we can look at the full set of load and store instructions.  We will examine instructions that move data from memory to the CPU in this section.

We have already used three SPASM instructions in the above examples.  There are several more instructions for loading data into registers.  These are listed in the table below.  There are also example instructions in the table and the result of the instruction is shown in the right-most column.  To interpret the results of the instructions assume that the memory address labelled with var1 and the following three memory addresses contain the following hex values:

  var1:    FF  FF  01  23
 
op-code description example R1 afterwards
MOV move between registers MOV   R1,R2 contents of R2
LB load byte LB    R1,var1 0xFFFFFFFF
LH load half-word LH    r1,var1 0xFFFFFFFF
LW load word lw    r1,var1 0xFFFF0123
LBU load unsigned byte lbu   r1,var1 0x000000FF
LHU load unsigned half-word LHU  r1,var1 0x0000FFFF
LLH load left half LLH  r1,var1 0xFFFF0000
LI load immediate LI  r1,0x123 0x00000123
LLHI load left half immediate LLHI r1,0x123 0x01230000
 
The meaning of the first four instructions in this table should be obvious.  They either load from a register or load a byte, half-word or word.  The only thing to note is that they load the value as a signed number.  This means that they extend the sign bit of a byte or half word to fill the remaining bits of the register.  In the example instructions in the above table the byte and half-words in memory are negative numbers when interpreted as signed values (see Topic 1).  In both cases the sign bit is extended to the left most bits so that the 32-bit register contains a 32-bit representation of the signed number.

Sometimes we do not want to interpret these values as signed numbers.  In these cases we would rather the bit pattern top be loaded as is, that is, without sign extension into the most significant bits.  The LBU and LHU are designed for this purpose.

It is also useful to load data into the left half word (bits 16-31).  This will allow us to build a 32-bit value from 16 bit components.  LLH can load the left half of a register from memory and LLHI can load it from the immediate value specified.  It is important to note that both of these instructions load zeros into the right half word of the register.  Nevertheless, by using LLHI and an ANDI instruction (Topic 3) we can load a 32-bit immediate value in only two instructions.

At this point you may be wondering why there is only one immediate instruction and not a separate one for loading byte and half-word immediate values.  The answer is that LI does this as well.  To see why you should observe that LW, LH and LB load signed values from memory.  Since the left-most bits are filled with the sign bit it makes no difference if a byte or half-word is loaded.  In addition, an immediate value can only be a 16-bit signed value to fit into the offset field of the instruction.

We will now look at instructions that store data from registers into memory.

Store instructions

We will now look at instructions that move data in the opposite direction, which is from the CPU to memory.  It turns out that we do not need as many different instructions for this direction.

The following table shows the four store instructions.  For the examples assume that R1 contains the value 0x12345678 and the four memory locations at var1 are zero.
 
op-code description example var1, var1+1, ...
SB staore byte SB   var1,R1 78 00 00 00
SH store half-word SH   var1,R1 56 78 00 00
SW store word SW   var1,R1 12 34 56 78
SLH store left half-word SLH  var1,R1 12 34 00 00
 
The first thing you should have noticed about the store instructions is that the register that store instructions operate on is the second argument to the instruction.  This makes the direction of data movement consistent with the load operations, that is, from the second argument to the first.

Note that these instructions only access the amount of memory specified in the instruction.  For example, an SBinstruction only modifies a single byte in memory.  Bytes following the modified byte or half-word are not changed at all.


Arithmetic instructions

SPASM implements arithmetic operations that are found on most electronic calculators.  Instructions for negation, additions, subtraction, multiplication and division of whole numbers are provided.  Most CPUs also implement operations to perform arithmetic on floating point numbers.  This has been left out of SPASM for simplicity.  However, we will examine the Intel 80x86 implementation in a later topic.

SPASM implements separate arithmetic instructions for signed and unsigned interpretation of numbers. It uses 2's complement arithmetic for signed numbers.  If you need to revise 2's complement numbers you should return to Topic 1 now.

A problem faced by all CPU designers is what to do if the result of an arithmetic operation does not fit into a register.  This can happen in any of the four binary operations listed above.  There are several solutions to this problem.  The first is to ignore it.  This is the simplest approach but means that the answer is not correct since bits which make up the correct value have been lost.  A second solution is to set one or more flags in the processor's status register.  This approach has been adopted by the Intel 80x86 designers.  A third approach is to generate a system interrupt whenever overflow occurs.  We will discuss interrupts in Topic 4.

SPASM adopts two of the above approaches.  When performing unsigned arithmetic any overflow is ignored.  The programmer is responsible for detecting any overflow in this case. When performing signed arithmetic an interrupt is generated if overflow occurs.  To avoid interrupts a programmer must either check the arguments to the instruction before its execution or to intercept the interrupt itself.  You will see how to do this in a future laboratory session.

The table below lists all of SPASM's arithmetic instructions.  Notice that instructions with two register arguments always modify the register on the left-hand side.
 
op-code example description
NEG NEG R1 Replace R1 with its 2s complement
ADD ADD R1,R2 Add signed R2 to signed R1. Interrupt on overflow.
ADDU ADDU R1,R2 Add unsigned R2 to unsigned R1. Ignore overflow.
ADDI ADDI R1,23 Add immediate value to signed R1. Interrupt if overflow.
ADDUI ADDUI R1,23 Add immediate value to unsigned R1. Ignore overflow.
SUB SUB R1,R2 Subtract signed R2 from signed R1. Interrupt on overflow.
SUBU SUBU R1,R2 Subtract unsigned R2 from unsigned R1. Ignore overflow.
SUBI SUBI R1,R2 Subtract immediate value from signed R1. Interrupt if overflow.
SUBUI SUBUI R1,R2 Subtract immediate value from unsigned R1. Ignore overflow.
MUL MUL R1,R2 Multiply signed R1 by signed R2 with result in R1. Interrupt on overflow.
MULU MULU R1,R2 MULU MULU  R1,R2 Multiply unsigned R1 by unsigned R2 with result in R1. Ignore overflow.
DIV DIV R1,R2 Divide signed R1 by signed R2 with dividend in R1 and remainder in R2. Interrupt if R2=0.
DIVU DIVU R1,R2 Divide unsigned R1 by unsigned R2 with dividend in R1 and remainder in R2. Interrupt if R2=0.
 
The simplest instruction is the NEG instruction.  It simply interprets its argument as a signed number and replaces it with the 2's complement value.  Note that a 1's complement value can be calculated with the NOT instruction described in the next section on logical instructions.

Both addition and subtraction have separate instructions for an immediate operand.  These are very useful for processing sequences of data such as character strings.  For example, if we have the address of a string in R3 then we can make the address change to the next character in the string by adding one to the register as follows.

When a register is used to hold an address then it is frequently referred to as a pointer.
The multiply and divide instructions do not have immediate versions.  We can of course simulate these with two instructions.  For example, to multiply the contents of R4 by 3 we can use the following two instructions.
 
        LI  R1,3
        MUL  R3,R1

Here, the immediate value of 3 is loaded into R1 and then R4 is multiplied by R1.

You should also note that the division instructions are unique because they modify both of their register
arguments.  It is sometimes useful to think of the result as the div and mod of the arguments (using programming language terminology).  This means that if we wished to calculate the modulus of two numbers then it would appear in the second argument.

Division is also unique because arithmetic overflow cannot occur.  However, division can generate an interrupt because a divide by zero is an illegal operation.  Both versions of the DIV instruction can cause such an interrupt.
 


Logical instructions

 Logical instructions perform bit-wise logical operations.  These include the Boolean operators NOT, AND, OR and XOR.  It is possible that you have not seen these logical operations before.  If this is so you should consult Appendix 3.1 now.  There are also shift and rotate logical instructions that shift or rotate the bits sideways in registers.

Figure 3.3 below lists all of the logical instructions.  You will see that there are immediate addressing mode versions of most instructions.  The final column of this table shows the value of R1 after the example instruction if initially R1=0b100100111, R2=0b1111101010 and R3=0b10.  Note that the binary values have been compressed with ellipsis () to fit in the table.
 
op-code description example R1 becomes
NOT Logical complement NOT  R1 1...1011011000
AND AND registers, result in first argument AND  R1,R2 0100100010
ANDI AND register with immediate value ANDI R1,0xF 0000000111
OR OR registers, result in first argument OR   R1,R2 1111101111
ORI OR register with immediate value ORI  R1,0x4 1100101111
XOR XOR registers, result in first argument XOR  R1,R2 1011001101
XORI XOR register with immediate value XORI R1,0x5 0100100010
SHL Shift bits of register1 left register2 places (zero fill) SHL  R1,R3 10010011100
SHLI Shift bits of register left immediate value places SHLI R1,1 1001001110
SHR Shift bits of register1 right register2 places (zero fill) SHR  R1,R3 0000100100
SHRI Shift bits of register right immediate value places SHR  R1,5 0000001001
ROL Rotate bits in register1 left register2 places ROL  R1,R3 00011100
ROLI Rotate bits in register left immediate value places ROLI R1,1 1001001110
ROR Rotate bits in register1 right register2 places ROR  R1,R3 11001001001
RORI Rotate bits in register right immediate value places RORI R1,2 11001001001
 
It is not immediately obvious why we would wish to use logical instructions.  In most programming languages we do not need to resort to manipulating individual bits.  However, bit manipulation is the main use of these instructions.  You will also find that some of these instructions are useful for manipulating bytes in 32 bit registers.  For example, we can move a byte sideways in a register by using shift and rotate instructions.
Logical immediate instructions are also treated differently to other instructions.  The 16-bit immediate value is not sign extended into the left-most bits.  This is an important point to remember since logical instructions are for manipulating bits so the left 16 bits are important.

The AND instruction is useful for masking bits.  A mask is a bit pattern that can be used to isolate individual bits from a large collection of bits.  To demonstrate a mask, suppose we wished to isolate the right most byte from a 32 bit value.  This can be done by performing an AND operation with the binary value 0b11111111 (ie. 0xFF).

 R1 = 0101 0001 1010 0010 1111 1110 0101 0111

    ANDI  R1,0b11111111

 R1 = 0000 0000 0000 0000 0000 0000 0101 0111

You should check the individual bits to prove to yourself that this works.

A mask is also useful for checking if an individual bit is set.  For example, we can check if bit number 3 is set in R1 by the following two instructions.

    ANDI    R1,0b100
    JNZ     R1,bit2_set

This sequence of instructions will jump to the label bit2_set only if bit 2 is set in R1.  You should write down the bit patterns to check this.

The OR instruction can be used in a similar way to set bits.  If you think about all of the possible arguments to an OR operation you will see that if a bit is set in one of the arguments then it will be set in the answer.  For example, to set bits 0, 3 and 5 in register R5 we can use the following instruction.

       ORI  R5,0b10101

We should also note a subtle feature of the shift instructions.  As bits are shifted left and right, 0 bits are inserted into the register where the bits have been removed.  This is significant if you use shift right operations on signed numbers.  As soon as a register interpreted as a negative number is shifted right any number of bits then it becomes a positive number since the sign bit is changed to a zero.  This means that a shift operation cannot be used on negative numbers for fast divide by two operations.
 



Jump instructions

Instructions in this category have the sole purpose of modifying the PC register.  These instructions are also known as branch instruction in some CPU designs. You will recall from Topic 2 that the PC contains the address of the next instruction to be fetched.   This means that a jump instruction can change the PC to force the CPU to fetch its next instruction from somewhere else in the program.

There are many jump instructions.  The simplest is an unconditional jump that always modifies the PC when it is encountered.  The remaining jump instructions only modify the PC if a certain condition is true.  In the SPASM computer this may be the result of comparing the contents of two registers or comparing a register with the value zero.  The large number of instructions results from the different possible comparisons and the different interpretation of the values as signed or unsigned numbers.

Note that other CPU designs also provide jump conditional instructions based on other conditions.  For example, the Intel 80x86 processors have jump instructions that change the PC based on the status of flags in the processors flags register.

The table below lists the SPASM jump instructions.  The table lists the jump instructions that use register comparison first.  These are followed by the instructions that compare a single register with zero.
 
op-code description example
JMP Always jump JMP  lab1
JE jump if registers are equal JE   R1,R2,lab1
JNE jump if registers are not equal JNE  R1,R2,lab1
JGT jump if reg1 > reg2 JGT  R1,R2,lab1
JLT jump if reg1 < reg2 JLT  R1,R2,lab1
JGE jump if reg1 >= reg2 JGE  R1,R2,lab1
JLE jump if reg1 <= reg2 JLE  R1,R2,lab1
JGTU jump if reg1 > reg2    (unsigned values) JGTU R1,R2,lab1
JLTU jump if reg1 < reg2    (unsigned values) JLTU R1,R2,lab1
JGEU jump if reg1 >= reg2    (unsigned values) JGEU R1,R2,lab1
JLEU jump if reg1 <= reg2    (unsigned values) JLEU R1,R2,lab1
JEZ jump if register = 0 JEZ  R1,lab1
JNEZ jump if register not = 0 JNEZ R1,lab1
JLTZ jump if register < 0 JLTZ R1,lab1
JGTZ jump if register > 0 JGTZ R1,lab1
JLEZ jump if register <= 0 JLEZ R1,lab1
JGEZ jump if register >= 0 JGEZ R1,lab1
 
Take note of the number of arguments to each of these instructions.  The unconditional jump instruction JMP has only one argument.  This is the address from which the next instruction should be fetched.  The instructions that compare registers have three arguments: the two registers to compare and the address to fetch the next instruction from if the comparison succeeds.  The final group of instructions that compare a register with zero only have two arguments which are the register to compare and the address to branch to if the comparison is true.

You should also note that the instructions that compare a register with zero interpret the register as a signed value.  It does not make sense to compare an unsigned value with zero since zero is always the lowest unsigned value.

It is also useful to note that the register compare instructions can be read by placing the Boolean comparison operation between the registers as they appear in the argument list.  For example, the following instruction is the equivalent of the Boolean expressions (R1 < R2).



Stack instructions

SPASM stack instructions are not derived from RISC processors.  These instructions were implemented in many processors designed in the 1970s and early 1980s.  They are included in SPASM as a programming aid.  Using these instructions will hopefully encourage good programming practice that will reinforce programming styles learnt in the programming units of your course.

What is a stack?

Before we see how the stack instructions work we must explain what a stack is.  You will learn about and use stacks in many units of your course.  However, in case you have not seen them before we will briefly look at enough detail here to allow you to use a stack effectively in your assembler programming.

A stack is, as the name suggests, a collection of items.  In SPASM the stack is an area in memory used to store a collection of 32-bit words.  A SPASM stack has two unique operations for altering the number of words in the stack.  We may put a word on top of the stack (a push operation) or we may take something off the stack (a pop operation).  We may never change the number of words in the stack by taking anything out of the middle of the stack (although we read or write values to words in the stack).  The following Figure 3.4 shows a diagram of a program's stack before and after three stack operations.  For this example, assume R1 contains the value 23 and R2 contains the value 17.
 
Here you can see that a good way to visualise what is happening is to visualise elements stacked onto the stack with the push operation.  When an element is removed with the pop operation then it is also placed into a register.

The CPU must remember the location of the top of the stack.  This is always stored in the stack pointer (SP) register.  You will see below that we can access the elements on the stack by copying SP into a register and using indirect addressing.  You will also see that the stack mechanism is a good way to implement subroutines.
We will now look at each of the four SPASM stack based instructions.   Before we start we should note an important thing about program stacks.  Stacks start at an address and grow backwards in memory, that is, towards address zero.  This is very useful because we can immediately detect when a stack overflows because an incorrect address will appear in the stack pointer.  In the case of the SPASM computer we have seen that the stack is the first data area in memory.  This means that if a stack becomes full we will try to access a negative address and thereby cause an interrupt to be generated.  We will examine interrupts in Topic 4.
PUSH and POP instructions

The next Figure 3.5 shows the PUSH instruction.  It simply adds a 32 bit value to the top of the stack. The PUSH instruction copies the value from a register to the top of the stack.  Since a register is 32 bits, the SP is decreased by four.  Note that the contents of R1 do not change.  Notice also that the SP is decreased rather an increased since the stack grows toward address zero.
 
The POP instruction is the opposite of the PUSH instruction.  Figure 3.6 shows that it takes a 32-bit value off the top of the stack and puts it into the specified register.
 
The POP instruction overwrites the current content of the specified register.  It also increases the SP since removing an item from the top of the stack requires the SP to move away from address zero.
The following activity is designed to test your understanding of the program's stack.  You should make sure that you understand the PUSH and POP instructions before continuing to the CALL and RET instructions in the following section since they are really more complex instructions.

We now move onto the CALL and RET instructions.  As mentioned above, these are used to implement subroutines in assembler language.

CALL and RET instructions

A CALL instruction is a combination of a PUSH instruction and a JMP instruction.  The idea is to push the address of the next instruction onto the stack and then jump to the subroutine.  This very neat idea allows the return address that is pushed onto the stack to be used at a later time to return to the instruction following the point where the original call was made.  This is illustrated in Figure 3.7.
 
This is the most complicated instruction in the SPASM CPU.  There are two addresses involved here.  There is the address of the subroutine that we are about to execute.  It is stored in the instruction itself (SUB1 in this case).  A second address is the address of the instruction to execute when the subroutine has completed.  This is stored on the top of the stack.  Not that this allows the subroutine to save further data on the stack with push and pop operations.  It also allows the subroutine to call its own subroutines, as the new return addresses will be stored in the stack on top of the current return address.

To return from a subroutine we execute the RET instruction.  This instruction has no arguments and simply pops the top of the stack into the PC so that the next instruction executed is at the address that was stored on the top of the stack by the CALL instruction.  Figure 3.8 shows the operation of the RET instruction.
 
Although the RET instruction is rather simple in its operation, great care must be taken in its use.  The programmer must be certain that a return address is on top of the stack.  Examine the following subroutine:

Just before we exit the subroutine we push the value zero onto the stack.  This causes the RET instruction to load the address 0x0 into the PC and the next fetch cycle of the CPU will execute whatever instruction it can find at address zero.  This is almost certainly not what the programmer intended.
To give you an example of a correct and complete subroutine, the following piece of assembler code increments the value in register R1. The following activity gives you some written examples to test your understanding of the SPASM subroutine mechanism.  You will also be given many examples of actual programming in the laboratory session.

The final operation of stacks is to access elements while the are still on the stack.

Accessing the stack

There is a small skill that is useful for accessing elements while they remain on the program's stack.  This is a special use of the MOV instruction.  The MOV instruction can copy the contents of SP to any general purpose register.  For example, we can move SP to register R1 with the following instruction. Once we have executed this instruction then we can access elements on the stack by using indirect addressing (as described in Topic 2).  For example, we can move the 32-bit top of the stack into register R2 with the following instruction.
1We can also access elements deeper in the stack by using indirect offset addressing (also see Topic 2).  Some examples follow. We can also access individual bytes on the stack but this is more difficult.  You must remember that data is stored in big-endian format so the byte at the top of the stack is the most significant byte (bits 24-31) of the 32-bit value pushed on the stack.  You will do activities on this aspect of the SPASM stack in the laboratory sessions.


Other instructions

The remaining SPASM instructions are shown in the table below.
 
op-code description example
NOP Do nothing at all NOP
HALT Halt CPU HALT
INT Force interrupt INT   10
 
The first of these instructions, NOP, does nothing at all.  Most CPUs have a NOP instruction that can be used to fill space in program code.  It is not very useful for our study in this unit.

The HALT instruction stops the SPASM CPU's fetch-execute cycle.  The program must be restarted before you can execute any more instructions.

The INT instruction can be used to force an interrupt on the CPU.  The most often use for this instruction is to call the operating system with interrupt number 7 (See Operating system calls).