Montag, 23. Mai 2016

TWI (or I2C) communication

Introduction

The I2C protocol is used for communication between different parts (up to 127 participants) of a system using only two wires. I will show a very simple example with two ATMEGA8 microcontrollers that communicate via this protocol. Atmel calles this not I2C, but TWI (two wire interface). A detailed description is on page 157 of the ATMEGA8 datasheet. It is quite long and there are many ways of using the TWI interface. It takes a bit of time reading the section and understand what is going on. The example here can be used as a "quickstart" guide into this protocol and to get some working code as a starting point for own implementations.
The following picture shows the topology of a TWI network: all participants are connected to two wires that are held at VCC potential by two pull-up resistors. There is a clock line (SCL) and a data line (SDA). Communication happens always between exactly two participants at one time (with the exception of a general call, but I'll not describe that). The participant that initiates the communication is called "master" and adresses another participant which is called "slave". As part of each transmission, the master has to sent the address of the slave. All slaves are continuously monitoring their TWI lines and have to react if their address is called.

modified picture from the ATMEGA8 datasheet, showing the layout of my small example with one master and one slave, indicated in red
My example will be the following: one ATMEGA8 measures an analog voltage level with it's ADC and transmit the information over a TWI connection to another ATMEGA8 for visualization. The measuring ATMEGA8 will provide the data as a TWI slave and I give it the address 0x01. The displaying ATMEGA8 will be the master and initiates the transmission. In this configuration, my TWI slave will always be in a "slave transmit" mode, and my master will always be in a "master receive" mode. Other combinations "slave receive" and "master transmit" are also possible, but I want to keep the example small.
I will use the TWI in connection with interrupts, so some basic knowledge of interrupt mechanisms is needed to understand this example.
The following picture shows a schematic drawing of my setup. I didn't draw the connections to the ISP programmer. Note that the two devices really are only connected by the power lines and the two TWI wires (in blue).  I used ADC3 input to connect a variable voltage divider. To keep things simple, I only transmit a single byte over the TWI interface. The two least significant bits from the 10-bit ADC value are truncated and only the 8 most significant bits are transferred. The reset pins of both controllers are connected to ensure that both start working at the same time.



The implementation on a bread board looks like this:


Relevant TWI Registers

The TWI subsystem in the ATMEGA8 is controlled by some registers. These are described on pages 165-167 of the datasheet.
The TWI Bit Rate Register (TWBR) contains a divisor for the clock frequency and determines the TWI clock speed. I leave that register untouched, which will result in a TWI clock speed that is 1/16 of the CPU clock speed.

The TWI Control Register (TWCR) is used to enable the TWI hardware (set TWEA bit to 1) and is also used to interact with the hardware between transmission actions (via the TWINT bit). In TWI tranmissions, the control is passed back and forth between the user application program and the TWI hardware on the chip. The application code has to set some bits in that register according to what the next TWI action should be. After setting the TWINT bit, the TWI hardware starts working again - independent from the application code. It will first clear the TWINT bit. Whenever the TWI hardware is done with its actions, it sets the TWINT bit again. The application code can either poll that bit to check when the TWI hardware is done, or it can request an interrupt that is triggered by the TWI hardware. In this example I've chosen the latter possibility.
If the application code is notified by the TWINT bit in the TWCR register, it can obtain the status of the TWI hardware from the TWS[3:7] bits in the TWI Status Register (TWSR).
For all four operation modes of the TWI interface (master transmit, master receive, slave transmit, slave receive) the application code has a finite number of options. All of them are listed in the ATMEGA8 datasheet in tables.

After the TWINT bit was set (and an interrupt was triggered) the application code might find the number 0xA8 in the TWS[3:7] bits. The table lists all the possible options, which are chosen by setting or clearing certain combination of bits in the TWSTA, TWSTO and TWEA bits in the TWCR register. After setting the desired action, the TWI hardware is activated again by setting the TWINT bit again. Given the tables for all four communication modes, there are many many ways of realizing communication between two TWI devices. Often, the slave will be a given part (e.g. a sensor with I2C interface) and its behavior will be documented in its datasheet. Using an ATMEGA8 to read such a sensor requires to implement the correct a master behavior. In this demonstration example, both devices will be ATMEGA8. I have implemented a very simple (might be the simplest possible) slave behavior in the following code.

Slave Transmitter Program

When the TWI was addressed, the interrupt routine ISR(TWI_vect) is called, and the switch statement will go to the first case. The transmission data is loaded into the TWI Data Register (TWDR) and the first row of the above table is chosen, i.e. indicating the last data byte. In the case of only one data byte, the first byte is also the last.
After sending the byte, the TWI hardware will get the acknowledge signal from the master. Then, the TWEA bit has to be set again which tells the TWI hardware to monitor the address line and trigger another interrupt if its address is called. The transferred data is generated by the ADC, using only the 8 most significant bits of the 10-bit ADC value.
#include <avr/io.h>
#include <avr/interrupt.h>

#define F_CPU 1000000UL  // 1 MHz
#include <util/delay.h>
#include <util/twi.h>

#define SLAVE_ADDRESS (0x01)

uint8_t adc_value; // the value to send

// interrupt routine for TWI message handling
ISR(TWI_vect)
{
  // react on TWI status and handle different cases
  uint8_t status = TWSR & 0xFC; // mask-out the prescaler bits
  switch(status)
  {
    case TW_ST_SLA_ACK:   // own SLA+R received, acknoledge sent
         TWDR = adc_value;
         TWCR &= ~((1<<TWSTO) | (1<<TWEA));
    break;
    
    case TW_ST_LAST_DATA: // last byte transmitted ACK received     
         TWCR |= (1<<TWEA); // set TWEA to enter slave mode
    break;
    }
    TWCR |= (1<<TWINT);  // set TWINT -> activate TWI hardware
}

int main(void)
{
  // TWI setup
  sei(); // enable global interrupt
 
  // set slave address to 0x01, ignore general call
  TWAR = (SLAVE_ADDRESS << 1) | 0x00;
  // TWI-ENable , TWI Interrupt Enable
  TWCR |= (1<<TWEA) | (1<<TWEN) | (1<<TWIE); 
    
  // ADC setup
  ADCSRA |= (1<<ADEN);
  ADMUX  |= ( (1<<REFS1) | (1<<REFS0) ); // select internal reference
  ADMUX  |= 3;   // select channel 5 (pin ADC5)
 
  // infinite loop
  for (;;)
  {
    ADCSRA |= (1<<ADSC);        // start single measurement
    while(ADCSRA & (1<<ADSC));  // wait until measurement done
    
    // read result bytes (low and high) and reduce to 8-bits
    adc_value = ADCL;
    adc_value >>= 2;          // drop 2 least significant bits
    adc_value |= (ADCH << 6); // add two most significant bits
  }
}

Master Receiver Program

Another ATMEGA8 is loaded with the following program that can obtain the ADC value from the slave transmitter described above. The transmission is initialized by setting the TWSTA bit in the TWCR register. This only makes sense if there is no other transmission in progress, as indicated by the ongoing_transmission global variable. If the transmission is initiated, the address of the slave is given by the content of the data register in the first case of the switch statement: the 7 most significant bits of the TWDR register contain the address, while the least significant bit is 1 for a reading action and 0 for a writing action (from the perspective of the master). In this example we are a master receiver and we want to read. Our TWDR content is thus (SLAVE_ADDRESS << 1) | 0x01;
In the next TWI interrupt, we have the confirmation of the slave that it has acknowledged our call. We say that we want to acknowledge after receiving the data in the next TWI action (the TWEA bit remains set). After the next interrupt, the data is available in the TWDR data register. The only thing left to do is to send a stop condition by setting the TWSTO bit.
In the first and last case of the switch statement, the ongoing_transmission variable is managed to indicate to the main program that a transmission is ongoing.
#include <avr/io.h>
#define F_CPU 1000000UL  // 1 MHz
#include <util/delay.h>

#include <avr/interrupt.h>
#include <util/twi.h>

#define SLAVE_ADDRESS (0x01)

uint8_t value; // contains the received value

uint8_t ongoing_transmission = 0;

// interrupt routine for the timer0 overflow interrupt
ISR(TWI_vect)
{
  // react on TWI status and handle different cases
  uint8_t status = TWSR & 0xFC; // mask-out the prescaler bits
  switch(status)
  {
    case TW_START:  // start transmitted
         ongoing_transmission = 1;
         // write SLA+R, SLA=0x01
         TWDR = (SLAVE_ADDRESS << 1) | 0x01;
         TWCR &= ~((1<<TWSTA)); // clear TWSTA
    break;
  
    case TW_MR_SLA_ACK: // SLA+R transmitted, ACK received 
         TWCR &= ~((1<<TWSTA) | (1<<TWSTO)); 
    break;
  
    case TW_MR_DATA_ACK: // data received, ACK returned
         ongoing_transmission = 0;
         value = TWDR;
         TWCR |= (1<<TWSTO);  // write stop bit
         TWCR &= ~(1<<TWSTA); // clear start bit
    break;
  }
  TWCR |=   (1<<TWINT);  // hand over to TWI hardware
}

int main(void) 
{
  // TWI setup
  sei(); // enable global interrupt
  // TWI-ENable , TWI Interrupt Enable
  TWCR |= (1<<TWEA) | (1<<TWEN) | (1<<TWIE); 

  // LED setup
  DDRB  = 0x3f; // PORTB[0:5] as output
  DDRC  = 0x03; // PORTC[0:1] as output
 
  for (;;) // infinite main loop
  {
    // initiate new transmission if 
    //    no transmission is in progress
    if (!ongoing_transmission) TWCR |= (1<<TWSTA); 
 
    PORTB = value;    // display number
    PORTC = value>>6; //   as LED pattern 
  }
}


Bus Action

Finally, a look at the TWI bus lines with the oscilloscope to see what is going on:

The upper signal is the SCL line, the bottom signal is the SDA line. Both lines are high if nothing is happening because of the pull-up resistors.
Start and stop conditions are indicated by a SDA change while SCL is high. During address and data transmissions, the SCL line goes high when the transmitter has a stable voltage level on the SDA line so that the receiver can sample the voltage level. The first burst of 9 clock pulses is during the 7-bit address transmission (plus read bit and acknowledge bit) from the master to all TWI participants. The second burst of clock pulses is the transmission and acknowledge of the data byte.

More

The example here is minimal, and not robust against failures on the TWI bus. A proper implementation would have to handle all cases that are describe in the four mode tables of TWI operation. Otherwise the TWI hardware might get stuck.
I also didn't mention at all the possibility of multiple masters on one TWI bus. There is a way of preventing bus collisions if two masters signal a start condition at the same time. 
There is the possibility that one master addresses all participants at the same time in a so called general call.

1 Kommentar: