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!

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.

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.

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:

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!

01OUTDATA# = &H410
02OUTLINES# = &H411
03OUT &H413, 0
04OUT &H413, 0
05CLOCKBIT = 1
06DATA 30, 8
07DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
08DATA 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
09DATA 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
10DATA 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
11DATA 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
12DATA 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
13DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
14DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
15READ DATALENGTH
16READ DATAHEIGHT
17DIM DATAPIXELARRAY((DATALENGTH - 1), (DATAHEIGHT - 1)) AS INTEGER
18FOR Y = 0 TO (DATAHEIGHT - 1)
19    FOR X = 0 TO (DATALENGTH - 1)
20        READ DATAPIXELARRAY(X, Y)
21    NEXT X
22NEXT Y
23DO
24    FOR ROWS = 0 TO (DATAHEIGHT - 1)
25        FOR DOT = 0 TO (DATALENGTH - 1)
26            BIT = DATAPIXELARRAY(DOT, ROWS)
27            OUT OUTDATA#, BIT
28            OUT OUTDATA#, (BIT + CLOCKBIT)
29            OUT OUTDATA#, 0
30        NEXT DOT
31        OUT OUTLINES#, 255 - (2 ^ ROWS)
32        FOR i = 0 TO 10
33        NEXT i
34        OUT OUTLINES#, 255
35    NEXT ROWS
36LOOP UNTIL INKEY$ = "q"

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.

01OUTDATA# = &H410
02OUTLINES# = &H411
03OUT &H413, 0
04OUT &H413, 0
05CLOCKBIT = 1
06 
07DATA 30, 8
08DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
09DATA 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
10DATA 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
11DATA 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
12DATA 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
13DATA 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
14DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
15DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
16 
17READ DATALENGTH
18READ DATAHEIGHT
19DIM DATAPIXELARRAY((DATALENGTH - 1), (DATAHEIGHT - 1)) AS INTEGER
20FOR Y = 0 TO (DATAHEIGHT - 1)
21   FOR X = 0 TO (DATALENGTH - 1)
22      READ DATAPIXELARRAY(X, Y)
23   NEXT X
24NEXT Y
25 
26DIM SCROLLOFFSET AS INTEGER
27DIM LOOPCOUNTER AS INTEGER
28 
29SCROLLOFFSET = 0
30LOOPCOUNTER = 0
31 
32DO
33    FOR ROWS = 0 TO (DATAHEIGHT - 1)
34        FOR DOT = SCROLLOFFSET TO (DATALENGTH - 1)
35            BIT = DATAPIXELARRAY(DOT, ROWS)
36            OUT OUTDATA#, BIT
37            OUT OUTDATA#, (BIT + CLOCKBIT)
38            OUT OUTDATA#, 0
39        NEXT DOT
40        FOR DOT = 0 TO (SCROLLOFFSET - 1)
41            BIT = DATAPIXELARRAY(DOT, ROWS)
42            OUT OUTDATA#, BIT
43            OUT OUTDATA#, (BIT + CLOCKBIT)
44            OUT OUTDATA#, 0
45        NEXT DOT
46        OUT OUTLINES#, 255 - (2 ^ ROWS)
47        FOR i = 0 TO 10
48        NEXT i
49        OUT OUTLINES#, 255
50    NEXT ROWS
51    LOOPCOUNTER = LOOPCOUNTER + 1
52    IF LOOPCOUNTER > 10 THEN
53        LOOPCOUNTER = 0
54        SCROLLOFFSET = SCROLLOFFSET + 1
55        IF SCROLLOFFSET > DATALENGTH THEN
56            SCROLLOFFSET = 0
57        END IF
58    END IF
59LOOP 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.

01OUTDATA# = &H410
02OUTLINES# = &H411
03OUT &H413, 0
04OUT &H413, 0
05CLOCKBIT = 1
06 
07DIM ACTUALDISPLAYWIDTH AS INTEGER
08ACTUALDISPLAYWIDTH = 60
09 
10DATA 30, 8
11DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
12DATA 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
13DATA 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
14DATA 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
15DATA 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
16DATA 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
17DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
18DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
19 
20READ DATALENGTH
21READ DATAHEIGHT
22DIM DATAPIXELARRAY((DATALENGTH - 1), (DATAHEIGHT - 1)) AS INTEGER
23FOR Y = 0 TO (DATAHEIGHT - 1)
24   FOR X = 0 TO (DATALENGTH - 1)
25      READ DATAPIXELARRAY(X, Y)
26   NEXT X
27NEXT Y
28 
29DIM SCROLLOFFSET AS INTEGER
30DIM LOOPCOUNTER AS INTEGER
31 
32SCROLLOFFSET = 0
33LOOPCOUNTER = 0
34 
35DO
36    FOR ROWS = 0 TO (DATAHEIGHT - 1)
37        FOR DOT = SCROLLOFFSET TO (ACTUALDISPLAYWIDTH - 1)
38            IF (DOT >= DATALENGTH) THEN
39                BIT = 0
40            ELSE
41                BIT = DATAPIXELARRAY(DOT, ROWS)
42            END IF
43            OUT OUTDATA#, BIT
44            OUT OUTDATA#, (BIT + CLOCKBIT)
45            OUT OUTDATA#, 0
46        NEXT DOT
47        FOR DOT = 0 TO (SCROLLOFFSET - 1)
48            IF (DOT >= DATALENGTH) THEN
49                BIT = 0
50            ELSE
51                BIT = DATAPIXELARRAY(DOT, ROWS)
52            END IF
53            OUT OUTDATA#, BIT
54            OUT OUTDATA#, (BIT + CLOCKBIT)
55            OUT OUTDATA#, 0
56        NEXT DOT
57        OUT OUTLINES#, 255 - (2 ^ ROWS)
58        FOR i = 0 TO 10
59        NEXT i
60        OUT OUTLINES#, 255
61    NEXT ROWS
62    LOOPCOUNTER = LOOPCOUNTER + 1
63    IF LOOPCOUNTER > 5 THEN
64        LOOPCOUNTER = 0
65        SCROLLOFFSET = SCROLLOFFSET + 1
66        IF SCROLLOFFSET >= ACTUALDISPLAYWIDTH THEN
67            SCROLLOFFSET = 0
68        END IF
69    END IF
70LOOP 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...

01OUTDATA# = &H410
02OUTLINES# = &H411
03OUT &H413, 0
04OUT &H413, 0
05CLOCKBIT = 1
06 
07DIM ACTUALDISPLAYWIDTH AS INTEGER
08ACTUALDISPLAYWIDTH = 90
09 
10DATA 104, 8
11DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
12DATA 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
13DATA 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
14DATA 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
15DATA 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
16DATA 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
17DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
18DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
19 
20READ DATALENGTH
21READ DATAHEIGHT
22DIM DATAFONTARRAY((DATALENGTH - 1), (DATAHEIGHT - 1)) AS INTEGER
23FOR y = 0 TO (DATAHEIGHT - 1)
24   FOR X = 0 TO (DATALENGTH - 1)
25      READ DATAFONTARRAY(X, y)
26   NEXT X
27NEXT y
28 
29DIM ACTUALPIXELBUFFER(ACTUALDISPLAYWIDTH - 1, DATAHEIGHT - 1) AS INTEGER
30 
31DIM SCROLLOFFSET AS INTEGER
32DIM LOOPCOUNTER AS INTEGER
33 
34SCROLLOFFSET = 0
35LOOPCOUNTER = 0
36 
37DO
38    FOR ROWS = 0 TO (DATAHEIGHT - 1)
39        FOR DOT = 0 TO (ACTUALDISPLAYWIDTH - 1)
40            BIT = ACTUALPIXELBUFFER(DOT, ROWS)
41            OUT OUTDATA#, BIT
42            OUT OUTDATA#, (BIT + CLOCKBIT)
43            OUT OUTDATA#, 0
44        NEXT DOT
45        OUT OUTLINES#, 255 - (2 ^ ROWS)
46        FOR i = 0 TO 10
47        NEXT i
48        OUT OUTLINES#, 255
49    NEXT ROWS
50    LOOPCOUNTER = LOOPCOUNTER + 1
51    pressedchar$ = INKEY$
52    IF (LEN(pressedchar$) = 1) THEN
53        lastchar = ASC(pressedchar$)
54        lastchar = lastchar - 97
55        IF (lastchar >= 0) THEN
56            FOR c = 0 TO 3
57                FOR y = 0 TO 6
58                    ACTUALPIXELBUFFER(c + offset, y) = DATAFONTARRAY((lastchar * 4) + c, y)
59                NEXT y
60            NEXT c
61            offset = offset + 4
62            IF ((offset + 4) >= ACTUALDISPLAYWIDTH) THEN
63                offset = 0
64            END IF
65 
66        END IF
67    ELSE
68        REM lastchar = ASC(RIGHT$(pressedchar$, 1))
69    END IF
70    PRINT lastchar
71    IF LOOPCOUNTER > 200 THEN
72        LOOPCOUNTER = 0
73        SCROLLOFFSET = SCROLLOFFSET + 1
74    END IF
75LOOP WHILE INKEY$ <> "."

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

Comments (0) Trackbacks (2)

No comments yet.


Leave a comment

*