PIC18 Explorer Board LCD Code With SPI Library

by Miguel on January 26, 2013

in PIC,PIC18 Explorer Board

PIC 18 explorer board LCD displaying characters PIC 18 explorer board LCD displaying characters

Two years ago when I was getting started with PIC Micros I wrote a tutorial on how to drive the LCD display of the PIC18 explorer board using some code that I had found online, now that I am far more comfortable with the SPI protocol and PICs I have rewritten that tutorial here but with my own code this time. This version is much shorter, understandable, and portable because it uses the MC18’s compiler SPI library, enjoy.

Full LCD With SPI Example Code:

Am going to begin this tutorial by first giving you all the code you need, I will explain each line below. Before you upload this code to your board please note that it was tested on the C18 compiler version 3.44, this code does not work on version 3.45 as of 01/25/2013, the compiler says that one of the SPI’s library functions is not defined which after hours of analyzing include files I believe it’s a bug on Microchip’s part.

#include <p18f8722.h>
#include <spi.h>
#include <delays.h>

// this is our chip select (CS) pin according to our pic18 explorer board's connections
#define CS PORTAbits.RA2
// addresses from MCP23S17's datasheet, think of the IODIR as TRIS and GPIO as PORT for the MCP23S17 (no the PIC micro)
#define IODIRA_ADDRESS 0x00
#define IODIRB_ADDRESS 0x01
#define GPIOA_ADDRESS 0x12
#define GPIOB_ADDRESS 0x13
// configuration bits
#pragma config OSC = HS         // Oscillator Selection bits (HS oscillator)
#pragma config FCMEN = OFF      // Fail-Safe Clock Monitor Enable bit (Fail-Safe Clock Monitor disabled)
#pragma config WDT = OFF        // Watchdog Timer (WDT disabled (control is placed on the SWDTEN bit))

void setIODIR(char, char);
void setGPIO(char, char);
void lcdCommand(char);
void lcdChar(unsigned char);
void lcdGoTo(char);
void lcdWriteString(rom unsigned char*);

void main(){
    TRISAbits.RA2=0; // our chip select pin needs to be an output so that we can toggle it
    CS=1; // set CS pin to high, meaning we are sending any information to the MCP23S17 chip

    // configure SPI: the MCP23S17 chip's max frequency is 10MHz, let's use 10MHz/64 (Note FOSC=10Mhz, our external oscillator)
    OpenSPI1(SPI_FOSC_64, MODE_10, SMPEND); // frequency, master-slave mode, sampling type
    // set LCD pins DB0-DB7 as outputs
    setIODIR(IODIRB_ADDRESS,0x00);
    // set RS and E LCD pins as outputs
    setIODIR(IODIRA_ADDRESS,0x00);
    // RS=0, E=0
    setGPIO(IODIRA_ADDRESS,0x00);
    // Function set: 8 bit, 2 lines, 5x8
    lcdCommand(0b00111111);
    // Cursor or Display Shift
    lcdCommand(0b00001111);
    // clear display
    lcdCommand(0b00000001);
    // entry mode
    lcdCommand(0b00000110);
    // send characters
    lcdWriteString("AllAboutEE.com"); // using the string function
    lcdGoTo(0x40); // go to line two
    lcdChar('S'); // using the single character function
    lcdChar('P');
    lcdChar('I');
    lcdChar(' ');
    lcdChar('L');
    lcdChar('i');
    lcdChar('b');
    lcdChar('r');
    lcdChar('a');
    lcdChar('r');
    lcdChar('y');

    while(1){

    }
}

/*
 * used to set the values of the ports ( think of it as when you use a PORT register)
 */
void setGPIO(char address, char value){
    CS=0; // we are about to initiate transmission
    // pins A2,A1 and A0 of the MCP23S17 chip are equal to 0 because they are grounded
    // we are just going to be writing so R/W=0 also
    WriteSPI1(0x40);    // write command 0b0100[A2][A1][A0][R/W] = 0b01000000 = 0x40
    WriteSPI1(address); // select register by providing address
    WriteSPI1(value);    // set value
    CS=1; // we are ending the transmission
}

/*
 * used to set the directions of the ports (like when you use TRIS registers)
 * this function is actually identical to setGPIO, but I think a different
 * for setting the port directio helps with keeping the code organized
 */
void setIODIR(char address, char dir){
    CS=0;
    WriteSPI1(0x40);    // write command (0b0100[A2][A1][A0][R/W]) also equal to 0x40
    WriteSPI1(address); // select IODIRB
    WriteSPI1(dir);    // set direction
    CS=1;
}

/*
 * used to send commands and settings information
 */
void lcdCommand(char command){
    setGPIO(GPIOA_ADDRESS,0x00); // E=0
    Delay10TCYx(0);
    setGPIO(GPIOB_ADDRESS, command); // send data
    Delay10TCYx(0);
    setGPIO(GPIOA_ADDRESS,0x40); // E=1
    Delay10TCYx(0);
    setGPIO(GPIOA_ADDRESS,0x00); // E=0
    Delay10TCYx(0);
}

/*
 * prints out a character to the lcd display
 */
void lcdChar(unsigned char letter){
    setGPIO(GPIOA_ADDRESS,0x80); // RS=1, we going to send data to be displayed
    Delay10TCYx(0); // let things settle down
    setGPIO(GPIOB_ADDRESS,letter); // send display character
    // Now we need to toggle the enable pin (EN) for the display to take effect
    setGPIO(GPIOA_ADDRESS, 0xc0); // RS=1, EN=1
    Delay10TCYx(0); // let things settle down, this time just needs to be long enough for the chip to detect it as high
    setGPIO(GPIOA_ADDRESS,0x00); // RS=0, EN=0 // this completes the enable pin toggle
    Delay10TCYx(0);
}

/*
 * the parameter is the position of the cursor according to the HD44780 specs
 * for the lcd display our board has the top row's position range is 01 to
 */
void lcdGoTo(char pos){
    // add 0x80 to be able to use HD44780 position convention
    lcdCommand(0x80+pos);
}

void lcdWriteString(rom unsigned char *s){
    while(*s)
    lcdChar(*s++);
}

Circuit Diagram

Before we move to the software it’s very important that you understand the hardware. I have cut out the connections between the PIC18F8722, the MCP23S17 SPI I/O expander, and the LCD display from the board’s schematic page, they are below for your convenience.

pic18f8722, spi chip, and lcd schematic connections pic18f8722, spi chip, and lcd schematic taken from board’s datasheet

How The MCP23S17 16 Bit I/O Expander Works

The MCP23S17 is an input/output expander chip, this just means that with this chip you are able to add more input or output pins to your circuits and because it uses the SPI protocol you are able to control them with very few pins. In the case of the MCP23S17 you are able to control 16 pins for input or output with only 4 pins from your PIC. However, the LCD only requires 10 pins: 8 for the data ( D0 to D07) and the enable (E), and register select (RS) pins.

In order to control the MCP23S17 you first have to specify if you want to write to or read from it. We will just be writing to it, the next step once you have selected the write command is to select the register you want to write to by sending the I/O expander chip the register’s address. Lastly, you send the value you want to give to that register.

This is how we’ll modify the contents of the IODIR (Input Output DIRection) registers

void setIODIR(char address, char dir){
    CS=0; // let MCP23S17 chip know we are about to start sending data to it
    WriteSPI1(0x40);    // write command (0b0100[A2][A1][A0][R/W]) also equal to 0x40
    WriteSPI1(address); // select IODIRB
    WriteSPI1(dir);    // set direction
    CS=1; // let MCP23S17 chip know we are done sending data to it
}

This is how we’ll modify the contents of the GPIO register. The code in both functions setIODIR and setGPIO is actually the same, I just thought it would be best to creat meaning full names. You can even use either function to modify the contents of other registers listed in the datasheet.

void setGPIO(char address, char value){
    CS=0; // we are about to initiate transmission
    // pins A2,A1 and A0 of the MCP23S17 chip are equal to 0 because they are grounded
    // we are just going to be writing so R/W=0 also
    WriteSPI1(0x40);    // write command 0b0100[A2][A1][A0][R/W] = 0b01000000 = 0x40
    WriteSPI1(address); // select register by providing address
    WriteSPI1(value);    // set value
    CS=1; // we are ending the transmission
}

All the MCP23S17’s register’s are shown in the figure below. We will only be working with the IODIRB, IODIRA, GPIOA, and GPIOB registers. The IODIR register’s are used to set the direction of the GPIO pins (input or output) and GPIO registers are used to set the value of the pins (On or Off), think of then as TRIS and PORT respectively.

MCP23S17 register address table MCP23S17 register address table

Am going to create a few constants from the table for the registers’ address am going to be using so that my code makes more sense.

// addresses from MCP23S17's datasheet, think of the IODIR as TRIS and GPIO as PORT for the MCP23S17 (no the PIC micro)
#define IODIRA_ADDRESS 0x00
#define IODIRB_ADDRESS 0x01
#define GPIOA_ADDRESS 0x12
#define GPIOB_ADDRESS 0x13

How LCD Displays Work

Very simply: LCD displays take either commands or data to display. Each time you send a command or data you need to toggle the enable pin (E) to confirm.

To send a command to the display you pull the RS line to 0 and send the respective command bits (listed in the datasheet). Commands include clearing the screen, positioning the cursor, and how cursor behaves.

This is the function I made to send commands to the LCD. The delay times are actually arbitrary, they just have to be “large enough” for the chip to detect the change in the value of the pins.

void lcdCommand(char command){
    setGPIO(GPIOA_ADDRESS,0x00); // RS=0, E=0
    Delay10TCYx(0);
    setGPIO(GPIOB_ADDRESS, command); // send data
    Delay10TCYx(0);
    setGPIO(GPIOA_ADDRESS,0x40); // RS=0, E=1
    Delay10TCYx(0);
    setGPIO(GPIOA_ADDRESS,0x00); // RS=0, E=0
    Delay10TCYx(0);
}

To send a character to be displayed you pull the RS line to 1 and follow with the ASCII number of the character you want to show, you can also just send a char type and not worry about having to know the ASCII number.

This is the function I made to send display characters to the LCD. Notice that this is the same as the previous function except that this time the RS bit, which is bit 7, is now equal to 1.

void lcdChar(unsigned char letter){
    // 0b10000000=0x80
    setGPIO(GPIOA_ADDRESS,0x80); // RS=1, we going to send data to be displayed
    Delay10TCYx(0); // let things settle down
    setGPIO(GPIOB_ADDRESS,letter); // send display character
    // Now we need to toggle the enable pin (EN) for the display to take effect
    setGPIO(GPIOA_ADDRESS, 0xc0); // RS=1, EN=1
    Delay10TCYx(0); // let things settle down, this time just needs to be long enough for the chip to detect it as high
    setGPIO(GPIOA_ADDRESS,0x00); // RS=0, EN=0 // this completes the enable pin toggle
    Delay10TCYx(0);
}

I could continue and write a whole article on this subject, however someone already has written what I think is the best tutorial on how to use LCD displays: HD44780 Character LCD Displays – Part 1

Either way, I will explain what goes on with the LCD but if there is something you don’t understand refer to this link. Here is also a list of the LCD command you can use.

HD44780 lcd commands HD44780 LCD display commands

How The SPI Protocol Works

This is the part that seems to confuse everyone that has had problems working with the display in the PIC18 board. Am going to summarize the SPI protocol for you.

The SPI protocol consists of 4 pins SCK (clock), CS (chip select), Master-Out-Slave-In (MOSI) or simply data out, Master-In-Slave-Out (MISO) or simply data in.

The basic steps to drive a SPI chip, in our case the I/O expander, are the following:

  1. Pull the CS line to 0 to let the SPI chip you are going to send it some data
  2. Send the data: this includes the commands i.e. read or write, as well as the register address and its value in the case of the I/O expander.
  3. Pull the CS line to 1 to let the SPI chip know we are ending the transmission of data

In the code these steps refer to the following lines

    CS=0; // we are about to initiate transmission
    // pins A2,A1 and A0 of the MCP23S17 chip are equal to 0 because they are grounded
    // we are just going to be writing so R/W=0 also
    WriteSPI1(0x40);    // write command 0b0100[A2][A1][A0][R/W] = 0b01000000 = 0x40
    WriteSPI1(address); // select register by providing address
    WriteSPI1(value);    // set value
    CS=1; // we are ending the transmission

Of course there is some set up you have to do such as specify the frequency at which you want to drive the SPI chip, as well as the master-slave mode and how you want to sample the data. All this stuff however is taken care off by SPI library which lets us select these values with just a few constants we can choose from. In the code we set up the SPI chip using the OpenSPI1 function

    // configure SPI: the MCP23S17 chip's max frequency is 10MHz, let's use 10MHz/64 (Note FOSC=10Mhz, our external oscillator)
    OpenSPI1(SPI_FOSC_64, MODE_10, SMPEND); // frequency, master-slave mode, sampling type

Bonus

Q: Why do SPI library functions all end with a “1”?
Ans: Because the pic18f8722 chip has two SPI ports, we are using SPI1 because the pins of SPI port 1 are the ones that are connected to the MCP23S17 chip.

Note: some chips do not have two SPI ports, in that case you can leave the “1” out of the function’s name, in fact leaving the “1” works in either case because there is a #define which replaces OpenSPI with OpenSPI1 for example.

Previous post:

Next post: