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);
}




No comments:

Post a Comment