FriendlyWire.com


beginner-friendly electronics tutorials and projects


Discover the joy of understanding electronics!

More microcontroller outputs with shift registers

September 29, 2019 tutorial

What do you do when your circuit has a lot of LEDs that need to be individually turned on and off? Do you always need a bigger controller with more I/O ports? The answer is no! Using a so-called shift register you can extend the number of outputs almost arbitrarily. In this tutorial we will learn how :)

In the binary clock project we made use of the PIC16F883 primarily because it had enough pins to accommodate the 17 LEDs that were required to display the hours, minutes, and seconds in binary:

Since this controller costs around the same as other controllers with a smaller footprint, there was no real issue. But imagine that you want to build something like this here:

This is not just a fascinating movie prop, but also a considerably more complicated circuit. If you count the segments and LEDs (don't forget that the month displays are alphanumeric), you arrive at more than 300 individually addressable LEDs. While it was still possible for us to just use a bigger PIC microcontroller for 17 LEDs in case of the binary clock, if you want to build the time circuits there is just no hope to buy instead a microcontroller with more than 300 pins.

The important thing to remember is the following: the individual LEDs, in this application, do not need to be switched rapidly. This is an important detail, as we will see below. In these cases it makes sense to think about enhancing the number of digital outputs using a shift register.

Okay, but what is a shift register?

Shift registers are conveyor belts for bits!

Shift registers are small integrated circuits that can turn a serial signal into a parallel signal. The idea is simple:

Okay, maybe it's not super simple, but we can understand it :) A shift register has three inputs: DATA, CLOCK and STROBE. It also has outputs, in this case eight, because it is an 8-bit register. There is also another output called CARRY OUT that we will ignore for now (it will be used to cascade multiple shift registers, as we will see later).

So how does it work?

  • We can think of the shift register as a conveyor belt for individual bits. Whenever the CLOCK like goes to 1 and back to 0 (we call that a “clock pulse”), the conveyor belt advances one step towards the right.
  • Imagine the DATA line to be a little bit like Morse code. It is either 1 or 0, and that bit gets placed on the conveyor belt (the box on the left).
  • Okay, so this goes on and on, and we can decide whatever we want to have on the conveyor belt. When the STROBE line goes to 1 and back to 0 (we call that a “strobe pulse”), the momentary state of the conveyor belt gets mirrored to the outputs.
  • The last bit gets kicked off of the conveyor belt during a clock pulse. If we want to pass that bit along to a second conveyor belt (that is, when we want to cascade multiple shift registers) then we can use the CARRY OUT pin to do so. More below!

I hope the main idea of shift registers has become clear! Let me answer two questions that I had when I first learned about them:

  • What is the disadvantage of the shift register?
    I hope the disadvantage became clear: say, bit number 8 of the shift register has value 0, and we want to change it to a 1, and keep the rest of the bits (bits 1-7) unchanged. Then we need to put a 1 into the shift register, and clock it forward until it reaches position 8. But we also need to rewrite bits 1-7 in the process. The bottom line: if we want to change even a single bit, all bits have to be rewritten. That can severely affect the update speed of each of these bits, especially if we cascade multiple shift registers. This is the main reason why shift registers are useful for slowly changing data.
  • Why do we need the strobe pulse?
    If we did not have the strobe pulse, we would see the bits traveling through the conveyor belt on the outputs. By having the STROBE pin we can decide when we make the current content of the conveyor belt visible.

Okay, enough theory, let's get our hands dirty with an actual real-life example!

The CD4094 shift register

The CD4094 is a very common shift register. Here you can see its pinout (you can download the datasheet in the resources box):

Yeah, that doesn't look very nice, but it's an old datasheet. The CD4094 has been around for a long time! What do the pins mean?

  • VDD is the positive power supply, in our case it will be +5V, and VSS denotes the ground potential GND.
  • OUTPUT ENABLE is an additional pin that is not always needed. If we tie it permanently to +5V the shift register works normally. If we set it to GND instead, all the outputs will remain 0, regardless of the current bit that is sent to them. This function can be used as a quick way to set all outputs to zero without having to write 0's explicitly everywhere.
  • DATA, CLOCK and STROBE function exactly as we already learned above :)
  • Q1 to Q8 denote the outputs. In our conveyor belt language, Q1 is the bit on the left, and Q8 is the bit furthest to the right.
  • Q'S works as what we called above the CARRY OUT pin. The pin QS is the inverted value of Q'S.

And that's it! How would we use it in a simple example circuit? Let's consider again the PIC16F627A as our microcontroller of choice (datasheet in the resources box) and take a look at the following schematic:

This is a pretty straightforward setup: the CD4094's inputs are all connected to outputs of the PIC16F627A so that we can control the shift register via software. The outputs of the CD4094 are left unconnected, but in a real application we would connect some LEDs or similar devices, of course.

Sending data to the shift register

Now we need to talk about how we can send data into the shift register using the software that runs on the PIC16F627A. Because we want to send 8 bits it is easier to talk instead about sending one byte: one byte consists of eight bits.

The number 0 corresponds to all outputs set to 0, and the number 255 corresponds to all outputs set to 1. The number 3, for example, means that outputs Q1 and Q2 are set to 1, and the rest is 0. The number 16 would mean that only pin Q5 is on. You see: this is just the binary system!

You can find the complete source code in the appendix. Here we will just focus on the main details of sending a byte to the shift register.

First it is useful to introduce symbols for the PIC16F627A's pins so that we can use them more easily:

// define locations of CD4094 controls
#define STROBE RB0
#define DATA RB1
#define CLOCK RB2
#define ENABLE RB3

Then, we declare a special subroutine that will send the data to the PIC. This is optional but I find this style of programming cleaner. These details are not so important (since this is not a programming tutorial), but if any of this confuses you, get in touch and we will work it out!

// declare our function
void sendValue (unsigned char value);

The idea is simple. Whenever we want to send, say, the number 128 to the CD4094 we will just have to write sendValue(128); and that's it. Of course we have to specify what that function does exactly.

The rest of the main program is very simple. It just initializes the four connections to the CD4094 as outputs (by setting the TRISTATE registers to 0), defines a dummy variable called myValue that contains the number we want to send to the CD4094, and we permanently set the Output Enable pin to 1 so that the CD4094's outputs actually show something. Then we start the main loop that sends that value over and over and over again. Clearly this is just a sample program with no real use:

// main function
void main (void) {
    
    // set outputs
    TRISB0 = 0;
    TRISB1 = 0;
    TRISB2 = 0;
    TRISB3 = 0;
	
    // enable the outputs permanently
    ENABLE = 1;
    
    // what value do we want to send?
    unsigned char myValue = 27;
    
    // main loop
    while (1) {
        sendValue(myValue);
    }
    
    return;
    
}

Okay, now we can finally address the most interesting part: the sendValue function! Here it is in all its glory:

// auxiliary function that sends a value
// to the CD4094 shift register
void sendValue (unsigned char value) {

    // auxiliary index variable
	unsigned char idx;
    
    // loop over all eight bits
	for (idx = 7; idx >= 0; idx--) {
		if (value & (1 << idx)) {
			DATA = 1;
		} else {
		 	DATA = 0;
		}
		NOP();
		CLOCK = 1;
		NOP();
		CLOCK = 0;
		NOP();
	}

    // make the data visible
	STROBE = 1;
    NOP();
    STROBE = 0;
	NOP();

}

I know, it looks like a lot of dark magic, but we can figure it out, step by step :) Let's start at the beginning.

  • We are sending eight bits of binary data which are combined in one byte. The lowest bit of a byte is called the least significant bit, or LSB for short, and the largest one is called most significant bit, or MSB. For an 8-bit number, the LSB is 1, and the MSB is 128. The way we have connected everything, Q1 is the LSB and Q8 is the MSB.
  • In line 58 we define a variable called idx which stands for “index.” This is just a name. You could also call this variable Jens or Hilary or anything you like. The only thing that is important is its purpose: it goes over all eight bits of the variable value.
  • As we explained above in our conveyor belt picture of shift registers, we send the MSB first, and the LSB the last. This is why in lines 61–72 we loop over all bits backwards, starting with the MSB and ending at the LSB.
  • If the bit at position idx is set (line 62) we set the DATA line to 1 (line 63), otherwise we set it to 0 (line 65).
  • Then we push that bit onto the conveyor belt by programming a clock pulse in lines 67–71. What does the NOP(); mean? NOP stands for “no operation” and forces the PIC16F627A to take a break for one instruction cycle. We include this line so that the clock signal is on level 1 for a bit longer before we set it back to 0. Without the NOP();-commands the pulse might be too short for the CD4094 to notice it.
  • We repeat these last two steps for each bit in our variable value.
  • Now we are almost done! At the end, all we need to to is apply a strobe pulse so that the eight bits we have just sent down the conveyor belt are mirrored into the outputs! We again make use of the NOP();-command to make sure that the CD4094 notices our pulse. And that's it!

I hope this makes sense! If it doesn't please get in touch on social media and we will work it out :) You can download the source code and the readily compiled .hex file in the resources box.

Cascading CD4094 shift registers: unlimited power?

As with pancakes, sometimes just one shift register is simply not enough. But kidding aside, with the above setup we can drive eight LEDs, all connected to the pins Q1–Q8. But what if we need to drive more? As we already said, then the CARRY OUT pin becomes important.

The CARRY OUT pin, as we understood in the conveyor belt explantion, simply spits our the last bit that is about to fall off the end of the belt. If we feed it instead to the next conveyor belt, it will simple be passed along, and then we have successfully cascaded two shift registers. This is how it looks like:

Okay, that may look a bit confusing, but it is actually very simple :) As we said just a few moments ago, the CARRY OUT pin (which is called Q'S in case of the CD4094) is connected to the DATA input of the next shift register. That's the main idea.

All the other pins, namely CLOCK, STROBE, and OUTPUT ENABLE are simply connected in parallel. This makes sense, right? We still need to drive the conveyor belt in the cascaded shift registers (that explains the CLOCK connection) and we still want to make sure that all data is updated and made visible at once (STROBE).

Last, it is of course neat to have all OUTPUT ENABLE pins connected as well, so that we can turn ALL outputs off and on at will. However, this is not always required, so this step is strictly optional. Just remember that if you want to use the CD4094 as a normal shift register with all outputs enabled you can just tie the OUTPUT ENABLE pin to +5V and leave it there.

Here is a more stylized version of the same schematic where we omitted the power supply symbols for the shift registers (these weird-looking “IC2P” abominations, see also the article on reading schematics here), and we are also hiding the PIC16F627A:

You get the idea! And yes, now it really looks like a conveyor belt, doesn't it?

Final thoughts

And that's it! Now you know what shift registers are and why they can be useful in microcontroller circuits! They require a bit of additional work on the software side, but it can be worth it!

Of course you might wonder: can't we, theoretically, extend the number of outputs to infinity? Yes, in principle we can do that! But there is of course a problem:

The more shift registers you cascade the longer it takes to update the outputs (since all the bits have to be re-written when you want to change even just one bit). Sometimes it might be more useful to only cascade, say, four registers, and then cascade another four, with separate DATA and STROBE lines. This way you use a few more precious IO ports, true, but the update speed will be faster.

I hope that it became clear why shift registers can be a great tool! I know that this tutorial did not present a concrete application where they are indispensable, but I am sure we will encounter a project somewhere down the line where we can make really good use of them! :)

Thanks for reading!

Appendix: the complete source code

/*
 * File:   main.c
 * Author: boos
 *
 * Created on September 29, 2019, 12:03 AM
 */

// CONFIG
#pragma config FOSC = INTOSCIO  // Oscillator Selection bits (INTOSC oscillator: I/O function on RA6/OSC2/CLKOUT pin, I/O function on RA7/OSC1/CLKIN)
#pragma config WDTE = OFF       // Watchdog Timer Enable bit (WDT disabled)
#pragma config PWRTE = OFF      // Power-up Timer Enable bit (PWRT disabled)
#pragma config MCLRE = ON       // RA5/MCLR/VPP Pin Function Select bit (RA5/MCLR/VPP pin function is MCLR)
#pragma config BOREN = ON       // Brown-out Detect Enable bit (BOD enabled)
#pragma config LVP = OFF        // Low-Voltage Programming Enable bit (RB4/PGM pin has digital I/O function, HV on MCLR must be used for programming)
#pragma config CPD = OFF        // Data EE Memory Code Protection bit (Data memory code protection off)
#pragma config CP = OFF         // Flash Program Memory Code Protection bit (Code protection off)

#include <xc.h>

// define locations of CD4094 controls
#define STROBE RB0
#define DATA RB1
#define CLOCK RB2
#define ENABLE RB3

// declare our function
void sendValue (unsigned char value);

// main function
void main (void) {
    
    // set outputs
    TRISB0 = 0;
    TRISB1 = 0;
    TRISB2 = 0;
    TRISB3 = 0;
    
    // enable the outputs permanently
    ENABLE = 1;
    
    // what value do we want to send?
    unsigned char myValue = 27;
    
    // main loop
    while (1) {
        sendValue(myValue);
    }
    
    return;
    
}

// auxiliary function that sends a value
// to the CD4094 shift register
void sendValue (unsigned char value) {

    // auxiliary index variable
	unsigned char idx;
    
    // loop over all eight bits
	for (idx = 7; idx >= 0; idx--) {
		if (value & (1 << idx)) {
			DATA = 1;
		} else {
		 	DATA = 0;
		}
		NOP();
		CLOCK = 1;
		NOP();
		CLOCK = 0;
		NOP();
	}

    // make the data visible
	STROBE = 1;
    NOP();
    STROBE = 0;
	NOP();

}

About FriendlyWire

Beginner-friendly electronics tutorials and projects. Discover the joy of electronics! Keep reading.

Let's build a community

How did you get interested in electronics? What do you want to learn? Connect and share your story!

Tag Cloud

  • shift registers
  • I/O ports
  • data
  • clock
  • strobe
  • cascading
  • CMOS
  • CD4094
  • tutorial