Getting the 74HC595 to talk

micrometer scale closeup of the 74HC595 circuit

Now that we have a basic understanding of the 595, let’s start using it by writing up some code to make it control our 5461AS. From the software side of things, using a shift register to drive our 8-segment display is mostly the same as driving it directly with GPIOs. We still have to illuminate one digit at a time for 1 millisecond each; what’s changed is how we send power to the anode pins on our display.

How to use the SN74HC595 shift register

The first thing we have to do is send a byte of data representing what pins we want HIGH and LOW to the 595. We’re going to do that with electrical pulses. If we want to send a 1, we pull the GPIO pin connected to the serial input HIGH, then we pulse the clock pin HIGH then LOW to signal that we’re sending a bit. If we want to send a 0 we pull the serial pin LOW when we pulse the clock. Pulsing the clock pin is our signal to the 595 that it should store the value (HIGH or LOW, 1 or 0) on the serial pin in its register. When we pulse the clock pin again, it’s going to shift that value down to the next register to make room for the new bit. Get it? Shift register.

When we’re done sending our byte (8 bits) to the 595, we have to pulse the latch pin (register clock) to signal that it can power the output pins according to the data in its registers

Coding for the 595 shift register

Let’s start with changing up our pins. We don’t need an array for our segment pins anymore because that’s handled by the 595, but we do need to update our “digit” pins, the ones on the cathode end of the 5461AS.

int digit[] = {32, 19, 18, 17};

We should also make some variables for our other 595 data pins, SER, SRCLK, and RCLK. I’m going to stick with those for the names so I can remember them. That’s just how I learn.

const int SER = 21;
const int SRCLK = 23;
const int RCLK = 22;

Boom, so we’ve gone from 12 GPIO pins down to 7. That’s not a huge savings but it’s significant. Let’s jump into setup() and get those setup as output pins.

void setup() {
  for (int i = 0; i < 4; i++) {
    pinMode(digit[i], OUTPUT);
    digitalWrite(digit[i], HIGH);
  }

  pinMode(SER, OUTPUT);
  digitalWrite(SER, LOW);

  pinMode(SRCLK, OUTPUT);
  digitalWrite(SRCLK, LOW);

  pinMode(RCLK, OUTPUT);
  digitalWrite(RCLK, LOW);
}

I also went ahead and set the pins to the starting voltage levels I want them at, just so there are no surprises.

Next, I want to learn something new, so I’m going to convert my number-segment-illumination matrix to an array of binary numbers, since that’s what it essentially already is. Then, when I send data to the 595, I can iterate over the digits with a bitwise operator (I’ll explain it more when I get to my transmission function). To represent a binary number in C++, prefix it with 0b. So instead of this matrix:

int nums[10][7] = {
  {1, 1, 1, 1, 1, 1, 0},  // 0
  {0, 1, 1, 0, 0, 0, 0},  // 1
  {1, 1, 0, 1, 1, 0, 1},  // 2
  {1, 1, 1, 1, 0, 0, 1},  // 3
  {0, 1, 1, 0, 0, 1, 1},  // 4
  {1, 0, 1, 1, 0, 1, 1},  // 5
  {0, 0, 1, 1, 1, 1, 1},  // 6
  {1, 1, 1, 0, 0, 0, 0},  // 7
  {1, 1, 1, 1, 1, 1, 1},  // 8
  {1, 1, 1, 0, 0, 1, 1}   // 9
};

we have this array:

int nums[10] = {
  0b11111100,  // 0
  0b01100000,  // 1
  0b11011010,  // 2
  0b11110010,  // 3
  0b01100110,  // 4
  0b10110110,  // 5
  0b00111110,  // 6
  0b11100000,  // 7
  0b11111110,  // 8
  0b11100110   // 9
};

You can see that it’s basically the same thing, we’re just storing the segment data in one number instead of an array of numbers. Let’s take a look at the actual logic we’re going to need.

Before I start typing up a new function in my IDE, I always think about what I want my function to do. For the function to send a byte to the 595, I know I need to pass it one of my number bytes, so there’s my parameter. I need a loop to iterate over my byte, sending one bit at a time, and I know I need to send pulses to the clock pins.

Let’s start with the pulse function quickly, because it’s pretty simple. We need to be able to pass a pin to it, pull it HIGH, then drop it LOW.

void pulse(int pin) {
  digitalWrite(pin, HIGH);
  digitalWrite(pin, LOW);
}

I like easy. Onto the trickier part. First, the shell:

void send(int data) {
  
}

Even though we’re using binary numbers, they’re still integers, so that dictates the data type for our parameter.

Let’s make the loop next, I’m thinking for loop since we know it’s always going to run 8 times (for now), and we’re going to need its iterator to do a neat trick with some bitwise operators.

Bitwise operators let you directly manipulate the binary data that you’re working with. There are six of them in C++, but today we’re just worried about the >> (right shift) operator which simply shifts all the digits in your number a fixed number of place-value positions to the right.

00010100≫1=0000101000010100 \gg 1 = 00001010
00010100≫2=0000010100010100 \gg 2 = 00000101

By using the right shift operator and applying a modulo operation to the result, we can read the individual values of our byte. Let’s look at the code to make more sense of it.

First we have the for loop. We’re working through a byte so we only have to iterate through 8 bits.

void send(int data) {
  for (int i = 0; i < 8; i++) {
    digitalWrite(SER, (data >> i) % 2);
    pulse(SRCLK);
  }
  pulse(RCLK);
}

Next we send our data bit. We’re sending it to the serial pin and our expression will always evaluate to 0 or 1 which is the same as LOW or HIGH.

void send(int data) {
  for (int i = 0; i < 8; i++) {
    digitalWrite(SER, (data >> i) % 2);
    pulse(SRCLK);
  }
  pulse(RCLK);
}

Next we have to send a pulse to the serial clock pin to let the 595 know that it needs to store the bit being sent on the serial pin.

void send(int data) {
  for (int i = 0; i < 8; i++) {
    digitalWrite(SER, (data >> i) % 2);
    pulse(SRCLK);
  }
  pulse(RCLK);
}

Once we’ve run through this process for all 8 bits, we need to pulse the latch pin to let the chip know to update the state of its output pins.

void send(int data) {
  for (int i = 0; i < 8; i++) {
    digitalWrite(SER, (data >> i) % 2);
    pulse(SRCLK);
  }
  pulse(RCLK);
}

This send() function basically supplants the display() function we had in our last program, so we don’t need that anymore. We still need a way to extract the place values from our number, and I’ve found a cleaner way to do that. Instead of this:

int numberDigits[4] = {
	number % 10,
	(number % 100 - number % 10) / 10,
	(number % 1000 - number % 100) / 100,
	(number % 10000 - number % 1000) / 1000
};

I’m going to use this (which is much cleaner):

int numberDigits[4] = {
	number % 10,
	(number / 10) % 10,
	(number / 100) % 10,
	(number / 1000) % 10
};

All that’s left to do is swap one line in the loop we use to multiplex our display. Instead of this:

for (int i = 0; i < 4; i++) {
  digitalWrite(digit[i], LOW);
  display(numberDigits[i]);
  delay(1);
  digitalWrite(digit[i], HIGH);
}

we switch it to this:

for (int i = 0; i < 4; i++) {
  digitalWrite(digit[i], LOW);
  send(nums[numberDigits[i]]);
  delay(1);
  digitalWrite(digit[i], HIGH);
}

And with that, we have the exact same behavior as our first implementation, just using five fewer GPIO pins, which we can use for buttons or a rotary encoder.


Buy somethin’ will ya?

If you like what I do, send me $1 to help pay for the infrastructure!

Comments

Leave a Reply

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

Share with