Saturday, April 18, 2015

Quick introduction to I2C Synchronous communication

My new board uses several devices that support communication only over the I2C protocol. These devices include two gyro/accelerometers MPU-6050 from Invensense and MPL3115A2 barometric altitude sensor from Freescale. Thus I have to implement I2C communication to read data from these devices.

PIC24EP512GP806 microcontroller provides hardware support through the I2C module to alleviate some of the difficulties of implementing I2C communication protocol. The I2C module controls the individual portions of the I2C message protocol; however, sequencing the protocol components to construct a complete message is a software task usually provided through the I2C library, which we will be discussing here. PIC I2C module supports two modes of operation – standard at 100 kHz and fast at 400 kHz. Luckily both MPU-6050 and MPL3115A2 support fast communication at 400 kHz.

PIC24EP512GP806 has two independent I2C modules so it can service two I2C buses at the same time (almost) independently except for the bus interrupt processing, which on a single-core MCU always happens serially. Thus there is some minimal dependency between the two I2C modules but, assuming that interrupt processing takes much less time than data transfer even at 400 kHz, we may ignore this dependency. Anyway, presence of two I2C modules allows me to put one MPU-6050 and MPL3115A2 on one bus and another MPU-6050 – on the second bus to alleviate contention for the bus resources.

Quick search on the Internet provides a lot of links explaining I2C bus operation. Microchip also provides good documentation on I2C and details of I2C hardware module in PIC24EFamily Reference Manual, Section 19 – I2C. The following discussion of the I2C library is based on the following assumptions:
  • I will consider only case when PIC MCU is the “master” of the bus and all attached peripheral devices (sensors) operate as slaves; and
  • Address width is set to 7 bits (most common) with the 8th bit identifying direction of communication.

Following the pattern for my SW modules, header I2C_Profile.h provides some basic definitions for initialization of I2C interface. Specifically, it provides defines that identify which of the available HW I2C modules will be used and configures respective MCU pins (I2C module is not subject to Peripheral Pin Select, so pins associated with I2C modules are predefined) for open-drain.

Each of the available two I2C modules uses its own set of MCU registers for control and status checking. To be able to use the same routines and avoid code duplication I grouped all resources related to a module into a structure _I2C_CB. Thus all routines in the library can operate either of two I2C HW modules given that they are provided pointer to a respective control block. Header I2C_Local.h provides definition for _I2C_CB as well as for a set of helper inline functions that facilitate extracting important pointers from the control block. This header also provides definition for some additional internal functions that we will discuss later. Related code file I2C_Local.c allocates storage for control blocks and some common variables and flags, which are brought into the scope through I2C_Local.h.

Header file I2C.h defines I2C library return codes as well as headers for the functions that allow external clients to utilize library’s functionality. This is the only I2C header that clients (modules servicing I2C sensors) need to include to access the functionality of the I2C library. Functions I2CGetIL() and I2CInit(…) used by both synchronous and asynchronous I2C clients.

Functions I2CSyncRead(…) and I2CSyncWrite(…) perform synchronous (executed on the main thread)  I2C read and write operations so all calls to these functions are blocking. I2CAsyncStart(…) and I2CAsyncStop(…) initiate and terminate respectively asynchronous operations (executed primarily on the interrupt thread), so calls to these functions are non-blocking. We will discuss these functions in more details later.

The first step in using I2C library is to initialize it; this is achieved through the call to I2CInit(…) function defined in the file I2CInit.c. This function takes 2 parameters; the first parameter is the number between 1 (lowest) and 7 (highest) that defines priority of the I2C interrupt routine. The second parameter is also a number taking value between 0 and 3, which defines the speed at which I2C module will operate.

Speed = 0 corresponds to the lowest speed achievable for the MCU running at 64 MHz, which, at 123 kHz, is slightly higher than the “slow” I2C speed standard of 100 kHz. Luckily most of the devices today are capable of running at “fast” I2C speed of 400 kHz. Speed = 1 sets I2C hardware module to operate at 200 kHz; Speed = 2 sets the module to “fast” speed of 400 kHz. And, finally, Speed = 3 sets the module to the new speed standard for I2C a 1 MHz.

While the MCU documentation as well as documentation for my sensors state that the highest speed at which they can operate is 400 kHz, I decided to test them at 1 MHz, which is a new standard of speed for I2C – surprisingly enough both the MCU and sensors worked fine at this speed! Maybe it is due to the I2C specification, which allows for “stretching” of timing interval if the device requires more time to provide the reply. However, running at higher speed will most probably result in excessive heating of the components due to the higher switching frequency, so I would not recommend exceeding specified frequency in “production” environment like a quad copter as failure of a sensor or the MCU will result in a crash.

Depending on the settings defined in I2C_Profile.h, I2C_Init(…) initializes one or two library  Control blocks (_I2C_CB1 and/or _I2C_CB2 discussed earlier) and then calls one or both of the local functions that actually initialize the I2C hardware module(s) of the MCU. The code in I2CInit.c comes with extensive comments detailing every initialization step. After the call to I2C_Init(…) the library and corresponding HW modules are ready for operation.

Let’s look at the steps involved in communication over the I2C bus. For example, reading a few bytes of data from a sensor would involve the following steps:
  1. Assert a Start condition on SDAx and SCLx.
  2. Send the I2C device address byte to the slave with a write indication.
  3. Wait for completion of the current bus operation.
  4. Verify an Acknowledge (ACK) from the slave.
  5. Send the data register address to the slave.
  6. Wait for completion of the current bus operation.
  7. Verify an Acknowledge (ACK) from the slave.
  8. Assert a Repeated Start condition on SDAx and SCLx.
  9. Send the device address byte to the slave with a read indication.
  10. Wait for completion of the current bus operation.
  11. Verify an Acknowledge (ACK) from the slave.
  12. Enable master reception and receive a byte of sensor data.
  13. Confirm reception of the byte of sensor data by raising Acknowledge (ACK) condition.
  14. Continue receiving data and raising Acknowledge (ACK) condition until required number of bytes is read from the sensor.
  15. Generate Negative Acknowledge (NACK) condition to indicate to the slave to stop sending bytes of data after receiving the last byte.
  16. Generate a Stop condition on SDAx and SCLx.
The I2C hardware module supports Master mode communication with the inclusion of Start and Stop generators, data byte transmission, data byte reception, Acknowledge generator and a generation of timing signals on SCLx. Generally, the software writes to a control register to start a particular step, and then wait for an interrupt or poll status to wait for completion. The I2C module does not allow queueing of events. For instance, the software is not allowed to initiate a Start condition and immediately write the I2CxTRN register to initiate transmission before the Start condition is complete. Similarly, the software has to wait for an interrupt or poll status to wait for completion of receive operation prior to sending ACK or NACK signal. Contrary to the SPI communication, that we discussed earlier, I2C requires software control multiple times for each byte received or sent. Let’s see how this can be implemented in a more straight-forward case of synchronous communication.

File I2CSync.c implements several functions that carry out I2C communication primitives defined above. All the functions implemented in this file are internal to the library and are not exposed to the library users. As such they are defined in the internal header I2C_Local.h and accept as a parameter a pointer to respective I2C Control Block (_I2C_CB), which is accessible only within the library.

I2CIdle(…) function performs polling of CONTROL and STATUS registers until the previous bus operation completes. I2CStart(…) function asserts START condition on I2C bus after assuring that the bus is in the idle state. I2CReStart(…) function asserts RESTART condition, and I2CStop(…) – asserts STOP condition.

Using these low-level functions we may implement higher level functional primitives I2CMasterWriteByte(…) and I2CMasterReadByte(…). The latter function besides the pointer to the control block and pointer to the location to store read byte also includes the Flag parameter, which indicates whether this byte is the last (Flag = 0) and should be followed by asserting NACK or we expect to read more bytes from the Slave (sensor), in which case the function asserts ACK condition on the bus. Every statement in these functions is commented in the code to explain what we do on every step.

Now, equipped with these primitives, we may implement user-accessible library functions I2CSyncRead(…) and I2CSyncWrite(…) implemented respectively in code files I2C_SyncRead.c and I2C_SyncWrite.c. Definitions for these functions are provided in I2C.h header file that clients of the library should include.

Both of these functions require 5 parameters – index of the I2C master hardware module (bus 1 or 2) on which the respective slave device resides, address of the slave, address of the register on the slave that read or write operation should start from, address of the buffer in the caller space that will receive data during Read operation or which will provide data for the Write operation, and, finally, the length of the buffer which will indicate how many bytes need to be read from or written to the slave.

Implementation of these two functions is rather similar – first, using the index of the I2C module they obtain pointer to the respective control block. Then, within the Critical Section bound to the level of I2C Interrupt Priority (specified at the library initialization through the call to I2CInit(…)), they check the Status of the respective bus and, if the bus is available, put exclusive lock on the bus for the duration of the operation. If the bus is busy, attempt to acquire bus is repeated up to I2C_BUSYRetry times. Maximum retry value I2C_BUSYRetry defined in the I2C.h header. If the bus cannot be acquired despite all the retry attempts, the functions return error code identifying bus status.

If the bus successfully acquired, functions I2CSyncRead(…) and I2CSyncWrite(…) carry out respective operations using bus primitives discussed above. Practically each statement (except the obvious) in these functions provided with comments that detail the individual bus operations carried out. It would be quite easy to match them to the I2C read/write steps outlined above.

This rather long post concludes overall discussion of the I2C library and, specifically, implementation of synchronous (performed on the main execution thread) polling-based read and write operations. In the next post we will focus on asynchronous interrupt-driven implementation of I2C bus operations.


Till later!

No comments:

Post a Comment