Arduino: a code library for MCP23S17 LCD

Teodor Costachioiu
Feb 8, 2019 · 7 min read
Image for post
Image for post

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

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.

Image for post
Image for post
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.

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 https://github.com/Electronza/MCP23S17_LCD.

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 https://www.arduino.cc/en/Reference/LiquidCrystal 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);
delay(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
expander_setOutput(packet);
delayMicroseconds(5);
// EN = 1
expander_setOutput(packet | (1<<3));
delayMicroseconds(5);
// EN = 0
expander_setOutput(packet);
delayMicroseconds(40);
}

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
expander_setOutput(packetHigh);
delayMicroseconds(10);
// EN = 1
expander_setOutput(packetHigh | (1<<3));
delayMicroseconds(10);
// EN = 0
expander_setOutput(packetHigh);
delayMicroseconds(10);
// EN = 0
expander_setOutput(packetLow);
delayMicroseconds(10);
// EN = 1
expander_setOutput(packetLow | (1<<3));
delayMicroseconds(10);
// EN = 0
expander_setOutput(packetLow);
delayMicroseconds(40);
}

// 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
SPI.begin();
// 3. briefly flash the reset pin
digitalWrite(_rst, 0);
delayMicroseconds(100);
digitalWrite(_rst, 1);
delayMicroseconds(100);
// 4. enable hardware addressing
expander_sendByte(IOCON, 0b00001000);
delayMicroseconds(50);
// configure LCD port direction as output
expander_sendByte(_iodir, 0);
// set LCD port as 0
expander_sendByte(_port, 0);
delayMicroseconds(50);
}


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

// 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) {
expander_setup();

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

// SEE PAGE 45/46 FOR INITIALIZATION SPECIFICATION!
// 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
delayMicroseconds(50000);
// 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);
delayMicroseconds(150);

// 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
_displaycontrol = LCD_DISPLAYON | LCD_CURSOROFF | LCD_BLINKOFF;
display();

// clear it off
clear();

// Initialize to default text direction (for romance languages)
_displaymode = LCD_ENTRYLEFT | LCD_ENTRYSHIFTDECREMENT;
// 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.


Originally published at https://electronza.com on February 8, 2019. Moved to Medium on April 26, 2020.

Electronza

DIY electronics projects and more

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store