Usually if we want to use Arduino with 16x2 LCD to display some message, usually 6 I/O pins of Arduino are required as illustrated in the blog post Arduino Keypad and LCD Interfacing with Code. However, sometimes we need those I/O pins for interfacing sensors, actuators etc. And so if I/O pins of microcontrollers, Arduino or Raspberry Pi Pico 2, PIC, AVR microcontroller, ESP32-CAM etc are in shortage then we can use popular I/O expander IC like the MCP23017 chip. If we use MCP23017 I/O expander IC, then only 2 I/O pins of Arduino that is the A4(SDA) and A5(SCL) pins are required. So we can reduce the Arduino I/O pin usages from 6 to 2 pins. All command-and-control signals is communicated between Arduino and MCP23017 IC using the I2C communication protocol.
Note: You can download the proteus project which contains schematic and program code from the link below.
Download MCP23017 LCD Arduino Proteus Project
In the previous MCP23017 chip usage example, Arduino + MCP23017: Turning an LED On with a Button, it was shown how to use its individual pins by illustrating LED control with push button which are connected to MCP23017. Here is another example of how to use MCP23017 with Arduino to interface 16x2 LCD and display a scrolling on the LCD.
Below is the circuit diagram.
MC23017 Video Demonstration
Circuit Overview
This is an Arduino Uno controlling a 16x2 character LCD via an MCP23017 I2C I/O expander, rather than driving the LCD directly with Arduino pins.
How it works, block by block
1. Arduino Uno → MCP23017 (I2C bus)
- The Arduino's analog pins A4 (SDA) and A5 (SCL) are wired to the MCP23017's SDA (pin 13) and SCL (pin 12) lines.
- R1 and R2 (4.7kΩ each) are pull-up resistors on SDA and SCL, pulled to +5V — standard practice for I2C buses, since I2C lines are open-drain and need pull-ups to sit high when idle.
- The MCP23017's address pins (A0, A1, A2) are tied to ground, setting its I2C address to the default (0x20).
- RESET is tied high (through the pull-up network) to keep the chip out of reset.
2. MCP23017 → LCD (parallel data)
- The MCP23017 is a 16-bit I/O expander with two 8-bit ports: GPA (GPA0–GPA7) and GPB (GPB0–GPB7).
- The GPA port drives the LCD's control and data lines: RS, E, and D4–D7 (this LCD is being run in 4-bit mode, which is why only D4–D7 are connected, not D0–D3).
- This lets the Arduino send commands/characters to the LCD by writing bytes to the MCP23017 over I2C, and the expander converts that into the parallel signals the LCD needs.
- The GPB port isn't connected to anything in this schematic — it's free for other uses (buttons, sensors, etc.).
3. LCD power and contrast
- VDD/VSS power the LCD (+5V/GND).
- RV1 (10kΩ potentiometer) is wired to VEE, the classic contrast-adjustment circuit — it forms a voltage divider between +5V and GND, feeding a variable voltage to VEE to control character visibility.
- R/W is tied low (grounded), meaning the circuit only ever writes to the LCD — it never reads busy-flag/status back, so timing is done with fixed delays in software.
4. Chip power
- MCP23017 VDD goes to +5V, VSS to GND — standard supply connections.
Why use this approach?
Driving an LCD directly from an Arduino uses 6 digital pins (RS, E, D4-D7). Using the MCP23017 instead uses only 2 pins (SDA/SCL) — freeing up 4 pins for other purposes and allowing multiple I2C devices to share the same two wires. This is functionally similar to commercial "I2C LCD backpack" modules, just built from discrete parts here.
Arduino Programming
Below is the Arduino code that displays "MCP23017 LCD Arduino Demo" on the LCD display.
// 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
// Bit positions on Port A
#define D7_BIT B00000001 // GPA0
#define D6_BIT B00000010 // GPA1
#define D5_BIT B00000100 // GPA2
#define D4_BIT B00001000 // GPA3
#define EN_BIT B00010000 // GPA4
#define RS_BIT B00100000 // GPA5
const char message[] = "MC23017 LCD Arduino Demo";
const int SCROLL_DELAY = 40; // ms between shifts — lower = faster scroll
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++);
}
// Set DDRAM cursor: row 0 or 1, col can go beyond 15 to write off-screen
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() {
Wire.begin();
lcdInit();
}
void loop() {
lcdCommand(0x01); // clear display
delay(2);
lcdSetCursor(0, 16); // start just off-screen to the right
lcdPrint(message);
int len = strlen(message);
int totalShifts = 16 + len; // scroll fully across and off the left edge
for (int i = 0; i < totalShifts; i++) {
lcdCommand(0x18); // shift entire display left by 1
delay(SCROLL_DELAY);
}
delay(500); // brief pause before looping the message again
}
Code Explanation
This code drives a 16x2 LCD in 4-bit mode using an MCP23017 I2C expander instead of direct Arduino pins: it writes to the MCP23017's GPIOA register over I2C to toggle the RS, Enable, and D4–D7 lines, sending each byte (command or character) as two 4-bit nibbles with an Enable pulse to latch each one, following the standard HD44780 reset/init sequence at startup to configure 4-bit mode, a 2-line display, and cursor settings. For the scrolling effect, it writes the message starting at DDRAM column 16 (just off-screen to the right), then repeatedly sends the "shift display left" command (0x18) enough times to slide the text fully across and off the left edge — letting the LCD's own hardware shift handle the animation rather than redrawing text at new positions each frame.
