Basic LCD display example for the PIC18F4620

I previously posted an LCD display example program for the dsPIC30F4011 microcontroller. This is the same program adapted for the PIC18F4620 microcontroller. I’ll have to update this later with a circuit diagram, but here’s a quick photo of the program running on a breadboard circuit. Hopefully you can work out which pins are which from the code and/or photo, as well as by referring to the more complete documentation for the dsPIC30F4011 example. Photo of LCD display example program for PIC18F4620 running on a breadboard circuit

//
// lcd.c - LCD display examplefor dsPIC18F4620
// Written by Ted Burke - Last updated 8-10-2013
//

#include <xc.h>

// Select clock oscillator (default frequency Fosc=1MHz -> Tcy = 4us).
// Disable reset pin, watchdog timer, low voltage programming and
// brown-out reset.
#pragma config OSC=INTIO67,MCLRE=OFF,WDT=OFF,LVP=OFF,BOREN=OFF

// Select which pins the program will use for the LCD screen
// control signals, RS, RW and E.
// NB I had to change these from the names used in my previous
// dsPIC30F4011 example to avoid a clash with the equivalent
// definitions in the XC8 compiler's peripheral library.
#define PIN_RS LATDbits.LATD7
#define PIN_RW LATDbits.LATD6
#define PIN_E LATDbits.LATD5

// Select a pin to use for the flashing LED
#define PIN_LED LATDbits.LATD4

// Function prototypes for transmitting to LCD
void delay_ms(unsigned int n);
void send_nibble(unsigned char nibble);
void send_command_byte(unsigned char byte);
void send_data_byte(unsigned char byte);
 
int main()
{
    TRISD = 0b00000000; // Set RD0-7 as digital outputs
    
    // Let's just write to the LCD and never read!
    // We'll wait 2ms after every command since we can't
    // check the busy flag.
    PIN_RW = 0;
    PIN_RS = 0;
    PIN_E = 1;
     
    // Some LCD modules require a delay after power-up
    // before you can begin communicating with them, so
    // I'm putting a 1 second delay here just in case.
    delay_ms(1000);
    
    // Initialisation
    delay_ms(16); // must be more than 15ms
    send_nibble(0b0011);
    delay_ms(5); // must be more than 4.1ms
    send_nibble(0b0011);
    delay_ms(1); // must be more than 100us
    send_nibble(0b0011);
    delay_ms(5); // must be more than 4.1ms
    send_nibble(0b0010); // select 4-bit mode
    
    // Display settings
    send_command_byte(0b00101000); // N=0 : 2 lines (half lines!), F=0 : 5x7 font
    send_command_byte(0b00001000); // Display: display off, cursor off, blink off
    send_command_byte(0b00000001); // Clear display
    send_command_byte(0b00000110); // Set entry mode: ID=1, S=0
    send_command_byte(0b00001111); // Display: display on, cursor on, blink on
     
    // Define two 8 character strings
    const char line1[] = "  Ted's ";
    const char line2[] = "PIC18F  ";
     
    // Write the two strings to lines 1 and 2
    int n;
    send_command_byte(0x02); // Go to start of line 1
    for (n=0 ; n<8 ; ++n) send_data_byte(line1[n]);
    send_command_byte(0xC0); // Go to start of line 2
    for (n=0 ; n<8 ; ++n) send_data_byte(line2[n]);
     
    // Now just blink LED indefinitely
    while(1)
    {
        PIN_LED = 1;
        delay_ms(500);
        PIN_LED = 0;
        delay_ms(500);
    }
}
 
// Delay by specified number of milliseconds
void delay_ms(unsigned int n)
{
    // At Fosc=1Mhz, Tcy is 4us. That's the time
    // taken to perform one machine code instruction.
    // Therefore a delay of 250 x Tcy = 1ms.
    while(n--) _delay(250);
}
 
void send_nibble(unsigned char nibble)
{
    // Set RD0-3 without affecting RD4-7
    LATD = (LATD & 0xF0) + nibble;
    delay_ms(1);
    // Note: data is latched on falling edge of pin E
    PIN_E = 0;
    delay_ms(1);
    PIN_E = 1;
    delay_ms(2); // Enough time even for slowest command
}
 
// Send a command byte (i.e. with pin RS low)
void send_command_byte(unsigned char byte)
{
    PIN_RS = 0;
    send_nibble(byte >> 4);
    send_nibble(byte & 0xF);
}
 
// Send a data byte (i.e. with pin RS high)
void send_data_byte(unsigned char byte)
{
    PIN_RS = 1;
    send_nibble(byte >> 4);
    send_nibble(byte & 0xF);
}

I compiled the program using Microchip’s XC8 compiler. This was the command I used:

xc8 --chip=18F4620 lcd.c

I then used the PICkit 2 application to transfer the compiled hex file onto the microcontroller.

EDIT 18-4-2015: Thanks to Brek Martin who left a comment below suggesting the 1-second delay at power-up, which apparently is required for some LCD modules.

This entry was posted in Uncategorized. Bookmark the permalink.

18 Responses to Basic LCD display example for the PIC18F4620

  1. David Egan says:

    Thanks for writing this Ted. Much appreciated.

  2. David Egan says:

    Yea, all working. The analog input is also working. I should have the whole system working tomorrow. I will keep you posted.
    Thanks again for the help.

  3. Darren says:

    Hi Ted,
    First off great article, I cannot get it to work though.
    I am using C18 compiler inside MPLAB v8.73, using a PIC18F2525 running @ 4MHz.
    When I run the simulator in MPLAB with suitable break points the PIN_E doesn’t seem to toggle within the send_nibble function.
    If you wouldn’t mind taking a quick look I would be grateful.
    Here is the amended code that I am using.
    Thank you.
    #include

    #pragma config OSC = XT
    #pragma config PWRT = OFF
    #pragma config BOREN = OFF
    #pragma config WDT = OFF
    #pragma config LVP = OFF
    #pragma config PBADEN = OFF
    #define _XTAL_FREQ 4000000

    #define PIN_RS LATAbits.LATA4
    //#define PIN_RW LATDbits.LATD6
    #define PIN_E LATAbits.LATA5

    // Select a pin to use for the flashing LED
    #define PIN_LED LATBbits.LATB1
    const char line1[] = “Ted’s “; //These cause buid failure
    const char line2[] = “PIC18F “; //when in main body

    int n; //…as does this!
    // Function prototypes for transmitting to LCD
    void delay_ms(unsigned int n);
    void send_nibble(unsigned char nibble);
    void send_command_byte(unsigned char byte);
    void send_data_byte(unsigned char byte);

    void main(void)
    {
    TRISC = 0b00000000; // Set RD0-7 as digital outputs

    // Let’s just write to the LCD and never read!
    // We’ll wait 2ms after every command since we can’t
    // check the busy flag.
    // PIN_RW = 0;
    PIN_RS = 0;
    PIN_E = 1;

    // Initialisation
    delay_ms(16); // must be more than 15ms
    send_nibble(0b0011);
    delay_ms(5); // must be more than 4.1ms
    send_nibble(0b0011);
    delay_ms(1); // must be more than 100us
    send_nibble(0b0011);
    delay_ms(5); // must be more than 4.1ms
    send_nibble(0b0010); // select 4-bit mode

    // Display settings
    send_command_byte(0b00101000); // N=0 : 2 lines (half lines!), F=0 : 5×7 font
    send_command_byte(0b00001000); // Display: display off, cursor off, blink off
    send_command_byte(0b00000001); // Clear display
    send_command_byte(0b00000110); // Set entry mode: ID=1, S=0
    send_command_byte(0b00001111); // Display: display on, cursor on, blink on

    // Define two 8 character strings
    // line1[] = “Ted’s “;
    // line2[] = “PIC18F “;

    // Write the two strings to lines 1 and 2

    send_command_byte(0x02); // Go to start of line 1
    for (n=0 ; n<8 ; ++n) send_data_byte(line1[n]);
    send_command_byte(0xC0); // Go to start of line 2
    for (n=0 ; n<8 ; ++n) send_data_byte(line2[n]);

    // Now just blink LED indefinitely
    while(1)
    {
    PIN_LED = 1;
    delay_ms(500);
    PIN_LED = 0;
    delay_ms(500);
    }
    }

    // Delay by specified number of milliseconds
    void delay_ms(unsigned int n)
    {
    // At Fosc=1Mhz, Tcy is 4us. That's the time
    // taken to perform one machine code instruction.
    // Therefore a delay of 250 x Tcy = 1ms.
    while(n–){ Delay10TCYx(25);}
    }

    void send_nibble(unsigned char nibble)
    {
    // Set RD0-3 without affecting RD4-7
    LATC = (LATC & 0xF0) + nibble;
    delay_ms(1);
    // Note: data is latched on falling edge of pin E
    PIN_E = 0;
    delay_ms(1);
    PIN_E = 1; //When simulated this pin remains unchanged > 4);
    send_nibble(byte & 0xF);
    }

    // Send a data byte (i.e. with pin RS high)
    void send_data_byte(unsigned char byte)
    {
    PIN_RS = 1;
    send_nibble(byte >> 4);
    send_nibble(byte & 0xF);
    }

    • batchloaf says:

      Hi Darren,

      In your modified delay_ms function, it looks like you have while(n-) rather than while(n–). If so, n won’t be decrementing and the program will get stuck in in the delay_ms function forever, rather than for the specified number of milliseconds. Is it possible that the problem is as simple as that?

      There could be lots of other things, but I’ll wait to hear whether the n– thing makes any difference. If the problem persists, I’ll have another more careful look, although unfortunately I don’t have a PIC18F2525 or MPLAB/C18 to test it with.

      Ted

      • batchloaf says:

        Oh yeah, also, because your clock speed is different (4MHz crystal), your delay_ms function will need to include a different number of instruction cycles per millisecond. I think it should be working on the basis of 1000 * Tcy = 1ms.

        • Basically, with your 4MHz crystal, Fosc = 4MHz.
        • Most PICs perform one instruction every four clock cycles (can’t guarantee this for the 18F2525 though, so do check!) which gives Fcy = 1MHz.
        • Therefore the instruction cycle, Tcy = 1us…
        • …and 1ms = 1000 * Tcy.

        So, I think your delay_ms function should probably look a bit like this:

        // Delay by specified number of milliseconds
        void delay_ms(unsigned int n)
        {
            // At Fosc=4Mhz, Tcy is 1us. That's the time
            // taken to perform one machine code instruction.
            // Therefore a delay of 1000 x Tcy = 1ms.
            while(n--){ Delay10TCYx(100); }
        }
        

        Give that a try and let me know how you get on.

        Ted

  4. Darren says:

    Hi Ted,
    Thanks for the speedy response.
    First of all, you were correct with regard to the crystal freq / timing issue. Lack of concentration on my part. The issue with the ENable pin was due to the ADCON1 register not being set for digital output (i.e. ADCON1 = 0b00001111;). The simulation in MPLAB appears to work, now to try it with the hardware..
    I will let you know if it works.
    Regards,Darren.

    • batchloaf says:

      Good stuff. Best of luck getting it working!

      Ted

      • Darren says:

        Hi Ted,
        The program doesn’t work.
        I know the hardware I’m using is fine because it has been used programmed with different software using another compiler/ ide.
        I have tried inserting the PIN_LED call at different points in the main function, however the led never lights, therefore the program isn’t running in the PIC at all. All in all baffling.
        I’ll keep trying, if you have any thoughts?

      • Darren says:

        Hi Ted,
        Ok, bit of a red herring with the led. I didn’t set LATB as digital outputs, so it now works. The lcd doesn’t.

      • batchloaf says:

        Hi Darren,

        Perhaps you could email me the exact C code you have at this point? If you include a high res photo showing your complete circuit, I’ll see if I can spot the problem. It’s probably something that will be easy to fix – it usually is, even when it’s hard to spot.

        My email is: ted.burke “at” dit.ie

        Ted

  5. knightd23 says:

    Hi Ted,

    How i can configure for 4*20 lcd?
    I know that i need chance de binary code from the first send_command_byte(), but i don’t know the new code for the lcd.

    knightd23

  6. SINDHU THYAGARAJAN says:

    Hi Ted,
    Can you help me out with interfacing 2×16 LCD with PIC18F46K20.? I am a new user to microcontrollers and I was trying with all background readings and documets but nothing displays on the LCD.
    I am using a PIC18F4XK20 starter kit, MPLAB and C18 compiler.
    Here’s the code I am using to display a name or a word, when i debug i can see the leds glowing on the kit but nothing displays on the LCD. First the voltage was too low to enable so i used level shifter between the pic and lcd even then i am not able to get any display on LCD. I guess my connections to the LCD are wrong. could you suggest me a harware connection diagram for the LCD and PIC18F46K20.
    Can you also check if my code is correct aswell!!?

    LCD.c FILE:
    // Hitachi HD44780 LCD library (4-bit mode)

    #include “p18f46k20.h”
    #include “delays.h”
    #include “LCD.h”

    // pin defines

    #define lcdRS PORTCbits.RC3 // RS
    #define lcdEN PORTCbits.RC2 // EN

    #define lcdDB PORTD // DB4 – RD0 , DB5 – RD1 , DB6 – RD2 , DB7 – RD3

    // pulse EN line
    void lcdENpulse (void) {

    lcdEN = 1;
    Delay10TCYx(20); // delay 200us
    lcdEN = 0;

    }

    // initialise routine
    void lcdInit (void) {

    TRISCbits.TRISC2 = 0;
    TRISCbits.TRISC3 = 0;

    TRISD=0;
    lcdDB=0; // initialise port

    Delay1KTCYx(20); // wait at least 15ms after Vcc on

    lcdRS = 0; // R/S = 0 for command
    lcdEN = 0;

    lcdDB = 0x3; // send 0011 (7 6 5 4)
    lcdENpulse(); // pulse EN line to latch command

    Delay1KTCYx(5); // delay 5ms

    lcdDB = 0x3; // send 0011 again
    lcdENpulse();

    Delay10TCYx(20); // delay 200us

    lcdDB = 0x3; // send 0011 again
    lcdENpulse();

    Delay10TCYx(20); // delay 200us

    lcdDB = 0x2; // Four bit mode
    lcdENpulse();

    // function set
    lcdWriteCommand(0b00101000); // 001DNF00: 4-bit,2 lines,5×8 font

    // enable display/cursor
    lcdWriteCommand(0b00001111); // 00001DCB: display on,cursor on,blink on

    lcdClearScreen(); // Clear and home screen

    // set entry mode
    lcdWriteCommand(0b00000110); // 000001IS: increment cursor,no display shift

    }

    // send command
    void lcdWriteCommand(unsigned char c) {

    lcdRS = 0; // for command
    Delay10TCYx(4); // small delay
    lcdDB = (( c >> 4) & 0b00001111 ); // send high nibble
    lcdENpulse();
    Delay1KTCYx(5); // delay 5ms
    lcdDB = ( c & 0b00001111 ); // send low nibble
    lcdENpulse();
    Delay1KTCYx(5); // delay 5ms
    }

    // send data
    void lcdWriteData(char c) {

    lcdRS = 1; // for data
    Delay10TCYx(4); // small delay
    lcdDB = (( c >> 4) & 0b00001111 ); // send high nibble
    lcdENpulse();
    Delay10TCYx(20); // delay 200us for data
    lcdDB = ( c & 0b00001111 ); // send low nibble
    lcdENpulse();
    Delay10TCYx(20); // delay 200us for data

    }

    // write string of characters
    void lcdWriteDataString(const rom char *string) {

    while( *string ) {
    lcdWriteData(*string);
    string++;
    }

    }

    // clear and home screen
    void lcdClearScreen(void) {
    lcdWriteCommand(0x1);

    }

    void lcdGoto(unsigned char address) {
    lcdWriteCommand(0x80+address);
    }

    LCD.h FILE:

    extern void lcdENpulse(void);
    extern void lcdInit(void);
    extern void lcdClearScreen(void);
    extern void lcdGoto(unsigned char address);
    extern void lcdWriteData(char c);
    extern void lcdWriteCommand(unsigned char c);
    extern void lcdWriteDataString(const rom char *string);

    main.c FILE:

    // config bits

    #pragma config FOSC = INTIO7, FCMEN = OFF, IESO = OFF // CONFIG1H
    #pragma config PWRT = OFF, BOREN = OFF, BORV = 27 // CONFIG2L
    #pragma config WDTEN = OFF, WDTPS = 32768 // CONFIG2H
    #pragma config MCLRE = ON, LPT1OSC = OFF, PBADEN = OFF, CCP2MX = PORTC // CONFIG3H
    #pragma config STVREN = ON, LVP = OFF, XINST = OFF // CONFIG4L
    #pragma config CP0 = OFF, CP1 = OFF, CP2 = OFF, CP3 = OFF // CONFIG5L
    #pragma config CPB = OFF, CPD = OFF // CONFIG5H
    #pragma config WRT0 = OFF, WRT1 = OFF, WRT2 = OFF, WRT3 = OFF // CONFIG6L
    #pragma config WRTB = OFF, WRTC = OFF, WRTD = OFF // CONFIG6H
    #pragma config EBTR0 = OFF, EBTR1 = OFF, EBTR2 = OFF, EBTR3 = OFF // CONFIG7L
    #pragma config EBTRB = OFF // CONFIG7H

    // includes

    #include “p18f46k20.h”
    #include “delays.h”
    #include “LCD.h”
    //#include “lcd.h”

    void initOsc(void);

    // static allocated uninitialised variables

    #pragma romdata

    const rom char *name = “HELLO”;

    #pragma udata

    // executable code

    #pragma code

    // functions

    void main(void)
    {
    initOsc();

    Delay10KTCYx(100);

    lcdInit();

    /*
    lcdGoto(0x05);
    lcdWriteData(‘H’);
    lcdWriteData(‘E’);
    lcdWriteData(‘L’);
    lcdWriteData(‘L’);
    lcdWriteData(‘O’);
    lcdGoto(0x45);
    lcdWriteData(‘W’);
    lcdWriteData(‘O’);
    lcdWriteData(‘R’);
    lcdWriteData(‘L’);
    lcdWriteData(‘D’);
    */
    Delay10KTCYx(100);

    lcdClearScreen();

    lcdWriteDataString(name);

    while(1);
    }

    void initOsc(void){

    OSCCON = 0b01010000; // bit 6-4 = 101 : 4 MHz internal oscillator
    // i.e. instruction cycle = 1 MHz (1 us)
    OSCTUNEbits.PLLEN = 0; // not 8 or 16 MHz so don’t need PLL

    }

    Thank you.
    SINDHU

  7. Brek Martin says:

    Thanks 🙂 It worked almost straight up on dsPIC33FJ64GP802. I’m using inverters for level conversion so I XOR the nibble before it goes out the port, and invert all control pin values.
    Also some LCD modules need a delay prior to initialisation. Mine is one of those.
    Some modules and programming languages recommend a second delay before talking to
    the LCD module at all, especially in the case where the micro is fast and can start initialisation before the LCD module is finished it’s own reset and looking for it.
    The code for two lines worked straight away. Cheers 🙂

    • batchloaf says:

      Hi Brek,

      Glad to hear you got it working and thanks for that useful tip about leaving a delay at the start – I’ll have to keep that in mind! I’ll add a note to the above article to point that out in case anyone runs into that problem.

      Ted

  8. Austin says:

    Great job Ted, it works perfectly ( PIC18F2550 on PicStart USB from Mikroe ).
    I especialy appreciate the simplicity of your code.
    ( don’t forget to connect the contrast voltage pin, it’s mandatory ! )
    Austin ( France )

    • batchloaf says:

      Thanks Austin!

      I can’t remember now what I was up to there with the contrast pin. I suppose I probably didn’t have a potentiometer to hand so I just tried a single pull-down resistor instead to set the contrast to maximum. I guess it must have worked, judging by the picture!

      Anyway, thanks again.

      Ted

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s