Some how-to/example code for controlling a large multiplexed display. The code and Arduino were a replacement for the original controller. Back when I designed it, the ATMega8 could just barely handle a program this size. Because of improvements in the Arduino IDE since then, and having an ATMega168 at your disposal, this project should compile with room to spare.
I don't have the schematic, sorry. The display belongs to somebody else. I haven't actually seen it in two years. But, it was pretty basic and I can describe it from memory.
The Arduino and display were connected common-ground, but the display needed its own 5V power supply capable of 2A. For brightness control, an adjustable bench supply was used, and set between 3V and 6V. Also, the higher the supply voltage, the hotter the display ran (it ran hot).
It consisted of a series of cascaded shift registers totalling 96 (or more) bits of RAM. (that's 12x8-bit, 8x12-bit, or 6x16-bit TTL Serial-in Parallel-out shift registers) This RAM stored the contents of a single row, which was reset, loaded and strobed seven times, to create the illusion of a solid image on the display. The first shift register's CLK (clock) and DATA inputs were exposed to the display controller board, for loading data. To cascade these registers, the final output bit of each is connected to the DATA pin of the next shift register, and all CLK pins are tied together. Generally, you should use the largest shift registers you can, but for TTL (5V logic) you're pretty much stuck with 8-bits, or 12-bits at best. Don't use 3.3V CMOS, unless they have 5V tolerant open collector outputs, and you put level-shifters ahead of the CE/CLK/DATA inputs to convert 5V to 3.3V.
There were 24 4x7 dot matrix displays, each had seven anodes, and four cathodes. The complicated part is how the shift registers are connected to the cathodes.
To make it simple, imagine a display with only one row of 96 LEDs. The outputs of the shift registers would be connected to each LED cathode. Now the strange part: this is how it actually works. For each dot matrix display "column", all cathodes are tied together and wired to a shift register output. This is possible because there is an anode for each row of the display, turning an anode on lights up one row. Only one row is lit at a time, so each serial register output sinks the current for only one LED (despite being connected to seven). Additionally, there is a current limiting resistor between the tied together cathodes, and the shift register output.
Anodes for each display "row" are also tied together, so that just seven power PNP transistors are needed. These transistors are not tied directly to the Arduino. There's one component left.
There were three wires, which selected the row that would be lit up. Those wires represented a binary number, up to seven (technically 3bits is 0-7, 8 values), which chose one row on the display. What made this possible was a BCD to Decimal decoder. The base of the transistors that control the anode for each row are connected to an output of this IC. There are ways to eliminate this component, but it keeps multiple rows from being lit at the same time, reduces power through the Arduino, and it reduces the number of IO used on the Arduino from 7 to 3.
Last but not least, there was a Reset wire to the display. For your shift registers, they should each have a clear pin. Connect those together and control it with one Arduino IO. My moving message display wasn't that sophisticated. Instead, it connected every VCC pin together and powered them on/off to clear their contents. (Don't do that, VCC is not TTL compatible.)
In total, there's 24 matrix displays, 96 resistors (value determined by each LED's max current), 12 8-bit shift registers (open collector), 7 PNP power transistors (minimum), and a BCD to Decimal decoder.
/* 96x7 Monochrome MOVING MESSAGE, Version 2
* ---------------
* Created 2007 by Amplificar (amplificar@gmail.com)
*
* optimized for performance & low memory use
*
* Comments
* An improvement over the original moving message controller
* that required externam SRAM. This program doesn't need
* external memory, and the Arduino outputs are intended to be directly
* connected to the Moving Message control wires (5V TTL compatible).
*
* The "Moving Message" display had an 8080 processor and
* control logic that were in bad shape. The internal battery had
* leaked and ruined traces on the board. Everytime the display was powered
* it had to be reprogrammed. My project was replacing the built-in logic
* with an Arduino. The end result was a greatly simplified controller,
* scriptable/reprogrammable through USB.
*
* The moving message display has internal shift registers; enough
* to store a single row. Drawing to the display requires loading 96bits
* with a two wire interface (data/clk), setting the row number with a
* four wire interface (BCD), and enabling the display (lights the selected row).
* This sequence is repeated for each row (seven rows), to render a single
* image across the display.
*
* This program contains:
* -A complete 4x7 pixel font stored in flash.
* -Scripted message sequence
* -Various animation effects
* -Display buffering
*/
/*
* In retrospect years later, this should have had the script
* stored in the microcontroller's EEPROM, with
* a simple text/RS232 interface for updating it, so the entire
* project doesn't need to be burnt again and again.
* (Can't fix it now, I'd have to build a display, the original wasn't mine.)
*/
/* DISPLAY INPUT WIRES
*
* red = +Vcc
* black = GND
* Vcc must be between 2.8VDC and 5VDC
*
* long individual red/black wires are LED voltage, high current draw 500mA-2A
* shorter bundled red/black are Logic voltage, low current draw 50mA-300mA
*
* you can use two different supply voltages for LED and Logic
* the higher the LED voltage, the more current it draws and heat it produces
* Warning: At LED Vcc 5V, it'll get pretty hot
*
* orange = data
* purple = clock
* yellow = reset
*
* gray w/1 stripe = row 1
* gray w/2 stripes = row 2
* gray w/3 stripes = row 3
*/
#include <avr/pgmspace.h>
#define POWER 13
#define DATA 12
#define CLK 11
#define RESET 10
#define ROW1 7
#define ROW2 6
#define ROW3 5
#define PORT_DC PORTB
#define PORT_RESET PORTB
#define PORT_ROW PORTD
#define bitDATA 4
#define bitCLK 3
#define bitRESET 2
#define bcdDATA (1<<bitDATA)
#define bcdCLK (1<<bitCLK)
#define bcdRESET (1<<bitRESET)
#define bitROW1 7
#define bitROW2 6
#define bitROW3 5
#define bcdROW1 (1<<bitROW1)
#define bcdROW2 (1<<bitROW2)
#define bcdROW3 (1<<bitROW3)
#define writeROW(r) { PORT_ROW=((PORT_ROW & ~(bcdROW1|bcdROW2|bcdROW3)) | ((!(r & 1))<<bitROW1) | ((!(r & 2))<<bitROW2) | ((!(r & 4))<<bitROW3)); }
#define writeDATA(a) { PORT_DC=((PORT_DC & ~bcdDATA) | (((a)>0)<<bitDATA)); }
#define writeCLK(a) { PORT_DC=((PORT_DC & ~bcdCLK) | (((a)>0)<<bitCLK)); }
#define writeRESET(a) { PORT_DC=((PORT_DC & ~bcdRESET) | (((a)>0)<<bitRESET)); }
#define MD_OUT(bit) { PORT_DC=((PORT_DC & ~(bcdDATA|bcdCLK)) | (((bit)>0)<<bitDATA)); PORT_DC=(PORT_DC | bcdCLK); }
// moving message display falling edge of clock cycle writes some data
#ifndef __font_LUCIDIA_MICRO
#define __font_LUCIDIA_MICRO
// All printable characters from ASCII 33 to 126
#define LUCIDIA_MICRO_MAX 509
unsigned char LUCIDIA_MICRO[LUCIDIA_MICRO_MAX] PROGMEM = {
0xDF,0x00,0x83,0x80,0x83,0x00,0x90,0xF4,0x9C,0xF7,0x9C,0x97,0x00,0xAC,0xAA,
0xFF,0xAA,0x92,0x00,0xC6,0xA9,0x96,0x88,0xB4,0xCA,0xB1,0x00,0xB0,0xCE,0xC9,
0xD6,0xE0,0xE0,0x98,0x00,0x83,0x00,0xBE,0xC1,0x80,0xC1,0xBE,0x00,0x82,0x8A,
0x85,0x8A,0x82,0x00,0x88,0x88,0xBE,0x88,0x88,0x00,0xD0,0xB0,0x00,0x88,0x88,
0x88,0x88,0x88,0x00,0xE0,0xE0,0x00,0xC0,0xB0,0x8E,0x81,0x00,0xBE,0xC1,0xC1,
0xC1,0xBE,0x00,0xC2,0xC1,0xFF,0xC0,0xC0,0x00,0xE1,0xD1,0xC9,0xC6,0x00,0xC1,
0xC9,0xC9,0xB6,0x00,0x98,0x94,0x92,0xFF,0x90,0x00,0xCF,0xC9,0xC9,0xB1,0x00,
0xBC,0xCA,0xC9,0xC9,0xB1,0x00,0x81,0x81,0xF9,0x87,0x00,0xB6,0xC9,0xC9,0xC9,
0xB6,0x00,0x86,0xC9,0xC9,0xC9,0xBE,0x00,0xB6,0xB6,0x00,0xD6,0xB6,0x00,0x88,
0x94,0xA2,0x00,0x94,0x94,0x94,0x94,0x00,0xA2,0x94,0x88,0x00,0x82,0x81,0xD1,
0x89,0x86,0x00,0x9C,0xA2,0xC9,0xD5,0xD5,0x9E,0x90,0x00,0xF8,0x96,0x91,0x96,
0xF8,0x00,0xFF,0xC9,0xC9,0xC9,0xB6,0x00,0x9C,0xA2,0xC1,0xC1,0xC1,0x00,0xFF,
0xC1,0xC1,0xC1,0xBE,0x00,0xFF,0xC9,0xC9,0xC1,0x00,0xFF,0x89,0x89,0x81,0x00,
0x9C,0xA2,0xC1,0xC9,0xF9,0x00,0xFF,0x88,0x88,0x88,0xFF,0x00,0xC1,0xC1,0xFF,
0xC1,0xC1,0x00,0xC0,0xC1,0xC1,0xBF,0x00,0xFF,0x88,0x94,0xA2,0xC1,0x00,0xFF,
0xC0,0xC0,0xC0,0x00,0xFF,0x86,0xB8,0x86,0xFF,0x00,0xFF,0x82,0x84,0x88,0xFF,
0x00,0xBE,0xC1,0xC1,0xC1,0xC1,0xBE,0x00,0xFF,0x89,0x89,0x89,0x86,0x00,0xBE,
0xC1,0xC1,0xD1,0xA1,0xDE,0x00,0xFF,0x89,0x99,0xA9,0xC6,0x00,0xC6,0xC9,0xC9,
0xB1,0x00,0x81,0x81,0xFF,0x81,0x81,0x00,0xBF,0xC0,0xC0,0xC0,0xBF,0x00,0x8F,
0xB0,0xC0,0xB0,0x8F,0x00,0x8F,0xF0,0x98,0x86,0x98,0xF0,0x8F,0x00,0xC1,0xA2,
0x94,0x88,0x94,0xA2,0xC1,0x00,0x83,0x84,0xF8,0x84,0x83,0x00,0xE1,0xD1,0xC9,
0xC5,0xC3,0x00,0xFF,0xC1,0xC1,0x00,0x81,0x86,0xB8,0xC0,0x00,0xC1,0xC1,0xFF,
0x00,0x84,0x82,0x81,0x82,0x84,0x00,0xC0,0xC0,0xC0,0xC0,0xC0,0x00,0x83,0x00,
0x90,0xAA,0xAA,0xAA,0xBC,0xA0,0x00,0xBF,0xA4,0xA2,0xA2,0x9C,0x00,0x9C,0xA2,
0xA2,0xA2,0xA2,0x00,0x9C,0xA2,0xA2,0xA2,0xBF,0x00,0x9C,0xAA,0xAA,0xAC,0x00,
0x84,0x84,0xBF,0x85,0x85,0x00,0x8C,0xD2,0xD2,0xD2,0xBE,0x00,0xBF,0x84,0x82,
0x82,0xBC,0x00,0xBD,0x00,0xC0,0xC0,0xBD,0x00,0xBF,0x88,0x94,0xA2,0x00,0xBF,
0x00,0xBE,0x84,0x82,0xBE,0x84,0x82,0xBE,0x00,0xBE,0x84,0x82,0x82,0xBE,0x00,
0x9C,0xA2,0xA2,0xA2,0x9C,0x00,0xFE,0x94,0x92,0x92,0x8C,0x00,0x8C,0x92,0x92,
0x92,0xFE,0x00,0xBE,0x84,0x82,0x86,0x00,0xA4,0xAA,0xAA,0x90,0x00,0x82,0x9F,
0xA2,0xA2,0x00,0xBE,0xA0,0x90,0xBE,0x00,0x8E,0x90,0xA0,0x90,0x8E,0x00,0x8E,
0xB0,0x8C,0x82,0x8C,0xB0,0x8E,0x00,0xA2,0x94,0x88,0x94,0xA2,0x00,0xC6,0xD8,
0xA0,0x98,0x86,0x00,0xA2,0xB2,0xAA,0xA6,0x00,0x88,0xBE,0xC1,0xC1,0x00,0xFF,
0x00,0xC1,0xC1,0xBE,0x88,0x00,0x88,0x84,0x84,0x8C,0x88,0x88,0x84,0x00
};
#endif
struct script_item {
unsigned int wait;
unsigned char entrance_fx:3;
unsigned char exit_fx:3;
unsigned char animation:2;
signed int position;
char *text;
};
enum fx_types {
fx_none = 0,
fx_left,
fx_right,
fx_up,
fx_down,
fx_fade
};
enum anim_types {
ani_none = 0,
ani_invert
};
#define SCR_MAX 15
script_item scr[SCR_MAX] = {
{ 900, fx_fade, fx_fade, ani_none, 20, "Welcome To"},
{ 15, fx_none, fx_left, ani_none, -1, "Anoka Technical College"},
{ 80, fx_down, fx_down, ani_none, 19, "Smart House"},
{ 80, fx_up, fx_up, ani_none, 20, "GREEN TEAM"},
{ 2800, fx_none, fx_none, ani_none, 27, "Presents"},
{ 2300, fx_none, fx_none, ani_none, 2, "CAPSTONE PROJECT"},
{ 2300, fx_none, fx_none, ani_none, 19, "Spring 2007"},
{ 2800, fx_none, fx_none, ani_none, 25, "Featuring"},
{ 15, fx_right,fx_up, ani_none, 27, "BECKHOFF"},
{ 15, fx_left, fx_down, ani_none, 21, "AUTOMATED"},
{ 80, fx_up, fx_down, ani_none, 9, "CONTROL SYSTEM"},
{ 800, fx_none, fx_none, ani_none, 0, ""},
{ 200, fx_fade, fx_fade, ani_none, 10, "Lighting Control"},
{ 10, fx_fade, fx_fade, ani_invert, 10, "Lighting Control"},
{ 1200, fx_none, fx_none, ani_none, 0, ""},
/*
{ 100, fx_up, fx_up, ani_none, 0, "!""#$%&'()*+,-./"},
{ 100, fx_up, fx_up, ani_none, 0, "[\\]^_':;<=>?@"},
{ 100, fx_up, fx_up, ani_none, 0, "0123456789"},
{ 100, fx_up, fx_up, ani_none, 0, "ABCDEFGHIJKLMNO"},
{ 100, fx_up, fx_up, ani_none, 0, "PQRSTUVWXYZ"},
{ 100, fx_up, fx_up, ani_none, 0, "abcdefghijklmno"},
{ 100, fx_up, fx_up, ani_none, 0, "pqrstuvwxyz{|}~"}
*/
};
// wait, entrance_fx, exit_fx, animation, position, text
unsigned char image_buffer[96];
void clear_buffer();
void draw_buffer(signed int hold);
unsigned char draw_char(char code, signed int offset, signed char bitshift);
unsigned int draw_scr(signed int offset, signed char bitshift);
void scroll_horiz(signed char direction, unsigned char mode);
void scroll_vert(signed char direction, unsigned char mode);
void setup(void) {
DDRB=0xFF;
DDRD=0xFF;
digitalWrite(POWER,HIGH);
digitalWrite(RESET,HIGH); // reset is active low, this disables it so things can be drawn
clear_buffer();
}
unsigned long int cycle = 0;
unsigned char scr_index = -1;
void loop(void) {
if (cycle<millis()) {
if (scr_index>=0) {
if (scr[scr_index].exit_fx==fx_left) {
scroll_horiz(-1,0);
} else if (scr[scr_index].exit_fx==fx_right) {
scroll_horiz(1,0);
} else if (scr[scr_index].exit_fx==fx_up) {
scroll_vert(-1,0);
} else if (scr[scr_index].exit_fx==fx_down) {
scroll_vert(1,0);
} else if (scr[scr_index].exit_fx==fx_fade) {
//clear_buffer();
//draw_scr(scr[scr_index].position,0);
for (signed char i = 80 ; i>4 ; i-- ) {
cycle=millis()+45-i/3;
while (cycle>millis()) {
draw_buffer(i);
}
}
}
}
scr_index++;
if (scr_index>=SCR_MAX) {
scr_index=0;
}
if (scr[scr_index].position==-1 && (scr[scr_index].exit_fx==fx_left || scr[scr_index].exit_fx==fx_right))
return;
if (scr[scr_index].entrance_fx==fx_none) {
clear_buffer();
draw_scr(scr[scr_index].position,0);
cycle=millis()+scr[scr_index].wait;
} else if (scr[scr_index].entrance_fx==fx_left) {
scroll_horiz(-1,1);
cycle=millis()+scr[scr_index].wait*100;
} else if (scr[scr_index].entrance_fx==fx_right) {
scroll_horiz(1,1);
cycle=millis()+scr[scr_index].wait*100;
} else if (scr[scr_index].entrance_fx==fx_up) {
scroll_vert(-1,1);
cycle=millis()+scr[scr_index].wait*20;
} else if (scr[scr_index].entrance_fx==fx_down) {
scroll_vert(1,1);
cycle=millis()+scr[scr_index].wait*20;
} else if (scr[scr_index].entrance_fx==fx_fade) {
clear_buffer();
draw_scr(scr[scr_index].position,0);
for (signed char i = -1 ; i<90 ; i++ ) {
cycle=millis()+55-i/2;
while (cycle>millis()) {
draw_buffer(i);
}
}
cycle=millis()+scr[scr_index].wait;
}
}
draw_buffer(100);
}
void clear_buffer() {
for (unsigned char i = 0 ; i<95 ; i++ )
image_buffer[i]=0;
}
void draw_buffer(signed int hold) {
for ( unsigned char row = 0 ; row<7 ; row++ ) {
writeROW(7);
for ( unsigned char col = 0 ; col<95 ; col++ ) {
MD_OUT( (scr[scr_index].animation==1) ^ ((image_buffer[col] & (1<<row))>0) );
}
writeROW(row);
if (hold>0) {
delayMicroseconds(hold);
}
}
writeROW(7);
}
unsigned char draw_char(char code, signed int offset, signed char bitshift) {
if (code<32 || code>126) {
return 0;
} else if (code==32) {
return 2;
}
unsigned int seek = 0;
for ( unsigned char cval = 33 ; cval<126 && cval<code-1 ; seek++,cval+=!pgm_read_byte(&LUCIDIA_MICRO[seek-1]) );
unsigned int pos = 0;
for ( ; (pgm_read_byte(&LUCIDIA_MICRO[pos+seek]) && (pos+seek)<LUCIDIA_MICRO_MAX) || pos<1 ; pos++ ) {
if (pos+offset>=0 && pos+offset<95) {
if (bitshift>0) {
image_buffer[pos+offset] |= ((pgm_read_byte(&LUCIDIA_MICRO[pos+seek]) &0x7F)<<bitshift);
} else {
image_buffer[pos+offset] |= ((pgm_read_byte(&LUCIDIA_MICRO[pos+seek]) &0x7F)>>(-bitshift));
}
}
}
return pos;
}
unsigned int draw_scr(signed int offset, signed char bitshift) {
unsigned int wide = 0;
for (unsigned char i = 0 ; scr[scr_index].text[i] ; i++) {
wide+=draw_char(scr[scr_index].text[i],offset+wide,bitshift)+1;
}
return wide;
}
void scroll_horiz(signed char direction, unsigned char mode) {
signed int wide=draw_scr(0,0);
signed int position = ( (scr[scr_index].position==-1) ? (-direction)*wide : scr[scr_index].position);
for ( signed int i = (mode ? (-direction)*wide+((direction<0)*96) : position+(direction>0)) ;
( (mode ? i<=position : i<=96) && direction>0) || // scroll right condition
( (mode ? i>position : i>-wide) && direction<0) ; // scroll left condition
i+=direction ) {
clear_buffer();
draw_scr(i,0);
cycle=millis()+scr[scr_index].wait;
while (cycle>millis()) {
draw_buffer(100);
}
}
}
void scroll_vert(signed char direction, unsigned char mode) {
for ( signed char i = (-direction)*8*mode ;
( (mode ? i<1 : i<7) && direction>0) || // scroll down condition
( (mode ? i>=0 : i>=-7) && direction<0) ; // scroll up condition
i+=direction ) {
clear_buffer();
draw_scr(scr[scr_index].position,i);
cycle=millis()+scr[scr_index].wait;
while (cycle>millis()) {
draw_buffer(100);
}
}
}