Arduino: a code library for MCP23S17 LCD

In today’s blog post I will explain to you how to control Liquid Crystal Displays (LCDs) based on the Hitachi HD44780 (or a compatible) chip...

In today’s blog post I will explain to you how to control Liquid Crystal Displays (LCDs) based on the Hitachi HD44780 (or a compatible) chipset, which is found on most text-based LCDs, using one MCP23S17 port expander.

I tried to make things as simple as possible, so everything comes as a code library — a fork of the well-known Adafruit LiquidCrystal library.

The reason for choosing that library as a starting point is that I wanted to make all the high-level functions work just as with the Arduino or the Adafruit I2C/SPI LCD backpack.

However, deep inside the library, there are many changes — all the low-level functions that are used to communicate with the Arduino board and with the LCD have been heavily modified.

My library comes with some simplifications:

  • Only five data lines are used for the communication between the Arduino board and the MCP23S17 port expander:
  • Standard SPI: MISO, MOSI, SCK
  • /CS line has to be specified by the user when he creates the LCD object
  • The/RST pin for MCP23S17 is also configured when creating the LCD object.
  • The LCD works in 4-bit mode only, so it uses only one data port of the MCP23S17. The user chooses between PORTA and PORTB when he creates the LCD object. However, within the data port, the connections to the LCD are hardwired and cannot be changed at this moment.

With the above simplifications, one does only need to have a basic understanding on how the MCP23S17 operates — so if you are not familiar with it, download the datasheet and study it before going any further.

Connections. Created with

For developing this library I used one Expand Click from MikroElektronika, configured for 5V operation. The Expand click was placed in mikroBUS socket #1 of an Arduino Uno Click Shield. The board used for testing is the ubiquitous Arduino Uno.

The connection diagram is as follows:

On the Arduino side, the layout Arduino Uno Click Shield dictates the pins used:

  •  /CS is D10
  •  /RST is connected to A3.
  •  The SPI pins are the standard pins:
  •  MOSI — 11
  •  MISO — 12
  •  SCK — 13

The MCP23S17 hardware address is set as 0 using the jumpers on the Expand click board. I felt no need to change this, and the 0.0.1 library version works only with this hardware address.

MikroElektronika Expand Click with MCP23S17 port expander

On the LCD side, the connections are as follows:

  • LCD D7 pin to GPx7
  • LCD D6 pin to GPx6
  • LCD D5 pin to GPx5
  • LCD D4 pin to GPx4
  • LCD EN pin to GPx3
  • LCD RS pin to GPx2
  • LCD R/W pin to ground
  • LCD VSS pin to ground
  • LCD VCC pin to 5V
  • 10K resistor:
  • ends to +5V and ground
  • wiper to LCD VO pin

Where GPx is either GPA or GPB of MCP23S17.

Choosing this pin order is not a random thing — the LCD mini click from MikroElektronika uses that pin order, and I have plans to create a derivative of this library that works with that click board.
Arduino Uno with Expand click and LDC display

Besides this, allowing the user to choose the LCD pins at random would complicate the code a bit, and the code will run much slower.

However, if you know other MCP23S17 boards that use different wiring, drop me a comment, and I will see what I can do.

The code library can be downloaded from

The goal of this library is to provide a set of high-level functions that mimic the existing Arduino LCD library:

The LCD object is initialized as follows:

MCP23S17_LCD object_name(uint8_t rst, uint8_t cs, uint8_t PORT);

  • object_name is any arbitrary name given to create the object (typically lcd)
  • parameters:
    • rst — RST pin for MCP23S17
    • cs — CS pin for MCP23S17
    • PORT — GPIO port of MCP23S17, where the LCD is connected

Then, all the high-level functions described at will work with the new LCd object, just like in the original LCd library.

Below is just a short example that writes “Hello world” on the LCD:

// include the library code:
#include <MCP23S17_LCD.h>

MCP23S17_LCD lcd(A3, 10, PORTB);

void setup() {
  // set up the LCD's number of columns and rows:
  lcd.begin(16, 2);
  // Print a message to the LCD.
  lcd.print("hello, world!");

void loop() {
  // set the cursor to column 0, line 1
  // (note: line 1 is the second row, since counting begins with 0):
  lcd.setCursor(0, 1);
  // print the number of seconds since reset:
  lcd.print(millis() / 1000);
You might wonder what are the changes needed to make the LCD work with the MCP23S17. Below there are some of the major changes:

In MCP23S17.h, besides updating some function naming and parameters, you will find some #defines related to MCP23S17. Most registers names and values are explained in the datasheet.
>// MCP23S17 uses SPI
#include <SPI.h>

// defines for MCP23S17
#define PORTA  0x14
#define PORTB  0x15
#define IODIRA 0x00
#define IODIRB 0x01
#define IOCON  0x0A
// Control byte and configuration register information
// Control Byte: "0100 A2 A1 A0 R/W" -- W=0
#define    OPCODEW       (0b01000000)  
// Opcode for MCP23S17 with LSB (bit0) set to write (0),
// address OR'd in later, bits 1-3
#define    OPCODER       (0b01000001)  
// Opcode for MCP23S17 with LSB (bit0) set to read (1),
// address OR'd in later, bits 1-3
In MCP23S17.c we have a new set of low-level functions for MCP23S17 and for communications with the LCD:
/************ low level data pushing commands **********/

void MCP23S17_LCD::write4bits(uint8_t value, bool RSbit) {
    uint8_t packet = (value << 4) | (RSbit << 2);
    // EN = 0
    // EN = 1
    expander_setOutput(packet | (1<<3));
    // EN = 0
void MCP23S17_LCD::write8bits(uint8_t value, bool RSbit) {
    uint8_t nibbleHigh = value >> 4;
    uint8_t nibbleLow = value & 0xF;
    uint8_t packetHigh = (nibbleHigh << 4) | (RSbit << 2);
    uint8_t packetLow = (nibbleLow << 4) | (RSbit << 2);
    // EN = 0
    // EN = 1
    expander_setOutput(packetHigh | (1<<3));
    // EN = 0
    // EN = 0
    // EN = 1
    expander_setOutput(packetLow | (1<<3));
    // EN = 0

// This function confugires the MCP23S17 port expander
void MCP23S17_LCD::expander_setup(void){
    // Select the correct IOCON register depending on the indicated port
    if (_port == PORTA){
      _iodir = IODIRA;
    if (_port == PORTB){
      _iodir = IODIRB;
    // Now, configure the expander    
    // 1. Configure the MCP23S17 control pins
    pinMode(_rst, OUTPUT);
    pinMode(_cs, OUTPUT);
    digitalWrite(_rst, 1);
    digitalWrite(_cs, 1);
    // 2. Start SPI
    // 3. briefly flash the reset pin
    digitalWrite(_rst, 0);
    digitalWrite(_rst, 1);
    // 4. enable hardware addressing
    expander_sendByte(IOCON, 0b00001000);
    // configure LCD port direction as output
    expander_sendByte(_iodir, 0);
    // set LCD port as 0
    expander_sendByte(_port, 0);    

// Writes to MCP23S17
void MCP23S17_LCD::expander_sendByte(uint8_t addr, uint8_t tbyte){
    SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
    digitalWrite(_cs, 0);
    digitalWrite(_cs, 1);

// Updates the status of MCP23S17 port
void MCP23S17_LCD::expander_setOutput(uint8_t output){
    expander_sendByte(_port, output);
The expander_setup function configures the MCP23S17 according to the parameters indicated at LCD object creation. The following initializations are made:

    the reset line is briefly flased, then kept at logical “1”
    hardware addressing is enabled
    the LCD port is configured as output and set at 0x00

Two new functions, expander_sendByte and expander_SetOutput, are used to update the pins of the LCD port.

The write4bits and write8bits functions have been modified to call the expander_setOutput function.

Also, the begin function becomes:
void MCP23S17_LCD::begin(uint8_t cols, uint8_t lines, uint8_t dotsize) {
  if (lines > 1) {
   _displayfunction |= LCD_2LINE;
  _numlines = lines;

  setRowOffsets(0x00, 0x40, 0x00 + cols, 0x40 + cols);  

  // for some 1 line displays you can select a 10 pixel high font
  if ((dotsize != LCD_5x8DOTS) && (lines == 1)) {
     _displayfunction |= LCD_5x10DOTS;

  // No RW pin
  // according to datasheet, we need at least 40ms after power rises above 2.7V
  // before sending commands. Arduino can turn on way before 4.5V so we'll wait 50
  // Now we pull both RS and R/W low to begin commands
  //put the LCD into 4 bit or 8 bit mode
  // this is according to the hitachi HD44780 datasheet
  // figure 24, pg 46

  // we start in 8bit mode, try to set 4 bit mode
  write4bits(0x03, 0);
  delayMicroseconds(4500); // wait min 4.1ms

  // second try
  write4bits(0x03, 0);
  delayMicroseconds(4500); // wait min 4.1ms
  // third go!
  write4bits(0x03, 0);

  // finally, set to 4-bit interface
  write4bits(0x02, 0);
  // finally, set # lines, font size, etc.
  command(LCD_FUNCTIONSET | _displayfunction);  

  // turn the display on with no cursor or blinking default

  // clear it off

  // Initialize to default text direction (for romance languages)
  // set the entry mode
  command(LCD_ENTRYMODESET | _displaymode);
One might observe that from the original code I have kept only the initialization in 4-bit mode. A call to expander_setup() was added.


Air quality,2,Arduino code library,2,Arduino projects,23,Buggy,1,Casual stuff,2,ESP8266,2,MikroElektronika,3,PIC projects,7,Review,26,Robots,1,Tutorial,20,
Electronza: Arduino: a code library for MCP23S17 LCD
Arduino: a code library for MCP23S17 LCD
Loaded All Posts Not found any posts VIEW ALL Readmore Reply Cancel reply Delete By Home PAGES POSTS View All RECOMMENDED FOR YOU LABEL ARCHIVE SEARCH ALL POSTS Not found any post match with your request Back Home Sunday Monday Tuesday Wednesday Thursday Friday Saturday Sun Mon Tue Wed Thu Fri Sat January February March April May June July August September October November December Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec just now 1 minute ago $$1$$ minutes ago 1 hour ago $$1$$ hours ago Yesterday $$1$$ days ago $$1$$ weeks ago more than 5 weeks ago Followers Follow THIS PREMIUM CONTENT IS LOCKED STEP 1: Share to a social network STEP 2: Click the link on your social network Copy All Code Select All Code All codes were copied to your clipboard Can not copy the codes / texts, please press [CTRL]+[C] (or CMD+C with Mac) to copy Table of Content