Friday, May 15, 2015

Timing of interrupt-driven IO

Throughout the blog I have been promoting interrupt-driven IO as a slightly more difficult, but much better performing option. In this post I would try to provide detailed examples allowing to quantify advantages of using interrupt-driven IO and prove that the libraries that we have been discussing for the past several posts actually do work.

In this post we will be working with a very simple project 15-MPLI2C-Async, which is almost identical to the project we used in the post AnalyzingMPL3115A2 Precision Altimeter performance with the exception that in the main routine main.c we put MPLlibrary into asynchronous mode prior to reading samples from the sensor. This project outputs the same data stream through Serial Data Logger with the exception that we do not loop over various values of OSR and we do not limit sampling to 5 second interval – sampling continues as long as power is applied to the board.

However instead of looking at the output of the board (as we did in AnalyzingMPL3115A2 Precision Altimeter), I will connect logic analyzer to the test pads on the board to capture the actual I2C traffic on the bus, the status of DREADY line, and the signal on the blinker LED.



The first logic analyzer screen capture allows us to measure time interval between the DREADY line going high and the I2C module sending the START command to the sensor. As you may recall, during this time the DREADY going high results in external interrupt, which is captured by the MPL_Interrupt() function (please see MLP_ISR.c code file). MPL_Interrupt() function subscribes to I2Clibrary to perform I2C data session with the sensor. As in this project there is only one sensor on the bus, subscription is successful and I2C Library initiates I2C data exchange with the sensor by setting the START condition on the bus, at which time control is returned to the main thread. As you may see from the screenshot, all of this takes 5.5 microseconds.

From now on all I2C bus control will take place in the _MPLCallBack(…) function, which will be called by the I2C ISR (as discussed in the earlier post) in response to I2C interrupts generated by the hardware I2C module. The whole I2C data session, which includes reading data from the sensor and triggering next sample acquisition by setting OST flag, captured in the following screenshot.


My logic analyzer provides I2C data parser, which interprets I2C bus signals and allows seeing the actual data moved across the bus as well as interprets bus conditions. The green circles on the I2C data capture represent START or REPEATED-START conditions and the red square represent the STOP condition on the bus. Measuring time from the START to STOP reveals that the whole packet takes about 467 microseconds. By the way, on this screenshot we may also see that the DREADY goes low after the sample is read.

Now it would be really interesting to find out how much time was actually spent in the ISR routine and the _MPLCallBack(…) function. For that I temporarily modified the I2C ISR routine so that it will bring up the ISR line at the entry to the interrupt routine and bring it down after the return from the _MPLCallBack(…) function right before performing the return. Results are presented on the following screenshot:


The narrow spikes at the bottom of the screen represent the time spent in the interrupt routine. They are too narrow to be visible as more than just a line at the scale factor sufficient to represent the whole packet. However, at a higher resolution I was able measure the duration of each of these “spikes” – they range from 0.6 microseconds to 0.825 microseconds except for the next to last one, which is about 3 microseconds. This one corresponds to the State 18 in the _MPLCallBack(…) State Machine, when the measurement is constructed from the bytes read from the sensor and stored in the library. The very last spike with the duration of 0.3 microsecond is the interrupt after the final STOP, which is processed completely in the I2C ISR without the call to _MPLCallBack(…).

Now let us do some calculations. The total time from the moment DREADY goes high to completion of the I2C operation is 471.3 microseconds. Out of this time the MPL and I2C ISR routines including the calls to _MPLCallBack(…) take (5.5 + 25*0.8 + 3) = 28.5 microseconds, or about 6% of the IO time; the rest of the time – 442.8 microseconds (94% of the IO time) is returned to the main execution thread!

In this particular example project the main thread does nothing but waits for the next available sample. However, in a real control loop the main thread could be doing attitude calculation or calculating control input for individual motors, or something else and for the cost of being briefly interrupted for a few microseconds here and there it will have a new sensor data ready for it! With more sensors that need to be queried for data, the benefits become even more pronounced!

Considering that the speed of the control loop and the frequency at which motors’ control can be updated are critical for stable flight, the interrupt-driven IO is the way to go! On my previous board I managed to keep the duration of control loop to about 5 milliseconds resulting in motor control update frequency of about 200 Hz. With the new board utilizing MCU running at 64 MHz (versus 40 MHz on the previous one) I expect to even further reduce the duration of the control loop eventually getting closer to the maximum frequency of motor control updates of about 400 Hz supported by my ESC.

In the next post we will look at MPU Library responsible for configuration and operation of two Invensense MPU-6050 sensors installed on the board.

Wednesday, May 13, 2015

Asynchronous (interrupt-driven) communication with MPL3115A2

Now that we are quite familiar with MPL3115A2 sensor, it is time to look at how we may read samples from this sensor without wasting time on polling. The implementation of asynchronous sample acquisition is split between the rather generic features implemented in the I2Clibrary, which we discussed earlier, and specific to this sensor operations implemented in the MPLlibrary.  Features of the MPLlibrary responsible for asynchronous communication implemented in two code files MPL_Async.c and MLP_ISR.c. The former implements related user-accessible and some internal helper functions, while the latter implements the interrupt service routine (ISR) to process DREADY (data ready) interrupt from the sensor and the callback function invoked by I2Clibrary  in response to I2C interrupts.

Let us first review functions implemented in MPL_Async.c code file. The first one is the MPLAsyncStart() function. It first checks for library initialization and then checks for Asynchronous Mode (_MPL_Async) flag. If it is already set, the function returns success code and does nothing else. Otherwise it sets the Asynchronous Mode flag in the library, clears external interrupt request, if any, and then enables external interrupt linked (in MPLInit() function through the call to _MPLInitPinMap() defined in MPL_Profile.h) to the sensor DREADY line.

One special point need to be noted here – as the sensor operates in the OST (one-shot) mode, if the last read was unsuccessful the one-shot was not triggered, so DREADY line may never go high triggering the interrupt. We deal with this possibility in the Read functions. However to facilitate this we need to capture the time of last “successful” read so that we may compare the interval since then with the maximum time to acquire the sample _MPL_MaxInt, which was calculated in MPLReset(…) function based upon the specified value of OSR during initialization of the sensor. As a “safe bet” in the MPLAsyncStart()  function we set the time of the last “successful” read to the current timestamp to guarantee that at most after _MPL_MaxInt, time the interrupt will be forcefully triggered in one of the Read functions. We will see shortly how that is being implemented.

On the other hand, if the one-shot was successfully triggered in preceding read operation, the DREADY line may already be high, but the respective interrupt request was ignored as the Asynchronous Mode was not enabled. To address this contingency, MPLAsyncStart()  function checks for the line status to be high and, if so, triggers “missed” interrupt forcefully by setting interrupt flag MPL_IF (which is link to the selected interrupt port in MPL_Profile.h). This concludes transition to Asynchronous Mode.

If we need to terminate Asynchronous Mode, for example – to reset sensor with a different value of OSR, we need to call MPLAsyncStop() function. This function resets all the flags associated with Asynchronous Mode and disables external interrupt due to DREADY line going high.

MPLlibrary provides two functions in MPL_Async.c file to read samples from MPL3115A2 in Asynchronous Mode. The first one, MPLAsyncReadIfReady(…), updates the MPLData structure specified as a parameter only if the new sample is available; otherwise the structure remain unchanged and respective return code (MPL_NRDY -  data sample is not ready) provided to the caller. If the new sample is not ready, the function compares the current time-stamp with the time-stamp of last successful read operation – if the time since last read exceeds maximum sample acquisition time, a DREADY interrupt is triggered to restore the cycle. We will get into more details of this when we review the ISR routines shortly. As the call to this function is non-blocking, this function is being used in the quad control loop to avoid slowing down the loop with the wait for altitude sample.

The second function, MPLAsyncReadWhenReady(…), is very similar to the former one with the exception that it waits until the next sample is available. I usually use this function in some test projects; it also can be used in situations when obtaining the next altitude sample prior to proceeding further may be important for the application.

To actually read the sample data from the library both MPLAsyncReadIfReady(…) and MPLAsyncReadWhenReady(…) functions call library internal function _MPLAsyncRead(…). This function establishes a Critical Section (to avoid potential corruption of data by the interrupt routine), within which retrieves the number of samples accumulated _MPL_Ready by the library, the sum of accumulated samples _MPL_Data, and the time-stamp of the last sample _MPL_DataTS. When these values are retrieved and stored in local variables, both the sample counter _MPL_Ready and the sum of sample _MPL_Data values are set to zero, which indicates that samples asynchronously retrieved by the library are consumed. This concludes the Critical Section.

The altitude values then converted from long integer in sensor units to a floating-point value representing altitude in meters. If more than one sample summed up in _MPL_Data, as indicated by _MPL_Ready > 1, the resulting value of altitude averaged over collected samples. Finally the value of altitude is adjusted by the altitude of the “ground” (if it was set earlier) to convert it from the altitude over the sea level to altitude over ground.

So what is the difference between this function and synchronous read function MPLReadSample(…) that we discussed in the earlierpost? The synchronous read function MPLReadSample(…) ties up the main execution thread for the whole duration of the read operation. Contrary to that, the asynchronous read function MPLAsyncReadWhenReady(…) after reading current sample immediately triggers acquisition of the next sample asynchronously and then returns control to the main thread, which may do some processing of the sample while the next one is being acquired asynchronously. Thus at the next call to MPLAsyncReadWhenReady(…) if the next sample already acquired there would not be any wait or, if sample is not yet ready, the wait will be reduced by the time the main execution thread spent processing previous sample.

Now we are ready to look at the interrupt routines implemented in MLP_ISR.c code file. The first one, MPL_Interrupt(), is the true interrupt routine linked to the DREADY line. When the interrupt is enabled (in MPLAsyncStart() routine) and the DREADY line goes from low to high MCU interrupts the main execution thread and, subject to interrupt priority, transfers control to MPL_Interrupt() function. Alternatively interrupt could be forced from the main execution thread by setting interrupt flag MPL_IF in code, which, under some circumstances discussed above, could be done in MPLAsyncStart() or Asynchronous Mode read routines.

MPL_Interrupt() function first and foremost resets the interrupt flag MPL_IF – otherwise upon return it will be called again by MCU indefinitely. Second, it captures the status of the DREADY line to distinguish between natural and forced interrupts. Then it prepares the I2CAsyncRqst structure and attempts to subscribe for I2C processing through the call to I2CAsyncStart(…). If subscription is successful – request could be initiated right away or put in a queue by I2CAsyncStart(…), MPL_Interrupt() function resets the state of the State Machine implemented in the _MPLCallBack(…) function and returns. If I2C subscription is successful, I2C interrupts will be routed by the I2C ISR (as discussed in the earlier post) to the _MPLCallBack(…) function.

The core of communication is implemented in the _MPLCallBack(…) function – let’s look at how it works. Basically the whole _MPLCallBack(…) function is a big SWITCH statement with each case corresponding to a single state in the communication process, starting with the initial state 0 corresponding to the first interrupt generated by the I2C module after the START condition. Each CASE block performs a simple operation like setting a flag or reading/writing one byte from/to I2C read/write registers following the sequencing protocol required to construct a complete message for MPL3115A2. At the end of each CASE block the state is usually advanced to the next one with a few exceptions.

State 5 may advance state either to the state 6 or to the state 7 depending on the counter of the bytes read from the sensor. We need to read 5 bytes from the sensor to reset the DREADY line – 3 bytes representing the altitude and 2 bytes representing the temperature; we ignore the temperature measurement, but we still have to read it. Thus, at State 5 if there are more bytes to read we advance to State 6; if all 5 bytes are read we advance to State 7.

State 6 is another exception – after preparing I2C bus for read of the next byte it moves the state back to State 5 to read the byte and make decision on further protocol steps based upon the number of bytes read from the sensor in this session.

Finally, at State 18 the message protocol is completed and there is no need to advance state – State 18 is the final one. When _MPLCallBack(…) function reaches State 18 it terminates current I2C operation by calling I2CAsyncStop(…). If the status of the DREADY line was high as captured in variable _MPL_PortLvl by the MPL_Interrupt() function, then the value read from the sensor represent the new sample. Bytes read from the sensor are interpreted according to the specified data format and converted into a long integer representing measurement of altitude in sensor units (LSB corresponds to 6.25 centimeters).

If this is the first sample in a series of samples that were not yet consumed by the client, the just calculated value of the altitude (in sensor units) is stored in the library variable _MPL_Data. If this is not the first sample (_MPL_Ready > 0), the new value is added to the value stored in _MPL_Data. The latter case takes place when the frequency of reading asynchronously collected samples is less than the frequency at which sensor produces samples.

There is a special case in this logic – if the number of samples summed up in the _MPL_Data variable exceeds 512, the series is ignored – the new value of altitude is stored in the _MPL_Data variable and _MPL_Ready is set to one. _MPLCallBack(…) function does this to avoid overflow of the long integer _MPL_Data with the sum of multiple samples. However, this is almost an impossible situation in a control loop – even if we set OSR to 0, the shortest sample acquisition time would be 6 milliseconds, so to accumulate 512 samples would take over 3 seconds, which is a too large interval for any reasonable control loop.

This concludes discussion of the components responsible for asynchronous acquisition of altitude samples from MPL3115A2. In the next post we will look at a sample project implementing asynchronous read of altitude samples and will try to evaluate some timing characteristics. 

Sunday, May 10, 2015

Analyzing MPL3115A2 Precision Altimeter performance

As I promised at the end of the previous post, today we will put together all the code we discussed thus far related to synchronous I2C communication using MPL and I2C  libraries. We will be working with the 14-MPLI2C-Sync project developed in MPLAB X IDE from Microchip. This project is rather simple and contains single code file main.c, which includes single function int main(void).

This function starts with initiating various components that we discussed in the earlier posts and then initializes I2Cl library  assigning it interrupt priority (level) of 5 and setting speed at 400 KHz. After a short delay of 3 seconds (I am using PIC Kit 3 for programming the MCU with the firmware and during programming it may put MCU in a working state for a few seconds, which a 3 second delay should cover), it initializes MPLlibrary, which, in turn, initializes the MPL3115A2 sensor.

A few local variables are defined, including Msg structure, which is used to communicate back over the XBee link collected sensor data together with the timestamps. The core of operation is in the loop, which goes through all eight (from zero to 7) possible values of OSR (over-sampling rate) for the sensor. After sensor is reconfigured for the current value of OSR through the call to MPLReset(…), the “ground” level is adjusted for this series of samples. Then the Alarm is set for 5 seconds and the timestamp (StartTS) of the start of the series is captured.

Following these configuration steps, a series of samples collected from the sensor for 5 seconds and reported back using SerialData Logger. Complete captured file is available here. The same data converted to Excel spreadsheet and split by series available here. While you could check the raw data on your own, I would like to bring attention to a few points. The following table provides a summary of the data collected from the sensor in this series of runs:
OSR
Avg. over
T (Msec)
F (Hz)
Err (max, m)
Err (avg, m)
Avg (m)
0
1
6.0
166.67
5.125
1.226
-0.179
1
2
9.0
111.11
3.906
0.912
-0.263
2
4
14.8
67.80
2.281
0.632
-0.119
3
8
26.5
37.74
1.375
0.407
-0.063
4
16
49.8
20.10
1.063
0.293
-0.035
5
32
96.4
10.37
0.688
0.258
-0.181
6
64
189.8
5.27
0.500
0.150
-0.016
7
128
376.4
2.66
0.438
0.152
0.098

The first two columns represent OSR and corresponding number of measurements averaged for each sample. T (msec) and F (Hz) columns present the average time required to obtain a sample and respective frequency of samples generated by the sensor. The next two columns, Err (max, m) and Err (avg, m), provide maximum absolute error (remember, we adjusted “ground” to zero, so in the best case all samples should read zero) and the average error of all collected samples. Finally, the last column provides the average of all samples.

It is very illustrative to look at the chart of the maximum and average sample error plotted against the number of measurements averaged for a single sample:


As we may see from the chart, there is a diminishing return in terms of precision of the sample with the increase in number of measurements averaged for a single sample. For example, when we increase the number of measurements averaged from 2 to 4, the maximum error drops from about 4 to about 2 demonstrating close to linear dependency. However, when we again double the number of measurements averaged from 16 to 32, the maximum error declines by about 25%. The average sample error even less dependent on number of measurements averaged.

On the next chart, maximum and average errors and sample acquisition time are plotted against OSR:


With OSR changing from 0 to about 3 the maximum and average sample error drop almost linearly and then decrease in error slows down while sample acquisition time grows exponentially. Considering that the average of all samples sustain minimal changes with the changes in OSR, it appears that for dynamic systems like a quad we may get the best balance between precision and sample acquisition time by limiting OSR to 2 or 3 and applying some low-pass filtering.
Due to the simplicity of implementation and speed of calculation, I prefer IIR filters. When I take this new board to the air, I will share altitude measurement results using various values of OSR and low-pass filtering.

Meanwhile in the next post we will discuss asynchronous (interrupt-driven) acquisition of samples from MPL3115A2.

Sunday, May 3, 2015

Configuration and Synchronous (polling-based) Communication with MPL3115A2 Precision Altimeter

To facilitate configuration and synchronous and asynchronous (interrupt-based) communication with MPL3115A2 precision barometer/altimeter and potential use in other projects I created a MPLlibrary, which relies on my generic I2Clibrary, which we discussed in the previous posts. In this post we will discuss components of the MPLlibrary, which are responsible for configuration and synchronous communication with MPL3115A2. Please note, that configuration of the sensor requires reading and writing data to sensor’s control registers, so we would have to first look at the low-level communication primitives prior to discussing configuration of the sensor.

Like every library that I build, the first component of the MPLlibrary is the MPL_Profile.h header. This header defines the I2C address of the sensor, the interrupt that will be associated with sensors READY line for asynchronous (interrupt-driven) communication and provide function to configure the PIC MCU pin that will trigger this interrupt.

MPL_Local.h header provides definitions for some common constants (like address of CTRL_REG1 register and masks to set and reset OST) and variables used to control the state of the library and store intermediate sensor data. Variables defined in MPL_Local.h header are instantiated in MPL_Local.c code file. The MPL_Local.h header also provides definitions for functions internal to the library that are being used for synchronous and asynchronous communication.

Two basic functions implementing core of synchronous communication are _MPL_Read(…) and _MPL_Write(…).  These functions allow for reading or writing, respectively, of one or more bytes starting from a specified sensor’s register. Both of these functions are rather simple wrappers around the respective I2CSyncRead(…) and I2CSyncWrite(…) functions from the I2Clibrary  providing parameters (like device address and I2C bus number from the MPL_Profile.h) and augmented with simple check to assure that the MPLlibrary is not engaged in asynchronous operation . Implementation of these functions provided in MPL_Sync.c code file.

Using these two functions it was quite easy to implement the whole slew of very simple management functions to access values of other MPL3115A2 control registers. These functions are implemented in MPL_Mgmt.c code file. As you may see from the implementation, these functions provide just a thin wrapper around the basic _MPL_Read(…) function. The wrapper provides the hard-coded addresses of the respective control and information registers and default the length to one byte. One function in this group stands on its own – MPLGetBase() just returns the value of the current _MPL_BaseAlt variable, which we will discuss a little later while reviewing processing of the raw data received from the sensor.

Now we are ready to review initialization of the library and initial configuration of the sensor implemented in function MPLInit(OSR), which is implemented in the MPL_Init.c code file. Parameter of this function, OSR, takes values from 0 to 7 and matches exactly the definition of the OSR from the MPL3115A2  documentation. First, function MPLInit(…) assures that this is the first call to library initialization and obtains the Interrupt Level (IL) from the I2C library – as we mentioned while discussing asynchronous I2C communication, I2Clibrary  clients should use the same interrupt level as the library itself so that critical section (synchronization mechanism) could assure exclusive access to library elements.

MPLlibrary requires timing support, so through the call to TMRInitDefault() (please see post related to TMRlibrary and timing support) function MPLInit(…) makes sure that timer is initialized. If timer was already initialized with specific parameters, call to TMRInitDefault() is ignored. Then MPLInit(…) initializes external interrupt line used to capture READY signal from the sensor (used for asynchronous access) and initializes respective MCU pins through the call to _MPLInitPinMap (), defined in MPL_Profile.h header. This concludes initialization of the library and now it is time to initialize the sensor itself.

Originally initialization of the sensor was part of the MPLInit(…) routine, but then I realized that it is quite possible to change configuration of the sensor several times after library is initialized. Obviously, this hardly ever required during quads flight, but having this functionality available would make it much easier to investigate sensor’s operation outside of the quad control. So the final decision was to implement sensor configuration in a separate routine MPLReset(…), which is called from the MPLInit(…), but also can be called any time after the library is initialized. This function is implemented in the same MPL_Init.c code file as the MPLInit(…) function.

First, MPLReset(…) assures that the library is initialized and if there is an asynchronous operation in progress, attempts to stop it. Then based upon the values of the OSR parameter and using estimates for maximum sample time discussed in the previous post, MPLReset(…) calculates maximum delay between samples and stores calculated value in the library shared variable _MPL_MaxInt. This value is important for assuring stability of the asynchronous read, which we will discuss in the further post.

To implement sensor’s reset MPLReset(…) function first puts the sensor into the STANDBY mode by setting respective bit in the CTRL_REG1 register. Transitioning to STANDBY may take some time, so MPLReset(…) continues polling CTRL_REG1 register until the STANDBY state is indicated by the sensor.

When sensor is in STANDBY mode, MPLReset(…) initiates sensor’s internal RESET by setting RESET bit in CTRL_REG1 register. After sensor completes internal RESET, it returns to STANDBY mode. Again, this transition may take some time so MPLReset(…) continues polling CTRL_REG1 register until the STANDBY state is indicated by the sensor. Now that the sensor is in STANDBY mode after implementing internal RESET, we may configure other parameters.

This includes configuring sensor to generate interrupt on READY by setting INT1 line high in the push-pull (versus the optional open-drain) mode and setting the OSR bits to control oversampling. As we are using sensor in the one-shot mode (OST) as discussed in the previous post, we can leave sensor in the STANDBY mode. To conclude reset of the sensor and make library ready for operation in the new mode we need to read one sample from the sensor, which is achieved through the call to MPLlibrary function MPLReadSample(…), which reads next sample from the sensor in synchronous mode.

MPLReadSample(…) function, implemented in MPL_Sync.c code file, takes as a parameter a pointer to MPLData structure, defined in MPL.h header file. First this function assures that the library is initialized and that there is no asynchronous operation in progress. If these conditions are met, it calls MPLlibrary internal function _MPLReadRawData(…), which outputs the latest sample (subject to OSR setting) from the sensor in binary format. If the call is successful, MPLReadSample(…) obtains current timestamp and then converts raw binary sample data from the sensor to floating point format, scales resulting value to meters (LSB in raw data sample is 6.25 centimeters), and, if the “ground level” (value stored in _MPL_BaseAlt, initially set to zero) is set, converts the altitude to the value relative to the ground altitude. Calculated altitude and the current timestamp are stored in the MPLData structure provided by the caller.

The brunt of the work to actually read data from the sensor is carried out by the _MPLReadRawData(…) function, implemented in the same MPL_Sync.c code file. This function first waits until the new sample is ready and then reads the STATUS register, 3 bytes of altitude data, and two bytes of temperature data. Temperature data is ignored, but had to be read to reset the READY line. Three bytes of altitude data are converted to long integer to be returned to the caller. As we use the sensor in the OST (one-shot) mode, to avoid wait for the next sample in subsequent reads, upon reading the latest sample _MPLReadRawData(…) function immediately instructs the sensor to initiate acquisition of the next sample by first reading and then setting the OST bit in the CTRL_REG1 register.

If we want to receive altitude measurements relative to the ground level in the flight zone (which is typical for quad controller), we need to establish “ground level”. MPLlibrary stores the ground level in the local variable _MPL_BaseAlt, initially set to zero in MPLInit(…) function. Adjusting value of this variable to the actual ground level could be achieved through the call to the MPLSetGround() function, which is considered a part of the initialization routines and implemented in MPL_Init.c code file.

This function first resets ground level to zero and then collects altitude samples for 1 second. These samples are being averaged and resulting value is stored in _MPL_BaseAlt variable as the ground level. All subsequent altitude samples obtained either in synchronous mode through calls to MPLReadSample(…) function or in asynchronous mode, which we will discuss later, are adjusted to be relative to the established ground level.


This concludes the overview of configuration and synchronous (polling-based) communication with MPL3115A2 Precision Altimeter. Now it is time to put everything discussed thus far together and have a look at the actual program that works with MPL3115A2 barometric altimeter, but that would be the topic of the next post.