I have an I2C interface with multiple Slaves and a Single master. If master wants to communicate with any of the slave it sends its address and read from the same.
But what should be done if one of the slave wants to communication with the master?
Electronic – Slave wants to send data to Master in I2C
embeddedfirmwaremicrocontroller
Related Solutions
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
/v
x.xx
/h/i2c.h
- C18 source:
installation_path
/v
x.xx
/src/pmc_common/i2c/
- XC8 header:
installation_path
/v
x.xx
/include/plib/i2c.h
- XC8 source:
installation_path
/v
x.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 addressSLAVE_10
- Slave mode, 10-bit addressMASTER
- 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:
- Master writes, last byte was address
- Master writes, last byte was data
- Master reads, last byte was address
- Master reads, last byte was data
- 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.
Before starting, if you want to work with digital communication such as I2C, SPI, I2S, CAN, etc. I strongly recommend you invest money in a logic analyzer. These protocols doesn't require a fast sampling rate, so you can most likely find an affordable device. In any case, it will save you valuable time. I personnaly uses Saleae devices.
Understanding registers
To understand the little piece of code you shared, we will have to calrifies a little how microprocesseors works and how they interracts with hardware. I assume that you are aware of how pointers works in C/C++ ; they points to a memory address. What you probably don't know is that they don't only points to a memory location, but can also point to a hardware register. When you read or write into a variable, you processor will put that address on it address bus and either read or write the content on the data bus.
Like I said, not all addresses refers to a memory location. If you look at the PIC16F887 datasheet, you will find many registers located between addresse 000h-1FFh. You can either read or write their content. Sometime they will be accessible only in for one or the other, that will be specified in the datasheet.
These registers are used for 1) Configuring your peripheral (like an I2C trasceiver) 2) Passing data to the a device (like the data you want to send/receive with I2c)
The pieces of code you gave is reading these resgisters. SSPSTAT (0x94) and SSPCON2 (0x91) are two constants defined in a header file that comes along with your compiler. They basically are pointers, and the code is making bitwise operation to get the value of a specific bit within these register. In other words, your program is waiting as long as SSPSTAT bit 2 or SSPCON2 bits 8 to 0 are set to high. This seems like it won't work because SSPCON2 contains configurations bits, like bit 7 : GCEN. If you set this bit to high, interrupts are enabled for I2C and this bit will not changes.
your problem
As for where the problem is, the wiring seems right. Problems could be :
- Bad configuration of register
- Slave never trasmit (that's where the logic analyzer becomes handy)
- Reception logic is not good. The one you have seems odd to me.
I think for reception, you should be looking at SSPSTAT bit0 (BF). This bit becomes high when your reception buffer is full.
Good luck
Best Answer
Case 1: Slave has an interrupt pin
You need to connect this interrupt pin to master microcontroller. Every time the slave has some data, it should raise an interrupt. At that point, master will read the available data.
Case 2: Slave doesn't have an interrupt pin
Polling is the only option in this case. Master keeps reading all the slaves at regular interval and keeps comparing the received data with old one. If the data has changed, master will take appropriate action. You need to decide the interval according to your application.