Go Down

Topic: LCD3Wire: a 3-wire driver for HD44780-based LCDs (Read 5 times) previous topic - next topic

dhenry

I was talking in a separate spread with someone else and as a result of that conversation decided to convert LiquidCrystal to a 3-wire solution.

LiquidCrystal requires anywhere between 6 lines (enable, rs, and d4..7). Sometimes, that can be difficult for a board with limited pins. Thus, the 3-wire solution.

The 3-wire solution implemented here is wholly based on LiquidCrystal, including the user interface and calling convention / syntax. It utilizes a shift register (HC164 or HC595) to shift the data pins (D0..7) to the shift register (Q0..7).

Connection:
MOSI pin: from the mcu to shift register's data pin (pin1+2 on HC164 and pin 14 on HC595). LCD's RS pin tied to this line as well.
SCK pin: from the mcu to shift register's serial clock pin (pin 8 on HC164 and pin 11 on HC595)
Enable pin: from the mcu to the lcd.

Advantages over other 3-wire solutions:
1) It allows 8-bit as well as 4-bit mode.
2) It uses no parts other than the shift register.
3) It allows HC164 as well as HC595.

You will need two files:

lcd3wire.h:
Code: [Select]

//header file to drive HD44780 using a shift register in a 3-wire connection
//based on LiquidCrystal
/*The 3-wire solution implemented here is wholly based on LiquidCrystal, including the user interface and calling convention / syntax. It utilizes a shift register (HC164 or HC595) to shift the data pins (D0..7) to the shift register (Q0..7).

Connection:
MOSI pin: from the mcu to shift register's data pin (pin1+2 on HC164 and pin 14 on HC595). LCD's RS pin tied to this line as well.
SCK pin: from the mcu to shift register's serial clock pin (pin 8 on HC164 and pin 11 on HC595)
Enable pin: from the mcu to the lcd.

Advantages over other 3-wire solutions:
1) It allows 8-bit as well as 4-bit mode.
2) It uses no parts other than the shift register.
3) It allows HC164 as well as HC595.

You will need two files: lcd3wire.h and lcd3wire.cpp
*/

#ifndef LiquidCrystal_h
#define LiquidCrystal_h

#include <inttypes.h>
#include "Print.h"

// commands
#define LCD_CLEARDISPLAY 0x01
#define LCD_RETURNHOME 0x02
#define LCD_ENTRYMODESET 0x04
#define LCD_DISPLAYCONTROL 0x08
#define LCD_CURSORSHIFT 0x10
#define LCD_FUNCTIONSET 0x20
#define LCD_SETCGRAMADDR 0x40
#define LCD_SETDDRAMADDR 0x80

// flags for display entry mode
#define LCD_ENTRYRIGHT 0x00
#define LCD_ENTRYLEFT 0x02
#define LCD_ENTRYSHIFTINCREMENT 0x01
#define LCD_ENTRYSHIFTDECREMENT 0x00

// flags for display on/off control
#define LCD_DISPLAYON 0x04
#define LCD_DISPLAYOFF 0x00
#define LCD_CURSORON 0x02
#define LCD_CURSOROFF 0x00
#define LCD_BLINKON 0x01
#define LCD_BLINKOFF 0x00

// flags for display/cursor shift
#define LCD_DISPLAYMOVE 0x08
#define LCD_CURSORMOVE 0x00
#define LCD_MOVERIGHT 0x04
#define LCD_MOVELEFT 0x00

// flags for function set
#define LCD_8BITMODE 0x10
#define LCD_4BITMODE 0x00
#define LCD_2LINE 0x08
#define LCD_1LINE 0x00
#define LCD_5x10DOTS 0x04
#define LCD_5x8DOTS 0x00

class LiquidCrystal : public Print {
public:
  LiquidCrystal(uint8_t mosi, uint8_t sck, uint8_t enable);
  void init(uint8_t fourbitmode, uint8_t mosi, uint8_t sck, uint8_t enable);

  void begin(uint8_t cols, uint8_t rows, uint8_t charsize = LCD_5x8DOTS);

  void clear();
  void home();

  void noDisplay();
  void display();
  void noBlink();
  void blink();
  void noCursor();
  void cursor();
  void scrollDisplayLeft();
  void scrollDisplayRight();
  void leftToRight();
  void rightToLeft();
  void autoscroll();
  void noAutoscroll();

  void createChar(uint8_t, uint8_t[]);
  void setCursor(uint8_t, uint8_t);
  virtual size_t write(uint8_t);
  void command(uint8_t);
 
  using Print::write;
private:
  void send(uint8_t, uint8_t);
  void write4bits(uint8_t);
  void write8bits(uint8_t);
  void pulseEnable();

  uint8_t _rs_pin; // LOW: command.  HIGH: character.
  //uint8_t _rw_pin; // LOW: write to LCD.  HIGH: read from LCD.
  uint8_t _enable_pin; // activated by a HIGH pulse.
  //uint8_t _data_pins[8];
  uint8_t _sck_pin; //SR's sck pin. data on rising edge
#define _mosi_pin _rs_pin //mosi pin share the same connection with rs pin

  uint8_t _displayfunction;
  uint8_t _displaycontrol;
  uint8_t _displaymode;

  uint8_t _initialized;

  uint8_t _numlines,_currline;
};

#endif


dhenry

The 2nd file you will need is lcd3wire.cpp:

Code: [Select]

#include "lcd3wire.h"

#include <stdio.h>
#include <string.h>
#include <inttypes.h>
#include "Arduino.h"

// When the display powers up, it is configured as follows:
//
// 1. Display clear
// 2. Function set:
//    DL = 1; 8-bit interface data
//    N = 0; 1-line display
//    F = 0; 5x8 dot character font
// 3. Display on/off control:
//    D = 0; Display off
//    C = 0; Cursor off
//    B = 0; Blinking off
// 4. Entry mode set:
//    I/D = 1; Increment by 1
//    S = 0; No shift
//
// Note, however, that resetting the Arduino doesn't reset the LCD, so we
// can't assume that its in that state when a sketch starts (and the
// LiquidCrystal constructor is called).

LiquidCrystal::LiquidCrystal(uint8_t mosi,  uint8_t sck, uint8_t enable)
{
  init(0, mosi, sck, enable);
}

void LiquidCrystal::init(uint8_t fourbitmode, uint8_t mosi, uint8_t sck, uint8_t enable)
{
  _mosi_pin = mosi;
  _sck_pin = sck;
  _enable_pin = enable;
 
  //_data_pins[0] = d0;
  //_data_pins[1] = d1;
  //_data_pins[2] = d2;
  //_data_pins[3] = d3;
  //_data_pins[4] = d4;
  //_data_pins[5] = d5;
  //_data_pins[6] = d6;
  //_data_pins[7] = d7;

  pinMode(_mosi_pin, OUTPUT);

  pinMode(_sck_pin, OUTPUT);
  // we can save 1 pin by not using RW. Indicate by passing 255 instead of pin#
  //if (_rw_pin != 255) {
  //  pinMode(_rw_pin, OUTPUT);
  //}
  pinMode(_enable_pin, OUTPUT);
 
  if (fourbitmode)
    _displayfunction = LCD_4BITMODE | LCD_1LINE | LCD_5x8DOTS;
  else
    _displayfunction = LCD_8BITMODE | LCD_1LINE | LCD_5x8DOTS;
 
  begin(16, 1); 
}

void LiquidCrystal::begin(uint8_t cols, uint8_t lines, uint8_t dotsize) {
  if (lines > 1) {
    _displayfunction |= LCD_2LINE;
  }
  _numlines = lines;
  _currline = 0;

  // for some 1 line displays you can select a 10 pixel high font
  if ((dotsize != 0) && (lines == 1)) {
    _displayfunction |= LCD_5x10DOTS;
  }

  // SEE PAGE 45/46 FOR INITIALIZATION SPECIFICATION!
  // according to datasheet, we need at least 40ms after power rises above 2.7V
  // before sending commands. Arduino can turn on way befer 4.5V so we'll wait 50
  delayMicroseconds(50000);
  // Now we pull both RS and R/W low to begin commands
  digitalWrite(_mosi_pin, LOW);
  digitalWrite(_sck_pin, LOW);
  digitalWrite(_enable_pin, LOW);
  //if (_rw_pin != 255) {
  //  digitalWrite(_rw_pin, LOW);
  //}
 
  //put the LCD into 4 bit or 8 bit mode
  if (! (_displayfunction & LCD_8BITMODE)) {
    // this is according to the hitachi HD44780 datasheet
    // figure 24, pg 46

    // we start in 8bit mode, try to set 4 bit mode
    write4bits(0x03);digitalWrite(_rs_pin, LOW); pulseEnable();
    delayMicroseconds(4500); // wait min 4.1ms

    // second try
    write4bits(0x03);    digitalWrite(_rs_pin, LOW); pulseEnable();
    delayMicroseconds(4500); // wait min 4.1ms
   
    // third go!
    write4bits(0x03);    digitalWrite(_rs_pin, LOW); pulseEnable();
    delayMicroseconds(150);

    // finally, set to 4-bit interface
    write4bits(0x02);    digitalWrite(_rs_pin, LOW); pulseEnable();
  } else {
    // this is according to the hitachi HD44780 datasheet
    // page 45 figure 23

    // Send function set command sequence
    command(LCD_FUNCTIONSET | _displayfunction);
    delayMicroseconds(4500);  // wait more than 4.1ms

    // second try
    command(LCD_FUNCTIONSET | _displayfunction);
    delayMicroseconds(150);

    // third go
    command(LCD_FUNCTIONSET | _displayfunction);
  }

  // finally, set # lines, font size, etc.
  command(LCD_FUNCTIONSET | _displayfunction); 

  // turn the display on with no cursor or blinking default
  _displaycontrol = LCD_DISPLAYON | LCD_CURSOROFF | LCD_BLINKOFF; 
  display();

  // clear it off
  clear();

  // Initialize to default text direction (for romance languages)
  _displaymode = LCD_ENTRYLEFT | LCD_ENTRYSHIFTDECREMENT;
  // set the entry mode
  command(LCD_ENTRYMODESET | _displaymode);

}

/********** high level commands, for the user! */
void LiquidCrystal::clear()
{
  command(LCD_CLEARDISPLAY);  // clear display, set cursor position to zero
  delayMicroseconds(2000);  // this command takes a long time!
}

void LiquidCrystal::home()
{
  command(LCD_RETURNHOME);  // set cursor position to zero
  delayMicroseconds(2000);  // this command takes a long time!
}

void LiquidCrystal::setCursor(uint8_t col, uint8_t row)
{
  int row_offsets[] = { 0x00, 0x40, 0x14, 0x54 };
  if ( row >= _numlines ) {
    row = _numlines-1;    // we count rows starting w/0
  }
 
  command(LCD_SETDDRAMADDR | (col + row_offsets[row]));
}

// Turn the display on/off (quickly)
void LiquidCrystal::noDisplay() {
  _displaycontrol &= ~LCD_DISPLAYON;
  command(LCD_DISPLAYCONTROL | _displaycontrol);
}
void LiquidCrystal::display() {
  _displaycontrol |= LCD_DISPLAYON;
  command(LCD_DISPLAYCONTROL | _displaycontrol);
}

// Turns the underline cursor on/off
void LiquidCrystal::noCursor() {
  _displaycontrol &= ~LCD_CURSORON;
  command(LCD_DISPLAYCONTROL | _displaycontrol);
}
void LiquidCrystal::cursor() {
  _displaycontrol |= LCD_CURSORON;
  command(LCD_DISPLAYCONTROL | _displaycontrol);
}

// Turn on and off the blinking cursor
void LiquidCrystal::noBlink() {
  _displaycontrol &= ~LCD_BLINKON;
  command(LCD_DISPLAYCONTROL | _displaycontrol);
}
void LiquidCrystal::blink() {
  _displaycontrol |= LCD_BLINKON;
  command(LCD_DISPLAYCONTROL | _displaycontrol);
}

// These commands scroll the display without changing the RAM
void LiquidCrystal::scrollDisplayLeft(void) {
  command(LCD_CURSORSHIFT | LCD_DISPLAYMOVE | LCD_MOVELEFT);
}
void LiquidCrystal::scrollDisplayRight(void) {
  command(LCD_CURSORSHIFT | LCD_DISPLAYMOVE | LCD_MOVERIGHT);
}

// This is for text that flows Left to Right
void LiquidCrystal::leftToRight(void) {
  _displaymode |= LCD_ENTRYLEFT;
  command(LCD_ENTRYMODESET | _displaymode);
}

// This is for text that flows Right to Left
void LiquidCrystal::rightToLeft(void) {
  _displaymode &= ~LCD_ENTRYLEFT;
  command(LCD_ENTRYMODESET | _displaymode);
}

// This will 'right justify' text from the cursor
void LiquidCrystal::autoscroll(void) {
  _displaymode |= LCD_ENTRYSHIFTINCREMENT;
  command(LCD_ENTRYMODESET | _displaymode);
}

// This will 'left justify' text from the cursor
void LiquidCrystal::noAutoscroll(void) {
  _displaymode &= ~LCD_ENTRYSHIFTINCREMENT;
  command(LCD_ENTRYMODESET | _displaymode);
}

// Allows us to fill the first 8 CGRAM locations
// with custom characters
void LiquidCrystal::createChar(uint8_t location, uint8_t charmap[]) {
  location &= 0x7; // we only have 8 locations 0-7
  command(LCD_SETCGRAMADDR | (location << 3));
  for (int i=0; i<8; i++) {
    write(charmap[i]);
  }
}

/*********** mid level commands, for sending data/cmds */

inline void LiquidCrystal::command(uint8_t value) {
  send(value, LOW);
}

inline size_t LiquidCrystal::write(uint8_t value) {
  send(value, HIGH);
  return 1; // assume sucess
}

/************ low level data pushing commands **********/

// write either command or data, with automatic 4/8-bit selection
void LiquidCrystal::send(uint8_t value, uint8_t mode) {
  //digitalWrite(_rs_pin, mode);

  // if there is a RW pin indicated, set it low to Write
  //if (_rw_pin != 255) {
  //  digitalWrite(_rw_pin, LOW);
  //}
 
  if (_displayfunction & LCD_8BITMODE) {
    write8bits(value);
  } else {
    write4bits(value>>4);
    digitalWrite(_rs_pin, mode); pulseEnable();
    write4bits(value);
  }
  digitalWrite(_rs_pin, mode); pulseEnable();
}

void LiquidCrystal::pulseEnable(void) {
  digitalWrite(_enable_pin, LOW);
  delayMicroseconds(1);   
  digitalWrite(_enable_pin, HIGH);
  delayMicroseconds(1);    // enable pulse must be >450ns
  digitalWrite(_enable_pin, LOW);
  delayMicroseconds(100);   // commands need > 37us to settle
}

void LiquidCrystal::write4bits(uint8_t value) {
  //for (int i = 0; i < 4; i++) {
  //  pinMode(_data_pins[i], OUTPUT);
  //  digitalWrite(_data_pins[i], (value >> i) & 0x01);
  //}
  shiftOut(_mosi_pin, _sck_pin, MSBFIRST, value<<4);

  //pulseEnable();
}

void LiquidCrystal::write8bits(uint8_t value) {
  //for (int i = 0; i < 8; i++) {
  //  pinMode(_data_pins[i], OUTPUT);
  //  digitalWrite(_data_pins[i], (value >> i) & 0x01);
  //}
  shiftOut(_mosi_pin, _sck_pin, MSBFIRST, value);
 
  //pulseEnable();
}


dhenry

Here is the user file - a copy from the LiquidCrystal demo, with minor modifications:

Code: [Select]

// include the library code:
#include "lcd3wire.h"

// initialize the library with the numbers of the interface pins
LiquidCrystal lcd(9, 8, 10);  //mosi = pin9, sck = pin8, enable = pin10

void setup() {
  // set up the LCD's number of columns and rows:
  lcd.begin(16, 2);
  // Print a message to the LCD.
  lcd.print("hello, world!");
}

void loop() {
  // set the cursor to column 0, line 1
  // (note: line 1 is the second row, since counting begins with 0):
  lcd.setCursor(0, 1);
  // print the number of seconds since reset:
  lcd.print(millis()/1000);
}


As you can see, the change is limited to the declaration - since we need only 3 pins.

dhenry

Some performance figures / notes:
1) each frame (16-char) line is transmitted in 10ms / 4bit and 5ms / 8bit, on a 16MIPS avr.
2) with a minor modification (commenting out a line), you can half that time.
3) LiquidCrystal utilizes the wrong initialization timing, and other oddities. So you have the ability to speed it up substantially. I made none of those corrections, just so you can drive any LCDs LiquidCrystal can drive.
4) the clk's period is 15us, with a high pulse of 4us. So it should work on pretty much any shift register (most shift registers are good for a 1us pulse width at least).

Disclaimer: Not extensively tested. So use at your own risk.

bperrybap

Try running the attached sketch with your library.
It will measure the byte transfer time as well as produce a FPS and an independent 16x2 FPS time
so that numbers can be compared regardless of LCD geometry.
Below are are some numbers that compare the original Arduino LiquidCrystal
library with the speeds of of using fm's new LiquidCrystal library replacement library
with different interfaces.

Code: [Select]
16Mhz AVR
Interface    ByteXfer    16x2FPS      Ftime
----------------------------------------------
4BIT          338uS        86.92     11.51ms (orignal Liquid Crystal)
4BIT           98uS       298.58      3.35ms
SR1W          312uS        94.14     10.62ms
SR2W           76uS       388.62      2.57ms
SR_2W          72uS       406.90      2.46ms
SR_3W          61uS       480.03      2.08ms
SR3W          102uS       287.92      3.47ms
i2c           957uS        30.74      32.25ms (pcf8574 @ 100kbit/sec)


80Mhz Pic32 (ChipKit Uno32)
Interface    ByteXfer    16x2FPS      Ftime
----------------------------------------------
4BIT          232uS       126.73      7.89ms (orignal mpide Liquid Crystal)
4BIT           57uS       517.41      1.93ms
SR2W           53uS       557.35      1.79ms
SR_2W          53uS       554.66      1.80ms
SR_3W          50uS       591.40      1.69ms
SR3W           56uS       524.91      1.91ms



--- bill


Go Up