16X84 2x16 Line LCD Driver

Ron Kreymborg


The four files described below provide a complete program to display characters anywhere on a 2 line by 16 character LCD using a 4 data and 3 control line interface. Figure 1 shows the required connections. The programs are written in Microchip MPASM assembler and can be included in an MPLAB project. Check the special setup required.

Fig 1. LCD Connector to 16X84 pin Connections
LCD pins Function 16X84 pins
14-11 D7-4 DB7-4
5 R/W DB3
4 RS DB2
6 E DB1

The complete test program consists of four files. The main program is LCD_TST.ASM, and is here used only to show examples of how the LCD driver is called and my method of creating a calibrated delay. The LCD driver itself is LCD.ASM and is simply included into your program, typically at the end. There are two definition files called RKMACROS.INC and TIMER.INC which are from my project toolbox. The former defines the macros used while the latter computes at compile time the required internal timer time constants. Both can be left out after appropriate editing of the source files.

This version of the test program shows sending strings from program memory directly to the lcd driver using the sendit subroutine. However, my requirement is that I can send strings from both program or data memory, and a later version will show how these strings are assembled in data memory and then passed to the LCD driver.
A zipped copy of these four files is available: LCD.ZIP

Please refer all queries to Ron Kreymborg

RKMACROS.INC

	NOLIST
;******************************************************************
;
; A set of standard macros.
;
; Ron Kreymborg
;
;******************************************************************

intoff	macro
	bcf	intcon,gie
	btfsc	intcon,gie
	goto	$-2
	endm
bank0	macro
	bcf	STATUS,RP0
	endm
bank1	macro
	bsf	STATUS,RP0
	endm
dotris	macro	arg1, arg2
	bsf	STATUS,RP0
	movlw	arg1
	movwf	arg2
	bcf	STATUS,RP0
	endm
dodelay	macro	arg1
	movlw	arg1
	call	delay
	endm
	LIST

TIMER.INC

; PRESCL and TIMER computation
	NOLIST
; Take the processor clock frequency in Hz (clock), and
; the required time delay in uSecs (dusec), and compute
; a value for the prescaler (PRESCL) and another for
; TMR0 (TMRVAL).

_cycle	equ	clock >> 2
_dfreq	equ	1000000 / dusec
_divr	equ	_cycle / _dfreq
	if (_divr < 512)
	error "Delay time too short. Assign PSA to WDT."
	endif
_test	set	_divr / 256
_testB	equ	_divr % 256
	if (_testB > 0)
_test	set	_test + 1
	endif
	if (_test > 1)
_temp	set	2
PRESCL	set	0
	endif
	if (_test > 2)
_temp	set	4
PRESCL	set	1
	endif
	if (_test > 4)
_temp	set	8
PRESCL	set	2
	endif
	if (_test > 8)
_temp	set	16
PRESCL	set	3
	endif
	if (_test > 16)
_temp	set	32
PRESCL	set	4
	endif
	if (_test > 32)
_temp	set	64
PRESCL	set	5
	endif
	if (_test > 64)
_temp	set	128
PRESCL	set	6
	endif
	if (_test > 128)
_temp	set	256
PRESCL	set	7
	endif
	if (_test > 255)
	error "Timer requirement is outside range"
	endif
TMRVAL	equ	256 - (_cycle / _temp / _dfreq)
	LIST

LCD_TST.ASM

;******************************************************************
;
; Test program for LCD driver.
;
;******************************************************************

	title	"LCD Driver (lcd.asm) Test"
	list p=pic16f84,r=dec,n=80,x=off,st=off
	include ‹p16f84.inc›
	include ‹rkmacros.inc›
	errorlevel -302		; no bank warnings

clock	equ	10000000	; my crystal frequency
dusec	equ	1000		; required delay is 1 mSec
	include	‹timer.inc›	; (note include must be after 
				;   previous two definitions)

; Data -
	CBLOCK	0x0C		; start of data area
	dval
	pntr
	offset
	ENDC

;****************************************************************

	org	0			; for future expansion
	goto	start			;
	org	4			;
	retfie				;
	
; All messages and the sendit/gbyt routines must be in the 
; first 256 bytes of program memory.

mess1	dt	0x00,"Lat  38",0xdf,"56'42.4",0x22,0
mess2	dt	0x40,"Lng 178",0xdf,"23'19.8",0x22,0


; Send the message in program memory pointed to by W to the LCD. 

sendit	movwf	pntr			; save pointer to data
	clrf	offset			; clear "next byte" offset
	call	gbyt			; get the LCD address
	iorlw	0x80			; set as address command
	call	lcd_c			; send command
snd1	incf	offset,f		; step offset to next byte
	call	gbyt			; get it
	iorlw	0			; terminating null?
	btfsc	STATUS,Z	
	return				; yes, done
	call	lcd_d			; send to LCD data memory
	goto	snd1			; around again
	
gbyt	movf	pntr,w			; get base address
	addwf	offset,w		; add offset
	movwf	PCL			; get char
	

;****************************************************************
; Main program begins

start	clrwdt				; setup 1mSec timer
	bsf	STATUS,RP0
	movlw	b'11010000' | PRESCL	; prescaler to TMR0
	movwf	OPTION_REG
	bcf	STATUS,RP0
	dotris	0,PORTA			; PORTA all outputs for now
	clrf	PORTA

	call	lcd_init		; initialise the LCD driver

	movlw	mess1			; send first line
	call	sendit
	movlw	mess2			; send second line
	call	sendit

loop	goto	loop	
	

; Delay routine using the internal timer. Will delay for W
; times the value of "dusec" in microseconds.

delay	movwf	dval
dy1	clrf	TMR0
	bcf	INTCON,T0IF
	movlw	TMRVAL			; set timer
	movwf	TMR0
dy2	btfss	INTCON,T0IF		; timer overflow yet?
	goto	dy2			; no
	decfsz	dval,f			; yes, all cycles?
	goto	dy1			; no
	return				; yes, delay finished
	
	include	‹lcd.asm›

	end

LCD.ASM

;******************************************************************
;
; Routines to display on a 2x16 character LCD display.
;
; LcdInit - call this subroutine before using the display. It uses
;           bits 7-1 of PORTB, leaving bit-0 free (although it will
;           be configured as an output).
;
; Lcd_C -   Sends the byte in W as a command to the display. Checks
;           if busy first. 
;
; Lcd_D -   Sends the byte in W as data. Checks for busy first.
;
; Assumes it can call a routine called "delay" with a required
; time delay in mSecs in W.
; Uses an additional three (3) levels of the stack.
;
; Address of first line on the LCD is 0
; Address of second line is 64
;
; Ron Kreymborg
;******************************************************************

lcd_com	macro	arg1
	movlw	arg1
	call	lcd_c
	endm
	
; Data -
	CBLOCK		; lcd data area
	lcd_flags
	lcd_temp
	ENDC
	
; Flags -
lcd_Bflg	equ	0
lcd_RSflg	equ	1

; I/O portB - 
lcd_BUSY	equ	7		; input - LCD is busy (0x80)
lcd_R_W		equ	3		; output - LCD Read/Write (0x08)
lcd_RS		equ	2		; output - Register Select (0x04)
lcd_E		equ	1		; output - LCD Enable (0x02)



lcd_init
	clrf	PORTB
	dotris	b'00000000',PORTB	; all outputs and low to start
	bcf	lcd_flags,lcd_RSflg
	dodelay	15			; 15mSec power up delay		
	movlw	b'00110000'		; 8-bit mode
	movwf	PORTB
	call	lcd_clk
	dodelay	4			; 4mSec wait
	movlw	b'00110000'		; 8-bit mode
	movwf	PORTB
	call	lcd_clk
	dodelay	1			; 1 mSec wait
	movlw	b'00110000'		; 8-bit mode
	movwf	PORTB
	call	lcd_clk
	dodelay	4			; 4mSec wait
	movlw	b'00100000'		; set for 4-bit
	movwf	PORTB
	call	lcd_clk

; Reset sequence is done - initialise for us

	lcd_com	b'00101000'		; 4-bits, 2-lines, 5x7
	lcd_com	b'00001000'		; display off, cursor off, blink off
	lcd_com	b'00001100'		; display on
	lcd_com	b'00000001'		; clear display
	lcd_com	b'00000110'		; increment, no display shift
	return
	
	
; Check whether the LCD is busy. Loop until it isn't. When
; it's free, output the byte passed in W, MS nibble first.

lcd_d	bsf	lcd_flags,lcd_RSflg	; set RS flag for data
	goto	lcd_o1
lcd_c	bcf	lcd_flags,lcd_RSflg	; clear RS flag for commands
lcd_o1	movwf	lcd_temp		; save control word
	dotris	b'11110000',PORTB	; RB7-4 as inputs for busy
	movlw	1 << lcd_R_W
	movwf	PORTB			; set up for read
	
lcd_o2	bcf	lcd_flags,lcd_Bflg	; assume not busy
	bsf	PORTB,lcd_E		; clock high
	nop				; little wait
	btfsc	PORTB,lcd_BUSY		; busy set?
	bsf	lcd_flags,lcd_Bflg	; yes
	bcf	PORTB,lcd_E		; clock low
	nop				; little wait
	nop
	call	lcd_clk			; get low bits but ignore (for now)
	btfsc	lcd_flags,lcd_Bflg	; was it busy?
	goto	lcd_o2			; yes
	dotris	b'00000000',PORTB	; RB7-4 outputs again

	movf	lcd_temp,w		; get word to send
	call	lcd_o3			; send high nibble
	swapf	lcd_temp,w		; get word again
lcd_o3	andlw	0xf0			; just high bits (clears all control bits)
	movwf	PORTB
	btfsc	lcd_flags,lcd_RSflg	; was lcd_RS set?
	bsf	PORTB,lcd_RS		; yes
	call	lcd_clk			; write current nibble
	return
	

lcd_clk	bsf	PORTB,lcd_E		; pulse E line high
	nop
	bcf	PORTB,lcd_E
	return
	

	end