Microcontroller GPIO Pins
When using microcontrollers, like the ATmega line of chips, or boards based on them, like the Arduino series, General Purpose Input/Output (GPIO) pins are often at a premium. The Arduino Uno R3 (based on the ATmega328P), for example, has a maximum of fourteen GPIO pins, and that’s if you’re not using some of the pins’ special functions.
It’s very easy to run out of these GPIO pins if multiple devices are connected directly to them. Each LED uses one pin, configured as an output. Each button uses one pin, configured as an input. A seven-segment display uses eight outputs (there’s a decimal point that’s not counted in the name for some reason). In its most minimal configuration, an LCD character display module uses six pins, but can be configured to use as many as eleven pins. All this quickly adds up.
Shift Registers
A common solution to the problem of too few GPIO pins is to use one or more shift registers. Simply put, a shift register is a chip that converts a single signal changing over time (i.e. serial) to multiple signals at the same time (i.e. parallel), or vice-versa. This solution is so common that the Arduino core library has functions specifically to support their use, and there are lots of Arduino/shift-register tutorials to be found on the internet. (I guess I’m adding to that list.)
There are many different types of shift register available, but two of the most popular are the 74HC595 (to add output pins) and the 74HC165 (to add input pins). These chips are easy to use, inexpensive, and readily available. You can get them from electronics suppliers like Mouser and DigiKey, and they’re even available from Amazon.
The basic principle behind shift registers, and the reason for their name, is that each chip holds a number of bits (typically eight) in a small memory circuit called a register, and these bits shift from one to the next when a clock pulse is applied. Bit 0 shifts into bit 1, bit 1 shifts into bit 2, and so on. The new value for bit 0 comes from a serial input pin at the time of the clock pulse. The last bit falls out of the shift register, and is made available on one of the chip’s output pins, called serial output. Typically, the shift happens on the rising edge of the clock pulse (i.e. from low to high).
The fact that each shift register has a serial input pin that feeds into bit 0 and a serial output pin that holds the value of the last bit as it’s shifted out means that these chips may be cascaded, or chained together, by connecting the serial output of one chip to the serial input of another, and by connecting their clocks together. Two 8-bit shift registers cascaded together, for example, behave as if they were a single 16-bit shift register. The number of chips that can be cascaded into a single effective register is typically limited only by things like available power and the capacitance of the connecting wires, but the chain would need to be pretty long for those things to start to matter.
There are a few disadvantages to using shift registers for GPIO expansion. First, it’s slower. Instead of reading or writing several pins at once, the microcontroller has to process them one at a time. Second, the pins can’t be read or written independently. To read/write bit 4, for example, bits 0-3 must first be read/written. When the shift register is being used to add output pins, this means the software needs to keep track of what bits have been written. Third, most shift registers are unidirectional. That is, each type of chip provides either inputs or outputs, but not both. (The 74HC299 is one exception to this.) In practice, however, these disadvantages are often easily overcome and are more than made up for by the added inputs and/or outputs.
74HC595
The 74HC595 is an 8-bit Serial-In / Parallel-Out (SIPO) shift register. It shifts in one bit from its serial input pin (pin 14) on each rising edge of its serial clock pin (pin 11), and has eight parallel output pins (pins 15 and 1-7). However, the bits in the shift register are not immediately made available on the output pins.
The 74HC595 internally has two registers: the shift register and the storage register (sometimes called the latch register). The shift register behaves as described above. However, the parallel output pins are not connected directly to the shift register, but rather to the storage register. The contents of the shift register are transferred to the storage register (and therefore the output pins) on the rising edge of a second clock pin (pin 12), called the storage clock pin (or sometimes the latch pin).
This dual-register configuration may seem like a needless complication. Worse, instead of just two GPIO pins (serial input and serial clock), it uses those plus a third (storage clock). However, there is a good reason for this design. Some devices connected to the 75HC595’s output pins may not react well to the data shifting along one bit at a time. The two-register approach allows data bits to be shifted in while keeping the output pins unchanged, then all made available effectively simultaneously once the complete set of bits is ready.
If a device can tolerate seeing the data shift one bit at a time, the serial clock and storage clock pins may be connected together. This causes the 74HC595 to behave (more or less) as if the outputs were connected directly to the internal shift register, and it only requires a total of two GPIO pins instead of three.
To interface a 74HC595 with an Arduino (or similar microcontroller), connect the serial input, serial clock, and storage clock pins of the 74HC595 to GPIO pins on the microcontroller, with all three configured as outputs. Then, in the microcontroller code (i.e. Arduino sketch), write each bit to the shift register as follows.
- Set the serial input pin to the value of the data bit.
- Bring the serial clock pin low, then high, to create a rising edge and shift the bit.
- Repeat steps 1 and 2 until all bits have been shifted.
- Once all bits have been shifted, bring the storage clock low then high to transfer the bits to the parallel output pins.
Step 4 can be omitted if the serial clock and storage clock are tied together. The Arduino core library function shiftOut() performs steps 1 and 2 in a loop to write eight bits.
The 74HC595 has a few other useful features, including tri-state outputs and a reset pin. The datasheet has more information about these if you’re interested, but they’re not particularly relevant to this post, so that’s all I’ll say about them here.
74HC165
The 74HC165 is an 8-bit Parallel-In / Serial-Out (PISO) shift register. Data bits are loaded into the chip all at once from eight parallel input pins (pins 11-14 and 3-6) while the parallel load pin (pin 1) is set low. Once the bits are loaded, they can be shifted out one at a time to the serial output pin (pin 9) by pulsing the serial clock pin (pin 2). Bits are shifted on the rising edge of the clock pulse, and the serial input pin (pin 10) provides a new value for bit 0 on each pulse.
The process to interface the 74HC165 with a microcontroller is very similar to the process for the 74HC595. Connect the 74HC165’s serial clock and parallel load pins to output pins on the microcontroller, and connect the 74HC165’s serial output pin to a microcontroller input pin. Then, in software, perform the following:
- Bring the parallel load pin low, then high to latch the inputs into the shift register.
- Bring the serial clock pin low.
- Bring the serial clock pin high to create a rising edge and shift out a bit.
- Read and process the data bit from the serial output pin.
- Bring the serial clock pin back low to prepare for the next bit.
- Repeat steps 3 through 5 for each bit.
The Arduino core library function shiftIn() performs steps 3 through 5 in a loop to read eight bits.
Like the 74HC595, the 74HC165 has additional features, such as clock inhibit and complementary serial output, that I’m not going to discuss here. For more information, see the datasheet.
Serial Peripheral Interface (SPI)
The Serial Peripheral Interface (SPI) protocol is another way to reduce the number of microntroller GPIO pins needed for a project. There are many devices that support SPI natively, and there are quite a few available on Amazon, for instance. The protocol is relatively simple, and it’s straightforward to implement in software. Ben Eater has an excellent video in which he interfaces an SPI device with a 65C02 microprocessor using assembly code. However, most microcontrollers, including Arduinos, have hardware support for communicating via SPI. This enables much faster speeds than would be possible with a software implementation.
SPI is, as its name implies, a serial protocol. This means that one bit is transferred at a time. Actually, that’s not quite true, since SPI supports full-duplex bidirectional communication. This means that one bit is sent and one bit is received at the same time. It’s also a sychronous protocol, which means that the timing is controlled by a dedicated clock signal. Finally, it’s an asymmetric protocol. This means that the communication is entirely controlled by one device, called the master (sometimes controller). This is usually the microcontroller. The other device is called the slave (sometimes peripheral), and it sends and receives data only as directed by the master. Multiple slave devices can be connected to a single master.
The basic setup for SPI uses four different signals, each on a separate pin:
- Serial Clock (SCK): This is the signal that controls the rate of the data transfer, and is driven by the master. One bit is transferred in each direction when this signal is pulsed. SPI doesn’t specify whether the bit is transferred on the rising or falling edge of the clock signal. Instead, that’s left up to the individual implementation. Most microcontrollers will have an option to specify this as part of the SPI mode setting. SPI modes 0 and 3 transfer on the rising edge, and modes 1 and 2 transfer on the falling edge. Mode 0 differs from 3, and 1 from 2, in the level of the clock signal when idle. Modes 0 and 1 keep the clock low when idle, and 2 and 3 keep it high. The datasheet for the slave device will specify which mode(s) it expects, but mode 0 is typical.
- Master Out / Slave In (MOSI): This is a data transmission signal over which the master sends data bits to the slave, at the rate dictated by the SCK signal. Note that this way of naming the signal is unambiguous about direction, and can be labeled the same on both master and slave devices. If the slave device is read-only (e.g. a sensor of some sort), this signal may be omitted. This signal is also sometimes called Controller Out / Peripheral In (COPI). On slave devices, this signal may also be called Data In (DI, DIN) or Serial Data In (SDI).
- Master In / Slave Out (MISO): This is a data transmission signal over which the slave sends data bits to the master, at the rate dictated by the SCK signal. If the slave device is write-only (e.g. a display of some sort), this signal may be omitted. This signal is also sometimes called Controller In / Peripheral Out (COPI). On slave devices, this signal may also be called Data Out (DO, DOUT) or Serial Data Out (SDO).
- Slave Select (SS): This is a control signal which activates the slave (i.e. tells it to pay attention to the other signals). The overbar on the abbreviation indicates that this signal is active low, and is pronounced by adding “bar” after the abbreviation (i.e. “ess ess bar”). In contexts where overbars are typographically difficult, this may be rendered as /SS or SSB (“B” for “Bar”). Active low means that the slave is selected when this signal is low, and deselected when the signal is high. When multiple slave devices are connected, each device is assigned its own SS signal. The other signals are shared among all slave devices. Usually, only one slave is selected at any one time. This signal is also sometimes called Chip Select (CS, /CS, CSB).
To perform SPI transfers in software, make sure the relevant signals are connected between the master and slave, and perform the following:
- Bring the SS pin low to select the slave device.
- Set the MOSI pin to the value of the outgoing bit. (Omit for read-only slave.)
- Pulse the SCK pin (high then low, or low then high, depending on mode).
- Read the incoming bit from the MISO pin. (Omit for write-only slave.)
- Repeat steps 2 through 4 until all bits are transferred.
- Bring the SS pin high to deselect the slave device.
As previously mentioned, most microcontrollers have SPI support built in hardware. The SPI hardware in the Arduino Uno’s ATmega328P can run the serial clock at up to half the rate of the system clock, which is 16 MHz on an Arduino Uno, so the Arduino can do SPI at up to 8 MHz. This is significantly faster than would be possible with a pure software implementation. Some microcontrollers can run the SPI serial clock at 10’s or even 100’s of MHz.
To use the microcontroller’s SPI hardware to perform transfers, MOSI, MISO, and SCK on the slave should be connected to the corresponding SPI pins on the microcontroller. On the Arduino Uno R3, MOSI is pin 11, MISO is pin 12, and SCK is pin 13. The slave’s SS pin can be connected to any free GPIO pin on the master, configured as an output. The Arduino has a pin labeled SS (pin 10), but it’s only necessary to use that when connecting the Arduino as a slave to another SPI master.
Once everything is connected, issue the microcontroller instructions to enable and use the SPI hardware. In an Arduino sketch, this can be done with the SPI library, specifically the functions SPI.begin() (called once to enable the SPI hardware), SPI.beginTransaction() (called once before each batch of transfers), SPI.transfer() (called any number of times to transfer a batch of eight bits each time), and SPI.endTransaction() (called once after each batch of transfers). If you need to disable the SPI hardware again for some reason, that can be done with function SPI.end(). The SS pin must still be handled explicitly by the software, but the others will be taken care of by the hardware.
Using Shift Registers with SPI
You may have noticed that the protocols for shift registers and SPI are remarkably similar, and wondered if they could be used together. In fact, they can! (If you didn’t notice, don’t worry too much; it took me way longer than I care to admit to notice it myself.) With just a few extra chips, 74HC595 and 74HC165 can be used to build a hardware SPI interface for a device which normally uses a parallel interface, thereby significantly reducing the number of GPIO pins used by the design, while only sacrificing a little bit of speed.
Shift registers and SPI each have serial clock signals, so those can simply be connected together. Similarly, SPI’s MOSI signal can be connected to the serial input signal of a 74HC595 shift register, and SPI’s MISO signal can be connected to the serial output signal of a 74HC165 shift register (although there’s a complication with MISO that we’ll discuss a little later). This takes care of three of the four SPI signals, and two of the three signals needed for each shift register.
The remaining signals, SPI’s SS, the 74HC595’s storage clock, and the 74HC165’s parallel load require just a little more thought. Recall that SS is brought low before an SPI transfer, and high after the transfer is complete. Recall also that the 74HC595’s storage clock needs a rising edge to move the data from the shift register to the storage register. That rising edge can be provided at just the right time by connecting SS to the 74HC595’s storage clock pin. This does mean that the slave device must be deselected after transferring each set of bits, but in practice, that’s not a significant disadvantage, just something to be aware of.
Before each transfer, the 74HC165’s parallel load pin needs to be set low to load data into the shift register, and high to enable that data to be shifted out. This is the inverse of the SS signal, which is set high then low before each transfer. To correct this, we can use an inverter (a.k.a. a NOT gate) to flip the SS signal to be what we need for the parallel load pin. The 74HC04 has six inverters, which is more than we need, but will work for our purposes. (The inverters we’re not using could be used in other parts of a larger circuit, or their inputs can simply be connected to GND or VCC to keep them from floating and introducing noise into the circuit.) If we connect SS to the input of one of these inverters, and the output of that inverter to the 74HC165’s parallel load pin, the 74HC165 will continually load data into its shift register until it’s selected, at which point it will latch that data and make it ready to be shifted out by the other SPI signals.
If this SPI interface that we’ve built out of shift registers is the only slave connected to the SPI pins, then we’re done; all that’s left is to connect the shift registers’ parallel inputs and outputs to the device. However, recall that if the SPI hardware is shared, MOSI, MISO, and SCK are also shared among all slave devices. For the 74HC595, this is no problem. It will shift bits in and out of its shift register even while another slave is selected, but its parallel output pins will stay latched, because they are controlled by its SS signal. The 74HC165 is another matter. If another slave is selected and SCK is pulsed, the 74HC165 will still shift bits out into the MISO pin, interfering with the signal coming from the other slave.
To fix this, we need to connect the 74HC165’s serial output to MISO only while its SS signal is low. This can be accomplished with a tri-state buffer. A tri-state buffer is a logic gate that has a data input, a control input, and a data output. The control input is active low, and when it’s low, the value on the data input is propagated to the data output. When the control input is high, the data output is set to high impedance, which basically means it’s not connected. The 74HC125 contains four tri-state buffers. Again, we only need one, but that’s okay. We connect the 74HC165’s serial output to the data input of one of the 74HC125’s tri-state buffers, connect the output of that tri-state buffer to MISO, and connect SS to the tri-state buffer’s control input. Now, the 74HC165 can only send data on MISO when it’s selected, which is exactly what we want.
As previously mentioned, shift registers of the same type may be cascaded together to create shift registers that hold more bits. This remains true when using them with SPI. If you’re building an SPI interface for a device that has sixteen inputs and twenty-four outputs, you can cascade two 74HC595’s together and three 74HC165’s. The principles are the same; there are just more bits. If the cascaded chains are of unequal length, then each transfer should include enough bits for the longest chain. The extra bits for the shorter chain can safely be ignored by the software. If the device is read-only or write-only, the interface can be built using only one of the two types of shift registers.
Conclusion
Both SPI and shift registers are useful ways to reduce the number of microcontroller GPIO pins needed for a design. Combining them (and adding a bit of glue logic) is a great way to do so without sacrificing much speed. I’ve used this technique in some of my projects and it’s worked very well. If you use this in your projects, I’d enjoy hearing about what went well or not so well. I hope that this post has been informative and enjoyable. If I’ve made any errors or left out important details, please do let me know.
Leave a Reply