More ESP8266 GPIO pins with IOexpander library
Library to auto-detect & support different incompatible I/O expander chips in a single firmware
For projects that need more I/O pins than the small ESP8266 provides, an easy solution is to use an I/O expander chip such as Microchip’s MCP23017 (28-pin SSOP) that gives access to 16× bidirectional I/O pins while only using up 2× pins on the ESP itself (to communicate over I2C). My WiFi-controlled stepper motors with ESP8266 used this chip until it was suddenly unavailable everywhere, with an 18 month lead-time
. So I started looking at alternatives that were available (and not outrageously expensive) and settled on the MaxLinear XRA1203, along with the PCA6416A and CAT9555 as backups. These all share the same smaller 24-pin TSSOP package and are mostly pin- and software-compatible with each other. The new revision of my board kept the same overall size and through-hole positions, so it is fully compatible with my existing test jig and end user projects.
Why a single, shared firmware? Apart from being less work than building and publishing multiple firmwares, it is also much easier for end users who can’t accidentally flash the wrong firmware during an upgrade. Instead of bloated firmware that bundles several off-the-shelf libraries, each specific to one chip, IOexpander supports multiple chips auto-detected at runtime. It is a compact, flexible library that abstracts the exact chip fitted to the board to provide a consistent interface and pin naming.
No matter whether you’re a hobbyist or a giant multinational, Surviving the Great Chip Shortage of 2022 was a nightmare for manufacturing electronics, with many common components completely unavailable or costing several times their normal price.
Shoutout to Mouser Electronics who did an amazing job with my order of I/O expanders. Just 2 days from website click to transatlantic delivery, and with free shipping!
Arduino compatible
The library provides a familiar interface to the I/O expanders, just like accessing the built-in pins on an Arduino or ESP8266:
// Initialise library
IOexpander io;
io.begin();
// Configure pin as input, output, or input with pullup resistor
io.pinMode(LIMIT1, INPUT);
io.pinMode(DIRN1, OUTPUT);
io.pinMode(GPA6, INPUT_PULLUP);
// Read/write pin HIGH or LOW
value = io.digitalRead(LIMIT1);
io.digitalWrite(DIRN1, LOW);
value2 = io.digitalRead(GPA6);
IOexpander adds 1120 bytes to a compiled sketch, and uses 32 bytes of memory.
Auto-detecting chip over I2C
The new revision of my board sets a different I2C address for theXRA1203, making it easy to distinguish between the two chips at runtime:
#define XRA_ADDR 0x22
#define MCP_ADDR 0x20
void IOexpander::begin() {
// Scan I2C bus for which I/O expander is connected
Wire.begin();
Wire.beginTransmission(MCP_ADDR);
if (Wire.endTransmission() == 0) {
_address = MCP_ADDR; // MCP20317 (revB/B2 boards with 28-pin SSOP)
_ismcp = true;
Serial.printf("IOexpander.begin(): MCP @ i2c addr 0x%02X\r\n", _address);
} else {
Wire.beginTransmission(XRA_ADDR);
if (Wire.endTransmission() == 0) {
_address = XRA_ADDR; // XRA1203 (revB3 boards with 24-pin TSSOP)
_ismcp = false;
Serial.printf("IOexpander.begin(): XRA @ i2c addr 0x%02X\r\n", _address);
} else {
Serial.println("IOexpander.begin(): error i2c expander not found!");
}
};
}
Future improvements? Further I/O expanders such as the PCA6416A, CAT9555, TCA9535 or PCA9539 could be easily supported by adding more tests for unique I2C addresses, with a flag to note those chips don’t offer configurable pullup resistors. The library could be extended to support extra features such as interrupt lines.
Pin-mapping array
Pins are named for the purpose of each the 16× I/O channels, or they can just be a simple number 0→15. For ease of track routing the two boards use a different channel for each pin name. eg,SPEED1_MS1 is channel 0 on the XRA1203 but is channel 4 on the MCP23017, so the library uses a simple array lookup to map the pins as required. This allows higher-level code to use the pin names without knowing which chip is actually fitted.
// revB3 board XRA1203 pin names
enum {SPEED1_MS1, SPEED1_MS3, DIRN1, SPEED1_MS2, SPEED2_MS1, MOTOR1, GPA6, GPA5, LIMIT1, GPB6, GPB5, LIMIT2, SPEED2_MS2, SPEED2_MS3, MOTOR2, DIRN2};
// revB/B2 board XRA-to-MCP23017 pin mapping array
const uint8_t mcpPinMap[] = {4, 2, 0, 3, 12, 1, 6, 5, 7, 14, 13, 15, 11, 10, 9, 8};
void IOexpander::pinMode(uint8_t pin, uint8_t IOMode) {
...
if (_ismcp) pin = mcpPinMap[pin];
...
Similarly the I2C registers (commands) to set the pin mode or output value are mapped by the library depending on which chip is fitted.
The library was written for my stepper motor controller, but it wouldn’t take much work to adapt into a more general purpose library with generic pin names.
Efficient pin writes
Internally all the different 16-channel I/O expander chips use 2× banks of 8 pins. You read from or write to either bank in a single I2C command, or both banks with an extended command. Unlike some other libraries that read a bank, update one pin then write that bank back, IOexpander maintains a local copy of both banks, so updating any pin can be done with just a single write command. You can see this on oscilloscope traces of the I2C data signal:Setting multiple pins is done with separate function calls but this can result in a lot of overhead from multiple I2C commands. For example using the Adafruit MCP23017 library:
// 5x I2C read + 5x I2C write commands = 4.7 ms (35 bytes)
mcp.digitalWrite(DIRN1, LOW);
mcp.digitalWrite(SPEED2_MS1, HIGH);
mcp.digitalWrite(SPEED2_MS2, LOW);
mcp.digitalWrite(SPEED2_MS3, LOW);
mcp.digitalWrite(MOTOR1, HIGH);
IOexpander can optionally buffer the pin changes and then flush them to the I/O expander chip in a single command that takes only 0.4 ms, or 12× faster1. It also detects whether all the changed pins are in one bank, or if it needs to update both banks:
// Single I2C write command = 0.4 ms (3 bytes)
io.bufferUpdates();
io.digitalWrite(DIRN1, LOW);
io.digitalWrite(SPEED2_MS1, HIGH);
io.digitalWrite(SPEED2_MS2, LOW);
io.digitalWrite(SPEED2_MS3, LOW);
io.digitalWrite(MOTOR1, HIGH);
io.flushUpdates();
1All timings done with a 100kHz bus
Bulk read/write all 16 pins
For the fastest read or write of all 16 pins at once then the library provides separate functions that use an unsigned 16-bit integer, one bit per pin. Internally the integer is reordered as required, using the pin mapping array.
// Invert the values on all 16 pins
uint16_t pins = io.digitalReadAll();
pins = ~pins;
io.digitalWriteAll(pins);
// Set 2 pins HIGH, all others LOW
pins = (1 << DIRN1) + (1 << MOTOR1);
io.digitalWriteAll(pins);
Download files
I created custom footprints for the 28-pin SSOP and 24-pin TSSOP. These chips have a narrow pin pitch, and extending the pads length to 1.80mm makes it much easier to drag solder with a bevel/hoof tip iron.- IOexpander library source code (zip archive)
- KiCad schematic & hand-soldering footprints for MCP23017 & XRA1203 (zip archive)
- Adafruit MCP23017 library (github)
- Arduino PCA9539 library by Nico Verduin & Shawn McClintock (github)
- MaxLinear XRA1203 documentation (PDF)
- Microchip MCP23017 documentation (PDF)
- NXP PCA9555 documentation (PDF)
- NXP PCA6416A documentation (PDF)
- Xinluda XL9535 documentation (PDF)
Changelog
- Added support for Xinluda
XL9535chip, and new_nopullupflag for expanders without configurable internal pullup resistors - Optional 400kHz I2C bus set in
begin() - Added new
digitalReadAll()anddigitalWriteAll()functions - Fixed serious bug in
pinMode()not setting upper bank of 8 pins
Rapid PCB manufacture by PCBWay.com
This article was kindly sponsored by PCBWay. From uploading the GERBER files to their website, it took an amazing 1 day to manufacture the PCBs for this project, plus 3 days for express shipping from China to the UK! I particularly like the tracking of each stage of manufacturing as your order goes around their factory:
Get US$5 voucher for your PCB: If you’re going to try PCBWay for your own electronics projects, please consider registering via my PCBWay affiliate link: you’ll get a US$5 voucher for registering, and I’ll also get a voucher to put towards my next open hardware project
Nikki Smith, May 2022. Last updated November 2022.


