One type of routine that you will require more frequently than perhaps
any other is a delay routine. Necessary because - you will
often need to time-scale down the Mhz magnitude operating speed of your
PIC to produce Hz magnitude, human-readable output timing.
For example, in the LED blinker tutorial, without some sort of delay
routine to "pause" the MCU, the LED would blink far too fast,
and your eye would percieve it as solidly lit. Of course, the routine
doesn't actually "pause" anything at all. It merely keeps
the MCU busy for a certain interval doing nothing useful. Well...
it is useful of course, in an indirect sort of way!
To figure out delay times, you have to know how long an instruction
takes to execute. According to the Data Sheet for all PICs, 1 instruction
requires 4 clock cycles. So first we have to divide clock frequency
by 4 to get "instruction frequency." If we are using the internal
oscillator (as in most of these tutorials), that gives us an instruction
frequency of 1 MHz. To get our instruction period, we simply
divide instruction frequency by 1: T = 1/f => T = 1 uS,
or 1 microsecond.
Armed with that knowledge, let's figure out how to get an accurate
delay! Let's aim for a delay of about one second.
The most obvious method would be to simply use a string of "nop"s
- the instruction for "no operation." Simply burn some clock
cycles... but it should be immediatly clear that this won't be very
practical! Because, see, there are 1,000,000 microseconds in a second
- and I'm pretty sure my PIC's don't have that much memory
to burn!
There are some more esoteric methods to burn instruction cycles a bit
more efficiently (i.e. "goto $+1"), but I won't cover those
at this point. If you're interested on more information, look up "tip
#1" in the MPASM assembler help file.
We are going to jump straight to the final solution - looping.
The general strategy here is to create a loop to burn all those
instruction cycles, utilizing very little program memory. Consider this
clip of code:
delay
movlw 0xFF
movwf delayA
loopA
decfsz delayA, f
goto loopA
return
|
Let's follow this through and see what happens.
delay -
this is the label of our routine. When you need the main routine to
"pause," you simply insert a "call delay" instruction,
and the program counter (the instruction the MCU is currently executing)
will jump to the instruction immediately after that label. In this case,
it will jump to movlw 0xFF:
movlw 0xFF
movwf delayA
These two lines insert the value 0xFF into the file register I have
named "delayA" (for information on defining variables, look
up UDATA_SHR in the assembler help file).
loopA
This label defines
the start of our actual timing loop.
decfsz delayA, f
The instruction
"decfsz" means: "decrement the value located at register
delayA, and skip the next instruction if the result is zero" and
the "f" means, "put the result of that operation back
into the file register" (as opposed to "w", which would
place the result in the working register).
To see how this
is going to work, let's consider that instruction and the next two together:
1.
decfsz delayA, f
2. goto
loopA
3. return
Earlier we loaded 0xFF (decimal 255) into delayA. When line 1 is reached,
delayA is decremented to 254. This is not 0, so the program does not
skip the next instruction, which is line 2. Line 2 is executed, and
the program returns to line 1. delayA is decremented again - 253. Still
not zero... and thus it continues to loop - until delayA reaches 1.
When delayA is 1, and line 1 executes, delayA decrements to 0, and so
line 2 is skipped, and the program jumps to line 3. Line 3
exits the routine, and our pause is complete.
So, how many instruction cycles (microseconds) has our loop burned?
Line 1 counts as one instruction, but line 2 counts as two (according
to the data sheets for all PIC's, any instruction that causes a skip
or jump burns 2 cycles). So as the program loops around lines
1 and 2, it is burning 3 cycles. So our delay time is 3 x 255 + 1 (the
extra one comes from when delayA reaches zero, and skips, taking
2 cycles to do it's job), or .765 milliseconds.
Still not good enough!
Our next trick is to use a nested loop. A loop within a loop.
Look at the code below:
delay
movlw 0xFF
movwf delayA
loopA
movlw 0xFF
movwf delayB
loopB
decfsz delayB, f
goto loopB
decfsz delayA, f
goto loopA
return
|
A little more complicated, but there's nothing here you haven't seen.
The basic logic is this: loopB is inside loopA, so loopB will
do it's business of burning 765 cycles 255 times! Throw in
the extra overhead instructions for loading and re-loading delayB with
0xFF 255 times, and now you have about .2 seconds. Now we're talking!
That nested loop burns over 195,000 instruction cycles for us.
We're almost there. We could use a 3rd loop, executed 5 times to get
us to about 1 second. But instead, we're just gonna place a couple of
strategic nop's in our
loopB to get our multiple of 5.
Here's the final delay routine:
delay
movlw 0xFF
movwf delayA
loopA
movlw 0xFF
movwf delayB
loopB
decfsz delayB, f
nop
nop
goto loopB
decfsz delayA, f
goto loopA
return
|
Now instead of loopB burning 3 instructions 255 times, it's burning
5 instructions - so our internal loop takes 5 x 255 + 1 to
execute -
top