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 aSTOP
condition and newSTART
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, useSTOP
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:
- Every time I wrote a MCP23018 register and immediately read the register again to check the value
- 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