Friday, November 29, 2013

Salvaged H-P printer LCD panel (CM160240) on Arduino using Custom Characters

This is a black & white LCD panel that came out of an H-P printer.

I didn't want to mess with it until I knew that the 44780 LCD would work.

It was mounted to a printed circuit board that had momentary contact switches at various points below the buttons.  On one end, there was a thin, flat cable that had something like 26 pins going out of it.  But the cable that came out to the LCD had only 14 lines.
LCD panel with cable
Close-up view of cable
Cable with 0.1-inch proto board for scale
 Since the cable was at 1mm separation, it was too fine for my soldering ability.  That meant that for me to mess around with the LCD, I'd need some way to break out the cable pins so that I'd get back to 0.1" separation.

I took some digging, searching on the wrong term ("ribbon cable") but I eventually found that the cable is referred to as an FPC (flexible printed circuit) or FFC (flat flex cable / flat flexible cable).

It turned out that there was a nice set of breakout boards available at Newhaven (google "Newhaven ffc adapter"), but they were around $10 each, not including shipping.

Instead, it turned out that Jameco had a little piece that fit a 14-pin, 1mm FFC, for only $0.39 each:
http://www.jameco.com/webapp/wcs/stores/servlet/Product_10001_10001_2144876_-1
The datasheet is at http://www.jameco.com/Jameco/Products/ProdDS/2144876.pdf

I got a couple of those -- always get a backup in case the first one fails!  The way it's set up, it has output pins that are in two rows, seven pins each separated at 2mm, and the pins are offset.  The rows themselves are also 2mm apart.  Since I didn't have any means (yet) to build my own PCB and my own breakout board, nor a way to drill holes at 2mm separation, I bent the rows apart to get more work room, and soldered wires to the pins.  Then, I used heatshrink tubing (from Halted, www.halted.com) for insulation.

The top of the breakout board looks like this:

I filed the holes a little larger so that the wires could be pulled through.  Then, I cut the wires to length and soldered them to push-in headers.  The end result was that all the odd-numbered wires ended up on one side, and the even-numbered ones were on the other side.  The back side of the board is here.  The outermost pins of the headers were soldered just for stability.

Of course with my soldering (non-)skills, I had to check all solder joints for proper connectivity, and make sure I didn't accidentally create any solder bridges.

Then I connected the FFC to it, just to double-check and make sure I knew which pins were which.

But which wires were which?

When I first looked at the LCD on its mount, it wasn't clear what chip was behind it, nor which pins would do what.

My first hint was that it had 14 pins.  Thus I hoped it would obey the same rules as the 44780.

The second hint was a closer inspection of the PCB that held the contact switches.  This is a backlit view of the board with pins in the order 1..14 from left to right.
The thing that stood out was that pin 3 had a thicker trace, and I took that to mean it represented Ground.

The flipside of the board is shown here.  It shows that pin 2 is connected via a capacitor (labeled C1) back to pin 3 (ground), suggesting pin 2 was power.  When I put 5V to pin 2 and ground to pin 3, the board showed a row of black squares.


But still, that didn't tell me anything else about the rest of the LCD pins.  The enable, reset, read-write, contract, and data pins could be in any combination.

I removed the LCD from its plastic mounting to get a closer look.  This took some *very* careful prying.  In retrospect, I might have done well to have applied some heat to loosen up the glue that was keeping it stuck on.  One time, when I was removing the panel in this way, I generated enough static with the plastic housing that it cause a few images to light up on the display, and I was afraid I'd fried it.  So be careful.  I'm not sure if heat is even a good idea, as that might loosen the glue, but damage the panel.


I gently removed the inspection sticker that was on top of the PX16214 identifier, and once outside the mounting, I could also see the DATA IMAGE vendor name.  (Earlier, only the word "IMAGE" was visible.)  Note also on the image above that there are three black bars.  Each of those, I think, is simply a piece of standoff foam with adhesive on it, and that's what kept the LCD panel stuck onto the PCB.

That allowed me to google "data image lcd px16214 p184 s-11" which gave me a single, solitary hit:

LCD mit 14 Pins, aber scheinbar keinem HD44780-kompatiblen ...

www.mikrocontroller.net/topic/187292
Thankfully, I'd gotten a bit of German in high school, and that in combination with knowing how to look for some technical terms let me know that I was on the right path.  Google translate then to help.

The page pointed to this first:
http://www.tstonramp.com/~pddwebacc/cdm/CDM-16214.pdf
and the pins for that are the same as for the 44780, but in reverse order.

However, it still didn't agree with my expectation that pin 3 was ground, and pin 2 was Vcc.

Later, though, in the same mikrocontroller.net web page, it referred to
http://www.optologic.ch/data/CHARACTER_DISPLAYS/PDF/CM160240.PDF

On that page, it showed the pin-shifted positioning that I was after:
pin 3 = Vss (0V)
pin 2 = Vdd (+5V)
pin 1 = Vo (LCD Drive Voltage)
and then it loops around to
pin 14 = RS
pin 13 = RW
pin 12 = E
pins 11..4 = DB0..DB7, respectively

I changed my 44780 sketch to initialize with a 16x2 display, and wired everything together, and it worked -- mostly.  For some reason, doing

lcd.setCursor(0,0);
lcd.print("tankdemo");

would only render the "demo" part to the screen.

I don't know why, but I had to offset all setCursor commands by adding 4 in order for things to line up properly.

The other thing I noticed is that there is no separating pixel row between lines 0 and 1 on the screen, but there still is a dead pixel separating each column.

The end result wiring is a bit of a spaghetti mess.



After I got all that together, I modified the code to allow for some variation to the message displayed on line 0, and messed around with the tank image a bit since this black & white LCD would erase pixels much faster than the 44780.

Here's the resulting code.
#include <LiquidCrystal.h>

// According to CM160240.html
// The pins on the salvaged HP printer's 16x2 LCD display might be
// 1 V0
// 2 Vdd +5v
// 3 Vcc GND
// 4..11 are DB7..DB0, respectively
// 12 EN (H, HL Enable signal)
// 13 RW (H read, L write)
// 14 RS (H data, L command)

// The blue 44780 uses
// 1 = gnd
// 2 = Vcc
// 3 = pot 10k-20k for contrast
// 4 = RS
// 5 = RW
// 6 = EN
// 11 = D4
// 12 = D5
// 13 = D6
// 14 = D7
// Ref. http://www.hacktronics.com/Tutorials/arduino-character-lcd-tutorial.html
// and HD44780 wiring pages

// and I'd used
LiquidCrystal lcd(7, 8, 9, 10, 11, 12);
  // RS, EN, DB4, DB5, DB6, DB7
 
// so... I need to wire up pins 14 RS, 12 EN, 7 DB4,6 DB5,5 DB6,4 DB7
// and wire RW to ground
// and consider using 10-20k for contrast on pin 1 (optional?)
// and set 2 to 5v
// and set 3 to 0v
// so ard7 = bd14
//    ard8 = bd12
//    ard9 = bd7
//    ard10 = bd6
//    ard11 = bd5
//    ard12 = bd4
// gnd = bd3
// vcc = bd2
// gnd = RW = bd13


// Board
// 1 = gnd
// 2 = Vcc
// 3 = pot 10k-20k for contrast
// 4 = RS
// 5 = RW
// 6 = EN
// 11 = D4
// 12 = D5
// 13 = D6
// 14 = D7
// Ref. http://www.hacktronics.com/Tutorials/arduino-character-lcd-tutorial.html
// and HD44780 wiring pages

// RW has to be wired low to write, else it remains in "read" mode

byte sprite0[8];
byte sprite1[8];
byte sprite2[8];
byte sprite3[8];

#define XOFFSET 4
#define LCD_CHAR_WIDTH 16
void setup() {
  // set up the LCD's number of columns and rows:
  lcd.begin(LCD_CHAR_WIDTH, 2);
  lcd.setCursor(XOFFSET,1);
  for (int i=0; i<LCD_CHAR_WIDTH; i++) lcd.write('@');
  memset(sprite0,7,8);
  memset(sprite1,7,8);
  memset(sprite2,7,8);
  memset(sprite3,7,8);
  lcd.setCursor(XOFFSET,1);
//  Serial.begin(9600);
}

// Need eight 32-bit quantities that I can use for shifting bits around.
// The original tank image is in these values.

long tankImg[] = {
  ((long)B000000 << 10) | ((long) B000000 << 5) | B000000 // antenna tip zeroed on HP LCD
 ,((long)B010011 << 10) | ((long) B011110 << 5) | B000000 // turret top
 ,((long)B011111 << 10) | ((long) B011111 << 5) | B011110 // turret mid with barrel
 ,((long)B000111 << 10) | ((long) B011110 << 5) | B000000 // turret base
 ,((long)B001111 << 10) | ((long) B011111 << 5) | B010000 // tread top
 ,((long)B010000 << 10) | ((long) B000000 << 5) | B001000
 ,((long)B010000 << 10) | ((long) B000000 << 5) | B001000
 ,((long)B001111 << 10) | ((long) B011111 << 5) | B010000 // tread bottom, 24 pixels total in tread
};

// tankx is the bitwise position across the screen.
// tankcharx is the character-wise position, thus tankx / 5.
// It can be negative.
// At tankx zero, the tank is on the left of the screen
// so tankImg bytes are broken into four custom chars
// the fourth of which being blank bits
// At tankx one, the tank bits shift a bit to the right
// and if I'm clever, the treads are computed so they "rotate"
// And so on
// Because there are five bits horizontally per custom char
// and the tank treads go every other, I can repeat the original
// tank treads starting at even char positions

#define RESTART_X_POS -15

int tankx = RESTART_X_POS;
int tankcharx;
int tankchary = 1;
int treadx = 0;

#define BITS_PER_CHAR 5

void writeAt(int x, int y, byte b)
{
  if (x >= 0 && x < LCD_CHAR_WIDTH) {
    lcd.setCursor(XOFFSET+x,y);
    lcd.write(b);
  }
}

int msgID = 0;
char *msgs[] = {
   "Tank demo!"
  ,"H-P printer LCD"
  ,"CM160240"
  ,"Thanks to the"
  ,"guys in Germany"
  ,"who wrote up the"
  ,"pin assignments!"
//  1234567890123456
};
#define NUM_MSGS 7
long lastMsgTime = 0;

void loop() {
  if (millis() - lastMsgTime > 2000) {
    lastMsgTime = millis();
    int j;
   
    lcd.setCursor(XOFFSET,0);
    int blanks = (LCD_CHAR_WIDTH - strlen(msgs[msgID]) )  / 2;
    for (j=0; j<blanks; j++) lcd.write(' ');
    lcd.print(msgs[msgID]);
    for (   ; j < LCD_CHAR_WIDTH; j++) {
      lcd.write(' ');
    }
   
    msgID++;
    if (msgID >= NUM_MSGS) {
      msgID = 0;
    }
  }
 
  tankcharx = tankx / BITS_PER_CHAR;
//  Serial.print("tankx = ");
//  Serial.print(tankx);
//  Serial.print("  tankcharx = ");
//  Serial.println(tankcharx);
 
  // Initial rendition, no rotation of treads
  if ((tankx % BITS_PER_CHAR) == 0) {
    // Full shift is on, need to draw a blank where the tank last was
    writeAt(tankcharx-1, tankchary, ' ');
    // Draw the tank's custom characters
    writeAt(tankcharx,   tankchary, 0);
    writeAt(tankcharx+1, tankchary, 1);
    writeAt(tankcharx+2, tankchary, 2);
    writeAt(tankcharx+3, tankchary, 3);
  }
 
  // Compute the bits of the individual custom chars
  int shiftbits = (tankx % BITS_PER_CHAR);
//  Serial.print("shiftbits = ");
//  Serial.println(shiftbits);
  if (shiftbits < 0) { shiftbits += BITS_PER_CHAR; }
  for (int y=0 ; y<8; y++)
  {
    long lval = tankImg[y];
    switch (treadx) {
      case 0:
        switch (y) {
          case 4:
            lval ^= 0x2cb0; break;
          case 7:
            lval ^= 0x2490; break;
        }
        break;
      case 1:
        switch (y) {
          case 4:
            lval ^= 0x1240; break;
//            lval ^= 0x36d0; break; // This setting has fewer pixels on on top
          case 5:
            lval ^= 0x0008; break;
          case 6:
            lval ^= 0x4000; break;
          case 7:
            lval ^= 0x0920; break;
        }
        break;
      case 2:
        switch (y) {
          case 4:
            lval ^= 0x0920; break;
//            lval ^= 0x1b60; break; // This setting has fewer pixels on on top
          case 5:
            lval ^= 0x4000; break;
          case 6:
            lval ^= 0x0008; break;
          case 7:
            lval ^= 0x1240; break;
        }
        break;
    }
   
    long lshifted = lval << (BITS_PER_CHAR-shiftbits);
    sprite0[y] = (byte)((lshifted >> (3*BITS_PER_CHAR)) & B011111);
    sprite1[y] = (byte)((lshifted >> (2*BITS_PER_CHAR)) & B011111);
    sprite2[y] = (byte)((lshifted >> (1*BITS_PER_CHAR)) & B011111);
    sprite3[y] = (byte)((lshifted >> (0*BITS_PER_CHAR)) & B011111);
  } // end computation of the four custom characters' eight lines
 
  // Update the custom chars.  This causes them to be updated
  // immediately on the LCD screen.  I haven't tried rendering
  // in reverse order to see if that makes for any better or worse
  // animation effect.
  lcd.createChar(0, sprite0);
  lcd.createChar(1, sprite1);
  lcd.createChar(2, sprite2);
  lcd.createChar(3, sprite3);
 
  // Move the tank to the next position and reset to the left off screen
  // if it goes far enough off to the right.  Also keep the "tread"
  // offset ticking.
  ++tankx;
  if (tankx >= 5*LCD_CHAR_WIDTH+5) { tankx = RESTART_X_POS; }
 
  ++treadx;
  if (treadx == 3) { treadx = 0; }
 
  // A short delay before we move the tank again.
  // Slower delay = less blur, given the 44780 I have
  // is slow to erase the lit-up pixels.  I like delay(100) milliseconds.
  delay(100);
}



And hopefully blogspot will allow me to post a video here... cross yer fingers...




2 comments:

  1. Hi, i have repeated your code and it totally worked, many thanks for sharing it

    ReplyDelete
  2. Thanks for the great write up. I pulled one of these out of a printer recently and now it's time to put it to use in a project.

    ReplyDelete