Electronics Software Development

Adding Digital I/O To Your Arduino: Part 1 – The 74HC595

Add More IO Blue Graphic
Written by John Woolsey

Last Updated: March 18, 2021
Originally Published: February 4, 2021

Skill Level: Intermediate

Table Of Contents

Introduction

Sometimes, a project needs more digital I/O than what is available on your Arduino board. This often happens when you connect to components that require a lot of pins for their interface, e.g. some displays, or your project uses many discrete sensors and/or actuators.

This three-part tutorial teaches you how to add more digital inputs and outputs to your Arduino development board. Each part focuses on a specific integrated circuit (IC) chip.

Part 1 – The 74HC595 (currently reading) describes how to add digital outputs using the 74HC595 8-bit serial-in parallel-out (SIPO) shift register IC.

Part 2 – The 74HC165 will describe how to add digital inputs using the 74HC165 8-bit parallel-in serial-out (PISO) shift register IC.

Part 3 – The MCP23017 will describe how to add both digital inputs and outputs using the MCP23017 16-Bit I2C I/O Expander With Serial Interface IC.

A basic understanding of electronics and programming is expected along with some familiarity with the Arduino platform. If you are new to Arduino, or would just like to refresh your knowledge, please see our Blink: Making An LED Blink On An Arduino Uno tutorial before proceeding with this one. In addition, this tutorial will use a solderless breadboard to build a circuit from a schematic diagram. The All About Circuit’s Understanding Schematics, SparkFun’s How to Read a Schematic, Core Electronics’ How to Use Breadboards, and Science Buddies’ How to Use a Breadboard guides are good resources for learning how to translate a schematic to a breadboard.

The resources created for this tutorial are available on GitHub for your reference.

What Is Needed

Background Information

If you only need additional digital outputs for your project, the low-cost standard 7400 series 74HC595 IC is a good choice to incorporate into your design. It is an 8-bit serial-in parallel-out (SIPO) shift register that provides the ability to serially shift data into the chip and latch that data into separate parallel digital outputs. Data can be shifted from an Arduino into the shift register by using either the dedicated SPI serial bus (hardware implementation) or the standard Arduino shiftOut() function (software implementation) on a generic digital pin. I chose to use the shiftOut() function for this tutorial due to its simplicity along with saving a digital I/O pin when the SPI bus is not needed for other connections.

A single 74HC595 chip will provide 8 additional digital outputs with a single 8-bit data transfer. One nice feature about the ‘595s is that they can be daisy chained together to get even more outputs without utilizing any additional connections to your Arduino. For instance, incorporating four daisy chained ‘595s into your design will provide you an additional 32 (4 x 8) outputs with a 32-bit data transfer. Daisy chaining simply involves connecting the QH’ output from one ‘595 to the SER input of another ‘595. When more than 8 bits are shifted into one shift register, they continue to be propagated into the next shift register.

There are multiple ways to visualize and send your digital output data to the shift register within an Arduino sketch. Each has its pros and cons and can be heavily dependent on the nature of the additional outputs required. For instance, are the additional outputs highly disparate and need to be manipulated separately, or are they more homogeneous and can be referred to as a single block? I will present four different approaches in this tutorial, all producing the same basic functionally, so that you can easily compare among them and choose the best approach for your own design. While all of these approaches eventually shift and update all the output values in a single write to the 74HC595 shift register, each approach provides a different way to manipulate individual output changes. I will also include a fun little LED animation example at the end.

My development system consists of the Arduino Uno WiFi Rev2 development board connected to a macOS based computer running the desktop Arduino IDE. If you are using a different Arduino board or computer setup, the vast majority of this tutorial should still apply, however, some minor changes may be necessary.

If you need assistance with your particular setup, post a question in the comments section below and I, or someone else, can try to help you.

Building The Circuit

Before connecting any circuitry to your Arduino board, disconnect it from power and your computer. This avoids accidental damage during wiring.

Place the components and wire up the circuit on a breadboard according to the schematic diagram shown below.

Arduino Uno And 74HC595 Schematic
Schematic Diagram Of A 74HC595 Digital Outputs Circuit Connected To An Arduino Uno

Eight LEDs are connected to the shift register’s outputs (QAQH) constituting the 8 digital outputs being added to the system.

The typical 74HC595 IC has a total maximum current draw of 70 mA. In order to not overload the IC, I chose to be extra conservative and used 560 Ω resistors instead of the 330 Ω resistors I typically use with LEDs. These higher resistance values will slightly reduce the brightness of the LEDs, but will make sure we do not overload the IC when using LEDs whose voltage and current specifications may vary among different manufactures.

I also included a 0.1 µF bypass capacitor placed across the Vcc and ground pins of the shift register, as recommended in the 74HC595 datasheet, in order to reduce any power supply noise that may be present.

The OE (13) pin of the 74HC595 IC is tied directly to ground to enable constantly driving outputs. The SRCLR (10) pin is tied directly to 5 V to disable hardware based clearing of the shift register.

The circuit should look similar to the one shown below once completed.

Arduino Uno And 74HC595 Circuit Photo
Completed Arduino Uno And 74HC595 Digital Outputs Circuit

Once the circuit is built, connect your Arduino to your computer with the USB cable.

Changing Single Outputs Using Familiar digitalWrite() Functionality

This first approach implements the same calling mechanism as Arduino’s standard digitalWrite() function. It should be very familiar to long time Arduino users and the easiest to understand. However, when updating multiple outputs, it will involve more shift operations than other approaches since only one output at a time can be changed.

Open the Arduino IDE and create a sketch named OutputShiftRegister with the code shown below.

const int OSRDataPin = 2;   // connected to 74HC595 SER (14) pin
const int OSRLatchPin = 3;  // connected to 74HC595 RCLK (12) pin
const int OSRClockPin = 4;  // connected to 74HC595 SRCLK (11) pin

void setup() {
   // 74HC595 shift register
   pinMode(OSRDataPin, OUTPUT);
   pinMode(OSRLatchPin, OUTPUT);
   pinMode(OSRClockPin, OUTPUT);
   osrWriteRegister(0);  // turn off all LEDs

   delay(1000);  // wait a second
}

void loop() {
   changeOutputsWithDigitalWrite();
}

void osrWriteRegister(uint8_t outputs) {
   // Initiate latching process, next HIGH latches data
   digitalWrite(OSRLatchPin, LOW);
   // Shift output data into the shift register, most significant bit first
   shiftOut(OSRDataPin, OSRClockPin, MSBFIRST, outputs);
   // Latch outputs into the shift register
   digitalWrite(OSRLatchPin, HIGH);
}

void osrDigitalWrite(uint8_t pin, uint8_t value) {
   static uint8_t outputs = 0;  // retains shift register output values

   if (value == HIGH) bitSet(outputs, pin);  // set output pin to HIGH
   else if (value == LOW) bitClear(outputs, pin);  // set output pin to LOW
   osrWriteRegister(outputs);  // write all outputs to shift register
}

void changeOutputsWithDigitalWrite() {
   // Output pin definitions
   const uint8_t LED0 = 0;
   const uint8_t LED1 = 1;
   const uint8_t LED2 = 2;
   const uint8_t LED3 = 3;
   const uint8_t LED4 = 4;
   const uint8_t LED5 = 5;
   const uint8_t LED6 = 6;
   const uint8_t LED7 = 7;

   // Set individual LEDs
   osrDigitalWrite(LED1, HIGH);  // turn on LED1 only
   delay(1000);
   osrDigitalWrite(LED1, LOW);   // turn off LED1 only
   osrDigitalWrite(LED6, HIGH);  // turn on LED6 only
   delay(1000);
   osrDigitalWrite(LED6, LOW);   // turn off LED6 only
   delay(1000);

   // Set multiple LEDs
   osrDigitalWrite(LED0, HIGH);  // turn on even numbered LEDs
   osrDigitalWrite(LED2, HIGH);
   osrDigitalWrite(LED4, HIGH);
   osrDigitalWrite(LED6, HIGH);
   delay(1000);
   osrDigitalWrite(LED0, LOW);   // turn off even numbered LEDs
   osrDigitalWrite(LED2, LOW);
   osrDigitalWrite(LED4, LOW);
   osrDigitalWrite(LED6, LOW);
   osrDigitalWrite(LED1, HIGH);  // turn on odd numbered LEDs
   osrDigitalWrite(LED3, HIGH);
   osrDigitalWrite(LED5, HIGH);
   osrDigitalWrite(LED7, HIGH);
   delay(1000);
   osrDigitalWrite(LED1, LOW);   // turn off odd numbered LEDs
   osrDigitalWrite(LED3, LOW);
   osrDigitalWrite(LED5, LOW);
   osrDigitalWrite(LED7, LOW);
   delay(1000);
}

Let’s take a look at some of the more interesting parts of the code.

The 74HC595 shift register only needs three pins to communicate with the Arduino. The OSRDataPin sends shifted data that is clocked with the OSRClockPin. The OSRLatchPin is used to latch the output data, after the data has been shifted in, into the register.

Since we will be applying multiple approaches, I separated each approach into its own distinct function. Therefore, the changeOutputsWithDigitalWrite() function, this first approach, is the only function I am currently calling within the loop() function.

The osrWriteRegister() function is the main workhorse in this sketch as it is the one that actually sends the output data to the 74HC595 shift register. It first drives the latch pin LOW to initiate the latching process. It then shifts out the output data into the register with the Arduino standard shiftOut() function. Finally, the latch pin is driven HIGH locking in and driving the new outputs.

The osrDigitalWrite() function implements the familiar mechanism we are used to with Arduino’s digitalWrite() function, but updates the shift register’s outputs instead of the Arduino board’s standard digital pins. An outputs variable is defined that keeps track of and retains (with the static keyword) the output values. Values are set (HIGH) and cleared (LOW) using bit operations. All outputs are then sent with the osrWriteRegister() function call. Note, as with digitalWrite(), the osrDigitalWrite() function only updates one output pin per call.

The changeOutputsWithDigitalWrite() function provides an example utilizing the osrDigitalWrite() approach. We first define our named constants, LED0LED7, to make our calls easier to understand. The constants are defined within this example function, instead of the typical location at the top of the file, in order to avoid collisions with different definitions in other approaches. The rest of the function just uses the osrDigitalWrite() function to turn on and off a couple of LEDs and then turn on and off either the even or odd numbered LEDs.

If there is something that needs further explanation, please let me know in the comment section and I will try to answer your question.

Save your program when you are done editing.

Now that our circuit is built and our software is written, let’s run and test our program. Upload the sketch to your Arduino board and you should see the 74HC595 shift register’s LED outputs being updated. First, LED1 turns on, then only LED6 is on, then only the even numbered LEDs are lit, then only the odd numbered LEDs are lit. This sequence will be continuously repeated.

Changing All Outputs Using Binary Values

This next approach is the simplest of all the approaches. It simply sends a byte of data to the shift register with each bit representing each output. It produces the most concise code, but it does not indicate the meaning of each of the individual outputs. This approach would be a good option to use when all outputs are of the same type and you want to refer to the entire collection as a single entity.

Add the following function to the end of your sketch

void changeOutputsWithBinaryValues() {
   // Set individual LEDs
   osrWriteRegister(0b00000010);  // turn on LED1 only
   delay(1000);
   osrWriteRegister(0b01000000);  // turn on LED6 only
   delay(1000);
   osrWriteRegister(0b00000000);  // turn off all LEDs
   delay(1000);

   // Set multiple LEDs
   osrWriteRegister(0b01010101);  // turn on only even numbered LEDs
   delay(1000);
   osrWriteRegister(0b10101010);  // turn on only odd numbered LEDs
   delay(1000);
   osrWriteRegister(0b00000000);  // turn off all LEDs
   delay(1000);
}

and then change the body of the loop() function to the following so that you’re calling the newly added function instead of the first one.

// changeOutputsWithDigitalWrite();
changeOutputsWithBinaryValues();

Save your work and upload the updated sketch to test the code. You should see the same LED sequence as we saw in the previous section.

Changing All Outputs Using Defined Names

This approach is an extension of the previous one using binary values. It adds constants that name each of the outputs (bits) that can then be grouped together before being sent as a single byte. The output values are determined by performing bitwise OR operations with all outputs that are to be set HIGH (1). All other outputs default to LOW (0).

Add the following function to the end of your sketch

void changeOutputsWithDefinedNames() {
   // Output pin definitions
   const uint8_t LED0 = 0b00000001;
   const uint8_t LED1 = 0b00000010;
   const uint8_t LED2 = 0b00000100;
   const uint8_t LED3 = 0b00001000;
   const uint8_t LED4 = 0b00010000;
   const uint8_t LED5 = 0b00100000;
   const uint8_t LED6 = 0b01000000;
   const uint8_t LED7 = 0b10000000;

   // Set individual LEDs
   osrWriteRegister(LED1);  // turn on LED1 only
   delay(1000);
   osrWriteRegister(LED6);  // turn on LED6 only
   delay(1000);
   osrWriteRegister(0);     // turn off all LEDs
   delay(1000);

   // Set multiple LEDs
   osrWriteRegister(LED0 | LED2 | LED4 | LED6);  // turn on only even numbered LEDs
   delay(1000);
   osrWriteRegister(LED1 | LED3 | LED5 | LED7);  // turn on only odd numbered LEDs
   delay(1000);
   osrWriteRegister(0);                          // turn off all LEDs
   delay(1000);
}

and then change the body of the loop() function to the following.

// changeOutputsWithDigitalWrite();
// changeOutputsWithBinaryValues();
changeOutputsWithDefinedNames();

Save your work and upload the sketch. You should see the same LED sequence as before.

Changing Outputs Using Bit Operations

This last approach is somewhat of a compromise among all the approaches shown so far. It has the ability of individually changing named outputs while also being able to update multiple outputs in a single write to the shift register, but it has the drawback of producing lengthy code when many outputs are required to change simultaneously.

Add the following function to the end of your sketch

void changeOutputsWithBitOperations() {
   // Output pin definitions
   const uint8_t LED0 = 0;
   const uint8_t LED1 = 1;
   const uint8_t LED2 = 2;
   const uint8_t LED3 = 3;
   const uint8_t LED4 = 4;
   const uint8_t LED5 = 5;
   const uint8_t LED6 = 6;
   const uint8_t LED7 = 7;

   uint8_t outputs = 0;  // holds shift register output values

   // Set individual LEDs
   bitSet(outputs, LED1);    // turn on LED1
   osrWriteRegister(outputs);
   delay(1000);
   bitClear(outputs, LED1);  // turn off LED1
   bitSet(outputs, LED6);    // turn on LED6
   osrWriteRegister(outputs);
   delay(1000);
   bitClear(outputs, LED6);  // turn off LED6
   osrWriteRegister(outputs);
   delay(1000);

   // Set multiple LEDs
   bitSet(outputs, LED0);    // turn on even numbered LEDs
   bitSet(outputs, LED2);
   bitSet(outputs, LED4);
   bitSet(outputs, LED6);
   osrWriteRegister(outputs);
   delay(1000);
   bitClear(outputs, LED0);  // turn off even numbered LEDs
   bitClear(outputs, LED2);
   bitClear(outputs, LED4);
   bitClear(outputs, LED6);
   bitSet(outputs, LED1);    // turn on odd numbered LEDs
   bitSet(outputs, LED3);
   bitSet(outputs, LED5);
   bitSet(outputs, LED7);
   osrWriteRegister(outputs);
   delay(1000);
   bitClear(outputs, LED1);  // turn off odd numbered LEDs
   bitClear(outputs, LED3);
   bitClear(outputs, LED5);
   bitClear(outputs, LED7);
   osrWriteRegister(outputs);
   delay(1000);
}

and then change the body of the loop() function to the following.

// changeOutputsWithDigitalWrite();
// changeOutputsWithBinaryValues();
// changeOutputsWithDefinedNames();
changeOutputsWithBitOperations();

An outputs variable is used to keep track of the output values. The LED0LED7 constants are defined as their bit position in the outputs variable. The bitSet() and bitClear() bit operations can then be used on the outputs variable to set (HIGH) and clear (LOW) each individual output (bit) respectively. Any number of bit operations can be performed before sending the final updated values with a call to osrWriteRegister().

Save your work and upload the sketch. You should see the same LED sequence as before.

An LED Cycling Example

This is just an example of a fun animation (Knight Rider style) that shows how to use the bit operations a little differently than the previous approach.

Add the following function to the end of your sketch

void cycleLEDs() {
   uint8_t outputs = 0;  // holds shift register output values

   // Cycle through individual LEDs from LED0 to LED6
   for (uint8_t i = 0; i < 7; i++) {
      bitSet(outputs, i);
      osrWriteRegister(outputs);
      bitClear(outputs, i);
      delay(100);
   }

   // Cycle through individual LEDs from LED7 to LED1
   for (uint8_t i = 7; i > 0; i--) {
      bitSet(outputs, i);
      osrWriteRegister(outputs);
      bitClear(outputs, i);
      delay(100);
   }
}

and then change the body of the loop() function to the following.

// changeOutputsWithDigitalWrite();
// changeOutputsWithBinaryValues();
// changeOutputsWithDefinedNames();
// changeOutputsWithBitOperations();
cycleLEDs();

For loops are used to cycle through the LEDs from one end to the other with a small delay between shifts.

Again, save your work and upload the sketch. The LED’s should be turning on and off in sequence, back and forth, across all LEDs.

Before we end, now would be a good time to upload the BareMinimum sketch (Main Menu > File > Examples > 01.Basics > BareMinimum) to reset all pins back to their default states. This ensures no outputs are being driven when plugging in your board for your next project.

Summary

In this tutorial, we learned how to add digital outputs to your Arduino board using the 74HC595 serial-in parallel-out (SIPO) shift register. I presented multiple approaches for how to represent the outputs in your Arduino code so that you can compare and choose the right implementation in your own designs. They range from a simplistic approach that updates all outputs with a single write to the shift register to updating a single output at a time using the familiar digitalWrite() mechanism. I also included an LED animation as a fun example.

The final source code and schematic used for this tutorial are available on GitHub. The GitHub version of the code is fully commented to include additional information, such as the program’s description, circuit connections, code clarifications, and other details. The comments are also Doxygen compatible in case you want to generate the code documentation.

The next part of this three-part tutorial, Part 2 – The 74HC165, will describe how to add digital inputs using the 74HC165 8-bit parallel-in serial-out (PISO) shift register IC.

Thank you for joining me along this journey and I hope you enjoyed the experience. Please feel free to share your thoughts or questions in the comments section below.

About the author

John Woolsey

John is an electrical engineer who loves science, math, and technology and teaching it to others even more.
 
He knew he wanted to work with electronics from an early age, building his first robot when he was in 8th grade. His first computer was a Timex/Sinclair 2068 followed by the Tandy 1000 TL (aka really old stuff).
 
He put himself through college (The University of Texas at Austin) by working at Motorola where he worked for many years afterward in the Semiconductor Products Sector in Research and Development.
 
John started developing mobile app software in 2010 for himself and for other companies. He has also taught programming to kids for summer school and enjoyed years of judging kids science projects at the Austin Energy Regional Science Festival.
 
Electronics, software, and teaching all culminate in his new venture to learn, make, and teach others via the Woolsey Workshop website.

Leave a Comment

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