Introduction: ATTiny USI I2C Introduction - a Powerful, Fast, and Convenient Communication Interface for Your ATTiny Projects!
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.
Step 2: What Is I2C - 2
On an I2C bus, there are masters and there are slaves. A master initiates a connection while a slave must wait for a master to address it before it sends or receives anything. I2C has multi-master capability, which means that more than one master may exist, and if two masters attempt a transmission at the same time, they must perform arbitration to correct the problem. This tutorial will not cover multi-master configurations, but it should be noted that they do exist.
A master may either request to send or receive data from a slave. During a send, the master writes data to the bus and the slave reads the data off the bus and stores it in its memory. During a receive, the master reads the bus for data sent out by the slave. In both situations, the master provides the clock signal on SCK.
At the end of every byte (which is 8 bits) transmitted on the I2C bus, the receiving device must provide an acknowledgement (ACK). The only time in which this does not happen is when the master is receiving data from the slave, in which it ends the transmission with a not-acknowledgement (NACK or NAK) indicating that the slave should stop sending data. An ACK is represented by a low (pulled-down or 0) state while a NACK is represented by a high (not pulled-down or 1) state. Since the default state of the bus is high, an ACK is a confirmation that the other device is present and has successfully processed the transmission.
In addition to ACK's and NACK's, I2C has two additional framing conditions known as a start condition and a stop condition. A start condition is transmitted by the master to indicate the start of a transmission. During a start transition, the SDA line first transitions from high to low and then, after a noticeable amount of time, the SCL does the same. A stop condition, which is issued by the master at the end of a transmission, is the reverse. First the SCL line goes from low to high, then the SDA does the same. Note that the SDA and SCL lines both are high when the bus is inactive.
The first byte in an I2C transmission is the address byte. This is sent by the master and is used to determine what slave to talk to and whether to perform a send or receive (also known as write and read, respectively). A slave address is 7 bits long, and there are several reserved addresses. One such reserved address is 0x00, which is often considered a global write (write to all slaves). You usually configure the slave device's address by tying address select pins high or low, though on a microcontroller you set the address programmatically as we will do on the ATTiny2313. The least-significant bit of the address byte is the Read/Write bit which indicates whether to perform a read or write. If one, the operation is a read, if zero a write.
Step 3: What Is I2C - 3
That basically covers the I2C protocol itself, in that a master can initiate a read or a write, and transfer continues until the master sends a stop condition. When the master is reading from a slave, it will issue a NACK instead of an ACK on the last byte, before the stop condition, to indicate that it is done receiving. From here, with a proper implementation thus far, you can communicate to all the devices you want. However, there is one extra thing that I want to point out, as it is used quite often.
On some I2C devices (or should I say most, it's very common), the access protocol is set up as a register bank. To read or write from these registers, you must first write an internal address which is the address of the register you wish to read or write. After writing an internal address, you may read or write multiple bytes and the internal address will increment with each byte. This is the preferred protocol for almost all I2C memory devices as well as most sensors and I/O expanders. While it is possible to have a protocol that doesn't follow the register bank protocol, the vast majority of devices do and many I2C tools are built around it. As such, it is worth pointing out. It is also the protocol that I will implement on the ATTiny2313.
As mentioned before, before reading or writing any register you must send the device internal address, which is done by performing a write operation of one byte, which contains the internal address. For write operations, the transmission may continue with data values, the first of which will be stored in the desired address and any additional bytes will increment upwards by one each time. For reads, the master will send a stop condition, then start a new transmission for reading. This is because you can not have both a write and a read in the same transmission. In some cases, a repeated start may be sent instead of a stop then start. A repeated start is a high-to-low transition on SDA while SCL is high.
Step 4: ATTiny USI I2C Code Implementation - Overview
At this point in the tutorial, you should be at least basically familiar with the I2C protocol. Now I'm going to go into detail on the actual I2C protocol implementation for the ATTiny USI hardware.
To preface this, the board shown on this step's main picture is a custom unipolar stepper motor controller that I designed as part of my senior project this Spring. The board is capable of driving a single unipolar stepper motor with PWM, variable speed, and three different stepping modes (single-stepping, power stepping, and half-stepping). It also operates in bursts, as the controller will only run the motor for the given number of steps. For continuous operation, the stepping counter must be reloaded by whatever is driving the board before it reaches zero. None of this is important for the tutorial.
The important part here is that the robot these boards power has three wheels (omnidirectional wheels, arranged in a triangular pattern). I wanted to build three identical boards but only use a single RS-232 serial interface from the robot's main computer (a laptop) to control all three. The idea I came up with was to use the serial port for the computer interface and the I2C bus to connect all three boards. In this setup, the board connected to the PC takes on the master role in addition to being a slave node. The PC then sends I2C formatted messages onto the bus for the three boards to operate upon.
For this task, my boards would have to support both I2C modes, being able to operate as both a slave and a master depending on serial port operations. Knowing very little about the USI hardware and only slightly more about the I2C protocol in general, I set out to master the I2C protocol, to make it my slave and command it to do my data transmissions. And that I did, and it worked well for the project.
All up until I got my Raspberry Pi at least, because when I finally got around to playing with the Pi, I tried hooking up my I2C motor boards to its I2C port in an attempt to have a Pi-powered robot. Unfortunately, no matter what commands I sent, the Pi could not make communication happen. Since I had never validated my protocol beyond my own master code talking to my own slave code, I figured I wasn't implementing the protocol just right, and sat down to make it all work correctly. That I did, with the new code much more streamlined, organized, and understandable (comments galore for anyone who wants to learn!). Since my journey into the world of I2C was rough, I decided to post here for all to see, and to go into as much detail as I could to make I2C's functionality clear.
In the next few steps I will talk about the USI hardware and how it works as both a master and a slave. I have also attached my USI code files. I want people to have a good USI implementation and I also want them to read how it works, knowing exactly what's going on is crucial when dealing with a complex, low-level system, so I've commented my files thoroughly.
Step 5: ATTiny USI I2C Code Implementation - USI Hardware
So, before we look at code, let's look at the datasheet. Specifically I'm looking at the ATTiny2313 datasheet as that's the chip I'm using, but the same USI hardware can be found in many different ATTiny models. Note that the output pins may be different between chips, but otherwise the hardware works the same way and has the same registers.
The USI hardware has three pins:
DO - Data Output, used for three-wire (SPI) communication mode only
DI/SDA - Data Input/Serial Data, used as SDA in I2C configuration
USCK/SCL - Clock, used as SCK in I2C configuration
In addition, the USI hardware has three registers:
USIDR - USI Data Shift Register - Shifts data in and out of the USI hardware
USISR - USI Status Register - Has state flags and 4-bit counter (more on this below)
USICR - USI Control Register - Has interrupt enables, clock modes, and software clock strobe functions
The 4-bit counter occupies the lower 4 bits of USISR and is used to time the overflow interrupts when operating in slave mode as well as to help generate SCK clock pulses in master mode. Being a 4-bit counter, it counts upwards from 0 to 15 before overflowing. Upon overflowing, it can trigger an interrupt (USI_OVERFLOW_vect, enabled by a bit in USICR). This is used for the USI slave state table to keep track of transmission as it switches between transmission states (more on this in the slave code section).
When acting as a master, the 4-bit counter is used along with the clock strobe bit in USICR to generate the SCK clock. You set the counter to overflow in the number of clock pulses you wish to generate (generally 8 or 1, with 8 being a data transmission and 1 being an ACK/NACK transmission). Then, you loop until the counter overflows, continuously setting the clock strobe bit and performing a delay wait. More on this can be seen in the master code section.
Two Wire Clock Control Unit (Start Condition Detector)
The TWI Clock Control Unit is a module in the USI that monitors the SCK line for start and stop conditions. Its primary purpose is the start condition detector, which, when enabled, generates a USI_START_vect interrupt whenever it detects a valid start condition. This interrupt handler is the starting point for slave-mode USI I2C transmission handling, and must set up the 4-bit counter to overflow after the address transmission has occurred. From there on, the overflow interrupt manages the rest of that I2C message and resets the start condition detector for the next message.
Read the Datasheet
I won't go into detail on each of the bits in each of these registers, but if you're looking at writing some USI code it is essential that you read these sections of the datasheet. I recommend reading the whole Universal Serial Interface - USI section (pages 142-150 of the ATTiny2313 full datasheet). This will give you all the information you'll need in addition to what I've pointed out here.
Step 6: ATTiny USI I2C Code Implementation - USI I2C Master
The USI I2C Master (usi_i2c_master.c/h) library provides I2C master mode capabilities using the USI hardware. There are two important functions that the user should be familiar with. The first is the initialization function which sets up the SDA/SCL pins and USI hardware, and the other is the transmission function, which performs either a read or a write operation on an I2C message, returning 1 (true) if successful and 0 (false) if an error (received a NACK) occurred. A third function, the transfer function, is used by the transceiver function to send and receive data. The transfer function should not be used outside of the usi_i2c_master library.
The transmission function takes two arguments. The first is a pointer to a data buffer in which to send or receive data from. It is assumed that the first byte in this buffer is the ADDRESS+R/W byte (upper 7 bits address, LSB is R/W). This byte, by protocol, is always transmitted and never received. The rest of the buffer will either be sent out or filled by received data according to the R/W bit (1 for read, 0 for write). The second argument is the total size of the buffer (including the address byte).
Here's a brief example. Let's say we want to output the value 0x70 to the internal address 0x12 of the device with address 0x40. First, we must create a buffer to store our transmission:
char i2c_transmit_buffer_len = 3;
i2c_transmit_buffer = (0x40 << 1) | 0 //Or'ing with 0 is unnecessary, but for clarity's sake this sets the R/W bit for a write.
i2c_transmit_buffer = 0x12; //Internal address
i2c_transmit_buffer = 0x70; //Value to write
//Transmit the I2C message
There you have it, message transmission complete! That was easy! If you want more information on the inner workings of the USI_I2C_Master code, just take a look inside the usi_i2c_master.c file where I've commented the states, transfer function, and other interesting sections. I've made use of one-line #define macros to make it more clear what each line's purpose is.
In the next step, I will introduce the slave-mode code, which is significantly more complex but also easy to use from an end-user standpoint. I've taken a different approach on implementing the slave code that I haven't seen in any other tutorials, it's interesting and useful!
Step 7: ATTiny USI I2C Code Implementation - USI I2C Slave
Unlike the master code, the USI I2C Slave code (usi_i2c_slave.c/h) is implemented almost entirely using the USI interrupts. As mentioned earlier, the USI module has two interrupts, one generated upon detection of a START condition and the other based on the overflow of the 4-bit counter. The counter is crucial for the slave code to work correctly, and was not explained very well in tutorials and code I read. In the flowchart image I have noted numbers for each state in the logic. These numbers (8's, 1's, and 0's) are counter count values indicating how many ticks the counter should count before transitioning to the next state. As the counter is clocked using SCL clocks, these values tell how many SCL clock pulses must occur before the next state. In general, something that waits 8 clock pulses is waiting on transmission/reception of a data byte while something that waits 1 clock pulse is waiting on transmission/reception of an ACK/NACK. A few things wait 0 clocks, meaning that they instantaneously continue to the next state or are an expanded part of the previous state (in the case of Write or Read?).
So, as an end-user, you probably are more interested in how to interface the library to your own code! This is easy, and here's why. I've done away with the receive/transmit buffers that were used in other USI I2C implementations (mainly those based on AVR312 app-note) and instead implemented the register bank protocol described at the beginning of this tutorial. The bank is stored as an array of pointers, not data values, so you must attach local variables in your code to memory addresses in the I2C register bank by setting the pointers to point to your variables. This means that your main-line code doesn't ever have to poll I2C buffers or handle data arrivals, the values are updated instantaneously whenever they arrive. It also allows program variables to be polled at any time by the I2C interface without affecting the main-line code (other than the delay due to interrupt). It's a pretty neat system. Let's again take a brief example.
For example, let's say we have a very basic software PWM generator that is driving an LED. We want to be able to change the PWM value (a 16-bit value, just for the sake of learning about pointers) without making the main loop complicated. With the magic of asynchronous I2C slave, we can do just that!
//Define a reference to the I2C slave register bank pointer array
extern char* USI_Slave_register_buffer;
//Create 16-bit PWM value
unsigned int pwm_val = 0;
//Assign the pwm value low byte to I2C internal address 0x00
//Assign the pwm value high byte to I2C internal address 0x01
USI_Slave_register_buffer = (unsigned char*)&pwm_val;
USI_Slave_register_buffer = (unsigned char*)(&pwm_val)+1;
//Initialize I2C slave with slave device address 0x40
//Set up pin A0 as output for LED (we'll assume that whatever chip we're on has pin A0 available)
DDRA |= 0x01;
PORTA |= 0x01; //Turn LED on
for(unsigned int i = 0; i < pwm_val; i++)
PORTA &= ~(0x01); //Turn LED off
And there you have it! The main loop makes no reference to I2C at all, but upon sending a PWM value to the 16-bit location in I2C internal address 0x00/0x01, we can totally control the PWM of the LED! For added stability (to make sure only the pointer values you're using are available and to prevent stray pointers) I suggest you change the #define USI_SLAVE_REGISTER_COUNT to be the number of register pointers you need, no more, no less. When an access (read or write) is attempted on a register index outside of the range 0x00 to USI_SLAVE_REGISTER_COUNT - 1, nothing is written and zero is returned.
Step 8: ATTiny USI I2C Code Implementation - Git the Code!
To get (git) the code, head to my GitHub page! This code was written as part of my Senior Project Stepper Motor Controller, so you can check out my implementation of the motor controller using my I2C drivers. I also have a decent interrupt-driven USART serial driver in there you can use as well.
Note that the names and functionality used in the I2C drivers may vary slightly due to bug fixes and updates that I implement. If it's anything serious I'll edit the tutorial, but for now it should be very accurate.
Now that you're armed with the knowledge of I2C, you're ready to go out and talk to stuff! With I2C's bus design, you can connect a lot (theoretically up to 128, limited by address restrictions though) of devices to the network!
Also, if you haven't got one yet, get yourself a Raspberry Pi, they're awesome. If you order one today, you might have it by the end of the year, but it'll be a very merry Christmas if that's the case. As far as I2C goes it's been a great help and I can't wait to build a robot around it.
We have a be nice policy.
Please be positive and constructive.