AVR basics: using the I2C bus #3 – sending data

In part 2 we looked at the fundamentals of how data is transmitted over the I2C bus. Now let’s actually do it. And, as usual with AVR microcontroller stuff, it’s all about registers.

As before, our focus here is on AVR microcontroller chips such as the ATMEGA328P that have a proper I2C interface.

There are four main registers that concern us when it comes to the I2C bus. We met the bit-rate register, TWBR, in part one, so we don’t need to mess with that again. But we’ll now be using the other three. Well, two anyway.

TWDR – data register. This is the simplest of the lot. When transmitting, you just write the value of the byte you want to send to this register. When receiving, this is where you’ll find the received byte.

TWCR – control register. Writing specific bits – each of which has its own name – to this register is what makes things happen on the I2C bus and also notes what has happened. The bits are:

7 6 5 4 3 2 1 0
 Interrupt flag  Enable acknowledge  Start condition  Stop condition  Write collision flag  I2C Enable  (not used)  Interrupt enable

The TWINT interrupt flag is especially noteworthy here: it is set by the hardware whenever the two-wire interface (TWI, or I2C to us civilians) has finished its current task. So it’s a way of flagging that reads or writes have completed. There’s a lot more to it than that, but that’ll do us for now.

In this post, we’re only interested in the TWCR bits shown in bold, all of which are read/write.

TWSR – status register. We’ve already met two bits from this register: bits 0 and 1 (TWPS0 and TWPS1) are used in setting the bit rate. Bit 2 isn’t used and always returns 0 if read. The other bits, 3-7 (aka TWS3 to TWS7) are read-only and return a value that corresponds to the result of the last operation. We’ll come back to this in a later post, but to give you an idea, if you’ve issued the appropriate command to initiate a start condition, then the TWCR should contain the value 0x08. To get this value, you need to mask out bits 0 & 1. So, for example, one way of getting the value might be something like:

uint8_t i2c_status = TWSR & 0b11111100;

Making things happen

For anything to happen over the I2C interface, the TWEN bit of the control register needs to be set (to 1). This is what turns ordinary, innocent-looking GPIO pins into the I2C interface.

You also need to clear the interrupt flag, TWINT, by writing a 1 to it. And so writing both of these bits to 1 is essentially our way of triggering the I2C bus, or prodding it into action.

And after each prod, we need to wait for the action to complete. We know this has happened because the TWINT bit gets set (to 0). Now, advanced users may use interrupt routines in clever ways that allow the microcontroller to get on with stuff while waiting for I2C actions to complete. But for our basic purposes here, we’ll just hang around in a blocking loop. My preferred way of doing this is:

while ( !(TWCR & (1 << TWINT)) );

Some people like to define that as a macro, which we’ll do here so it’s obvious what’s going on:

#define wait_for_completion while(!(TWCR & (1 << TWINT)));

Now we can just use wait_for_completion whenever we need to pause until the action is complete.

Set a register

As with the last post, let’s look at an example where we set the value of a register in the slave device – a common way of getting the device to do something. In this example, we’re going to write the value 0xE2 to register 9 (or 0x09, if you prefer).

[Click to see larger version]

This process consists of:

  • Setting a start condition.
  • Sending the slave device’s bus address plus the write bit down the bus.
  • Sending the address for the slave device’s register down the bus (eg, 0x09).
  • Sending the value we want to write (eg, 0xE2) down the bus.
  • Setting the stop condition.

Start condition

To set a start condition and basically get the whole thing rolling, you need to set the start bit, TWSTA, in the control register. This is accompanied by setting the TWINT bit (to clear the interrupt) and the TWEN bit (to enable the I2C bus). And after doing all that, we wait for this action to complete. So this is what we do:

// set the start condition
TWCR = ((1 << TWEN) | (1 << TWINT) | (1 << TWSTA));

Sending the address

Let’s say the address of our slave device, typically referred to as SLA, is 0x6C. To this we need to add the appropriate bit for read (R) or write (W). As write mode is set using 0, then the next byte we send (SLA+W) is actually just the address. And we send a byte by first loading its value into the data register, TWDR, and then setting TWINT and TWEN again. In this example, what we need to do is:

// send the address
TWDR = 0x6C;                         // data to send - ie, address + write bit
TWCR = ((1 << TWEN | (1 << TWINT));  // trigger I2C action

Register and data

Sending the register address is no different from sending any other byte of data. So now we’re going to send the register address and the byte of data we want to store in it.

// send the register address
TWDR = 0x09;                         // register address
TWCR = ((1 << TWEN | (1 << TWINT));  // trigger I2C action
// send the data byte
TWDR = 0xE2;                         // data byte
TWCR = ((1 << TWEN | (1 << TWINT));  // trigger I2C action


Now that we’ve done what we set out to do, we need to set a stop condition. This frees the I2C bus for other operations. This is nearly identical to setting the start condition, except that we use TWSTO in place of TWSTA.

// set the stop condition
TWCR = ((1 << TWEN) | (1 << TWINT) | (1 << TWSTO));

Coming soon

And that’s pretty much it as far as sending a simple command is concerned.

You’ll note that we haven’t made any mention of the acknowledge bits that we talked about last time. Nor have we been paying attention to the status register TWSR. We’ll talk a little more about those in part 5.


Leave a Reply

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