Lesson I2C

I²C (Inter-Integrated Circuit)

What is I2C

I2C is pronounced "eye-squared see". It is also known as "TWI" because of the initial patent issues of this BUS. This is a popular, low throughput (100-1000Khz), half-duplex 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.

 

Figure x. of some devices connected up to an I2C bus

Pins of I2C

There are two pins for I2C:

  • SCL: Serial clock pin 
  • SDA: Serial data pin

The clock line is usually controlled by the Master with the exception that the slave may pull it low to indicate to the master that it is not ready to send data. 

The data line is bi-directional and is controlled by the Master while sending data, and by the slave when it sends data back after a repeat-start condition described below.

Open-Collector/Open-Drain 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".  This achieves safe operation of the bus (no case of short circuit), even if a device incorrectly assumes control of the bus.

 

Figure x. Devices connected to I2C bus.

 

Figure x. I2C device pin output stage.

Pull-up resistor

Using a smaller pull-up can acheive 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.

Try this link, and if the link doesn't work, import this into the circuit simulator:

$ 3 0.000005 10.20027730826997 50 5 50
172 352 200 352 152 0 6 5 5 0 0 0.5 Voltage
r 352 240 352 304 0 1000
g 352 368 352 384 0
c 352 304 352 368 0 0.00001 0
S 384 304 432 304 0 1 false 0 2
w 352 240 352 200 0
w 352 304 384 304 0
w 432 320 432 368 0
w 432 368 352 368 0
o 6 64 0 4098 5 0.025 0 2 6 3

Why Use I2C

Pros

  • IO/Pin Count:
    • 2 pins bus regardless of the number of devices.
  • Synchronous:
    • No need for agreed timing before hand
  • Multi-Master
    • Possible to have multiple masters on a I2C bus
  • Multi-slave:
    • 7-bit address allows up to an absolute maximum of 119 devices (because 8 addresses are reserved)
    • You can increase this number using I2C bus multiplexers

Cons

  • Slow Speed:
    • Typical I2C devices have a maximum speed of 400kHz
    • Some devices can sense speeds up to 1000kHz or more
  • Half-Duplex:
    • Only one device can talk at a time
  • Complex State Machine:
    • Requires a rather large and complex state machine in order to handle communication
  • Master Only Control:
    • Only a master can drive the bus
    • Exception to that rule is that a slave can stop stop the clock if it needs to hold the master in a wait state
  • Hardware Signal Protocol Overhead
    • This protocol includes quite a few bits, not associated with data to handle routing and handshaking. This slows the bus throughput even further

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.

I2C at its foundation is about sending and receiving bytes, but there is a layer of unofficial protocol about how the bytes are interpreted.  For instance, for an I2C write transaction, the master sends three bytes and 99% of the cases, they are interpreted like the following:

  1. Device Address
  2. Device Register
  3. Data

The code samples 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.

Signal Timing Diagram

 

Figure x. I2C communication timing diagram.

 

Write Transaction

Figure x. Master Transmit format

The master always initiates the transfer, and the device reading the data should always "ACK" the byte. For example, when the master sends the 8-bit address after the START condition, then 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.

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 that is addressed should then "ACK" using the 9th bit.
  2. Master sends device's "memory address" (1 or more bytes).
    Each byte should be ACK'd by the addressed slave.
  3. Master sends the data to write (1 or more bytes).
    Each byte should be ACK'd by the addressed slave.
  4. Master sends the STOP condition.

To maximize throughput and avoid having to send three I2C bytes for each slave memory write, 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 will be a state machine.

void i2c_write_slave_reg(void)
{
    // This will accomplish this:
    // slave_addr[slave_reg] = data;
  
    i2c_start();
    i2c_write(slave_addr);
    i2c_write(slave_reg);  // This is "M" for "memory address of the slave"
    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();
}

Read Transaction

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.  This transition provides the protocol to allow the slave device to start to control the data line.  You can consider an I2C even address being "write-mode" and I2C odd address being "read-mode".

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.

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_read_slave_reg(void) 
{ 
  i2c_start(); 
  i2c_write(slave_addr); 
  i2c_write(slave_reg); 
  
  i2c_start();                  // Repeat start 
  i2c_write(slave_addr | 0x01); // Odd address (last byte Master writes, then Slave begins to control the data line)
  char data = i2c_read(0);      // NACK last byte
  i2c_stop();
}

void i2c_read_multiple_slave_reg(void) 
{ 
  i2c_start(); 
  i2c_write(slave_addr); 
  i2c_write(slave_reg); 
  
  // This will accomplish this:
  // d1 = slave_addr[slave_reg];
  // d2 = slave_addr[slave_reg + 1];
  // d3 = slave_addr[slave_reg + 2];
  i2c_start(); 
  i2c_write(slave_addr | 0x01);
  char d1 = i2c_read(1);      // ACK
  char d2 = i2c_read(1);      // ACK
  char d3 = i2c_read(0);      // NACK last byte
  i2c_stop();
}

I2C Slave State Machine Planning

Before you jump right into the assignment, do the following:

  • Read and understand how an I2C master performs slave register read and write operation
    Look at existing code to see how the master operation handles the I2C state machine function
    This is important so you can understand the existing code base
  • Next to each of the master state, determine which slave state is entered when the master enters its state
  • Determine how your slave memory or registers will be read or written

It is important to understand the states, and use the datasheet to figure out what to do in the state to reach the next desired state given in the diagrams below.

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

  1. Perform the planned action after observing the current state
  2. Clear the "SI" (state change) bit for HW to take the next step
  3. The HW will then take the next step, and trigger the interrupt when the step is complete

Master Write

In the diagram below, note that when the master sends the "R#", which is the register to write, then the slave state machine should save this data byte as it's INDEX location. Upon the next data byte, the indexed data byte should be written.

Stop here and do the following:
1.  Check I2C_Base::i2cStateMachine
2. Compare the code to the state diagram below

Figure x. I2C Master Write Transaction

 

Figure x. Section 19.9.1 in LPC17xx User Manual

Master Read

In the diagram below, the master will write the index location (the first data byte), and then perform a repeat start. After that, you should start returning your indexed data bytes.

Figure x. I2C Master Read Transaction 

  

Figure x. Section 19.9.2 in LPC17xx User Manual

Assignment

Design your I2C slave state machine diagrams with the software intent in each state.

  • Re-draw the Master Read and Master Write diagrams while simultaneously showing the slave state.
    • In each slave state, show the action you will perform.
      (Refer to section 19.9.3 and 19.9.4 in LPC17xx user manual for the slave states)
    • For instance, when the Master is at state 0x08, determine which state your slave will be at.
      In this state when your slave gets addressed, an appropriate intent may be to reset your variables.
  • You will be treated like an engineering professional, and imagine that your manager has given you this assignment and is asking for the state machine diagrams before you start the code implementation. 
    • Demonstrate excellence, and do not rely on word by word instructions.  If you get points deducted, do not complain that "I was not asked to do this".  Do whatever you feel is necessary to demonstrate excellence.

 

Lab Assignment: I2C Slave Driver

Assignment

  • The I2C#2 master driver is already implemented and used for the on-board SJ-One I2C devices.
  • Study the existing I2C code: i2c_base.cpp. Please ask any questions if you have any, but the driver was implemented using the state machine diagrams as part of the I2C information at this website.
  • You will use the sample project on one board (completely unmodified) and connect it to your 2nd Slave Board which will contain your I2C2 Slave driver
    • On your Master Board, you can just use I2C terminal command to read or write an I2C register of a slave device
    • See help i2c for the help of this command (run this command on the Master Board)

Part 0: Get I2C Slave Interrupt to fire

Using the state machine diagrams you drew will be critical before you begin.  Make sure you fully understand the slave states because you will write the code that matches exactly to your state machine diagram.  In this Part 0 of the assignment, your objective is simply to initialize your I2C slave to get its first state interrupt to fire, and then the rest will be easy.

  • Start with the Master Board running the default, unmodified FreeRTOS sample project
  • Familiarize yourself with the Master Board's terminal command: help i2c
  • Connect this Master Board to the Slave Board by wire-wrapping the I2C SCL/SDA wires together.
    • Your pull-up resistor values will be halved but this is okay
    • There will be two of each sensor with the same address, which too is sort of okay.  For example, the temperature sensor at the same address (from the 2 boards) now appears twice on your I2C bus.
  • Initialize the Slave Board slave address such that you will get an interrupt when your address is sent by the Master Board
  • Add printfs to the I2C state machine code to identify what states you enter when the Master Board is trying to do an I2C transaction
  • Use your I2C slave state machine diagrams to augment the existing code, particularly at the state machine function to complete the rest of the assignment
#include <stdint.h>

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

// TODO: Modify the handleInterrupt at the I2C base class
// 1. Add your slave init method
// 2. Add the "slave address recognized" state in your I2C slave driver and print a msg when you hit this state inside the ISR
// 3. To test that your slave driver init is working, invoke "i2c discover" on the Master board

// Slave Board sample code reference
int main(void)
{
    I2C2& i2c = I2C2::getInstance(); // Get I2C driver instance
    const uint8_t slaveAddr = 0xC0;  // Pick any address other than an existing one at i2c2.hpp
    volatile uint8_t buffer[256] = { 0 }; // Our slave read/write buffer (This is the memory your other master board will read/write)

    // I2C is already initialized before main(), so you will have to add initSlave() to i2c base class for your slave driver
    i2c.initSlave(slaveAddr, &buffer[0], sizeof(buffer));

    // I2C interrupt will (should) modify our buffer.
    // So just monitor our buffer, and print and/or light up LEDs
    // ie: If buffer[0] == 0, then LED ON, else LED OFF
    uint8_t prev = buffer[0];
    while(1)
    {
        if (prev != buffer[0]) {
            prev = buffer[0];
            printf("buffer[0] changed to %#x by the other Master Board\n", buffer[0]);
        }
    }

    return 0;
}

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.

Guideline

Design your I2C slave interface by extending the existing I2C master driver.  Use one person's board as Master and communicate with the second person's Slave board.

  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 class 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 the I2C slave operation.
    • Use the diagrams you drew from the previous I2C assignment

Requirements

  1. Demonstrate that you are able to read and write the slave memory from the Master Board
    • You can use the i2c terminal command for this.
    • On the slave device, you can print out when the data sent to you has changed.
  2. Submit logic analyzer screenshots of a read and a write transaction occurring with your slave.
  • Extra Credit:

    + For extra credit and bragging rights, get multi byte read and write operation to work and impress the ISA team.


    + Do something creative with your slave board since you have essentially memory mapped the slave device over I2C. Maybe use buffer[0] to enable a blinking LED, and buffer[1] controls the blink frequency? You can do a lot more, so do not just blink LEDs