I2C communication between RaspberryPi and an AVR based I2C slave
04 May 2019 - tsp
Last update 07 Feb 2021
9 mins
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:
- 100 kHz: 100000 bit per second (12500 Bytes which are in worst case always address and data so 100 kHz deliver just 6.1 kByte/second)
- 400 kHz: Four times that fast so at least 24 kByte/second are possible
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:
- And hardware address match unit that detects addressing cycles on the I2C bus and ignores transactions
that have a different destination. This unit checks hardware addresses against a 7 bit address in the
TWAR
register. Acknowledgement of adresses is supported. By using the TWAMR
mask register the controller
may react to a masked subset of the address space or to a specific address.
- The interface unit which reads and writes data to/from the
TWDR
data and address shift register, implements
start and stop as well as arbitration detection.
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:
TW_SR_SLA_ACK
: The slave has been adressed by the master, slave will receive data
TW_SR_DATA_ACK
: The master has sent data to the receiving slave
TW_ST_SLA_ACK
: The slave has been adressed by the master, slave will transmit data
TW_ST_DATA_ACK
: The master is requesting data from the slave
TW_BUS_ERROR
: Triggered in case of an bus error
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();
/*
Respond to general calls and calls towards us
*/
TWAR = (address << 1) | 0x01;
/*
Set TWIE (TWI Interrupt enable), TWEN (TWI Enable),
TWEA (TWI Enable Acknowledgement), TWINT (Clear
TWINT flag by writing a 1)
*/
TWCR = 0xC5;
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;
}
/*
Set TWIE (TWI Interrupt enable), TWEN (TWI
Enable), TWEA (TWI Enable Acknowledgement),
TWINT (Clear TWINT flag by writing a 1)
*/
TWCR = 0xC5;
}
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();
/*
Respond to general calls and calls towards us
*/
TWAR = (address << 1) | 0x01;
/*
Set TWIE (TWI Interrupt enable), TWEN (TWI
Enable), TWEA (TWI Enable Acknowledgement),
TWINT (Clear TWINT flag by writing a 1)
*/
TWCR = 0xC5;
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;
}
/*
Set TWIE (TWI Interrupt enable), TWEN (TWI Enable),
TWEA (TWI Enable Acknowledgement), TWINT (Clear
TWINT flag by writing a 1)
*/
TWCR = 0xC5;
}
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;
/*
Assuming the code above for the
AVR has been used with address 0x14
*/
msg[0].slave = 0x14 << 1;
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;
/*
Assuming the code above for the AVR
has been used with address 0x14
*/
msg.slave = 0x14 << 1;
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.

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