First LCD steps...

[ Writing Commands ] [ Writing Data ] [ Reading Data/Busy Flag ] [ Init Sequence ]

Writing Commands

As already described, writing commands is writing data to the LCD with RS low. With PortD -> Data, PortC.0 -> RS and PortC.2 -> E the code for writing a command can look like this:

LCD_Command:
cbi PortC, LCD_RS
out PortD, r16
sbi PortC, LCD_E
nop
nop
nop
cbi PortC, LCD_E
ret
;write command to LCD routine (command is in r16)
;clear RS for command register select
;put data on bus
;set Enable
;the number of nops here depends on your clock
;speed. One or two works well at 7.3728 MHz
;
;clear Enable
;return from subroutine

The nop delay between setting and clearing E is important for the line to settle. I've tried the code without them and sometimes it didn't work due to the long wires. After a command is issued you have to either insert a wait routine or check the busy flag (see "Reading Data").

So if we want to initialise the LCD for 8-bit mode, 2 lines and 5x7 font, we need to write 0b00111000 (0x38) to the command register:

ldi r16, 0x38
rcall LCD_Command
;Load command (8bits, 2 lines, 5x7 font)
;and write it to the LCD using the routine above

Writing Data

Writing data to the LCD is just as easy as writing commands to it, but now we have to SET the RS line. We'll use r16 for the routine argument again:

LCD_w_Data:
sbi PortC, LCD_RS
out PortD, r16
sbi PortC, LCD_E
nop
nop
nop
cbi PortC, LCD_E
cbi PortC, LCD_RS
ret
;write data to LCD routine (data is in r16)
;set RS for data register select
;put data on bus
;set Enable
;the number of nops here depends on your clock
;speed. One or two works well at 7.3728 MHz
;
;clear Enable
;and RS
;return from subroutine

As AVR Studio can convert ascii characters to their hex value, we can use 'A' for loading r16:

ldi r16, 'A'
rcall LCD_w_data
;Load character 'A'
;and write it to the LCD using the routine above

Reading Data and Busy Flag

For reading data from the LCD we have some more work to do. Again, we'll need to make two routines: One will access the command register for reading the address counter and the busy flag, one will be used for reading display data (characters). As reading requires PortD (the data port) to be configured as input, we'll have to take a close look at the data direction. If it is not reset (to output) correctly before returning from the routine, the next write routine could run into problems.

The LCD will put its data on the bus while E is high. So we need to take E high, wait a bit (for the LCD to give us the data), then get the data byte and then take E low again. For the address/busy flag read (command reg, RS low) the routine would look like:

LCD_r_Addr:
cbi PortC, LCD_RS
sbi PortC, LCD_RW
ldi r16, 0x00
out DDRD, r16
sbi PortC, LCD_E
nop
in r16, PinD
cbi PortC, LCD_E
ldi r17, 0xFF
out DDRD, r17
cbi PortC, LCD_RW
ret
;read address from the LCD
;clear RS for command register select
;set RW for read direction
;configure Data port
;as input
;now take E high
;wait a bit
;get the address and busy flag
;clear E
;PortD as output again
;
;clear RW again
;return

For reading data from the LCD, just rewrite the routine with LCD_RS = 1 (data register select). It will then read the data at the location the Address counter is pointing to. I won't rewrite this routine now, as you just have to change one single line.

The good thing is that we can now use the LCD_r_Addr routine to check the LCDs busy flag. Before we would have needed to include delays between the command and data writes. Now we can wait until the LCD has finished (AND NOT ANY LONGER!) and then proceed with the next command. The LCD_wait routine can have sepereate read data code (this will speed up things, but require more code space) or it can use LCD_r_Addr for reading the busy flag:

LCD_wait:
rcall LCD_r_Addr
sbrc r16, 7
rjmp LCD_wait
ret
; LCD_wait: wait for busy flag to clear
; read address and busy flag
; if busy flag cleared, return
; else repeat read/check
; return when busy flag cleared

This way of writing the routine has a good side effect: When it returns, the busy flag is cleared from r16 (because the LCD cleared it), but r16 still holds the address we just read from the display. It can be used for other purposes then.

The Init Sequence

The LCD init sequence has to be executed after startup. It tells the LCD which font size it has, what kind of interface to use, if and how the cursor should be shown and so on. Here's a working init sequence for a 16 x 2 LCD, 8 bit interface, 5 x 7 font; show cursor as underscore; auto-increment cursor:

ldi r16, 0b00111000
rcall lcd_command
rcall lcd_wait
ldi r16, 0x00001110
rcall lcd_command
rcall lcd_wait
ldi r16, 0x01
rcall lcd_command
rcall lcd_wait
ldi r16, 0b00000110
rcall lcd_command
; 0x38: 8 bit interface, 5 x 7 font, 2 lines
;
;
; display on, show cursor, don't blink
;
;
; clear display, cursor home
;
;
; auto-increment cursor
;

Though no data has been written to the LCD before issuing the clear display/cursor home command, the cursor can be at a position that's not visible, so this command is important if you want to see what you write to the LCD!

Some really slow LCDs might require your app to write 0x30 (8 bit interface) to the LCD before any other command. If your LCD refuses to work, try that.