longchute

about

ATmega32 Bare Metal 7-Segment LED

24 Oct 2013

This builds on the last post, which booted an ATmega32 from bare metal using avr-as (but not avr-libc). After fighting with avr-as and avr-ld for over 6 hours, I switched to AVRA. It's a free, open-source drop-in replacement for Atmel's own assembler, sharing the same syntax. Building is accomplished in one command now, instead of three. I continue to use avrdude to program the ATmega32, but a slight tweak in the command line is necessary, as AVRA produces Intel Hex files instead of plain, unadorned binaries.

7-Segment LED display on an ATmega32

The big new thing in this version is the use of LPM, or Load Program Memory. It uses register Z, which is a 16-bit pointer register composed of two 8-bit registers, r31 (aka ZH) and r30 (aka ZL). This is used to place a table into program Flash (much larger at 32k than SRAM at 2k) containing the segments of a 7-segment LED to turn on for each numeral.

The table should be 16-bit (word) aligned, and consist of an even number of bytes, padded with a final null byte if necessary. They should also be specified word-wise, with pairs of bytes being swapped to account for word endianness. We multiply the word address of sevenseg_numbers by 2 to get the byte address, because LPM retrieves one byte. The least-significant bit, or LSB, determines whether the low byte (0) or the high byte (1) of the word addressed is retrieved. In our case, we set the LSB by adding r16 to r30, setting the offset at the same time. The resulting byte is placed in r0, and output to PORTD.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    ;   Outputs the number passed in r16 to the 7-segment display
    sevenseg_num:
        push r31
        push r30
        push r0
        ldi r31, HIGH(sevenseg_numbers*2)
        ldi r30, LOW(sevenseg_numbers*2)
        add r30, r16
        lpm
        out 0x12, r0
        pop r0
        pop r30
        pop r31
        ret

    sevenseg_numbers:
        .dw 0x063F, 0x4F5B, 0x6D66,  0x077D, 0x6F7F

On boot, the LED will cycle three times quickly, then begin the main program loop. The LED will toggle, the 7-segment LED on PORTD will cycle through all the numbers (but not the decimal point), and then the loop will begin again.

The only necessary parts are an LED on PORTB0 and a 7-segment LED, with segments A-F (and the period) attached to PORTD0:7, respectively.

To build and program:

$ avra sevenseg.s
$ sudo avrdude -c usbasp -p m32 -P usb -U flash:w:sevenseg.s.hex:i

sevenseg.s

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
    ;   Interrupt Handlers
    jmp boot            ;   RESET
    jmp ignore_int      ;   INT0
    jmp ignore_int      ;   INT1
    jmp ignore_int      ;   INT2
    jmp ignore_int      ;   TIMER2 COMP
    jmp ignore_int      ;   TIMER2 OVF
    jmp ignore_int      ;   TIMER1 CAPT
    jmp ignore_int      ;   TIMER1 COMPA
    jmp ignore_int      ;   TIMER1 COMPB
    jmp ignore_int      ;   TIMER1 OVF
    jmp ignore_int      ;   TIMER0 COMP
    jmp ignore_int      ;   TIMER0 OVF
    jmp ignore_int      ;   SPI, STC
    jmp ignore_int      ;   USART, RXC
    jmp ignore_int      ;   USART, UDRE
    jmp ignore_int      ;   USART, TXC
    jmp ignore_int      ;   ADC
    jmp ignore_int      ;   EE_RDY
    jmp ignore_int      ;   ANA_COMP
    jmp ignore_int      ;   TWI
    jmp ignore_int      ;   SPM_RDY

    ;   On RESET
    boot:
        enable_stack:
            ldi r16, 0x08
            out 0x3E, r16
            ldi r16, 0x5F
            out 0x3D, r16
        call boot_finish
        call start

    ;   Dummy interrupt handler (should be the 1st thing after `boot`.)
    ignore_int:
        reti

    ;   Additional boot chores post-stack-initialization, but before `start`
    boot_finish:
        call enable_led
        call flash_led
        call enable_watchdog
        ret

    ;   Main program
    start:
        call enable_sevenseg
        main_loop:
            wdr
            call toggle_led
            clr r16
            sevenseg_loop:
                cpi r16, 0x0A
                breq main_loop
                call sevenseg_num
                call delay
                inc r16
                rjmp sevenseg_loop
            rjmp main_loop
        ;   Returns to `boot`, which drops through to `ignore_int`, calls 
        ;   `reti` and resets the processor. This is never executed.
        ret

    ;  LED driver (reserves r26) for PORTB, pin 0
    enable_led:
        ldi r26, 0x01
        out 0x17, r26
        clr r26
        ret

    toggle_led:
        cpi r26, 0x01
        breq call_off
        call led_on
        ret
        call_off:
        call led_off
            ret 

    led_on:
        ldi r26, 0x01
        out 0x18, r26
        ret

    led_off:
        clr r26 
        out 0x18, r26
        ret
       
    ;   Library functions
    flash_led:
        push r16
        push r17
        push r18
        ldi r16, 0x06
        loop_a:
            call toggle_led
            clr r17
            loop_b:
                clr r18
                loop_c:
                    dec r18
                    brne loop_c
                dec r17
                brne loop_b
            dec r16
            brne loop_a
        pop r18
        pop r17
        pop r16
        ret

    ;   Enables the Watchdog Timer with a roughly 2.2 second timeout
    enable_watchdog:
        push r16
        ldi r16, 0x0F
        out 0x21, r16
        pop r16
        ret

    delay_small:
        push r16
        clr r16
        delay_small_while:
            cpi r16, 0x10
            breq end_delay_small_while
            inc r16
            rjmp delay_small_while
        end_delay_small_while:
        pop r16
        ret

    delay:
        push r16
        push r17
        clr r16
        while_a:
            cpi r16, 0x10
            breq end_while_a 
            inc r16
            clr r17
            while_b:
                cpi r17, 0xFF
                breq end_while_b
                inc r17
                call delay_small
                rjmp while_b
            end_while_b:
            rjmp while_a
        end_while_a:
        pop r17
        pop r16
        ret

    ;   7-segment LED driver (reserves r25) for PORTD
    enable_sevenseg:
        ldi r25, 0xFF
        out 0x11, r25
        clr r25
        ret

    sevenseg_on:
        ldi r25, 0xFF
        out 0x12, r25
        ret

    sevenseg_off:
        clr r25
        out 0x12, r25
        ret

    ;   Outputs the number passed in r16 to the 7-segment display
    sevenseg_num:
        push r31
        push r30
        push r0
        ldi r31, HIGH(sevenseg_numbers*2)
        ldi r30, LOW(sevenseg_numbers*2)
        add r30, r16
        lpm
        out 0x12, r0
        pop r0
        pop r30
        pop r31
        ret

    sevenseg_numbers:
        .dw 0x063F, 0x4F5B, 0x6D66, 0x077D, 0x6F7F