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:
- Assert a Start
condition on SDAx and SCLx.
- Send the I2C device address byte to the slave with a write indication.
- Wait for completion of the current bus operation.
- Verify an Acknowledge (ACK)
from the slave.
- Send the data register address to the slave.
- Wait for completion of the current bus operation.
- Verify an Acknowledge (ACK)
from the slave.
- Assert a Repeated
Start condition on SDAx and SCLx.
- Send the device address byte to the slave with a read indication.
- Wait for completion of the current bus operation.
- Verify an Acknowledge (ACK)
from the slave.
- Enable master reception and receive a byte of sensor data.
- Confirm reception of the byte of sensor data by raising
Acknowledge (ACK) condition.
- Continue receiving data and raising Acknowledge (ACK) condition until required number of
bytes is read from the sensor.
- Generate Negative Acknowledge (NACK) condition to indicate to the slave to stop sending bytes of
data after receiving the last byte.
- 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!