I love the Atmel AVR microcontrollers! Since building the Ghetto Development System described in this Instructable, I’ve had no end of fun experimenting with the AVR ATtiny2313 and the ATmega168 in particular. I even went so far as to write an Instructable on using switches as inputs, and extended the Ghetto Development System concept to CPLDs.
During a recent project, I needed several switches for setting control values. The AVRs did not have enough I/O pins, so I had to think of something. I could have tried a complex input system with a keyboard and display, but the ATtiny2313 would have run out of resources. Fortunately, Atmel has provided a way around this problem by including an interface that can link to additional chips (such as memory or I/O ports) with a simple two wire interface. That’s right, by using just two I/O pins on an AVR we can access many additional I/O pins, and other resources as well.
This two wire interface is formally known as the Inter-Integrated Circuit bus, or just the I2C bus and was invented by NXP when it was still Philips Semiconductors. If you’re reading this Instructable then you’ve probably heard of the I2C bus and may even have used it on a PIC or other microcontroller. While conceptually very simple, and supported by hardware resources on the AVRs, software drivers are still necessary to use the I2C bus. Atmel provides Application Notes (see the Resources later in this Instructable), but these are incomplete and don’t show any examples beyond communicating with another AVR device.
It is not the purpose of this Instructable to teach anyone how to create I2C drivers for the AVRs. Rather, I’ll provide expanded versions of the Atmel drivers for ATtiny2313 and ATmega168 devices, I’ll explain the requirements and restrictions that apply when using these, and I’ll show you working examples of I2C devices. After you work through this Instructable you’ll be able to use the I2C bus successfully in your AVR projects. Obviously, you can ignore the drivers for either tiny or MEGA if you’re only interested in one of them. For those interested in learning more about the I2C bus, I’ll provide links to appropriate material.
Step 1: What’s All This I2C Stuff Anyway?
The I2C bus is a simple, two-wire connection that can link multiple devices together and allow them to exchange data. In its simplest form there is one master device that communicates to multiple slave devices. All devices are connected in parallel to the two wires of the I2C bus. The two wires are known as SCL and SDA. SCL is the clock line and is controlled by the master device. SDA is the bi-directional data line. To transfer data, the master sends out a slave address combined with a one bit read/write flag. If a write is desired, the master will continue to send data to the addressed slave. If a read is requested, the slave will respond with data. To coordinate transactions, the SCL and SDA lines are manipulated by the master and the slave to signal several conditions. These include START, STOP, ACK (acknowledge) and NAK (no acknowledge). The details of these conditions are handled by the drivers. The true geeks among you can learn all the details in the links provided at the end of this Instructable.
The electrical requirements are pretty simple. The master and the slaves must use the same level for Vcc, the grounds must be connected, and the SCL and SDA lines must be pulled up to Vcc. The value of the pull-up resistors is precisely determined by a calculation based on the total capacitance on the bus, but practically can be pretty much any value between 1.8K and 10K. I start with 5.1K and use lower values until it works. This usually isn’t an issue unless you have a lot of devices or long lengths of wire between devices.
The nominal data rate on the I2C bus is 100Kbits/second. Rates of 400Kbits/second, 1Mbits/second, and beyond are possible as well, but aren’t supported by the drivers in this Instructable. All I2C devices will work at 100Kbits/second.
The ATtiny2313 and the ATmega168 each implement the I2C bus differently. ATtiny2313 uses the Universal Serial Interface (USI) hardware – which can also be used for the SPI bus. ATmega168 has dedicated hardware for the I2C bus known as the Two Wire Interface (TWI). Once the drivers are written, these differences are mostly transparent to the user. One significant difference is in the software: The ATmega168 I2C driver is interrupt driven while that for the ATtiny2313 is not. This means that an ATmega168 program does not have to wait for I2C data transfers to take place, but only needs to wait before initiating another transfer, or until data arrives from a read operation. The examples and discussion to follow should make this clear.
I2C addresses are 7 bits long, so up to 127 devices can be on the bus if each has a unique address. As shown in the figure, this 7 bit address is shifted left one bit and the least significant bit is used to flag a read or write of the device at the address. Thus the complete slave address is an 8 bit byte. The actual address is partially determined internally to the device and can’t be changed (4 most significant bits), and partially determined by bits that may be connected to device pins (3 least significant bits) that can be tied high or low to set a specific address.
Sounds confusing, but an example will make this clear. The PCA8574A data sheet shows that the four most significant bits of the I2C address will always be 0111. The next three bits are determined by the settings on pins AD0, AD1 and AD2. These pins can be tied to ground or to the positive voltage supply (5 volts) to represent 0 or 1 respectively. So the range of possible addresses is 38 to 3F hexadecimal, as shown in the other figure from the PCA8574 data sheet. So by changing the address bit settings, up to 8 PCA8574As can be on the I2C bus at the same time. Each will respond to its specific slave address only. If even more I/O ports are needed, the PCA8574 can be used. The only difference between the PCA8574 and the PCA8574A is that the I2C slave address range of the PCA8574 is 20 to 27 hexadecimal.
Determining the address of a given device can be confusing since some data sheets consider the read/write bit to be part of the address. Read the data sheet carefully and keep in mind that the slave address will be 7 bits long. The read/write bit should be treated separately. Again, an example will help. The data sheet for the 24C16 EEPROM we’ll experiment with says the first (most significant) four bits of the slave address are 1010. The next three bits can be determined by A0, A1 and A2; but note the data sheet also covers 24C01 through 24C08 which are smaller sized EEPROMs. The figure from the data sheet shows that the settings of these address bits are ignored as the size increases and are completely ignored for the 24C16. That is, the last three bits don’t matter and the 24C16 really uses all I2C slave addresses 50 through 57 hexadecimal. The range of slave addresses will actually address different sections within the 24C16. The first 256 bytes are at address 50h, the next 256 at 51h, and so on up to the last 256 at 57h – for a total of 2K bytes. Since the address of the PCF8570 RAM we also experiment with is in this range, the 24C16 and the PCF8570 can’t be used together.
Step 2: Order Some I2C Devices
Now that you know a little about the I2C Bus and want to use it, why not order some I2C devices to experiment with now so they can be on the way to you while you’re getting the software ready?
Appropriate devices include an I/O Interface Expander (my favorite), a Static Ram, and an EEPROM. There’s lots more, but these are a great start. The AVR processors we’ll use are the ATtiny2313 and the Atmega168 (used in Arduino). If you’re new to these, then have a look at this great Instructable to learn about them and build your Ghetto Development System. The schematic of the ATmega168 in the present Instructable shows how to implement the Ghetto Development System for this processor. The parallel port cable is the same as the one for the ATtiny2313. (I haven’t tried the USB version of the Ghetto Development System, so I’m not sure how the I2C bus is accessed on it. Same for the Arduino.)
Here are Digikey part numbers.
IC I2C I/O EXPANDER 568-4236-5-ND
IC SRAM 256X8 W/I2C 568-1071-5-ND
IC EEPROM SERIAL 16K CAT24C16LI-G-ND
Step 3: I2C Drivers
Here are the descriptions of the driver functions for the I2C bus. These were developed using the Atmel Apps Notes for starters. I couldn’t have done this without them as a base to build on. Development was done using WinAVR and the gcc C compiler.
Clock rate restrictions are described below for each processor. Since I am not able to test all the processor flavor / clock rate combinations possible, I’ll just stick to what I actually can test and try to indicate the restrictions and limitations.
Here are the driver functions and how to use them. Please look at the examples for more details and to see the functions in use in complete programs.
For the ATtiny2313:
The drivers are designed for a clock rate of 1MHz (the default rate) for ATtiny2313. If you want to run at other rates, then you’ll have to adjust constants in the drivers. Email me if you need help doing this. You can also get some hints from the Atmel apps notes in the links in the Resources Step.
This function initializes the USI hardware for I2C mode operation. Call it once at the start of your program. It returns void and there are no arguments.
This function returns I2C error information and is used if an error occurred during an I2C transaction. Since this function only returns an error code, I use the function TWI_Act_On_Failure_In_Last_Transmission(TWIerrorMsg) to flash an error LED. The error codes are defined in USI_TWI_Master.h. Here’s how to call it:
This function is used to read and write single bytes to I2C devices. It is also used to write multiple bytes. There are 6 steps to using this function.
1)Declare a message buffer in your program to hold the slave address and the data byte to be sent or received.
unsigned char messageBuf (MESSAGEBUF_SIZE);
2)Put the Slave Address as the first byte in the buffer. Shift it one bit left and OR in the Read/Write bit. Note the Read/Write bit will be 1 for a Read and 0 for a Write. This example is for a Read.
messageBuf(0) = (TWI_targetSlaveAddress<<TWI_ADR_BITS) | (TRUE<<TWI_READ_BIT);
3)When doing a Write, put the byte to be written into the next location in the buffer.
4)Call the USI_TWI_Start_Read_Write function with the message buffer and the message size as arguments.
temp = USI_TWI_Start_Read_Write( messageBuf, 2 );
5)The returned value (temp in this case) can be tested to see if an error occurred. If so, it is handled as discussed above. See examples in the programs.
6)If a Read was requested, the byte read will be in the second location in the buffer.
If multiple bytes are to be written (such as to a memory device), this same routine can be used. Setting up the buffer and calling the routine are slightly different. The second byte in the buffer will be the starting memory address to which to write. The data to be written will be in subsequent bytes. The message size will be the size including all the valid data. So if 6 bytes are to be written, then the message size will be 8 (slave address + memory address + 6 bytes of data).
This function is used to read multiple bytes from an I2C device, typically it’s only meaningful for a memory of some sort. Using this routine is very similar to the previous routine, with two exceptions.
The setting of the Read/Write bit doesn’t matter. Calling this routine will always cause a Read operation.
The messageSize should be 2 plus the number of bytes to be read.
If no errors occurred, the data will be in the buffer beginning at the second location.
For the ATmega168:
The drivers are designed for a clock rate of 4MHz for ATmega168. The example code shows how to set this clock rate. If you want to run at other rates, then you’ll have to adjust constants in the drivers. Email me if you need to do this.
This function initializes the TWI hardware for I2C mode operation. Call it once at the start of your program. It returns void and there are no arguments. Be sure to enable interrupts by calling swi() after initializing.
This function returns I2C error information and is used if an error occurred during an I2C transaction. Since this function only returns an error code, I use the function TWI_Act_On_Failure_In_Last_Transmission(TWIerrorMsg) to flash an error LED. The error codes are defined in TWI_Master.h, but are modified for signaling on an error LED. See the example code for details. Here’s how to call it:
Note that error checking is done by making sure that the I2C transaction is complete (function described below) and then testing a bit in the global status word.
These two functions work the same as the corresponding functions described above but with a few exceptions.
They don’t return any error values.
Data read is not transferred into the buffer. Doing this will be done with the function described next.
When calling TWI_Start_Random_Read, the messageSize should be the number of data bytes requested plus one, not two.
The I2C driver for the ATmega168 is interrupt driven. That is, the I2C transactions are started and then execute independently while the main routine continues to run. When the main routine wants data from an I2C transaction that it started, it must check to see if the data is available. The situation is the same for error checking. The main routine must be sure that the I2C transaction is complete before checking for errors. The next two functions are used for these purposes.
Call this function to see if an I2C transaction is complete before checking for errors. The example programs show how to use this.
Call this function to transfer data from the I2C driver’s receive buffer into the message buffer. This function will make sure the I2C transaction is complete before transferring the data. While a value is returned by this function, I find checking the error bit directly to be more reliable. Here’s how to call it. The message Size should be one greater than the number of data bits desired. The data will be in messageBuf starting at the second location.
temp = TWI_Read_Data_From_Buffer( messageBuf, messageSize );
Read more: I2C COMMUNICATION WITH PIC MICROCONTROLLER