Last Updated: August 18, 2021
Originally Published: February 18, 2021
Skill Level: Intermediate
Table Of Contents
- Introduction
- What Is Needed
- Background Information
- Building The Circuit
- Reading Single Inputs Using Familiar digitalRead() Functionality
- Reading All Inputs Using Binary Values
- Reading All Inputs Using Defined Names And Bit Operations
- Printing Changing Inputs Example
- Summary
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
- Linux, macOS, Or Windows Based Computer With A USB Port
- Arduino IDE
- Arduino Uno (R3 available on Arduino and SparkFun; WiFi Rev2 on Arduino and SparkFun) With Compatible USB Cable
- Solderless Breadboard (available on Adafruit and SparkFun)
- Preformed Breadboard Jumper Wire Kit (available on SparkFun and CanaKit)
- 5 x Male/Male Jumper Wires (available on Adafruit and Arrow)
- 8-Position Dip Switch (available on SparkFun and Digi-Key)
- 74HC165 8-Bit Parallel-Load Shift Registers IC (available on Digi-Key and Arrow)
- 0.1 µF Ceramic Capacitor (available on SparkFun and Jameco)
- 8 x 10 KΩ Resistors (available on SparkFun and Amazon)
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.
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 Pins | TI 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) |
An 8-position dip switch, with attached pull-down resistors, is connected to the shift register’s inputs (A – H) 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.
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 InputA
– InputH
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 InputA
– InputH
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 InputA
–InputH
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.
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.
Hey John,
Great work!
I am using Hall sensor A3144 with Shift register 74HC165 connected to Arduino. I used above code for this sensor but it does not seem to work. Could you guide me through this? I use Arduino IDE on windows.
Sure, I will try. First of all, is everything working for you as expected with just the switches as shown in the tutorial? If so, then we can move on to the hall effect sensor.
After a little research, it appears the A3144 sensor needs to be connected a little differently than the switches currently being used. The 10K pull-up resistor needs to be placed on the high side (power) instead of the low side (ground). See this component information page for more detail. Also, keep in mind that the hall effect sensor will register as LOW in the code when a magnet comes close to the sensor.
Thank You John for quick reply!
I had only 3 push buttons with me so I connected them to the register 165 and rest of the Input pins were connected to the ground. The pin1 of 165 is connected to 3 at arduino, pin2 to 4 and pin 7 to 2 at arduino (as per the above code).Pin16 to 5V. Pin9 no connection. Pin8,10 & 15 and input pins except pin3,4 & 5 of 165 to GND. Pin 3,4,&5 are connected to one end of each push button.
The 3 push buttons are connected to 10 ohm resistor each, connecting them to GND. Adjacent to resistor is 5V connection of push buttons & the other pin of buttons is connected to the shift register 165.
I am getting output as HIGH from all inputs of shift register. No change after pressing the buttons.
I also used the code from this link https://playground.arduino.cc/Code/ShiftRegSN74HC165N/
but the output is HIGH from all inputs of shift register. No change.
Is something wrong with my shift register? I even replaced it with another one which I bought from the same online store.
What could be wrong in this?
Pin 9 (not 7) of the 165 goes to pin 2 of the Arduino with pin 7 of the 165 having no connection. Switching them will cause you to read incorrect data. That could be par
t of the problem.
You stated you connected 10 ohm resistors to the buttons. I hope that is a typo. The button resistors should be 10K ohm, not 10 ohm. Using such small resistors will cause too much current to flow through the 5V supply and likely damage the Arduino and possibly the buttons themselves.
Also, I could not quite tell from your connection description, but to be clear, the 165 inputs should be connected to the low side of the buttons, those connected to ground through the 10K ohm resistors. This means if you are using momentary closed push buttons, then when you press and hold the buttons, the inputs will register as HIGH, otherwise, they will register them as LOW.
Hope this helps!
Dear John,
I found that the problem was with the shift register. I bought another one from different E-store and it works great with the hall switches. Really appreciate your work on 74HC165.
Thank you for your time!
You are welcome. Glad to hear you got it working.
Hello John,
I am currently using two 74HC165s cascaded together on a PCB board for a school project. I have checked the PCB board’s connections using a multimeter and everything is sound. This leads me to believe that I have a software issue, as I am only able to read the first four bits with my buttons. For context I can only read the A, B, C, and D inputs of the first chip and no inputs of the second chip. Any insight you can offer to me on how to alter the code would be greatly appreciated. I am using the first four inputs of the first chip and trying to use all the inputs of the second chip, for a total of 12 inputs. I have not altered the code from what you have shown in the first example as I do not know how to change it for cascaded chips. I would also like to add that I was able to use this code for a single chip on a breadboard, but have not tried to cascade two on a breadboard.
Thanks for reading.
If you look at the InputShiftRegister sketch located in the GitHub repository, it provides more detail in the comments about how to update the sketch to use daisy chained shift registers. The most important part is getting the
isrReadRegister()
function working. I believe the following code should work for your two cascaded shift registers.After changing your
isrReadRegister()
function, I would suggest starting with the simple example that uses thereadInputsWithBinaryValues()
function to make sure the shifting operations are working properly before going back and making modifications to the first example.Let me know if you run into any difficulty and I will try to assist further.
Hello John,
The project works very well now with that new code to implement both ICs, thank you very much for your assistance.
You are very welcome. Glad to hear that worked for you.
Hi John,
Like you, I wanted a 3 pin solution. However, when I tried your code snippet above, I noticed that D0 was being skipped. After looking at the code for the standard Arduino shiftIn(), I notice that it sets the clock high. So I changed the following line from:
digitalWrite(ISRClockPin, HIGH);
to:
digitalWrite(ISRClockPin, LOW);
And now it works.
Thoughts?
I don’t understand why it works differently for you, but I’m glad you discovered a solution.