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.

Using I2C with a 4×4 Matrix Keypad

Using a matrix keypad is a very easy way to add multiple control buttons to a project, be it to enter a password, or to control different devices. These keypads do unfortunately have some serious flaws (in my view anyway)

1) They are usually of extremely low quality ( especially some of the membrane types from China). This means they dont last very long.
2) A typical 4×4 Matrix keypad will require 8 of your precious IO pins for itself.

These two flaws can however easily be solved, if we use a bit of technology, and are willing to to a bit of simple circuit construction by ourselves.

What does this mean ? Most of us makers will inevitably have a piece of proto-board or strip-board lying around, as well as a few momentary push-button switches. These can easily be used to make out own, much more reliable keypad. Let us look at the circuit

Circuit diagram for a 4×4 Matrix Keypad

As we can see, to build a 4×4 matrix keypad, we will need 16 momentary switches. These are connected together as shown above. You can then interface it with your favourite micro-controller to read the key(s) pressed…

This definitely solves the first of my problems, but we still need 8 pins to control this keypad… or do we? No, we don’t, we need only 2 pins. That is to say if we use one of those PCF8574 I2C IO port expander modules. They are much more reliable, as well as quite cheap as well. all depending on where you buy them from, and how long you are willing to wait for shipping 🙂

Let us see how to connect the keypad to the I2C Module

a 4×4 Membrane Matrix Keypad with PCF8574 I2C port expander module
Connecting the two together, note that we do not connect the INT pin
Connect Power (VCC, GND and I2C lines
Connect to Arduino or your preferred microcontroller. We have used Arduino Uno, Note that you can also connect the I2C to A4 (SDA) and A5(SCL) if you prefer.

Now, we need to install some libraries

The first one is the actual Keypad library, you can download it from the link below

The second library that we will need, is the keypad_i2c library, once again, download it from the link below.

Coding the keypad



#include <Key.h>
#include <Keypad.h>
#include <Keypad_I2C.h>

#define I2CADDR 0x26 // Set the Address of the PCF8574

const byte ROWS = 4; // Set the number of Rows
const byte COLS = 4; // Set the number of Columns

// Set the Key at Use (4x4)
char keys [ROWS] [COLS] = {
  {'1', '2', '3', 'A'},
  {'4', '5', '6', 'B'},
  {'7', '8', '9', 'C'},
  {'*', '0', '#', 'D'}
};

// define active Pin (4x4)
byte rowPins [ROWS] = {0, 1, 2, 3}; // Connect to Keyboard Row Pin
byte colPins [COLS] = {4, 5, 6, 7}; // Connect to Pin column of keypad.

// makeKeymap (keys): Define Keymap
// rowPins:Set Pin to Keyboard Row
// colPins: Set Pin Column of Keypad
// ROWS: Set Number of Rows.
// COLS: Set the number of Columns
// I2CADDR: Set the Address for i2C
// PCF8574: Set the number IC
Keypad_I2C keypad (makeKeymap (keys), rowPins, colPins, ROWS, COLS, I2CADDR, PCF8574);

void setup () {
  Wire .begin (); // Call the connection Wire
  keypad.begin (makeKeymap (keys)); // Call the connection
  Serial.begin (9600);

}
void loop () {
 
  char key = keypad.getKey (); // Create a variable named key of type char to hold the characters pressed
 
  if (key) {// if the key variable contains
    Serial.println (key); // output characters from Serial Monitor
  }
}

Upload this to your Arduino device and enjoy. This sketch can also be adapted for 1×4, and 4×3 keypads, and with a little modification, will also work perfectly on ESP32 or ESP8266 as well…

The OLED Display

Introduction

Adding a display to any project can instantly increase its visual appeal, as well as make the project easier to control. Displays available to Electronic enthusiasts mostly include some sort of LCD or even TFT display. LCD displays are usually bulky and very limited in their ability to display a lot of information, whereas TFT type displays are still a bit on the expensive side, and not very easy to interface with for the beginner.

Today, I would like to introduce a different type of display, which is available in an I2C as well as SPI version. These displays are very easily readable in almost any light conditions, lightweight, and most importantly, they are extremely cheap. I am talking about the OLED display of course… Many of us may already have one of them in our mobile phones, or even TV screen…

128×32 I2C OLED Display (40mmx10mm) [0.91″] Front view

Some Technical Data

An organic light-emitting diode (OLED or Organic LED), also known as an organic EL (organic electroluminescent) diode,[1][2] is a light-emitting diode (LED) in which the emissive electroluminescent layer is a film of organic compound that emits light in response to an electric current. This organic layer is situated between two electrodes; typically, at least one of these electrodes is transparent. OLEDs are used to create digital displays in devices such as television screens, computer monitors, portable systems such as smartphoneshandheld game consoles and PDAs. A major area of research is the development of white OLED devices for use in solid-state lighting applications.[3][4][5]

There are two main families of OLED: those based on small molecules and those employing polymers. Adding mobile ions to an OLED creates a light-emitting electrochemical cell (LEC) which has a slightly different mode of operation. An OLED display can be driven with a passive-matrix (PMOLED) or active-matrix (AMOLED) control scheme. In the PMOLED scheme, each row (and line) in the display is controlled sequentially, one by one,[6] whereas AMOLED control uses a thin-film transistor backplane to directly access and switch each individual pixel on or off, allowing for higher resolution and larger display sizes.

An OLED display works without a backlight because it emits visible light. Thus, it can display deep black levels and can be thinner and lighter than a liquid crystal display (LCD). In low ambient light conditions (such as a dark room), an OLED screen can achieve a higher contrast ratio than an LCD, regardless of whether the LCD uses cold cathode fluorescent lamps or an LED backlight. OLED displays are made in the same way as LCDs, but after TFT (for active matrix displays), addressable grid (for passive matrix displays) or ITO segment (for segment displays) formation, the display is coated with hole injection, transport and blocking layers, as well with electroluminescent material after the 2 first layers, after which ITO or metal may be applied again as a cathode and later the entire stack of materials is encapsulated. The TFT layer, addressable grid or ITO segments serve as or are connected to the anode, which may be made of ITO or metal.[7][8] OLEDs can be made flexible and transparent, with transparent displays being used in smartphones with optical fingerprint scanners and flexible displays being used in foldable smartphones.

The full article is available here if you are interested.

128×32 I2C OLED Display (40mmx10mm) [0.91″] Back view

Connecting the circuit

This display is once again extremely easy to connect, as it uses the very versatile I2C protocol. (An SPI version is also available).

Connecting 128×32 OLED display to an Arduino Uno Clone

Connect the following wires to the Arduino / ESP32
+5v (red) to the VCC pin on the display
Gnd to Gnd
SDA (A4 on Uno) to SDA, and SCL (A5 on Uno) to SCL

The Software Libraries

The 128×32 OLED display that we will be using today, is based on the SSD1306. We will thus be using a library suplied by Adafruit to interface with this chip. There are various other libraries available, but I have found the Adafruit library the most stable.

To load this, start by opening the Arduino IDE, and go to the Sketch->Include Library->Manage Libraries option on the menu

The Library Manager will now open

We need to install two (2) Libraries

– Adafruit GFX ( this is for graphics)
– Adafruit SSD1306 ( to control the actual display )

Click on “Close” after installation is completed.

Using the display

We will use one of the standard Adafruit examples to show you the capabilities of the tiny little screen. The example are so straight forward to use, that I find it unnecessary to say anything else about it 🙂

Open the ssd1306_128x32_ic2 Example from the Examples menu in the Arduino IDE and upload it to your Arduino, making sure that you set the dimensions of your screen first (in my case 128×32 )

/**************************************************************************
 This is an example for our Monochrome OLEDs based on SSD1306 drivers

 Pick one up today in the adafruit shop!
 ------> http://www.adafruit.com/category/63_98

 This example is for a 128x32 pixel display using I2C to communicate
 3 pins are required to interface (two I2C and one reset).

 Adafruit invests time and resources providing this open
 source code, please support Adafruit and open-source
 hardware by purchasing products from Adafruit!

 Written by Limor Fried/Ladyada for Adafruit Industries,
 with contributions from the open source community.
 BSD license, check license.txt for more information
 All text above, and the splash screen below must be
 included in any redistribution.
 **************************************************************************/

#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 32 // OLED display height, in pixels

// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
#define OLED_RESET     4 // Reset pin # (or -1 if sharing Arduino reset pin)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

#define NUMFLAKES     10 // Number of snowflakes in the animation example

#define LOGO_HEIGHT   16
#define LOGO_WIDTH    16
static const unsigned char PROGMEM logo_bmp[] =
{ B00000000, B11000000,
  B00000001, B11000000,
  B00000001, B11000000,
  B00000011, B11100000,
  B11110011, B11100000,
  B11111110, B11111000,
  B01111110, B11111111,
  B00110011, B10011111,
  B00011111, B11111100,
  B00001101, B01110000,
  B00011011, B10100000,
  B00111111, B11100000,
  B00111111, B11110000,
  B01111100, B11110000,
  B01110000, B01110000,
  B00000000, B00110000 };

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

  // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3C for 128x32
    Serial.println(F("SSD1306 allocation failed"));
    for(;;); // Don't proceed, loop forever
  }

  // Show initial display buffer contents on the screen --
  // the library initializes this with an Adafruit splash screen.
  display.display();
  delay(2000); // Pause for 2 seconds

  // Clear the buffer
  display.clearDisplay();

  // Draw a single pixel in white
  display.drawPixel(10, 10, SSD1306_WHITE);

  // Show the display buffer on the screen. You MUST call display() after
  // drawing commands to make them visible on screen!
  display.display();
  delay(2000);
  // display.display() is NOT necessary after every single drawing command,
  // unless that's what you want...rather, you can batch up a bunch of
  // drawing operations and then update the screen all at once by calling
  // display.display(). These examples demonstrate both approaches...

  testdrawline();      // Draw many lines

  testdrawrect();      // Draw rectangles (outlines)

  testfillrect();      // Draw rectangles (filled)

  testdrawcircle();    // Draw circles (outlines)

  testfillcircle();    // Draw circles (filled)

  testdrawroundrect(); // Draw rounded rectangles (outlines)

  testfillroundrect(); // Draw rounded rectangles (filled)

  testdrawtriangle();  // Draw triangles (outlines)

  testfilltriangle();  // Draw triangles (filled)

  testdrawchar();      // Draw characters of the default font

  testdrawstyles();    // Draw 'stylized' characters

  testscrolltext();    // Draw scrolling text

  testdrawbitmap();    // Draw a small bitmap image

  // Invert and restore display, pausing in-between
  display.invertDisplay(true);
  delay(1000);
  display.invertDisplay(false);
  delay(1000);

  testanimate(logo_bmp, LOGO_WIDTH, LOGO_HEIGHT); // Animate bitmaps
}

void loop() {
}

void testdrawline() {
  int16_t i;

  display.clearDisplay(); // Clear display buffer

  for(i=0; i<display.width(); i+=4) {
    display.drawLine(0, 0, i, display.height()-1, SSD1306_WHITE);
    display.display(); // Update screen with each newly-drawn line
    delay(1);
  }
  for(i=0; i<display.height(); i+=4) {
    display.drawLine(0, 0, display.width()-1, i, SSD1306_WHITE);
    display.display();
    delay(1);
  }
  delay(250);

  display.clearDisplay();

  for(i=0; i<display.width(); i+=4) {
    display.drawLine(0, display.height()-1, i, 0, SSD1306_WHITE);
    display.display();
    delay(1);
  }
  for(i=display.height()-1; i>=0; i-=4) {
    display.drawLine(0, display.height()-1, display.width()-1, i, SSD1306_WHITE);
    display.display();
    delay(1);
  }
  delay(250);

  display.clearDisplay();

  for(i=display.width()-1; i>=0; i-=4) {
    display.drawLine(display.width()-1, display.height()-1, i, 0, SSD1306_WHITE);
    display.display();
    delay(1);
  }
  for(i=display.height()-1; i>=0; i-=4) {
    display.drawLine(display.width()-1, display.height()-1, 0, i, SSD1306_WHITE);
    display.display();
    delay(1);
  }
  delay(250);

  display.clearDisplay();

  for(i=0; i<display.height(); i+=4) {
    display.drawLine(display.width()-1, 0, 0, i, SSD1306_WHITE);
    display.display();
    delay(1);
  }
  for(i=0; i<display.width(); i+=4) {
    display.drawLine(display.width()-1, 0, i, display.height()-1, SSD1306_WHITE);
    display.display();
    delay(1);
  }

  delay(2000); // Pause for 2 seconds
}

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

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

  delay(2000);
}

void testfillrect(void) {
  display.clearDisplay();

  for(int16_t i=0; i<display.height()/2; i+=3) {
    // The INVERSE color is used so rectangles alternate white/black
    display.fillRect(i, i, display.width()-i*2, display.height()-i*2, SSD1306_INVERSE);
    display.display(); // Update screen with each newly-drawn rectangle
    delay(1);
  }

  delay(2000);
}

void testdrawcircle(void) {
  display.clearDisplay();

  for(int16_t i=0; i<max(display.width(),display.height())/2; i+=2) {
    display.drawCircle(display.width()/2, display.height()/2, i, SSD1306_WHITE);
    display.display();
    delay(1);
  }

  delay(2000);
}

void testfillcircle(void) {
  display.clearDisplay();

  for(int16_t i=max(display.width(),display.height())/2; i>0; i-=3) {
    // The INVERSE color is used so circles alternate white/black
    display.fillCircle(display.width() / 2, display.height() / 2, i, SSD1306_INVERSE);
    display.display(); // Update screen with each newly-drawn circle
    delay(1);
  }

  delay(2000);
}

void testdrawroundrect(void) {
  display.clearDisplay();

  for(int16_t i=0; i<display.height()/2-2; i+=2) {
    display.drawRoundRect(i, i, display.width()-2*i, display.height()-2*i,
      display.height()/4, SSD1306_WHITE);
    display.display();
    delay(1);
  }

  delay(2000);
}

void testfillroundrect(void) {
  display.clearDisplay();

  for(int16_t i=0; i<display.height()/2-2; i+=2) {
    // The INVERSE color is used so round-rects alternate white/black
    display.fillRoundRect(i, i, display.width()-2*i, display.height()-2*i,
      display.height()/4, SSD1306_INVERSE);
    display.display();
    delay(1);
  }

  delay(2000);
}

void testdrawtriangle(void) {
  display.clearDisplay();

  for(int16_t i=0; i<max(display.width(),display.height())/2; i+=5) {
    display.drawTriangle(
      display.width()/2  , display.height()/2-i,
      display.width()/2-i, display.height()/2+i,
      display.width()/2+i, display.height()/2+i, SSD1306_WHITE);
    display.display();
    delay(1);
  }

  delay(2000);
}

void testfilltriangle(void) {
  display.clearDisplay();

  for(int16_t i=max(display.width(),display.height())/2; i>0; i-=5) {
    // The INVERSE color is used so triangles alternate white/black
    display.fillTriangle(
      display.width()/2  , display.height()/2-i,
      display.width()/2-i, display.height()/2+i,
      display.width()/2+i, display.height()/2+i, SSD1306_INVERSE);
    display.display();
    delay(1);
  }

  delay(2000);
}

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

  display.setTextSize(1);      // Normal 1:1 pixel scale
  display.setTextColor(SSD1306_WHITE); // Draw white text
  display.setCursor(0, 0);     // Start at top-left corner
  display.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') display.write(' ');
    else          display.write(i);
  }

  display.display();
  delay(2000);
}

void testdrawstyles(void) {
  display.clearDisplay();

  display.setTextSize(1);             // Normal 1:1 pixel scale
  display.setTextColor(SSD1306_WHITE);        // Draw white text
  display.setCursor(0,0);             // Start at top-left corner
  display.println(F("Hello, world!"));

  display.setTextColor(SSD1306_BLACK, SSD1306_WHITE); // Draw 'inverse' text
  display.println(3.141592);

  display.setTextSize(2);             // Draw 2X-scale text
  display.setTextColor(SSD1306_WHITE);
  display.print(F("0x")); display.println(0xDEADBEEF, HEX);

  display.display();
  delay(2000);
}

void testscrolltext(void) {
  display.clearDisplay();

  display.setTextSize(2); // Draw 2X-scale text
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(10, 0);
  display.println(F("scroll"));
  display.display();      // Show initial text
  delay(100);

  // Scroll in various directions, pausing in-between:
  display.startscrollright(0x00, 0x0F);
  delay(2000);
  display.stopscroll();
  delay(1000);
  display.startscrollleft(0x00, 0x0F);
  delay(2000);
  display.stopscroll();
  delay(1000);
  display.startscrolldiagright(0x00, 0x07);
  delay(2000);
  display.startscrolldiagleft(0x00, 0x07);
  delay(2000);
  display.stopscroll();
  delay(1000);
}

void testdrawbitmap(void) {
  display.clearDisplay();

  display.drawBitmap(
    (display.width()  - LOGO_WIDTH ) / 2,
    (display.height() - LOGO_HEIGHT) / 2,
    logo_bmp, LOGO_WIDTH, LOGO_HEIGHT, 1);
  display.display();
  delay(1000);
}

#define XPOS   0 // Indexes into the 'icons' array in function below
#define YPOS   1
#define DELTAY 2

void testanimate(const uint8_t *bitmap, uint8_t w, uint8_t h) {
  int8_t f, icons[NUMFLAKES][3];

  // Initialize 'snowflake' positions
  for(f=0; f< NUMFLAKES; f++) {
    icons[f][XPOS]   = random(1 - LOGO_WIDTH, display.width());
    icons[f][YPOS]   = -LOGO_HEIGHT;
    icons[f][DELTAY] = random(1, 6);
    Serial.print(F("x: "));
    Serial.print(icons[f][XPOS], DEC);
    Serial.print(F(" y: "));
    Serial.print(icons[f][YPOS], DEC);
    Serial.print(F(" dy: "));
    Serial.println(icons[f][DELTAY], DEC);
  }

  for(;;) { // Loop forever...
    display.clearDisplay(); // Clear the display buffer

    // Draw each snowflake:
    for(f=0; f< NUMFLAKES; f++) {
      display.drawBitmap(icons[f][XPOS], icons[f][YPOS], bitmap, w, h, SSD1306_WHITE);
    }

    display.display(); // Show the display buffer on the screen
    delay(200);        // Pause for 1/10 second

    // Then update coordinates of each flake...
    for(f=0; f< NUMFLAKES; f++) {
      icons[f][YPOS] += icons[f][DELTAY];
      // If snowflake is off the bottom of the screen...
      if (icons[f][YPOS] >= display.height()) {
        // Reinitialize to a random position, just off the top
        icons[f][XPOS]   = random(1 - LOGO_WIDTH, display.width());
        icons[f][YPOS]   = -LOGO_HEIGHT;
        icons[f][DELTAY] = random(1, 6);
      }
    }
  }
}

I hope that you find this useful and inspiring.
Thank you

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

What exactly is I2C?

In this post, I will tell you all the basics of the I2C protocol. What it is, where it comes from and also how it is configured and setup. We will also look at how data is transferred and received

Table of contents
1. Introduction
2. The Features of I2C
3. The Hardware
3.1 The physical I2C Bus
3.2 The Master and Slave devices on the bus
4. The data transfer protocol
4.1 The Start Condition
4.2 The Address Block
4.3 The Read/Write Bit
4.4 The ACK/NACK Bit
4.5 The Data Block
4.6 The Stop Condition
5. How does I2C work in practice
5.1 Sending data to a Slave Device
5.2 Reading data from a Slave Device
5.3 The Clock stretching concept

Introduction

I2C communication is the short form name for inter-integrated circuit protocol. It is a communication protocol developed by Philips Semiconductors for the transfer of data between a central processor and multiple integrated circuits on the same circuit board by using just two common wires.

Due to its simplicity, it is widely adopted for communication between microcontrollers and sensor arrays, displays, IoT devices, EEPROMs etc.

This is a synchronous serial communication protocol. It means that data bits are transferred one by one at regular intervals of time set by a reference clock line.

The Features of I2C

The I2C protocol has the following important features

  • Only two common bus lines (wires) are required to control any device/IC on the I2C network.
  • There is no need for a prior agreement on data transfer rate like in UART communications. The data transfer speed can thus be adjusted whenever it is required.
  • It has a simple mechanism for validating the transferred data.
  • It uses a 7-bit addressing system to target a specific device/IC on the I2C bus.
  • I2C networks are extremely easy to scale. New devices can simply be connected to the two common I2C bus lines.

The Hardware

The physical I2C Bus

The I2C Bus (Interface wires) consists of just two wires and are named Serial Clock Line (SCL) and Serial Data Line (SDA). The data to be transferred is sent through the SDA wire and is synchronized with the clock signal from SCL. All the devices/ICs on the I2C network are connected to the same SCL and SDA lines as shown in the image below:

The physical I2C Bus. All devices are connected to the same 2 wired on the bus, namely SDA and SCL

Both the I2C bus lines (SDA, SCL) are operated as in open-drain driver mode. It means that any device/IC on the I2C network can drive(pull) SDA and SCL low, but they cannot drive them high. So, a pull-up resistor is used on each bus line, to keep them high (at positive voltage) by default.

This is to prevent the bus from shorting, which might happen when one device tries to pull the line high and some other device tries to pull the line low.

The Master and Slave Devices on the I2C Bus

The devices connected to the I2C bus are categorized as either masters or slaves. At any instant of time, only a single master stays active on the I2C bus. It controls the SCL clock line and decides what operation is to be done on the SDA data line.

All the devices that respond to instructions from this master device are slaves. For differentiating between multiple slave devices connected to the same I2C bus, each slave device is physically assigned a permanent 7-bit address.

When a master device wants to transfer data to or from a slave device, it specifies this particular slave device address on the SDA line and then proceeds with the transfer. So effectively communication takes place between the master device and a particular slave device.

All the other slave devices don’t respond unless their address is specified by the master device on the SDA line.

The Master and Slave Devices on the I2C Bus. Note that each Slave device has it’s own address.

The Data Transfer Protocol

The protocol (set of rules) that is followed by the master device and slave devices for the transfer of data between them works as follows:

Data is transferred between the master device and slave devices through the SDA data line, via patterned sequences of 0’s and 1’s (bits). Each sequence of 0’s and 1’s is called a transaction and each data transaction is structured as in the image below:

The structure of an I2C Data transaction

The Start Condition

Whenever a master device/IC decides to start a transaction, it switches the SDA line from a high level to a low level before the SCL line switches from high to low.

Once a start condition is sent by the master device, all the slave devices get active even if they were in sleep mode, and wait for the address bits to see which device should respond.

The I2C Start Condition. Note that SDA Switches LOW before SCL. All slave devices on the bus will now listen for an address bit to decide which device should respond.

The Address Block

The Address block is comprised of 7 bits and are filled with the address of slave device (in binary) to/from which the master device needs to send/receive data. All the slave devices on the I2C bus will compare these address bits with their own address.

The Read/Write Bit

This bit specifies the direction that the data must be transferred in. If the master device/IC needs to send data to a slave device, this bit is set to ‘0’. If the master device/IC needs to receive data from the slave device, it is set to ‘1’.

The ACK/NACK Bit

This is the Acknowledged/Not-Acknowledged bit. If the physical address of any slave device is the same as the address that was broadcasted by the master device, that slave device will set the value of this bit to ‘0’ . If there are no slave device(s) with the broadcasted address, this bit will remain at logic ‘1’ (default). This will tell the master that the data/command has been received and/or acknowledged by a slave device.

The Data Block

The data block is comprised of 8 bits and they are set by the transmitter,wheather this be the master or the slave, depending on wheather a read or a write operation was requested, with the data bits that needs to transfered to the receiver. This block is followed by an ACK/NACK bit that is set to ‘0’ by the receiver if it successfully receives data. Otherwise it stays at logic ‘1’.

This combination of data blocks followed by an ACK/NACK bit is repeated until all the data is completely transferred.

The Stop Condition

After all the required data blocks are transferred through the SDA line, the master device switches the SDA line from low to high before the SCL line switches back from high to low.

The I2C Stop condition. This signals the end of a transaction. Note SDA returns to High BEFORE the SCL line is pulled High.

How does I2C work in practice

When an I2C transaction is initiated by a master device either to send or receive data to/from a slave device, all of the processes mentioned above will happen at least one.
Let us look at a typical scenario for each of the different type of scenarios.

Sending Data to a Slave Device

The following sequence of operations will take place when a master device tries to send data to a particular slave device through I2C bus:

  • The master device sends a start condition
  • The master device sends the 7 address bits which correspond to the slave device to be targeted
  • The master device sets the Read/Write bit to ‘0’, which signifies a write
  • Now two scenarios are possible:
    • If no slave device matches with the address sent by the master device, the next ACK/NACK bit stays at ‘1’ (default). This signals the master device that the slave device identification is unsuccessful. The master clock will end the current transaction by sending a Stop condition or a new Start condition
    • If a slave device exists with the same address as the one specified by the master device, the slave device sets the ACK/NACK bit to ‘0’, which signals the master device that a slave device is successfully targeted
  • If a slave device is successfully targeted, the master device now sends 8 bits of data which is only considered and received by the targeted slave device. This data means nothing to the remaining slave devices
  • If the data is successfully received by the slave device, it sets the ACK/NACK bit to ‘0’, which signals the master device to continue
  • The previous two steps are repeated until all the data is transferred
  • After all the data is sent to the slave device, the master device sends the Stop condition which signals all the slave devices that the current transaction has ended

The image below represents the transaction with the data bits sent on the SDA line and the device that controls each of them:

I2C Master sending data to a slave device

Reading Data from a Slave Device

The sequence of operations remain the same as in previous scenario except for the following:

  • The master device sets the Read/Write bit to ‘1’ instead of ‘0’ which signals the targeted slave device that the master device is expecting data from it
  • The 8 bits corresponding to the data block are sent by the slave device and the ACK/NACK bit is set by the master device
  • Once the required data is received by the master device, it sends a NACK bit. Then the slave device stops sending data and releases the SDA line

If the master device to read data from specific internal location of a slave device, it first sends the location data to the slave device using the steps in previous scenario. It then starts the process of reading data with a repeated start condition.

The below figure represents the overall data bits sent on the SDA line and the device that controls each of them:

Reading data from a Slave device on the I2C bus

The Clock Stretching concept

Let say the master device started a transaction and sent address bits of a particular slave device followed by a Read bit of ‘1’. The specific slave device needs to send an ACK bit, immediately followed by data.

But if the slave device needs some time to fetch and send data to master device, during this gap, the master device will think that the slave device is sending some data.

To prevent this, the slave device holds the SCL clock line low until it is ready to transfer data bits. By doing this, the slave device signals the master device to wait for data bits until the clock line is released

Conclusion

This concludes this tutorial. In a future post, I will show you how to use I2C to transfer data between two micro-controllers.