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...




Thursday, November 28, 2013

LCD Panel (RT0802B-1, 44780) on Arduino using Custom Characters

My latest project has been to figure out how to interface an LCD with an Arduino Uno.  I was inspired to do this, because someone had left their LCD project in a parts bin at the Tech Shop in Menlo Park.  In fact, the board I got had an Arduino Mini Pro on it, along with the LCD panel and some other components, so that will be saved for some future project.

The board I got was fairly awfully wired and soldered.  All the wires were blue, and many of the solder joints fell apart as I handled it.  Originally, I thought I'd get the Arduino working and powered, but I ended up just cutting the leads to the LED panel and rewiring it.

The LED panel is the normal 8x2 display that most hobbyists use.  There are tons of pages out there that describe the pins, and the LiquidCrystal library works fine.

First, the panel.  This is a 14-pin LED with twelve pins marked on the left side, and two marked A and K.  This is the top view:
 and the bottom view:

On the back side you can see that the pins are marked 1,2 at the bottom and 13,14 at the top.  On the left, you see the RA and RK markings, which correspond to Anode and Cathode, respectively.

In addition, there's the RT0802B-1 VER2.0 designation on the back, which follows a 44780 pinout.

When I first got this on its original board, it was a tangled mess of wires.  So after taking it apart and cutting wires to a more manageable and consistent length, I wired them to a header that would be more prototype-compatible.  (The wires themselves were of a gauge that is too thin to hold properly in a solderless breadboard, too.)  Now I have a board that I can plug wires into, and the wires are in the order 1..14, RA, RK.

The first time I built this, I totally blew the wire ordering.  I got 1,2 set up properly, which is good because they represent ground and Vcc, but from then on, I swapped pairs, so 3,4 became 4,3 on the headers and so on.  The unit would show a bunch of squares, because it was getting power properly, but the rest of the system naturally would not work at all.  The basic lesson here is: don't use the same wire color for everything!  (Why are they still all blue?  I got it this way, and didn't want to risk damaging it by un-soldering and re-soldering.)

The next step was basic breadboarding, following examples online.  Here's the wired-up result:
(In the image above, you can see I had a mistake in the wiring of the power to the contrast resistor, but it worked anyway in this configuration.)

Following the wires, it ends up with
LCD1 = GND
LCD2 = Vcc
LCD3 = Contrast voltage
LCD4 = RS (reset), orange, Ard 7
LCD5 = RW (read/write, tied to ground)
LCD6 = EN (enable), yellow, Ard 8
LCD7 = DB4 (data 4), blue, Ard 9
LCD8 = DB5 (data 5), orange, Ard 10
LCD9 = DB6 (data 6), white, Ard 11
LCD10 = DB7 (data 7), yellow, Ard 12
LCD11,12,13,14 = floating
LCD RA = anode = Vcc
LCD RK = cathode = Ground

The sketch code would then simply import the LiquidCrystal library and set up the LCD object as:

LiquidCrystal lcd(7, 8, 9, 10, 11, 12);
  // RS, EN, DB4, DB5, DB6, DB7



From there, the basic sketch code worked.  I built a few other quick sketches that would scroll Jabberwocky across and up the screen.  I found that this LCD has a slow fade, so if you do something like text scrolling, you have to slow it down a lot (longer delay() calls between redraws).

Custom characters for an LCD


After that, I started messing with the custom character function.  The 44780 (as all these sites will tell you) allows you to store eight characters' worth of special images where you get to specify the 5x8 bit pattern.  For example, code like this would set up a special character in slot 1, set up some funky bits for it, and draw it in column 0 (first column), line 0 (first line) of the LCD.
#include <LiquidCrystal.h>


LiquidCrystal lcd(7, 8, 9, 10, 11, 12);
  // RS, EN, DB4, DB5, DB6, DB7


byte bits[] = {
  B00000
, B00100
, B01010
, B10001
, B11111
, B10001
, B01010
, B00100
};

void setup() {
  lcd.begin(8,2); // n chars wide, m lines high
  lcd.setCursor(0,0);
  lcd.createChar(1, bits);
  lcd.write((byte)1);
}

void loop() {
}



One thing that's funny is that the LiquidCrystal example code on the Arduino web site declares a byte array of eight bytes, but only specifies seven of them.

LCD Sprites

I found that assigning new values to a custom character's definition causes immediate screen updates, wherever that character already exists on the screen.  You could use the code above and draw eight of the same custom character on the screen (repeat this line eight times)
  lcd.write((byte)1);

but after that, you can change the bits of the custom character, and it'll cause all eight characters to change at once.  So, you can change what's inside the bits[] byte array, and repeatedly call lcd.createChar(1, bits), and cause some form of animation to occur.

So I updated my sketch to do that, just making a basic marquee line.  It would set a few of the bits on three of the lines, and then shift them around after a short delay.

Gutters

If you look at your LCD under magnification, you'll probably find that the bits of each character are not adjacent to each other.  For my RT0802B LCD, there's about (maybe precisely?) one pixel separation vertically and horizontally between lines and characters, respectively.

Tank

I took it upon myself, then, to write some code that would take advantage of the custom characters and the ability to update them in-place.  I chose to draw a little tank, similar to what I'd seen in ye olde tymes when playing Rescue Raiders!

Basically, I drew a tank on some graph paper, assuming I'd eat up three custom characters with it.  I'd shift it over a bit at a time until I'd shifted four times, after which I could repeat.  Since I'd be shifting bits out of the third custom character to a fourth position, I'd really end up using four custom characters.

Here's the original sketch on graph paper.


The code defines the tank using eight long integers, each providing more than enough room for the twenty bits that I'd really be using for four characters.

For the main loop, I'd have a tank "x" position in pixels.  I could then just take a modulo 5 of that value to find out how many bits I'd have to shift the image around.  Then, I split the 20-bit long ints into individual bytes, masked off the upper three bits (logical AND the value with B011111) and after assembling all four sets of eight bytes, I'd push the values into the LCD's custom character memory.

I'd also divide the tank x position by 5 to figure out which character location I'd draw at.  To avoid leaving trailing bits around, I'd wipe out the space to the left with a whitespace character, then draw custom chars 0, 1, 2, and 3.  Anything to the right would just get overwritten as the tank would move.

In action


Sorry for the sideways video!  Here you can see the tank moving along and see how the fade speed of pixels affects the result.


As the code evolved, I messed with the delay between loops and the imagery of the tank (initial version had every-other treads, but later revs had an on-on-off pattern for a better look).

What's next

After messing with the 44780, I went on to working on an LCD panel that I salvaged from an old H-P printer that I got via Freecycle.

The code

The resulting code is below.

Note: even though there's the gutter "pixel" to deal with, I left the code using 5 as the modulo value.  I could move it to 6 in various places in the code, and that would make it look like the tank was moving behind some kinds of bars, but I tried that and it messed up the treading presentation too much.

To do: I think I need to put some Arduino license attribution in here somewhere...

#include <LiquidCrystal.h>

LiquidCrystal lcd(7, 8, 9, 10, 11, 12);
  // RS, EN, DB4, DB5, DB6, DB7

// 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];



void setup() {
  // set up the LCD's number of columns and rows:
  lcd.begin(8, 2);
  lcd.setCursor(0,0);
  lcd.print("tankdemo");
  lcd.setCursor(0,1);
  lcd.print("@@@@@@@@");
  memset(sprite0,7,8);
  memset(sprite1,7,8);
  memset(sprite2,7,8);
  memset(sprite3,7,8);
  lcd.setCursor(0,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)B010000 << 10) | ((long) B000000 << 5) | B000000 // antenna tip
 ,((long)B010111 << 10) | ((long) B011110 << 5) | B000000 // turret top
 ,((long)B001111 << 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
int tankx = -15;
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 <= 7) {
    lcd.setCursor(x,y);
    lcd.write(b);
  }
}

void loop() {
  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 >= 45) { tankx = -15; }
 
  ++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(200);
}




Saturday, November 9, 2013

Light panel from 15" monitor

I'm building a light panel.

I looked at some examples on youtube where people used surface-mounted LED light strips, and am heading down that path.

Materials selection

How do I choose an LED light strip?

The parameters appear to be:
- 3528 vs 5050.  The numbers indicate LED size.  The 3528s are 3.5mm x 2.8mm, whereas the 5050s are 5mm x 5mm.  The 5mm LEDs reportedly emit about 3x the light compared to the 3528s.
- Double density.  You can get 3528s at 300 LEDs / meter, or 600 LEDs / meter.
- Color.  You can get cool white, warm white, pure white, and various colors include RGB combinations.
- Addressable vs. non-addressable.  The addressable ones look really fun, and would allow me to do things like make dot matrix displays, but they're much more expensive.
- Waterproof vs. non-waterproof.

The page I liked best with images showing comparative layouts is at
http://www.ledlightsworld.com/page.html?id=32
but there are tons of sites out there that describe the choices.

Roughly speaking, I'm trying to get a rectangular layout, approximately 30cm wide x 20cm high, and holding about 600 lights.

I ordered a warm white light strip over eBay from someone in Portland.  The listing said it would be a 600 LEDs/m strip, but when I opened it, I only got 300 LEDs/m.  I chose that place over a place in China just to get a faster delivery.  After some complaining, we negotiated a $5 refund, so I ended up with a 300 LEDs/m strip to experiment with.  When I'm done, I intend to take that strip, and put it in the back of my TV so I can get some inexpensive, warm backlighting.  (The seller said they later checked their inventory and would update their listing.)

Width, density, and layout

The strip I got is 8mm wide, which is just as the web site said.  The lights themselves are 3 LEDs every 5cm (therefore 300 total in 5 meters), so laid out in one strip, I'd get a light every 1.66cm, quite a bit less dense than I'd like.

The "single density" 3528 strips allow cut points every 3 lights, or every 5cm. 

The double density strips are still 8mm wide, but put 6 lights every 5 cm.  The images online suggest that they allow cut points every 3 lights, still, so I may be able to cut at the 2.5cm mark.

The 5050s are on a 10mm-wide strip (1cm wide), and still 3 lights per 5cm.

Plain rectangular layout

In a basic layout, I could lay out each strip horizontally.  I'd cut at every sixth cut point (6 x 5cm = 30cm) and so I'd have 18 lights per mini strip, and each mini strip would be 30cm wide.  That would give me 16 strips and some leftovers.

Since I'm trying to lay out vertically into 20cm height, and the strips are 0.8cm wide, I could put about 24 of them in, densely packed.  Having only 16 ministrips, I'm well within the 24 count, and would have to space them out more.  Roughly speaking, I could space them at 1.2cm separation.  The first would be at 0cm, and then 16th would be at (n-1)*1.2 = 18cm origin, and stretch to 18.8cm endpoint.  Or, I could separate them by 1.4cm and stretch to 22.4cm terminus.

Now, with this kind of "rectangular" layout, I'd end up with 16 strips, thus 16 lights x 18 lights total, or 288 lights of the 300 on the whole strip.

If I were to go to a double density setup, I could get double the number of lights, or 576 lights.

If I were to use the 5050s on a 300 LEDs/m strip, my strip width would increase to 1cm.  But, since the layout plan still has the strips laid out every 1.2cm or 1.4cm, the 5050s would fit.

So, using the double-density 3528s would double the light in the same space, and using the 5050s would triple the light in the same space.  The only design point complication is that I'm not really getting 600 lights, and if I use an "egg crate diffuser", it would have to be rectangular or else the diffuser might actually block LEDs here and there.

Diamond layout

Instead of laying out the strips so that lights are directly aligned from top to bottom, I could stagger them, getting a "diamond" layout instead.  Doing this with the single density 3528s or 5050s would work, but it would make as much sense with the double density 3528s.

Power requirements


Each LED strip has different power requirements and capabilities.  The web site listed earlier suggests that I can run them at
SD 3528 = 12V, < 2A, < 24W
DD 3528 = 12V, < 3A, < 36W
SD 5050 = 12V, < 6A, < 72W

I don't quite understand those ratings.  You would think that the DD 3528s should handle double the amperage, and hence could be run at 12V, < 4A, < 48W.  Maybe there's something else in the circuitry that is current- or heat-limited on those strips, or maybe it's particular to a manufacturer's design.

I'm not sure if the LEDs are current-limited or current-hungry.

Power supplies

In my trip to Weird Stuff, I found several power supplies: a 12V, 2A one from Condor, and another 12V, 5A one from Condor.  I didn't find anything that was in the 12V, 3A range, so when my DD 3528s come in, I'll just have to see how they behave with the 2A supply, and look for another supply via eBay.

A cursory scan of eBay puts a 12V, 3A power supply with connectors at $13.99 with free shipping.

Amazon has one for $9.99 with free two-day shipping:
Lighting EVER® Power Adaptor, Transformers, Power Supply For LED Strip, 12V, 3A Max, 36 Watt Max, US Plug

From a physical standpoint, I don't want a massive 12A power supply such as you'd find from an ATX motherboard.  It has to be more like a laptop power supply brick with a 9V-style jack coming out.

I may be able to use a larger power supply and build my own voltage regulator board, but it'd have to handle high wattage.

Dimming

I also want to be able to set the light level of the LEDs, and there are various dimmer switches available.  The most appealing at this point is a dial-based mechanism that uses PWM to control the brightness.  It's rated at 6A max current "per color":
Amazon, $9.99 plus $4.99 shipping: LEDPRO PWM Dimming Controller For LED Lights or Ribbon 3528 5050 strip 12V 8A Dimmer
but similar can be ordered from China for $2 or $3 with just $3 shipping.  China is whoopin' our manufacturing butt, but probably at the cost of pollution.

The dial-based controls are attractive because I want one that doesn't "forget" its setting.  There are wireless models out there, but they might fail, or you might lose the little adjustment fob.  There are also push-button ones out there, but they won't integrate cleanly with the build.  With a dial setting, I could just drill a hole in the back, and screw it on, I think.

The dial-based one is 89x59x35mm (LxWxH).  Example on eBay: LED Light Dimmer Modulator Brightness Adjustable Control 12V~24V 8A #JT1 E0Xc.
It'd be ugly, but I could mount it outside the box just above the stand connection.  At least there it's behind the scenes, and not likely to be bumped.  That one is rated at 8A, too.

A few other alternatives:
12V inline dimmer on eBay, Min LED Single Color Dimmer Amplify PWM for 3528/5050 LED Light/Strip
 That one is only $4 with free shipping, but rated at 5A

eBay: NEW DIGITAL 12V 60 WATT PWM VARIABLE BRIGHTNESS LED DIMMER DRIVER KIT
That one is $11 from Bay City, OR, and requires assembly, but it would totally fit inside the box.

eBay: DC 10A 12V 24V 36V 25Khz Motor Speed Driver Adjuster PWM Control Controller
This one is $8 with $1 shipping from within the US, but the page's English is so mangled, I suspect it's originating from China.
What's really intriguing about that one is that it has a separable potentiometer and small form factor.  I found it when looking for motor speed controllers, not LED dimmers.  But both are PWM, and it appears to meet the electronic requirements.  I'm not sure if it would be noisier or if having constant current is a requirement.

Containing box

While at Weird Stuff, I also looked at options for light diffusion (hoping for a silver egg crate, no luck) and having a container for the panel.  I looked at old child-sized laptops, and they also had several other things.  On one simplest end of the spectrum, there was a plain old frame.  I could mount one of those on top of a containing box, and use the existing glass.  On the other end of the spectrum, there were stacks of old, torn-apart laptops.

Then I noticed the monitors that were on sale.  Most were too large for my needs, but I found a KDS 15" monitor that looked right.  My basic requirements were that it be cheap, and able to be disassembled.

The KDS 15 was on a stand, and had little rubber covers that, I guessed correctly, were simply stuck on top of normal Phillips screws.

One thing to note: as with other electronics hackery, the older and more foreign-made stuff is easier to deal with, most of the time.  I chose this monitor partly because it wasn't something as sleek as a Mac.  I've dealt with removing the cover and innards of a Mac, and it's a pain.  This one could be pried apart and unscrewed with pretty normal tools.

Disassembly of the KDS 15-inch monitor


The KDS monitor was pretty easy to disassemble.

For all of this, if you intend to do something similar to your own monitor, proceed with caution, and of course you are doing this at your own risk.  Be absolutely sure the monitor is not plugged in to any power supply while doing this disassembly, and reuse and/or dispose of parts responsibly!  If you care to reuse the monitor itself, be sure to restore its grounding and shielding circuitry, and be careful with static safety whenever handling it.

Step 1: Remove the little adhesive, rubber screw covers x  12.

First, I put the monitor face-down on a clean towel.  In the following picture, you can see that there are still some screw holes that are covered by the adhesive, rubberized covers, and some where I've already removed them.


Step 2: Remove the screws

Four that held the panel to the stand
Four along the left and right edges
Two at the bottom
Two where the cord went into the back.


Step 3: Remove the cord cover panel

Interestingly, the KDS 15 did not have an internal power adapter, and thus did not have a socket point for a standard power plug.  Based on the input cords, it seems it was supposed to have had its power converted outside the box.

Once I had all the screws off, I removed a little plastic panel that held the cord to the box.  That just required a little bit of prying.  The simplest directions: set the panel face down with the cord coming to you, then slip a small, flathead screwdriver into the top slit and lever outward.



Step 4: Gently pull off the rubber band, and unclip the pressure clips

On the KDS 15, there's a rubber "band" that goes all the way around the edge of the box.  It hides the seam where the front and back plastic pieces meet.  Removing this band, and unclipping the front plastic from the back were the hardest parts of the disassembly process.


The rubber band's weakest point turns out to be right where the power button is at the middle, bottom.  By prying that out a little bit, you can get a decent hold on it, and slowly pull it away from the plastic edges that hold it in place.  Try not use any sharp tools when removing it, as you would risk cutting it.  At first, all I could get off was the bottom edge.




Wherever the rubber band has been removed, you'll be able to pry the front and back plastic panels apart slightly.  As you do that, you start to see little holes behind the crack.  Unclipping these as you go makes it easier to remove more and more of the band.

I was able to pry the panel apart at each clip point by using two forces.  First, I was gently separating the front and back panels from each other with one hand.  With the other, I stuck the tip of a small, flathead screwdriver (2mm blade width) into the clip hole, and very gently pulled the top of the screwdriver from monitor front to monitor back, until I heard a small click, and it would pop apart. 

Be careful as you'll not want to break these clips.  You'll need them to snap the frame together again when done.  Also, do not try to open the container too widely as the clips are being undone, or else you risk breaking them.

Here's what it looked like when I finally loosened up a corner.


Clip locations:
Bottom edge: 2 clips, each 2.25cm from right/left edge
Sides: 4 clips, 2 per side.  Top one is about 2.7cm from top.  Middle one is about 13.3cm from top.
Top edge: 5 clips at 1.7cm from edge, 6.2cm from edge, and midpoint

After enough of these were off, I was able to free up the rubber band enough so that I could get it fully off, give me much better access to all remaining clips.

With all the clips off...

...I could remove the front plastic from the back, exposing the electronics.

Electronics removal

All the electronics within the monitor were pretty easily removed.  First, I removed/untaped/loosened connectors wherever possible. A screw held the grounding wire in place, and tape held the I/O and power wire to the assembly.

Second came removal of the metal boxes that shielded the electronics.  These were taped down with grounding, conductive tape to shield RF, and in some areas, cutting that tape was required.  The metal boxes themselves could then be unclipped by pressing in at certain points and lifting upward.


With some more screw removal and connector disconnecting, I eventually had all the electronics out.  This is the picture with just the boards out.  The thin board on the left, I presume, controlled input selection (A/B channels).  The main square board is what controls the display circuitry.


This is a picture with the monitor panel removed:

The physical molding stamp on the back box plastic shows: 02 / 11 / 1 / 4, which I take to mean 14-NOV-2002.  The front of the panel is stamped 02 / 11 / 2 / 2.

The front panel had two little boards.  The first connected to four contact switches for menu selection, and brightness/contrast control.  It's a bit of a clever design, because it has four switches, but only three wires coming out.

It has resistors in place, and I assume the circuit could detect different resistance/voltage levels depending on which button was pressed.  If I'm right, then this is quite similar to what we were considering using for wiring up the WBMD four-position joystick, though the WBMD design would have only had two wires.  I didn't bother to test it, and ended up nixing the idea of using these buttons for my light box, as it would have introduced a need for more complicated circuitry and some form of memory.

The other circuit board was for power, but it had four wires coming out, so it wasn't just a simple contact switch.  It turns out that two of the wires are there for lighting an LED to indicate power state to the user.  I chose not to use this board yet.

End result: I have a box with visible dimensions of 30.5cm x 22.9cm.  There's about an extra 1cm clearance on all edges on the inside of the front panel.  Within the box, I have about 1cm max clearance for putting in a translucent front, the light strips themselves, and some kind of backing material to which the light strips will be affixed.

It might seem a bit pricey but for $15 I got a form factor that mounts to a stand, has its own ventilation, and looks better than I think I could produce on my own.


Light panel


I decided for a first run, I'd use the 3528 single-density LEDs that I'd gotten, and mount them on foam board.  I measured the enclosure, cut the foam board to size, and then test-fit it to the interior, adjusting where needed.

To assemble the LEDs, I decided to go with the rectangular layout.  The display dimensions were over 30cm wide, so I could get six segments per length.  There were three LEDs per segment.  As such, I'd get floor(500cm/30cm) as the number of strips to use, yielding 16 strips and a bit of leftover.

Since I'd be mounting the lights on foam board, I decided I'd need to wire all the connections first with the adhesive backing still on the strips.  I couldn't solder while on the foam board, and I couldn't remove bits of the adhesive backing for fear of tacking the strip in some place where it should not be.

For layout purposes, I had the option of two layout approaches.  The first, and the one I chose, was to lay out each strip the same way.  The second choice would have the first strip right-side up, and the second one upside-down, etc.  The first option would mean having the connecting wires cross at each junction, whereas the second option would have the two closer points connect, and the two farther points connect.

I also broke from normal convention, wherein I'd use red wires for positive, and black for negative.  Instead, I went with all white wires for a cleaner look.

Here's a section of one set of soldered strips.

From a tips & tricks standpoint, I found that I was most successful in getting good-looking, pre-tinned contact points by doing this:
1.  Lay out all strips, side by side.  Orientation of each strip doesn't matter much, since all contact points on both sides, except the end strips, need pre-tinning.
2.  Touch the solder tip to the contact point such that the iron handle is over free space, not over the strip; only the tip is over the contact point, pointing toward the strip, if you will.
3.  Touch the solder to the contact point
4.  As the solder flows onto the contact, pull the tip down and away from the strip -- in my case, touching a granite countertop.  This helped flow the solder toward the endpoint.
5.  Repeat about 60 times.

This is what the full set of strips looked like after soldering was complete.  The red and black leads only connect to the bottom-most strip, though the picture suggests otherwise.

I then mounted the foam board inside the enclosure, and traced lines on it to figure out what my vertical limitations would be.  After a bit of computation, I figured I should place each strip at 1.4cm intervals, and since each strip was 8mm wide, that meant there would be a gap of 6mm between each strip.

Not having a precise, perpendicular edge on the foam board that I'd cut, I chose not to use a T-square to help with drawing lines.  Instead, I did my best to mark both sides at 1.4-cm, and draw lines between them.  In the next build, I hope to use a laser-cut and laser-etched backing board.

This picture shows how it'd look before I started taping each strip down.

As I went about pre-assembling this, I realized I needed some longer leads so that power could be supplied to the light strips.  So, I soldered some additional wire to the existing leads, and used some heatshrink tubing to keep it together.  I also bent the leads around the foam board.

Taping the LEDs to the board was a bit of a challenge.  The hardest part was making sure things would stay aligned.  I didn't do a great job of this, messing up on the second-to-last strip.  In a better assembly process, I'd choose to do this:
1.  Use more flexible wire
2.  Use longer wire to allow easier adjustment
3.  Use some kind of clamped straightedge that bumps up against some column of LEDs (spanning all strips at once).
4.  Strip off a very small amount of adhesive backing from each strip, tack each down, aligned with the pre-drawn row lines.
5.  For each row, remove the remaining adhesive strip and tack it down to the foam board.  The end that was tacked already should stay in place, and the pre-alignment forced in step 3 would keep things from getting too far out of alignment.

This is a shot of the front of the panel after initial completion.  Power is off.  Note that at this point, I've added the dimming circuit in the back, and added a 5.5mm DC power jack for easy connection to the power supply.
Here's a picture of the same with power on at a low level.  Note: the color warmth is messed up between these two pictures as the camera was trying to auto-adjust exposure settings.  I've tried to compensate some with Photoshop, but only partially succeeded.
In the picture above, you might also notice that the lights are diffused a bit on the left.  That's because I'd hot-glued a random piece of acrylic to the front panel to serve as a window and to see how the diffusion would behave.  In the final version, I'd like something that provides better, even diffusion.

In the end, this is about 24 watts, 16x18 = 288 LEDs in a 600-square-centimeter space.  It's quite bright.

The back of the panel shows the clunky, exposed dimming circuit.


It's quite nice.  The potentiometer operates across its entire range with no humming.  Only at the very lowest levels do I see flicker in the LEDs.  The picture here shows the hacked-up initial state, where I just taped it to the back of the monitor stand.  Later, I drilled a few holes and mounted it to some standoffs, lower on the stand.  I chose to move it lower because the stand does allow the monitor/light box face to pivot, and I didn't want to risk putting a hole or screw in a place that would interfere with that motion.

Eventually, I'd *like* to rebuild the dimming PWM circuit such that it's flat and inside the box, but I'm leaving it outside for now.  I also would *like* to mount the potentiometer to the removable panel where the power cord went in.

I also cleaned up the Weird Stuff label on the stand using a spiffy new $20 heat gun and some other techniques for removing leftover adhesive (without marring the underlying plastic).

Final steps

- Get double-density 3528s (done)
- Choose another material that is laser-cuttable and laser-etchable for the new backing board (probably white acrylic).  Build it and mount the new LED strips to it much more precisely.
- Find and mount a diffusive, but mostly clear panel/window in front of the LEDs
- Optionally add an on/off switch
- Optionally replace the dimmer potentiometer with one that has its own on/off switch
- Mount the potentiometer in a nice place
- Re-clip all the clips
- Put the "rubber band" back on
- Put back all screws and rubber screw covers
- Clean up any marring of the original painted finish somehow
- Possibly cover the KDS logo with a personalized logo plate
- Reuse/recycle the original monitor componentry

Extra credit

- Using the existing monitor buttons to control power and brightness, while also remembering brightness state, even if powered off.