You are on page 1of 8

IO Expander for ESP32, ESP8266, and Arduino

By Fernando Koyanagi in TechnologyMicrocontrollers

https://youtu.be/ie8f_QkAmbU

Would you like to expand the IOs of your ESP32, ESP8266, or Arduino? And have you thought about the
possibility of 16 new GPIOs that can be controlled using the I2C bus? Well today, I'm going to introduce you
to the GPIO expander MCP23016. Also, I’ll show you how to communicate a microcontroller with the
MCP23016. I’ll also talk about creating a program where we’ll use only 2 pins of this microcontroller to
communicate with the expander. We’ll use these for controlling the LEDs and the button.

Introduction

The MCP23016 device provides 16 bits for GPIO expansion using the I2C bus. Each bit can be configured
individually (input or output).
The MCP23016 consists of multiple 8-bit settings for input, output, and polarity selection.
The expanders provide a simple solution when the IOs are needed for switches, sensors, buttons, and
LEDs, among other examples.

Characteristics
 16 Input / Output pins (16 input standard)
 Fast I2C bus clock frequency (0-400 kbits/s)
 Three hardware address pins allow the use of up to eight devices
 Interrupt Port Capture Recorder
 Polarity reversing register for setting the polarity of the input port data
 Compatible with most microcontrollers

ESP01 Can Have 128 GPIOs!

An example that shows the magnitude of this expander is its use with ESP01, which can be connected to
up to eight expanders with only two IOS, reaching 128 GPIOs.

MCP23016

Here, we have the schematic of the expander, which has two groups of eight bits. This makes for a total of
16 ports. In addition to an interrupt pin, it has the CLK pin, which connects the capacitor and the resistor, which
are internally connected in a logic port. This is to form the clock, using the idea of a crystal oscillator, which
needs 1MHz clock. The TP pin is used to measure the clock. Pins A0, A1, and A2 are binary addresses.

CLOCK
The MCP23016 therefore uses an external RC circuit to determine the speed of the internal Clock. An
internal clock of 1 MHz is required (usually) for the device to work properly. The internal clock can be measured
on the TP pin. The recommended values for REXT and CEXT are shown below.

Address
To define the address of the MCP23016, we then use pins A0, A1, and A2. Just leave them at HIGH or
LOW for the address change.
The address will be formed as follows:

MCP_Address = 20 + (A0 A1 A2)


Where A0 A1 A2 can take HIGH / LOW values, this forms a binary number from 0 to 7.

For example:
A0> GND, A1> GND, A2> GND (means 000, then 20 + 0 = 20)

Or else,
A0> HIGH, A1> GND, A2> HIGH (meaning 101, then 20 + 5 = 25)

Commands

Below is a table with the commands for communication. Let's use GP0 and GP1, as well as IODIR0 and
IODIR1.

Categories:
GP0 / GP1 - Data Port Registers
There are two registers that provide access to the two GPIO ports.
The register reading provides the status of the pins on that port.
Bit = 1> HIGH Bit = 0> LOW

OLAT0 / OLAT1 - Output LACTCH REGISTERS


There are two registers that provide access to the output ports of the two ports.

IPOL0 / IPOL1 - Input Polarity Registers


These registers allow the user to configure the polarity of the input port data (GP0 and GP1).

IODIR0 / IODIR1
There are two registers that control the pin mode. (Input or Output)
Bit = 1> INPUT Bit = 0> OUTPUT
INTCAP0 / INTCAP1 - Interrupt Capture Registers
These are registers that contain the value of the port that generated the interrupt.

IOCON0 / IOCON1 - I / O Expander Control Register


This controls the functionality of the MCP23016.
Setting bit 0 (IARES> Interrupt Activity Resolution) controls the sampling frequency of the GP port pins.
Bit0 = 0> (default) Maximum port activity detection time is 32ms (low-power consumption)
Bit0 = 1> maximum activity detection time on the port is 200usec (higher-power consumption)

Structure for Communication

I show here the Wire class, which is the I2C communication in our core Arduino, which also allows the
expander to work with the Arduino Uno and Mega. However, the latter already has several IOs. We deal here
with the addresses of the chip, the access control, which are the codes of the registers, as well as the data.

Program

Our program consists of communicating the ESP32 with the MCP23016 to have more GPIOs to use. We
will then have a button and some LEDs connected to the MCP23016. We will control all of them using only the
I2C bus. Thus, only two ESP32 pins will be used. You can see the picture circuit below in the video.

ESP01

Here, I show the Pinout of ESP01.


Mounting ESP01

In this example, we have the GPIO0 connected in the SDA, and the GPIO2 connected in the SCL. We also
have a relay board, a buzzer, and an LED. On the other port, in GP1.0, we have one more LED with a resistor.

NodeMCU ESP-12E

Here, we have the Pinout of the NodeMCU ESP-12E.

Mounting NodeMCU ESP-12E

In this case, the only difference from the first example is that you have connected D1 and D2 in the SDA
and SCL, respectively.

WiFi NodeMCU-32S ESP-WROOM-32

Here's the Pinout of the WiFi NodeMCU-32S ESP-WROOM-32.

WiFi Mounting NodeMCU-32S ESP-WROOM-32

This time, the main difference from the other two examples is the button, and the three blinking LEDs. Here,
the SDA is connected to the GPIO19, while the SCL is connected to the GPIO23.

Libraries and Variables


First, we’ll include Wire.h, which is responsible for i2c communication, as well as setting the i2c address of
MCP23016. I show several commands, even some that we do not use in this project.

#include <Wire.h> // specify use of Wire.h library.

//endereço I2C do MCP23016


#define MCPAddress 0x20

// COMMAND BYTE TO REGISTER RELATIONSHIP : Table: 1-3 of Microchip MCP23016 -


DS20090A
//ENDEREÇOS DE REGISTRADORES
#define GP0 0x00 // DATA PORT REGISTER 0
#define GP1 0x01 // DATA PORT REGISTER 1
#define OLAT0 0x02 // OUTPUT LATCH REGISTER 0
#define OLAT1 0x03 // OUTPUT LATCH REGISTER 1
#define IPOL0 0x04 // INPUT POLARITY PORT REGISTER 0
#define IPOL1 0x05 // INPUT POLARITY PORT REGISTER 1
#define IODIR0 0x06 // I/O DIRECTION REGISTER 0
#define IODIR1 0x07 // I/O DIRECTION REGISTER 1
#define INTCAP0 0x08 // INTERRUPT CAPTURE REGISTER 0
#define INTCAP1 0x09 // INTERRUPT CAPTURE REGISTER 1
#define IOCON0 0x0A // I/O EXPANDER CONTROL REGISTER 0
#define IOCON1 0x0B // I/O EXPANDER CONTROL REGISTER 1

Setup
Here we have the functions to initialize four different types of microcontrollers. We also check the frequency,
set up the GPIOs, and set the pins. In the Loop, we check the status of the button.

void setup() {
Serial.begin(9600);

delay(1000);
Wire.begin(19,23); //ESP32
// Wire.begin(D2,D1); //nodemcu ESP8266
// Wire.begin(); //arduino
// Wire.begin(0,2);//ESP-01

Wire.setClock(200000); //frequencia

//configura o GPIO0 como OUTPUT (todos os pinos)


configurePort(IODIR0, OUTPUT);
//configura o GPIO1 como INPUT o GP1.0 e como OUTPUT os outros GP1
configurePort(IODIR1, 0x01);
//seta todos os pinos do GPIO0 como LOW
writeBlockData(GP0, B00000000);
//seta todos os pinos do GPIO1 como LOW
writeBlockData(GP1, B00000000);
}

void loop() {
//verifica e o botão GP foi pressionado
checkButton(GP1);

} // end loop

ConfigurePort
In this step, we configure the mode of the GPIO pins and identify the mode of the ports.

//configura o GPIO (GP0 ou GP1)


//como parametro passamos:
//port: GP0 ou GP1
//custom: INPUT para todos as portas do GP trabalharem como entrada
// OUTPUT para todos as portas do GP trabalharem como saida
// custom um valor de 0-255 indicando o modo das portas (1=INPUT,
0=OUTPUT)
// ex: 0x01 ou B00000001 ou 1 : indica que apenas o GPX.0 trabalhará
como entrada, o restando como saida
void configurePort(uint8_t port, uint8_t custom)
{
if(custom == INPUT)
{
writeBlockData(port, 0xFF);
}
else if(custom == OUTPUT)
{
writeBlockData(port, 0x00);
}
else
{
writeBlockData(port, custom);
}
}

WriteBlockData & CheckButton


Here, we send data to the MCP23016 through the i2c bus, check the status of the button, and indicate the
next step while taking into account the condition of being pressed or not.

//envia dados para o MCP23016 através do barramento i2c


//cmd: COMANDO (registrador)
//data: dados (0-255)
void writeBlockData(uint8_t cmd, uint8_t data)
{
Wire.beginTransmission(MCPAddress);
Wire.write(cmd);
Wire.write(data);
Wire.endTransmission();
delay(10);
}
//verifica se o botão foi pressionado
//parametro GP: GP0 ou GP1
void checkButton(uint8_t GP)
{
//faz a leitura do pino 0 no GP fornecido
uint8_t btn = readPin(0,GP);
//se botão pressionado, seta para HIGH as portas GP0
if(btn)
{
writeBlockData(GP0, B11111111);
}
//caso contrario deixa todas em estado LOW
else{
writeBlockData(GP0, B00000000);
}
}

ReadPin & ValueFromPin


We deal here with the reading of a specific pin, and the return of the bit value to the desired position.

//faz a leitura de um pino específico


//pin: pino desejado (0-7)
//gp: GP0 ou GP1
//retorno: 0 ou 1
uint8_t readPin(uint8_t pin, uint8_t gp)
{
uint8_t statusGP = 0;
Wire.beginTransmission(MCPAddress);
Wire.write(gp);
Wire.endTransmission();
Wire.requestFrom(MCPAddress, 1); // ler do chip 1 byte
statusGP = Wire.read();

return valueFromPin(pin, statusGP);


}

//retorna o valor do bit na posição desejada


//pin: posição do bit (0-7)
//statusGP: valor lido do GP (0-255)
uint8_t valueFromPin(uint8_t pin, uint8_t statusGP)
{
return (statusGP &( 0x0001 << pin)) == 0 ? 0 : 1;
}

ESP8266 Program
From here, we will see how the program we used in ESP-01 and in the nodeMCU ESP-12E was created,
which allows us to understand how differences between them are minimal.
We will only modify the line of the i2c communication constructor, which is the beginning method of the
Wire object.
Just uncomment the line according to the plate that we are going to compile.

// Wire.begin(D2,D1); //nodemcu ESP8266


// Wire.begin(0,2); //ESP-01

Setup
Notice that the builder is still commented out. Therefore, uncomment according to your board (ESP-01 or
nodeMCU ESP12-E).

void setup() {
Serial.begin(9600);

delay(1000);
// Wire.begin(D2,D1); //nodemcu ESP8266
// Wire.begin(0,2); //ESP-01

Wire.setClock(200000); //frequencia

//configura o GPIO0 como OUTPUT (todos os pinos)


configurePort(IODIR0, OUTPUT);
//configura o GPIO1 como OUTPUT (todos os pinos)
configurePort(IODIR1, OUTPUT);
//seta todos os pinos do GPIO0 como LOW
writeBlockData(GP0, B00000000);
//seta todos os pinos do GPIO1 como LOW
writeBlockData(GP1, B00000001);
}

Loop
In the loop, we switch the pins every 1 second. Thus, when pin0 of GP0 is on, the pins of GP1 are off.
When pin0 of GP1 is on, the GP0 pins are off.

void loop() {
//seta o pino 7 do GP0 como HIGH e os demais como LOW
writeBlockData(GP0, B10000000);
//seta todos os pinos do GPIO1 como LOW
writeBlockData(GP1, B00000000);

delay(1000);

//seta todos os pinos do GPIO0 como LOW


writeBlockData(GP0, B00000000);
//seta o pino 0 do GP1 como HIGH e os demais como LOW
writeBlockData(GP1, B00000001);
delay(1000);

} // end loop

IMPORTANT
The variables and library used are the same as those of the program we did for ESP32, as well as the
configurePort and writeBlockData methods.

Files
Download the files:
PDF
INO (ESP8266)
INO (ESP32)

You might also like