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 delay10ms. The instruction rcall simply branches to this label and pushes a return address to The Stack.

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 .include 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.

rjhcoding.com 2018