Multiplexing 7 segment displays with a shift register and transistors

In this example I will show you how to multiplex a 7 segment LED display from a microcontroller such as an Arduino without placing too much load on the microcontroller pins as I have seen in some other examples. Quite a few tutorials I’ve seen are not really good practice for electronics design – some even don’t use current limiting resistors!

Direct method (commonly seen in a lot of online examples)

A multiplexed display can be run directly from a micro without a shift register as long as the current limit of the microcontroller pins is observed, in the ATMega328’s case is 20mA typical, 40 absolute maximum per pin. But you have to watch out for the total current allowed per port and for the whole chip to avoid excessive power dissipation leading to bonding wire failure or the magic smoke. In this configuration the segments are driven from one set of pins and the digits another. This method uses a lot of pins on the microcontroller and in most cases can overload the pins used for the digits. Hence this is only suitable for 2-4 digit displays and limiting the average current per segment to 5mA or less. High efficiency LED displays are required for this method otherwise you will have a dim display. So, to solve the problem of overloading the pins on the microcontroller, transistors are a must for the digit selection as, with all digits on showing 8. 40mA could flow through one pin if an average of 5mA were to be used. This is right on the absolute maximum for the microcontroller. A PNP transistor (common anode) or NPN transistor (common cathode) is recommended. An example circuit is shown above.

One thing that you have to remember with multiplexing a single digit is only on for x number of total digits e.g. a 4 digit display is only on for 1/4 of the time so you would need to increase the current by 4 times so the segment current needs to be 20mA. That times 8 segments is 160mA – OK for some of the ports on the ATMega328P but the chip has an absolute maximum of 200mA for the whole chip. This limits the amount of current and the number of digits on your display. You could use a driver chip like the MAX7219 instead (which I really would recommend) but this isn’t the subject of this article.

The shift register method

So, to avoid overloading and following good practice use a shift register to drive the segments instead. The common 74HC595 is a likely go to chip for hobbyists but it has even less sink / source current than the ATMEGA series of chips. A transistor array would be required between the HC595 and the LED displays but this increases cost and circuit complexity. Enter the TPIC6B595; a power logic shift register that can sink up to 150mA absolute maximum per pin and a total limit of 500mA for the whole chip. Note that unlike the 74HC595 the TPIC6B595 has a couple of drawbacks the first of which is it only accepts 5V logic level inputs so to use with a 3.3V microcontroller you need a logic level shifter. The second is it can only sink current due to it’s open drain outputs so it can only be used with common anode LED displays. If you use this chip you can safely increase the current through the displays to increase the brightness or use for larger displays – but watch out the total current if all segments were on does not exceed 500mA. In all cases, the datasheet for the chip should be read! An example circuit is shown above that uses LED displays with a forward voltage of 2.2V. For displays that use less forward voltage you need to increase the current limiting resistor value accordingly. 68ohms is a good example in this 5 digit display if using 1.8V red displays. Example code for multiplexing using the method shown in the schematic above left is shown below. I have only included the part of the program that handles the multiplexing.

Example code snippet

void loop()
{
    LoadCell.update(); // retrieves data from the load cell
    long int grams = LoadCell.getData() -offset; // get output value
    //long int grams = 12345; // for testing
    //Serial.println(grams);
    static uint16_t btndbc = 0;
    btndbc=(btndbc<<1) | digitalRead(tare_sw) | 0xe000; // debounce button
     if (btndbc==0xf000)
     {
        delay(1); // wait 1 millisecond
        offset = LoadCell.getData(); // read initial HX711 value and store it as offset
     }
    // get digit data. Digit 0 is leftmost
    int dig4 = (grams % 10); // ones
    int dig3 = (grams / 10) % 10; // tens
    int dig2 = (grams / 100) % 10; // hundreds
    int dig1 = (grams / 1000) % 10; // thousands
    int dig0 = (grams / 10000) %10; // ten thousands
    delay(1);
    // turn all digits off by setting digit pin high on each loop through.
    PORTD |= (1<<PORTD2);
    PORTD |= (1<<PORTD3);
    PORTD |= (1<<PORTD4);
    PORTD |= (1<<PORTD5);
    PORTD |= (1<<PORTD6);
   if (grams <-9 || grams >10010) // show error if minus figure or scale is overloaded
    {
     switch (digit)
     {
      case 0:
        SPI.beginTransaction(SPISettings(8000000, LSBFIRST, SPI_MODE0));
        PORTB &= ~(1<<PORTB2); // set latch LOW
        SPI.transfer(B11110010); //E
        PORTB |= (1<<PORTB2); // set latch HIGH
        SPI.endTransaction();
        PORTD &= ~(1<<PORTD2); // set digit pin 0 LOW
        delay(2);
        break;
      case 1:
        SPI.beginTransaction(SPISettings(8000000, LSBFIRST, SPI_MODE0));
        PORTB &= ~(1<<PORTB2); // set latch LOW
        SPI.transfer(B10100000); // r
        PORTB |= (1<<PORTB2); // set latch HIGH
        SPI.endTransaction();
        PORTD &= ~(1<<PORTD3);
        delay(2);
        break;
      case 2:
        SPI.beginTransaction(SPISettings(8000000, LSBFIRST, SPI_MODE0));
        PORTB &= ~(1<<PORTB2); // set latch LOW
        SPI.transfer(B10100000); //r
        PORTB |= (1<<PORTB2); // set latch HIGH
        SPI.endTransaction();
        PORTD &= ~(1<<PORTD4);
        delay(2);
        break;
      case 3:
        SPI.beginTransaction(SPISettings(8000000, LSBFIRST, SPI_MODE0));
        PORTB &= ~(1<<PORTB2); // set latch LOW
        SPI.transfer(B10111000); // o
        PORTB |= (1<<PORTB2); // set latch HIGH
        SPI.endTransaction();
        PORTD &= ~(1<<PORTD5);
        delay(2);
        break;
      case 4:
        SPI.beginTransaction(SPISettings(8000000, LSBFIRST, SPI_MODE0));
        PORTB &= ~(1<<PORTB2); // set latch LOW
        SPI.transfer(B10100000); //r
        PORTB |= (1<<PORTB2); // set latch HIGH
        SPI.endTransaction();
        PORTD &= ~(1<<PORTD6);
        delay(2);
        break;
      }
        digit++;
    if (digit == 5)
      {
        digit = 0;
      }
   }
    else // display normally
    {
      switch (digit)
      {
        case 0:
          SPI.beginTransaction(SPISettings(8000000, LSBFIRST, SPI_MODE0));
          PORTB &= ~(1<<PORTB2); // set latch LOW
          SPI.transfer(ssddigits[dig0]);
          PORTB |= (1<<PORTB2); // set latch HIGH
          SPI.endTransaction();
          PORTD &= ~(1<<PORTD2); // set digit pin 0 LOW
          delay(2);
          break;
        case 1:
          SPI.beginTransaction(SPISettings(8000000, LSBFIRST, SPI_MODE0));
          PORTB &= ~(1<<PORTB2); // set latch LOW
          SPI.transfer(ssddigitsDP[dig1]); // put a decimal point here. HX711 output is in grams and display will show in KG.
          PORTB |= (1<<PORTB2); // set latch HIGH
          SPI.endTransaction();
          PORTD &= ~(1<<PORTD3);
          delay(2);
          break;
        case 2:
          SPI.beginTransaction(SPISettings(8000000, LSBFIRST, SPI_MODE0));
          PORTB &= ~(1<<PORTB2); // set latch LOW
          SPI.transfer(ssddigits[dig2]);
          PORTB |= (1<<PORTB2); // set latch HIGH
          SPI.endTransaction();
          PORTD &= ~(1<<PORTD4);
          delay(2);
          break;
        case 3:
          SPI.beginTransaction(SPISettings(8000000, LSBFIRST, SPI_MODE0));
          PORTB &= ~(1<<PORTB2); // set latch LOW
          SPI.transfer(ssddigits[dig3]);
          PORTB |= (1<<PORTB2); // set latch HIGH
          SPI.endTransaction();
          PORTD &= ~(1<<PORTD5);
          delay(2);
          break;
          case 4:
          SPI.beginTransaction(SPISettings(8000000, LSBFIRST, SPI_MODE0));
          PORTB &= ~(1<<PORTB2); // set latch LOW
          SPI.transfer(ssddigits[dig4]);
          PORTB |= (1<<PORTB2); // set latch HIGH
          SPI.endTransaction();
          PORTD &= ~(1<<PORTD6);
          delay(2);
          break;
      }
     digit++;
    if (digit == 5)
      {
        digit = 0;
      }
   }
}

As you can see a switch case is used to switch between the digits and whilst each digit is active the binary bit patterns are shifted out to the TPIC6B595. I have used direct port manipulation here instead of digitalwrite to speed up program execution.

Video of multiplexing in slow motion

Complete project using this method

I hope you find this useful.

 

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.