ATmega32 Bare Metal HD44780 and LCD

This example initializes a 1x16 LCD with an HD44780 controller and displays dummy text on the screen. The ROM appears to be corrupted on mine, so I'll be exploring how to program the CGRAM (character generator RAM) to display a custom character map in another post.

Joshua Galloway has an excellent page with a schematic and command reference for HD44780-controlled LCDs. In this example, the LCD is wired for 8-bit operation. The data bus D7-0 is connected to PORTD7-0, RS (register select) is on PORTC0, and EN (enable) is on PORTC1. The LCD is powered by its own 5V supply, separate from that powering the ATmega32.

HD44780 LCD display on an ATmega32

The watchdog timer is not used in this example, and the 7-segment LED code has been stripped out. The delay function has been rewritten to take two arguments in r24 and r25. The delay is approximately 250 microseconds * r24 * r25. This gives a reasonable range of values from about 250 microseconds up to 1.5 seconds. Most loops have been converted to PC-relative addressing, removing the need to invent increasingly verbose (and unique) labels for common constructs.

;   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 enable_hd44780
    call flash_led
    ret

;   Main program
start:
    main_loop:
        call toggle_led
        ldi r24, 0x3E   ;   wait 1/4 second 
        ldi r25, 0xFF
        call delay

        ldi r16, 0x80   ;   jump to 1st line
        call hd44780_write_command

        ldi r16, 0x41   ;   write 'A@?>=<;:'
        call hd44780_write_data
        dec r16
        brne PC - 3

        ldi r16, 0xC0   ;   jump to 3rd line
        call hd44780_write_command

        ldi r16, 0x42   ;   write 'BBBBBBBB'
        ldi r17, 0x08
        call hd44780_write_data
        dec r17
        brne PC - 3

        rjmp main_loop

    ;   Returns to `boot`, which drops through to `ignore_int`, calls 
    ;   `reti` and resets the processor. This is never executed.
    ret

;   HD44780 driver (reserves r23) for PORTD and PORTC
;   D0-D7 on PORTD0-7, R/S on PORTC0, EN on PORTC1
enable_hd44780:
    push r25
    push r24
    push r17
    push r16

    ldi r17, 0xFF   ;   set DDRD to output
    out 0x11, r17

    ldi r17, 0x03   ;   set DDRC(0:1) to output
    out 0x14, r17

    ldi r24, 0x50   ;   wait 20ms
    ldi r25, 0x01
    call delay

    ldi r16, 0x30   ;   init
    call hd44780_write_command

    ldi r24, 0x14   ;   wait 5ms
    ldi r25, 0x01
    call delay

    ldi r16, 0x30   ;   init, round #2
    call hd44780_write_command

    ldi r24, 0x01   ;   wait 250us
    ldi r25, 0x01
    call delay

    ldi r16, 0x30   ;   init, round #3
    call hd44780_write_command

    ldi r24, 0x01   ;   wait 250us
    ldi r25, 0x01
    call delay

    ldi r16, 0x3C   ;   set interface (8-bit, 2 lines, 5x10 font)
    call hd44780_write_command

    ldi r16, 0x0C   ;   enable cursor/display (display on, cursor off)
    call hd44780_write_command

    ldi r16, 0x01   ;   clear and home display
    call hd44780_write_command

    ldi r16, 0x06   ;   shift display (off, left)
    call hd44780_write_command

    ldi r16, 0x0C   ;   turn on display
    call hd44780_write_command

    pop r16
    pop r17
    pop r24
    pop r25
    ret

;   Pass D0-7 in r16
hd44780_write_command:
    push r24
    push r25
    clr r23         ;   RS=0, EN=0 on PORTC(0:1)
    out 0x15, r23
    out 0x12, r16   ;   D0-7
    ldi r23, 0x02   ;   RS=0, EN=1
    out 0x15, r23
    clr r23         ;   wait 450ns (< 1 cycle), RS=0, EN=0
    out 0x15, r23
    ldi r24, 0x14   ;   wait 5ms
    ldi r25, 0x01
    call delay
    pop r25
    pop r24
    ret

;   Pass D0-7 in r16
hd44780_write_data:
    push r24
    push r25
    ldi r23, 0x01   ;   RS=1, EN=0 on PORTC(0:1)
    out 0x15, r23
    out 0x12, r16   ;   D0-7
    ldi r23, 0x03   ;   RS=1, EN=1
    out 0x15, r23
    ldi r23, 0x01   ;   wait 450ns (< 1 cycle), RS=1, EN=0
    out 0x15, r23
    ldi r24, 0x01   ;   wait 250us
    ldi r25, 0x01
    call delay
    pop r25
    pop r24
    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 PC + 4
    call led_on
    ret
    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 r24
    push r25
    ldi r16, 0x06
        ldi r24, 0x7D
        ldi r25, 0x10
        call delay
        call toggle_led
        dec r16
        brne PC - 5
    pop r25
    pop r24
    pop r16
    ret

;   Delay is for r24 * r25 * 250 microseconds
;   r24 and r25 must be > 0 
delay:
    push r16
    push r17
    push r24
    push r25

    mov r17, r24
        ldi r16, 0x3E
            dec r16
            brne PC - 1
        dec r17
        brne PC - 4
    dec r25
    brne PC - 7

    pop r25
    pop r24
    pop r17
    pop r16
    ret

Sections