8×7 LED Array and MCP23017

December 7, 2018 at 8:21 pm (maker) (, , , , , , )

I can’t resist playing with an LED array, especially a surface mount one, so when I stumbled across some cheap (<$2) surface mount 8×7 LED arrays, charlieplexed onto 8 IO pins on ebay, I had to grab some.

Now I should say, I can’t really imagine why anyone would bother doing what I’m about to describe, as you already have many other options, including:

But as I say, I can’t resist a surface mount LED board, so I started to play.  When these arrived and I started poking around, I soon found out they were actually a cheap knock-off of the Sparkfun 8×7 LED Array, something I didn’t even know existed!  Apologies to Sparkfun for not getting an official board, but many thanks to them for open sourcing the design.

Initially they are fairly easy to hook-up and drive.  I used an Arduino Uno for my first experiments, as they could be paired straight away with pins 2-9 and then driven directly using the Arduino Charlieplex library (the Sparkfun Tutorial uses the Chaplex library and a special Sparkfun library for the board, but I was just doing fairly simple things for now).

With the neat serial interface to the Pi-Lite in the back of my mind, I wanted a better way to connect to the board.  Ideally, something that could be soldered onto the back to make it a single unit.  Some options I explored were:

  • Using an IO expander – such as the MCP23017 (the subject of this post).
  • Using an Arduino Mini Pro – to provide a serial interface (bit more on that later).
  • Using an LED driver module.

For this first experiment, I thought having an I2C addressable LED module would be quite neat (I didn’t know about the IS31FL3731 used on the Adafruit and Pimoroni boards at this time – that is an experiment for another day), so I grabbed some cheap MCP23017 breakout boards and after breadboarding it, ended up simply soldering one on the back.  It overlaps ever so slightly, but I think I could still place a few of these side by side to chain them.

2018-12-01-16-06-19.jpg2018-12-01-16-04-34.jpg2018-12-01-16-04-20.jpg

Link-up wise, this ties the 8 IO pins of the matrix to port A pins 0 to 7 on the MCP23017.  The 7 pins with header connections are (in order top to bottom):

  • VCC
  • GND
  • SCL/SCK
  • SDA/SI
  • NC/CS
  • NC/SO
  • RESET

And the three connections with no header pins are the three address encoding pins.  As I’m using I2C I only need four connections: VCC, GND, SCL and SDA (I could have left off the other three header pins really).

In terms of the code, there are libraries for the MCP23017, Charlieplexing and even the aforementioned Sparkfun library for this module, but to link them all together is quite inefficient.  The simple approach would be to replace the low-level pin setting/clearing code in the charlieplex library with commands for the MCP23017.  But as every setting of a single LED, even when triggered as part of a ‘scan’ managing a display buffer, requires I2C commands to be sent over the bus, and each command requires bit manipulation to process the IO pins individually, this is massively inefficient.

However, the MCP23017 is really only be driven by two registers: GPIOA to set the IO pins HIGH or LOW, and IODIRA to set the pins to INPUT or OUTPUT.  Recall that for Charlieplexing, all three states of pins are significant: INPUT, OUTPUT+HIGH, OUTPUT+LOW.

To work out which pins do what, we need to refer to the wiring layout of the LEDs on the board, for which the Sparkfun schematic is key (my thanks again for the open source design).  Here is an annotated version showing which IO pins (numbered 1 to 8 on the schematic) are connected to which LED in the matrix.

2018-12-07-19-29-34.jpg

The key thing to notice is how for every column listed, there is, naturally, a gap in the numbering of the rows (you can’t have an LED connected with both leads to the same IO pin).

So a more efficient scanning algorithm would thus run something like this:

// IODIRA = INPUT (1) or OUTPUT (0)
// GPIOA  = HIGH (1) or LOW (0)

FOREACH col (0 to 7)  // scan one column at a time
  gpiomask = (1<<col) // only pin that is HIGH in the col pin
  dirmask = 0         // default to all OUTPUTS
  FOREACH iopin (0 to 7)
    led = iopin
    IF (iopin == col) SKIP
    IF (iopin > col) THEN
       // led we want is one less than iopin due to charlieplex mapping
    IF (led for col is OFF) THEN
       dirmask |= (1<<iopin)  // set this one to an INPUT i.e. OFF
  NEXT iopin
  IODIRA = dirmask
  GPIOA  = gpiomask
NEXT col

The basic principle being that for a single scan, set one pin HIGH/OUTPUT (for one column in the matrix) and then work out which other LEDs need to be active by setting the direction of the pins accordingly – LOW/OUTPUT will light the LED for that pin combination; INPUT will mean the led for that combination is off.  Then on the next scan, do the same for the next column.

The following is some basic code, based on a whole range of sources (and to be honest, not particularly well structured in terms of functional separation) to provide an I2C optimised scanning function for the MCP23017 and LED array combination.

Usual caveats apply – I’m not really a cpp person, use at your own risk, etc, etc.

Key gotchas from doing this:

  • You can set the I2C bus to run at a higher speed, which is well worth doing in this case.  Note that I don’t know what happens if you have lots of things on the bus – this was working with just this LED array and nothing else.  See Wire.SetClock() in the Arduino Wire library.
  • You can’t do I2C from an interrupt routine.  I wanted the scanning routine to run off a timer interrupt, but nothing works if you try to do that – in fact, everything actually completely locks up.

So that’s it for now.  It scans quite well and with not a bad refresh rate, but as I said right at the start, with so many options of LED array available, not quite sure why anyone would want to do this.

Next, I’ll see if I can get the Pi-Lite firmware onto an Arduino Pro Mini and solder that on the back of one of these too.  That will create a standalone, drivable over serial port, module similar to the original Pi-Lite (ish).  Not really sure why anyone would want to do that either.

Kevin

I2CCharlieExample.ino

/*
 * Charlieplex Example for the Sparkfun LED Array board connected
 * to an MCP23017 IO expander chip and accessed via I2C.
 */
#include "I2CCharlieplex.h"
#include "Wire.h" // required for I2CCharlieplex.h

#define MCPADDR  0   // 0 to 7 for 0x20 to 0x27 for MCP23017 expander

// Define the pins in the order you want to address them.
//
// Note: for the MCP23017, pin numbers are as follows:
//         Port A pins 0 to 7 = pins 0 to 7
//         Port B pins 0 to 7 = pins 8 to 15
//
#define NUMBER_OF_PINS 8
byte pins[] = {0,1,2,3,4,5,6,7};

I2CCharlieplex charlieplex = I2CCharlieplex(MCPADDR, pins, NUMBER_OF_PINS);

void setup(){
  charlieplex.init();
  charlieplex.clr();
  charlieplex.scan();
}

int x=0;
int y=0;
int scan=0;
int clr=0;

void loop(){
  delay(1);
  charlieplex.scan();

  // Update the LED pattern from time to time
  if (scan > 200) {
    scan = 0;

    if (clr) {
      charlieplex.clr();
      clr=0;
    } else {
      charlieplex.led (x, y, HIGH);
      x++;
      if (x>=8) {
        x=0;
        y++;
        if (y>=7) {
          y=0;
          clr=1;
        }
      }
    }
  }
  scan++;
}

I2CCharliePlex.cpp

/*
 * I2C MCP23017 I2C IO Expander Charlieplex
 * Based on elements of MCP23017 Library from Adafruit and
 * the Charlieplex library by Alexander Brevig
 *
 * Pins are numbered as follows:
 *   0-7  Port A 0-7
 *   8-15 Port B 0-7
 */

#include "I2CCharlieplex.h"
#include "Wire.h"

// Default address
#define MCP23017_ADDRESS 0x20

// registers (using same definitions as Adafruit_MCP23017.h)
// Note: MCP23017 can be configured for interleaving registers or
//       sequential, depending on the setting of IOCON[AB].BANK.
//       By default on power up IOCON.BANK = 0; i.e. interleaved
//       as defined below..
#define MCP23017_IODIRA 0x00
#define MCP23017_IPOLA 0x02
#define MCP23017_GPINTENA 0x04
#define MCP23017_DEFVALA 0x06
#define MCP23017_INTCONA 0x08
#define MCP23017_IOCONA 0x0A
#define MCP23017_GPPUA 0x0C
#define MCP23017_INTFA 0x0E
#define MCP23017_INTCAPA 0x10
#define MCP23017_GPIOA 0x12
#define MCP23017_OLATA 0x14

#define MCP23017_IODIRB 0x01
#define MCP23017_IPOLB 0x03
#define MCP23017_GPINTENB 0x05
#define MCP23017_DEFVALB 0x07
#define MCP23017_INTCONB 0x09
#define MCP23017_IOCONB 0x0B
#define MCP23017_GPPUB 0x0D
#define MCP23017_INTFB 0x0F
#define MCP23017_INTCAPB 0x11
#define MCP23017_GPIOB 0x13
#define MCP23017_OLATB 0x15

#define MCP23017_INT_ERR 255

// Address in range 0 to 7.  MCP23017 address set in the range
//  0x20 to 0x27 using the A0, A1, A2 pins
I2CCharlieplex::I2CCharlieplex(uint8_t address, byte* userPins, byte numberOfUserPins){
  pins = userPins;
  numberOfPins = numberOfUserPins;

  // Initialise the MCP23017 IO Expander using I2C
  if (address > 7) {
    addr = MCP23017_ADDRESS;  // default to first address if not sure
  } else {
    addr = MCP23017_ADDRESS + address;
  }
}

void I2CCharlieplex::init () {
  Wire.begin();  // We are the bus master

  // Note on I2C clock speed - as a synchronous comms, the clock is set
  // by the master and the slave will be triggered by it.
  // The default is 100kbit/s (standard); many devices will also support 400kbit/s (full speed).
  // Some might support 1Mbit/s (fast mode) or 3.2Mbit/s (high speed).
  // See https://www.i2c-bus.org/speed/
  //
  // 400000 bps is 400000/8 = 50 KB/s
  Wire.setClock(400000); // Use I2C in full speed mode

  // Clear all leds (set all pins to INPUT)
  mcpAllInputs();
}

// LED Buffer access routines
void I2CCharlieplex::led(uint8_t x, uint8_t y, bool state) {
  if ((x<NUMXLEDS) && (y<NUMYLEDS)) {
    if (state == HIGH) {
      leds[y] |= (1<<x);
    } else {
      leds[y] &= ~(1<<x);
    }
  }
}

void I2CCharlieplex::clr() {
  for (uint8_t i=0; i<NUMYLEDS; i++) {
    leds[i] = 0;
  }
}

// WARNING: This cannot be called from an ISR if it is using I2C
//          (everything locks up if you do)
void I2CCharlieplex::scan() {
  // Only scan one column at a time

  // Scan according to the following algorithm:
  //  IODIRA = INPUT (1) or OUTPUT (0)
  //  GPIOA  = HIGH (1) or LOW (0)
  //  
  //  FOREACH col (0 to 7)  // scan one column at a time
  //    gpiomask = (1<<col) // only pin that is HIGH in the col led
  //    dirmask = 0         // default to all OUTPUTS
  //    FOREACH iopin (0 to 7)
  //      IF (iopin == col) SKIP
  //      IF (iopin > col) THEN
  //         led we want is one less than iopin due to charlieplex mapping
  //      IF (led for col is OFF) THEN
  //         dirmask |= (1<<iopin)  // set this one to an INPUT i.e. OFF
  //    NEXT iopin
  //    IODIRA = dirmask
  //    GPIOA  = gpiomask
  //  NEXT col

  // First kill all LEDS
  mcpWrite(MCP23017_IODIRA,0xff);

  uint8_t ledmask = (1<<col);
  uint8_t dirmask = 0;
  for (uint8_t p=0; p<NUMPINS; p++) {
    uint8_t y = p;
    if (p == col) {
      // skip this one
      continue;
    } else if (p > col) {
      // LED index will be one less than pin value due to charlieplex pattern
      y--;
    }
    // check the 'col'th LED in each 'y' row to see if pin 'p' is IN or OUT
    if ((leds[y] & (1<<col)) == 0) {
      // Want IO pin to be low to illuminate the LED
      // So only set IO pin if LED is OFF
      dirmask |= (1<<p);
    }
  }
  // Always need "col" pin as an OUTPUT (low) though
  dirmask &= ~(1<<col);
  
  // Only High pin will be the col being scanned
  mcpWrite (MCP23017_GPIOA, ledmask);
  // All other pins are low, so direction will determine if they are
  // a low OUTPUT or INPUT (which will disable that LED)
  mcpWrite (MCP23017_IODIRA, dirmask);
  // next column
  col++;
  if (col >= NUMXLEDS) {
    col = 0;
  }
}

// I2C MCP23017 IO Handling
// Not: No DigitalRead as yet

void I2CCharlieplex::mcpPinMode (uint8_t p, uint8_t mode) {
  if (mode == INPUT) {
    // Set appropriate bit in register
    mcpBitUpdate(p, 1, pin2reg(p, MCP23017_IODIRA, MCP23017_IODIRB));
  } else {
    // Clear appropriate bit in register
    mcpBitUpdate(p, 0, pin2reg(p, MCP23017_IODIRA, MCP23017_IODIRB));
  }
}

void I2CCharlieplex::mcpAllInputs () {
    // Set all pins to INPUT at once
    // (makes a full clear much quicker than setting individual bits)
    mcpWrite(MCP23017_IODIRA,0xff);
    mcpWrite(MCP23017_IODIRB,0xff);
}

void I2CCharlieplex::mcpDigitalWrite (uint8_t p, uint8_t val) {
  // Need to read current I/O states from the latch,
  // set/clear the appropriate bit and
  // write it back to the GPIO register
  uint8_t io;
  io = mcpRead(pin2reg(p, MCP23017_OLATA, MCP23017_OLATB));
  bitWrite(io, pin2bit(p), val);
  mcpWrite(pin2reg(p, MCP23017_GPIOA, MCP23017_GPIOB), io);
}

//
// I2C MCP23017 Register Handling
//

void I2CCharlieplex::mcpWrite(uint8_t reg, uint8_t val) {
  // Write sequence:
  //    Send register address to write
  //    Send the byte of data
  Wire.beginTransmission(addr);
  Wire.write(reg);
  Wire.write(val);
  Wire.endTransmission();
}

uint8_t I2CCharlieplex::mcpRead(uint8_t reg) {
  // Read sequence:
  //    Send register address to read
  //    Request 1 byte of data from the device
  //    Read the byte of data
  Wire.beginTransmission(addr);
  Wire.write(reg);
  Wire.endTransmission();
  Wire.requestFrom(addr, (uint8_t)1);
  return Wire.read();
}

void I2CCharlieplex::mcpBitUpdate (uint8_t bit, uint8_t val, uint8_t reg) {
  uint8_t regval;
  regval = mcpRead(reg);
  bitWrite(regval,bit, val);
  mcpWrite(reg, regval);
}

// Register and pin helper functions
uint8_t I2CCharlieplex::pin2bit(uint8_t pin){
  // Returns bit 0 to 7 for range of pins 0 to 7 and pins 8 to 15
  return pin%8;
}
uint8_t I2CCharlieplex::pin2reg(uint8_t pin, uint8_t portAaddr, uint8_t portBaddr){
  if (pin < 8) {
    return portAaddr;
  } else {
    return portBaddr;
  }
}

I2CCharlieplex.h

#ifndef I2CCHARLIEPLEX_H
#define I2CCHARLIEPLEX_H

#include <Arduino.h>

class I2CCharlieplex {

// LED matrix is 8 across by 7 deep
// Wired up in a charlieplex mode to map to 8 IO pins
#define NUMXLEDS 8
#define NUMYLEDS 7
#define NUMPINS  8

public:
  I2CCharlieplex(uint8_t address, byte* userPins,byte numberOfUserPins);
  void init ();

  // LED buffer handler functions
  void led(uint8_t x, uint8_t y, bool state);
  void clr();
  void scan();

  // MCP23017 handler functions
  void    mcpPinMode(uint8_t p, uint8_t mode);
  void    mcpAllInputs();
  void    mcpDigitalWrite(uint8_t p, uint8_t val);
  void    mcpWrite(uint8_t reg, uint8_t val);
  uint8_t mcpRead(uint8_t reg);
  void    mcpBitUpdate (uint8_t bit, uint8_t val, uint8_t reg);
  uint8_t pin2bit(uint8_t pin);
  uint8_t pin2reg(uint8_t pin, uint8_t portAaddr, uint8_t portBaddr);

private:
  byte    numberOfPins;
  byte*   pins;
  uint8_t addr;
  uint8_t leds[NUMYLEDS];
  uint8_t col;
};

#endif

2 Comments

  1. Charlieplex LED Failure Modes | Kevin's Blog said,

    […] been playing with Charlieplex LED arrays for a while now, and have been linking up my range of 8×7 LED Array and MCP23017 modules to create a larger display.  However, I’ve had that fault you sometimes get with […]

  2. Pimoroni ScrollPhat using Adafruit IS31FL3731 Library | Kevin's Blog said,

    […] finally got myself a genuine Adafruit IS31FL3731 driver board and LED matrix (that I mentioned in a previous post) and glued an Arduino Nano on the back, thus creating a very neat little self-contained USB-driven […]

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: