Author Topic: Initialising a 4x20 LCD with I2C Interface  (Read 1217 times)

0 Members and 1 Guest are viewing this topic.

Offline PerArduaTopic starter

  • Regular Contributor
  • *
  • Posts: 52
  • Country: gb
Initialising a 4x20 LCD with I2C Interface
« on: June 27, 2022, 01:01:21 pm »
Hi, in order to learn about LCDs and PICs at a lower abstraction level, I'm trying to write my own library for controlling a 4x20 LCD. However, I am falling at the first hurdle - initialising the LCD and printing a single character. My hardware consists of a PIC24FJ256GA412, which is connected through level shifters to a PCF8574 which is connected to my LCD in 4 bit configuration. As discussed later, I am fairly content that the hardware works.

For the software, I have used MCC to create an I2C library, and written a header and implementation file called lcd.h/lcd.c. These, along with the rest of the project files, I have attached as a zip. The main file should simply initialise the LCD, and print a letter A on the LCD. The initialisation procedure, I have tried to follow from page 46 of the datasheet (https://www.sparkfun.com/datasheets/LCD/HD44780.pdf). Forgive me for the less than polished code, I am quite new and have sprinkled a few Nops() and generous delays to aid my previous debugging.

However, on running the setup, the LCD fails to show a character - just a blank screen. I have played with the contrast, but it is not that at fault. I have probed the I2C lines at the input of the PCF8574 board, and the data transfer looks as I'd expect. I've written out the PCF8574 connections to I2C bits in the file titled LCD Plan.ods, and shared the measurements in the file LCD debug data 4.ods. This leads me to suspect that the issue lies in how I think the LCD should be initialised/used, rather than my implementation. However, after trying a variety of potential solutions (clearing the LCD after init, tunring LCD display on after init, etc), I still don't see a character.

Would anyone be able to review my code/LCD plan and suggest where I may be going wrong? If you'd like to give feedback on my more general programming, so i can make code more legible/easier to understand, that would be appreciated too.
 

Offline pqass

  • Frequent Contributor
  • **
  • Posts: 725
  • Country: ca
Re: Initialising a 4x20 LCD with I2C Interface
« Reply #1 on: June 27, 2022, 02:29:15 pm »
Since I see your lower nibble from bits 0 to 3 are attached to LCD pins RS, RW, EN, BACKLIGHT, I take it that you're going to use 4 bit LCD mode.  But you haven't switched the LCD to that mode in your LCD_init() function!  There is a three byte incantation that needs to be performed first.  Oh, that's the "LCD_CURSORSHIFT | LCD_FUNCTIONSET" three times call.  However, there is a difference with my init sequence (see below) since I send two separate LCD_FUNCTIONSET commands (after the first three) before the "LCD_2LINE | LCD_5x8DOTS" command.  Oh, nevermind.  After the fourth call (sending just LCD_FUNCTIONSET), then you switch to using LCD_send_cmd() which send two bytes for every command.

See below for my minimalist LCD functions (I use SPI to a '595 then LCD). 

I wait 75ms after reset/powerup. You have just 20ms.  Confirm that your delays are consistant with what I've programmed below.

In LCD_I2C_send(), there should be a delay (50us) after the second byte sent.  2ms delay after the first byte is a bit overkill.  In LCD_send_cmd() and LCD_send_data(), why aren't you reusing LCD_I2C_send() vs. calling I2C routines directly?  Only the RS bit is different between the two, right? This is unnecessary duplication.

Why the Nop() calls? Alignment? Surely not for delay.

Code: [Select]
void lcdSend(uint8_t data) {
  // HD44780 display is attached to a 74HC595
  // bit0=backlight(1=on; 0=off), bit1=RS, bit2=RW, bit3=EN, bits4-7=DB4-7
  // below, we override the lower nibble except for the RS bit

  data |=  _BV(0);  // backlight always on
  data &= ~_BV(2);  // RW in write mode
  data &= ~_BV(3);  // keep EN low while DB4-7, and control bits settle

  spi_transfer(data);

  // toggle EN pin; high, then low
  data |=  _BV(3);  spi_transfer(data);  _delay_us(1);
  data &= ~_BV(3);  spi_transfer(data);  _delay_us(50);
}

void lcdClear() {
  lcdSend(0b00000000); //RS=0;
  lcdSend(0b00010000); //RS=0; clear display
  _delay_ms(5);
}

void lcdHome() {
  lcdSend(0b00000000); //RS=0;
  lcdSend(0b00100000); //RS=0; return home
  _delay_ms(5);
}

void lcdInit() {
  spi_setup();         _delay_ms(75); //min. after start before sending commands
  lcdSend(0b00110000); _delay_ms(5);  //RS=0; initialize
  lcdSend(0b00110000); _delay_ms(5);  //RS=0; initialize
  lcdSend(0b00110000); _delay_ms(5);  //RS=0; initialize; "Thrice the brinded cat hath mewed."
  lcdSend(0b00100000); _delay_ms(5);  //RS=0; switch to 4-bit mode
  lcdSend(0b00100000);                //RS=0;
  lcdSend(0b10000000); _delay_ms(5);  //RS=0; 5x8 chars, 2 lines/display
  lcdSend(0b00000000);                //RS=0;
  lcdSend(0b11000000); _delay_ms(5);  //RS=0; display on, no cursor
  lcdClear();
  lcdHome();
}

void setCursor(uint8_t col, uint8_t row) {
  uint8_t row_offsets[] = { 0x00, 0x40, 0x14, 0x54 };
  uint8_t data = 0b10000000|((col&0x0f)+row_offsets[row&0x03]);
  lcdSend((data&0xf0)|0b0000); //RS=0;
  data <<= 4;
  lcdSend((data&0xf0)|0b0000); //RS=0; setddramaddr
}

void lcdChar(uint8_t data) {
  lcdSend((data&0xf0)|0b0010); //RS=1; high nibble
  data <<= 4;
  lcdSend((data&0xf0)|0b0010); //RS=1; low nibble
}

void lcdString(char *datap) {
  while (*datap != '\0')
    lcdChar(*datap++);
}


// somewhere in your main():

  lcdInit();
  lcdString("FOOBAR v1.0");
  _delay_ms(1000);

« Last Edit: June 27, 2022, 04:14:49 pm by pqass »
 
The following users thanked this post: PerArdua

Online Andy Watson

  • Super Contributor
  • ***
  • Posts: 2084
Re: Initialising a 4x20 LCD with I2C Interface
« Reply #2 on: June 27, 2022, 02:37:37 pm »
I'm not seeing the code where you initialise the display! Could you just post the display initialisation code?

Also, Can I draw your attention to page 46 of the Hitiachi document that you linked. It gives the initialisation sequence. Note that (particularly in four bit mode) you do not know which phase of the byte-writing cycle you are in when you start the reset sequence, therefore you must follow the sequence given on page 46.  In particular note that the BF flag cannot be checked until the display has performed its internal initialisation - i.e. those delays between writing commands must be present in your software - I usually doulbe the delay times to account for clones that do not meet Hitachi's spec.

 
The following users thanked this post: PerArdua

Offline PerArduaTopic starter

  • Regular Contributor
  • *
  • Posts: 52
  • Country: gb
Re: Initialising a 4x20 LCD with I2C Interface
« Reply #3 on: June 27, 2022, 02:58:39 pm »
Thanks both.

Andy, I've attached three files (main.c, LCD.h and LCD.c) for review - the init code is from line 13 of LCD.c. Thanks for your comments - hopefully you will be able to see I've given plenty of delay between instructions/bytes - perhaps even overkill.

pqass, many thanks for your comments too. Noted on the delays (I do have a 2s delay in main.c before init, but have changed the first delay in init to 75ms). I will optimise the delays after bytes when I (hopefully!) can get this to work. I will also condense the send_cmd/data functions as you point out - thanks! Nops() are mostly only there to give a convenient line for me to place a breakpoint for the debugger. I will also remove those at a later stage, but they shouldn't affect the function/non-functionality of the LCD in this regard?

Forgive me, but I think I am performing the 3 byte incantation as per pg 46 - the 000011 (RS, RW, 7,6,5,4) byte is given by the three "LCD_CURSORSHIFT | LCD_FUNCTIONSET | LCD_NOBACKLIGHT" lines - or have I misused these? I have tried illustrating how I think the figure on pg 46 lines up with my plan.ods and the measured data on the I2C lines. Apologies for the messy red lines, but I hope it illustrates how I think they corroborate.

 

Offline pqass

  • Frequent Contributor
  • **
  • Posts: 725
  • Country: ca
Re: Initialising a 4x20 LCD with I2C Interface
« Reply #4 on: June 27, 2022, 04:00:20 pm »
Forgive me, but I think I am performing the 3 byte incantation as per pg 46 - the 000011 (RS, RW, 7,6,5,4) byte is given by the three "LCD_CURSORSHIFT | LCD_FUNCTIONSET | LCD_NOBACKLIGHT" lines - or have I misused these? I have tried illustrating how I think the figure on pg 46 lines up with my plan.ods and the measured data on the I2C lines. Apologies for the messy red lines, but I hope it illustrates how I think they corroborate.

Yeah, I now see you are doing the 3 byte incantation.  Then I suspected that after going into 4bit mode, you weren't sending two bytes per command. But then I noticed that you switched to using the LCD_send_cmd() call which does breakout lower and upper nibbles and sends them as two bytes.

However, your init code differs from mine in that I send a LCD_DISPLAYON command whereas you send LCD_DISPLAYOFF.  And therefore, nothing is displayed? 
 
The following users thanked this post: PerArdua

Online Andy Watson

  • Super Contributor
  • ***
  • Posts: 2084
Re: Initialising a 4x20 LCD with I2C Interface
« Reply #5 on: June 27, 2022, 04:20:53 pm »
However, your init code differs from mine in that I send a LCD_DISPLAYON command whereas you send LCD_DISPLAYOFF.

Good catch!

I've compared the initialisation code to some that I wrote to directly interface with a two line LCD - and I can't find a difference save for the DISPLAYOFF and longer time delays.

 
The following users thanked this post: PerArdua

Offline PerArduaTopic starter

  • Regular Contributor
  • *
  • Posts: 52
  • Country: gb
Re: Initialising a 4x20 LCD with I2C Interface
« Reply #6 on: June 27, 2022, 05:18:53 pm »
Thanks both - I've managed to get it to work when I replace that DISPLAYOFF with DISPLAYON! Great news! But now I'm kind of puzzled, as perhaps my understanding of this flag isn't correct.

This following code works:

LCD.c:
Code: [Select]
    LCD_send_cmd(LCD_FUNCTIONSET | LCD_2LINE | LCD_5x8DOTS | LCD_NOBACKLIGHT);  // First byte - Number of lines, Char size //maybe i shouldnt be using sendcmd here
    __delay_ms(10);
    LCD_send_cmd(LCD_DISPLAYCONTROL | LCD_DISPLAYON | LCD_NOBACKLIGHT);        // Display ON
    __delay_ms(10);
    LCD_send_cmd(LCD_CLEARDISPLAY);                                             // Clear display
    __delay_ms(10);

main.c:
Code: [Select]
    SYSTEM_Initialize();
    __delay_ms(2000);
 
    LCD_init(LCD_ADDRESS, LCD_COLS, LCD_ROWS);
    __delay_ms(1000);

    LCD_send_data(0x41); //Print A

If I change the LCD.c to have the display off:

LCD.c:
Code: [Select]
    LCD_send_cmd(LCD_FUNCTIONSET | LCD_2LINE | LCD_5x8DOTS | LCD_NOBACKLIGHT);  // First byte - Number of lines, Char size //maybe i shouldnt be using sendcmd here
    __delay_ms(10);
    LCD_send_cmd(LCD_DISPLAYCONTROL | LCD_DISPLAYOFF | LCD_NOBACKLIGHT);        // Display off
    __delay_ms(10);
    LCD_send_cmd(LCD_CLEARDISPLAY);       

and use the same main.c as above, then there's only a brief flicker of the backlight (as it's turned off and on in init), but no character displayed. Fine, that's because I've not turned the display on! So I change main.c to:

main.c
Code: [Select]
    SYSTEM_Initialize();
    __delay_ms(2000);
 
    LCD_init(LCD_ADDRESS, LCD_COLS, LCD_ROWS);
    __delay_ms(1000);
    LCD_send_cmd(LCD_DISPLAYON);
    __delay_ms(100);

    LCD_send_data(0x41); //Print A

And nothing is displayed on the LCD. I try changing the send_data and send_cmd around to:

main.cd
Quote
    SYSTEM_Initialize();
    __delay_ms(2000);
 
    LCD_init(LCD_ADDRESS, LCD_COLS, LCD_ROWS);
    __delay_ms(1000);
    LCD_send_data(0x41); //Print A
    __delay_ms(100);
    LCD_send_cmd(LCD_DISPLAYON);

And the backlight turns off (makes sense due to the hex value I'm sending for DISPLAY turns it off (due to I2c bit 3 being 0). When I turn the backlight on, as per:

main.c
Code: [Select]
    SYSTEM_Initialize();
    __delay_ms(2000);
 
    LCD_init(LCD_ADDRESS, LCD_COLS, LCD_ROWS);
    __delay_ms(1000);
    LCD_send_data(0x41); //Print A
    __delay_ms(100);
    LCD_send_cmd(LCD_DISPLAYON);
    __delay_ms(100);
    LCD_backlight(true);
Backlight behaves as expected, but again no character is displayed.

Am I completely misunderstanding how cleardisplay should be used? I've checked the hex value in lcd.h, and they look fine - but something about using LCD_DISPLAYOFF prevents the LCD from displaying any characters, even if I turn the display on (before or after sending data). Could this be a bug in a dodgy controller?

Appreciate all the help so far, thanks again.
 

Offline pqass

  • Frequent Contributor
  • **
  • Posts: 725
  • Country: ca
Re: Initialising a 4x20 LCD with I2C Interface
« Reply #7 on: June 27, 2022, 05:38:03 pm »
Try the following instead in your main().  Just LCD_DISPLAYON is not the same (0b00000100 vs 0b00001100).
Code: [Select]
LCD_send_cmd(LCD_DISPLAYCONTROL | LCD_DISPLAYON | LCD_NOBACKLIGHT);        // Display ON
I don't know if the behaviour of LCD_DISPLAYOFF/LCD_DISPLAYON just causes no updates or if it ignores any commands in between.

Quote
Am I completely misunderstanding how cleardisplay should be used?

Or, maybe you mean to use this instead?
Code: [Select]
    LCD_send_cmd(LCD_CLEARDISPLAY);                                             // Clear display
BTW: I'd just declare a global backlight var and use that whenever a byte to the LCD is sent (and remove LCD_[NO]BACKLIGHT from most calls).  And LCD_backlight() should just update that var and send a byte with at least LCD.EN=0x0.

« Last Edit: June 27, 2022, 05:48:44 pm by pqass »
 
The following users thanked this post: PerArdua

Offline PerArduaTopic starter

  • Regular Contributor
  • *
  • Posts: 52
  • Country: gb
Re: Initialising a 4x20 LCD with I2C Interface
« Reply #8 on: June 27, 2022, 05:59:06 pm »
I think that's the exact problem pqass - using:

main.c:
Code: [Select]
    SYSTEM_Initialize();
    __delay_ms(2000);
 
    LCD_init(LCD_ADDRESS, LCD_COLS, LCD_ROWS);
    __delay_ms(1000);
    LCD_send_data(0x41); //Print A
    __delay_ms(100);
    LCD_send_cmd(LCD_DISPLAYON|LCD_DISPLAYCONTROL);
    __delay_ms(100);
    LCD_backlight(true);

Works completely as expected with the original lcd.c - so it seems that the LCD_DISPLAYOFF/ON needs that LCD_DISPLAYCONTROL bit set, and it does accept at least data transfer with the display off (presumably commands too). I've learned a lot through this thread, thank you. :)
 


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf