8
\$\begingroup\$

For a project I'd like three PICs (two slaves PIC18F4620, one master PIC18F46K22) to communicate over the I2C bus. Later on, more slaves may be added (like EEPROM, SRAM, ...). I'm writing the code for these PICs in C using the C18 compiler. I've looked around on the internet a lot, but couldn't find libraries to handle the (M)SSP peripheral. I've read the datasheet of both PICs on the (M)SSP peripheral in I2C mode but couldn't find out how to interface the bus.

So I need master and slave libraries.

What do you recommend? Do you have such a library somewhere? Is it built-in in the compiler and if yes, where? Is there a good tutorial somewhere on the net?

\$\endgroup\$
5
  • 2
    \$\begingroup\$ I had similar problems few months ago. You can read about them here. Here are libraries for C18 which work with I^2C, but there's one big thing missing: You need to set bus speed manually by writing to appropriate register and that isn't mentioned anywhere in the documentation of the library. \$\endgroup\$
    – AndrejaKo
    Commented Jan 20, 2013 at 17:19
  • \$\begingroup\$ Thanks, that was helpful! It however only did the master part, not the slave part. \$\endgroup\$
    – user17592
    Commented Jan 20, 2013 at 17:34
  • \$\begingroup\$ Yeah, I didn't need to work with slave back then, so no slave examples. Sorry. \$\endgroup\$
    – AndrejaKo
    Commented Jan 20, 2013 at 17:36
  • 2
    \$\begingroup\$ No that's fine, it was useful for the master part! :-) \$\endgroup\$
    – user17592
    Commented Jan 20, 2013 at 17:51
  • \$\begingroup\$ please also disable analog on the ports ANSELC=0; \$\endgroup\$
    – user54853
    Commented Oct 9, 2014 at 9:25

3 Answers 3

23
\$\begingroup\$

Microchip wrote application notes about this:

  • AN734 on implementing an I2C slave
  • AN735 on implementing an I2C master
  • There's also a more theoretical AN736 on setting up a network protocol for environmental monitoring, but it isn't needed for this project.

The application notes are working with ASM but that can be ported to C easily.

Microchip's free C18 and XC8 compilers have I2C functions. You can read more about them in the Compiler libraries documentation, section 2.4. Here's some quick start info:

Setting up

You already have Microchip's C18 or XC8 compiler. They both have built-in I2C functions. To use them, you need to include i2c.h:

#include i2c.h

If you want to have a look at the source code, you can find it here:

  • C18 header: installation_path/vx.xx/h/i2c.h
  • C18 source: installation_path/vx.xx/src/pmc_common/i2c/
  • XC8 header: installation_path/vx.xx/include/plib/i2c.h
  • XC8 source: installation_path/vx.xx/sources/pic18/plib/i2c/

In the documentation, you can find in which file in the /i2c/ folder a function is located.

Opening the connection

If you're familiar with Microchip's MSSP modules, you'll know they first have to be initialized. You can open an I2C connection on a MSSP port using the OpenI2C function. This is how it's defined:

void OpenI2C (unsigned char sync_mode, unsigned char slew);

With sync_mode, you can select if the device is master or slave,and, if it's a slave, whether it should use a 10-bit or 7-bit address. Most of the time, 7-bits is used, especially in small applications. The options for sync_mode are:

  • SLAVE_7 - Slave mode, 7-bit address
  • SLAVE_10 - Slave mode, 10-bit address
  • MASTER - Master mode

With slew, you can select if the device should use slew rate. More about what it is here: What is slew rate for I2C?

Two MSSP modules

There's something special about devices with two MSSP modules, like the PIC18F46K22. They have two sets of functions, one for module 1 and one for module 2. For example, instead of OpenI2C(), they have OpenI2C1() and openI2C2().

Okay, so you've set it all up and opened the connection. Now let's do some examples:

Examples

Master write example

If you're familiar with the I2C protocol, you'll know a typical master write sequence looks like this:

Master : START | ADDR+W |     | DATA |     | DATA |     | ... | DATA |     | STOP
Slave  :       |        | ACK |      | ACK |      | ACK | ... |      | ACK |

At first, we send a START condition. Consider this picking up the phone. Then, the address with a Write bit - dialing the number. At this point, the slave with the sent address knows he's being called. He sends an Acknowledgement ("Hello"). Now, the master device can go send data - he starts talking. He sends any amount of bytes. After each byte, the slave should ACK the received data ("yes, I hear you"). When the master device has finished talking, he hangs up with the STOP condition.

In C, the master write sequence would look like this for the master:

IdleI2C();                         // Wait until the bus is idle
StartI2C();                        // Send START condition
IdleI2C();                         // Wait for the end of the START condition
WriteI2C( slave_address & 0xfe );  // Send address with R/W cleared for write
IdleI2C();                         // Wait for ACK
WriteI2C( data[0] );               // Write first byte of data
IdleI2C();                         // Wait for ACK
// ...
WriteI2C( data[n] );               // Write nth byte of data
IdleI2C();                         // Wait for ACK
StopI2C();                         // Hang up, send STOP condition

Master read example

The master read sequence is slightly different from the write sequence:

Master : START | ADDR+R |     |      | ACK |      | ACK | ... |      | NACK | STOP
Slave  :       |        | ACK | DATA |     | DATA |     | ... | DATA |      |

Again, the master initiates the call and dials the number. However, he now wants to get information. The slave first answers the call, then starts talking (sending data). The master acknowledges every byte until he has enough information. Then he sends a Not-ACK and hangs up with a STOP condition.

In C, this would look like this for the master part:

IdleI2C();                         // Wait until the bus is idle
StartI2C();                        // Send START condition
IdleI2C();                         // Wait for the end of the START condition
WriteI2C( slave_address | 0x01 );  // Send address with R/W set for read
IdleI2C();                         // Wait for ACK
data[0] = ReadI2C();               // Read first byte of data
AckI2C();                          // Send ACK
// ...
data[n] = ReadI2C();               // Read nth byte of data
NotAckI2C();                       // Send NACK
StopI2C();                         // Hang up, send STOP condition

Slave code

For the slave, it's best to use an Interrupt Service Routine or ISR. You can setup your microcontroller to receive an interrupt when your address is called. That way you don't have to check the bus constantly.

First, let's set up the basics for the interrupts. You'll have to enable interrupts, and add an ISR. It's important that PIC18s have two levels of interrupts: high and low. We're going to set I2C as a high priority interrupt, because it's very important to reply to an I2C call. What we're going to do is the following:

  • Write an SSP ISR, for when the interrupt is an SSP interrupt (and not another interrupt)
  • Write a general high-priority ISR, for when the interrupt is high priority. This function has to check what kind of interrupt was fired, and call the right sub-ISR (for example, the SSP ISR)
  • Add a GOTO instruction to the general ISR on the high priority interrupt vector. We can't put the general ISR directly on the vector because it's too large in many cases.

Here's a code example:

// Function prototypes for the high priority ISRs
void highPriorityISR(void);

// Function prototype for the SSP ISR
void SSPISR(void);

// This is the code for at the high priority vector
#pragma code high_vector=0x08
void interrupt_at_high_vector(void) { _asm GOTO highPriorityISR _endasm }
#pragma code

// The actual high priority ISR
#pragma interrupt highPriorityISR
void highPriorityISR() {
    if (PIR1bits.SSPIF) {        // Check for SSP interrupt
        SSPISR();            // It is an SSP interrupt, call the SSP ISR
        PIR1bits.SSPIF = 0;  // Clear the interrupt flag
    }
    return;
}

// This is the actual SSP ISR
void SSPISR(void) {
    // We'll add code later on
}

Next thing to do is to enable the high priority interrupt when the chip initializes. This can be done by some simple register manipulations:

RCONbits.IPEN = 1;          // Enable interrupt priorities
INTCON &= 0x3f;             // Globally enable interrupts
PIE1bits.SSPIE = 1;         // Enable SSP interrupt
IPR1bits.SSPIP = 1;         // Set SSP interrupt priority to high

Now, we have interrupts working. If you're implementing this, I'd check it now. Write a basic SSPISR() to start blinking an LED when an SSP interrupt occurs.

Okay, so you got your interrupts working. Now let's write some real code for the SSPISR() function. But first some theory. We distinguish five different I2C interrupt types:

  1. Master writes, last byte was address
  2. Master writes, last byte was data
  3. Master reads, last byte was address
  4. Master reads, last byte was data
  5. NACK: end of transmission

You can check at what state you are by checking the bits in the SSPSTAT register. This register is as follows in I2C mode (unused or irrelevant bits are omitted):

  • Bit 5: D/NOT A: Data/Not address: set if the last byte was data, cleared if the last byte was an address
  • Bit 4: P: Stop bit: set if a STOP condition occurred last (there's no active operation)
  • Bit 3: S: Start bit: set if a START condition occurred last (there's an active operation)
  • Bit 2: R/NOT W: Read/Not write: set if the operation is a Master Read, cleared if the operation is a Master Write
  • Bit 0: BF: Buffer Full: set if there's data in the SSPBUFF register, cleared if not

With this data, it's easy to see how to see what state the I2C module is in:

State | Operation | Last byte | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 0
------+-----------+-----------+-------+-------+-------+-------+-------
1     | M write   | address   |   0   |   0   |   1   |   0   |   1
2     | M write   | data      |   1   |   0   |   1   |   0   |   1
3     | M read    | address   |   0   |   0   |   1   |   1   |   0
4     | M read    | data      |   1   |   0   |   1   |   1   |   0
5     | none      | -         |   ?   |   ?   |   ?   |   ?   |   ?

In software, it's best to use state 5 as the default, which is assumed when the requirements for the other states aren't met. That way, you don't reply when you don't know what's going on, because the slave doesn't respond to a NACK.

Anyway, let's have a look at the code:

void SSPISR(void) {
    unsigned char temp, data;

    temp = SSPSTAT & 0x2d;
    if ((temp ^ 0x09) == 0x00) {            // 1: write operation, last byte was address
        data = ReadI2C();
        // Do something with data, or just return
    } else if ((temp ^ 0x29) == 0x00) {     // 2: write operation, last byte was data
        data = ReadI2C();
        // Do something with data, or just return
    } else if ((temp ^ 0x0c) == 0x00) {     // 3: read operation, last byte was address
        // Do something, then write something to I2C
        WriteI2C(0x00);
    } else if ((temp ^ 0x2c) == 0x00) {     // 4: read operation, last byte was data
        // Do something, then write something to I2C
        WriteI2C(0x00);
    } else {                                // 5: slave logic reset by NACK from master
        // Don't do anything, clear a buffer, reset, whatever
    }
}

You can see how you can check the SSPSTAT register (first ANDed with 0x2d so that we only have the useful bits) using bitmasks in order to see what interrupt type we have.

It's your job to find out what you have to send or do when you respond to an interrupt: it depends on your application.

References

Again, I'd like to mention the application notes Microchip wrote about I2C:

  • AN734 on implementing an I2C slave
  • AN735 on implementing an I2C master
  • AN736 on setting up a network protocol for environmental monitoring

There's documentation for the compiler libraries: Compiler libraries documentation

When setting up something yourself, check the datasheet of your chip on the (M)SSP section for I2C communication. I used the PIC18F46K22 for the master part and the PIC18F4620 for the slave part.

\$\endgroup\$
3
\$\begingroup\$

Firstly, I'd recommend changing to the XC8 compiler simply because it's the latest. There are peripheral libraries available, but I've never used them much. Check on Microchips website for details and the documentation.

Okay, I have some very old basic routines here for I2C eeprom comms I used a long time ago with a PIC16F and the old Microhip mid-range compiler (it may have been the Hi-Tech one), but I think they may work okay with the PIC18, since I think the peripheral is the same. You will find out very quickly anyway if it's all different.
They were part of a larger file which was used with a temperature logger project, so I quickly ripped out all the other unrelated functions and saved as the below files, so it's possible I have made a bit of a mess of it, but hopefully you will be able to get some idea of what is needed (it may even just work, you never know ;-) )

You will need to make sure the main header file is correct, and check/alter the pins to make sure they are the correct I2C peripheral pins, and the register names if they are from the Hi-Tech compiler, which did things a bit differently regarding the register naming convention.

I2C.c file:

#include "I2C.h"
#include "delay.h"
#include <pic.h>

#define _XTAL_FREQ 20000000


void write_ext_eeprom(unsigned int address, unsigned char data)
 {
    unsigned char a0 = ((address & 0x8000) >> 14);  
    unsigned char msb = (address >> 8);
    unsigned char lsb = (address & 0x00FF);


   i2c_start();
   i2c_write(0xa0 | a0);
   i2c_write(msb);
   i2c_write(lsb);
   i2c_write(data);
   i2c_stop();
   DelayMs(11);
}

/******************************************************************************************/

unsigned char read_ext_eeprom(unsigned int address)
{
   unsigned char a0 = ((address & 0x8000) >> 14);  
   unsigned char data;
   unsigned char msb = (address >> 8);
   unsigned char lsb = (address & 0x00FF);

   i2c_start();
   i2c_write(0xa0 | a0);
   i2c_write(msb);
   i2c_write(lsb);
   i2c_repStart();
   i2c_write(0xa1 | a0);
   data=i2c_read(0);
   i2c_stop();
   return(data);
}

void i2c_init()
{
 TRISC3=1;           // set SCL and SDA pins as inputs
 TRISC4=1;

 SSPCON = 0x38;      // set I2C master mode
 SSPCON2 = 0x00;

 //SSPADD = 9;          // 500kHz bus with 20MHz xtal 
 SSPADD = 49;           // 100kHz bus with 20Mhz xtal

 CKE=0;     // use I2C levels      worked also with '0'
 SMP=1;     // disable slew rate control  worked also with '0'

 PSPIF=0;      // clear SSPIF interrupt flag
 BCLIF=0;      // clear bus collision flag
}

/******************************************************************************************/

void i2c_waitForIdle()
{
 while (( SSPCON2 & 0x1F ) | RW ) {}; // wait for idle and not writing
}

/******************************************************************************************/

void i2c_start()
{
 i2c_waitForIdle();
 SEN=1;
}

/******************************************************************************************/

void i2c_repStart()
{
 i2c_waitForIdle();
 RSEN=1;
}

/******************************************************************************************/

void i2c_stop()
{
 i2c_waitForIdle();
 PEN=1;
}

/******************************************************************************************/

int i2c_read( unsigned char ack )
{
 unsigned char i2cReadData;

 i2c_waitForIdle();

 RCEN=1;

 i2c_waitForIdle();

 i2cReadData = SSPBUF;

 i2c_waitForIdle();

 if ( ack )
  {
  ACKDT=0;
  }
 else
  {
  ACKDT=1;
  }
  ACKEN=1;               // send acknowledge sequence

 return( i2cReadData );
}

/******************************************************************************************/

unsigned char i2c_write( unsigned char i2cWriteData )
{
 i2c_waitForIdle();
 SSPBUF = i2cWriteData;
//if(ACKSTAT)
{
//while(ACKSTAT);
}
 return ( ! ACKSTAT  ); // function returns '1' if transmission is acknowledged
}

I2C.h Header file:

extern void i2c_init();
extern void i2c_waitForIdle();
extern void i2c_start();
extern void i2c_repStart();
extern void i2c_stop();
extern int i2c_read( unsigned char ack );
extern unsigned char i2c_write( unsigned char i2cWriteData );
\$\endgroup\$
2
  • \$\begingroup\$ Thanks, that is the master part and indeed probably the same as PIC18. Also thanks for the compiler note :-) Asking around a bit gave me some application notes, so I'll add them as an answer myself. \$\endgroup\$
    – user17592
    Commented Jan 20, 2013 at 15:45
  • \$\begingroup\$ You should consider adding a section on setting up the baud rate generator. The code typically looks like SSP1ADD = ((_XTAL_FREQ/100000)/4)-1; for 1KHz, etc. \$\endgroup\$ Commented Jan 15, 2014 at 21:33
1
\$\begingroup\$

XC8 and XC16 compilers include libraries for I2C.

The problem I encountered is that the documentation is not very good! If you use the examples from Microchip documentation, you are out of luck. Even Microchip support can't help you. I have been there myself.

Some time ago I worked with PIC24EP512GP series microcontroller and the library did not work for me as documented by Microchip.

\$\endgroup\$
3
  • \$\begingroup\$ Ah, that's a shame! So what did you do? \$\endgroup\$
    – user17592
    Commented Jan 22, 2013 at 6:36
  • \$\begingroup\$ Improvised my own unfortunately. \$\endgroup\$ Commented Jan 22, 2013 at 6:41
  • 1
    \$\begingroup\$ Are they useful for others as well? I'd like to see them! \$\endgroup\$
    – user17592
    Commented Jan 22, 2013 at 7:00

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.