Jan312010

Arduino: cascading shift registers to drive 7-segment displays with PC input

Published by paul at 3:33 PM under Arduino | Example Code | Example Movies | LED Projects | Shift Registers

In the last post I figured out how to drive a 74HC595 shift register to control 8 LEDs from only 3 digital outputs of the Arduino. Now I've taken that a step further and cascaded (sometimes called daisy-chained) four 595s together to drive 7-segment displays and also added code to accept input from the PC.

Instead of using 10-LED bar graphs like I did last time, I've moved on to 7-segment displays - which are more challenging and provide a more meaningful output. The 7-segment displays I have are common-cathode, wired as shown below:

    a           a - pin 14       dp (decimal point) - pin 9
   ---          b - pin 13       common cathode - pins 12 and 4
f |   | b       c - pin 8
   --- g        d - pin 7
e |   | c       e - pin 6
   --- . dp     f - pin 2
    d           g - pin 1
               

The code to illuminate decimal digits on a 7-segment display is pretty straightforward. I assigned each of the 7 segment-pins on the display to an output on the 595, from QA through QG (see the circuit diagram below). Each pin from QA to QG is assigned a binary value of 2 ^ pin#, so figuring out which pins to make high to produce each decimal digit is just a matter of adding together the binary values representing each pin necessary to light up the right segments. You can see this array being initialized in the setup() function.

I've also moved to a proper coding style, with g_ prefixing each global variable to distinguish them from local variables inside functions and methods. It's a good practice to define unchanging variables as const to allow the compiler to optimize the code around them - this also prevents mistakes where the code accidentally tries to change the value - the compiler won't allow it as the variable is effectively read-only and a difficult-to-debug bug is avoided.

To daisy-chain 595s together is really simple - connect the serial output (pin 9) of the 'lowest' 595 to the serial input (pin 14) of the next one in the chain, and connect all the latch (pin 12) and clock (pin 11) inputs together. When the first 595 accepts a new bit, the highest bit in the register will be pushed out of it's serial line as input to the next 595 in the chain. Enough bits must be pushed to fill all the 595s with new data correctly. In the previous post I only had a single 595 so I only had to push 8 bits of data from the Arduino. With two 595s I need to push 16 bits, with the first 8 bits effectively flowing through the first 595 and into the second. And so on with more 595s in the chain.

Care must be taken to push the 8-bit values in the correct order - the 8 bits for the 595 furthest from the Arduino must be pushed first, and the 8 bits for the 595 nearest the Arduino in the chain must be pushed last. I do this in the code in the sendSerialData() function by passing in the number of registers in use, plus a pointer to an array of bytes. Each element in the array has the byte to be pushed to the corresponding register - with the highest register number the furthest away from the Arduino. The code then iterates backwards through the away, pushing each byte in succession.

Rather than doing the obvious count-from-0-to-9999 code, I decided to figure out how to read input from the PC. This is done in the readNumberFromPC() function. If it detects that something has been sent from the PC, it reads each character, with a 10ms delay between each character read to ensure that all characters are received from the PC. Without the delay, the Arduino executes so fast that it may read the first character from the input stream, try again, and the next character hasn't made it over the slow (compared to the Arduino!) link from the PC - resulting in the input being chopped into two parts. This delay doesn't cause a problem with the 595s as their outputs are latched, and so stay the way they were set until the next update - preventing any display flickering.

In the loop() function, the logic to put the correct digit-byte in the register array is hard-coded. In the next rev I'll make this register-count agnostic.

As far as the circuit is concerned, I've got one 220ohm resistor on each 7-segment common cathode going to ground, which produces a bright enough display. I've also added a 100nF decoupling capacitor on the Vcc pin of each 595 to forestall any noise problems.

The complete circuit diagram is as below (click for a larger version):

 

Here's the breadboard layout, with a close up of IC2 (click for larger versions):

  

Once I'd figured out the wiring for one of the displays, adding the other three was pretty easy. At this point I ran out of 595s so I couldn't take the experiment any further, but I do have a bunch more on the way from Jameco.

I took a short (40s) video of the board in action - check it out here on YouTube.

And all the code is below(to download as a text file click here). Drop me a comment if you find this stuff useful!

[Edit: 12/19/10 - here's a link to someone who built a clock based on my code below.]

Next up - replacing some of the shift registers with transistors to allow a bigger fan-out from the Arduino.

/*
  Driving multiple 7-seg displays with 74HC595 shift registers.
 
  Feel free to re-use.
 
   01/30/2010
*/

// This pin gets sets low when I want the 595s to listen
const int  g_pinCommLatch = 6;

// This pin is used by ShiftOut to toggle to say there's another bit to shift
const int  g_pinClock     = 7;

// This pin is used to pass the next bit
const int  g_pinData    = 4;

// Definitions of the 7-bit values for displaying digits
byte g_digits [10];

// Current number being displayed
int g_numberToDisplay = 0;

// Number of shift registers in use
const int g_registers = 4;

// Array of numbers to pass to shift registers
byte g_registerArray [g_registers];

void setup()
{
  pinMode (g_pinCommLatch, OUTPUT);
  pinMode (g_pinClock, OUTPUT);
  pinMode (g_pinData, OUTPUT);
 
  Serial.begin (56600);
 
  // Setup the digits array
  // a = 8 b = 4 c = 2 d = 64 e = 32 f = 1 g = 16
  g_digits [0] = 8 + 4 + 2 + 64 + 32 + 1;
  g_digits [1] = 4 + 2;
  g_digits [2] = 8 + 4 + 16 + 32 + 64;
  g_digits [3] = 8 + 4 + 16 + 2 + 64;
  g_digits [4] = 1 + 16 + 4 + 2;
  g_digits [5] = 8 + 1 + 16 + 2 + 64;
  g_digits [6] = 8 + 1 + 16 + 2 + 64 + 32;
  g_digits [7] = 8 + 4 + 2;
  g_digits [8] = 8 + 4 + 2 + 64 + 32 + 1 + 16;
  g_digits [9] = 8 + 4 + 2 + 1 + 16 + 64;
} // setup

// Simple function to send serial data to one or more shift registers by iterating backwards through an array.
// Although g_registers exists, they may not all be being used, hence the input parameter.
void sendSerialData (
  byte registerCount,  // How many shift registers?
  byte *pValueArray)   // Array of bytes with LSByte in array [0]
{
  // Signal to the 595s to listen for data
  digitalWrite (g_pinCommLatch, LOW);
 
  for (byte reg = registerCount; reg > 0; reg--)
  {
    byte value = pValueArray [reg - 1];
   
    for (byte bitMask = 128; bitMask > 0; bitMask >>= 1)
    {
      digitalWrite (g_pinClock, LOW);
   
      digitalWrite (g_pinData, value & bitMask ? HIGH : LOW);
       
      digitalWrite (g_pinClock, HIGH);
    }
  }
  // Signal to the 595s that I'm done sending
  digitalWrite (g_pinCommLatch, HIGH);
}  // sendSerialData

// Print a message specifying valid inputs, given the number of registers defined and then consume all current input.
void badNumber ()
{
  int dummy;
 
  Serial.print ("Please enter a number from 0 to ");
  for (int loop = 0; loop < g_registers; loop++)
  {
    Serial.print ("9");
  }
  Serial.println (" inclusive.");
 
  while (Serial.available () > 0)
  {
    dummy = Serial.read ();
   
    // Necessary to get all input in one go.
    delay (10);
  }
} //badNumber

// Read a number from the PC with no more digits than the defined number of registers.
// Returns: number to display. If an invalid number was read, the number returned is the current number being displayed
//
int readNumberFromPC ()
{
  byte incomingByte;
  int  numberRead;
  byte incomingCount;
 
  if (Serial.available () > 0)
  {
    numberRead = 0;
    incomingCount = 0;
     
    while (Serial.available () > 0)
    {
      incomingByte = Serial.read () - 48;
      incomingCount++;
     
      if (incomingByte < 0 || incomingByte > 9 || incomingCount > g_registers)
      {
        badNumber ();
        return g_numberToDisplay;
      }
     
      numberRead = 10 * numberRead + incomingByte;
     
      // Necessary to get all input in one go.
      delay (10);
    }

    Serial.print ("Now displaying: ");
    Serial.println (numberRead, DEC);
   
    return numberRead;
  }
 
  return g_numberToDisplay;
} // readNumberFromPC


void loop()
{
  g_numberToDisplay = readNumberFromPC ();
 
  if (g_numberToDisplay < 10)
  {
    g_registerArray [3] = g_digits [0];
    g_registerArray [2] = g_digits [0];
    g_registerArray [1] = g_digits [0];
    g_registerArray [0] = g_digits [g_numberToDisplay];
  }
  else if (g_numberToDisplay < 100)
  {
    g_registerArray [3] = g_digits [0];
    g_registerArray [2] = g_digits [0];
    g_registerArray [1] = g_digits [g_numberToDisplay / 10];
    g_registerArray [0] = g_digits [g_numberToDisplay % 10];
  }
  else if (g_numberToDisplay < 1000)
  {
    g_registerArray [3] = g_digits [0];
    g_registerArray [2] = g_digits [g_numberToDisplay / 100];
    g_registerArray [1] = g_digits [(g_numberToDisplay % 100) / 10];
    g_registerArray [0] = g_digits [g_numberToDisplay % 10];
  }
  else
  {
    g_registerArray [3] = g_digits [g_numberToDisplay / 1000];
    g_registerArray [2] = g_digits [(g_numberToDisplay % 1000) / 100];
    g_registerArray [1] = g_digits [(g_numberToDisplay % 100) / 10];
    g_registerArray [0] = g_digits [g_numberToDisplay % 10];
  }

  sendSerialData (g_registers, g_registerArray);
} // loop



[KickIt] [Dzone] [Digg] [Reddit] [del.icio.us] [Facebook] [Technorati] [Google] [StumbleUpon]

Currently rated 2.2 by 110 people

  • Currently 2.2/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

E-mail | Permalink | Trackback | Post RSSRSS comment feed 5 Responses

Jan172010

Arduino projects: LED 10-bar array

Published by paul at 7:39 PM under Arduino | LED Projects | Example Code | Example Movies

I finally had some time over the last two days to play with the Arduino board I picked up late last year. The Arduino is a pretty neat concept - wrapping a microcontroller up in a neat board that makes playing with sensors, displays, motors, etc and prototyping very simple. It's all open source and you can read more about it on their homepage (http://www.arduino.cc/) which also has a freeware IDE to use for programming. The board I have uses Amtel's ATmega328P processor, with 32K of flash memory and can do 20MIPS. They're very popular and opening up electronics and gadget hacking to non-techies.

Here's the 2009 rev of the Arduino Duemilanove board (image from their website, click for larger version):

 

They cost about $30 - I got mine as part of a kit from the Nuts'n'Volts magazine store but loads of online stores have them.  I just picked up some accessories yesterday from SparkFun who have the full range, including the Arduino Mega which has 54 IO pins - can't wait for that to arrive!

The possibilities for this are just endless. The IDE provides a full C++ environment with a bunch of helper classes already defined, which takes a lot out of the tedium of programming microcontrollers. If you're going to play with this, I recommend using some of the samples that come with the IDE and on their very extensive web site.

My current interest is with making things light up in clever ways so I thought I'd start off by writing a simple program to play with an LED array. The circuit's very simple: pins 2-11 from the Arduino connected to the LED array, which is connected through 220ohm resistors to ground on the other side. First time I goofed and put the resistors into the breadboard on the Arduino side of the circuit, to no effect whatsoever. Future, more complex projects will include a circuit diagram (once I find a nice freeware program to do it), and of course, correct resistor positioning :-)

Here's a photo of the board connected up and a close up of the very simple circuit (click for larger versions):

  

Don't try to use pin 1 as a digital output, it won't work.

I put together two easy programs - one to move the lit LED from right-to-left and back again and one to move the lit LEDs from the middle out to the two sides and back in again. Kind of Knightrider-esque, but also the way the old SUN machines I used at university had their status lights on the back of the machine.

The code for the first one is:

/*
  10-bar LED array
 
  Connect the LED array to pins 11-2 and through a 220R resistor to ground on the other side.
  01/14/2010
*/

void setup ()
{
  for (int loop = 2; loop < 12; loop++)
  {
    pinMode (loop, OUTPUT);    // Set the pin IO mode
    digitalWrite (loop, LOW);
  }
}

void loop ()
{
  // The loop has to start at 2 because 1 isn't an IO pin.
  // Go from right to left
  for (int loop = 2; loop < 12; loop++)
  {
    // Switch the LED on for 10 milliseconds
    digitalWrite (loop, HIGH);
    delay (10);
    digitalWrite (loop, LOW);
    delay (20);
  }
 
  // And from left to right again
  for (int loop = 11; loop > 1; loop--)
  {
    digitalWrite (loop, HIGH);
    delay (10);
    digitalWrite (loop, LOW);
    delay (20);
  }
}

And the second one changes the loop to be:

void loop ()
{
  // Start on LED 6 and go up to 10 (remember, shifted by 1)
  for (int loop = 7; loop < 12; loop++)
  {
    // Light the LED on the LHS of middle and the matching one on the RHS
    // This will light LEDs 6-5, 7-4, 8-3, 9-2, 10-1 (shifted by 1)

    digitalWrite (loop, HIGH);

    digitalWrite (13 - loop, HIGH);
    delay (10);
    digitalWrite (loop, LOW);
    digitalWrite (13 - loop, LOW);
    delay (20);
  }

  // And back down to the middle again
  for (int loop = 11; loop > 6; loop--)
  {
    digitalWrite (loop, HIGH);
    digitalWrite (13 - loop, HIGH);
    delay (10);
    digitalWrite (loop, LOW);
    digitalWrite (13 - loop, LOW);
    delay (20);
  }
}

Note there's no main() function - it's all taken care of. The wrapper calls your setup() function and calls the loop() function in an infinite loop. You only have to provide these functions and you can use all the C++ programming constructs (if you want to) or keep it pretty simple.

I took some short (5 second) movies of each program running - you can get them from SideToSide movie and MiddleToSides movie (around 2MB each).

This was just my introduction to the environment, the real fun will start with some of the projects I have lined up:

  • 3-D LED cubes (3x3x3, 4x4x4, 8x8x8 and maybe higher - I have 1000 3mm red LEDs winging their way towards me - $29.95 from Rackmount-Devices.com)
  • panel of 8x8 LED arrays controlled through MAX7219 chips
  • object recognition with a simple CMOS camera
It's going to be a fun year! This is going to satisfy not only my desire to get back into electronics and start tinkering, but also to write some nifty low-level code.

As always, let me know if this is interesting, you're doing something similar, want something explained, or you have an idea for a cool project.

Enjoy! 



[KickIt] [Dzone] [Digg] [Reddit] [del.icio.us] [Facebook] [Technorati] [Google] [StumbleUpon]

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

E-mail | Permalink | Trackback | Post RSSRSS comment feed 4 Responses