Initialising a 4x20 LCD with I2C Interface - Page 1
Forum section: electronics / general

author: PerArdua
author_rank: Regular Contributor
timestamp: June 27, 2022, 01:01:21 pm
content: 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.

author: pqass
author_rank: Super Contributor
timestamp: June 27, 2022, 02:29:15 pm
content: 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: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 bitdata |= _BV(0); // backlight always ondata &= ~_BV(2); // RW in write modedata &= ~_BV(3); // keep EN low while DB4-7, and control bits settlespi_transfer(data);// toggle EN pin; high, then lowdata |= _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 commandslcdSend(0b00110000); _delay_ms(5); //RS=0; initializelcdSend(0b00110000); _delay_ms(5); //RS=0; initializelcdSend(0b00110000); _delay_ms(5); //RS=0; initialize; "Thrice the brinded cat hath mewed."lcdSend(0b00100000); _delay_ms(5); //RS=0; switch to 4-bit modelcdSend(0b00100000); //RS=0;lcdSend(0b10000000); _delay_ms(5); //RS=0; 5x8 chars, 2 lines/displaylcdSend(0b00000000); //RS=0;lcdSend(0b11000000); _delay_ms(5); //RS=0; display on, no cursorlcdClear();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 nibbledata <<= 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);

author: Andy Watson
author_rank: Super Contributor
timestamp: June 27, 2022, 02:37:37 pm
content: 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 performeditsinternal 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.

author: PerArdua
author_rank: Regular Contributor
timestamp: June 27, 2022, 02:58:39 pm
content: 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.

author: pqass
author_rank: Super Contributor
timestamp: June 27, 2022, 04:00:20 pm
content: Quote from: PerArdua on June 27, 2022, 02:58:39 pmYeah, 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?

author: Andy Watson
author_rank: Super Contributor
timestamp: June 27, 2022, 04:20:53 pm
content: Quote from: pqass on June 27, 2022, 04:00:20 pmGood 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.

author: PerArdua
author_rank: Regular Contributor
timestamp: June 27, 2022, 05:18:53 pm
content: 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: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:SYSTEM_Initialize();__delay_ms(2000);LCD_init(LCD_ADDRESS, LCD_COLS, LCD_ROWS);__delay_ms(1000);LCD_send_data(0x41); //Print AIf I change the LCD.c to have the display off:LCD.c:Code: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.cCode: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 AAnd nothing is displayed on the LCD. I try changing the send_data and send_cmd around to:main.cdQuoteAnd 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.cCode: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.

author: pqass
author_rank: Super Contributor
timestamp: June 27, 2022, 05:38:03 pm
content: Try the following instead in your main(). Just LCD_DISPLAYON is not the same (0b00000100 vs 0b00001100).Code:LCD_send_cmd(LCD_DISPLAYCONTROL | LCD_DISPLAYON | LCD_NOBACKLIGHT); // Display ONI don't know if the behaviour of LCD_DISPLAYOFF/LCD_DISPLAYON just causes no updates or if it ignores any commands in between.QuoteOr, maybe you mean to use this instead?Code:LCD_send_cmd(LCD_CLEARDISPLAY); // Clear displayBTW: I'd just declare a global backlight var and use that whenever a byte to the LCD is sent (and remove LCD_BACKLIGHT from most calls). And LCD_backlight() should just update that var and send a byte with at least LCD.EN=0x0.

author: PerArdua
author_rank: Regular Contributor
timestamp: June 27, 2022, 05:59:06 pm
content: I think that's the exact problem pqass - using:main.c:Code: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.