Introduction: Hundreds of LEDs on Arduino: a New Way From the Past

Finally managed to put a universal library for DM633/DM634 family together; time to share it with the world.

The DM63x family is a bunch of cheap and useful LED drivers from an obscure Taiwanese manufacturer called SiTI. I stumbled upon these drivers by chance, spent a year testing them and building some projects with them, designing boards to accommodate them and now I’m rather sure these chips are almost perfect for the hobbyist purposes that do not involve huge LED matrixes or cubes. Unlike the now-popular programmable LEDs like WS2812, this is the old-fashioned way of controlling LEDs; any kind of an LED can be used here. The drivers are rather old (like 8 years or so), but no library was ever made for an Arduino. Up until now I just included all the driver controlling functions in the sketch – it was cumbersome, not compatible with different drivers and not easily shared. The library solves it; it can even work with 12- and 16-bit drivers simultaneously.

This instructable is also a kind of support for my UltiBlink site, but you’ll be able to make your own working prototyping contraption with the following:

- an Arduino;

- a LED driver chip (DM631,632,633 or 634) in SOP, SSOP or TSSOP package (not SOPB);

- an SOP/SSOP – DIP adapter (breakout) board with at least 24 pins;

- some soldering skills or another way to solder the last two together.

I’ll start this instructable with some basic knowledge about LED drivers in general, continue with DM63x particulars and then go on to the library; so if you’re already familiar with these you should skip to step 4.

Step 1: LED Driver?

The common LED driving chips (not to be confused with LED power supplies, also named LED drivers) are in fact just current-sinking shift registers with inbuilt resistors. Unlike the usual shift registers they do not provide voltage on their outputs, but sink it (hence the name). Their outputs are physical inputs. This means the LED cathodes are connected to them, which also means only common anode RGB LEDs can be used. The inbuilt resistors limit the current going through each channel; pretty useful with LEDs, as you won’t need a resistor per each channel, just one resistor per LED driver (used as a reference). This is the reason they are called the LED drivers: while these chips can be used in most applications where a shift register is needed, they are more expensive, thus most useful when the price increase is justified by the design efficiency, namely when used to drive LEDs.

Like the shift registers, these common LED drivers are binary, meaning they have a single control bit for each channel that can be either open or closed. If you want something more interesting, namely a partial brightness for a single color, you have to generate an appropriate PWM signal somewhere else and keep it running to the LED driver at all times (refreshing it). In Arduino world, the ShiftPWM library works with these common LED drivers. Still, this eats resources of the microcontroller that, as in the case of Atmega328, is not exactly a powerhouse. And most of the time, you need it to do something else as well.

Thus, the TLC5940 became popular in the world of Arduino. Unlike most ‘common’ LED drivers the drivers from TI can generate PWM signals themselves, thus keeping the desired modulated brightness of connected LEDs without a constant data stream from the controller. These drivers take more than one bit per channel on the input; the famous TLC5940, in fact, is 12-bit, so you send it a string of ones and zeros forming numbers from 0 to 4096 per each channel. You still have to provide this chip with the external PWM frequency – this is essential for multiplexing, the mode of operation it was designed for.

The DM63x line of LED drivers goes even further than that – these are equipped with their own 'free-runnung' ~18MHz PWM engine, meaning you don’t have to give them an external PWM frequency (although it is possible). Thus, the number of connections required to actually control these LED drivers is reduced to the bare minimum of three: clock, data, and latch (or chip select in SPI terms). It also means that all your Arduino timers are free, as none of them is used to provide PWM reference clock. Excellent! So, where’s the catch? Well, these chips don’t fare as well in the multiplexing environment (although multiplexing is possible, but that's a topic for some future article). But the ‘free-running PWM’ is excellent for the hobbyist crowd that just needs to light up some LEDs, not build a high-res billboard full of them.

There are four chips in the line. DM631 and DM633 are 12-bit while DM632 and DM634 are 16-bit. DM633 and DM634 also feature a 7-bit global brightness control.

Like the shift registers, LED drivers can be chained, increasing the number of outputs. LED drivers with PWM capabilities can be used with anything that requires PWM signals, not just LEDs.

Step 2: The SMD Problem

DM63x chips are somewhat known in hobbyist electronics circles, I’ve read about some projects using them (most notably the Lightpack – yep, it was built with the DM631 chip), but they aren’t as widespread as the TLC5940 chip. Two reasons: the manufacturer is obscure and not present in places like Digikey; and they are available only in SMD, or surface mount, packages. In fact, the genuine TLC5940 in a breadboard-friendly DIP package is also extinct now, but at least you can get a fake one on eBay. Same eBay can offer DM63x chips too – and genuine ones by the way, as no one fakes them.

Still, the SMD package can be a problem for a hobbyist although this is a problem that is rather rewarding to solve. After all, most of the modern interesting chips come in SMD packages, so the equipment and skills to use them in your projects will come in handy. And DM63x LED drivers can be a good place to start.

To use one on a breadboard you just need to solder it on a standard SOP-DIP breakout board (also called a connector or an adapter board). There are a lot of instructions on how to solder an SOP/SSOP chip (a useful skill), but I guess you can also ask someone else to do it.

There is one nasty catch here, however. These chips come in three package sizes: SOP, SOP-B, and SSOP/TSSOP. The usual two-sided SOP-DIP connector is compatible with both SOP and SSOP/TSSOP packages. I never saw a connector for the unusual SOP-B package. Problem is, the sellers on eBay and similar sites have absolutely no idea of the exact package they are offering. They usually just copy-paste descriptions and photos from each other, so mostly they are selling ‘SOP packages’ that are in fact anything but. You may try to ask them, but I personally am yet to receive any coherent answer. My experience shows that 12-bit DM633s are more prone to be in SOP-B packages, and 16-bit DM634s usually come in SSOP packages. I never actually saw any in SOP (the biggest one) package and I have doubts they are actually produced nowadays, so SSOP is your best bet. By the way, TSSOP package has a thermal pad on the bottom, but with DM chips you can ignore it.

The manufacturer can sell you the exact chip in an exact package if you order 60+ pieces.

You can also, of course, just get one of my UltiBlink boards.

Step 3: Meet the Chip

The nice thing about DM chips is the ease of connection (and actually remembering their layout). Each has 24 pins in the same configuration. The first one is ground. Then come three main data lines: DATA_IN, CLOCK, and LATCH. You program an LED driver by sending a stream of 12- or 16-bit integers into the DATA_IN line. The CLOCK line allows the driver to discern separate bits in that stream: it ‘catches’ the current state of the DATA_IN line as 0 or 1 at either the falling or the rising edge of the CLOCK signal. The LATCH is used to copy the current data in the stream into the internal memory, which directly controls outputs. While LATCH is low, the stream of data just flows through all the connected chips; each next incoming bit pushes the string forward. Once the LATCH goes high, the data is latched and kept. Thus, you send the values for all connected LED drivers keeping LATCH low, then get it high – and bingo, you have some lights on.

Pins 5 to 20 are outputs, you connect LED cathodes to them.

21 is SOMODE/GCK. By default, this selects the edge of the CLOCK signal to use – rising or falling. It can also be used to provide external PWM frequency to the driver. In practice, you can just let this leg hang in the air, but it is better to connect it to ground.

22 is DATA_OUT – you connect it to the next driver’s DATA_IN.

23 is REXT pin: here sits the single resistor (connected to ground) that regulates the current going through outputs. For standard LEDs, you should use something like 3K resistor for DM631 and DM632 chips and around 2.2K for DM633 and DM634 ones. In the case of these latter chips, you can just connect REXT to ground as they have internal Global Brightness Control (just make sure you don’t raise the GBC over the default 70%). I recommend having the resistor here in any case.

The final pin 24 is Vcc. As usual, it is a good idea to have a decoupling capacitor here – I use 100nF.

With such a simple setup programming these drivers is also rather easy. In fact, you can use a technique called bit-banging, which amounts to manually sending data. Keep the latch pin low; send a single bit through the data line, pulse the clock pin, send the second bit, pulse the clock again, repeat as much as necessary, then pulse the latch. Use direct port manipulation, not digitalWrite() for this.

It is better and faster to use the SPI interface, as it was designed to do just that. It also has data (digital pin 11 on Arduino) and clock (pin 13) lines plus the Chip Select (CS – you can use any pin for this) line that works as a latch. You can still have other SPI devices connected to your Arduino on different CS lines.

My library uses this method.

Step 4: Library: Memory, Declaration, Initialization

Before we dig into the library particulars there is one important thing to understand. You cannot just turn on a single led somewhere in the middle of the LED driver by addressing said LED directly. You always have to send the full set of bytes according to the full number of LED drivers in a chain. So you have to assemble all this data in memory and then send it in one burst (refresh). This means you have to allocate some memory just for this task. Considering the drivers are 12- or 16-bit it means up to 2 bytes per single LED, so in fact the number of chained LEDs you can connect to an Arduino is limited by the size of its available RAM, which is also needed for other stuff your program intends to do. While all this allocation and later usage will be done by the library, it is important to keep the memory size in mind when designing any project involving a lot of LEDs.

So, the DMdriver library (finally).

The library is compatible with any chip in the DM63x line. In fact, you can use different ones in the same project, provided they are connected independently (not chained, different latch pins). It does all the basic stuff you’ll expect from such a library, leaving you to worry about what you want to achieve, not how to achieve that. So, head on to GitHub, download it and unzip in your Libraries folder.

First of all, don’t forget to

#include "DMdriver.h"

in your sketch. Now we can declare an object of the DMdriver type called, for example, TestObject:

DMdriver TestObject (DM634, 3, 9);

On declaration, you have to provide three values. First, the type of the driver you have – it can be DM631, DM632, DM633 or DM634. Secondly, the number of LED drivers chained in this particular object. Lastly, the pin connected to the latch line of this object.

You can declare multiple objects connected to different latch pins. For example, the contraption in the video and an illustration above has 4 such objects running on a mix of DM633 and DM634 chips. You can also declare the same physical object connected to one latch pin multiple times as different virtual objects if needed, just note that it will allocate more video memory (however, the actual memory allocation won’t happen at this step).

To use the DMdriver object you’ll have to initialize it once in the setup() section:

TestObject.init();

This will set up the SPI interface and allocate the needed memory to your object. The memory will be allocated as a dynamic array, this is the weird way of C++. If you initialize all your objects during setup() and never try to re-allocate their memory later that won’t be a problem.

Now, if you do the init() with no parameters, the LEDs connected will be addressed by the output pins on LED drivers, meaning 0 for the first one, 1 for the second, …, 16 for the first pin on the second chained chip and so on. Sometimes when you are designing your own board, especially if you are making it yourself, you won’t be able to install your LEDs in this particular order. This is especially cumbersome with RGB LEDs: your first RGB LED may, in fact, be connected to outputs 0, 4 and 6, the second to 1, 3 and 12 and so on. The init() function can take an optional parameter pointing to an array of values representing the physical LED connections, a look-up table.

The look-up table is declared as an array of bytes representing the string of physical LED driver pins according to the order you want them to be in. In the aforementioned case, this array should begin like this:

byte orderLED[] = {0, 4, 6, 1, 3, 12, …… }

and contain the number of bytes according to the number of all the outputs in the object, in this example 48. It should be declared before the setup() section, and now you’ll initialize the TestObject like this:

TestObject.init(orderLED);

There are also two destructors that will free the memory used by both arrays, but you should never use them. They only reason they are there is the C++ tradition of ‘if you have a dynamic array you must have the means to get rid of it’. On Arduino, clearing this memory won’t do any actual good and trying to allocate a new array in place of the destructed one will lead to ugliness.

Step 5: Library: Let There Be Light

Now that we have our object declared and initialized, we can start using it. As explained before, we will be building a needed picture in the ‘video memory’ and then sending it in full for the LED driver to display.

First,

TestObject.clearAll();

fills the video memory with zeros.

TestObject.setPoint(num, value);

sets the individual LED at address num to value. If the look-up table was provided during initialization, the library will get the actual pin number from it. For now, the change happens in memory only.

value = TestObject.getPoint(num);

Returns the current value of the LED #num. Note that this value is also from memory, so it won’t necessarily correspond to the actually visible color.

TestObject.setRGBpoint(num, red, green, blue);

sets the RGB LED num to the color corresponding to red, green and blue values. The num is the position of the RGB LED in your chain of RGB LEDs (starting with 0). This will set red value to the output calculated as num*3, green to num*3+1 and blue to num*3+2; it is assumed that you have either connected your RGB LEDs in the right order or used a look-up table as described before.

There are two other ways of setting up an RGB LED. Firstly,

TestObject.setRGBled(num, red, green, blue);

sets up the RGB LED that has it’s red cathode connected to output num, assuming that green is num+1 and blue is num+2. This is a tiny bit faster. Secondly,

TestObject.setRGBmax(num, red, green, blue <, max>);

works the same as the setRGBpoint() function, but has an optional max parameter. This parameter limits the overall brightness of the RGB LED; if the sum of red, green and blue exceeds max, the values will be reduced, keeping their proportions intact. This function is useful if you need to keep the power consumption in check. It can also be used for brightness control and fade-out effects; just keep in mind that it is more complex than previous functions, so it eats a bit more memory and is a bit slower.

Note that you should use only one of these three setRGB functions in your sketch to save program memory. Just select the one that suits your needs best and stick to it. That’s why max is optional in setRGBmax().

Now that we have the needed values stored in video memory, we can do

TestObject.sendAll();

to actually display the result.

With DM633 and DM634 chips you can also set the global brightness using

TestObject.setGlobalBrightness(val);

where val is a 7-bit number from 0 to 127. Note that 0 doesn’t mean ‘shut off’, it is more like ‘very weak’. It is better to always set the GBC just after the init(), especially with DM634 chips, as they tend to change it randomly on startup.

You can also

TestObject.setGBCbyDriver(byteArray);

to set the global brightness individually for each connected DM633 or DM634 chip. This is useful if you have separate drivers controlling red, green and blue colors in all the RGB LEDs, so you can do some color calibration. The byteArray here is an array of brightness values for each of the drivers, starting with the first one in the chain.

Step 6: Library: Configuration

There is a DMconfig.h file in the library directory that has a couple of configuration options mostly intended for reducing memory usage.

The first one is

#DEFINE DM256PWM

If uncommented, this define will reduce the size of the allocated video memory to a single byte per LED, so you’ll be limited to 0-255 values, like with the PWM outputs on Arduino. Seems a loss when compared to the 16-bit initial resolution, but is not. Firstly, less memory is used. Secondly, by default this option will square the value before sending it to the driver, meaning it will actually use the full 16-bit spectrum and look rather good. At 127 it will actually look like 50% brightness (the actual value sent will be 16,129, not 32,768). Better seen than explained, try it. If you change the second part of this define to ‘<<8’, a simple bit shift will be used: faster, but doesn’t look as cool as the square option.

The DM256PWM option can also be used to produce a universal code compatible with both 12- and 16-bit LED drivers (check the examples in the library).

#DEFINE DMCHAIN X

It is best explained in the illustration above. In default mode, with no optional look-up table specified, the library will consider all the LEDs connected sequentially to the driver outputs. In chains it will also mean that first driver outputs go first, then the second one’s, etc. In real life this is rarely achievable; usually, you’ll want the sides of the drivers to be connected in series. You can solve this with a look-up table, but it will take memory; so you may just use this config. The X means the number of drivers in each segment (check the second picture above).

You can also play around with the #pragma define; sometimes setting the optimization to O2 produces smaller code. Do not change the last define, it is there for future compatibility reasons.

That's it. Please note that the library is still work in progress, so some errors and bugs are possible; please report them. Also, suggestions are welcome.