I2C communication between RaspberryPi and an AVR based I2C slave

04 May 2019 - tsp

There is an AppNote from Atmel that one should really read when one plans to use the AVR as an I2C slave. That’s AVR311: Using the TWI Module as I2C Slave.

All code used in this article is available in an GitHub repository.

What is I2C

I2C is a bussystem that is built around two signal lines - the data line SDA and the clock line SCL. Of course the system also requires a common ground. Because multi master operation is supported on I2C both lines require pullup resistors - the bus members only use open-drain / open-collector outputs to pull the line low, they are recharged by the external pullup resistors. Due to this it is perfectly possible to combine 5V and 3.3V devices on the same I2C bus - if one doesn’t pull the line to 5V but only to 3.3V Vcc.

For a typical application the pullup resistors have a value of around $4.7 k\Omega$. This of course depends on the bus capacitance (number of devices, length of the bus, etc.) as well as on the operating voltage.

The I2C bus works at two standardized frequencies. These are 100 kHz or 400 kHz (the AVR CPU frequency has to be at least 16 times as high). Other frequencies are of course possible too but may not be supported by all I2C devices.

The worst case transfer rates (single byte transfers) that one can expect with a single master system are:

The I2C bus of course also supports multiple masters on the same bus - then bus arbitration gets interesting.

The AVR has hardware support for the two wire interface. It supports an slave mode that is fully compatible with I2C. The main components consist of:

Basic slave code

Basically the whole TWI interface code is interrupt driven (for master as well as for slave operation). The only interrupt vector used is TWI_vect so one requires a handler for that:

ISR(TWI_vect) {
   // Whatever will happen here
}

The TWI_vect vector triggers in case any of the following events is raised:

This allows us to assemble the basic slave code:


static void i2cEventReceived(uint8_t data) {
	// Do whatever we want with the received data
}
static void i2cEventBusError() {
	// What do we do in case of bus error?
}
static uint8_t i2cEventTransmit() {
	// Generate next byte that will be sent to the master
}


static void i2cSlaveInit(uint8_t address) {
	cli();

	TWAR = (address << 1) | 0x01; // Respond to general calls and calls towards us
	TWCR = 0xC5; // Set TWIE (TWI Interrupt enable), TWEN (TWI Enable), TWEA (TWI Enable Acknowledgement), TWINT (Clear TWINT flag by writing a 1)

	sei();
	return;
}

static void i2cSlaveShutdown() {
	cli();

	TWCR = 0;
	TWAR = 0;

	sei();
	return;
}

ISR(TWI_vect) {
	switch(TW_STATUS) { /* Note: TW_STATUS is an macro that masks status bits from TWSR) */
		case TW_SR_SLA_ACK:
		case TW_SR_DATA_ACK:
			/*
				We have received data. This is now contained in the TWI
				data register (TWDR)
			*/
			i2cEventReceived(TWDR);
			break;
		case TW_ST_SLA_ACK:
		case TW_ST_DATA_ACK:
			/*
				Either slave selected (SLA_ACK) and data requested or data transmitted, ACK received
				and next data requested
			*/
			TWDR = i2cEventTransmit();
			break;
		case TW_BUS_ERROR:
			i2cEventBusError();
			break;
		default:
			break;
	}
	TWCR = 0xC5; // Set TWIE (TWI Interrupt enable), TWEN (TWI Enable), TWEA (TWI Enable Acknowledgement), TWINT (Clear TWINT flag by writing a 1)
}

Test code

To demonstrate the behaviour of this I2C slave one can use the following program that just remembers the last byte received via I2C, increments it by 1 every time it is read out and returns this value. For sake of simplicity Arduino libraries are used for serial I/O and pullup settings:

#include <avr/wdt.h>
#include <avr/interrupt.h>
#include <util/twi.h>
#include <stdint.h>
#define TWI_ADDRESS 0x14

static uint8_t lastByte = 0x00;

static void i2cEventReceived(uint8_t data) {
	// Do whatever we want with the received data
	lastByte = data;
}
static void i2cEventBusError() {
	// Ignore
	return;
}
static uint8_t i2cEventTransmit() {
	lastByte = lastByte + 1;
	return lastByte;
}


static void i2cSlaveInit(uint8_t address) {
	cli();

	TWAR = (address << 1) | 0x01; // Respond to general calls and calls towards us
	TWCR = 0xC5; // Set TWIE (TWI Interrupt enable), TWEN (TWI Enable), TWEA (TWI Enable Acknowledgement), TWINT (Clear TWINT flag by writing a 1)

	sei();
	return;
}

static void i2cSlaveShutdown() {
	cli();

	TWCR = 0;
	TWAR = 0;

	sei();
	return;
}

ISR(TWI_vect) {
	switch(TW_STATUS) { /* Note: TW_STATUS is an macro that masks status bits from TWSR) */
		case TW_SR_SLA_ACK:
		case TW_SR_DATA_ACK:
			/*
				We have received data. This is now contained in the TWI
				data register (TWDR)
			*/
			i2cEventReceived(TWDR);
			break;
		case TW_ST_SLA_ACK:
		case TW_ST_DATA_ACK:
			/*
				Either slave selected (SLA_ACK) and data requested or data transmitted, ACK received
				and next data requested
			*/
			TWDR = i2cEventTransmit();
			break;
		case TW_BUS_ERROR:
			i2cEventBusError();
			break;
		default:
			break;
	}
	TWCR = 0xC5; // Set TWIE (TWI Interrupt enable), TWEN (TWI Enable), TWEA (TWI Enable Acknowledgement), TWINT (Clear TWINT flag by writing a 1)
}


void setup() {
	/*
		Disable watchdog, initialize serial
	*/
	wdt_disable();

	/*
		Inputs are - after reset - configured as
		input (tristate) with pullups disabled (as
		they should be)
	*/

	/*
		Initialize TWI slave
	*/
	i2cSlaveInit(TWI_ADDRESS);
}

void loop() {
	// Do nothing here - this is entirely interrupt driven
}

The I2C master is realized with an Raspberry Pi running FreeBSD. This code is built around the work and examples of Vadim Zaigrin - if one is interested in why one can only use ioctl and not read/write calls to access the I2C bus and how he discovered that one should read his blogpost from 2014 about working with I2C in FreeBSD on RaspberryPi.

#include <sys/cdefs.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <unistd.h>
#include <dev/iicbus/iic.h>

#define IIC_DEVICE "/dev/iic1"

/*
	This method provides a simple way
	of "scanning" an I2C bus by checking
	on which adresses devices exist. Note that
	since there is no real auto discovery on I2C
	this MAY trigger actions on some devices that
	are not desired - it's realized just as a write
	followed by a read.
*/
static void scanI2CBus(int fd) {
	unsigned int i;
	uint8_t buf[2] = { 0, 0 };
	struct iic_msg msg[2];
	struct iic_rdwr_data rdwr;

	msg[0].flags = !IIC_M_RD;
	msg[0].len   = sizeof(buf);
	msg[0].buf   = buf;

	msg[1].flags = IIC_M_RD;
	msg[1].len  = sizeof(buf);
	msg[1].buf  = buf;

	rdwr.nmsgs = 2;

	for(i = 1; i < 128; i++) {
		// Set address
		msg[0].slave = i;
		msg[1].slave = i;

		rdwr.msgs = msg;
		if(ioctl(fd, I2CRDWR, &rdwr) >= 0) {
			// Success - we have found a device
			printf("Device found: %02x\n", i);
		}
	}
}

static char i2cRead(int fd) {
        uint8_t buf[1];

        struct iic_msg msg[1];
        struct iic_rdwr_data rdwr;

        msg[0].slave = 0x14 << 1; // Assuming the code above for the AVR has been used with address 0x14
        msg[0].flags = IIC_M_RD;
        msg[0].len   = sizeof(buf);
        msg[0].buf   = buf;

        rdwr.msgs = msg;
        rdwr.nmsgs = 1;

        if(ioctl(fd, I2CRDWR, &rdwr) < 0)  {
                perror("I2CRDWR");
                return(0xFF);
        }

        return buf[0];
}

static void i2cSend(int fd, char value) {
        uint8_t buf[1];
        struct iic_msg msg;
        struct iic_rdwr_data rdwr;

        buf[0] = value;
        msg.slave = 0x14 << 1; // Assuming the code above for the AVR has been used with address 0x14
        msg.flags = 0;
        msg.len   = sizeof( buf );
        msg.buf   = buf;

        rdwr.msgs = &msg;
        rdwr.nmsgs = 1;

        if (ioctl(fd, I2CRDWR, &rdwr) < 0)  {
                perror("I2CRDWR");
        }
}


int main ( int argc, char **argv )  {
	int fd;

	if ((fd = open(IIC_DEVICE, O_RDWR)) < 0 )  {
		perror("open");
		return -1;

	}

	scanI2CBus(fd);

	printf("\n\nDoing some read and write tests\n\n");

	i2cSend(fd, 0x08);
	printf("First read: %u\n", (unsigned int)i2cRead(fd));
	printf("Second read: %u\n", (unsigned int)i2cRead(fd));
	printf("Third read: %u\n", (unsigned int)i2cRead(fd));


	close(fd);

	return 0;
}

After uploading the code to the AVR (one can use stock Arduino IDE for that) and compiling the code on the RaspberryPi using

clang -o i2cexample -Wall -ansi -std=c99 -pedantic ./i2cexample.c

one just has to connect the AVRs SCK and SDA pins as well as common ground to the Raspberry Pi’s I2C interface pins.

I2C communication between RaspberryPi B+ and Arduino pro mini I2C communication between RaspberryPi B+ and Arduino pro mini

Execution should lead to the following output if everything worked out:

user@FreePI_TSP:~/i2ctest # ./i2cexample
Device found: 28
Device found: 29


Doing some read and write tests

First read: 9
Second read: 10
Third read: 11

Note that - if you get permission denied errors - the user that you’re running your code with should have read and write permissions to /dev/iic1 - so one has to set permissions accordingly. Of course one could also run the application as root user but that is as usual not a good idea and really bad practice.

Bug: There seems to be a bug that does not allow combined read and write transaction with this hardware combination. Although the IOCTL should support a combined transaction one has to do reads and writes with separate syscalls.

This article is tagged: Programming, Electronics, DIY, AVR


Dipl.-Ing. Thomas Spielauer, Wien (webcomplains389t48957@tspi.at)

This webpage is also available via TOR at http://jugujbrirx3irwyx.onion/

Valid HTML 4.01 Strict Powered by FreeBSD IPv6 support