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.