Microcontroller SPI-I2C Bus Communication

Microcontroller SPI-I2C Bus Communication

Hello everyone, I am Daji Zong, and today I will talk about two commonly used communication buses on microcontrollers: SPI and I2C. As the names suggest, SPI stands for Serial Peripheral Interface, while I2C stands for Inter-Integrated Circuit Bus. Both are common methods for chip-to-chip communication.

Introduction to SPI

The SPI bus consists of four lines: Master Out Slave In (MOSI), Master In Slave Out (MISO), Serial Clock (SCLK), and Chip Select (CS). Its working principle is straightforward: the master sends a clock signal via SCLK, and sends and receives data through MOSI and MISO respectively. The CS line is used to select the slave that needs to communicate.

I often liken SPI to an intercom system in a community: the master is like the security guard, and the slave is the community owner. When the guard needs to speak to a specific owner, he first “calls” the owner’s apartment number (i.e., pulls CS low to select that slave), and then speaks into the intercom (sending data via MOSI). If a response is needed from the owner, the guard switches to receive mode (reads data from MISO). The SCLK acts like a fixed rhythm for “calling” or “listening” intervals.

Introduction to I2C

Compared to SPI, I2C operates in a slightly more complex manner. It only requires two lines: Serial Data Line (SDA) and Serial Clock Line (SCL). All devices are connected in parallel on these two lines, and data transmission is achieved by setting unique device addresses.

We can imagine I2C as a “data highway”: SDA is the lane that actually carries the data, while SCL is the fixed “green wave” rhythm. When the master needs to transmit data, it first “honks” on the SDA line (start signal) to prepare all devices for communication. Then it “calls out” the target slave address, as if “calling a number”. Only the slave with the corresponding number will “respond”. Next, the master can “drive through” and start transmitting data. When the communication ends, the master must also “honk” again on the SDA line (stop signal).

What are the advantages and disadvantages of both?

SPI is relatively simple, requiring only four lines to operate, and the master has complete control over the slave, allowing for real-time transmission speeds that are fast. However, if there are too many slaves, a separate CS line must be drawn for each slave, complicating the wiring.

I2C requires only two lines, making it easy to connect multiple devices in parallel, saving IO resources. However, its real-time performance and transmission speed cannot match that of SPI, and control is more distributed.

In practical applications, I2C is often used for reading and writing communication with simple peripherals like sensors. For large data transmissions, such as information exchange with camera modules or storage devices, the faster SPI is preferred. However, the specific solution must be combined with actual conditions, and both can also be used in combination.

Code Examples

Now that we’ve covered the theory, let’s take a look at some actual code for SPI and I2C communication on a microcontroller.

SPI Example:

//Master sends and receives example
#define SPI_CS     P1.0  //Define the chip select pin for the slave  

void SPI_MasterXfer(uint8 dat) //Function: Send/Receive one byte
{
    uint8 i;
    for(i=0; i<8; i++)  //Loop 8 times to send one byte of data
    {
        SPI_MOSI = (dat & 0x80); //Output the current highest bit to MOSI
        dat <<= 1;
        SPI_CLK=1;        //Pull up the clock
        dat |= SPI_MISO;   //Read MISO into the lowest bit of dat
        SPI_CLK=0;        //Pull down the clock
    }
}

//Master reads from slave example
uint8 Read_XXByte(uint8 addr)
{
    uint8 value;
    SPI_CS = 0;         //Enable chip
    SPI_MasterXfer(addr); //Send register address
    value = SPI_MasterXfer(0); //Receive data
    SPI_CS = 1;         //Disable chip
    return value;       //Return the read value
}

I2C Example:

//Start I2C communication
void I2C_Start(void)
{
    SDA = 1;        //Pull up the data line
    _nop_();        //Establish time delay
    SCL = 1;        //Pull up the clock line
    _nop_();        //Establish time delay
    SDA = 0;        //Pull down the data line
    _nop_();        //Establish time delay
}

//Send one byte
void I2C_SendByte(uint8 dat)
{
    uint8 i;
    for(i=0; i<8; i++)    //Loop 8 times to transmit one byte
    {
        dat <<= 1;     //Left shift data by one
        SDA = CY;       //Send data bit
        SCL = 1;        //Pull up the clock line
        _nop_();        //Establish time delay
        SCL = 0;     //Pull down the clock line
    }
    SDA = 1;        //Release the bus
    SCL = 1;        //Pull up the clock line to wait for acknowledgment
    _nop_();        //Establish time delay
    SCL = 0;        //Pull down the clock line to end acknowledgment  
}

//Read one byte
uint8 I2C_ReadByte(void)
{ 
    uint8 i, value;
    SDA = 1;        //Set data line as input
    for(i=0; i<8; i++) //Loop 8 times to receive data
    {
        value <<= 1; //Shift to make room for the lowest data bit
        SCL = 1;      //Pull up the clock line
        _nop_();     //Establish time delay
        value |= SDA; //Read data bit
        SCL = 0;      //Pull down the clock line 
    }
    return value;     //Return the received data
}

Application Case

Suppose we need to read values from an I2C interface temperature and humidity sensor. The host computer sends a read command via the serial port, and the microcontroller reads the sensor data via the I2C bus and sends it back to the host computer. The complete code is as follows:

#include <reg52.h>;
#define ADDR 0x40 //Sensor device address 

//Start I2C communication
void I2C_Start(void)
{
    ...//same as above
}

//Send one byte 
void I2C_SendByte(uint8 dat)
{
    ...//same as above
}

//Read one byte
uint8 I2C_ReadByte(void) 
{
    ...//same as above
}

//Stop I2C communication
void I2C_Stop(void)
{
    SCL = 0;       
    _nop_();
    SDA = 0;
    _nop_();
    SCL = 1;
    _nop_();
    SDA = 1;
}

//I2C sensor reading program
void I2C_ReadTHSensor(uint8 *humi, uint8 *temp)
{
    I2C_Start();
    I2C_SendByte(ADDR); //Send sensor address
    I2C_SendByte(0x11); //Send command to read humidity register
    *humi = I2C_ReadByte(); //Read humidity data
    I2C_SendByte(ADDR+1);  //Send sensor address + 1
    I2C_SendByte(0x07); //Send command to read temperature register  
    *temp = I2C_ReadByte(); //Read temperature data
    I2C_Stop();
}

//Serial port receive and respond command
void SerialProcess(void)
{
    if(RI == 1) //Check if there is serial data received
    {
        RI = 0; //Clear receive flag
        P0 = SBUF; //Display received data
        if(SBUF == 0x12) //Read temperature and humidity command
        {
            uint8 humi, temp;         
            I2C_ReadTHSensor(&humi, &temp); //Read humidity and temperature values
            //Return temperature and humidity values via serial port to host
            SBUF = humi; 
            while(!TI); //Wait for send completion
            TI = 0;
            SBUF = temp;
            while(!TI); //Wait for send completion  
            TI = 0;
        }
    }
}

void main()
{     
    //Initialize serial port
    TMOD = 0x20;
    TH1 = ...;
    TL1 = ...;
    TR1 = 1;
    SM0 = 0;
    SM1 = 1;
    REN = 1; 
    EA = 1; 
    
    while(1)
    {
        SerialProcess(); //Loop to check serial commands
    }
}

The above is a simplified example of reading temperature and humidity. In practical applications, we may also need to perform data verification, error retransmission, and other processing, which will not be elaborated here. It is important to emphasize that regardless of the communication method used, matching parameters such as timing and baud rate is crucial to avoid communication anomalies.

Tip: When writing microcontroller communication programs, it is easy to lose data or cause communication anomalies due to improper timing handling. I have made such a basic mistake myself; at that time, I did not have a good grasp of the internal working principles of the microcontroller, leading to repeated failures in timing control. Therefore, before writing such programs, it is essential to thoroughly understand the working mechanism of the chip and have a clear understanding of the execution timing of each statement.

In addition, the I2C bus has the risk of bus contention, meaning that if other slaves continuously pull down the SDA/SCL lines, the bus will be occupied indefinitely. I encountered this tricky problem in the past and ultimately solved it using hardware watchdog. Therefore, in I2C applications, it is vital to consider exception protection mechanisms.

I want to reiterate the “slash-and-burn” nature of microcontroller programming. Unlike host computers, microcontrollers cannot easily perform breakpoint debugging. Besides using an oscilloscope for debugging, your best friend is careful “thinking”. Consider the state and timing of the microcontroller when executing each line of code and repeatedly ponder over any difficult issues. Microcontroller development is about “thoroughly understanding” the learning process.

Alright, that concludes today’s sharing. For beginners, SPI and I2C may seem simple, but they actually require understanding and mastering a lot of theoretical knowledge. Keep your passion for learning and diligence, and with perseverance in research, you will surely be able to apply them proficiently. Feel free to communicate and exchange questions anytime!

Leave a Comment