One-chip 11x10 LED matrix.

This project is pretty cool for a few reasons, and driving a huge LED matrix with a single 8-bit controller is just one of them. The idea was born when I bought 120 LEDs of the wrong type, and decided to do something with them. With that many LEDs, there are only a few things you can do, and a matrix is the natural first-place-winner in the competition of those ideas. One of the LEDs did not work, so a 12x10 matrix was out, so I settled for an 11x10 matrix. This meant I had to drive 110 LEDs. The only controller I had free was a PIC16F688 with 11 pins that can be used for output. After deciding not to use any other chips, charlieplexing was the way to go. The maximum number of LEDs one can charlieplex using N pins is N * (N - 1), so for 11 pins that number is 110. What a coincidence! :)

The problem with charlieplexing is that for any two arbitrary LEDs, only one may be lit at a time. While it is true that some combinations of LEDs can be lit simultaneously. Not all are possible. Thus to use this matrix to show an image, one has to scan trough it rather quickly and turn each LED that needs to be on in succession. A slight nit: If one only scans through LEDs that need to be on, the more LEDs are lit, the dimmer they will be. This is annoying, so always scan through them all, but do not turn some on. This way timing is preserved and all lit LEDs always have the same duty cycle (1/110) regardless of how many of them are actually lit. How fast do you need to scan? Well, you'll be relying on persistence-of-vision to make them appear solidly lit. For human eye to have this illusion, each LED needs to turn on at least 25 times a second. I chose about 30 times a second as a good value. This means that given the fact that we have 25 LEDs total, we need to switch which LED is on 30x110 = 3300 times a second. That's a lot! I do not want to write my code with that constraint in mind, so interrupts are used. On PIC16F688 (8 MHz = 2 MIPS) this actually means that interrupts will be happening quite often. Timer0 is used with no prescaler (so it increments once per instruction), and only 32 instructions are allowed to go by between the end of one interrupt handler and the beginning of the next. So in technical terms: timer0 overflow interrupt updated which LED is on, and at the end reloads timer0 with 0xD0, causing it to overflow 0x20 instructions later. On PIC16F1823 this is simpler since it runs at 32MHz ( = 8 MIPS). Here timer0 uses a 1/8 prescaler and is reloaded with 0x60.

Shematic? Don't be silly. Make a 110 LED charlieplex matrix, connect its 11 control wires to: all 6 pins of of PORTC and all 6 pins of PORTA except for RA3. You're done. Now fill-in it led2wire[] array to match you LED connections.

Having display be interrupt driven, while giving very little CPU time to the main code, allows the main code itself to take its time to produce the image to display. I wrote a few test functions to draw different things, but settled on the current marquee app. It uses a simple bitmapped font to render a "clock-like" demo text onscreen and scrolls it at a random vertical position. while this demo only uses numbers and puctuation, other characters are also supported by my font renderer, in fact all characters in the printable ASCII range are: 0x20-0x7F inclusive. Any other code that produces an 11x10 image would work too, however.

PIC16F688 does have one advantage over PIC16F1823 - twice as much ram. This allows me to double-buffer the screen, providing a rathr cool set of APIs to the drawing code: appBufGet() returns a pointer to the front or back buffer. appBufFlip() flips the meaning of "front" and "back" buffer, thus instantaneously and taer-freely replacing the current screen image with the one in the backbuffer. appBufCopy() copies back buffer to the front buffer. This is not tear-free, BUT has uses for apps that cannot cope with buffer flipping. appGetFrameNo() returns the current frame number as an 8-bit counter that overflow to 0 at 256. appReadRtc() returns the current uptime of the chip, in units of RTC_TICKS_PER_SEC. The main iscillator is used so do not expect this to be exact over months-long time. appGetRand() returns a random 16-bit number using the ADC noise as the source of entropy. PIC16F1823 does not have enough RAM for screen double-buffering, but being faster, it does not have the tearing problem as the main code gets a LOT more cycles to produce each frame between screen redraw interrupts.

PIC16F688 has disadvantages too: it is slow. Getting the performance up on the PIC16F688 was difficult, so quite a few lookup tables were used. One converts a LED number to a pair of wires that represent which wires it is connected to. A few tables then convert each wire into TRIS and PORT values needed to set that wire. And another table stores bit shifts. Since PICs lack an instruction to shift a value left or right an arbitrary number of times, it is faster to do table[x] than (1 << x). That first table (LED -> wires) is the one you'll have to edit when you wire up your matrix. It has 55 entries (since each two consecutive LEDs are wired in parallel but in reverse order, we do not need 110 entries, just 55. Each entry there is one byte. Top nibble is the wire we need to pull high, bottom is the wire we need to pull low to light that LED. Since we have 11 wires, that fits nicely in a nibble. The last entry in that table is 0xBB meaning wire 11 up and wire 11 down. Since our wires are wired 0-10, why this? Well, as mentioned before, to avoid fewer LEDs lit leading to brighter LEDs we always need to do the same thing, and thus in spirit of that we create (in our imagination) a 112-th LED. It is "wired up" to nonexistent wire 11. Wire tables also have this wire. TRIS tables say to set all pins to INPUT. This allows us to always "light" an LED each cycle whether it be a real one that need to be lit, or a fake one if the real one doe snot need ot be lit.

As you can see in the video - it all works well. Code can be downloaded here: [DOWNLOAD]. Feedback is always welcome in [EMAIL] form.

© 2012-2022