You are on page 1of 11

Programming AVR I2C interface

embedds.com /programming-avr-i2c-interface/

admin

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;
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;
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

You might also like