[Home] [Downloads] [Search] [Help/forum]

Gammon Software Solutions forum

See www.mushclient.com/spam for dealing with forum spam. Please read the MUSHclient FAQ!

[Folder]  Entire forum
-> [Folder]  Electronics
. -> [Folder]  Microprocessors
. . -> [Subject]  Hacking a scrolling LED strip
Home  |  Users  |  Search  |  FAQ
Username:
Register forum user name
Password:
Forgotten password?

Hacking a scrolling LED strip

Postings by administrators only.

[Refresh] Refresh page


Posted by Nick Gammon   Australia  (19,470 posts)  [Biography] bio   Forum Administrator
Date Thu 14 Nov 2013 05:07 AM (UTC)  quote  ]

Amended on Mon 25 Nov 2013 09:50 PM (UTC) by Nick Gammon

Message
Introduction to the scrolling LED strip


This post describes working out how a 72 x 7 pixel LED strip works, and then connecting it up to an Arduino to display custom text under program control.

The intention was to use the strip to display custom data (for example, the temperature) rather than having to manually (and somewhat laboriously) type the text in through the inbuilt keypad.

The strip




This was purchased a while ago for around $50 from memory. It has 72 pixels horizontally and 7 deep, giving a total of 504 pixels. Also there is a 55-key keypad visible in the photo which lets you enter text:



The internals


The device consists of 5 circuit boards. Two hold the LEDs themselves:

Part number 80-947C 2002-09-12:



Part number 80-947D 2002-09-12:



Visible also in the above photos is the board containing the keyboard.

Once opened you can fold out the other two boards with the control logic on them:

Part number 80-947A-2 2004-04-15:



Part number 80-947B 2002-09-12:




Various parts are labelled following the investigation described below.

Reverse engineering


Some work with a magnifying glass revealed that the chips were 74HC595 8-bit shift registers.



More details about them here:

http://www.gammon.com.au/forum/?id=11518

These are standard output shift registers with a latch, which means you can shift out to multiple chips and then "latch" the data (copy from a temporary register to the output pins) in one operation, providing flicker-free updating.

With 72 columns of LEDs, and the 595 chips providing 8 bits each, it was reasonable to deduce that 9 of the chips were dedicated to driving the columns. Underneath each of those chips were 8 x 150 ohm resistors, which would be for current-limiting of the LEDs. Measurements indicate that there is around 4 mA per LED going through the resistors (a 600 mV voltage drop). This is a total drain of 32 mA for each 595 chip (if all LEDs are lit) which is within the spec for that chip.

The column drivers sink current (so to light an LED the corresponding output has to be zero).

The tenth 595 chip labelled "row driver" on the photo was clearly intended to source current for the rows, via the 7 x 8550 PNP transistors on the right, driven from that chip via 7 x 4.7k base resistors.

Since the row drivers are driven via a transistor which inverts the output, the row driver must also have an output as zero, in order for the transistor to source current.

In other words, if all 595 chips are outputting zero, then all LEDs are lit.

This video demonstrates how the multiplexing works:




As you can see from that video, column are lit in a batch, but one row at a time. When done really slowly you can't make out the whole letter. When sped up, persistence of vision makes it appear that they are all on at once.

Detail from the reverse side:



Keypad driver


One slightly puzzling aspect was the absence of any extra chips for the 55-key keypad, as all 10 chips had now been accounted for. There were 8 wires leading to it (on a ribbon cable) on the left, in the photo, and 7 wires on the right. This led to a deduction that this was a 7 x 8 key keypad matrix. Since that would give 56 possible keys, it seemed very likely.

A close inspection of the cable on the left showed 8 x 10k pull-up resistors for that cable, plus each of the 8 wires were connected directly to the CPU. On the other side, the "row driver" chip was also connected to the 7-wire cable going to the keypad.

Thus it seems that the design was that the row driver would be repeatedly activated for one row at a time (driving it low) which would source current for the LEDs via the transistors which invert the signal, plus at the same time sink current for the keypad (the keypad does not go via the transistors). The pull-up resistors would raise the input to the CPU high, except if a key was pressed. Thus the CPU could deduce which key was being pressed, while it was also outputting to the LEDs.




Taking control!


Knowing how the 595 works, it seemed reasonable to assume that if I could disconnect the clock, data, and latch signals from the processor, and supply my own, then I could make the LEDs show whatever I wanted.

After a considerable amount of time tracing PCB traces, I worked out that it was very easy to do, because each of those reached the processor chip via a jumper on the front.

To do this cut the three jumpers indicated and then solder wires onto the circled ends of the now-cut link (that is the part furthest from the processor).



Now the processor thinks it is sending data to the 595 chips, but it isn't.

Those three wires (clock, data, latch) are then connected to the Arduino as shown (pin numbers for a Uno or similar). You also need to connect the ground wire as well, I took it from the electrolytic capacitor nearby as shown.


[EDIT] Subsequent testing shows that the ground point was not well-chosen. That is disconnected from the circuit if you plug in a wall-wart into the power socket. I suggest that you choose another ground point (eg. pin 8 of any of the 595 chips).

Programming it


This test sketch should light up every second column:


#include <SPI.h>

const byte LATCH = 10;


void setup ()
{
  SPI.begin ();
  SPI.setClockDivider(SPI_CLOCK_DIV8);

  digitalWrite (LATCH, LOW);
  for (byte col = 0; col < 9; col++)
    SPI.transfer (0xAA);
  SPI.transfer (0);      // <------- row driver
  digitalWrite (LATCH, HIGH);

}  // end of setup

void loop ()
 {
 }


If you change the line marked "row driver" to this:



  SPI.transfer (0xCC);      // <------- row driver



Then you will see that now only 4 of the rows are alight.


Assuming that works, now you can try something more elaborate.

Main sketch:


#include <SPI.h>
#include "font5x7.h"

const byte LATCH = 10;

const byte CHIPS = 9;
const byte ROWS = 7;
const byte PIXELS_PER_LETTER = 5;

// what to show on the LEDs
byte bitmap [CHIPS] [ROWS];

// timer Interrupt Service Routine (ISR) to update the LEDs
ISR (TIMER2_COMPA_vect) 
  {
  static byte row = 0;
  
  digitalWrite (LATCH, LOW);
  for (byte col = 0; col < CHIPS; col++)
    SPI.transfer (~bitmap [col] [row]);
  SPI.transfer (~ (1 << row));  
  digitalWrite (LATCH, HIGH);
    
  // wrap if necessary
  if (++row >= ROWS)
    row = 0;
  }  // end of TIMER2_COMPA_vect
  

// Build one letter into the bitmap.
// The variable column is updated to point past the current letter.
// Negative columns are OK, and will just skip without writing (for sideways scrolling purposes).
// Returns true once we have gone past the end of the bitmap, so the outer loop can stop.

bool outputLetter (byte c, int & column)
  {
  if (c < 0x20 || c > 0x7F)
    c = 0x7F;  // unknown glyph
  
  c -= 0x20; // force into range of our font table (which starts at 0x20)
    
  for (byte i = 0; i < PIXELS_PER_LETTER; i++)
    {
    if (column >= 0)
      {
      // work out which byte (chip) this will end up in
      int whichChip = column / 8;
      if (whichChip >= CHIPS)
        return true;
      
      // work out which column within the chip it will be in      
      byte whichColumn = column % 8;
      
      char pixels =  pgm_read_byte (&font [c] [i]);
      for (byte bit = 0; bit < ROWS; bit++)
         if (bitRead (pixels, bit))
            bitSet (bitmap [(CHIPS - 1) - whichChip] [(ROWS - 1) - bit], whichColumn);
      }  // end of column not negative
    column++;  // next row of pixels
      
    }  // end of for each of the 5 pixels in the glyph

  // gap between letters
  column++;
    
  return false;
  }  // end of outputLetter
  
// Turn a text string into a bitmap, return its length
int convertMessageToBitmap (const char * message, int column = 0)
  {
  noInterrupts ();
  memset (bitmap, 0, sizeof bitmap);
  
  for (const char * s = message; *s; s++)
    {
    if (outputLetter (*s, column))
      break;
    }  // end of for each byte in the message
  interrupts ();
  return strlen (message);
  }  // end of convertMessageToBitmap
  
void setup ()
  {
  SPI.begin ();
  SPI.setClockDivider(SPI_CLOCK_DIV8);
  memset (bitmap, 0, sizeof bitmap);
  
  // Stop timer 2
  TCCR2A = 0;
  TCCR2B = 0;

  // Timer 2 - gives us a constant interrupt to refresh the LED display
  TCCR2A = bit (WGM21) ;   // CTC mode
  OCR2A  = 63;            // count up to 64  (zero relative!!!!)
  // Timer 2 - interrupt on match at about 2 kHz
  TIMSK2 = bit (OCIE2A);   // enable Timer2 Interrupt
  // start Timer 2
  TCCR2B =  bit (CS20) | bit (CS22) ;  // prescaler of 128
  }  // end of setup


int column;

void loop ()
  {
  static char buf [20];
  
  sprintf (buf, "Uptime %ld", millis ());
  int len = convertMessageToBitmap (buf, column--);
  
  delay (25);

  // work out when to wrap
  if (column > CHIPS * 8)
    column = - (len * (PIXELS_PER_LETTER + 1));
  else if (column < - (len * (PIXELS_PER_LETTER + 1)))
    column = CHIPS * 8;
     
  }  // end of loop


For the font data make a new tab in the IDE called "font5x7.h" and put this into it.

font5x7.h


// font data - each character is 8 pixels deep and 5 pixels wide

byte font [96] [5] PROGMEM = {
  { 0x00, 0x00, 0x00, 0x00, 0x00 }, // space  (0x20)
  { 0x00, 0x00, 0x2F, 0x00, 0x00 }, // !
  { 0x00, 0x07, 0x00, 0x07, 0x00 }, // "
  { 0x14, 0x7F, 0x14, 0x7F, 0x14 }, // #
  { 0x24, 0x2A, 0x7F, 0x2A, 0x12 }, // $
  { 0x23, 0x13, 0x08, 0x64, 0x62 }, // %
  { 0x36, 0x49, 0x55, 0x22, 0x50 }, // &
  { 0x00, 0x05, 0x03, 0x00, 0x00 }, // '
  { 0x00, 0x1C, 0x22, 0x41, 0x00 }, // (
  { 0x00, 0x41, 0x22, 0x1C, 0x00 }, // (
  { 0x14, 0x08, 0x3E, 0x08, 0x14 }, // *
  { 0x08, 0x08, 0x3E, 0x08, 0x08 }, // +
  { 0x00, 0x50, 0x30, 0x00, 0x00 }, // ,
  { 0x08, 0x08, 0x08, 0x08, 0x08 }, // -
  { 0x00, 0x30, 0x30, 0x00, 0x00 }, // .
  { 0x20, 0x10, 0x08, 0x04, 0x02 }, // /
   
  { 0x3E, 0x51, 0x49, 0x45, 0x3E }, // 0  (0x30)
  { 0x00, 0x42, 0x7F, 0x40, 0x00 }, // 1
  { 0x42, 0x61, 0x51, 0x49, 0x46 }, // 2
  { 0x21, 0x41, 0x45, 0x4B, 0x31 }, // 3
  { 0x18, 0x14, 0x12, 0x7F, 0x10 }, // 4
  { 0x27, 0x45, 0x45, 0x45, 0x39 }, // 5
  { 0x3C, 0x4A, 0x49, 0x49, 0x30 }, // 6
  { 0x01, 0x71, 0x09, 0x05, 0x03 }, // 7
  { 0x36, 0x49, 0x49, 0x49, 0x36 }, // 8
  { 0x06, 0x49, 0x49, 0x29, 0x1E }, // 9
  { 0x00, 0x36, 0x36, 0x00, 0x00 }, // :
  { 0x00, 0x56, 0x36, 0x00, 0x00 }, // ;
  { 0x08, 0x14, 0x22, 0x41, 0x00 }, // <
  { 0x14, 0x14, 0x14, 0x14, 0x14 }, // =
  { 0x00, 0x41, 0x22, 0x14, 0x08 }, // >
  { 0x02, 0x01, 0x51, 0x09, 0x06 }, // ?
  
  { 0x32, 0x49, 0x79, 0x41, 0x3E }, // @  (0x40)
  { 0x7E, 0x11, 0x11, 0x11, 0x7E }, // A
  { 0x7F, 0x49, 0x49, 0x49, 0x36 }, // B
  { 0x3E, 0x41, 0x41, 0x41, 0x22 }, // C
  { 0x7F, 0x41, 0x41, 0x22, 0x1C }, // D
  { 0x7F, 0x49, 0x49, 0x49, 0x41 }, // E
  { 0x7F, 0x09, 0x09, 0x09, 0x01 }, // F
  { 0x3E, 0x41, 0x49, 0x49, 0x7A }, // G
  { 0x7F, 0x08, 0x08, 0x08, 0x7F }, // H
  { 0x00, 0x41, 0x7F, 0x41, 0x00 }, // I
  { 0x20, 0x40, 0x41, 0x3F, 0x01 }, // J
  { 0x7F, 0x08, 0x14, 0x22, 0x41 }, // K
  { 0x7F, 0x40, 0x40, 0x40, 0x40 }, // L
  { 0x7F, 0x02, 0x0C, 0x02, 0x7F }, // M
  { 0x7F, 0x04, 0x08, 0x10, 0x7F }, // N
  { 0x3E, 0x41, 0x41, 0x41, 0x3E }, // O
  
  { 0x3F, 0x09, 0x09, 0x09, 0x06 }, // P  (0x50)
  { 0x3E, 0x41, 0x51, 0x21, 0x5E }, // Q
  { 0x7F, 0x09, 0x19, 0x29, 0x46 }, // R
  { 0x46, 0x49, 0x49, 0x49, 0x31 }, // S
  { 0x01, 0x01, 0x7F, 0x01, 0x01 }, // T
  { 0x3F, 0x40, 0x40, 0x40, 0x3F }, // U
  { 0x1F, 0x20, 0x40, 0x20, 0x1F }, // V
  { 0x3F, 0x40, 0x30, 0x40, 0x3F }, // W
  { 0x63, 0x14, 0x08, 0x14, 0x63 }, // X
  { 0x07, 0x08, 0x70, 0x08, 0x07 }, // Y
  { 0x61, 0x51, 0x49, 0x45, 0x43 }, // Z
  { 0x00, 0x7F, 0x41, 0x41, 0x00 }, // [
  { 0x02, 0x04, 0x08, 0x10, 0x20 }, // backslash
  { 0x00, 0x41, 0x41, 0x7F, 0x00 }, // ]
  { 0x04, 0x02, 0x01, 0x02, 0x04 }, // ^
  { 0x40, 0x40, 0x40, 0x40, 0x40 }, // _
  
  { 0x00, 0x01, 0x02, 0x04, 0x00 }, // `  (0x60)
  { 0x20, 0x54, 0x54, 0x54, 0x78 }, // a
  { 0x7F, 0x50, 0x48, 0x48, 0x30 }, // b
  { 0x38, 0x44, 0x44, 0x44, 0x20 }, // c
  { 0x38, 0x44, 0x44, 0x48, 0x7F }, // d
  { 0x38, 0x54, 0x54, 0x54, 0x18 }, // e
  { 0x08, 0x7E, 0x09, 0x01, 0x02 }, // f
  { 0x0C, 0x52, 0x52, 0x52, 0x3E }, // g
  { 0x7F, 0x08, 0x04, 0x04, 0x78 }, // h
  { 0x00, 0x44, 0x7D, 0x40, 0x00 }, // i
  { 0x20, 0x40, 0x44, 0x3D, 0x00 }, // j
  { 0x7F, 0x10, 0x28, 0x44, 0x00 }, // k
  { 0x00, 0x41, 0x7F, 0x40, 0x00 }, // l
  { 0x7C, 0x04, 0x18, 0x04, 0x78 }, // m
  { 0x7C, 0x08, 0x04, 0x04, 0x78 }, // n
  { 0x38, 0x44, 0x44, 0x44, 0x38 }, // o
  
  { 0x7C, 0x14, 0x14, 0x14, 0x08 }, // p  (0x70)
  { 0x08, 0x14, 0x14, 0x08, 0x7C }, // q
  { 0x7C, 0x08, 0x04, 0x04, 0x08 }, // r
  { 0x48, 0x54, 0x54, 0x54, 0x20 }, // s
  { 0x04, 0x3F, 0x44, 0x40, 0x20 }, // t
  { 0x3C, 0x40, 0x40, 0x20, 0x7C }, // u
  { 0x1C, 0x20, 0x40, 0x20, 0x1C }, // v
  { 0x3C, 0x40, 0x30, 0x40, 0x3C }, // w
  { 0x44, 0x28, 0x10, 0x28, 0x44 }, // x
  { 0x0C, 0x50, 0x50, 0x50, 0x3C }, // y
  { 0x44, 0x64, 0x54, 0x4C, 0x44 }, // z
  { 0x00, 0x08, 0x36, 0x41, 0x00 }, // {
  { 0x00, 0x00, 0x7F, 0x00, 0x00 }, // |
  { 0x00, 0x41, 0x36, 0x08, 0x00 }, // }
  { 0x30, 0x08, 0x10, 0x20, 0x18 }, // ~
  { 0x7F, 0x55, 0x49, 0x55, 0x7F }  // unknown char (0x7F)
  
};


This should scroll the words "Uptime xxxxxxxx" where xxxxxxxx is the number of milliseconds since the sketch started.

You can change the scroll rate by altering this line:


  delay (25);


As you can see the main loop is very simple. The updating of the display is done in a timer interrupt routine, called about 2000 times a second. Each time it is called it draws another row, wrapping back to row 0 after drawing row 7.

Because this is interrupt driven you could be doing other things in the main loop, like finding the temperature, time, etc.

To display some other message, simply do something like this as required:


  convertMessageToBitmap ("Hello world!");


The second argument is the column number to start the text at (it can be negative). By updating the column number as in the sketch above, the text can be made to scroll. It is clipped to the bitmap, so it is OK to have a column which is negative, or larger than the size of the display.

Bear in mind you have 72 pixels horizontally and each letter takes 6 pixels, so you can fit 12 characters across the screen at once.

By subtracting one from the column variable it scrolls from right to left, if you add one it scrolls from left to right. Scrolling from right to left might sound counter-intuitive, but since you read from left to right, it is much easier to read a sentence if it scrolls from right to left.

You could "blink" text by alternating some text with blank text, eg.


  convertMessageToBitmap ("Hello world!");
  delay (1000);
  convertMessageToBitmap ("");
  delay (1000);


This photo shows it displaying the main sketch. The letters are blurred because of the time taken to take the photo:



This shows the connection between the Uno and the LED device. (It is actually a Ruggeduino because I was worried I might blow something up during testing).



As you can see, only four wires are needed between the Arduino and the display. The display was independently powered because of the power required to drive all the LEDs.

Schematic


A partial schematic is below. It doesn't show all the 595 chips, nor all of the LEDs, transistors, keyboard interface etc. However the main points which show the LED multiplexing are there:




Demonstration video





Completed project


After researching all the above I turned my sign (for the time being) into a temperature and humidity display.

Example display:







I made up an freestanding "Arduino-like" board using the Evil Mad Scientist Atmega target board. This was bolted to the inside of the battery compartment. The battery holder had been discarded because the batteries had leaked and corroded it.




Opening the door to the compartment you can see the board in place with five wires running into the connection points on the LED strip (the four wires described earlier, plus a connection to the +3.3v Vcc line to power the processor).




The board with the various parts shown in detail:



The wiring and relevant code were taken from the temperature and humidity sensor project described here:

http://www.gammon.com.au/forum/?id=12106

- Nick Gammon

www.gammon.com.au, www.mushclient.com
[Go to top] top

The dates and times for posts above are shown in Universal Co-ordinated Time (UTC).

To show them in your local time you can join the forum, and then set the 'time correction' field in your profile to the number of hours difference between your location and UTC time.


2,056 views.

Postings by administrators only.

[Refresh] Refresh page

Go to topic:           Search the forum


[Go to top] top

Quick links: MUSHclient. MUSHclient help. Forum shortcuts. Posting templates. Lua modules. Lua documentation.

[Home]

Written by Nick Gammon - 5K

Comments to: Gammon Software support
[RH click to get RSS URL] Forum RSS feed ( http://www.gammon.com.au/rss/forum.xml )

[Best viewed with any browser - 2K]    [Web site powered by FutureQuest.Net]