The 8051


 
<-Previous
   
  Subroutines and the Stack
   
  We have already looked at calling a subroutine in the previous section. Now we will look at the actual call instructions and the effect they have on the stack.
   
  ACALL and LCALL
  ACALL stands for absolute call while LCALL stands for long call. These two instructions allow the programmer to call a subroutine. There is a slight difference between the two instructions, the same as the difference between AJMP and LJMP.
  ACALL allows you to jump to a subroutine within the same 2K page while LCALL allows you to jump to a subroutine anywhere in the 64K code space.
  The advantage of ACALL over LCALL is that it is a 2-byte instruction while LCALL is a 3-byte instruction.
   
  Help from the Assembler
  With many 8051 assemblers, you can use JMP for an unconditional jump and CALL to call a subroutine. JMP and CALL are not 8051 instructions. They are replaced by the assmebler with:
  • EdSim51: CALL is replaced by ACALL and JMP is replaced by either SJMP or AJMP. LCALL and LJMP must be programmed explicitly.
  • Some other assemblers: CALL is replaced by either ACALL or LCALL and JMP is replaced by either SJMP or AJMP or LJMP.
   
  LCALL Operation
  Since the ACALL and LCALL instructions perform almost the same functions we will look at the operation of the LCALL instruction only.
   
  LCALL add16 - long call to subroutine
Encoding - 0001 0010 aaaaaaaa aaaaaaaa (3-byte instruction)
Operation -
 

(PC) <- (PC) + 3
(SP) <- (SP) + 1
((SP)) <- (PC7-PC0)
(SP) <- (SP) + 1
((SP)) <- (PC15 - PC8)
(PC) <- add15 - add0

   
  The operation is as follows. The PC is increased by 3 (because this is a 3-byte instruction). The stack pointer is incremented so that it points to the next empty space on the stack.
  The third line reads: the contents of the contents of the SP get the low byte of the PC. On system reset the SP is initialised with the value 07H. Therefore, the first item pushed onto the stack will be stored in location 08H. Therefore:
 
((SP)) is equivalent to (08) - meaning location 08H in memory gets the low bye of the PC - ((SP)) <- (PC7 - PC0)
  After the low byte of the PC has been stored on the stack the SP is incremented to point to the next empty space on the stack (ie; the SP now contains 09H).
 
((SP)) is now equivalent to (09) - meaning location 09H in memory gets the high bye of the PC - ((SP)) <- (PC15 - PC8)
  Now that the PC has been stored on the stack the PC is loaded with the 16-bit address (add15 - add0).
   
  Subroutines are generally sections of code that will be used many times by the system. A subroutine might be used for taking information from a keyboard or writing data to a serial link. A particular subroutine will be stored at some point in code memory, but it can be called from any location in the program. Therefore, the system needs some way of knowing where to jump back to once execution of the subroutine is complete.
   
  The first diagram below shows the contents of the PC and the SP as the instruction LCALL sub (at location 103BH in code memory) is about to be executed. Notice the SP is at its reset value of 07H and the PC contains the address of the next instruction to be executed (LCALL sub).
   
 
   
  The diagram below shows the state of the PC, the SP and the stack after the LCALL sub instruction has been executed. Notice the return address is stored on the stack, the low-byte in location 08H and the high-byte in location 09H.
  It should also be noted that the return address is 3 locations after the LCALL sub instruction itself. This is because the LCALL instruction is three bytes long; the next instruction after the LCALL instruction (ie; the one to be executed once the subroutine has been executed) is three bytes further on in memory - 103EH).
  The PC now contains the address of the subroutine, which is marked by the label sub at address 402AH.
   
 
   
   
  RET
  A subroutine must end with the RET instruction, which simply means return from subroutine. Since a subroutine can be called from anywhere in code memory, the RET instruction does not specify where to return to. The return address may be different each time the subroutine is called. If you take the flashing LED program from the last section, we called twoLoopDelay in the main program after we had turned on the LED. But we also called twoLoopDelay from within threeLoopDelay. In these two calls the return address is different; in the first call we are returning to the main program once twoLoopDelay has completed, but on the second call we are returning to threeLoopDelay.
  The system knows where to return to because, as we have seen above, the return address (ie; the address of the next instruction after the LCALL) is stored on the stack. Therefore, the operation of the RET instruction is:
   
  RET - return from subroutine
Encoding - 0010 0010
Operation -
 
(PC15 - PC8) <- ((SP))
(SP) <- (SP) - 1
(PC7 - PC0) <- ((SP))
(SP) <- (SP) - 1
   
  If you look at the diagram above, note the SP contains 09H - it's pointing at the high-byte of the return address. Therefore, the contents of location 09H are placed in the high-byte of the PC (PC15 - PC8).
 

The SP is then decremented (it now contains 08H) so that it points at the low-byte of the return address. So, the contents of location 08H are placed in the low-byte of the PC (PC7 - PC0).

  In our example above, the PC will now contain 103EH, and execution takes up immediately after the LCALL sub instruction.
  Also note that the stack is now empty - SP contains 07H.
   
   
  Passing Data to Subroutines
  The three-loop and two-loop time delays from the previous section are shown below.
   
 
threeLoopDelay:

MOV R2, #0AH

loop1: CALL twoLoopDelay

DJNZ R2, loop1
RET

   
 
twoLoopDelay:

MOV R0, #0FFH

loop: MOV R1, #0FFH

DJNZ R1, $
DJNZ R0, loop
RET

   
  threeLoopDelay, calls twoLoopDelay ten times and then returns, resulting in an overall number of iterations of 10*255*255.
   
  If we could initialise threeLoopDelay before we called it then we could use the same subroutine to produce different lengths of time delay.
   
 
;the main program

MOV R2, #03H
CALL threeLoopDelay
MOV R2, #08H
CALL threeLoopDelay

 
;the subroutine
threeLoopDelay:

loop: CALL twoLoopDelay

DJNZ R2, loop
RET

   
  The only change made to threeLoopDelay is that we did not load a value into R2. In the main program we load the required value into R2 and then call threeLoopDelay. In the first instance we load 03H into R2 and then call the delay, resulting in 3 * 255 * 255 iterations. We then load R2 with 08H and call the delay, resulting in 8 * 255 * 255 iterations. In this manner, the same subroutine is producing two different time delays.
   
   
  Disadvantage of Passing Data to Subroutines
  The advantage of initialising the subroutine is obvious - the same piece of code can be used to perform slightly different tasks. However, there is a major disadvantage. In high level languages such as C, functions are equivalent to subroutines in assembly language. The function for comparing two strings:
 
strcmp(str1, str2)
  expects two strings (str1 and str2) to be passed to it. If you try to compile code without passing two strings to strcmp then the compiler will signal an error.
   
  However, if we forget to initialise a subroutine before calling it the assembler sees nothing wrong and the error goes unnoticed. For example, if we call threeLoopDelay without first putting the required value into R2 then we have no way of knowing what length of delay will result. The length of the delay will be determined by the random data in R2.
   
   
  Another Example - Getting the Average of a Set of Numbers
   
 

average:

MOV A, 30H
ADD A, 31H
ADD A, 32H
ADD A, 33H
ADD A, 34H
MOV B, #05H
DIV AB
MOV 20H, A
MOV 21H, B
RET

   
  The above subroutine calculates the average of five numbers, stored in locations 30H, 31H, 32H, 33H and 34H. It stores the integer of the result in A and the remainder in B.
  Remember, DIV AB divides the number in A by the number in B, storing the integer of the result in A and the remainder in B.
   
  However, as a subroutine it's not much use because it's restricted to getting the average of the data stored from locations 30H to 34H.
  Also, the sum of the five numbers must be less than or equal to 255. At a later date we will see how we can get around this problem.
   
 
;the main program

MOV R0, #30H; initialise the subroutine by putting the start address into R0
MOV R1, #05H; and by putting the size of the set into R1
CALL average

 
;the subroutine
average:

MOV B, R1; copy the size of the set into the B register
CLR A ; clear the ACC

loop: ADD A, @R0; add to the ACC the data in the location pointed to by R0
INC R0; increment R0 so that it points to the next memory location
DJNZ R1, loop; decrement R1 and if it is still not zero jump back to loop
DIV AB; once all the numbers have been added together, divide them by the size of the set, which is stored in B
RET; return from subroutine
   
  The above version of average uses indirect addressing. To make this clear, let us look at the difference between the following two instructions:
 
ADD A, R0
ADD A, @R0
   
  If R0 contains 30H (as is the case when the subroutine is called in the main program above) the first instruction above (ADD A, R0) adds the contents of R0 (30H) to the accumulator. The second instruction (ADD A, @R0) - the indirect instruction used in the subroutine - adds the contents of the location pointed to by R0 to the accumulator. Since R0 contains 30H, the contents of location 30H are added to the accumulator. R0 is then incremented so that the next time ADD A, @R0 adds the contents of location 31H to the accumulator, and so on until R1 reaches zero.
   
  In this way the subroutine can be used to get the average of a set of numbers of any size (up to 255) and at any location in data memory.
  But the problem of neglecting to initialise the subroutine still remains. If the two lines MOV R0, #30H and MOV R1, #05H are not placed before the call to average then there is no way of knowing how the subroutine will behave.
   
   
  Saving the Controller Status
  Large programs are usually written by a team of programmers. One person in the team might write one particular set of subroutines, while another programmer might write the main program that calls all these subroutines. How can we ensure the subroutines do not corrupt the main program's status.
  For example, if the main program stores a value in R2, and then calls a subroutine that also stores a value in R2, the main program's data in R2 will be lost.
  One solution: the main program uses a different register, say R7. In this way, the subroutine using R2 does not effect the main program using R7. However, this approach presents many difficulties. The programmer working on the main program must communicate with the programmer working on the subroutine. They must both ensure they use different areas of memory. Also, what happens when both the main program and the subroutine need to make use of the accumulator? In reality, this approach simply will not work.
  The correct approach is to save the data in all memory locations that will be used by the subroutine on the stack at the start of the subroutine. Then, just before returning from the subroutine, take all the saved data back off the stack and put it back in the locations it was originally in.
   
 
;the main program
using 0 ; assembler directive that indicates to the assembler which register bank is being used (in this case bank 0)

MOV R0, #30H; initialise the subroutine by putting the start address into R0
MOV R1, #05H; and by putting the size of the set into R1
CALL average

 
;the subroutine
average:

PUSH PSW
PUSH AR0
PUSH AR1

MOV B, R1; copy the size of the set into the B register
CLR A ; clear the ACC

loop: ADD A, @R0; add to the ACC the data in the location pointed to by R0
INC R0; increment R0 so that it points to the next memory location
DJNZ R1, loop; decrement R1 and if it is still not zero jump back to loop
DIV AB; once all the numbers have been added together, divide them by the size of the set, which is stored in B
POP R1
POP R0
POP PSW
RET; return from subroutine
   
  The PSW should always be pushed onto the stack. The PSW contains the carry bit, the parity bit, the overflow bit, etc. These bits should be saved on the stack so that when returning from the subroutine they will have the same values as they had when entering the subroutine.
  R0 and R1 are also saved on the stack because their values are changed by the subroutine.
   
  Notice AR0 and AR1 are used instead of R0 and R1. If you look at the instruction set you will see why this is so. The PUSH instruction and the POP instruction take as an operand an 8-bit address from 00H to FFH. The instruction PUSH PSW is changed by the assembler to PUSH D0H (D0H is the address of the PSW). In the same way, PUSH AR0 is replaced by PUSH 00 (00 is the address of R0 if the register bank being used is the default register bank).
  This is simply a convenience to the programmer. The programmer could write PUSH 01 to push R1 onto the stack. But the code is more readable as PUSH AR1.
  For this to work, the programmer must tell the assembler which register bank is being used. The first line of code does this: using 0
   
   
  Why were A and B not pushed onto the stack in the example above?
  The only memory locations that should not be pushed onto the stack at the start of a subroutine are the locations that will store the return data. Return data is any data that the subroutine is providing to the calling program. In our example, the return data is the average, the integer in A and the remainder in B. Therefore, the programmer working on the main program will know, from communicating with the programmer developing the average subroutine, that the answer (ie; the average) will be stored in A and B. The programmer knows this subroutine is going to change those registers and it is his/her responsibility to ensure any data stored in A and B is saved somewhere else before calling the subroutine.
   
   
 
<-Previous
 
   
 
Copyright (c) 2005-2006 James Rogers