LED Bar Graph with Arduino and shift registers

Intro

So, this article was supposed to be called ‘LED Bar Graph with MAX7221’. We’ve got a pile of these neat little 12-segment bar graphs over here that have red and green LEDs in each digit. As it turns out, however, we’ve got common anode, while the MAX7221/7219 only drives common cathode. This is a bummer, and probably why Adafruit discontinued the common anode and only carries the common cathode now. We’d just pick up some common cathode and be done with it, except, like I said, we’ve got a pile of them. And it seemed like a cool project to make it work with even simpler components than our MAX7221.

Multiplexing?

Connecting up a bunch of LEDs on a shift register really isn’t newsworthy, in fact we posted about it earlier. The key here is that, while we can control all LEDs on one anode individually (each anode of three controls four segments), we can’t turn one LED in one group and one in another group without turning a bunch of others on inadvertently. Looking at our datasheet, which is similar to this one, reveals that we have 14 pins total, three of which are duplicated. So we’ve got eight cathodes and three anodes.

Some basic math is illuminating: we have 24 individual LEDs, each with two states. That gives 2^24 possible state combinations. We only have 14 pins, 3 of which are duplicates, leaving 11. This is only 2^11 possible pin states. We obviously need some trickery. And that’s where LED multiplexing comes in. It works the same way as a row/column setup does: each LED is lit very quickly in succession. It tricks our eyes, and allows fewer pins for a fixed number of LEDs. Take a typical 7-segment plus dot digit, for example. If you have eight eight-segment digits (what you can put on a MAX7221), you have 8×8=64 segments. Individually wired, this would take 128 pins, or 64 with a common anode or cathode. But with only eight cathodes and eight anodes, one per digit and one per segment type for a total of 16, you can multiplex it and operate it at one fourth the pin count. You could do the same thing with two eight-bit shift registers, such as the 74HC595 we’re using here. In our case, we have eight total cathodes per group (four green, four red), and three anodes. We’ll use one register for the cathodes (as we did above), and use the other register for our three anodes. So on to it.

Shift Registers

Each shift register gives us eight bits (we’ll use 74HC595s), so we can get away with two of them and have five bits to spare. They’re about $0.10 apiece. A MAX7221 is something like a dollar, so it’s marginally cheaper.

Testing reveals that driving these guys from 5V seems to be just fine, so our wiring looks a bit like this:

Wiring schematic for two shift registers and a 12-segment bar display.

Notice that we’ve wired our output enable pin to ground, which will make it permanently active. This is fine, since we aren’t sharing our SPI lines with other devices.

We’ve hooked up our clock and data lines on outputs 8-10 on the ‘duino, and we borrowed and modified some code to allow us to set the color on each of the first four digits (eight LEDs):

 void setDisplay(int byte1, int byte2) {
  int numtoset1 = 255-byte1;
  int numtoset2 = byte2;
  digitalWrite(latchPin, LOW);
  
    // shift out the bits:
    shiftOut(dataPin, clockPin, MSBFIRST, numtoset2);
    shiftOut(dataPin, clockPin, MSBFIRST, numtoset1);  

    //take the latch pin high so the LEDs will light up:
    digitalWrite(latchPin, HIGH);
}

Note that we’ve added provision for two bytes, which we’ll need for our remaining digits (actually, for controlling the anodes, but we’ll get to that later). This means that our closest shift register will get the second byte we send, meaning for our one shift register testing, we’ll put our test value in byte1. To start, we have set up the first four bits for red, and the second four for green. Our cathodes are the outputs, so low turns them on, but we take the complement so we can use 1 for ‘on’. This yields the following values to turn on each LED:

segment color value
1 R 1
1 G 16
2 R 2
2 G 32
3 R 4
3 G 64
4 R 8
4 G 128

So to get yellow, we just apply both red and green. 255 is all on, 0 is all off. We don’t need to remember these, as we can just use the built-in bit() function, passing 0-3 for red and 4-7 for green. We wrap it up again in a function that allows us to set, using two arguments: digit and color. We’ll modify this later when we create control for all digits. Here is the wrapper function:

void setDigit(int digit, int colorint) {
  int byte2 = 0;
  int byte1 = 0;
  digit = digit %4;
  if (colorint > 0){
    if (colorint == 1){
      byte1 = bit(digit);
    }
    else if (colorint == 2){
      byte1 = bit(digit + 4);
    }
    else if (colorint == 3){
      byte1 = bit(digit) + bit(digit +4);
    }    
  }  
  setDisplay(byte1,byte2);
}

We set up a basic function to set the first digits:

void loop() {
  setDigit(0,0);
  setDigit(1,1);
  setDigit(2,2);
  setDigit(3,3);
}

This will set the digits one at a time. Persistence of vision dictates that we can’t tell the difference whether or not they are on simultaneously or in sequence (as long as the frequency is >25Hz (!)) … and we get what we expect:

Our segments, lit in series, appear to be lit simultaneously, the core principle of multiplexed LED illumination.
Our segments, lit in series, appear to be lit simultaneously, the core principle of multiplexed LED illumination.

Neither I nor the camera can tell that they aren’t all constantly on. Now, the astute reader should ask: “Why are you turning the digits on separately, instead of all together?” This is a great question, and really the point of this post. See Mutiplexing, above. The short answer is that we don’t have enough pins to set all states simultaneously and uniquely, so we have to shuffle through them.

Now on to the remaining digits. We’ll modify our setDigit() routine to tolerate digits greater than four, and set the proper anode. Note the use of the mod operator to bring all digit values into the range of 0-3 for our cathodes.

void setDigit(int digit, int colorint) {
  int byte2 = 0;
  int byte1 = 0;
  if (digit < 4){
    byte2 = 1;
  }
  else if (digit < 8){
    byte2 = 2;
  }
  else if (digit < 16) {
    byte2 = 4;
  }
  digit = digit %4;
  if (colorint > 0){
    if (colorint == 1){
      byte1 = bit(digit);
    }
    else if (colorint == 2){
      byte1 = bit(digit + 4);
    }
    else if (colorint == 3){
      byte1 = bit(digit) + bit(digit +4);
    }    
  }  
  setDisplay(byte1,byte2);
}

We’ll call it like so:

setDigit(0,0);
  setDigit(1,1);
  setDigit(2,2);
  setDigit(3,3);
  setDigit(4,0);
  setDigit(5,1);
  setDigit(6,2);
  setDigit(7,3);
  setDigit(8,0);
  setDigit(9,1);
  setDigit(10,2);
  setDigit(11,3);

This seems fine, but what we get are fluttering LEDs, as shown here:

Obviously, 8×8 matrices make eight calls work, so our timing is probably suspect, or the difference between eight and twelve calls breaks the camel’s back. We don’t feel like doing the math. We don’t need to troubleshoot that, however. Remember that we’re making eight calls for only three anodes, so we can further optimize. We can combine the twelve calls into three (there are only three anodes). We’ll just restructure our algorithm a bit.

First, we define a quick function to return a value from digit and color:

int getByteFromColorInt(int digit, int colorint){
  int returnvalue=0;
  if (colorint > 0){
    if (colorint == 1){
      returnvalue = bit(digit);
    }
    else if (colorint == 2){
      returnvalue = bit(digit + 4);
    }
    else if (colorint == 3){
      returnvalue = bit(digit) + bit(digit +4);
    }    
  }
  return returnvalue;
}

Then, we split up the calls into groups:

void setBarDigits(int barColors[]) {
  int i;
  int byte21 = 0;
  int byte22 = 0;
  int byte23 = 0;
  for (i=0;i<4;i++) {
    byte21 += getByteFromColorInt(i, barColors[i]);
  }
  Serial.print("byte21: ");
  Serial.println(byte21);
  setDisplay(byte21,1);
  for (i=4;i<8;i++){
    byte22 += getByteFromColorInt(i%4, barColors[i]);
  }
  Serial.print("byte22: ");
  Serial.println(byte22);
  setDisplay(byte22,2);
  for (i=8;i<12;i++){
    byte23 += getByteFromColorInt(i%4, barColors[i]);
  } 
  Serial.print("byte23: ");
  Serial.println(byte23);
  setDisplay(byte23,4);
}

And simply call it like so:

void loop() {
  int barColors[12]={0,1,0,1,0,1,0,1,0,1,0,1};
  setBarDigits(barColors);
}

And we have success:

 

Alternating LEDs show that our segment control of multiple anodes is workign as advertised.
Alternating LEDs show that our segment control of multiple anodes is working as advertised.

One thing to remember is that the setBarDigits() function is running three states in series, and ending on one. So if you throw in a delay after the call, or do something time-consuming between when it ends and when it’s called again, it’s going to remain in the last state. So doing this, for example:

 void loop() {
  int barColors[12]={0,1,0,1,0,1,0,1,0,1,0,1};
  setBarDigits(barColors);
  delay(1000);
}

Will give you this:

 

Which is probably not what you want. So you need to run it often. This is why separate chips dedicated to this like the MAX7219/7221 make sense: they do all of this elsewhere so you don’t have to worry about it. Anyway, so let’s have some fun with some animation. First, we modify our setBarDigits() function to accept a runtime argument. This way the function will run its loop to maintain an set of digits for the time we specify:

void setBarDigits(byte barColors[], int runtime) {
  int i;
  int byte21 = 0;
  int byte22 = 0;
  int byte23 = 0;
  long starttime = millis();
  boolean doRun = true;
  
  while (doRun){
    byte21 = 0;
    byte22 = 0;
    byte23 = 0;
    for (i=0;i<4;i++) {
      byte21 += getByteFromColorInt(i, barColors[i]);
    }
    Serial.print("byte21: ");
    Serial.println(byte21);
    setDisplay(byte21,1);
    for (i=4;i<8;i++){
      byte22 += getByteFromColorInt(i%4, barColors[i]);
    }
    Serial.print("byte22: ");
    Serial.println(byte22);
    setDisplay(byte22,2);
    for (i=8;i<12;i++){
      byte23 += getByteFromColorInt(i%4, barColors[i]);
    } 
    Serial.print("byte23: ");
    Serial.println(byte23);
    setDisplay(byte23,4);
    
    if (millis()-starttime < runtime) {
      doRun = true;
    }
    else {
      doRun = false;
    }
  }
}

Then we create an array of states we’d like to see and pass them one by one:

void loop() {
  byte barColors1[12]={1,0,0,0,1,0,0,0,1,0,0,0};
  byte barColors2[12]={0,1,0,0,0,1,0,0,0,1,0,0};
  byte barColors3[12]={0,0,1,0,0,0,1,0,0,0,1,0};
  byte barColors4[12]={0,0,0,1,0,0,0,1,0,0,0,1};
  byte barColors5[12]={2,0,0,0,2,0,0,0,2,0,0,0};
  byte barColors6[12]={0,2,0,0,0,2,0,0,0,2,0,0};
  byte barColors7[12]={0,0,2,0,0,0,2,0,0,0,2,0};
  byte barColors8[12]={0,0,0,2,0,0,0,2,0,0,0,2};
  byte barColors9[12]={3,0,0,0,3,0,0,0,3,0,0,0};
  byte barColors10[12]={0,3,0,0,0,3,0,0,0,3,0,0};
  byte barColors11[12]={0,0,3,0,0,0,3,0,0,0,3,0};
  byte barColors12[12]={0,0,0,3,0,0,0,3,0,0,0,3};
  
  byte* barColorArray[12];
  barColorArray[0] = barColors1;
  barColorArray[1] = barColors2;
  barColorArray[2] = barColors3;
  barColorArray[3] = barColors4;
  barColorArray[4] = barColors5;
  barColorArray[5] = barColors6;
  barColorArray[6] = barColors7;
  barColorArray[7] = barColors8;
  barColorArray[8] = barColors9;
  barColorArray[9] = barColors10;
  barColorArray[10] = barColors11;
  barColorArray[11] = barColors12;

  for (int i=0;i<12;i++){
    setBarDigits(barColorArray[i],200);
  }
}

And we get this charming display:

But we still need to verify that we can set digits independently. The above doesn’t really demonstrate that, as we could be tying all the anodes together. So let’s switch it up a bit:

Perfect!

Leave a Reply

Your email address will not be published. Required fields are marked *