I2C with the MCP23016 16 bit expander

In this test project I used the MCP23016 for building and testing an i2c library. The MCP23016 is controllable by the i2c protocol. It contains 16 digital input/output pins. This can be usefull if your microcontroller has not enough free digital pins left. Some i2c displays also use a simular IC to control the display. For easier testing I used this IC as a preparation to get a display working in a later blog. The complete source code can be downloaded here.

Test setup

The i2c protocol

The i2c protocol is meant for getting communication between microcontrollers, ic’s, sensors and actuators. The following things will summarize it a bit:

  • The distance you can use the protocol is only a few meters.
  • The communication speed goes up to 400KHz, but for testing I used a lower speed, around 20KHz.
  • It’s posible to connect up to 128 devices parallel. Each device got his own address and can be changed by connecting some pins to gnd or Vcc.
  • Only 1 device at a time can control the bus. The one who controls the bus is called the master and the other devices are the slaves. So in this case the Atmega328p will act as the master and the MCP23016 acts as a slave.
  • I2c only uses 2 wires. CLK is the clock and the data will be send over the SDA line. When the clock is high (5V) the SDA line can be read.

 

To send a message to a device a START condition have to be send first. The device that sends the START condition will remain the master untill it sends a STOP condition.

As you can see  a start condition is created by reading when the clock is high and the SDA line will go from high to low. A stop condition is simular only now the SDA line goes from low to high. Remember that the bits from a message send will also be read when the clock is high, but in that case the SDA line should be stable 0V or 5V. The good thing about this theory is that the microcontroller will take care of the voltage levels with respect to time, we only have to tell the microcontroller when to send a start or stop condition.

After sending a start condition we have to send the addres of the device we want to communcicate with. In case of the MCP23016 the default address is 0x20h. After this a message can be send or received. Therefor I wrote the following functions.

i2c_init(), Enables the Atmega328p to become i2c instead of the default ADC.
i2c_start(), Send the start condition
i2c_write(), send a byte/character
i2c_read(), Read a byte/character
i2c_stop(), send a stop condition.

 

The MCP23016

There are a few settings that can be set in the MCP23016. For example the pins you want to be inputs or outputs. That’s the first thing during initialize() that happens in the code.

The list with settings for the MCP23016.

 

The basics of controlling the MCP23016 are easy. The first byte send is a command. For example the byte 0x06h. This will select the IODIR0 register. This register controls how the first 8 pins should work (input or output). The next byte send will tell which of the 8 pins should be input or output. A ‘0’ is an output. That means sending 00000000b (0x0h) will turn all the pins to outputs. This only have to be done once and therefor it’s done in the initialize() function.

After setting the pins to become outputs we can use them in the application.c by sending the command byte 0x0h to select the first 8 pins. The next byte send will turn them on or off. For example if you want to have the first and last pins to become 5V and the other pins should be 0V send the following bits 10000001b ( in hex that’s 0x81h). In application1.c all the pins will be set high for 2 seconds and then low for 2 seconds just for testing.

Keep in mind I didn’t do any i2c error checking at all to keep the code clear for learning and easy testing. To get the code working I used a logic analyzer (link antatek) a lot before I finally managed to get it working. That doesn’t mean that this code doesn’t work! Instead it works fine, but could have a bit more fault checking. See some screendumps below to get an easier understanding of the bytes that have to be send to the MCP23016.

Screendump from the initialization of the MCP23016

 

Screendump when i2c controls the blinking.

 

Connecting the Atmega to the MCP23016

The schematic is straight forward. Keep in mind that the 4k7 pull-up resistors are very important. Without them things seems to work, but after a while the Atmega328p receives to many NACK’s from the MCP23016 and will freeze (at least in my case…).

Schematic of the project.

 

The source code

I used the same framework as in my last blog’s. The files that are changed are initialize.c and application.c. Besides that I kept application2.c and application3.c the same as in the “Multi thread 3 leds” blog.

initialize.c

/*
 * projectInit.c
 *
 * Created: 20-5-2017 15:07:22
 * Author: HdH
 * Description: initialization off the project
 */ 

#define PIN_LED1 6
#define PIN_LED2 7
#define PIN_LED3 8

#include <avr/interrupt.h>
#include "gpiolib.h"
#include "timer4clocklib.h"
#include "i2c.h"


void initialize(){

	cli();//interrupts disable
	tmr2_init(); // initialize timer2
	i2c_init(); // initialize i2c protocol
	i2c_start(); // send a i2c startbit
	i2c_write((0x20<<1)|0); //send address of the MCP230
	i2c_write(0x06); //send cmd to select IODIR0
	i2c_write(0x00); //set IODIR0 to 0 to make it all outputs
	i2c_write(0x00); //set IODIR1 to 0 to make it all outputs
	i2c_stop();
	sei();//interrupts enable
	
	pinMode(PIN_LED1,PINMODEOUTPUT); //set pin to output
	pinMode(PIN_LED2,PINMODEOUTPUT); //set pin to output
	pinMode(PIN_LED3,PINMODEOUTPUT); //set pin to output	
}

application.c

/*
 * operational.c
 *
 * Created: 20-5-2017 15:07:22
 * Author: HdH
 * Description: Application for multi threat
 */ 

#define PIN_LED1 6
#define DELAYMILIS 2000 // Led blinking wait time in milliseconds
#define TIMER_IS_NOT_SET 0
#define TIMER_IS_SET 1

#include <avr/io.h>
#include "gpiolib.h"
#include "timer4clocklib.h"
#include "i2c.h"

void toggleLed1();

extern int alarmCode;
int previsiousvalue = 0;
int timerSet = TIMER_IS_NOT_SET;
unsigned long int endTime = 0;

void Application1(void){
	
	if(timerSet == TIMER_IS_NOT_SET){
		endTime = msCounter + DELAYMILIS;
		timerSet = TIMER_IS_SET;
		toggleLed1();
	}
	if(endTime < msCounter){ // Timer is expired
		timerSet = TIMER_IS_NOT_SET; // reset timer
		return;
	}
}

void toggleLed1(){
	if (previsiousvalue == 0){
		i2c_start(); // send i2c start bit
		i2c_write((0x20<<1)|0); // send address of the MCP23016
		i2c_write(0x00); // send cmd to access GP0
		i2c_write(0xFF); // set all the pins of GP0 high
		i2c_write(0xFF); // set all the pins of GP1 high
		i2c_stop();
		previsiousvalue = 1;
	}
	else{
		i2c_start(); // send i2c start bit
		i2c_write((0x20<<1)|0); // send address of the MCP23016
		i2c_write(0x00); // send cmd to access GP0
		i2c_write(0x00); // set all the pins of GP0 low
		i2c_write(0x00); // set all the pins of GP1 low
		i2c_stop();
		previsiousvalue = 0;
	}
}


The i2c library only contains the necessary things needed for this moment. That means no fault checkings, or resends of i2c yet.

I2c.h

/*
* I2C.h
*
* Created: 27-5-2017
* Author: HdH
* Description: functions to control the I2C pins
* Global functions:
*
*/


#ifndef I2C_H_
#define I2C_H_

void i2c_init(); // Change ADC pin 4 and 5 to i2c
void i2c_start(); // Send a start condition
void i2c_write(char x); // Send a byte
void i2c_stop(); // Send a stop condition



#endif /* I2C_H_ */

I2c.c

/*
* I2C.c
*
* Created: 27-5-2017
* Author: HdH
* Description: functions to control the I2C pins
* Global functions:
* 
*/
#include <avr/io.h>
#include "i2c.h"

void i2c_init()
{
	TWBR = 0x08;        // SCL freq = CPU clock / (16+2*TWBR*prescaler), 16+2(TWBR)*1 = 16MHz / 100KHz. TWBR=72=48HEX
	TWCR = (1<<TWEN);   // Enable I2C, changes the pin's from ADC to i2c pin's
	TWSR = 0x00;        // Prescaler set to 1
}

 
void i2c_start()
{
    TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWSTA); // Start Condition 
    while (!(TWCR & (1<<TWINT))); // Check for Start condition successful
}
 
 
void i2c_write(char x)
{
    TWDR = x; // Move value to I2C register
    TWCR = (1<<TWINT) | (1<<TWEN); // Enable I2C and Clear Interrupt
    while (!(TWCR & (1<<TWINT))); // Write Successful with TWDR Empty
	
}
 

char i2c_read()
{
    TWCR = (1<<TWEN) | (1<<TWINT); // Enable I2C and Clear Interrupt
    while (!(TWCR & (1<<TWINT))); // Read successful with all data received in TWDR
    return TWDR;
} 


void i2c_stop()
{
	TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWSTO); // Stop Condition 
}


In the next blog I will use the i2c library to communicate with a display. So finaly something more visual and more interesting!