Using the TWI/I2C interface

From NYC Resistor Wiki
Jump to navigation Jump to search

The more advanced AVR microcontrollers, as one would expect, support on the chip the I2C two wire (plus ground) interface. It is not called I2C in the Atmel documentation however, as I found out, rather it is called the 2-wire Serial Interface (TWI). But once you locate the right section and understand how Atmel's implementation works, it is relatively uncomplicated to write a code library for it, even despite all the detail of the protocol and all the messages generated by the AVR hardware.

First, the TWI interface needs to be initialized. The TWI bit rate register (TWBR) and the TWI prescaler bits set the frequency of the bus. The standard I2C bus runs at a frequency of 100 kHz. I used a lower rate of 10 kHz to provide for a less critical design, namely to avoid having to use external pull-ups on the bus.

#define TWI_PRE     1       // my TWI prescaler value 
#define TWI_FREQ    10000   // my TWI bit rate

TWBR = (F_CPU / TWI_FREQ - 16) / TWI_PRE / 2;  // set bit rate
TWSR = (0<<TWPS0);          // use a prescaler of one
TWCR = _BV(TWEN);           // activate the TWI interface

Since the I2C bus normally operates at a relatively high speed, the two bus lines need external pull-up resistors. The internal pull-ups of AVR MCUs with their large resistances, as high as 50 kohms according to the documentation, would degrade the bandwidth of the bus. Nevertheless I still choose to use internal pull-ups, but I did it at a cost of using a slower bus. Since there shouldn't be any shame in reducing your parts count, the next step for me was to enable the internal pull-ups on the TWI clock (SCL) and data (SDA) pins.

#define SCL_PORT    PORTC   // pin assignments specific to the ATmega328p
#define SCL_BIT     PC5
#define SDA_PORT    PORTC
#define SDA_BIT     PC4

SCL_PORT |= _BV(SCL_BIT);   // enable pull up on TWI clock line
SDA_PORT |= _BV(SDA_BIT);   // enable pull up on TWI data line

However, if you do decide to use external pull-ups on the bus, the resistance should not be more than the 1 / (10 * Bus Capacitance * Bus Rate) in my estimation - for a maximum rise time of one fifth the I2C bus period. In my design I've assumed neither of the two I2C bus lines have a bus capacitance (Cb) of more than 200 pF. I've measured the capacitance of my solderless breadboard at 150 pF so that puts the internal pull-ups within an acceptable range. Myke Predko recommends a 1 kohm resistor for 400 kHz buses and a 10 kohm resistor for 100 kHz. Atmel gives a maximum of (300 ns / Cb) ohms for bus speeds above 100 kHz and (1000 ns / Cb) ohms below.

To initiate a TWI transmission - and following the code example given in the documentation - a TWI operation must be enabled (TWEN) and the TWI interrupt flag (TWINT) must be cleared. This is done by setting the TWEN and TWINT bits in the TWI control register (TWCR). Clearing the TWI interrupt flag immediately starts a TWI transmission. For certain TWI transmissions another bit has to be set too, for example to send a START or STOP condition. From my experience both this additional bit and the TWINT bit must be set at the same time. And although it might seem repetitive, the TWEN bit must be explicitly set again, even though it was set in the initialization code, otherwise the operation will not work.

Once a TWI transmission is started, you need to wait for it to complete. This can be done by polling the control register until the TWINT bit is set. The status of the TWI transmission can then be checked by reading the TWI status register (TWSR).

uint8_t twi_poll (uint8_t u)
{
  TWCR = u | _BV(TWINT) | _BV(TWEN);  // initiate a TWI transmission
  if (u == _BV(TWSTO)) return 0;      // don't poll if the STOP condition was sent
  while (!(TWCR & _BV(TWINT))) ;      // wait for the interrupt flag to be set
  return (TWSR & 0xf8);               // return the status code
}

The best analogy I can come up with to explain how to talk to I2C perpherals is that you talk to them using packets. There is a write packet and a read packet. To perform a send transaction you need to transmit a write packet. To perform a receive transaction, you need to send two packets, one to send the command and the other to read the results.

The beginning of a packet is indicated by the START condition, or if you are in the middle of a transaction the RESTART condition. The end of a packet is indicated by the STOP condition, or if you are again in the middle of the transaction, and serving a double duty, the RESTART condition. The STOP condition is important not just because it ends the transaction but also because it forces the TWI hardware to release the clock bus line so its pull-up can bring it back up to high.

To do a transaction, the START condition is first sent. This is performed by setting the TWSTA bit in the control register.

u = twi_poll(_BV(TWSTA));
if (u != TW_START && u != TW_REP_START) return 0;

Next the 7-bit slave address of the peripheral and another bit, the read/write bit, is sent over the bus. The resulting 8-bit value is placed in the TWI data register before the transmission is initiated. After the addressed device receives this SLA+W/R value, the device will acknowledge (ACK) its reception by pulling the data bus low in the 9-th bit position. If there is no acknowledgement, my code releases the bus by jumping to code that sends a STOP condition and then errors out.

TWDR = slave_address | TW_WRITE;
if (twi_poll(0) != TW_MT_SLA_ACK) goto release;

While there are a lot of other status codes to handle and the documentation gives a rather complex diagram of when and where they are generated, there is a definite sequence to it all, so if anything is not in the right order it usually means the transaction failed making it relatively straightforward to handle errors.

To write a byte over the TWI interface use:

TWDR = byte;
if (twi_poll(0) != TW_MT_DATA_ACK) goto release;

And to read a byte use:

if (twi_poll(_BV(TWEA)) != TW_MR_DATA_ACK) goto release;
byte = TWDR;

The TWEA bit tells the TWI to acknowledge reception by sending a ACK as the 9-th bit. But when reading the last byte, the TWI needs to send a NACK instead to say it is done. This is done by not setting the TWEA bit.

if (twi_poll(0) != TW_MR_DATA_NACK) goto release;
byte = TWDR;

To change the direction of the transaction and, for example, start reading bytes instead of writing them, the RESTART condition must be sent.

if (twi_poll(_BV(TWSTA)) != TW_REP_START) goto release;
TWDR = sla | TW_READ;
if (twi_poll(0) != TW_MR_SLA_ACK) goto release;

Lastly when the transaction is finished the STOP condition is sent releasing the bus. This is performed by setting the TWSTO bit.

twi_poll(_BV(TWSTO));

As an example, to change the frequency of the signal generated by the DS1077 chip, I use the following code where "m" is the prescaler value and "n" is the divider value.

#include <compat/twi.h>

uint8_t twi_send16 (uint8_t sla, uint8_t cmd, uint8_t msb, uint8_t lsb)
{
  uint8_t u, status = 0;
  u = twi_poll(_BV(TWSTA));                    // send START
  if (u != TW_START && u != TW_REP_START) return 0;
  TWDR = sla | TW_WRITE;                       // send SLA+W
  if (twi_poll(0) != TW_MT_SLA_ACK) goto release;
  TWDR = cmd;                                  // send command
  if (twi_poll(0) != TW_MT_DATA_ACK) goto release;
  TWDR = msb;                                  // send msb
  if (twi_poll(0) != TW_MT_DATA_ACK) goto release;
  TWDR = lsb;                                  // send lsb
  if (twi_poll(0) != TW_MT_DATA_ACK) goto release;
  status = 1;                                  // success
 release:
  twi_poll(_BV(TWSTO));                        // send STOP
  return status;
}

uint8_t twi_receive16  (uint8_t sla, uint8_t cmd, uint8_t *msb, uint8_t *lsb)
{
  uint8_t u, status = 0;
  u = twi_poll(_BV(TWSTA));                    // send START
  if (u != TW_START && u != TW_REP_START) return 0;
  TWDR = sla | TW_WRITE;                       // send SLA+W
  if (twi_poll(0) != TW_MT_SLA_ACK) goto release;
  TWDR = cmd;                                  // send command
  if (twi_poll(0) != TW_MT_DATA_ACK) goto release;
  if (twi_poll(_BV(TWSTA)) != TW_REP_START) goto release; // send RESTART
  TWDR = sla | TW_READ;                        // send SLA+R
  if (twi_poll(0) != TW_MR_SLA_ACK) goto release;
  if (twi_poll(_BV(TWEA)) != TW_MR_DATA_ACK) goto release;
  *msb = TWDR;                                 // read msb
  if (twi_poll(0) != TW_MR_DATA_NACK) goto release;
  *lsb = TWDR;                                 // read lsb
  status = 1;                                  // success
 release:
  twi_poll(_BV(TWSTO));                        // send STOP
  return status;
}

...

#define DS1077_SLA  0xb0
#define DS1077_MUX  0x02
#define DS1077_DIV  0x01

if (!twi_send16(DS1077_SLA, DS1077_MUX, m>>1, m<<7)) return; 
if (!twi_send16(DS1077_SLA, DS1077_DIV, n>>2, n<<6)) return; 

And that about does it.

If you are interested in actually observing the I2C bus lines and don't have a logic analyzer but are lucky enough to still have a parallel port on your computer, the following program will capture the logic levels present at the input of the two data pins, D0 and D1, for 7 seconds and then will write them out to the standard output as a raw unsigned 8-bit stereo audio file which you can import and view in, say, Audacity. One word of caution though, you must run this program once before attaching your circuit to the D0 and D1 pins since the parallel port data pins default to outputs. They have to be converted over to inputs which this program does. To build this analyzer, you can use a 25-pin RS232 cable. Pins 2 and 3 go to D0 and D1 and pin 20 goes to ground. At least on my computer, it can capture around 750,000 samples per second.

- George

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/io.h>

#define samples   (5*1024*1024)
#define channels  2
#define base      0x378

static char log[samples];

int main(int argc, char *argv[])
{
  int i, n, c;
  if (ioperm(base,3,1)) fprintf(stderr, "ioperm() failed\n"), exit(1);
  n = inb(base+2);
  fprintf(stderr, "Data lines set as %s, changing to input\n", 
          n & (1<<5) ? "input" : "output");
  outb(n | (1<<5), base+2);    // set data lines to inputs
  for (n=0; n<samples; n++) log[n] = inb(base);
  for (n=0; n<samples; n++) {
    for (i=0; i<channels; i++) {
      c = ((log[n] >> i) & 1) ? 224 : 32;
      putchar(c);
    }
  }
  return 0;
}