I2C Advanced Functions
In part 1 of the I2C tutorial, I showed how the I2C standard works at the electrical level and at the physical level. In part 2, I showed how the I2C module functions on the PIC24. In addition, the basic I2C functions were listed and explained. In this portion of the I2C tutorial, I will demonstrate two advanced read and write functions.
Sequential Write and Read
Similar to the random read and write, the sequential read and write is a way to transfer large amounts of data with as little protocol overhead as possible. Recall that in a random write, 3 bytes needs to be transferred for a single piece of data to be written. In a random read, 4 bytes must be transferred for a single piece of data to be read. The only way to reduce this over head is to use the sequential read and write. This is where the ACK signal plays a huge part of how the data get transferred.
In a random write, three bytes are sent to the slave device. After each byte, the slave must respond with an ACK. After the last byte, the master device must send a stop bit.

A sequential write is very similar to a random write. However, after the third byte (the data byte) is sent to the slave, the master does not send a stop bit. It sends the byte it wishes to write to the subsequent memory address.
For example, let’s say I want my PIC24 to write to a slave device with the address 0xA0. I want to write three bytes of data to the memory address 0×10, 0×11, 0×12. I want to write 0×44, 0×55 and 0×66, respectively, to those memory addresses on the slave device.

The packet I must create on my PIC24 would be like this:
1. Start bit
2. A0 (slave address + write command)
3. ACK from slave
4. 0×10 (start of chip sub-address)
5. ACK from slave
6. 0×44 (first data byte, written to address 0×10)
7. ACK from slave
8. 0×55 (second data byte, written to address 0×11)
9. ACK from slave
10. 0×66 (last data byte, written to address 0×12)
11. ACK from slave
12. Stop bit
As you can see, we only specified the start address of the sequential write at 0×10. After each data byte that we send to the device, internally, the device increments the chip sub-address. In this manner, a large number of bytes can be written in one packet. Note that the same operation can also be completed with a series of individual packets, albeit with a slower total access time. Note that after every byte, the slave device MUST return an ACK bit to indicate a successful write.
On most EEPROM chips however, there is a limit to the number of bytes you can write per packet. Usually this number is 8 byes. This is because the EEPROM has to erase the previously written data, and then load the current data. The write cycles on EEPROMs are usually on the order of several milliseconds. The only way to know if the EEPROM is ready or not to receive the next byte is to look at the ACK after every byte sent by the master device. For example, on Microchip’s 128 bit EEPROM 24LC22A, there is an 8 byte page buffer. This means that I can use a page write to write up to 8 bytes. On the 9th byte written to the device, it will no longer return an ACK bit back to the master, signaling an error. Therefore, the programmer MUST terminate the packet with a STOP bit after the 8th data byte.

In a sequential read, the mechanism is very similar. However there are usually no read buffer limitations because there are no electrical cycles to limit how fast you can read data. Contrary to the sequential write however, instead of the slave device giving an ACK bit to indicate a successful write, the master device signals the ACK to the slave device to indicate that it is ready to receive the next byte.

In a similar manner to the sequential write example above, if I want to read the data in the addresses 0×10, 0×11, 0×12 on the slave device 0xA0, the packet that I need to send would be something like the following:
1. Start bit
2. A0 (slave address + write command)
3. ACK from slave
4. 0×10 (start of chip sub-address)
5. ACK from slave
6. Restart bit
7. A1 (slave address + read command)
8. ACK from slave
9. First data read from slave (read from address 0×10)
10. ACK from master (signal to slave that the master device wants more data)
11. Second data read from slave (read from address 0×11)
12. ACK from master (signal to slave that the master device wants more data)
13. Third data read from slave (read from address 0×12)
14. NOACK from master (signal to slave that the master device wants no more data)
15. Stop bit
As you can see the ACK and NOACK plays an important role in signifying when to end the transmission.
The Functions
Typically, in my standard I2C API, I don’t keep a sequential read/write function of variable lengths. When an application require such a function, I find the most efficient way to implement the solution is to write a custom function with a specific length of sequential read/write.
I do use a sequential read/write function of length 2 very often, and I will share the code on this tutorial. The functions I show here use the basic functions found in part 2 of the tutorial. The following function writes two bytes in a row to the slave device:
“void I2Cwritedouble(char addr, char subaddr, char valuelow, char valuehigh)”
void I2Cwritedouble(char addr, char subaddr, char valuelow, char valuehigh)
{
i2c_start();
send_i2c_byte(addr);
send_i2c_byte(subaddr);
send_i2c_byte(valuelow);
send_i2c_byte(valuehigh);
reset_i2c_bus();
}
Similarly, the following function reads two bytes in a row from the slave device:
“struct doublechar I2Creaddouble(char addr, char subaddr)”
struct doublechar I2Creaddouble(char addr, char subaddr)
{
dchar temp;
i2c_start();
send_i2c_byte(addr);
send_i2c_byte(subaddr);
DelayuSec(10);
i2c_restart();
send_i2c_byte(addr | 0x01);
temp.x = i2c_read_ack();
temp.y = i2c_read();
reset_i2c_bus();
return temp;
}
You can see the problem with a function that wants to return two bytes of data. I need a way to pass two bytes of data, but a function returns only allow one. To get around this problem, I create my own data type with two chars in it, inserted into the header file:
typedef struct doublechar
{
char x, y;
}dchar;
When my I2Creaddbouble() function returns a value, I can now read both pieces of data. Similarly you can create loops for longer length sequential transitions. It should be obvious that optimizing the loops for read and write speed is the key to having robust I2C transactions. I’m sure you can figure out how read and write larger lengths of data from these examples.
The last part of the tutorial will be an example of how to use all of these functions, along with the “#include” directives and file structures.
Table of Contents
Your site is very helpful, BTW when can you post the last part of your tutorial? I’m really looking forward to it.
Im new to C30 compiler so i would like to know the file structure for C30. Im also doing an I2C interface using PIC24 but it’s a SLAVE and I’m planning to do it in C30.
Thanks.
Which part do you mean? The ADC or the PWM?
-J
both!!
Great tutorial, really helped coming from pic basic
“The last part of the tutorial will be an example of how to use all of these functions, along with the “#include” directives and file structures.”
Will this part be added?
Yeah, I was thinking about that. However, I don’t think there is too much to write about. What in particular are you looking for? There is a tutorial on a Uart to I2C converter already that deals with usage. If there’s enough topics that’ll fill up a section I might write something. What I was thinking is maybe a way to unify driver writing (such that a driver will work on all PICs, independent of model, but that hardly seems appropriate in the I2C section). Please reply with exactly what you are looking forward to reading about.
-J
Thanks for the response. I was looking for the next tutorial as I was having a problem implementing the code but I’ve managed to sort it.
I’ve a new question and a problem
In the reset bus function you use “if (x > 20) break;” this is a timeout? If so why do would you want it to time out, wouldn’t you want to wait till it was confirmed that the bus has been put into the idle state?
I ask because I’m getting a problem where I write it doesn’t always reset the bus, think this is problem with my code somewhere because another program works fine.
You should not always expect your code to work, and there should be code to be able to recover from conditions in which errors might happen. There is a timeout because if my connection is bad, or a short has developed somewhere, or I forgot to turn a dipswitch, the code keeps on executing. However, if it is not there, it might go into an infinite loop and never recover.
-J
Awfully quiet…
Thank you very much for your help.
I am using 24c512 and rtcc ds1337 on I2C.
I have implemented your code but it doesn’t work instead it hangs on RBF receive flag.If I put delay of 10000 instruction on 16MHz clock at every read and write then it works all right.
Could you help me ?
@Hitesh. Can you give me some more information? It is difficult to understand without an overall picture of what you are trying to do and under what circumstances this happens, etc.
Thanks,
-J