Ruben Laguna's blog

Jan 16, 2014 - 12 minute read - microcontroller i2c teensy arduino atmel avr mcp23018

MCP23018 I2C communication advice

The past few pays I’ve been trying to get the MCP23018 working with the Teensy 2.0. I’m doing this to replace the firmware in my ErgoDox with my own since I can’t the the original ergodox-firmware to work. Finally I got the Teensy<->MCP23018 communication working so here is a summary of my experience.

Although my experience is in particular with Teensy 2.0 I guess it applies to any AVR / Atmel microcontroller (I guess including Arduino). First of all some advice with I2C in general and MCP23018 in particular:

  • NACKs : The last byte read from MCP23018 but be NACKed instead of ACKed. Otherwise the slave can continue to send another byte, miss the STOP condition and and weird behaviour will show up.
  • Speed bus: 100Khz or 400Khz. The speed you select has implication on the timing and delay that you have to insert in your code. 100Khz require higher delays so it’s more sentitive to timing issues in your code. 400Khz is a better choice, faster and less prone to this kind of errors.
  • Timing: There are some minimum time between certain operations that you must account for. I was naive enough to think that the TWI module would take care of that for me but it doesn’t.
    • tBUF: time to wait between a STOP condition and new START condition. You can’t just send those 2 in a row. You must wait at least 4.7us@100Khz or 1.3us@400Khz.
  • Don’t use REPEATED START condition, use STOP after a complete register read or register write

Troubleshooting I2C

When I started using I2C with the MCP23018 I just used repeated START. Since there was no other master on the bus, I thought I would be better to never release the bus. I thought communication was working fine since I was getting the correct TW_STATUS values. But the values that I was reading from the MCP23018 seemed wrong. So I decided to do two things:

  1. Every time I wrote a MCP23018 register and immediately read the register again to check the value
  2. Read and print all the MCP23018 registers

I quickly noticed that all the checks were showing incorrect results (although the TW_STATUS were ok) and that the reads were just returning the MCP23018 register address instead of the register data. In other words if I sent the 0x01 byte to select the IODIRB register then I was getting alwayx 0x01 as the register value.

So my first try was to use STOP conditions instead of the REPEATED START using the following:

void twi_stop(void) {
  TWCR = _BV(TWINT) | _BV(TWSTO) | _BV(TWEN); /* STOP condition */
  loop_until_bit_is_clear(TWCR, TWSTO);
  return;
}

and the program stuck usually at the second STOP, clearly the TWSTO flag was never cleared. That made me wonder about timing. So I re-read the MCP23018 datasheet and then I noticed the tBUF stuff and after googling a bit it seemed clear that I had to take into account this, I was assuming that the TWI module of the ATmel was taking care of these things for me but I was wrong. So then I went ahead and modified the code for the STOP condition like this:

#define TBUF   4.7   // 4.7us @ 100Khz // Bus free time between stop and start

void mcp23018_stopcond(void) {
  print("mcp23018 stopcond\n");
  twi_stop();
  _delay_us(TBUF);
}

Things got better, somehow. But still wasn’t enough, I was missing an important part: the NACK. If you read carefully the 20.8.2 Master Receiver Mode of ATMega32u4 datasheet it says that:

“After the last byte has been received, the MR should inform the ST by sending a NACK after the last received data byte.”

and it’s crucial since as explained in 3 :

“If the master ACKs the last byte and then attempts a STOP condition, the slave might put a 0 on the data line that blocks the STOP condition. If the master NACKs the last byte then the slave IC gives up and everybody exits cleanly.”

I fixed that and that’s it now I have a working communication with the MCP23018.

MCP23018 AVR Library

So at the end this is what I got, it contains some specific functions on using the MCP23018 on my specific project (using the MCP23018 of my ergodox) but mcp23018_read_register and mcp23018_write_register are usable on any project I believe

mpc23018.h

#ifndef _MCP23018_H
#define _MCP23018_H
#include <stdbool.h>

/********MCP23018 pinout***/
/* GPIOA-COLS  |GPIOB-ROWS*/
/**************|***********/
/* GPA7 ?      |GPB7 ?    */
/* GPA6 col6   |GPB6 ?    */
/* GPA5 col5   |GPB5 row0 */
/* GPA4 col4   |GPB4 row1 */
/* GPA3 col3   |GPB3 row2 */
/* GPA2 col2   |GPB2 row3 */
/* GPA1 col1   |GPB1 rwo4 */
/* GPA0 col0   |GPB0 row5 */
/**************************/

// [1] http://ww1.microchip.com/downloads/en/DeviceDoc/22103a.pdf

// I2C address A2 A1 A0 are 000 in ergodox 
// 0 1 0 0 A2 A1 A0 RW  from [1] section 1.4.2 Figure 1.6
// 0 1 0 0 is always the same for all MCP23018
// A2 A1 A0 are set to 0 by bringin the ADDR pin to low (hard wired)
// R = 1 / W = 0
#define TWI_MCP23018_CONTROLBYTEREAD  0b01000001 // 0 1 0 0 A2 A1 A0 RW  from [1] section 1.4.2 Figure 1.6
#define TWI_MCP23018_CONTROLBYTEWRITE 0b01000000 // 0 1 0 0 A2 A1 A0 RW  from [1] section 1.4.2 Figure 1.6

#define IODIRA 0x00 // From [1] Read Table 1-1 and 1.6.1 I/O DIRECTION REGISTER (page 18)
#define IODIRB 0x01 // From [1] Read Table 1-1 and 1.6.1 I/O DIRECTION REGISTER (page 18)
#define IOCONA 0x0A // From [1] Read Table 1-1 and 1.6.6 CONFIGURATION REGISTER
#define IOCONB 0x0B // From [1] Read Table 1-1 and 1.6.6 CONFIGURATION REGISTER
#define IOCON  IOCONA // From [1] Read Table 1-1 and 1.6.6 CONFIGURATION REGISTER
#define GPPUA  0x0C // From [1] Read Table 1-1 and 1.6.7 pull-up resistor configuration register
#define GPPUB  0x0D // From [1] Read Table 1-1 and 1.6.7 pull-up resistor configuration register
#define GPIOA  0x12 // From [1] Read Table 1-1 and 1.6.10 PORT REGISTER (page 28)
#define GPIOB  0x13 // From [1] Read Table 1-1 and 1.6.10 PORT REGISTER (page 28)
#define OLATA  0x14 // From [1] Read Table 1-1 and 1.6.11 OUTPUT LATCH REGISTER (page 29)
#define OLATB  0x15 // From [1] Read Table 1-1 and 1.6.11 OUTPUT LATCH REGISTER (page 29)


#define TWI100K 100000UL
#define TWI400K 400000UL

#define F_SCL TWI400K 
#define USE_TWI_STOP
/* #define TWI_DEBUG */

/* MCP23018 I2C timing */
#define TGPVO  0.5 // 500ns Table 2-2
#define TGPIV  0.450 // 450ns Table 2-2

#if F_SCL==TWI100K
//table 2-4
#define TBUF   4.7   // 4.7us @ 100Khz // Bus free time between stop and start
#elif F_SCL==TWI400K
//table 2-4
#define TBUF   1.3 // 1.3us @ 400Khz
#else
#error "F_SCL must be TWI100K or TWI400K"
#endif

static const uint8_t GPBROWS = _BV(0) | _BV(2) | _BV(3) | _BV(4) | _BV(5); // GPPU 1 means pullup enable and 0 means pullup disabled

/* low level TWI */
void twi_init(void);
bool twi_start(void);
bool twi_repstart(void);
void twi_stop(void);
bool twi_send(uint8_t data);
bool twi_read(uint8_t *data);
void twi_print_error(const char *data);

/* mcp23018 specific */
bool mcp23018_write_register(uint8_t reg, uint8_t data);
bool mcp23018_read_register(uint8_t reg, uint8_t *data);
bool mcp23018_init(void);
bool mcp23018_all_cols_highz(void);
bool mcp23018_col_low(uint8_t col);
bool mcp23018_check(void);
bool mcp23018_check_reg(uint8_t reg, const uint8_t data);
uint8_t mcp23018_read_rows(void);

#endif

mpc23018.c

#include <util/twi.h>
#include <stdbool.h>
#include "print.h"
#include "mcp23018.h"
#include <util/delay.h>
#include <math.h>

/* [1] ATMega32u4 datasheet  http://www.atmel.com/images/7766s.pdf*/
/* [2] Atmel page on ATMega32u4 http://www.atmel.com/devices/atmega32u4.aspx */
/* [3] ATMega32u4 manual http://www.atmel.com/Images/doc7766.pdf */

static void mcp23018_stopcond(void); // just to be used internally in this file
static bool mcp23018_startcond(void); // just to be used internally in this file
static bool mcp23018_repstartcond(void); // just to be used internally in this file
static bool mcp23018_sendbyte(uint8_t data);
static bool mcp23018_readbyte_nack(uint8_t *data);

static bool _twi_read(uint8_t *data);

void twi_init(void) {
  TWSR = 0; //prescaler 0
  TWBR = (F_CPU / F_SCL) / 2; // TWI @ 100Khz or TWI @ 400Khz
}

bool twi_start(void) {
  TWCR = _BV(TWINT) | _BV(TWSTA) | _BV(TWEN); /* START condition. See [3] See Section 20.7 page 234 */
  loop_until_bit_is_set(TWCR, TWINT); /* hardware sets TWINT when status is avaible in TWSR*/
  switch(TW_STATUS) {
    case TW_START:
      return true;
    case TW_REP_START: //repeated start
      print("TWI/IC2 got a REPEATED START when we were expecting START\n");
      return false;
    default:
      print("TWI/IC2 START condition failed\n");
      return false;
  }
  return false;
}

bool twi_repstart(void) {
  TWCR = _BV(TWINT) | _BV(TWSTA) | _BV(TWEN); /* START condition. See [3] See Section 20.7 page 234 */
  loop_until_bit_is_set(TWCR, TWINT); /* hardware sets TWINT when status is avaible in TWSR*/
  switch(TW_STATUS) {
    case TW_START:
      print("got a START when we were trying a REPEATED START\n");
      return false;
    case TW_REP_START: //repeated start
      return true;
    default:
      print("TWI/IC2 START condition failed\n");
      return false;
  }
  return false;
}

#ifdef USE_TWI_STOP

void twi_stop(void) {
  TWCR = _BV(TWINT) | _BV(TWSTO) | _BV(TWEN); /* STOP condition */
  loop_until_bit_is_clear(TWCR, TWSTO);
  return;
}
#endif

bool twi_send(uint8_t data) {
  TWDR = data; /* TWDR hold the data to be transmitted */
  TWCR = _BV(TWINT) | _BV(TWEN); /* clear TWINT to initiate transmision */
  loop_until_bit_is_set(TWCR, TWINT); /* hardware sets TWINT when status is avaible in TWSR*/
  switch(TW_STATUS) {
    case TW_MT_SLA_ACK:  //SLA+W transmitted, ACK received
    case TW_MT_DATA_ACK: //data transmitted, ACK received
    case TW_MR_SLA_ACK:  //SLA+R transmitted, ACK received
      return true;
    default:
      print("TWI/I2C: twi_send failed (");
      phex(TW_STATUS);
      print(")\n");
      return false;
  }
  return false;
}

bool twi_read_nack(uint8_t *data) {
  TWCR = _BV(TWINT) | _BV(TWEN); /* no TWEA mean NACK */
  return _twi_read(data);
}

bool twi_read_ack(uint8_t *data) {
  TWCR = _BV(TWINT) | _BV(TWEN) | _BV(TWEA); /* TWEA to send ACK*/
  return _twi_read(data);
}

bool _twi_read(uint8_t *data) {
  /* TWCR must be already set by the caller */
  loop_until_bit_is_set(TWCR, TWINT); /* hardware sets TWINT when status is avaible in TWSR*/
  switch(TW_STATUS) {
    case TW_MR_DATA_ACK: //SLA+W transmitted, ACK received
    case TW_MR_DATA_NACK: //SLA+W transmitted, ACK received
      *data = TWDR;
      return true;
    default:
      goto fail;
  }
  return true;
fail:
  twi_print_error("twi_read failed");
  return false;
}

void twi_print_error(const char *data) {
  print_P(data);
  print(" (");
  char *errorCode;
  switch(TW_STATUS) {
    case TW_MR_DATA_NACK: errorCode = "TW_MR_DATA_NACK"; break;
    case TW_BUS_ERROR: errorCode = "TW_BUS_ERROR"; break;
    case TW_MT_ARB_LOST: errorCode = "TW_MT_ARB_LOST / TW_MR_ARB_LOST"; break;
    default:
      errorCode = "unknown";
  }
  print_P(errorCode);
  print(")");

}

#ifdef USE_TWI_STOP
void mcp23018_stopcond(void) {
#ifdef TWI_DEBUG
  print("mcp23018 stopcond\n");
#endif
  twi_stop();
  _delay_us(TBUF); 
}
#endif

static bool mcp23018_startcond(void) {
#ifdef TWI_DEBUG
  print("mcp23018_startcond\n");
#endif
  if(twi_start()) {
    return true;
  }
  return false;
}

static bool mcp23018_repstartcond(void) {
#ifdef TWI_DEBUG
  print("mcp23018_repstartcond\n");
#endif
  if(twi_repstart()) {
    return true;
  }
  return false;
}

/*
 * Sends a byte over TWI/I2C but taking into account
 * the waiting  times that MCP23018 imposes
 */
static bool mcp23018_sendbyte(uint8_t data) {
#ifdef TWI_DEBUG
  print("mcp23018_sendbyte\n");
#endif
  bool result = twi_send(data);
  //_delay_us(THDDAT);
  return result;
}

static bool mcp23018_readbyte_nack(uint8_t *data) {
#ifdef TWI_DEBUG
  print("mcp23018_readbyte\n");
#endif
  bool result = twi_read_nack(data);
  return result;
}

bool mcp23018_write_register(uint8_t reg, uint8_t data) {
#ifdef TWI_DEBUG
  print("mcp23018_write_register\n");
#endif
  if (!mcp23018_startcond()) return false; // if START fails, no stopcond 
  if (!mcp23018_sendbyte(TWI_MCP23018_CONTROLBYTEWRITE)) goto fail; // to address the mcp23018
  if (!mcp23018_sendbyte(reg)) goto fail; // first we send the register bytes
  if (!mcp23018_sendbyte(data)) goto fail;  // then the value we want to store in that register
#ifdef USE_TWI_STOP
  mcp23018_stopcond();
#endif
#ifdef TWI_DEBUG
  mcp23018_check_reg(reg,data);
#endif
  return true;
fail:
  print("MCP23018: error trying to write register (");
  phex(reg);
  print(") with value(");
  phex(data);
  print(")\n");
#ifdef USE_TWI_STOP
  mcp23018_stopcond();
#endif
  return false;
}

bool mcp23018_read_register(uint8_t reg, uint8_t *data) {
#ifdef TWI_DEBUG
  print("mcp23018_read_register\n");
#endif
  if (!mcp23018_startcond()) return false; // if START fails, without stop cond 
  if (!mcp23018_sendbyte(TWI_MCP23018_CONTROLBYTEWRITE)) goto fail; // to address the mcp23018
  if (!mcp23018_sendbyte(reg)) goto fail; // first we tell which register we want to read
  if (!mcp23018_repstartcond()) return false;   // then we change the mode to READ
  if (!mcp23018_sendbyte(TWI_MCP23018_CONTROLBYTEREAD)) goto fail;
  if (!mcp23018_readbyte_nack(data)) goto fail; // last byte read = NACK
#ifdef USE_TWI_STOP
  mcp23018_stopcond();
#endif
  return true;
fail:
  print("MCP23018: error trying to read register (");
  phex(reg);
  print(")\n");
#ifdef USE_TWI_STOP
  mcp23018_stopcond();
#endif
  return false;
}

bool mcp23018_init() {
#ifdef TWI_DEBUG
  print("mcp23018 init\n");
#endif
  //bring all GPIO pins to high impedance (inputs without pull-up resistors)  this is the default
  if (!mcp23018_write_register(IOCON,0x00)) goto fail;
  if (!mcp23018_write_register(GPPUA, 0x00)) goto fail; //GPA0-A7 no pull up
  if (!mcp23018_write_register(GPPUB, 0x00)) goto fail; //GPA0-A7 no pull up
  if (!mcp23018_write_register(IODIRA,0xFF)) goto fail; // IODIR 0 = output | 1 = input
  if (!mcp23018_write_register(IODIRB,0xFF)) goto fail; // IODIR 0 = output | 1 = input
  return true;
fail:
  print("mcp23018_init failed\n");
  return false;
}

/* GPA7  | GPA6  | GPA5  | GPA4  | GPA3  | GPA2  | GPA1  | GPA0  */
/* highz | highz | highz | highz | highz | highz | highz | highz */
bool mcp23018_all_cols_highz(void) {
#ifdef TWI_DEBUG
  print("mcp23018_all_cols_highz\n");
#endif
  if (!mcp23018_write_register(GPPUA,0x00)) goto fail;  // GPPU 0 = pullup disabled 
  if (!mcp23018_write_register(IODIRA,0xFF)) goto fail; // IODIR 0 = output | 1 = input
  _delay_us(TGPVO); /* tGPOV 500ns */
  return true;
fail:
  print("mcp23018_all_cols_highz failed\n");
  return false;
}



/********MCP23018 pinout***/
/* GPIOA-COLS  |GPIOB-ROWS*/
/**************|***********/
/* GPA7 ?      |GPB7 ?    */
/* GPA6 col6   |GPB6 ?    */
/* GPA5 col5   |GPB5 row0 */
/* GPA4 col4   |GPB4 row1 */
/* GPA3 col3   |GPB3 row2 */
/* GPA2 col2   |GPB2 row3 */
/* GPA1 col1   |GPB1 row4 */
/* GPA0 col0   |GPB0 row5 */
/**************************/
uint8_t mcp23018_read_rows() {
#ifdef TWI_DEBUG
  print("mcp23018_read_rows\n");
#endif
  /* GPB7   | GPB6   | GPB5 | GPB4 | GPB3 | GPB2 | GPB1 | GPB0 */
  /* high-z | high-z | in-p | in-p | in-p | in-p | in-p | in-p */
  // whole GPB as input (high-z are also inputs)
  if (!mcp23018_write_register(IODIRB, 0xFF)) goto fail;//IODIR 1=input 0=output

  // Pullups of GPIOB only for GPB0-GPB5
  if (!mcp23018_write_register(GPPUB, GPBROWS)) goto fail;

  // and read from GPIOB
  _delay_us(TGPIV); /* tGPIV 450ns Table 2-2 page 36*/
  uint8_t data = 0;
  if (!mcp23018_read_register(GPIOB, &data)) goto fail;

  return (data & GPBROWS); //mask GPB7 and GPB6 out
fail:
  print("mcp23018_read_rows failed\n");
  return 0;
}


/* Bring one of the GPAn pins to ground */
/* GPA7   | GPA6   | GPA5 | GPA4 | GPA3 | GPA2 | GPA1 | GPA0 */
/* high-z | col6   | col5 | col4 | col3 | col2 | col1 | col0 */
bool mcp23018_col_low(uint8_t n) {
#ifdef TWI_DEBUG
  print("mcp23018_col_low\n");
#endif
  if (n > 6) goto fail;

  //make sure that pullups are properly configured
  if (!mcp23018_write_register(GPPUA,0x00)) goto fail; // GPPU 0 = pullup disabled  1=enabled

  // make sure the output is low 
  if (!mcp23018_write_register(OLATA, 0x00)) goto fail; // output latch 0 means ouput low (ground)

  //set one of the GPAn pins as output and the rest as inputs
  if (!mcp23018_write_register(IODIRA, ~_BV(n))) goto fail; // IODIR 0 = output / 1 = input

  _delay_us(TGPVO); /* tGPVO 500ns Table 2-2 page 36 */

  return true;
fail:
  print("mcp23018_setGPAn_low failed\n");
  return false;
}

bool mcp23018_check(void) {
#ifdef TWI_DEBUG
  print("mcp23018_check\n");
#endif
  for(size_t reg = IODIRA; reg <= OLATB; ++reg) { // all registers 0x00-0x15
    print("mcp23018_check: reg(");
    phex(reg);
    print(")\n");
    uint8_t data = 0;
    if(!mcp23018_read_register(reg,&data)) goto fail;
    print("mcp23018_check: reg(");
    phex(reg);
    print(") value(");
    phex(data);
    print(")\n");
  }
  return true;
fail:
  print("mcp23018_check failed\n");
  return false;
}

#ifdef TWI_DEBUG
bool mcp23018_check_reg(uint8_t reg, const uint8_t expected) {
  print("mcp23018_check_reg\n");
  uint8_t data = 0;
  if(!mcp23018_read_register(reg,&data)) goto fail;

  if (data == expected) return true;

  // expected != actual
  print("assert failed reg(");
  phex(reg);
  print(") expected(");
  phex(expected);
  print(") actual(");
  phex(data);
  print(")\n");
  return false;
fail:
  print("mcp23018_check_reg failed\n");
  return false;
}
#endif