Programming AVR I2C interface

I2C (also referred as IIC or TWI) is widely used interface in embedded applications. Two wire bus initially was used by Philips and become a standard among chip vendors. I2C bus consists of two lines called Serial Data Line (SDA) and Serial Clock Line (SCL). Communication is relatively fast and short distance mainly used to communicate between sensors, RTC, EEPROM, LCD. I2C protocol allows up to 128 devices connected to those two lines where each of them has unique address. Communication between devices is master and slave based. Master generates clock signal, initiates and terminates data transfer.

From electrical point of view I2C devices use open drain (open collector) pins. In order to operate correctly SDA and SCL lines require pull up resistors. Typically 4.7kΩ resistors are used.

Each communication is initiated by START signal and finished by STOP. These are always generated by master. START and STOP signals are generated by pulling SDA line low while SCL line is high. In other cases when data is transferred data line must be stable during clock high and can be changed when clock is low:

Bus is considered to be busy between START and STOP signals. So if there are more than one master each of them has to wait until bus is freed by current master with STOP signal.

I2C communication packet consists of several parts:

START signal;

Address packet – seven address bits lead by data direction bit (read or write) + acknowledge bit;

Data packet – eight data bits + acknowledge bit;

STOP signal.

Acknowledge bit is a ninth bit of every byte sent. Receiver always has to confirm successful receive with ACK by pulling SDA low or in case receiver cannot accept data it will leave SDA high (NACK) so master could stop transmitting and do other scenario if needed.

I2C devices can work in four different modes:

  1. Master Transmitter – initiates transfer sends data to slave device;
  2. Master Receiver – initiates transfer reads data from slave device;
  3. Slave Transmitter – waits for master request and then sends data;
  4. Slave Receiver – waits for master transmission and accepts data.

It is worth mention that I2C interface supports multi-master transmission. It doesn’t mean that all master are able to transmit data at same time. As matter of fact each master must wait for current transmission to be finished and then can initiate transfer. It may be situation when multiple masters tries to initiate transfer. In this case so called arbitration happens where each transmitter check level of bus signal and compares it with expected. If master loses arbitration it must leave bus immediately and/or switch to slave mode.

Bursting multiple bytes

I2C can send and receive multiple bytes inside single packet. This is handy for instance to write or read memory. For instance we need to read 3 bytes from EEPROM memory address 0x0F. Say that EEPROM slave address is 0x1111000. This is how whole reading process would look:

Note that first master has to write in order to select initial memory address. Then send start signal again in order to initiate master read mode and then after reading of all bytes is done free line by sending stop signal.

AVR I2C registers

AVR microcontroller is using TWI (Two Wire Interface) nomenclature when talking about I2C. So all registers are named as TWI.

First important register is bit rate register TWBR. It is used to scale down CPU frequency in to SCL. Additionally there are two bits (TWPS1 and TWPS2) in status register TWSR to prescale SCL frequency with values 1, 4, 16 and 64. You can find formula in datasheet that is used to calculate SCL end frequency:

As usually there is control register TWCR which has a set of bits that are used to enable TWI interrupt, TWI enable, Start, Stop.

Status register TWSR holds earlier mentioned prescaller bits but its main purpose to sense I2C bus status with TWS[7:3] bits. TWDR is data register which is used to hold next byte to transmit or received byte. TWAR and TWARM register are used when AVR works as I2C slave.

Example of using I2C in AVR

As example we are going to interface old good 24C16 I2C EEPROM chip to Atmega328P.

As you can see connection is simple – only SDA and SCL lines has to be connected. For different EEPROM capacities you may need to connect A0, A1 and A2 pins to GND or pull high in order to set device address. 24C16 doesn’t use these pins for addressing chip. We leave them open. For demonstration I am using Arduino328P board which is used as general AVR test board.

You can use any other AVR development board to test this example. If you have Arduino board laying arround I suggest not to clear original bootloader by writing hex with some ISP adapter, but use built in bootloader to upload hex. Download http://russemotto.com/xloader/ program that communicates to bootloader so as from Arduino IDE:

Hardware is really simple lets head to software writing.

For debugging purposes USART is used which was discussed in earlier tutorials. So we are gonna set it to 9600 baud and use as library usart.h.

In order to have nice control of I2C interface lets split whole process in to multiple functions. First of all we need to initialize TWI (I2C):

void TWIInit(void)
{
	//set SCL to 400kHz
	TWSR = 0x00;
	TWBR = 0x0C;
	//enable TWI
	TWCR = (1<<TWEN);
}

so we set bit rate register to 0x0C value which sets SCL to 400kHz. We don’t need any additional prescallers so set TWSR to 0. And finally we simply enable TWI by setting TWEN bit to “1”.

Next we take care of TWIStart and TWIStop functions that generate start and stop signals.

void TWIStart(void)
{
	TWCR = (1<<TWINT)|(1<<TWSTA)|(1<<TWEN);
	while ((TWCR & (1<<TWINT)) == 0);
}
//send stop signal
void TWIStop(void)
{
	TWCR = (1<<TWINT)|(1<<TWSTO)|(1<<TWEN);
}

For start we need to set TWSTA and for stop TWSTO bits along with TWINT and TWEN bits. After start signal is sent we need to wait for status (until TWINT resets to zero).

Another function is TWIWrite:

void TWIWrite(uint8_t u8data)
{
	TWDR = u8data;
	TWCR = (1<<TWINT)|(1<<TWEN);
	while ((TWCR & (1<<TWINT)) == 0);
}

it writes data byte to TWDR register which is shifted to SDA line. It is important to wait for transmission complete within while loop. After which status can be read from status register TWSR.

Reading is done in similar way. I have wrote two functions where one transmits ACK signal after byte transfer while another doesn’t:

uint8_t TWIReadACK(void)
{
	TWCR = (1<<TWINT)|(1<<TWEN)|(1<<TWEA);
	while ((TWCR & (1<<TWINT)) == 0);
	return TWDR;
}
//read byte with NACK
uint8_t TWIReadNACK(void)
{
	TWCR = (1<<TWINT)|(1<<TWEN);
	while ((TWCR & (1<<TWINT)) == 0);
	return TWDR;
}

And finally last function we gonna use is reading status:

uint8_t TWIGetStatus(void)
{
	uint8_t status;
	//mask status
	status = TWSR & 0xF8;
	return status;
}

We need to read upper five bits from TWSR register so we simply mask out three lower bits. As we will see reading status messages is essential part in detecting failures in I2C communication.

After we set up TWI functions we can use them to communicate with 24C16 EEPROM chip. This chip contains 2048 bytes of EEPROM memory in order to address all bytes 11 byte addressing is used. 24Cxx chips have four high bit fixed ID which his 0b1010 lower three bits are used for addressing chip memory. This way we avoid sending two bytes for memory addressing memory. But instead we need to split 11 bits in to fit three high bits that goes to device ID 1, 2, 3 bit locations while rest byte is sent next as normal address selection.

Having this in mind we can implement EEPROM byte write function:

uint8_t EEWriteByte(uint16_t u16addr, uint8_t u8data)
{
	TWIStart();
	if (TWIGetStatus() != 0x08)
		return ERROR;
	//select devise and send A2 A1 A0 address bits
	TWIWrite((EEDEVADR)|(uint8_t)((u16addr & 0x0700)>>7));
	if (TWIGetStatus() != 0x18)
		return ERROR;	
	//send the rest of address
	TWIWrite((uint8_t)(u16addr));
	if (TWIGetStatus() != 0x28)
		return ERROR;
	//write byte to eeprom
	TWIWrite(u8data);
	if (TWIGetStatus() != 0x28)
		return ERROR;
	TWIStop();
	return SUCCESS;
}

As you can see after each TWI command we check status. Status codes can be found on AVR datasheet. In case of communication failure we return ERROR. Using status codes may help to track bugs in program or detect hardware failures.

Before writing byte to memory we first start I2C communication then we write device device address combined with three high memory address bits. Lowest device bit is “0” for write.

Next byte we send is 8 lower memory address bits and then finally if we get ACK by checking status (0x18) we send data byte. Lastly we end communication by sending Stop signal.

Reading requires a bit more code:

uint8_t EEReadByte(uint16_t u16addr, uint8_t *u8data)
{
	//uint8_t databyte;
	TWIStart();
	if (TWIGetStatus() != 0x08)
		return ERROR;
	//select devise and send A2 A1 A0 address bits
	TWIWrite((EEDEVADR)|((uint8_t)((u16addr & 0x0700)>>7)));
	if (TWIGetStatus() != 0x18)
		return ERROR;
	//send the rest of address
	TWIWrite((uint8_t)(u16addr));
	if (TWIGetStatus() != 0x28)
		return ERROR;
	//send start
	TWIStart();
	if (TWIGetStatus() != 0x10)
		return ERROR;
	//select devise and send read bit
	TWIWrite((EEDEVADR)|((uint8_t)((u16addr & 0x0700)>>7))|1);
	if (TWIGetStatus() != 0x40)
		return ERROR;
	*u8data = TWIReadNACK();
	if (TWIGetStatus() != 0x58)
		return ERROR;
	TWIStop();
	return SUCCESS;
}

Because first we need to select memory address by writing device ID and rest of memory address as write command. Then after this we repeat START signal and then we send device address with read command (last bit set to “1”). If read status is OK we can store received data in to variable. For single byte we don’t need to send ACK signal just STOP.

Similarly EEPROM page write and read are implemented. 24C16 is divided in to 128 pages of 16 bytes . Each page start address is located in high 7 bits of address. When writing page be sure to start from first byte of page because if page address reaches its end address rols-over and writing starts from beginning of page. This way you can overwrite existing data. I’m just giving my way of page write and read implementation:

uint8_t EEWritePage(uint8_t page, uint8_t *u8data)
{
	//calculate page address
	uint8_t u8paddr = 0;
	uint8_t i;
	u8paddr = page<<4;
	TWIStart();
	if (TWIGetStatus() != 0x08)
		return ERROR;
	//select page start address and send A2 A1 A0 bits send write command
	TWIWrite(((EEDEVADR)|(u8paddr>>3))&(~1));
	if (TWIGetStatus() != 0x18)
		return ERROR;
	//send the rest of address
	TWIWrite((u8paddr<<4));
	if (TWIGetStatus() != 0x28)
		return ERROR;
	//write page to eeprom
	for (i=0; i<16; i++)
	{
		TWIWrite(*u8data++);
			if (TWIGetStatus() != 0x28)
				return ERROR;
	}
	TWIStop();
	return SUCCESS;
}
uint8_t EEReadPage(uint8_t page, uint8_t *u8data)
{
	//calculate page address
	uint8_t u8paddr = 0;
	uint8_t i;
	u8paddr = page<<4;
	TWIStart();
	if (TWIGetStatus() != 0x08)
		return ERROR;
	//select page start address and send A2 A1 A0 bits send write command
	TWIWrite(((EEDEVADR)|(u8paddr>>3))&(~1));
	if (TWIGetStatus() != 0x18)
		return ERROR;
	//send the rest of address
	TWIWrite((u8paddr<<4));
	if (TWIGetStatus() != 0x28)
		return ERROR;
	//send start
	TWIStart();
	if (TWIGetStatus() != 0x10)
		return ERROR;
	//select devise and send read bit
	TWIWrite(((EEDEVADR)|(u8paddr>>3))|1);
	if (TWIGetStatus() != 0x40)
		return ERROR;
	for (i=0; i<15; i++)
	{
		*u8data++ = TWIReadACK();
			if (TWIGetStatus() != 0x50)
				return ERROR;
	}	
	*u8data = TWIReadNACK();
	if (TWIGetStatus() != 0x58)
		return ERROR;
	TWIStop();
	return SUCCESS;
}

As you can see when receiving multiple bytes ACK must e generated after each reception. Juster after final byte ACK is not needed.

In main program you can see EEPROM testing routines that shows everything is working correctly. Test routine checks single byte write to custom address location and then reading. In terminal screen you can view if written and read results are same. Also a page write test is done. It writes 16 bytes of information to page 5 and then reads them to different buffer. Then write and read buffers are compared and if both are equal – a success message is displayed in terminal screen. Main program:

#include <stdio.h>
#include <avr/io.h>
#include <avr/pgmspace.h>
#include "usart.h"
#include "ee24c16.h"
//set stream pointer
FILE usart0_str = FDEV_SETUP_STREAM(USART0SendByte, USART0ReceiveByte, _FDEV_SETUP_RW);
int main(void)
{
	uint8_t u8ebyte;
	uint8_t u8erbyte;
	uint16_t u16eaddress = 0x07F0;
	uint8_t page = 5;
	uint8_t i;
	uint8_t eereadpage[16];
	uint8_t eewritepage[16] = { 10, 44, 255, 46, 80, 87, 43, 130,
								210, 23, 1, 58, 46, 150, 12, 46 };
//Initialize USART0
USART0Init();
//
TWIInit();
//assign our stream to standard I/O streams
stdin=stdout=&usart0_str;
printf("\nWrite byte %#04x to eeprom address %#04x", 0x58, u16eaddress);
if (EEWriteByte(u16eaddress, 0x58) != ERROR)
{
	printf_P(PSTR("\nRead byte From eeprom"));
	if (EEReadByte(u16eaddress, &u8ebyte) != ERROR)
	{
		printf("\n*%#04x = %#04x", u16eaddress, u8ebyte);
	}
	else printf_P(PSTR("\nStatus fail!"));

}	
else printf_P(PSTR("\nStatus fail!"));
	
printf_P(PSTR("\nWriting 16 bytes to page 5 "));
if(EEWritePage(page, eewritepage) != ERROR)
{
	printf_P(PSTR("\nReading 16 bytes from page 5 "));
	if (EEReadPage(page, eereadpage) != ERROR)
	{
		//compare send and read buffers
		for (i=0; i<16; i++)
		{
			if (eereadpage[i] != eewritepage[i])
			{
				break;
			}		
				else continue;
		}
		if (i==16)
			printf_P(PSTR("\nPage write and read success!"));
		else
			printf_P(PSTR("\nPage write and read fail!"));
	} else printf_P(PSTR("\nStatus fail!"));

}else printf_P(PSTR("\nStatus fail!"));

printf_P(PSTR("\nContinue testing EEPROM from terminal!"));
    while(1)
    {
		printf("\nEnter EEPROM address to write (MAX = %u): ", EEMAXADDR);
		scanf("%u",&u16eaddress);
		printf("Enter data to write to EEPROM at address %u: ", u16eaddress);
		scanf("%u",&u8ebyte);
		printf_P(PSTR("\nWriting..."));
		EEWriteByte(u16eaddress, u8ebyte);
		printf_P(PSTR("\nTesting..."));
		if (EEReadByte(u16eaddress, &u8erbyte) !=ERROR)
			{
				if (u8ebyte==u8erbyte)
					printf_P(PSTR("\nSuccess!"));
				else
					printf_P(PSTR("\nFail!"));
			}
			else printf_P(PSTR("\nStatus fail!"));

        //TODO:: Please write your application code 
    }
}

You can play around by sending data bytes to custom address locations from terminal screen and test various data memory locations.

AVR Studio 5 project files for download [I2CEE.zip]. Have fun!

Read

Bookmark the permalink.

13 Comments

  1. AVR studio 5 can’t seem to understand your project? What am I doing wrong I wonder?

  2. This project was created with Avr Studio 5.1 so AVR Studio 5.0 won’t understand it. Upgrade studio to latest release or simply create new project and import existing source files.

  3. Done deed. Compiled perfectly on 5.1.

  4. Hello, I like your code, it is fine, but what is the ERROR function, how does it to implement?

    and what will happens in the loops while if the response never done?

    thanks

  5. Really simple and easy tutorial on AVR I2C. Thanks!

  6. How to select SCL frequency of atmega 8?

  7. For interfacing with ds1307?

  8. I have a board (with ATMega328) where only MISO/MOSI pins are broken out. Is it possible to configure TWI to use those two pins? Or would i have to do “software”/bitbang TWI on these?

  9. The only option is to use software TWI library. You can find few good libraries by googling. For instance the first that pops out:
    http://extremeelectronics.co.in/avr-tutorials/software-i2c-library-for-avr-mcus/

  10. void TWI_stop(void) {
    TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWSTO);
    while (TWCR & (1<<TWSTO));
    }

  11. Thank GOD for this page!
    Been trying to do I2C on an Olimex MOD-IO for ages, using AVR315 and the datasheet etc.
    Tried this, with small mod to the write function as I am writing to a MCP23017 and it worked first time, genuinely shocking me!

  12. thanks for tutorial.
    but what’s EEDEVADR?

  13. EEDEVADR is EEPROM chip address on I2C line.
    In this example:
    #define EEDEVADR 0b10100000

Add Comment Register



Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>