2022-02-04

1. Subroutines

First, we consider functions with zero input arguments only.

Sequence to call a function:

  1. Bookmark the current position in the program.

  2. Jump to the function’s first instruction.

  3. … execute the function’s code …

  4. Jump back to the bookmarked position.

RCALL does steps 1 and 2.
RET does step 4.

Where does the bookmarked position get stored?

Click to reveal “on the stack”

On the stack.

A stack is a last-in, first-out (LIFO) structure. It is my favorite way to organize physical data, c.f. my office, which has many different stacks.

IF you do no bounds checking, a stack only requires a single pointer that indicates where the data that is on the “top of the stack” is located. Because it is a perfect fit for the needs of calling subroutines, a CPU architecture has a special register called … the Stack Pointer.

Since the SP needs to potentially point to any location in Data Memory (SRAM), which is 512 byte locations on the ATtiny85, it must be larger than 8-bits. Hence, the entire SP is split into two byte-wide registers that are treated as a 16-bit entity, SPH : SPL.

It is usually a good idea to check that the stack doesn’t grow larger than the space allocated for it, and that it always points to a location within the allocated range.

Neither are true for most CPUs. Much fun results when the SP sneaks out of bounds 😬 [1]

1.1. Stack Pointer setup

The code template that avr_sim uses for a New Project is worth a look.

Begin with the purpose of the code and critical information such as the targeted MCU(s).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
;
; ***********************************
; * (Add program task here)         *
; * (Add AVR type and version here) *
; * (C)2021 by Gerhard Schmidt      *
; ***********************************
;
.nolist
.include "tn85def.inc" ; Define device ATtiny85
.list
;

Some program constants are intended as “knobs” for a user to turn. Good practice is to group all the things that are OK to mess with into one location.

27
28
29
30
31
32
33
34
35
36
37
38
39
; **********************************
;   A D J U S T A B L E   C O N S T
; **********************************
;
; (Add all user adjustable constants here, e.g.)
; .equ clock=1000000 ; Define the clock frequency
;
; **********************************
;  F I X  &  D E R I V.  C O N S T
; **********************************
;
; (Add symbols for fixed and derived constants here)
;

A separate, later, section holds symbols / constants that either don’t change or are computed from the “input” constants. Having a clear separation helps reduce later confusion!

40
41
42
43
44
45
46
47
48
49
; **********************************
;       R E G I S T E R S
; **********************************
;
; free: R0 to R14
.def rSreg = R15 ; Save/Restore status port
.def rmp = R16 ; Define multipurpose register
; free: R17 to R29
; used: R31:R30 = Z for ...
;

Notes to self about how registers are used in this program. Creating symbolic names according to these purposes makes the code easier to read.

50
51
52
53
54
55
56
57
58
59
; **********************************
;           S R A M
; **********************************
;
.dseg
.org SRAM_START
; (Add labels for SRAM locations here, e.g.
; sLabel1:
;   .byte 16 ; Reserve 16 bytes)
;

Segment dedicated to organizing SRAM and variables.

On a MCU and especially when programming in assembly:

  • The initial state of the variables is completely unknown.

  • It is not possible to do:

    uint8_t var_name = 10;  // declare variable and initial value

    in assembly:

    .dseg
    .org SRAM_START
    ; define variable with initial value
    .db var_name = 10

    Which will trigger an assembler error.

What is the value of SRAM_START on the ATtiny85?

0x60

See §5 AVR Memories of the ATtiny85_Datasheet.pdf. The on-chip SRAM is in the same Data Memory address space as the two classes of registers, and so starts at location 96(10)

Now the Main event and the program code. On an AVR architecture, the first 15 program addresses are the Reset and the Interrupt Vectors. Each should either be an rjmp to the appropriate code location, or be an instruction that does not create a problem if an interrupt is triggered that has no ISR defined — here it simply returns from the interrupt with reti.

60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
; **********************************
;         C O D E
; **********************************
;
.cseg
.org 000000
;
; **********************************
; R E S E T  &  I N T - V E C T O R S
; **********************************
	rjmp Init ; Reset vector
	reti ; INT0
	reti ; PCI0
	reti ; OC1A
	reti ; OVF1
	reti ; OVF0
	reti ; ERDY
	reti ; ACI
	reti ; ADCC
	reti ; OC1B
	reti ; OC0A
	reti ; OC0B
	reti ; WDT
	reti ; USI_START
	reti ; USI_OVF
;
; **********************************
;  I N T - S E R V I C E   R O U T .
; **********************************
;
; (Add all interrupt service routines here)
;
  • Change the template’s Main rjmp target to jump to Init instead. Don’t forget to also change the Main: label to Init:. It is a little better description of the intent of that first segment of code.

 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
; **********************************
;  M A I N   P R O G R A M   I N I T
; **********************************
;
Init:
.ifdef SPH ; if SPH is defined (it always is for ATtiny85)
	ldi rmp,High(RAMEND)
	out SPH,rmp ; Init MSB stack pointer
.endif
	ldi rmp,Low(RAMEND)
	out SPL,rmp ; Init LSB stack pointer
; ...
	sei ; Enable interrupts
;

Now we’re ready to see what lines 97..103 are about — setting the stack pointer to its initial position. The extra work is simply because SP is a 12-bit entity stored in 16-bits into SRAM data memory addressed in 8-bit units.

What is the value of RAMEND on the ATtiny85?

0x025F

See §5 AVR Memories of the ATtiny85_Datasheet.pdf.

So, the Stack Pointer gets initialized as:
SPH = 0x02
SPL = 0x5F

According to the ATtiny85_Datasheet.pdf, the SREG is initialized to all zeros after reset. Therefore the I bit is cleared, which disables all interrupts. The sei enables those interrupts by setting the I bit, which is encoded to the same as the instruction bset 7.

1.2. Delay subroutine

Begin with the slow-blink.asm from a previous day’s example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
.equ INNER_COUNT = 0x03  ; 8b unsigned
.equ OUTER_COUNT = 0x02  ; 8b unsigned

; ...

Main:
    ;set PB3 high
    sbi PORTB, 3

    ldi r17, OUTER_COUNT
Loop00:
    ldi r16, INNER_COUNT
Loop0:
    dec r16
    brne Loop0
    dec r17
    brne Loop00

    ;set PB3 low
    cbi PORTB, 3

    ldi r17, OUTER_COUNT
Loop10:
    ldi r16, INNER_COUNT
Loop1:
    dec r16
    brne Loop1
    dec r17
    brne Loop10

    rjmp Main

Factor the double loop out to a subroutine labeled DelayA:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
.equ INNER_COUNT = 0x03  ; 8b unsigned
.equ OUTER_COUNT = 0x02  ; 8b unsigned


Main:
    sbi PORTB, 3    ;set PB3 high
    rcall DelayA    ; and wait

    cbi PORTB, 3    ;set PB3 low
    rcall DelayA    ; and wait

    rjmp Main       ; only to do it again!


DelayA:
; do pointless things for some time
    ldi r17, OUTER_COUNT

DelayA_LoopOuter:         ; ---+
    ldi r16, INNER_COUNT  ;    |
                          ;    |
DelayA_LoopInner:         ;-+  |
    dec r16               ; |  |
    brne DelayA_LoopInner ;-+  |
                          ;    |
    dec r17               ;    |
    brne DelayA_LoopOuter ; ---+
;
    ret  ;return from subroutine

Now notice that we are doing two loops that have the same form. Factor out so the outer loop calls another subroutine. Create a new subroutine DelayB which calls a (sub)subroutine DelayBShort:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
DelayB:
    ldi r17, OUTER_COUNT

DelayB_LoopOuter:         ; ---+
    rcall DelayBShort     ; -+ |
    dec r17               ;    |
    brne DelayB_LoopOuter ; ---+
;
    ret     ; return from outer delay


DelayBShort:
    ldi r16, INNER_COUNT
DelayB_LoopInner:         ;-+
    dec r16               ; |
    brne DelayB_LoopInner ;-+
;
    ret     ; return from inner delay

What happens with the stack and stack pointer in this new set of subroutines?


1. Have you heard of “stack overflow”? Programmers have been overflowing stacks long before Stack Exchange the company existed.