Interfacing 16x2 LCD with ESP32 Using MCP23017 via I2C

The ESP32 devkit v1 and alike ESP32 boards have 25 usable GPIO pins exposed, many of which are input only pins (4 pins which can only be used as digital input pin), many are strapping pins (5 pins which are used for boot control and should not be set high or low during the boot process), and there are pins directly connected to internal SPI flash (6 pins). So out of 25 GPIO pins, 11 pins are either input only and requires precaution when using them or/and cannot be used for connecting external components. This makes ESP32 to have low usable I/O pins. 

If one needs more I/O pins that ESP32 can provide then one solution is to use I/O expanding IC like the MCP23017 chip. One can connect MCP23017 to ESP32(or other boards like Arduino, Raspberry Pi Pico 2, microcontrollers etc) using only two pins which are the I2C pins. How to use MCP23017 with Arduino was illustrated in the blog posts Control a 16x2 LCD with Only 2 Wires — Arduino + MCP23017 I2C Expander and Arduino + MCP23017: Turning an LED On with a Button. Here it is shown ow 

How the MCP23017 Expands Your I/O Capabilities

The MCP23017 is a 16-bit I/O expander that uses the I2C communication protocol to turn just 2 pins on the ESP32 into 16 fully configurable digital pins.

  • 2-Wire Efficiency: The ESP32 communicates with the chip using its standard I2C hardware pins (SDA on GPIO 21 and SCL on GPIO 22). This leaves the remaining 23 ESP32 GPIO pins free for other components like sensors, SPI displays, or relays.

  • 16 Independent Channels: The chip features two 8-bit ports (Port A: GPA0GPA7 and Port B: GPB0GPB7). Each pin can be individually set as an input or output in your code.

  • Internal Pull-Up Resistors: Each pin features software-selectable 100kΩ pull-up resistors, which simplifies hardware wiring for buttons, switches, and sensors.

  • Interrupt Capability: Instead of making the ESP32 continuously check (poll) the MCP23017 pins for state changes, the chip includes two dedicated interrupt pins (INTA and INTB). These can trigger an ESP32 interrupt pin instantly whenever an input pin state changes.

  • Address Stacking (Up to 128 Pins): The MCP23017 has three hardware address pins (A0, A1, A2). By tying these pins high or low, you can assign up to 8 unique I2C addresses on the same two I2C lines (0x20 to 0x27). This allows you to expand a single ESP32 up to 128 additional I/O pins.

ESP32, MCP23017, LCD Interfacing Circuit Diagram and Hardware

Below is circuit diagram that shows how to interface 16x2 LCD with ESP32 devkit v1 using MCP23017 via I2C.

Interfacing 16x2 LCD with ESP32 Using MCP23017 via I2C

This circuit illustrates an ESP32 microcontroller driving a 16x2 HD44780 character LCD display using an MCP23017 I2C I/O expander to conserve GPIO pins. The ESP32 communicates with the MCP23017 over a 2-wire I2C bus (SCL connected to GPIO 22 and SDA to GPIO 21), supported by dual 4.7kΩ pull-up resistors (R1, R2) to ensure signal stability. The MCP23017—configured at I2C hardware address 0x20 by tying its address selection pins (A0, A1, A2) to ground and holding RESET high—acts as an intermediary by offloading the LCD's parallel drive connections onto Port A: pins GPA0–GPA3 route data bits D7–D4 for 4-bit mode operation, while GPA4 and GPA5 drive the Enable (E) and Register Select (RS) control lines, respectively (with RW grounded for permanent write mode). Finally, a 10k potentiometer (RV1) connected across +5V and GND provides variable voltage to the LCD's VEE pin to control text contrast, enabling full display functionality through just two ESP32 pins.

Components Used:

  • ESP32 DevKit V1

  • MCP23017 I/O Expander IC

  • 16x2 Character LCD (HD44780)

  • 10k Potentiometer & 4.7k Resistors

Video demonstration

The following video shows how the ESP32 MCP23017 LCD circuit works.

ESP32 MCP23017 LCD programming and coding

Following is program code for ESP32 devkit v1 and alike ESP32 version to control LCD using the MCP23017 I/O expander IC.


// 16x2 LCD driven through MCP23017 GPA pins, 4-bit mode — quick scrolling text
#include <Wire.h>

#define MCP23017 0x20
#define IODIRA   0x00
#define GPIOA    0x12

// ESP32 explicit I2C pin definitions
#define SDA_PIN 21
#define SCL_PIN 22

// Standard C++ binary notation replacing AVR macros
#define D7_BIT 0b00000001  // GPA0
#define D6_BIT 0b00000010  // GPA1
#define D5_BIT 0b00000100  // GPA2
#define D4_BIT 0b00001000  // GPA3
#define EN_BIT 0b00010000  // GPA4
#define RS_BIT 0b00100000  // GPA5

const char message[] = "ESP32 MCP23017 LCD Demo";
const int SCROLL_DELAY = 20;   // ms between shifts

void I2Cwrite(uint8_t addr, uint8_t reg, uint8_t val) {
  Wire.beginTransmission(addr);
  Wire.write(reg);
  Wire.write(val);
  Wire.endTransmission(true);
}

void lcdWrite4bits(uint8_t rs, uint8_t nibble) {
  uint8_t data = 0;
  if (rs) data |= RS_BIT;
  if (nibble & 0x01) data |= D4_BIT;
  if (nibble & 0x02) data |= D5_BIT;
  if (nibble & 0x04) data |= D6_BIT;
  if (nibble & 0x08) data |= D7_BIT;

  I2Cwrite(MCP23017, GPIOA, data);
  I2Cwrite(MCP23017, GPIOA, data | EN_BIT);
  delayMicroseconds(1);
  I2Cwrite(MCP23017, GPIOA, data);
  delayMicroseconds(100);
}

void lcdSend(uint8_t rs, uint8_t value) {
  lcdWrite4bits(rs, value >> 4);
  lcdWrite4bits(rs, value & 0x0F);
}

void lcdCommand(uint8_t cmd)  { lcdSend(0, cmd);  delayMicroseconds(50); }
void lcdData(uint8_t data)    { lcdSend(1, data); delayMicroseconds(50); }

void lcdPrint(const char* str) {
  while (*str) lcdData(*str++);
}

void lcdSetCursor(uint8_t row, uint8_t col) {
  uint8_t rowBase = (row == 0) ? 0x00 : 0x40;
  lcdCommand(0x80 | (rowBase + col));
}

void lcdInit() {
  I2Cwrite(MCP23017, IODIRA, 0x00);
  delay(50);

  lcdWrite4bits(0, 0x03); delay(5);
  lcdWrite4bits(0, 0x03); delayMicroseconds(150);
  lcdWrite4bits(0, 0x03); delayMicroseconds(150);
  lcdWrite4bits(0, 0x02);

  lcdCommand(0x28); // 4-bit, 2 line, 5x8 font
  lcdCommand(0x0C); // Display on, cursor off, blink off
  lcdCommand(0x06); // Entry mode: increment, no shift
  lcdCommand(0x01); // Clear display
  delay(2);
}

void setup() {
  // Pass SDA and SCL explicitly for ESP32
  Wire.begin(SDA_PIN, SCL_PIN)
  lcdInit();
}

void loop() {
  lcdCommand(0x01); // Clear display
  delay(2);

  lcdSetCursor(0, 16); // Start off-screen to the right
  lcdPrint(message);

  int len = strlen(message);
  int totalShifts = 16 + len;

  for (int i = 0; i < totalShifts; i++) {
    lcdCommand(0x18); // Shift entire display left
    delay(SCROLL_DELAY);
  }

  delay(50);
}

This program controls a 16x2 LCD display connected to an ESP32 via an MCP23017 I2C port expander using custom register-level communications rather than external LCD libraries. Upon boot, the ESP32 initializes the I2C bus on GPIO pins 21 (SDA) and 22 (SCL), configures Port A of the MCP23017 as outputs, and executes a standard 4-bit HD44780 initialization sequence. Data and commands are sent as 8-bit bytes split into two 4-bit nibbles using the lcdSend function; these nibbles are mapped to specific bit positions corresponding to the MCP23017 GPA pins driving the display's data lines (D4–D7) and control lines (RS, EN), with I2Cwrite toggling the Enable (EN) pin high and low to latch each command. In the main loop, the text string "eedairy.com" is set to start at off-screen cursor position (0, 16), and a for loop repeatedly sends HD44780 display shift commands (0x18) with a 150 ms delay to smoothly scroll the text leftward across the screen before clearing the display and repeating the sequence.




Post a Comment

Previous Post Next Post