I2C, it's a standard that's been around for around 20 years and has found uses in nearly every corner of the electronics universe.  It's an incredibly useful technology for us microcontroller hobbyists but can seem daunting for new users.  This tutorial will solve that problem, first by reviewing what I2C is and how it works, then by going in-depth on how to implement I2C in Atmel's ATTiny USI (Universal Serial Interface) hardware.

I2C is commonly used in GPIO expanders, EEPROM/Flash memory chips, temperature sensors, real-time clocks, LED drivers, and tons of other components.  If you spend much time looking for new, cool parts you'll probably wind up with several I2C parts.  Fortunately it is a protocol that is available on most microcontrollers, though it is a bit more complex than others.  Learning it is tough at first, but once you know I2C it is a powerful tool.

I2C Tools of Interest:
Before you dig too deep into I2C communications, you'll want to have some things on hand that will make your learning experience easier.

1. Various I2C-compatible parts - Anything goes, as long as it's I2C.  If you're writing a master driver you need some things to talk to.  I like Texas Instruments' TMP100 temperature sensor as it's cheap (free if you sample) and has a simple protocol (just send an I2C read command to get temp values).  I more recently purchased some Microchip MCP23017 GPIO expanders which give you 16 bits of additional GPIO over the I2C bus.

2.  Something that has a working I2C master - You'll want something to test/compare against if possible.  An Arduino with the Wire library will work, but more recently I prefer my Raspberry Pi with the Linux i2ctools package.  i2cdetect, i2cset, i2cget, and i2cdump are invaluable when writing code, especially slave-mode code.

3.  Oscilloscope.  I know this is a big one, but if you can work with one (either own one, borrow one, or go to a lab where you can use one) it's a super amazing help.  I2C uses two wires, so a two-channel scope works great.  I used my Rigol DS1052E (100Mhz modded) and it helped a TON.  Of course, I did most of the work with it and am telling you what I learned, so hopefully it'll be easier for you.

Step 1: What Is I2C - 1

I2C (Inter-Integrated Circuit bus), originally developed by Phillips (now NXP Semiconductor) and also commonly known as TWI (Two Wire Interface) by Atmel and other companies who don't want to get into trademark issues, is a two-wire synchronous serial bus.  Let's take a look at what each of those words means:

Two wire - This one's easy, I2C uses two wires (in addition to ground, of course!)  They're called SDA (serial data) and SCL (serial clock).  These are wired in an open-drain configuration, which means that the outputs of all connected devices cannot directly output a logic-level 1 (high) and instead can only pull low (connect to ground, outputting a 0).  To make the line go high, all devices release their pull on the line and a pull-up resistor between the line and the positive rail pulls the voltage up.  A good pull-up resistor is 1-10K ohms, low enough that the signal can be seen as a high level by all devices but high enough that it can easily be shorted out (pulled down) and not cause damage or significant power usage.  There is one pull-up resistor on SDA and one on SCL.

Synchronous - This means that data transfer is synchronized via a clock signal that is present to all connected devices.  This is generated by the master.  To contrast, an asynchronous serial system does not have a clock signal.  Instead, it uses a pre-determined time-base, or baud rate.  An example of asynchronous serial is RS-232 (the common serial port on many computers).

Serial - Data transferred serially means that one single bit is transferred at a time over a single wire.  To contrast, parallel data transfer has multiple wires, each carrying one bit, which are all sampled at once to transfer multiple bits in parallel.

Bus - A bus is a system which allows many devices to communicate to each other over a single set of wires.  While it may be called a bus, USB is not a true bus at the hardware level, as connecting multiple devices requires a hub.  A bus such as I2C allows new devices to be added simply by attaching their SDA and SCL connections to the existing line.  Busses (I2C, USB, PCI, etc) all use an addressing system, in which each device has a unique address.  An address in this case is simply a binary number, and all messages to that device must be sent with that address.
<p>Nice work! I've used the I2C slace code in order to be able to read logging messages from an ATtiny85 which doesn't come with a USART (after adding the appropriate pinout definitions to usi_i2c_slave.h)</p><p>I read data using python on Raspberry Pi using smbus library. It works perfectly if I use the single byte method:</p><p><strong>bus.read_byte_data(DEVICE_ADDRESS, REG_ADDRESS) </strong></p><p>but didn't work when I tried to read a block of bytes in one call:</p><p><strong>data = bus.read_i2c_block_data(DEVICE_ADDRESS, REG_ADDRESS)</strong></p><p>which returns a list of 32 bytes - the 1st is correct, followed by 31 0xff s</p><p>Not a problem, as reading bytes works fine, just wondered if there's some way to define the I2C registers in usi_i2c_slave.c to handle blocks of bytes(?)</p><p>I've tried a couple of other I2C libs and yours is the only one to work for me with ATtiny85.</p>
<p>I am trying to use an ATtiny85 as well with i2c tools on a raspberry pi. I have changed the pin definitions, but for some reason it will not show up when I run <strong>i2cdetect</strong> on the pi. I think it might be the clock speed, what is yours set at for the microcontroller? </p>
<p>I've read that the SDA &amp; SCL lines need to be pulled up to improve odds of successful detection. I wasn't getting i2cdetect to work until I added pullups .. 470k Ohms</p>
<p>Hi Alec</p><p>Yes, I think I had to reduce the clock speed below about 40000 - I've got it set to 20000 (the default is 100000).</p><p>The default can be configured by editing/creating a file</p><p> <em>/etc/modprobe.d/</em><em>i2c_bcm2708</em><em>.conf</em></p><p>and adding a line</p><p><em>options i2c_bcm2708 baudrate=20000</em></p><p>Reboot/Test:</p><p><em>cat /sys/module/i2c_bcm2708/parameters/baudrate<br>20000</em></p>
<p>I'm looking at the slave code. It looks like the main code that uses the slave code could be accessing the memory buffer that the ISR writes to. Shouldn't there be a mutual exclusion mechanism in place to prevent this? As an example, in your two byte PWM sample, say there are only two possible PWM values being sent by the master, 0x11 0x11 and 0x22 0x22. Even if the code read two bytes as a 16-bit, the underlying assembly could read 0x11, get interrupted as the incoming 0x22 0x22 is written by the ISR, and then continue to read the second byte as 0x22. So it would have read 0x11 0x22. Even if the architecture did atomic reads of 16-bit, it would still be an issue if the buffer was bigger, say 32 bytes, and the main code was only halfway through reading the bytes when the ISR wakes up and changes values. I'd like to use this code, so hopefully I'm missing something.</p>
<p>God I hate that these wortheless pedantic instructibles with low information density end up at the top of Googles hit list and I end up clicking on them before reading that the site is Instructibles.com</p>
<p>This instructable was exactly what I needed. Google worked well and the instructable was great.</p>
<p>Nice job.</p>
<p>This is most excellent. Thanks for the Instructable.</p>
<p>I think the reason that the slave wouldn't send more than 1 byte at a time to the master is that the slave code always interprets the master's response as NACK even when it sends ACK. This can be fixed by changing in usi_i2c_slave.c</p><p>case USI_SLAVE_SEND_DATA_ACK_CHECK:<br>if(USIDR<strong> &amp; 0x01</strong>)<br> {</p><p>So that it only test the LSB of USIDR - which is the 1bit that the master has just written to SDA for ACK (SDA low) or NACK (SDA high). I can now read multiple bytes correctly using the python smbus library.</p>
<p>Thanks. Very useful and good to follow.</p>
Thank you so much. I've used an I2C module before, but never the USI in the ATTiny's. Now I finally understand what the heck that 4-bit counter is for. I haven't looked at the code entirely, but I'm guessing you use the 4-bit counter overflow flag to check when the transfer is done. I've been staring at the datasheet all day, but now it finally makes sense! I will probably write my own libraries to get a good understanding of it, but I will base it heavily off of yours.
You're welcome! <br> <br>I based my driver heavily off the Atmel app note for USI I2C, even though that one worked it was really hard to understand and I wrote mine to clean it up and figure out how it all works. Definitely try writing your own if you want to really understand how it works!
Everyone on the inter webs kept telling me that the AVR app note had a major flaw, but no one cared to actually tell me what it was, and instead just insisted that I download Don Blake's code. Well I finally found someone to explain it for me. If anyone else is interested: http://www.aca-vogel.de/TINYUSII2C_AVR312/APN_TINYUSI_I2C.html#mozTocId854460 <br> <br>It's always a good idea to try and write one. You will learn a lot! For instance, I learned that the USI code doesn't actually check that the start condition actually completed. I originally did it without the while loop at the beginning of the start vector. I didn't want my routine to use interrupts, so I didn't think it was necessary. I would only work once. Every subsequent read would only return 0x01. o_O
I don&acute;t unsderstand this line: <br>i2c_transmit_buffer[0] = (0x40 &lt;&lt; 1) | 0 //Or'ing with 0 is unnecessary, but for clarity's sake this sets the R/W bit for a write. <br>Why do you make the displacement &lt;&lt;1?? Aren&acute;t you sending 0x80?? and it&acute;s a diferent address that you wanted... <br>About the OR&acute;ing with 0... if you want to put a 0 at the last bit (to write on the salve... I imagine), don&acute;tn you need to make somethig like (address &amp; 0xfe)?? In the contrary case, if you want to read from the slave, you need to put a 1 on the last bit, so (I think) you need make something like (address | 0x01)... or am I worng?
The i2c slave address is shifted to the left by 1. That line sets the address to 0x40 (0b1000000, i2c addresses are 7 bits long) and then sets the R/W bit to zero (write mode). The or'ing with zero is pointless, I just did it for context, to show readers that the write bit is zero and the read bit is one (where I use | 1). Since the operation already shifts left by 1 the 0 bit will never actually be one, so the | 0 part can be removed. An optimizing compiler will do this for you.
Thank you for making this instructable, I have the I2C Slave code running on an ATTiny 167 clocked at 8 MHz. It doesn't acknowledge it's address when the bus is running at 100kHz but it all works beautifully on a 5kHz bus. Have you any Idea why this is?
Have you tried more intermediate speeds? My first guess is to assume you're running the attiny on the internal RC and it's pretty inaccurate, making more timing issues at a higher speed. (Non-ECE person here, just hobbyist) <br>
I have been trying to use your slave code to read the 2 byte ADC value from an ATtiny45. The first problem I found is the default clock of 8MHz is not fast enough to allow the ACK to take place before the clock edge. A 16MHz clock works with about 1uS to spare. The code at case USI_SEND_DATA needed 3 lines adding as follows: <br> USISR = USI_SLAVE_CLEAR_START_USISR; <br> PORT_USI |= (1 &lt;&lt; PORT_USI_SDA); <br> USI_SET_SDA_OUTPUT(); <br>at case USI_SLAVE_SEND_DATA_ACK_WAIT you need to add <br> USIDR = 0; <br>after this the code works fine. So thanks for this code which is well written and very useful.
It's not working. I tried the code. But when function switches from transceiver function to Transfer function pointer loses its reference. If possible post main function &amp; updated code
Serial communications always struck me as pretty complicated so I never messed with the stuff. From what you've described I2C sounds even more involved. So congratulations for figuring out how to get it to work for you. I would like to see more about the stepper motor driver you made. A Raspberry PI might be better if the odds of getting one wasn't like hitting the lottery.

About This Instructable




Bio: I finally graduated from Missouri University of Science and Technology (Missouri S&T, formerly University of Missouri Rolla) with a computer engineering degree. Originally from ... More »
More by CalcProgrammer1:Cheap and Easy Tachometer (RPM Sensor) for Brushed DC Motors Cheap Home Automation using Wireless Outlet Modules ATTiny USI I2C Introduction - A powerful, fast, and convenient communication interface for your ATTiny projects! 
Add instructable to: