Programming the I2C protocol

This 14th article in the series of “Do It Yourself: Electronics”, demonstrates programming the I2C protocol for the AVR micro-controller ATmega16.

<< Previous Article

Equipped with debugging power, Pugs dwelled further into stuff of more involved programming – the protocols – specifically the I2C protocol.

What is I2C protocol?

It is a 2-wire protocol to communicate between a master controller with its slave devices, which understand the same protocol. Physically, it consists of communication over two wires or lines, namely SDA and SCL – the data & clock lines. The data could be clocked either at 100kbps (standard mode) or 400kbps (fast mode). Many slaves can be connected on the same I2C bus. Hence, each slave is identified by a unique 7-bit address. Overall the communication can be summarized as follows:

For Master Transmission to Slave, a single transaction typically goes like this:

  • Indicate START of communication
  • Send out the slave address of the device to transmit to
  • Send out the data
  • Indicate STOP of communication

There may be confirmation handshakings using acknowledgments at various stages of the transaction.

For Master Reception from Slave, a single transaction typically goes like this:

  • Indicate START of communication
  • Send out the slave address of the device to receive from
  • Receive the data
  • Indicate STOP of communication

Here also, there may be confirmation handshakings using acknowledgments at various stages of the transaction, except typically the last one.

Many a times a transaction consisting of both transmission & reception is needed. That would go as follows:

  • Indicate START of communication
  • Send out the slave address of the device to transmit to
  • Send out the data
  • Indicate (re)START of communication
  • Send out the slave address of the device to receive from
  • Receive the data
  • Indicate STOP of communication

Notice that there is no stop indication between the transmit and receive, and the second start is very often termed as restart.

I2C on ATmega16

Based on the above principle, the 2-wire interface (TWI) for ATmega16 to behave as a master is implemented using just 4 registers:

  • Bit rate register TWBR to set up the data transfer rate along with TWPS bits in TWSR register
  • Control register TWCR for all controls including start (TWSTA), stop (TWSTO), ack (TWEN), enable (TWEN), interrupt (TWINT) using corresponding bits mentioned in the ()
  • Status register TWSR for the transaction status
  • Data register TWDR for transmitting and receiving data

Now, formula for the bit rate is given by:
f_{SCL} = \frac{f_{cpu}}{16 + 2 . {TWBR} . 4^{TWPS}}
where TWBR & TWPS are to be set in the ATmega16 registers.

On analyzing the formula, one would figure out that, we may not be able to use TWI at either 100kbps or 400kbps with the 1 MHz (f_{cpu}) default clock of the ATmega16, as there exists no values for TWBR & TWPS to achieve the same. So, we would need to change the fuse settings of ATmega16, to get a different clock. We would do that when downloading our code, say to 8MHz, the maximum possible without an external crystal. Assuming that, the values for TWBR and TWPS for the standard and fast bit rates would come out as follows:

  • Standard: 32 & 0
  • Fast: 2 & 0

Using all these information as per the ATmega16 datasheet pg 177-200, the TWI can be prototyped & programmed as follows in twi.h & twi.c, respectively. The header twi.h would be:

#ifndef TWI_H
#define TWI_H

typedef enum
{
	standard,
	fast
} TwiMode;

void twi_init(TwiMode mode);
void twi_shut(void);
int twi_master_tx(uint8_t addr, uint8_t *data, int len);
int twi_master_rx(uint8_t addr, uint8_t *data, int len);
int twi_master_tx_rx(uint8_t addr, uint8_t *tx_data, int tx_len, uint8_t *rx_data,
			int rx_len);
#endif

And the complete implementation of twi.c goes here:

#include <avr/io.h>

#include "twi.h"

#define QUIT_TWI_OP { send_stop(); return -1; }

typedef enum
{
	/* TWI Master Status Codes */
	st_start = 0x08,
	st_restart = 0x10,
	st_sla_w_ack = 0x18,
	st_sla_w_noack = 0x20,
	st_data_w_ack = 0x28,
	st_data_w_noack = 0x30,
	st_arb_lost = 0x38,
	st_sla_r_ack = 0x40,
	st_sla_r_noack = 0x48,
	st_data_r_ack = 0x50,
	st_data_r_noack = 0x58,
} TwiStatus;
typedef enum
{
	dir_write,
	dir_read
} TwiOperation;

void twi_init(TwiMode mode)
{
	// 1 = output, 0 = input
	DDRC &= ~0b00000011; // PC0 = SCL; PC1 = SDA
	PORTC |= 0b00000011; // Internal pull-up on both lines

	TWBR = (mode == standard) ? 32 : 2;
	TWSR &= ~(0b11 << TWPS0); // Clearing TWSP to 0

	TWCR |= (1 << TWEN); // Enable TWI, generating the SCLK
}
void twi_shut(void)
{
	TWCR &= ~(1 << TWEN); // Disable TWI

	TWBR = 0;
	TWSR &= ~(0b11 << TWPS0);

	// 1 = output, 0 = input
	DDRC &= ~0b00000011; // PC0 = SCL; PC1 = SDA
	PORTC &= ~0b00000011; // Clear pull-up on both lines
}

static uint8_t get_status(uint8_t status)
{
	uint8_t st;

	while (!(TWCR & (1 << TWINT)))
		;
	if ((st = (TWSR & 0xF8)) == status)
		return 0;
	else
		return st;
}
static int send_start(uint8_t status)
{
	TWCR = (1 << TWINT) | (1 << TWSTA) | (1 << TWEN);
	return get_status(status);
}
static void send_stop(void)
{
	TWCR = (1 << TWINT) | (1 << TWSTO) | (1 << TWEN);
}
static int send_data(uint8_t data, uint8_t status)
{
	TWDR = data;
	TWCR = (1 << TWINT) | (1 << TWEN);
	return get_status(status);
}
static int recv_data(uint8_t *data, uint8_t status, uint8_t ack)
{
	TWCR = (1 << TWINT) | (ack << TWEA) | (1 << TWEN);
	if (get_status(status) == 0)
	{
		*data = TWDR;
		return 0;
	}
	else
	{
		return -1;
	}
}

int twi_master_tx(uint8_t addr, uint8_t *data, int len)
{
	int i;

	if (send_start(st_start)) QUIT_TWI_OP;
	if (send_data((addr << 1) | dir_write, st_sla_w_ack)) QUIT_TWI_OP;
	for (i = 0; i < len; i++)
	{
		if (send_data(data[i], st_data_w_ack)) QUIT_TWI_OP;
	}
	send_stop();
	return 0;
}
int twi_master_rx(uint8_t addr, uint8_t *data, int len)
{
	int i;

	if (send_start(st_start)) QUIT_TWI_OP;
	if (send_data((addr << 1) | dir_read, st_sla_r_ack)) QUIT_TWI_OP;
	for (i = 0; i < len - 1; i++)
	{
		if (recv_data(&data[i], st_data_r_ack, 1)) QUIT_TWI_OP;
	}
	if (recv_data(&data[i], st_data_r_noack, 0)) QUIT_TWI_OP;
	send_stop();
	return 0;
}
int twi_master_tx_rx(uint8_t addr, uint8_t *tx_data, int tx_len, uint8_t *rx_data,
			int rx_len)
{
	int i;

	if (send_start(st_start)) QUIT_TWI_OP;
	if (send_data((addr << 1) | dir_write, st_sla_w_ack)) QUIT_TWI_OP;
	for (i = 0; i < tx_len; i++)
	{
		if (send_data(tx_data[i], st_data_w_ack)) QUIT_TWI_OP;
	}
	if (send_start(st_restart)) QUIT_TWI_OP;
	if (send_data((addr << 1) | dir_read, st_sla_r_ack)) QUIT_TWI_OP;
	for (i = 0; i < rx_len - 1; i++)
	{
		if (recv_data(&rx_data[i], st_data_r_ack, 1)) QUIT_TWI_OP;
	}
	if (recv_data(&rx_data[i], st_data_r_noack, 0)) QUIT_TWI_OP;
	send_stop();
	return 0;
}

All coded for the AVR master, but how to test without a slave. Exactly for that, we need a I2C device, say a real time clock (RTC). We would connect one, say the chip DS1307, and try it out in our next article. Till then, thrash out your understanding on the above I2C implementation and get the DS1307 chip and 32768Hz (clock) crystal ready.

Next Article >>

Anil Kumar Pugalia (123 Posts)

The author is a hobbyist in open source hardware and software, with a passion for mathematics, and philosopher in thoughts. A gold medallist from the Indian Institute of Science, Linux, mathematics and knowledge sharing are few of his passions. He experiments with Linux and embedded systems to share his learnings through his weekend workshops. Learn more about him and his experiments at https://sysplay.in.


   Send article as PDF   

6 thoughts on “Programming the I2C protocol

  1. Pingback: Debugging Micro-controller Programs over USART | Playing with Systems

  2. Pingback: Accessing the Real Time Clock | Playing with Systems

Leave a Reply

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