Interfacing the BeagleBoard with an ATtiny85 Microcontroller over I2C

The next part of my project involves connecting a dozen or so sensors and actuators to the BeagleBoard, ideally in a modular, extensible way. The obvious way to do this is use some  microcontrollers, and interface them with the BeagleBoard over I2C.

In the obligatory breadboard photo below I’ve got an ATtiny85 microcontroller with an LED hooked up to pin 2 (to test writing to an I2C register), a variable voltage divider hooked up to pin 3 (to test reading an ADC value from an I2C register), and a second I2C slave, the SRF08, just to make sure there were no issues with multiple slaves on the same bus.

Behold the breadboard:

There’s two complications:

  • The I2C bus exposed on the BeagleBoard runs at 1.8V not the usual 3-5V a microcontroller can cope with.
  • The I2C protocol isn’t trivial to implement in a microcontroller, particularly if you want to make it interrupt driven.

I’ve discussed the level translation issue previously in this post. Unfortunately I couldn’t find an easy way of doing it without using surface mount ICs. Luckily surface mount isn’t as hard as I’d expected (you can see my first surface mount PCB in the photo above, labeled TXS0102). I’m about as clumsy as it gets, so if I can solder a 0.5mm pitch IC, anyone can.

Implementing the I2C protocol on an Atmel micrcontroller is made a lot easier by some code written by Donald Blake from avrfreaks.net (based on Atmel application note AVR312). Note that this code will only work on microcontrollers with Universal Serial Interface (USI) hardware.

While Donald’s code is fantastic, it uses a byte-stream abstraction which hides some of the features of I2C I wanted to use (essentially it exposes the I2C bus as two way byte stream, much like a serial bus).  Rather than thinking of I2C as a stream of bytes, it makes more sense to me to think of it as a sequence of read and write operations on a set of registers – this is the way most sensors and actuators actually use I2C.

While it is possible to build this abstraction on top of the byte stream abstraction, this approach seemed a bit fragile for a couple of reasons:

  • The byte stream abstraction hides the I2C start and stop sequences, so client code can’t tell which bytes belong to which I2C transaction – its all just a stream of bytes. This seems a bit dangerous: if things got slightly out of step, bytes would queue up in the internal buffer, and be sent back in response to the wrong requests, and it may never get back into step.
  • There didn’t seem to be a clean way of handling reading and writing to the same register (a register would have to be exclusively read or exclusively write).

The upshot of all of this was that I modified the code slightly to make it register based. Here’s some demo client code which illustrates how to use the library:

// Callback function triggered when the slave receives a read request from the master.
uint8_t i2cReadFromRegister(uint8_t reg){
  switch (reg) {
    case 0:  return VERSION;
    case 1:  return adcRead();
    default: return 0xff;
  }
}

// Callback function triggered when the slave receives a write request from the master.
void i2cWriteToRegister(uint8_t reg, uint8_t value){
  switch (reg)  {
    case 0: i2cReg0 = value; break;
    case 1: i2cReg1 = value; break;
  }
}
int main() {
  ...
  usiTwiSlaveInit(I2C_SLAVE_ADDR, i2cReadFromRegister, i2cWriteToRegister);
  ...
}

When the library detects a read or write request from the i2c master, it calls the appropriate callback function.  The callback code then determines what to return to the master, or how to store the value just written by the master.

One thing to be aware of is that the i2cReadFromRegister and i2cWriteToRegister callback functions are both called in interrupt context, so you’ll want to keep those functions fast if you care about keeping your I2C bus and the rest of the microcontroller code running smoothly (my example above of calling adcRead() is a perfect example of what not to do). You can download the code from here (its an AVR Studio project for AVR GCC).

As described in the SRF08 I2C post, the easiest way to manipulate the I2C bus on the BeagleBoard is using the i2ctools package. Here’s the result of running i2cdetect on the above setup:

root@beagleboard:~# i2cdetect -r 2
WARNING! This program can confuse your I2C bus, cause data loss and worse!
I will probe file /dev/i2c-2 using read byte commands.
I will probe address range 0x03-0x77.
Continue? [Y/n]
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- 26 -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- UU -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: 70 -- -- -- -- -- -- --

The 70 in the bottom left is the SRF08, and the 26 is the microcontroller. The UU indicates that i2cdetect believes the address is in use by a system driver, so it didn’t poll the address (no idea what driver it is yet).

To get the values from I2C registers use i2cget. With the demo code I2C register 0 contains a constant (0x0A), and register 1 contains the ADC value, as determined by the position of the variable resistor. Here’s the i2cget output:

root@beagleboard:~# i2cget -y 2 0x26 0
0x0a
root@beagleboard:~# i2cget -y 2 0x26 1
0xc0

We can write values to the I2C registers using i2cset. The demo code is a bit daft: it lights the LED on pin 2 if the values stored in register 2 and 3 are the same. Initially the LED is lit, because both registers default to 0.

root@beagleboard:~# i2cset -y 2 0x26 2 10
[LED pin goes low, because the registers are now different]
root@beagleboard:~# i2cset -y 2 0x26 3 10
[LED pin goes high again]

One last thing to look out for is that you will need to have a clock rate greater than 1MHz to support 100KHz I2C. On the ATtiny85, the easiest way to do this is to set the internal oscilator at 8MHz (the factory default, I think), and turn off the 8x clock prescaler (the CKDIV8 fuse):

Advertisements
This entry was posted in BeagleBoard, Electronics, I2C, Microcontrollers and tagged , , , , , . Bookmark the permalink.

4 Responses to Interfacing the BeagleBoard with an ATtiny85 Microcontroller over I2C

  1. Nick Johnson says:

    Thank you so much for this! I searched high and low for a decent implementation of I2C slave support on the attiny’s USI. I’d found Donald Blake’s port of the AVR sample, but wasn’t entirely happy with it – and this is precisely what I need.

  2. Greg says:

    What version BeagleBoard are you using? If it’s the XM, have you tried Ubuntu on it? Do you ever use Ubuntu on your PC? I would also really like to see a tutorial on interfacing the BeagleBoard with a Zigbee.

    • bengalvin says:

      My BeagleBoard is a revision C4. I’m running Ubuntu 11.04 with a 3.04-x3 kernel on it at the moment which works great. Wireless networking worked straight away which was something I’d had trouble with when using Angstrom. There’s a guide here on getting Ubuntu up and running on the BeagleBoard. Good luck!

  3. IPGuru says:

    OMG you would not believe how hard it has been to find a working I2c slave for the Tiny85

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s