Example Program #4: Delay Subroutine
We saw before how to blink an LED using counters to create a delay. Here we will see how to create a modular subroutine from this code that can be used in other programs.
In the below program, we write a subroutine that will create a delay that is a multiple of 10ms, passed as a parameter in r18, which we define as loopCt.
.include "m328pdef.inc"
.def mask = r16 ; mask register
.def ledR = r17 ; led register
.def loopCt = r18 ; delay loop count
.def iLoopRl = r24 ; inner loop register low
.def iLoopRh = r25 ; inner loop register high
.equ iVal = 39998 ; inner loop value
.cseg
.org 0x00
ldi r16,LOW(RAMEND) ; initialize
out SPL,r16 ; stack pointer
ldi r16,HIGH(RAMEND) ; to RAMEND
out SPH,r16 ; "
clr ledR ; clear led register
ldi mask,(1<<PINB0) ; load 00000001 into mask register
out DDRB,mask ; set PINB0 to output
start: eor ledR,mask ; toggle PINB0 in led register
out PORTB,ledR ; write led register to PORTB
ldi loopCt,50 ; initialize delay multiple for 0.5 sec
rcall delay10ms ; call delay subroutine
rjmp start ; jump back to start
delay10ms:
ldi iLoopRl,LOW(iVal) ; intialize inner loop count in inner
ldi iLoopRh,HIGH(iVal) ; loop high and low registers
iLoop: sbiw iLoopRl,1 ; decrement inner loop registers
brne iLoop ; branch to iLoop if iLoop registers != 0
dec loopCt ; decrement outer loop register
brne delay10ms ; branch to oLoop if outer loop register != 0
nop ; no operation
ret ; return from subroutine
Code Breakdown
In the beginning of our code, we have our standard include and define directives, as we've seen before
.include "m328pdef.inc"
.def mask = r16 ; mask register
.def ledR = r17 ; led register
.def loopCt = r18 ; delay loop count
.def iLoopRl = r24 ; inner loop register low
.def iLoopRh = r25 ; inner loop register high
.equ iVal = 39998 ; inner loop value
.cseg
.org 0x00
Initializing The Stack Pointer
Following this is something we have not seen before. We setup something called the Stack Pointer.
ldi r16,LOW(RAMEND) ; initialize
out SPL,r16 ; stack pointer
ldi r16,HIGH(RAMEND) ; to RAMEND
out SPH,r16 ; "
When a program calls a subroutine, the microcontroller needs to know where to return when the subroutine ends. It handles this by pushing a return address to a location in SRAM referred to as The Stack.
The location of The Stack is defined by the address loaded in a 16-bit I/O Register called The Stack Pointer, the high and low bytes of which are defined in the header file as SPH and SPL.
In the code above, we initialize The Stack Pointer with the address of RAMEND - the last value of SRAM. Now, when a subroutine is called, a return address will be pushed to this location.
Following this, we setup a bit mask and a set of instructions to toggle PINB0, just like in the LED Blink example.
clr ledR ; clear led register
ldi mask,(1<<PINB0) ; load 00000001 into mask register
out DDRB,mask ; set PINB0 to output
start: eor ledR,mask ; toggle PINB0 in led register
out PORTB,ledR ; write led register to PORTB
Calling Our Subroutine
Next, we load 50 into loopCt (which will be used as an input by our subroutine) and use rcall to call our delay subroutine.
ldi loopCt,50 ; initialize delay multiple for 0.5 sec
rcall delay10ms ; call delay subroutine
Note that the subroutine is defined by the label rcall simply branches to this label and pushes a return address to The Stack. . The instruction
The delay subroutine is mostly a cut and paste from the code we saw previously in the LED Blink tutorial, except that the outer loop count is initialized outside of the subroutine. This allows us to pass it in as a parameter in the register loopCt.
delay10ms:
ldi iLoopRl,LOW(iVal) ; intialize inner loop count in inner
ldi iLoopRh,HIGH(iVal) ; loop high and low registers
iLoop: sbiw iLoopRl,1 ; decrement inner loop registers
brne iLoop ; branch to iLoop if iLoop registers != 0
dec loopCt ; decrement outer loop register
brne delay10ms ; branch to oLoop if outer loop register != 0
nop ; no operation
ret ; return from subroutine
In the above there are a few instructions we haven't seen before, the first of which is nop - no operation. nop does exactly what it sounds like - nothing. In this case it lets us waste a clock cycle to make our count lineup nicely.
nop is the most useful of useless instructions. It simply instructs the microcontroller to do nothing for a clock cycle.
The next new instruction is ret - return from subroutine. When this instruction is reached, the microcontroller will jump back to the instruction immediately following the subroutine call.
Execution Time
The purpose of this subroutine is to creat delays that are multiples of 10ms. Let's take a look at the cycle count for this.
The inner loop, defined by the code below
iLoop: sbiw iLoopRl,1 ; 2 cycles
brne iLoop ; 2 or 1 cycles
will take
innerLoopCount = iVal*(2 + 2) - 1
= 39998*4 - 1
= 159991 cycles
The clock cycles for the outer loop.
delay10ms:
ldi iLoopRl,LOW(iVal) ; 1 cycle
ldi iLoopRh,HIGH(iVal) ; 1 cycle
iLoop: sbiw iLoopRl,1 ; 159991 cycles
brne iLoop ; '
dec loopCt ; 1 cycle
brne delay10ms ; 2 or 1 cycles
will take
outerLoopCount = loopCt*(1 + 1 + 159991 + 1 + 2) - 1
= loopCt*(159996) - 1
Adding in 1 cycle for nop and 4 cycles for ret gives
outerLoopCount = outerLoopCount + 1 + 4
= loopCt*(159996) - 1 + 1 + 4
= loopCt*(159996) + 4
The number of cycles required for ret will vary between microcontrollers depending on how much flash memory they have. Check your datasheet to get the value specific to your microcontroller.
If loopCt is initialized to 1, the routine will take 160000 cycles to complete, exactly 10ms for a 16MHz clock.
Unfortunately, for loopCt values greater than 1, we will have slight errors in the clock count. For example, initializing loopCt with 50 for a delay of 0.5 seconds gives a cycle count of 7999804. This results in a delay of 0.49998775 seconds, or an error of 0.00245%.
Ok, it's not perfect, but it's a small price to pay for a modular subroutine and more than accurate enough for blinking an LED.
Creating an Include File
If you want to use this delay subroutine in multiple programs, the best method is to place it in another file and use the directive to place it in our code.
For example, the following could be saved into a file delay10ms.asm.
;**************************************************************
;* subroutine: delay10ms
;*
;* inputs: r18 - sets multiple of 10ms for delay
;*
;* registers modified: r18, r24, r25
;**************************************************************
.def oLoopR = r18 ; outer loop register
.def iLoopRl = r24 ; inner loop register low
.def iLoopRh = r25 ; inner loop register high
.equ iVal = 39998 ; inner loop value
delay10ms:
ldi iLoopRl,LOW(iVal) ; intialize inner loop count in inner
ldi iLoopRh,HIGH(iVal) ; loop high and low registers
iLoop: sbiw iLoopRl,1 ; decrement inner loop registers
brne iLoop ; branch to iLoop if iLoop registers != 0
dec oLoopR ; decrement outer loop register
brne delay10ms ; branch to oLoop if outer loop register != 0
nop ; no operation
ret ; return from subroutine
Now, the original program we wrote can simply include the delay subroutine and call it just like before.
.include "m328pdef.inc"
.def mask = r16 ; mask register
.def ledR = r17 ; led register
.def loopCt = r18 ; delay loop count
.equ iVal = 39998 ; inner loop value
.cseg
.org 0x00
ldi r16,LOW(RAMEND) ; initialize
out SPL,r16 ; stack pointer
ldi r16,HIGH(RAMEND) ; to RAMEND
out SPH,r16 ; "
clr ledR ; clear led register
ldi mask,(1<<PINB0) ; load 00000001 into mask register
out DDRB,mask ; set PINB0 to output
start: eor ledR,mask ; toggle PINB0 in led register
out PORTB,ledR ; write led register to PORTB
ldi loopCt,50 ; initialize delay multiple for 0.5 sec
rcall delay10ms ; call delay subroutine
rjmp start ; jump back to start
.include "delay10ms.asm" ; include delay subroutine
If you include a file at the end of a program, make sure to place a newline after it. Not doing so will cause an error.
Conclusion
Now you've seen how to initialize The Stack Pointer and call subroutines. In the next tutorial we will look at The Stack Pointer and subroutine calls in more detail.
Post a Comment