By dave | August 15, 2019

IoAbstraction: Arduino Pins, IO Expanders, Shift Registers using same code

Using IoAbstraction you can write a sketch / program that uses Arduino pins, shift registers and IO expander devices at the same time, very much like you’d normally use Arduino pins. This library also provides simple interrupt handling that again is consistent across Arduino, mbed and IO expander ICs.

What do we mean by consistent, we mean that configuring a pin, adding an interrupt, reading from pins, and writing to pins is the same across Arduino, mbed, PCF8574, MCP32017 and shift registers. Even analog operations are standardized too. For most cases, such as adding switches, encoders and checking analog levels, it is device independent.

There are several sketches in the examples folder showing how to use most of the capabilities mentioned here. They cover Arduino pins, shift registers, IO expander devices, and MultiIo abstraction that allows many devices to be treated as a single large IO device.

You can look at BasicIoAbstraction in the reference docs.

There is also a fork of the LiquidCrystal library that works with this abstraction, and therefore can be used with pins, IO expanders or a shift register simply by changing the IoAbstraction it’s using. Also see This blog post about using IO Abstraction

Using IO Abstraction in your sketches

The IoAbstraction interface deals with pin configuration, reading, writing and processing interrupts. Each implementation has its own way of handling these functions, but to the outside user they look the same, apart from creating an instance where there are clear differences. When you create an IoAbstraction instance you are returned an IoAbstractionRef. It is this reference that you pass to all the ioDevice* functions below.

First let’s take a look at how to set the direction of a pin, here it is very similar to Arduino, apart from we use ioDevicePinMode(..):

ioDevicePinMode(ioExpander, pin, INPUT);
ioDevicePinMode(ioExpander, pin, OUTPUT);
ioDevicePinMode(ioExpander, pin, INPUT_PULLUP);

To register a raw interrupt handler for a pin again similar to Arduino we use ioDeviceAttachInterrupt(..), see the TaskManagerIO section on events and interrupt handling for better strategies, where you can either marshal interrupts to task manager, or use events.

ioDeviceAttachInterrupt(ioDevice, pin, pinMode)

Where ioDevice is any IOAbstractionRef pin is the pin on the device to attach to pinMode one of CHANGE, RISING, FALLING.

Reading from and writing to pins.

Reading and writing from pins works slightly differently with the library, this is because the IO may well be going over a serial bus. Due to this inefficiency, the serial implementations use a buffer to reduce reads and writes; but Arduino and mbed wrappers deal with pins directly without buffering. However, even when writing for Arduino or mbed, include the synchronisation code becuase otherwise it may not work with another IO device.

It’s up to you how and when you call ioDeviceSync(ioExpander); but using the sync is most optimal when you first write, then sync, then read. This is demonstrated below:

ioDeviceDigitalWrite(ioExpander, pinWrite, newValue);
value = ioDeviceDigitalRead(ioExpander, pinRead)

If you’re only doing one operation, you can use the shorthand read and write functions with an ‘S’ at the end, these automatically sync the device as appropriate. However, if you’re doing many operations over serial they are much less efficient.

ioDeviceDigitalWriteS(ioExpander, pinWrite, newValue);
value = ioDeviceDigitalReadS(ioExpander, pinRead)

Reading from and writing to ports

It is also possible to read from and write to ports, 8 bits at a time. For some tasks this can be much easier than writing one bit at a time. On I2C expanders and shift registers there’s no risk using ports whatsoever. However, with the Arduino pin abstraction, be careful to understand what other functions you may interfere with on that port. At the moment mbed does not support this option.

void ioDeviceDigitalWritePortS(IoAbstractionRef ioDev, uint8_t pinOnPort, uint8_t portVal);
uint8_t ioDeviceDigitalReadPortS(IoAbstractionRef ioDev, uint8_t pinOnPort, uint8_t portVal)

Creating an Io Abstraction

Firstly you always need to include the main header:

#include <IoAbstraction.h>

Also, if you are using i2c IoExpander’s such as the PCF8574 or MCP23017 also include this header:

#include <IoAbstractionWire.h>

To use with Arduino pins directly

The simplest IoAbstraction type of all is for Arduino pins, it’s pretty much a pass through, that calls through to pinMode, digitalRead, digitalWrite etc.

IoAbstactionRef arduinoPins = internalDigitalIo(); 

Using mbed pins directly

On mbed we fully support creating IoAbstractionRefs for the inbuilt pins. We wrap the mbed DigitalIn, DigitalOut and InterruptIn support using our simple collection. We use the underlying gpio_* functions to configure, read and write, while interrupts are handled using the InterruptIn class. In a future version, we will expose the gpio_t for mbed, and make it possible to configure gpio’s differently.

IoAbstactionRef mbedPins = internalDigitalIo(); 

Using a PCF8574 i2c expander:

For PCF8574, the i2c communication bus is used to read and write values. You will need to know the address of the i2c device on the bus. If you are not sure what address it’s on, use this i2c address scanner. Interrupts are supported but read the notes to understand the limitations:

  • Inputs actually set the output to HIGH, and then sense changes on the port. This means that inputs are always INPUT_PULLUP.
  • Outputs are quite low current, wire components such that the device is acting as a sink (lower voltage side).
  • Interrupts can not be enabled on a per pin basis, once you register an interrupt, it’s for all pins.
  • You wire the interrupt pin of the PCF8574 to a suitable interrupt enabled pin on your Arduino. It is this pin that you provide as optionalInterruptPin.

Creating an instance:

IoAbstactionRef ioExpander = ioFrom8754(i2cAddress, optionalInterruptPin, optionalWireClass);


  • i2cAddress is the address of the device on the bus
  • optionalInterruptPin is the pin on which you’ve connected the PCF8574’s INT pin to the Arduino. Optional, can be omitted.
  • optionalWireClass is the wire implementation class - normally Wire, on Arduino you can omit this.

Using a MCP23017 i2c expander:

As above, the i2c communication bus is used to read and write values. You will need to know the address of the device, use this i2c address scanner if unsure. IO capabilities are almost identical to that of the Arduino, there are few limitations. However, I recommend if you are doing heavy writing and heavy interrupt based reading, then try to keep each on separate ports if possible.

  • The device has 16 IO pins, and is therefore represented as two ports A and B. Pins are 0 thru 15.
  • Port A is on pins 0-7, B is on pins 8-15.
  • Inputs can be of type INPUT or INPUT_PULLUP.
  • Interrupts are configurable per pin, with either single (whole chip) or per port to the Arduino interrupt pin(s).
  • Interupts support CHANGE, RISING, FALLING modes.
  • Ensure that you tie the /RESET pin to Vcc, or it is otherwise on a pin set HIGH by your sketch. Otherwise, severe instability will result.

Creating an instance

// create an IO device that has no interrupts
IoAbstractionRef io23017 = ioFrom23017(address, optionalWirePtr);
// create an IO device that uses one interrupt pin on the Arduino for both ports
IoAbstractionRef io23017 = ioFrom23017(address, interruptMode, arduinoIntPin, optionalWirePtr);
// create an IO device that uses two interrupt pins on the Arduino, one for each port
IoAbstractionRef io23017 = ioFrom23017(address, interruptMode, intPinA, intPinB, optionalWirePtr);
  • address is the i2c address on which the device is addressable.

  • intPinA & intPinB are the interrupt pins on the board that you’ve connected the IOExpander interrupt pins to.

  • optionalWirePtr if you’re using Wire on Arduino, you can omit this, otherwise set it to a pointer to the i2c class you are using.

  • interruptMode IoAbstraction does most of the work to do with the InterruptMode parameter. There is little work on your side, just choose the mode and library will do the rest.. Options are:

  • NOT_ENABLED - interrupts are not enabled, in this case you could use the simpler single argument version.

  • ACTIVE_LOW_OPEN - it is easiest, and the library will enable pull up resistors. More than one device can share this pin.

  • ACTIVE_HIGH_OPEN - not really useful.

  • ACTIVE_HIGH - interrupt line will go high when there is an interrupt.

  • ACTIVE_LOW - interrupt line will go low when there is an interrupt

Using with shift registers:

Unlike other IO devices, shift registers have a known direction upfront. The implementation handles the well-known 74HC165 for input and 74HC595 for output. You can define if an IO abstraction should handle input, output or both. For shift-registers inputs are always on pins 0..32, and outputs are always from 32 SHIFT_REGISTER_OUTPUT_CUTOVER upward. For more information about shift registers see [] and []. Interrupts cannot be supported on shift registers.

You can theoretically chain together up to 4 input shift registers and up to 4 output shift registers. However, we’ve only ever tested chaining two registers.

For input only:

IoAbstractionRef inputOnlyFromShiftRegister(readClockPin, dataPin, latchPin, numOfDevicesRead = 1);

For output only:

IoAbstractionRef outputOnlyFromShiftRegister(writeClockPin, writeDataPin, writeLatchPin, numOfDevicesWrite = 1);

For input and output:

IoAbstractionRef inputOutputFromShiftRegister(readClockPin, readDataPin, readLatchPin, numOfReadDevices,
                                              writeClockPin, writeDataPin, writeLatchPin, numOfWriteDevices);

To use more than one IO expander at the same time

To use more than one IoAbstraction at once in the same code, simply create a multi IO as below, and add as many IO expander devices as needed. numberOfPinsForArduino allocates 0 thru numberOfPinsForArduino - 1 as for Arduino pins. Following this will be each expander that you add. The type is MultiIoAbstractionRef and is created as below. See the full MultiIoAbstraction guide

MultiIoAbstractionRef multiIo = multiIoExpander(100);  // allocate pins 0..99 to arduino
multiIoAddExpander(multiIo, ioFrom8574(0x20), 10);     // then have an 8574 from 100..109

... then use just like an other IoAbstraction

Above example:

  • 0..99 allocated as Arduino pins,
  • and 100..109 for the expander at i2c address 0x20

If you decide to create your IoAbstractions explicitly instead of using the provided functions, you’ll have an object of type BasicIoAbstraction, to get an IoAbstractionRef we would:

IoAbstractionRef multiIoRef = &manuallyCreatedIo;

Want a different IO device?

Either submit a patch on github, raise an issue on IoAbstraction, or get in touch using the contact form.

Using with a different Wire instance

On some Arduino boards there’s more than one wire instance, most of the IoExpanders can optionally take a wire instance as the last parameter, for example such as Wire1 or Wire2

For example:

IoAbstractionRef io23017 = ioFrom23017(address, &Wire1); 

Go back to the IoAbstraction page

Other pages within this category

comments powered by Disqus

We use cookies to analyse traffic and to personalise content. We also embed Twitter, Youtube and Disqus content on some pages, these companies have their own privacy policies.

Please see our privacy policy should you need more information or wish to adjust your settings.