Electronics Software Development

Adding Digital I/O To Your Arduino: Part 2 – The 74HC165

Add More IO Pink Graphic
Written by John Woolsey

Last Updated: August 18, 2021
Originally Published: February 18, 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 described how to add digital outputs using the 74HC595 8-bit serial-in parallel-out (SIPO) shift register IC.

Part 2 – The 74HC165 (currently reading) describes 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 inputs for your project, the low-cost standard 7400 series 74HC165 IC is a good choice to incorporate into your design. It is an 8-bit parallel-in serial-out (PISO) shift register that provides the ability to read and latch in separate parallel digital inputs and serially shift the input data into an Arduino. Data can be shifted from the chip into an Arduino by using either the dedicated SPI serial bus (hardware implementation) or the standard Arduino shiftIn() function (software implementation) on a generic digital pin. I chose to use the shiftIn() 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 74HC165 chip will provide 8 additional digital inputs with a single 8-bit data transfer. One nice feature about the ‘165s is that they can be daisy chained together to get even more inputs without utilizing any additional connections to your Arduino. For instance, incorporating four daisy chained ‘165s into your design will provide you an additional 32 (4 x 8) inputs with a 32-bit data transfer. Daisy chaining simply involves connecting the QH output from one ‘165 to the SER input of another ‘165. 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 read your digital input data from the shift register within an Arduino sketch. Each has its pros and cons and can be heavily dependent on the nature of the additional inputs required. For instance, are the additional inputs highly disparate and need to be read separately, or are they more homogeneous and can be referred to as a single block? I will present three different approaches in this tutorial so that you can easily compare them and choose the best approach for your own design. While all of these approaches initially read and shift all the input values in a single read from the 74HC165 shift register, each approach provides a different way to read individual input changes. I will also include an additional example at the end that will only print the input values if a change in one of them is detected.

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 74HC165 Schematic
Schematic Diagram Of A 74HC165 Digital Inputs Circuit Connected To An Arduino Uno

The 74HC165 symbol used in the KiCad schematic utilizes a different pin naming convention than what is used in the Texas Instruments datasheet for this part. The table below shows the corresponding equivalent pins between the two naming conventions. The _n suffix used in the names denote active low or complementary signals, those with a bar above their pin names in the datasheet and schematic. I will be using the pin naming convention used by the TI datasheet throughout this tutorial.

KiCad Schematic 74HC165 PinsTI Datasheet 74HC165 Pins
CE_n (clock enable input)CLK INH (clock inhibit input)
CP (clock input)CLK (clock input)
D0-D7 (parallel data inputs)A-H (parallel inputs)
DS (serial data input)SER (serial input)
PL_n (parallel load input)SH/LD_n (shift/load input)
Q7 (serial output)QH (serial output)
Q7_n (complementary serial output)QH_n (complementary serial output)
74HC165 Pin Equivalence Between Schematic And Datasheet

An 8-position dip switch, with attached pull-down resistors, is connected to the shift register’s inputs (AH) constituting the 8 digital inputs being added to the system. Eight individual standard single-pole single-throw (SPST) switches may be used instead of the integrated 8-position dip switch if you prefer.

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

The SER (10) and CLK INH (15) pins are tied directly to ground since we will not be shifting any data into the shift register or inhibiting the clock signal.

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

Arduino Uno And 74HC165 Circuit Photo
Completed Arduino Uno And 74HC165 Digital Inputs Circuit

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

Reading Single Inputs Using Familiar digitalRead() Functionality

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

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

const unsigned long SamplePeriod = 5000;  // sampling period in milliseconds

const uint8_t ISRDataPin = 2;   // connected to 74HC165 QH (9) pin
const uint8_t ISRLatchPin = 3;  // connected to 74HC165 SH/LD (1) pin
const uint8_t ISRClockPin = 4;  // connected to 74HC165 CLK (2) pin

const uint8_t InputA = 0;  // bit position for 74HC165 A input
const uint8_t InputB = 1;  // bit position for 74HC165 B input
const uint8_t InputC = 2;  // bit position for 74HC165 C input
const uint8_t InputD = 3;  // bit position for 74HC165 D input
const uint8_t InputE = 4;  // bit position for 74HC165 E input
const uint8_t InputF = 5;  // bit position for 74HC165 F input
const uint8_t InputG = 6;  // bit position for 74HC165 G input
const uint8_t InputH = 7;  // bit position for 74HC165 H input

void setup() {
   // Serial Monitor
   Serial.begin(9600);  // initialize serial bus
   while (!Serial);     // wait for serial connection

   // 74HC165 shift register
   pinMode(ISRDataPin, INPUT);
   pinMode(ISRLatchPin, OUTPUT);
   pinMode(ISRClockPin, OUTPUT);
}

void loop() {
   // Read and print inputs at the specified sampling rate
   static unsigned long previousTime = 0;
   unsigned long currentTime = millis();
   if (currentTime - previousTime >= SamplePeriod) {
      readInputsWithDigitalRead();
      previousTime = currentTime;
   }
}

uint8_t isrReadRegister() {
   uint8_t inputs = 0;
   digitalWrite(ISRClockPin, HIGH);  // preset clock to retrieve first bit
   digitalWrite(ISRLatchPin, HIGH);  // disable input latching and enable shifting
   inputs = shiftIn(ISRDataPin, ISRClockPin, MSBFIRST);  // capture input values
   digitalWrite(ISRLatchPin, LOW);  // disable shifting and enable input latching
   return inputs;
}

int isrDigitalRead(uint8_t pin) {
   return bitRead(isrReadRegister(), pin);
}

void readInputsWithDigitalRead() {
   // Read and print individual inputs
   Serial.print("Input A = ");  Serial.println(isrDigitalRead(InputA) ? "HIGH" : "LOW");
   Serial.print("Input B = ");  Serial.println(isrDigitalRead(InputB) ? "HIGH" : "LOW");
   Serial.print("Input C = ");  Serial.println(isrDigitalRead(InputC) ? "HIGH" : "LOW");
   Serial.print("Input D = ");  Serial.println(isrDigitalRead(InputD) ? "HIGH" : "LOW");
   Serial.print("Input E = ");  Serial.println(isrDigitalRead(InputE) ? "HIGH" : "LOW");
   Serial.print("Input F = ");  Serial.println(isrDigitalRead(InputF) ? "HIGH" : "LOW");
   Serial.print("Input G = ");  Serial.println(isrDigitalRead(InputG) ? "HIGH" : "LOW");
   Serial.print("Input H = ");  Serial.println(isrDigitalRead(InputH) ? "HIGH" : "LOW");
   Serial.println();
}

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

We first define a sampling period that governs how often we will check the inputs. I chose 5 seconds in order to give you time to change the switches between readings.

The 74HC165 shift register only needs three pins to communicate with the Arduino. The ISRDataPin receives shifted data that is clocked with the ISRClockPin. The shift register’s SH/LD pin, attached to ISRLatchPin, is a dual function pin. When the pin is driven low, the shift register continuously latches in (captures) the input values. When driven high, the shift register enters its shifting mode of operation.

Constants InputAInputH are used to refer to the individual inputs of the shift register. These will be utilized in a couple of the approaches we will cover, including this one.

Since we will be applying multiple approaches, I separated each approach into its own distinct function. Therefore, the readInputsWithDigitalRead() function, this first approach, is the only function I am currently calling within the loop() function. The rest of the loop() function checks and compares the time so that the readInputsWithDigitalRead() function is only called at the specified sampling rate.

The isrReadRegister() function is the main workhorse in this sketch as it is the one that actually reads the input data from the 74HC165 shift register. The standard Arduino shiftIn() function reads values after a rising clock edge. The 74HC165 requires values to be read before a rising clock edge. While doing research, I saw that most people solved this problem by using an additional Arduino digital pin connected to the shift register’s CLK INH pin in order to manage the clock directly. I believed there had to be a way to save the use of this extra pin. I could have written my own shiftIn() function that reverses when the data is being read, but I chose instead to apply a little trick that I found so that I could continue using the standard shiftIn() function and still save that extra pin. This trick involves driving the clock pin high before calling shiftIn() so that the first rising clock being performed within the shiftIn() function is ignored allowing us to read the data beforehand. We then proceed with the normal shifting process by enabling shifting on the shift register, calling shiftIn() to shift the input data into the Arduino, and then disabling shifting. Note, the 74HC165 shift register remains in latching mode, capturing inputs, when not in the process of shifting.

The isrDigitalRead() function implements the familiar mechanism we are used to with Arduino’s digitalRead() function, but reads the shift register’s inputs instead of the Arduino board’s standard digital pins. All inputs are read with the isrReadRegister() function call and then the particular input (bit) of interest is captured with the bitRead() function. Note, as with digitalRead(), the isrDigitalRead() function only returns one input value per call.

The readInputsWithDigitalRead() function provides an example utilizing the isrDigitalRead() approach. This function simply reads and prints out all of the individual input values, represented as either HIGH or LOW, read from the shift register using the isrDigitalRead() function. It uses the global constants InputAInputH to make our calls easier to understand.

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. We will be utilizing the Serial Monitor to display the inputs read from the 74HC165 shift register. Open the Serial Monitor window and upload the sketch to the board. You should see input values being printed in the Serial Monitor every 5 seconds. Flip a few of the dip switches and watch the changes being shown in the Serial Monitor.

Reading All Inputs Using Binary Values

This next approach is the simplest of all the approaches. It produces the most concise code, but it does not indicate the meaning of each of the individual inputs. It is a good option to use when all inputs 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 readInputsWithBinaryValues() {
   // Read and print all inputs from shift register in binary format
   Serial.print("Inputs: 0b");
   Serial.println(isrReadRegister(), BIN);
}

and then change the calling statement within the loop() function from

readInputsWithDigitalRead();

to the following so that you’re calling the newly added function instead of the first one.

// readInputsWithDigitalRead();
readInputsWithBinaryValues();

This function simply reads all the input data from the shift register and then prints that data in binary format with each bit representing each input. Note, the BIN formatting option specified in the Serial.println() statement does not pad with leading zeros, so it will not always print the full 8 bits for all input values.

Save your work and upload the updated sketch to test the code. You should see all input values being printed in binary format every 5 seconds. Flip a few of the dip switches and watch the changes being shown in the Serial Monitor.

Reading All Inputs Using Defined Names And Bit Operations

This last approach is somewhat of a compromise between the other two approaches we covered. It only reads the shift register once, but allows you to refer to the individual inputs.

Add the following function to the end of your sketch

void readInputsWithDefinedNamesAndBitOperations() {
   // Read all inputs from shift register
   uint8_t inputs = isrReadRegister();

   // Read and print individual inputs
   Serial.print("Input A = ");  Serial.println(bitRead(inputs, InputA) ? "HIGH" : "LOW");
   Serial.print("Input B = ");  Serial.println(bitRead(inputs, InputB) ? "HIGH" : "LOW");
   Serial.print("Input C = ");  Serial.println(bitRead(inputs, InputC) ? "HIGH" : "LOW");
   Serial.print("Input D = ");  Serial.println(bitRead(inputs, InputD) ? "HIGH" : "LOW");
   Serial.print("Input E = ");  Serial.println(bitRead(inputs, InputE) ? "HIGH" : "LOW");
   Serial.print("Input F = ");  Serial.println(bitRead(inputs, InputF) ? "HIGH" : "LOW");
   Serial.print("Input G = ");  Serial.println(bitRead(inputs, InputG) ? "HIGH" : "LOW");
   Serial.print("Input H = ");  Serial.println(bitRead(inputs, InputH) ? "HIGH" : "LOW");
   Serial.println();
}

and then change the calling statements within the loop() function from

// readInputsWithDigitalRead();
readInputsWithBinaryValues();

to the following so that you’re calling the newly added function instead.

// readInputsWithDigitalRead();
// readInputsWithBinaryValues();
readInputsWithDefinedNamesAndBitOperations();

This approach is very similar to the readInputsWithDigitalRead() approach, but it only reads the shift register once and uses bit operations to read each individual input (bit) captured in the inputs variable. The InputAInputH constants, defined earlier, are used as the bit positions when referring to the individual inputs.

Save your work and upload the updated sketch to test the code. You should see all input values being printed every 5 seconds. Flip a few of the dip switches and watch the changes being shown in the Serial Monitor.

Printing Changing Inputs Example

All of the example approaches we covered previously required an arbitrary sample rate that printed the shift register’s input values every 5 seconds regardless of whether any of the inputs had changed. This example only prints the values once a change in inputs is detected.

Add the following function to the end of your sketch,

void readAndPrintInputsOnChange() {
   static uint8_t previousInputs = 0;
   uint8_t currentInputs = isrReadRegister();  // read all inputs from shift register
   if (currentInputs != previousInputs) {  // print values only if they changed
      Serial.print("Inputs: 0b");  // print all inputs represented as a full byte
      for (int8_t i = 7; i >= 0; i--) {
         Serial.print(bitRead(currentInputs, i));  // print value for each bit
      }
      Serial.println();
      previousInputs = currentInputs;
   }
}

comment out all the existing code in the body of the loop() function, and then add the following statement at the end of the loop() function.

readAndPrintInputsOnChange();

The readAndPrintInputsOnChange() function saves the previous and current input values retrieved from the shift register and compares them before printing. It also uses bit operations a little differently in that it sequences through all of the input bits instead of using named inputs. In addition, since we are printing each individual input (bit) ourselves, we are able to provide the full 8-bit binary representation for all inputs in the Serial Monitor.

Since the only active statement now in the loop() function calls the readAndPrintInputsOnChange() function, this function constantly repeats and alerts us to any input changes.

Again, save your work and upload the sketch. You should now only see inputs printed in the Serial Monitor after you have flipped one or more of the switches.

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 inputs to your Arduino board using the 74HC165 parallel-in serial-out (PISO) shift register. I presented multiple approaches for how to represent the inputs 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 reads all inputs with a single read from the shift register to reading a single input at a time using the familiar digitalRead() mechanism. I also included an example that only prints the input values when a change is detected.

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 3 – The MCP23017, will describe how to add both digital inputs and outputs using the MCP23017 16-bit I/O expander 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.

2 Comments

  • Hi John,

    really exiting, the arduino-projects you present. I’m actually building a CO2-measurement device with arduino. It’s working fine, but there is one simple problem.

    I bought some really small arduino-compatible boards with an uart ch340. Until today I didn’t manage to get an usb-connection between my MacBook Air with 10.15.7 Catalina and several boards, all using this (dammed 🙂 ) ch340.

    The driver is signed by the vendor – there is no security warning from SPI. Setup is smooth and easy going. But it doesn’t work at all.

    Yes, I checked the net und tried several methods, to make it run. It’s no problem to install the usbserial.kext in /Library/Extension. That’s working. But when I plug the board in and look for
    ls /dev/tty* resp. ls /dev/cu*, no ch340-board occurs. As soon as I plug in an Arduino, I get the device there instantly and the connection to the IDE.

    That’s my concern at the moment. Do have any idea or experience with that stuff?

    Kind regards
    Mike

    • I only dealt with this with one board in the past and hear the horror stories regularly. For this reason, I never purchase compatibles and only get my Arduino boards directly from Arduino. I may pay a little more, but it saves me a lot of time and frustration.

Leave a Comment

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