Wednesday, April 29, 2015

A quick introduction to MPL3115A2 Precision Altimeter

Prior to delving into the details of developing PIC MCU software to configure and operate MPL3115A2 precision altimeter from Freescale Semiconductors, we should get some basic details about this sensor. Conveniently enough, Freescale provides extensive documentation for this sensor, including some sample code and Application Notes.

However some of the features of this sensor make it less convenient to use it on the flight control board. For example, in the Auto Acquisition mode, when sensor’s firmware direct it to make periodic samples automatically, the shortest Time Step (interval between subsequent measurements) is 1 second, which is too low frequency to use altimeter data to control hovering or automatic landing. Thus for all practical purposes we would have to resort to One-Shot Mode when setting the OST bit in the CTRL_REG1 control register initiates immediate measurement. Unfortunately to set OST bit respective control register need to be read first, which results in necessity to issue multiple I2C commands – set WRITE mode on I2C bus, send register address, change direction to READ, read register value, set OST bit, change direction to WRITE, write register address and then write register value. And all of this need to be added to each measurement read operation to instruct sensor to start next measurement, which ties I2C bus for much longer time than would be required for a simple read if the sensor would be able to automatically initiate next measurement.

A single measurement in the OST mode takes just 6 msec, so sensor is capable of providing altitude data at a rate of about 166 Hz. However at this rate the precision of the measurements will be less than the stated 0.3 m. To achieve this level of precision multiple measurements need to be averaged. Averaging can be done externally (in the software) or can be done utilizing advanced feature of MPL3115A2 firmware – oversampling, which is controlled by the value of the OSR flags (3 bits OS0, OS1, and OS2 of the CTRL_REG1 Register). OSR takes values from 0 to 7; number of measurements that will be taken and averaged by the firmware to produce one sample defined by 2^OSR – that is from 1 to 128 measurements.

MPL3115A2 documentation provides upper bound of time (in milliseconds) required for producing a sample depending on the value of OSR. Multiple tests that I performed using MPL3115A2 indicate that the actual time required to produce a sample is significantly less than the upper bound. Moreover, capturing a long series of measurements I never saw any meaningful deviation of sample time between samples. The upper bound of the sample time from the documentation and my results from testing MPL3115A2 are summarized in the following table:



Both series of data are also plotted on the following chart:


As it follows from the chart, both series (the one from the documentation and the results of the actual testing) perfectly match linear trend lines. Looks like single measurement takes about 3 msec plus another 3 msec to prepare sensor to take the first measurement. Alternative interpretation of this data could mean that a single measurement takes about 6 msec, but the firmware allows about 3 msec of overlap in some processing for a series of measurements. Later on we will look at how averaging of multiple measurements reduce noise and will try to find the optimal balance between the level of noise and time required to produce a sample.

With this background we may proceed to the topics of developing MPL library facilitating configuration and reading data from this sensor. Obviously this specialized library will rely on my generic I2C library, which we discussed in the previous posts.

Tuesday, April 21, 2015

Implementing Asynchronous (interrupt-driven) I2C communication

Now that we reviewed synchronous (polling-based) I2C communication in the previous post, we should be ready to tackle asynchronous, interrupt-driven components of the I2C library.

The I2C master interrupt is generated on completion of the following master message events:
  • Start condition
  • Stop condition
  • Data transfer byte transmitted/received
  • Acknowledge transmit
  • Repeated Start
  • Detection of a bus collision event

The latter interrupt is a general error condition and should be treated directly in the ISR (Interrupt Service Routine), while the rest of the interrupts are related to the various states of the message protocol. The actual message protocol is dependent on the target slave device, thus it is practically impossible to construct a single ISR capable of communicating with multiple types of slave devices. My approach to addressing this problem was to delegate processing of the message-related interrupts to the client’s callback function, while leaving in the library ISR the code responsible for dealing with generic error conditions and some required housekeeping of the library.

The I2C callback prototype is defined in the I2C.h header file. When requesting asynchronous I2C operation from the I2C library, client is expected to provide I2CAsyncRqst structure (also defined in the I2C.h header file), which should contain the address of the client-provided I2C callback function and an unsigned integer (an opaque value) that will be provided to the callback function to relate the call to the callback function with the respective asynchronous operation request. I2C library passes this client parameter “as-is” back to the callback with no changes or modification. The client program decides on the value and meaning of this parameter. It could be an index, a pointer to some client-defined structure, or completely ignored – this is up to client’s implementation. In the later posts when I will discuss use of the I2C library to communicate with various slave devices, we will look at some possible uses of this “client parameter”.

Besides the client parameter, the I2C callback function will receive pointers to I2C CONTROL and STATUS registers, as well as pointers to I2C TRANSMIT and RECEIVE registers to enable it to read and write I2C data and control I2C message state. It is important to remember that the I2C callback routine will be called directly from the ISR routine and will run at the elevated ISR priority. Thus it is important to make sure that the callback routine is implemented efficiently and concludes its processing in as short time as possible. Typically the callback routine is implemented as a “state machine” and at each state performs a very simple set of operations. In subsequent posts dealing with MPL3115A2 and MPU-6050 clients I will provide examples of the I2C callback routines.

To request support for an asynchronous I2C operation from the library, or, in other words, to subscribe client’s I2C callback routine to receive I2C interrupts, client issues a call to I2CAsyncStart(…) function defined in I2C.h header file and implemented in I2C_Async.c code file. This functions takes two parameters – the numeric ID of the I2C module (1 or 2) where the respective slave device is connected and a pointer to I2CAsyncRqst structure preloaded with the address of the callback routine and client callback parameter.

I2CAsyncStart(…) function verifies initialization of the I2C library, converts index of the I2C module to the pointer to respective I2C control block (_I2C_CB) and retrieves from it pointers to I2C CONTROL and STATUS registers. Then it enters I2C Critical Section by raising its priority to the level of I2C interrupt established in the call to library initialization function I2CInit(…). Critical section guarantees that I2C interrupts will be suspended for the duration of the critical section thus providing exclusive access to I2C-related registers, flags, and control values.

Several tests are performed within the critical section – first we check for the pointer to callback function in the respective _I2C_CB – non-null value indicates that there is an active I2C operation in progress with the registered callback function. In this case we compare the callback address from the I2CAsyncRqst structure with the active callback address – if they are the same the new request is ignored. Otherwise we scan the queue of pending I2C requests – if the callback address from the I2CAsyncRqst is found in the queue, then the request for this device is already scheduled and new request could be ignored.

If this is a new request, the queue is scanned for the empty slot; if an empty slot is found, the new request is put in the queue, otherwise an error return code is provided to the client. Usually we have very few devices sharing I2C bus – due to the relatively slow speed of the bus (400 kHz) and frequent updates from sensors having too many devices on the bus would result in high level of contention and skipped samples. In the case of my board, I have just two sensors sharing the bus, so it is enough to have a queue of depth one, so scanning the queue is very fast operation. The depth of the queue is controlled by the I2CMaxAsyncQueue value defined in I2C_Profile.h.

If there is no active callback routine in the _I2C_CB, then we may try to initiate asynchronous request right away. However first we need to check the status of the I2C bus – it may be busy with some synchronous operation. If that is the case, the asynchronous request is rejected and client receives an error code. This is really a rather seldom condition – usually after slave devices are initialized (in a synchronous mode) I switch all of them to asynchronous mode; however there might be some special cases when one of the devices need to repeat initialization or perform some other synchronous operation.

If the bus is free (most typical case), the request is copied to _I2C_CB as “active” callback, I2C interrupt is enabled, and START command issued on the bus. At the completion of the START sequence by the hardware module, the interrupt will be raised, which will invoke ISR and, if everything is OK, ISR will invoke the client’s callback routine.

Now it is time to look at the implementation of the ISR routine. PIC architecture defines separate interrupt vectors for each (of the two) I2C hardware modules. If both I2C modules requested in defined in I2C_Profile.h, then we will have two interrupt routines, _MI2C1Interrupt and _MI2C2Interrupt (these names are predefined). Interrupt routines are defined in the I2C_ISR.c file. As soon as respective interrupt is raised and control transferred to respective ISR, interrupt routine first clears the interrupt flag (to avoid endless loop). Then it checks for the active callback routine – if there is no one, the interrupt assumed to be spurious and interrupt processing for this module is disabled (as you may recall, it is enabled by the I2CAsyncStart(…) routine when new request comes) and interrupt processing ends at this.

If there is an active callback routine, further interrupt processing is delegated to common (shared between modules) interrupt routine I2CInterrupt(…) implemented in the same code file. To avoid extra overhead of the function call in the interrupt routine I2CInterrupt(…) is defined as “inline” function.

I2CInterrupt(…) routine first checks whether this interrupt is the result of a STOP condition on the bus. If this is the case, then it is an indication that the current asynchronous operation is completed. The Callback pointer in _I2C_CB is nulled (indicating that there is no active asynchronous operation) and the queue is scanned for pending requests. If one is found, it is being promoted to “active” and START command is issued on the I2C bus. Otherwise interrupts on the bus are disabled and function returns.

If the interrupt was not due to the STOP condition, I2CInterrupt(…) routine checks for potential error conditions on the bus. If one is found, it clears the error status and issues STOP on the bus. This would result in abandoned client request – the client code should be ready for the possibility that a request may not run to completion. When we will look at the code for clients, I will indicate how this condition could be dealt with (if necessary) for various slave devices.

Finally, if this is neither the STOP nor error on the bus, the interrupt is being passed to the client’s callback routine for further processing. Later we will look at implementation of callback routines for various slave devices. For now we just need to remember that when the callback routine concludes communication with the slave device (based upon the device and the callback logic) it should indicate this to the I2C library by calling I2CAsyncStop(…) routine defined in I2C.h header file. This is a very small function (again, defined as “inline” to save time on call), which checks for some bus conditions and issues STOP on the bus. Eventually interrupt resulting from the STOP condition will be processed by the ISR as we already discussed above.

Now that we became familiar with the internals of the I2C library, it is time to put our knowledge to test by using the library to facilitate synchronous and asynchronous communication with various sensors. That we will start investigating in the next post.

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!