Using the MCP23017 to increase your GPIO’s

Today I will show you another useful IO Expander chip, The MCP23017. This chip, although similar to the PCF8475, which I have already covered in a previous article, has many additional features that may make it a very attractive solution when you need some more extra GPIO pins for a big project…

Features

Let us look at some of the features of this chip

  • 16-Bit Remote Bidirectional I/O Port:
  • I/O pins default to input
    • High-Speed I2C Interface (MCP23017):
  • 100 kHz
  • 400 kHz
  • 1.7 MHz
    • High-Speed SPI Interface (MCP23S17):
  • 10 MHz (maximum)
    • Three Hardware Address Pins to Allow Up to
    Eight Devices On the Bus
    • Configurable Interrupt Output Pins:
  • Configurable as active-high, active-low or
    open-drain
    • INTA and INTB Can Be Configured to Operate
    Independently or Together
    • Configurable Interrupt Source:
  • Interrupt-on-change from configured register
    defaults or pin changes
    • Polarity Inversion Register to Configure the
    Polarity of the Input Port Data
    • External Reset Input
    • Low Standby Current: 1 µA (max.)
    • Operating Voltage:
  • 1.8V to 5.5V @ -40°C to +85°C
  • 2.7V to 5.5V @ -40°C to +85°C
  • 4.5V to 5.5V @ -40°C to +125°C
MCP23017 Pinout Diagram

The sixteen I/O ports are separated into two ‘ports’ – A (on the right) and B (on the left. Pin 9 connects to 5V, 10 to GND, 11 isn’t used, 12 is the I2C bus clock line (Arduino Uno/Duemilanove analogue pin 5, Mega pin  21), and 13 is the I2C bus data line (Arduino Uno/Duemailnove analogue pin 4, Mega pin 20).

External pull-up resistors should be used on the I2C bus – in our examples we use 4.7k ohm values. Pin 14 is unused, and we won’t be looking at interrupts, so ignore pins 19 and 20. Pin 18 is the reset pin, which is normally high – therefore you ground it to reset the IC. So connect it to 5V!

Finally we have the three hardware address pins 15~17. These are used to determine the I2C bus address for the chip. If you connect them all to GND, the address is 0x20. If you have other devices with that address or need to use multiple MCP23017s, see figure 1-2 in the datasheet.

You can alter the address by connecting a combination of pins 15~17 to 5V (1) or GND (0). For example, if you connect 15~17 all to 5V, the control byte becomes 0100111 in binary, or 0x27 in hexadecimal.

It is also available on a convenient breakout PCB, for about $USD0.80 from AliExpress

MCP23017 on Breakout PCB – Back
MCP23017 on Breakout PCB – Front

Please Note: THIS BREAKOUT PCB IS NOT SUITED FOR USE ON A BREADBOARD. YOU WILL SHORT OUT VCC AND GROUND AS WELL AS ALL THE IO PINS IF YOU TRY TO USE IT ON A BREADBOARD.

As you can see, the pins are however very clearly labelled, and thus easy to use. I have also purposely soldered my header pins “the wrong way round” to prevent using it on a breadboard, as this will short out Vcc to Ground!

Having interrupt outputs is one of the most important features of the MCP23017, since the microcontroller does not have to continuously poll the device to detect an input change. Instead an interrupt service routine can be used to react quickly to an input change such a key press…

To make life even easier each GPIO input pin can be configured with an internal pullup (~100k) and that means you won’t have to wire up external pull up resistors for keyboard input. You can also mix and match inputs and outputs the same as any standard microcontroller 8 bit port.

Addressing

The 23017 has three input pins to allow you to set a different address for each attached MCP23017.

The above corresponds to a hardware address for the three lines A0, A1, A2 corresponding to the input pin values at the IC. You must set the value of these hardware inputs as 0V or (high) volts and not leave them floating otherwise they will get random values from electrical noise and the chip will do nothing!

The four left most bits are fixed a 0100 (specified by a consortium who doles out address ranges to manufacturers).

So the MCP23017 I2C address range is 32 decimal to 37 decimal or 0x20 to 0x27 for the MCP23017.

Please note: The addresses are the same as those for the PCF8475. You must thus be careful if you use these two devices on the same i2c bus!

MCP23017 Non interrupt registers

IODIR I/O direction register

For controlling I/O direction of each pin, register IODIR (A/B) lets you set the pin to an output when a zero is written and to an input when a ‘1’ is written to the register bit. This is the same scheme for most microcontrollers – the key is to remember that zero (‘0’) equates to the ‘O’ in Output.

GPPU Pullup register

Setting a bit high sets the pullup active for the corresponding I/O pin.

OLAT Output Latch register

This is exactly the same as the I/O port in 18F series PIC chips where you can read back the “desired” output of a port pin whether or not the actual state of that pin is reached. i.e. consider a strong current LED attached to the pin – it is easily possible to pull down the output voltage at the pin to below the logic threshold i.e. you would read back a zero if reading from the pin itself when in fact it should be a one. Reading the OLAT register bit returns a ‘one’ as you would expect from a software engineering point of view.

IPOL pin inversion register

The IPOL(A/B) register allows you to selectively invert any input pin. This reduces the glue logic needed to interface other devices to the MCP23017 since you won’t need to add inverter logic chips to get the correct signal polarity into the MCP23017.

It is also very handy for getting the signals the right way up e.g. it is common to use a pull up resistor for an input so when a user presses an input key the voltage input is zero, so in software you have to remember to test for zero.

Using the MCP23017 you could invert that input and test for a 1 (in my mind a key press is more equivalent to an on state i.e. a ‘1’) however I use pullups all the time (and uCs in general use internal pullups when enabled) so have to put up with a zero as ‘pressed’. Using this device would allow you to correct this easily.Note: The reason that active low signals are used everywhere is a historical one: TTL (Transistor Transistor Logic) devices draw more power in the active low state due to the internal circuitry, and it was important to reduce unnecessary power consumption – therefore signals that are inactive most of the time e.g. a chip select signal – were defined to be high. With CMOS devices either state causes the same power usage so it now does not matter – however active low is used because everyone uses it now and used it in the past.

SEQOP polling mode : register bit : (Within IOCON register)

If you have a design that has critical interrupt code e.g. for performing a timing critical measurement you may not want non critical inputs to generate an interrupt i.e. you reserve the interrupt for the most important input data.

In this case, it may make more sense to allow polling of some of the device inputs. To facilitate this “Byte mode” is provided. In this mode, you can read the same set of GPIOs using clocks but not needling to provide other control information. i.e. it stays on the same set of GPIO bits, and you can continuously read it without the register-address updating itself. In non-byte mode, you either have to set the address you read from (A or B bank) as control input data.

Now to examine how to use the IC in our sketches.

As you should know by now most I2C devices have several registers that can be addressed. Each address holds one byte of data that determines various options. So before using we need to set whether each port is an input or an output. First, we’ll examine setting them as outputs. So to set port A to outputs, we use:

Wire.beginTransmission(0x20);
Wire.write(0x00); // IODIRA register
Wire.write(0x00); // set all of port A to outputs
Wire.endTransmission();

Then to set port B to outputs, we use:

Wire.beginTransmission(0x20);
Wire.write(0x01); // IODIRB register
Wire.write(0x00); // set all of port B to outputs
Wire.endTransmission();

So now we are in void loop()  or a function of your own creation and want to control some output pins. To control port A, we use:

Wire.beginTransmission(0x20);
Wire.write(0x12); // address port A
Wire.write(??);  // value to send
Wire.endTransmission();

To control port B, we use:

Wire.beginTransmission(0x20);
Wire.write(0x13); // address port B
Wire.write(??);  // value to send
Wire.endTransmission();

… replacing ?? with the binary or equivalent hexadecimal or decimal value to send to the register.

To calculate the required number, consider each I/O pin from 7 to 0 matches one bit of a binary number – 1 for on, 0 for off. So you can insert a binary number representing the status of each output pin. Or if binary does your head in, convert it to hexadecimal. Or a decimal number.

So for example, you want pins 7 and 1 on. In binary that would be 10000010, in hexadecimal that is 0x82, or 130 decimal. (Using decimals is convenient if you want to display values from an incrementing value or function result).

For example, we want port A to be 11001100 and port B to be 10001000 – so we send the following (note we converted the binary values to decimal):

Wire.beginTransmission(0x20);
Wire.write(0x12); // address port A
Wire.write(204); // value to send
Wire.endTransmission();
Wire.beginTransmission(0x20);
Wire.write(0x13); // address port B 
Wire.write(136);     // value to send
Wire.endTransmission();

A complete Example

// pins 15~17 to GND, I2C bus address is 0x20
#include "Wire.h"
void setup()
{
 Wire.begin(); // wake up I2C bus
// set I/O pins to outputs
 Wire.beginTransmission(0x20);
 Wire.write(0x00); // IODIRA register
 Wire.write(0x00); // set all of port A to outputs
 Wire.endTransmission();
Wire.beginTransmission(0x20);
 Wire.write(0x01); // IODIRB register
 Wire.write(0x00); // set all of port B to outputs
 Wire.endTransmission();
}
void binaryCount()
{
 for (byte a=0; a<256; a++)
 {
 Wire.beginTransmission(0x20);
 Wire.write(0x12); // GPIOA
 Wire.write(a); // port A
 Wire.endTransmission();
Wire.beginTransmission(0x20);
 Wire.write(0x13); // GPIOB
 Wire.write(a); // port B
 Wire.endTransmission();
 }
}
void loop()
{
 binaryCount();
 delay(500);
}

Using the pins as inputs

Although that may have seemed like a simple demonstration, it was created show how the outputs can be used. So now you know how to control the I/O pins set as outputs. Note that you can’t source more than 25 mA of current from each pin, so if switching higher current loads use a transistor and an external power supply and so on.

Now let’s turn the tables and work on using the I/O pins as digital inputs. The MCP23017 I/O pins default to input mode, so we just need to initiate the I2C bus. Then in the void loop() or other function all we do is set the address of the register to read and receive one byte of data.

// pins 15~17 to GND, I2C bus address is 0x20
#include "Wire.h"
byte inputs=0;
void setup()
{
 Serial.begin(9600);
 Wire.begin(); // wake up I2C bus
}
void loop()
{
 Wire.beginTransmission(0x20);
 Wire.write(0x13); // set MCP23017 memory pointer to GPIOB address
 Wire.endTransmission();
 Wire.requestFrom(0x20, 1); // request one byte of data from MCP20317
 inputs=Wire.read(); // store the incoming byte into "inputs"
 if (inputs>0) // if a button was pressed
 {
 Serial.println(inputs, BIN); // display the contents of the GPIOB register in binary
 delay(200); // for debounce
 }
}

Other Libraries

You can also download and install the MCP23017 Library from Adafruit for the Arduino IDE.
This library will make using this chip even easier… I will discuss this library in another post

I hope this will be useful to somebody.

Multiple I2C Devices on the same Bus, I2C Part 3

Today I will continue my series on I2C by showing you how to use multiple devices on the I2C bus. This will be an extremely short post, as it builds on skills that we have already covered.

I will connect the following

1 x 16×2 I2C LCD Screen address 0x27
1x 128×32 I2C OLED Display address 0x3C
2x PCF8574 I2C Io Extenders address 0x20 and 0x21

All of these devices will be controlled from Arduino Uno, using the following libraries


LiquidCrystal_I2C.h to control the LCD screen,
Wire.h and PCF8574.h to control the I2C IO extenders and
Adafruit_GFX, Adafruit_SSD1306.h and SPI.h to control the SSD1306 128×32 OLED display.

With DuPont wires and breadboards being the reliable things they are, I decided that, after initial testing, I will not show you how to do button inputs on the PCF8574 at this stage. The amount of stray capacitance floating around on the breadboards, and small momentary push-button switches, made for a very impressive but unreliable mess of wires, with no real learning value to it 😉 Maybe some more on that later when I do a decent real-world example using these technologies 🙂

As the total distance between the devices is relatively short, it was not necessary to use pull-up resistors on the I2C bus in my setup. I suspect that that is due to the fact that they may already be included on some of my devices.

The circuit is quite straight forward.

  1. Connect all SDA pins on the I2C devices together serially, and connect that to the Arduino SDA pin ( That is usually A4)
  2. Connect all SCL pins on the I2C devices together serially, and connect that to the Arduino SCL pin ( That is usually A5)

    A note: On my Uno clone, there is an additional I2C breakout at the top of the device, near the USB adapter. I chose to use that as well as A4 and A5, as the bus hung itself up when connected to the breadboard. Your mileage may vary on this one 🙂
  3. Connect all 5v (Vcc) lines to 5v on the Arduino, and all Ground (GND) lines to GND on the Arduino.
  4. Now connect 4 LEDs, through a suitable resistor ( 640 ohms up to 1k ohm ) to pin P0 and P1 on both of the PCF8574 IO extenders. Also, connect the other leg of the LED to ground.
  5. I have powered my Uno from an external 5v power supply, as I did not want to pull too much current from the regulator on the actual Uno clone.

That should complete your hardware setup. Double check all your connections, and then load the i2c scanner sketch in the Arduino IDE, you may find it under the examples for the Wire.h library.

Power up the circuit, and upload the sketch to the Uno. Open the Serial Monitor.

You should see 4 I2C devices being detected. Note their addresses. If you dont see 4 devices, check your wiring and addresses. You may have a device with a conflicting address or a bad connection. If you used the breadboard to connect the bus, chances are very good that you will not see all the devices.

Good, if all of that is working, copy paste the following code into a new Arduino IDE window.
I will explain the code in the section below:

/*
  Multiple devices on the I2C bus
  Maker and Iot Ideas, MakerIoT2020
*/
// Include the libraries that we will need
#include <SPI.h> // needed for OLED display. 
#include <PCF8574.h> // PCF8574
#include <Wire.h> // Generic I2C library
#include <Adafruit_GFX.h> // for OLED display
#include <Adafruit_SSD1306.h> // for OLED display
#include <LiquidCrystal_I2C.h> // For I2C LCD display

// we need to define the size of the OLED screen

#define OLED_WIDTH 128
#define OLED_HEIGHT 32

// mine does not have an onboard reset pin. If yours do, specify the 
// pin that it is connected to on the Arduino here. To use the 
// Arduino reset pin, specify -1 as below

#define OLED_RESET -1

// Define the OLED display, width,hight protocol and reset pin
Adafruit_SSD1306 oled(OLED_WIDTH,OLED_HEIGHT, &Wire, OLED_RESET);

// Define the I2C LCD screen address and pin configuration
LiquidCrystal_I2C lcd(0x27,2,1,0,4,5,6,7,3,POSITIVE);

// Define the PCF8574 devices ( you can have up to 8 on a bus )
// but in this case, my LCD uses address 0x27, so I will have a 
// conflicting address if I were to use 8 of them together with the
// LCD

PCF8574 Remote_1(0x20); 
PCF8574 Remote_2(0x21);

// Note the I2C addresses. You can obtain them from the i2c_scanner

void setup() {
  // serial debugging if needed
  Serial.begin(115200);
  // Start OLED Display Init

  if (!oled.begin(SSD1306_SWITCHCAPVCC,0x3C)) { // Init the OLED 
    Serial.println(F("OLED INIT FAILED"));
    for(;;); // Dont proceed ... loop forever
  }
  oled.display();
  delay(2000); // This delay is required to give display time to 
  // initialise properly
  oled.clearDisplay();
  oled.setTextSize(0);
  oled.setTextColor(SSD1306_WHITE);
  oled.setCursor(0,0);
  oled.println("TEST SCREEN");
  oled.display();
  delay(2000);
  oled.clearDisplay();
  oled.setCursor(1,0);
  oled.println("OLED SCREEN ON");
  oled.display();

  // Start the LCD

  lcd.begin(16,2);
  
  // Set the initial state of the pins on the PCF8574 devices
  // I found that the PCF8574 library sometimes does funny things
  // This is also an example of how to use native i2c to set the 
  // status of the pins
  
  Wire.begin();
  Wire.beginTransmission(0x20); // device 1
  Wire.write(0x00); // all ports off
  Wire.endTransmission();
  Wire.begin();
  Wire.beginTransmission(0x21); // device 2
  Wire.write(0x00); // all ports off
  Wire.endTransmission();
  // Set pinModes for PCF8574 devices
  // Note that there are two of them

  Remote_1.pinMode(P0,OUTPUT);
  Remote_1.pinMode(P1,OUTPUT);
  Remote_2.pinMode(P0,OUTPUT);
  Remote_2.pinMode(P1,OUTPUT);
  
  // Start both IO extenders

  Remote_1.begin();
  Remote_2.begin();

  // and set ports to low on both
  // you may find that if you ommit this step, they come up in an
  // unstable state.

  Remote_1.digitalWrite(P0,LOW);
  Remote_1.digitalWrite(P1,LOW);
  Remote_2.digitalWrite(P0,LOW);
  Remote_2.digitalWrite(P1,LOW);
  
}

void loop() {
  // Draw a character map on the OLED display.
  // This function is borrowed from the Adafruit library

  testdrawchar();

  // Write to the IO extenders

  Remote_1.digitalWrite(P0,HIGH);
  Remote_1.digitalWrite(P1,LOW);
  Remote_2.digitalWrite(P0,HIGH);
  Remote_2.digitalWrite(P1,LOW);
  
  // Display their status on the LCD
  lcd.setCursor(0,0);
  lcd.print(" R1 P0=1 P1=0");
  lcd.setCursor(0,1);
  lcd.print(" R2 P0=1 P1=0");
  delay(500);

  // Change status
  Remote_1.digitalWrite(P1,HIGH);
  Remote_1.digitalWrite(P0,LOW);
  Remote_2.digitalWrite(P1,HIGH);
  Remote_2.digitalWrite(P0,LOW);

  // Update LCD
  lcd.setCursor(0,0);
  lcd.print(" R1 P0=0 P1=1");
  lcd.setCursor(0,1);
  lcd.print(" R2 P0=0 P1=1");
  delay(500);
  // Do some graphics on the OLED display
  // Function borrowed from Adafruit
  testdrawrect();
  oled.clearDisplay();
  delay(500);
  // repeat indefinitely

}

void testdrawrect(void) {
  oled.clearDisplay();

  for(int16_t i=0; i<oled.height()/2; i+=2) {
    oled.drawRect(i, i, oled.width()-2*i, oled.height()-2*i, SSD1306_WHITE);
    oled.display(); // Update screen with each newly-drawn rectangle
    delay(1);
  }

  delay(500);
}

void testdrawchar(void) {
  oled.clearDisplay();

  oled.setTextSize(1);      // Normal 1:1 pixel scale
  oled.setTextColor(SSD1306_WHITE); // Draw white text
  oled.setCursor(0, 0);     // Start at top-left corner
  oled.cp437(true);         // Use full 256 char 'Code Page 437' font

  // Not all the characters will fit on the display. This is normal.
  // Library will draw what it can and the rest will be clipped.
  for(int16_t i=0; i<256; i++) {
    if(i == '\n') oled.write(' ');
    else          oled.write(i);
  }

  oled.display();
  delay(500);
}

This concludes a quick and dirty show and tell… I hope that it will stimulate questions and ideas for a lot of people.

Thank you

Extending Arduino/Esp32/STM32 GPIO Pins – PART 2

In the first part of this series, I showed you how to extend the available output pins on your microprocessor by using a SIPO (Serial In, Parallel Out) Shift Register. These work great to extend your outputs, but they do tend to involve a bit of extra work and organisation in your code. They are also a bit slower than the normal GPIO pins, because data has to be serially shifted into them, and then latched out onto the parallel port.

I have also mentioned that there are I2C devices available that can make this much easier… In today’s article, I will show you how to use one of these I2C devices, the PCF8574.

These little modules have some quite impressive features, for one, allowing you to cascade up to 8 of them together, giving you a quite impressive 64 GPIO ports ! I am also happy to tell you, that if you can find the PCF8574A variant, as well, you can increase the total amount of ports to 128! ( If you chain 8x PCF8574 as well as 8x PCF8574A together) This is possible because the I2C addresses of the two series of chips are different. Thus allowing us to add a total of 16 of them to the I2C bus.

It must however be said that you should calculate your bus resistance very carefully if you plan on doing that. For most of us, I do not believe we will need that much GPIO on a single microprocessor!

Enough introduction, let us start by looking closely at the chip, as well as the modules that you can purchase for around 1 USD each…

A word of caution, there is also another version of these available, which is specifically designed to be used with LCD screens. You should thus be careful when you buy a premade module, that you choose the io-extender version, and not the LCD controller version.

PCF8574 I2C IO Extender module – Front View
PCF8574 I2C IO Extender – Back View

As we can see, the GPIO ports are clearly labeled, from P0 to P7, with the INT (Interrupt Pin) on the very right.

As I have said before, you can cascade up to 8 of these onto the I2C bus. This is done by setting the I2C address of the module. This is done by setting the jumpers as seen in the picture below.

Address Jumpers on the PCF8574 I2C IO Extender Module

The Address can be set by using the following table to lookup the address and set the jumpers accordingly.

A2A1A0I2C Device Address
0000x20
0010x21
0100x22
0110x23
1000x24
1010x25
1100x26
1110x27
Available I2C Addresses for PCF8574 selected by setting the jumpers

Connecting the device is very easy. You only have to supply 5v and Ground, as well as connect it to the SCL and SDA Pins on your microprocessor. For Arduino Uno / Nano that is A4 (SDA) and A5 (SCL)

As far as the coding is concerned, you have two options. You can either use the built-in Wire library, or you can download a special library. Both works equally well, but I do believe that the built-in Wire library might be a little bit faster.

Another point to make is that there are a lot of “fake” modules on the market these days. These modules work, but some of them have extremely weak current sourcing abilities. ( I recently bought a pair online, and they are unable to properly light an LED even without a current limiting resistor. I fixed that issue by driving the LED through a small BJT transistor, like the 2n2222a.

You should also take note that the ports will start up in a weak HIGH state when the module is powered up. This should be taken into consideration when designing your circuit to drive external devices through the outputs. In other words, you should take precautions to prevent the devices from switching on before the microprocessor takes control of the module.

Let us start to look at the coding that you will need to do to use this device.
I will start with the built-in wire Library that is included with the Arduino IDE.

#include <Wire.h> // Wire.h provide access to I2C functions

void setup()
{
Wire.begin(); // Start I2C
}

void loop()
{

Wire.beginTransmission(0x20); // Our device is on Address 0x20
Wire.write(0x0F); // This is equal to 0b00001111, meaning it will switch ports P0 to P3 High
Wire.endTransmission();
delay(1000);
Wire.beginTransmission(0x20);
Wire.write(0xF0); // This is equal to 0b11110000, meaning it will switch ports P4 to P7 High
Wire.endTransmission();
delay(1000);
}

The code above will alternate between switching 4 ports high and low every one second.
You can observe this by connecting 8 LEDs through 330ohm resistors to ports P0 through P7



Reading the status of a port (meaning that you configured it as an input) can be done using the following code

#include<Wire.h>

void setup()
{
  Serial.begin(9600);
  Wire.begin();
  Wire.beginTransmission(0x20);
  Wire.write(0x00);  //LED1 is OFF
  Wire.endTransmission();
}

void loop()
{
  Wire.requestFrom(0x20, 4); // Read the state of P4
  byte x = Wire.read();
  if (bitRead(x, 4) == LOW)
  {
    Wire.beginTransmission(0x20);
    Wire.write(0x01);  //LED1 is ON
    Wire.endTransmission();
  }
  else
  {
    Wire.beginTransmission(0x20);
    Wire.write(0x00);  //LED1 is OFF
    Wire.endTransmission();
  }
  delay(1000);
}

This code assumes that you have connected a LED throught a resistor to P0, and that you have connected a pullup resistor of 10k to P4, with a pushbutton to GROUND.

The LED should switch on when you press the switch, and go off again once you release it.

If you want to use the special library, you can download it below:

Install this into your Arduino Libraries, and use the following code:

include “Arduino.h”

include “PCF8574.h

// Set i2c address
PCF8574 pcf8574(0x20);

void setup()
{
Serial.begin(115200);

// Set pinMode to OUTPUT

pcf8574.pinMode(P0, OUTPUT);

pcf8574.begin();

}

void loop()
{
pcf8574.digitalWrite(P0, HIGH);
delay(1000);
pcf8574.digitalWrite(P0, LOW);
delay(1000);
}

Reading the status of an Input can be done like this:

include “Arduino.h”

include “PCF8574.h”

// Set i2c address
PCF8574 pcf8574(0x20);

void setup()
{
Serial.begin(115200);

pcf8574.pinMode(P0, OUTPUT);
pcf8574.pinMode(P1, INPUT);
pcf8574.begin();

}

void loop()
{
uint8_t val = pcf8574.digitalRead(P1);
if (val==HIGH) Serial.println(“KEY PRESSED”);
delay(50);

}

There are also excellent examples included with the library. These include using the interrupt pin.

I hope that this will be useful to somebody.