AVR basics: using the I2C bus #4 – receiving data

This builds on the previous two posts that dealt with the fundamentals of sending data over the I2C bus on AVR microcontrollers and then how you actually do it.

Receiving is a similar process, except for a couple of slight wrinkles. The procedure is something like this:

  1. Set a start condition.
  2. Send the slave’s address across the bus along with the write bit.
  3. Send a byte with the value of the register in the slave that you want to read from.
  4. Send another start condition.
  5. Send the slave’s address, this time with the read bit set.
  6. Read a byte of data. Repeat this as many times as appropriate.
  7. Set a stop condition.

Starting over

Yes, there are two start conditions set during that procedure, with only one stop condition at the end. This is perfectly normal behaviour. The slave’s address is also sent twice, once with the write bit set because we’re instructing the slave which register we want to deal with (think of this as a command), then with the read bit set to tell the slave this it should go into transmitter mode while the master switches into receiver mode.

This is a good point at which to remind ourselves that the read/write bit is bit 0 of the data register and is 0 for write and 1 for read. You add this to the slave’s address (which is always an even number in order to leave this bit clear precisely for this purpose). And like any other data that you want to send, these values are put into the TWDR data register and then sent by clearing the interrupt bit TWINT (by setting it to 1) and setting the enable bit TWEN (to enable I2C operations) in the TWCR control register.

Steps 1-5, then, are close to what we did in part 3. Using the same example of a slave device at address 0x6C, but this time reading from register 0x03, the first bit of the code would go like this:

#define wait_for_completion while(!(TWCR &amp; (1 << TWINT)));
#define I2C_WRITE 0
#define I2C_READ 1

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

// send the address
TWDR = 0x6C + I2C_WRITE;            // SLA+W, address + write bit
TWCR = ((1 << TWEN | (1 << TWINT)); // trigger I2C action

// specify the register
TWDR = 0x03;                        // register value in the data register
TWCR = ((1 << TWEN | (1 << TWINT)); // trigger I2C action

// switch master to read (receiver) mode and slave to transmitter
TWCR = ((1 << TWEN) | (1 << TWINT) | (1 << TWSTA)); // set another start condition
TWDR = 0x6C + I2C_READ;                             // SLA+R, switch to reading
TWCR = ((1 << TWEN | (1 << TWINT));                 // trigger I2C action

At this point, the master is in receiver mode, the slave is in transmitter mode and the slave should also have prepared the data from the specified register so that it’s ready to send. All we have to do to actually receive the data is trigger another I2C action via the usual method of setting the TWINT and TWEN bits. But here’s where the extra wrinkle comes in.

Taking responsibility

You may remember from part 2 that we talked about acknowledge bits. When the master is in transmitter mode, it’s the slave that’s responsible for setting the acknowledge bit (by holding the SDA line low or allowing it to go high). As we’ve dealt only with the most basic operations so far we haven’t bothered our pretty little heads about these acknowledgements.

However, once the master is in receiver mode, it assumes the responsibility for setting the acknowledge bit. How the slave deals with this bit is a matter of the slave’s own implementation. But it’s probably going to be expecting this bit to be set to the right value.

Whether you set this bit depends on how many bytes you’re expecting to read. For all but the last byte, you set an acknowledge bit, ACK. For the last byte, you set it to ‘no acknowledge’, or NOT ACK.

Let’s assume, for the sake of our example, that we’re reading a two-byte (ie, 16-bit) value from the slave. The slave’s data sheet will tell you if this is going to be sent most significant byte (MSB) first or least significant byte (LSB) first. We’ll assume here that it’s MSB first.

What you’d do here is perform one read operation to get the MSB, sending an ACK to let the slave know it was received okay and more is expected. Then you do a second read to get the LSB but this time without an acknowledge bit being set (NOT ACK) which essentially tells the slave that we’re done. You then set a stop condition.

To set ACK when reading, just set the TWEA (enable acknowledge) bit in the TWCR control register at the same time that you set TWINT and TWEN. To read using NOT ACK, don’t do that.

Here’s the code:

uint16_t register_value = 0;
// perform first read to get the MSB
TWCR = ((1 << TWINT) | (1 << TWEN) | (1 << TWEA)); // with ACK set
// the received byte is now in the TWDR data register
register_value = (TWDR << 8);                     // put value in top half of variable
// second read to get LSB
TWCR = ((1 << TWINT) | (1 << TWEN));              // no acknowledge bit set, NOT ACK
// the second byte is now in TWDR
register_value += TWDR;

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

And that’s largely it for reading.

We’ve now covered the basics of I2C bus operation on the AVR microcontroller. In the next post we’ll wrap up with a few observations on stuff like error checking.


Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.