LCD Port Manipulation Problem

Hi All,

This is my first time posting to the forum, I apologize in advance if I mess up any rules/norms/formatting.

I emphasized in embedded systems during undergrad, but have been working as an analog engineer since graduating. I am pretty rusty and would like to get back to digital design. I figured before buying a PIC or ST32 and diving headfirst back into assembly, it would be easier and quicker to work with what I already have...an Arduino Uno, an HD44780 LCD, plus some various other components.

I am trying to refresh/challenge myself by using port manipulation and staying away from libraries, but I am stuck just trying to get the LCD set up. I was able to program the LiquidCrystal.h "Hello World!" code onto my Arduino+LCD no problem so I am confident that my circuit is correct. I adjusted the contrast so the example text was clearly visible. It must be an issue with my code or understanding of how to initialize the LCD.

I expected my code below to set up the LCD in 4-bit mode, 2 lines, 5x7 dots, display on, cursor on, blink on, print the character 'A' and auto increment the blinking cursor to the right. However, the LCD screen remains blank without even a single flicker. I thought it might be an issue with not delaying long enough but it doesn't seem to matter how long I set each of the delays. I confirmed the minimum timing requirements with the LCD datasheet and made sure to leave plenty of headroom in the delays used. I used a series of "Serial.println(PORTD, HEX)" statements throughout each of the functions to confirm PORTD was behaving as expected. I did the same for the bytes "lowerNibble" and "upperNibble" and the logic seems to be fine.

Does anyone have any ideas what I could be doing wrong with my code?

Circuit Diagram and Arduino code file are attached.

Thank you!

Adam

System: Arduino Uno connected to MacBook Pro 16" 2019 running OSX 10.15.17 using an HD44780 LCD screen.

LCD Circuit.png

LCD Circuit.png

RYOLCD.ino (10.2 KB)

First off. Test your hardware schematic with proven Arduino code.

Your code looks about right. You have some unnecessary long delays.

I suggest that you print your code on paper. Then trace execution compared with the HD44780 datasheet.
Make sure that your signals operate in the correct timing sequence e.g. from datasheet

Seriously. A few pennies for paper, printer ink, tea and a biscuit is well worth your time (and your head hurting).

David.

Using an "I2C LCD interface module" (with PCF8574) would make it a lot simpler... :wink:

The OP is doing a perfectly sensible academic exercise. i.e. how to follow a datasheet to implement LCD functions.
Using an "I2C LCD interface module" (with PCF8574) is considerably harder.

I would start with straight Port Manipulation.
When successful, you can implement each pin wiggle via Wire.h commands on a PCF8574.

For most users it is wise to use proven LCD libraries. Then they can concentrate on the program logic for their application.

David.

I don't see the initial power-on delay which belongs before the reset sequence.

One way to simplify things while you are trying to get your program working the first time is to stick that initial delay (I use 100 mS) in setup and then after that use a 10 mS delay everywhere else.

I see that you did include the missing delay at the end of the reset sequence and that you did remember to turn the display back on after the initialization. Both of these are missing from the data sheet flowchart. I suspect you may have stumbled across my 'LCD Initialization' page.

Don

I don't see the initial power-on delay which belongs before the reset sequence.

Which is why I suggested that the OP compares his code with the datasheet.

Yes, there is a power-on delay for the voltage to rise and the HD44780 to start.
Note that some real-life power supplies might be slower especially if they have massive electrolytic capacitors.

I would allow a generous 100ms. And work with 50us per lcd_command() and lcd_data() instead of the "typical" 37us. Also 2000us for CLR and HOME.

The initialisation timing is important. But there is no point in superfluous 5000us delays during normal operation.

David.

I was able to get it working!!! :grinning:

David, I really like your idea to print out code and trace it.

Erik_Baas, I2C is somewhere in my near future. While I am familiar with the protocol, I am looking to incorporate it into some future projects to get better at it. Thank you for the part recommendation.

Don, I actually hadn't seen your LCD Initialization page (assuming you are the Alfred State page?). For some reason my browser marks your page as "Not Secure" so I passed over it in my initial research. However, I took a second look and found what I was missing...I skipped over step 5! I didn't realize 0x2 was the last step in resetting the LCD. I was sending three 0x3 commands to reset it, then immediately jumping into the 0x28 function set so my LCD was not properly reset before receiving that first 0x28 instruction.

With that finally working, I was able to cut out a lot of the unnecessary delays I had. As for the power-on delay, I have that as step 2 of my LCDreset() function. I took your advice and bumped it up to 100ms.

Thank you all for your help and prompt replies!

Adam

I was able to get it working!!!

than pls share your working code!

I spent some time experimentally determining the shortest delays I could get before the code stopped working properly and this is the fastest I could write it:

void setup()
{
  // LCD Setup
  DDRD |= B11111100;                    // Initialize digital pins 7-2 as outputs for the LCD screen, leave 1,0 unchanged as they are used for serial/programming
  delay(100);                           // Wait >40ms for Led Vcc to rise to the correct voltage
  LCDinit4bit();                        // Initialize LCD in 4-bit mode
}

void loop()
{
  
}

void LCDinit4bit()          // Initialize the LCD screen in 4-bit mode
{
  LCDreset();               // 1. Reset LCD into 4-bit mode
  LCD4BitWriteCMD(0x28);    // 2. Configure "Function Set" Register (0x28)  : 4 bit mode, 2 lines, and 5x7 dots
  LCD4BitWriteCMD(0x08);    // 3. Turn off all "Display Control" (0x08)     : Display, cursor, and blink all off
  LCD4BitWriteCMD(0x01);    // 4. "Display clear" command (0x01)            : Clears the display and returns the cursor to address 0
  LCD4BitWriteCMD(0x06);    // 5. Configure "Entry Mode" Register           : Sets auto increment cursor and disables display shift
  LCD4BitWriteCMD(0x0F);    // 6. Configure "Display Control" Register      : Enable screen, cursor, and blink
  LCD4BitWriteDAT(0x41);    // Write 'A' for testing purpose
}

void LCDreset()             // Reset LCD into 4-bit mode. Only needs to be called once at start up. 
{ 
  // Data sheet calls this initialization by instruction. Send 0x3 three times, then 0x2 to reset into 4-bit mode.
  PORTD &= B00000011;       // 01. Clear D7-D4, En, RS (digital pins 7-2, respectively) to 0 as a neutral starting point, leave pins 1,0 unchanged
  PORTD |= B00111000;       // 02. Write upper nibble 0x3, En=1
  PORTD &= B11110111;       // 03. En=0. Command is sent on trailing edge of enable
  delay(5);                 // 04. Wait >4.1ms for command to process
  PORTD |= B00111000;       // 05. Write upper nibble 0x3, En=1
  PORTD &= B11110111;       // 06. En=0. Command is sent on trailing edge of enable
  delay(0.5);               // 07. Wait >100us for command to process
  PORTD |= B00111000;       // 08. Write upper nibble 0x3, En = 1
  PORTD &= B11110111;       // 09. En=0. Command is sent on trailing edge of enable
  delay(1);                 // 10. Wait >100us for command to process
  PORTD &= B00000011;       // 11. Clear PORTD to overwrite next instruction in buffer
  PORTD |= B00101000;       // 12. Write upper nibble 0x2, En = 1
  PORTD &= B11110111;       // 13. En=0. Command is sent on trailing edge of enable
  delay(1);                 // 14. Wait >100us for command to process
  // At this point LCD is reset and listening for 4-bit commands.
}

void LCD4BitWriteCMD(byte CMD)            // Write a command byte to the LCD one nibble at a time using 4 bit mode.
{
  byte upperNibble = CMD & 0xF0;          // 01. Mask upper nibble of CMD, clear lower nibble so result takes the form: 0xUUUU0000
  byte lowerNibble = (CMD & 0x0F) << 4;   // 02. Mask lower nibble of CMD, clear upper nibble then shift left 4 so result takes the form 0xLLLL0000
  PORTD &= B00000011;                     // 03. Clear LCD buffer, enable, and register select
  PORTD |= upperNibble;                   // 04. Write upperNibble to LCD buffer
  PORTD |= B00001000;                     // 05. En=1, RS = 0
  PORTD &= B11110111;                     // 06. En=0. Command is sent on trailing edge of enable
  delay(2);                               // 07. Wait >40us for command to process
  PORTD &= B00000011;                     // 08. Clear LCD buffer, enable, and register select
  PORTD |= lowerNibble;                   // 09. Write lowerNibble to LCD buffer
  PORTD |= B00001000;                     // 10. En=1, RS = 0
  PORTD &= B11110111;                     // 11. En=0. Command is sent on trailing edge of enable
  delay(2);                               // 12. Wait >40us for command to process
}


void LCD4BitWriteDAT(byte DAT)            // Write a data byte to the LCD one nibble at a time using 4 bit mode.
{
  byte upperNibble = DAT & 0xF0;          // 01. Mask upper nibble of DAT, clear lower nibble so result takes the form: 0xUUUU0000
  byte lowerNibble = (DAT & 0x0F) << 4;   // 02. Mask lower nibble of DAT, clear upper nibble then shift left 4 so result takes the form 0xLLLL0000
  PORTD &= B00000011;                     // 03. Clear LCD buffer, enable, and register select 
  PORTD |= upperNibble;                   // 04. Write upperNibble to LCD buffer
  PORTD |= B00001100;                     // 05. En=1, RS = 1
  PORTD &= B11110111;                     // 06. En=0. Command is sent on trailing edge of enable
  delay(2);                               // 07. Wait >40us for command to process
  PORTD &= B00000011;                     // 08. Clear LCD buffer, enable, and register select
  PORTD |= lowerNibble;                   // 09. Write lowerNibble to LCD buffer
  PORTD |= B00001100;                     // 10. En=1, RS = 1
  PORTD &= B11110111;                     // 11. En=0. Command is sent on trailing edge of enable
  delay(2);                               // 12. Wait >40us for command to process
}

Arduino has a delayMicroseconds() function. But actually you can use _delay_us() for an AVR.

Don't use Trial and Error. Read the datasheet. A safety margin is wise e.g. 50us instead of 37us. You are currently using 4000us.

David.

Thank you for the info. I figured delay(0.5) would be exactly equivalent to delayMicroseconds(500)? But I guess not because certain parts of the code that didn’t work with delay(0.5) now work with _delay_us(150) which is a big improvement.

Although I still have longer than expected delays writing commands and data. The datasheet claims 37us to write to DDRAM, but experimentally, I can’t get it to work with anything less than 1600us and I can’t find an explanation for that in the datasheet

void LCDreset()             // Reset LCD into 4-bit mode. Only needs to be called once at start up. 
{ 
  // Data sheet calls this initialization by instruction. Send 0x3 three times, then 0x2 to reset into 4-bit mode.
  PORTD &= B00000011;       // 01. Clear D7-D4, En, RS (digital pins 7-2, respectively) to 0 as a neutral starting point, leave pins 1,0 unchanged
  PORTD |= B00111000;       // 02. Write upper nibble 0x3, En=1
  PORTD &= B11110111;       // 03. En=0. Command is sent on trailing edge of enable
  _delay_ms(5);             // 04. Wait >4.1ms for command to process
  PORTD |= B00111000;       // 05. Write upper nibble 0x3, En=1
  PORTD &= B11110111;       // 06. En=0. Command is sent on trailing edge of enable
  _delay_us(150);           // 07. Wait >100us for command to process
  PORTD |= B00111000;       // 08. Write upper nibble 0x3, En = 1
  PORTD &= B11110111;       // 09. En=0. Command is sent on trailing edge of enable
  _delay_us(150);           // 10. Wait >100us for command to process
  PORTD &= B00000011;       // 11. Clear PORTD to overwrite next instruction in buffer
  PORTD |= B00101000;       // 12. Write upper nibble 0x2, En = 1
  PORTD &= B11110111;       // 13. En=0. Command is sent on trailing edge of enable
  _delay_us(150);           // 14. Wait >100us for command to process
  // At this point LCD is reset and listening for 4-bit commands.
}

void LCD4BitWriteCMD(byte CMD)            // Write a command byte to the LCD one nibble at a time using 4 bit mode.
{
  byte upperNibble = CMD & 0xF0;          // 01. Mask upper nibble of CMD, clear lower nibble so result takes the form: 0xUUUU0000
  byte lowerNibble = (CMD & 0x0F) << 4;   // 02. Mask lower nibble of CMD, clear upper nibble then shift left 4 so result takes the form 0xLLLL0000
  PORTD &= B00000011;                     // 03. Clear LCD buffer, enable, and register select
  PORTD |= upperNibble;                   // 04. Write upperNibble to LCD buffer
  PORTD |= B00001000;                     // 05. En=1, RS = 0
  PORTD &= B11110111;                     // 06. En=0. Command is sent on trailing edge of enable
  _delay_us(1600);                        // 07. Wait >40us for command to process
  PORTD &= B00000011;                     // 08. Clear LCD buffer, enable, and register select
  PORTD |= lowerNibble;                   // 09. Write lowerNibble to LCD buffer
  PORTD |= B00001000;                     // 10. En=1, RS = 0
  PORTD &= B11110111;                     // 11. En=0. Command is sent on trailing edge of enable
  _delay_us(1600);                        // 12. Wait >40us for command to process
}


void LCD4BitWriteDAT(byte DAT)            // Write a data byte to the LCD one nibble at a time using 4 bit mode.
{
  byte upperNibble = DAT & 0xF0;          // 01. Mask upper nibble of DAT, clear lower nibble so result takes the form: 0xUUUU0000
  byte lowerNibble = (DAT & 0x0F) << 4;   // 02. Mask lower nibble of DAT, clear upper nibble then shift left 4 so result takes the form 0xLLLL0000
  PORTD &= B00000011;                     // 03. Clear LCD buffer, enable, and register select 
  PORTD |= upperNibble;                   // 04. Write upperNibble to LCD buffer
  PORTD |= B00001100;                     // 05. En=1, RS = 1
  PORTD &= B11110111;                     // 06. En=0. Command is sent on trailing edge of enable
  _delay_us(1600);                        // 07. Wait >40us for command to process
  PORTD &= B00000011;                     // 08. Clear LCD buffer, enable, and register select
  PORTD |= lowerNibble;                   // 09. Write lowerNibble to LCD buffer
  PORTD |= B00001100;                     // 10. En=1, RS = 1
  PORTD &= B11110111;                     // 11. En=0. Command is sent on trailing edge of enable
  _delay_us(1600);                        // 12. Wait >40us for command to process
}

LCD Circuit.png
Disconnect the connection from the potentiometer to 5 V - pin 2. This is a silly error that has been perpetuated by people mindlessly copying earlier errors. :roll_eyes:

The LCD module has effectively, internal pull-downs on pins 7 to 10. There is no harm in connecting them to ground, but it is not actually necessary.