Controlling Lots of Outputs from a Microcontroller

Making LED displays is fun. There are a a few tools that get used all the time, from row-column scanning to LED driver chips to multplexers and shift registers. This tutorial discusses some of the more popular methods for controlling large amounts of LEDs from a microcontroller, including their various strengths and weaknesses, and how they work. For more on this subject see chapter 14 of “Physical Computing“, where Dan O’Sullivan and I discussed it in more depth.  I’ll also include some notes on how to apply these ideas to controlling multiple motors or other high-current loads.

Most microcontroller modules have a limited number of outputs. Even if you use the analog inputs as digital I/O, there are only 19 pins on an Arduino, for example. That’s a fairly typical number for an 8-bit controller, and it seems not nearly enough if you want to control, say, 100 LEDs or more.  There are a couple ways around this problem.  Without adding any additional hardware, you can make a matrix of your LEDs and control them using row-column scanning.  If you want discrete analog control over one output at a time, you can use a multiplexer. For digital control over multiple pins, you could use an addressable latch or a shift register. If you need pseudo-analog control over multiple pins, you could use a PWM driver.  There are also several LED driver chips that are designed specifically to control groups of LEDS.

Basic Principles

Before getting into the details of the actual methods, there are a few principles that will make your life easier.  If you already understand these, feel free to skip to the next section.

Bit Manipulation

All of the methods for output control mentioned below share one common process: you store the states of the pins in variables, as arrays of bits, and manipulate the bits to turn things on or off.  For example, a row of eight LEDs in a shift register can be represented by an eight-bit (i.e. a byte) variable. If every other LEd were turned off, the byte would look like this:

0 1 0 1 0 1 0 1

It doesn’t matter what the decimal value of this byte is, because all you care about is which bits are on and which bits are off.  To change the states of the LEDs, you manipulate the bits.

Computer variables are stored in arrays of transistors that can be turned on or off; in other words, in binary arrays. At the lowest level of abstraction, these arrays of bits are manipulated using various logic operations, like AND, OR, NOT, and XOR.  They’re sometimes called bitwise operators because they operate by manipulating the bits. Here’s how they work:

  • AND checks to see if both values are 1, and if so, returns a 1. Otherwise, it returns 0.
  • OR checks to see if either value is 1. If one or the other is, it returns a 1.
  • NOT reverses the value.
  • XOR checks to see that the two values are different. If they are, it returns a 1. Otherwise it returns a 0.

These operations have their own symbols in programming languages. Here they are in table form:

AND (&)
0 1
0 0 0
1 0 1
OR (|)
0 1
0 0 1
1 0 1
XOR (^)
0 1
0 0 1
1 1 0
NOT (~)
0 1
1 0

So, if you had a row of eight bits turned on, and you wanted the fifth one turned off, you could use the XOR operation, like so:

Bit number 7 6 5 4 3 2 1 0
1 1 1 1 1 1 1 1
^ 0 0 1 0 0 0 0 0

1 1 0 1 1 1 1 1

This is called bitmasking. It allows you to test the states of the bits one by one, and to turn them on or off. Turning on a bit is called setting the bit, and turning it off is clearing the bit.

There are two other useful operations for manipulating bits, the bit shift operators, >> and <<.  These move the bits of a byte left or right. For example:

00000001 << 2 = 00000100
10000000 >> 3 = 00010000

The bitwise operators in combination with the bit shift operators give you a lot of power over the individual bits of a byte.  Here are a couple of handy examples:

    x &= ~(1 << n);      // forces nth bit of x to be 0.  all other bits left alone.
    x |= (1 << n);       // forces nth bit of x to be 1.  all other bits left alone.
    x ^= (1 << n);       // toggles nth bit of x.  all other bits left alone.
    x = ~x;              // toggles ALL the bits in x.

For more on this, see the excellent Bitmath tutorial by CosineKitty on the Arduino playground. There are also some handy commands in Arduino for manipulating bits: bitRead, bitWrite, bitSet, and bitClear. You’ll see these used to write and read bits of a byte in the examples to follow.

Synchronous Serial Communication

Many of the integrated circuits (ICs) used to control lots of outputs communicate using synchronous serial communication. Unlike asynchronous serial communication, in which there are two independent computers with their own clocks, synchronous serial communication has only one clock.  The microcontroller (sometimes called the master device) sends a continuous pulse to the external IC  (or slave device) on one pin (the clock pin). Each time the clock pin changes from low to high, the external IC reads another output pin from the microcontroller (the data pin) to see if it’s high or low, and saves the result in a memory bit. As new bits are read each clock pulse, they’re shifted bitwise into the IC’s memory registers, like a bucket brigade.

Once the whole byte’s been received, the slave interprets the byte and changes its outputs.  Depending on the chip, this can happen in a variety of ways.  Some chips have an additional pin, called the latch pin or output enable pin, that the master device pulses high or low to activate the output.

The general procedure for using an external IC like this goes as follows:

  1. arrange the bits you need to shift into a byte using bit manipulation
  2. toggle the latch to disable output
  3. shift the bits in, pulsing the clock every bit
  4. toggle the latch to enable output with the new state

Many programming languages, including pBasic, PicBasic Pro, BX Basic, and Arduino, feature commands for shifting bits out like this, called shiftOut.  Here’s a link to the Arduino shiftOut command. There are several synchronous serial protocols, including I2C/TwoWire and SPI.  The Arduino Wire library implements the I2C protocol, and the Spi library implements SPI. Different ICs will use different protocols, so check the datasheet of your chip to know what to use. The examples here mostly use generic synchronous serial communication implemented using the shiftOut command.

Row-Column Scanning

You can control more than one LED per output pin of a microcontroller, even with no other external hardware, using row-column scanning.  Even with the 19 pins of an Arduino, this method allows you to control up to 64 LEDs in an 8×8 matrix with a few pins to spare.  To do this, you arrange your outputs in a matrix, like so:

led-matrix
LED matrix. This matrix shows columns as anodes and rows as cathodes, but some matrices have this reversed. If you're using a pre-made matrix, check the datasheet.

To control the matrix, you connect the row and column pins to the outputs of your microcontroller.  To turn on a given LED, you take its column high and its low row, so that there’s a voltage difference across the LED. You can turn it off either by taking the column low, or by taking the row high.  Either way, the voltage across the LED will be zero.  In order to turn on some of the LEDs in a given column, but not others, do the following:

  1. Take the column low.  This turns off all the LEDs in the column.
  2. Take the rows of the LEDs that you want to turn ON low.
  3. Take the rows of the LEDs that you want to turn OFF high.
  4. Take the column high.  This will create a voltage difference across the LEDs with low rows, and they’ll turn on.

Here’s a basic Arduino code snippet that turns on each of the LEDs in the matrix in turn, one at a time:

for (int thisCol = 0; thisCol < 8; thisCol++) {
  // take the row pin (anode) high:
  digitalWrite(col[thisCol], HIGH);
  // iterate over the rows (cathodes):
  for (int thisRow = 0; thisRow < 8; thisRow++) {
    // when the column is HIGH and the row is LOW,
    // the LED where they meet turns on:
    digitalWrite(row[thisRow], LOW);
    // delay long enough to see the LED on:
    delay(100);
    // turn the pixel off:
    digitalWrite(row[thisRow], HIGH);
  }
  // take the column pin low to turn off the whole column:
  digitalWrite(col[thisCol], LOW);
}

Row-column scanning is a common technique for controlling a larger number of outputs than you have pins.  You can combine it with many of the external ICs mentioned below to create even larger control arrays.  In fact, some of the LED driver chips, like the MAX7219, are designed for use with row-column scanning matrices. You can see examples of row-column scanning in action in the 8×8 LED matrix control on an Arduino Mega example, the Tale of Two Pongs example, and the Tilty Ball example.

Shift Registers

Shift registers use synchronous serial communication to shift data from the microcontroller into the shift register to light up  LEDs.  There are commonly three pins you need to connect between the microcontroller and the shift register:

  • Data: microcontroller takes this pin high or low to send a 1 or 0
  • Clock: microcontroller pulses this, and every time it goes from high to low, the shift register reads the data line
  • Latch: enables or disables the output of the shift register

Different shift registers may add other features, but those are the most basic interface elements that shift registers share in common. Many LED drivers use synchronous serial communication as well.

The data that you shift into a shift register get connected directly to the outputs when you toggle the latch.  For example, when you shift sixteen bits into a 16-bit shift register, the 16 bits you send are mapped directly to the  16 output pins. This is not necessarily the case for specialized LED driver chips, though.  The bits you shift in are often commands for the driver to do specific things, like set a particular pin to a particular PWM rate.

The ST16C596 shift register example shows how to use a shift register.

Multiplexers and Addressable Latches

Addressable latches and multiplexers generally have a series of address pins that you set in order to control which of the ouputs is connected to the chip’s input.  For example, an 8-channel latch or multiplexer has 8 output pins, and three address pins. Turning on or off the address pins connects one of the eight outputs to the input.  Why three address pins? Three pins that can be switch on or off makes for 23, or 8 possible combinations.  That allows you to choose any of the 8 outputs. For a 16-channel chip, you’d have 4 address pins, giving you 24, or 16 possible combinations. Latches and multiplexers usually have an output enable or inhibit pin that allows you to turn off the output while you’re changing the address pins.  Addressable latches also feature a pin that allows you to latch the outputs on or off so that you can control more than one at a time.   The disadvantage of latches, however, is that they’re always digital; you can only turn a latch’s pins on or off.  Analog multiplexers, on the other hand, allow you to connect any voltage from the  input pin to any of the output pins. The disadvantage of a multiplexer is that you can control only one pin at a time.

The CD4067 Multiplexer with LEDs example shows how to use a multiplexer.

The CD4099 Addressable Latch with LEDs example shows how to use an addressable latch.

So How Many Outputs Can You Control From One Microcontroller?

Here are some estimates based on personal experience.  These may not be the limits, these are just based on some quick tests I’ve done:

Using an Arduino Duemilanove/Diecimila:

17 usable digital I/O (not counting analog)

Attaching TLC5940 chips:
5 pins per set of ICs
Cascade 8 ICs deep?
3 sets of 8 ICs each
24 x 16 LEDs =
384 LEDs

Using an Arduino Mega:

51 usable digital I/O (not counting analog)

Attaching TLC5940 chips:
5 pins per set of ICs
Cascade 8 ICs deep
10 sets of 8 ICs each
80 x 16 LEDs =
1280 LEDs

These are conservative estimates, as there is not an 8-chip limit on cascading TLC5940s.

Comparison Chart

Option Fading? Multiple on at once? Synchronous serial? Address pins? Pins used Cascading?
Row-Column scanning No Sort of No No 2n pins = n2 outs No
Shift register No Yes Yes No 3 pins = 16 outs Yes
Multiplexer Yes No No Yes 5 pins = 16 outs No
Addressable latch No Yes No Yes 5 pins = 8 outs No
PWM driver Yes Yes Yes No 5 pins = 16 outs Yes