PIC MCU's - Understanding Delay Routines

 

> Home
> PIC Microchip Tutorials
> PIC Getting Started Kit
> Robotics
> Contact
> About me

 

 

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

Copyright © 2005 Anthony Rogers