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.
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:
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.TWDR
data and address shift register, implements
start and stop as well as arbitration detection.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 dataTW_SR_DATA_ACK
: The master has sent data to the receiving slaveTW_ST_SLA_ACK
: The slave has been adressed by the master, slave will transmit dataTW_ST_DATA_ACK
: The master is requesting data from the slaveTW_BUS_ERROR
: Triggered in case of an bus errorThis 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;
}
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
Dipl.-Ing. Thomas Spielauer, Wien (webcomplains389t48957@tspi.at)
This webpage is also available via TOR at http://rh6v563nt2dnxd5h2vhhqkudmyvjaevgiv77c62xflas52d5omtkxuid.onion/