I2C (also: I2C) is a circuit arrangement and communication protocol. It allows one or more processors to communicate with one or more special-purpose ICs using a single pair of wires. A group of wires for communication is called a bus, and so this pair is called an I2C bus.
The simpletools library has functions for common I2C tasks. The Propeller microcontroller “master” orchestrates communication with I2C device “subordinates.” Here, we’ll prototype some code to talk with the 64 KB I2C EEPROM built into the Propeller Activity Board (original or WX version). The simpletools library already has functions to do just that, but this activity will give you a starting point for prototyping with other I2C devices that do not already have library support.
Need a quick way to save data to your Activity Board’s EEPROM (original or WX version)? Try these simpletools functions: ee_putByte, ee_getByte, ee_putInt, ee_getInt, ee_putFloat32, ee_getFloat32, ee_putStr, and ee_getStr.
Need more I2C options? The simplei2c library provides access to lower-level communication details with more flexibility. In SimpleIDE, click Help and select Simple Library Reference. Propeller GCC [1] has even more I2C functions with advanced features and faster communication rates.
Many Propeller development boards already have a 64 KB I2C EEPROM connected to P28 and P29:
If you are using a different Propeller board, you will need:
If the EEPROM circuit is already built into your Propeller development board, you are good to go. Just use the code examples as-is with SCL = P28 and SDA = P29.
If you are using a different Propeller set-up, connect the 24FC512 EEPROM using the schematic shown below. You can use a different pair of I/O pins (other than P28 and P29). Just make sure to update the code examples to reflect the different pair of pins.
Let’s look at the connections that the EEPROM on the Propeller Activity Board (original or WX version) needs, in addition to Vcc (3.3 V) and Vss (0 V).
Address pins A2…A0
Each subordinate device on an I2C bus needs its own address. The lowest three digits in EEPROM’s address are determined by applying voltage to the address pins: 3.3 V = 1, and 0 V = 0. In our example, A2...A0 are connected to ground, so those digits will be 000. Up to eight similar EEPROMs could be placed on the same I2C bus, each with a different pattern of 3.3 V and 0 V applied to A2…A0.
SCL
The I2C bus’ SCL (serial clock) line is used to coordinate communication between devices. The EEPROM’s SCL pin connects Propeller I/O pin P28. A pull-up resistor holds the voltage on the SCL line at Vdd (3.3 V in our example below) when no devices are talking.
SDA
The SDA (serial data) line is used to transmit data from one device to another. The EEPROM’s SDA pin connects Propeller I/O pin P29. The SDA line also has a pull-up resistor.
WP
WP stands for write protect, and when it is enabled with a high signal, the data stored inside the EEPROM cannot be changed. Since it is receiving a GND (0 V) low signal here, write protect is disabled. To enable write protect, you could change that connection to 3.3 V.
This test program stores a string of seven character values "abcdefg" to the EEPROM. Then, it retrieves them back and prints them in the SimpleIDE Terminal. To do so, the code performs these steps:
Library Alert! This example relies on Simple Libraries in the (5/14/2014) Learn folder or later. Update your Learn Folder [2].
The i2c_in function will still be able to fetch and display the string, since the EEPROM is designed to hold data without power.
/* Test 24LC512 with I2C.c Test writes data to I2C EEPROM, then reads it back and displays it. */ #include "simpletools.h" // Include simpletools header i2c *eeBus; // I2C bus ID int main() // Main function { eeBus = i2c_newbus(28, 29, 0); // Set up I2C bus, get bus ID // Use eeBus to write to device i2c_out(eeBus, 0b1010000, // with I2C address 0b1010000, 32768, 2, "abcdefg", 8); // send 2 byte address of 32768 // and 8 byte data string. while(i2c_busy(eeBus, 0b1010000)); // Wait for EEPROM to finish char testStr[] = {0, 0, 0, 0, 0, 0, 0, 0}; // Set up test string // Use eeBus to read from device i2c_in(eeBus, 0b1010000, // with I2C address 0b1010000, 32768, 2, testStr, 8); // send 2 byte address of 32768 // & store data in 8 byte array. print("testStr = %s \n", testStr); // Display result }
On this page, we’ll go through the Test 24LC512 with I2C.c code a few lines at a time, to see how the i2c_out and i2c_in functions communicate with the I2C device.
First, let’s get the EEPROM datasheet. You would need the datasheet to prototype code for any I2C device that doesn’t already have library support.
The i2c_out and i2c_in functions we’ll be using have six parameters:
First, the simpletools library is included, to let us use its i2c_in and i2c_out functions. It also includes other libraries, like simpletext, which has the print function.
#include "simpletools.h"
Each I2C bus you declare needs a bus identifier. This identifier keeps the address in Propeller memory where information about the bus is stored. It is declared globally — above main and not inside a function — so it can be used by any function in the program.
i2c *eeBus;
Next, inside main, use the identifier to set up the I2C bus. The i2c_newbus function has three parameters: sclPin, sdaPin, mode.
int main() { eeBus = i2c_newbus(28, 29, 0);
Setting sclPin to 28 and sdaPin to 29 corresponds to the circuit we are using. Mode = 0 is for our circuit’s normal I2C configuration, with a pull-up resistor on the SCL line. Although less common, some boards do not have a pull-up resistor on the SCL line and let the microcontroller drive it, which is when mode = 1 would be used. The Propeller Demo Board and PE Kit platform are examples.
The i2c_newbus function call returns information for the I2C bus ID, which gets stored in eeBus. From this point forward, your code can pass eeBus to the i2c_in and i2c_out function’s busID parameter to select this bus (and not some other I2C bus a project might have) for communication.
Sending data to an I2C device with i2c_out involves telling it to find some memory address and store a value there. Or, in the case of our EEPROM, find a memory address, and store eight values starting from that memory address. Let’s look at how the i2c_out function parameters are used here:
Why 8 bytes for 7 characters? The string are zero terminated, so there’s a 0 that follows those characters that doesn’t show. The print function needs that zero to properly display strings. So, make sure to add 1 to the size of your character strings contained in quotes.
i2c_out(eeBus, 0b1010000, 32768, 2, "abcdefg", 8);
So, how would you go about finding out details like the EEPROM’s I2C address? Or the number of bytes that you have to send it to pick one of its internal memory addresses? Read the device’s datasheet. They are usually written for industry professionals, so you might need to read and re-read certain sections and write pieces of test code to check how the device replies. But with time and persistence, you will succeed, and it gets easier with practice.
Read some selections from the I2C datasheet to understand the I2C address above. As you read, the main tasks are to focus on the I2C address, how to select one of the EEPROM’s memory addresses, and how to send it data bytes. The i2c_out and i2c_in functions take care of details like start conditions, read/write bits, and ACK/NACK signals, so don’t worry about those portions.
Some I2C devices don’t also need a memory address, just data. If that’s the case for your device, use NULL in place of the memory address, and 0 for the number of bytes in the memory address. Also use NULL and 0 in transactions that involve just issuing a memory address without data, in which case, NULL and 0 would be in place of *data and dataCount.
Some I2C devices stop responding while they are processing, such as our EEPROM while it is storing data it just received. Devices like this can be polled for availability with the i2c_busy function, which returns 1 if the device is not responding (because it’s busy), or 0 if it responds (which means it’s ready for more). Putting the i2c_busy function call in a while loop lets the program execution pause until the I2C device is available again.
while(i2c_busy(eeBus, 0b1010000));
To verify that the i2c_out function actually stored the data in the EEPROM, we’ll use i2c_in to retrieve it. But first, we need to make a character array full of zeroes as a place to store the data that’s about to be retrieved from the EEPROM.
char testStr[] = {0, 0, 0, 0, 0, 0, 0, 0};
(An alternate approach to this string of zeroes would be char testStr[8]; followed by memset(testStr, 0, 8);)
Now the program is ready for the read operation. This i2c_in call selects the same I 2 C bus, I2 C chip address, and points to the EEPROM’s 32768 memory address. The difference is the *data parameter. Instead of sending a pointer to "abcdefg" string, this call passes a pointer to the testStr array that was just created.
i2c_in(eeBus, 0b1010000, 32768, 2, testStr, 8);
Last, this print statement displays the data in the testStr array with the SimpleIDE Terminal. If testStr contains "abcdefg" then mission accomplished!
print("testStr = %s \n", testStr); // Display result
The term I2C comes from the name Inter-Integrated Circuit, which would be abbreviated IIC. If those three letters were variables in an equation multiplied together, the two I terms would be I x I or I2. So, that’s where I2C comes from. It’s often pronounced I-squared-C, though I-two-C is also understood.
The Load EEPROM button copies the example Propeller program to your board's EEPROM, filling addresses 0 to 32767. So, 32768 is the first available memory address for data storage. Some Simple Libraries keep calibration data in the EEPROM. By convention, these libraries use the highest available EEPROM addresses, so if your application is logging data, it’s best to start from 32768 and allow data to accumulate upward to higher addresses.
Libraries that use EEPROM this way list the memory locations they use in their documentation. For an example, open …\Documents\SimpleIDE\Learn\Simple Libraries\Convert\libabvolts\Documentation abvolts Library.html, scroll to the Detailed Description heading, and check the EEPROM Usage info. You might also notice these addresses in the Macros section.
A typical I2C packet has a first byte that begins with the microcontroller master (the Propeller in this case) sending a start condition signal that takes the SDA line low, while the SCL line is high. After that, the Propeller repeatedly pulls the SCL line low and then releases it, allowing the resistor to pull it up to 3.3 V; this sends a low…high…low…high… clock signal. Each time SCL is high, the Propeller transmits the next binary data value by either pulling the SDA line low to send a binary 0, or leaving it pulled high by the resistor to send a binary 1.
The first seven bits sent are the target chip’s I2C address, transmitted most significant bit first. For example, if the address is 0b1010000, it will send the 1 first, then the 0, another 1, and four zeroes. The last data bit in the first byte is called the read/write bit. If this bit is 1 (read), it tells the I2C chip to respond with data. If it’s 0, it means the I2C chip is about to receive data. In this case, the read/write bit is set to 0, so the Propeller is going to send more information to the EEPROM. When the Propeller sends the 9th CLK pulse, it releases control of the SDA line so that the I2C chip can either respond with a low value to acknowledge (ACK) that it is ready to continue, or a high value to not acknowledge (NACK).
Note: These timing diagrams assume that the program used i2c_out(eeBus, 0b1010000, 32771, 2, "abcdefg", 8); and are showing the timing for i2c_in(eeBus, 0b1010000, 32771, 2, testStr, 8); The memAddr parameter has been changed from 32768 (0b10000000000000000) to 32771 (0b10000000000000011) to help make the start and end of the values more apparent in the diagrams.
Next, the Propeller sends two more bytes to select the memory address in the chip for reading data from or writing data to. Many I2C devices only need one memory address byte. The 24LC512 has 65536 memory addresses, so it takes two bytes to describe all the values from 0 to 65535. Although the Propeller chip controls the both the SCL and SDA lines during the first 8 pulses, it releases the SDA line on each 9th pulse to allow the I2C device to send either an ACK or NACK reply.
In the case of our EEPROM, that command byte tells the chip what memory address to set its pointer to. An I2C sensor might be pointed to a memory address to receive configuration values, or one that stores its sensor measurements for reading. Other devices like I/O expanders might be pointing to a memory address that contains the input or output pin states. After the command, one or more data bytes are sent to (write) or received from (read) the chip. Whether the data is one byte or many bytes depends on how the chip is designed, and it’s one of many details that get explained the device’s datasheet.
At this point, the i2c_in function has set the EEPROM’s memory address pointer. Now, it’s time to read the data bytes from the EEPROM. This is initiated by another byte with the start condition, I2C address, and read/write bit and ACK from the I2C device. This time, the read/write bit is set to 1 for a read operation, meaning the I2C device will have to start replying with data.
For a read operation, the Propeller continues supplying SCL pulses, but now the I2C chip controls the SDA line for 8 pulses as it replies with data, and the Propeller has to reply with an ACK or NACK on SDA during every 9th SCL pulse.
Now that we have done some basic communication with simple byte data and one I2C device, let’s expand on that. First, let’s modify the original code to work with other data types besides bytes. Then, if you are interested in going a bit further, the Advanced Topic section will show you a method you could use to put two additional EEPROM DIP chips (plugged into your breadboard) on a second I2C bus.
Data is written to and read from I2C devices in the form of one or more eight-bit bytes. In many cases the data written to an I2C chip originated as a different data type, like maybe a 32-bit int or even a float.
Likewise, data bytes read from I2C chips may need to be reassembled back into the data those bytes actually represent. For example, a sensor might be returning a signed 16-bit short value with two bytes, or an EEPROM or other memory device might be storing and returning ints, floats, or some other type.
So, here is an example that shows how an int variable can be converted to bytes, stored in EEPROM, retrieved from EEPROM, and reassembled back into an int value. It does this process with the number 5280, stored in an int variable.
Along with storing and retrieving an int value, this program gives names to some of the constant values from the previous example. The main trick for the variable-to-bytes conversion is to pass the address of the variable in question, cast as a char pointer, and make sure to specify the correct number of bytes for the data type.
So, this program starts with int val = 5280. Notice how i2c_out(eeBus, eeAddr, memAddr, 2, (char*) &val, 4) uses (char*) &val to cast the address of the val int variable as a pointer to a character variable. The dataCount parameter following it is also set to 4, since an int variable has four bytes. The reverse of this process is accomplished with i2c_in(eeBus, eeAddr, memAddr, 2, (char*) &val, 4).
This is meant to be a theoretical exercise, not a hands-on activity. If you have the two additional EEPROMs required for this example and wish to try adding a second bus to your board, then the information provided here can be used as a guide.
Below is an example of an I2C bus you could add to your Activity Board’s breadboard (original or WX version). It has two more I2C EEPROMs just like the one on the Activity Board’s P28/P29 bus.
Please Note: The top 24LC512 shown in the schematic has its A0, A1 and A2 pins set the same as the one on P28 and P29. But keep in mind, it’s on a completely different bus, so it’s okay for it to have the same address. Since the bottom 24LC512 is on the same bus, it has to have a different address.
(2) 24LC512 EEPROM
It’s easy to expand your program in order to communicate with all three EEPROMs; these two and the one built into your Propeller board. The steps are pretty much the same:
i2c *eeBus2;
eeBus2 = i2c_newbus(7, 6, 0);
// Write to upper EEPROM on second bus i2c_out(eeBus2, 0b1010000, cmd, 2, "mnop", 5); // Write to lower EEPROM on second bus i2c_out(eeBus2, 0b1010001, cmd, 2, "qrstuvw", 8); ...
Links
[1] https://sites.google.com/site/propellergcc/
[2] http://learn.parallax.com/propeller-c-set-simpleide/update-your-learn-folder
[3] https://learn.parallax.com/sites/default/files/content/propeller-c-tutorials/Protocols/DIY I2C/21754M.pdf