One of the SITCOM training computer prototypes
 SITCOM 85 Training Computer
 
 

Lesson 3

Our last program has grown to almost 100 bytes already. Which is no problem for us, as we have more than 32000 bytes to our disposal. But there is some room for improvement to our last programs.
If you look at the program a bit closer you'll see that it uses exactly the same code for both delay loops. The only difference being the label names, as they have to be unique in the program to avoid confusion. Wouldn't it be nice to have only one delay loop that can be used more than once?
It can be done, using subroutines. A subroutine is a piece of code that can be called from all other parts of the program as often as we like. When such a subroutine is called, the current program counter (PC) is placed in a special part of memory, called the stack. Then the PC is reloaded with the starting address of the subroutine, making the program continue from there. At the end of the subroutine a special instruction causes the PC to be restored to its pre-subroutine contents, which causes the program to continue where it was just before the subroutine was called.

So far the brief description, and I bet that it sounds quite confusing if you have never seen it work before. That's why we are going to explain the process in more detail now.

 
 

The stack

My favourite job around the house The stack is a dedicated piece of memory which can store temporary values, like return addresses or register contents. You may think of the stack as being a pile of clean, wet plates while doing the dishes together. One person cleans the dishes and puts them on top of a pile, the other person takes them from the top of the pile to wipe them dry. So the last plate that's put up the pile is taken off first, until all plates are gone.

How do we know where the stack is? The current location of the stack is pointed to by the stack pointer SP, a 16-bit register in the 8085. Being a 16-bit register means that the stack can be located anywhere in RAM (Read/write) memory.
On an 8085 only pairs of bytes can be stored on the stack, so it is impossible to save an odd number of bytes on the stack.
The stack can save the AF register pair (PUSH PSW), the BC register pair (PUSH B), the DE register pair (PUSH D), the HL register pair (PUSH H) or the program counter (CALL or interrupt).
Below you see the push sequence:

  • Decrement SP
  • Save Most Significant byte to location pointed to by SP
  • Decrement SP
  • Save Least Significant byte to location pointed to by SP

First the stack pointer is decremented (it was pointing to the top of the stack, to the last byte pushed). Then the most significant byte of the byte pair to be saved is placed in the memory location pointed to by the current value of the SP register. Then SP is decremented once more, pointing to the next free memory location. Finally the least significant byte of the byte pair is stored at that location.

Pulling words from the stack is done in the reverse order:

  • Load Least Significant byte from location pointed to by SP
  • Increment SP
  • Load Most Significant byte from location pointed to by SP
  • Increment SP

Please note that after one push and one pull operation, the stack is exactly in its previous situation.

Why do we need a stack to perform subroutine calls? Isn't it enough to simply save the PC somewhere in memory, and restore it from that memory once the subroutine is done?
NO! That is definitely not enough, because we can call another subroutine while we're in a subroutine our selves. This process is called subroutine nesting. We'll see some examples of subroutine nesting later on in this lesson.

But before we can use the stack, and thus before we can use subroutines, we have to initialize it. Initializing the stack means no more than setting the SP register to point to a free part of memory. In our examples we will place the stack at the top of memory, so it can grow down from 0FFFFH. This gives plenty of stack space, without having to worry that it gets in our way some time.

 
   

            LXI   SP,00000H       Initialize the stack pointer
 
 

Placing this single program line at the beginning of our program will do the trick. It may sound strange that we have to initialize the stack pointer to 00000H, if we want the stack to grow down from 0FFFFH. Remember that the stack pointer SP is decremented before the first value is stored in the stack. Decrementing a register that contains 00000H, will result in a wrap around to 0FFFFH, which is just what we wanted.

 
 

Subroutines

As we've seen in the last program of lesson 2, there were two identical delay loops. These are ideal to be replaced by a subroutine.
You can think of a subroutine as being the chorus of a song. In the lyrics of a song, the chorus is often written out only once. All subsequent choruses only refer to the real chorus by a single line named "Chorus".

Subroutines work quite similarly. Only this time the subroutine code itself is placed outside the main program, either before or after it. In our example we place the subroutine after the main program.
Here's the same program from lesson 2, the Kitt scanner, but this time the delay loop is implemented as subroutine.

 
   

;------------------------------------------------------------------------
;
; LESSON3A.ASM
;
; A Kitt scanner, with subroutines
;
;------------------------------------------------------------------------

            .IN   INIT01           Initialize the assembler

;------------------------------------------------------------------------

            LXI   SP,00000H        Initialize stack pointer
            MVI   D,00011111B      Initialize pattern
            MVI   E,11111111B
            MOV   H,E
            MOV   L,E

WALKR       MOV   A,E              Output the bit pattern to port A
            OUT   PORTA
            MOV   A,D              Output the bit pattern to port B
            OUT   PORTB

            CALL  DELAY            Slow the processor down a bit

            MOV   A,L              See if we have to change direction
            ANI   01000000B
            JZ    GOLEFT           We must if b6 is a 0!
GORIGHT     STC                    Set the carry
            MOV   A,H              Rotate entire pattern, including
            RAR                     the extra carry byte
            MOV   H,A
            MOV   A,D
            RAR
            MOV   D,A
            MOV   A,E
            RAR
            MOV   E,A
            MOV   A,L
            RAR
            MOV   L,A

            JMP   WALKR            Always jump back to WALKR

;------------------------------------------------------------------------

WALKL       MOV   A,E              Output the bit pattern to port A
            OUT   PORTA
            MOV   A,D              Output the bit pattern to port B
            OUT   PORTB

            CALL  DELAY            Slow the processor down a bit

            MOV   A,H              See if we have to change direction
            ANI   000000010B
            JZ    GORIGHT          We must if b1 is a 0!
GOLEFT      STC                    Set the carry
            MOV   A,L              Rotate entire pattern, including
            RAL                     the extra carry byte
            MOV   L,A
            MOV   A,E
            RAL
            MOV   E,A
            MOV   A,D
            RAL
            MOV   D,A
            MOV   A,H
            RAL
            MOV   H,A

            JMP   WALKL            Always jump back to WALKL

;------------------------------------------------------------------------

DELAY       MVI   B,50             Load outer loop counter

DELAY1      MVI   C,250            Load inner loop counter

DELAY2      NOP                    Begin of inner loop
            NOP
            NOP
            NOP
            DCR   C                Decrement inner loop counter
            JNZ   DELAY2           Repeat until loop counter = 0

            DCR   B                Decrement outer loop counter
            JNZ   DELAY1           Repeat until loop counter = 0
            RET                    Return to calling program
 
 

We have seen most of this program before. The first change is immediately in the first line of code, where the stack pointer is initialized.
Both delay loops are replaced by a CALL DELAY instruction. Here is what this does:

  • The current program counter is pushed onto the stack.
  • A new value is loaded into the program counter, which is the location of the DELAY subroutine.
  • The program continues executing the entire delay routine, until.....
  • it reaches the RET instruction. The RET instruction causes the program to return to its previous task. This is done by pulling the previous program counter value from the stack.
  • Next the instruction following the CALL DELAY program line is executed.

Easy enough, isn't it?! This program is 92 bytes long, as opposed to the 98 bytes of the original program.
Was it really worth all the fuss? A saving of only 6 bytes? Yes it was! Subroutines are a very substantial part of programming. They allow you to break up the entire task into little pieces, which is a very good programming practice. Each piece of the program can be debugged separately, and can be reused as often as you like. You can make subroutines more flexible by passing some parameters to them, as we will do further on in this lesson.

 
 

Nesting subroutines

If we look a bit closer at this last program we see another small portion of code appearing twice. It's the part that copies the bit patterns to the output ports. These can also be put inside a subroutine.
But look a bit closer! There is one more line that can be placed inside this new subroutine, the CALL DELAY line.
Here's what the new program will look like:

 
   

;------------------------------------------------------------------------
;
; LESSON3B.ASM
;
; A Kitt scanner, with nested subroutines
;
;------------------------------------------------------------------------

            .IN   INIT01           Initialize the assembler

;------------------------------------------------------------------------

            LXI   SP,00000H        Initialize stack pointer
            MVI   D,00011111B      Initialize pattern
            MVI   E,11111111B
            MOV   H,E
            MOV   L,E

WALKR       CALL  OUTPUT           Output both bit patterns and delay

            MOV   A,L              See if we have to change direction
            ANI   01000000B
            JZ    GOLEFT           We must if b6 is a 0!
GORIGHT     STC                    Set the carry
            MOV   A,H              Rotate entire pattern, including
            RAR                     the extra carry byte
            MOV   H,A
            MOV   A,D
            RAR
            MOV   D,A
            MOV   A,E
            RAR
            MOV   E,A
            MOV   A,L
            RAR
            MOV   L,A

            JMP   WALKR            Always jump back to WALKR

;------------------------------------------------------------------------

WALKL       CALL  OUTPUT           Output both bit patterns and delay

            MOV   A,H              See if we have to change direction
            ANI   000000010B
            JZ    GORIGHT          We must if b1 is a 0!
GOLEFT      STC                    Set the carry
            MOV   A,L              Rotate entire pattern, including
            RAL                     the extra carry byte
            MOV   L,A
            MOV   A,E
            RAL
            MOV   E,A
            MOV   A,D
            RAL
            MOV   D,A
            MOV   A,H
            RAL
            MOV   H,A

            JMP   WALKL            Always jump back to WALKL

;------------------------------------------------------------------------

DELAY       MVI   B,50             Load outer loop counter

DELAY1      MVI   C,250            Load inner loop counter

DELAY2      NOP                    Begin of inner loop
            NOP
            NOP
            NOP
            DCR   C                Decrement inner loop counter
            JNZ   DELAY2           Repeat until loop counter = 0

            DCR   B                Decrement outer loop counter
            JNZ   DELAY1           Repeat until loop counter = 0
            RET                    Return to calling program

;------------------------------------------------------------------------

OUTPUT      MOV   A,E              Output the bit pattern to port A
            OUT   PORTA
            MOV   A,D              Output the bit pattern to port B
            OUT   PORTB

            CALL  DELAY            Slow the processor down a bit
            RET                    Return to calling program
 
 

Our program is now only 90 bytes long! This program demonstrates the nesting of subroutines.
The subroutine OUTPUT copies the two bit patterns to the output ports A and B, and then it calls the DELAY subroutine.

Here we can see the power of the stack. Without any problem the processor remembers its way back, even if we have altered the program counter twice!
When we call the subroutine OUTPUT the program counter is pushed on the stack. After a few other instructions we call the subroutine DELAY, pushing the program counter on the stack once more. Now there are two program counter values stored on the stack. This is no problem for the processor, because the last pushed program counter is pulled from the stack first. So after the DELAY routine we return to the last program line of our program, the RET instruction that will eventually pull the first pushed program counter value from the stack.

Nesting subroutines is a very common practice in assembly programming, and we are likely to see many more examples of it in the upcoming lessons and projects.

 
  Continue With Lesson 3 - Creating a dynamic delay subroutine
 
  [Home] [Latest News] [Essentials] [Hardware] [The Build] [Programs] [Projects] [Downloads]
 
  Made in the UK