10.2 I2C – Part 2 – Basic Functions

10.2 I2C – Part 2

In this section, I will outline some of the basic functions for the I2C module on a PIC24. Since the bulk of the electrical functions were explained in section 1 of the tutorial, this section mainly focuses on the I2C module of the PIC24.

Word of Caution: Silicon Errata

There is a bug in the PIC24FJ64GA004 series of chips (16GA002, 32GA002, 48GA002, 64GA002, 16GA004, 32GA004, 48GA004, 64GA004). The bug is listed in the silicon errata section on Mircochip’s website. Go there and search for Silicon Errata, and select the chip you are using. If the chip you are using is not affected by the error then you are good to go. However, if the error is present you’ll need to try to get around the problem. In the PIC24F series of chips, there are two I2C modules. The I2C1 module does NOT work correctly in the master mode. You can read more about it in my post on Microchip’s developer forums.

There is a work around to making the I2C1 module work, but it is clumsy and does not always guarantee success. For this reason, if you are thinking about using the I2C module, I would suggest using only the I2C2 module on the PIC24F64 family, or just avoid the whole PIC24FJ64 family all together. I much prefer the PIC24H family anyways.

The Registers

As always, the starting place to understanding how to a module is with the registers. In this case, there are mainly 3 registers of great concern.

The first is I2CxCON. This register is the main control register. With it, you can initiate start bit, stop bits, and test to see if acknowledge bits were received. The next one is the I2CxSTAT. This register allows you to see what the current status of the I2C module. This may include polling to see if there are errors, bit collisions, receive buffer overflows and other occurrences. Lastly there is the I2CxBRG, which determines the bit rate at which the module operates.

There are other minor registers such as I2CxRCV, I2CxTRN and I2CxMSK. The I2CxRCV register is used to retrieve received data. The I2CxTRN register is used to send data. The I2CxMSK is not used in our example since it is only used when the module is set to Slave mode.

When using the I2CxCON register, you must be careful with some of the initiation bits. When setting a Start or Stop bit, the hardware will automatically clear the set bit when it is ready to proceed with processing data. This means that it is not enough to just initiate a start bit and then proceed to send or receive data. You must keep polling that bit until the PIC automatically clears the bit, and only then proceed with processing data. There will be more on this matter later in the basic functions.

Using the I2C Module

The way I approach the I2C module is analogous to making sentences. I start with letters, which adds up to words, and combine them to make sentences. The individual base commands and the fiddling with the registers are kind of like the “letters”. I use these commands to form basic functions, functions that write or read one byte (words), and finally I combine these functions into full packets, with address, sub-address, and data (full sentences).

Recall that a basic “write” packet is composed of a chip (or slave) address, a sub-address, and the data. A “read” packet is composed of a chip address and a sub-address in the write configuration, followed by a chip address and the data sent by the slave chip.

Each of the individual bits and bytes must be sent is a specific order. The whole packet must be sent correctly for the slave to understand the intention of the master.

There are quite a few functions in this tutorial. I don’t think I need to go through every one of them, but I will annotate here and there if things are not very clear.

Initiation

Several things must be initiated before using the module. I use the following function.

“void i2c_init(int BRG)”


//function initiates I2C1 module to baud rate BRG
void i2c_init(int BRG)
{
   int temp;

   // I2CBRG = 194 for 10Mhz OSCI with PPL with 100kHz I2C clock
   I2C1BRG = BRG;
   I2C1CONbits.I2CEN = 0;	// Disable I2C Mode
   I2C1CONbits.DISSLW = 1;	// Disable slew rate control
   IFS1bits.MI2C1IF = 0;	 // Clear Interrupt
   I2C1CONbits.I2CEN = 1;	// Enable I2C Mode
   temp = I2CRCV;	 // read buffer to clear buffer full
   reset_i2c_bus();	 // set bus to idle
}

The “reset_i2c_bus()” function is a basic function that set the I2C bus to an idle state. This way the bus is ready to be used immediately after initiation.

Usually, slave devices are rated for up to 100 kHz. The “fast” specifications require that devices function up to 400 kHz. In the newest spec for the I2C bus, the “ultra fast” allows devices to function up to 1 Mhz, which is really fast. I rarely need anything that fast, and generally just stick to something slow like 40 kHz or so. It doesn’t really matter what baud rate you pick since the controller is sending the clock signal anyways. With a 10 MHz oscillator, I usually set the BRG value to about 100. This works for most applications.

Basic Functions

In order to initiate a start bit or a stop bit, there are some intricacies that a user must be aware. First, as I mentioned earlier, the hardware automatically clears certain bits in the I2CxCON register. Again, I cannot emphasize how important it is to read and UNDERSTAND the datasheet. Because of the way these bits work, when I want to initiate a start or stop condition, I must write a loop that keeps on polling the bit until the hardware clear, after which I can proceeding to do my sending and receiving. Another way to do this would be to put a long delay, long enough that assure the occurrence of the hardware clear. I usually take the former approach, as it requires more coding, but is more efficient. The following is how I write the function to initiate a start bit and restart bit:

“void i2c_start(void)”


//function iniates a start condition on bus
void i2c_start(void)
{
   int x = 0;
   I2C1CONbits.ACKDT = 0;	//Reset any previous Ack
   DelayuSec(10);
   I2C1CONbits.SEN = 1;	//Initiate Start condition
   Nop();

   //the hardware will automatically clear Start Bit
   //wait for automatic clear before proceding
   while (I2C1CONbits.SEN)
   {
      DelayuSec(1);
      x++;
      if (x > 20)
      break;
   }
   DelayuSec(2);
}

“void i2c_restart(void)”


//Resets the I2C bus to Idle
void reset_i2c_bus(void)
{
   int x = 0;

   //initiate stop bit
   I2C1CONbits.PEN = 1;

   //wait for hardware clear of stop bit
   while (I2C1CONbits.PEN)
   {
      DelayuSec(1);
      x ++;
      if (x > 20) break;
   }
   I2C1CONbits.RCEN = 0;
   IFS1bits.MI2C1IF = 0; // Clear Interrupt
   I2C1STATbits.IWCOL = 0;
   I2C1STATbits.BCL = 0;
   DelayuSec(10);
}

I also need a function to initiate a stop bit. However, I realize that when I initiate a stop bit, what I really want to do is put the I2C bus in an idle state. For this reason, I reset a whole bunch of registers every time I initiate a stop bit to clear them of any previous errors and ACKs. Again beware of automatic hardware clears as in the case of I2C1CONbits.PEN.

“void reset_i2c_bus(void)”


//basic I2C byte send
char send_i2c_byte(int data)
{
   int i;

   while (I2C1STATbits.TBF) { }
   IFS1bits.MI2C1IF = 0; // Clear Interrupt
   I2CTRN = data; // load the outgoing data byte

   // wait for transmission
   for (i=0; i<500; i++)
   {
      if (!I2C1STATbits.TRSTAT) break;
      DelayuSec(1);

      }
      if (i == 500) {
      return(1);
   }

   // Check for NO_ACK from slave, abort if not found
   if (I2C1STATbits.ACKSTAT == 1)
   {
      reset_i2c_bus();
      return(1);
   }

   DelayuSec(2);
   return(0);
}

Send and Receive, Write and Read

The basic “send” function is written as follows:

“char send_i2c_byte(int data)”


//basic I2C byte send
char send_i2c_byte(int data)
{
   int i;

   while (I2C1STATbits.TBF) { }
   IFS1bits.MI2C1IF = 0; // Clear Interrupt
   I2CTRN = data; // load the outgoing data byte

   // wait for transmission
   for (i=0; i<500; i++)
   {
      if (!I2C1STATbits.TRSTAT) break;
      DelayuSec(1);

      }
      if (i == 500) {
      return(1);
   }

   // Check for NO_ACK from slave, abort if not found
   if (I2C1STATbits.ACKSTAT == 1)
   {
      reset_i2c_bus();
      return(1);
   }

   DelayuSec(2);
   return(0);
}

The return of the function sends back a 1 if there is an error, and a 0 if the transmission went through correctly.

I use two different read functions. This is because a sequential read requires the use of an ACK sent by the master to initiate the next byte. A random read of a single byte of data does not require an ACK to be sent.

“char i2c_read(void)”


//function reads data, returns the read data, no ack
char i2c_read(void)
{
   int i = 0;
   char data = 0;

   //set I2C module to receive
   I2C1CONbits.RCEN = 1;

   //if no response, break
   while (!I2C1STATbits.RBF)
   {
      i ++;
      if (i > 2000) break;
   }

   //get data from I2CRCV register
   data = I2CRCV;

   //return data
   return data;
}

“char i2c_read_ack(void)”


//function reads data, returns the read data, with ack
char i2c_read_ack(void)	//does not reset bus!!!
{
   int i = 0;
   char data = 0;

   //set I2C module to receive
   I2C1CONbits.RCEN = 1;

   //if no response, break
   while (!I2C1STATbits.RBF)
   {
      i++;
      if (i > 2000) break;
   }

   //get data from I2CRCV register
   data = I2CRCV;

   //set ACK to high
   I2C1CONbits.ACKEN = 1;

   //wait before exiting
   DelayuSec(10);

   //return data
   return data;
}

Putting it Together

There you have it. Those are the basic “words” in a sentence. To use my basic I2C functions, I need to follow the I2C standard.

The I2C standard requires that a random write be sent in the following manner:

The PIC (master) must send a start bit, followed by a device address, with the WRITE command. The slave then sends an ACK to acknowledge the byte. Next the master sends the sub-address, followed by a slave ACK. Lastly it sends the data, followed by a slave ACK, and finishes with a stop bit.

If you look closely at the “send_i2c_byte(int data)” function, you’ll see that it waits for an ACK from the slave. If a /ACK is detected (no acknowledge detected), then it returns an error and sets the I2C bus back to an IDLE state. This is exactly what we need.

A random write function is composed of “words” of basic functions in the exact manner required by the I2C specifications:

“void I2Cwrite(char addr, char subaddr, char value)”


void I2Cwrite(char addr, char subaddr, char value)
{
   i2c_start();
   send_i2c_byte(addr);
   send_i2c_byte(subaddr);
   send_i2c_byte(value);
   reset_i2c_bus();
}

In the same way, a random read must be sent in the following manner:

The random read function starts off exactly the same way as a random write. However, after the 2nd byte, the master must initiate a RESTART bit, followed by the device address with a read command. The slave sends and ACK, followed by the data requested by the master. The master then sends a /ACK, before issuing a stop bit. I wrote the following function to emulate the I2C requirements:

“char I2Cread(char addr, char subaddr)


char I2Cread(char addr, char subaddr)
{
   char temp;

   i2c_start();
   send_i2c_byte(addr);
   send_i2c_byte(subaddr);
   DelayuSec(10);

   i2c_restart();
   send_i2c_byte(addr | 0x01);
   temp = i2c_read();

   reset_i2c_bus();
   return temp;
}

The function returns the data received.

Polling

Polling is an important function during I2C operations. I usually use a poll function before any read or writes because I want to make sure that the device I think is on my I2C bus is actually on my I2C bus. I eliminates communication errors that otherwise might escape the regular functions’ error handling abilities. The premise of the polling function is to wait for the ACK from a slave device. If a slave device sends back an ACK, it means that it is on the I2C bus and functioning properly.

“unsigned char I2Cpoll(char addr)”


unsigned char I2Cpoll(char addr)
{
   unsigned char temp = 0;

   i2c_start();
   temp = send_i2c_byte(addr);
   reset_i2c_bus();

   return temp;
}

There you have it. That’s how I2C works on an PIC24 I2C module. The last section of the I2C tutorial will deal with usage, examples, and advanced functions.

Table of Contents
Previous – I2C – Part 1 – Understanding I2C Basics
Next – I2C – Part 3 – Advanced Functions

27 Responses to “10.2 I2C – Part 2 – Basic Functions”
  1. Ryan Tensmeyer says:

    I borrowed your I2C code for my project and it worked on the very first try. Thank you for saving me some time here.

    Do you happen to have an example for SPI communication? My write process is working great but I cannot get my reads to work.

  2. jliu83 says:

    Hi Ryan, unfortunately my current list of projects does not deal with SPI, I don’t have any code as of right now. The good news is that C30 and C18 comes with a lot of the peripheral code in the “src” folder. I should be in the folder in which you installed C30. The read/write commands are probably already there. If you’re still stuck, leave me a comment and I can take a look at what’s the problem.

    -J

  3. Vincent says:

    HI,

    Im using PIC24FJ64GA002 for my i2c slave.
    Now, i need to use both i2c module since i’m required to have
    a redundancy feature on my design.

    Problem is, i already have a code that works perfectly on I2C2
    module but when i use the code on I2C1 module [Registers configured
    to I2C1] it will not work.

    Did you experience this error before? I’m really stuck on this..

    Thanks.

  4. jliu83 says:

    Yes. Check the FJ64GA002 Silicon Errata. The I2C1 module has a silicon-level bug. I2C2 works just fine, but the work around for the I2C1 module is posted here (I actually posted the thread on MC’s site):

    http://www.microchip.com/forums/tm.aspx?m=271183&mpage=1&key=&#271183

    I think there is a new version of the errata that is out. You might need to verify your silicon level to make sure which workaround is needed.

    -J

  5. Joan says:

    “The bug is listed in the silicon errata section on —>Mircochip’s website”

    Little typo

  6. jliu83 says:

    Wait… where’s the typo?

    -J

  7. Julia says:

    The Silicon Errata Rev B4 no longer lists the Master Mode bus collision and the Slave Mode ACK problems that were listed in SIlicon Errata A3. Does that mean we can safely assume those problems have been corrected? Anyone verified this? There are new problems having to do with 10-bit addressing but thankfully that doesn’t affect me :-)

  8. jliu83 says:

    There is a register (check the datasheet), that lists your silicon revision. If you got your chip from Digikey or any distributors, they might have been bought and manufactured for a while. You don’t really know how long they have been in stock. The only way to know is to check that register.

    -J

  9. Julia says:

    Thanks, J, that’s good to know. My question, however, wasn’t really about knowing the revision level of a particular part, but to make sure that the B4 errata supercedes the A3 errata and is not simply appended to it. In other words, did Microchip fix the A3 problems?

  10. jliu83 says:

    Yes, a subsequent revision should correct all flaws in the errata. However, fixing the old bugs might introduce new bugs. In that sense PCB layout guys are very much like with VLSI (silicon level layout) people: they are both very superstitious. If something works, don’t change it. There are other reason however, for taking a long time before fixing a problem. If there is a workaround for the problem, and the part is, in general, very usable, then manufacturers are reluctant to fix it. This is because creating the lithography mask for the silicon die is very expensive. Usually, as a manufacturer, you want the product to be released for a decent amount of time for all the silicon level bugs to be found, and then do a spin that corrects all the bugs in one retooling of the masks.

    -J

  11. Peter says:

    I believe your “char i2c_read(void)” read routine has an error in it. You still have to do an acknowledge sequence by sending out a NACK. The ACKDT bit in I2CICON should be set to ’1′ and then ACKEN should be set to initiate the nack event at the end of the read.

  12. jliu83 says:

    True.. you can do that, but since it’s being pulled up with the pull-up resistor, it’ll be and NACK anyways. The result is a logic high on the SDA line whether you put that line or not.

    -J

  13. Peter says:

    But, you are not generating the 9th clock pulse necessary to recognize the NACK if you do it that way.

  14. jliu83 says:

    Nope. Check the scope trace, the 9th pulse is generated regardless of whether you send an ACK or NACK or send anything at all. The module is designed this way because many IC’s will go into an infinite loop waiting for the 9th clock pulse, so the 9th clock is guaranteed.

    -J

  15. vipao says:

    Thanks for your work!
    I’m trying to interface a PIC24HJ256GP206 (as master) with an Analog Device AD5934 (as slave), that is an impdence converter. For this use I use your I2C code with Microchip C30 Compiler, making some arrangements in writing function due to my particular slave device. The problem is that… nothing works. It’s the first time I use I2C and I expect to see on SCL the clock but if I put a scopemeter on SCL or SDA pins i find a 3.3Vdc constant voltage and nothing more (system Vcc is 3.3Vdc and PIC crystal clock is 10MHz with internal PLL). I start using a 4.7KOhm pull-up resistror, but now I’m trying a 1KOhm without any change. Possible? Someone has an idea of what is wrong? Thanks!

  16. jliu83 says:

    Well your pull up resistors are not your problem. Anything in the 7 to 2 KOhm range will work. First of all, I assume that when you say “scopemeter” you mean the oscilloscope. I am also assuming that you know how to use an oscilloscope properly. By no means do I mean the previous sentence in an offensive manner. It’s just that you are trying to catch digital signal that occurs within several microseconds, so proper trigger settings and time division is absolutely critical in order to see something on the scope.

    You need to first make sure that the microcontroller is working correctly. You can test this by putting in a delay function and turning the IO’s up and down, OR you can set the OSCO pin up so that it outputs Fcy (you should see an oscillator output on OSCO). These tests ensure that your microcontroller circuit is working. Next you need to test your I2C bus. The easiest way to see if your I2C bus is configured properly is to send a start command followed by a delay, followed by a stop. You should see the SDA line go to a logic low. That said, I prefer to trigger on the SDA line rather than the SCL line. This is because when a start bit is initiated, the SDA line gets lowered first, followed by the clock. If you see a start and stop bit, then you should be able to debug the rest.

    -J

  17. Vishal says:

    Hi,
    I am programming for I2C communication on PIC24FJ192GA010. I have written code for I2C, but when I try to write control byte into I2C2TRN buffer, the ACKSTAT bit in I2C2STAT register gets set (indicating NACK). Does this indicate that the slave is not sending acknowledgment to MASTER (MASTER not able to communicate with SLAVE?)?

    Thanks,
    Vishal

  18. Vishal says:

    Ok, I found that I had to change my buad rate setting value for the problem.

    Thanks,
    Vishal

  19. jliu83 says:

    NACK means a logic high, so no slave devices is pulling SDA to logic low (GND). Either you are sending the byte incorrectly, or your slave devices are not functioning correctly.

    -J

  20. verminsky says:

    Thanks for a great site and tutorial. I have spent many days attempting to get I2C1 working as a Master on a PIC24 HJ 64GP202 series chip without success. No matter what I do, or what value pu resistor I choose, I cannot get an ACK from my slave when I write to the slave device from the PIC I can see on my digital scope that the SCL clock bursts are being generated at the correct pulse width (10us), and the SDA data is correct, but my slave never pulls down the SDA line with an ACK.

    I have a little board that generates I2C data and is controlled by a PC over USB. It has a simple GUI that allows one to send and receive I2C from the PC’s keyboard. When I substitute this card’s SCL and SDA lines for the ones from the PIC, going to the same slave device on the same breadboard and send the same data to the slave, the waveforms look the same, except fnow the slave’s ACK pulses are there, and indeed the slave now executes the proper command that I have sent to it.

    Any ideas would be greatly appreciated. Thank you.

  21. jliu83 says:

    Hi,
    My instinct tells me that your slave/PIC is probably not powered/connected correctly. If you are able to talk to it using the PC, then the resistors are probably correct. Also check the I2CxSTAT to see if there is a collision. That might give you some clues. Out of curiosity, what is the slave device?

    -J

  22. verminsky says:

    Thanks for getting back to me…didn’t see your reply untiil just now. The slave device is a CIrrus Logic 8 channel audio volume control CS3318…. I got busy and haven’t had much time to continue working on this code – also I was so exasperated from continual failure over many days that I needed a break.

    I am just getting back to this. I will report back when/if I get any further. Thanks

  23. Bill says:

    Hey, I had a question about the code posted. It makes use of the DelayuSec() function, but where is this function found?

  24. jliu83 says:

    Look up the Timer section.

    -J

  25. Britt Foiles says:

    By far the most concise and up to date information I found on this topic. Sure glad that I navigated to your page by accident. I’ll be subscribing to your feed so that I can get the latest updates. Appreciate all the information here

  26. MT says:

    JLiu,

    I’m using I2C to interface with an EEPROM chip and when I have the PLL enabled on the microcontroller PIC24FJ64GA104, the I2C does not work. I have verified this by keeping the I2C clock at 100 KHz with and without the PLL option. The PLL option does not work. Any ideas why this would be ?

    Thanks for the wonderful tutorial!
    MT

  27. jliu83 says:

    On the PIC24 devices, there is a register that controls the peripheral clock. There are limits to how fast this clock can be, and with the PLL enabled, the speed might be too fast. Check the OSCILLATOR section of the datasheets and try to find this register (aptly named something like, “Peripheral clock divide”). See if this works. Also check your OSCO pin, and see if you are actually getting a clock out (which means that your PLL is actually locked and your processor is correctly clocked). Always perform this check because there are jitter and timing specs for the PLL, so you might think the PLL is locked when in fact it might not be.

    -J

  28.  
Leave a Reply