2022-02-04
1. Subroutines
First, we consider functions with zero input arguments only.
Sequence to call a function:
-
Bookmark the current position in the program.
-
Jump to the function’s first instruction.
-
… execute the function’s code …
-
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”
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
.
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?
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 toInit
instead. Don’t forget to also change theMain:
label toInit:
. 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?
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?