My own custom LCD driver doesn't work with Arduino Uno R3

I want to drive the LCD TC1602A with my own code without using the LiquidCrystal library. The lcd doesn't display any character and remains in its initial state (as shown in the image). I wrote the code by referring to the datasheet but couldn't find any errors. Please help me out here.

// My own driver to test the TC1602A LCD screen using Arduino Uno

// LCD register pins
const int lcd_rs = 3;
const int lcd_rw = 4;
const int lcd_en = 5;
const int lcd_d0 = 6;
const int lcd_d1 = 7;
const int lcd_d2 = 8;
const int lcd_d3 = 9;
const int lcd_d4 = 10;
const int lcd_d5 = 11;
const int lcd_d6 = 12;
const int lcd_d7 = 13;

void writeToLCD(unsigned char lcd_data) {
  if (lcd_data & 0x01 == 0x01) {
    digitalWrite(lcd_d0, HIGH);
  }
  else {
    digitalWrite(lcd_d0, LOW);
  }

  if (lcd_data & 0x02 == 0x02) {
    digitalWrite(lcd_d1, HIGH);
  }
  else {
    digitalWrite(lcd_d1, LOW);
  }

  if (lcd_data & 0x04 == 0x04) {
    digitalWrite(lcd_d2, HIGH);
  }
  else {
    digitalWrite(lcd_d2, LOW);
  }

  if (lcd_data & 0x08 == 0x08) {
    digitalWrite(lcd_d3, HIGH);
  }
  else {
    digitalWrite(lcd_d3, LOW);
  }

  if (lcd_data & 0x10 == 0x10) {
    digitalWrite(lcd_d4, HIGH);
  }
  else {
    digitalWrite(lcd_d4, LOW);
  }

  if (lcd_data & 0x20 == 0x20) {
    digitalWrite(lcd_d5, HIGH);
  }
  else {
    digitalWrite(lcd_d5, LOW);
  }

  if (lcd_data & 0x40 == 0x40) {
    digitalWrite(lcd_d6, HIGH);
  }
  else {
    digitalWrite(lcd_d6, LOW);
  }

  if (lcd_data & 0x80 == 0x80) {
    digitalWrite(lcd_d7, HIGH);
  }
  else {
    digitalWrite(lcd_d7, LOW);
  }
}

void sendToDataRegister (unsigned char lcd_data) {
  writeToLCD(lcd_data);
  
  // To write data to data register in lcd, RS is high and RW is low (check datasheet)
  digitalWrite(lcd_rs, HIGH);
  digitalWrite(lcd_rw, LOW);
  digitalWrite(lcd_en, HIGH);
  delay(2);
  digitalWrite(lcd_en, LOW);
}

void sendToInstructionRegister (unsigned char lcd_cmd) {
  // Function to execute various commands
  writeToLCD(lcd_cmd);
  
  digitalWrite(lcd_rs, LOW);
  digitalWrite(lcd_rw, LOW);
  digitalWrite(lcd_en, HIGH);
  delay(2);
  digitalWrite(lcd_en, LOW);
}

void writeString (unsigned char str[], int stringLength) {
  for (int i = 0; i < stringLength; i++) {
    sendToDataRegister(str[i]);
  }
}

void initializeLCD() {
  sendToInstructionRegister(0x0F);
  sendToInstructionRegister(0x38);  // Sets the display to 16 cols and 2 rows
  sendToInstructionRegister(0x0C);  // Turns on the display and turns off the cursor
  sendToInstructionRegister(0x06);  // Autoincrement the cursor
  sendToInstructionRegister(0x01);  // Clears the display
}

void setup() {
  // put your setup code here, to run once:
  pinMode(lcd_rs, OUTPUT);
  pinMode(lcd_rw, OUTPUT);
  pinMode(lcd_en, OUTPUT);
  pinMode(lcd_d0, OUTPUT);
  pinMode(lcd_d1, OUTPUT);
  pinMode(lcd_d2, OUTPUT);
  pinMode(lcd_d3, OUTPUT);
  pinMode(lcd_d4, OUTPUT);
  pinMode(lcd_d5, OUTPUT);
  pinMode(lcd_d6, OUTPUT);
  pinMode(lcd_d7, OUTPUT);
  initializeLCD();
Serial.begin(9600);
  
}

void loop() {
  // put your main code here, to run repeatedly:
  sendToInstructionRegister(0x80);  // Sets the cursor at 1st row 1st col 
  writeString("Hello",  5);
  sendToInstructionRegister(0xC0);
  delay(500);
}

LCD-Driver.ino (2.9 KB)

It looks about right. But you must allow 2ms for CLR and HOME commands. And probably some delays inside the initializeLCD() function.
Note that 2ms EN pulse is just crazy. The datasheet requires 0.0005ms.

It is always wise to test your wiring with a proven 8-bit library first. e.g. Fleury C code.

I don't think that there are many "Arduino" 8-bit libraries. After all, it is a complete waste of pins.

David.

@david_prentice,
Thanks for the fast reply!
Sorry, I should have mentioned this earlier. The hardware setup works fine with the LiquidCrystal library. The issue is in my library.
I made the changes and tested it right now. It still doesn't work.

// My own driver to test the TC1602A LCD screen using Arduino Uno

// LCD register pins
const int lcd_rs = 3;
const int lcd_rw = 4;
const int lcd_en = 5;
const int lcd_d0 = 6;
const int lcd_d1 = 7;
const int lcd_d2 = 8;
const int lcd_d3 = 9;
const int lcd_d4 = 10;
const int lcd_d5 = 11;
const int lcd_d6 = 12;
const int lcd_d7 = 13;

void writeToLCD(unsigned char lcd_data) {
  if (lcd_data & 0x01 == 0x01) {
    digitalWrite(lcd_d0, HIGH);
  }
  else {
    digitalWrite(lcd_d0, LOW);
  }

  if (lcd_data & 0x02 == 0x02) {
    digitalWrite(lcd_d1, HIGH);
  }
  else {
    digitalWrite(lcd_d1, LOW);
  }

  if (lcd_data & 0x04 == 0x04) {
    digitalWrite(lcd_d2, HIGH);
  }
  else {
    digitalWrite(lcd_d2, LOW);
  }

  if (lcd_data & 0x08 == 0x08) {
    digitalWrite(lcd_d3, HIGH);
  }
  else {
    digitalWrite(lcd_d3, LOW);
  }

  if (lcd_data & 0x10 == 0x10) {
    digitalWrite(lcd_d4, HIGH);
  }
  else {
    digitalWrite(lcd_d4, LOW);
  }

  if (lcd_data & 0x20 == 0x20) {
    digitalWrite(lcd_d5, HIGH);
  }
  else {
    digitalWrite(lcd_d5, LOW);
  }

  if (lcd_data & 0x40 == 0x40) {
    digitalWrite(lcd_d6, HIGH);
  }
  else {
    digitalWrite(lcd_d6, LOW);
  }

  if (lcd_data & 0x80 == 0x80) {
    digitalWrite(lcd_d7, HIGH);
  }
  else {
    digitalWrite(lcd_d7, LOW);
  }
}

void sendToDataRegister (unsigned char lcd_data) {
  writeToLCD(lcd_data);
  
  // To write data to data register in lcd, RS is high and RW is low (check datasheet)
  digitalWrite(lcd_rs, HIGH);
  digitalWrite(lcd_rw, LOW);
  digitalWrite(lcd_en, HIGH);
  delayMicroseconds(2);
  digitalWrite(lcd_en, LOW);
}

void sendToInstructionRegister (unsigned char lcd_cmd) {
  // Function to execute various commands
  writeToLCD(lcd_cmd);
  
  digitalWrite(lcd_rs, LOW);
  digitalWrite(lcd_rw, LOW);
  digitalWrite(lcd_en, HIGH);
  delayMicroseconds(2);
  digitalWrite(lcd_en, LOW);
}

void writeString (unsigned char str[], int stringLength) {
  for (int i = 0; i < stringLength; i++) {
    sendToDataRegister(str[i]);
  }
}

void initializeLCD() {
  sendToInstructionRegister(0x38);  // Sets the display to 16 cols and 2 rows
  delayMicroseconds(100);
  sendToInstructionRegister(0x0C);  // Turns on the display and turns off the cursor
  delayMicroseconds(100);
  sendToInstructionRegister(0x06);  // Autoincrement the cursor
  delayMicroseconds(100);
  sendToInstructionRegister(0x01);  // Clears the display
  delay(2);
  sendToInstructionRegister(0x02);  // Returns the cursor to its original position
}

void setup() {
  // put your setup code here, to run once:
  pinMode(lcd_rs, OUTPUT);
  pinMode(lcd_rw, OUTPUT);
  pinMode(lcd_en, OUTPUT);
  pinMode(lcd_d0, OUTPUT);
  pinMode(lcd_d1, OUTPUT);
  pinMode(lcd_d2, OUTPUT);
  pinMode(lcd_d3, OUTPUT);
  pinMode(lcd_d4, OUTPUT);
  pinMode(lcd_d5, OUTPUT);
  pinMode(lcd_d6, OUTPUT);
  pinMode(lcd_d7, OUTPUT);
  initializeLCD();
Serial.begin(9600);
  
}

void loop() {
  // put your main code here, to run repeatedly:
  sendToInstructionRegister(0x80);  // Sets the cursor at 1st row 1st col 
  writeString("Hello",  5);
  sendToInstructionRegister(0xC0);
  delay(500);
}

It doesn't help your main problem but there is some scope to make the code more compact.

// long version
 if (lcd_data & 0x01 == 0x01) {
    digitalWrite(lcd_d0, HIGH);
  }
  else {
    digitalWrite(lcd_d0, LOW);
  }

// short version
  digitalWrite(lcd_d0, lcd_data & 0x01 == 0x01 ? HIGH : LOW );

Incidentally, with driving the display with 8 bits instead of the usual 4, you are less likely to see the weird flashing and appearance of strange characters caused by a synchronisation error.

A simple Google search for 'LCD Programming Examples' should point you to this ancient material (written by an ancient programmer). LCD Programming Examples
Don
Edit: The Email address on those pages is no longer valid. Respond on this forum.

I'm quite curious about this because I have an audio application which benefits from a non-blocking version of the LCD1602 driver and this looks like it could be simple basis when you get it working.
I put the output through a logic analyser, which understands HD44780 protocol, and got this. There are also, incidentaly, a number of compiler warnings which should be looked at.

Time [s], Analyzer Name, Decoded Protocol Result
2.399939500000000,HD44780,Write Command 0b  0000  0000 ()
2.400088000000000,HD44780,Write Command 0b  0000  0000 ()
2.400236250000000,HD44780,Write Command 0b  0000  0000 ()
2.400385500000000,HD44780,Write Command 0b  1111  1111 (Set DDRAM Addr 0b  0111  1111)
2.402443250000000,HD44780,Write Command 0b  0000  0000 ()
2.402497500000000,HD44780,Write Command 0b  0000  0000 ()
2.402547250000000,HD44780,Write Data 0b  0000  0000 (Data 'NUL')
2.402598000000000,HD44780,Write Data 0b  1111  1111 (Data)
2.402647750000000,HD44780,Write Data 0b  0000  0000 (Data 'NUL')
2.402697500000000,HD44780,Write Data 0b  0000  0000 (Data 'NUL')
2.402748250000000,HD44780,Write Data 0b  1111  1111 (Data)
2.402798000000000,HD44780,Write Command 0b  0000  0000 ()
2.902882500000000,HD44780,Write Command 0b  0000  0000 ()
2.902932250000000,HD44780,Write Data 0b  0000  0000 (Data 'NUL')
2.902983000000000,HD44780,Write Data 0b  1111  1111 (Data)
2.903032750000000,HD44780,Write Data 0b  0000  0000 (Data 'NUL')
2.903082500000000,HD44780,Write Data 0b  0000  0000 (Data 'NUL')
2.903133250000000,HD44780,Write Data 0b  1111  1111 (Data)
2.903183000000000,HD44780,Write Command 0b  0000  0000 ()
3.403268750000000,HD44780,Write Command 0b  0000  0000 ()

Your "Hello" appears (if I interpret it correctly) to be coming through as 0x00 0xFF 0x00 0x00 0xFF

EDIT

It looks better if you clear the operator precedence problem:

  if ( (lcd_data & 0x01) == 0x01) {  //  '==' is stronger than '&'
    digitalWrite(lcd_d0, HIGH);
  }
  else {
    digitalWrite(lcd_d0, LOW);
  }

If you can make it work with any library, look up how they did it

With just the operator precedence issue cleared (for all 8 occurrences)

this is how it now appears:

Time [s], Analyzer Name, Decoded Protocol Result
2.933312750000000,HD44780,Write Command 0b  0011  1000 (Function Set 8-bit; 2 Lines; 5x8)
2.933462500000000,HD44780,Write Command 0b  0000  1100 (Display On; Cursor Off; Blink Off)
2.933612000000000,HD44780,Write Command 0b  0000  0110 (Entry Mode Set Increment; No Shift)
2.933761500000000,HD44780,Write Command 0b  0000  0001 (Clear Display)
2.935820250000000,HD44780,Write Command 0b  0000  0010 (Return Home)
3.436266000000000,HD44780,Write Command 0b  1000  0000 (Set DDRAM Addr 0b  0000  0000)
3.436316750000000,HD44780,Write Data 0b  0100  1000 (Data 'H')
3.436368250000000,HD44780,Write Data 0b  0110  0101 (Data 'e')
3.436419500000000,HD44780,Write Data 0b  0110  1100 (Data 'l')
3.436470750000000,HD44780,Write Data 0b  0110  1100 (Data 'l')
3.436522500000000,HD44780,Write Data 0b  0110  1111 (Data 'o')
3.436573500000000,HD44780,Write Command 0b  1100  0000 (Set DDRAM Addr 0b  0100  0000)
3.936659250000000,HD44780,Write Command 0b  1000  0000 (Set DDRAM Addr 0b  0000  0000)
3.936710250000000,HD44780,Write Data 0b  0100  1000 (Data 'H')
3.936761500000000,HD44780,Write Data 0b  0110  0101 (Data 'e')
3.936819000000000,HD44780,Write Data 0b  0110  1100 (Data 'l')
3.936870250000000,HD44780,Write Data 0b  0110  1100 (Data 'l')
3.936921750000000,HD44780,Write Data 0b  0110  1111 (Data 'o')

@6v6gt
Thanks a lot for this. It works now.

@floresta
Thank you for this. I've been looking for something like this since the last two days.

@6v6gt
May I know how do I get the logic analyzer for myself?

@killzone_kid
I did take a look at the LiquidCrystal library but it didn't work then until @6v6gt pointed out the operator precedence problem. I'll be careful about this next time.

Or just eliminate the potential precedence issue possibility all together.
i.e. There is no need to actually compare the bit masking result.
testing for non zero is enough.

  if (lcd_data & 0x01) { 
    digitalWrite(lcd_d0, HIGH);
  }
  else {
    digitalWrite(lcd_d0, LOW);
  }

or

digitalWrite(lcd_d0, (lcd_data & 0x01) ? HIGH : LOW );

--- bill

I bought one like this for myself and it works great using the free PulseView software.

Edit: the PulseView version that I have does not have a decoder for the hd44780.

Your problem highlights the benefits of a 16 channel logic analyser. Under normal circumstances, 8 channels is enough.
Mine looks like this example: https://www.ebay.com/itm/363228673866

Just ordered one

I love mine. It is so easy to use and the decoders are awesome. I put my scope on the shelf and haven't use the scope for some time.

I almost regret now buying scope earlier, this seems easier to use for what I need

For, probably, 90% of Arduino stuff a logic analyzer is all you need. Very little analog stuff for which a scope is necessary. Speaking for myself, only, of course.

Just a little note.

You have the contrast potentiometer connected to 5 V. Do not connect the 10k contrast potentiometer to 5 V - just leave that end unconnected (or tie it to the wiper, or if using a 10k potentiometer, tie it to the other end)!

This is just a very silly mistake that has become ingrained in hobby (and no doubt, some professional) designs since the "early days". Not connecting it to 5 V makes contrast setting twice as easy! Using a 1k pot makes it even easier. If using a 10k pot, you can connect both ends to ground.

Correcting this mistake also saves you something like 500 µA which considering the actual LCD without the backlight otherwise draws only about 1 mA, is significant if you should propose battery operation. :grin: