By dave | June 25, 2017

The same code for shift registers, Arduino Pins or 8574 IO chips.

When writing Arduino libraries that need quite a few IO pins, it can be difficult to decide if the library should use an IO expansion device such as the ubiquitous shift register, popular PCF8574 chip, or just use Arduino pins directly. This library is designed to handle all scenarios for the vast majority of cases.

The ubiquitous LiquidCrystal library; which almost all Arduino developers will have come across at some point, has two versions (LiquidCrystal and LiquidCrystalI2C). If a similar abstraction method had been used, there would be no need for two versions. I’m not saying anything bad about the library; more just pointing out an observation as I use it a lot (and someone has to keep those versions working and reasonably in sync).

Getting the source

This library is hosted on our github account, it comes complete with a working example showing both modes of operation.

Git hub library page [].

To use the library, get the latest source as an zip and expand it into your Arduino libraries directory.

Fork of the LiquidCrystal library

Latest news: I’ve forked the LiquidCrystal library and made it work with this abstraction, it took all of about an hour to get everything working. Internally, the library was very well structured and easy to modify. It’s available here.


The library has two additional examples, one for i2c and one for shift register use.

How my IO abstraction library works

This library works by abstracting away the access to read and write data on pins, such that there is an implementation that works with Arduino pins direct, another for shift registers and yet another that uses an 8574 IO expander. Other chips could easily be added in the future and I’d happily accept pull requests or patches!

When using Arduino pins mode, there is very little overhead, it just calls directly out to Arduino underlying functions. However, for serial operations, you really need to understand what the library is doing. As there is an overhead to sending things over the wire, rather than sending each bit change one at a time, the library waits for a runLoop() call, and then sends a write command if needed followed by a read.

Although I’ve packaged this as a library, I assume that some people may want to copy the header and source into their own library directly, and although I’d prefer you just package the library with your code as another library, it’s fine to just package the class with your library. But in this case please do rename the classes and global functions to avoid conflicts and consider an attribution to me.

Many libraries could make use of this code, so I’ve put a very open and permissive license on it: Apache 2.0; which allows commercial use. For the sake of example say we have a library called SuperLib that needs 6 IO pins, we could first off have a constructor that took the 6 pins needed, assuming the pins were direct to Arduino ioUsingArduino():

SuperLib sf(PIN1, PIN2, PIN3, PIN4, PIN5, PIN6)

But then we could add an overloaded constructor, so that we could provide an alternative IO device such as a shift register or an 8574 IO expander:

SuperLib sf(PIN1, PIN2, PIN3, PIN4, PIN5, PIN6, ioUsing8574(0x20));

Trying out the IO abstraction library

In all cases include the library

#include <BasicIoAbstraction.h>

For arduino pins

To create an instance for direct Arduino pins, simply:

BasicIoAbstraction* pins = ioUsingArduino();

To use an 8574 IO Expander

To create an instance for an 8574 IO expander provide the i2c address and include the Wire library:

#include <Wire.h>

BasicIoAbstraction* pins = ioFrom8754(0x20); // where 0x20 is the i2c address

To use shift registers

To create an instance that uses serial shift registers for read / write we would need a shift register for read and a shift register for write. We share the clock and data between the two chips, but we have two enable pins, that enables change on the registers output. If you are only using read, or write, you can use the two later functions. Because we need two shift registers and the directions are fixed, 0-23 are the read ‘pins’ and 24 onwards are the output ‘pins’. You can still call pin mode, but it won’t do anything on this variant. For more information about shift registers see [] and [].

BasicIoAbstraction* shiftReg = inputOutputFromShiftRegister(readClockPin, readDataPin, readLatchPin, readClockEnaPin, writeClockPin, writeDataPin, writeLatchPin)


BasicIoAbstraction* shiftReg = inputOnlyFromShiftRegister(readClkPin, readClkEnaPin, dataPin, latchPin);


BasicIoAbstraction* shiftReg = outputOnlyFromShiftRegister(writeClkPin, dataPin, latchPin);

To configure input and output pins use the library instead of the usual pinMode call. Use standard Arduino constants for INPUT and OUTPUT

pins->pinDirection(0, INPUT);
pins->pinDirection(6, OUTPUT);

In the main loop read and write values using the library. For the Arduino version, the pin number is used, for i2c 8574 it is the bit 0-7 of the port.

uint8_t switchValue = pins->readValue(0);
pins->writeValue(6, switchValue);

// this is absolutely needed on the serial versions, it makes it send the commands

Please take a look at the examples folder in the library, all the above cases are covered in detail.

Built circuit examples

Example circuit using two shift registers for input and output

Using two shift registers - input and output - click for full-size

Example circuit using an 8574 IO expander over i2c

Using an 8574 IO Expander for input and output


Generally, for most cases where the amount of IO is limited, I believe this library is a good fit. If the library is constantly setting and testing bits thousands of times a second, then this library is probably not appropriate; but I’d argue that in that case neither is using an IO expander.

As far as I can tell, it uses no RAM beyond the object creation, the virtual method table usage is all in flash memory, so that should not be a problem for most devices. As far as I can tell, the shift register version is the heaviest on memory and probably worst on performance too. If you have a Mega chip then the i2c version will use hardware serial.

Reducing overhead

As the old saying goes, it’s better to work smarter than harder (although it’s often said at inappropriate times). Rather than polling for a switch transition, why not use the interrupt support available on the 8574 chip, and only read from it when needed.

If you are bit blasting at a very high frequency or using PWM, you’re better off using Arduino pins directly.

Overhead involved in using serial IO / abstraction

There are few main sources of overhead in this library. For most cases, these should not pose a problem:

  1. it uses an abstract class with virtual method calls, AVR chips have no loop unravelling so cannot optimise out the jump.
  2. If you are using i2c, there is the obvious overhead of sending commands to the chip.
  3. There is an extra object, albeit small.
comments powered by Disqus
We use cookies to analyse traffic and to personalise content and adverts. Our social buttons may also use cookies.