If 8-bit mode cannot be used (not enough free pins on the AVR side or due to PCB design problems), the 4 bit mode of HD44780 compatible LCDs can be used. In this mode only the upper 4 data lines are used and the control lines stay the same, which makes a total of 7 lines for the LCD. However, the code size and execution time increase, as each data or control byte has to be sent to the LCD in 2 nibbles.
The basic principles of using LCDs are the same as in 8-bit mode. The commands are the same and the busy flag should be checked before any operation. Have a look at the pages about LCDs in 8-bit mode before reading this.
Writing data or commands to the LCD in 4-bit mode is done high nibble first, then low nibble, so the enable pin (E) has to be strobed twice. Special care should be taken when using a single AVR port, as mixing up input and output lines can damage both the AVR and the LCD.
Here's a one-port setup for using an LCD in 4-bit mode (PortD):
PortD.4 ... PortD.7 -> Data.4 ... Data.7
PortD.3 -> LCD Enable line
PortD.2 -> LCD RW line
PortD.1 -> LCD RS line
The control lines are always output lines from the controller to the LCD, but the data line direction changes depending on the current operation (as already noted, special care should be taken here!).
I'll now explain how to write commands and data to the LCD as well as reading the address counter and data. For having a better overview, I suggest opening the LCD 4-bit mode code m8_LCD_4bit.asm in a seperate window. The code contains init and some other routines which are not of interest now. They are commented though and should be easy to understand. Scroll down to LCD_command8, LCD putchar and LCD_command.
For initialising the LCD, we need to write commands to it. So have a look at LCD_command8 and LCD_command now. LCD commands was written after LCD_putchar, but it shares LCD_putchar's comments. I didn't repeat them.
After power-up the LCD is in 8-bit mode by default. For switching to 4-bit mode, we need to write an 8-bit command to it : 0b00100000. Hey! We've just got 4 data lines, so how should we write an 8-bit command? The good news about this is that the data length bit is in the upper nibble, which is connected to the LCD (see above). The lower 4 bits are not important now. We just want to set the data length to 4 bits now. As it is an 8-bit command, we only have to strobe E once, not twice as in 4-bit mode. The extra routine is ONLY needed for init. The only thing to watch out for is the data direction for each pin, as control and data lines share the same port. It's all in the comments, really! Have a look (send me an email if it's not commented enough!).
Now comes the interesting part. Writing characters to the LCD works EXACTLY like writing commands, but when writing character RS is taken high, while for commands it is taken low. Nothing special :-) First of all, the data direction bits for the data lines are set for output, as in
DDRD |= 0xF0;
Then, for safety, all LCD lines are cleared. PortD.0 is not used by the LCD, so this bit is saved through this process (see code, only the upper 7 bits are cleared). In 4-bit mode, the high nibble of our data byte is written first, then the low nibble is written. For writing the high nibble, the low nibble of the argument is cleared. Then the rest (the high nibble) is combined with the PortD data:
PortD |= (argument & 0xF0);
Then the control lines are set as needed: RS high (char) or low (command), RW low. Then E is strobed to write the high nibble. Now we need the low data nibble, which we destroyed by clearing the low nibble before for writing the high nibble. This means that the argument has to be saved at the beginning of the routine (in this case it's pushed onto the stack). For getting the low nibble again, we now pop the argument again and clear the high nibble. The data lines are on the high port lines though, so we also need to swap the argument now. The high port data nibble is cleared, then the port data is ORed with the argument to set the data lines as required. Again, E is strobed. Before returning from the routine, the data direction of the LCD data port is set to input again, the control lines stay outputs. This procedure is the same for data and commands, as already mentioned.
With these tools (LCD_command and LCD_command8) it's possible to init the the LCD (a small delay routine is also needed). First, the LCD is set to 4-bit mode. Then the usual settings are made (-> LCD_init at the end of the code!).
We still can't check the busy flag or read data from the LCD. Checking the busy flag is especially useful during LCD init (we can get rid of those looooong delays!). I'll now just describe reading in general, as all read operations are again almost equal. When reading from the LCD, the high nibble is also tansmitted first. We have to read it while E is high during the E strobe. The read routines first make sure that the data direction bits for the data lines are zero (input). Then E is taken high and the PIN data is read. Now the pin data still contains some unknown bits (especially PinD.0, which might be used by other app code!) and these bits are masked away. The remaining value is the high nibble of the data read, which will be stored in the high nibble of our return value (mov is used for this so that the return value doesn't have to be cleared before). Then the low nibble is read in the same manner (read PINs while E is high). For combining the high nibble with the new low nibble we again have to clear the unused bits in the value from the PIN register. The low nibble of the LCD data is now in the high nibble of the PIN value, so we need to swap the pin value. Then OR is used to combine the return value (high nibble) with the new low nibble. That's it!
A routine that waits for the busy flag is no problem then: Just read the address counter (which inclues BF), mask off the lower 7 bits and see if the result is zero. Then the busy flag is cleared. Have a look at LCD_wait.
The main code first inits the LCD and then writes 'A' to the first LCD position. Then LCD_command is used to set the address counter to zero again: If bit 7 of the command is set, the lower 7 bits are interpreted as an address for the cursor. Then the character at position zero is read (after a read operation, the cursor is also auto-incremented!) and written to the next position. The LCD now shows "AA".