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. 

No comments:

Post a Comment