First Play with a 16x2 LCD with HD44780 Controller and a PIC Micro

I've been playing with PIC microcontrollers for a while now - I started because I wanted an inductance meter to measure coils for the crystal sets I am making... and googling led me to this circuit, which seemed fairly simple... and from there I fell headlong into microcontrollers.

The meter is complete, and functions well despite being hideously ugly (I used what I had at hand - eventually I'll build it into something tidier... maybe)... but the way it worked led me on to see what else I could make using one of these... and some cheap 16x2 LCDs based on the very common HD44780 controller came my way... so I started playing.

First I needed to be able to drive the LCD - and since I had no idea how, I had to do some searching and reading.  There is a LOT of info out there, much of it not particularly helpful - and often its not in a useful format.  The following stood out immediately though:

  • These displays generally have a 16-wire interface, of which 11 IO lines from the micro are needed.
  • 8 of these lines are for data (1 byte at a time)
  • Only 4 of these lines are actually neccessary, as you can split and send data in nibbles (4-bit)
  • The R/W (Read / Write) line is not neccessary as each operation has a specified maximum time to process - so just waiting that time rather than reading this line to see if you can write yet should be ok unless you need very fast display updates - and to be honest, these displays are slow anyway... so if that was the case you probably wouldn't be using one.  Tieing the R/W line to ground removes it from the required IO pool.
  • Dropping 4 data lines and the R/W line mean only 6 IO lines are needed for basic operation

The conclusion I came to, based on the fact I've got several PIC16F628A micros (I bought one for the LC meter, and got two spares to play with) with limited IO, is that I was happy to live with slightly slower updates.  

Next step was how to drive it.  I understood the theory - and thats what most of the sites I found had in spades... what I needed was some simple practical code to have a look at... and eventually I stumbled across the flex_lcd driver by a forum member on the CCSInfo forums,  It worked out of the box using a friends CCS PIC C compiler, and I was able to quickly get the LCD working (CCS has its own built-in driver, but it requires all 8 data lines and the R/W line to be connected).

As I used it, I discovered how it worked, and then tweaked and tidied it to suit me better.  My modified version follows:

// My 16x2 HD44780 LCD Driver, based entirely on the flex_lcd.c 
// driver by "PCM Programmer" of the CCS Info Forum
// http://www.ccsinfo.com/forum/viewtopic.php?t=24661
//
// It has just been slightly modified to suit my purposes, and 
// documented for my own reference.  Hopefully it will prove 
// useful to others.
//
// This driver ONLY supports 6-wire communications, so the LCD
// R/W (pin 5) must be tied to GND, and only 4 data lines are
// used (D4-D7) along with RS and Enable.
//
//  1              16
//  ________________________________________
// |oooooooooooooooo                        |
// |   __________________________________   |
// |  | [][][][][][][][][][][][][][][][] |  |
// |  | [][][][][][][][][][][][][][][][] |  |
// |  |__________________________________|  |
// |________________________________________|
//  Front View of a typical 16-wire 16x2 LCD
//
// The wiring for the LCD is as follows:
// 1 : GND
// 2 : 5V
// 3 : Contrast (0-5V)*
// 4 : RS (Register Select)
// 5 : R/W (Read Write)       - GROUND THIS PIN
// 6 : Enable or Strobe
// 7 : Data Bit 0             - NOT USED
// 8 : Data Bit 1             - NOT USED
// 9 : Data Bit 2             - NOT USED
// 10: Data Bit 3             - NOT USED
// 11: Data Bit 4
// 12: Data Bit 5
// 13: Data Bit 6
// 14: Data Bit 7
// 15: LCD Backlight +5V**
// 16: LCD Backlight GND
//
// *A 10k potentiometer with the two outside legs across 5V
// and GND, with the center (wiper) connected to this pin
// works perfectly here.
//
// **Some backlighting on some LCDs does not incorporate a
// current limiting resistor - check to see if yours does - if
// not, add an approx 180R resistor between 5V and pin 15.
// Alternatively, some LCDs hard-wire the backlighting on the
// board and provide a jumper you can remove if you don't want
// it... check the datasheet for your display.
//
// LCD Pin Assignments:
// The following are configured to suit the PIC16F628A so the
// 4 data pins (B4-B7) align with the 4 I/O pins used.  If
// you want to use other I/O pins then define the ones you
// want in your code, before including this file so that
// they override these ones.
#ifndef LCD_DB4
#define LCD_DB4   PIN_B4 
#endif

#ifndef LCD_DB5
#define LCD_DB5   PIN_B5
#endif

#ifndef LCD_DB6
#define LCD_DB6   PIN_B6
#endif

#ifndef LCD_DB7
#define LCD_DB7   PIN_B7
#endif

#ifndef LCD_E
#define LCD_E     PIN_B1 
#endif

#ifndef LCD_RS
#define LCD_RS    PIN_B2
#endif

// Define the LCD type and details
#define lcd_type 2        // 0=5x7, 1=5x10, 2=2 lines 
#define lcd_line_two 0x40 // LCD RAM address for the 2nd line 



// Send a nibble to the display
void lcdSendNibble(int8 nibble) { 
	// Note:  !! converts an integer expression 
	// to a boolean (1 or 0). 
	output_bit(LCD_DB4, !!(nibble & 1)); 
	output_bit(LCD_DB5, !!(nibble & 2));  
	output_bit(LCD_DB6, !!(nibble & 4));    
	output_bit(LCD_DB7, !!(nibble & 8));    
	
	delay_cycles(1); 
	output_high(LCD_E); 
	delay_us(2); 
	output_low(LCD_E); 
} 


// Send a byte to the display. 
void lcdSendByte(int8 address, int8 n) { 
	output_low(LCD_RS); 
	delay_us(60);  

	if(address) {
		output_high(LCD_RS);
	}   
	else { 
		output_low(LCD_RS);
	}    

	delay_cycles(1); 
	output_low(LCD_E); 

	lcdSendNibble(n >> 4); 
	lcdSendNibble(n & 0xf); 
} 


// Initialise the display.  This is the outline of what is
// required:
// 1 : Wait 20ms for the display to stabilise after power-up
// 2 : Set RS and ENABLE low
// 3 : Wait at least 200us
// 4 : Send nibble cmd 0x03
// 5 : Wait at least 200us 
// 6 : Send nibble cmd 0x03 again
// 7 : Wait at least 200us
// 8 : Send nibble cmd 0x03 a third time
// 9 : Wait at least 200us
// 10: Send nibble cmd 0x02 to enable 4-bit mode
// 11: Wait 5ms
// 12: Send byte cmd 0x40 (4-bit communications, 2 lines, 5x8)
// 13: Send byte cmd 0x0c (Turn the display on)
// 14: Send byte cmd 0x01 (Clear and home the display)
// 15: Send byte cmd 0x06 (Set left-to-right direction)
void initLCD (void) { 
	int8 i; 
	output_low(LCD_RS); 
	output_low(LCD_E); 

	delay_ms(15); 

	for(i = 0; i < 3; i++) { 
	    lcdSendNibble(0x03); 
	    delay_ms(5); 
	} 

	lcdSendNibble(0x02); // Home the cursor

	lcdSendByte(0, 0x40); // 4-bit, 2 line, 5x8 mode
	delay_ms(5);
	lcdSendByte(0, 0x0c); // Turn on display
	delay_ms(5);
	lcdSendByte(0, 0x01); // Clear & home display
	delay_ms(5);
	lcdSendByte(0, 0x06); // Left to Right
	delay_ms(5);
} 


// Position the cursor on the display.  (1, 1) is the top left
void lcdGoto(int8 x, int8 y) { 
	int8 address; 
	address = (y != 1)? lcd_line_two : 0;
	address += x-1; 
	lcdSendByte(0, 0x80 | address); 
} 


// Write characters to the display
void lcdPutC(char c) { 
	switch(c) { 
		case '\f': 
			lcdSendByte(0,1); 
			delay_ms(2); 
			break; 
    
		case '\n': 
			lcdGoto(1,2); 
			break; 
    
		case '\b': 
			lcdSendByte(0,0x10); 
 			break; 
    
		default: 
			lcdSendByte(1,c); 
			break; 
	} 
} 

 

And here is some test code I threw together to use the library and demonstrate custom characters as well (you can make 8 of them, and they are char(0) through (7)):

 

#include "16F628A.h"
#include <stdlib.h>
#include <string.h>

#fuses INTRC_IO, NOWDT, NOPUT, NOBROWNOUT, MCLR, NOLVP, NOPROTECT 
#use delay(clock = 4MHz)

#include "my_lcd.c" 


void main(){ 
	int i = 0, p = 0;
	char b1[8] = {0x0,0x0,0x1,0x7,0x1c,0xf,0x7,0x1};
	char b2[8] = {0x4,0x18,0x12,0xc,0x18,0x12,0xc,0x18};
	char* sentence = "This is a sentence, I want it to scroll past nicely... \0";
	int length = strlen(sentence);
	
	
	// Must initialise the display before using it
	initLCD(); 
	
	// Make a couple of custom characters:
	// First, send the 'write to CGRAM' command using send-byte function
	// Commands are sent by making the first parameter 0 (data uses a 1)
	lcdSendByte(0, 0x40);
	
	// Send custom character code
	// Checker pattern - 5 x 7 (make last byte 0 for symmetry)
	for (i = 0; i < 7; i++) {
		lcdSendByte(1, (i % 2 == 0)? 0b10101 : 0b01010);
	}
	lcdSendByte(1, 0);
	
	// Create a degree character using byte array from above
	for (i = 0; i < 8; i++) {
		lcdSendByte(1, b1[i]);
	}
	for (i = 0; i < 8; i++) {
		lcdSendByte(1, b2[i]);
	}
	
	// Put the two characters we made on the display
	lcdPutC("\f");
	lcdPutC(1); 
	lcdPutC(2);
	lcdPutC("Yummy Bananas!\n");
	// The above is living proof of my appalling art skills

	p = 0;
	
	while(TRUE) {
		lcdGoto(1,2);
		for (i = 0; i < 16; i++) {
			lcdPutC(sentence[(i + p) % length]);
		}
		delay_ms(400);
		p = (p < length)? p + 1 : 1;
	}	
}

There is more to come, but thats most of what I have for now.  I'll post some photos of my LCD wired up to the PIC shortly

Hope it helps someone