Subroutines on the PIC

This is our third, and final, web page about the PIC. The first web page described PIC arithmetic and logic operations and the second discussed PIC's special registers. Here we discuss PIC subroutine calls.

We start with a warning. The PIC family of processors does have a simple stack to hold subroutine call return points. However, that stack is not very large. On the PIC12c671 it can hold only eight return points. This means that the depth of ongoing subroutine calls must be eight or less. In any case a computer with only 128 bytes of memory isn't the appropriate place for heavy recursion, so this should be a great problem.

Program counter

You can't understand the subroutines without understanding the program counter. The program counter records the address of the instruction being executed by the computer. Generally, the program counter is incremented by one each time an instruction is executed; however, some instructions, such as the goto, directly modify the program counter. For example, the real meaning of the instruction

is "Set the program counter to location l".

If you've guessed that the program counter is contained in a special file register, you're doing a good job of paying attention. If you've guessed it's in two special file registers, you're doing even better. You see, the PIC12C671 has room for 1024 machine instructions. Consequently, 10 bits are required to address all these locations. The lower 8 of these 10 are stored in special register PCL (02h) and the upper two are stored in PCLATH (0Ah). Fortunately we very rarely worry about PCL and PCLATH and we'll just refer to the register duo as "PC" in this document.

Call and return

The PIC12C671 has one instruction for calling a subroutine and two instructions for returning from a subroutine call. The call is very similar to the goto. The difference is that the call pushes the address of the next instruction, or PC+1, onto the top of an eight-slot instruction address stack. The return instructions are very similar. Both pop a value off the top of the stack and set the program counter to that value. That way, the subroutine routines to the instruction after the call.

Maybe we better look at an example.

A simple subroutine

How about a routine that increments a 24-bit number stored in the register pair 70h, 71h, and 72h. Assume the lower 8-bits are in 70h and the higher 8-bits are in 72h. The code for doing the incrementing isn't that hard. All we have to do if look out for the case when one of the 8-bit registers "rolls over", that is, turns from 255 to 0.

Using the property that bit ZERO of the STATUS register is set when the result of the increment is zero, we can write the PIC code as:

incr24                          ; start of the subroutine
        incr    70h,F
        btfss   STATUS,ZERO
        incr    71h,F
        btfss   STATUS,ZERO
        incr    72h,F

Yep, that's it. So, how do we use it? All we need do is code in:

        call    incr24
                                ; return to here

Of course, we may need to first make sure that registers 70h, 71h, and 72h are loaded with our arguments.

Calling conventions

The only means of passing values to and receiving results from PIC subroutine are the file registers. The writer of a PIC subroutine must clearly document the file registers used by his/her code. Furthermore, because it is likely that a non-trivial PIC routine will require additional "scratch" file registers for "local" variables, all used routines should be clearly documented.

In the real world, assembly language programmers follow calling conventions when writing subroutines. Calling conventions are rules which specify which registers or memory locations are used for arguments and which registers or memory locations are available for temporary results. Because we don't anticipate anyone writing elaborate PIC subroutines in CSCI 255, we will not introduce detailed calling conventions. Instead, we just advise you to carefully document your code.

A subroutine which uses subroutines

Let's suppose you want to frequently reverse the bits of a byte, that is, turn 00110101 into 10101100 and turn 1010101011 into 11010101. So, you decide to write a suboutine called revb that performs this task. You also decide that revb will receive the input byte in the W register and will also store its output there.

You begin by write your routine's specification:

Unca Student
Reverses the bits of a single byte
A single byte stored in the W register
A single byte returned in the W register
Registers 60h, 61h, and 62h may be modified by this routine.

You added the last clause because you figured you couldn't write this routine while modifying a few registers. Three seemed a tad excessive, but it does make life easier for you.

After working on this programming task for a couple of weeks with little progress, you ask a friend if she will write a PIC subroutine called revn, for you. This routine will reverse the last four bits of the W register. You give her the following specification:

Unca Student
Reverses the lower four bits, or nibble, of a single byte
A single byte stored in the W register
A single byte returned in the W register

You feel confident. If your friend's code can reverse the bits of a half byte, or nibble, all you need do is

Notice that you even made your life easier by not allowing your friend to use any temporary registers.

Your friend looks at the specification for ten minutes and points out that it really doesn't specify what happens to the upper four bits of the W register. She says she'll write the routine, if you agree to modify the Output section to be:

A single byte returned in the W register. The lower nibble of the W register contains the reversed bits. The upper nibble of the W register is all zeroes.

You agree. She comes back in 30 minutes with the routine.

Now you can write your implementation of revb by using your friend's implementation of revn.

revb                    ; subroutine to reverse bits of a byte
                        ; input  -- passed in W
                        ; output -- returned in W
                        ; temps  -- register 60h is modified
        MOVWF   60h     ; 60h now has the original byte
        CALL    revn    ; W now has the reversed lower four bits
        MOVWF   61h     ; 61h now has the reversed lower four bits
        SWAPF   60h,W   ; W has has the original upper four bits
        CALL    revn    ; W now has the reversed upper four bits
        SWAPF   61h,F   ; 61h now has the reversed nibble in the upper half
        IORWF   61h,W   ; W now has the whole enchilada reversed

The other return

The other return instruction, retlw, not only pops the instruction stack and sets the PC, it also stores a new value into the W register. This provides an easy way to return, with a value, in a single instruction. However, the most useful purpose of retlw is for table lookups. We'll talk about that in lab.

Return to Lab 1.