Using Microchips C18 compiler with the C18 libraries has been very straightforward when using a PIC18F series microcontroller as an I2C master device. If you wanted to write a single byte of data to a slave device with an address of say 0xB0, it can be implemented in the following way:
OpenI2C( MASTER, SLEW_OFF);
SSPADD = 0x27; //SSPADD Baud Register used to calculate I2C clock speed in MASTER mode (in this case 100Khz)
StartI2C();
IdleI2C();
putcI2C( 0xB0 ); //send address
IdleI2C();
putcI2C( databyte ); //send data
IdleI2C();
StopI2C();
This code is simply placing the target device address onto the I2C bus, the upper seven bits of this byte contain the devices address with the lsb bit indicating whether the op will begin a read(0) or a write(1). The data byte is then sent following the address. A standard I2C diagram will explain this the most clearly including the Ack and NAck data.
While still in master mode, reading data from an I2C device can be easily done as well. The following code can be used to read two bytes of data from an I2C slave device from a PIC18 micro operating in master mode:
StartI2C(); // Start condition
IdleI2C();
WriteI2C( 0xB0 ); // addresses the chip with a read bit
IdleI2C();
inbit = ReadI2C(); // read the value from the RTC and store in result
IdleI2C();
AckI2C();
IdleI2C();
inbit2 = ReadI2C(); // read the value from the RTC and store in result
IdleI2C();
NotAckI2C();
IdleI2C();
StopI2C(); // Stop condition I2C on bus
This is also clarified by looking at I2C timing:
Now things become tricky when you want to use two PIC18F devices where one is a master device while the other is a slave. Utilizing the I2C libraries, you would think that implementing something like this on the Slave device would work:
OpenI2C( SLAVE_7, SLEW_OFF);
SSPADD = 0xB0; //SSPADD contains I2C device address in SLAVE mode
while ( !DataRdyI2C() )
{
addr = ReadI2C();
AckI2C();
IdleI2C();
data = ReadI2C();
NAckI2C();
IdleI2C();
}
What the above is attempting to do is wait until the SSPBUF register contains address data, when it does read the byte, acknowledge, wait until the next byte, read that byte, then NAck the data. Of course any data that appears on the bus will not be accepted by the specific PIC at all, so if SSPBUF does ever contain data it will be destined for this device. Another important note is the SSPBUF register will contain the address that is sent upon first byte received. The ReadI2C() function will clear this buffer so even if we have no need for the data, it still must be read.
Now you can play with this code all you want adjusting timing, Ack and NAck sequencing, delays, etc... but ultimately it will not do what you would like it to do / think it should do. Why? Looking into the C18 I2C libraries themselves you will see that some of the functions will only work in MASTER mode, or put simply they were not designed to be used in SLAVE mode. This is where most of my time was wasted.
The solution? Read the datasheet and application notes, one particular app note in particular AN734 will tell you everything you need to know. I would recommend reading AN734 fully if you really want to understand slave communication on a PIC18F. Instead of attempting to modify the C18 I2C predefined functions I decided to implement my own at the register level. Here are the important registers and bits within the PIC18F25K20 (among others) you need to be aware of:
SSPBUF : Serial TX/RX data buffer.
PIR1bits.SSPIF : Interrupt flag bit. This will be 1 when data is received into SSPBUF
SSPSTATbits.BF : SSPBUF buffer full status bit. 1 = full, 0 = empty.
SSPSTATbits.D_A : Data / Address bit. When receiving it indicates what the data received was. 0 = address, 1 = data.
With the above data you can easily implement I2C slave data reception, so here is how. There are two ways you can handle data received. If you code timing is critical, the best and preferred method will be to implement an ISR and place the code within it. If the timing on your device is not as critical you can implement the code in a separate function or within your main loops themselves. Implementation will be up to you.
If you are looking for an interrupt, your ISR will be ran when
PIR1bits.SSPIF == 1
. Alternately you can look for a few thing to be true while would indicate that a first address byte has been received. Checking for the following will guarantee this case:if ( PIR1bits.SSPIF == 1 && SSPSTATbits.BF == 1 && SSPSTATbits.D_A == 0 )
With this you are checking to see that an interrupt has been received, SSPBUF is indeed full, and SSPBUF contains an address (not data).
From there you will need to immediately clear the interrupt flag.
PIR1bits.SSPIF = 0;
Then read the byte in SSPBUF
addr = SSPBUF;
Note that the addr byte may not need to be read at all so you can skip this read if not necessary, but be sure to then clear the SSPBUF BF bit. If this is not cleared the next byte sent will cause an SSP overflow resulting in a NAck condition. Reading SSPBUF automatically clears the BF bit.
SSPSTATbits.BF = 0;
Now that the data has been read and/or the BF bit has been cleared you can prepare to receive the data. Depending on the speed of your I2C bus, the speed of your microntrollers, etc the data byte may have already arrived. Before reading SSPBUF blindly as we don't exactly know what is there, we can perform the following check:
if ( PIR1bits.SSPIF == 0 && SSPSTATbits.BF == 0 && SSPSTATbits.D_A == 1 )
This checks to see if an interrupt has been received, SSPBUF is indeed full again, and SSPBUF contains data (not an address). If all checks out we can then read the data byte:
data = SSPBUF;
Now that we have our data we have to do some further housekeeping:
PIR1bits.SSPIF = 0; //clear interrupt
SSPSTATbits.D_A = 0; //set D_A bit to 0 so that we can check if the subsequent byte is more data.
If you wish to receive more than a single byte of data you can then loop through the checks again waiting for each additional byte of data. If you continue to have trouble receiving data from the master, try adding a delay between the first address byte and data byte to be sent on the master. If you are still seeing issues, slow your I2C bus speeds down so you can more clearly see what is going on. A logic analyzer can also instantly show you what is going on with your data. Remember to also watch timing. I was using a 100Khz SCL in the above examples which is slow enough to keep things under control. If you are using a 400Khz SCL (or faster) be sure to enable clock stretching to keep things manageable. Without clock stretching, your ISR may not have enough time to read SSPBUF before the master sends another byte of data.