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.

No comments:

Post a Comment