Daisy chaining and bus sharing with shift registers

 Posted by:   Posted on:   Updated on:  2023-04-02T12:19:01Z

How to use input and output shift registers to get as many digital input and output pins as you need with only 5 control signals from MCU

A shift register is a digital circuit that is used to store and manipulate data in a sequential manner. It is composed of a series of flip-flops, which can be viewed as basic memory units able to store binary values. The outputs of each flip-flop are connected to the inputs of the next flip-flop in the sequence, such that the data is shifted from one flip-flop to the next with each clock pulse.

In this post I will use the 74HC595 serial-in-parallel-out shift register and its counterpart, 74HC165 parallel-in-serial-out shift register. Both are commonly employed when a microcontroller with limited available I/O pins has to control a large number of digital outputs or read a similar number of inputs (for example in home automation). What I want to show you in this post is how to daisy chain these ICs and how to make them share some control lines in order to keep the serial interface data lines to a minimum.

Shift registers circuit on breadboard
Shift registers circuit on breadboard

Shift registers can be used for various purposes, including data storage, data transfer, and data manipulation. They are commonly used in applications such as serial-to-parallel data conversion, parallel-to-serial data conversion, and data manipulation in digital signal processing.

There are several types of shift registers, including serial-in-serial-out (SISO), serial-in-parallel-out (SIPO), parallel-in-serial-out (PISO), and parallel-in-parallel-out (PIPO) shift registers. The choice of the type of shift register to use depends on the specific requirements of the application.

Daisy chaining is a method of connecting multiple devices or components in a series or chain-like configuration, so that the output of one device is connected to the input of the next. This is typically used to simplify the wiring process and reduce the number of interconnections required. However, the overall performance of the system may be limited by the total number of components, and any failure of one shift register in the chain can affect the performance of all the other.

The basic unit of a shift register is the D-type flip-flop which in its basic form is a temporary 1-bit storage device. It is called a "flip-flop" because the state of the output changes based on the state of the inputs. A D flip-flop has two inputs: a "data" input and a "clock" input. The data input is used to set or reset the output, depending on the state of the clock. When the clock input goes high, the flip-flop stores the current state of data and represents it on the output. When the clock state is low the output state does not change.

Output only

I will not go into details about the internal arrangement of flip-flops in a shift register IC, however in this post we'll see some examples of daisy chaining the common 74HC595 and 74HC165 shift registers. To complicate things a bit, I will use a 4 output registers and 3 input registers which will share the same clock (see below). What I'm trying to achieve here is use as few pins as possible to control a large number of parallel I/O lines. These parallel I/Os can be used to drive LEDs, activate relays (with driver) and read pushbuttons. I built the following circuit on breadboard (I will split the schematic in two parts). Note there are some wiring differences on the breadboard because I wired them wrong when I made the photo.

Output shift registers daisy chaining
Output shift registers daisy chaining

The output section requires a minimum of three control lines, although it is recommended to take control of the fourth, the OE (output enable). This is because on startup, outputs may have random states which is not desirable. If you don't have a spare microcontroller pin for OE, you can use a simple RC circuit which creates a delayed high signal and enables outputs after the software running on MCU has initialized them.

Internally 74HC595 has two sets of flip-flops, the first one receives serial data and forwards it to the second one after all eight bits have been written. There is even a pin (SRCLR) which allows you to reset first set of flip-flops.

The serial control is SPI-like and it is rather easy (for convenience I used SPI signal naming). At every clock pulse (more specifically, on the rising edge), a bit (the current state of MOSI) is pushed into IC 0 (the one which receives MOSI), causing a shift of the already existing bits. In my schematic, the serial bits go up, and the 32th pulse of the clock will actually set the state of green LED 7. After serial data transfer is finished, LATCH pin is pulsed high and data goes from storage register to output pins. Here is a logic analyzer capture of the following bytes sent to first IC: 0xFF 0x00 0x0F 0x00.

Bit shifting with four 74HC595 ICs
Bit shifting with four 74HC595 ICs

I used these simple bytes to make it clear how they are shifted and you can actually see that my first sent 0xFF went last at TP3:2, just before LATCH pulse, causing all LEDs connected to IC 3 to turn on. In other words, 74HC595 will output the last 8 bits that were pushed into it before LATCH goes high. This gives an insight of what it would happen if I send 5 ore more bytes to only 4 ICs. Only the last 4 bytes are taken into account, while the rest lost, unless there are some more shift registers to catch them. However, if you sent three bytes, the fourth IC will not get any data and will set its output to 0 (because idle state of MOSI is low). Let me show you the Arduino code I used for this, before getting to the next part.

#define HC595_LATCH 10
#define SCLK 13
#define MOSI 11
#define MISO 12

uint8_t out_IC3  = 0xff, out_IC2 = 0x00, out_IC1 = 0x0f, out_IC0 = 0x00;

void setup() {
  // Pin init
  pinMode(HC595_LATCH, OUTPUT);
  pinMode(SCLK, OUTPUT);
  pinMode(MOSI, OUTPUT);

  digitalWrite(HC595_LATCH, LOW);
  
}

void loop() {
  for (int8_t i = 7; i >= 0; i--) {
    digitalWrite(MOSI, bitRead(out_IC3 , i));
    digitalWrite(SCLK, HIGH);
    digitalWrite(SCLK, LOW);
  }

  for (int8_t i = 7; i >= 0; i--) {
    digitalWrite(MOSI, bitRead(out_IC2, i));
    digitalWrite(SCLK, HIGH);
    digitalWrite(SCLK, LOW);
  }

  for (int8_t i = 7; i >= 0; i--) {
    digitalWrite(MOSI, bitRead(out_IC1, i));
    digitalWrite(SCLK, HIGH);
    digitalWrite(SCLK, LOW);
  }

  for (int8_t i = 7; i >= 0; i--) {
    digitalWrite(MOSI, bitRead(out_IC0, i));
    digitalWrite(SCLK, HIGH);
    digitalWrite(SCLK, LOW);
  }

  digitalWrite(HC595_LATCH, HIGH);
  digitalWrite(HC595_LATCH, LOW);
  
  delay(1000);
}

I could have used Arduino shiftOut() function to generate clock and data pulses in a single line of code. The reason I wrote my own code is because I will also read input registers within the same loops, using the same clock pulses.

Mixed input-output

Sometimes we need digital inputs too. This is where 74HC165 comes handy. It is similar to 74HC595, however it features a parallel 8-bit input port. It can also be daisy chained when more inputs are needed. Let's see the schematic of the input section. Note that the input port may be connected to switches (for example); on the breadboard I wired pins to either ground or supply voltage, just to get a different input for each IC. Since 74HC165 is CMOS based, all inputs must be pulled high or low, if nothing is connected.

Input shift registers daisy chaining
Input shift registers daisy chaining

Assuming the first IC is the one which sends serial data to MCU, this time data is passed from last IC. The first port you read is from IC 0. If you continue to send clock pulses, data from next ICs is shifted towards IC 0 and read by MCU.

In my example, starting from IC 0 the input data should be: 0x0D 0x09 0x06. Again, the serial protocol is similar. Before reading data, we must tell these ICs to load current port state in their internal flip-flops. This is done by pulsing low LOAD. After this, data will be read at every clock rising edge.

Bit shifting with the added 74HC165
Bit shifting with the added 74HC165

You can see in the above analyzer record how data is shifted from IC 2 towards IC 0 to create the complete MOSI signal. What happens if you continue to generate clock signal? Well, as long as serial input pin (DS) of last IC is connected to ground, you will read 0x00. Don't forget to ground this pin.

In my example, LOAD pin is pulsed after I wrote the first output register. You can do this anytime you like, as long as you provide enough clock pulses to read all ICs in chain. Here is the code, with the lines I added to read data.

  for (int8_t i = 7; i >= 0; i--) {
    digitalWrite(MOSI, bitRead(dataout3, i));
    digitalWrite(SCLK, HIGH);
    digitalWrite(SCLK, LOW);
  } 

  digitalWrite(HC165_LOAD, LOW);
  digitalWrite(HC165_LOAD, HIGH);

  for (int8_t i = 7; i >= 0; i--) {
    bitWrite(datain0, i, digitalRead(MISO));
    digitalWrite(MOSI, bitRead(dataout2, i));
    digitalWrite(SCLK, HIGH);
    digitalWrite(SCLK, LOW);
  }

  for (int8_t i = 7; i >= 0; i--) {
    bitWrite(datain1, i, digitalRead(MISO));
    digitalWrite(MOSI, bitRead(dataout1, i));
    digitalWrite(SCLK, HIGH);
    digitalWrite(SCLK, LOW);
  }

  for (int8_t i = 7; i >= 0; i--) {
    bitWrite(datain2, i, digitalRead(MISO));
    digitalWrite(MOSI, bitRead(dataout0, i));
    digitalWrite(SCLK, HIGH);
    digitalWrite(SCLK, LOW);
  }

  digitalWrite(HC595_LATCH, HIGH);
  digitalWrite(HC595_LATCH, LOW);

This is why I haven't used shiftOut() and shiftIn() provided by Arduino. Both of these functions would generate their own clock pulse and I'm using a common clock to do simultaneous read and write.

Overview

What you need to remember when daisy chaining these shift registers is that:

  1. In an output configuration, the first byte you send is pushed to the last IC in chain.
  2. In an input configuration, the first byte you read is from the first IC in chain.
Data shifting between chained registers
Data shifting between chained registers

Perhaps the choice of less input than output registers wasn't the best. We've already seen that an attempt to read extra input registers (which do not exist in the chain) will return a byte made by the state of serial input pin of last register in chain. Assuming this is grounded, you will read 0x00 and that will not complicate things.

But what happens when there are less output than input registers in chain? Well, as long as you generate clock pulses and you need to maintain all output ports unchanged, you must send some data to the output register(s). Let me show you how.

Example with more input registers
Example with more input registers

Since you know, the first sent byte goes to last register (which does not exist in this chain), you will send an amount of bytes to be discarded that is equal to the difference between input and output ports. In the above example, I have three output registers and five input registers. I will start the serial output with two 0x00 bytes which will be pushed to the fourth and fifth IC (which do not exist). In fact I can send whatever I want, it will get discarded. In fact these two bytes don't really get discarded - they are pulsed on QH pin of last IC in chain. However, since there is no other shift register to "catch" them, we can say they are discarded.

I don't think I mentioned the signals which are required to drive such a circuit:

  • CLOCK - common to both input and output registers;
  • MOSI - master output serial data to output shift register;
  • MISO - master input data from input shift register;
  • LATCH - command sent by master to output register to signal data has been transferred and can be outputted on the parallel port;
  • LOAD - command sent by master to input register before reading data - triggers the storage of current port state in internal flip-flops;
  • OE - output enable is an optional pin to keep output shift register disabled before the master has initialized and sent correct output data.

The minimum required signals are the first 5. For OE, if no other pin is available, you can use a RC circuit which will charge the capacitor and drive a Schmitt trigger after enough time has passed and master MCU is already running and sending correct data to output register.

Finally, this kind of I/O expansion is less used today since better solutions are available. Shift registers advantage is the theoretical unlimited number of circuits you can add to chain (despite a decrease in speed), however if one circuit in chain fails, all the other following will no longer receive or be able to send data. Another disadvantage is the need to poll input registers for new data.

There are now on the market new ICs with improved functionality such as: configurable port mode (input or output) and I2C interface. Despite the number of configurable addresses which will set a limit on the total ICs you can connect on the same bus, in case one fails, the others do not depend on it. In the end, the choice of one or the other depends on application, availability and costs.

No comments :

Post a Comment

Please read the comments policy before publishing your comment.