8 bit and 4 bit LCD interfacing with ATtiny

Since my brief journey to controlling LCD display directly with ATtiny2313 I purchased a display with Hitachi HD44780 compatible driver chip. The web is already pretty full of LCD tutorials and libraries, but most seemed to either skip details and rely on external libraries, or were just overly complex. So I decided it wouldn’t hurt to share the rather short (and functionally limited) versions I came up with.

8-bit mode

In 8-bit mode, you will be needing 8 pins for sending or reading a whole byte of data at once, and 3 control lines: enable (EN), register select (RS), and read/write (RW). Basic procedure is to prepare all other lines, and then pulse the enable line high for a short while in which LCD reads your command (when RW is low) or writes data (when RW is high). For control messages, RS line is low, and for writing letters, RS line is high.

I started with ATtiny2313 and used the 8 pins in port B as LCD data lines, and PD4, PD5, and PD6 as RW, RS, and EN, respectively. With such a setup, working write command became:

#define DATA_PORT_DIR DDRB
#define DATA_PORT PORTB
#define DATA_PORT_IN PINB

#define RW_PIN (1<<PD4)
#define RS_PIN (1<<PD5)
#define EN_PIN (1<<PD6)

#define SET_CTRL_BIT(pin) (PORTD |= pin)
#define CLEAR_CTRL_BIT(pin) (PORTD &= ~pin)

// assumes EN_PIN is LOW in the beginning
void lcd_write(char rs, unsigned char data) {
    if(DATA_PORT_DIR != 0xFF)
        DATA_PORT_DIR = 0xFF;
        
    CLEAR_CTRL_BIT(RW_PIN);
    
    if(rs)
        SET_CTRL_BIT(RS_PIN);
    else
        CLEAR_CTRL_BIT(RS_PIN); 
        
    DATA_PORT = data;
    
    _delay_us(2);
    SET_CTRL_BIT(EN_PIN);
    _delay_us(2);
    CLEAR_CTRL_BIT(EN_PIN);
}

To read data, we change the data port direction, and instead of just waiting a while when enable pin is high, we read the data LCD is sending us:

unsigned char lcd_read(char rs) {
    unsigned char data;
    
    if(DATA_PORT_DIR != 0)
        DATA_PORT_DIR = 0;
        
    SET_CTRL_BIT(RW_PIN);
    
    if(rs)
        SET_CTRL_BIT(RS_PIN);
    else
        CLEAR_CTRL_BIT(RS_PIN); 
        
    _delay_us(2);
    SET_CTRL_BIT(EN_PIN);
    _delay_us(2);
    data = DATA_PORT_IN;
    CLEAR_CTRL_BIT(EN_PIN);
    
    return data;
}

Most LCD write commands are associated with some delay. You can either check the datasheets and use _delay_us(), or read the status from LCD and loop until “busy flag” at the most significant bit (0x80) clears:

void lcd_wait() {
    while(lcd_read(0) & 0x80); // wait until display is ready
}

Note that if something goes wrong with the LCD, your program will just hang. Use a watchdog timer or limited loop to avoid that.

Initializing the LCD to 8 bit mode is done automatically if you manage to provide satisfactory power supply for the LCD, but if that fails (with my breadboard setup, it did), you need to do it manually. The data sheet outlines the steps and delays needed. I chose to add some extra delays and came up with this, which seemed to work most of the time. Note that I’m using a 2-line display here (my 16 character LCD is internally thinking it’s two-line, 8-column display :) If you have a 1-line display, you’d probably want 0x30 instead of 0x38 there:

void lcd_init() {
    _delay_ms(50); // wait for VDD to rise
    lcd_write(0, 0x30);
    _delay_ms(5);
    lcd_write(0, 0x30);
    _delay_ms(1); // _delay_us(120);
    lcd_write(0, 0x30);
    _delay_ms(1); // _delay_us(120);

    lcd_write(0, 0x38); // 2 lines, normal font
    _delay_ms(1);
    lcd_write(0, 0xC); // display on
    _delay_ms(1);
    lcd_write(0, 1); // display clear
    _delay_ms(1);
    lcd_write(0, 0x6); // increment, don't shift
    _delay_ms(1);    
}

Once initialized (i.e. after calling the method above), the LCD should have its internal data cursor pointing at the first letter of your LCD. Writing out a single character to there would look like this:

lcd_write(1, 'x');
lcd_wait();

However, while the last lcd_write(0, 0x6) statement in initialization code means that the LCD increments the data cursor automatically by one after a write, my model had the letters 1-8 in memory addresses 0x00-0x07 and letters 9-16 (the “second line”) in 0x40-0x07. So after 8 letters, we need to reposition the data cursor, otherwise the next 56 letters will go hidden, before we finally get to 0x40. The command needed to reposition the cursor is 0x80 + offset, in this case 0x40:

lcd_write(0, 0x80+0x40); // move to 2nd line
lcd_wait();

So how to get back to line 1? Yep, you guessed it right: lcd_write(0, 0x80 + 0) (you can of course omit the +0 there). That’s it, with these commands we’re equipped to do basic printing to LCD. I personally went just a bit further and wrote a simple lcd_puts("string") method to output a whole string at a time. The complete example is available (and relased to public domain) in lcd8.c.

4 bits

Once you have 8 bits running correctly, not much needs to be changed to enable 4-bit mode and go from 11 data pins to 7. Basically, I took out jumper wires going to DB0..DB3 in the LCD, and relocated DB4..DB7 from PB4..PB7 to now available PB0..PB3. That’s it for hardware setup. On the software side, I renamed lcd_write to lcd_write_nibble (in 4-bit communication, one byte consists of two 4-bit “nibbles” sent to / read from LCD via DB4..DB7). Additional lcd_write_byte just calls the new routine twice to send a whole byte:

void lcd_write_byte(char rs, unsigned char data) {
    lcd_write_nibble(rs, data >> 4);
    lcd_write_nibble(rs, data & 0xF);
}

Reading works exactly the same way. We read a nibble much like we read a byte in 8-bit mode, and for reading the whole byte, we call lcd_read_nibble() twice and reconstruct the byte read.

Also, the initialization code needs to be tweaked a bit. Remember that when starting, LCD does not know if we are talking to it in 4-bit or 8-bit mode, so first three calls in the initialization stay the same (except when we send a “nibble”, we write 0x3 instead of 0x30 because we are directly addressing the upper 4 bits in DBx). Then we reveal that we want to use 4-bit mode with the third call, passing 0x2 instead of 0x3. This is repeated on the next call when sending the full initialization byte – we send 0x28 instead of 0x38. That’s it, everything else is the same. You can see the new initialization code in lcd4.c.

When making this short journey to LCD land, I initially had some serious trouble getting it all to work. After three hours of banging my head to the wall, I learned a few very basic things about troubleshooting electronics:

  • If there’s holes instead of pins you’re connecting to, just “plugging a pin header” into the holes does not create a stable electrical connection – you really need to solder it
  • However diligently you check DB7 bit for “busy” when waiting for LCD, you will get zero all the time until you remember to reconfigure the pin as input. Duh!
  • I’ve also tried reading PORTB instead of PINB in several occasions… to no avail.

When debugging the LCD it occurred to me I really would need a logic analyzer. I think I’m building one as soon as I have a bit of extra time. :)

Published by

Joonas Pihlajamaa

Coding since 1990 in Basic, C/C++, Perl, Java, PHP, Ruby and Python, to name a few. Also interested in math, movies, anime, and the occasional slashdot now and then. Oh, and I also have a real life, but lets not talk about it!

13 thoughts on “8 bit and 4 bit LCD interfacing with ATtiny”

  1. Hi

    Just wanted to let you know that links to your code are not working, can you please fix the problem when you get a chance.

    Thanks

  2. Thanks for the info. I have no idea why the files were missing from data folder, I find it hard to believe that I would’ve forgotten to upload them but on the other hand don’t know how they could’ve been deleted afterwards…

    I (re?)uploaded the missing files and the links should now work (although for some really annoying reason Firefox caches the “not found” -page so I had to switch to Chrome to get them…)

  3. Thank you for this nice project it works fine, i will try with atmega16 too.
    I am looking for a solution(command), to move the cursor to any position on display.
    putcursor(x,y) x=0 line 1, x=1 line 2 y=0-15 would you so kind to help me.

  4. Sorry, i meant the cursor on the LCD.I already tried with atmega16 same config,8MHZ almost ok.
    I found on the net a similar LCD project for testing pinball machines.That code was realy complex, i was not able to follow it.Your code is much better for me.I am rookie with C . There are(in pinball CPU board) 3*8byte output,2 byte show the position of the cursos(called – strobe), 1 byte lower 4 bit (character – upper line)upper 4 bit (character – lower line). I try to merge this codes – i hope i don’t hurt your copirights.

    1. No problem, my bad – I’ve just received the same question for USB mice too many times. :)

      There are commands in the HD44780 instruction set to reset cursor position, move the cursor and set exact cursor position (0x80) – see either the datasheet or the following post for details:

      http://dawes.wordpress.com/2010/01/05/hd44780-instruction-set/

      The code above is simple enough that you can consider it public domain (and that’s actually mentioned in the .c files :). So modify and use to your heart’s content!

      1. I would like to use portA,B,C as input,so i tried to change the program to use the lower 4 bit of portD to drive the LCD.Unfortunatly i failed.I made the following changes:
        #define F_CPU 12000000UL // 12 MHz
        -x-x-
        #define F_CPU 8000000UL // 8 MHz (atmega16)

        #define DATA_PORT_DIR DDRB
        #define DATA_PORT PORTB
        #define DATA_PORT_IN PINB
        -x-x-
        #define DATA_PORT_DIR DDRD
        #define DATA_PORT PORTD
        #define DATA_PORT_IN PIND

        DDRD = RS_PIN + EN_PIN + RW_PIN; // Control outputs
        -x-x-
        DDRD = RS_PIN + EN_PIN + RW_PIN +0xf; // Control outp

        it doesn’t work, i expect the program call several times the value of that port and my change confuse it.I tried to cut the lower bits and change them only but it doesn’t work.
        If you have time can you give me some instruction.
        regards Imre from Hungary

        1. Sorry, your question was buried in my inbox for a few weeks…

          I assume your RS, EN, RW_PINs are in PD4-7 region so they don’t overlap the bitmask 0xF you used to enable PD0-PD3 for 4-bit function?

          If you’re using the higher bits of port D for signalling pins, the lcd_write_nibble will probably need adjustment too to avoid messing with higher 4 bits of DDRD when setting lower 4 bits for output. So instead of:

          DATA_PORT_DIR = 0xF;

          You might need:

          DATA_PORT_DIR |= 0xF; // will leave pins PD4-PD7 unchanged

          Similarly, instead of DATA_PORT = data, you’ll need to have something like DATA_PORT = (data&0xF) | (DATA_PORT&0xF0). The lcd_read_nibble needs similar modification:

          DATA_PORT_DIR = 0 –> DATA_PORT_DIR &= 0xF0;

          1. thanks for reply, i have solved similar the problem.Now works fine the 4 bit mode on portD.Your site gave me a lot of instuctions.Have a nice day..

    1. Sorry, I cannot help much with individual issues, I have only the bullet points in the end of my post for basic troubleshooting to offer. I suggest you ask some electronics forum, they’ll sure help you out!

Leave a Reply

Your email address will not be published. Required fields are marked *

Time limit is exhausted. Please reload the CAPTCHA.