Understanding I2C

 

I2C (a.k.a. TWI, 2 Wire Interface) is a communication protocol that can allow your “master” microcontroller to speak to up 120+ “slave” devices. The protocol requires only two wires, one for data (SDA) and the other for the clock (SCL). Each device is connected to the same two wires but each also has a unique 7 bit address so it knows when the master is communicating with it. Data can go both ways, from the Master to the Slave but also from the Slave to the Master.

Data is read when the SCL goes high (a.k.a. when SCL is “released”, i.e. not pulled down, and allowed to be pulled up by the 4.7k resistors) and data can be changed while SCL is low.

After the Master sends the seven bit address of the slave it wishes to speak with, it sends Read (1) or Write (0) bit. The slave then sends an Acknowledge (ACK) – 0 / Not Acknowledge (NACK) – 1 bit. This means that each communication portion is 9 bits in total.

If the master wants to write to the slave the next communication portion will be 8 bits of data followed by an ACK/NACK from the Slave.

Pins 27 (Data) and 28 (Clock) for the Atmega 168 / 328 are the pertinent pins.

Here is my breadboard set up for testing two Atmega168 with I2C. I have I2C cables which makes changing the sender and receiver code with the atmelICE less of a chore. I first wanted to use attiny but then learned that the Attiny family does not have the same level of I2C support as the Atmega family. The Attiny Universal Serial Interface (USI) can be used with I2C but the user has to do more work in software and can rely on fewer hardware peripherals to help with the job.

While there are many examples of Atmega master codes, I found it hard to find examples of Atmega slave code. Most often we use I2C to speak with a sensor which is the slave. I was interested in microchip to microchip communication and so decided to get two atmegas talking to one another. Elliot Williams’ Make: AVR Programming was a great resource for making the code.

For the breadboard set up I have a power LED and a yellow LED for successfully sending the slave address and a green LED for successfully sending the data. On the RX side I have a yellow LED for successfully receiving the slave address and a green LED for successfully receiving the data. I have two 4.7K pull-up resistors for the SDA and SCL lines and a 10K pullup on reset. It’s powered with a 5V adapter.

Digiscope Fablab has a 100MHz 4 channel Agilent DSO-X 2014A Oscilloscope with a demo version of the I2C, SPI and USART decoder software. To get the oscilloscope into the right mode I selected the Serial button (on the right side), select the type of protocol to decode (I2C) and then the Lister Button to show the decoded data above the signals. To set the trigger press trigger and select Serial (I2C) from the Trigger Type menu and then select to Trigger on Start. Put the oscilloscope into Single Run Control mode and then connect the circuit to power and hope it triggers. If you are triggering on the power spike that occurs when you plug in the circuit, try adding a delay at the start of the code so you can comfortably do the plugging in, then setting into Single Run Mode and waiting for the trigger.

I have my signals in 1V per square and am between 200ms and 20ms per time division. I use the smaller horizontal knob to move the recorded signal side to side.

In these first examples I had difficulty getting past the first part of the I2C communication of sending the slave address (7 bits) + the Read (1) or Write (0) bit.

Eventually I could get communication going, including reading bytes sent from the slave and multiple starts during a conversation. The issue turned out to be strange, if I set both the TWEN and TWEA bits of the TWCR in the same line, I never got past the first slave address stage of the I2C conversation. When I set then in different lines everything works smoothly. Still not sure why this is the case!

Here is the code I developed (for Master):


/*
 * I2C atmega168.c
 *
 * Created: 2/22/2019 10:57:39 AM
 * Author : FablabDigiscope
 
 4.7K pull up resistors on SDA and SCL
 10K pull up on reset
 
 2x atmega168
 
 */ 

#include <avr/io.h>
#include <util/delay.h>

#define F_CPU                8000000
#define SLAVE_ADDRESS_W	     0b10010000/* 0x90: 7 bit slave address + R(1)/W(0) bit (then slave responds with ACK(0)/NACK(1) bit) */
#define SLAVE_ADDRESS_R		 0b10010001/* 0x91: 7 bit slave address + R(1)/W(0) bit (then slave responds with ACK(0)/NACK(1) bit) */

#define SLAVE2_ADDRESS_W	 0b10110000/* 0x90: 7 bit slave address + R(1)/W(0) bit (then slave responds with ACK(0)/NACK(1) bit) */
#define SLAVE2_ADDRESS_R	 0b10110001/* 0xB1*/

#define DATA_TO_SEND		 0b00000000/* 0x00 */
#define DATA_TO_SEND_2		 0b11111111/* 0xFF */
#define DATA_TO_RECEIVE		 0b10101010/* OxAA */
#define DATA_TO_RECEIVE_2	 0b01010101/* Ox55 */

//function prototypes
void i2cInit(void);
void i2cStart(void);
void i2cWaitForComplete1(void);
void i2Send(uint8_t data); //data sent same way as address
void i2cWaitForComplete2(void);
							//data sent same way as address
void i2cWaitForComplete3(void);
void i2cStop(void);
uint8_t  i2cRead(void);


int main(void)
{
	DDRB = 0b11111111; /* green LED for all good */
	DDRD = 0b11111111; /* yellow LED for all good, red for errors */

	i2cInit();
		
    while (1) 
    {
		_delay_ms(1000);
		

		i2cStart();
		i2cWaitForComplete1();
		i2Send(SLAVE_ADDRESS_W);
		i2cWaitForComplete2();
		i2Send(DATA_TO_SEND);
		i2cWaitForComplete3();
		
				i2cStart();
				i2cWaitForComplete1();
				i2Send(SLAVE_ADDRESS_R);
				i2cWaitForComplete2();
				
				if (DATA_TO_RECEIVE == i2cRead())
				{
					PORTD |= (1<<PD7); // turn on green led
				}				
				
								i2cStart();
								i2cWaitForComplete1();
								i2Send(SLAVE2_ADDRESS_W);
								i2cWaitForComplete2();
								i2Send(DATA_TO_SEND_2);
								i2cWaitForComplete3();
								i2cStop();
								
								i2cStart();
								i2cWaitForComplete1();
								i2Send(SLAVE2_ADDRESS_R);
								i2cWaitForComplete2();
								
								if (DATA_TO_RECEIVE_2 == i2cRead())
								{
									PORTD |= (1<<PD6); // turn on green led
								}
								

	}
}


//function declarations

void i2cInit(void)
{
		TWBR = 32; /*set bit rate to 100kHz*/
		TWCR |= (1 << TWEN); /*enable I2C*/
}

void i2cStart(void)
		{
			/*
	
		I2C start
					   _____________
					  |				|
		SCL: _________|				|_____
	
				  __________
				 |		    |
		SDA: ____|		    |_____________
	
		*/

		TWCR = (1 << TWEN) | (1 << TWINT) | (1 << TWSTA); /*set to enable the 2-wire serial interface ; clear the TWINT flag ; TWSTA written to one to transmit a START condition */
			
		}

void i2cWaitForComplete1(void)
	{
	
	/*I2C wait for complete*/
	while (!(TWCR & (1<<TWINT)));
	
	if ((TWSR & 0xF8) != 0x08) // 0x08 = A START condition has been transmitted
	{
		PORTD |= (1 << PD0);	//ERROR
	}
	
	if ((TWSR & 0xF8) == 0x00)
	{
		PORTD |= (1 << PD5);	//ERROR	 Bus error
	}
	}

void i2Send(uint8_t data)
{
		/*
	I2C send data
	
	
			 ___     ___	 ___	 ___	 ___	 ___	 ___	 ___	 ___
		    |	|	|	|	|	|	|	|	|	|	|	|	|	|	|	|	|	|
	SCL: ___|	|___|	|___|	|___|	|___|	|___|	|___|	|___|	|___|	|___
	

	SDA:	 D8		 D7		  D6	 D5       D4      D3      D2     D1       D0	
	
	*/

	TWDR = data; /*send data - do this BEFORE setting TWINT*/
	TWCR = (1 << TWINT) | (1 << TWEN);
	
}

void i2cWaitForComplete2(void)
{
	
/*I2C wait for complete*/
while (!(TWCR & (1<<TWINT)));

if ((TWSR & 0xF8) != 0x18) // 0x18 = SLA+W has been transmitted; ACK has been received
{
	if ((TWSR & 0xF8) == 0x20)
	{
		PORTD |= (1 << PD1);	//ERROR	 Not acknowledge received after the slave address
	}
	if ((TWSR & 0xF8) == 0x38)
	{
		PORTD |= (1 << PD2);	//ERROR	 Arbitration lost in slave address or data byte
	}
	if((TWSR & 0xF8) == 0x68)
	{
		PORTD |= (1 << PD3);	//ERROR	 Arbitration lost and addressed as slave
	}
	if ((TWSR & 0xF8) == 0x00)
	{
		PORTD |= (1 << PD5);	//ERROR	 Bus error
	}
}
}
void i2cWaitForComplete3(void)
{
	
	/*I2C wait for complete*/
	
	while (!(TWCR & (1<<TWINT)));

	if ((TWSR & 0xF8) != 0x28) //0x28 = Data byte has been transmitted; ACK has been received
	{
		if ((TWSR & 0xF8) == 0x30)
		{
			PORTD |= (1 << PD4);	//ERROR	Not acknowledge received after a data byte
		}
		if ((TWSR & 0xF8) == 0x00)
		{
			PORTD |= (1 << PD5);	//ERROR	 Bus error
		}
	}
}

void i2cStop(void)
{
	/*
	
	I2C stop
	
				   _____________
				  |				|
	SCL: _________|				|_____
	
					  ________________
					 |		    
	SDA: ____________|		    
	
	*/

	TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWSTO);
	//_delay_us(10);
}

uint8_t i2cRead(void)
{
	
	TWCR = (1 << TWINT) | (1 << TWEN); //read with no ack
	while (!(TWCR & (1<<TWINT)));
	return (TWDR);
}

Here is the code I developed (for Slave):


/*
 * atmega I2C slave.c
 *
 * Created: 2/27/2019 7:02:34 PM
 * Author : FablabDigiscope
 */ 

#include <avr/io.h>


#define F_CPU                8000000
#define SLAVE_ADDRESS_W	     0b10010000/* 0x90: 7 bit slave address + R(1)/W(0) bit (then slave responds with ACK(0)/NACK(1) bit) */
#define SLAVE_ADDRESS_R		 0b10010001/* 0x91: 7 bit slave address + R(1)/W(0) bit (then slave responds with ACK(0)/NACK(1) bit) */

#define SLAVE2_ADDRESS_W	 0b10110000/* 0x90: 7 bit slave address + R(1)/W(0) bit (then slave responds with ACK(0)/NACK(1) bit) */
#define SLAVE2_ADDRESS_R	 0b10110001/* 0xB1*/

#define DATA_TO_SEND		 0b00000000/* 0x00 */
#define DATA_TO_SEND_2		 0b11111111/* 0xFF */
#define DATA_TO_RECEIVE		 0b10101010/* OxAA */
#define DATA_TO_RECEIVE_2	 0b01010101/* Ox55 */

int main(void)
{
	DDRB = 0b11111111;
	DDRD = 0b11111111;

    while (1) 
    {


	TWAR = SLAVE2_ADDRESS_W;    // Fill slave address in TWAR register
	TWCR |= (1 << TWEN);  // I2c enable; MYSTERY: if I set these two bits at the same time nothing works.
	TWCR |= (1 << TWEA);  // enable acknowledge (automatically generates ACK)
	
	while (!(TWCR & (1<<TWINT))); // interrupt flag which tells slave when it's address is being called
	
		if((TWSR & 0xF8) == 0x60) // SLA+W received, ACK'd
		{
			PORTB |= (1<<PB0);
		}
		
			
		if((TWSR & 0xF8) == 0x80) // Data received, ACK'd...
		{
			if (TWDR == DATA_TO_SEND_2) // ...and data matches what was send
				{
				PORTD |= (1<<PD7);
				}
		}
		

	while (!(TWCR & (1<<TWINT))); // interrupt flag which tells slave when it's address is being called
	
			if((TWSR & 0xF8) == 0xA8) // Own SLA+R has been received; ACK has been returned
			{
				TWDR = DATA_TO_RECEIVE_2;
			}
			
		while (!(TWCR & (1<<TWINT))); // interrupt flag which tells slave when it's address is being called
		
		if((TWSR & 0xF8) == 0xC0) // Data byte in TWDR has been transmitted; NOT ACK has been received
		{
			PORTD |= (1<<PD6);
		}
		
// 		if((TWSR & 0xF8) == 0xB8) // Data byte in TWDR has been transmitted; ACK has been received
// 			PORTD |= (1<<PD6);
// 		}	
		
// 		if((TWSR & 0xF8) == 0xC8) // Own SLA+R has been received; ACK has been returned
// 		{
// 			PORTD |= (1<<PD6);
// 		}
// 			
	
	}
}

In the end there were so many issues that I had added LEDs for each different error type with little notes describing the error in question. This experience also convinced me that it would help to have a dedicated I2C board for first-timers.