Electronics Software Development

Adding Digital I/O To Your CircuitPython Compatible Board: Part 1 – The 74HC595

More I/O Graphic
Written by John Woolsey

Skill Level: Intermediate

Table Of Contents

Introduction

Sometimes, a project needs more digital I/O than what is available on your development 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 CircuitPython compatible 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 (available soon) will describe how to add digital inputs using the 74HC165 8-bit parallel-in serial-out (PISO) shift register IC.

Part 3 – The MCP23017 (available soon) 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 CircuitPython ecosystem. If you are new to CircuitPython, or would just like to refresh your knowledge, please see our Getting Started With CircuitPython On Compatible Microcontroller Boards or Getting Started With CircuitPython On Raspberry Pi With Blinka tutorials before proceeding with this one.

This tutorial uses 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

The low-cost standard 7400 series 74HC595 IC is a good choice to incorporate into your design when you only need additional digital outputs for your project. 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 your CircuitPython compatible board into the shift register by using the dedicated SPI serial bus.

A single 74HC595 chip provides 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 development board. 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 a CircuitPython program. 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 three different approaches in this tutorial, all producing the same basic functionality, 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.

I am using Adafruit’s Feather M4 Express microcontroller board connected to a macOS based computer with the Mu Python editor for this tutorial. I also verified the CircuitPython program created in this tutorial works on a Raspberry Pi 3 Model B running the Raspberry Pi OS operating system using the Blinka library for CircuitPython support. If you are using a different CircuitPython compatible board, computer setup, or code editor, 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 CircuitPython compatible 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.

Schematic Diagram Of A 74HC595 Digital Outputs Circuit Connected To A CircuitPython Compatible Board
Schematic Diagram Of A 74HC595 Digital Outputs Circuit Connected To A CircuitPython Compatible Board

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

A 0.1 µF bypass capacitor was 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 3.3 V to disable hardware based clearing of the shift register.

The SPI_MOSI, SPI_SCK, and D5 pins shown on the schematic should be connected to the associated pins of your development board, e.g. the MO, SCK, and 5 pins of a general CircuitPython compatible microcontroller board or the GPIO10 (SPI0 MOSI), GPIO11 (SPI0 SCLK), and GPIO5 pins of a Raspberry Pi.

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

Completed 74HC595 Digital Outputs Circuit Connected To A Feather M4 Express Board
Completed 74HC595 Digital Outputs Circuit Connected To A Feather M4 Express Board

Once the circuit is built, connect your general CircuitPython compatible microcontroller board to your computer with the USB cable. For a Raspberry Pi, connect the power and boot it up.

Installing The Library

Before installing any libraries, make sure you are running the latest stable release of CircuitPython on your compatible microcontroller board or the Blinka library on a Raspberry Pi.

$ pip3 install --upgrade Adafruit-Blinka

We will take advantage of the existing Adafruit_CircuitPython_74HC595 driver library to communicate with the 74HC595 IC in our circuit. On a general CircuitPython compatible microcontroller board, copy the adafruit_74hc595.mpy file from the latest stable bundle to the lib directory of the board’s CIRCUITPY drive. On a Raspberry Pi, install the library with the following command.

$ pip3 install adafruit-circuitpython-74hc595

Changing Single Outputs Using Familiar Pin Setting Functionality

This first approach implements the standard CircuitPython pin setting mechanism by changing the value attribute of a single pin. It should be very familiar to most CircuitPython 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 Mu or your favorite code editor and create a CircuitPython program with the code shown below.

# Imports
import board
import digitalio
import adafruit_74hc595
from time import sleep

# Pin Mapping
osr_latch_pin = digitalio.DigitalInOut(board.D5)

# Global Constants
SHIFT_REGISTERS_NUM = 1

# Global Instances
osr = adafruit_74hc595.ShiftRegister74HC595(board.SPI(), osr_latch_pin, SHIFT_REGISTERS_NUM)

# Functions
def change_single_outputs():
    # Output pin definitions (pin references)
    led_0 = osr.get_pin(0)
    led_1 = osr.get_pin(1)
    led_2 = osr.get_pin(2)
    led_3 = osr.get_pin(3)
    led_4 = osr.get_pin(4)
    led_5 = osr.get_pin(5)
    led_6 = osr.get_pin(6)
    led_7 = osr.get_pin(7)

    # Set individual LEDs
    led_1.value = True   # turn on LED 1 only
    sleep(1)
    led_1.value = False  # turn off LED 1 only
    led_6.value = True   # turn on LED 6 only
    sleep(1)
    led_6.value = False  # turn off LED 6 only
    sleep(1)

    # Set multiple LEDs
    led_0.value = True   # turn on even numbered LEDs
    led_2.value = True
    led_4.value = True
    led_6.value = True
    sleep(1)
    led_0.value = False  # turn off even numbered LEDs
    led_2.value = False
    led_4.value = False
    led_6.value = False
    led_1.value = True   # turn on odd numbered LEDs
    led_3.value = True
    led_5.value = True
    led_7.value = True
    sleep(1)
    led_1.value = False  # turn off odd numbered LEDs
    led_3.value = False
    led_5.value = False
    led_7.value = False
    sleep(1)

# Main
while True:
    change_single_outputs()

The 74HC595 shift register only needs three pins to communicate with a CircuitPython compatible board. The SPI MOSI pin sends data that is shifted (clocked) into the register with the SPI SCK pin. Together, along with the ignored SPI MISO pin, these pins constitute the SPI serial bus.

The osr_latch_pin, specified as D5 on line 8, is used to latch the data, after the data has been shifted in, into the register.

The 74HC595 shift register library instance is defined on line 14 as osr (short for output shift register) and requires references to the board’s SPI port and the latch pin that will be utilized for shifting operations. The last argument, SHIFT_REGISTERS_NUM defined on line 11, is optional and specifies the number of 74HC595 shift registers that are daisy chained together.

Since we will be applying multiple approaches, I separated each approach into its own distinct function. Therefore, the change_single_outputs() function, this first approach, is the only function I am currently calling within the main section’s endless loop (while True:).

Within the change_single_outputs() function, we first define our named variables, led_0led_7, to make our calls easier to understand. They use the library’s get_pin() method to retrieve references to the 74HC595’s output pins. The variables 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 standard CircuitPython pin setting mechanism to turn on and off a couple of LEDs and then turn on and off either the even or odd numbered LEDs.

Now that our circuit is built and our software is written, let’s run and test our program.

On a general CircuitPython compatible microcontroller board, save the program as code.py to the top level of the board’s CIRCUITPY drive. It will begin running automatically.

On a Raspberry Pi, save the program as output_shift_register.py and then run the program.

$ python3 output_shift_register.py

You should see the 74HC595 shift register’s LED outputs being updated. First, LED 1 turns on, then only LED 6 is on, then only the even numbered LEDs are lit, then only the odd numbered LEDs are lit. This sequence will be continuously repeated.

On a Raspberry Pi, press CTRL-C to exit the program when you are done.

Changing All Outputs Using Binary Values

This next approach is the simplest of all the approaches. It 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 your program, just before the main section,

def change_outputs_with_binary_values():
    outputs = osr.gpio  # retrieve current shift register output values

    # Set individual LEDs
    outputs[0] = 0b00000010  # turn on LED 1 only
    osr.gpio = outputs       # set new shift register output values
    sleep(1)
    outputs[0] = 0b01000000  # turn on LED 6 only
    osr.gpio = outputs
    sleep(1)
    outputs[0] = 0b00000000  # turn off all LEDs
    osr.gpio = outputs
    sleep(1)

    # Set multiple LEDs
    outputs[0] = 0b01010101  # turn on only even numbered LEDs
    osr.gpio = outputs
    sleep(1)
    outputs[0] = 0b10101010  # turn on only odd numbered LEDs
    osr.gpio = outputs
    sleep(1)
    outputs[0] = 0b00000000  # turn off all LEDs
    osr.gpio = outputs
    sleep(1)

and then change the contents of the endless loop within the main section to the following so that you’re calling the newly added function instead of the first one.

# change_single_outputs()
change_outputs_with_binary_values()

The library’s current shift register output values (osr.gpio) are retrieved and assigned to the outputs variable on line 2. This ensures we are obtaining not only the correct data type for holding the output values, but also the full array of bytes available in the case we are using multiple daisy chained 74HC595 ICs. It also enables us to view the current outputs before they are changed, if we so choose.

As shown on lines 5 and 6, to write values to the shift register, the outputs variable is updated and then assigned back to the library’s register’s values (osr.gpio) that initiates the shift operation. If you are using daisy chained ICs, make sure to set the appropriate index (0 for the first 74HC595, 1 for the second 74HC595, and so on) of the outputs variable when updating values.

Save and run your program 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 variables 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 True (1). All other outputs default to False (0).

Add the following function to your program, just before the main section,

def change_outputs_with_defined_names():
    # Output pin definitions (bit positions)
    led_0 = 0b00000001
    led_1 = 0b00000010
    led_2 = 0b00000100
    led_3 = 0b00001000
    led_4 = 0b00010000
    led_5 = 0b00100000
    led_6 = 0b01000000
    led_7 = 0b10000000

    outputs = osr.gpio  # retrieve current shift register output values

    # Set individual LEDs
    outputs[0] = led_1  # turn on LED 1 only
    osr.gpio = outputs  # set new shift register output values
    sleep(1)
    outputs[0] = led_6  # turn on LED 6 only
    osr.gpio = outputs
    sleep(1)
    outputs[0] = 0      # turn off all LEDs
    osr.gpio = outputs
    sleep(1)

    # Set multiple LEDs
    outputs[0] = led_0 | led_2 | led_4 | led_6  # turn on only even numbered LEDs
    osr.gpio = outputs
    sleep(1)
    outputs[0] = led_1 | led_3 | led_5 | led_7  # turn on only odd numbered LEDs
    osr.gpio = outputs
    sleep(1)
    outputs[0] = 0                              # turn off all LEDs
    osr.gpio = outputs
    sleep(1)

and then change the contents of the endless loop to the following.

# change_single_outputs()
# change_outputs_with_binary_values()
change_outputs_with_defined_names()

Save and run your program and you should see the same LED sequences we saw previously.

An LED Cycling Example

This is just an example of a fun animation (Knight Rider style) that shows how to set outputs a little differently than the previous approaches.

Add the following function to your program, just before the main section,

def cycle_leds():
    leds = [osr.get_pin(n) for n in range(8 * osr.number_of_shift_registers)]
    for position, led in enumerate(leds):
        if position == len(leds) - 1: break  # skip the last LED
        led.value = True
        sleep(0.1)
        led.value = False
    for position, led in enumerate(reversed(leds)):
        if position == len(leds) - 1: break  # skip the first LED
        led.value = True
        sleep(0.1)
        led.value = False

and then change the contents of the endless loop to the following.

# change_single_outputs()
# change_outputs_with_binary_values()
# change_outputs_with_defined_names()
cycle_leds()

All LED output pins are grouped together into a list (leds), on line 2, and then for loops are used, beginning on lines 3 and 8, to cycle through the LEDs from one end to the other with a small delay between shifts. The last LED is ignored in each loop in order to provide the same timing across all LEDs.

Again, save and run your program. The LED’s should be turning on and off in sequence, back and forth, across all LEDs.

Additional Resources

Summary

In this tutorial, we learned how to add digital outputs to your CircuitPython compatible board using the 74HC595 serial-in parallel-out (SIPO) shift register. I presented multiple approaches for how to represent the outputs in your CircuitPython 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 CircuitPython’s familiar pin setting 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 Sphinx compatible in case you want to generate the code documentation.

The next part of this three-part tutorial, Part 2 – The 74HC165 (available soon), 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.