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.
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.
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.
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)
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)
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.
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.
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
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
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); } }
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); } }
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; }
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
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.