Lecture 10


LEARNING GOALS:

1. Learn about address registers.

2. Learn about addressing modes and their Motorola syntax.

3. Understand the principles behind some basic 68000 programs.
 


TABLE OF CONTENTS

10.1 ADDRESS REGISTERS

10.2 ADDRESSING MODES

10.2.1 The 3 fundamental addressing modes

10.2.2 The other addressing modes

10.3 PROGRAMMING EXAMPLES
10.3.1 Loops and Conditionals

10.3.2 An application of loops - counting the number of ones in a binary number

10.3.3 Nested loops

10.3.4 An example of the use of shifts and logical operations
 


10.1 ADDRESS REGISTERS

Address registers:

   Back to top of page


10.2 ADDRESSING MODES

Addressing modes represent the different ways in which the location of an operand can be specified. When somebody asks you where your friend Frankie lives, you can say "123 Gargamel Street", or you can say "The second house left of the cemetery entrance". Those are two different ways of specifying a physical location. In the first case, you specify the absolute address, while in the second case you specify an address relative to a known point. Computers operate in much the same fashion, but with many other ways of specifying a location..

The most important addressing modes of the 68000 are examined in minute detail in the NeXT textbook, so we won't repeat here what is already available to you. We'll meet only with the 3 fundamental addressing modes of the 68000, then we'll discuss the Motorola syntax of the addressing modes and provide some examples.
 

10.2.1 The 3 fundamental addressing modes

The 3 fundamental addressing modes of the 68000 processor are:

Absolute addressing:

This is the most straight-forward of all addressing modes. The location of the operand is specified by using its absolute address. For example, the instruction MOVE.B $1200,$14FF loads the memory location $14FF with the contents of memory location $1200. In RTL, that would be represented as [M($14FF)]<-[M($1200)].

Programmers avoid to use actual numerical values, which are replaced by identifiers whenever possible. The instruction MOVE.B X,Y also uses the absolute addressing mode, because the identifiers X and Y also refer to the actual location where the operands are found.

In theory, absolute addressing mode is the only one that you need to write programs, since all the other modes can be derived from the absolute addressing mode. However, more complicated programs can greatly be simplified by the use of more sophisticated addressing modes.
 
Immediate addressing:

In the immediate addressing mode, the actual operand is supplied with the instruction itself (the operand is a literal). An immediate value and a literal are synonyms.

Here are two examples of instructions that use the immediate addressing mode for their source operand:

Example 1:
MOVE.W #7,D5

Example 2:
JULY EQU 7
....
     ADD.W #JULY,D6

In the second example, JULY is a name equated to the value 7 to make the program more readable. But be careful, the following is not an example of immediate addressing mode:

JULY DC.W 7
....
     ADD.W JULY,D6

The operand JULY here is a value stored in memory, not a value provided with the instruction itself.

Note that it doesn't make sense to use the immediate addressing mode for a destination operand: for example, you cannot move the contents of a data register into a number. The following instruction is thus illegal: MOVE.B D1,#42.

The main advantage of this addressing mode is in its speed: it reduces the number of accesses to memory required to perform an operation, since the operand is encoded with the instruction itself.
 
Address register indirect addressing

Here is where address registers come into play. In this addressing mode, the address of the operand is considered as the contents of an address register. In other words, the address register points to the operand's location.

In the Motorola syntax, this addressing mode is indicated by enclosing the address register with round brackets. For example, the instruction MOVE.B (A0),D0 means move the contents of the memory location whose address is found in register A0 to the data register D0. In RTL: [D0]<-[M([A0])]. Note that this instruction does not move the contents of A0 to D0, but the contents of the location at which A0 is pointing.

This mode is called indirect, because two accesses are required instead of one in order to reach the operand: one access to the address register that holds the operand's address, and another access to the operand itself.

Although you probably don't appreciate it at this point yet, the address register indirect mode and its derivatives are one of the most powerful features of not only the 68000, but of any processor worth to be called a processor. Without those addressing modes, maintaining data structures like arrays, C structs, linked lists, trees etc. would be practically impossible, even if theoretically it would still be doable.
 

One final note: instructions that have two operands don't have to use the same addressing mode for both of their operands. Here are some examples:

MOVE.B #45,(A1)
ADD.W  (A3),12324       12324 and TIME are absolute addresses
SUB.B  #HOURS, TIME

Back to top of page
 

9.2.2 The other addressing modes

Once again, the details of the most popular 68000 addressing modes together with their Milo syntax are described in detail in the NeXT textbook. Here we will only give their Motorola syntax, and some examples to clarify the ideas.
 

There are other addressing modes as well, but they are rarely used. We'll concentrate on using the above ones, and later we'll add one more addressing mode: the Program Counter Relative addressing mode.
 
Back to top of page


10.3 PROGRAMMING EXAMPLES

At this point, you are supposed to have read the NeXT textbook and to be familiar with the instructions that are described in it. Everything that is valid about an instruction in the Milo syntax is also valid in the Motorola syntax. The instructions are written in the same way in both syntaxes, and they are executed in exactly the same way, have exactly the same effect on the CCR, etc. The difference between the Milo syntax and the Motorola syntax resides in the syntax of the assembler directives and addressing modes, but not in the syntax of the instructions (except that Milo requires them in lowercase, while Motorola requires them in uppercase). Thus, we will not cover instructions here, and we'll go directly to actual programming examples.
 

10.3.1 Loops and Conditionals
 

Here is a C fragment of code that we'll translate into Assembly language.
 
/************************************ 
 This loop calculates the sum  
 1 + 2 + 3 + ... + N  
 where N is a 16-bit positive integer 
*************************************/ 

int Sum = 0; 
for (i=1; i<=N; i++) 
     Sum = Sum + i; 
 

 

And here is the corresponding Assembly language program (in Motorola syntax):
 
 
*This program assumes that integers are 2 bytes in size 
* 
           CLR.W  D0            D0 represents Sum; Sum = 0; 
           MOVE.W N,D2          D2 holds N, N has already been stored in memory 
           MOVE.W #1,D1         D1 represents i; i=1; loop counter initialization 
* 
LOOP:      CMP.W D2,D1          Compare N with i; check loop condition 
           BHI   EXIT_LOOP      if i>N then Branch to EXIT_LOOP 
           ADD.W D1,D0          Sum = Sum + i; 
           ADDI.W #1,D1         i++ ; increment loop counter 
           BRA   LOOP           Always branch to LOOP (repeat the loop)   
* 
EXIT_LOOP: ........             Code that follows the loop 

Now the milo syntax for the same piece of code:
 
 
#This program assumes that integers are 2 bytes in size 
 
           clrw  d0            |d0 represents Sum; Sum = 0; 
           movew N,d2          |d2 holds N, N has already been stored in memory 
           movew #1,d1         |d1 represents i; i=1; loop counter initialization  
 
loop:      cmpw d2,d1          |Compare N with i; check loop condition 
           bhi  exit_loop      |if i>N then Branch to exit_loop 
           addw d1,d0          |Sum = Sum + i; 
           addiw #1,d1         |i++; increment loop counter 
           bra  loop           |Always branch to LOOP (repeat the loop)   
 
exit_loop: ........            |Code that follows the loop 
 
This is the basic technique of writing loops in Assembly language; we will next see how to write loops with the DBcc (decrement and branch on condition code) instruction. But before doing that, we'll just include a note on branching instructions.

Some Branch instructions of the form Bcc or Jcc can be divided in two groups: those that branch on a signed condition and those that branch on an unsigned condition. For example, 0xFA is greater than 0x11 when the numbers are regarded as unsigned, i.e. 250 is greater than 17, but 0xFA is less than 0x11 if the numbers are signed: -6 is less than 17.
 
The signed comparisons are: The corresponding unsigned comparisons are:
BGE    branch on greater than or equal 
BGT    branch on greater than  
BLE    branch on less than or equal 
BLT    branch on less than 
BCC or BHS branch on higher than or same 
BHI        branch on higher than  
BLS        branch on lower than or same 
BCS or BLO branch on lower than

Many assemblers accept the official mnemonics BCC (branch on carry clear) and BCS (branch on carry set) to be renamed BHS (branch on higher than or same) and BLO (branch on lower than), respectively.
 

Let's now rewrite the the same loop as above, this time by using the DBcc instruction. Just as a reminder, here is how the action of the DBcc instruction can be described in C-like pseudocode:

Syntax:    DBcc Dn,label

Action:

If (cc is True) go to EXIT
else
     {
       [Dn] = [Dn] - 1;
       if ([Dn] == -1) go to EXIT;
       else [PC] <- label;
     }

EXIT: .....

Note that a DBcc instruction takes the branch on condition cc false while a Bcc instruction takes the branch on condition cc true. Here is an alternative way of describing the action of DBcc in pseudocode:

If (cc is False) then
     {
       [Dn] = [Dn] - 1;
       if ([Dn] != -1) then
             [PC] <- label;
     }

EXIT: .....

The cc codes for the DBcc instruction are the same as for the regular branch instructions, except that DBcc allows also the conditions F (i.e. False) and T (i.e. True) to be specified by the cc. The instruction DBF Dn,label always causes Dn to be decremented and a branch to label to be made while the contents of Dn are not -1. DBRA has the same effect as DBF, and both can be used interchangeably. On the other hand, DBT (decrement and branch true) never branches. When either the condition cc is True, or the contents of Dn are equal to -1, the DBcc instruction is not entered anymore and the instruction immediately following the DBcc instruction is executed.

Here is the assembly language translation of the above C for loop using the DBRA instruction:
 
*This program assumes that integers are 2 bytes in size 
* 
           CLR.W   D0            Sum = 0; 
           MOVEQ.W #1,D1         i = 1; 
           MOVE.W  N,D2          Load the loop counter D2  
           SUBQ.W  #1,D2         with N - 1; [D2]<- N-1; 
* 
LOOP:      ADD.W  D1,D0         Sum = Sum + i; 
           ADDQ.W #1,D1         i++; 
           DBRA   D2,LOOP       Decrement D2 and branch until [D2] becomes -1   
* 
           ........             Code that follows the loop 

Note that the loop is executed [Dn] + 1 times. Since we want our loop to be executed N times, we have to load Dn (D2 in the above case) with N-1. The DBcc instructions are very useful when you want to execute a loop a predetermined number of times.
 

Back to top of page
 

10.3.2 An application of loops - counting the number of ones in a binary number

Here we present an assembly language program that counts the number of 1's found in a 16-bit number. The key feature of this program is the use of the ASL (arithmetic shift left) instruction which shifts the bit at the left end of the number into the C bit and inserts a 0 at the right end to replace the evinced bit.. Then the C bit of the CCR is tested; if it holds a 1, the count of the number of 1's is incremented, otherwise the next arithmetic shift left is performed, all this until the 16-bit number is entirely composed of 0's.
 
* This program counts the number of 1's in the 16-bit number N 
* that resides in D0 

          CLR.W D1        D1 will hold the count of the # of 1's 
* 
LOOP:     ASL.W #1,D0     shift one bit to the left 
          BCC   IS_ZERO   if the C bit is zero, test for exit condition  
          ADD.W #1,D1     if the C bit is 1, increment the count 
IS_ZERO:  TST.W D0        check if [D0(0:15)] = 0 
          BNE   LOOP      repeat loop until [D0(0:15)] becomes 0  
* 
OUTPUT_RESULT: .....      code for outputting the result


Here is the milo version of the same piece of code:
 
# This program counts the number of 1's in the 16-bit number N 
# that resides in D0 

          clrw d1        |d1 will hold the count of the # of 1's 
# 
loop:     aslw #1,d0     |shift one bit to the left 
          bcc  is_zero   |if the C bit is zero, test for exit condition  
          addw #1,d1     |if the C bit is 1, increment the count 
is_zero:  tstw d0        |check if [d0(0:15)] = 0 
          bne  loop      |repeat loop until [d0(0:15)] becomes 0  
# 
output_result: .....      code for outputting the result

 

Back to top of page
 

10.3.3 Nested loops

Let's translate the following C code into assembly language:



int b = 0;
int i = K;

while (i>0)
{
    for (j=10; j>0; --j)
         if ((i%j)==0)
               b++;
    --i;
}


Here is the translation (before you start examining the following code, it is highly recommended that you familiarize yourself with how the DIVU instruction works)::
 
*This program assumes that integers are 2 bytes in size 

             CLR.W  D0            D0 represents b;  
             CLR.L  D1            D1 represents i; we want to be sure all 
*                                 32 bits are 0 before moving in i;  
             MOVE.W K,D1          i = K; 

WHILE:       TST.W  D1            Compares i with 0; 
             BLE    EXIT_WHILE    If i<=0 exit the while loop;  
             MOVE.W #9,D6         D6 is used as a loop counter for DBRA; 
             MOVE.W #10,D5        D5 represents j; j=10; 
FOR:         CLR.L  D7            D7 is a working register used so that DIVU 
*                                 doesn't alter the original value of i in D1 
             MOVE.W D1,D7         Copy i to D7; 
             DIVU   D5,D7         Divide i by j; 
             SWAP   D7            Get the remainder in the low order word of D7;  
             TST.W  D7            If the remainder is not 0  
             BNE    DECR_J        skip the b++ statement and just do --j; 
             ADD.W  #1,D0         else add 1 to b (b++);  
DECR_J:      SUB.W  #1,D5         --j;    
             DBRA   D6,FOR        Repeat the for loop; 
* 
             SUB.W  #1,D1         --i;  
             BRA    WHILE         Repeat the while loop;  
* 
EXIT_WHILE:  .......              Code that follows the while loop; 
 


Note that when we test the exit condition for the while loop by comparing i to 0, we use the signed comparison BLE and not its unsigned equivalent BLS. This is so because we work with signed numbers and BLS cannot detect negative values, so if ever i becomes negative (for example, K may be a negative number, in this case the while loop should never be entered) the while loop will not be exited. The fact that we test the condition if i<=0 implies in itself that i can be negative. Never assume you work with unsigned numbers unless you have the proof for the contrary. For example, when we divide i by j with the instruction DIVU D5,D7 we are certain that at this point in the program both i and j are positive and so we can use unsigned division.

An important note: if our for loop was of the form
for (j=x; j>=0; --j)
then we can use the same data register as both a loop counter for DBRA and to hold the value of j. In fact, the DBRA instruction is written to mimic the behaviour of this kind of loops, which is very often used when dealing with arrays and other data structures, as we'll see later. We just have to remember that if ever we corrupt the value of j, then we'll also change the number of times the loop is executed. Unfortunately, in the above program we can't use this "trick" (which would have simplified the code since the decrementation of j would be done automatically), because our for loop is of the form  for (j=x;j>0;--j).

This is not the only way to translate the above C code into assembly language; in fact, if you write the above C code as a complete C program called exampl.c (shown below) and compile it on milo by typing  gcc exampl.c -S   this is the actual assembly code that is generated by the compiler on milo:
 
 

/*********** 
  exampl.c 
***********/ 

void main() 
{ 
 int b=0; 
 int i=20; /*we give an arbitrary initial value to i*/ 
           /*to avoid input-output complications*/  
 int j; 

 while (i>0) 
  { 
    for (j=10;j>0;--j) 
          if ((i%j)==0) 
               b++; 
    --i; 
  } 


The actual assembly code generated on milo:
 
.text 
      .align 1 
.globl _main 
_main: 
      link a6,#-12 
      movel d3,sp@- 
      movel d2,sp@- 
      clrl a6@(-4) 
      moveq #20,d3 
      movel d3,a6@(-8) 
L2: 
      tstl a6@(-8) 
      jgt L4 
      jra L3 
L4: 
      moveq #10,d3 
      movel d3,a6@(-12) 
L5: 
      tstl a6@(-12) 
      jgt L8 
      jra L6 
L8: 
      movel a6@(-8),d2 
      movel d2,d0 
      divsll a6@(-12),d1:d0 
      tstl d1 
      jne L9 
      addql #1,a6@(-4) 
L9: 
L7: 
      subql #1,a6@(-12) 
      jra L5 
L6: 
      subql #1,a6@(-8) 
      jra L2 
L3: 
L1: 
      movel a6@(-20),d2 
      movel a6@(-16),d3 
      unlk a6 
      rts

Since milo is a real 68000 processor, and since the assembly code was generated by a (real) compiler, the code inevitably contains some complications that we haven't covered yet, like the use of a stack frame which we'll see after the midterm when we cover subroutines and parameter passing mechanisms. For such a simple program, the above code is unnecessarily complicated, and after a moderate simplification we can get to the following lines of code:
 
 
      clrl a6@(-4) 
      moveq #20,d3 
      movel d3,a6@(-8) 
L2: 
      tstl a6@(-8) 
      jgt L4 
      jra L3 
L4: 
      moveq #10,d3 
      movel d3,a6@(-12) 
L5: 
      tstl a6@(-12) 
      jgt L8 
      jra L6 
L8: 
      movel a6@(-8),d2 
      movel d2,d0 
      divsll a6@(-12),d1:d0 
      tstl d1 
      jne L9 
      addql #1,a6@(-4) 
L9: 
      subql #1,a6@(-12) 
      jra L5 
L6: 
      subql #1,a6@(-8) 
      jra L2 
L3: 
      code that follows the while loop, 
      i.e. some closing ceremonies

Note that the compiler uses instructions of the form jcc rather than bcc. This is because jcc instructions have a larger branching range than bcc instructions. However, if you replace the jcc instructions with their bcc equivalents, the code will still work perfectly. If you are wondering what a6@(-8) and a6@(-4) are, those are locations in memory (on the stack frame, to be seen later) that store the variables i, j and b. Here is how they are allocated:

a6@(-4) refers to 4 bytes in memory that store variable b
a6@(-8)    "           "            "            "            "         "    i
a6@(-12)  "           "            "            "            "         "    j

You can see that there is a lot of data movement between registers and memory. This is unnecessary in our case, since the 8 general purpose data registers are more than enough to hold the small amount of data needed by our program.

It is important to note that on milo, integers are 4 bytes in size. This is why all the operations are longword operations.

Don't worry if you have difficulties following the above code. Compiler-generated assembly code is in general more mysterious and more inefficient than the equivalent code generated by a human programmer; however it is error free, while human writing is prone to errors, especially when the programs get long. The point is that there is no one way to write an assembly language program. For example, you can see that the compiler does not use the dbra instruction to create the for loop.
 

Back to top of page

10.3.4 An example of the use of shifts and logical operations

We will leave the loops aside for a moment and consider an example of how shifts and logical operations can be used in a program.

We are going to see a program that converts an 8-bit binary number in the range 0 to 255 to three BCD (packed decimal) characters. For example, the binary number 10001101 will be converted to 0000 0001 0100 0001 (i.e. 14110). The source (the binary number) resides initially in the low-order byte of D1, and the result is deposited in the low-order word of D1. We use the instruction LSL.W #4,D1 which shifts the contents of the low-order word of D1 four places left. Zeros fill the vacated positions at the right end. We also use the AND.L instruction to clear bits, and the OR.W instruction to insert bits.

Here is the program:
 
CLR.L D0             D0 used as working space for DIVU. Clear all 32 bits. 
MOVE.B D1,D0         Copy the source to D0 
DIVU #100,D0         Divide D0 by 100 to get the hundredths digit in D0(0:15) 
MOVE.W D0,D1         Save the hundredths digit in D1 in least sign. position  
SWAP D0              Get the remainder in the low order word of D0 
AND.L #$0000FFFF,D0  Clear the most significant word before division 
DIVU #10,D0          Divide remainder by 10 to get the tens digit in D0(0:15)  
LSL.W #4,D1          Shift 100s digit in result 1 place (4 binary places)left  
OR.W D0,D1           Insert the tens digit into result in least sign. position 
LSL.W #4,D1          Shift both 100s and 10s digit 1 place (4 binary places)left 
SWAP D0              Get the remainder in the low order word of D0  
OR.W D0,D1           Insert ones digit in least significant position 
 

Let's trace the execution of this program for [D1] = 10001101. In the comment field of each instruction, we show (in hexadecimal) the contents of D1 and D0 after the execution of this instruction.
 
CLR.L D0              D1 = 0000008D   D0 = 00000000 
MOVE.B D1,D0          D1 = 0000008D   D0 = 0000008D 
DIVU #100,D0          D1 = 0000008D   D0 = 00290001 
MOVE.W D0,D1          D1 = 00000001   D0 = 00290001  
SWAP D0               D1 = 00000001   D0 = 00010029 
AND.L #$0000FFFF,D0   D1 = 00000001   D0 = 00000029  
DIVU #10,D0           D1 = 00000001   D0 = 00010004 
LSL.W #4,D1           D1 = 00000010   D0 = 00010004 
OR.W D0,D1            D1 = 00000014   D0 = 00010004  
LSL.W #4,D1           D1 = 00000140   D0 = 00010004  
SWAP D0               D1 = 00000140   D0 = 00040001 
OR.W D0,D1            D1 = 00000141   D0 = 00040001 
 

The logical operations are very powerful tools because they allow you to play with the specific bits of your choice in a register (or any memory location). Even if you can't directly clear the most significant word of a register with the CLR instruction, you can always do so by ANDing it with 0x0000FFFF. If you want to set (to 1) bits 5,7 and 31 of a register, then all you have to do is ORing it with 0x800000A0. If you want to toggle (change the state of) the bits of the high-order byte of register D7, nothing simpler: just execute the instruction EOR.L #$FF000000,D7. If you are given a register, say D0, and you want to see what is the state of bits 9 to 13 inclusive, then you can perform a sequence of operations like:

CLR.L D1         Clear all D1
OR.W D0,D1       Low-order word of D1 reflects state of bits in low-order word of D0
AND.W #$3E00,D1  If you want, you can clear all bits in D1
                 except bits 9 to 13 inclusive
 

Back to top of page

 

Copyright © McGill University, 1998. All rights reserved.
Reproduction of all or part of this work is permitted for educational or research purposes provided that this copyright notice is included in any copy.