George Cave / designedbycave.co.uk

☰ Menu

Chaining LCDs

The humble LCD screen is the staple display output in every electronics starter kit. They typically consist of a 16x2 character display, powered by a COB (chip-on-board) Hitachi HD44780U display driver. Sparkfun have a very cool video of this manufacturing process for these should you be curious. Despite higher resolution OLED SPI screens being quite ubiquituous, these displays still have a certain retro appeal to me.

I worked on a robotic arcade style toy recently that needed four of these screens to operate together. Since their biggest inconvenience is that each screen requires a minimum of 6 control pins, I first had to get them working in parallel to avoid running out of IO on the 32u4.

A single LCD display wired up for testing

Chaining LCD displays

Each display has an enable pin, four data lines and a control line for read/write. Only the enable line needs to be unique to the display, the other lines can be shared across all of the displays. The LiquidCrystal library will pull the enable line high when writing to a given display and the other displays will ignore the data if their enable line is not high.

This means additional displays can be chained together at a cost of only one (additional) pin per display. Here’s a breadboard wiring diagram for an Arduino Uno since its the easiest way to show what’s happening:

LCDs in series

As the picture below shows, it’s also possible to use a single trimpot to set display contrast for all displays at once. However, this doesn’t save any pins on the micro (which was the main objective) and I found it better to use one trimpot per display to get the best possible contrast ratio.

Two LCDs running from one trimpot on a breadboard

Control

My basic init() function for four displays looks like this:

// Change these Pin numbers as required.
#define PIN_LCD_RS    A4
#define PIN_LCD_DB4   A3
#define PIN_LCD_DB5   A2
#define PIN_LCD_DB6   A1
#define PIN_LCD_DB7   A0

#define PIN_LCD1_EN   28
#define PIN_LCD2_EN   30
#define PIN_LCD3_EN   32
#define PIN_LCD4_EN   34

LiquidCrystal lcd0(PIN_LCD_RS, PIN_LCD1_EN, PIN_LCD_DB4, PIN_LCD_DB5, PIN_LCD_DB6, PIN_LCD_DB7);
LiquidCrystal lcd1(PIN_LCD_RS, PIN_LCD2_EN, PIN_LCD_DB4, PIN_LCD_DB5, PIN_LCD_DB6, PIN_LCD_DB7);
LiquidCrystal lcd2(PIN_LCD_RS, PIN_LCD3_EN, PIN_LCD_DB4, PIN_LCD_DB5, PIN_LCD_DB6, PIN_LCD_DB7);
LiquidCrystal lcd3(PIN_LCD_RS, PIN_LCD4_EN, PIN_LCD_DB4, PIN_LCD_DB5, PIN_LCD_DB6, PIN_LCD_DB7);

void init_lcds(){
  lcd0.begin(16, 2);
  lcd1.begin(16, 2);
  lcd2.begin(16, 2);
  lcd3.begin(16, 2);
}

Since I found myself writing data to many displays at once, it became convenient to store an array of the LCDs. I use a struct to record the last string written to a display. This helps avoid any flicker caused by continually clearing and writing to the displays in a loop().

struct LCD_t{
    LiquidCrystal *lcd;
    String        curr_text;
};

LCD_t lcds[] = {
  {&lcd0, ""},
  {&lcd1, ""},
  {&lcd2, ""},
  {&lcd3, ""}
}; 

The basic function to write a line to the display is shown here. These displays look much better if the text is centered, so this function takes a boolean argument to determine whether to pad the start of the string with spaces.

void lcd_write_line(uint8_t index, uint8_t line, String str, bool is_centered){

  uint8_t col = 0;
  if((str.length() < 16)&&(is_centered > 0)){
    col = (16-str.length())/2;
  }
  
  lcds[index].lcd->setCursor(col, line);
  lcds[index].lcd->print(str);
}

Multi-line text

The print() call to the LCD has no inherent concept of the text wrapping from one line to the next. Since most of my display text is pre-prepared strings, I use a line break character '/' to manually inject a line break where I would like it. This just needs to be a character that you don’t expect to come up in your strings (lest you want to implement a parser to check for an escape character too).

I then choose not to call lcd_write_line() directly, but instead via a couple of helper functions:

const char L_BREAK = '/';

void lcd_set_text(String str, uint8_t index){

  lcd_set_text(str, index, true); // default is to centre

}

void lcd_set_text(String str, uint8_t index, bool is_centered){
 if(lcds[index].curr_text.compareTo(str) != 0){ // Evaluates to 0 if strings are equal

    lcds[index].lcd->clear();

    uint16_t brk_index = str.indexOf(L_BREAK);
    if(brk_index > 0){
      lcd_write_line(index,0,str.substring(0,brk_index),is_centered);
      lcd_write_line(index,1,str.substring(brk_index+1),is_centered);
    }else{
      lcd_write_line(index,0,str.substring(0,brk_index),is_centered);
    }
    lcds[index].curr_text = str;
  }
}

Finally, one last helper function to set all text simultaneously completes the set:

void lcd_set_all_text(String text0, String text1, String text2, String text3){
  lcd_set_text(text0,0);
  lcd_set_text(text1,1);
  lcd_set_text(text2,2);
  lcd_set_text(text3,3);
}

It’s also possible to implement custom characters inside the displays. There’s a great website from Mark Owen with a custom LCD character builder.

Better wiring

The wiring for multiple LCDs is straightforward but messy, so after testing them out with some stripboard I put together a tiny helper PCB to wrap the trimpot and connections into one. There’s a single EN line socket, plus pairs of sockets for PWR/GND and the 5 DATA lines.

Before and after

The board is very simple, just a few rows of sockets and one component:

Schematic Render

Here they are all wired together:

LCD chaining test LCD panels chained and installed in final panel