Jump to content


View Other Content



Search Articles



Recent Comments


* * * * *

HD44780 LCD


As always, make sure you have a copy of the datasheet for your PIC. Here's a link to the PIC18F4550 datasheet.

Introduction

An LCD can be a great addition to just about any project. In this tutorial, you will learn all about the commonly used HD44780 LCD. The 16x2 LCD I am using has two lines of 16 characters each. Here is a picture of my HD44780 LCD:

Posted Image


In the above picture, the LCD is displaying data coming from an accelerometer. The PIC microcontroller and accelerometer can be seen above the HD44780. This picture was taken when the HD44780 was in 8-bit mode and the contrast and R/W lines were connected to the microcontroller.

After changing to 4-bit mode and connecting the R/W lines to GND, the HD44780 only takes up 6 I/O pins on the microcontroller, as can be seen below:

HD44780 Pinout
There are 16 pins on 16x2 HD44780 LCDs:
  • Vss (Ground)
  • VCC (5V)
  • Contrast (use a potentiometer for variable contrast, or just connect to GND)
  • Register Select (RS), 0 = command write, 1 = data write
  • Read/Write (R/W), 0 = write to display, 1 = read from display
  • Enable (EN) - used to clock in data
  • DB0 (not used in 4-bit mode) - LSb
  • DB1 (not used in 4-bit mode)
  • DB2 (not used in 4-bit mode)
  • DB3 (not used in 4-bit mode)
  • DB4 - LSb in 4-bit mode
  • DB5
  • DB6
  • DB7 - MSb
  • Backlight + (5V)
  • Backlight - (GND)

4-bit and 8-bit modes

An HD44780 LCD can be operated in two different modes: 4-bit mode and 8-bit mode. In 8-bit mode, pins 7-14 of the LCD are connected to eight I/O pins on the microcontroller; while in 4-bit mode, pins 11-14 on the LCD are connected to four I/O pins on the microcontroller. The advantage to operating in 8-bit mode is that the programming is a bit simpler and data can be updated more quickly. The obvious reason to operate in 4-bit mode is to save four I/O pins on the PIC microcontroller (or Arduino, etc..).


To read, or not to read?
There is a minimum amount of time that must elapse between sending each command byte or data byte to the HD44780. One decision you have to make is whether you want to use a timer to achieve this delay, or read from the HD44780 to see if it's busy.

Method		  Timer					  Reading from LCD
Pros   	  Easy to implement		   Fastest update speed
Cons   	  Slightly slower update speed	  Requires an extra I/O pin (RW can't just be tied to GND)

At first, it seemed reading from the LCD was the way to go; but after experimenting with it a little, it really doesn't seem necessary. Reading from the HD44780 to see when it's "done being busy" may allow for the fastest possible update speed, but the programming is more complicated, an extra I/O pin must be used, and you need to ask yourself how fast you really need the display to update? In the 4-bit mode program posted below there is a 1ms minimum delay between each state in the state machine that sends data to the LCD. MPLAB SIM shows the program taking about 170ms to update the screen (~5ms per byte x 34 bytes); this includes the code in the main loop. For me, 5 screen updates per second is more than enough. However, the LCD still works fine with the minimum delay turned down to 0.3ms, which means the LCD takes about 51ms to update – an update rate of about 20 Hz and, seriously, who needs a faster update rate than that?.

There is one thing you need to be careful of if you decide to read, however, and that is making sure that the TRIS bit for your I/O pin is not set to an output after you've set the RW line high. If you set the RW line high (read from LCD) then the LCD will have DB7 high or low, and if your output pin connected to DB7 is the opposite logic level, then you have a short circuit.

Clocking in data and commands
The HD44780 has two 8-bit registers: the instruction register (IR) and the data register (DR). The DR is used as a buffer for reading from and writing to the DDRAM and CGRAM registers, while the IR can only be written to (it can't be read from) and is the register that LCD commands are written to.

Posted Image

As shown in the table, when reading or writing data, the register select (RS) line is always high; when reading the busy flag or writing a command, the RS line is always low.

When reading DDRAM or CGRAM data, or the busy flag, from the LCD, the R/W line is made high; when writing data to the DR register or commands to the IR register, the R/W line is made low. Should you decide to only write to the LCD and never read from it, you can save an I/O pin on your microcontroller by connecting the R/W line to ground.

After each read or write, the address counter on the HD44780 is automatically incremented or decremented, depending on whether you set the cursor moving right or left, respectively. This saves a lot of address writes to the LCD, since you only have to write the "set DDRAM (or CGRAM) address" instruction for the first byte, instead of having to write the address of each byte every time.

To write an instruction, set the RW and RS control lines to the appropriate level, put the data or command on the I/O pins (by changing their logic levels), then toggle the EN line from low to high to low. The HD44780 latches data on the falling edge on the EN line. Here's a timing diagram from an HD44780 datasheet:


Posted Image




HD44780 Command Set


Posted Image

Detailed function descriptions

Clear Display
Clear display writes the space character to every DDRAM address, which effectively “clears” the display. It then resets the cursor to position 0 by putting DDRAM address 0 in the address counter.

Return Home
Return home resets the cursor to position 0 by putting DDRAM address 0 in the address counter.

Entry Mode Set
Entry mode set is where the cursor behavior and display shift are configured.

D = 0: The DDRAM address in the address counter is decremented upon DDRAM read/write. (i.e. the cursor moves left.)
D = 1: The DDRAM address in the address counter is incremented upon DDRAM read/write. (i.e. the cursor moves right.)
S = 0: The display is not shifted.
S = 1: The display is shifted to the right if D = 0, or to the left if D = 1, on every DDRAM write.

Display Control
Display control sets the display, cursor, and cursor blink on or off.

D = 0: Display off – the screen is blank, but characters can still be written to the display.
D = 1: Display on – text is shown on the screen.
C = 0: Hide the cursor.
C = 1: Show the cursor.
B = 0: Cursor blink off.
B = 1: Cursor blink on.

Cursor/Display Shift
Cursor/display Shift can be used to move the cursor or shift the display without having to read or write data.

S/M = 0: Move the cursor.
S/M = 1: Shift the display.
R/L = 0: Move the cursor/shift the display left.
R/L = 1: Move the cursor/shift the display right.

Function Set
Function set controls whether the display is in 8-bit/4-bit mode, sets the number of lines on the display (i.e. a 16x2 LCD display has two lines), and sets the font.

DL = 0: Put display in 4-bit mode
DL = 1: Put display in 8-bit mode
N = 0: 1 line
N = 1: 2 lines
F = 0: 5x8 (these are the number of “dots” on the display for each character)
F = 1: 5x10

Set DDRAM Address
Data Display Random Access Memory (DDRAM) is the data buffer for the display; it’s where the characters that are being displayed on the LCD are stored. For my 16x2 display, the DDRAM consists of 32 bytes. If I were to write a character to location 0, then that character would show up in first position on the first row, while writing to location 0x0F would make the character show up in the last position in the first row. Take a look at the memory map below:



Posted Image



Take note that the DDRAM address is not the value you send to the HD44780! The “set DDRAM address” command must be used to set the address where you want to read or write. The “set DDRAM address command” is designated by setting the 8th or MSB to 1, and then the DDRAM address follows in the next 7 bits (DB6:DB0). So, to give a few examples:

To put DDRAM address 0x00 (first position, first line) in the address counter, send command:
0x80

To put DDRAM address 0x41 (first position, second line) in the address counter, send command:
0xC0

Clear as mud?

Set CGRAM Address
Character Generator Random Access Memory (CGRAM) allows the HD44780 to be programmed with up to eight user defined characters or symbols when the font is set to 5x8 dots (which is usually the case).

The CGRAM address is 6 bits long (DB5:DB0), so we can use these bits to address 64 bytes. Each custom defined character (in 5x8 dot mode) takes 8 bytes, so, as previously mentioned, there can be eight custom defined characters.

Here is an example of a custom character:

Posted Image

Here are the bytes that would need to be sent to the HD44780 in order to store the above custom character in CGRAM:

  
0x40, // Set CGRAM address: Set CGRAM address 0 in the address counter
0x00, // up arrow ..
0x04,
0x0E,
0x1F,
0x04,
0x04,
0x04,
0x00

The first byte is sent to the IR register (a command), after which the others - containing the character data - are sent to the DR register (data). This process can be repeated to store other custom characters. After setting the CGRAM address to zero for the first character, setting the CGRAM address for following custom characters is optional since the CGRAM address increments on every write. Either way, after you're done writing your custom characters to the CGRAM, a command must be sent to put the cursor back into the DDRAM (e.g. Clear Display, or Return Home) before writing display data.

You can use the HD44780 custom character hex code generator below to design custom characters and get the hex codes.



I used six custom characters to make the bow tie in the picture below:



Posted Image



This is from Doctor Who, if you didn't get the reference.. You can download the code used to generate the text and graphics in the above image here: HD44780 LCD Framework Basic.


8-bit Mode Initialization
Send the following bytes to configure the HD44780 to operate in 8-bit mode:

0x38, // Function set: 8-bit mode, 2 lines
0x1,  // Clear Display: Clears the display & set cursor position to line 1 column 0
0x6,  // Entry mode set: Cursor moves to the right
0xC,  // Display control: Display on, cursor off, cursor blink off

8-bit protocol
  • The EN line should be low
  • Set the RS line (low for command, or high for data), set the EN line high
  • Load the byte onto DB0-DB7 (i.e. set each digital output to 1 or 0)
  • Wait a little while (time depends on the display, but about a µs is good)
  • Set the EN line low
  • Wait a little while (again, depends on the display)

4-bit Mode Initialization
Send the following bytes to configure the HD44780 to operate in 4-bit mode:

0x2,  // Function set: 4-bit mode
0x28, // Function set: 4-bit mode, 2 lines
0x1,  // Clear Display: Clears the display & set cursor position to line 1 column 0
0x6,  // Entry mode set: Cursor moves to the right
0xC,  // Display control: Display on, cursor off, cursor blink off

On reset, the HD44780 defaults to 8-bit mode. So when we write 0x02 using DB7:DB4, the HD44780 sees 0010 0000 – the lowest four bits being zero since those lines aren’t connected to anything. While the high nibble (which is what we sent; DB7:DB4) is by itself the number 2, when the two nibbles are concatenated, the number becomes 0b00100000 (20 decimal), which is the command to put the HD44780 into 4-bit mode.

4-bit protocol
The 4-bit protocol is exactly the same as the 8-bit, except the high nibble (the four most significant bits) is sent first and then the low nibble is sent. In other words, the byte has to be broken in to two halves and sent in two parts.


Example Code
While I have posted programs demonstrating both 4-bit and 8-bit mode operation, I recommend using the 4-bit code because it uses fewer I/O pins and has been updated to be much more robust than the old 8-bit sample code.
  • HD44780 in 4-bit mode using a timer
  • HD44780 in 8-bit mode using a timer

Example code: HD44780 in 4-bit mode using a timer

Here is an example program that polls an analog input (an accelerometer, in my case) and outputs the value to the LCD:

/**************************************
* Author: Nathan House				  *
* Device: PIC18F4550				  *
* E-Mail: roboticsguy@roboticsguy.com *
* Website: www.robotenthusiasts.com	  *
* Date Modified: 07/24/2011			  *
**************************************/

#include <p18f4550.h> // Always include the header file

/***************************
*   Device configuration   *
***************************/
#pragma config FOSC = HSPLL_HS	// Using 20 MHz crystal with PLL
#pragma config PLLDIV = 5 		// Divide by 5 to provide the 96 MHz PLL with 4 MHz input
#pragma config CPUDIV = OSC1_PLL2 // Divide 96 MHz PLL output by 2 to get 48 MHz system clock
#pragma config USBDIV = 2 		// USB clock comes from 96 MHz PLL output / 2
#pragma config FCMEN = OFF // Disable Fail-Safe Clock Monitor
#pragma config IESO = OFF  // Disable Oscillator Switchover mode
#pragma config PWRT = OFF  // Disable Power-up timer
#pragma config BOR = OFF   // Disable Brown-out reset
#pragma config VREGEN = ON // Use internal USB 3.3V voltage regulator
#pragma config WDT = OFF   // Disable Watchdog timer
#pragma config MCLRE = ON  // Enable MCLR Enable
#pragma config LVP = OFF   // Disable low voltage ICSP
#pragma config ICPRT = OFF // Disable dedicated programming port (only on 44-pin devices)
#pragma config CP0 = OFF   // Disable code protection

/***************************
*		  Defines 		*
***************************/
#define LCD_COMMAND 0
#define LCD_DATA 1

/** Note: If you change the pins used on your PIC, make sure to change the below definitions and
	the corresponding TRIS bits in config(). **/

// Only six I/O lines are needed in 4-bit mode. I put the lines into two groups of three
// so that I could use 3-pin male header connectors to make plugging/unplugging them easy.

#define LCD_RS LATBbits.LATB2 // High means text data, low means command data. (pin 10)
#define LCD_EN  LATBbits.LATB1 // Enable, works like the clock for transfers. High to indicate-
							  // -start of transmission, low for transmission complete. (pin 9)

#define LCD_DB7 LATDbits.LATD5 // MSb (pin 3)
#define LCD_DB6 LATDbits.LATD6 // (pin 4)
#define LCD_DB5 LATDbits.LATD7 // (pin 5)
#define LCD_DB4 LATBbits.LATB0 // LSb in 4-bit mode (pin 8)

/****************************
*	Function declarations	*
****************************/
void config(void);
void LCD(void);
void LCD_concat(unsigned char* str1, unsigned char* str2, unsigned char* result, int resultSize);
void LCD_itoa(int num, unsigned char* dest);

/************************************
*	Variables / Data structures	*
************************************/
union
{
	struct
	{
		unsigned bit0: 1;
		unsigned bit1: 1;
		unsigned bit2: 1;
		unsigned bit3: 1;
		unsigned bit4: 1;
		unsigned bit5: 1;
		unsigned bit6: 1;
		unsigned bit7: 1;
	};
	unsigned char LCD_dataByte;
}LCD_dataBits;


// Timer variables
unsigned int timerValue = 0;
unsigned char* timerValuePtr = (unsigned char*)&timerValue; // timerValuePtr is used to put TMRL & TMRH into timerValue.

// Character arrays for LCD,
unsigned char LCD_data[2][17] = {{ 0x09, 0x0A, 0xB, 0x08, 'B', 'o', 'w', 't', 'i', 'e', 's', ' ', ' ', 0x09, 0x0A, 0xB}, {0xC, 0xD, 0xE, ' ', 'a ', 'r ', 'e', ' ', 'c ', 'o ', 'o ', 'l ', ' ', 0xC, 0xD, 0xE}}; // These arrays are where you put your data - ONLY do so when the lcd is NOT updating
unsigned char ADCResult[5] = {0};

unsigned char LCD_string1[17] = "Accel. Data "; // These are where you put your data
unsigned char LCD_string2[17] = "line 2 text";

// Misc.
char LCD_cursorPos = 0;
char LCD_configComplete = 0;
char LCD_linePos = 1;
char LCD_state = 0;
char LCD_dataType = 0; // 1 = data, 0 = command
char LCD_configurationState = 0;
char LCD_doneUpdating = 1; // 1 = done updating, 0 = updating LCD
int  LCD_initialDelay = 703; // Initial delay of about 15ms to allow LCD to start up

// Initialization commands for LCD (16x2, 4-bit mode)
#define LCD_numConfigStates 5
unsigned char LCD_initializationData[LCD_numConfigStates] = {
0x2,  // Return cursor to home position (if this is below 0x28, text does not appear on the screen unless ICD3 is connected to board. Why?)
0x28, // Function set: 4-bit mode, 2 lines
0x1,  // Clear Display & set cursor position to line 1 column 0
0x6,  // Cursor moves to the right
0xC,   // Display on, cursor off, cursor blink off
};

#define LCD_numCustomChars 7
int LCD_customCharsConfigured = 0;
unsigned char LCD_customChars[LCD_numCustomChars][9] =
{
	{
		0x40, 0x00, 0x04, 0x0E, 0x1F, 0x04, 0x04, 0x04, 0x00 // up arrow
	},
	{
		0x48, 0x00, 0x0C, 0x12, 0x11, 0x10, 0x10, 0x10, 0x10
	},
	{
		0x50, 0x00, 0x00, 0x00, 0x00, 0x11, 0x0E, 0x0E, 0x11
	},
	{
		0x58, 0x00, 0x06, 0x09, 0x11, 0x01, 0x01, 0x01, 0x01
	},
	{
		0x60, 0x11, 0x12, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00
	}	,
	{
		0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
	},
	{
		0x70, 0x11, 0x09, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00
	}
};

void main (void)
{
	int i = 0;
  
	config(); // configures I/O and timers
	
	while(1) // Program loop
	{
		if(LCD_doneUpdating) // we don't want to try to update the LCD when it's in the middle of updating..
		{
			//LCD_itoa(ADRES, &ADCResult[0]); // Convert A/D result to a string and store in ADCResult char array.
			//LCD_concat(&LCD_string1[0], &ADCResult[0], &LCD_line1Data[0], 17); // concatenate ADCResult to LCD_string1 and store in LCD_line1Data
		  
			LCD_doneUpdating = 0; // Set this to zero to update LCD. Do not modify LCD strings while this is zero.

			if(ADCON0bits.GO_DONE == 0)
				ADCON0bits.GO_DONE = 1; // get the next sample
		}

		LCD(); // Execute every loop; Set LCD_doneUpdating = 0 (then leave it alone until it's set) to actually update the screen.

		if(ADRES > 512)
			LATDbits.LATD4 = 1;
		else
			LATDbits.LATD4 = 0;
	}
}

void LCD_concat (unsigned char* str1, unsigned char* str2, unsigned char* result, int resultSize) // concatenates second string to the end of the first string and stores in result
{
	int str1End = 0;
	int concat_i = 0;
  
	while(*(str1 + str1End) != 0)
	{
		*(result + str1End) = *(str1 + str1End); // copy str1 over to result
		str1End++; // find the end of str1 (where we start appending str2)
	}
  
	for(concat_i = 0; *(str2 + concat_i) != 0 && (str1End + concat_i) < (resultSize-2); concat_i++)
		*(result + str1End + concat_i) = *(str2 + concat_i);
}

void LCD_itoa (int num, unsigned char* dest)
{
	int itoa_i = 0;
	int digits = 0;
  
	// Get the number of digits
	if(num >= 1000)
		digits = 4;
	else if(num >= 100)
			digits = 3;
	else if(num >= 10)
			digits = 2;
	else
		digits = 1;
  
	// Convert digits to ascii and put in dest array
	for(itoa_i = (digits-1); itoa_i >= 0; itoa_i--)
	{
		*(dest + itoa_i) = num%10 + '0'; // '0' + int = ascii representation of int
		num /= 10; // discard least significant digit
	}
	*(dest + digits) = 0; // null terminate the string
}


void LCD (void)
{
	if(LCD_doneUpdating) // No point in executing all the code below if there's nothing to do..
		return;

	// Get the timer's value
	*timerValuePtr 	= TMR0L;
	*(timerValuePtr+1) = TMR0H;

	if(timerValue >= 47 + LCD_initialDelay) // about 1ms (initialDelay is used to let the LCD start up before trying to send commands to it)
	{
		LCD_initialDelay = 0;

		LCD_EN = 0; // should be low at initialization

		if(LCD_state == 0) // if data is not currently being written
		{
			if(LCD_configComplete == 0) // only enter here during configuration
			{
				if(LCD_configurationState < LCD_numConfigStates) // Sends all the LCD configuration bytes
				{
					LCD_dataBits.LCD_dataByte = LCD_initializationData[LCD_configurationState];
					
					LCD_dataType = LCD_COMMAND;
					LCD_state = 1; // start the data transmission state machine
					LCD_configurationState++;
						
				}
				else if(LCD_customCharsConfigured < LCD_numCustomChars)
				{
					if(LCD_cursorPos < 9)
					{
						if(LCD_cursorPos == 0) // If we are at the first byte in the custom char array
							LCD_dataType = LCD_COMMAND;
						else
							LCD_dataType = LCD_DATA;
					  
						LCD_dataBits.LCD_dataByte = LCD_customChars[LCD_customCharsConfigured][LCD_cursorPos];
						LCD_state = 1; // start the data transmission state machine
						LCD_cursorPos++; // next loop move on to the next byte in the array
					}
					else
					{
						LCD_cursorPos = 0; // reset the cursor position to 0
						LCD_customCharsConfigured++;
					}
				}
				else if(LCD_configurationState == LCD_numConfigStates )
				{
					LCD_configComplete = 1; // THIS SHOULD GO IN THE LAST CONFIGURATION STATE
				}			  
			}

			else if(LCD_configComplete == 1 && LCD_state == 0) // Once the LCD has been configured, continuously update screen contents
			{
				if(LCD_cursorPos == 16 || LCD_data[LCD_linePos-1][LCD_cursorPos] == 0) // If we're at the end of the string, or the rest of the string is empty..
				{
					if(LCD_linePos == 1)
						LCD_linePos = 2;
					else if (LCD_linePos == 2)
					{
						LCD_linePos = 1;
						LCD_doneUpdating = 1;
					}  
				  
					LCD_cursorPos = 0; // moving back to first spot

					if(LCD_linePos == 1)
						LCD_dataBits.LCD_dataByte = 0x80; // Set DDRAM address 0
					else if(LCD_linePos == 2)
						LCD_dataBits.LCD_dataByte = 0xC0; // Set DDRAM address 40 hex, or 64 dec. (i.e. the first position on the second line.)

					LCD_dataType = LCD_COMMAND; // high for text data or low for command
					LCD_state = 1; // start the data transmission state machine
				}
				else
				{
					LCD_dataBits.LCD_dataByte = LCD_data[LCD_linePos-1][LCD_cursorPos];
				  
					LCD_dataType = LCD_DATA;
					LCD_state = 1; // start the data transmission state machine
					LCD_cursorPos++;
				}  
			}

		} // if LCD_state == 0

		else if(LCD_state == 1) // E line should already be low
		{
			LCD_RS = LCD_dataType; // Set the Register Select line (data = 1, command = 0)

			LCD_state++;
		}
		else if(LCD_state == 2)
		{
			LCD_EN = 1; // Set E high to latch control state

			// Put most significant nibble on I/O pins
			LCD_DB4 = LCD_dataBits.bit4;
			LCD_DB5 = LCD_dataBits.bit5;
			LCD_DB6 = LCD_dataBits.bit6;
			LCD_DB7 = LCD_dataBits.bit7;

			LCD_state++;
		}
		else if(LCD_state == 3)
		{
			LCD_EN = 0; // Set E low to latch data

			LCD_state++;
		}
		else if(LCD_state == 4)
		{
			LCD_EN = 1; // Set E high so it can be set to low again to latch the second nibble.

			// Put least significant nibble on I/O pins
			LCD_DB4 = LCD_dataBits.bit0;
			LCD_DB5 = LCD_dataBits.bit1;
			LCD_DB6 = LCD_dataBits.bit2;
			LCD_DB7 = LCD_dataBits.bit3;

			LCD_state++;
		}
		else if(LCD_state == 5)
		{
			LCD_EN = 0; // Set E low to latch data. Remains low until another write sequence is started.

			LCD_state = 0;
		}

		TMR0H = 0;
		TMR0L = 0;
	}
}  


void config (void) // configures I/O and timers
{
	// Interrupt control register configuration (disable interrupts)
	INTCONbits.GIEH = 0;
	INTCONbits.TMR0IE = 0;
	
	// Timer0 configuration
	T0CONbits.TMR0ON = 0; // Stop the timer
	T0CONbits.T08BIT = 0; // Run in 16-bit mode
	T0CONbits.T0CS = 0;   // Use system clock to increment timer
	T0CONbits.PSA = 0;	// A prescaler is assigned for Timer0
	T0CONbits.T0PS2 = 1;  // Use a 1:256 prescaler
	T0CONbits.T0PS1 = 1;
	T0CONbits.T0PS0 = 1;
	INTCONbits.TMR0IF = 0; // Clear the timer interrupt flag
	T0CONbits.TMR0ON = 1; // Start the timer
	
	// Pin TRIS configurations
	TRISDbits.TRISD4 = 0; // LED output

	TRISDbits.TRISD5 = 0; // output
	TRISDbits.TRISD6 = 0; // output
	TRISDbits.TRISD7 = 0; // output

	TRISBbits.TRISB0 = 0; // output
	TRISBbits.TRISB1 = 0; // output
	TRISBbits.TRISB2 = 0; // output

	TRISAbits.TRISA0 = 1; // RA0/AN0 is an input

	// ADC configuration
	ADCON0bits.ADON = 0; // Disable A/D module
	ADCON0bits.CHS0 = 0; // Channel select bits
	ADCON0bits.CHS1 = 0;
	ADCON0bits.CHS2 = 0;
	ADCON0bits.CHS3 = 0;

	ADCON1bits.VCFG1 = 0; // Use VSS for Vref- source
	ADCON1bits.VCFG0 = 0; // Use VDD for Vref+ source
	ADCON1bits.PCFG0 = 0; // A/D port configuration bits -- makes pins either analog or digital. AN0 pin is analog, all others are digital
	ADCON1bits.PCFG1 = 1;
	ADCON1bits.PCFG2 = 1;
	ADCON1bits.PCFG3 = 1;

	ADCON2bits.ADFM = 1;  // A/D result is right justified
	ADCON2bits.ACQT0 = 1; // Aquisition time
	ADCON2bits.ACQT1 = 0;
	ADCON2bits.ACQT2 = 0;
	ADCON2bits.ADCS0 = 0; // A/D conversion clock
	ADCON2bits.ADCS1 = 1;
	ADCON2bits.ADCS2 = 1;

	ADCON0bits.ADON = 1; // Enable A/D module
}

Example code: HD44780 in 8-bit mode using a timer

I strongly recommend using the 4-bit code. The following 8-bit code was poorly written and has not been updated in some time.

Here is an example program that polls an analog input (an accelerometer, in my case) and outputs the value to the LCD:

/**************************************
* Author: Nathan House				*
* Device: PIC18F4550				  *
* E-Mail: roboticsguy@roboticsguy.com *
* Website: www.roboticsguy.com		*
* Date Modified: 02/9/2011			*
**************************************/

#include <p18f4550.h> // Always include the header file


/***************************
*   Device configuration   *
***************************/
#pragma config FOSC = HSPLL_HS	// Using 20 MHz crystal with PLL
#pragma config PLLDIV = 5 		// Divide by 5 to provide the 96 MHz PLL with 4 MHz input
#pragma config CPUDIV = OSC1_PLL2 // Divide 96 MHz PLL output by 2 to get 48 MHz system clock
#pragma config USBDIV = 2 		// USB clock comes from 96 MHz PLL output / 2
#pragma config FCMEN = OFF // Disable Fail-Safe Clock Monitor
#pragma config IESO = OFF  // Disable Oscillator Switchover mode
#pragma config PWRT = OFF  // Disable Power-up timer
#pragma config BOR = OFF   // Disable Brown-out reset
#pragma config VREGEN = ON // Use internal USB 3.3V voltage regulator
#pragma config WDT = OFF   // Disable Watchdog timer
#pragma config MCLRE = ON  // Enable MCLR Enable
#pragma config LVP = OFF   // Disable low voltage ICSP
#pragma config ICPRT = OFF // Disable dedicated programming port (44-pin devices)
#pragma config CP0 = OFF   // Disable code protection
  
/***************************
*		  Defines 		*
***************************/
#define COMMAND_DATA 0
#define TEXT_DATA 1

/** If you change the pins used on your PIC, make sure to change the below definitions and the corresponding TRIS bits in config(). **/

#define LCD_RS LATDbits.LATD7 // High means text data, low means command data.
#define LCD_RW LATDbits.LATD6 // Read/Write. As of now, we only write to the LCD.
#define LCD_E  LATDbits.LATD5 // Enable, works like the clock for transfers. High to indicate start of transmission, low for transmission complete.

#define LCD_DB7 LATBbits.LATB0 // MSb
#define LCD_DB6 LATBbits.LATB1
#define LCD_DB5 LATBbits.LATB2
#define LCD_DB4 LATBbits.LATB3

#define LCD_DB3 LATDbits.LATD0
#define LCD_DB2 LATDbits.LATD1
#define LCD_DB1 LATDbits.LATD2
#define LCD_DB0 LATDbits.LATD3 // LSb

/****************************
*	Function prototypes	*
****************************/
void config();
void LCD();
void LCD_setupStrings(unsigned char*, unsigned char*, int, int);
void LCD_itoa(int, int, int, int );

/************************************
*	Variables / Data structures	*
************************************/
union
{
	struct
	{
		unsigned bit0: 1;
		unsigned bit1: 1;
		unsigned bit2: 1;
		unsigned bit3: 1;
		unsigned bit4: 1;
		unsigned bit5: 1;
		unsigned bit6: 1;
		unsigned bit7: 1;
	};
	unsigned char LCD_dataByte;
}LCD_dataBits;


// Timer variables
unsigned int timerValue = 0;
unsigned char* timerValuePtr = 0;

// Character arrays for LCD
unsigned char LCD_line1Data[16] = {0}; // Do not directly put data in here
unsigned char LCD_line2Data[16] = {0};
unsigned char LCD_string1[16] = "Accel. Data"; // These are where you put your data
unsigned char LCD_string2[16] = "line 2 text";
unsigned char emptyString[16] = {0};

// Misc.
char LCD_charPos = 0;
char LCD_configComplete = 0;
char LCD_writingLine = 1;
char LCD_dataState = 0;
char LCD_dataType = 0; // 1 = text data, 0 = means command data.
char LCD_configurationState = 0;
char LCD_update = 0; // 1 = update screen
char LCD_doneUpdating = 1;
int LCD_initialDelay = 4500; // ~ .1 sec
int initialDelay = 705; // Initial delay of about 15ms to allow LCD to start up

// Initialization commands for LCD (16x2, 8-bit mode)
unsigned char LCD_initializationData[10] = {
0x30,
0x30,
0x30,
0x38,
0x8,
0x1,
0x6,
0xC,
0x80,
};
	
void main(void)
{
	config(); // configures I/O and timers
	timerValuePtr = (unsigned char*)&timerValue; // timerValuePtr is used to put TMRL & TMRH into timerValue.
	LCD_RW = 0; // Set low to write to the display. We only write to the display in this program.
	ADCON0bits.GO_DONE = 1; // Get sample
	
	while(1) // Program loop
	{
		if(LCD_doneUpdating) // we don't want to try to update the LCD when it's in the middle of updating..
		{
			LCD_setupStrings(&LCD_string1[0], emptyString, ADRES, 1024);
			LCD_doneUpdating = 0; // Set this to zero to update LCD. Do not modify LCD strings while this is zero.
			
			if(ADCON0bits.GO_DONE == 0)
				ADCON0bits.GO_DONE = 1; // get the next sample
		}

		LCD(); // Execute every loop; Set LCD_update = 1 to actually update the screen.
	}
}

void LCD_setupStrings(unsigned char* LCD_string1Ptr, unsigned char* LCD_string2Ptr, int append1, int append2)
{
	int j = 0; // counter var
	unsigned char tempLine1[16] = {0};
	unsigned char tempLine2[16] = {0};
	char EOL1 = 0;
	char EOL2 = 0;
		
	for(j = 0; j < 16; j++)
	{
		if(*(LCD_string1Ptr+j) != 0)
			tempLine1[j] = *(LCD_string1Ptr+j);
		else if(EOL1 == 0) // end of string (number 0) reached & end of line not yet set
			EOL1 = j;
			
		if(*(LCD_string2Ptr+j) != 0)
			tempLine2[j] = *(LCD_string2Ptr+j);
		else  if(EOL2 == 0) // end of string (number 0) reached & end of line not yet set
			EOL2 = j;
	}  
	
	for(j = 0; j < 16; j++) // copy array over
	{
		LCD_line1Data[j] = tempLine1[j];
		LCD_line2Data[j] = tempLine2[j];
	}
	
	LCD_itoa(append1, EOL1, append2, EOL2); // append numbers to end of string
}


void LCD_itoa(int append1, int EOL1, int append2, int EOL2)
{
	int itoa_i = 0;
	
	if(append1 < 1024) // ADC numbers are < 1024
	{
		LCD_line1Data[EOL1] = ':';
		
		for(itoa_i = 0; itoa_i < 4; itoa_i++) // clear the array
		{
			LCD_line1Data[EOL1 + 4 - itoa_i] = append1%10 + '0';
			append1 /= 10;
		}
	}  
	
	if(append2 < 1024) // ADC numbers are < 1024
	{
		LCD_line2Data[EOL2] = ':';
		
		for(itoa_i = 0; itoa_i < 4; itoa_i++) // clear the array
		{
			LCD_line2Data[EOL2 + 4 - itoa_i] = append2%10 + '0';
			append2 /= 10;
		}
		itoa_i=10;
	}
}


void LCD()
{
	if(LCD_doneUpdating) // No point in executing all the code below if there's nothing to do..
			return;
	
	// Get the timer's value
	*timerValuePtr = TMR0L;
	*(timerValuePtr+1) = TMR0H;

	if(timerValue >= 94 + initialDelay) // about 2ms (initialDelay is used to let the LCD start up before trying to send commands to it)
	{
		initialDelay = 0;

		LCD_E = 0; // should be low at initialization
				
		if(LCD_dataState == 0) // if data is not currently being written	  
		{
			if(LCD_configComplete == 0) // only enter here during configuration, or curser reset.
			{
				if(LCD_configurationState <= 8) // first 3 bytes are 0x30 (which signify 8-bit mode operation)
				{
					LCD_dataBits.LCD_dataByte = LCD_initializationData[LCD_configurationState];
				
					if(LCD_configurationState == 8)
						LCD_configComplete = 1; // THIS SHOULD GO IN THE LAST CONFIGURATION STATE & the reset state
				}
				else if(LCD_configurationState == 100) // reset curser position
				{
					if(LCD_writingLine == 1)
						LCD_dataBits.LCD_dataByte = 0x80;
					else if(LCD_writingLine == 2)
						LCD_dataBits.LCD_dataByte = 0xC0;
					
					LCD_configComplete = 1; // THIS SHOULD GO IN THE LAST CONFIGURATION STATE & the reset state
				}
				
				LCD_dataType = COMMAND_DATA; // high for text data or low for command
				LCD_dataState = 1; // start the data transmission state machine
				LCD_configurationState++;
			}
			
			if(LCD_configComplete == 1 && LCD_dataState == 0) // Once the LCD has been configured, continuously update screen contents
			{
				if(LCD_writingLine == 1)
				{
					LCD_dataBits.LCD_dataByte = LCD_line1Data[LCD_charPos];
					LCD_dataType = TEXT_DATA; // high for text data or low for command
					LCD_dataState = 1; // start the data transmission state machine
					
					if(LCD_charPos == 16 || LCD_line1Data[LCD_charPos] == 0) // if we're at the end of the string, or the rest of the string is empty..
					{
						LCD_writingLine = 2;
						LCD_charPos = 0; // moving back to first spot
						LCD_configComplete = 0; //
						LCD_configurationState = 100; // moves curser to first address on line one
						LCD_dataState = 0; // without this, the program can't get to the configuration state
					}  
					else
						LCD_charPos++;
				}  
				else if(LCD_writingLine == 2)
				{
					LCD_dataBits.LCD_dataByte = LCD_line2Data[LCD_charPos];
					LCD_dataType = TEXT_DATA; // high for text data or low for command
					LCD_dataState = 1; // start the data transmission state machine
					
					if(LCD_charPos == 16 || LCD_line2Data[LCD_charPos] == 0) // if we're at the end of the string, or the rest of the string is empty..
					{
						LCD_writingLine = 1;
						LCD_charPos = 0; // moving back to first spot
						LCD_configComplete = 0; //
						LCD_configurationState = 100; // moves curser to first address on line two
						LCD_dataState = 0; // without this, the program can't get to the configuration state
						
						LCD_doneUpdating = 1;
					}  
					else
						LCD_charPos++;
				}  
			}
			
			LCD_RS = LCD_dataType; // Set the Register Select line (command or text data)
			
			// Set the pin's logic levels
			LCD_DB0 = LCD_dataBits.bit0;
			LCD_DB1 = LCD_dataBits.bit1;
			LCD_DB2 = LCD_dataBits.bit2;
			LCD_DB3 = LCD_dataBits.bit3;
			LCD_DB4 = LCD_dataBits.bit4;
			LCD_DB5 = LCD_dataBits.bit5;
			LCD_DB6 = LCD_dataBits.bit6;
			LCD_DB7 = LCD_dataBits.bit7;
		} // if LCD_dataState == 0
			
		if(LCD_dataState == 0)
		{
			// waiting state.. nothing happens here
		}	  
		else if(LCD_dataState == 1) // Set E high to transmit data
		{
			LCD_E = 1;
			LCD_dataState++;
		}
		else if(LCD_dataState == 2) // Set E low to signal transmission complete
		{
			LCD_E = 0;
			LCD_dataState++;
		}
		else if(LCD_dataState == 3)
		{	  
			LCD_dataState = 0; // done for now. return to waiting state.
		}
			
		TMR0H = 0;
		TMR0L = 0;
	}
}  


void config() // configures I/O and timers
{
	// Interrupt control register configuration (disable interrupts)
	INTCONbits.GIEH = 0;
	INTCONbits.TMR0IE = 0;
	
	// Timer0 configuration
	T0CONbits.TMR0ON = 0; // Stop the timer
	T0CONbits.T08BIT = 0; // Run in 16-bit mode
	T0CONbits.T0CS = 0;   // Use system clock to increment timer
	T0CONbits.PSA = 0;	// A prescaler is assigned for Timer0
	T0CONbits.T0PS2 = 1;  // Use a 1:256 prescaler
	T0CONbits.T0PS1 = 1;
	T0CONbits.T0PS0 = 1;
	INTCONbits.TMR0IF = 0; // Clear the timer interrupt flag
	T0CONbits.TMR0ON = 1; // Start the timer
	
	// Pin TRIS configurations
	TRISDbits.TRISD0 = 0; // output
	TRISDbits.TRISD1 = 0; // output
	TRISDbits.TRISD2 = 0; // output
	TRISDbits.TRISD3 = 0; // output

	TRISDbits.TRISD4 = 0; // LED output
		
	TRISDbits.TRISD5 = 0; // output
	TRISDbits.TRISD6 = 0; // output
	TRISDbits.TRISD7 = 0; // output
	
	TRISBbits.TRISB0 = 0; // output
	TRISBbits.TRISB1 = 0; // output
	TRISBbits.TRISB2 = 0; // output
	TRISBbits.TRISB3 = 0; // output
	
	TRISAbits.TRISA0 = 1; // RA0/AN0 is an input
	
	// ADC configuration
	ADCON0bits.ADON = 0; // Disable A/D module
	ADCON0bits.CHS0 = 0; // Channel select bits
	ADCON0bits.CHS1 = 0;
	ADCON0bits.CHS2 = 0;
	ADCON0bits.CHS3 = 0;
	
	ADCON1bits.VCFG1 = 0; // Use VSS for Vref- source
	ADCON1bits.VCFG0 = 0; // Use VDD for Vref+ source
	ADCON1bits.PCFG0 = 0; // A/D port configuration bits -- makes pins either analog or digital. AN0 pin is analog, all others are digital
	ADCON1bits.PCFG1 = 1;
	ADCON1bits.PCFG2 = 1;
	ADCON1bits.PCFG3 = 1;
	
	ADCON2bits.ADFM = 1;  // A/D result is right justified
	ADCON2bits.ACQT0 = 1; // Aquisition time
	ADCON2bits.ACQT1 = 0;
	ADCON2bits.ACQT2 = 0;
	ADCON2bits.ADCS0 = 0; // A/D conversion clock
	ADCON2bits.ADCS1 = 1;
	ADCON2bits.ADCS2 = 1;
	
	ADCON0bits.ADON = 1; // Enable A/D module
}

More resources
Here are some websites I found helpful:

http://joshuagalloway.com/lcd.html
http://en.wikipedia....0_Character_LCD
http://www.avrbeginn...0_lcd/4bit.html

Conclusion

I hope this tutorial has helped you understand how the HD44780 LCD works. Feel free to comment or ask questions!


0 Comments


or Sign In