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:
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:
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:
1234567
#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);
}
“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
#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 */staticvoidmcp23018_stopcond(void);// just to be used internally in this filestaticboolmcp23018_startcond(void);// just to be used internally in this filestaticboolmcp23018_repstartcond(void);// just to be used internally in this filestaticboolmcp23018_sendbyte(uint8_tdata);staticboolmcp23018_readbyte_nack(uint8_t*data);staticbool_twi_read(uint8_t*data);voidtwi_init(void){TWSR=0;//prescaler 0TWBR=(F_CPU/F_SCL)/2;// TWI @ 100Khz or TWI @ 400Khz}booltwi_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){caseTW_START:
returntrue;caseTW_REP_START: //repeated startprint("TWI/IC2 got a REPEATED START when we were expecting START\n");returnfalse;default:print("TWI/IC2 START condition failed\n");returnfalse;}returnfalse;}booltwi_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){caseTW_START:
print("got a START when we were trying a REPEATED START\n");returnfalse;caseTW_REP_START: //repeated startreturntrue;default:print("TWI/IC2 START condition failed\n");returnfalse;}returnfalse;}#ifdef USE_TWI_STOPvoidtwi_stop(void){TWCR=_BV(TWINT)|_BV(TWSTO)|_BV(TWEN);/* STOP condition */loop_until_bit_is_clear(TWCR,TWSTO);return;}#endifbooltwi_send(uint8_tdata){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){caseTW_MT_SLA_ACK: //SLA+W transmitted, ACK receivedcaseTW_MT_DATA_ACK: //data transmitted, ACK receivedcaseTW_MR_SLA_ACK: //SLA+R transmitted, ACK receivedreturntrue;default:print("TWI/I2C: twi_send failed (");phex(TW_STATUS);print(")\n");returnfalse;}returnfalse;}booltwi_read_nack(uint8_t*data){TWCR=_BV(TWINT)|_BV(TWEN);/* no TWEA mean NACK */return_twi_read(data);}booltwi_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){caseTW_MR_DATA_ACK: //SLA+W transmitted, ACK receivedcaseTW_MR_DATA_NACK: //SLA+W transmitted, ACK received*data=TWDR;returntrue;default:gotofail;}returntrue;fail:twi_print_error("twi_read failed");returnfalse;}voidtwi_print_error(constchar*data){print_P(data);print(" (");char*errorCode;switch(TW_STATUS){caseTW_MR_DATA_NACK: errorCode="TW_MR_DATA_NACK";break;caseTW_BUS_ERROR: errorCode="TW_BUS_ERROR";break;caseTW_MT_ARB_LOST: errorCode="TW_MT_ARB_LOST / TW_MR_ARB_LOST";break;default:errorCode="unknown";}print_P(errorCode);print(")");}#ifdef USE_TWI_STOPvoidmcp23018_stopcond(void){#ifdef TWI_DEBUGprint("mcp23018 stopcond\n");#endiftwi_stop();_delay_us(TBUF);}#endifstaticboolmcp23018_startcond(void){#ifdef TWI_DEBUGprint("mcp23018_startcond\n");#endifif(twi_start()){returntrue;}returnfalse;}staticboolmcp23018_repstartcond(void){#ifdef TWI_DEBUGprint("mcp23018_repstartcond\n");#endifif(twi_repstart()){returntrue;}returnfalse;}/* * Sends a byte over TWI/I2C but taking into account * the waiting times that MCP23018 imposes */staticboolmcp23018_sendbyte(uint8_tdata){#ifdef TWI_DEBUGprint("mcp23018_sendbyte\n");#endifboolresult=twi_send(data);//_delay_us(THDDAT);returnresult;}staticboolmcp23018_readbyte_nack(uint8_t*data){#ifdef TWI_DEBUGprint("mcp23018_readbyte\n");#endifboolresult=twi_read_nack(data);returnresult;}boolmcp23018_write_register(uint8_treg,uint8_tdata){#ifdef TWI_DEBUGprint("mcp23018_write_register\n");#endifif(!mcp23018_startcond())returnfalse;// if START fails, no stopcond if(!mcp23018_sendbyte(TWI_MCP23018_CONTROLBYTEWRITE))gotofail;// to address the mcp23018if(!mcp23018_sendbyte(reg))gotofail;// first we send the register bytesif(!mcp23018_sendbyte(data))gotofail;// then the value we want to store in that register#ifdef USE_TWI_STOPmcp23018_stopcond();#endif#ifdef TWI_DEBUGmcp23018_check_reg(reg,data);#endifreturntrue;fail:print("MCP23018: error trying to write register (");phex(reg);print(") with value(");phex(data);print(")\n");#ifdef USE_TWI_STOPmcp23018_stopcond();#endifreturnfalse;}boolmcp23018_read_register(uint8_treg,uint8_t*data){#ifdef TWI_DEBUGprint("mcp23018_read_register\n");#endifif(!mcp23018_startcond())returnfalse;// if START fails, without stop cond if(!mcp23018_sendbyte(TWI_MCP23018_CONTROLBYTEWRITE))gotofail;// to address the mcp23018if(!mcp23018_sendbyte(reg))gotofail;// first we tell which register we want to readif(!mcp23018_repstartcond())returnfalse;// then we change the mode to READif(!mcp23018_sendbyte(TWI_MCP23018_CONTROLBYTEREAD))gotofail;if(!mcp23018_readbyte_nack(data))gotofail;// last byte read = NACK#ifdef USE_TWI_STOPmcp23018_stopcond();#endifreturntrue;fail:print("MCP23018: error trying to read register (");phex(reg);print(")\n");#ifdef USE_TWI_STOPmcp23018_stopcond();#endifreturnfalse;}boolmcp23018_init(){#ifdef TWI_DEBUGprint("mcp23018 init\n");#endif//bring all GPIO pins to high impedance (inputs without pull-up resistors) this is the defaultif(!mcp23018_write_register(IOCON,0x00))gotofail;if(!mcp23018_write_register(GPPUA,0x00))gotofail;//GPA0-A7 no pull upif(!mcp23018_write_register(GPPUB,0x00))gotofail;//GPA0-A7 no pull upif(!mcp23018_write_register(IODIRA,0xFF))gotofail;// IODIR 0 = output | 1 = inputif(!mcp23018_write_register(IODIRB,0xFF))gotofail;// IODIR 0 = output | 1 = inputreturntrue;fail:print("mcp23018_init failed\n");returnfalse;}/* GPA7 | GPA6 | GPA5 | GPA4 | GPA3 | GPA2 | GPA1 | GPA0 *//* highz | highz | highz | highz | highz | highz | highz | highz */boolmcp23018_all_cols_highz(void){#ifdef TWI_DEBUGprint("mcp23018_all_cols_highz\n");#endifif(!mcp23018_write_register(GPPUA,0x00))gotofail;// GPPU 0 = pullup disabled if(!mcp23018_write_register(IODIRA,0xFF))gotofail;// IODIR 0 = output | 1 = input_delay_us(TGPVO);/* tGPOV 500ns */returntrue;fail:print("mcp23018_all_cols_highz failed\n");returnfalse;}/********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_tmcp23018_read_rows(){#ifdef TWI_DEBUGprint("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))gotofail;//IODIR 1=input 0=output// Pullups of GPIOB only for GPB0-GPB5if(!mcp23018_write_register(GPPUB,GPBROWS))gotofail;// and read from GPIOB_delay_us(TGPIV);/* tGPIV 450ns Table 2-2 page 36*/uint8_tdata=0;if(!mcp23018_read_register(GPIOB,&data))gotofail;return(data&GPBROWS);//mask GPB7 and GPB6 outfail:print("mcp23018_read_rows failed\n");return0;}/* 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 */boolmcp23018_col_low(uint8_tn){#ifdef TWI_DEBUGprint("mcp23018_col_low\n");#endifif(n>6)gotofail;//make sure that pullups are properly configuredif(!mcp23018_write_register(GPPUA,0x00))gotofail;// GPPU 0 = pullup disabled 1=enabled// make sure the output is low if(!mcp23018_write_register(OLATA,0x00))gotofail;// output latch 0 means ouput low (ground)//set one of the GPAn pins as output and the rest as inputsif(!mcp23018_write_register(IODIRA,~_BV(n)))gotofail;// IODIR 0 = output / 1 = input_delay_us(TGPVO);/* tGPVO 500ns Table 2-2 page 36 */returntrue;fail:print("mcp23018_setGPAn_low failed\n");returnfalse;}boolmcp23018_check(void){#ifdef TWI_DEBUGprint("mcp23018_check\n");#endiffor(size_treg=IODIRA;reg<=OLATB;++reg){// all registers 0x00-0x15print("mcp23018_check: reg(");phex(reg);print(")\n");uint8_tdata=0;if(!mcp23018_read_register(reg,&data))gotofail;print("mcp23018_check: reg(");phex(reg);print(") value(");phex(data);print(")\n");}returntrue;fail:print("mcp23018_check failed\n");returnfalse;}#ifdef TWI_DEBUGboolmcp23018_check_reg(uint8_treg,constuint8_texpected){print("mcp23018_check_reg\n");uint8_tdata=0;if(!mcp23018_read_register(reg,&data))gotofail;if(data==expected)returntrue;// expected != actualprint("assert failed reg(");phex(reg);print(") expected(");phex(expected);print(") actual(");phex(data);print(")\n");returnfalse;fail:print("mcp23018_check_reg failed\n");returnfalse;}#endif