Difference between revisions of "Embedded System I2C Tutorial"

From Embedded Systems Learning Academy
Jump to: navigation, search
(Assignment)
(Assignment)
Line 117: Line 117:
 
#*  The functions you add to this base class are accessible by the I2C2 instance.
 
#*  The functions you add to this base class are accessible by the I2C2 instance.
 
#  Add <B><CODE>initSlave()</CODE></B> method to the I2C to initialize the slave operation.
 
#  Add <B><CODE>initSlave()</CODE></B> method to the I2C to initialize the slave operation.
#*  You decide what parameters the user can supply (ie: slave address).
+
#*  Allow the user to supply a memory to be read or written by another master.
#*  Hint: Maybe you can use a callback function that is called upon each byte read/written byI2C.
 
#*  Hint: Or you can have the user supply memory pointer which is read/written.
 
 
#  Extend the state machine for I2C slave operation.
 
#  Extend the state machine for I2C slave operation.
 
#*  Study the CPU user manual first, and create a state machine diagram on paper.
 
#*  Study the CPU user manual first, and create a state machine diagram on paper.
#*  I recommend designing a "slave write" first since that should be easier.
+
#*  The first register supplied after the slave address should be used as an "offset" of the memory to read or write.
#*  Hint: You may not have the privilege to print data or hold the CPU inside ISR for too long because I2C may "timeout".
 
#*:  Maybe you can save your data at a buffer, and print it all once the STOP has been transmitted.
 
 
#  Demonstrate the following :
 
#  Demonstrate the following :
#*  For full credit, allow your slave to be written to.
+
#*  Demonstrate that you are able to read and write the slave memory.
#*:  You can print the data out, or maybe light up LEDs based on what the master sends.
+
#*  For extra credit and bragging rights, create state machine diagrams, and if you can make better ones, I will use your diagrams at this wikipedia page :)
#*  For extra credit, allow your slave to be read.
 
#*  For super extra credit and bragging rights, create state machine diagrams, and if you can make better ones, I will use your diagrams at this wikipedia page :)
 
  
 +
=== Sample Code ===
 +
<syntaxhighlight lang="cpp">
 +
#include "i2c2.hpp"
 +
#include <stdint.h>
 +
#include <stdio.h>
 +
 +
int main(void)
 +
{
 +
    I2C2& i2c = I2C2::getInstance(); // Get I2C driver instance
 +
    const uint8_t slaveAddr = 0xC0;  // Pick any address
 +
    uint8_t buffer[256] = { 0 };    // Our slave read/write buffer
 +
 +
    /* high_level_init() will init() I2C, let's init slave */
 +
    i2c.initSlave(&buffer, sizeof(buffer));
 +
 +
    /* I2C interrupt will (should) modify our buffer.
 +
    * So just monitor our buffer and either print it out
 +
    * or light up LEDs based on our buffer
 +
    */
 +
    while(1)
 +
    {
 +
        printf("buffer[0] = %0#x\n", buffer[0]);
 +
    }
 +
 +
    return 0;
 +
}
 +
</syntaxhighlight>
 
=== Warning ===
 
=== Warning ===
 
Since the I2C state machine function is called from inside an interrupt, you may not be able to to use <B><CODE>printf()</CODE></B>, especially if you are running FreeRTOS.  As an alternative, use the debug printf methods from the <B><CODE>printf_lib.h</CODE></B> file.
 
Since the I2C state machine function is called from inside an interrupt, you may not be able to to use <B><CODE>printf()</CODE></B>, especially if you are running FreeRTOS.  As an alternative, use the debug printf methods from the <B><CODE>printf_lib.h</CODE></B> file.

Revision as of 22:15, 10 March 2014

Theory of Operation

I2C is prounced "eye-squared see". It is also known as "TWI" because of the intial patent issues of this BUS. This is a popular, low throughput (100-1000Khz), half-duplix BUS that only uses two wires regardless of how many devices are on this BUS. Many sensors use this BUS because of its ease of adding to a system.

Open-Collector BUS

I2C is an open-collector BUS, which means that no device shall have the capability of internally connecting either SDA or SCL wires to power source. The communication wires are instead connected to the power source through a "pull-up" resistor. When a device wants to communicate, it simply lets go of the wire for it to go back to logical "high" or "1" or it can connect it to ground to indicate logical "0".

Pull-up resistor

Using a smaller pull-up can acheiver higher speeds, but then each device must have the capability of sinking that much more current. For example, with a 5v BUS, and 1K pull-up, each device must be able to sink 5mA.

I2C Circuit Simulation


Protocol Information

I2C was designed to be able to read and write memory on a slave device. The protocol may be complicated, but a typical "transaction" involving read or write of a register on a slave device is simple granted a "sunny-day scenario" in which no errors occur.

The code given below illustrates I2C transaction split into functions, but this is the wrong way of writing an I2C driver. An I2C driver should be "transaction-based" and the entire transfer should be carried out using a state machine. The idea is to design your software to walk the I2C hardware through its state to complete an I2C transfer.

In the diagrams given below, your software should take the step given in the arrow, and the hardware will go to the next state granted that no errors occur. To implement this in your software, you should follow the following steps :

  1. Perform the action given by the arrow
  2. Clear the "SI" (state change) bit for HW to take the next step
  3. Wait for "SI" (state change) bit to set, then take the next action

The master will always initiate the transfer, and the device reading the data should always "ACK" the byte. For example, the master sends the 8-bit address after the START condition and the addressed slave should ACK the 9th bit (pull the line LOW). Likewise, when the master sends the first byte after the address, the slave should ACK that byte if it wishes to continue the transfer.

When the master enters the "read mode" after transmitting the read address after a repeat-start, the master begins to "ACK" each byte that the slave sends. When the master "NACKs", it is an indication to the slave that it doesn't want to read anymore bytes from the slave.


Write Transaction

Code Sample State Machine

A typical I2C write is to be able to write a register or memory address on a slave device. Here are the steps:

  1. Master sends START condition followed by device address.
    Device should then "ACK" using 9th bit.
  2. Master sends device's "memory address" (1 or more bytes).
    Each byte should be ACK'd by slave.
  3. Master sends the data to write (1 or more bytes).
    Each byte should be ACK'd by slave.
  4. Master sends the STOP condition.

To maximize throughput and avoid having to do this for each memory location, the memory address is considered "starting address". If we continue to write data, we will end up writing data to M, M+1, M+2 etc.

The ideal way of writing an I2C driver is one that is able to carry out an entire transaction given by the function below. Note that the function only shows the different actions hardware should take to carry out the transaction, but your software should be state machine based as illustrated on the state machine diagram on the right.


void i2c_write_slave_reg(void)
{
    i2c_start();
    i2c_write(slave_addr);
    i2c_write(slave_reg);
    i2c_write(data);
    
    /* Optionaly write more data to slave_reg+1, slave_reg+2 etc. */
    // i2c_write(data); /* M + 1 */
    // i2c_write(data); /* M + 2 */

    i2c_stop();
}
I2C Write Transaction


Read Transaction

Code Sample State Machine

An I2C read is slightly more complex and involves more protocol to follow. What we have to do is switch from "write-mode" to "read-mode" by sending a repeat start, but this time with an ODD address. To simplify things, you can consider an I2C even address being "write-mode" and I2C odd address being "read-mode".

Again, the function shows what we want to accomplish. The actual driver should use state machine logic to carry-out the entire transaction.


void i2c_write_slave_reg(void)
{
    i2c_start();
    i2c_write(slave_addr);
    i2c_write(slave_reg);
   
    i2c_start();                  // Repeat start
    i2c_write(slave_addr | 0x01); // Odd address
    
    char data = i2c_read(0);      // NACK if reading last byte

    /* If we wanted to read 3 register, it would look like this:
     * char d1 = i2c_read(1);
     * char d2 = i2c_read(1);
     * char d3 = i2c_read(0);
     */

    i2c_stop();
}
I2C Read Transaction

Assignment

Extend the I2C base class to also support slave operation. Test your I2C driver by using one board as a master, and another board as a slave.

  1. Study i2c_base.cpp, particularly the following methods:
    • init()
    • i2cStateMachine()
      Note that this function is called by the hardware interrupt asynchronously whenever I2C state changes.
      The other I2C master will simply "kick off" the START state, and this function carries the hardware through its states to carry out the transaction.
    • The functions you add to this base class are accessible by the I2C2 instance.
  2. Add initSlave() method to the I2C to initialize the slave operation.
    • Allow the user to supply a memory to be read or written by another master.
  3. Extend the state machine for I2C slave operation.
    • Study the CPU user manual first, and create a state machine diagram on paper.
    • The first register supplied after the slave address should be used as an "offset" of the memory to read or write.
  4. Demonstrate the following :
    • Demonstrate that you are able to read and write the slave memory.
    • For extra credit and bragging rights, create state machine diagrams, and if you can make better ones, I will use your diagrams at this wikipedia page :)

Sample Code

#include "i2c2.hpp"
#include <stdint.h>
#include <stdio.h>

int main(void)
{
    I2C2& i2c = I2C2::getInstance(); // Get I2C driver instance
    const uint8_t slaveAddr = 0xC0;  // Pick any address
    uint8_t buffer[256] = { 0 };     // Our slave read/write buffer

    /* high_level_init() will init() I2C, let's init slave */
    i2c.initSlave(&buffer, sizeof(buffer));

    /* I2C interrupt will (should) modify our buffer.
     * So just monitor our buffer and either print it out
     * or light up LEDs based on our buffer
     */
    while(1)
    {
        printf("buffer[0] = %0#x\n", buffer[0]);
    }

    return 0;
}

Warning

Since the I2C state machine function is called from inside an interrupt, you may not be able to to use printf(), especially if you are running FreeRTOS. As an alternative, use the debug printf methods from the printf_lib.h file.