Posts

Interfacing LCD via SPI.

HD44780 LCD display

HD44780 LCD display

Introduction.

As time goes by, microcontrollers become more powerful, cheaper, and smaller. A typical micro of the past could have had 40 pins and no internal memory. On the contrary, modern J-series PICs are made with 96K program memory and 28 pins. We can drive a lot of peripherals with that amount of memory, however we are getting short on pins.

In this article I will show how to drive a parallel interface peripheral serially. A HD44780-compatible LCD module is good candidate – it is popular, inexpensive, and slow, so you won’t be losing any speed while converting parallel to serial. And you could even save some money using a micro with fewer pins.

SPI is very easy to implement thanks to synchronous serial communication hardware support in newer PICs. The only other thing necessary on the LCD end is 74HC595 double-buffered shift register. It can be easily obtained from on-line electronic components distributors, such as Mouser or Digi-Key for as little as 48 cents in SMT package, slightly more in DIP. Since it’s very simple device, most functionality that we need will be done in software.

HD44780-compatible LCD (LCD for short) is a parallel device. It has 8 data lines and 3 control signals which tell the controller when data lines contain valid data, what kind of data it is and whether you want to write to the controller or read from it (we won’t be using the last one BTW). SPI is serial – it sends out a byte one bit at a time, “marking” appearance of each bit with a clock pulse. The 74HC595 receives SPI data and sends is to it’s parallel outputs.

Take a look at 74HC595 schematic symbol. It has two buffers – one for serial data, another for parallel. Serial data go on pin 14. Serial clock goes on pin 11. When pin 12 goes from low to high serial buffer gets copied to parallel buffer and its contents appear on parallel output pins. The following animation shows how two bytes – 0x55 ( 0101 0101 ) and 0xAA ( 1010 1010 ), are transferred from SPI to parallel output.

Let’s talk briefly about native LCD interface. The device has 8 data lines and three control signals. It can work in either 8-bit or 4-bit mode. In 8-bit mode all data lines are used and LCD control is slightly easier, however, if we utilize this mode our little shift register won’t be able to accommodate any of the control signals. In 4-bit mode only upper 4 bits of the data bus are used; the data byte needs to be split in two halves and sent in two transfers. This is the mode that we will be using.
Control signals are RS, RW, and E. RS defines what kind of data is being sent to LCD. If RS is 0, we are sending a command, if RS is 1, the data is just a character to be printed on the screen. Falling edge of E initiates transferring of data bus and RS state to LCD controller. The last signal is RW – it controls data direction. Since we are using unidirectional shift register and won’t be able to read back the data bus, RW is simply tied to ground. In this mode, instead of reading LCD state we will just wait long enough before sending next byte to LCD.

Hardware.

The SPI-to-LCD circuit is made on a little daughter board. The idea is to attach it to LCD display and have 3 signal and 2 power wires run to the main board with MCU. The daughter board is designed to be attached to standard LCD 2×7 header. Here is a schematic. The board is available to order at BatchPCB

To keep you focused on communication with LCD the “main board with MCU” is made very simple. It’s ever-popular PIC18F4520 mounted on a breadboard. No crystal oscillator is used. Here is a schematic of breadboard connections.

What do we need to send a byte to LCD? First, RS and high nibble of data byte needs to be set. Then, E needs to be set and cleared – controller reads data bus on the falling edge of E. After that, we need to set low nibble of data bus and toggle E again. We need to wait some time before sending next byte. The delay depends on characteristics of the specific HD44780 clone, however, for most of them 1ms seems to be enough.

There is plenty of Internet resources about low-level details of controlling HD44780 LCDs; I don’t want to make another one. This LCD simulator allows you to play with LCD signals and see the results; give it and accompanying pages a try if you need more information about low-level LCD control.

Software: definitions.

Let’s talk about software to drive this circuit. I prefer using C language and code presented here has been checked to compile and work using Microchip C18 compiler. The code is almost standard; I use some custom typedefs which are self-explanatory. For example, BYTE means ‘unsigned char’ and WORD means ‘unsigned int’. I’m not trying to demonstrate clever or highly-efficient coding techniques – the goal of this article is to show basic principles. The whole set including MPLAB project files can be found in the downloads section.

The code will work on any PIC18 micro with built-in MSSP (master synchronous serial port, which supports SPI and I2C). Which of 18F micro to use is up to you, however, you will need to change some variable names if your PIC has 2 MSSP modules and you want this code to work out of MSSP2. For the demonstration I will be using PIC18F4520. The MPLAB project is also set up to use this micro.

Also, although not strictly necessary, the in-circuit debugger is very handy. By providing ability to step through code and see variable and register changes in real time, it helps understanding what is going on. I use ICD-2, you will need to change the project “Debugger” settings if you use other type of debugger or none at all.

Now let’s look at the problem from shift register’s point of view. Sometimes we need to change just one pin, sometimes we need to change several. Any time we need to change a single pin we send the whole byte where bit corresponding to this pin is different from the previous state. In order to do that we need to know the previous state of the register – after all, since we don’t specifically select the chip before transmitting, it will swallow anything that flies over SPI bus, even data sent to other peripherals. Let’s define a variable which holds a copy of shift register output pins. When we need to change a single pin we modify corresponding bit and send out the whole byte. This variable is:

BYTE LCDpins;

Elsewhere in the code BYTE is defined as “unsigned char”, which means 8 bits where each bit is a part of the number. On the other hand, “char” is signed by default; the most significant bit of it means “minus” if set and “plus” if cleared. Unsigned char ff is 255 decimal, char ff is -1.

We shall treat this variable as several independent entities. Bits 4-7 go to data lines DB4-DB7 of the LCD. Bit 3 is E line, bit 2 is RS line. We want to be able to change any of those without changing the rest. The easiest way to do it is to use bit operations. Single bits are easy. First we make two masks:

#define RS  0x04    // RS pin
#define E   0x08    // E pin

Then we write macros using the masks:

#define SET_RS  LCDpins |= RS
#define CLR_RS  LCDpins &= ~RS
#define SET_E   LCDpins |= E
#define CLR_E   LCDpins &= ~E

The first macro uses OR operation to set RS to 1. The RS mask has bit 2 set to 1 and zeroes in all other positions. OR operation works the following way: if any of the bits is 1 result would also be 1. When both are 0 result would be 0. When you OR a byte with a byte which has bit 2 set to 1 and all other bits set to 0 you will get a byte where bit 2 is 1 and all other bits stay whatever they were before the operation.

The second macro uses two operations. AND operation is opposite of OR – if any of the bits is zero result is also zero. For result to become 1 both operands must be 1. Before doing AND we invert our mask (make it look 11111011, that’s what tilde in ~RS does), so other bits would stay intact.

Later in the program we can use expressions like:

SET_RS;

or

CLR_E;

to change the corresponding bit without touching the rest of the byte.

The technique to change 4 upper bits is similar. Let’s assume there is a byte called ‘tosend’ which we need to send to LCD. We need to send it 4-bits at a time using 4 upper bits of LCDpins variable. We can’t simply OR or AND it since we need to modify all four and we can’t simply assign one to another because we don’t want to change the control signals. We’ll do it in two operations. First we set upper bits to known value (zero):

LCDpins &= 0x0f;

then we put upper nibble of tosend to the upper nibble of LCDpins , leaving the rest of LCDpins intact:

LCDpins |= ( tosend & 0xf0 );

then we send it.

To send the lower nibble we first repeat what we did in the beginning – clean the place:

LCDpins &= 0x0f;

Then we shift tosend 4 times to the left to move lower nibble to the upper four bits and OR it with LCDpins just as we did before:

LCDpins |= ( tosend << 4 ) & 0xf0;

The last hardware definition is RCK pin. I’m always trying to avoid using MCU pin names in the code. Instead, I put a #define statement like this:

#define RCK PORTEbits.RE0

in project header file. In the code I use LCD_RCK name only; this way, if I later decide to assign RCK function to some other pin, all I need to do is to change one line and recompile.

Software. Low-level functions.

The sequence to send a byte to the LCD is:

  1. Initialize SPI to work with 74HC595.
  2. Set RS low for command or high for data.
  3. Set E low, copy high nibble of a command or data byte to high nibble of LCD variable.
  4. Send LCD variable to SPI.
  5. Set E high.
  6. Send LCD variable to SPI.
  7. Set E low.
  8. Send LCD variable to SPI.
  9. Copy low nibble of a command or data byte to high nibble of LCD variable.
  10. Send LCD variable to SPI.
  11. Set E high.
  12. Send LCD variable to SPI.
  13. Set E low.
  14. Send LCD variable to SPI.

There is lot of repetitions here. Let’s make our life a little easier and write some functions. First of all we need to have something to talk to SPI hardware on a PIC. As I said before, SPI is easy. We will need two functions – one for initializing SPI and another to send a byte. The following SPI initialization function is part of the standard library included with Microchip C18 compiler.

/* SPI initialization */
/* Borrowed from Microchip library. I heard they are going to stop providing peripherals support code in future
    releases of C18 so I made a local copy just in  case */
/*  sync_mode:
              SPI_FOSC_4        SPI Master mode, clock = FOSC/4
              SPI_FOSC_16      SPI Master mode, clock = FOSC/16
              SPI_FOSC_64      SPI Master mode, clock = FOSC/64
              SPI_FOSC_TMR2  SPI Master mode, clock = TMR2 output/2
              SLV_SSON SPI    Slave mode, /SS pin control enabled
              SLV_SSOFF SPI   Slave mode, /SS pin control disabled
    bus_mode:
              MODE_00       SPI bus Mode 0,0
              MODE_01       SPI bus Mode 0,1
              MODE_10       SPI bus Mode 1,0
              MODE_11       SPI bus Mode 1,1
    smp_phase:
              SMPEND        Input data sample at end of data out
              SMPMID        Input data sample at middle of data out
*/
void init_SPI ( BYTE sync_mode, BYTE bus_mode, BYTE smp_phase )
{
  SSPSTAT &= 0x3f;    //power-on state
  SSPCON1 = 0x00;     //power-on state
  SSPCON1 |= sync_mode;
  SSPSTAT |= smp_phase;
 
  switch( bus_mode ) {
    case 0:                       // SPI bus mode 0,0
      SSPSTATbits.CKE = 1;       // data transmitted on rising edge
      break;
    case 2:                       // SPI bus mode 1,0
      SSPSTATbits.CKE = 1;       // data transmitted on falling edge
      SSPCON1bits.CKP = 1;       // clock idle state high
      break;
    case 3:                       // SPI bus mode 1,1
      SSPCON1bits.CKP = 1;       // clock idle state high
      break;
    default:                      // default SPI bus mode 0,1
      break;
  }
 
  switch( sync_mode ) {
    case 4:                       // slave mode w /SS enable
      TRISAbits.TRISA5 = 1;       // define /SS pin as input
    case 5:                       // slave mode w/o /SS enable
      TRISCbits.TRISC3 = 1;       // define clock pin as input
      SSPSTATbits.SMP = 0;        // must be cleared in slave SPI mode
      break;
    default:                      // master mode, define clock pin as output
      TRISCbits.TRISC3 = 0;       // define clock pin as output
      break;
  }
 
  TRISC &= 0xDF;                  // define SDO as output (master or slave)
  TRISC |= 0x10;                  // define SDI as input (master or slave)
  SSPCON1 |= SSPENB;              // enable synchronous serial port
}

In order to work with 74HC595, we call this function with following parameters:

init_SPI ( SPI_FOSC_4, MODE_00, SMPMID );

In order to send ( and at the same time receive ) a byte via SPI you need to write this byte to SSPBUF. This starts the transfer. While sending out this byte one bit at a time MSSP also reads the SDI pin copying what it sees there in it’s buffer. From programmer’s point of view MSSP receives data in the same register we use for sending data – SSPBUF. In reality, there are three separate places, SSPBUF being the only one accessible to the program. When transmission is complete, MSSP copies receive buffer contents to SSPBUF and sets the BF flag. In order to send the next byte we need to clear this flag. The only way to clear the flag is to read SSPBUF even if there’s nothing interesting to read. The procedure is:

/* writes to SPI. BF is checked inside the procedure */
/* returns SSPBUF */
BYTE wr_SPI ( BYTE data )
{
 SSPBUF = data;             // write byte to SSPBUF register
 while( !SSPSTATbits.BF );  // wait until bus cycle complete
 return ( SSPBUF );         //
}

This procedure is very easy and not very efficient – you can see that we are wasting time waiting for the transfer to finish. There are better ways to do it and I’m going to describe those ways in another article some day.

Now, when we got SPI under control, it’s time to start sending bytes to the shift register. The following procedure copies contents of LCDpins variable to parallel outputs of 74HC595:

/* copies LCDpins variable to parallel output of the shift register */
void SPI_to_74HC595( void )
{
    wr_SPI ( LCDpins );     // send LCDpins out the SPI
    RCK = 1;                //move data to parallel pins
    RCK = 0;
}

The next step is to start sending bytes to LCD. in 4-bit mode, two transfers are made for each byte – first with high nibble, then with low nibble. In addition to that, we need two transfers to change state of E signal. That’s how it looks like:

/* sends a byte to LCD in 4-bit mode */
void LCD_sendbyte( BYTE tosend )
{
    LCDpins &= 0x0f;                //prepare place for the upper nibble
    LCDpins |= ( tosend & 0xf0 );   //copy upper nibble to LCD variable
    SET_E;                          //send
    SPI_to_74HC595();
    CLR_E;
    SPI_to_74HC595();
    LCDpins &= 0x0f;                    //prepare place for the lower nibble
    LCDpins |= ( tosend << 4 ) & 0xf0;    //copy lower nibble to LCD variable
    SET_E;                              //send
    SPI_to_74HC595();
    CLR_E;
    SPI_to_74HC595();
}

Now, paraphrasing “The Good, the Bad, and the Ugly” characters, there are two kinds of bytes you send to the LCD: those that contain a command and those you want to see on the screen (i.e., characters). To tell the LCD controller that byte that you’re sending is a charcter, you set RS high. Commands go with RS low. To make code writing easier, let’s define a couple of macros:

#define LCD_sendcmd(a)  {   CLR_RS;             \
                            LCD_sendbyte(a);    \
                        }
 
#define LCD_sendchar(a) {   SET_RS;             \
                            LCD_sendbyte(a);    \
                        }

That’s pretty much it – everything we need to start using our LCD. Except for one thing – initialization.

On power-up HD44780 sets itself to 8-bit mode. In order for us to use it we need to switch it to 4-bit mode. The initialization routine is quite different from the rest; it’s a mix of setting/clearing control pins, sending bytes and delays. In HD44780 datasheet it’s called “initialization by instruction”. This is the code of the LCD initialization routine for your viewing pleasure:

/* LCD initialization by instruction                */
/* 4-bit 2 line                                     */
/* wait times are set for 8MHz clock (TCY 500ns)    */
void LCD_init ( void )
{
  CLR_RS;
  RCK = 0;
  /* wait 100msec */
  Delay10KTCYx ( 20 );
  /* send 0x03 */
  LCDpins =  0x30;  // send 0x3
  SET_E;
  SPI_to_74HC595 ();
  CLR_E;
  SPI_to_74HC595 ();
  /* wait 10ms */
  Delay10KTCYx(2);
  SET_E;          // send 0x3
  SPI_to_74HC595 ();
  CLR_E;
  SPI_to_74HC595 ();
  /* wait 10ms */
  Delay10KTCYx(2);
  SET_E;          // send 0x3
  SPI_to_74HC595 ();
  CLR_E;
  SPI_to_74HC595 ();
  /* wait 1ms */
  Delay1KTCYx( 2 );
  LCDpins =  0x20;        // send 0x2 - switch to 4-bit
  SET_E;
  SPI_to_74HC595();
  CLR_E;
  SPI_to_74HC595();
  /* regular transfers start here */
  Delay1KTCYx( 2 );
  LCD_sendcmd ( 0x28 );   //4-bit 2-line 5x7-font
  Delay10KTCYx( 1 );
  LCD_sendcmd ( 0x01 );   //clear display
  Delay10KTCYx( 1 );
  LCD_sendcmd ( 0x0c );   //turn off cursor, turn on display
  Delay10KTCYx( 1 );
  LCD_sendcmd ( 0x06 );   //Increment cursor automatically
}

This strange looking list of transfers and delays is specific to 4-bit initialization. Delays are set for 8MHz internal oscillator clock of PIC18F4520. If your clock is slower and you don’t worry that much about execution time, you can leave delays unchanged. If, on the other hand, your clock is higher, you need to change delay values here. Delay routines are standard Microchip library routines, refer to compiler documentation to see what they are and how to set certain delays. The correct delay time is given in the comments of LCD initialization function.

I want to show you couple of interesting things that happen during initialization. First of all, we start sending data with a single transition of E. This is because LCD powers-up in 8-bit mode; in order to work, initialization sequence should not depend on how many data pins are actually connected. So first four transfers are made in 8-bit mode; for those only upper nibble makes sense, the contents or lower nibble are irrelevant. We send ‘3’ three times – that’s a hard reset. Then we send ‘2’ – this switches LCD to 4-bit mode. After that transfer we can use our regular “2-stroke” routines for data transfer; we first send “0x28”, where ‘2’ again means 4-bit mode, ‘8’ means 2-line display and 5×7 font. If you have 1-line display you will need to change this command to ‘0x20’. The rest of the commands is just usual display preparation – clear screen, turn off cursor and other useful stuff. Leave them where they are for now – they are not important. What is important is that we have every piece of code to start printing characters on LCD.

Software. High-level functions.

You can interact with your LCD using functions described in the previous section. However, if you need to output strings longer than 2-3 characters, as well as move output position around the screen, the following pieces of code could be helpful.

The first one outputs a string of characters:

/* send NULL-terminated string */
void LCD_send_string( const rom char *str_ptr )
{
    while (*str_ptr) {
        LCD_sendchar(*str_ptr);
        str_ptr++;
    }
}

The usage is simple. First, you need to define a constant containing the output string, like:

const rom char *const rom hello = "hello, world\n"

Then you call the function:

LCD_send_string( hello );
hello, world

will be printed on LCD screen.

You can see that all this function really does is continuously calls LCD_sendchar until it encounters NULL character.

Next function outputs a byte as a pair of characters. This is handy if you need to output a value of a variable, like counter or a result of analog-to-digital conversion.

/* sending 2 ASCII symbols representing input byte in hex */
void LCD_send_hexbyte ( BYTE data )
{
    BYTE temp = data>>4;            //prepare first output character
 
    if ( temp > 9 ) temp+=7;        //jump to letters in ASCII table
    LCD_sendchar ( temp + 0x30 );
 
    data = data & 0x0f;             // mask 4 high bits
    if ( data > 9 ) data+=7;
    LCD_sendchar ( data + 0x30 );
}

The last function demonstrates usage of LCD commands. It clears the screen and returns the cursor to the home position.

void LCD_Home( void )
{
    LCD_sendcmd( LCD_CLRSCR );
    Delay10KTCYx(1);
    LCD_sendcmd( LCD_HOME );
}

As you can see, there is a delay inserted after the “Clear screen” command. This delay is essential; we need to wait that long before sending other data to the display to satisfy timing requirements.

Many commands can be used as-is without writing additional functions. For example, very often we need to print at the second line of 2-line display by moving the cursor to position 0x40. This can simply be done by calling:

LCD_sendcmd( 0xc0 );

The “set cursor” command code is 0x80 + address. 0x80 + 0x40 is 0xc0. Alternatively, since “set sursor” command is defined in the header as SET_CURSOR, we can write the previous call as:

LCD_sendcmd( SET_CURSOR + 0x40 );

There is a lot more that can be done with HD44780 dispaly. However, the is a plenty of information about it already. Google ‘+HD44780 +commands’ is your friend.

Now it’s time to run our code.

Putting it all together

It’s time to build a circuit. As i said before, it can be made on a breadboard of suitable size. On this picture you can see how it looks like. The SPI daughter board is riding on the back of LCD module.

It’s also time to install MPLAB and C18. They can be downloaded from Microchip site in “Design Tools” section.

Finally, download the project itself from here.

Unzip the archive, compile, load, and enjoy.

Most likely, this will work right away. If not, leave a note in the comments and I will help you out.

Good luck!

Oleg.

61 comments to Interfacing LCD via SPI.

  • susilah

    Hi

    I’m doing my final year project in the university.
    I want to use PIC18F4520 to LCD (2×16) to sent message for a home control security system.

    Can I connect the MCU port direct to the LCD
    I’m new to this and have not done any programming

    Could you please advise
    Thanks

    • You can connect LCD directly to the MCU port and program signals like I explained in this article. Also, Microchip has standard LCD library included with it’s C18 compiler, which uses parallel connection; you may want to take a look at this library also and maybe use their code instead of writing your own.

  • Mateo

    Hi
    How about if i would like to use this on a PIC that doesn’t have a MSSP? (master synchronous serial port, which supports SPI and I2C).
    Almost all PICs has a SPI port, and the ones i got has SPI too (dsPIC30F4011 & 4013), but it has no MSSP.
    Do i have to completly rewrite the SPI routines or can i just make small adjustments?
    I know will have to use the C30 compiler as it is a completly different PIC (16bit).

    /Mateo

  • Mike

    Hi Oleg,

    I am trying to do this on a PIC18F87J11 with a PICDEM HPC Explorer board. It is pretty similar to your project. Could you please help me? I am just starting out working with PICs but I can write the code. From the research I’ve done the LCD is connected to the SPI I/O expander just like yours and uses MSSP. I have tried to use the C18 Library but it is not working.

    Thanks,
    Mike

    • I believe c18 routines are written for parallel LCD. You can use my code but you would need to re-write spi-to-595 routine to work with i/o expander.

  • Gibson

    hi,

    I am quite new to this P18F4520 chips and i intend to use it independently (without demo board). Could you help me with the steps to separate the chips?

    thanks.

    • I don’t think separating the chips is a good idea. Get a new PIC18F4520 and build a circuit around it; breadboard will do if you keep connections short.

  • keem

    Hi,
    Can I implement this type of configuration for a PIC16 microcontroller?

    Thanks! =)

  • Yes you can; however, as far as I know, PIC16 don’t have hardware SPI so you would have to bit bang it.

  • mahendar

    i want to know about 9 bit spi for lcd nokia6100. please help

  • Dinnin

    Hey where can i find that LCD with the Shift/SPI attached? or did you make it yourself? Also i was wondering the SL1 does that correspond to the layout of the dautghter board there. I bought the actual CHIP shift register that you used it did not come all pretty like yours.

    • I made SPI interface myself. It can be done on a piece of protoboard or built on a breadboard if you just want to play with it. Link to schematic is in the article.

  • i am newbie at pic programming. i am try spi with mplab ide my pic is 18f4520 and i want use the D port but i cant make the config. how can i do it?

    thanks in advance

  • Hello Oleg,
    Just as a matter of interest I designed a very similar project 10 years ago. You can check it out here:
    http://www.rasmicro.com/projects.htm#SPI

    Roger

  • Ahmed

    Hi Mr Oleg

    “Thanks for providing the tutorial which is very useful.”
    I am trying to implement your project using Ur code with pic18f4520 and 74CH595N register, but I am not having any display on LCD:(.

    Do I need to add or modify the code given at all?

    ( I am new with MCU)

    Regards

    • The code should work. Check if your display is hooked up right – when you apply power you should see a row of black rectangles. If you don’t see them, check contrast bias.

  • Ahmed

    Hi Oleg,

    Thanks for reply.

    Well I can see the black rectangles while I power up the circuit, but there is no display of the text (Hello, World). I read the output from the SPO pin and translated it to binary and I got the right letter which meant the data is being transmitted succesfully.

    Although the SCK (RB4)output is 1.6V PK-KP and SCL(RB3) 0.8V PK-PK. However, the wave form looked like clock signal.

    Could you suggest what can be wrong with the outputs driven by MICRO?

    Regards,

    • What is SPO pin?

      Assuming that your power is 5V the signal levels you see could mean that your outputs are shorted to some other outputs driven low. You also can get low levels if your oscilloscope bandwidth is not high enough. Check the other side of the shift register to see if it is shifting correctly.

  • Ahmed

    Hi,

    Correction: I meant( should have typed) SDO.

    The output from the shift register for E line was constantly as well as a single bit of data line. I think the clock signal’s amplitude was too low to drive the register. As I am working in lab, tomorrow I will check the output of the shift register tomorrow and update the result.

    I am using MPLAB ICD 3 to program the Micro and the pins for SPI are identified as follows:

    SDO= RC5
    SCK= RC3
    SDI= RC4
    but I dont know what RCK ( if not reset clock) is and which pin in pic18f4520 is and I have it at all, as I am just writting data to LCD.

    Regards,

  • Ahmed

    Correction2!:

    The ouput from teh shift register for E line was constantly high as….

  • Ahmed

    Hi Oleg,

    “Many thanks for your help so far.”

    I connected the RE0 to RCK and now I am having kind of garbage display. Just to make sure, I am using external crystal of 20Mhz and 2*16 LCD.

    Do I have to change anything else from the delay functions in the code to have the problem sorted!?

    Regards,

    • You need to increase delays. My code is written for 4MHz internal oscillator. Look at the comments in the code, they show necessary delays in ms.

  • Ahmed

    Hi Oleg,

    how can should I configure the pic to use external Oscillator ?

    Regards,

  • ahmed

    Hi Mr Oleg,

    I configured the HS and now its working fine. Thanks for providing the source and on spot help. God bless you.

    Regards,

  • Ahmed

    Hi everyone, “I am back again”

    Now I am trying to program 4×20 standard Hitachi LCD using pic18f4520 using the library provided by c18 compiler “xlcd.h”. I just tried this simple program below, but I could not get any display. I think there something wrong with the configuration of prama (but dont know exactly). Could you check if anything wrong with the code?

    #pragma config OSC = HS
    #pragma config WDT = OFF
    #pragma config LVP = OFF
    #pragma config DEBUG = OFF
    #include “delays.h”
    #include “p18f4520.h”
    #include “xlcd.h”

    void DelayXLCD(void);
    void DelayPORXLCD(void);
    void DelayFor18TCY(void);
    void Delay1KTCYx(int);

    rom char *msg1 = “SSE8720 demo board.”;
    rom char *msg2 = “LCD display test.”;
    rom char *msg3 = “This is line three.”;
    rom char *msg4 = “This is line four.”;

    void WriteDataXLCD(char);
    void OpenXLCD(unsigned char);
    //void putrsXLCD(char);
    void main (void)
    {

    LATB=0x00;
    TRISB=0x00;
    LATD=0x00;
    TRISD=0x00;

    ADCON1 = 0x0E; /* configure PortA pins for digital */

    OpenXLCD(EIGHT_BIT&LINES_5X7);

    WriteCmdXLCD(0x80); /* set cursor to column 1 row 1 */
    putrsXLCD(msg1);
    WriteCmdXLCD(0xC0); /* set cursor to column 1 row 2 */
    putrsXLCD(msg2);
    WriteCmdXLCD(0x94); /* set cursor to column 1 row 3 */
    putrsXLCD(msg3);
    WriteCmdXLCD(0xD4); /* set cursor to column 1 row 4 */
    putrsXLCD(msg4);

    while (1); /* hold the program not to output to LCD */
    }

    /*********************************************************/
    /* delay*/
    /*********************************************************/
    void DelayFor18TCY(void)
    {
    Nop();Nop();
    Nop();
    Nop();
    Nop();
    Nop();
    Nop();
    Nop();
    Nop();Nop();

    }

    /*********************************************************/
    /* delay */
    /*********************************************************/
    void DelayPORXLCD(void)
    {
    Delay1KTCYx(75); // Delay of 15ms
    // Cycles = (TimeDelay * Fosc) / 4
    // Cycles = (15ms * 20Mhz ) / 4
    // Cycles = 75,000
    return;
    }

    /*********************************************************/
    /* delay */
    /*********************************************************/
    void DelayXLCD(void)
    {
    Delay1KTCYx(25); // Delay of 5ms
    // Cycles = (TimeDelay * Fosc) / 4
    // Cycles = (5ms * 20Mhz) / 4
    // Cycles = 25,000
    return;
    }

    Regards,

  • Ahmed

    hi all,
    I just simplified the code as below, but still not having the messages on display, although I checked the signals on control and data port are present.

    #include
    #define LCD_DATA PORTA
    #define LCD_D_DIR TRISA
    #define LCD_RS PORTCbits.RC4
    #define LCD_RS_DIR TRISCbits.TRISC4
    #define LCD_RW PORTCbits.RC6
    #define LCD_RW_DIR TRISCbits.TRISC6
    #define LCD_E PORTCbits.RC7
    #define LCD_E_DIR TRISCbits.TRISC7

    void LCD_rdy(void); /* These are function prototype definitions */
    void LCD_cmd(char cx);
    void LCD_init(void);
    char get_DDRAM_addr(void);
    void LCD_putch(char dx);
    void LCD_putstr(rom char *ptr);
    void send2LCD(char xy);

    rom char *msg1 = “SSE8720 demo board.”;
    rom char *msg2 = “LCD display test.”;
    rom char *msg3 = “This is line three.”;
    rom char *msg4 = “This is line four.”;

    void main (void)
    {

    ADCON1 = 0x0E;

    for(;;){ /* configure PortA pins for digital */

    LCD_init();
    LCD_cmd(0x80); /* set cursor to column 1 row 1 */
    LCD_putstr(msg1);
    LCD_cmd(0xC0); /* set cursor to column 1 row 2 */
    LCD_putstr(msg2);
    LCD_cmd(0x94); /* set cursor to column 1 row 3 */
    LCD_putstr(msg3);
    LCD_cmd(0xD4); /* set cursor to column 1 row 4 */
    LCD_putstr(msg4);
    }
    // while (1); /* hold the program not to output to LCD */
    }
    /******************************************************************/
    /* the foloowing function waits until the LCD is not busy. */
    /******************************************************************/
    void LCD_rdy(void)
    {
    char test;
    LCD_D_DIR = 0xFF; /* configure LCD data bus for input */
    test = 0x80;
    while (test) {
    LCD_RS = 0; /* select IR register */
    Nop(); Nop();Nop(); Nop();
    LCD_RW = 1; /* Set read mode */
    LCD_E = 1; /* Setup to clock data */
    test = LCD_DATA;
    Nop(); Nop();
    LCD_E = 0; /* complete a read cycle */
    test &= 0x80;/* check bit 7 */
    }
    LCD_D_DIR = 0x00; /* configure LCD data bus for output */
    }
    /******************************************************************/
    /* The following function sends a command to the LCD. */
    /******************************************************************/
    void LCD_cmd(char cx)
    {
    LCD_rdy(); /* wait until LCD is ready */
    LCD_RS = 0; /* select IR register */
    LCD_RW = 0; /* Set write mode */
    LCD_E = 1; /* Setup to clock data */
    Nop(); Nop();
    LCD_DATA = cx; /* send out the command */
    Nop(); Nop(); /* small delay to lengthen E pulse */
    LCD_E = 0; /* complete an external write cycle */
    }
    /*****************************************************************/
    /* The following function initializes the LCD kit properly. */
    /*****************************************************************/
    void LCD_init(void)
    {
    PORTA = 0; /* make sure LCD control port is low */
    LCD_E_DIR = 0;
    LCD_RS_DIR = 0;
    LCD_RW_DIR = 0;
    LCD_cmd(0x3c); /* configure display to 2×40 */
    LCD_cmd(0x0F); /* turn on display, cursor, and blinking */
    LCD_cmd(0x14); /* shift cursor right */
    LCD_cmd(0x01); /* clear display and move cursor to home */
    }
    /******************************************************************/
    /* The following function obtains the LCD cursor address. */
    /******************************************************************/
    char get_DDRAM_addr (void)
    {
    char temp;
    LCD_D_DIR = 0xFF; /* configure LCD data port for input */
    LCD_RS = 0; /* select IR register */
    LCD_RW = 1; /* setup to read busy flag */
    LCD_E = 1; /* pull LCD E-line to high */
    temp = LCD_DATA & 0x7F; /* read DDRAM address */
    Nop(); Nop(); /* small delay to length E pulse */
    Nop(); Nop(); Nop(); Nop();

    LCD_E = 0; /* pull LCD E-line to low */
    return temp;
    }
    /*******************************************************************/
    /* The following function sends a character to the LCD kit. */
    /*******************************************************************/
    void LCD_putch (char dx)
    {
    char addr;
    LCD_rdy(); /* wait until LCD internal operation is complete */
    send2LCD(dx);
    LCD_rdy(); /* wait until LCD internal operation is complete */
    addr = get_DDRAM_addr();
    if (addr == 0x13) {
    LCD_cmd(0xC0);
    LCD_rdy();
    send2LCD(dx);
    }
    else if(addr == 0x53) {
    LCD_cmd(0x94);
    LCD_rdy();
    send2LCD(dx);
    }
    else if(addr == 0x27){
    LCD_cmd(0xD4);
    LCD_rdy();
    send2LCD(dx);
    }
    }
    /*******************************************************/
    /* The following function outputs a string to the LCD. */
    /*******************************************************/
    void LCD_putstr(rom char *ptr)
    {
    while (*ptr) {
    LCD_putch(*ptr);
    ptr++;
    }
    }
    /*********************************************************/
    /* The following function writes a character to the LCD. */
    /*********************************************************/
    void send2LCD(char xy)
    {
    LCD_RS = 1;
    Nop(); Nop();
    LCD_RW = 0;
    Nop(); Nop();
    LCD_E = 1;
    LCD_DATA = xy;
    Nop();
    Nop();
    LCD_E = 0;
    }

  • Microchip LCD library functions won’t work with this circuit

  • Dinnin

    I noticed a few things on the picture and your schematic and had a few questions.

    First in the picture you have what looks like a capacitor between pin 11 and 12 on the pic18 but its not in ur schematic. If there is one there what is its value?

    Also how much voltage does the SPI require i noticed you have the same VCC for the LCD and the SPI is that 5V? 3V? just curious b/c i am planning on building this and wanted to do it right!

    • 0.1uF capacitors are recommended by spec between VCC and ground, in case of 18F452 between both VCCs and ground (there is another VCC pin on the other side ). VCC voltage can be 5V or 3V – it depends on your LCD contrast bias more than anything else. If you have 3.3V-rated LCD(not common), use 3.3V at VCC, otherwise use 5V.

  • Dinnin

    Ok i just want to clarify, I need a cap between VDD and VCC on the pic18f452, on both sides? and do i also need one between both VSS and ground? Just wondering b/c in your picture it looks like the cap is between pin 11 and 12 which is VDD and VSS. And this is different from you schematic and i dont want to mess this up since i am mid build.

    Also i have the pic18f452 I/P will that work for this build? I do not know the difference?

    Thanks for you help sorry if the questions are trivial.

  • VCC is VDD, VSS is GND. You need capacitors between pins 11,12 and 31,32.

    AFAIK PIC18F452 doesn’t have internal oscillator. You will need to build an external one. It will be hard to make it work well on a breadboard.

    • In order to print something, you need to clear the screen. Do this in main() -> insert LCD_Home(); before while( 1 ); you shall get blank screen. To print a string, define it like strings “hello” and “second_line” at the beginning of SPI_LCD.c and then print it using LCD_send_string().

      SPI_LCD.c


      const rom char *const rom temperature = “TEMP = “;

      ….

      void main( void )

      LCD_Home();
      LCD_send_string( temperature );
      LCD_send_hexbyte( ReadADC(0) );

      while( 1 );
      }

  • Tom

    So far i built your entire project and got the hello world sent to the screen and everything!!! good work i learned a lot

    I am now trying to send a reading to the LCD say Its an ADC value is this how i would send it? Let me know where i have gone wrong, using ReadADC(0) as my value,

    void LCD_send_hexbyte ( BYTE ReadADC(0) )
    {
    BYTE temp = ReadADC(0)>>4; //prepare first output character

    if ( temp > 9 ) temp+=7; //jump to letters in ASCII table
    LCD_sendchar ( temp + 0x30 );

    ReadADC(0) = ReadADC(0) & 0x0f; // mask 4 high bits
    if ( ReadADC(0) > 9 ) data+=7;
    LCD_sendchar ( data + 0x30 );
    }

    Also what if i wanted my screen to read TEMP = ( the ADC VALUE here ) how would i manage that?

  • afzal khan

    hello sir is this possible to make any lcd to support spi mode through the hardware connection….because all lcd have same pin and drivers…so is it possible to spi with do -d7 pins

  • afzal khan

    sir we are using at32uc3a (atmel 32 bit ) which supported spi bt sir at lcd therer are 16 pins/20 pin but how can we know that they will support spi .sir i have an lcd 20into4 JHD 204A .if u have any schematic or project please provide me schematic…thanks for your reply

  • afzal khan

    m also having a graphic lcd ogm 128*64, plz provide me the schematic to connect it by the serial connection i.e spi…

    thank you so much

  • coolj

    /* writes to SPI. BF is checked inside the procedure */
    /* returns SSPBUF */
    BYTE wr_SPI ( BYTE data )
    {
    SSPBUF = data; // write byte to SSPBUF register
    while( !SSPSTATbits.BF ); // wait until bus cycle complete
    return ( SSPBUF ); //
    }

    Hi Oleg,

    Thanks for posting a great tutorial!

    Can you give us an idea of how you would implement a routine that doesn’t need to wait on the flag to clear, based on your statement above?

    A simple sentence would be suffice – no need to go into code development (unless you really want to! 😉 )

    Thanks,
    Justin

  • Peter

    This solution does not read the busy flag of the HD44780 and consequently does not do the busy wait stipulated in the HD44780 specs, does it? It’s just guessing that the chip is not busy. Some operations like “clear” will take 2+ milliseconds to complete…

    • This is correct. Implementing busy read would require bi-directional interface to LCD; I haven’t seen 2ms delays on clear in a long time, if your LCD is that slow just add delay to Clear() command – you won’t be calling it very often anyway.

  • Soujanya

    Hello,
    Actually i am trying to interface a TRULY LCD which has 24data lines and few control signals to OMAP(a processor which is used on mobiles) , actually i am connecting LCD parallel mode but i have to use SPI to communicate from processor to LCD,my doubt is if i have to set the clock signal high or else set the reset signal etc…can i do it in my code i am not able to understand how to know when the clock signal s high or how to set it high etc…

    • If you are using SPI facility on a micro, SPI clock is usually in phase with CPU clock. You can find it out in the datasheet for your micro.

  • Soujanya

    Hi,

    Actually i am trying to interface an LCD with my board, in the datasheet provide by LCD people they said that for the initial ower on sequence first we have to set the RESET ping high and then low for 50ms then write few registers inside the LCD, here i am not able to understand how to set the clock high in my Code.

  • Soujanya

    Hi,
    Thanks a lot,sorry for troubling with such a silly doubt, what i am doing is whenever i want to set the reset pin of LCD panel high, i am setting the corresponding microprocessor pin which is connected to reset pin of LCD high….

  • Hi,

    This microprocessor that you used, the shift register in SPI(SSPSR) first shifts MSB bit i.e. (MSB bit will be appear first on QA and LSB bit will be appear on QH in 74HC595(8 bit mode)), whereas LCD receive data from LSB to MSB (DB0 to DB7). My question is, is it necessary in your program to add some function before “SSPBUF = data;” that change shift operation form (MSB to LSB) to (LSB to MSB)?
    Thanks,

    Ivan

  • SEB

    bsr oleg ,je travaille avec STM32F100RB ,je souhaite contrôler un module d’affichage à base de MAX7219
    http://www.mikroe.com/eng/products/view/163/serial-7-seg-display-2-board/ via l’interface SPI;
    et Implémenter un compteur piloté par les appuis du bouton USER.
     en utilisant les périphériques: SPI et GPIO.est ce que vous pouvez m aider à ce propos v que je suis nouveau avec les STM32

  • A. Rafay

    Hi everyone,
    can anybody please share the CIRCUIT DIAGRAM of SPI (pic 18f452).
    actually I just want to check the code given above. i need it for my FYP(Final Year Project).

  • In your “Software. Low-level functions.” section, step 4 says to send the variable over SPI (with the enable bit not set). Looking at your LCD_sendbyte() method, you skip step 4 and the first transfer over SPI has the enable bit set. Same thing for step 10. The algorithm says there should be 3 SPI transfers per nibble but LCD_sendbyte() only has 2 SPI transfers per nibble.

    Is this intentional? The LCD controller latches RS on the rising edge of E so theoretically it could “miss” RS if the RS and E bits are set simultaneously in the first transfer.

    • You are right – per datasheet, the data and RS must be valid some time before E strobe. However, setting E, RS and data at the same time works fine for all HD44780-compatible displays I’ve seen so far.

      You can change LCD_sendbyte() to do “set E”, “clear E” to “clear E”, “set E”, if you want the signals to appear per datasheet.

      • The reason I brought it up is because I have implemented the same circuit using an ATmega and for some reason it only works reliably if the software is written the way you have. If I do 3 SPI transfers (value, value | E, value) I eventually end up with the LCD being shifted off in that the last nibble of the previous byte write is being interpreted with the first nibble of the current byte write. I thought maybe you had seen the same behavior and perhaps adapted your code to fit. I think I’ll just follow your experience and ignore the datasheet for what actually works. Thanks!

  • sdb

    And you can use one of the currently unused outputs of the ‘565 to control the backlight of the LCD. (Might also need a transistor current amplifier and/or a current limiting resistor.) Too bad the board doesn’t have routing for that. (My LCD has a 16 pin DIL header, some have 16 pin SIL.)

  • Great Post. I have build this interface and is working!! Thanks. I would like to know if you can help me incorporate this algorhytm into real time(No RTOS)(No Waits/Delays) Thanks Again

    • The delays are pretty short here. If you need to eliminate them altogether, you can read D7 to detect the end of operation.

  • James

    Just a quick note to say thank you for the excellent article. Very clear and concise. Even though character LCD’s have become somewhat overshadowed by today’s numerous HDMI-output, wiz-bang-embedded-PC’s, they remain one of the most basic building blocks for someone experimenting with micros. Hope to see this article alive in years to come.

  • Nice explanation.

    thank you Oleg