| |
 |
The 8051
|
|
|
|
| |
|
| |
|
| |
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 |
| |
In the same way that you, the programmer, may use the assembler
instruction JMP anytime you need an unconditional jump and the assembler
will replace this with the appropriate 8051 jump instruction (SJMP,
AJMP or LJMP), you may use the assembler CALL instruction
and it will be replaced by the appropriate 8051 subroutine call instruction
(ACALL or LCALL - note there is no SCALL instruction). |
| |
|
| |
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. |
| |
|
| |
|
| |
|
| |
|
| |
|
| |
Copyright
(c) 2005-2006 NyCelt LLC
|