Subscribe via RSS
17Oct/190

Red LED Shop Display

Found this thing at the flea markets on the weekend. It's a standard LED marquee/shop display with two cables hanging out one side. One looks like a power cable, the other data with an RJ45 phone plug on the end. Manufactured by Data Signs Australia, there's no mention of this relic on their site... seems they only care about road signs now!

DSC01945

As you can see, it's what's on the inside that counts. At the end there seems to be a controller board with an Atmel microcontroller. This is then connected to five display modules, all on a standard bus. The build of the headers is nice as it all connects seamlessly together to give one full display. It looks like the wiring of the phone jack goes straight into a MAX232... so we can talk serial to this thing. I'll do that later though, I'm more interested in the display modules.

DSC01957

DSC01959 DSC01960 DSC01967

DSC01969

DSC01965 DSC01966 DSC01971

Each module contains six 5x7 LED matrices. These are powered by TIP125 transistors and data is driven by MC14015BCP shift registers connected in series. The data bus on the side has 2 lines at either end for VCC and GND. The pins in the middle are then Clock, Data, Reset (for the shift registers) and then enable lines for the 7 matrix rows.

DSC01940

Hacking started in earnest to work out the pinout...

Lighting it up

For any LED matrix, the basic idea is to send data really quickly to them and 'emulate' that all LEDs are on at one time. Whenever you try to take photos of LED lights, be that traffic lights, train destination boards, etc... you'll find that, unless your shutter speed is slow, you don't get the entire set of 'lit' LEDs visible at once. Here's a good example:

DSC08481

Not the clearest example... blame the snow in early November... but you can see that the LEDs in the destination board aren't of a consistent illumination. The microcontroller is still busy rendering the screen and the frame is 75% drawn.

Microcontrollers 'print' the LED signal across the matrices to get them to light up. They do this so quickly, that to the naked eye, the LEDs are always lit. We're going to have to do this here as well. I've decided to use that huge IO board I investigated recently to drive this thing.

Based on the datasheet for the MC14015BCP shift registers, as the clock signal is driven high, whatever is on the data line will be fed into the first parallel data out line. These shift registers contain two sets of 4-bit outputs. Following the wiring on the card, the final bit of the first 4-bit output is chained to the data input of the second register. The final bit of that is then chained to the next shift register IC's data in. The clocks are all tied together, meaning that any data fed in to the clock/data lines on the bus at the side of the board will eventually trickle down all the way through the board, and then to any boards attached further. Quite the shift-register-centipede!

Writing some code...

Thanks to the previous work with this IO card, the interfacing would be quite simple. Clock, Data and Reset would be pins 1,2,3 on the first block-of-eight at the base IO address. I then put the 7 row-enable pins on the second block-of-eight. This meant less bit-twiddling. QBasic allows a nice mechanism to record data that can then be read into arrays. The data happens to be the pin value of the data pin, so here "2" will set the second pin HIGH when sent to the port, meaning that the data line will be high for the shift registers and therefore illuminating the LED in that column. I then just need to enable the first pin briefly to send through that number. Of course, I'd send a 0 (or LOW) to pin 2 to send a 0 through the shift registers, thereby turning that column's LED off.

The above needs to be done for each row. i.e. I have to send out 30 values (length of one row on one module) then 'flicker' the enable row. Then I need to send the next 30 values and 'flicker' the second enable row. The speed at which the row enable pin is 'flickered' is also an issue... if it's too slow, then you just get one line of LEDs and it's very jittery... but if it's too fast the LEDs start to dim, as they're starved of power to actually illuminate!

OUTDATA# = &H410
OUTLINES# = &H411
OUT &H413, 0
OUT &H413, 0
CLOCKBIT = 1
DATA 30, 8
DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
DATA 0,0,0,0,0,0,2,2,2,0,2,2,2,0,2,2,2,0,2,2,2,0,2,0,0,0,0,0,0,0
DATA 0,0,0,0,0,0,0,2,0,0,2,0,0,0,2,0,0,0,0,2,0,0,2,0,0,0,0,0,0,0
DATA 0,0,0,0,0,0,0,2,0,0,2,2,2,0,2,2,2,0,0,2,0,0,2,0,0,0,0,0,0,0
DATA 0,0,0,0,0,0,0,2,0,0,2,0,0,0,0,0,2,0,0,2,0,0,0,0,0,0,0,0,0,0
DATA 0,0,0,0,0,0,0,2,0,0,2,2,2,0,2,2,2,0,0,2,0,0,2,0,0,0,0,0,0,0
DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
READ DATALENGTH
READ DATAHEIGHT
DIM DATAPIXELARRAY((DATALENGTH - 1), (DATAHEIGHT - 1)) AS INTEGER
FOR Y = 0 TO (DATAHEIGHT - 1)
	FOR X = 0 TO (DATALENGTH - 1)
		READ DATAPIXELARRAY(X, Y)
	NEXT X
NEXT Y
DO
	FOR ROWS = 0 TO (DATAHEIGHT - 1)
		FOR DOT = 0 TO (DATALENGTH - 1)
			BIT = DATAPIXELARRAY(DOT, ROWS)
			OUT OUTDATA#, BIT
			OUT OUTDATA#, (BIT + CLOCKBIT)
			OUT OUTDATA#, 0
		NEXT DOT
		OUT OUTLINES#, 255 - (2 ^ ROWS)
		FOR i = 0 TO 10
		NEXT i
		OUT OUTLINES#, 255
	NEXT ROWS
LOOP UNTIL INKEY$ = "q"

DSC01951

The only real trickery in the code above is the two zeroes sent out to 0x413 at the start... it seemed to be required to get the IO card to treat the ports as output and pull them to ground by default. Otherwise, data is defined and loaded into the array. A non-stop loop (unless you press "q") is then started where we load a single row of dots and then trigger the row enable pin. Each row-enable pin needs to be brought low to activate the LEDs, hence the 255 - (2 ^ rownumber). Finally, that crappy little for-loop is there to provide a mini delay so that the LEDs get a chance to light up.

Let's make a marquee

Right, a display of static pixels is fine, but what if you want one of those really ugly perpetually-scrolling marquees that are seen in every $2 (100-yen) shop? Should be pretty simple, just throw in an offset and increment it at every loop. That'll work, but then once it's off the screen it'll start back at '0' which happens to be fully-on-screen. That jump is a little jarring, so instead you need to actually shift it width-of-data off the start. For now, we'll just wrap it without padding in the middle.

OUTDATA# = &H410
OUTLINES# = &H411
OUT &H413, 0
OUT &H413, 0
CLOCKBIT = 1

DATA 30, 8
DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
DATA 0,0,0,0,0,0,2,2,2,0,2,2,2,0,2,2,2,0,2,2,2,0,2,0,0,0,0,0,0,0
DATA 0,0,0,0,0,0,0,2,0,0,2,0,0,0,2,0,0,0,0,2,0,0,2,0,0,0,0,0,0,0
DATA 0,0,0,0,0,0,0,2,0,0,2,2,2,0,2,2,2,0,0,2,0,0,2,0,0,0,0,0,0,0
DATA 0,0,0,0,0,0,0,2,0,0,2,0,0,0,0,0,2,0,0,2,0,0,0,0,0,0,0,0,0,0
DATA 0,0,0,0,0,0,0,2,0,0,2,2,2,0,2,2,2,0,0,2,0,0,2,0,0,0,0,0,0,0
DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0

READ DATALENGTH
READ DATAHEIGHT
DIM DATAPIXELARRAY((DATALENGTH - 1), (DATAHEIGHT - 1)) AS INTEGER
FOR Y = 0 TO (DATAHEIGHT - 1)
   FOR X = 0 TO (DATALENGTH - 1)
      READ DATAPIXELARRAY(X, Y)
   NEXT X
NEXT Y

DIM SCROLLOFFSET AS INTEGER
DIM LOOPCOUNTER AS INTEGER

SCROLLOFFSET = 0
LOOPCOUNTER = 0

DO
	FOR ROWS = 0 TO (DATAHEIGHT - 1)
		FOR DOT = SCROLLOFFSET TO (DATALENGTH - 1)
			BIT = DATAPIXELARRAY(DOT, ROWS)
			OUT OUTDATA#, BIT
			OUT OUTDATA#, (BIT + CLOCKBIT)
			OUT OUTDATA#, 0
		NEXT DOT
		FOR DOT = 0 TO (SCROLLOFFSET - 1)
			BIT = DATAPIXELARRAY(DOT, ROWS)
			OUT OUTDATA#, BIT
			OUT OUTDATA#, (BIT + CLOCKBIT)
			OUT OUTDATA#, 0
		NEXT DOT
		OUT OUTLINES#, 255 - (2 ^ ROWS)
		FOR i = 0 TO 10
		NEXT i
		OUT OUTLINES#, 255
	NEXT ROWS
	LOOPCOUNTER = LOOPCOUNTER + 1
	IF LOOPCOUNTER > 10 THEN
		LOOPCOUNTER = 0
		SCROLLOFFSET = SCROLLOFFSET + 1
		IF SCROLLOFFSET > DATALENGTH THEN
			SCROLLOFFSET = 0
		END IF
	END IF
LOOP UNTIL INKEY$ = "q"

TEST!

I'm sure there's some MOD function I can use to prevent the need for two loops, but it works! Of course, it only works as long as the data is the length of the screen! What happens when I hook up the other modules?...

Oh... that's no good... it's offsetting by 1 row per module? What's the go there? Let's edit the code to fill the entire row.

OUTDATA# = &H410
OUTLINES# = &H411
OUT &H413, 0
OUT &H413, 0
CLOCKBIT = 1

DIM ACTUALDISPLAYWIDTH AS INTEGER
ACTUALDISPLAYWIDTH = 60

DATA 30, 8
DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
DATA 0,0,0,0,0,0,2,2,2,0,2,2,2,0,2,2,2,0,2,2,2,0,2,0,0,0,0,0,0,0
DATA 0,0,0,0,0,0,0,2,0,0,2,0,0,0,2,0,0,0,0,2,0,0,2,0,0,0,0,0,0,0
DATA 0,0,0,0,0,0,0,2,0,0,2,2,2,0,2,2,2,0,0,2,0,0,2,0,0,0,0,0,0,0
DATA 0,0,0,0,0,0,0,2,0,0,2,0,0,0,0,0,2,0,0,2,0,0,0,0,0,0,0,0,0,0
DATA 0,0,0,0,0,0,0,2,0,0,2,2,2,0,2,2,2,0,0,2,0,0,2,0,0,0,0,0,0,0
DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0

READ DATALENGTH
READ DATAHEIGHT
DIM DATAPIXELARRAY((DATALENGTH - 1), (DATAHEIGHT - 1)) AS INTEGER
FOR Y = 0 TO (DATAHEIGHT - 1)
   FOR X = 0 TO (DATALENGTH - 1)
      READ DATAPIXELARRAY(X, Y)
   NEXT X
NEXT Y

DIM SCROLLOFFSET AS INTEGER
DIM LOOPCOUNTER AS INTEGER

SCROLLOFFSET = 0
LOOPCOUNTER = 0

DO
	FOR ROWS = 0 TO (DATAHEIGHT - 1)
		FOR DOT = SCROLLOFFSET TO (ACTUALDISPLAYWIDTH - 1)
			IF (DOT >= DATALENGTH) THEN
				BIT = 0
			ELSE
				BIT = DATAPIXELARRAY(DOT, ROWS)
			END IF
			OUT OUTDATA#, BIT
			OUT OUTDATA#, (BIT + CLOCKBIT)
			OUT OUTDATA#, 0
		NEXT DOT
		FOR DOT = 0 TO (SCROLLOFFSET - 1)
			IF (DOT >= DATALENGTH) THEN
				BIT = 0
			ELSE
				BIT = DATAPIXELARRAY(DOT, ROWS)
			END IF
			OUT OUTDATA#, BIT
			OUT OUTDATA#, (BIT + CLOCKBIT)
			OUT OUTDATA#, 0
		NEXT DOT
		OUT OUTLINES#, 255 - (2 ^ ROWS)
		FOR i = 0 TO 10
		NEXT i
		OUT OUTLINES#, 255
	NEXT ROWS
	LOOPCOUNTER = LOOPCOUNTER + 1
	IF LOOPCOUNTER > 5 THEN
		LOOPCOUNTER = 0
		SCROLLOFFSET = SCROLLOFFSET + 1
		IF SCROLLOFFSET >= ACTUALDISPLAYWIDTH THEN
			SCROLLOFFSET = 0
		END IF
	END IF
LOOP UNTIL INKEY$ = "q"

So now we have a ACTUALDISPLAYWIDTH variable that tells us how much space we actually need to fill. Using the SCROLLOFFSET, we then start printing out the data array and, if we're past it then just zeroes. Once we've gotten to our full width, we then go back to the array and attack it from the start.

And if we increase to three panels and up the width to 90?

Gosh, it's getting slow loading that full row each time... maybe time to switch to an Arduino? Before we do that... here's a typewriter...

OUTDATA# = &H410
OUTLINES# = &H411
OUT &H413, 0
OUT &H413, 0
CLOCKBIT = 1

DIM ACTUALDISPLAYWIDTH AS INTEGER
ACTUALDISPLAYWIDTH = 90

DATA 104, 8
DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
DATA 0,2,2,2,0,2,2,0,0,2,2,2,0,2,2,0,0,2,2,2,0,2,2,2,0,2,2,2,0,2,0,2,0,2, 2,2,0,0,2,2,0,2,0,2,0,2,0,0,0,2,0,2,0,2,2,0,0,0,2,0,0,2,2,2,0,2,2,2,0,2, 2,2,0,2,2,2,0,2,2,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,2,2
DATA 0,2,0,2,0,2,0,2,0,2,0,0,0,2,0,2,0,2,0,0,0,2,0,0,0,2,0,0,0,2,0,2,0,0, 2,0,0,0,0,2,0,2,0,2,0,2,0,0,0,2,2,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2, 0,2,0,2,0,0,0,0,2,0,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,0,0,2
DATA 0,2,2,2,0,2,2,0,0,2,0,0,0,2,0,2,0,2,2,2,0,2,2,2,0,2,0,2,0,2,2,2,0,0, 2,0,0,0,0,2,0,2,2,0,0,2,0,0,0,2,0,2,0,2,0,2,0,2,0,2,0,2,2,2,0,2,0,2,0,2, 2,2,0,2,2,2,0,0,2,0,0,2,0,2,0,2,0,2,0,2,2,2,0,0,2,0,0,0,2,0,0,0,2,0
DATA 0,2,0,2,0,2,0,2,0,2,0,0,0,2,0,2,0,2,0,0,0,2,0,0,0,2,0,2,0,2,0,2,0,0, 2,0,0,2,0,2,0,2,0,2,0,2,0,0,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,0,0,2,2,2,0,2, 2,0,0,0,0,2,0,0,2,0,0,2,0,2,0,2,0,2,0,2,2,2,0,2,0,2,0,0,2,0,0,2,0,0
DATA 0,2,0,2,0,2,2,0,0,2,2,2,0,2,2,0,0,2,2,2,0,2,0,0,0,2,2,2,0,2,0,2,0,2, 2,2,0,0,2,0,0,2,0,2,0,2,2,2,0,2,0,2,0,2,0,2,0,0,2,0,0,2,0,0,0,2,2,2,0,2, 0,2,0,2,2,2,0,0,2,0,0,2,2,2,0,0,2,0,0,2,0,2,0,2,0,2,0,0,2,0,0,2,2,2
DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0

READ DATALENGTH
READ DATAHEIGHT
DIM DATAFONTARRAY((DATALENGTH - 1), (DATAHEIGHT - 1)) AS INTEGER
FOR y = 0 TO (DATAHEIGHT - 1)
   FOR X = 0 TO (DATALENGTH - 1)
      READ DATAFONTARRAY(X, y)
   NEXT X
NEXT y

DIM ACTUALPIXELBUFFER(ACTUALDISPLAYWIDTH - 1, DATAHEIGHT - 1) AS INTEGER

DIM SCROLLOFFSET AS INTEGER
DIM LOOPCOUNTER AS INTEGER

SCROLLOFFSET = 0
LOOPCOUNTER = 0

DO
	FOR ROWS = 0 TO (DATAHEIGHT - 1)
		FOR DOT = 0 TO (ACTUALDISPLAYWIDTH - 1)
			BIT = ACTUALPIXELBUFFER(DOT, ROWS)
			OUT OUTDATA#, BIT
			OUT OUTDATA#, (BIT + CLOCKBIT)
			OUT OUTDATA#, 0
		NEXT DOT
		OUT OUTLINES#, 255 - (2 ^ ROWS)
		FOR i = 0 TO 10
		NEXT i
		OUT OUTLINES#, 255
	NEXT ROWS
	LOOPCOUNTER = LOOPCOUNTER + 1
	pressedchar$ = INKEY$
	IF (LEN(pressedchar$) = 1) THEN
		lastchar = ASC(pressedchar$)
		lastchar = lastchar - 97
		IF (lastchar >= 0) THEN
			FOR c = 0 TO 3
				FOR y = 0 TO 6
					ACTUALPIXELBUFFER(c + offset, y) = DATAFONTARRAY((lastchar * 4) + c, y)
				NEXT y
			NEXT c
			offset = offset + 4
			IF ((offset + 4) >= ACTUALDISPLAYWIDTH) THEN
				offset = 0
			END IF

		END IF
	ELSE
		REM lastchar = ASC(RIGHT$(pressedchar$, 1))
	END IF
	PRINT lastchar
	IF LOOPCOUNTER > 200 THEN
		LOOPCOUNTER = 0
		SCROLLOFFSET = SCROLLOFFSET + 1
	END IF
LOOP WHILE INKEY$ <> "."

Hah... that worked better than expected...

Comments (0) Trackbacks (1)

No comments yet.


Leave a comment


*