Subroutines Explained:: Memor y Location PCL
Subroutines Explained:: Memor y Location PCL
We have already used subroutines in our example 4-bit counter. They hopefully made sense at the
time, but as is often the case, there is slightly more to them that at first appears. So now is a good
time to look in more detail, uncovering some essential facts along the way.
We've made the assumption that one line executes after another, in a logical fashion - and this is
quite right for the most part. But, what actually happens inside the PIC?
It's time to introduce the concept of a programme counter. This is a register that is reset to zero at
power-on, or reset, and always contains the address in programme memory of the next instruction to
be executed. As the next instruction is fetched from programme memory, this register is automatically
incremented. The only exception to this rule is when the instruction causes a branch.
The program counter is available to us - we can write to it at will, and as we'll see later this opens the
door to some very powerful techniques. But for now, refer to the memory map, and note PCL, which is
at memory address 02h - it's also mapped to 82h in Bank 1.
Just to clarify things, this short program has been shown in table form to highlight how PCL relates to
commands in memory. This simple program flashes an LED connected to RB0 (bit 0 of PORTB - pin 6)
- only don't try to use it because as it stands, there is no time delay so it will flash far too fast to be
visible to the naked eye.
Memor
y PCL
location
ORG 0 ;Reset vector 0
0 clrf PORTA ;all of porta low 1
1 clrf PORTB ;all of portb low 2
2 bsf STATUS, RP0 ;change to bank1 3
3 clrf TRISA ;all of porta outputs 4
4 clrf TRISB ;all of portb outputs 5
5 bcf STATUS, RP0 ;back to bank0 6
6
6 Main_loop bsf PORTB, 0 ;Set RB0 - LED on 7
7 bcf PORTB, 1 ;Clear RB0 - LED off 8
8 goto Main_loop ;and repeat... 9 6
This table attempts to demonstrate the behaviour described above. PCL starts at zero, and changes to
1 as soon as the first instruction has been fetched. This simple pattern repeats until the PIC gets to
the goto in memory location 8 - PCL changed to 9 as the fetch occurred, but upon decoding the
instruction and realising it was a "goto", it changed PCL to equal 6. It knew that 6 was the required
value because the assembler realised that Main_loop is in fact a label for memory location 6. In actual
fact, this simple modification of the programme counter is all that a goto does.
But this isn't the whole story. Remember, data memory is 8 bits, which means a total of 256 different
values. PCL exists in data memory, which means that it's only able to refer to 256 different locations
in programme memory. This might be OK for small PICs, but the PIC16F84 has 1024 words of
programme memory, which needs 10 bits. These extra bits are stored in a register called PCLATH
(0Ah). So, while PCL stands for program counter low - PCLATH stands for programmecounter latch
(holding).
As the latter name suggests, it's slightly more complicated than that. Strictly speaking, PCLATH
is not the high bits of PC (program counter). Rather, it's a holding register that we can set up prior to
doing a computated goto. We will look at this later in much more detail.
Subroutines:
We know already that we can jump to a subroutine using a "call" statement, and we've also seen that
the program will return to where it left off when the processor meets a "return". So how does "call"
differ from "goto"?
Memor
y PCL
location
10 incf Counter, f ;Increment Counter 11
11 call Output ;Output 12 100
12 movwf Loop ;W = Loop 13
100 Output movfw Counter ;W = Counter 101
101 movwf PORTB ;Output W to portb 102
102 return ;done... 103 12
This table shows two snippets of code taken from a larger program. The first 3 lines are an extract
from the main programme, where a variable called Counter is incremented, then a subroutine called
Output is called. This subroutine simply writes the current value of Counter to PORTB. The actual code
is not really important, but the program flow is.
The first line shown above happens to be in memory location 10, hence PCL is pointing to 11, the next
location. When the processor fetches and decodes this next instruction, it realises that it is a "call". As
before with "goto", the PIC will load PC with the new address, which is 100 in this case. But, before
doing this, it will store the current value of PCL somewhere safe - this is how it knows where to go
when it meets a "return". Understanding this is key!
This "safe house" is called a "stack", which is a commonly found construct in microprocessors. It is a
simple buffer where the last number "pushed" onto the stack will be the first number taken from the
stack. Think of a simple pile of playing cards on your desk - just don't shuffle them!
This "last-on, first off" behaviour is what allows us to "nest" subroutines. You saw this when we looked
at the timing routines - it is perfectly acceptable to call a subroutine from within a subroutine:
PROCESSOR STACK:
0 1 2
Loop movlw d'100'
call WaitNms
WaitNms movwf timer_lcl
etc...
call loop1ms
loop1ms addlw d'255'
etc...
return
etc...
return
rest of code...
This diagram shows a simplified program that creates a 100ms delay using the delay routines studied
before. Starting at the top left, the main program puts 100 into the working register and calls
WaitNms. At this point, the return address is pushed onto the stack, and the stack pointer is
incremented, moving us across the page. We then enter the WaitNms subroutine, represented by the
different background colour. When the processor meets "call loop1ms", it pushes another return
address onto the stack, and jumps to another subroutine. At the end of "loop1ms", the processor
meets the first "return" statement, and "pops" the last number from the stack. This moves us left, as
the stack pointer is decremented. The processor will meet the next return, and again will retrieve the
return address from the stack, and we're back in our main program.
The stack is a separate section of memory - it is not in programme memory or data memory, and we
can not access it in any way. It is entirely managed by the processor during subroutine operations.
This is in contrast to some larger processors that let you store your own variables there.
VERY IMPORTANT:
The stack is subject to one major limitation - it can only contain 8 locations!
This means that you CAN NOT nest more than 8 subroutines. This is vitally important! Subroutines are
an excellent way of making code structured and logical, but use them carefully! Unfortunately, there is
no mechanism within the processor to tell you that you have exceeded the maximum number of
locations.
Worse than that, the stack operates as a circular buffer - this means that when you push the ninth
location onto the stack, it wraps around and overwrites the first location in the stack! Using the
example above, imagine that the WaitNms subroutine somehow managed to call to many nested
subroutines and used too many locations in the stack - when the processor meets the return, it will try
to recover the return address from the stack and find the wrong address because it was overwritten
during the subroutine. At this point, the program will return to the wrong point and probably crash!
When writing your program, be careful to match every call with a return. Don't jump out of a
subroutine back into the main programme loop because the stack will quickly overflow.
Optimising subroutines:
This next tip might seem somewhat esoteric, but it's surprising how frequently you'll be able to
implement this.
This subroutine generates a 1 second delay by calling WaitNms four times with each time taking 250
milliseconds. Note that the last time we call WaitNms - when returning from the WaitNms subroutines,
we meet a return statement, and the Wait1Second subroutines ends.
When the processor arrives at the goto statement, it will jump to the WaitNms subroutine instead of
calling it. At the end of the WaitNms subroutine, the processor will meet a return. This return actually
serves to mark the end of the Wait1Second subroutine.
PROCESSOR STACK:
0 1 2
Start bsf PORTB,0
call Wait1Second
Wait1Second movlw d'250'
call WaitNms
WaitNms movwf
etc...
return
movlw d'250'
call WaitNms
WaitNms movwf
etc...
return
movlw d'250'
call WaitNms
WaitNms movwf
etc...
return
movlw d'250'
goto WaitNms
WaitNms movwf
etc...
return
rest of code...
This diagram attempts to show this. Note how the subroutines are called normally the first three
times, but for the last time, the program just jumps to WaitNms. This shortcut has saved a
programme memory word, and a couple of processor cycles. Although it might not seem like a big
deal, I promise you that you'll be able to use this often, and the savings soon stack up.