4-bit Mode LCD in AVR


This project was another attempt to build up more AVR Embedded C libraries with commonly used electronics components, similar to my LDR project. This one involved a common china export 1602A Green LCD used for displaying a limited amount of text for user interfaces. The firmware sets up the LCD for operation in 4-bit mode, which allows you to send data to the LCD a nibble at a time to really save on I/O pins in exchange for speed. Great trade-off if you ask me! For the demo I simply had it display the text Swallowtail Electronics staggered between the two rows.



The microcontroller used here is the ATtiny26L which I managed to find in a package of chips in the trash bin near the Oregon State robotics lab. This program is split between the main.c, which is the program entry point, and the LCD.h header file which includes the functions to operate the LCD.

The 1602A LCD screen has 16 pins. VSS is Ground, VDD is positive supply (5V in this case), VE is the voltage input that controls screen contrast, RE is the register select (allows the user to tell the LCD controller to expect either a data byte on the I/O or a command byte), E is the enable bit, D0-D7 is the data input, A is the anode to the backlight LED, and K is the backlight cathode. I will be using this LCD in its 4-bit operation mode which only uses the data inputs D4-D7.

Let’s step though the LCD library file and work though the functions.

LCD_1602A_init()
//Initialize the outputs for this LCD display
void LCD_1602A_init(){
    /* Initialize the I/O Registers */
    /*				-I/O Map-
        *	RS (Selects either data or instruction register): PA2 (1: Output)
        *  E (Enable): PA3 (1: Output)
        *	D4: PA4 (1: Output)
        *	D5: PA5 (1: Output)
        *	D6: PA6 (1: Output)
        *	D7: PA7 (1: Output)
        */
    DDR_LCD = (1<<D4) | (1<<D5) | (1<<D6) | (1<<D7) | (1<<RS) | (1<<E);
    //Set the default values for outputs to zero and inputs to have pull-up resistors
    PORT_LCD = (0<<D4) | (0<<D5) | (0<<D6) | (0<<D7) | (0<<RS) | (0<<E);
    //Wait a brief second
    _delay_ms(20);
    //Set-up the LCD for 4-bit operation with 2-line operation
    LCD_1602A_Write_Cmd(0x02);
    LCD_1602A_Write_Cmd(0x28);
    //Turn off the display cursor
    LCD_1602A_Write_Cmd(0x0C);
    //Allow the cursor to auto-increment
    LCD_1602A_Write_Cmd(0x06);
    //Clear the display and return the cursor to the home position
    LCD_1602A_Clear();
    return; //Return to call point
}
                                        

Starting with LCD_1602A_init(), we initialize the output and write commands to put the LCD into 4-bit mode and 2 line operation. Then we turn off the display cursor and allow the cursor to auto increment as we add characters. Finally, we clear the LCD.

LCD_1602A_Write_Cmd()
//Write a single byte (char) to the display at the given location
void LCD_1602A_Write_Cmd(char cmd){
    char cmd_high;
    char cmd_low;
    cmd_high = cmd & 0xF0; //Mask lower nibble and send the high nibble over the port
    PORT_LCD = cmd_high;
    PORT_LCD &= ~(1<<RS);	//When RS is 0 the command register is accessed
    PORT_LCD |= (1<<E); //Enable the data nibble to be read
    _delay_us(1);
    PORT_LCD &= ~(1<<E); //Disable the read
    _delay_ms(3);
    cmd_low = ((c<<4) & 0xF0); //Shift 4-bits and mask to send the high nibble
    PORT_LCD = cmd_low;
    PORT_LCD &= ~(1<<RS);	//When RS is 0 the command register is accessed
    PORT_LCD |= (1<<E); //Enable the data nibble to be read
    _delay_us(1);
    PORT_LCD &= ~(1<<E); //Disable the read
    _delay_ms(3);
}

Next the LCD_1602A_Write_Cmd() we send the command byte to the LCD. We write RS low, so the data we send is interpreted as commands. The byte is masked into nibbles and sent out one at a time.

LCD_1602A_Clear()
//Clear the LCD screen
void LCD_1602A_Clear(){
    LCD_1602A_Write_Cmd(0x01);
    LCD_1602A_Write_Cmd(0x80);
}

In LCD_1602A_Clear(), we simple send the command to reset the screen and reset the controller system.

LCD_1602A_Write_Char()
//Write a single byte (char) to the display at the given location
void LCD_1602A_Write_Char(char c){
    uint8_t c_high;
    uint8_t c_low;
    c_high = c & 0xF0; //Mask lower nibble and send the high nibble over the port
    PORT_LCD = c_high;
    PORT_LCD |= (1<<RS);	//When RS is 1 the data register is accessed
    PORT_LCD |= (1<<E); //Enable the data nibble to be read
    _delay_us(1);
    PORT_LCD &= ~(1<<E); //Disable the read
    _delay_ms(3);
    c_low = ((c<<4) & 0xF0); //Shift 4-bits and mask to send the high nibble
    PORT_LCD = c_low;
    PORT_LCD |= (1<<RS);	//When RS is 1 the data register is accessed
    PORT_LCD |= (1<<E); //Enable the data nibble to be read
    _delay_us(1);
    PORT_LCD &= ~(1<<E); //Disable the read
    _delay_ms(3);
}

LCD_1602A_Write_Char() is identical to the Write_Cmd() function except the RS input is written high so that data is given to the data register (rather than the command register).

LCD_1602A_Write_String()
//Write a string to the LCD
void LCD_1602A_Write_String(char *str){
    int i; //Iterator variable
    for(i=0;str[i]!=0;i++) //Write each character until we reach null
    {
        LCD_1602A_Write_Char(str[i]); //Write the char
    }
}

LCD_1602A_Write_String() loops through a C style string until it reaches the null terminating character, calling the Write_Char() on each individual character.

LCD_1602A_Set_Cursor()
//Moves the LCD cursor to a specified location on the screen
void LCD_1602A_Set_Cursor(uint8_t x, uint8_t y){
    if (y == 0 && x<16){
        LCD_1602A_Write_Cmd((x & 0x0F)|0x80);	/* Command of first row and required position<16 */
    }
    else if (y == 1 && x<16){
        LCD_1602A_Write_Cmd((x & 0x0F)|0xC0);	/* Command of first row and required position<16 */
    }
}

The last function, LCD_1602A_Set_Cursor(), will move the cursor to the location specified so long as it fits on the screen.

There is a lot more you can do with the 1602A such as creating custom characters and writing to the LCD controllers’ memory to quickly recall string. I’ll link the datasheet if you’re interested in making use of those functions and if I ever find a need for those functions, I’ll add the code to the library. Thanks for reading!

Here’s the GitHub link to the project repository.

Here’s the Bill of Materials for getting this hardware.

BOM Item # Item Name Quantity Price/Unit Total Cost
1 ATtiny26L 1 $1.00
2 1602A 15x2 LCD 1 $5.00
3 1k Resistor 1 $0.50
4 10k Resistor 1 $0.50
5 220 Resistor 1 $0.50
$7.50

This article was last edited: April 2020