ATmega32 Bare Metal 7-Segment LED

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.

;   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

;   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

Sections